Compare commits

...

21 commits

Author SHA1 Message Date
6e50f2395e Perform fetch_object queue tasks. 2023-04-17 23:19:25 -05:00
c7cfef88ae Write out queue_worker for verify_inbox, plumb necessary data. 2023-04-14 00:06:52 -05:00
f71c7c36fc Ensure http agent actually executes.
Fixes #3
2023-04-12 18:11:18 -05:00
d90c8ded64 Add basic nodeinfo. 2023-04-12 00:06:39 -05:00
340244cdab In order to verify the signature, we need to have the signature. 2023-04-11 23:24:37 -05:00
35c029b124 Set inbox to queue important tasks rather than handling them on arrival. 2023-04-11 08:41:00 -05:00
c4bd237051 Accept Announce, handle content-not-included, correct DB inserts. 2023-04-07 23:41:31 -05:00
053f1e6fa1 Add http agent, update keys to ensure string, allow async promise executors. 2023-03-30 08:39:16 -05:00
552fc41c5e We should make sure the key pair is available. 2023-03-29 23:44:26 -05:00
031eba7936 Ensure we're only sending the public key with the actor. 2023-03-29 08:46:46 -05:00
7f98b0ac20 Remove eslint annoyance related to quotes. 2023-03-29 08:38:48 -05:00
f9c1223e0b Ensure req.hostname is shared as the preferredUsername 2023-03-29 08:17:16 -05:00
9c6f997c2a Fix logging. 2023-03-28 23:45:03 -05:00
401efee7cb Add basic instance actor, along with rudimentary http signature key generation support. 2023-03-28 23:33:04 -05:00
fb08c1e7ec Acknowledge (in code) "Announce" objects. 2023-03-28 08:43:38 -05:00
1d9bdf2508 Add logging. 2023-03-27 08:53:13 -05:00
ef89bec646 Return 422 Unprocessable Content for unimplemented functionality. 2023-03-25 21:55:42 -05:00
8ef2b9bd72 Ensure that we have the proper context for JSON-LD manipulation in DB. 2023-03-25 12:52:00 -05:00
58adbdc459 Allow routes to use app.route()
Concerns were raised that the previous method potentially made middleware more complicated. In order to allay this, hoist the full route into the route file. Still a single route per file, but facilitates simpler middleware.
2023-03-24 01:33:25 -05:00
dd5499a21d Initial groundwork for storing inbox in database. 2023-03-24 01:24:27 -05:00
1e8d9dfe06 Webfinger for instance actor. 2023-03-21 07:42:25 -05:00
23 changed files with 1819 additions and 25 deletions

View file

@ -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
View file

@ -130,3 +130,5 @@ dist
.yarn/install-state.gz
.pnp.*
dev.sqlite3
knexfile.js

View file

@ -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
View file

@ -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
View 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
View 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
View 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
View 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
View 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
View 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"]
});
}
}
}
};

View 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');
};

View 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');
};

View 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");
});
};

View 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");
});
};

View 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');
};

View 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

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View 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();
});
}
};

View 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
View 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
View 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
View 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();
});
}
};