From 03b0ae732ee13a22708d5a3b2ea2e059278b3927 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 22 Sep 2019 00:05:30 -0500 Subject: [PATCH] more organization, remote object resolution, signature verification as middleware --- index.js | 14 +++++++++----- net/index.js | 4 ++-- net/security.js | 27 +++++++++++++++++++++++++++ pub/actor.js | 23 ++++++++++++++++++++++- pub/federation.js | 15 +++++++++++++++ pub/index.js | 4 +++- pub/object.js | 16 ++++++++++++++++ routes/inbox.js | 23 ++++------------------- routes/user.js | 2 +- routes/webfinger.js | 4 ++-- store/actor.js | 23 +++-------------------- store/index.js | 3 ++- store/object.js | 19 +++++++++++++++++++ store/setup.js | 25 +++++++++++++------------ 14 files changed, 138 insertions(+), 64 deletions(-) create mode 100644 net/security.js create mode 100644 pub/federation.js create mode 100644 pub/object.js create mode 100644 store/object.js diff --git a/index.js b/index.js index 3044863..ae6b843 100644 --- a/index.js +++ b/index.js @@ -3,21 +3,22 @@ const path = require('path') const express = require('express'); const MongoClient = require('mongodb').MongoClient; const fs = require('fs'); -const routes = require('./routes') const bodyParser = require('body-parser') const cors = require('cors') const http = require('http') const https = require('https') const basicAuth = require('express-basic-auth'); -const config = require('./config.json'); -const { USER, PASS, DOMAIN, KEY_PATH, CERT_PATH, PORT, PORT_HTTPS } = config; +const routes = require('./routes') +const pub = require('./pub') +const config = require('./config.json') +const { USER, PASS, DOMAIN, KEY_PATH, CERT_PATH, PORT, PORT_HTTPS } = config const app = express(); // Connection URL const url = 'mongodb://localhost:27017'; -const dbSetup = require('./store/setup'); +const store = require('./store'); // Database Name const dbName = 'test'; @@ -87,7 +88,10 @@ client.connect({useNewUrlParser: true}) console.log("Connected successfully to server"); db = client.db(dbName); app.set('db', db); - return dbSetup(db, DOMAIN) + return pub.actor.createLocalActor('dummy', 'Person') + }) + .then(dummy => { + return store.setup(db, DOMAIN, dummy) }) .then(() => { https.createServer(sslOptions, app).listen(app.get('port-https'), function () { diff --git a/net/index.js b/net/index.js index 2fd4a7b..3269200 100644 --- a/net/index.js +++ b/net/index.js @@ -1,6 +1,6 @@ 'use strict'; -// middleware and networking utils +// middleware module.exports = { validators: require('./validators'), -// comms: require('./comms'), + security: require('./security'), }; diff --git a/net/security.js b/net/security.js new file mode 100644 index 0000000..fd84893 --- /dev/null +++ b/net/security.js @@ -0,0 +1,27 @@ +'use strict' +const httpSignature = require('http-signature') +const pub = require('../pub') +// http communication middleware +module.exports = { + verifySignature, +} + +async function verifySignature (req, res, next) { + if (!req.get('authorization')) { + // support for apps not using signature extension to ActivityPub + // TODO check if actor has a publicKey and require signature + return next() + } + // workaround for node-http-signature#87 + const tempUrl = req.url + req.url = req.originalUrl + const sigHead = httpSignature.parse(req) + req.url = tempUrl + const signer = await pub.object.resolveObject(sigHead.keyId, req.app.get('db')) + const valid = httpSignature.verifySignature(sigHead, signer.publicKey.publicKeyPem) + console.log('signature validation', valid) + if (!valid) { + return res.status(400).send('Invalid http signature') + } + next() +} diff --git a/pub/actor.js b/pub/actor.js index 2923814..a004a80 100644 --- a/pub/actor.js +++ b/pub/actor.js @@ -1,13 +1,17 @@ const crypto = require('crypto') +const request = require('request-promise-native') const {promisify} = require('util') +const store = require('../store') +const federation = require('./federation') const pubUtils = require('./utils') const config = require('../config.json') const generateKeyPairPromise = promisify(crypto.generateKeyPair) module.exports = { - createLocalActor + createLocalActor, + getOrCreateActor } function createLocalActor (name, type) { @@ -46,3 +50,20 @@ function createLocalActor (name, type) { } }) } + +async function getOrCreateActor (preferredUsername, db, includeMeta) { + const id = pubUtils.usernameToIRI(preferredUsername) + let user = await store.actor.getActor(id, db, includeMeta) + if (user) { + return user + } + // auto create groups whenever an unknown actor is referenced + user = await createLocalActor(preferredUsername, 'Group') + await db.collection('objects').insertOne(user) + // only executed on success + delete user._id + if (includeMeta !== true) { + delete user._meta + } + return user +} \ No newline at end of file diff --git a/pub/federation.js b/pub/federation.js new file mode 100644 index 0000000..4bd82e3 --- /dev/null +++ b/pub/federation.js @@ -0,0 +1,15 @@ +'use strict' +const request = require('request-promise-native') + +// federation communication utilities +module.exports = { + requestObject, +} + +function requestObject (id) { + return request({ + url: id, + headers: {Accept: 'application/activity+json'}, + json: true, + }) +} \ No newline at end of file diff --git a/pub/index.js b/pub/index.js index 6912b68..02d6b8b 100644 --- a/pub/index.js +++ b/pub/index.js @@ -2,6 +2,8 @@ // ActivityPub / ActivityStreams utils module.exports = { actor: require('./actor'), - utils: require('./utils'), consts: require('./consts'), + federation: require('./federation'), + object: require('./object'), + utils: require('./utils'), } diff --git a/pub/object.js b/pub/object.js new file mode 100644 index 0000000..246781d --- /dev/null +++ b/pub/object.js @@ -0,0 +1,16 @@ +'use strict' +const store = require('../store') +module.exports = { + resolveObject +} + +// find object in local DB or fetch from origin server +async function resolveObject (id, db) { + let object = await store.object.get(id, db) + if (object) { + return object + } + object = await federation.requestObject(id) + await store.object.save(object) + return object +} \ No newline at end of file diff --git a/routes/inbox.js b/routes/inbox.js index a695c68..fd936bb 100644 --- a/routes/inbox.js +++ b/routes/inbox.js @@ -8,36 +8,21 @@ const request = require('request-promise-native') const httpSignature = require('http-signature') const {ObjectId} = require('mongodb') -router.post('/', net.validators.activity, function (req, res) { +router.post('/', net.validators.activity, net.security.verifySignature, function (req, res) { const db = req.app.get('db'); let outgoingResponse req.body._meta = {_target: pub.utils.usernameToIRI(req.user)} // side effects switch(req.body.type) { case 'Accept': - // workaround for node-http-signature#87 - const tempUrl = req.url - req.url = req.originalUrl - sigHead = httpSignature.parse(req, {clockSkew: 3000}) - req.url = tempUrl - // TODO - real key lookup with remote fetch - store.actor.getActor(sigHead.keyId.replace(/.*\//, ''), db) - .then(user => { - const valid = httpSignature.verifySignature(sigHead, user.publicKey.publicKeyPem) - console.log('key validation', valid) - return res.status(valid ? 200 : 401).send() - }) + // TODO - side effect ncessary for following collection? break case 'Follow': req.body._meta._target = req.body.object.id // send acceptance reply Promise.all([ - store.actor.getOrCreateActor(req.user, db, true), - request({ - url: pub.utils.actorFromActivity(req.body), - headers: {Accept: 'application/activity+json'}, - json: true, - }) + pub.actor.getOrCreateActor(req.user, db, true), + pub.object.resolveObject(pub.utils.actorFromActivity(req.body), db), ]) .then(([user, actor]) => { if (!actor || !actor.inbox) { diff --git a/routes/user.js b/routes/user.js index 72cf8fe..aafe967 100644 --- a/routes/user.js +++ b/routes/user.js @@ -12,7 +12,7 @@ router.get('/:name', async function (req, res) { } else { let db = req.app.get('db') - const user = await store.actor.getOrCreateActor(name, db) + const user = await pub.actor.getOrCreateActor(name, db) if (user) { return res.json(pub.utils.toJSONLD(user)) } diff --git a/routes/webfinger.js b/routes/webfinger.js index c792719..fb2f9ba 100644 --- a/routes/webfinger.js +++ b/routes/webfinger.js @@ -2,7 +2,7 @@ const express = require('express') const router = express.Router() -const store = require('../store') +const pub = require('../pub') const acctReg = /acct:[@~]?([^@]+)@?(.*)/ router.get('/', function (req, res) { let resource = req.query.resource; @@ -14,7 +14,7 @@ router.get('/', function (req, res) { return res.status(400).send('Requested user is not from this domain') } let db = req.app.get('db'); - store.actor.getOrCreateActor(acct[1], db) + pub.actor.getOrCreateActor(acct[1], db) .then(result => { if (!result) { return res.status(404).send(`${acct[1]}@${acct[2]} not found`) diff --git a/store/actor.js b/store/actor.js index 4ed787c..1f73dd9 100644 --- a/store/actor.js +++ b/store/actor.js @@ -1,34 +1,17 @@ 'use strict' -const pub = require('../pub') + module.exports = { getActor, - getOrCreateActor, } const actorProj = {_id: 0, _meta: 0} const metaActorProj = {_id: 0} -function getActor (preferredUsername, db, includeMeta) { +function getActor (id, db, includeMeta) { return db.collection('objects') - .find({id: pub.utils.usernameToIRI(preferredUsername)}) + .find({id: id}) .limit(1) // strict comparison as we don't want to return private keys on accident .project(includeMeta === true ? metaActorProj : actorProj) .next() } - -async function getOrCreateActor(preferredUsername, db, includeMeta) { - let user = await getActor(preferredUsername, db, includeMeta) - if (user) { - return user - } - // auto create groups whenever an unknown actor is referenced - user = await pub.actor.createLocalActor(preferredUsername, 'Group') - await db.collection('objects').insertOne(user) - // only executed on success - delete user._id - if (includeMeta !== true) { - delete user._meta - } - return user -} \ No newline at end of file diff --git a/store/index.js b/store/index.js index 18f48d8..687d00d 100644 --- a/store/index.js +++ b/store/index.js @@ -1,7 +1,8 @@ -'use strict'; +'use strict' // database interface module.exports = { setup: require('./setup'), actor: require('./actor'), + object: require('./object'), // stream: require('./stream'), } diff --git a/store/object.js b/store/object.js new file mode 100644 index 0000000..267e96d --- /dev/null +++ b/store/object.js @@ -0,0 +1,19 @@ +'use strict' + +module.exports = { + get, + save, +} + +function get (id, db) { + return db.collection('objects') + .find({id: id}) + .limit(1) + .project({_id: 0, _meta: 0}) + .next() +} + +function save (object) { + return db.collection('objects') + .insertOne(object) +} \ No newline at end of file diff --git a/store/setup.js b/store/setup.js index 2a31200..3955287 100644 --- a/store/setup.js +++ b/store/setup.js @@ -1,6 +1,6 @@ -const pub = require('../pub') +'use strict' -module.exports = async function dbSetup (db, domain) { +module.exports = async function dbSetup (db, domain, dummyUser) { // inbox await db.collection('streams').createIndex({ '_meta._target': 1, @@ -24,13 +24,14 @@ module.exports = async function dbSetup (db, domain) { await db.collection('objects').createIndex({ id: 1 }) - const dummyUser = await pub.actor.createLocalActor('dummy', 'Person') - await db.collection('objects').findOneAndReplace( - {preferredUsername: 'dummy'}, - dummyUser, - { - upsert: true, - returnOriginal: false, - } - ) -} \ No newline at end of file + if (dummyUser) { + return db.collection('objects').findOneAndReplace( + {preferredUsername: 'dummy'}, + dummyUser, + { + upsert: true, + returnOriginal: false, + } + ) + } +}