From a64c44ad466fe64c6a38e3587d9ce14c040324dc Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 23 Sep 2019 22:18:35 -0500 Subject: [PATCH] production security: require signature for users with public keys, block outbox posting. Add missing routes to fetch objects and activities by id --- index.js | 30 +++++------ net/security.js | 14 +++++- pub/activity.js | 2 +- pub/utils.js | 8 +++ public/admin/index.html | 108 ---------------------------------------- routes/index.js | 5 +- routes/message.js | 20 -------- routes/object.js | 26 ++++++++++ routes/outbox.js | 2 +- routes/stream.js | 26 ++++++++++ routes/user.js | 3 +- store/stream.js | 18 ++++--- 12 files changed, 101 insertions(+), 161 deletions(-) delete mode 100644 public/admin/index.html delete mode 100644 routes/message.js create mode 100644 routes/object.js create mode 100644 routes/stream.js diff --git a/index.js b/index.js index ca368de..adc2af9 100644 --- a/index.js +++ b/index.js @@ -8,21 +8,11 @@ const https = require('https') const routes = require('./routes') const pub = require('./pub') -const config = require('./config.json') -const { DOMAIN, KEY_PATH, CERT_PATH, PORT, PORT_HTTPS } = config +const store = require('./store') +const { DOMAIN, KEY_PATH, CERT_PATH, PORT, PORT_HTTPS, DB_URL, DB_NAME } = require('./config.json') const app = express() -// Connection URL -const url = 'mongodb://localhost:27017' - -const store = require('./store') -// Database Name -const dbName = 'test' - -// Create a new MongoClient -const client = new MongoClient(url, { useUnifiedTopology: true }) - -let db +const client = new MongoClient(DB_URL, { useUnifiedTopology: true, useNewUrlParser: true }) const sslOptions = { key: fs.readFileSync(path.join(__dirname, KEY_PATH)), @@ -50,18 +40,22 @@ app.get('/', (req, res) => res.send('Hello World!')) // admin page app.use('/.well-known/webfinger', cors(), routes.webfinger) app.use('/u', cors(), routes.user) -app.use('/m', cors(), routes.message) +app.use('/o', cors(), routes.object) + +// admin page +app.use('/.well-known/webfinger', cors(), routes.webfinger) +app.use('/u', cors(), routes.user) +app.use('/o', cors(), routes.object) +app.use('/s', cors(), routes.stream) app.use('/u/:name/inbox', routes.inbox) app.use('/u/:name/outbox', routes.outbox) app.use('/admin', express.static('public/admin')) app.use('/f', express.static('public/files')) -// app.use('/hubs', express.static('../hubs/dist')); -// Use connect method to connect to the Server client.connect({ useNewUrlParser: true }) .then(() => { - console.log('Connected successfully to server') - db = client.db(dbName) + console.log('Connected successfully to db') + const db = client.db(DB_NAME) app.set('db', db) store.connection.setDb(db) return pub.actor.createLocalActor('dummy', 'Person') diff --git a/net/security.js b/net/security.js index a20b109..7f79093 100644 --- a/net/security.js +++ b/net/security.js @@ -3,13 +3,25 @@ const httpSignature = require('http-signature') const pub = require('../pub') // http communication middleware module.exports = { + auth, verifySignature } +function auth (req, res, next) { + // no client-to-server support at this time + if (req.app.get('env') !== 'development') { + return res.status(405).send() + } + next() +} + 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 + const actor = await pub.object.resolveObject(pub.utils.actorFromActivity(req.body)) + if (actor.publicKey && req.app.get('env') !== 'development') { + return res.status(400).send('Missing http signature') + } return next() } // workaround for node-http-signature#87 diff --git a/pub/activity.js b/pub/activity.js index b1dda40..88434c1 100644 --- a/pub/activity.js +++ b/pub/activity.js @@ -14,7 +14,7 @@ function build (type, actorId, object, to, cc, etc) { const oid = new ObjectId() const act = Object.assign({ // _id: oid, - id: pubUtils.objectIdToIRI(oid), + id: pubUtils.actvityIdToIRI(oid), type, actor: actorId, object, diff --git a/pub/utils.js b/pub/utils.js index cddcb83..4545f2f 100644 --- a/pub/utils.js +++ b/pub/utils.js @@ -3,6 +3,7 @@ const config = require('../config.json') const pubConsts = require('./consts') module.exports = { + actvityIdToIRI, usernameToIRI, toJSONLD, arrayToCollection, @@ -47,6 +48,13 @@ function objectIdToIRI (oid) { return `https://${config.DOMAIN}/o/${oid}` } +function actvityIdToIRI (oid) { + if (oid.toHexString) { + oid = oid.toHexString() + } + return `https://${config.DOMAIN}/s/${oid}` +} + function validateObject (object) { if (object && object.id) { // object['@context'] = object['@context'] || pubConsts.ASContext diff --git a/public/admin/index.html b/public/admin/index.html deleted file mode 100644 index ea66339..0000000 --- a/public/admin/index.html +++ /dev/null @@ -1,108 +0,0 @@ - - - - - Admin Page - - - -

Admin Page

-

Create Account

-

Create a new ActivityPub Actor (account). Requires the admin user/pass on submit.

-

- -

- -

-

Send Message To Followers

-

Enter an account name, its API key, and a message. This message will send to all its followers.

-

- -

-

-
a long hex key you got when you created your account -

-

-
-

- -

- - - - diff --git a/routes/index.js b/routes/index.js index 58e5eb9..b29d088 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,9 +1,10 @@ 'use strict' module.exports = { - user: require('./user'), - message: require('./message'), inbox: require('./inbox'), + object: require('./object'), outbox: require('./outbox'), + stream: require('./stream'), + user: require('./user'), webfinger: require('./webfinger') } diff --git a/routes/message.js b/routes/message.js deleted file mode 100644 index d21fb5d..0000000 --- a/routes/message.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' -const express = require('express') -const router = express.Router() - -router.get('/:guid', function (req, res) { - const guid = req.params.guid - if (!guid) { - return res.status(400).send('Bad request.') - } else { - const db = req.app.get('db') - const result = db.prepare('select message from messages where guid = ?').get(guid) - if (result === undefined) { - return res.status(404).send(`No record found for ${guid}.`) - } else { - res.json(JSON.parse(result.message)) - } - } -}) - -module.exports = router diff --git a/routes/object.js b/routes/object.js new file mode 100644 index 0000000..8b86657 --- /dev/null +++ b/routes/object.js @@ -0,0 +1,26 @@ +'use strict' +const express = require('express') +const router = express.Router() +const store = require('../store') +const pub = require('../pub') + +router.get('/:name', function (req, res) { + const name = req.params.name + if (!name) { + return res.status(400).send('Bad request.') + } else { + store.object.get(pub.utils.objectIdToIRI(name)) + .then(obj => { + if (obj) { + return res.json(pub.utils.toJSONLD(obj)) + } + return res.status(404).send('Object not found') + }) + .catch(err => { + console.log(err) + res.status(500).send() + }) + } +}) + +module.exports = router diff --git a/routes/outbox.js b/routes/outbox.js index 909e875..04298dc 100644 --- a/routes/outbox.js +++ b/routes/outbox.js @@ -4,7 +4,7 @@ const net = require('../net') const pub = require('../pub') const store = require('../store') -router.post('/', net.validators.outboxActivity, function (req, res) { +router.post('/', net.security.auth, net.validators.outboxActivity, function (req, res) { store.actor.get(pub.utils.usernameToIRI(req.user), true) .then(actor => { return pub.activity.addToOutbox(actor, req.body) diff --git a/routes/stream.js b/routes/stream.js new file mode 100644 index 0000000..a7cb6e2 --- /dev/null +++ b/routes/stream.js @@ -0,0 +1,26 @@ +'use strict' +const express = require('express') +const router = express.Router() +const store = require('../store') +const pub = require('../pub') + +router.get('/:name', function (req, res) { + const name = req.params.name + if (!name) { + return res.status(400).send('Bad request.') + } else { + store.stream.get(pub.utils.actvityIdToIRI(name)) + .then(obj => { + if (obj) { + return res.json(pub.utils.toJSONLD(obj)) + } + return res.status(404).send('Activity not found') + }) + .catch(err => { + console.log(err) + res.status(500).send() + }) + } +}) + +module.exports = router diff --git a/routes/user.js b/routes/user.js index de37014..71299c1 100644 --- a/routes/user.js +++ b/routes/user.js @@ -8,8 +8,7 @@ router.get('/:name', async function (req, res) { if (!name) { return res.status(400).send('Bad request.') } else { - const db = req.app.get('db') - const user = await pub.actor.getOrCreateActor(name, db) + const user = await pub.actor.getOrCreateActor(name) if (user) { return res.json(pub.utils.toJSONLD(user)) } diff --git a/store/stream.js b/store/stream.js index f7175fd..c603544 100644 --- a/store/stream.js +++ b/store/stream.js @@ -1,21 +1,23 @@ 'use strict' const connection = require('./connection') module.exports = { - // get, + get, save } -// function get (id, type, db) { -// return db.collection('objects') -// .find({ id: id }) -// .limit(1) -// .project({ _id: 0, _meta: 0 }) -// .next() -// } +function get (id) { + const db = connection.getDb() + return db.collection('streams') + .find({ id: id }) + .limit(1) + .project({ _id: 0, _meta: 0 }) + .next() +} async function save (activity) { const db = connection.getDb() const q = { id: activity.id } + // activities may be duplicated for multiple local targets if (activity._meta && activity._meta._target) { q['_meta._target'] = activity._meta._target }