diff --git a/.gitignore b/.gitignore index e3d61be..726d581 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ package-lock.json *.db config.json public/files +certs/ +.vscode diff --git a/db/actor.js b/db/actor.js new file mode 100644 index 0000000..6f0c13d --- /dev/null +++ b/db/actor.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + +} diff --git a/db/index.js b/db/index.js new file mode 100644 index 0000000..18f48d8 --- /dev/null +++ b/db/index.js @@ -0,0 +1,7 @@ +'use strict'; +// database interface +module.exports = { + setup: require('./setup'), + actor: require('./actor'), +// stream: require('./stream'), +} diff --git a/db/setup.js b/db/setup.js index 459cf72..2a31200 100644 --- a/db/setup.js +++ b/db/setup.js @@ -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, diff --git a/index.js b/index.js index 13d68dd..4b6b2a2 100644 --- a/index.js +++ b/index.js @@ -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); diff --git a/net/index.js b/net/index.js new file mode 100644 index 0000000..2fd4a7b --- /dev/null +++ b/net/index.js @@ -0,0 +1,6 @@ +'use strict'; +// middleware and networking utils +module.exports = { + validators: require('./validators'), +// comms: require('./comms'), +}; diff --git a/utils/validators.js b/net/validators.js similarity index 88% rename from utils/validators.js rename to net/validators.js index 3ce8014..e1325bb 100644 --- a/utils/validators.js +++ b/net/validators.js @@ -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, diff --git a/pub/actor.js b/pub/actor.js new file mode 100644 index 0000000..2923814 --- /dev/null +++ b/pub/actor.js @@ -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 + }, + } + }) +} diff --git a/pub/consts.js b/pub/consts.js new file mode 100644 index 0000000..11774f4 --- /dev/null +++ b/pub/consts.js @@ -0,0 +1,3 @@ +module.exports = { + ASContext: 'https://www.w3.org/ns/activitystreams', +} \ No newline at end of file diff --git a/pub/index.js b/pub/index.js new file mode 100644 index 0000000..6912b68 --- /dev/null +++ b/pub/index.js @@ -0,0 +1,7 @@ +'use strict'; +// ActivityPub / ActivityStreams utils +module.exports = { + actor: require('./actor'), + utils: require('./utils'), + consts: require('./consts'), +} diff --git a/pub/utils.js b/pub/utils.js new file mode 100644 index 0000000..fd76e07 --- /dev/null +++ b/pub/utils.js @@ -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}` +} diff --git a/routes/admin.js b/routes/admin.js deleted file mode 100644 index bb8f38d..0000000 --- a/routes/admin.js +++ /dev/null @@ -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; diff --git a/routes/api.js b/routes/api.js deleted file mode 100644 index d80a474..0000000 --- a/routes/api.js +++ /dev/null @@ -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; diff --git a/routes/inbox.js b/routes/inbox.js index d7a0d59..5f0f2b4 100644 --- a/routes/inbox.js +++ b/routes/inbox.js @@ -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() diff --git a/routes/index.js b/routes/index.js index fec804c..1332fa0 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,8 +1,6 @@ 'use strict'; module.exports = { - api: require('./api'), - admin: require('./admin'), user: require('./user'), message: require('./message'), inbox: require('./inbox'), diff --git a/routes/outbox.js b/routes/outbox.js index 6eb81c3..30771ab 100644 --- a/routes/outbox.js +++ b/routes/outbox.js @@ -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() diff --git a/routes/user.js b/routes/user.js index 6f76119..24fec37 100644 --- a/routes/user.js +++ b/routes/user.js @@ -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) diff --git a/utils/consts.js b/utils/consts.js index 0a2839e..631f375 100644 --- a/utils/consts.js +++ b/utils/consts.js @@ -1,3 +1,2 @@ module.exports = { - ASContext: 'https://www.w3.org/ns/activitystreams', } diff --git a/utils/db.js b/utils/db.js deleted file mode 100644 index e69de29..0000000 diff --git a/utils/index.js b/utils/index.js index be11f8d..d32dabc 100644 --- a/utils/index.js +++ b/utils/index.js @@ -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 \ No newline at end of file +module.exports.getOrCreateActor = getOrCreateActor \ No newline at end of file