Steps to allow follow requests
This commit is contained in:
parent
23f753e4b3
commit
55b210e4e7
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue