pouchdb / pouchdb-server

CouchDB-compatible server built on PouchDB and Node
Apache License 2.0
944 stars 155 forks source link

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

Open cjshearer opened 2 years ago

cjshearer commented 2 years ago

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.