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:
parent
53fa6353d2
commit
a64c44ad46
12 changed files with 101 additions and 161 deletions
30
index.js
30
index.js
|
@ -8,21 +8,11 @@ const https = require('https')
|
||||||
|
|
||||||
const routes = require('./routes')
|
const routes = require('./routes')
|
||||||
const pub = require('./pub')
|
const pub = require('./pub')
|
||||||
const config = require('./config.json')
|
const store = require('./store')
|
||||||
const { DOMAIN, KEY_PATH, CERT_PATH, PORT, PORT_HTTPS } = config
|
const { DOMAIN, KEY_PATH, CERT_PATH, PORT, PORT_HTTPS, DB_URL, DB_NAME } = require('./config.json')
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
// Connection URL
|
const client = new MongoClient(DB_URL, { useUnifiedTopology: true, useNewUrlParser: true })
|
||||||
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 sslOptions = {
|
const sslOptions = {
|
||||||
key: fs.readFileSync(path.join(__dirname, KEY_PATH)),
|
key: fs.readFileSync(path.join(__dirname, KEY_PATH)),
|
||||||
|
@ -50,18 +40,22 @@ 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('/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/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'));
|
|
||||||
|
|
||||||
// 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 db')
|
||||||
db = client.db(dbName)
|
const db = client.db(DB_NAME)
|
||||||
app.set('db', db)
|
app.set('db', db)
|
||||||
store.connection.setDb(db)
|
store.connection.setDb(db)
|
||||||
return pub.actor.createLocalActor('dummy', 'Person')
|
return pub.actor.createLocalActor('dummy', 'Person')
|
||||||
|
|
|
@ -3,13 +3,25 @@ const httpSignature = require('http-signature')
|
||||||
const pub = require('../pub')
|
const pub = require('../pub')
|
||||||
// http communication middleware
|
// http communication middleware
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
auth,
|
||||||
verifySignature
|
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) {
|
async function verifySignature (req, res, next) {
|
||||||
if (!req.get('authorization')) {
|
if (!req.get('authorization')) {
|
||||||
// support for apps not using signature extension to ActivityPub
|
// 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()
|
return next()
|
||||||
}
|
}
|
||||||
// workaround for node-http-signature#87
|
// workaround for node-http-signature#87
|
||||||
|
|
|
@ -14,7 +14,7 @@ function build (type, actorId, object, to, cc, etc) {
|
||||||
const oid = new ObjectId()
|
const oid = new ObjectId()
|
||||||
const act = Object.assign({
|
const act = Object.assign({
|
||||||
// _id: oid,
|
// _id: oid,
|
||||||
id: pubUtils.objectIdToIRI(oid),
|
id: pubUtils.actvityIdToIRI(oid),
|
||||||
type,
|
type,
|
||||||
actor: actorId,
|
actor: actorId,
|
||||||
object,
|
object,
|
||||||
|
|
|
@ -3,6 +3,7 @@ const config = require('../config.json')
|
||||||
const pubConsts = require('./consts')
|
const pubConsts = require('./consts')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
actvityIdToIRI,
|
||||||
usernameToIRI,
|
usernameToIRI,
|
||||||
toJSONLD,
|
toJSONLD,
|
||||||
arrayToCollection,
|
arrayToCollection,
|
||||||
|
@ -47,6 +48,13 @@ function objectIdToIRI (oid) {
|
||||||
return `https://${config.DOMAIN}/o/${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) {
|
function validateObject (object) {
|
||||||
if (object && object.id) {
|
if (object && object.id) {
|
||||||
// object['@context'] = object['@context'] || pubConsts.ASContext
|
// object['@context'] = object['@context'] || pubConsts.ASContext
|
||||||
|
|
|
@ -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} — 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>
|
|
|
@ -1,9 +1,10 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
user: require('./user'),
|
|
||||||
message: require('./message'),
|
|
||||||
inbox: require('./inbox'),
|
inbox: require('./inbox'),
|
||||||
|
object: require('./object'),
|
||||||
outbox: require('./outbox'),
|
outbox: require('./outbox'),
|
||||||
|
stream: require('./stream'),
|
||||||
|
user: require('./user'),
|
||||||
webfinger: require('./webfinger')
|
webfinger: require('./webfinger')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
26
routes/object.js
Normal 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
|
|
@ -4,7 +4,7 @@ const net = require('../net')
|
||||||
const pub = require('../pub')
|
const pub = require('../pub')
|
||||||
const store = require('../store')
|
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)
|
store.actor.get(pub.utils.usernameToIRI(req.user), true)
|
||||||
.then(actor => {
|
.then(actor => {
|
||||||
return pub.activity.addToOutbox(actor, req.body)
|
return pub.activity.addToOutbox(actor, req.body)
|
||||||
|
|
26
routes/stream.js
Normal file
26
routes/stream.js
Normal 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
|
|
@ -8,8 +8,7 @@ router.get('/:name', async function (req, res) {
|
||||||
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')
|
const user = await pub.actor.getOrCreateActor(name)
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
const connection = require('./connection')
|
const connection = require('./connection')
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// get,
|
get,
|
||||||
save
|
save
|
||||||
}
|
}
|
||||||
|
|
||||||
// function get (id, type, db) {
|
function get (id) {
|
||||||
// return db.collection('objects')
|
const db = connection.getDb()
|
||||||
// .find({ id: id })
|
return db.collection('streams')
|
||||||
// .limit(1)
|
.find({ id: id })
|
||||||
// .project({ _id: 0, _meta: 0 })
|
.limit(1)
|
||||||
// .next()
|
.project({ _id: 0, _meta: 0 })
|
||||||
// }
|
.next()
|
||||||
|
}
|
||||||
|
|
||||||
async function save (activity) {
|
async function save (activity) {
|
||||||
const db = connection.getDb()
|
const db = connection.getDb()
|
||||||
const q = { id: activity.id }
|
const q = { id: activity.id }
|
||||||
|
// activities may be duplicated for multiple local targets
|
||||||
if (activity._meta && activity._meta._target) {
|
if (activity._meta && activity._meta._target) {
|
||||||
q['_meta._target'] = activity._meta._target
|
q['_meta._target'] = activity._meta._target
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue