From 7b5cb165ee070474668ff64149a224b3c31d7296 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 31 Dec 2022 15:30:54 -0700 Subject: [PATCH] Updates to WebFinger resource parsing --- core/listening_server.js | 3 +- core/module_util.js | 8 ++ core/msg_network.js | 4 +- core/servers/content/web.js | 20 +++- .../servers/content/web_handlers/webfinger.js | 99 +++++++++++++------ 5 files changed, 99 insertions(+), 35 deletions(-) diff --git a/core/listening_server.js b/core/listening_server.js index 494446bc..a840be69 100644 --- a/core/listening_server.js +++ b/core/listening_server.js @@ -28,9 +28,10 @@ function getServer(packageName) { function startListening(cb) { const moduleUtil = require('./module_util.js'); // late load so we get Config + const cats = moduleUtil.moduleCategories; async.each( - ['login', 'content', 'chat'], + [cats.Login, cats.Content, cats.Chat], (category, next) => { moduleUtil.loadModulesForCategory( `${category}Servers`, diff --git a/core/module_util.js b/core/module_util.js index 82453b1e..5b2698a6 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -21,6 +21,14 @@ exports.loadModulesForCategory = loadModulesForCategory; exports.getModulePaths = getModulePaths; exports.initializeModules = initializeModules; +exports.moduleCategories = { + Login: 'login', + Content: 'content', + Chat: 'chat', + ScannerTossers: 'scannerTossers', + WebHandlers: 'webHandlers', +}; + function loadModuleEx(options, cb) { assert(_.isObject(options)); assert(_.isString(options.name)); diff --git a/core/msg_network.js b/core/msg_network.js index e9df2e1e..0f34b19a 100644 --- a/core/msg_network.js +++ b/core/msg_network.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const loadModulesForCategory = require('./module_util.js').loadModulesForCategory; +const { loadModulesForCategory, moduleCategories } = require('./module_util'); // standard/deps const async = require('async'); @@ -18,7 +18,7 @@ function startup(cb) { [ function loadModules(callback) { loadModulesForCategory( - 'scannerTossers', + moduleCategories.ScannerTossers, (module, nextModule) => { const modInst = new module.getModule(); diff --git a/core/servers/content/web.js b/core/servers/content/web.js index db8a1fcb..d3c56638 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -17,7 +17,7 @@ const mimeTypes = require('mime-types'); const forEachSeries = require('async/forEachSeries'); const findSeries = require('async/findSeries'); -const { loadModulesForCategory } = require('../../module_util'); +const { loadModulesForCategory, moduleCategories } = require('../../module_util'); const ModuleInfo = (exports.moduleInfo = { name: 'Web', @@ -26,6 +26,11 @@ const ModuleInfo = (exports.moduleInfo = { packageName: 'codes.l33t.enigma.web.server', }); +exports.WellKnownLocations = { + Rfc5785: '/.well-known', + Internal: '/_enig', +}; + class Route { constructor(route) { Object.assign(this, route); @@ -79,6 +84,17 @@ exports.getModule = class WebServerModule extends ServerModule { 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) { // // Create a URL such as @@ -146,7 +162,7 @@ exports.getModule = class WebServerModule extends ServerModule { } loadModulesForCategory( - 'webHandlers', + moduleCategories.WebHandlers, (module, nextModule) => { const moduleInst = new module.getModule(); try { diff --git a/core/servers/content/web_handlers/webfinger.js b/core/servers/content/web_handlers/webfinger.js index 120c85f0..a5f99060 100644 --- a/core/servers/content/web_handlers/webfinger.js +++ b/core/servers/content/web_handlers/webfinger.js @@ -3,9 +3,11 @@ const Config = require('../../../config').get; const { Errors } = require('../../../enig_error'); const WebServerPackageName = require('../web').moduleInfo.packageName; +const { WellKnownLocations } = require('../web'); const _ = require('lodash'); const User = require('../../../user'); +const EnigAssert = require('../../../enigma_assert'); const Log = require('../../../logger').log; exports.moduleInfo = { @@ -21,7 +23,9 @@ exports.getModule = class WebFingerServerModule extends ServerModule { } init(cb) { - if (!_.get(Config(), 'contentServers.web.handlers.webFinger.enabled')) { + const config = Config(); + + if (!_.get(config, 'contentServers.web.handlers.webFinger.enabled')) { return cb(null); } @@ -29,12 +33,30 @@ exports.getModule = class WebFingerServerModule extends ServerModule { // we rely on the web server 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!')); } - 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', + // https://www.rfc-editor.org/rfc/rfc7033.html#section-10.1 path: /^\/\.well-known\/webfinger\/?\?/, handler: this._webFingerRequestHandler.bind(this), }); @@ -42,12 +64,16 @@ exports.getModule = class WebFingerServerModule extends ServerModule { return cb(null); } + _webServer() { + return this.webServer.instance; + } + _webFingerRequestHandler(req, resp) { const url = new URL(req.url, `https://${req.headers.host}`); const resource = url.searchParams.get('resource'); if (!resource) { - return this.webServer.instance.respondWithError( + return this._webServer().respondWithError( resp, 400, '"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) { // |resp| already written to return Log.warn({ error: err.message }, `WebFinger failed: ${req.url}`); } + const domain = this._webServer().getDomain(); + const body = JSON.stringify({ - subject: `acct:${accountName}`, + subject: `acct:${user.username}@${domain}`, 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 = { @@ -78,7 +110,9 @@ exports.getModule = class WebFingerServerModule extends ServerModule { } _profileUrl(user) { - return this.webServer.instance.buildUrl(`/wf/@${user.username}`); + return this._webServer().buildUrl( + WellKnownLocations.Internal + `/wf/@${user.username}` + ); } _profilePageLink(user) { @@ -91,9 +125,12 @@ exports.getModule = class WebFingerServerModule extends ServerModule { } _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) { const href = this._selfUrl(user); return { @@ -103,18 +140,28 @@ exports.getModule = class WebFingerServerModule extends ServerModule { }; } + // :TODO: only if ActivityPub is enabled _subscribeLink() { return { 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) { - // we only handle "acct:NAME" URIs + _getAccountName(resource) { + 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 = () => { - this.webServer.instance.respondWithError( + this._webServer().respondWithError( resp, 404, 'Resource not found', @@ -122,25 +169,17 @@ exports.getModule = class WebFingerServerModule extends ServerModule { ); }; - const acctIndex = resource.indexOf('acct:', 0); - if (0 != acctIndex) { + const accountName = this._getAccountName(resource); + if (!accountName || accountName.length < 1) { 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); - 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) => { + User.getUserIdAndName(accountName, (err, userId) => { if (err) { notFound(); return cb(err); @@ -152,7 +191,7 @@ exports.getModule = class WebFingerServerModule extends ServerModule { return cb(err); } - return cb(null, user, accountName); + return cb(null, user); }); }); }