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:
Iuliia Ivashko
2026-03-23 15:07:16 +02:00
committed by GitHub
parent fb57ee4cc3
commit dc100ed0da
5 changed files with 46 additions and 12 deletions
+2
View File
@@ -13,6 +13,7 @@ export interface WebUiServerController {
export interface StartWebUiServerOptions {
port?: number;
host?: string;
attachSignals?: boolean;
exitOnShutdown?: boolean;
uiPassword?: string | null;
@@ -27,6 +28,7 @@ export declare function setupProxy(app: Express): void;
export declare function restartOpenCode(): Promise<void>;
export declare function parseArgs(argv?: string[]): {
port: number;
host?: string;
uiPassword: string | null;
tryCfTunnel: boolean;
tunnelProvider?: string;
+21 -11
View File
@@ -5788,6 +5788,7 @@ function parseArgs(argv = process.argv.slice(2)) {
const options = {
port: DEFAULT_PORT,
host: undefined,
uiPassword: envPassword,
tryCfTunnel: envCfTunnel,
tunnelProvider: envTunnelProvider,
@@ -5826,6 +5827,13 @@ function parseArgs(argv = process.argv.slice(2)) {
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') {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
@@ -7095,6 +7103,7 @@ async function gracefulShutdown(options = {}) {
async function main(options = {}) {
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 shouldUseCanonicalTunnelConfig = typeof options.tunnelMode === 'string'
|| typeof options.tunnelProvider === 'string'
@@ -14235,9 +14244,10 @@ async function main(options = {}) {
let activePort = port;
const bindHost = typeof process.env.OPENCHAMBER_HOST === 'string' && process.env.OPENCHAMBER_HOST.trim().length > 0
? process.env.OPENCHAMBER_HOST.trim()
: null;
const bindHost = host
|| (typeof process.env.OPENCHAMBER_HOST === 'string' && process.env.OPENCHAMBER_HOST.trim().length > 0
? process.env.OPENCHAMBER_HOST.trim()
: '127.0.0.1');
await new Promise((resolve, reject) => {
const onError = (error) => {
@@ -14256,9 +14266,12 @@ async function main(options = {}) {
// ignore
}
console.log(`OpenChamber server running on port ${activePort}`);
console.log(`Health check: http://localhost:${activePort}/health`);
console.log(`Web interface: http://localhost:${activePort}`);
const displayHost = (bindHost === '0.0.0.0' || bindHost === '::' || bindHost === '[::]')
? 'localhost'
: (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) {
const startupModeLabel = startupTunnelRequest.mode === TUNNEL_MODE_QUICK
@@ -14308,11 +14321,7 @@ async function main(options = {}) {
resolve();
};
if (bindHost) {
server.listen(port, bindHost, onListening);
} else {
server.listen(port, onListening);
}
server.listen(port, bindHost, onListening);
});
if (attachSignals && !signalsAttached) {
@@ -14355,6 +14364,7 @@ if (isCliExecution) {
exitOnShutdown = true;
main({
port: cliOptions.port,
host: cliOptions.host,
tryCfTunnel: cliOptions.tryCfTunnel,
tunnelProvider: cliOptions.tunnelProvider,
tunnelMode: cliOptions.tunnelMode,