production security: require signature for users with public keys, block outbox posting. Add missing routes to fetch objects and activities by id

This commit is contained in:
Will Murphy 2019-09-23 22:18:35 -05:00
parent 53fa6353d2
commit a64c44ad46
12 changed files with 101 additions and 161 deletions

View file

@ -8,21 +8,11 @@ const https = require('https')
const routes = require('./routes')
const pub = require('./pub')
const config = require('./config.json')
const { DOMAIN, KEY_PATH, CERT_PATH, PORT, PORT_HTTPS } = config
const store = require('./store')
const { DOMAIN, KEY_PATH, CERT_PATH, PORT, PORT_HTTPS, DB_URL, DB_NAME } = require('./config.json')
const app = express()
// Connection URL
const url = 'mongodb://localhost:27017'
const store = require('./store')
// Database Name
const dbName = 'test'
// Create a new MongoClient
const client = new MongoClient(url, { useUnifiedTopology: true })
let db
const client = new MongoClient(DB_URL, { useUnifiedTopology: true, useNewUrlParser: true })
const sslOptions = {
key: fs.readFileSync(path.join(__dirname, KEY_PATH)),
@ -50,18 +40,22 @@ app.get('/', (req, res) => res.send('Hello World!'))
// admin page
app.use('/.well-known/webfinger', cors(), routes.webfinger)
app.use('/u', cors(), routes.user)
app.use('/m', cors(), routes.message)
app.use('/o', cors(), routes.object)
// admin page
app.use('/.well-known/webfinger', cors(), routes.webfinger)
app.use('/u', cors(), routes.user)
app.use('/o', cors(), routes.object)
app.use('/s', cors(), routes.stream)
app.use('/u/:name/inbox', routes.inbox)
app.use('/u/:name/outbox', routes.outbox)
app.use('/admin', express.static('public/admin'))
app.use('/f', express.static('public/files'))
// app.use('/hubs', express.static('../hubs/dist'));
// Use connect method to connect to the Server
client.connect({ useNewUrlParser: true })
.then(() => {
console.log('Connected successfully to server')
db = client.db(dbName)
console.log('Connected successfully to db')
const db = client.db(DB_NAME)
app.set('db', db)
store.connection.setDb(db)
return pub.actor.createLocalActor('dummy', 'Person')

View file

@ -3,13 +3,25 @@ const httpSignature = require('http-signature')
const pub = require('../pub')
// http communication middleware
module.exports = {
auth,
verifySignature
}
function auth (req, res, next) {
// no client-to-server support at this time
if (req.app.get('env') !== 'development') {
return res.status(405).send()
}
next()
}
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
const actor = await pub.object.resolveObject(pub.utils.actorFromActivity(req.body))
if (actor.publicKey && req.app.get('env') !== 'development') {
return res.status(400).send('Missing http signature')
}
return next()
}
// workaround for node-http-signature#87

View file

@ -14,7 +14,7 @@ function build (type, actorId, object, to, cc, etc) {
const oid = new ObjectId()
const act = Object.assign({
// _id: oid,
id: pubUtils.objectIdToIRI(oid),
id: pubUtils.actvityIdToIRI(oid),
type,
actor: actorId,
object,

View file

@ -3,6 +3,7 @@ const config = require('../config.json')
const pubConsts = require('./consts')
module.exports = {
actvityIdToIRI,
usernameToIRI,
toJSONLD,
arrayToCollection,
@ -47,6 +48,13 @@ function objectIdToIRI (oid) {
return `https://${config.DOMAIN}/o/${oid}`
}
function actvityIdToIRI (oid) {
if (oid.toHexString) {
oid = oid.toHexString()
}
return `https://${config.DOMAIN}/s/${oid}`
}
function validateObject (object) {
if (object && object.id) {
// object['@context'] = object['@context'] || pubConsts.ASContext

View file

@ -1,108 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Admin Page</title>
<style>
body {
font-family: sans-serif;
max-width: 900px;
margin: 30px;
}
img {
max-width: 100px;
}
li {
margin-bottom: 0.2em;
}
.account {
}
input {
width: 300px;
font-size: 1.2em;
}
.hint {
font-size: 0.8em;
}
button {
font-size: 1.2em;
}
</style>
</head>
<body>
<h1>Admin Page</h1>
<h2>Create Account</h2>
<p>Create a new ActivityPub Actor (account). Requires the admin user/pass on submit.</p>
<p>
<input id="account" type="text" placeholder="myAccountName"/>
</p>
<button onclick="createAccount()">Create Account</button>
<p id="createOutput"></p>
<h2>Send Message To Followers</h2>
<p>Enter an account name, its API key, and a message. This message will send to all its followers.</p>
<p>
<input id="acct" type="text" placeholder="myAccountName"/>
</p>
<p>
<input id="apikey" type="text" placeholder="1234567890abcdef"/><br><span class="hint">a long hex key you got when you created your account</span>
</p>
<p>
<input id="message" type="text" placeholder="Hello there."/><br>
</p>
<button onclick="sendMessage()">Send Message</button>
<p id="sendOutput"></p>
<script>
function queryStringFromObject(obj) {
return Object.keys(obj).map(key => key + '=' + obj[key]).join('&');
}
function sendMessage() {
let acct = document.querySelector('#acct').value;
let apikey = document.querySelector('#apikey').value;
let message = document.querySelector('#message').value;
postData('/api/sendMessage', {acct, apikey, message})
.then(data => {
console.log(data);
if (data.msg && data.msg === 'ok') {
let outputElement = document.querySelector('#sendOutput');
outputElement.innerHTML = 'Message sent successfully!';
}
}) // JSON-string from `response.json()` call
.catch(error => console.error(error));
}
function createAccount() {
let account = document.querySelector('#account').value;
postData('/api/admin/create', {account})
.then(data => {
console.log('data', data);
if (data.msg && data.msg === 'ok') {
let outputElement = document.querySelector('#createOutput');
outputElement.innerHTML = `Account created successfully! To confirm, go to <a href="/u/${account}">this URL</a>, you should see JSON for the new account Actor. Next verify that there is some JSON being served from <a href="/.well-known/webfinger?resource=acct:${account}@${window.location.hostname}">at the account's webfinger URL</a>. Then try to find ${account}@${window.location.hostname} from the search in Mastodon or another ActivityPub client. You should be able to follow the account. <br><br>Your API key for sending messages is ${data.apikey} &mdash; please save this somewhere!`;
}
}) // JSON-string from `response.json()` call
.catch(error => console.error(error));
}
function postData(url = ``, data = {}) {
// Default options are marked with *
return fetch(url, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
mode: "cors", // no-cors, cors, *same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, same-origin, *omit
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
redirect: "follow", // manual, *follow, error
referrer: "no-referrer", // no-referrer, *client
body: queryStringFromObject(data), // body data type must match "Content-Type" header
})
.then(response => response.json()); // parses response to JSON
}
</script>
</body>
</html>

View file

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

View file

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

26
routes/object.js Normal file
View file

@ -0,0 +1,26 @@
'use strict'
const express = require('express')
const router = express.Router()
const store = require('../store')
const pub = require('../pub')
router.get('/:name', function (req, res) {
const name = req.params.name
if (!name) {
return res.status(400).send('Bad request.')
} else {
store.object.get(pub.utils.objectIdToIRI(name))
.then(obj => {
if (obj) {
return res.json(pub.utils.toJSONLD(obj))
}
return res.status(404).send('Object not found')
})
.catch(err => {
console.log(err)
res.status(500).send()
})
}
})
module.exports = router

View file

@ -4,7 +4,7 @@ const net = require('../net')
const pub = require('../pub')
const store = require('../store')
router.post('/', net.validators.outboxActivity, function (req, res) {
router.post('/', net.security.auth, net.validators.outboxActivity, function (req, res) {
store.actor.get(pub.utils.usernameToIRI(req.user), true)
.then(actor => {
return pub.activity.addToOutbox(actor, req.body)

26
routes/stream.js Normal file
View file

@ -0,0 +1,26 @@
'use strict'
const express = require('express')
const router = express.Router()
const store = require('../store')
const pub = require('../pub')
router.get('/:name', function (req, res) {
const name = req.params.name
if (!name) {
return res.status(400).send('Bad request.')
} else {
store.stream.get(pub.utils.actvityIdToIRI(name))
.then(obj => {
if (obj) {
return res.json(pub.utils.toJSONLD(obj))
}
return res.status(404).send('Activity not found')
})
.catch(err => {
console.log(err)
res.status(500).send()
})
}
})
module.exports = router

View file

@ -8,8 +8,7 @@ router.get('/:name', async function (req, res) {
if (!name) {
return res.status(400).send('Bad request.')
} else {
const db = req.app.get('db')
const user = await pub.actor.getOrCreateActor(name, db)
const user = await pub.actor.getOrCreateActor(name)
if (user) {
return res.json(pub.utils.toJSONLD(user))
}

View file

@ -1,21 +1,23 @@
'use strict'
const connection = require('./connection')
module.exports = {
// get,
get,
save
}
// function get (id, type, db) {
// return db.collection('objects')
// .find({ id: id })
// .limit(1)
// .project({ _id: 0, _meta: 0 })
// .next()
// }
function get (id) {
const db = connection.getDb()
return db.collection('streams')
.find({ id: id })
.limit(1)
.project({ _id: 0, _meta: 0 })
.next()
}
async function save (activity) {
const db = connection.getDb()
const q = { id: activity.id }
// activities may be duplicated for multiple local targets
if (activity._meta && activity._meta._target) {
q['_meta._target'] = activity._meta._target
}