mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Docker delivers the host's own browser to the container from the container network's gateway, never from 127.0.0.1, so the loopback-only admission gate answered every POWER UP request with 403 and no key could be saved. The README and CHANGELOG claimed otherwise; both are corrected here. - keySetupCore: GEV_KEY_SETUP_TRUSTED_PEERS, an exact-address list (no wildcards, prefixes or hostnames) admitted alongside loopback. Unset trusts nothing, and the sharing, Host, Origin, Content-Type and proxy-header checks all still apply to a listed address. Twelve assertions pin each refusal. - scripts/docker-start.mjs: the container entry point, shaped after scripts/pinokio-start.mjs. It reads the default gateway from /proc/net/route, trusts exactly that address, and starts Vite. No gateway (--network none) trusts nothing. The pure helpers are unit-tested and a contract test pins the Dockerfile CMD and the loopback-only publish. - Dockerfile: install git. `npm test` builds fixtures with `git init`, so the suite failed 71 tests inside the image and the README's claim that it runs in the container was wrong. - SECURITY.md and .env.example document the setting and why it is safe only behind a 127.0.0.1 publish; README gains the Linux/NAS id note and .dockerignore drops macOS AppleDouble sidecars, both reported on the PR. Verified in Docker on a clean build: saving a key from the panel writes the bind-mounted .env (0600 in the container), it survives `compose down`/`up`, and REMOVE comments it back out. A foreign Origin, a foreign Host, a proxy header and a sibling container on the same network are each refused. `npm run doctor`, `npm test` (5067 passed, 1 skipped, 0 failed) and `npm run build` are green inside the container. Reported-by: @jklaze Co-Authored-By: Bogdan Petrut <[email protected]> Co-Authored-By: Claude Opus 5 <[email protected]>
133 lines
5.1 KiB
JavaScript
133 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Docker launcher — the container's entry point (see Dockerfile).
|
|
*
|
|
* `compose.yaml` publishes the port on the host's loopback, but Docker still
|
|
* delivers the host's own browser to the container FROM the container
|
|
* network's gateway address, never from 127.0.0.1. Provider Settings admits
|
|
* loopback only, so without this the POWER UP panel refuses the very machine
|
|
* running the container. This launcher reads the gateway out of the kernel
|
|
* routing table and declares exactly that one address trusted, then starts
|
|
* Vite the way scripts/pinokio-start.mjs does.
|
|
*/
|
|
import { readFileSync, realpathSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { isDirectInvocation } from './pinokio-install.mjs';
|
|
import { loadViteFromCanonicalRoot } from './pinokio-start.mjs';
|
|
|
|
const MODULE_PATH = fileURLToPath(import.meta.url);
|
|
const ROOT = realpathSync(path.resolve(path.dirname(MODULE_PATH), '..'));
|
|
const ROUTE_TABLE = '/proc/net/route';
|
|
/** Host names the native launcher accepts; the container matches it. */
|
|
const LOCAL_ALLOWED_HOSTS = Object.freeze(['localhost', '127.0.0.1', '.local']);
|
|
|
|
/**
|
|
* Extract the IPv4 default gateway from Linux `/proc/net/route` text. The
|
|
* addresses there are little-endian hex and only the default route (a
|
|
* destination of 00000000 on an interface flagged up) names a gateway.
|
|
* Returns null when there is none — the `--network none` case, which must
|
|
* never fabricate an address.
|
|
* @param {string} text Raw routing-table contents.
|
|
* @returns {string|null} Dotted-quad gateway, or null.
|
|
*/
|
|
export function defaultGatewayFromRouteTable(text) {
|
|
const lines = String(text || '').split(/\r?\n/).slice(1);
|
|
for (const line of lines) {
|
|
const fields = line.trim().split(/\s+/);
|
|
if (fields.length < 4) continue;
|
|
const [, destination, gateway, flags] = fields;
|
|
if (destination !== '00000000' || !/^[0-9a-f]{8}$/i.test(gateway)) continue;
|
|
if ((Number.parseInt(flags, 16) & 0x1) === 0) continue; // RTF_UP
|
|
const octets = [6, 4, 2, 0].map((offset) =>
|
|
Number.parseInt(gateway.slice(offset, offset + 2), 16),
|
|
);
|
|
if (octets.every((octet) => octet === 0)) continue;
|
|
return octets.join('.');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Decide this launch's trusted-peer list. An operator value always wins, so a
|
|
* compose override stays authoritative; otherwise the gateway alone is
|
|
* trusted. No gateway trusts nothing and Provider Settings stays loopback-only,
|
|
* exactly as under `npm run dev`.
|
|
* @param {{env?: NodeJS.ProcessEnv, routeTable?: string}} input
|
|
* @returns {string} Value for GEV_KEY_SETUP_TRUSTED_PEERS ('' trusts nothing).
|
|
*/
|
|
export function resolveTrustedPeers({ env = process.env, routeTable = '' } = {}) {
|
|
const explicit = String(env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim();
|
|
if (explicit) return explicit;
|
|
return defaultGatewayFromRouteTable(routeTable) ?? '';
|
|
}
|
|
|
|
/**
|
|
* Host headers this container answers to. A wildcard bind is unavoidable
|
|
* inside a container, but build/vite.js reads that as "allow every Host"
|
|
* (allowedHosts: true), dropping the DNS-rebinding protection `npm run dev`
|
|
* keeps. The published port only ever delivers the host's own browser, so pin
|
|
* the same names the native launcher uses, and still honour an operator's
|
|
* GEV_ALLOWED_HOSTS so a LAN hostname stays addable here too. See issue #21.
|
|
* @param {NodeJS.ProcessEnv} [env]
|
|
* @returns {string[]}
|
|
*/
|
|
export function resolveAllowedHosts(env = process.env) {
|
|
const configured = String(env.GEV_ALLOWED_HOSTS ?? '')
|
|
.split(',')
|
|
.map((host) => host.trim())
|
|
.filter(Boolean);
|
|
return [...new Set([...LOCAL_ALLOWED_HOSTS, ...configured])];
|
|
}
|
|
|
|
function readRouteTable() {
|
|
try {
|
|
return readFileSync(ROUTE_TABLE, 'utf8');
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
async function start() {
|
|
const trustedPeers = resolveTrustedPeers({ routeTable: readRouteTable() });
|
|
if (trustedPeers) {
|
|
// Set before Vite is imported: the dev server snapshots process.env during
|
|
// configuration, and the admission gate reads it per request.
|
|
process.env.GEV_KEY_SETUP_TRUSTED_PEERS = trustedPeers;
|
|
console.log(`[Docker] Provider Settings trusts the container gateway (${trustedPeers}).`);
|
|
} else {
|
|
console.log('[Docker] No default gateway; Provider Settings stays loopback-only.');
|
|
}
|
|
|
|
const port = Number.parseInt(process.env.PORT, 10) || 4173;
|
|
const { createServer } = await loadViteFromCanonicalRoot(ROOT);
|
|
const server = await createServer({
|
|
root: ROOT,
|
|
server: {
|
|
// The container's own namespace. compose publishes this to the host's
|
|
// 127.0.0.1 only; widening that mapping is the LAN opt-in.
|
|
host: '0.0.0.0',
|
|
port,
|
|
strictPort: true,
|
|
allowedHosts: resolveAllowedHosts(),
|
|
},
|
|
});
|
|
await server.listen();
|
|
server.printUrls();
|
|
console.log(`[Docker] Ready at http://localhost:${port}/`);
|
|
|
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
process.once(signal, async () => {
|
|
await server.close();
|
|
process.exit(0);
|
|
});
|
|
}
|
|
}
|
|
|
|
if (isDirectInvocation(process.argv[1], MODULE_PATH)) {
|
|
start().catch((error) => {
|
|
console.error(`[Docker] Start refused: ${error.message}`);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|