diff --git a/app.js b/app.js index 9a69689..502ae6e 100644 --- a/app.js +++ b/app.js @@ -5,9 +5,26 @@ const glob = require('glob'); const log4js = require('log4js'); const { match: createPathMatch } = require('path-to-regexp'); const log = require('./lib/log'); +var bodyParser = require('body-parser'); (async () => { const app = express(); + app.use(bodyParser.json({ type: 'application/*+json', + verify: function (req, _res, buf, _encoding) { + req.rawBody = buf; + } + })); + app.use(bodyParser.json({ + verify: function (req, _res, buf, _encoding) { + req.rawBody = buf; + } + })); + app.use(bodyParser.urlencoded({ + extended: false, + verify: function (req, _res, buf, _encoding) { + req.rawBody = buf; + } + })); const routes = await glob('**/*.js', { cwd: './routes', dot: true, diff --git a/lib/queue_worker.js b/lib/queue_worker.js new file mode 100644 index 0000000..5a1e218 --- /dev/null +++ b/lib/queue_worker.js @@ -0,0 +1,75 @@ +'use strict'; + +var db = require('./db'); +const http_agent = require('./http_agent'); +const jsonld = require('jsonld'); +var crypto = require('crypto'); + +module.exports = { + do_one_queue: async () => { + var result = await db('queue').orderBy('id', 'asc').limit(1); + if ( result[0] ) { + await db('queue').del().where('id', '=', result[0]["id"]); + if ( result[0]["type"] === "verify_inbox" ) { + var data = JSON.parse(result[0]["data"]); + var sig_header = data.sig_header; + var signature_split = sig_header.split(/,/); + var signature_elements = {}; + signature_split.forEach((obj) => { + signature_elements[obj.split('"')[1].split(/=/)[0]] = obj.split('"')[1].split(/=/)[1]; + }); + var keyUrl = new URL(signature_elements["keyId"]); + var secondKeyUrl = new URL(keyUrl); + secondKeyUrl.hash = undefined; + var actor = (await db("object_box").where("origin", secondKeyUrl.toString()).andWhere("expires", ">=", (new Date()).getTime()))[0]["object"]; + if ( !actor ) { + actor = http_agent.request(secondKeyUrl, {actor: data["actor"]})["data"]; + await db("object_box").insert({ + origin: secondKeyUrl.toString(), + to: null, + cc: null, + object: actor, + signedby: "fetch", + // TODO: We should respect the caching instructions provided by origin. + expires: (new Date()).getTime() + 604800000 // 7 days. + }); + } + // TODO: Should we standardize on a primary schema at some point? + var actor_parsed = await jsonld.compact(JSON.parse(actor), ["https://www.w3.org/ns/activitystreams", { security: "https://w3id.org/security#"}]); + var publicKey = ""; + if ( Array.isArray(actor_parsed["security:publicKey"]) ) { + actor_parsed["security:publicKey"].forEach((value) => { + if ( value["id"] === keyUrl.toString() ) { + publicKey = value["security:publicKeyPem"]; + } + }); + } else { + publicKey = actor_parsed["security:publicKey"]["security:publicKeyPem"]; + } + var valid = crypto.verify("RSA-SHA256", data["body"], publicKey, signature_elements["signature"]); + var parsedBody = await jsonld.compact(JSON.parse(data["body"]), ["https://www.w3.org/ns/activitystreams", { security: "https://w3id.org/security#"}]); + var to = parsedBody.to; + if ( typeof to === "string" ) { + to = [to]; + } + var cc = parsedBody.cc; + if ( typeof cc === "string" ) { + cc = [cc]; + } + if ( valid ) { + // TODO: We should care that the key hostname and the object hostname match. + await db("object_box").insert({ + origin: parsedBody.id, + to, + cc, + object: parsedBody, + signedby: keyUrl.toString(), + expires: (new Date()).getTime() + 604800000 // 7 days. + }); + } else { + await db("queue").insert({task: "fetch_object", data: JSON.stringify({object: parsedBody.id, actor: data["actor"]})}); + } + } + } + } +}; diff --git a/migrations/20230414021844_rename_inbox_to_object_box.js b/migrations/20230414021844_rename_inbox_to_object_box.js new file mode 100644 index 0000000..2971e1a --- /dev/null +++ b/migrations/20230414021844_rename_inbox_to_object_box.js @@ -0,0 +1,19 @@ +/* eslint-disable jsdoc/valid-types */ +// eslint doesn't seem to understand, but vscode does. +'use strict'; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema.renameTable("inbox", "object_box"); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema.renameTable("object_box", "inbox"); +}; diff --git a/package-lock.json b/package-lock.json index 332c058..e67f610 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "WTFPL", "dependencies": { + "body-parser": "^1.20.2", "express": "^4.18.2", "glob": "^9.3.0", "jsonld": "^8.1.1", @@ -439,12 +440,12 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -452,7 +453,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -1138,11 +1139,48 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/express/node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2522,9 +2560,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", diff --git a/package.json b/package.json index cd8cd3d..2d3d69f 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "author": "Andrew Pietila ", "license": "WTFPL", "dependencies": { + "body-parser": "^1.20.2", "express": "^4.18.2", "glob": "^9.3.0", "jsonld": "^8.1.1", diff --git a/routes/inbox.js b/routes/inbox.js index 8790f69..f4bc949 100644 --- a/routes/inbox.js +++ b/routes/inbox.js @@ -19,7 +19,7 @@ module.exports = { var signature_split = req.header("signature").split(/,/); var signature_elements = {}; signature_split.forEach((obj) => { - signature_elements[obj.split(/=/)[0]] = obj.split(/=/)[1]; + signature_elements[obj.split('"')[1].split(/=/)[0]] = obj.split('"')[1].split(/=/)[1]; }); var headers_to_check = signature_elements["headers"].split(/ /); if ( headers_to_check.includes("host") && headers_to_check.includes("date") && headers_to_check.includes("digest") && headers_to_check.includes("(request-target)")) { @@ -35,7 +35,7 @@ module.exports = { } }).join('\n'); // ... then lets dump this in the queue for now. - await db("queue").insert({task: "verify_inbox", data: JSON.stringify({sig_header: req.header("signature"), signed_block, body: req.body, date: (new Date()).toISOString()})}); + await db("queue").insert({task: "verify_inbox", data: JSON.stringify({sig_header: req.header("signature"), signed_block, body: req.rawBody.toString("UTF-8"), date: (new Date()).toISOString(), actor: `${req.hostname}@${req.hostname}`})}); res.status(204); return res.end(); } @@ -44,7 +44,7 @@ module.exports = { if ( !take_at_face_value ) { // 2. If the signature is not valid, or non-existant, then we fetch the resource manually. var bodyParsed = await jsonld.compact(req.body, 'https://www.w3.org/ns/activitystreams'); - await db("queue").insert({task: "fetch_object", data: bodyParsed.id}); + await db("queue").insert({task: "fetch_object", data: JSON.stringify({object: bodyParsed.id, actor: `${req.hostname}@${req.hostname}`})}); res.status(204); return res.end(); }