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 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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
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')
|
||||
}
|
||||
|
|
|
@ -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 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
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) {
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue