sync: update from private repo (e321ac3)
Some checks failed
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync 2026-06-17 01:49:11 +00:00
parent 35e4fd30d2
commit 517142c61d
7 changed files with 106 additions and 23 deletions

View File

@ -328,8 +328,9 @@ tools:
# self_signed_dir: ./data/tls # self_signed_dir: ./data/tls
# self_signed_hosts: [] # localhost / 127.0.0.1 / ::1 / hostname は常に含まれる # self_signed_hosts: [] # localhost / 127.0.0.1 / ::1 / hostname は常に含まれる
# http_redirect: true # http_redirect: true
# http_redirect_port: 9080 # HTTPS ポートと異なる値にすること # http_redirect_port: 9080 # HTTP→HTTPS リダイレクトの待ち受けポート。単一値か配列で複数指定可 (例: [80, 9876])。
# redirect_host: null # リダイレクト先のホスト; null = バインドホストを使用 # # 各ポートは HTTPS ポートと異なる必要があり、1024 未満 (80 等) の bind には権限が必要
# redirect_host: null # リダイレクト先のホスト; null かつワイルドカード bind の場合はリクエストの Host ヘッダから導出
# # リバースプロキシ構成: このブロックを省略するか enabled: false のままにすること。 # # リバースプロキシ構成: このブロックを省略するか enabled: false のままにすること。
# # プロキシが TLS を終端している場合に native TLS を有効にすると二重終端になり壊れる。 # # プロキシが TLS を終端している場合に native TLS を有効にすると二重終端になり壊れる。
# # noVNC / SSH コンソールを wss で使う場合は「信頼済み証明書」が必要 # # noVNC / SSH コンソールを wss で使う場合は「信頼済み証明書」が必要

View File

@ -1352,11 +1352,16 @@ export function startCoreServer(opts: CoreServerOptions, port: number = 9876): v
); );
} }
const pinnedHost = tls.redirectHost ?? (isWildcardBind || isLoopbackBind ? 'localhost' : host); 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 }); const redirector = createHttpRedirectServer({ httpsPort: port, pinnedHost, preferRequestHost });
redirector.on('error', (e) => logger.warn(`[server] HTTP redirect listener error: ${(e as Error).message}`)); redirector.on('error', (e) =>
redirector.listen(tls.httpRedirectPort, host, () => logger.warn(`[server] HTTP redirect listener error on :${rport}: ${(e as Error).message}`),
logger.info(`HTTP->HTTPS redirect listening on http://${host}:${tls.httpRedirectPort}`),
); );
redirector.listen(rport, host, () =>
logger.info(`HTTP->HTTPS redirect listening on http://${host}:${rport}`),
);
}
} }
} else { } else {
server = finalApp.listen(port, host, () => { server = finalApp.listen(port, host, () => {

View File

@ -22,13 +22,43 @@ describe('mergeServerConfig', () => {
expect(cfg.tls.minVersion).toBe(SERVER_TLS_DEFAULTS.minVersion); expect(cfg.tls.minVersion).toBe(SERVER_TLS_DEFAULTS.minVersion);
expect(cfg.tls.selfSignedDir).toBe(SERVER_TLS_DEFAULTS.selfSignedDir); expect(cfg.tls.selfSignedDir).toBe(SERVER_TLS_DEFAULTS.selfSignedDir);
expect(cfg.tls.httpRedirect).toBe(true); expect(cfg.tls.httpRedirect).toBe(true);
expect(cfg.tls.httpRedirectPort).toBe(9080); expect(cfg.tls.httpRedirectPorts).toEqual([9080]);
expect(cfg.tls.selfSignedHosts).toEqual([]); 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(() => 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); ).toThrow(/redirect.*port/i);
}); });
@ -47,7 +77,7 @@ describe('mergeServerConfig', () => {
it('does not throw on port collision when httpRedirect is false', () => { it('does not throw on port collision when httpRedirect is false', () => {
expect(() => expect(() =>
mergeServerConfig( mergeServerConfig(
{ tls: { enabled: true, httpRedirect: false, httpRedirectPort: 9876 } }, { tls: { enabled: true, httpRedirect: false, httpRedirectPort: 9876 } as never },
{ freshInstall: false, httpsPort: 9876 }, { freshInstall: false, httpsPort: 9876 },
), ),
).not.toThrow(); ).not.toThrow();

View File

@ -6,7 +6,12 @@ export interface ServerTlsConfig {
selfSignedDir: string; selfSignedDir: string;
selfSignedHosts: string[]; selfSignedHosts: string[];
httpRedirect: boolean; httpRedirect: boolean;
httpRedirectPort: number; /**
* Ports the HTTPHTTPS 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 HTTPHTTPS redirect `Location` header. * Host used to build the HTTPHTTPS redirect `Location` header.
* When null the bind host is used instead. * When null the bind host is used instead.
@ -52,6 +57,9 @@ export function resolveListenPort(
return DEFAULT_SERVER_PORT; 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 = { export const SERVER_TLS_DEFAULTS: ServerTlsConfig = {
enabled: false, enabled: false,
certFile: null, certFile: null,
@ -60,10 +68,25 @@ export const SERVER_TLS_DEFAULTS: ServerTlsConfig = {
selfSignedDir: './data/tls', selfSignedDir: './data/tls',
selfSignedHosts: [], selfSignedHosts: [],
httpRedirect: true, httpRedirect: true,
httpRedirectPort: 9080, httpRedirectPorts: [...DEFAULT_HTTP_REDIRECT_PORTS],
redirectHost: null, 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 { export interface MergeServerOpts {
freshInstall: boolean; freshInstall: boolean;
/** /**
@ -78,13 +101,20 @@ export function mergeServerConfig(
partial: Partial<ServerConfig> | undefined, partial: Partial<ServerConfig> | undefined,
opts: MergeServerOpts, opts: MergeServerOpts,
): ServerConfig { ): 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 enabledDefault = opts.freshInstall;
const tls: ServerTlsConfig = { const tls: ServerTlsConfig = {
...SERVER_TLS_DEFAULTS, ...SERVER_TLS_DEFAULTS,
...tlsPartial, ...tlsPartial,
enabled: tlsPartial.enabled ?? enabledDefault, 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)]; tls.selfSignedHosts = [...(tlsPartial.selfSignedHosts ?? SERVER_TLS_DEFAULTS.selfSignedHosts)];
if (tls.enabled) { if (tls.enabled) {
@ -93,8 +123,11 @@ export function mergeServerConfig(
if (hasCert !== hasKey) { if (hasCert !== hasKey) {
throw new Error('server.tls: set both cert_file and key_file, or neither'); throw new Error('server.tls: set both cert_file and key_file, or neither');
} }
if (opts.httpsPort != null && tls.httpRedirect && tls.httpRedirectPort === opts.httpsPort) { if (opts.httpsPort != null && tls.httpRedirect) {
throw new Error(`server.tls: http_redirect_port (${tls.httpRedirectPort}) must differ from the HTTPS port`); 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 // opts.httpsPort is the already-resolved listen port in production; the

View File

@ -97,18 +97,30 @@ export function ServerTlsForm({ config, onChange }: SectionFormProps) {
</label> </label>
</div> </div>
{/* HTTP redirect port */} {/* HTTP redirect port(s) — accepts a single port or a comma-separated list */}
<div> <div>
<FieldLabel>{t('serverTls.httpRedirectPort')}</FieldLabel> <FieldLabel>{t('serverTls.httpRedirectPort')}</FieldLabel>
<FieldInput <FieldInput
type="number" value={
value={tls.httpRedirectPort != null ? String(tls.httpRedirectPort) : ''} Array.isArray(tls.httpRedirectPort)
? tls.httpRedirectPort.join(', ')
: tls.httpRedirectPort != null
? String(tls.httpRedirectPort)
: ''
}
onChange={v => { onChange={v => {
const n = parseInt(v, 10); const ports = v
onChange('server.tls.httpRedirectPort', isNaN(n) ? undefined : n); .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> </div>
{/* Redirect host (optional) */} {/* Redirect host (optional) */}

View File

@ -790,7 +790,8 @@
"keyFile": "Private key file (PEM)", "keyFile": "Private key file (PEM)",
"certHelp": "Leave both empty to use an auto-generated self-signed certificate.", "certHelp": "Leave both empty to use an auto-generated self-signed certificate.",
"httpRedirect": "Redirect HTTP to HTTPS", "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)", "redirectHost": "Redirect host (optional)",
"selfSignedHosts": "Additional certificate hostnames", "selfSignedHosts": "Additional certificate hostnames",
"restartBanner": "Changes to HTTPS settings require a server restart to take effect." "restartBanner": "Changes to HTTPS settings require a server restart to take effect."

View File

@ -791,6 +791,7 @@
"certHelp": "両方を空欄にすると、自動生成の自己署名証明書を使用します。", "certHelp": "両方を空欄にすると、自動生成の自己署名証明書を使用します。",
"httpRedirect": "HTTP を HTTPS へリダイレクト", "httpRedirect": "HTTP を HTTPS へリダイレクト",
"httpRedirectPort": "HTTP リダイレクトポート", "httpRedirectPort": "HTTP リダイレクトポート",
"httpRedirectPortHelp": "平文 HTTP を HTTPS へリダイレクトするポート。単一ポートか、カンマ区切りの複数指定が可能(例: 80, 9876。各ポートは HTTPS ポートと異なる必要があり、1024 未満80 など)の bind には権限が必要。",
"redirectHost": "リダイレクト先ホスト(省略可)", "redirectHost": "リダイレクト先ホスト(省略可)",
"selfSignedHosts": "証明書の追加ホスト名", "selfSignedHosts": "証明書の追加ホスト名",
"restartBanner": "HTTPS 設定の変更を反映するには、サーバーの再起動が必要です。" "restartBanner": "HTTPS 設定の変更を反映するには、サーバーの再起動が必要です。"