fix: bind web server to 127.0.0.1 by default and add --host CLI flag (#750)
## Summary Fixes #736 — OpenChamber listens on `0.0.0.0` (all interfaces) by default, exposing the server to the network without warning. The log output shows `visit: http://127.0.0.1:...` which is misleading. ## Changes - **Default bind address changed to `127.0.0.1`** — server is only accessible locally unless explicitly configured otherwise - **New `--host` CLI flag** — `openchamber --host 0.0.0.0 -p 8080` to listen on all interfaces - **`OPENCHAMBER_HOST` env var** — documented in help text and docker-compose.yml as an alternative to `--host` - **Docker entrypoint** defaults to `OPENCHAMBER_HOST=0.0.0.0` so container port mapping continues to work - **Startup logs** show the actual bind address instead of hardcoded `localhost` ### Resolution priority ``` --host flag > OPENCHAMBER_HOST env var > 127.0.0.1 (default) ``` ### What doesn't break - **Desktop app** — already forces `OPENCHAMBER_HOST=127.0.0.1` via Tauri - **VS Code extension** — doesn't use the web server - **Docker** — entrypoint sets `OPENCHAMBER_HOST=0.0.0.0`, preserving current behavior - **Tunnels** — cloudflared connects to `127.0.0.1` origin internally, works regardless of bind address ## Testing Automated: - `bun run type-check` / `bun run lint` — pass Manual (CLI, direct `node` execution): - Default bind → `127.0.0.1` (verified via `lsof`/netstat) - `--host 0.0.0.0` → binds all interfaces - `--host=0.0.0.0` (inline) → works - `--host` without value → error exit 2 - `OPENCHAMBER_HOST` env var → respected - `--host` flag overrides env var - IPv6 `::1` → correct bracketed URL, health check 200 - CLI daemon start/stop → works - `visit:` URL → correct - Help text → `--host` in OPTIONS, `OPENCHAMBER_HOST` in ENVIRONMENT - Browser UI → loads and works - Tunnel via UI → works - Desktop app → no regression Docker (tested on Ubuntu with native Docker): - SSH key generated successfully - `OpenChamber server listening on 0.0.0.0:3000` - Health check 200 - `uid=1000(openchamber)` confirmed
This commit is contained in:
@@ -16,6 +16,7 @@ services:
|
|||||||
- ./data/ssh:/home/openchamber/.ssh
|
- ./data/ssh:/home/openchamber/.ssh
|
||||||
- ./workspaces:/home/openchamber/workspaces
|
- ./workspaces:/home/openchamber/workspaces
|
||||||
#environment:
|
#environment:
|
||||||
|
# OPENCHAMBER_HOST: 0.0.0.0 # Bind address (default in Docker: 0.0.0.0)
|
||||||
# UI_PASSWORD: your_secure_password_here # Uncomment to set UI password
|
# UI_PASSWORD: your_secure_password_here # Uncomment to set UI password
|
||||||
# OPENCHAMBER_TUNNEL_PROVIDER: cloudflare
|
# OPENCHAMBER_TUNNEL_PROVIDER: cloudflare
|
||||||
# OPENCHAMBER_TUNNEL_MODE: quick # quick | managed-remote | managed-local
|
# OPENCHAMBER_TUNNEL_MODE: quick # quick | managed-remote | managed-local
|
||||||
|
|||||||
@@ -570,6 +570,7 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|||||||
const args = Array.isArray(argv) ? [...argv] : [];
|
const args = Array.isArray(argv) ? [...argv] : [];
|
||||||
const options = {
|
const options = {
|
||||||
port: DEFAULT_PORT,
|
port: DEFAULT_PORT,
|
||||||
|
host: undefined,
|
||||||
uiPassword: process.env.OPENCHAMBER_UI_PASSWORD || undefined,
|
uiPassword: process.env.OPENCHAMBER_UI_PASSWORD || undefined,
|
||||||
json: false,
|
json: false,
|
||||||
all: false,
|
all: false,
|
||||||
@@ -658,6 +659,15 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|||||||
options.explicitPort = true;
|
options.explicitPort = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'host': {
|
||||||
|
const { value, nextIndex } = consumeValue(i, inlineValue);
|
||||||
|
i = nextIndex;
|
||||||
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||||
|
throw new TunnelCliError('Missing value for --host.', EXIT_CODE.USAGE_ERROR);
|
||||||
|
}
|
||||||
|
options.host = value.trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'ui-password': {
|
case 'ui-password': {
|
||||||
const { value, nextIndex } = consumeValue(i, inlineValue);
|
const { value, nextIndex } = consumeValue(i, inlineValue);
|
||||||
i = nextIndex;
|
i = nextIndex;
|
||||||
@@ -842,11 +852,13 @@ COMMANDS:
|
|||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
-p, --port Web server port (default: ${DEFAULT_PORT})
|
-p, --port Web server port (default: ${DEFAULT_PORT})
|
||||||
|
--host Bind address (default: 127.0.0.1)
|
||||||
--ui-password Protect browser UI with single password
|
--ui-password Protect browser UI with single password
|
||||||
-h, --help Show help
|
-h, --help Show help
|
||||||
-v, --version Show version
|
-v, --version Show version
|
||||||
|
|
||||||
ENVIRONMENT:
|
ENVIRONMENT:
|
||||||
|
OPENCHAMBER_HOST Bind address (e.g. 0.0.0.0 for all interfaces)
|
||||||
OPENCHAMBER_UI_PASSWORD Alternative to --ui-password flag
|
OPENCHAMBER_UI_PASSWORD Alternative to --ui-password flag
|
||||||
OPENCHAMBER_DATA_DIR Override OpenChamber data directory
|
OPENCHAMBER_DATA_DIR Override OpenChamber data directory
|
||||||
OPENCODE_HOST External OpenCode server base URL, e.g. http://hostname:4096
|
OPENCODE_HOST External OpenCode server base URL, e.g. http://hostname:4096
|
||||||
@@ -2730,6 +2742,10 @@ const commands = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const serverArgs = [serverPath, '--port', String(targetPort)];
|
const serverArgs = [serverPath, '--port', String(targetPort)];
|
||||||
|
const effectiveHost = typeof options.host === 'string' && options.host.length > 0 ? options.host : undefined;
|
||||||
|
if (effectiveHost) {
|
||||||
|
serverArgs.push('--host', effectiveHost);
|
||||||
|
}
|
||||||
|
|
||||||
const serveSpin = showOutput ? createSpinner(options) : null;
|
const serveSpin = showOutput ? createSpinner(options) : null;
|
||||||
|
|
||||||
@@ -2741,6 +2757,7 @@ const commands = {
|
|||||||
...process.env,
|
...process.env,
|
||||||
OPENCHAMBER_PORT: String(targetPort),
|
OPENCHAMBER_PORT: String(targetPort),
|
||||||
OPENCODE_BINARY: opencodeBinary,
|
OPENCODE_BINARY: opencodeBinary,
|
||||||
|
...(effectiveHost ? { OPENCHAMBER_HOST: effectiveHost } : {}),
|
||||||
...(effectiveUiPassword ? { OPENCHAMBER_UI_PASSWORD: effectiveUiPassword } : {}),
|
...(effectiveUiPassword ? { OPENCHAMBER_UI_PASSWORD: effectiveUiPassword } : {}),
|
||||||
...(process.env.OPENCODE_SKIP_START ? { OPENCHAMBER_SKIP_OPENCODE_START: process.env.OPENCODE_SKIP_START } : {}),
|
...(process.env.OPENCODE_SKIP_START ? { OPENCHAMBER_SKIP_OPENCODE_START: process.env.OPENCODE_SKIP_START } : {}),
|
||||||
},
|
},
|
||||||
|
|||||||
Vendored
+2
@@ -13,6 +13,7 @@ export interface WebUiServerController {
|
|||||||
|
|
||||||
export interface StartWebUiServerOptions {
|
export interface StartWebUiServerOptions {
|
||||||
port?: number;
|
port?: number;
|
||||||
|
host?: string;
|
||||||
attachSignals?: boolean;
|
attachSignals?: boolean;
|
||||||
exitOnShutdown?: boolean;
|
exitOnShutdown?: boolean;
|
||||||
uiPassword?: string | null;
|
uiPassword?: string | null;
|
||||||
@@ -27,6 +28,7 @@ export declare function setupProxy(app: Express): void;
|
|||||||
export declare function restartOpenCode(): Promise<void>;
|
export declare function restartOpenCode(): Promise<void>;
|
||||||
export declare function parseArgs(argv?: string[]): {
|
export declare function parseArgs(argv?: string[]): {
|
||||||
port: number;
|
port: number;
|
||||||
|
host?: string;
|
||||||
uiPassword: string | null;
|
uiPassword: string | null;
|
||||||
tryCfTunnel: boolean;
|
tryCfTunnel: boolean;
|
||||||
tunnelProvider?: string;
|
tunnelProvider?: string;
|
||||||
|
|||||||
@@ -5788,6 +5788,7 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
port: DEFAULT_PORT,
|
port: DEFAULT_PORT,
|
||||||
|
host: undefined,
|
||||||
uiPassword: envPassword,
|
uiPassword: envPassword,
|
||||||
tryCfTunnel: envCfTunnel,
|
tryCfTunnel: envCfTunnel,
|
||||||
tunnelProvider: envTunnelProvider,
|
tunnelProvider: envTunnelProvider,
|
||||||
@@ -5826,6 +5827,13 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (optionName === 'host') {
|
||||||
|
const { value, nextIndex } = consumeValue(i, inlineValue);
|
||||||
|
i = nextIndex;
|
||||||
|
options.host = typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (optionName === 'ui-password') {
|
if (optionName === 'ui-password') {
|
||||||
const { value, nextIndex } = consumeValue(i, inlineValue);
|
const { value, nextIndex } = consumeValue(i, inlineValue);
|
||||||
i = nextIndex;
|
i = nextIndex;
|
||||||
@@ -7095,6 +7103,7 @@ async function gracefulShutdown(options = {}) {
|
|||||||
|
|
||||||
async function main(options = {}) {
|
async function main(options = {}) {
|
||||||
const port = Number.isFinite(options.port) && options.port >= 0 ? Math.trunc(options.port) : DEFAULT_PORT;
|
const port = Number.isFinite(options.port) && options.port >= 0 ? Math.trunc(options.port) : DEFAULT_PORT;
|
||||||
|
const host = typeof options.host === 'string' && options.host.length > 0 ? options.host : undefined;
|
||||||
const tryCfTunnel = options.tryCfTunnel === true;
|
const tryCfTunnel = options.tryCfTunnel === true;
|
||||||
const shouldUseCanonicalTunnelConfig = typeof options.tunnelMode === 'string'
|
const shouldUseCanonicalTunnelConfig = typeof options.tunnelMode === 'string'
|
||||||
|| typeof options.tunnelProvider === 'string'
|
|| typeof options.tunnelProvider === 'string'
|
||||||
@@ -14235,9 +14244,10 @@ async function main(options = {}) {
|
|||||||
|
|
||||||
let activePort = port;
|
let activePort = port;
|
||||||
|
|
||||||
const bindHost = typeof process.env.OPENCHAMBER_HOST === 'string' && process.env.OPENCHAMBER_HOST.trim().length > 0
|
const bindHost = host
|
||||||
? process.env.OPENCHAMBER_HOST.trim()
|
|| (typeof process.env.OPENCHAMBER_HOST === 'string' && process.env.OPENCHAMBER_HOST.trim().length > 0
|
||||||
: null;
|
? process.env.OPENCHAMBER_HOST.trim()
|
||||||
|
: '127.0.0.1');
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const onError = (error) => {
|
const onError = (error) => {
|
||||||
@@ -14256,9 +14266,12 @@ async function main(options = {}) {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`OpenChamber server running on port ${activePort}`);
|
const displayHost = (bindHost === '0.0.0.0' || bindHost === '::' || bindHost === '[::]')
|
||||||
console.log(`Health check: http://localhost:${activePort}/health`);
|
? 'localhost'
|
||||||
console.log(`Web interface: http://localhost:${activePort}`);
|
: (bindHost.includes(':') ? `[${bindHost}]` : bindHost);
|
||||||
|
console.log(`OpenChamber server listening on ${bindHost}:${activePort}`);
|
||||||
|
console.log(`Health check: http://${displayHost}:${activePort}/health`);
|
||||||
|
console.log(`Web interface: http://${displayHost}:${activePort}`);
|
||||||
|
|
||||||
if (startupTunnelRequest) {
|
if (startupTunnelRequest) {
|
||||||
const startupModeLabel = startupTunnelRequest.mode === TUNNEL_MODE_QUICK
|
const startupModeLabel = startupTunnelRequest.mode === TUNNEL_MODE_QUICK
|
||||||
@@ -14308,11 +14321,7 @@ async function main(options = {}) {
|
|||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (bindHost) {
|
server.listen(port, bindHost, onListening);
|
||||||
server.listen(port, bindHost, onListening);
|
|
||||||
} else {
|
|
||||||
server.listen(port, onListening);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (attachSignals && !signalsAttached) {
|
if (attachSignals && !signalsAttached) {
|
||||||
@@ -14355,6 +14364,7 @@ if (isCliExecution) {
|
|||||||
exitOnShutdown = true;
|
exitOnShutdown = true;
|
||||||
main({
|
main({
|
||||||
port: cliOptions.port,
|
port: cliOptions.port,
|
||||||
|
host: cliOptions.host,
|
||||||
tryCfTunnel: cliOptions.tryCfTunnel,
|
tryCfTunnel: cliOptions.tryCfTunnel,
|
||||||
tunnelProvider: cliOptions.tunnelProvider,
|
tunnelProvider: cliOptions.tunnelProvider,
|
||||||
tunnelMode: cliOptions.tunnelMode,
|
tunnelMode: cliOptions.tunnelMode,
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ if [ "${OH_MY_OPENCODE:-false}" = "true" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Docker containers need to listen on all interfaces for port mapping to work.
|
||||||
|
OPENCHAMBER_HOST="${OPENCHAMBER_HOST:-0.0.0.0}"
|
||||||
|
export OPENCHAMBER_HOST
|
||||||
|
|
||||||
echo "[entrypoint] starting..."
|
echo "[entrypoint] starting..."
|
||||||
|
|
||||||
if [ "$#" -gt 0 ]; then
|
if [ "$#" -gt 0 ]; then
|
||||||
@@ -67,4 +71,4 @@ if [ -n "${UI_PASSWORD:-}" ]; then
|
|||||||
fi
|
fi
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
bun packages/web/bin/cli.js logs
|
exec bun packages/web/bin/cli.js logs
|
||||||
|
|||||||
Reference in New Issue
Block a user