This commit is contained in:
Will Murphy 2019-09-21 16:20:14 -05:00
parent 4d7e99461d
commit 8039b04799
20 changed files with 147 additions and 293 deletions

2
.gitignore vendored
View file

@ -3,3 +3,5 @@ package-lock.json
*.db
config.json
public/files
certs/
.vscode

5
db/actor.js Normal file
View file

@ -0,0 +1,5 @@
'use strict'
module.exports = {
}

7
db/index.js Normal file
View file

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

View file

@ -1,5 +1,4 @@
const utils = require('../utils')
const crypto = require('crypto')
const pub = require('../pub')
module.exports = async function dbSetup (db, domain) {
// inbox
@ -25,7 +24,7 @@ module.exports = async function dbSetup (db, domain) {
await db.collection('objects').createIndex({
id: 1
})
const dummyUser = await utils.createLocalActor('dummy', 'Person')
const dummyUser = await pub.actor.createLocalActor('dummy', 'Person')
await db.collection('objects').findOneAndReplace(
{preferredUsername: 'dummy'},
dummyUser,

View file

@ -72,9 +72,6 @@ app.param('name', function (req, res, next, id) {
app.get('/', (req, res) => res.send('Hello World!'));
// admin page
app.options('/api', cors());
app.use('/api', cors(), routes.api);
app.use('/api/admin', cors({ credentials: true, origin: true }), basicUserAuth, routes.admin);
app.use('/.well-known/webfinger', cors(), routes.webfinger);
app.use('/u', cors(), routes.user);
app.use('/m', cors(), routes.message);

6
net/index.js Normal file
View file

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

View file

@ -1,10 +1,10 @@
const {ObjectId} = require('mongodb')
// const activities = ['Create', ]
const {ASContext} = require('./consts')
const pub = require('../pub')
function validateObject (object) {
if (object && object.id) {
object['@context'] = object['@context'] || ASContext
object['@context'] = object['@context'] || pub.consts.ASContext
return true
}
}
@ -31,7 +31,7 @@ module.exports.outboxActivity = function outboxActivity (req, res, next) {
const newID = new ObjectId()
req.body = {
_id: newID,
'@context': ASContext,
'@context': pub.consts.ASContext,
type: 'Create',
id: `https://${req.app.get('domain')}/o/${newID.toHexString()}`,
actor: req.body.attributedTo,

48
pub/actor.js Normal file
View file

@ -0,0 +1,48 @@
const crypto = require('crypto')
const {promisify} = require('util')
const pubUtils = require('./utils')
const config = require('../config.json')
const generateKeyPairPromise = promisify(crypto.generateKeyPair)
module.exports = {
createLocalActor
}
function createLocalActor (name, type) {
return generateKeyPairPromise('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
}
}).then(pair => {
const actorBase = pubUtils.usernameToIRI(name);
return {
_meta: {
privateKey: pair.privateKey,
},
id: `${actorBase}`,
"type": type,
"following": `${actorBase}/following`,
"followers": `${actorBase}/followers`,
"liked": `${actorBase}/liked`,
"inbox": `${actorBase}/inbox`,
"outbox": `${actorBase}/outbox`,
"preferredUsername": name,
"name": "Dummy Person",
"summary": "Gotta have someone in the db",
"icon": `https://${config.DOMAIN}/f/${name}.png`,
publicKey: {
'id': `${actorBase}#main-key`,
'owner': `${actorBase}`,
'publicKeyPem': pair.publicKey
},
}
})
}

3
pub/consts.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
ASContext: 'https://www.w3.org/ns/activitystreams',
}

7
pub/index.js Normal file
View file

@ -0,0 +1,7 @@
'use strict';
// ActivityPub / ActivityStreams utils
module.exports = {
actor: require('./actor'),
utils: require('./utils'),
consts: require('./consts'),
}

39
pub/utils.js Normal file
View file

@ -0,0 +1,39 @@
'use strict'
const config = require('../config.json')
const consts = require('./consts')
module.exports = {
usernameToIRI,
toJSONLD,
arrayToCollection,
actorFromActivity,
}
function actorFromActivity (activity) {
if (Object.prototype.toString.call(activity.actor) === '[object String]') {
return activity.actor
}
if (activity.actor.type === 'Link') {
return activity.actor.href
}
return activity.actor.id
}
function arrayToCollection (arr, ordered) {
return {
'@context': consts.ASContext,
totalItems: arr.length,
type: ordered ? 'orderedCollection' : 'collection',
[ordered ? 'orderedItems' : 'items']: arr,
}
}
function toJSONLD (obj) {
obj['@context'] = obj['@context'] || consts.ASContext;
return obj;
}
function usernameToIRI (user) {
return `https://${config.DOMAIN}/u/${user}`
}

View file

@ -1,75 +0,0 @@
'use strict';
const express = require('express'),
router = express.Router(),
crypto = require('crypto');
function createActor(name, domain, pubkey) {
return {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1'
],
'id': `https://${domain}/u/${name}`,
'type': 'Person',
'preferredUsername': `${name}`,
'inbox': `https://${domain}/api/inbox`,
'followers': `https://${domain}/u/${name}/followers`,
'publicKey': {
'id': `https://${domain}/u/${name}#main-key`,
'owner': `https://${domain}/u/${name}`,
'publicKeyPem': pubkey
}
};
}
function createWebfinger(name, domain) {
return {
'subject': `acct:${name}@${domain}`,
'links': [
{
'rel': 'self',
'type': 'application/activity+json',
'href': `https://${domain}/u/${name}`
}
]
};
}
router.post('/create', function (req, res) {
// pass in a name for an account, if the account doesn't exist, create it!
const account = req.body.account;
if (account === undefined) {
return res.status(400).json({msg: 'Bad request. Please make sure "account" is a property in the POST body.'});
}
let db = req.app.get('db');
let domain = req.app.get('domain');
// create keypair
var pair = crypto.generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: 'top secret'
}
});
let actorRecord = createActor(account, domain, pair.publicKey);
let webfingerRecord = createWebfinger(account, domain);
const apikey = crypto.randomBytes(16).toString('hex');
try {
db.prepare('insert or replace into accounts(name, actor, apikey, pubkey, privkey, webfinger) values(?, ?, ?, ?, ?, ?)').run(`${account}@${domain}`, JSON.stringify(actorRecord), apikey, pair.publicKey, pair.privateKey, JSON.stringify(webfingerRecord));
res.status(200).json({msg: 'ok', apikey});
}
catch(e) {
res.status(200).json({error: e});
}
});
module.exports = router;

View file

@ -1,119 +0,0 @@
'use strict';
const express = require('express'),
router = express.Router(),
request = require('request'),
crypto = require('crypto');
router.post('/sendMessage', function (req, res) {
let db = req.app.get('db');
let domain = req.app.get('domain');
let acct = req.body.acct;
let apikey = req.body.apikey;
let message = req.body.message;
// check to see if your API key matches
let result = db.prepare('select apikey from accounts where name = ?').get(`${acct}@${domain}`);
if (result.apikey === apikey) {
sendCreateMessage(message, acct, domain, req, res);
}
else {
res.status(403).json({msg: 'wrong api key'});
}
});
function signAndSend(message, name, domain, req, res, targetDomain, inbox) {
// get the private key
let db = req.app.get('db');
let inboxFragment = inbox.replace('https://'+targetDomain,'');
let result = db.prepare('select privkey from accounts where name = ?').get(`${name}@${domain}`);
if (result === undefined) {
console.log(`No record found for ${name}.`);
}
else {
let privkey = result.privkey;
const signer = crypto.createSign('sha256');
let d = new Date();
let stringToSign = `(request-target): post ${inboxFragment}\nhost: ${targetDomain}\ndate: ${d.toUTCString()}`;
signer.update(stringToSign);
signer.end();
const signature = signer.sign(privkey);
const signature_b64 = signature.toString('base64');
let header = `keyId="https://${domain}/u/${name}",headers="(request-target) host date",signature="${signature_b64}"`;
request({
url: inbox,
headers: {
'Host': targetDomain,
'Date': d.toUTCString(),
'Signature': header
},
method: 'POST',
json: true,
body: message
}, function (error, response){
console.log(`Sent message to an inbox at ${targetDomain}!`);
if (error) {
console.log('Error:', error, response);
}
else {
console.log('Response Status Code:', response.statusCode);
}
});
}
}
function createMessage(text, name, domain, req, res, follower) {
const guidCreate = crypto.randomBytes(16).toString('hex');
const guidNote = crypto.randomBytes(16).toString('hex');
let db = req.app.get('db');
let d = new Date();
let noteMessage = {
'id': `https://${domain}/m/${guidNote}`,
'type': 'Note',
'published': d.toISOString(),
'attributedTo': `https://${domain}/u/${name}`,
'content': text,
'to': ['https://www.w3.org/ns/activitystreams#Public'],
};
let createMessage = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${domain}/m/${guidCreate}`,
'type': 'Create',
'actor': `https://${domain}/u/${name}`,
'to': ['https://www.w3.org/ns/activitystreams#Public'],
'cc': [follower],
'object': noteMessage
};
db.prepare('insert or replace into messages(guid, message) values(?, ?)').run( guidCreate, JSON.stringify(createMessage));
db.prepare('insert or replace into messages(guid, message) values(?, ?)').run( guidNote, JSON.stringify(noteMessage));
return createMessage;
}
function sendCreateMessage(text, name, domain, req, res) {
let db = req.app.get('db');
let result = db.prepare('select followers from accounts where name = ?').get(`${name}@${domain}`);
let followers = JSON.parse(result.followers);
console.log(followers);
console.log('type',typeof followers);
if (followers === null) {
console.log('aaaa');
res.status(400).json({msg: `No followers for account ${name}@${domain}`});
}
else {
for (let follower of followers) {
let inbox = follower+'/inbox';
let myURL = new URL(follower);
let targetDomain = myURL.hostname;
let message = createMessage(text, name, domain, req, res, follower);
signAndSend(message, name, domain, req, res, targetDomain, inbox);
}
res.status(200).json({msg: 'ok'});
}
}
module.exports = router;

View file

@ -1,14 +1,16 @@
const express = require('express')
const router = express.Router()
const utils = require('../utils')
const pub = require('../pub')
const net = require('../net')
const request = require('request-promise-native')
const httpSignature = require('http-signature')
const {ObjectId} = require('mongodb')
router.post('/', utils.validators.activity, function (req, res) {
router.post('/', net.validators.activity, function (req, res) {
const db = req.app.get('db');
let outgoingResponse
req.body._meta = {_target: utils.usernameToIRI(req.user)}
req.body._meta = {_target: pub.utils.usernameToIRI(req.user)}
// side effects
switch(req.body.type) {
case 'Accept':
@ -31,7 +33,7 @@ router.post('/', utils.validators.activity, function (req, res) {
Promise.all([
utils.getOrCreateActor(req.user, db, true),
request({
url: utils.actorFromActivity(req.body),
url: pub.utils.actorFromActivity(req.body),
headers: {Accept: 'application/activity+json'},
json: true,
})
@ -53,7 +55,7 @@ router.post('/', utils.validators.activity, function (req, res) {
headers: ['(request-target)', 'host', 'date'],
},
json: true,
body: utils.toJSONLD({
body: pub.utils.toJSONLD({
_id: newID,
type: 'Accept',
id: `https://${req.app.get('domain')}/o/${newID.toHexString()}`,
@ -80,11 +82,11 @@ router.post('/', utils.validators.activity, function (req, res) {
router.get('/', function (req, res) {
const db = req.app.get('db');
db.collection('streams')
.find({'_meta._target': utils.usernameToIRI(req.user)})
.find({'_meta._target': pub.utils.usernameToIRI(req.user)})
.sort({_id: -1})
.project({_id: 0, _meta: 0, '@context': 0, 'object._id': 0, 'object.@context': 0, 'object._meta': 0})
.toArray()
.then(stream => res.json(utils.arrayToCollection(stream, true)))
.then(stream => res.json(pub.utils.arrayToCollection(stream, true)))
.catch(err => {
console.log(err)
return res.status(500).send()

View file

@ -1,8 +1,6 @@
'use strict';
module.exports = {
api: require('./api'),
admin: require('./admin'),
user: require('./user'),
message: require('./message'),
inbox: require('./inbox'),

View file

@ -1,8 +1,10 @@
const express = require('express')
const router = express.Router()
const utils = require('../utils')
const net = require('../net')
const pub = require('../pub')
router.post('/', utils.validators.outboxActivity, function (req, res) {
router.post('/', net.validators.outboxActivity, function (req, res) {
const db = req.app.get('db');
Promise.all([
db.collection('objects').insertOne(req.body.object),
@ -17,11 +19,11 @@ router.post('/', utils.validators.outboxActivity, function (req, res) {
router.get('/', function (req, res) {
const db = req.app.get('db');
db.collection('streams')
.find({actor: utils.usernameToIRI(req.user)})
.find({actor: pub.utils.usernameToIRI(req.user)})
.sort({_id: -1})
.project({_id: 0, _meta: 0, 'object._id': 0, 'object.@context': 0, 'object._meta': 0})
.toArray()
.then(stream => res.json(utils.arrayToCollection(stream, true)))
.then(stream => res.json(pub.utils.arrayToCollection(stream, true)))
.catch(err => {
console.log(err)
return res.status(500).send()

View file

@ -2,7 +2,7 @@
const express = require('express'),
router = express.Router();
const utils = require('../utils')
const {toJSONLD} = require('../utils/index.js');
const pub = require('../pub')
router.get('/:name', async function (req, res) {
let name = req.params.name;
@ -13,7 +13,7 @@ router.get('/:name', async function (req, res) {
let db = req.app.get('db')
const user = await utils.getOrCreateActor(name, db)
if (user) {
return res.json(toJSONLD(user))
return res.json(pub.utils.toJSONLD(user))
}
return res.status(404).send('Person not found')
}
@ -28,13 +28,13 @@ router.get('/:name/followers', function (req, res) {
db.collection('streams')
.find({
type: 'Follow',
'_meta._target': utils.usernameToIRI(name),
'_meta._target': pub.utils.usernameToIRI(name),
})
.project({_id: 0, actor: 1})
.toArray()
.then(follows => {
const followers = follows.map(utils.actorFromActivity)
return res.json(utils.arrayToCollection(followers))
const followers = follows.map(pub.utils.actorFromActivity)
return res.json(pub.utils.arrayToCollection(followers))
})
.catch(err => {
console.log(err)

View file

@ -1,3 +1,2 @@
module.exports = {
ASContext: 'https://www.w3.org/ns/activitystreams',
}

View file

View file

@ -1,9 +1,11 @@
const crypto = require('crypto')
const {promisify} = require('util')
const {ASContext} = require('./consts')
const utils = require('../utils')
const config = require('../config.json')
const db = require('../db')
const pub = require('../pub')
module.exports.validators = require('./validators');
module.exports.consts = require('./consts')
function isObject(value) {
return value && typeof value === 'object' && value.constructor === Object
@ -20,68 +22,11 @@ function traverseObject(obj, f) {
Object.keys(obj).forEach(traverse)
return f(obj);
}
module.exports.toJSONLD = function (obj) {
obj['@context'] = obj['@context'] || ASContext;
return obj;
}
module.exports.arrayToCollection = function (arr, ordered) {
return {
'@context': ASContext,
totalItems: arr.length,
type: ordered ? 'orderedCollection' : 'collection',
[ordered ? 'orderedItems' : 'items']: arr,
}
}
function usernameToIRI (user) {
return `https://${config.DOMAIN}/u/${user}`
}
module.exports.usernameToIRI = usernameToIRI
const generateKeyPairPromise = promisify(crypto.generateKeyPair)
function createLocalActor (name, type) {
return generateKeyPairPromise('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
}
}).then(pair => {
const actorBase = usernameToIRI(name);
return {
_meta: {
privateKey: pair.privateKey,
},
id: `${actorBase}`,
"type": type,
"following": `${actorBase}/following`,
"followers": `${actorBase}/followers`,
"liked": `${actorBase}/liked`,
"inbox": `${actorBase}/inbox`,
"outbox": `${actorBase}/outbox`,
"preferredUsername": name,
"name": "Dummy Person",
"summary": "Gotta have someone in the db",
"icon": `https://${config.DOMAIN}/f/${name}.png`,
publicKey: {
'id': `${actorBase}#main-key`,
'owner': `${actorBase}`,
'publicKeyPem': pair.publicKey
},
}
})
}
module.exports.createLocalActor = createLocalActor
const actorProj = {_id: 0, _meta: 0}
const metaActorProj = {_id: 0}
async function getOrCreateActor(preferredUsername, db, includeMeta) {
const id = usernameToIRI(preferredUsername)
const id = pub.utils.usernameToIRI(preferredUsername)
let user = await db.collection('objects')
.find({id: id})
.limit(1)
@ -92,7 +37,7 @@ async function getOrCreateActor(preferredUsername, db, includeMeta) {
return user
}
// auto create groups whenever an unknown actor is referenced
user = await createLocalActor(preferredUsername, 'Group')
user = await pub.actor.createLocalActor(preferredUsername, 'Group')
await db.collection('objects').insertOne(user)
// only executed on success
delete user._id
@ -101,15 +46,4 @@ async function getOrCreateActor(preferredUsername, db, includeMeta) {
}
return user
}
module.exports.getOrCreateActor = getOrCreateActor
function actorFromActivity (activity) {
if (Object.prototype.toString.call(activity.actor) === '[object String]') {
return activity.actor
}
if (activity.actor.type === 'Link') {
return activity.actor.href
}
return activity.actor.id
}
module.exports.actorFromActivity = actorFromActivity
module.exports.getOrCreateActor = getOrCreateActor