Rework most of the ActivityPub routing handling
This commit is contained in:
parent
39a49f00be
commit
c5f0e0e6ef
|
@ -2,9 +2,9 @@ const {
|
||||||
ActivityStreamMediaType,
|
ActivityStreamMediaType,
|
||||||
WellKnownActivityTypes,
|
WellKnownActivityTypes,
|
||||||
WellKnownActivity,
|
WellKnownActivity,
|
||||||
WellKnownRecipientFields,
|
|
||||||
HttpSignatureSignHeaders,
|
HttpSignatureSignHeaders,
|
||||||
} = require('./const');
|
} = require('./const');
|
||||||
|
const { recipientIdsFromObject } = require('./util');
|
||||||
const Endpoints = require('./endpoint');
|
const Endpoints = require('./endpoint');
|
||||||
const ActivityPubObject = require('./object');
|
const ActivityPubObject = require('./object');
|
||||||
const { Errors } = require('../enig_error');
|
const { Errors } = require('../enig_error');
|
||||||
|
@ -110,21 +110,8 @@ module.exports = class Activity extends ActivityPubObject {
|
||||||
return postJson(inboxEndpoint, activityJson, reqOpts, cb);
|
return postJson(inboxEndpoint, activityJson, reqOpts, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
// :TODO: we need dp/support a bit more here...
|
|
||||||
recipientIds() {
|
recipientIds() {
|
||||||
const ids = [];
|
return recipientIdsFromObject(this);
|
||||||
|
|
||||||
WellKnownRecipientFields.forEach(field => {
|
|
||||||
let v = this[field];
|
|
||||||
if (v) {
|
|
||||||
if (!Array.isArray(v)) {
|
|
||||||
v = [v];
|
|
||||||
}
|
|
||||||
ids.push(...v);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(new Set(ids));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static activityObjectId(webServer) {
|
static activityObjectId(webServer) {
|
||||||
|
|
|
@ -73,6 +73,10 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static outbox(collectionId, page, cb) {
|
||||||
|
return Collection.publicOrderedById('outbox', collectionId, page, null, cb);
|
||||||
|
}
|
||||||
|
|
||||||
static addFollower(owningUser, followingActor, webServer, ignoreDupes, cb) {
|
static addFollower(owningUser, followingActor, webServer, ignoreDupes, cb) {
|
||||||
const collectionId = Endpoints.followers(webServer, owningUser);
|
const collectionId = Endpoints.followers(webServer, owningUser);
|
||||||
return Collection.addToCollection(
|
return Collection.addToCollection(
|
||||||
|
@ -116,10 +120,6 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static outbox(collectionId, page, cb) {
|
|
||||||
return Collection.publicOrderedById('outbox', collectionId, page, null, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
static addOutboxItem(owningUser, outboxItem, isPrivate, webServer, ignoreDupes, cb) {
|
static addOutboxItem(owningUser, outboxItem, isPrivate, webServer, ignoreDupes, cb) {
|
||||||
const collectionId = Endpoints.outbox(webServer, owningUser);
|
const collectionId = Endpoints.outbox(webServer, owningUser);
|
||||||
return Collection.addToCollection(
|
return Collection.addToCollection(
|
||||||
|
|
|
@ -3,7 +3,12 @@ const ActivityPubObject = require('./object');
|
||||||
const { Errors } = require('../enig_error');
|
const { Errors } = require('../enig_error');
|
||||||
const { getISOTimestampString } = require('../database');
|
const { getISOTimestampString } = require('../database');
|
||||||
const User = require('../user');
|
const User = require('../user');
|
||||||
const { parseTimestampOrNow, messageToHtml, htmlToMessageBody } = require('./util');
|
const {
|
||||||
|
parseTimestampOrNow,
|
||||||
|
messageToHtml,
|
||||||
|
htmlToMessageBody,
|
||||||
|
recipientIdsFromObject,
|
||||||
|
} = require('./util');
|
||||||
const { isAnsi } = require('../string_util');
|
const { isAnsi } = require('../string_util');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
|
@ -26,11 +31,19 @@ module.exports = class Note extends ActivityPubObject {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.type !== 'Note') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// :TODO: validate required properties
|
// :TODO: validate required properties
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recipientIds() {
|
||||||
|
return recipientIdsFromObject(this);
|
||||||
|
}
|
||||||
|
|
||||||
static fromPublicNoteId(noteId, cb) {
|
static fromPublicNoteId(noteId, cb) {
|
||||||
Collection.objectByEmbeddedId(noteId, (err, obj, objInfo) => {
|
Collection.objectByEmbeddedId(noteId, (err, obj, objInfo) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ const { Errors, ErrorReasons } = require('../enig_error');
|
||||||
const UserProps = require('../user_property');
|
const UserProps = require('../user_property');
|
||||||
const ActivityPubSettings = require('./settings');
|
const ActivityPubSettings = require('./settings');
|
||||||
const { stripAnsiControlCodes } = require('../string_util');
|
const { stripAnsiControlCodes } = require('../string_util');
|
||||||
|
const { WellKnownRecipientFields } = require('./const');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
@ -27,6 +28,7 @@ exports.messageToHtml = messageToHtml;
|
||||||
exports.htmlToMessageBody = htmlToMessageBody;
|
exports.htmlToMessageBody = htmlToMessageBody;
|
||||||
exports.userNameFromSubject = userNameFromSubject;
|
exports.userNameFromSubject = userNameFromSubject;
|
||||||
exports.extractMessageMetadata = extractMessageMetadata;
|
exports.extractMessageMetadata = extractMessageMetadata;
|
||||||
|
exports.recipientIdsFromObject = recipientIdsFromObject;
|
||||||
|
|
||||||
// :TODO: more info in default
|
// :TODO: more info in default
|
||||||
// this profile template is the *default* for both WebFinger
|
// this profile template is the *default* for both WebFinger
|
||||||
|
@ -241,3 +243,19 @@ function extractMessageMetadata(body) {
|
||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recipientIdsFromObject(obj) {
|
||||||
|
const ids = [];
|
||||||
|
|
||||||
|
WellKnownRecipientFields.forEach(field => {
|
||||||
|
let v = obj[field];
|
||||||
|
if (v) {
|
||||||
|
if (!Array.isArray(v)) {
|
||||||
|
v = [v];
|
||||||
|
}
|
||||||
|
ids.push(...v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(new Set(ids));
|
||||||
|
}
|
||||||
|
|
|
@ -78,7 +78,9 @@ function _makeRequest(url, options, cb) {
|
||||||
if (res.statusCode < 200 || res.statusCode > 299) {
|
if (res.statusCode < 200 || res.statusCode > 299) {
|
||||||
return cb(
|
return cb(
|
||||||
Errors.HttpError(
|
Errors.HttpError(
|
||||||
`HTTP error ${res.statusCode}: ${truncate(body, { length: 128 })}`
|
`URL ${url} HTTP error ${res.statusCode}: ${truncate(body, {
|
||||||
|
length: 128,
|
||||||
|
})}`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,10 @@ const {
|
||||||
DefaultProfileTemplate,
|
DefaultProfileTemplate,
|
||||||
} = require('../../../activitypub/util');
|
} = require('../../../activitypub/util');
|
||||||
const Endpoints = require('../../../activitypub/endpoint');
|
const Endpoints = require('../../../activitypub/endpoint');
|
||||||
const { ActivityStreamMediaType } = require('../../../activitypub/const');
|
const {
|
||||||
|
ActivityStreamMediaType,
|
||||||
|
WellKnownActivity,
|
||||||
|
} = require('../../../activitypub/const');
|
||||||
const Config = require('../../../config').get;
|
const Config = require('../../../config').get;
|
||||||
const Activity = require('../../../activitypub/activity');
|
const Activity = require('../../../activitypub/activity');
|
||||||
const ActivityPubSettings = require('../../../activitypub/settings');
|
const ActivityPubSettings = require('../../../activitypub/settings');
|
||||||
|
@ -57,10 +60,12 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: /^\/_enig\/ap\/users\/.+\/inbox$/,
|
path: /^\/_enig\/ap\/users\/.+\/inbox$/,
|
||||||
handler: (req, resp) => {
|
handler: (req, resp) => {
|
||||||
return this._enforceSigningPolicy(
|
return this._enforceMainKeySignatureValidity(
|
||||||
req,
|
req,
|
||||||
resp,
|
resp,
|
||||||
this._inboxPostHandler.bind(this)
|
(req, resp, signature) => {
|
||||||
|
return this._inboxPostHandler(req, resp, signature, 'inbox');
|
||||||
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -68,14 +73,27 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
this.webServer.addRoute({
|
this.webServer.addRoute({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: /^\/_enig\/ap\/shared-inbox$/,
|
path: /^\/_enig\/ap\/shared-inbox$/,
|
||||||
handler: this._sharedInboxPostHandler.bind(this),
|
handler: (req, resp) => {
|
||||||
|
return this._enforceMainKeySignatureValidity(
|
||||||
|
req,
|
||||||
|
resp,
|
||||||
|
(req, resp, signature) => {
|
||||||
|
return this._inboxPostHandler(
|
||||||
|
req,
|
||||||
|
resp,
|
||||||
|
signature,
|
||||||
|
'sharedInbox'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.webServer.addRoute({
|
this.webServer.addRoute({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: /^\/_enig\/ap\/users\/.+\/outbox(\?page=[0-9]+)?$/,
|
path: /^\/_enig\/ap\/users\/.+\/outbox(\?page=[0-9]+)?$/,
|
||||||
handler: (req, resp) => {
|
handler: (req, resp) => {
|
||||||
return this._enforceSigningPolicy(
|
return this._enforceMainKeySignatureValidity(
|
||||||
req,
|
req,
|
||||||
resp,
|
resp,
|
||||||
this._outboxGetHandler.bind(this)
|
this._outboxGetHandler.bind(this)
|
||||||
|
@ -87,7 +105,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: /^\/_enig\/ap\/users\/.+\/followers(\?page=[0-9]+)?$/,
|
path: /^\/_enig\/ap\/users\/.+\/followers(\?page=[0-9]+)?$/,
|
||||||
handler: (req, resp) => {
|
handler: (req, resp) => {
|
||||||
return this._enforceSigningPolicy(
|
return this._enforceMainKeySignatureValidity(
|
||||||
req,
|
req,
|
||||||
resp,
|
resp,
|
||||||
this._followersGetHandler.bind(this)
|
this._followersGetHandler.bind(this)
|
||||||
|
@ -99,7 +117,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: /^\/_enig\/ap\/users\/.+\/following(\?page=[0-9]+)?$/,
|
path: /^\/_enig\/ap\/users\/.+\/following(\?page=[0-9]+)?$/,
|
||||||
handler: (req, resp) => {
|
handler: (req, resp) => {
|
||||||
return this._enforceSigningPolicy(
|
return this._enforceMainKeySignatureValidity(
|
||||||
req,
|
req,
|
||||||
resp,
|
resp,
|
||||||
this._followingGetHandler.bind(this)
|
this._followingGetHandler.bind(this)
|
||||||
|
@ -124,7 +142,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
_enforceSigningPolicy(req, resp, next) {
|
_enforceMainKeySignatureValidity(req, resp, next) {
|
||||||
// the request must be signed, and the signature must be valid
|
// the request must be signed, and the signature must be valid
|
||||||
const signature = this._parseAndValidateSignature(req);
|
const signature = this._parseAndValidateSignature(req);
|
||||||
if (!signature) {
|
if (!signature) {
|
||||||
|
@ -134,6 +152,28 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
return next(req, resp, signature);
|
return next(req, resp, signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_parseAndValidateSignature(req) {
|
||||||
|
let signature;
|
||||||
|
try {
|
||||||
|
// :TODO: validate options passed to parseRequest()
|
||||||
|
signature = httpSignature.parseRequest(req);
|
||||||
|
} catch (e) {
|
||||||
|
this.log.warn(
|
||||||
|
{ error: e.message, url: req.url, method: req.method },
|
||||||
|
'Failed to parse HTTP signature'
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// quick check up front
|
||||||
|
const keyId = signature.keyId;
|
||||||
|
if (!keyId || !keyId.endsWith('#main-key')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
_selfUrlRequestHandler(req, resp) {
|
_selfUrlRequestHandler(req, resp) {
|
||||||
this.log.trace({ url: req.url }, 'Request for "self"');
|
this.log.trace({ url: req.url }, 'Request for "self"');
|
||||||
|
|
||||||
|
@ -172,14 +212,15 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
if (sendActor) {
|
if (sendActor) {
|
||||||
return this._selfAsActorHandler(localUser, localActor, req, resp);
|
return this._selfAsActorHandler(localUser, localActor, req, resp);
|
||||||
} else {
|
} else {
|
||||||
return this._standardSelfHandler(localUser, localActor, req, resp);
|
return this._selfAsProfileHandler(localUser, localActor, req, resp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_inboxPostHandler(req, resp, signature) {
|
_inboxPostHandler(req, resp, signature, inboxType) {
|
||||||
EnigAssert(signature, 'Called without signature!');
|
EnigAssert(signature, 'Called without signature!');
|
||||||
|
EnigAssert(signature.keyId, 'No keyId in signature!');
|
||||||
|
|
||||||
const body = [];
|
const body = [];
|
||||||
req.on('data', d => {
|
req.on('data', d => {
|
||||||
|
@ -187,54 +228,98 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('end', () => {
|
req.on('end', () => {
|
||||||
|
// Collect and validate the posted Activity
|
||||||
const activity = Activity.fromJsonString(Buffer.concat(body).toString());
|
const activity = Activity.fromJsonString(Buffer.concat(body).toString());
|
||||||
if (!activity || !activity.isValid()) {
|
if (!activity || !activity.isValid()) {
|
||||||
this.log.error(
|
this.log.error(
|
||||||
{ url: req.url, method: req.method, endpoint: 'inbox' },
|
{ url: req.url, method: req.method, inboxType },
|
||||||
'Invalid or unsupported Activity'
|
'Invalid or unsupported Activity'
|
||||||
);
|
);
|
||||||
|
|
||||||
return activity
|
return activity
|
||||||
? this.webServer.badRequest(resp)
|
? this.webServer.badRequest(resp)
|
||||||
: this.webServer.notImplemented(resp);
|
: this.webServer.notImplemented(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Delete is a special beast:
|
||||||
|
// We will *likely* get a 410, 404, or a Tombstone when fetching the Actor
|
||||||
|
// Thus, we need some short circuiting
|
||||||
|
//
|
||||||
|
if (WellKnownActivity.Delete === activity.type) {
|
||||||
|
return this._inboxDeleteActivity(inboxType, signature, resp, activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch and validate the signature of the remote Actor
|
||||||
|
Actor.fromId(activity.actor, (err, remoteActor) => {
|
||||||
|
if (err) {
|
||||||
|
return this.webServer.internalServerError(resp, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._validateActorSignature(remoteActor, signature)) {
|
||||||
|
return this.webServer.accessDenied(resp);
|
||||||
|
}
|
||||||
|
|
||||||
switch (activity.type) {
|
switch (activity.type) {
|
||||||
case 'Follow':
|
case WellKnownActivity.Accept:
|
||||||
return this._collectionRequestHandler(
|
break;
|
||||||
signature,
|
|
||||||
'inbox',
|
|
||||||
activity,
|
|
||||||
this._inboxFollowRequestHandler.bind(this),
|
|
||||||
req,
|
|
||||||
resp
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'Delete':
|
case WellKnownActivity.Add:
|
||||||
return this._collectionRequestHandler(
|
break;
|
||||||
signature,
|
|
||||||
'inbox',
|
|
||||||
activity,
|
|
||||||
this._inboxDeleteRequestHandler.bind(this),
|
|
||||||
req,
|
|
||||||
resp
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'Update':
|
case WellKnownActivity.Create:
|
||||||
return this.inboxUpdateObject('inbox', req, resp, activity);
|
return this._inboxCreateActivity(resp, activity);
|
||||||
|
|
||||||
case 'Undo':
|
case WellKnownActivity.Update:
|
||||||
return this._collectionRequestHandler(
|
{
|
||||||
|
// Only Notes currently supported
|
||||||
|
const type = _.get(activity, 'object.type');
|
||||||
|
if ('Note' === type) {
|
||||||
|
return this._inboxMutateExistingObject(
|
||||||
|
inboxType,
|
||||||
signature,
|
signature,
|
||||||
'inbox',
|
resp,
|
||||||
activity,
|
activity,
|
||||||
this._inboxUndoRequestHandler.bind(this),
|
this._inboxUpdateObjectMutator.bind(this)
|
||||||
req,
|
|
||||||
resp
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
this.log.warn(
|
||||||
|
`Unsupported Inbox Update for type "${type}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WellKnownActivity.Follow:
|
||||||
|
// Follow requests are only allowed directly
|
||||||
|
if ('inbox' === inboxType) {
|
||||||
|
return this._inboxFollowActivity(resp, remoteActor, activity);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WellKnownActivity.Reject:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WellKnownActivity.Undo:
|
||||||
|
// We only Undo from private inboxes
|
||||||
|
if ('inbox' === inboxType) {
|
||||||
|
// Only Follow Undo's currently supported
|
||||||
|
const type = _.get(activity, 'object.type');
|
||||||
|
if (WellKnownActivity.Follow === type) {
|
||||||
|
return this._inboxUndoActivity(
|
||||||
|
resp,
|
||||||
|
remoteActor,
|
||||||
|
activity
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.log.warn(`Unsupported Undo for type "${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.log.warn(
|
this.log.warn(
|
||||||
{ type: activity.type },
|
{ type: activity.type, inboxType },
|
||||||
`Unsupported Activity type "${activity.type}"`
|
`Unsupported Activity type "${activity.type}"`
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -242,52 +327,14 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
|
|
||||||
return this.webServer.notImplemented(resp);
|
return this.webServer.notImplemented(resp);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
_sharedInboxPostHandler(req, resp) {
|
|
||||||
const body = [];
|
|
||||||
req.on('data', d => {
|
|
||||||
body.push(d);
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('end', () => {
|
|
||||||
const activity = Activity.fromJsonString(Buffer.concat(body).toString());
|
|
||||||
if (!activity || !activity.isValid()) {
|
|
||||||
this.log.error(
|
|
||||||
{ url: req.url, method: req.method, endpoint: 'sharedInbox' },
|
|
||||||
'Invalid or unsupported Activity'
|
|
||||||
);
|
|
||||||
return activity
|
|
||||||
? this.webServer.badRequest(resp)
|
|
||||||
: this.webServer.notImplemented(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (activity.type) {
|
|
||||||
case 'Create':
|
|
||||||
return this._sharedInboxCreateActivity(req, resp, activity);
|
|
||||||
|
|
||||||
case 'Update':
|
|
||||||
return this.inboxUpdateObject('sharedInbox', req, resp, activity);
|
|
||||||
|
|
||||||
default:
|
|
||||||
this.log.warn(
|
|
||||||
{ type: activity.type },
|
|
||||||
'Invalid or unknown Activity type'
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't understand the 'type'
|
|
||||||
return this.webServer.notImplemented(resp);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_sharedInboxCreateActivity(req, resp, activity) {
|
_inboxCreateActivity(resp, activity) {
|
||||||
const deliverTo = activity.recipientIds();
|
|
||||||
const createWhat = _.get(activity, 'object.type');
|
const createWhat = _.get(activity, 'object.type');
|
||||||
switch (createWhat) {
|
switch (createWhat) {
|
||||||
case 'Note':
|
case 'Note':
|
||||||
return this._deliverSharedInboxNote(req, resp, deliverTo, activity);
|
return this._inboxCreateNoteActivity(resp, activity);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.log.warn(
|
this.log.warn(
|
||||||
|
@ -298,120 +345,28 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inboxUpdateObject(inboxType, req, resp, activity) {
|
_inboxCreateNoteActivity(resp, activity) {
|
||||||
const updateObjectId = _.get(activity, 'object.id');
|
|
||||||
const objectType = _.get(activity, 'object.type');
|
|
||||||
|
|
||||||
this.log.info(
|
|
||||||
{ inboxType, objectId: updateObjectId, type: objectType },
|
|
||||||
'Inbox Object "Update" request'
|
|
||||||
);
|
|
||||||
|
|
||||||
// :TODO: other types...
|
|
||||||
if (!updateObjectId || !['Note'].includes(objectType)) {
|
|
||||||
return this.webServer.notImplemented(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note's are wrapped in Create Activities
|
|
||||||
Collection.objectByEmbeddedId(updateObjectId, (err, obj) => {
|
|
||||||
if (err) {
|
|
||||||
return this.webServer.internalServerError(resp, err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!obj) {
|
|
||||||
// no match, but respond as accepted and hopefully they don't ask again
|
|
||||||
return this.webServer.accepted(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// OK, the object exists; Does the caller have permission
|
|
||||||
// to update? The origin must match
|
|
||||||
//
|
|
||||||
// "The receiving server MUST take care to be sure that the Update is authorized
|
|
||||||
// to modify its object. At minimum, this may be done by ensuring that the Update
|
|
||||||
// and its object are of same origin."
|
|
||||||
try {
|
|
||||||
const updateTargetUrl = new URL(obj.object.id);
|
|
||||||
const updaterUrl = new URL(activity.actor);
|
|
||||||
|
|
||||||
if (updateTargetUrl.host !== updaterUrl.host) {
|
|
||||||
this.log.warn(
|
|
||||||
{
|
|
||||||
objectId: updateObjectId,
|
|
||||||
type: objectType,
|
|
||||||
updateTargetHost: updateTargetUrl.host,
|
|
||||||
requestorHost: updaterUrl.host,
|
|
||||||
},
|
|
||||||
'Attempt to update object from another origin'
|
|
||||||
);
|
|
||||||
return this.webServer.accessDenied(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
Collection.updateCollectionEntry(
|
|
||||||
'inbox',
|
|
||||||
updateObjectId,
|
|
||||||
activity,
|
|
||||||
err => {
|
|
||||||
if (err) {
|
|
||||||
return this.webServer.internalServerError(resp, err);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log.info(
|
|
||||||
{
|
|
||||||
objectId: updateObjectId,
|
|
||||||
type: objectType,
|
|
||||||
collection: 'inbox',
|
|
||||||
},
|
|
||||||
'Object updated'
|
|
||||||
);
|
|
||||||
return this.webServer.accepted(resp);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return this.webServer.internalServerError(resp, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_deliverSharedInboxNote(req, resp, deliverTo, activity) {
|
|
||||||
// When an object is being delivered to the originating actor's followers,
|
|
||||||
// a server MAY reduce the number of receiving actors delivered to by
|
|
||||||
// identifying all followers which share the same sharedInbox who would
|
|
||||||
// otherwise be individual recipients and instead deliver objects to said
|
|
||||||
// sharedInbox. Thus in this scenario, the remote/receiving server participates
|
|
||||||
// in determining targeting and performing delivery to specific inboxes.
|
|
||||||
const note = new Note(activity.object);
|
const note = new Note(activity.object);
|
||||||
if (!note.isValid()) {
|
if (!note.isValid()) {
|
||||||
// :TODO: Log me
|
this.log.warn({ note }, 'Invalid Note');
|
||||||
return this.webServer.notImplemented();
|
return this.webServer.notImplemented();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipientActorIds = note.recipientIds();
|
||||||
async.forEach(
|
async.forEach(
|
||||||
deliverTo,
|
recipientActorIds,
|
||||||
(actorId, nextActor) => {
|
(actorId, nextActorId) => {
|
||||||
switch (actorId) {
|
switch (actorId) {
|
||||||
case Collection.PublicCollectionId:
|
case Collection.PublicCollectionId:
|
||||||
this._deliverInboxNoteToSharedInbox(
|
this._deliverNoteToSharedInbox(activity, note, err => {
|
||||||
req,
|
return nextActorId(err);
|
||||||
resp,
|
});
|
||||||
activity,
|
|
||||||
note,
|
|
||||||
err => {
|
|
||||||
return nextActor(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this._deliverInboxNoteToLocalActor(
|
this._deliverNoteToLocalActor(actorId, activity, note, err => {
|
||||||
req,
|
return nextActorId(err);
|
||||||
resp,
|
});
|
||||||
actorId,
|
|
||||||
activity,
|
|
||||||
note,
|
|
||||||
err => {
|
|
||||||
return nextActor(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -425,7 +380,217 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_deliverInboxNoteToSharedInbox(req, resp, activity, note, cb) {
|
_inboxDeleteActivity(inboxType, signature, resp /*, activity*/) {
|
||||||
|
// :TODO: Implement me!
|
||||||
|
// :TODO: we need to DELETE the existing stored Message object if this is a Note, or associated if this is an Actor
|
||||||
|
// :TODO: delete / invalidate any actor cache if actor
|
||||||
|
return this.webServer.accepted(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
_inboxFollowActivity(resp, remoteActor, activity) {
|
||||||
|
this.log.info(
|
||||||
|
{ remoteActorId: remoteActor.id, localActorId: activity.object },
|
||||||
|
'Incoming Follow Activity'
|
||||||
|
);
|
||||||
|
|
||||||
|
userFromActorId(activity.object, (err, localUser) => {
|
||||||
|
if (err) {
|
||||||
|
return this.webServer.resourceNotFound(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User accepts any followers automatically
|
||||||
|
const activityPubSettings = ActivityPubSettings.fromUser(localUser);
|
||||||
|
if (!activityPubSettings.manuallyApproveFollowers) {
|
||||||
|
this._recordAcceptedFollowRequest(localUser, remoteActor, activity);
|
||||||
|
return this.webServer.ok(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User manually approves requests; add them to their requests collection
|
||||||
|
Collection.addFollowRequest(
|
||||||
|
localUser,
|
||||||
|
remoteActor,
|
||||||
|
this.webServer,
|
||||||
|
true, // ignore dupes
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
return this.internalServerError(resp, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.webServer.ok(resp);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_inboxUndoActivity(resp, remoteActor, activity) {
|
||||||
|
const localActorId = _.get(activity, 'object.object');
|
||||||
|
|
||||||
|
this.log.info(
|
||||||
|
{ remoteActorId: remoteActor.id, localActorId },
|
||||||
|
'Incoming Undo Activity'
|
||||||
|
);
|
||||||
|
|
||||||
|
userFromActorId(localActorId, (err, localUser) => {
|
||||||
|
if (err) {
|
||||||
|
return this.webServer.resourceNotFound(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection.removeOwnedById('followers', localUser, remoteActor.id, err => {
|
||||||
|
if (err) {
|
||||||
|
return this.webServer.internalServerError(resp, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.info(
|
||||||
|
{
|
||||||
|
username: localUser.username,
|
||||||
|
userId: localUser.userId,
|
||||||
|
remoteActorId: remoteActor.id,
|
||||||
|
},
|
||||||
|
'Undo "Follow" (un-follow) success'
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.webServer.accepted(resp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_localUserFromCollectionEndpoint(req, collectionName, cb) {
|
||||||
|
// turn a collection URL to a Actor ID
|
||||||
|
let actorId = this.webServer.fullUrl(req).toString();
|
||||||
|
const suffix = `/${collectionName}`;
|
||||||
|
if (actorId.endsWith(suffix)) {
|
||||||
|
actorId = actorId.slice(0, -suffix.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
userFromActorId(actorId, (err, localUser) => {
|
||||||
|
return cb(err, localUser);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_validateActorSignature(actor, signature) {
|
||||||
|
const pubKey = actor.publicKey;
|
||||||
|
if (!_.isObject(pubKey)) {
|
||||||
|
this.log.debug('Expected object of "pubKey"');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signature.keyId !== pubKey.id) {
|
||||||
|
this.log.warn(
|
||||||
|
{
|
||||||
|
actorId: actor.id,
|
||||||
|
signatureKeyId: signature.keyId,
|
||||||
|
actorPubKeyId: pubKey.id,
|
||||||
|
},
|
||||||
|
'Key ID mismatch'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!httpSignature.verifySignature(signature, pubKey.publicKeyPem)) {
|
||||||
|
this.log.warn(
|
||||||
|
{
|
||||||
|
actorId: actor.id,
|
||||||
|
keyId: signature.keyId,
|
||||||
|
},
|
||||||
|
'Actor signature verification failed'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_inboxMutateExistingObject(inboxType, signature, resp, activity, mutator) {
|
||||||
|
const targetObjectId = _.get(activity, 'object.id');
|
||||||
|
const objectType = _.get(activity, 'object.type');
|
||||||
|
|
||||||
|
Collection.objectByEmbeddedId(targetObjectId, (err, obj) => {
|
||||||
|
if (err) {
|
||||||
|
return this.webServer.internalServerError(resp, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!obj) {
|
||||||
|
this.log.warn(
|
||||||
|
{ targetObjectId, type: objectType, activityType: activity.type },
|
||||||
|
`Could not ${activity.type} Object; Not found`
|
||||||
|
);
|
||||||
|
return this.webServer.resourceNotFound(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Object exists; Validate we allow the action by origin
|
||||||
|
// comparing the request's keyId origin to the object's
|
||||||
|
//
|
||||||
|
try {
|
||||||
|
const updateTargetHost = new URL(obj.object.id).host;
|
||||||
|
const keyIdHost = new URL(signature.keyId).host;
|
||||||
|
|
||||||
|
if (updateTargetHost !== keyIdHost) {
|
||||||
|
this.log.warn(
|
||||||
|
{
|
||||||
|
targetObjectId,
|
||||||
|
type: objectType,
|
||||||
|
updateTargetHost,
|
||||||
|
keyIdHost,
|
||||||
|
activityType: activity.type,
|
||||||
|
},
|
||||||
|
`Attempt to ${activity.type} Object of non-matching origin`
|
||||||
|
);
|
||||||
|
return this.webServer.accessDenied(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mutator(inboxType, resp, objectType, targetObjectId, activity);
|
||||||
|
} catch (e) {
|
||||||
|
return this.webServer.internalServerError(resp, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// _inboxDeleteActivityMutator(inboxType, resp, objectType, targetObjectId) {
|
||||||
|
// Collection.removeById(inboxType, targetObjectId, err => {
|
||||||
|
// if (err) {
|
||||||
|
// return this.webServer.internalServerError(resp, err);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.log.info(
|
||||||
|
// {
|
||||||
|
// inboxType,
|
||||||
|
// objectId: targetObjectId,
|
||||||
|
// objectType,
|
||||||
|
// },
|
||||||
|
// `${objectType} Deleted`
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // :TODO: we need to DELETE the existing stored Message object if this is a Note
|
||||||
|
|
||||||
|
// return this.webServer.accepted(resp);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
_inboxUpdateObjectMutator(inboxType, resp, objectType, targetObjectId, activity) {
|
||||||
|
Collection.updateCollectionEntry(inboxType, targetObjectId, activity, err => {
|
||||||
|
if (err) {
|
||||||
|
return this.webServer.internalServerError(resp, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.info(
|
||||||
|
{
|
||||||
|
inboxType,
|
||||||
|
objectId: targetObjectId,
|
||||||
|
objectType,
|
||||||
|
},
|
||||||
|
`${objectType} Updated`
|
||||||
|
);
|
||||||
|
|
||||||
|
// :TODO: we need to UPDATE the existing stored Message object if this is a Note
|
||||||
|
|
||||||
|
return this.webServer.accepted(resp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_deliverNoteToSharedInbox(activity, note, cb) {
|
||||||
|
this.log.info({ noteId: note.id }, 'Delivering Note to Public inbox');
|
||||||
|
|
||||||
Collection.addSharedInboxItem(activity, true, err => {
|
Collection.addSharedInboxItem(activity, true, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
@ -441,6 +606,33 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_deliverNoteToLocalActor(actorId, activity, note, cb) {
|
||||||
|
this.log.info(
|
||||||
|
{ noteId: note.id, actorId },
|
||||||
|
'Delivering Note to local Actor Private inbox'
|
||||||
|
);
|
||||||
|
|
||||||
|
userFromActorId(actorId, (err, localUser) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(null); // not found/etc., just bail
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection.addInboxItem(activity, localUser, this.webServer, false, err => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._storeNoteAsMessage(
|
||||||
|
activity.id,
|
||||||
|
localUser,
|
||||||
|
Message.WellKnownAreaTags.Private,
|
||||||
|
note,
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_storeNoteAsMessage(activityId, localAddressedTo, areaTag, note, cb) {
|
_storeNoteAsMessage(activityId, localAddressedTo, areaTag, note, cb) {
|
||||||
//
|
//
|
||||||
// Import the item to the user's private mailbox
|
// Import the item to the user's private mailbox
|
||||||
|
@ -478,31 +670,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_deliverInboxNoteToLocalActor(req, resp, actorId, activity, note, cb) {
|
_actorCollectionRequest(collectionName, req, resp) {
|
||||||
userFromActorId(actorId, (err, localUser) => {
|
|
||||||
if (err) {
|
|
||||||
return cb(null); // not found/etc., just bail
|
|
||||||
}
|
|
||||||
|
|
||||||
Collection.addInboxItem(activity, localUser, this.webServer, false, err => {
|
|
||||||
if (err) {
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._storeNoteAsMessage(
|
|
||||||
activity.id,
|
|
||||||
localUser,
|
|
||||||
Message.WellKnownAreaTags.Private,
|
|
||||||
note,
|
|
||||||
cb
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_getCollectionHandler(collectionName, req, resp, signature) {
|
|
||||||
EnigAssert(signature, 'Missing signature!');
|
|
||||||
|
|
||||||
const getCollection = Collection[collectionName];
|
const getCollection = Collection[collectionName];
|
||||||
if (!getCollection) {
|
if (!getCollection) {
|
||||||
return this.webServer.resourceNotFound(resp);
|
return this.webServer.resourceNotFound(resp);
|
||||||
|
@ -511,6 +679,22 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
const url = this.webServer.fullUrl(req);
|
const url = this.webServer.fullUrl(req);
|
||||||
const page = url.searchParams.get('page');
|
const page = url.searchParams.get('page');
|
||||||
const collectionId = url.toString();
|
const collectionId = url.toString();
|
||||||
|
|
||||||
|
this._localUserFromCollectionEndpoint(req, collectionName, (err, localUser) => {
|
||||||
|
if (err) {
|
||||||
|
return this.webServer.resourceNotFound(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apSettings = ActivityPubSettings.fromUser(localUser);
|
||||||
|
if (apSettings.hideSocialGraph) {
|
||||||
|
this.log.info(
|
||||||
|
{ user: localUser.username },
|
||||||
|
`User has ${collectionName} hidden`
|
||||||
|
);
|
||||||
|
return this.webServer.accessDenied(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// :TODO: these getters should take a owningUser; they are not strictly public
|
||||||
getCollection(collectionId, page, (err, collection) => {
|
getCollection(collectionId, page, (err, collection) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return this.webServer.internalServerError(resp, err);
|
return this.webServer.internalServerError(resp, err);
|
||||||
|
@ -521,22 +705,25 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
'Content-Type': ActivityStreamMediaType,
|
'Content-Type': ActivityStreamMediaType,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// :TODO: we need to validate the local user allows access to the particular collection
|
||||||
}
|
}
|
||||||
|
|
||||||
_followingGetHandler(req, resp, signature) {
|
_followingGetHandler(req, resp) {
|
||||||
this.log.debug({ url: req.url }, 'Request for "following"');
|
this.log.debug({ url: req.url }, 'Request for "following"');
|
||||||
return this._getCollectionHandler('following', req, resp, signature);
|
return this._actorCollectionRequest('following', req, resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
_followersGetHandler(req, resp, signature) {
|
_followersGetHandler(req, resp) {
|
||||||
this.log.debug({ url: req.url }, 'Request for "followers"');
|
this.log.debug({ url: req.url }, 'Request for "followers"');
|
||||||
return this._getCollectionHandler('followers', req, resp, signature);
|
return this._actorCollectionRequest('followers', req, resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://docs.gotosocial.org/en/latest/federation/behaviors/outbox/
|
// https://docs.gotosocial.org/en/latest/federation/behaviors/outbox/
|
||||||
_outboxGetHandler(req, resp, signature) {
|
_outboxGetHandler(req, resp) {
|
||||||
this.log.debug({ url: req.url }, 'Request for "outbox"');
|
this.log.debug({ url: req.url }, 'Request for "outbox"');
|
||||||
return this._getCollectionHandler('outbox', req, resp, signature);
|
return this._actorCollectionRequest('outbox', req, resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
_singlePublicNoteGetHandler(req, resp) {
|
_singlePublicNoteGetHandler(req, resp) {
|
||||||
|
@ -558,173 +745,6 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_accountNameFromUserPath(url, suffix) {
|
|
||||||
const re = new RegExp(`^/_enig/ap/users/(.+)/${suffix}(\\?page=[0-9]+)?$`);
|
|
||||||
const m = url.pathname.match(re);
|
|
||||||
if (!m || !m[1]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return m[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
_parseAndValidateSignature(req) {
|
|
||||||
let signature;
|
|
||||||
try {
|
|
||||||
// :TODO: validate options passed to parseRequest()
|
|
||||||
signature = httpSignature.parseRequest(req);
|
|
||||||
} catch (e) {
|
|
||||||
this.log.warn(
|
|
||||||
{ error: e.message, url: req.url, method: req.method },
|
|
||||||
'Failed to parse HTTP signature'
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// quick check up front
|
|
||||||
const keyId = signature.keyId;
|
|
||||||
if (!this._validateKeyId(keyId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
_validateKeyId(keyId) {
|
|
||||||
if (!keyId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we only accept main-key currently
|
|
||||||
return keyId.endsWith('#main-key');
|
|
||||||
}
|
|
||||||
|
|
||||||
_inboxFollowRequestHandler(activity, remoteActor, localUser, resp) {
|
|
||||||
this.log.info(
|
|
||||||
{ user_id: localUser.userId, actor: activity.actor },
|
|
||||||
'Follow request'
|
|
||||||
);
|
|
||||||
|
|
||||||
//
|
|
||||||
// If the user blindly accepts Followers, we can persist
|
|
||||||
// and send an 'Accept' now. Otherwise, we need to queue this
|
|
||||||
// request for the user to review and decide what to do with
|
|
||||||
// at a later time.
|
|
||||||
//
|
|
||||||
const activityPubSettings = ActivityPubSettings.fromUser(localUser);
|
|
||||||
if (!activityPubSettings.manuallyApproveFollowers) {
|
|
||||||
this._recordAcceptedFollowRequest(localUser, remoteActor, activity);
|
|
||||||
return this.webServer.ok(resp);
|
|
||||||
} else {
|
|
||||||
Collection.addFollowRequest(
|
|
||||||
localUser,
|
|
||||||
remoteActor,
|
|
||||||
this.webServer,
|
|
||||||
true, // ignore dupes
|
|
||||||
err => {
|
|
||||||
if (err) {
|
|
||||||
return this.internalServerError(resp, err);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.webServer.ok(resp);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// :TODO: DRY: update/delete are mostly the same code other than the final operation
|
|
||||||
_inboxDeleteRequestHandler(activity, remoteActor, localUser, resp) {
|
|
||||||
this.log.info(
|
|
||||||
{ user_id: localUser.userId, actor: activity.actor },
|
|
||||||
'Delete request'
|
|
||||||
);
|
|
||||||
|
|
||||||
// :TODO:only delete if it's owned by the sender
|
|
||||||
|
|
||||||
return this.webServer.accepted(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
_inboxUndoRequestHandler(activity, remoteActor, localUser, resp) {
|
|
||||||
this.log.info(
|
|
||||||
{ user: localUser.username, actor: remoteActor.id },
|
|
||||||
'Undo Activity request'
|
|
||||||
);
|
|
||||||
|
|
||||||
// we only understand Follow right now
|
|
||||||
if (!activity.object || activity.object.type !== 'Follow') {
|
|
||||||
return this.webServer.notImplemented(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
Collection.removeOwnedById('followers', localUser, remoteActor.id, err => {
|
|
||||||
if (err) {
|
|
||||||
return this.webServer.internalServerError(resp, err);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log.info(
|
|
||||||
{
|
|
||||||
username: localUser.username,
|
|
||||||
userId: localUser.userId,
|
|
||||||
actor: remoteActor.id,
|
|
||||||
},
|
|
||||||
'Undo "Follow" (un-follow) success'
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.webServer.accepted(resp);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_collectionRequestHandler(
|
|
||||||
signature,
|
|
||||||
collectionName,
|
|
||||||
activity,
|
|
||||||
activityHandler,
|
|
||||||
req,
|
|
||||||
resp
|
|
||||||
) {
|
|
||||||
// turn a collection URL to a Actor ID
|
|
||||||
let actorId = this.webServer.fullUrl(req).toString();
|
|
||||||
const suffix = `/${collectionName}`;
|
|
||||||
if (actorId.endsWith(suffix)) {
|
|
||||||
actorId = actorId.slice(0, -suffix.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
userFromActorId(actorId, (err, localUser) => {
|
|
||||||
if (err) {
|
|
||||||
return this.webServer.resourceNotFound(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
Actor.fromId(activity.actor, (err, actor) => {
|
|
||||||
if (err) {
|
|
||||||
return this.webServer.internalServerError(resp, err);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pubKey = actor.publicKey;
|
|
||||||
if (!_.isObject(pubKey)) {
|
|
||||||
// Log me
|
|
||||||
return this.webServer.accessDenied();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signature.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.keyId,
|
|
||||||
signature: req.headers['signature'] || '',
|
|
||||||
},
|
|
||||||
'Invalid signature supplied for Follow request'
|
|
||||||
);
|
|
||||||
return this.webServer.accessDenied(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
return activityHandler(activity, actor, localUser, resp);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_recordAcceptedFollowRequest(localUser, remoteActor, requestActivity) {
|
_recordAcceptedFollowRequest(localUser, remoteActor, requestActivity) {
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
|
@ -803,11 +823,6 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_authorizeInteractionHandler(req, resp) {
|
|
||||||
console.log(req);
|
|
||||||
console.log(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
_selfAsActorHandler(localUser, localActor, req, resp) {
|
_selfAsActorHandler(localUser, localActor, req, resp) {
|
||||||
this.log.info(
|
this.log.info(
|
||||||
{ username: localUser.username },
|
{ username: localUser.username },
|
||||||
|
@ -819,7 +834,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
return this.webServer.ok(resp, body, { 'Content-Type': ActivityStreamMediaType });
|
return this.webServer.ok(resp, body, { 'Content-Type': ActivityStreamMediaType });
|
||||||
}
|
}
|
||||||
|
|
||||||
_standardSelfHandler(localUser, localActor, req, resp) {
|
_selfAsProfileHandler(localUser, localActor, req, resp) {
|
||||||
let templateFile = _.get(
|
let templateFile = _.get(
|
||||||
Config(),
|
Config(),
|
||||||
'contentServers.web.handlers.activityPub.selfTemplate'
|
'contentServers.web.handlers.activityPub.selfTemplate'
|
||||||
|
|
Loading…
Reference in New Issue