fix(api): unfurl links to a self-hosted instance's own domain (#2891)

This commit is contained in:
Hampus
2026-09-22 02:00:51 +02:00
committed by GitHub
parent 31c53d2dff
commit 1ab7e7dfcc
4 changed files with 40 additions and 36 deletions
-2
View File
@@ -272,8 +272,6 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
donationProxyKey,
},
hosts: {
invite: extractHostname(master.endpoints.invite),
gift: extractHostname(master.endpoints.gift),
marketing: extractHostname(master.endpoints.marketing),
unfurlIgnored: master.services.api.unfurl_ignored_hosts,
},
-2
View File
@@ -143,8 +143,6 @@ export interface APIConfig {
donationProxyKey: string;
};
hosts: {
invite: string;
gift: string;
marketing: string;
unfurlIgnored: Array<string>;
};
+6 -1
View File
@@ -5,13 +5,18 @@ import * as RegexUtils from '@app/api/utils/RegexUtils';
let _invitePattern: RegExp | null = null;
function getInviteEndpointBase(): string {
const url = new URL(Config.endpoints.invite);
return `${url.hostname}${url.pathname.replace(/\/+$/, '')}`;
}
function getInvitePattern(): RegExp {
if (!_invitePattern) {
_invitePattern = new RegExp(
[
'(?:https?:\\/\\/)?',
'(?:',
`${RegexUtils.escapeRegex(Config.hosts.invite)}(?:\\/#)?\\/(?!invite\\/)([a-zA-Z0-9\\-]{2,32})(?![a-zA-Z0-9\\-])`,
`${RegexUtils.escapeRegex(getInviteEndpointBase())}(?:\\/#)?\\/(?!invite\\/)([a-zA-Z0-9\\-]{2,32})(?![a-zA-Z0-9\\-])`,
'|',
`${RegexUtils.escapeRegex(new URL(Config.endpoints.webApp).hostname)}(?:\\/#)?\\/invite\\/([a-zA-Z0-9\\-]{2,32})(?![a-zA-Z0-9\\-])`,
')',
+34 -31
View File
@@ -8,24 +8,17 @@ import * as InviteUtils from '@app/api/utils/InviteUtils';
import {URL_REGEX} from '@fluxer/constants/src/Core';
import * as idna from 'idna-uts46-hx';
const MARKETING_PATH_PREFIXES = ['/channels/', '/theme/'];
const CLIENT_ROUTE_PATH_PREFIXES = ['/channels/', '/theme/'];
interface ExcludedLinkBase {
hostname: string;
pathPrefix: string;
}
function normalizeHostname(hostname: string | undefined) {
return hostname?.trim().toLowerCase() || '';
}
let _marketingHostname: string | null = null;
function getMarketingHostname() {
if (!_marketingHostname) {
_marketingHostname = normalizeHostname(Config.hosts.marketing);
}
return _marketingHostname;
}
const isMarketingPath = (hostname: string, pathname: string) =>
hostname === getMarketingHostname() && MARKETING_PATH_PREFIXES.some((prefix) => pathname.startsWith(prefix));
function getWebAppHostname() {
try {
return new URL(Config.endpoints.webApp).hostname;
@@ -34,23 +27,32 @@ function getWebAppHostname() {
}
}
let _excludedHostnames: Set<string> | null = null;
function getExcludedHostnames(): Set<string> {
if (!_excludedHostnames) {
_excludedHostnames = new Set<string>();
const addHostname = (hostname: string | undefined) => {
const normalized = normalizeHostname(hostname);
if (normalized) {
_excludedHostnames!.add(normalized);
}
};
addHostname(Config.hosts.invite);
addHostname(Config.hosts.gift);
Config.hosts.unfurlIgnored.forEach(addHostname);
addHostname(getWebAppHostname());
function endpointLinkBase(endpoint: string): ExcludedLinkBase | null {
try {
const url = new URL(endpoint);
return {hostname: normalizeHostname(url.hostname), pathPrefix: `${url.pathname.replace(/\/+$/, '')}/`};
} catch {
return null;
}
return _excludedHostnames;
}
let _excludedLinkBases: Array<ExcludedLinkBase> | null = null;
function getExcludedLinkBases(): Array<ExcludedLinkBase> {
if (!_excludedLinkBases) {
const bases: Array<ExcludedLinkBase | null> = [
...Config.hosts.unfurlIgnored.map((hostname) => ({hostname: normalizeHostname(hostname), pathPrefix: '/'})),
endpointLinkBase(Config.endpoints.invite),
endpointLinkBase(Config.endpoints.gift),
];
for (const hostname of [getWebAppHostname(), Config.hosts.marketing]) {
for (const pathPrefix of CLIENT_ROUTE_PATH_PREFIXES) {
bases.push({hostname: normalizeHostname(hostname), pathPrefix});
}
}
_excludedLinkBases = bases.filter((base): base is ExcludedLinkBase => base !== null && base.hostname !== '');
}
return _excludedLinkBases;
}
function idnaEncodeURL(url: string) {
@@ -80,8 +82,9 @@ function isFluxerAppExcludedURL(url: string) {
try {
const parsedUrl = new URL(url);
const hostname = normalizeHostname(parsedUrl.hostname);
const isMarketingPathMatch = isMarketingPath(hostname, parsedUrl.pathname);
return isMarketingPathMatch || getExcludedHostnames().has(hostname);
return getExcludedLinkBases().some(
(base) => base.hostname === hostname && parsedUrl.pathname.startsWith(base.pathPrefix),
);
} catch {
return false;
}