From 37b6193616dbbd826a53113cc82257393053268a Mon Sep 17 00:00:00 2001 From: Abraham Ingersoll <586805+aberoham@users.noreply.github.com> Date: Thu, 9 Apr 2026 01:26:31 +0100 Subject: [PATCH 1/2] wip: docker support + NICTOOL_BIND_HOST override Dockerfile and entrypoint in docker/ for container deployments. The entrypoint generates nictool.toml from env vars (remote API mode) and lets the server auto-generate self-signed TLS on first start. NICTOOL_BIND_HOST env var overrides the listen address so the server can bind 0.0.0.0 inside a container while keeping the TLS cert hostname for display. Co-Authored-By: Claude Opus 4.6 (1M context) --- docker/Dockerfile | 10 ++++++++++ docker/entrypoint.sh | 27 +++++++++++++++++++++++++++ index.js | 2 +- 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 docker/Dockerfile create mode 100644 docker/entrypoint.sh diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..07cb773 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,10 @@ +FROM node:22-trixie-slim +RUN apt-get update && apt-get install -y --no-install-recommends openssl && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY package*.json . +RUN npm install --omit=dev +COPY . . +RUN chmod +x docker/entrypoint.sh \ + && ln -s node_modules/@nictool/api/conf.d conf.d +EXPOSE 8443 +ENTRYPOINT ["./docker/entrypoint.sh"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..5e74057 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/sh +set -e + +TOML="/data/etc/nictool.toml" +mkdir -p /data/etc + +if [ ! -f "$TOML" ]; then + cat > "$TOML" < { server.once('error', reject) - server.listen(port, host, resolve) + server.listen(port, process.env.NICTOOL_BIND_HOST || host, resolve) }) const url = `https://${host}${port === 443 ? '' : `:${port}`}` From f2be8084cbea5843b24ff9e696d7cdff1dc5265b Mon Sep 17 00:00:00 2001 From: Abraham Ingersoll <586805+aberoham@users.noreply.github.com> Date: Thu, 9 Apr 2026 22:39:26 +0100 Subject: [PATCH 2/2] feat: HTTP dev mode, Groups tab, fix proxy crashes NICTOOL_TLS=false gives you plain HTTP on 8080 -- no more self-signed cert dance in local dev. Left "auto" as the default for production (discovers or generates certs as before). Added a Groups tab to the web UI: list, create, edit, delete, restore, and you can click a group name to navigate into it. Follows the same patterns as the Users/Nameservers tabs. Fixed a few things that were crashing the server: - buildRemoteUrl assumed non-localhost meant https, which blew up with EPROTO when proxying to the plain-HTTP API container. Defaults to http now, configurable via scheme. - forwardToRemote was missing an await so proxy errors bypassed the try/catch entirely - added clientError/tlsClientError/connection handlers plus process-level uncaughtException/unhandledRejection for EPROTO so a stray bad request can't take down the server Dockerfile now exposes both 8080 and 8443. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/start.js | 62 ++++++++++---- docker/Dockerfile | 2 +- html/index.html | 81 ++++++++++++++++++ html/js/main.js | 205 ++++++++++++++++++++++++++++++++++++++++++++++ index.js | 22 +++-- 5 files changed, 350 insertions(+), 22 deletions(-) diff --git a/bin/start.js b/bin/start.js index 2a5d969..0768ef4 100644 --- a/bin/start.js +++ b/bin/start.js @@ -15,6 +15,28 @@ import { init as initAPI } from '@nictool/api/routes/index.js' const execFileAsync = promisify(execFile) +// --------------------------------------------------------------------------- +// Prevent stray TLS errors (e.g. plain HTTP hitting the HTTPS port) from +// crashing the process. Only EPROTO is swallowed; everything else exits. +// --------------------------------------------------------------------------- +process.on('uncaughtException', (err) => { + if (err.code === 'EPROTO') { + console.error(`[uncaughtException] TLS error (ignored): ${err.message}`) + return + } + console.error('[uncaughtException] Fatal:', err) + process.exit(1) +}) + +process.on('unhandledRejection', (reason) => { + if (reason?.code === 'EPROTO') { + console.error(`[unhandledRejection] TLS error (ignored): ${reason.message}`) + return + } + console.error('[unhandledRejection] Fatal:', reason) + process.exit(1) +}) + // --------------------------------------------------------------------------- // CLI arguments // --------------------------------------------------------------------------- @@ -55,23 +77,29 @@ try { } // --------------------------------------------------------------------------- -// TLS – discover existing certs or generate a self-signed one +// TLS – skip entirely in development mode, otherwise discover or generate // --------------------------------------------------------------------------- +const useTLS = (process.env.NICTOOL_TLS ?? 'auto') !== 'false' const osHostname = os.hostname() -const tlsDir = path.join(configDir, 'etc', 'tls') - -const discovered = await discoverTLS(tlsDir, osHostname) -let tls, host +let tls = null +let host = osHostname -if (discovered) { - const { hostname: certHost, ...pemMaterial } = discovered - tls = pemMaterial - host = certHost +if (!useTLS) { + console.log('TLS disabled (NICTOOL_TLS=false) — running plain HTTP') + host = 'localhost' } else { - console.log(`Generating self-signed cert for ${osHostname}`) - tls = await generateTLS(tlsDir, osHostname) - host = osHostname + const tlsDir = path.join(configDir, 'etc', 'tls') + const discovered = await discoverTLS(tlsDir, osHostname) + + if (discovered) { + const { hostname: certHost, ...pemMaterial } = discovered + tls = pemMaterial + host = certHost + } else { + console.log(`Generating self-signed cert for ${osHostname}`) + tls = await generateTLS(tlsDir, osHostname) + } } // --------------------------------------------------------------------------- @@ -82,10 +110,12 @@ const tomlPath = path.join(configDir, 'etc', 'nictool.toml') const nicConfig = await readNicToolToml(tomlPath) // --------------------------------------------------------------------------- -// Port selection – prefer 443, fall back to 8443 +// Port selection – HTTP prefers 8080, HTTPS prefers 443/8443 // --------------------------------------------------------------------------- -const port = (await resolvePort(host, 443)) ?? (await resolvePort(host, 8443)) ?? (await randomAvailablePort(host)) +const port = useTLS + ? ((await resolvePort(host, 443)) ?? (await resolvePort(host, 8443)) ?? (await randomAvailablePort(host))) + : ((await resolvePort(host, 8080)) ?? (await resolvePort(host, 80)) ?? (await randomAvailablePort(host))) // --------------------------------------------------------------------------- // If already configured, skip the configurator and go straight to services @@ -237,9 +267,9 @@ function storeTypeToEnv(type) { */ function buildRemoteUrl(config) { if (!config?.api || config.api.mode !== 'remote') return null - const { host, port } = config.api + const { host, port, scheme: configScheme } = config.api if (!host || !port) return null - const scheme = /^(localhost|127\.|::1)/.test(host) ? 'http' : 'https' + const scheme = configScheme ?? 'http' return `${scheme}://${host}:${port}` } diff --git a/docker/Dockerfile b/docker/Dockerfile index 07cb773..7f23ed6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,5 +6,5 @@ RUN npm install --omit=dev COPY . . RUN chmod +x docker/entrypoint.sh \ && ln -s node_modules/@nictool/api/conf.d conf.d -EXPOSE 8443 +EXPOSE 8080 8443 ENTRYPOINT ["./docker/entrypoint.sh"] diff --git a/html/index.html b/html/index.html index ac7dab9..a182ef5 100644 --- a/html/index.html +++ b/html/index.html @@ -84,6 +84,18 @@ aria-selected="false" >Users +