mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 04:17:11 +00:00
- Export validatePort() from config-generator with full edge case handling - Add YAML port validation in proxy-config-resolver before use - Add final port validation in cliproxy-executor after config merge - Add default port parameter and validation to proxy-detector - Add 38 new tests covering all port validation edge cases Fixes undefined port bug when upgrading from 7.11.1 to 7.12.0
335 lines
9.4 KiB
TypeScript
335 lines
9.4 KiB
TypeScript
/**
|
|
* Proxy Config Resolver
|
|
*
|
|
* Resolves proxy configuration from multiple sources with priority:
|
|
* CLI flags > Environment variables > config.yaml > defaults
|
|
*
|
|
* Supports both local (spawn CLIProxyAPI) and remote (connect to external) modes.
|
|
*/
|
|
|
|
import { ResolvedProxyConfig } from './types';
|
|
import { CLIPROXY_DEFAULT_PORT, validatePort } from './config-generator';
|
|
|
|
/** CLI flags for proxy configuration */
|
|
export const PROXY_CLI_FLAGS = [
|
|
'--proxy-host',
|
|
'--proxy-port',
|
|
'--proxy-protocol',
|
|
'--proxy-auth-token',
|
|
'--proxy-timeout',
|
|
'--local-proxy',
|
|
'--remote-only',
|
|
'--allow-self-signed',
|
|
] as const;
|
|
|
|
/** Environment variable names for proxy configuration */
|
|
export const PROXY_ENV_VARS = {
|
|
host: 'CCS_PROXY_HOST',
|
|
port: 'CCS_PROXY_PORT',
|
|
protocol: 'CCS_PROXY_PROTOCOL',
|
|
authToken: 'CCS_PROXY_AUTH_TOKEN',
|
|
timeout: 'CCS_PROXY_TIMEOUT',
|
|
fallbackEnabled: 'CCS_PROXY_FALLBACK_ENABLED',
|
|
allowSelfSigned: 'CCS_ALLOW_SELF_SIGNED',
|
|
} as const;
|
|
|
|
/** Parsed CLI proxy flags */
|
|
interface ParsedProxyFlags {
|
|
host?: string;
|
|
port?: number;
|
|
protocol?: 'http' | 'https';
|
|
authToken?: string;
|
|
timeout?: number;
|
|
localProxy: boolean;
|
|
remoteOnly: boolean;
|
|
allowSelfSigned?: boolean;
|
|
}
|
|
|
|
/** Proxy config from environment variables */
|
|
interface EnvProxyConfig {
|
|
host?: string;
|
|
port?: number;
|
|
protocol?: 'http' | 'https';
|
|
authToken?: string;
|
|
timeout?: number;
|
|
fallbackEnabled?: boolean;
|
|
allowSelfSigned?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Parse proxy-related CLI flags from argv.
|
|
* Returns parsed flags and remaining args (with proxy flags removed).
|
|
*/
|
|
export function parseProxyFlags(args: string[]): {
|
|
flags: ParsedProxyFlags;
|
|
remainingArgs: string[];
|
|
} {
|
|
const flags: ParsedProxyFlags = {
|
|
localProxy: false,
|
|
remoteOnly: false,
|
|
};
|
|
const remainingArgs: string[] = [];
|
|
|
|
let i = 0;
|
|
while (i < args.length) {
|
|
const arg = args[i];
|
|
|
|
if (arg === '--proxy-host' && args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
flags.host = args[i + 1];
|
|
i += 2;
|
|
continue;
|
|
}
|
|
|
|
if (arg === '--proxy-port' && args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
const port = parseInt(args[i + 1], 10);
|
|
if (!isNaN(port) && port > 0 && port <= 65535) {
|
|
flags.port = port;
|
|
}
|
|
i += 2;
|
|
continue;
|
|
}
|
|
|
|
if (arg === '--proxy-protocol' && args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
const proto = args[i + 1].toLowerCase();
|
|
if (proto === 'http' || proto === 'https') {
|
|
flags.protocol = proto;
|
|
}
|
|
i += 2;
|
|
continue;
|
|
}
|
|
|
|
if (arg === '--proxy-auth-token' && args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
flags.authToken = args[i + 1];
|
|
i += 2;
|
|
continue;
|
|
}
|
|
|
|
if (arg === '--proxy-timeout' && args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
const timeout = parseInt(args[i + 1], 10);
|
|
if (!isNaN(timeout) && timeout >= 100 && timeout <= 60000) {
|
|
flags.timeout = timeout;
|
|
}
|
|
i += 2;
|
|
continue;
|
|
}
|
|
|
|
if (arg === '--local-proxy') {
|
|
flags.localProxy = true;
|
|
i += 1;
|
|
continue;
|
|
}
|
|
|
|
if (arg === '--remote-only') {
|
|
flags.remoteOnly = true;
|
|
i += 1;
|
|
continue;
|
|
}
|
|
|
|
if (arg === '--allow-self-signed') {
|
|
flags.allowSelfSigned = true;
|
|
i += 1;
|
|
continue;
|
|
}
|
|
|
|
// Not a proxy flag - keep in remaining args
|
|
remainingArgs.push(arg);
|
|
i += 1;
|
|
}
|
|
|
|
return { flags, remainingArgs };
|
|
}
|
|
|
|
/**
|
|
* Get proxy configuration from environment variables.
|
|
*/
|
|
export function getProxyEnvVars(): EnvProxyConfig {
|
|
const config: EnvProxyConfig = {};
|
|
|
|
const host = process.env[PROXY_ENV_VARS.host];
|
|
if (host) {
|
|
config.host = host;
|
|
}
|
|
|
|
const port = process.env[PROXY_ENV_VARS.port];
|
|
if (port) {
|
|
const portNum = parseInt(port, 10);
|
|
if (!isNaN(portNum) && portNum > 0 && portNum <= 65535) {
|
|
config.port = portNum;
|
|
}
|
|
}
|
|
|
|
const protocol = process.env[PROXY_ENV_VARS.protocol];
|
|
if (protocol) {
|
|
const proto = protocol.toLowerCase();
|
|
if (proto === 'http' || proto === 'https') {
|
|
config.protocol = proto;
|
|
}
|
|
}
|
|
|
|
const authToken = process.env[PROXY_ENV_VARS.authToken];
|
|
if (authToken) {
|
|
config.authToken = authToken;
|
|
}
|
|
|
|
const timeout = process.env[PROXY_ENV_VARS.timeout];
|
|
if (timeout) {
|
|
const timeoutNum = parseInt(timeout, 10);
|
|
if (!isNaN(timeoutNum) && timeoutNum >= 100 && timeoutNum <= 60000) {
|
|
config.timeout = timeoutNum;
|
|
}
|
|
}
|
|
|
|
const fallback = process.env[PROXY_ENV_VARS.fallbackEnabled];
|
|
if (fallback !== undefined) {
|
|
// Accept: '1', 'true', 'yes' as enabled; '0', 'false', 'no' as disabled
|
|
const lower = fallback.toLowerCase();
|
|
if (lower === '1' || lower === 'true' || lower === 'yes') {
|
|
config.fallbackEnabled = true;
|
|
} else if (lower === '0' || lower === 'false' || lower === 'no') {
|
|
config.fallbackEnabled = false;
|
|
}
|
|
}
|
|
|
|
const allowSelfSigned = process.env[PROXY_ENV_VARS.allowSelfSigned];
|
|
if (allowSelfSigned !== undefined) {
|
|
const lower = allowSelfSigned.toLowerCase();
|
|
if (lower === '1' || lower === 'true' || lower === 'yes') {
|
|
config.allowSelfSigned = true;
|
|
} else if (lower === '0' || lower === 'false' || lower === 'no') {
|
|
config.allowSelfSigned = false;
|
|
}
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Default proxy configuration values.
|
|
*/
|
|
const DEFAULT_PROXY_CONFIG: ResolvedProxyConfig = {
|
|
mode: 'local',
|
|
port: CLIPROXY_DEFAULT_PORT,
|
|
protocol: 'http',
|
|
fallbackEnabled: true,
|
|
autoStartLocal: true,
|
|
remoteOnly: false,
|
|
forceLocal: false,
|
|
};
|
|
|
|
/**
|
|
* Resolve proxy configuration with priority: CLI > ENV > config.yaml > defaults.
|
|
*
|
|
* @param cliArgs - Raw CLI arguments
|
|
* @param configYamlProxy - Proxy section from config.yaml (optional, Phase 1)
|
|
* @returns Resolved configuration and remaining args (without proxy flags)
|
|
*/
|
|
export function resolveProxyConfig(
|
|
cliArgs: string[],
|
|
|
|
_configYamlProxy?: {
|
|
remote?: {
|
|
enabled?: boolean;
|
|
host?: string;
|
|
port?: number;
|
|
protocol?: 'http' | 'https';
|
|
auth_token?: string;
|
|
timeout?: number;
|
|
fallback_enabled?: boolean;
|
|
};
|
|
local?: {
|
|
port?: number;
|
|
auto_start?: boolean;
|
|
};
|
|
}
|
|
): { config: ResolvedProxyConfig; remainingArgs: string[] } {
|
|
// 1. Parse CLI flags (highest priority)
|
|
const { flags: cliFlags, remainingArgs } = parseProxyFlags(cliArgs);
|
|
|
|
// 2. Get environment variables
|
|
const envConfig = getProxyEnvVars();
|
|
|
|
// 3. config.yaml proxy section (passed as parameter - Phase 1 provides this)
|
|
// For now, we use empty object if not provided; Phase 1 integrates unified config loading
|
|
const yamlConfig = _configYamlProxy || {};
|
|
|
|
// 4. Build resolved config with priority merge
|
|
const resolved: ResolvedProxyConfig = {
|
|
...DEFAULT_PROXY_CONFIG,
|
|
};
|
|
|
|
// Determine mode: remote if host is specified AND remote is not explicitly disabled
|
|
// Priority: CLI flags > ENV > YAML config
|
|
// If YAML config has enabled: false, it only blocks YAML-sourced config (CLI/ENV override)
|
|
const yamlRemoteEnabled = yamlConfig.remote?.enabled !== false;
|
|
const hasRemoteHost =
|
|
cliFlags.host || envConfig.host || (yamlRemoteEnabled && yamlConfig.remote?.host);
|
|
|
|
// --local-proxy forces local mode regardless of remote config
|
|
if (cliFlags.localProxy) {
|
|
resolved.mode = 'local';
|
|
resolved.forceLocal = true;
|
|
} else if (hasRemoteHost) {
|
|
resolved.mode = 'remote';
|
|
}
|
|
|
|
// Merge host: CLI > ENV > config.yaml
|
|
resolved.host = cliFlags.host ?? envConfig.host ?? yamlConfig.remote?.host;
|
|
|
|
// Merge port: CLI > ENV > config.yaml (remote or local) > default
|
|
// Validate YAML-sourced ports before use (CLI/ENV already validated during parse)
|
|
const yamlPort = resolved.mode === 'remote' ? yamlConfig.remote?.port : yamlConfig.local?.port;
|
|
resolved.port =
|
|
cliFlags.port ??
|
|
envConfig.port ??
|
|
(yamlPort !== undefined ? validatePort(yamlPort) : undefined) ??
|
|
DEFAULT_PROXY_CONFIG.port;
|
|
|
|
// Merge protocol: CLI > ENV > config.yaml > default
|
|
resolved.protocol =
|
|
cliFlags.protocol ?? envConfig.protocol ?? yamlConfig.remote?.protocol ?? 'http';
|
|
|
|
// Merge auth token: CLI > ENV > config.yaml
|
|
resolved.authToken = cliFlags.authToken ?? envConfig.authToken ?? yamlConfig.remote?.auth_token;
|
|
|
|
// Merge timeout: CLI > ENV > config.yaml > default (2000ms in executor)
|
|
resolved.timeout = cliFlags.timeout ?? envConfig.timeout ?? yamlConfig.remote?.timeout;
|
|
|
|
// Merge allowSelfSigned: CLI > ENV > default (true for backwards compatibility)
|
|
resolved.allowSelfSigned = cliFlags.allowSelfSigned ?? envConfig.allowSelfSigned ?? true;
|
|
|
|
// Merge fallback enabled: ENV > config.yaml > default
|
|
resolved.fallbackEnabled =
|
|
envConfig.fallbackEnabled ?? yamlConfig.remote?.fallback_enabled ?? true;
|
|
|
|
// --remote-only from CLI
|
|
resolved.remoteOnly = cliFlags.remoteOnly;
|
|
|
|
// If --remote-only, disable fallback
|
|
if (resolved.remoteOnly) {
|
|
resolved.fallbackEnabled = false;
|
|
}
|
|
|
|
// Auto-start local from config.yaml > default
|
|
resolved.autoStartLocal = yamlConfig.local?.auto_start ?? true;
|
|
|
|
return { config: resolved, remainingArgs };
|
|
}
|
|
|
|
/**
|
|
* Check if args contain any proxy flags.
|
|
* Used for quick filtering before full parse.
|
|
*/
|
|
export function hasProxyFlags(args: string[]): boolean {
|
|
return args.some(
|
|
(arg) =>
|
|
arg === '--proxy-host' ||
|
|
arg === '--proxy-port' ||
|
|
arg === '--proxy-protocol' ||
|
|
arg === '--proxy-auth-token' ||
|
|
arg === '--proxy-timeout' ||
|
|
arg === '--local-proxy' ||
|
|
arg === '--remote-only' ||
|
|
arg === '--allow-self-signed'
|
|
);
|
|
}
|