Start work on Activity and Actor objects, validation, fetching, etc.
This commit is contained in:
parent
416f86a0cc
commit
f86d9338a1
|
@ -0,0 +1,44 @@
|
|||
const { isString, isObject } = require('lodash');
|
||||
|
||||
module.exports = class Activity {
|
||||
constructor(obj) {
|
||||
Object.assign(this, obj);
|
||||
}
|
||||
|
||||
static get ActivityTypes() {
|
||||
return [
|
||||
'Create',
|
||||
'Update',
|
||||
'Delete',
|
||||
'Follow',
|
||||
'Accept',
|
||||
'Reject',
|
||||
'Add',
|
||||
'Remove',
|
||||
'Like',
|
||||
'Announce',
|
||||
'Undo',
|
||||
];
|
||||
}
|
||||
|
||||
static fromJson(json) {
|
||||
const parsed = JSON.parse(json);
|
||||
return new Activity(parsed);
|
||||
}
|
||||
|
||||
isValid() {
|
||||
if (
|
||||
this['@context'] !== 'https://www.w3.org/ns/activitystreams' ||
|
||||
!isString(this.id) ||
|
||||
!isString(this.actor) ||
|
||||
(!isString(this.object) && !isObject(this.object)) ||
|
||||
!Activity.ActivityTypes.includes(this.type)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// :TODO: we could validate the particular types
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
|
@ -5,28 +5,111 @@
|
|||
const actorDb = require('./database.js').dbs.actor;
|
||||
const { Errors } = require('./enig_error.js');
|
||||
const Events = require('./events.js');
|
||||
const ActorProps = require('./activitypub_actor_property.js');
|
||||
const { ActorProps } = require('./activitypub_actor_property');
|
||||
const { isValidLink } = require('./activitypub_util');
|
||||
|
||||
// deps
|
||||
const assert = require('assert');
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const https = require('https');
|
||||
|
||||
const isString = require('lodash/isString');
|
||||
|
||||
// https://www.w3.org/TR/activitypub/#actor-objects
|
||||
module.exports = class Actor {
|
||||
constructor() {
|
||||
constructor(obj) {
|
||||
if (obj) {
|
||||
Object.assign(this, obj);
|
||||
} else {
|
||||
this['@context'] = ['https://www.w3.org/ns/activitystreams'];
|
||||
this.id = '';
|
||||
this.type = '';
|
||||
this.inbox = '';
|
||||
this.outbox = '';
|
||||
this.following = '';
|
||||
this.followers = '';
|
||||
this.liked = '';
|
||||
}
|
||||
|
||||
this.actorId = 0;
|
||||
this.actorUrl = '';
|
||||
this.properties = {}; // name:value
|
||||
this.groups = []; // group membership(s)
|
||||
}
|
||||
|
||||
isValid() {
|
||||
if (
|
||||
!Array.isArray(this['@context']) ||
|
||||
this['@context'][0] !== 'https://www.w3.org/ns/activitystreams'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isString(this.type) || this.type.length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const linksValid = ['inbox', 'outbox', 'following', 'followers'].every(p => {
|
||||
return isValidLink(this[p]);
|
||||
});
|
||||
if (!linksValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static getRemoteActor(url, cb) {
|
||||
const headers = {
|
||||
Accept: 'application/activity+json',
|
||||
};
|
||||
|
||||
https.get(url, { 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', () => {
|
||||
let actor;
|
||||
try {
|
||||
actor = Actor.fromJson(body);
|
||||
} catch (e) {
|
||||
return cb(e);
|
||||
}
|
||||
|
||||
if (!actor.isValid()) {
|
||||
return cb(Errors.Invalid('Invalid Actor'));
|
||||
}
|
||||
|
||||
return cb(null, actor);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static fromJson(json) {
|
||||
const parsed = JSON.parse(json);
|
||||
return new Actor(parsed);
|
||||
}
|
||||
|
||||
create(cb) {
|
||||
assert(0 === this.actorId);
|
||||
|
||||
if (
|
||||
_.isEmpty(this.actorUrl)
|
||||
) {
|
||||
if (_.isEmpty(this.actorUrl)) {
|
||||
return cb(Errors.Invalid('Blank actor url'));
|
||||
}
|
||||
|
||||
|
@ -135,14 +218,14 @@ module.exports = class Actor {
|
|||
}
|
||||
|
||||
persistProperty(propName, propValue, cb) {
|
||||
// update live props
|
||||
// update live props
|
||||
this.properties[propName] = propValue;
|
||||
|
||||
return Actor.persistPropertyByActorId(this.actorId, propName, propValue, cb);
|
||||
}
|
||||
|
||||
removeProperty(propName, cb) {
|
||||
// update live
|
||||
// update live
|
||||
delete this.properties[propName];
|
||||
|
||||
actorDb.run(
|
||||
|
@ -206,7 +289,6 @@ module.exports = class Actor {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
static getActor(actorId, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
|
@ -243,7 +325,7 @@ module.exports = class Actor {
|
|||
ActorProps.Summary,
|
||||
ActorProps.IconUrl,
|
||||
ActorProps.BannerUrl,
|
||||
ActorProps.PublicKeyMain
|
||||
ActorProps.PublicKeyMain,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
// This IS NOT a full list. For example, custom modules
|
||||
// can utilize their own properties as well!
|
||||
//
|
||||
module.exports = {
|
||||
exports.ActorProps = {
|
||||
Type: 'type',
|
||||
PreferredUsername: 'preferred_user_name',
|
||||
Name: 'name',
|
||||
|
@ -16,3 +16,5 @@ module.exports = {
|
|||
BannerUrl: 'banner_url',
|
||||
PublicKeyMain: 'public_key_main_rsa_pem', // RSA public key for user
|
||||
};
|
||||
|
||||
exports.AllActorProperties = Object.values(exports.ActorProps);
|
||||
|
|
|
@ -11,6 +11,7 @@ const fs = require('graceful-fs');
|
|||
const paths = require('path');
|
||||
const moment = require('moment');
|
||||
|
||||
exports.isValidLink = isValidLink;
|
||||
exports.makeUserUrl = makeUserUrl;
|
||||
exports.webFingerProfileUrl = webFingerProfileUrl;
|
||||
exports.selfUrl = selfUrl;
|
||||
|
@ -31,6 +32,10 @@ Affiliations: %AFFILIATIONS%
|
|||
Achievement Points: %ACHIEVEMENT_POINTS%
|
||||
`;
|
||||
|
||||
function isValidLink(l) {
|
||||
return /^https?:\/\/.+$/.test(l);
|
||||
}
|
||||
|
||||
function makeUserUrl(webServer, user, relPrefix) {
|
||||
return webServer.buildUrl(
|
||||
WellKnownLocations.Internal + `${relPrefix}${user.username}`
|
||||
|
|
|
@ -41,7 +41,7 @@ function getModDatabasePath(moduleInfo, suffix) {
|
|||
// filename. An optional suffix may be supplied as well.
|
||||
//
|
||||
const HOST_RE =
|
||||
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
|
||||
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
|
||||
|
||||
assert(_.isObject(moduleInfo));
|
||||
assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!');
|
||||
|
@ -81,7 +81,7 @@ function getISOTimestampString(ts) {
|
|||
|
||||
function sanitizeString(s) {
|
||||
return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => {
|
||||
// eslint-disable-line no-control-regex
|
||||
// eslint-disable-line no-control-regex
|
||||
switch (c) {
|
||||
case '\0':
|
||||
return '\\0';
|
||||
|
@ -97,7 +97,7 @@ function sanitizeString(s) {
|
|||
return '\\r';
|
||||
|
||||
case '"':
|
||||
case '\'':
|
||||
case "'":
|
||||
return `${c}${c}`;
|
||||
|
||||
case '\\':
|
||||
|
|
|
@ -10,6 +10,7 @@ const {
|
|||
} = require('../../../activitypub_util');
|
||||
const UserProps = require('../../../user_property');
|
||||
const Config = require('../../../config').get;
|
||||
const Activity = require('../../../activitypub_activity');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
|
@ -17,6 +18,7 @@ const enigma_assert = require('../../../enigma_assert');
|
|||
const httpSignature = require('http-signature');
|
||||
const https = require('https');
|
||||
const { Errors } = require('../../../enig_error');
|
||||
const Actor = require('../../../activitypub_actor');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name: 'ActivityPub',
|
||||
|
@ -105,6 +107,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
return this.webServer.resourceNotFound(resp);
|
||||
}
|
||||
|
||||
// quick check up front
|
||||
const keyId = signature.keyId;
|
||||
if (!this._validateKeyId(keyId)) {
|
||||
return this.webServer.resourceNotFound(resp);
|
||||
|
@ -116,30 +119,37 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
});
|
||||
|
||||
req.on('end', () => {
|
||||
let activity;
|
||||
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);
|
||||
}
|
||||
activity = Activity.fromJson(body);
|
||||
} catch (e) {
|
||||
this.log.error(
|
||||
{ error: e.message, url: req.url, method: req.method },
|
||||
'Failed to parse Activity'
|
||||
);
|
||||
return this.webServer.resourceNotFound(resp);
|
||||
}
|
||||
|
||||
if (!activity.isValid()) {
|
||||
// :TODO: Log me
|
||||
return this.webServer.webServer.badRequest(resp);
|
||||
}
|
||||
|
||||
switch (activity.type) {
|
||||
case 'Follow':
|
||||
return this._inboxFollowRequestHandler(
|
||||
signature,
|
||||
activity,
|
||||
req,
|
||||
resp
|
||||
);
|
||||
|
||||
default:
|
||||
this.log.debug(
|
||||
{ type: activity.type },
|
||||
`Unsupported Activity type "${activity.type}"`
|
||||
);
|
||||
return this.webServer.resourceNotFound(resp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -156,15 +166,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
}
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
|
||||
_inboxFollowRequestHandler(signature, activity, req, resp) {
|
||||
const accountName = accountFromSelfUrl(activity.object);
|
||||
if (!accountName) {
|
||||
return this.webServer.badRequest(resp);
|
||||
|
@ -175,7 +177,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
return this.webServer.resourceNotFound(resp);
|
||||
}
|
||||
|
||||
this._fetchActor(activity.actor, (err, actor) => {
|
||||
Actor.getRemoteActor(activity.actor, (err, actor) => {
|
||||
if (err) {
|
||||
// :TODO: log, and probably should be inspecting |err|
|
||||
return this.webServer.internalServerError(resp);
|
||||
|
@ -187,7 +189,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
|||
return this.webServer.accessDenied();
|
||||
}
|
||||
|
||||
if (keyId !== pubKey.id) {
|
||||
if (signature.keyId !== pubKey.id) {
|
||||
// :TODO: Log me
|
||||
return this.webServer.accessDenied(resp);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue