diff --git a/CHANGELOG.md b/CHANGELOG.md index fdb9e55..8f9b6ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +### Added + +* new `baseUrl` config option that allows you to specify server origin instead of `domain` which specifies the host but assumes https protocol + ## v4.1.2 (2023-01-06) ### Changed diff --git a/README.md b/README.md index e1e93e3..0c593df 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ Option | Description **Required** | name | String. Name of your app to list in nodeinfo version | String. Version of your app to list in nodeinfo +baseUrl | String. Server URL origin. Also used as URI prefix. Should be the public-facing URL when using a reverse proxy. Overrides `domain` if set domain | String. Hostname for your app actorParam | String. Express route parameter used for actor name objectParam | String. Express route parameter used for object id diff --git a/index.js b/index.js index a81c744..089ad45 100644 --- a/index.js +++ b/index.js @@ -24,7 +24,15 @@ module.exports = function (settings) { } } apex.settings = settings - apex.domain = settings.domain + if (settings.baseUrl !== undefined) { + apex.baseUrl = settings.baseUrl + const url = new URL(apex.baseUrl) + apex.domain = url.host + } else { + // Assumes settings.domain is set (backward-compatible) + apex.baseUrl = `https://${settings.domain}` + apex.domain = settings.domain + } apex.context = settings.context ? pub.consts.ASContext.concat(settings.context) : pub.consts.ASContext @@ -42,16 +50,16 @@ module.exports = function (settings) { apex.offlineMode = settings.offlineMode apex.requestTimeout = settings.requestTimeout ?? 5000 apex.utils = { - usernameToIRI: apex.idToIRIFactory(apex.domain, settings.routes.actor, apex.actorParam), - objectIdToIRI: apex.idToIRIFactory(apex.domain, settings.routes.object, apex.objectParam), - activityIdToIRI: apex.idToIRIFactory(apex.domain, settings.routes.activity, apex.activityParam), - userCollectionIdToIRI: apex.userAndIdToIRIFactory(apex.domain, settings.routes.collections, apex.actorParam, apex.collectionParam), - nameToActorStreams: apex.nameToActorStreamsFactory(apex.domain, settings.routes, apex.actorParam), - nameToBlockedIRI: apex.idToIRIFactory(apex.domain, settings.routes.blocked, apex.actorParam), - nameToRejectedIRI: apex.idToIRIFactory(apex.domain, settings.routes.rejected, apex.actorParam), - nameToRejectionsIRI: apex.idToIRIFactory(apex.domain, settings.routes.rejections, apex.actorParam), - idToActivityCollections: apex.idToActivityCollectionsFactory(apex.domain, settings.routes, apex.activityParam), - iriToCollectionInfo: apex.iriToCollectionInfoFactory(apex.domain, settings.routes, apex.actorParam, apex.activityParam, apex.collectionParam) + usernameToIRI: apex.idToIRIFactory(apex.baseUrl, settings.routes.actor, apex.actorParam), + objectIdToIRI: apex.idToIRIFactory(apex.baseUrl, settings.routes.object, apex.objectParam), + activityIdToIRI: apex.idToIRIFactory(apex.baseUrl, settings.routes.activity, apex.activityParam), + userCollectionIdToIRI: apex.userAndIdToIRIFactory(apex.baseUrl, settings.routes.collections, apex.actorParam, apex.collectionParam), + nameToActorStreams: apex.nameToActorStreamsFactory(apex.baseUrl, settings.routes, apex.actorParam), + nameToBlockedIRI: apex.idToIRIFactory(apex.baseUrl, settings.routes.blocked, apex.actorParam), + nameToRejectedIRI: apex.idToIRIFactory(apex.baseUrl, settings.routes.rejected, apex.actorParam), + nameToRejectionsIRI: apex.idToIRIFactory(apex.baseUrl, settings.routes.rejections, apex.actorParam), + idToActivityCollections: apex.idToActivityCollectionsFactory(apex.baseUrl, settings.routes, apex.activityParam), + iriToCollectionInfo: apex.iriToCollectionInfoFactory(apex.baseUrl, settings.routes, apex.actorParam, apex.activityParam, apex.collectionParam) } function onFinishedHandler (err, res) { diff --git a/net/well-known.js b/net/well-known.js index b24e18c..3059ec5 100644 --- a/net/well-known.js +++ b/net/well-known.js @@ -28,11 +28,11 @@ function respondNodeInfoLocation (req, res, next) { links: [ { rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', - href: `https://${apex.domain}/nodeinfo/2.1` + href: `${apex.baseUrl}/nodeinfo/2.1` }, { rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', - href: `https://${apex.domain}/nodeinfo/2.0` + href: `${apex.baseUrl}/nodeinfo/2.0` } ] }) diff --git a/pub/utils.js b/pub/utils.js index 5fd5dfe..7dcbd99 100644 --- a/pub/utils.js +++ b/pub/utils.js @@ -91,7 +91,7 @@ function actorIdFromActivity (activity) { return actor.id } -function iriToCollectionInfoFactory (domain, routes, pActor, pActivity, pCollection) { +function iriToCollectionInfoFactory (baseUrl, routes, pActor, pActivity, pCollection) { pActor = `:${pActor}` pActivity = `:${pActivity}` pCollection = `:${pCollection}` @@ -100,7 +100,7 @@ function iriToCollectionInfoFactory (domain, routes, pActor, pActivity, pCollect let pattern = this.settings.routes.collections const isActorFirst = pattern.indexOf(pActor) < pattern.indexOf(pCollection) pattern = pattern.replace(pActor, '([^/]+)').replace(pCollection, '([^/]+)') - const re = new RegExp(`^https://${this.domain}${pattern}$`) + const re = new RegExp(`^${baseUrl}${pattern}$`) tests.push(iri => { const match = re.exec(iri)?.slice(1) return match && { name: 'collections', actor: match[+!isActorFirst], id: match[+isActorFirst] } @@ -108,7 +108,7 @@ function iriToCollectionInfoFactory (domain, routes, pActor, pActivity, pCollect // standard actor streams actorStreamNames.forEach(name => { const pattern = this.settings.routes[name].replace(pActor, '([^/]+)') - const re = new RegExp(`^https://${this.domain}${pattern}$`) + const re = new RegExp(`^${baseUrl}${pattern}$`) tests.push(iri => { const actor = re.exec(iri)?.[1] return actor && { name, actor } @@ -117,7 +117,7 @@ function iriToCollectionInfoFactory (domain, routes, pActor, pActivity, pCollect // activity object streams activityStreamNames.forEach(name => { const pattern = this.settings.routes[name].replace(pActivity, '([^/]+)') - const re = new RegExp(`^https://${this.domain}${pattern}$`) + const re = new RegExp(`^${baseUrl}${pattern}$`) tests.push(iri => { const activity = re.exec(iri)?.[1] return activity && { name, activity } @@ -201,24 +201,24 @@ function stringifyPublicJSONLD (obj) { return JSON.stringify(obj, skipPrivate) } -function idToIRIFactory (domain, route, param) { +function idToIRIFactory (baseUrl, route, param) { const colonParam = `:${param}` return id => { if (!id) { id = this.store.generateId() } - return `https://${domain}${route.replace(colonParam, id)}`.toLowerCase() + return `${baseUrl}${route.replace(colonParam, id)}`.toLowerCase() } } -function userAndIdToIRIFactory (domain, route, userParam, param) { +function userAndIdToIRIFactory (baseUrl, route, userParam, param) { param = `:${param}` userParam = `:${userParam}` return (user, id) => { if (!id) { id = this.store.generateId() } - return `https://${domain}${route.replace(param, id).replace(userParam, user)}`.toLowerCase() + return `${baseUrl}${route.replace(param, id).replace(userParam, user)}`.toLowerCase() } } @@ -229,7 +229,7 @@ function isLocalCollection (object) { } function isLocalIRI (id) { - return id.startsWith(`https://${this.domain}`) + return id.startsWith(this.baseUrl) } function isPublic (object) { @@ -267,11 +267,11 @@ function mergeJSONLD (target, source) { return merge(target, source, overwriteArrays) } -function nameToActorStreamsFactory (domain, routes, actorParam) { +function nameToActorStreamsFactory (baseUrl, routes, actorParam) { const colonParam = `:${actorParam}` const streamTemplates = {} actorStreamNames.forEach(s => { - streamTemplates[s] = `https://${domain}${routes[s]}` + streamTemplates[s] = `${baseUrl}${routes[s]}` }) return name => { const streams = {} @@ -282,11 +282,11 @@ function nameToActorStreamsFactory (domain, routes, actorParam) { } } -function idToActivityCollectionsFactory (domain, routes, activityParam) { +function idToActivityCollectionsFactory (baseUrl, routes, activityParam) { const colonParam = `:${activityParam}` const streamTemplates = {} activityStreamNames.forEach(s => { - streamTemplates[s] = `https://${domain}${routes[s]}` + streamTemplates[s] = `${baseUrl}${routes[s]}` }) return id => { const streams = {} diff --git a/spec/unit/apex.spec.js b/spec/unit/apex.spec.js new file mode 100644 index 0000000..aaa8541 --- /dev/null +++ b/spec/unit/apex.spec.js @@ -0,0 +1,51 @@ +/* global describe, it, expect */ + +const ActivitypubExpress = require('../../index') + +const routes = { + actor: '/u/:actor', + object: '/o/:id', + activity: '/s/:id', + inbox: '/inbox/:actor', + outbox: '/outbox/:actor', + followers: '/followers/:actor', + following: '/following/:actor', + liked: '/liked/:actor', + shares: '/s/:id/shares', + likes: '/s/:id/likes', + collections: '/u/:actor/c/:id', + blocked: '/u/:actor/blocked', + rejections: '/u/:actor/rejections', + rejected: '/u/:actor/rejected', + nodeinfo: '/nodeinfo' +} + +describe('apex', function () { + it('should use base URL if set and no domain', function () { + const apex = ActivitypubExpress({ + baseUrl: 'https://localhost', + routes + }) + expect(apex.domain).toBe('localhost') + expect(apex.baseUrl).toBe('https://localhost') + }) + + it('should use domain if set and no base URL', function () { + const apex = ActivitypubExpress({ + domain: 'somedomain:4321', + routes + }) + expect(apex.domain).toBe('somedomain:4321') + expect(apex.baseUrl).toBe('https://somedomain:4321') + }) + + it('should prefer baseURL if domain is also set', function () { + const apex = ActivitypubExpress({ + domain: 'somedomain', + baseUrl: 'https://someotherdomain:9876', + routes + }) + expect(apex.domain).toBe('someotherdomain:9876') + expect(apex.baseUrl).toBe('https://someotherdomain:9876') + }) +})