enigma-bbs/core/servers/content/web_handlers/webfinger.js

251 lines
7.8 KiB
JavaScript
Raw Normal View History

2022-12-31 22:48:51 +00:00
const WebHandlerModule = require('../../../web_handler_module');
2022-12-31 05:39:39 +00:00
const Config = require('../../../config').get;
const { Errors, ErrorReasons } = require('../../../enig_error');
2022-12-31 22:30:54 +00:00
const { WellKnownLocations } = require('../web');
const {
localActorId,
webFingerProfileUrl,
userFromActorId,
2023-01-05 03:29:18 +00:00
getUserProfileTemplatedBody,
DefaultProfileTemplate,
} = require('../../../activitypub/util');
const EngiAssert = require('../../../enigma_assert');
const User = require('../../../user');
const UserProps = require('../../../user_property');
const ActivityPubSettings = require('../../../activitypub/settings');
2022-12-31 05:39:39 +00:00
// deps
2022-12-31 05:39:39 +00:00
const _ = require('lodash');
exports.moduleInfo = {
name: 'WebFinger',
2023-01-01 00:51:03 +00:00
desc: 'A simple WebFinger Handler.',
2022-12-31 22:39:54 +00:00
author: 'NuSkooler, CognitiveGears',
packageName: 'codes.l33t.enigma.web.handler.webfinger',
2022-12-31 05:39:39 +00:00
};
2023-01-01 00:51:03 +00:00
//
// WebFinger: https://www.rfc-editor.org/rfc/rfc7033
//
2023-01-03 05:25:32 +00:00
exports.getModule = class WebFingerWebHandler extends WebHandlerModule {
2022-12-31 05:39:39 +00:00
constructor() {
super();
}
init(webServer, cb) {
2022-12-31 05:39:39 +00:00
// we rely on the web server
this.webServer = webServer;
EngiAssert(webServer, 'WebFinger Web Handler init without webServer');
this.log = webServer.logger().child({ webHandler: 'WebFinger' });
2022-12-31 05:39:39 +00:00
2023-01-03 05:25:32 +00:00
const domain = this.webServer.getDomain();
2022-12-31 22:30:54 +00:00
if (!domain) {
return cb(Errors.UnexpectedState('Web server does not have "domain" set'));
}
this.acceptedResourceRegExps = [
// acct:NAME@our.domain.tld
2023-01-01 00:51:03 +00:00
// https://www.rfc-editor.org/rfc/rfc7565
2022-12-31 22:30:54 +00:00
new RegExp(`^acct:(.+)@${domain}$`),
2023-01-01 00:51:03 +00:00
// profile page
// https://webfinger.net/rel/profile-page/
2023-01-03 05:25:32 +00:00
new RegExp(
`^${this.webServer.buildUrl(WellKnownLocations.Internal + '/wf/@')}(.+)$`
),
2022-12-31 22:30:54 +00:00
// self URL
new RegExp(
2023-01-03 05:25:32 +00:00
`^${this.webServer.buildUrl(
WellKnownLocations.Internal + '/ap/users/'
)}(.+)$`
2022-12-31 22:30:54 +00:00
),
];
2023-01-03 05:25:32 +00:00
this.webServer.addRoute({
2022-12-31 05:39:39 +00:00
method: 'GET',
2022-12-31 22:30:54 +00:00
// https://www.rfc-editor.org/rfc/rfc7033.html#section-10.1
2022-12-31 05:39:39 +00:00
path: /^\/\.well-known\/webfinger\/?\?/,
handler: this._webFingerRequestHandler.bind(this),
});
2023-01-03 05:25:32 +00:00
this.webServer.addRoute({
method: 'GET',
2023-01-02 02:19:51 +00:00
path: /^\/_enig\/wf\/@.+$/,
handler: this._profileRequestHandler.bind(this),
});
2022-12-31 05:39:39 +00:00
return cb(null);
}
_profileRequestHandler(req, resp) {
const actorId = this.webServer.fullUrl(req).toString();
userFromActorId(actorId, (err, localUser) => {
if (err) {
2023-01-07 01:55:24 +00:00
this.log.warn(
{ error: err.message, type: 'Profile' },
'Could not fetch profile for WebFinger request'
2023-01-07 01:55:24 +00:00
);
2023-01-08 08:22:02 +00:00
return this.webServer.resourceNotFound(resp);
}
2023-01-05 03:29:18 +00:00
let templateFile = _.get(
Config(),
'contentServers.web.handlers.webFinger.profileTemplate'
);
if (templateFile) {
templateFile = this.webServer.resolveTemplatePath(templateFile);
}
2023-01-05 03:29:18 +00:00
getUserProfileTemplatedBody(
templateFile,
localUser,
2023-01-05 03:29:18 +00:00
DefaultProfileTemplate,
'text/plain',
(err, body, contentType) => {
if (err) {
2023-01-08 08:22:02 +00:00
return this.webServer.resourceNotFound(resp);
2023-01-05 03:29:18 +00:00
}
const headers = {
'Content-Type': contentType,
'Content-Length': body.length,
};
resp.writeHead(200, headers);
return resp.end(body);
2023-01-02 02:19:51 +00:00
}
2023-01-05 03:29:18 +00:00
);
});
}
2022-12-31 05:39:39 +00:00
_webFingerRequestHandler(req, resp) {
const url = this.webServer.fullUrl(req);
const resource = url.searchParams.get('resource');
if (!resource) {
2023-01-03 05:25:32 +00:00
return this.webServer.respondWithError(
resp,
400,
'"resource" is required',
'Missing "resource"'
);
}
const accountName = this._getAccountName(resource);
if (!accountName || accountName.length < 1) {
this.log.warn(`Failed to parse "account name" for resource: ${resource}`);
return this.webServer.resourceNotFound(resp);
}
this._localUserFromWebFingerAccountName(accountName, (err, localUser) => {
if (err) {
2023-01-07 01:55:24 +00:00
this.log.warn(
{ url: req.url, error: err.message, type: 'WebFinger' },
`No account for "${accountName}" could be retrieved`
);
2023-01-08 08:22:02 +00:00
return this.webServer.resourceNotFound(resp);
}
2023-01-03 05:25:32 +00:00
const domain = this.webServer.getDomain();
2022-12-31 22:30:54 +00:00
const body = JSON.stringify({
subject: `acct:${localUser.username}@${domain}`,
aliases: [this._profileUrl(localUser), this._userActorId(localUser)],
2022-12-31 22:30:54 +00:00
links: [
this._profilePageLink(localUser),
this._selfLink(localUser),
2022-12-31 22:30:54 +00:00
this._subscribeLink(),
],
});
const headers = {
'Content-Type': 'application/jrd+json',
'Content-Length': body.length,
};
resp.writeHead(200, headers);
return resp.end(body);
});
}
_localUserFromWebFingerAccountName(accountName, cb) {
if (accountName.startsWith('@')) {
accountName = accountName.slice(1);
}
User.getUserIdAndName(accountName, (err, userId) => {
if (err) {
return cb(err);
}
User.getUser(userId, (err, user) => {
if (err) {
return cb(err);
}
const accountStatus = user.getPropertyAsNumber(UserProps.AccountStatus);
if (
User.AccountStatus.disabled == accountStatus ||
User.AccountStatus.inactive == accountStatus
) {
return cb(
Errors.AccessDenied('Account disabled', ErrorReasons.Disabled)
);
}
const activityPubSettings = ActivityPubSettings.fromUser(user);
if (!activityPubSettings.enabled) {
return cb(Errors.AccessDenied('ActivityPub is not enabled for user'));
}
return cb(null, user);
});
});
}
2022-12-31 19:05:59 +00:00
_profileUrl(user) {
return webFingerProfileUrl(this.webServer, user);
2022-12-31 19:05:59 +00:00
}
_profilePageLink(user) {
2022-12-31 19:05:59 +00:00
const href = this._profileUrl(user);
return {
rel: 'http://webfinger.net/rel/profile-page',
type: 'text/plain',
href,
};
}
_userActorId(user) {
return localActorId(this.webServer, user);
2022-12-31 19:05:59 +00:00
}
2022-12-31 22:30:54 +00:00
// :TODO: only if ActivityPub is enabled
2022-12-31 19:05:59 +00:00
_selfLink(user) {
const href = this._userActorId(user);
2022-12-31 19:05:59 +00:00
return {
rel: 'self',
type: 'application/activity+json',
href,
};
}
2022-12-31 22:30:54 +00:00
// :TODO: only if ActivityPub is enabled
2022-12-31 19:05:59 +00:00
_subscribeLink() {
return {
rel: 'http://ostatus.org/schema/1.0/subscribe',
2023-01-03 05:25:32 +00:00
template: this.webServer.buildUrl(
2022-12-31 22:30:54 +00:00
WellKnownLocations.Internal + '/ap/authorize_interaction?uri={uri}'
),
2022-12-31 19:05:59 +00:00
};
}
2022-12-31 22:30:54 +00:00
_getAccountName(resource) {
for (const re of this.acceptedResourceRegExps) {
const m = resource.match(re);
if (m && m.length === 2) {
return m[1];
}
}
}
2022-12-31 05:39:39 +00:00
};