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,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"plugins": [
|
||||
"jsdoc"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:jsdoc/recommended"
|
||||
],
|
||||
"overrides": [
|
||||
],
|
||||
"parserOptions": {
|
||||
|
@ -21,8 +27,7 @@
|
|||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
"off"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
|
@ -49,6 +54,19 @@
|
|||
"function-call-argument-newline": [
|
||||
"error",
|
||||
"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
|
||||
.pnp.*
|
||||
|
||||
dev.sqlite3
|
||||
knexfile.js
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
---
|
||||
allowedLicenses:
|
||||
- (BSD-2-Clause OR MIT OR Apache-2.0)
|
||||
- (MIT AND CC-BY-3.0)
|
||||
- (MIT OR CC0-1.0)
|
||||
- (MIT OR WTFPL)
|
||||
- Apache-2.0
|
||||
- BSD-2-Clause
|
||||
- BSD-3-Clause
|
||||
|
|
32
app.js
32
app.js
|
@ -2,16 +2,39 @@
|
|||
|
||||
const express = require('express');
|
||||
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,
|
||||
});
|
||||
app.use(log4js.connectLogger(log.accessLog, {
|
||||
level: 'auto',
|
||||
format: ':remote-addr - - ":method :url HTTP/:http-version" :status :content-length ":referrer" ":user-agent"'
|
||||
}));
|
||||
const pathMatches = [];
|
||||
app.use((req, res, next) => {
|
||||
app.use((req, _res, next) => {
|
||||
const requestUrl = new URL(req.url, 'https://example.com/');
|
||||
let candidateUrl = '';
|
||||
let secondCandidateUrl = '';
|
||||
|
@ -33,7 +56,6 @@ const { match: createPathMatch } = require('path-to-regexp');
|
|||
}
|
||||
if ( candidateUrl !== '' ) {
|
||||
req.url = candidateUrl;
|
||||
console.log(candidateUrl);
|
||||
return next();
|
||||
}
|
||||
if ( secondCandidateUrl !== '' ) {
|
||||
|
@ -50,6 +72,12 @@ const { match: createPathMatch } = require('path-to-regexp');
|
|||
if ( 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);
|
||||
})();
|
||||
|
|
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>",
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.2",
|
||||
"express": "^4.18.2",
|
||||
"glob": "^9.3.0",
|
||||
"jsonld": "^8.1.1",
|
||||
"knex": "^2.4.2",
|
||||
"log4js": "^6.9.1",
|
||||
"path-to-regexp": "^6.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-plugin-jsdoc": "^40.1.0",
|
||||
"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…
Reference in a new issue