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_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 で使う場合は「信頼済み証明書」が必要
|
||||||
|
|||||||
@ -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, () => {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -6,7 +6,12 @@ export interface ServerTlsConfig {
|
|||||||
selfSignedDir: string;
|
selfSignedDir: string;
|
||||||
selfSignedHosts: string[];
|
selfSignedHosts: string[];
|
||||||
httpRedirect: boolean;
|
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.
|
* Host used to build the HTTP→HTTPS 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
|
||||||
|
|||||||
@ -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) */}
|
||||||
|
|||||||
@ -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."
|
||||||
|
|||||||
@ -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 設定の変更を反映するには、サーバーの再起動が必要です。"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user