Steps to allow follow requests

This commit is contained in:
Bryan Ashby 2023-01-08 01:22:02 -07:00
parent 23f753e4b3
commit 55b210e4e7
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
4 changed files with 246 additions and 29 deletions

View File

@ -15,6 +15,7 @@ exports.makeUserUrl = makeUserUrl;
exports.webFingerProfileUrl = webFingerProfileUrl;
exports.selfUrl = selfUrl;
exports.userFromAccount = userFromAccount;
exports.accountFromSelfUrl = accountFromSelfUrl;
exports.getUserProfileTemplatedBody = getUserProfileTemplatedBody;
// :TODO: more info in default
@ -44,6 +45,11 @@ function selfUrl(webServer, user) {
return makeUserUrl(webServer, user, '/ap/users/');
}
function accountFromSelfUrl(url) {
// https://some.l33t.enigma.board/_enig/ap/users/Masto -> Masto
return url.substring(url.lastIndexOf('/') + 1);
}
function userFromAccount(accountName, cb) {
if (accountName.startsWith('@')) {
accountName = accountName.slice(1);

View File

@ -316,6 +316,10 @@ exports.getModule = class WebServerModule extends ServerModule {
});
}
badRequest(resp) {
return this.respondWithError(resp, 400, 'Bad request.', 'Bad Request');
}
accessDenied(resp) {
return this.respondWithError(resp, 401, 'Access denied.', 'Access Denied');
}
@ -324,6 +328,24 @@ exports.getModule = class WebServerModule extends ServerModule {
return this.respondWithError(resp, 404, 'File not found.', 'File Not Found');
}
resourceNotFound(resp) {
return this.respondWithError(
resp,
404,
'Resource not found.',
'Resource Not Found'
);
}
internalServerError(resp) {
return this.respondWithError(
resp,
500,
'Internal server error.',
'Internal Server Error'
);
}
tryRouteIndex(req, resp, cb) {
const tryFiles = Config().contentServers.web.tryFiles || [
'index.html',

View File

@ -6,15 +6,17 @@ const {
userFromAccount,
getUserProfileTemplatedBody,
DefaultProfileTemplate,
accountFromSelfUrl,
} = require('../../../activitypub_util');
const UserProps = require('../../../user_property');
const { Errors } = require('../../../enig_error');
const Config = require('../../../config').get;
// deps
const _ = require('lodash');
const { trim } = require('lodash');
const enigma_assert = require('../../../enigma_assert');
const httpSignature = require('http-signature');
const https = require('https');
const { Errors } = require('../../../enig_error');
exports.moduleInfo = {
name: 'ActivityPub',
@ -40,6 +42,20 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
handler: this._selfUrlRequestHandler.bind(this),
});
this.webServer.addRoute({
method: 'POST',
//inbox: makeUserUrl(this.webServer, user, '/ap/users/') + '/inbox',
path: /^\/_enig\/ap\/users\/.+\/inbox$/,
handler: this._inboxPostHandler.bind(this),
});
// :TODO: NYI
// this.webServer.addRoute({
// method: 'GET',
// path: /^\/_enig\/authorize_interaction\?uri=(.+)$/,
// handler: this._authorizeInteractionHandler.bind(this),
// });
return cb(null);
}
@ -59,10 +75,10 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
userFromAccount(accountName, (err, user) => {
if (err) {
this.log.info(
{ reason: error.message, accountName: accountName },
{ reason: err.message, accountName: accountName },
`No user "${accountName}" for "self"`
);
return this._notFound(resp);
return this.webServer.resourceNotFound(resp);
}
// Additionally, serve activity JSON if the proper 'Accept' header was sent
@ -82,6 +98,196 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
});
}
_inboxPostHandler(req, resp) {
// the request must be signed, and the signature must be valid
const signature = this._parseSignature(req);
if (!signature) {
return this.webServer.resourceNotFound(resp);
}
const keyId = signature.keyId;
if (!this._validateKeyId(keyId)) {
return this.webServer.resourceNotFound(resp);
}
let body = '';
req.on('data', data => {
body += data;
});
req.on('end', () => {
try {
const activity = JSON.parse(body);
switch (activity.type) {
case 'Follow':
return this._inboxFollowRequestHandler(
keyId,
signature,
activity,
req,
resp
);
default:
this.log.debug(
{ type: activity.type },
`Unsupported Activity type "${activity.type}"`
);
return this.webServer.resourceNotFound(resp);
}
} catch (e) {
this.log.error(
{ error: e.message, url: req.url, method: req.method },
'Failed to parse Activity'
);
}
});
}
_parseSignature(req) {
try {
// :TODO: validate options passed to parseRequest()
return httpSignature.parseRequest(req);
} catch (e) {
this.log.warn(
{ error: e.message, url: req.url, method: req.method },
'Failed to parse HTTP signature'
);
}
}
_inboxFollowRequestHandler(keyId, signature, activity, req, resp) {
if (
activity['@context'] !== 'https://www.w3.org/ns/activitystreams' ||
!_.isString(activity.actor) ||
!_.isString(activity.object)
) {
return this.webServerbadRequest(resp);
}
const accountName = accountFromSelfUrl(activity.object);
if (!accountName) {
return this.webServer.badRequest(resp);
}
userFromAccount(accountName, (err, user) => {
if (err) {
return this.webServer.resourceNotFound(resp);
}
this._fetchActor(activity.actor, (err, actor) => {
if (err) {
// :TODO: log, and probably should be inspecting |err|
return this.webServer.internalServerError(resp);
}
const pubKey = actor.publicKey;
if (!_.isObject(pubKey)) {
// Log me
return this.webServer.accessDenied();
}
if (keyId !== pubKey.id) {
// :TODO: Log me
return this.webServer.accessDenied(resp);
}
if (!httpSignature.verifySignature(signature, pubKey.publicKeyPem)) {
this.log.warn(
{
actor: activity.actor,
keyId,
signature: req.headers['signature'] || '',
},
'Invalid signature supplied for Follow request'
);
return this.webServer.accessDenied(resp);
}
// :TODO: return OK and kick off a async job of persisting and sending and 'Accepted'
resp.writeHead(200, { 'Content-Type': 'text/html' });
return resp.end('');
});
});
}
// :TODO: replace me with a fetch-and-cache in Actor, wrapped in e.g. Actor.fetch(url, options, cb)
_fetchActor(actorUrl, cb) {
const headers = {
Accept: 'application/activity+json',
};
https
.get(actorUrl, { headers }, res => {
if (res.statusCode !== 200) {
return cb(Errors.Invalid(`Bad HTTP status code: ${req.statusCode}`));
}
const contentType = res.headers['content-type'];
if (
!_.isString(contentType) ||
!contentType.startsWith('application/activity+json')
) {
return cb(Errors.Invalid(`Invalid Content-Type: ${contentType}`));
}
res.setEncoding('utf8');
let body = '';
res.on('data', data => {
body += data;
});
res.on('end', () => {
try {
const actor = JSON.parse(body);
if (
!Array.isArray(actor['@context']) ||
actor['@context'][0] !==
'https://www.w3.org/ns/activitystreams'
) {
return cb(
Errors.Invalid('Invalid or missing Actor "@context"')
);
}
return cb(null, actor);
} catch (e) {
return cb(e);
}
});
})
.on('error', err => {
return cb(err);
});
}
_validateKeyId(keyId) {
if (!keyId) {
return false;
}
// we only accept main-key currently
return keyId.endsWith('#main-key');
}
_authorizeInteractionHandler(req, resp) {
console.log(req);
}
// _populateKeyIdInfo(keyId, info) {
// if (!_.isString(keyId)) {
// return false;
// }
// const m = /^https?:\/\/.+\/(.+)#(main-key)$/.exec(keyId);
// if (!m || !m.length === 3) {
// return false;
// }
// info.accountName = m[1];
// info.keyType = m[2];
// return true;
// }
_selfAsActorHandler(user, req, resp) {
this.log.trace(
{ username: user.username },
@ -90,6 +296,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
const userSelfUrl = selfUrl(this.webServer, user);
// :TODO: something like: Actor.makeActor(...)
const bodyJson = {
'@context': [
'https://www.w3.org/ns/activitystreams',
@ -161,7 +368,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
'text/plain',
(err, body, contentType) => {
if (err) {
return this._notFound(resp);
return this.webServer.resourceNotFound(resp);
}
const headers = {
@ -174,13 +381,4 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
}
);
}
_notFound(resp) {
this.webServer.respondWithError(
resp,
404,
'Resource not found',
'Resource Not Found'
);
}
};

View File

@ -89,8 +89,8 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
}
const userPosition = resource.indexOf('@');
if (-1 == userPosition || userPosition == resource.length - 1) {
this._notFound(resp);
if (-1 === userPosition || userPosition === resource.length - 1) {
this.webServer.resourceNotFound(resp);
return Errors.DoesNotExist('"@username" missing from path');
}
@ -102,7 +102,7 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
{ url: req.url, error: err.message, type: 'Profile' },
`No profile for "${accountName}" could be retrieved`
);
return this._notFound(resp);
return this.webServer.resourceNotFound(resp);
}
let templateFile = _.get(
@ -120,7 +120,7 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
'text/plain',
(err, body, contentType) => {
if (err) {
return this._notFound(resp);
return this.webServer.resourceNotFound(resp);
}
const headers = {
@ -150,7 +150,7 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
const accountName = this._getAccountName(resource);
if (!accountName || accountName.length < 1) {
this._notFound(resp);
this.webServer.resourceNotFound(resp);
return Errors.DoesNotExist(
`Failed to parse "account name" for resource: ${resource}`
);
@ -162,7 +162,7 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
{ url: req.url, error: err.message, type: 'WebFinger' },
`No account for "${accountName}" could be retrieved`
);
return this._notFound(resp);
return this.webServer.resourceNotFound(resp);
}
const domain = this.webServer.getDomain();
@ -232,13 +232,4 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
}
}
}
_notFound(resp) {
this.webServer.respondWithError(
resp,
404,
'Resource not found',
'Resource Not Found'
);
}
};