This commit is contained in:
+10
-5
@@ -1352,11 +1352,16 @@ export function startCoreServer(opts: CoreServerOptions, port: number = 9876): v
|
||||
);
|
||||
}
|
||||
const pinnedHost = tls.redirectHost ?? (isWildcardBind || isLoopbackBind ? 'localhost' : host);
|
||||
const redirector = createHttpRedirectServer({ httpsPort: port, pinnedHost, preferRequestHost });
|
||||
redirector.on('error', (e) => logger.warn(`[server] HTTP redirect listener error: ${(e as Error).message}`));
|
||||
redirector.listen(tls.httpRedirectPort, host, () =>
|
||||
logger.info(`HTTP->HTTPS redirect listening on http://${host}:${tls.httpRedirectPort}`),
|
||||
);
|
||||
// One listener per configured redirect port (e.g. 80 plus a high port).
|
||||
for (const rport of tls.httpRedirectPorts) {
|
||||
const redirector = createHttpRedirectServer({ httpsPort: port, pinnedHost, preferRequestHost });
|
||||
redirector.on('error', (e) =>
|
||||
logger.warn(`[server] HTTP redirect listener error on :${rport}: ${(e as Error).message}`),
|
||||
);
|
||||
redirector.listen(rport, host, () =>
|
||||
logger.info(`HTTP->HTTPS redirect listening on http://${host}:${rport}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
server = finalApp.listen(port, host, () => {
|
||||
|
||||
@@ -22,13 +22,43 @@ describe('mergeServerConfig', () => {
|
||||
expect(cfg.tls.minVersion).toBe(SERVER_TLS_DEFAULTS.minVersion);
|
||||
expect(cfg.tls.selfSignedDir).toBe(SERVER_TLS_DEFAULTS.selfSignedDir);
|
||||
expect(cfg.tls.httpRedirect).toBe(true);
|
||||
expect(cfg.tls.httpRedirectPort).toBe(9080);
|
||||
expect(cfg.tls.httpRedirectPorts).toEqual([9080]);
|
||||
expect(cfg.tls.selfSignedHosts).toEqual([]);
|
||||
});
|
||||
|
||||
it('throws when http_redirect_port equals the https port', () => {
|
||||
it('normalizes a single http_redirect_port (legacy scalar) into an array', () => {
|
||||
const cfg = mergeServerConfig(
|
||||
{ tls: { enabled: true, httpRedirectPort: 8080 } as never },
|
||||
{ freshInstall: false },
|
||||
);
|
||||
expect(cfg.tls.httpRedirectPorts).toEqual([8080]);
|
||||
// The legacy scalar key must not leak onto the resolved object.
|
||||
expect((cfg.tls as Record<string, unknown>).httpRedirectPort).toBeUndefined();
|
||||
});
|
||||
|
||||
it('accepts a list of redirect ports and de-duplicates / drops invalid', () => {
|
||||
const cfg = mergeServerConfig(
|
||||
{ tls: { enabled: true, httpRedirectPort: [80, 9876, 80, 70000, 0] } as never },
|
||||
{ freshInstall: false },
|
||||
);
|
||||
expect(cfg.tls.httpRedirectPorts).toEqual([80, 9876]);
|
||||
});
|
||||
|
||||
it('throws when any redirect port equals the https port (scalar)', () => {
|
||||
expect(() =>
|
||||
mergeServerConfig({ tls: { enabled: true, httpRedirectPort: 9876 } }, { freshInstall: false, httpsPort: 9876 }),
|
||||
mergeServerConfig(
|
||||
{ tls: { enabled: true, httpRedirectPort: 9876 } as never },
|
||||
{ freshInstall: false, httpsPort: 9876 },
|
||||
),
|
||||
).toThrow(/redirect.*port/i);
|
||||
});
|
||||
|
||||
it('throws when one port in a redirect list collides with the https port', () => {
|
||||
expect(() =>
|
||||
mergeServerConfig(
|
||||
{ tls: { enabled: true, httpRedirectPort: [80, 9876] } as never },
|
||||
{ freshInstall: false, httpsPort: 9876 },
|
||||
),
|
||||
).toThrow(/redirect.*port/i);
|
||||
});
|
||||
|
||||
@@ -47,7 +77,7 @@ describe('mergeServerConfig', () => {
|
||||
it('does not throw on port collision when httpRedirect is false', () => {
|
||||
expect(() =>
|
||||
mergeServerConfig(
|
||||
{ tls: { enabled: true, httpRedirect: false, httpRedirectPort: 9876 } },
|
||||
{ tls: { enabled: true, httpRedirect: false, httpRedirectPort: 9876 } as never },
|
||||
{ freshInstall: false, httpsPort: 9876 },
|
||||
),
|
||||
).not.toThrow();
|
||||
|
||||
+38
-5
@@ -6,7 +6,12 @@ export interface ServerTlsConfig {
|
||||
selfSignedDir: string;
|
||||
selfSignedHosts: string[];
|
||||
httpRedirect: boolean;
|
||||
httpRedirectPort: number;
|
||||
/**
|
||||
* Ports the HTTP→HTTPS redirector listens on, resolved to a non-empty,
|
||||
* de-duplicated array. Config accepts `http_redirect_port` as either a single
|
||||
* number (e.g. `9080`) or a list (e.g. `[80, 9876]`); both map here.
|
||||
*/
|
||||
httpRedirectPorts: number[];
|
||||
/**
|
||||
* Host used to build the HTTP→HTTPS redirect `Location` header.
|
||||
* When null the bind host is used instead.
|
||||
@@ -52,6 +57,9 @@ export function resolveListenPort(
|
||||
return DEFAULT_SERVER_PORT;
|
||||
}
|
||||
|
||||
/** Default HTTP→HTTPS redirect port when none is configured. */
|
||||
export const DEFAULT_HTTP_REDIRECT_PORTS = [9080];
|
||||
|
||||
export const SERVER_TLS_DEFAULTS: ServerTlsConfig = {
|
||||
enabled: false,
|
||||
certFile: null,
|
||||
@@ -60,10 +68,25 @@ export const SERVER_TLS_DEFAULTS: ServerTlsConfig = {
|
||||
selfSignedDir: './data/tls',
|
||||
selfSignedHosts: [],
|
||||
httpRedirect: true,
|
||||
httpRedirectPort: 9080,
|
||||
httpRedirectPorts: [...DEFAULT_HTTP_REDIRECT_PORTS],
|
||||
redirectHost: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Coerce a raw `http_redirect_port` / `http_redirect_ports` value into a
|
||||
* non-empty, de-duplicated array of valid ports. Accepts a single number, an
|
||||
* array, or numeric strings. Invalid/out-of-range entries are dropped; if
|
||||
* nothing valid remains, falls back to the default.
|
||||
*/
|
||||
export function normalizeRedirectPorts(input: unknown): number[] {
|
||||
const arr = Array.isArray(input) ? input : input == null ? [] : [input];
|
||||
const valid = arr
|
||||
.map((v) => (typeof v === 'string' ? Number(v) : v))
|
||||
.filter((n): n is number => typeof n === 'number' && Number.isInteger(n) && n >= 1 && n <= 65535);
|
||||
const unique = [...new Set(valid)];
|
||||
return unique.length > 0 ? unique : [...DEFAULT_HTTP_REDIRECT_PORTS];
|
||||
}
|
||||
|
||||
export interface MergeServerOpts {
|
||||
freshInstall: boolean;
|
||||
/**
|
||||
@@ -78,13 +101,20 @@ export function mergeServerConfig(
|
||||
partial: Partial<ServerConfig> | undefined,
|
||||
opts: MergeServerOpts,
|
||||
): ServerConfig {
|
||||
const tlsPartial = (partial?.tls ?? {}) as Partial<ServerTlsConfig>;
|
||||
// Accept the legacy scalar `http_redirect_port` alongside the resolved
|
||||
// `http_redirect_ports` array — both arrive untyped from YAML/JSON.
|
||||
const tlsPartial = (partial?.tls ?? {}) as Partial<ServerTlsConfig> & {
|
||||
httpRedirectPort?: number | number[];
|
||||
};
|
||||
const enabledDefault = opts.freshInstall;
|
||||
const tls: ServerTlsConfig = {
|
||||
...SERVER_TLS_DEFAULTS,
|
||||
...tlsPartial,
|
||||
enabled: tlsPartial.enabled ?? enabledDefault,
|
||||
httpRedirectPorts: normalizeRedirectPorts(tlsPartial.httpRedirectPorts ?? tlsPartial.httpRedirectPort),
|
||||
};
|
||||
// Drop the legacy scalar so the resolved object exposes only httpRedirectPorts.
|
||||
delete (tls as unknown as Record<string, unknown>)['httpRedirectPort'];
|
||||
tls.selfSignedHosts = [...(tlsPartial.selfSignedHosts ?? SERVER_TLS_DEFAULTS.selfSignedHosts)];
|
||||
|
||||
if (tls.enabled) {
|
||||
@@ -93,8 +123,11 @@ export function mergeServerConfig(
|
||||
if (hasCert !== hasKey) {
|
||||
throw new Error('server.tls: set both cert_file and key_file, or neither');
|
||||
}
|
||||
if (opts.httpsPort != null && tls.httpRedirect && tls.httpRedirectPort === opts.httpsPort) {
|
||||
throw new Error(`server.tls: http_redirect_port (${tls.httpRedirectPort}) must differ from the HTTPS port`);
|
||||
if (opts.httpsPort != null && tls.httpRedirect) {
|
||||
const clash = tls.httpRedirectPorts.find((p) => p === opts.httpsPort);
|
||||
if (clash != null) {
|
||||
throw new Error(`server.tls: http_redirect_port (${clash}) must differ from the HTTPS port`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// opts.httpsPort is the already-resolved listen port in production; the
|
||||
|
||||
Reference in New Issue
Block a user