dev #47
@@ -1,47 +1,11 @@
|
||||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
import { inject, REQUEST } from '@angular/core';
|
||||
|
||||
type RequestLike = {
|
||||
protocol?: string;
|
||||
get?: (name: string) => string | undefined;
|
||||
headers?: Record<string, string | string[] | undefined>;
|
||||
};
|
||||
import { RequestLike, resolveRequestOrigin } from '../../../core/request-origin';
|
||||
|
||||
function isAbsoluteUrl(url: string): boolean {
|
||||
return /^[a-z][a-z\d+\-.]*:/i.test(url) || url.startsWith('//');
|
||||
}
|
||||
|
||||
function firstHeaderValue(value: string | string[] | undefined): string | null {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0] ?? null;
|
||||
}
|
||||
return typeof value === 'string' ? value : null;
|
||||
}
|
||||
|
||||
function resolveOrigin(request: RequestLike | null): string | null {
|
||||
if (!request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const host =
|
||||
request.get?.('host') ??
|
||||
firstHeaderValue(request.headers?.['host']) ??
|
||||
firstHeaderValue(request.headers?.['x-forwarded-host']);
|
||||
if (!host) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const forwardedProtoRaw = firstHeaderValue(
|
||||
request.headers?.['x-forwarded-proto'],
|
||||
);
|
||||
const forwardedProto = forwardedProtoRaw
|
||||
?.split(',')
|
||||
.map((part) => part.trim().toLowerCase())
|
||||
.find(Boolean);
|
||||
const protocol = forwardedProto || request.protocol || 'http';
|
||||
return `${protocol}://${host}`;
|
||||
}
|
||||
|
||||
function normalizeRelativePath(url: string): string {
|
||||
const withoutDot = url.replace(/^\.\//, '');
|
||||
return withoutDot.startsWith('/') ? withoutDot : `/${withoutDot}`;
|
||||
@@ -53,7 +17,7 @@ export const serverOriginInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
}
|
||||
|
||||
const request = inject(REQUEST, { optional: true }) as RequestLike | null;
|
||||
const origin = resolveOrigin(request);
|
||||
const origin = resolveRequestOrigin(request);
|
||||
if (!origin) {
|
||||
return next(req);
|
||||
}
|
||||
|
||||
40
frontend/src/core/request-origin.spec.ts
Normal file
40
frontend/src/core/request-origin.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { resolveRequestOrigin } from './request-origin';
|
||||
|
||||
describe('resolveRequestOrigin', () => {
|
||||
it('prefers forwarded host and protocol when present', () => {
|
||||
expect(
|
||||
resolveRequestOrigin({
|
||||
protocol: 'http',
|
||||
headers: {
|
||||
host: 'internal:4000',
|
||||
'x-forwarded-host': '3d-fab.ch',
|
||||
'x-forwarded-proto': 'https',
|
||||
},
|
||||
}),
|
||||
).toBe('https://3d-fab.ch');
|
||||
});
|
||||
|
||||
it('falls back to request protocol and host', () => {
|
||||
expect(
|
||||
resolveRequestOrigin({
|
||||
protocol: 'http',
|
||||
headers: {
|
||||
host: 'localhost:4000',
|
||||
},
|
||||
}),
|
||||
).toBe('http://localhost:4000');
|
||||
});
|
||||
|
||||
it('uses the first forwarded value when proxies append multiple entries', () => {
|
||||
expect(
|
||||
resolveRequestOrigin({
|
||||
protocol: 'http',
|
||||
headers: {
|
||||
host: 'internal:4000',
|
||||
'x-forwarded-host': '3d-fab.ch, proxy.local',
|
||||
'x-forwarded-proto': 'https, http',
|
||||
},
|
||||
}),
|
||||
).toBe('https://3d-fab.ch');
|
||||
});
|
||||
});
|
||||
50
frontend/src/core/request-origin.ts
Normal file
50
frontend/src/core/request-origin.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export type RequestLike = {
|
||||
protocol?: string;
|
||||
get?: (name: string) => string | undefined;
|
||||
headers?: Record<string, string | string[] | undefined>;
|
||||
};
|
||||
|
||||
function firstHeaderValue(
|
||||
value: string | string[] | undefined,
|
||||
): string | null {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0] ?? null;
|
||||
}
|
||||
return typeof value === 'string' ? value : null;
|
||||
}
|
||||
|
||||
function firstForwardedValue(
|
||||
value: string | string[] | undefined,
|
||||
): string | null {
|
||||
const raw = firstHeaderValue(value);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
raw
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.find(Boolean) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveRequestOrigin(request: RequestLike | null): string | null {
|
||||
if (!request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const host =
|
||||
firstForwardedValue(request.headers?.['x-forwarded-host']) ??
|
||||
request.get?.('host') ??
|
||||
firstHeaderValue(request.headers?.['host']);
|
||||
if (!host) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const forwardedProto = firstForwardedValue(
|
||||
request.headers?.['x-forwarded-proto'],
|
||||
)?.toLowerCase();
|
||||
const protocol = forwardedProto || request.protocol || 'http';
|
||||
return `${protocol}://${host}`;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import express from 'express';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import bootstrap from './main.server';
|
||||
import { resolveRequestOrigin } from './core/request-origin';
|
||||
|
||||
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
|
||||
const browserDistFolder = resolve(serverDistFolder, '../browser');
|
||||
@@ -39,13 +40,14 @@ app.get(
|
||||
* Handle all other requests by rendering the Angular application.
|
||||
*/
|
||||
app.get('**', (req, res, next) => {
|
||||
const { protocol, originalUrl, baseUrl, headers } = req;
|
||||
const { originalUrl, baseUrl } = req;
|
||||
const origin = resolveRequestOrigin(req);
|
||||
|
||||
commonEngine
|
||||
.render({
|
||||
bootstrap,
|
||||
documentFilePath: indexHtml,
|
||||
url: `${protocol}://${headers.host}${originalUrl}`,
|
||||
url: `${origin ?? 'http://localhost:4000'}${originalUrl}`,
|
||||
publicPath: browserDistFolder,
|
||||
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user