more organization, remote object resolution, signature verification as middleware

This commit is contained in:
Will Murphy 2019-09-22 00:05:30 -05:00
parent 958951aa4b
commit 03b0ae732e
14 changed files with 138 additions and 64 deletions

View file

@ -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 () {

View file

@ -1,6 +1,6 @@
'use strict';
// middleware and networking utils
// middleware
module.exports = {
validators: require('./validators'),
// comms: require('./comms'),
security: require('./security'),
};

27
net/security.js Normal file
View file

@ -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()
}

View file

@ -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
}

15
pub/federation.js Normal file
View file

@ -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,
})
}

View file

@ -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'),
}

16
pub/object.js Normal file
View file

@ -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
}

View file

@ -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) {

View file

@ -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))
}

View file

@ -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`)

View file

@ -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
}

View file

@ -1,7 +1,8 @@
'use strict';
'use strict'
// database interface
module.exports = {
setup: require('./setup'),
actor: require('./actor'),
object: require('./object'),
// stream: require('./stream'),
}

19
store/object.js Normal file
View file

@ -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)
}

View file

@ -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,8 +24,8 @@ 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(
if (dummyUser) {
return db.collection('objects').findOneAndReplace(
{preferredUsername: 'dummy'},
dummyUser,
{
@ -34,3 +34,4 @@ module.exports = async function dbSetup (db, domain) {
}
)
}
}