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 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 () {
|
||||
|
|
|
@ -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
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 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
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
|
||||
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
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 {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) {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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`)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
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
|
||||
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,
|
||||
}
|
||||
)
|
||||
if (dummyUser) {
|
||||
return db.collection('objects').findOneAndReplace(
|
||||
{preferredUsername: 'dummy'},
|
||||
dummyUser,
|
||||
{
|
||||
upsert: true,
|
||||
returnOriginal: false,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue