Updates to WebFinger resource parsing

This commit is contained in:
Bryan Ashby 2022-12-31 15:30:54 -07:00
parent 3db35bc5b6
commit 7b5cb165ee
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
5 changed files with 99 additions and 35 deletions

View File

@ -28,9 +28,10 @@ function getServer(packageName) {
function startListening(cb) { function startListening(cb) {
const moduleUtil = require('./module_util.js'); // late load so we get Config const moduleUtil = require('./module_util.js'); // late load so we get Config
const cats = moduleUtil.moduleCategories;
async.each( async.each(
['login', 'content', 'chat'], [cats.Login, cats.Content, cats.Chat],
(category, next) => { (category, next) => {
moduleUtil.loadModulesForCategory( moduleUtil.loadModulesForCategory(
`${category}Servers`, `${category}Servers`,

View File

@ -21,6 +21,14 @@ exports.loadModulesForCategory = loadModulesForCategory;
exports.getModulePaths = getModulePaths; exports.getModulePaths = getModulePaths;
exports.initializeModules = initializeModules; exports.initializeModules = initializeModules;
exports.moduleCategories = {
Login: 'login',
Content: 'content',
Chat: 'chat',
ScannerTossers: 'scannerTossers',
WebHandlers: 'webHandlers',
};
function loadModuleEx(options, cb) { function loadModuleEx(options, cb) {
assert(_.isObject(options)); assert(_.isObject(options));
assert(_.isString(options.name)); assert(_.isString(options.name));

View File

@ -2,7 +2,7 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const loadModulesForCategory = require('./module_util.js').loadModulesForCategory; const { loadModulesForCategory, moduleCategories } = require('./module_util');
// standard/deps // standard/deps
const async = require('async'); const async = require('async');
@ -18,7 +18,7 @@ function startup(cb) {
[ [
function loadModules(callback) { function loadModules(callback) {
loadModulesForCategory( loadModulesForCategory(
'scannerTossers', moduleCategories.ScannerTossers,
(module, nextModule) => { (module, nextModule) => {
const modInst = new module.getModule(); const modInst = new module.getModule();

View File

@ -17,7 +17,7 @@ const mimeTypes = require('mime-types');
const forEachSeries = require('async/forEachSeries'); const forEachSeries = require('async/forEachSeries');
const findSeries = require('async/findSeries'); const findSeries = require('async/findSeries');
const { loadModulesForCategory } = require('../../module_util'); const { loadModulesForCategory, moduleCategories } = require('../../module_util');
const ModuleInfo = (exports.moduleInfo = { const ModuleInfo = (exports.moduleInfo = {
name: 'Web', name: 'Web',
@ -26,6 +26,11 @@ const ModuleInfo = (exports.moduleInfo = {
packageName: 'codes.l33t.enigma.web.server', packageName: 'codes.l33t.enigma.web.server',
}); });
exports.WellKnownLocations = {
Rfc5785: '/.well-known',
Internal: '/_enig',
};
class Route { class Route {
constructor(route) { constructor(route) {
Object.assign(this, route); Object.assign(this, route);
@ -79,6 +84,17 @@ exports.getModule = class WebServerModule extends ServerModule {
this.routes = {}; this.routes = {};
} }
getDomain() {
const config = Config();
const overridePrefix = _.get(config.contentServers.web.overrideUrlPrefix);
if (_.isString(overridePrefix)) {
const url = new URL(overridePrefix);
return url.hostname;
}
return config.contentServers.web.domain;
}
buildUrl(pathAndQuery) { buildUrl(pathAndQuery) {
// //
// Create a URL such as // Create a URL such as
@ -146,7 +162,7 @@ exports.getModule = class WebServerModule extends ServerModule {
} }
loadModulesForCategory( loadModulesForCategory(
'webHandlers', moduleCategories.WebHandlers,
(module, nextModule) => { (module, nextModule) => {
const moduleInst = new module.getModule(); const moduleInst = new module.getModule();
try { try {

View File

@ -3,9 +3,11 @@ const Config = require('../../../config').get;
const { Errors } = require('../../../enig_error'); const { Errors } = require('../../../enig_error');
const WebServerPackageName = require('../web').moduleInfo.packageName; const WebServerPackageName = require('../web').moduleInfo.packageName;
const { WellKnownLocations } = require('../web');
const _ = require('lodash'); const _ = require('lodash');
const User = require('../../../user'); const User = require('../../../user');
const EnigAssert = require('../../../enigma_assert');
const Log = require('../../../logger').log; const Log = require('../../../logger').log;
exports.moduleInfo = { exports.moduleInfo = {
@ -21,7 +23,9 @@ exports.getModule = class WebFingerServerModule extends ServerModule {
} }
init(cb) { init(cb) {
if (!_.get(Config(), 'contentServers.web.handlers.webFinger.enabled')) { const config = Config();
if (!_.get(config, 'contentServers.web.handlers.webFinger.enabled')) {
return cb(null); return cb(null);
} }
@ -29,12 +33,30 @@ exports.getModule = class WebFingerServerModule extends ServerModule {
// we rely on the web server // we rely on the web server
this.webServer = getServer(WebServerPackageName); this.webServer = getServer(WebServerPackageName);
if (!this.webServer || !this.webServer.instance.isEnabled()) { const ws = this._webServer();
if (!ws || !ws.isEnabled()) {
return cb(Errors.UnexpectedState('Cannot access web server!')); return cb(Errors.UnexpectedState('Cannot access web server!'));
} }
this.webServer.instance.addRoute({ const domain = ws.getDomain();
if (!domain) {
return cb(Errors.UnexpectedState('Web server does not have "domain" set'));
}
this.acceptedResourceRegExps = [
// acct:NAME@our.domain.tld
new RegExp(`^acct:(.+)@${domain}$`),
// profile URL
new RegExp(`^${ws.buildUrl(WellKnownLocations.Internal + '/wf/@')}(.+)$`),
// self URL
new RegExp(
`^${ws.buildUrl(WellKnownLocations.Internal + '/ap/users/')}(.+)$`
),
];
ws.addRoute({
method: 'GET', method: 'GET',
// https://www.rfc-editor.org/rfc/rfc7033.html#section-10.1
path: /^\/\.well-known\/webfinger\/?\?/, path: /^\/\.well-known\/webfinger\/?\?/,
handler: this._webFingerRequestHandler.bind(this), handler: this._webFingerRequestHandler.bind(this),
}); });
@ -42,12 +64,16 @@ exports.getModule = class WebFingerServerModule extends ServerModule {
return cb(null); return cb(null);
} }
_webServer() {
return this.webServer.instance;
}
_webFingerRequestHandler(req, resp) { _webFingerRequestHandler(req, resp) {
const url = new URL(req.url, `https://${req.headers.host}`); const url = new URL(req.url, `https://${req.headers.host}`);
const resource = url.searchParams.get('resource'); const resource = url.searchParams.get('resource');
if (!resource) { if (!resource) {
return this.webServer.instance.respondWithError( return this._webServer().respondWithError(
resp, resp,
400, 400,
'"resource" is required', '"resource" is required',
@ -55,16 +81,22 @@ exports.getModule = class WebFingerServerModule extends ServerModule {
); );
} }
this._getUser(resource, resp, (err, user, accountName) => { this._getUser(resource, resp, (err, user) => {
if (err) { if (err) {
// |resp| already written to // |resp| already written to
return Log.warn({ error: err.message }, `WebFinger failed: ${req.url}`); return Log.warn({ error: err.message }, `WebFinger failed: ${req.url}`);
} }
const domain = this._webServer().getDomain();
const body = JSON.stringify({ const body = JSON.stringify({
subject: `acct:${accountName}`, subject: `acct:${user.username}@${domain}`,
aliases: [this._profileUrl(user), this._selfUrl(user)], aliases: [this._profileUrl(user), this._selfUrl(user)],
links: [this._profilePageLink(user), this._selfLink(user), this._subscribeLink()], links: [
this._profilePageLink(user),
this._selfLink(user),
this._subscribeLink(),
],
}); });
const headers = { const headers = {
@ -78,7 +110,9 @@ exports.getModule = class WebFingerServerModule extends ServerModule {
} }
_profileUrl(user) { _profileUrl(user) {
return this.webServer.instance.buildUrl(`/wf/@${user.username}`); return this._webServer().buildUrl(
WellKnownLocations.Internal + `/wf/@${user.username}`
);
} }
_profilePageLink(user) { _profilePageLink(user) {
@ -91,9 +125,12 @@ exports.getModule = class WebFingerServerModule extends ServerModule {
} }
_selfUrl(user) { _selfUrl(user) {
return this.webServer.instance.buildUrl(`/users/${user.username}`); return this._webServer().buildUrl(
WellKnownLocations.Internal + `/ap/users/${user.username}`
);
} }
// :TODO: only if ActivityPub is enabled
_selfLink(user) { _selfLink(user) {
const href = this._selfUrl(user); const href = this._selfUrl(user);
return { return {
@ -103,18 +140,28 @@ exports.getModule = class WebFingerServerModule extends ServerModule {
}; };
} }
// :TODO: only if ActivityPub is enabled
_subscribeLink() { _subscribeLink() {
return { return {
rel: 'http://ostatus.org/schema/1.0/subscribe', rel: 'http://ostatus.org/schema/1.0/subscribe',
template: this.webServer.instance.buildUrl('/authorize_interaction?uri={uri}'), template: this._webServer().buildUrl(
WellKnownLocations.Internal + '/ap/authorize_interaction?uri={uri}'
),
}; };
} }
_getUser(resource, resp, cb) { _getAccountName(resource) {
// we only handle "acct:NAME" URIs for (const re of this.acceptedResourceRegExps) {
const m = resource.match(re);
if (m && m.length === 2) {
return m[1];
}
}
}
_getUser(resource, resp, cb) {
const notFound = () => { const notFound = () => {
this.webServer.instance.respondWithError( this._webServer().respondWithError(
resp, resp,
404, 404,
'Resource not found', 'Resource not found',
@ -122,25 +169,17 @@ exports.getModule = class WebFingerServerModule extends ServerModule {
); );
}; };
const acctIndex = resource.indexOf('acct:', 0); const accountName = this._getAccountName(resource);
if (0 != acctIndex) { if (!accountName || accountName.length < 1) {
notFound(); notFound();
return cb(Errors.DoesNotExist('"acct:" missing')); return cb(
Errors.DoesNotExist(
`Failed to parse "account name" for resource: ${resource}`
)
);
} }
const accountName = resource.substring(acctIndex + 5); User.getUserIdAndName(accountName, (err, userId) => {
const domain = _.get(Config(), 'contentServers.web.domain', 'localhost');
if (!accountName.endsWith(`@${domain}`)) {
notFound();
return cb(Errors.Invalid(`Invalid "acct" value: ${accountName}`));
}
const searchQuery = accountName.substring(
0,
accountName.length - (domain.length + 1)
);
User.getUserIdAndName(searchQuery, (err, userId) => {
if (err) { if (err) {
notFound(); notFound();
return cb(err); return cb(err);
@ -152,7 +191,7 @@ exports.getModule = class WebFingerServerModule extends ServerModule {
return cb(err); return cb(err);
} }
return cb(null, user, accountName); return cb(null, user);
}); });
}); });
} }