sync: update from private repo (e321ac3)
Some checks failed
CI / build-and-test (push) Has been cancelled
Some checks failed
CI / build-and-test (push) Has been cancelled
This commit is contained in:
parent
35e4fd30d2
commit
517142c61d
@ -328,8 +328,9 @@ tools:
|
||||
# self_signed_dir: ./data/tls
|
||||
# self_signed_hosts: [] # localhost / 127.0.0.1 / ::1 / hostname は常に含まれる
|
||||
# http_redirect: true
|
||||
# http_redirect_port: 9080 # HTTPS ポートと異なる値にすること
|
||||
# redirect_host: null # リダイレクト先のホスト; null = バインドホストを使用
|
||||
# http_redirect_port: 9080 # HTTP→HTTPS リダイレクトの待ち受けポート。単一値か配列で複数指定可 (例: [80, 9876])。
|
||||
# # 各ポートは HTTPS ポートと異なる必要があり、1024 未満 (80 等) の bind には権限が必要
|
||||
# redirect_host: null # リダイレクト先のホスト; null かつワイルドカード bind の場合はリクエストの Host ヘッダから導出
|
||||
# # リバースプロキシ構成: このブロックを省略するか enabled: false のままにすること。
|
||||
# # プロキシが TLS を終端している場合に native TLS を有効にすると二重終端になり壊れる。
|
||||
# # noVNC / SSH コンソールを wss で使う場合は「信頼済み証明書」が必要
|
||||
|
||||
@ -1352,11 +1352,16 @@ export function startCoreServer(opts: CoreServerOptions, port: number = 9876): v
|
||||
);
|
||||
}
|
||||
const pinnedHost = tls.redirectHost ?? (isWildcardBind || isLoopbackBind ? 'localhost' : host);
|
||||
// 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: ${(e as Error).message}`));
|
||||
redirector.listen(tls.httpRedirectPort, host, () =>
|
||||
logger.info(`HTTP->HTTPS redirect listening on http://${host}:${tls.httpRedirectPort}`),
|
||||
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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -97,18 +97,30 @@ export function ServerTlsForm({ config, onChange }: SectionFormProps) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* HTTP redirect port */}
|
||||
{/* HTTP redirect port(s) — accepts a single port or a comma-separated list */}
|
||||
<div>
|
||||
<FieldLabel>{t('serverTls.httpRedirectPort')}</FieldLabel>
|
||||
<FieldInput
|
||||
type="number"
|
||||
value={tls.httpRedirectPort != null ? String(tls.httpRedirectPort) : ''}
|
||||
value={
|
||||
Array.isArray(tls.httpRedirectPort)
|
||||
? tls.httpRedirectPort.join(', ')
|
||||
: tls.httpRedirectPort != null
|
||||
? String(tls.httpRedirectPort)
|
||||
: ''
|
||||
}
|
||||
onChange={v => {
|
||||
const n = parseInt(v, 10);
|
||||
onChange('server.tls.httpRedirectPort', isNaN(n) ? undefined : n);
|
||||
const ports = v
|
||||
.split(',')
|
||||
.map(s => parseInt(s.trim(), 10))
|
||||
.filter(n => !isNaN(n));
|
||||
onChange(
|
||||
'server.tls.httpRedirectPort',
|
||||
ports.length === 0 ? undefined : ports.length === 1 ? ports[0] : ports,
|
||||
);
|
||||
}}
|
||||
placeholder="9080"
|
||||
placeholder="9080 (or 80, 9876)"
|
||||
/>
|
||||
<HelpText>{t('serverTls.httpRedirectPortHelp')}</HelpText>
|
||||
</div>
|
||||
|
||||
{/* Redirect host (optional) */}
|
||||
|
||||
@ -790,7 +790,8 @@
|
||||
"keyFile": "Private key file (PEM)",
|
||||
"certHelp": "Leave both empty to use an auto-generated self-signed certificate.",
|
||||
"httpRedirect": "Redirect HTTP to HTTPS",
|
||||
"httpRedirectPort": "HTTP redirect port",
|
||||
"httpRedirectPort": "HTTP redirect port(s)",
|
||||
"httpRedirectPortHelp": "Port(s) that redirect plain HTTP to HTTPS. One port, or a comma-separated list (e.g. 80, 9876). Each must differ from the HTTPS port; ports below 1024 (like 80) require privileges.",
|
||||
"redirectHost": "Redirect host (optional)",
|
||||
"selfSignedHosts": "Additional certificate hostnames",
|
||||
"restartBanner": "Changes to HTTPS settings require a server restart to take effect."
|
||||
|
||||
@ -791,6 +791,7 @@
|
||||
"certHelp": "両方を空欄にすると、自動生成の自己署名証明書を使用します。",
|
||||
"httpRedirect": "HTTP を HTTPS へリダイレクト",
|
||||
"httpRedirectPort": "HTTP リダイレクトポート",
|
||||
"httpRedirectPortHelp": "平文 HTTP を HTTPS へリダイレクトするポート。単一ポートか、カンマ区切りの複数指定が可能(例: 80, 9876)。各ポートは HTTPS ポートと異なる必要があり、1024 未満(80 など)の bind には権限が必要。",
|
||||
"redirectHost": "リダイレクト先ホスト(省略可)",
|
||||
"selfSignedHosts": "証明書の追加ホスト名",
|
||||
"restartBanner": "HTTPS 設定の変更を反映するには、サーバーの再起動が必要です。"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user