more organization, remote object resolution, signature verification as middleware
This commit is contained in:
parent
958951aa4b
commit
03b0ae732e
14 changed files with 138 additions and 64 deletions
14
index.js
14
index.js
|
@ -3,21 +3,22 @@ const path = require('path')
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const MongoClient = require('mongodb').MongoClient;
|
const MongoClient = require('mongodb').MongoClient;
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const routes = require('./routes')
|
|
||||||
const bodyParser = require('body-parser')
|
const bodyParser = require('body-parser')
|
||||||
const cors = require('cors')
|
const cors = require('cors')
|
||||||
const http = require('http')
|
const http = require('http')
|
||||||
const https = require('https')
|
const https = require('https')
|
||||||
const basicAuth = require('express-basic-auth');
|
const basicAuth = require('express-basic-auth');
|
||||||
|
|
||||||
const config = require('./config.json');
|
const routes = require('./routes')
|
||||||
const { USER, PASS, DOMAIN, KEY_PATH, CERT_PATH, PORT, PORT_HTTPS } = config;
|
const pub = require('./pub')
|
||||||
|
const config = require('./config.json')
|
||||||
|
const { USER, PASS, DOMAIN, KEY_PATH, CERT_PATH, PORT, PORT_HTTPS } = config
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
// Connection URL
|
// Connection URL
|
||||||
const url = 'mongodb://localhost:27017';
|
const url = 'mongodb://localhost:27017';
|
||||||
|
|
||||||
const dbSetup = require('./store/setup');
|
const store = require('./store');
|
||||||
// Database Name
|
// Database Name
|
||||||
const dbName = 'test';
|
const dbName = 'test';
|
||||||
|
|
||||||
|
@ -87,7 +88,10 @@ client.connect({useNewUrlParser: true})
|
||||||
console.log("Connected successfully to server");
|
console.log("Connected successfully to server");
|
||||||
db = client.db(dbName);
|
db = client.db(dbName);
|
||||||
app.set('db', db);
|
app.set('db', db);
|
||||||
return dbSetup(db, DOMAIN)
|
return pub.actor.createLocalActor('dummy', 'Person')
|
||||||
|
})
|
||||||
|
.then(dummy => {
|
||||||
|
return store.setup(db, DOMAIN, dummy)
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
https.createServer(sslOptions, app).listen(app.get('port-https'), function () {
|
https.createServer(sslOptions, app).listen(app.get('port-https'), function () {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
// middleware and networking utils
|
// middleware
|
||||||
module.exports = {
|
module.exports = {
|
||||||
validators: require('./validators'),
|
validators: require('./validators'),
|
||||||
// comms: require('./comms'),
|
security: require('./security'),
|
||||||
};
|
};
|
||||||
|
|
27
net/security.js
Normal file
27
net/security.js
Normal 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()
|
||||||
|
}
|
23
pub/actor.js
23
pub/actor.js
|
@ -1,13 +1,17 @@
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
|
const request = require('request-promise-native')
|
||||||
const {promisify} = require('util')
|
const {promisify} = require('util')
|
||||||
|
|
||||||
|
const store = require('../store')
|
||||||
|
const federation = require('./federation')
|
||||||
const pubUtils = require('./utils')
|
const pubUtils = require('./utils')
|
||||||
const config = require('../config.json')
|
const config = require('../config.json')
|
||||||
|
|
||||||
const generateKeyPairPromise = promisify(crypto.generateKeyPair)
|
const generateKeyPairPromise = promisify(crypto.generateKeyPair)
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createLocalActor
|
createLocalActor,
|
||||||
|
getOrCreateActor
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLocalActor (name, type) {
|
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
15
pub/federation.js
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -2,6 +2,8 @@
|
||||||
// ActivityPub / ActivityStreams utils
|
// ActivityPub / ActivityStreams utils
|
||||||
module.exports = {
|
module.exports = {
|
||||||
actor: require('./actor'),
|
actor: require('./actor'),
|
||||||
utils: require('./utils'),
|
|
||||||
consts: require('./consts'),
|
consts: require('./consts'),
|
||||||
|
federation: require('./federation'),
|
||||||
|
object: require('./object'),
|
||||||
|
utils: require('./utils'),
|
||||||
}
|
}
|
||||||
|
|
16
pub/object.js
Normal file
16
pub/object.js
Normal 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
|
||||||
|
}
|
|
@ -8,36 +8,21 @@ const request = require('request-promise-native')
|
||||||
const httpSignature = require('http-signature')
|
const httpSignature = require('http-signature')
|
||||||
const {ObjectId} = require('mongodb')
|
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');
|
const db = req.app.get('db');
|
||||||
let outgoingResponse
|
let outgoingResponse
|
||||||
req.body._meta = {_target: pub.utils.usernameToIRI(req.user)}
|
req.body._meta = {_target: pub.utils.usernameToIRI(req.user)}
|
||||||
// side effects
|
// side effects
|
||||||
switch(req.body.type) {
|
switch(req.body.type) {
|
||||||
case 'Accept':
|
case 'Accept':
|
||||||
// workaround for node-http-signature#87
|
// TODO - side effect ncessary for following collection?
|
||||||
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()
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
case 'Follow':
|
case 'Follow':
|
||||||
req.body._meta._target = req.body.object.id
|
req.body._meta._target = req.body.object.id
|
||||||
// send acceptance reply
|
// send acceptance reply
|
||||||
Promise.all([
|
Promise.all([
|
||||||
store.actor.getOrCreateActor(req.user, db, true),
|
pub.actor.getOrCreateActor(req.user, db, true),
|
||||||
request({
|
pub.object.resolveObject(pub.utils.actorFromActivity(req.body), db),
|
||||||
url: pub.utils.actorFromActivity(req.body),
|
|
||||||
headers: {Accept: 'application/activity+json'},
|
|
||||||
json: true,
|
|
||||||
})
|
|
||||||
])
|
])
|
||||||
.then(([user, actor]) => {
|
.then(([user, actor]) => {
|
||||||
if (!actor || !actor.inbox) {
|
if (!actor || !actor.inbox) {
|
||||||
|
|
|
@ -12,7 +12,7 @@ router.get('/:name', async function (req, res) {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let db = req.app.get('db')
|
let db = req.app.get('db')
|
||||||
const user = await store.actor.getOrCreateActor(name, db)
|
const user = await pub.actor.getOrCreateActor(name, db)
|
||||||
if (user) {
|
if (user) {
|
||||||
return res.json(pub.utils.toJSONLD(user))
|
return res.json(pub.utils.toJSONLD(user))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
const store = require('../store')
|
const pub = require('../pub')
|
||||||
const acctReg = /acct:[@~]?([^@]+)@?(.*)/
|
const acctReg = /acct:[@~]?([^@]+)@?(.*)/
|
||||||
router.get('/', function (req, res) {
|
router.get('/', function (req, res) {
|
||||||
let resource = req.query.resource;
|
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')
|
return res.status(400).send('Requested user is not from this domain')
|
||||||
}
|
}
|
||||||
let db = req.app.get('db');
|
let db = req.app.get('db');
|
||||||
store.actor.getOrCreateActor(acct[1], db)
|
pub.actor.getOrCreateActor(acct[1], db)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return res.status(404).send(`${acct[1]}@${acct[2]} not found`)
|
return res.status(404).send(`${acct[1]}@${acct[2]} not found`)
|
||||||
|
|
|
@ -1,34 +1,17 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
const pub = require('../pub')
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getActor,
|
getActor,
|
||||||
getOrCreateActor,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const actorProj = {_id: 0, _meta: 0}
|
const actorProj = {_id: 0, _meta: 0}
|
||||||
const metaActorProj = {_id: 0}
|
const metaActorProj = {_id: 0}
|
||||||
|
|
||||||
function getActor (preferredUsername, db, includeMeta) {
|
function getActor (id, db, includeMeta) {
|
||||||
return db.collection('objects')
|
return db.collection('objects')
|
||||||
.find({id: pub.utils.usernameToIRI(preferredUsername)})
|
.find({id: id})
|
||||||
.limit(1)
|
.limit(1)
|
||||||
// strict comparison as we don't want to return private keys on accident
|
// strict comparison as we don't want to return private keys on accident
|
||||||
.project(includeMeta === true ? metaActorProj : actorProj)
|
.project(includeMeta === true ? metaActorProj : actorProj)
|
||||||
.next()
|
.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
|
|
||||||
}
|
|
|
@ -1,7 +1,8 @@
|
||||||
'use strict';
|
'use strict'
|
||||||
// database interface
|
// database interface
|
||||||
module.exports = {
|
module.exports = {
|
||||||
setup: require('./setup'),
|
setup: require('./setup'),
|
||||||
actor: require('./actor'),
|
actor: require('./actor'),
|
||||||
|
object: require('./object'),
|
||||||
// stream: require('./stream'),
|
// stream: require('./stream'),
|
||||||
}
|
}
|
||||||
|
|
19
store/object.js
Normal file
19
store/object.js
Normal 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)
|
||||||
|
}
|
|
@ -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
|
// inbox
|
||||||
await db.collection('streams').createIndex({
|
await db.collection('streams').createIndex({
|
||||||
'_meta._target': 1,
|
'_meta._target': 1,
|
||||||
|
@ -24,13 +24,14 @@ module.exports = async function dbSetup (db, domain) {
|
||||||
await db.collection('objects').createIndex({
|
await db.collection('objects').createIndex({
|
||||||
id: 1
|
id: 1
|
||||||
})
|
})
|
||||||
const dummyUser = await pub.actor.createLocalActor('dummy', 'Person')
|
if (dummyUser) {
|
||||||
await db.collection('objects').findOneAndReplace(
|
return db.collection('objects').findOneAndReplace(
|
||||||
{preferredUsername: 'dummy'},
|
{preferredUsername: 'dummy'},
|
||||||
dummyUser,
|
dummyUser,
|
||||||
{
|
{
|
||||||
upsert: true,
|
upsert: true,
|
||||||
returnOriginal: false,
|
returnOriginal: false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue