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.webFingerProfileUrl = webFingerProfileUrl;
|
||||||
exports.selfUrl = selfUrl;
|
exports.selfUrl = selfUrl;
|
||||||
exports.userFromAccount = userFromAccount;
|
exports.userFromAccount = userFromAccount;
|
||||||
|
exports.accountFromSelfUrl = accountFromSelfUrl;
|
||||||
exports.getUserProfileTemplatedBody = getUserProfileTemplatedBody;
|
exports.getUserProfileTemplatedBody = getUserProfileTemplatedBody;
|
||||||
|
|
||||||
// :TODO: more info in default
|
// :TODO: more info in default
|
||||||
|
@ -44,6 +45,11 @@ function selfUrl(webServer, user) {
|
||||||
return makeUserUrl(webServer, user, '/ap/users/');
|
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) {
|
function userFromAccount(accountName, cb) {
|
||||||
if (accountName.startsWith('@')) {
|
if (accountName.startsWith('@')) {
|
||||||
accountName = accountName.slice(1);
|
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) {
|
accessDenied(resp) {
|
||||||
return this.respondWithError(resp, 401, 'Access denied.', 'Access Denied');
|
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');
|
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) {
|
tryRouteIndex(req, resp, cb) {
|
||||||
const tryFiles = Config().contentServers.web.tryFiles || [
|
const tryFiles = Config().contentServers.web.tryFiles || [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|
|
@ -6,15 +6,17 @@ const {
|
||||||
userFromAccount,
|
userFromAccount,
|
||||||
getUserProfileTemplatedBody,
|
getUserProfileTemplatedBody,
|
||||||
DefaultProfileTemplate,
|
DefaultProfileTemplate,
|
||||||
|
accountFromSelfUrl,
|
||||||
} = require('../../../activitypub_util');
|
} = require('../../../activitypub_util');
|
||||||
const UserProps = require('../../../user_property');
|
const UserProps = require('../../../user_property');
|
||||||
const { Errors } = require('../../../enig_error');
|
|
||||||
const Config = require('../../../config').get;
|
const Config = require('../../../config').get;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const { trim } = require('lodash');
|
|
||||||
const enigma_assert = require('../../../enigma_assert');
|
const enigma_assert = require('../../../enigma_assert');
|
||||||
|
const httpSignature = require('http-signature');
|
||||||
|
const https = require('https');
|
||||||
|
const { Errors } = require('../../../enig_error');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name: 'ActivityPub',
|
name: 'ActivityPub',
|
||||||
|
@ -40,6 +42,20 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
handler: this._selfUrlRequestHandler.bind(this),
|
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);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,10 +75,10 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
userFromAccount(accountName, (err, user) => {
|
userFromAccount(accountName, (err, user) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.log.info(
|
this.log.info(
|
||||||
{ reason: error.message, accountName: accountName },
|
{ reason: err.message, accountName: accountName },
|
||||||
`No user "${accountName}" for "self"`
|
`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
|
// 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) {
|
_selfAsActorHandler(user, req, resp) {
|
||||||
this.log.trace(
|
this.log.trace(
|
||||||
{ username: user.username },
|
{ username: user.username },
|
||||||
|
@ -90,6 +296,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
|
|
||||||
const userSelfUrl = selfUrl(this.webServer, user);
|
const userSelfUrl = selfUrl(this.webServer, user);
|
||||||
|
|
||||||
|
// :TODO: something like: Actor.makeActor(...)
|
||||||
const bodyJson = {
|
const bodyJson = {
|
||||||
'@context': [
|
'@context': [
|
||||||
'https://www.w3.org/ns/activitystreams',
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
@ -161,7 +368,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
'text/plain',
|
'text/plain',
|
||||||
(err, body, contentType) => {
|
(err, body, contentType) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return this._notFound(resp);
|
return this.webServer.resourceNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = {
|
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('@');
|
const userPosition = resource.indexOf('@');
|
||||||
if (-1 == userPosition || userPosition == resource.length - 1) {
|
if (-1 === userPosition || userPosition === resource.length - 1) {
|
||||||
this._notFound(resp);
|
this.webServer.resourceNotFound(resp);
|
||||||
return Errors.DoesNotExist('"@username" missing from path');
|
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' },
|
{ url: req.url, error: err.message, type: 'Profile' },
|
||||||
`No profile for "${accountName}" could be retrieved`
|
`No profile for "${accountName}" could be retrieved`
|
||||||
);
|
);
|
||||||
return this._notFound(resp);
|
return this.webServer.resourceNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
let templateFile = _.get(
|
let templateFile = _.get(
|
||||||
|
@ -120,7 +120,7 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
|
||||||
'text/plain',
|
'text/plain',
|
||||||
(err, body, contentType) => {
|
(err, body, contentType) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return this._notFound(resp);
|
return this.webServer.resourceNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
|
@ -150,7 +150,7 @@ exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
|
||||||
|
|
||||||
const accountName = this._getAccountName(resource);
|
const accountName = this._getAccountName(resource);
|
||||||
if (!accountName || accountName.length < 1) {
|
if (!accountName || accountName.length < 1) {
|
||||||
this._notFound(resp);
|
this.webServer.resourceNotFound(resp);
|
||||||
return Errors.DoesNotExist(
|
return Errors.DoesNotExist(
|
||||||
`Failed to parse "account name" for resource: ${resource}`
|
`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' },
|
{ url: req.url, error: err.message, type: 'WebFinger' },
|
||||||
`No account for "${accountName}" could be retrieved`
|
`No account for "${accountName}" could be retrieved`
|
||||||
);
|
);
|
||||||
return this._notFound(resp);
|
return this.webServer.resourceNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const domain = this.webServer.getDomain();
|
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