Skip to content

Fixed express-pouchdb's replication not handling all forms of basic authentication #463

Open
@cjshearer

Description

@cjshearer

Currently, express-pouchdb does not handle sources and targets in any form other than a url. However, CouchDB specifies three ways to provide authentication for the replication route, two of which provide source and target as objects that are unhandled, so attempting to use these alternate forms of authentication cause the PouchDB server to throw a 500 error. I fixed this by replacing the source and target with PouchDB objects generated by req.PouchDB.new, which handles these forms of authentication. Is this the right way to go about this? If so, I'll make a PR and link it here.

Reproducible setup and test:

const axios = require("axios");
const importFresh = require("import-fresh");
const btoa = require("btoa");

/**
 * Creates an in-memory PouchDB server, a PouchDB object to access it, and
 * expose the server on localhost:port.
 *
 * @param {number} port - the port at which the in-memory server should be
 * made accessible.
 *
 * @returns {PouchDB} a PouchDB object to access the in-memory PouchDB server
 */
function pouchDBMemoryServer(username, password, port) {
  // we need to disable caching of required modules to create separate pouchdb
  // server instances
  var InMemPouchDB = importFresh("pouchdb").defaults({
    db: importFresh("memdown"),
  });

  const app = importFresh("express-pouchdb")(InMemPouchDB, {
    inMemoryConfig: true,
  });

  // add admin
  app.couchConfig.set("admins", username, password, () => {});

  app.listen(port);
  return InMemPouchDB;
}

async function testIt() {
  // create two in-memory PouchDB servers
  let sourcePort = 5984;
  let targetPort = 5985;
  let username = "username";
  let password = "password";
  let sourceInstance = pouchDBMemoryServer(username, password, sourcePort);
  let targetInstance = pouchDBMemoryServer(username, password, targetPort);

  // create a database in the source instance
  let dbName = "u-test";
  let db = new sourceInstance(dbName);

  await db.put({ _id: "0", exampleDoc: "test" });

  // create basic authorization as base64 string. Note that the _replicate route
  // could also take this as { auth: { username, password } }
  const authOpts = {
    headers: {
      Authorization: "Basic " + btoa(`${username}:${password}`),
    },
  };

  // ensure database is present
  await axios
    .get(`http://localhost:${sourcePort}/${dbName}`, authOpts)
    .then((r) => console.log(r.status, r.statusText, r.data));

  // attempt to replicate
  await axios
    .post(
      `http://localhost:${sourcePort}/_replicate`,
      {
        source: {
          url: `http://localhost:${sourcePort}/_replicate/${dbName}`,
          ...authOpts,
        },
        target: {
          url: `http://localhost:${targetPort}/_replicate/${dbName}`,
          ...authOpts,
        },
        create_target: true,
        continuous: true,
      },
      authOpts
    )
    .then((r) => console.log(r.data))
    .catch((err) => console.log(err.status));
}

await testIt();

Which throws:

> await testIt();
200 OK {
  doc_count: 1,
  update_seq: 1,
  backend_adapter: 'MemDOWN',
  db_name: 'u-test',
  auto_compaction: false,
  adapter: 'leveldb',
  instance_start_time: '1650047291155'
}
undefined
undefined
> Error: Missing/invalid DB name
    at PouchAlt.PouchDB (/home/.../node_modules/pouchdb/lib/index.js:2649:11)
    at new PouchAlt (/home/.../node_modules/pouchdb/lib/index.js:2786:13)
    at staticSecurityWrappers.replicate (/home/.../node_modules/pouchdb-security/lib/index.js:211:62)
    at callHandlers (/home/.../node_modules/pouchdb-wrappers/lib/index.js:467:17)
    at Function.replicate (/home/.../node_modules/pouchdb-wrappers/lib/index.js:428:12)
    at /home/.../node_modules/express-pouchdb/lib/routes/replicate.js:38:17
    at Layer.handle [as handle_request] (/home/.../node_modules/express/lib/router/layer.js:95:5)
    at next (/home/.../node_modules/express/lib/router/route.js:137:13)
    at /home/.../node_modules/express-pouchdb/lib/utils.js:41:7
    at /home/.../node_modules/body-parser/lib/read.js:130:5
    at invokeCallback (/home/.../node_modules/raw-body/index.js:224:16)
    at done (/home/.../node_modules/raw-body/index.js:213:7)
    at IncomingMessage.onEnd (/home/.../node_modules/raw-body/index.js:273:7)
    at IncomingMessage.emit (node:events:402:35)
    at IncomingMessage.emit (node:domain:537:15)
    at endReadableNT (node:internal/streams/readable:1343:12)

But after applying the below diff (generated by patch-package), everything works as expected:

> await testIt();
200 OK {
  doc_count: 1,
  update_seq: 1,
  backend_adapter: 'MemDOWN',
  db_name: 'u-test',
  auto_compaction: false,
  adapter: 'leveldb',
  instance_start_time: '1650046811592'
}
{ ok: true }
diff --git a/node_modules/express-pouchdb/lib/routes/replicate.js b/node_modules/express-pouchdb/lib/routes/replicate.js
index aa1a790..30d5f50 100644
--- a/node_modules/express-pouchdb/lib/routes/replicate.js
+++ b/node_modules/express-pouchdb/lib/routes/replicate.js
@@ -1,17 +1,30 @@
 "use strict";
 
 var utils  = require('../utils'),
-    extend = require('extend');
+    extend = require('extend'),
+    cleanFilename = require('../clean-filename');
 
 module.exports = function (app) {
   var histories = {};
 
   // Replicate a database
   app.post('/_replicate', utils.jsonParser, function (req, res) {
+    var dbOpts = utils.makeOpts(req);
 
-    var source = req.body.source,
-        target = req.body.target,
-        opts = utils.makeOpts(req, {continuous: !!req.body.continuous});
+    // handle various forms of authorization using PouchDB.new
+    var source = req.PouchDB.new(
+          cleanFilename(
+            req.body.source.url ? req.body.source.url : req.body.source
+          ),
+          dbOpts
+        ),
+        target = req.PouchDB.new(
+          cleanFilename(
+            req.body.source.url ? req.body.source.url : req.body.source
+          ),
+          dbOpts
+        ),
+        opts = utils.makeOpts(req, { continuous: !!req.body.continuous })
 
     if (req.body.filter) {
       opts.filter = req.body.filter;

This issue body was partially generated by patch-package.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions