package details, lint

This commit is contained in:
Will Murphy 2019-09-22 00:20:37 -05:00
parent 03b0ae732e
commit a8230136f8
22 changed files with 302 additions and 339 deletions

View file

@ -1,93 +1,68 @@
const { promisify } = require('util')
const path = require('path') 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 bodyParser = require('body-parser') const bodyParser = require('body-parser')
const cors = require('cors') const cors = require('cors')
const http = require('http')
const https = require('https') const https = require('https')
const basicAuth = require('express-basic-auth');
const routes = require('./routes') const routes = require('./routes')
const pub = require('./pub') const pub = require('./pub')
const config = require('./config.json') const config = require('./config.json')
const { USER, PASS, DOMAIN, KEY_PATH, CERT_PATH, PORT, PORT_HTTPS } = config const { 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 store = require('./store'); const store = require('./store')
// Database Name // Database Name
const dbName = 'test'; const dbName = 'test'
// Create a new MongoClient // Create a new MongoClient
const client = new MongoClient(url, {useUnifiedTopology: true}); const client = new MongoClient(url, { useUnifiedTopology: true })
let db; let db
const sslOptions = {
let sslOptions;
sslOptions = {
key: fs.readFileSync(path.join(__dirname, KEY_PATH)), key: fs.readFileSync(path.join(__dirname, KEY_PATH)),
cert: fs.readFileSync(path.join(__dirname, CERT_PATH)) cert: fs.readFileSync(path.join(__dirname, CERT_PATH))
}; }
app.set('domain', DOMAIN)
app.set('domain', DOMAIN); app.set('port', process.env.PORT || PORT)
app.set('port', process.env.PORT || PORT); app.set('port-https', process.env.PORT_HTTPS || PORT_HTTPS)
app.set('port-https', process.env.PORT_HTTPS || PORT_HTTPS); app.use(bodyParser.json({
app.use(bodyParser.json({type: [ type: [
'application/activity+json', 'application/activity+json',
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
]})); // support json encoded bodies ]
app.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies })) // support json encoded bodies
app.use(bodyParser.urlencoded({ extended: true })) // support encoded bodies
// basic http authorizer
let basicUserAuth = basicAuth({
authorizer: asyncAuthorizer,
authorizeAsync: true,
challenge: true
});
function asyncAuthorizer(username, password, cb) {
let isAuthorized = false;
const isPasswordAuthorized = username === USER;
const isUsernameAuthorized = password === PASS;
isAuthorized = isPasswordAuthorized && isUsernameAuthorized;
if (isAuthorized) {
return cb(null, true);
}
else {
return cb(null, false);
}
}
app.param('name', function (req, res, next, id) { app.param('name', function (req, res, next, id) {
req.user = id req.user = id
next() next()
}) })
app.get('/', (req, res) => res.send('Hello World!')); app.get('/', (req, res) => res.send('Hello World!'))
// admin page // admin page
app.use('/.well-known/webfinger', cors(), routes.webfinger); app.use('/.well-known/webfinger', cors(), routes.webfinger)
app.use('/u', cors(), routes.user); app.use('/u', cors(), routes.user)
app.use('/m', cors(), routes.message); app.use('/m', cors(), routes.message)
app.use('/u/:name/inbox', routes.inbox) app.use('/u/:name/inbox', routes.inbox)
app.use('/u/:name/outbox', routes.outbox) app.use('/u/:name/outbox', routes.outbox)
app.use('/admin', express.static('public/admin')); app.use('/admin', express.static('public/admin'))
app.use('/f', express.static('public/files')); app.use('/f', express.static('public/files'))
// app.use('/hubs', express.static('../hubs/dist')); // app.use('/hubs', express.static('../hubs/dist'));
// Use connect method to connect to the Server // Use connect method to connect to the Server
client.connect({ useNewUrlParser: true }) client.connect({ useNewUrlParser: true })
.then(() => { .then(() => {
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 pub.actor.createLocalActor('dummy', 'Person') return pub.actor.createLocalActor('dummy', 'Person')
}) })
.then(dummy => { .then(dummy => {
@ -95,9 +70,9 @@ client.connect({useNewUrlParser: true})
}) })
.then(() => { .then(() => {
https.createServer(sslOptions, app).listen(app.get('port-https'), function () { https.createServer(sslOptions, app).listen(app.get('port-https'), function () {
console.log('Express server listening on port ' + app.get('port-https')); console.log('Express server listening on port ' + app.get('port-https'))
}); })
}) })
.catch(err => { .catch(err => {
throw new Error(err) throw new Error(err)
}); })

View file

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

View file

@ -3,7 +3,7 @@ const httpSignature = require('http-signature')
const pub = require('../pub') const pub = require('../pub')
// http communication middleware // http communication middleware
module.exports = { module.exports = {
verifySignature, verifySignature
} }
async function verifySignature (req, res, next) { async function verifySignature (req, res, next) {

View file

@ -40,7 +40,7 @@ module.exports.outboxActivity = function outboxActivity (req, res, next) {
to: req.body.to, to: req.body.to,
cc: req.body.cc, cc: req.body.cc,
bcc: req.body.cc, bcc: req.body.cc,
audience: req.body.audience, audience: req.body.audience
} }
} }
next() next()

View file

@ -1,7 +1,7 @@
{ {
"name": "hubbubpub", "name": "guppe",
"version": "1.0.0", "version": "0.0.1",
"description": "", "description": "Decentralized social groups with ActivityPub, NodeJS, Express, and Mongodb",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
@ -17,6 +17,7 @@
"node": ">=10.10.0" "node": ">=10.10.0"
}, },
"devDependencies": { "devDependencies": {
"standard": "^14.3.1",
"standardjs": "^1.0.0-alpha" "standardjs": "^1.0.0-alpha"
}, },
"scripts": { "scripts": {
@ -24,5 +25,6 @@
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "Will Murphy", "author": "Will Murphy",
"license": "AGPL-3.0-or-later" "license": "AGPL-3.0-or-later",
"repository": "https://github.com/wmurphyrd/guppe"
} }

View file

@ -1,9 +1,7 @@
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 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')
@ -23,30 +21,30 @@ function createLocalActor (name, type) {
}, },
privateKeyEncoding: { privateKeyEncoding: {
type: 'pkcs8', type: 'pkcs8',
format: 'pem', format: 'pem'
} }
}).then(pair => { }).then(pair => {
const actorBase = pubUtils.usernameToIRI(name); const actorBase = pubUtils.usernameToIRI(name)
return { return {
_meta: { _meta: {
privateKey: pair.privateKey, privateKey: pair.privateKey
}, },
id: `${actorBase}`, id: `${actorBase}`,
"type": type, type: type,
"following": `${actorBase}/following`, following: `${actorBase}/following`,
"followers": `${actorBase}/followers`, followers: `${actorBase}/followers`,
"liked": `${actorBase}/liked`, liked: `${actorBase}/liked`,
"inbox": `${actorBase}/inbox`, inbox: `${actorBase}/inbox`,
"outbox": `${actorBase}/outbox`, outbox: `${actorBase}/outbox`,
"preferredUsername": name, preferredUsername: name,
"name": "Dummy Person", name: 'Dummy Person',
"summary": "Gotta have someone in the db", summary: 'Gotta have someone in the db',
"icon": `https://${config.DOMAIN}/f/${name}.png`, icon: `https://${config.DOMAIN}/f/${name}.png`,
publicKey: { publicKey: {
'id': `${actorBase}#main-key`, id: `${actorBase}#main-key`,
'owner': `${actorBase}`, owner: `${actorBase}`,
'publicKeyPem': pair.publicKey publicKeyPem: pair.publicKey
}, }
} }
}) })
} }

View file

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

View file

@ -3,13 +3,13 @@ const request = require('request-promise-native')
// federation communication utilities // federation communication utilities
module.exports = { module.exports = {
requestObject, requestObject
} }
function requestObject (id) { function requestObject (id) {
return request({ return request({
url: id, url: id,
headers: { Accept: 'application/activity+json' }, headers: { Accept: 'application/activity+json' },
json: true, json: true
}) })
} }

View file

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

View file

@ -1,5 +1,6 @@
'use strict' 'use strict'
const store = require('../store') const store = require('../store')
const federation = require('./federation')
module.exports = { module.exports = {
resolveObject resolveObject
} }
@ -11,6 +12,6 @@ async function resolveObject (id, db) {
return object return object
} }
object = await federation.requestObject(id) object = await federation.requestObject(id)
await store.object.save(object) await store.object.save(object, db)
return object return object
} }

View file

@ -6,7 +6,7 @@ module.exports = {
usernameToIRI, usernameToIRI,
toJSONLD, toJSONLD,
arrayToCollection, arrayToCollection,
actorFromActivity, actorFromActivity
} }
function actorFromActivity (activity) { function actorFromActivity (activity) {
@ -20,18 +20,17 @@ function actorFromActivity (activity) {
} }
function arrayToCollection (arr, ordered) { function arrayToCollection (arr, ordered) {
return { return {
'@context': consts.ASContext, '@context': consts.ASContext,
totalItems: arr.length, totalItems: arr.length,
type: ordered ? 'orderedCollection' : 'collection', type: ordered ? 'orderedCollection' : 'collection',
[ordered ? 'orderedItems' : 'items']: arr, [ordered ? 'orderedItems' : 'items']: arr
} }
} }
function toJSONLD (obj) { function toJSONLD (obj) {
obj['@context'] = obj['@context'] || consts.ASContext; obj['@context'] = obj['@context'] || consts.ASContext
return obj; return obj
} }
function usernameToIRI (user) { function usernameToIRI (user) {

View file

@ -1,16 +1,12 @@
const express = require('express') const express = require('express')
const router = express.Router() const router = express.Router()
const utils = require('../utils')
const pub = require('../pub') const pub = require('../pub')
const net = require('../net') const net = require('../net')
const store = require('../store')
const request = require('request-promise-native') const request = require('request-promise-native')
const httpSignature = require('http-signature')
const { ObjectId } = require('mongodb') const { ObjectId } = require('mongodb')
router.post('/', net.validators.activity, net.security.verifySignature, 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
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) {
@ -22,7 +18,7 @@ router.post('/', net.validators.activity, net.security.verifySignature, function
// send acceptance reply // send acceptance reply
Promise.all([ Promise.all([
pub.actor.getOrCreateActor(req.user, db, true), pub.actor.getOrCreateActor(req.user, db, true),
pub.object.resolveObject(pub.utils.actorFromActivity(req.body), db), pub.object.resolveObject(pub.utils.actorFromActivity(req.body), db)
]) ])
.then(([user, actor]) => { .then(([user, actor]) => {
if (!actor || !actor.inbox) { if (!actor || !actor.inbox) {
@ -33,12 +29,12 @@ router.post('/', net.validators.activity, net.security.verifySignature, function
method: 'POST', method: 'POST',
url: actor.inbox, url: actor.inbox,
headers: { headers: {
'Content-Type': 'application/activity+json', 'Content-Type': 'application/activity+json'
}, },
httpSignature: { httpSignature: {
key: user._meta.privateKey, key: user._meta.privateKey,
keyId: user.id, keyId: user.id,
headers: ['(request-target)', 'host', 'date'], headers: ['(request-target)', 'host', 'date']
}, },
json: true, json: true,
body: pub.utils.toJSONLD({ body: pub.utils.toJSONLD({
@ -46,8 +42,8 @@ router.post('/', net.validators.activity, net.security.verifySignature, function
type: 'Accept', type: 'Accept',
id: `https://${req.app.get('domain')}/o/${newID.toHexString()}`, id: `https://${req.app.get('domain')}/o/${newID.toHexString()}`,
actor: user.id, actor: user.id,
object: req.body, object: req.body
}), })
} }
return request(responseOpts) return request(responseOpts)
}) })
@ -63,10 +59,10 @@ router.post('/', net.validators.activity, net.security.verifySignature, function
console.log(err) console.log(err)
res.status(500).send() res.status(500).send()
}) })
}); })
router.get('/', function (req, res) { router.get('/', function (req, res) {
const db = req.app.get('db'); const db = req.app.get('db')
db.collection('streams') db.collection('streams')
.find({ '_meta._target': pub.utils.usernameToIRI(req.user) }) .find({ '_meta._target': pub.utils.usernameToIRI(req.user) })
.sort({ _id: -1 }) .sort({ _id: -1 })
@ -77,7 +73,6 @@ router.get('/', function (req, res) {
console.log(err) console.log(err)
return res.status(500).send() return res.status(500).send()
}) })
;
}) })
module.exports = router; module.exports = router

View file

@ -1,9 +1,9 @@
'use strict'; 'use strict'
module.exports = { module.exports = {
user: require('./user'), user: require('./user'),
message: require('./message'), message: require('./message'),
inbox: require('./inbox'), inbox: require('./inbox'),
outbox: require('./outbox'), outbox: require('./outbox'),
webfinger: require('./webfinger'), webfinger: require('./webfinger')
}; }

View file

@ -1,22 +1,20 @@
'use strict'; 'use strict'
const express = require('express'), const express = require('express')
router = express.Router(); const router = express.Router()
router.get('/:guid', function (req, res) { router.get('/:guid', function (req, res) {
let guid = req.params.guid; const guid = req.params.guid
if (!guid) { if (!guid) {
return res.status(400).send('Bad request.'); return res.status(400).send('Bad request.')
} } else {
else { const db = req.app.get('db')
let db = req.app.get('db'); const result = db.prepare('select message from messages where guid = ?').get(guid)
let result = db.prepare('select message from messages where guid = ?').get(guid);
if (result === undefined) { if (result === undefined) {
return res.status(404).send(`No record found for ${guid}.`); return res.status(404).send(`No record found for ${guid}.`)
} } else {
else { res.json(JSON.parse(result.message))
res.json(JSON.parse(result.message));
} }
} }
}); })
module.exports = router; module.exports = router

View file

@ -1,11 +1,10 @@
const express = require('express') const express = require('express')
const router = express.Router() const router = express.Router()
const utils = require('../utils')
const net = require('../net') const net = require('../net')
const pub = require('../pub') const pub = require('../pub')
router.post('/', net.validators.outboxActivity, function (req, res) { router.post('/', net.validators.outboxActivity, function (req, res) {
const db = req.app.get('db'); const db = req.app.get('db')
Promise.all([ Promise.all([
db.collection('objects').insertOne(req.body.object), db.collection('objects').insertOne(req.body.object),
db.collection('streams').insertOne(req.body) db.collection('streams').insertOne(req.body)
@ -14,10 +13,10 @@ router.post('/', net.validators.outboxActivity, function (req, res) {
console.log(err) console.log(err)
res.status(500).send() res.status(500).send()
}) })
}); })
router.get('/', function (req, res) { router.get('/', function (req, res) {
const db = req.app.get('db'); const db = req.app.get('db')
db.collection('streams') db.collection('streams')
.find({ actor: pub.utils.usernameToIRI(req.user) }) .find({ actor: pub.utils.usernameToIRI(req.user) })
.sort({ _id: -1 }) .sort({ _id: -1 })
@ -28,7 +27,6 @@ router.get('/', function (req, res) {
console.log(err) console.log(err)
return res.status(500).send() return res.status(500).send()
}) })
;
}) })
module.exports = router; module.exports = router

View file

@ -1,35 +1,32 @@
'use strict' 'use strict'
const express = require('express') const express = require('express')
const router = express.Router() const router = express.Router()
const utils = require('../utils')
const pub = require('../pub') const pub = require('../pub')
const store = require('../store')
router.get('/:name', async function (req, res) { router.get('/:name', async function (req, res) {
let name = req.params.name; const name = req.params.name
if (!name) { if (!name) {
return res.status(400).send('Bad request.'); return res.status(400).send('Bad request.')
} } else {
else { const db = req.app.get('db')
let db = req.app.get('db')
const user = await pub.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))
} }
return res.status(404).send('Person not found') return res.status(404).send('Person not found')
} }
}); })
router.get('/:name/followers', function (req, res) { router.get('/:name/followers', function (req, res) {
let name = req.params.name; const name = req.params.name
if (!name) { if (!name) {
return res.status(400).send('Bad request.'); return res.status(400).send('Bad request.')
} }
const db = req.app.get('db') const db = req.app.get('db')
db.collection('streams') db.collection('streams')
.find({ .find({
type: 'Follow', type: 'Follow',
'_meta._target': pub.utils.usernameToIRI(name), '_meta._target': pub.utils.usernameToIRI(name)
}) })
.project({ _id: 0, actor: 1 }) .project({ _id: 0, actor: 1 })
.toArray() .toArray()
@ -41,6 +38,6 @@ router.get('/:name/followers', function (req, res) {
console.log(err) console.log(err)
return res.status(500).send() return res.status(500).send()
}) })
}); })
module.exports = router; module.exports = router

View file

@ -5,36 +5,36 @@ const router = express.Router()
const pub = require('../pub') 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; const resource = req.query.resource
let acct = acctReg.exec(resource) const acct = acctReg.exec(resource)
if (!acct || acct.length < 2) { if (!acct || acct.length < 2) {
return res.status(400).send('Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.'); return res.status(400).send('Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.')
} }
if (acct[2] && acct[2].toLowerCase() !== req.app.get('domain').toLowerCase()) { if (acct[2] && acct[2].toLowerCase() !== req.app.get('domain').toLowerCase()) {
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'); const db = req.app.get('db')
pub.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`)
} }
const finger = { const finger = {
'subject': resource, subject: resource,
'links': [ links: [
{ {
'rel': 'self', rel: 'self',
'type': 'application/activity+json', type: 'application/activity+json',
'href': result.id href: result.id
} }
] ]
} }
return res.json(finger) return res.json(finger)
}) })
.catch(err => { .catch(err => {
console.log(err); console.log(err)
res.status(500).send() res.status(500).send()
}) })
}); })
module.exports = router; module.exports = router

View file

@ -1,7 +1,7 @@
'use strict' 'use strict'
module.exports = { module.exports = {
getActor, getActor
} }
const actorProj = { _id: 0, _meta: 0 } const actorProj = { _id: 0, _meta: 0 }

View file

@ -3,6 +3,6 @@
module.exports = { module.exports = {
setup: require('./setup'), setup: require('./setup'),
actor: require('./actor'), actor: require('./actor'),
object: require('./object'), object: require('./object')
// stream: require('./stream'), // stream: require('./stream'),
} }

View file

@ -2,7 +2,7 @@
module.exports = { module.exports = {
get, get,
save, save
} }
function get (id, db) { function get (id, db) {
@ -13,7 +13,7 @@ function get (id, db) {
.next() .next()
} }
function save (object) { function save (object, db) {
return db.collection('objects') return db.collection('objects')
.insertOne(object) .insertOne(object)
} }

View file

@ -4,13 +4,13 @@ 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,
_id: -1, _id: -1
}, { }, {
name: 'inbox' name: 'inbox'
}) })
// followers // followers
await db.collection('streams').createIndex({ await db.collection('streams').createIndex({
'_meta._target': 1, '_meta._target': 1
}, { }, {
partialFilterExpression: { type: 'Follow' }, partialFilterExpression: { type: 'Follow' },
name: 'followers' name: 'followers'
@ -18,7 +18,7 @@ module.exports = async function dbSetup (db, domain, dummyUser) {
// outbox // outbox
await db.collection('streams').createIndex({ await db.collection('streams').createIndex({
actor: 1, actor: 1,
_id: -1, _id: -1
}) })
// object lookup // object lookup
await db.collection('objects').createIndex({ await db.collection('objects').createIndex({
@ -30,7 +30,7 @@ module.exports = async function dbSetup (db, domain, dummyUser) {
dummyUser, dummyUser,
{ {
upsert: true, upsert: true,
returnOriginal: false, returnOriginal: false
} }
) )
} }