sync: update from private repo (f6b8c40)
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
a0923abc40
commit
e0c03ef10b
@ -319,6 +319,7 @@ tools:
|
|||||||
# プロキシが TLS を終端しているので、ここで有効にすると二重終端になり接続が壊れる。
|
# プロキシが TLS を終端しているので、ここで有効にすると二重終端になり接続が壊れる。
|
||||||
#
|
#
|
||||||
# server:
|
# server:
|
||||||
|
# port: 9876 # HTTP(S) 待ち受けポート (default 9876)。環境変数 PORT (.env 含む) が優先。反映には再起動が必要
|
||||||
# tls:
|
# tls:
|
||||||
# enabled: true # フレッシュインストールのデフォルト; ブロック未記載=アップグレード時 false
|
# enabled: true # フレッシュインストールのデフォルト; ブロック未記載=アップグレード時 false
|
||||||
# cert_file: null # PEM 証明書パス (任意); cert_file と key_file は両方設定するか両方省略
|
# cert_file: null # PEM 証明書パス (任意); cert_file と key_file は両方設定するか両方省略
|
||||||
|
|||||||
@ -107,7 +107,7 @@ import { readGatewayConfig } from '../gateway/config.js';
|
|||||||
import { createAdminGatewayStatusRouter } from './admin-gateway-status-api.js';
|
import { createAdminGatewayStatusRouter } from './admin-gateway-status-api.js';
|
||||||
import { createServer as createHttpsServer } from 'https';
|
import { createServer as createHttpsServer } from 'https';
|
||||||
import { X509Certificate } from 'crypto';
|
import { X509Certificate } from 'crypto';
|
||||||
import { mergeServerConfig } from '../server/config.js';
|
import { mergeServerConfig, resolveListenPort } from '../server/config.js';
|
||||||
import { resolveTlsOptions } from '../net/tls-options.js';
|
import { resolveTlsOptions } from '../net/tls-options.js';
|
||||||
import { createHttpRedirectServer } from '../net/http-redirect.js';
|
import { createHttpRedirectServer } from '../net/http-redirect.js';
|
||||||
|
|
||||||
@ -297,7 +297,9 @@ export function createCoreServer(opts: CoreServerOptions): {
|
|||||||
// tls.enabled and the cookie secure flag.
|
// tls.enabled and the cookie secure flag.
|
||||||
const serverCfg = mergeServerConfig(loadConfig().server, {
|
const serverCfg = mergeServerConfig(loadConfig().server, {
|
||||||
freshInstall: false,
|
freshInstall: false,
|
||||||
httpsPort: opts.listenPort ?? Number(process.env['PORT'] ?? 9876),
|
// startCoreServer threads the resolved port in via listenPort; the fallback
|
||||||
|
// (for callers that bypass it) applies the same PORT-env > config precedence.
|
||||||
|
httpsPort: opts.listenPort ?? resolveListenPort(process.env['PORT'], loadConfig().server?.port),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (authActive) {
|
if (authActive) {
|
||||||
@ -1152,9 +1154,7 @@ export function createCoreServer(opts: CoreServerOptions): {
|
|||||||
// (startCoreServer always sets it) over the PORT env-var guess so
|
// (startCoreServer always sets it) over the PORT env-var guess so
|
||||||
// the status endpoint matches what the bridge actually bound.
|
// the status endpoint matches what the bridge actually bound.
|
||||||
// env-var fallback is kept for callers that bypass startCoreServer.
|
// env-var fallback is kept for callers that bypass startCoreServer.
|
||||||
const envPortRaw = Number(process.env['PORT']);
|
const actualPort = opts.listenPort ?? resolveListenPort(process.env['PORT'], loadConfig().server?.port);
|
||||||
const envPort = Number.isFinite(envPortRaw) && envPortRaw > 0 ? envPortRaw : 9876;
|
|
||||||
const actualPort = opts.listenPort ?? envPort;
|
|
||||||
const statusRouter = createAdminGatewayStatusRouter({
|
const statusRouter = createAdminGatewayStatusRouter({
|
||||||
mount: gatewayMount,
|
mount: gatewayMount,
|
||||||
configManager: opts.configManager ?? null,
|
configManager: opts.configManager ?? null,
|
||||||
@ -1267,7 +1267,9 @@ function deriveCallbackBaseUrl(authConfig: AuthConfig | undefined): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const port = Number(process.env.PORT ?? 9876);
|
// Same precedence as the listener (PORT env > server.port > default) so the
|
||||||
|
// derived OAuth callback origin matches the port the bridge actually binds.
|
||||||
|
const port = resolveListenPort(process.env.PORT, loadConfig().server?.port);
|
||||||
return `http://localhost:${port}`;
|
return `http://localhost:${port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { mergeServerConfig, SERVER_TLS_DEFAULTS } from './config.js';
|
import { mergeServerConfig, SERVER_TLS_DEFAULTS, resolveListenPort, DEFAULT_SERVER_PORT } from './config.js';
|
||||||
|
|
||||||
describe('mergeServerConfig', () => {
|
describe('mergeServerConfig', () => {
|
||||||
it('upgrade-safe: an absent server block disables TLS', () => {
|
it('upgrade-safe: an absent server block disables TLS', () => {
|
||||||
@ -58,4 +58,56 @@ describe('mergeServerConfig', () => {
|
|||||||
mergeServerConfig({ tls: { enabled: false, certFile: '/x/c.pem' } }, { freshInstall: false }),
|
mergeServerConfig({ tls: { enabled: false, certFile: '/x/c.pem' } }, { freshInstall: false }),
|
||||||
).not.toThrow();
|
).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolves port: httpsPort wins, else config port, else default', () => {
|
||||||
|
expect(mergeServerConfig({ port: 3000 }, { freshInstall: false }).port).toBe(3000);
|
||||||
|
expect(mergeServerConfig({ port: 3000 }, { freshInstall: false, httpsPort: 8443 }).port).toBe(8443);
|
||||||
|
expect(mergeServerConfig(undefined, { freshInstall: false }).port).toBe(DEFAULT_SERVER_PORT);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolveListenPort', () => {
|
||||||
|
it('PORT env wins over config port and default', () => {
|
||||||
|
expect(resolveListenPort('4000', 5000)).toBe(4000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to config port when env is unset/empty', () => {
|
||||||
|
expect(resolveListenPort(undefined, 5000)).toBe(5000);
|
||||||
|
expect(resolveListenPort('', 5000)).toBe(5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the default when neither is set', () => {
|
||||||
|
expect(resolveListenPort(undefined, undefined)).toBe(DEFAULT_SERVER_PORT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores an invalid env PORT and warns, then uses config', () => {
|
||||||
|
const warns: string[] = [];
|
||||||
|
expect(resolveListenPort('not-a-port', 5000, (m) => warns.push(m))).toBe(5000);
|
||||||
|
expect(resolveListenPort('70000', 5000, (m) => warns.push(m))).toBe(5000); // out of range
|
||||||
|
expect(warns.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores an invalid config port and warns, then uses the default', () => {
|
||||||
|
const warns: string[] = [];
|
||||||
|
expect(resolveListenPort(undefined, 0, (m) => warns.push(m))).toBe(DEFAULT_SERVER_PORT);
|
||||||
|
expect(resolveListenPort(undefined, 99999, (m) => warns.push(m))).toBe(DEFAULT_SERVER_PORT);
|
||||||
|
expect(resolveListenPort(undefined, 1.5, (m) => warns.push(m))).toBe(DEFAULT_SERVER_PORT); // non-integer
|
||||||
|
expect(warns.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts the boundary ports 1 and 65535', () => {
|
||||||
|
expect(resolveListenPort('1', undefined)).toBe(1);
|
||||||
|
expect(resolveListenPort('65535', undefined)).toBe(65535);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects env "0" and falls through', () => {
|
||||||
|
expect(resolveListenPort('0', 5000)).toBe(5000);
|
||||||
|
expect(resolveListenPort('0', undefined)).toBe(DEFAULT_SERVER_PORT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts a quoted-string config port symmetrically with env', () => {
|
||||||
|
expect(resolveListenPort(undefined, '5000' as unknown as number)).toBe(5000);
|
||||||
|
expect(resolveListenPort(undefined, '' as unknown as number)).toBe(DEFAULT_SERVER_PORT);
|
||||||
|
expect(resolveListenPort(undefined, 'abc' as unknown as number)).toBe(DEFAULT_SERVER_PORT);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,9 +16,42 @@ export interface ServerTlsConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerConfig {
|
export interface ServerConfig {
|
||||||
|
/** HTTP(S) listen port. Resolved by mergeServerConfig (default 9876). */
|
||||||
|
port: number;
|
||||||
tls: ServerTlsConfig;
|
tls: ServerTlsConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Default HTTP(S) listen port when neither PORT nor server.port is set. */
|
||||||
|
export const DEFAULT_SERVER_PORT = 9876;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the effective listen port from the precedence
|
||||||
|
* `PORT env > server.port (config) > default`. Invalid values (non-integer or
|
||||||
|
* out of the 1–65535 range) are ignored with a caller-supplied warn hook so a
|
||||||
|
* typo can't bind port 0 / NaN. Pure: the env string is passed in.
|
||||||
|
*/
|
||||||
|
export function resolveListenPort(
|
||||||
|
envPort: string | undefined,
|
||||||
|
configPort: number | string | undefined,
|
||||||
|
warn?: (msg: string) => void,
|
||||||
|
): number {
|
||||||
|
const isValid = (n: unknown): n is number =>
|
||||||
|
typeof n === 'number' && Number.isInteger(n) && n >= 1 && n <= 65535;
|
||||||
|
|
||||||
|
if (envPort !== undefined && envPort !== '') {
|
||||||
|
const n = Number(envPort);
|
||||||
|
if (isValid(n)) return n;
|
||||||
|
warn?.(`[server] ignoring invalid PORT env "${envPort}" (want an integer 1–65535)`);
|
||||||
|
}
|
||||||
|
if (configPort !== undefined && configPort !== '') {
|
||||||
|
// Accept a numeric YAML value or a quoted "9876" string symmetrically with env.
|
||||||
|
const n = typeof configPort === 'string' ? Number(configPort) : configPort;
|
||||||
|
if (isValid(n)) return n;
|
||||||
|
warn?.(`[server] ignoring invalid server.port "${configPort}" (want an integer 1–65535)`);
|
||||||
|
}
|
||||||
|
return DEFAULT_SERVER_PORT;
|
||||||
|
}
|
||||||
|
|
||||||
export const SERVER_TLS_DEFAULTS: ServerTlsConfig = {
|
export const SERVER_TLS_DEFAULTS: ServerTlsConfig = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
certFile: null,
|
certFile: null,
|
||||||
@ -33,6 +66,11 @@ export const SERVER_TLS_DEFAULTS: ServerTlsConfig = {
|
|||||||
|
|
||||||
export interface MergeServerOpts {
|
export interface MergeServerOpts {
|
||||||
freshInstall: boolean;
|
freshInstall: boolean;
|
||||||
|
/**
|
||||||
|
* The already-resolved effective listen port (PORT env > server.port >
|
||||||
|
* default), threaded in by the caller. Becomes the merged `port` and is the
|
||||||
|
* value the http_redirect_port collision check compares against.
|
||||||
|
*/
|
||||||
httpsPort?: number;
|
httpsPort?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,5 +97,8 @@ export function mergeServerConfig(
|
|||||||
throw new Error(`server.tls: http_redirect_port (${tls.httpRedirectPort}) must differ from the HTTPS port`);
|
throw new Error(`server.tls: http_redirect_port (${tls.httpRedirectPort}) must differ from the HTTPS port`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { tls };
|
// opts.httpsPort is the already-resolved listen port in production; the
|
||||||
|
// fallback validates/coerces a raw config value for callers that don't pass it.
|
||||||
|
const port = opts.httpsPort ?? resolveListenPort(undefined, partial?.port);
|
||||||
|
return { port, tls };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
import { Repository, BrowserSessionRepo } from './db/repository.js';
|
import { Repository, BrowserSessionRepo } from './db/repository.js';
|
||||||
import { startCoreServer } from './bridge/server.js';
|
import { startCoreServer } from './bridge/server.js';
|
||||||
|
import { resolveListenPort } from './server/config.js';
|
||||||
import { runMigrations } from './db/migrate.js';
|
import { runMigrations } from './db/migrate.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { accessSync, existsSync, mkdirSync, constants } from 'fs';
|
import { accessSync, existsSync, mkdirSync, constants } from 'fs';
|
||||||
@ -147,7 +148,10 @@ export async function start(opts: StartWorkerOptions = {}): Promise<void> {
|
|||||||
const workerManager = new WorkerManager(repo, configManager);
|
const workerManager = new WorkerManager(repo, configManager);
|
||||||
workerManager.start();
|
workerManager.start();
|
||||||
|
|
||||||
const port = parseInt(process.env['PORT'] ?? '9876', 10);
|
// Listen port precedence: PORT env (incl. .env loaded by scripts/server.sh)
|
||||||
|
// > config.yaml server.port > default 9876. UI/config changes apply on the
|
||||||
|
// next restart (no live re-bind), same as the TLS settings.
|
||||||
|
const port = resolveListenPort(process.env['PORT'], config.server?.port, (m) => logger.warn(m));
|
||||||
|
|
||||||
// タイトル自動生成関数を作成(roles に 'title' を持つ worker を優先、なければ最初の worker)
|
// タイトル自動生成関数を作成(roles に 'title' を持つ worker を優先、なければ最初の worker)
|
||||||
const titleWorker =
|
const titleWorker =
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export function ServerTlsForm({ config, onChange }: SectionFormProps) {
|
|||||||
|
|
||||||
// Navigate to the server.tls sub-object; fall back to empty object if absent.
|
// Navigate to the server.tls sub-object; fall back to empty object if absent.
|
||||||
const tls = (config?.server?.tls) ?? {};
|
const tls = (config?.server?.tls) ?? {};
|
||||||
|
const server = (config?.server) ?? {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
@ -35,6 +36,21 @@ export function ServerTlsForm({ config, onChange }: SectionFormProps) {
|
|||||||
{t('serverTls.restartBanner')}
|
{t('serverTls.restartBanner')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* HTTP(S) listen port */}
|
||||||
|
<div>
|
||||||
|
<FieldLabel>{t('serverTls.port')}</FieldLabel>
|
||||||
|
<FieldInput
|
||||||
|
type="number"
|
||||||
|
value={server.port != null ? String(server.port) : ''}
|
||||||
|
onChange={v => {
|
||||||
|
const n = parseInt(v, 10);
|
||||||
|
onChange('server.port', isNaN(n) ? undefined : n);
|
||||||
|
}}
|
||||||
|
placeholder="9876"
|
||||||
|
/>
|
||||||
|
<HelpText>{t('serverTls.portHelp')}</HelpText>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Enable HTTPS */}
|
{/* Enable HTTPS */}
|
||||||
<div>
|
<div>
|
||||||
<label className="inline-flex items-center gap-2 text-[13px] text-slate-700 dark:text-slate-200">
|
<label className="inline-flex items-center gap-2 text-[13px] text-slate-700 dark:text-slate-200">
|
||||||
|
|||||||
@ -782,6 +782,8 @@
|
|||||||
},
|
},
|
||||||
"serverTls": {
|
"serverTls": {
|
||||||
"title": "HTTPS / TLS",
|
"title": "HTTPS / TLS",
|
||||||
|
"port": "HTTP(S) port",
|
||||||
|
"portHelp": "Port the server listens on (default 9876). The PORT environment variable (incl. .env) takes precedence over this value. Requires a restart to apply.",
|
||||||
"enabled": "Serve over HTTPS",
|
"enabled": "Serve over HTTPS",
|
||||||
"enabledHelp": "Terminate TLS in the app. Self-signed by default — browsers (and the noVNC / SSH console over wss) will warn until you install a real certificate. Requires a restart to apply.",
|
"enabledHelp": "Terminate TLS in the app. Self-signed by default — browsers (and the noVNC / SSH console over wss) will warn until you install a real certificate. Requires a restart to apply.",
|
||||||
"certFile": "Certificate file (PEM)",
|
"certFile": "Certificate file (PEM)",
|
||||||
|
|||||||
@ -782,6 +782,8 @@
|
|||||||
},
|
},
|
||||||
"serverTls": {
|
"serverTls": {
|
||||||
"title": "HTTPS / TLS",
|
"title": "HTTPS / TLS",
|
||||||
|
"port": "HTTP(S) ポート",
|
||||||
|
"portHelp": "サーバーが待ち受けるポート番号(デフォルト 9876)。環境変数 PORT(.env 含む)がこの値より優先されます。反映にはサーバーの再起動が必要です。",
|
||||||
"enabled": "HTTPS で配信",
|
"enabled": "HTTPS で配信",
|
||||||
"enabledHelp": "アプリ内で TLS を終端します。デフォルトは自己署名証明書のため、正式な証明書を導入するまでブラウザ(および wss 経由の noVNC・SSH コンソール)に警告が表示されます。反映にはサーバーの再起動が必要です。",
|
"enabledHelp": "アプリ内で TLS を終端します。デフォルトは自己署名証明書のため、正式な証明書を導入するまでブラウザ(および wss 経由の noVNC・SSH コンソール)に警告が表示されます。反映にはサーバーの再起動が必要です。",
|
||||||
"certFile": "証明書ファイル(PEM)",
|
"certFile": "証明書ファイル(PEM)",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user