Compare commits
21 commits
main
...
Accept-Rel
Author | SHA1 | Date | |
---|---|---|---|
6e50f2395e | |||
c7cfef88ae | |||
f71c7c36fc | |||
d90c8ded64 | |||
340244cdab | |||
35c029b124 | |||
c4bd237051 | |||
053f1e6fa1 | |||
552fc41c5e | |||
031eba7936 | |||
7f98b0ac20 | |||
f9c1223e0b | |||
9c6f997c2a | |||
401efee7cb | |||
fb08c1e7ec | |||
1d9bdf2508 | |||
ef89bec646 | |||
8ef2b9bd72 | |||
58adbdc459 | |||
dd5499a21d | |||
1e8d9dfe06 |
23 changed files with 1819 additions and 25 deletions
|
@ -5,7 +5,13 @@
|
||||||
"es2021": true,
|
"es2021": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"extends": "eslint:recommended",
|
"plugins": [
|
||||||
|
"jsdoc"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:jsdoc/recommended"
|
||||||
|
],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
],
|
],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
@ -21,8 +27,7 @@
|
||||||
"unix"
|
"unix"
|
||||||
],
|
],
|
||||||
"quotes": [
|
"quotes": [
|
||||||
"error",
|
"off"
|
||||||
"single"
|
|
||||||
],
|
],
|
||||||
"semi": [
|
"semi": [
|
||||||
"error",
|
"error",
|
||||||
|
@ -49,6 +54,19 @@
|
||||||
"function-call-argument-newline": [
|
"function-call-argument-newline": [
|
||||||
"error",
|
"error",
|
||||||
"consistent"
|
"consistent"
|
||||||
|
],
|
||||||
|
"object-property-newline": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowAllPropertiesOnSameLine": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strict": [
|
||||||
|
"error",
|
||||||
|
"safe"
|
||||||
|
],
|
||||||
|
"jsdoc/require-param-description": [
|
||||||
|
"off"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -130,3 +130,5 @@ dist
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
dev.sqlite3
|
||||||
|
knexfile.js
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
---
|
---
|
||||||
allowedLicenses:
|
allowedLicenses:
|
||||||
|
- (BSD-2-Clause OR MIT OR Apache-2.0)
|
||||||
- (MIT AND CC-BY-3.0)
|
- (MIT AND CC-BY-3.0)
|
||||||
- (MIT OR CC0-1.0)
|
- (MIT OR CC0-1.0)
|
||||||
|
- (MIT OR WTFPL)
|
||||||
- Apache-2.0
|
- Apache-2.0
|
||||||
- BSD-2-Clause
|
- BSD-2-Clause
|
||||||
- BSD-3-Clause
|
- BSD-3-Clause
|
||||||
|
|
32
app.js
32
app.js
|
@ -2,16 +2,39 @@
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const glob = require('glob');
|
const glob = require('glob');
|
||||||
|
const log4js = require('log4js');
|
||||||
const { match: createPathMatch } = require('path-to-regexp');
|
const { match: createPathMatch } = require('path-to-regexp');
|
||||||
|
const log = require('./lib/log');
|
||||||
|
var bodyParser = require('body-parser');
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const app = express();
|
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', {
|
const routes = await glob('**/*.js', {
|
||||||
cwd: './routes',
|
cwd: './routes',
|
||||||
dot: true,
|
dot: true,
|
||||||
});
|
});
|
||||||
|
app.use(log4js.connectLogger(log.accessLog, {
|
||||||
|
level: 'auto',
|
||||||
|
format: ':remote-addr - - ":method :url HTTP/:http-version" :status :content-length ":referrer" ":user-agent"'
|
||||||
|
}));
|
||||||
const pathMatches = [];
|
const pathMatches = [];
|
||||||
app.use((req, res, next) => {
|
app.use((req, _res, next) => {
|
||||||
const requestUrl = new URL(req.url, 'https://example.com/');
|
const requestUrl = new URL(req.url, 'https://example.com/');
|
||||||
let candidateUrl = '';
|
let candidateUrl = '';
|
||||||
let secondCandidateUrl = '';
|
let secondCandidateUrl = '';
|
||||||
|
@ -33,7 +56,6 @@ const { match: createPathMatch } = require('path-to-regexp');
|
||||||
}
|
}
|
||||||
if ( candidateUrl !== '' ) {
|
if ( candidateUrl !== '' ) {
|
||||||
req.url = candidateUrl;
|
req.url = candidateUrl;
|
||||||
console.log(candidateUrl);
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
if ( secondCandidateUrl !== '' ) {
|
if ( secondCandidateUrl !== '' ) {
|
||||||
|
@ -50,6 +72,12 @@ const { match: createPathMatch } = require('path-to-regexp');
|
||||||
if ( routeObj.get ) {
|
if ( routeObj.get ) {
|
||||||
app.get(`/${route}`, routeObj.get);
|
app.get(`/${route}`, routeObj.get);
|
||||||
}
|
}
|
||||||
|
if ( routeObj.post ) {
|
||||||
|
app.post(`/${route}`, routeObj.post);
|
||||||
|
}
|
||||||
|
if ( routeObj.route ) {
|
||||||
|
routeObj.route(app.route(`/${route}`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
app.listen(process.env.PORT || 3000);
|
app.listen(process.env.PORT || 3000);
|
||||||
})();
|
})();
|
||||||
|
|
49
knexfile-sample.js
Normal file
49
knexfile-sample.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Update with your config settings.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type { Object.<string, import("knex").Knex.Config> }
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
|
||||||
|
development: {
|
||||||
|
client: 'better-sqlite3',
|
||||||
|
connection: {
|
||||||
|
filename: './dev.sqlite3'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
staging: {
|
||||||
|
client: 'postgresql',
|
||||||
|
connection: {
|
||||||
|
database: 'my_db',
|
||||||
|
user: 'username',
|
||||||
|
password: 'password'
|
||||||
|
},
|
||||||
|
pool: {
|
||||||
|
min: 2,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
tableName: 'knex_migrations'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
production: {
|
||||||
|
client: 'postgresql',
|
||||||
|
connection: {
|
||||||
|
database: 'my_db',
|
||||||
|
user: 'username',
|
||||||
|
password: 'password'
|
||||||
|
},
|
||||||
|
pool: {
|
||||||
|
min: 2,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
tableName: 'knex_migrations'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
5
lib/db.js
Normal file
5
lib/db.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Knex = require('knex');
|
||||||
|
const db = new Knex(require('../knexfile')[process.env.NODE_ENV??'development']);
|
||||||
|
module.exports = db;
|
124
lib/http_agent.js
Normal file
124
lib/http_agent.js
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
const zlib = require('zlib');
|
||||||
|
|
||||||
|
const { getKeyPair } = require('./keys');
|
||||||
|
|
||||||
|
const handleDataDecode = (requestUrl, options, res, resolve, reject, data) => {
|
||||||
|
if ( options && (!options["headers"] || !options["headers"]["accept-encoding"]) && res.headers["content-encoding"] && res.headers["content-encoding"].toLowerCase() === "br") {
|
||||||
|
delete res.headers["content-encoding"];
|
||||||
|
zlib.brotliDecompress(data, (error, result) => {
|
||||||
|
if ( error ) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
handleDataDecode(requestUrl, options, res, resolve, reject, result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if ( options && (!options["headers"] || !options["headers"]["accept-encoding"]) && res.headers["content-encoding"] && res.headers["content-encoding"].toLowerCase() === "gzip") {
|
||||||
|
delete res.headers["content-encoding"];
|
||||||
|
zlib.gunzip(data, (error, result) => {
|
||||||
|
if ( error ) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
handleDataDecode(requestUrl, options, res, resolve, reject, result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let isBuffer = true;
|
||||||
|
if ( (!options["headers"] || !options["headers"]["accept-encoding"]) && res.headers["content-type"] ) {
|
||||||
|
var charset = /.*charset=(\S+)/.exec(res.headers["content-type"])[1];
|
||||||
|
if ( charset.toLowerCase() === "iso-8859-1" ) {
|
||||||
|
charset = "latin1";
|
||||||
|
}
|
||||||
|
if ( charset ) {
|
||||||
|
isBuffer = false;
|
||||||
|
data = data.toString(charset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve({headers: res.headers, data, isBuffer});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCallback = (requestUrl, options, res, resolve, reject) => {
|
||||||
|
let data;
|
||||||
|
data = Buffer.alloc(0);
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data = Buffer.concat([data, chunk]);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
handleDataDecode(requestUrl, options, res, resolve, reject, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('error', (error) => reject(error));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
request: (requestUrl, options) => {
|
||||||
|
options = options??{};
|
||||||
|
// TODO: Support following redirects.
|
||||||
|
options["follow-redirects"] = options["follow-redirects"]??true;
|
||||||
|
var headers = {"accept-encoding": "br, gzip", ...options["headers"]};
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
(async () => {
|
||||||
|
const parsedUrl = new URL(requestUrl);
|
||||||
|
var method = "GET";
|
||||||
|
if ( options["method"] ) {
|
||||||
|
method = options["method"];
|
||||||
|
}
|
||||||
|
var body = "";
|
||||||
|
if ( method === "POST" ) {
|
||||||
|
if ( typeof options["body"] === "string" ) {
|
||||||
|
body = options["body"];
|
||||||
|
} else if ( Buffer.isBuffer(options["body"]) ) {
|
||||||
|
body = options["body"];
|
||||||
|
} else if ( options["headers"]["content-type"].toLowerCase() === "application/ld+json" ||
|
||||||
|
options["headers"]["content-type"].toLowerCase() === "application/activity+json" ||
|
||||||
|
options["headers"]["content-type"].toLowerCase() === "application/json") {
|
||||||
|
body = JSON.stringify(options["body"]);
|
||||||
|
} else {
|
||||||
|
return reject(new Error("Unrecognized body content-type, passed as object."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( body !== "" ) {
|
||||||
|
options["headers"]["content-length"] = Buffer.byteLength(body);
|
||||||
|
}
|
||||||
|
if ( options["actor"] ) {
|
||||||
|
var keyPair = getKeyPair("actor");
|
||||||
|
var rsaSha256Sign = crypto.createSign("RSA-SHA256");
|
||||||
|
headers["date"] = headers["date"]??(new Date()).toUTCString();
|
||||||
|
var toSign = `(request-target): ${method.toLowerCase()} ${parsedUrl.pathname}?${parsedUrl.search}\nhost: ${parsedUrl.host}\ndate: ${headers["date"]}`;
|
||||||
|
if ( method === "POST" ) {
|
||||||
|
var digest = crypto.createHash("sha256").update(body).end().digest("hex");
|
||||||
|
headers["digest"] = digest;
|
||||||
|
toSign = `${toSign}\ndigest: ${headers["digest"]}`;
|
||||||
|
}
|
||||||
|
headers["signature"] = `keyId=${options["actor"]}#main-key,headers="(request-target) host date${method === "post" ? " digest":""}",signature=${rsaSha256Sign.update(toSign).end().sign((await keyPair).privateKey, "base64")}`;
|
||||||
|
}
|
||||||
|
if ( parsedUrl.protocol.toLowerCase() === "https:" ) {
|
||||||
|
const httpsRequest = https.request(parsedUrl, {headers}, (res) => {
|
||||||
|
handleCallback(requestUrl, options, res, resolve, reject);
|
||||||
|
}).on('error', (error) => reject(error));
|
||||||
|
if ( body !== "" ) {
|
||||||
|
httpsRequest.write(body);
|
||||||
|
}
|
||||||
|
httpsRequest.end();
|
||||||
|
} else if ( parsedUrl.protocol.toLowerCase() === "http:" ) {
|
||||||
|
const httpRequest = http.request(parsedUrl, {headers}, (res) => {
|
||||||
|
handleCallback(requestUrl, options, res, resolve, reject);
|
||||||
|
}).on('error', (error) => reject(error));
|
||||||
|
if ( body !== "" ) {
|
||||||
|
httpRequest.write(body);
|
||||||
|
}
|
||||||
|
httpRequest.end();
|
||||||
|
} else {
|
||||||
|
reject("Unrecognized protocol.");
|
||||||
|
}
|
||||||
|
})().then();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
49
lib/keys.js
Normal file
49
lib/keys.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const db = require('./db');
|
||||||
|
|
||||||
|
var generateKeyPair = (type, options) => {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
crypto.generateKeyPair(type, options, (err, pubkey, privkey) => {
|
||||||
|
if ( err != null ) {
|
||||||
|
rej(err);
|
||||||
|
} else {
|
||||||
|
res({publicKey: pubkey, privateKey: privkey});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var getKeyPair = async (actor) => {
|
||||||
|
var result = await db('keys').where({actor}).andWhere('expiry', '>', (new Date()).getTime()/1000);
|
||||||
|
if ( result.length != 0 ) {
|
||||||
|
return {
|
||||||
|
publicKey: result[0].public.toString(),
|
||||||
|
privateKey: result[0].private.toString()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
var {publicKey, privateKey} = await generateKeyPair('rsa', {
|
||||||
|
modulusLength: 4096,
|
||||||
|
publicKeyEncoding: {
|
||||||
|
format: 'pem'
|
||||||
|
},
|
||||||
|
privateKeyEncoding: {
|
||||||
|
format: 'pem'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await db('keys').insert({
|
||||||
|
expiry: (new Date().setDate(new Date().getDate()+7).getTime()/1000),
|
||||||
|
public: publicKey.toString(),
|
||||||
|
private: privateKey.toString(),
|
||||||
|
actor: actor
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
publicKey: publicKey.toString(), privateKey: publicKey.toString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getKeyPair
|
||||||
|
};
|
19
lib/log.js
Normal file
19
lib/log.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var log4js = require('log4js');
|
||||||
|
|
||||||
|
log4js.configure({
|
||||||
|
appenders: {
|
||||||
|
console: { type: 'console' },
|
||||||
|
accessLog: { type: 'file', filename: 'access.log' }
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
access: { appenders: ['accessLog', 'console'], level: 'info' },
|
||||||
|
default: { appenders: ['console'], level: 'info'}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
default: log4js.getLogger('default'),
|
||||||
|
accessLog: log4js.getLogger('access')
|
||||||
|
};
|
106
lib/queue_worker.js
Normal file
106
lib/queue_worker.js
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
'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);
|
||||||
|
var data = JSON.parse(result[0]["data"]);
|
||||||
|
if ( result[0] ) {
|
||||||
|
await db('queue').del().where('id', '=', result[0]["id"]);
|
||||||
|
if ( result[0]["type"] === "verify_inbox" ) {
|
||||||
|
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: `[${to.join(',')}]`,
|
||||||
|
cc: `[${cc.join(',')}]`,
|
||||||
|
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"]})});
|
||||||
|
}
|
||||||
|
} else if ( result[0] === "fetch_object" ) {
|
||||||
|
await db('queue').del().where('id', '=', result[0]["id"]);
|
||||||
|
var exists = await db("object_box").where("origin", "=", data["object"]).andWhere("expires", ">=", (new Date()).getTime());
|
||||||
|
if ( exists.length !== 0 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let parsedBody = await jsonld.compact(JSON.parse(http_agent.request(secondKeyUrl, {actor: data["actor"]})["data"]), ["https://www.w3.org/ns/activitystreams", { security: "https://w3id.org/security#"}]);
|
||||||
|
let to = parsedBody.to;
|
||||||
|
if ( typeof to === "string" ) {
|
||||||
|
to = [to];
|
||||||
|
}
|
||||||
|
let cc = parsedBody.cc;
|
||||||
|
if ( typeof cc === "string" ) {
|
||||||
|
cc = [cc];
|
||||||
|
}
|
||||||
|
await db("object_box").insert({
|
||||||
|
origin: parsedBody.id,
|
||||||
|
to: `[${to.join(',')}]`,
|
||||||
|
cc: `[${cc.join(',')}]`,
|
||||||
|
object: parsedBody,
|
||||||
|
signedby: "fetch",
|
||||||
|
// TODO: We should respect the caching instructions provided by origin.
|
||||||
|
expires: (new Date()).getTime() + 604800000 // 7 days.
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// TODO: Surface this one as an error.
|
||||||
|
await db('queue').del().where('id', '=', result[0]["id"]);
|
||||||
|
await db('queue').insert({
|
||||||
|
task: result[0]["task"],
|
||||||
|
data: result[0]["data"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
25
migrations/20230324060224_inbox.js
Normal file
25
migrations/20230324060224_inbox.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/* eslint-disable jsdoc/valid-types */
|
||||||
|
// eslint doesn't seem to understand, but vscode does.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema.createTable('inbox', (table) => {
|
||||||
|
table.increments('id');
|
||||||
|
table.string('origin');
|
||||||
|
table.string('to');
|
||||||
|
table.string('cc');
|
||||||
|
table.string('object');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema.dropTable('inbox');
|
||||||
|
};
|
25
migrations/20230329035124_keys.js
Normal file
25
migrations/20230329035124_keys.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/* eslint-disable jsdoc/valid-types */
|
||||||
|
// eslint doesn't seem to understand, but vscode does.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema.createTable('keys', (table) => {
|
||||||
|
table.increments('id');
|
||||||
|
table.integer('expiry');
|
||||||
|
table.string('private');
|
||||||
|
table.string('public');
|
||||||
|
table.text('actor');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema.dropTable('keys');
|
||||||
|
};
|
23
migrations/20230411044004_inbox_signedby.js
Normal file
23
migrations/20230411044004_inbox_signedby.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/* eslint-disable jsdoc/valid-types */
|
||||||
|
// eslint doesn't seem to understand, but vscode does.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema.alterTable("inbox", (table) => {
|
||||||
|
table.string("signedby");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema.alterTable("inbox", (table) => {
|
||||||
|
table.dropColumn("signedby");
|
||||||
|
});
|
||||||
|
};
|
23
migrations/20230411044233_inbox_expiry.js
Normal file
23
migrations/20230411044233_inbox_expiry.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/* eslint-disable jsdoc/valid-types */
|
||||||
|
// eslint doesn't seem to understand, but vscode does.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema.alterTable("inbox", (table) => {
|
||||||
|
table.string("expires");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema.alterTable("inbox", (table) => {
|
||||||
|
table.dropColumn("expires");
|
||||||
|
});
|
||||||
|
};
|
23
migrations/20230411052346_queue.js
Normal file
23
migrations/20230411052346_queue.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/* eslint-disable jsdoc/valid-types */
|
||||||
|
// eslint doesn't seem to understand, but vscode does.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema.createTable('queue', (table) => {
|
||||||
|
table.increments('id');
|
||||||
|
table.string('task');
|
||||||
|
table.string('data');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema.dropTable('queue');
|
||||||
|
};
|
19
migrations/20230414021844_rename_inbox_to_object_box.js
Normal file
19
migrations/20230414021844_rename_inbox_to_object_box.js
Normal file
|
@ -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<void> }
|
||||||
|
*/
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema.renameTable("inbox", "object_box");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema.renameTable("object_box", "inbox");
|
||||||
|
};
|
1105
package-lock.json
generated
1105
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -13,12 +13,20 @@
|
||||||
"author": "Andrew Pietila <a.pietila@protonmail.com>",
|
"author": "Andrew Pietila <a.pietila@protonmail.com>",
|
||||||
"license": "WTFPL",
|
"license": "WTFPL",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"glob": "^9.3.0",
|
"glob": "^9.3.0",
|
||||||
|
"jsonld": "^8.1.1",
|
||||||
|
"knex": "^2.4.2",
|
||||||
|
"log4js": "^6.9.1",
|
||||||
"path-to-regexp": "^6.2.1"
|
"path-to-regexp": "^6.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.36.0",
|
||||||
|
"eslint-plugin-jsdoc": "^40.1.0",
|
||||||
"license-checker": "^25.0.1"
|
"license-checker": "^25.0.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"better-sqlite3": "^8.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
20
routes/.well-known/nodeinfo.js
Normal file
20
routes/.well-known/nodeinfo.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* @param {express.IRoute} routeObj
|
||||||
|
*/
|
||||||
|
route: (routeObj) => {
|
||||||
|
routeObj.get(async (req, res, _next) => {
|
||||||
|
res.json({
|
||||||
|
"links": [{
|
||||||
|
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||||
|
"href": `https://${req.headers.host}/nodeinfo/2.0`
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
32
routes/.well-known/webfinger.js
Normal file
32
routes/.well-known/webfinger.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* @param {express.IRoute} routeObj
|
||||||
|
*/
|
||||||
|
route: (routeObj) => {
|
||||||
|
routeObj.get(async (req, res, _next) => {
|
||||||
|
res.setHeader("content-type", "application/jrd+json");
|
||||||
|
res.json(
|
||||||
|
{
|
||||||
|
"subject": `acct:${req.headers.host}@${req.headers.host}`,
|
||||||
|
"aliases": [`https://${req.headers.host}/actor`],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "http://webfinger.net/rel/profile-page",
|
||||||
|
"type": "text/html",
|
||||||
|
"href": `https://${req.headers.host}/about/more?instance_actor=true`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": `https://${req.headers.host}/actor`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
41
routes/actor.js
Normal file
41
routes/actor.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const { getKeyPair } = require('../lib/keys');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* @param {express.IRoute} routeObj
|
||||||
|
*/
|
||||||
|
route: (routeObj) => {
|
||||||
|
routeObj.get(async (req, res, _next) => {
|
||||||
|
res.setHeader('content-type', 'application/activity+json');
|
||||||
|
res.json({
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
security: 'https://w3id.org/security#'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'id': `https://${req.hostname}/actor`,
|
||||||
|
'type': 'Application',
|
||||||
|
'inbox': `https://${req.hostname}/actor/inbox`,
|
||||||
|
'security:publicKey': {
|
||||||
|
'id': `https://${req.hostname}/actor#main-key`,
|
||||||
|
'security:owner': {
|
||||||
|
id: `https://${req.hostname}/actor`
|
||||||
|
},
|
||||||
|
'security:publicKeyPem': (await getKeyPair(`https://${req.hostname}/actor`))["publicKey"]
|
||||||
|
},
|
||||||
|
'endpoints': {
|
||||||
|
sharedInbox: `https://${req.hostname}/inbox`
|
||||||
|
},
|
||||||
|
'as:manuallyApprovesFollowers': true,
|
||||||
|
'outbox': `https://${req.hostname}/actor/outbox`,
|
||||||
|
'preferredUsername': `${req.hostname}`,
|
||||||
|
'url': `https://${req.hostname}/about/more?instance_actor=true`
|
||||||
|
});
|
||||||
|
return res.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
54
routes/inbox.js
Normal file
54
routes/inbox.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var db = require('../lib/db');
|
||||||
|
var express = require('express');
|
||||||
|
var jsonld = require('jsonld');
|
||||||
|
var crypto = require('crypto');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* @param {express.IRoute} routeObj
|
||||||
|
*/
|
||||||
|
route: (routeObj) => {
|
||||||
|
routeObj.post(async (req, res, _next) => {
|
||||||
|
// So, inbox only cares about the request. So what do we care about on the request?
|
||||||
|
var take_at_face_value = false;
|
||||||
|
// 1. If it is signed...
|
||||||
|
if ( req.header("signature") ) {
|
||||||
|
// ... and said signature is valid...
|
||||||
|
var signature_split = req.header("signature").split(/,/);
|
||||||
|
var signature_elements = {};
|
||||||
|
signature_split.forEach((obj) => {
|
||||||
|
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)")) {
|
||||||
|
// ... and checks all the correct headers...
|
||||||
|
var digest = crypto.createHash("sha256").update(req.body).end().digest("hex");
|
||||||
|
if ( digest === req.header("digest") ) {
|
||||||
|
// ... and the body is un-tampered...
|
||||||
|
var signed_block = headers_to_check.map((header) => {
|
||||||
|
if ( header === "(request-target)" ) {
|
||||||
|
return `(request-target): ${req.method.toLowerCase()} ${req.originalUrl}`;
|
||||||
|
} else {
|
||||||
|
return `${header}: ${req.header(header)}`;
|
||||||
|
}
|
||||||
|
}).join('\n');
|
||||||
|
// ... then lets dump this in the queue for now.
|
||||||
|
// TODO: This'll probably be a bug someday: https://stackoverflow.com/a/69299910
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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: JSON.stringify({object: bodyParsed.id, actor: `${req.hostname}@${req.hostname}`})});
|
||||||
|
res.status(204);
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
34
routes/nodeinfo/2.0.js
Normal file
34
routes/nodeinfo/2.0.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* @param {express.IRoute} routeObj
|
||||||
|
*/
|
||||||
|
route: (routeObj) => {
|
||||||
|
routeObj.get(async (req, res, _next) => {
|
||||||
|
res.header("content-type", "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.0#\"");
|
||||||
|
res.json({
|
||||||
|
version: "2.0",
|
||||||
|
software: {
|
||||||
|
name: "haxsocial",
|
||||||
|
// TODO: Include a git depth and revision hash.
|
||||||
|
version: "0.0.0"
|
||||||
|
},
|
||||||
|
protocols: [
|
||||||
|
"activitypub"
|
||||||
|
],
|
||||||
|
services: {
|
||||||
|
outbound: [],
|
||||||
|
inbound: []
|
||||||
|
},
|
||||||
|
usage: {
|
||||||
|
},
|
||||||
|
openRegistrations: false,
|
||||||
|
metadata: {}
|
||||||
|
});
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
Loading…
Add table
Reference in a new issue