Browse Source

Add proxy route to FetchService

pull/7153/head
Alexander Linder 3 weeks ago
parent
commit
4d04117b25
Failed to extract signature
  1. 49
      apps/api/src/services/fetch/fetch.service.ts
  2. 22
      apps/api/src/services/fetch/interfaces/proxy-route.interface.ts
  3. 1
      libs/common/src/lib/config.ts

49
apps/api/src/services/fetch/fetch.service.ts

@ -4,6 +4,7 @@ import {
PROPERTY_API_KEY_OPENROUTER, PROPERTY_API_KEY_OPENROUTER,
PROPERTY_OPENROUTER_MODEL, PROPERTY_OPENROUTER_MODEL,
PROPERTY_OPENROUTER_MODEL_WEB_FETCH, PROPERTY_OPENROUTER_MODEL_WEB_FETCH,
PROPERTY_PROXY_ROUTES,
PROPERTY_WEB_FETCH_ROUTES PROPERTY_WEB_FETCH_ROUTES
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
@ -12,6 +13,7 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { generateText, jsonSchema, tool } from 'ai'; import { generateText, jsonSchema, tool } from 'ai';
import ms from 'ms'; import ms from 'ms';
import { ProxyRoute } from './interfaces/proxy-route.interface';
import { WebFetchRoute } from './interfaces/web-fetch-route.interface'; import { WebFetchRoute } from './interfaces/web-fetch-route.interface';
@Injectable() @Injectable()
@ -21,11 +23,17 @@ export class FetchService implements OnModuleInit {
private static readonly REDACTED_QUERY_PARAM_NAMES = ['apikey', 'api_token']; private static readonly REDACTED_QUERY_PARAM_NAMES = ['apikey', 'api_token'];
private static readonly WEB_FETCH_TIMEOUT = ms('30 seconds'); private static readonly WEB_FETCH_TIMEOUT = ms('30 seconds');
private proxyRoutes: ProxyRoute[] = [];
private webFetchRoutes: WebFetchRoute[] = []; private webFetchRoutes: WebFetchRoute[] = [];
public constructor(private readonly propertyService: PropertyService) {} public constructor(private readonly propertyService: PropertyService) {}
public async onModuleInit() { public async onModuleInit() {
this.proxyRoutes =
(await this.propertyService.getByKey<ProxyRoute[]>(
PROPERTY_PROXY_ROUTES
)) ?? [];
this.webFetchRoutes = this.webFetchRoutes =
(await this.propertyService.getByKey<WebFetchRoute[]>( (await this.propertyService.getByKey<WebFetchRoute[]>(
PROPERTY_WEB_FETCH_ROUTES PROPERTY_WEB_FETCH_ROUTES
@ -33,6 +41,10 @@ export class FetchService implements OnModuleInit {
} }
public async fetch(input: RequestInfo | URL, init?: RequestInit) { public async fetch(input: RequestInfo | URL, init?: RequestInit) {
// Apply a generic per-domain origin override (e.g. route a provider through
// a local proxy) before doing anything else with the request
input = this.applyProxyRoute(input);
const method = ( const method = (
init?.method ?? init?.method ??
(input instanceof Request ? input.method : undefined) ?? (input instanceof Request ? input.method : undefined) ??
@ -171,18 +183,53 @@ export class FetchService implements OnModuleInit {
} }
} }
/**
* Rewrites the origin (protocol, host and port) of a request when its domain
* matches a configured {@link ProxyRoute}, preserving path and query. Returns
* the input unchanged when no route matches or parsing fails.
*/
private applyProxyRoute(input: RequestInfo | URL): RequestInfo | URL {
try {
const url = new URL(
input instanceof Request ? input.url : input.toString()
);
const route = this.proxyRoutes.find(({ domain }) => {
return this.matchesDomain(url.hostname, domain);
});
if (!route) {
return input;
}
const target = new URL(route.url);
url.protocol = target.protocol;
url.host = target.host;
return input instanceof Request
? new Request(url.toString(), input)
: url.toString();
} catch {
return input;
}
}
private getMatchingWebFetchRoute(url: string) { private getMatchingWebFetchRoute(url: string) {
try { try {
const { hostname } = new URL(url); const { hostname } = new URL(url);
return this.webFetchRoutes.find(({ domain }) => { return this.webFetchRoutes.find(({ domain }) => {
return hostname === domain || hostname.endsWith(`.${domain}`); return this.matchesDomain(hostname, domain);
}); });
} catch { } catch {
return undefined; return undefined;
} }
} }
private matchesDomain(hostname: string, domain: string): boolean {
return hostname === domain || hostname.endsWith(`.${domain}`);
}
private redactUrl(rawUrl: string): string { private redactUrl(rawUrl: string): string {
try { try {
const url = new URL(rawUrl); const url = new URL(rawUrl);

22
apps/api/src/services/fetch/interfaces/proxy-route.interface.ts

@ -0,0 +1,22 @@
/**
* Overrides the origin (protocol, host and port) of outgoing requests for a
* given domain. This lets a self-hosted instance transparently route a
* provider's traffic through a local proxy (e.g. one that solves a bot
* challenge) without changing any call sites the request path and query are
* preserved.
*
* Configured via the `PROXY_ROUTES` property as a JSON array, e.g.
*
* [
* {
* "domain": "www.trackinsight.com",
* "url": "http://trackinsight-proxy:8191"
* }
* ]
*
* Matches the domain itself and its subdomains (e.g. `api.example.com`).
*/
export interface ProxyRoute {
domain: string;
url: string;
}

1
libs/common/src/lib/config.ts

@ -259,6 +259,7 @@ export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE';
export const PROPERTY_IS_USER_SIGNUP_ENABLED = 'IS_USER_SIGNUP_ENABLED'; export const PROPERTY_IS_USER_SIGNUP_ENABLED = 'IS_USER_SIGNUP_ENABLED';
export const PROPERTY_OPENROUTER_MODEL = 'OPENROUTER_MODEL'; export const PROPERTY_OPENROUTER_MODEL = 'OPENROUTER_MODEL';
export const PROPERTY_OPENROUTER_MODEL_WEB_FETCH = 'OPENROUTER_MODEL_WEB_FETCH'; export const PROPERTY_OPENROUTER_MODEL_WEB_FETCH = 'OPENROUTER_MODEL_WEB_FETCH';
export const PROPERTY_PROXY_ROUTES = 'PROXY_ROUTES';
export const PROPERTY_REFERRAL_PARTNERS = 'REFERRAL_PARTNERS'; export const PROPERTY_REFERRAL_PARTNERS = 'REFERRAL_PARTNERS';
export const PROPERTY_SLACK_COMMUNITY_USERS = 'SLACK_COMMUNITY_USERS'; export const PROPERTY_SLACK_COMMUNITY_USERS = 'SLACK_COMMUNITY_USERS';
export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG'; export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG';

Loading…
Cancel
Save