From 27ec6f84799c16531479c35de4d949c6793e0569 Mon Sep 17 00:00:00 2001 From: tcatlas <23132187+tcatlas@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:28:44 +0000 Subject: [PATCH 1/5] example flag to disable automatic self signed cert deployment --- .env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index d357c3f0a..6381c0264 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ ENCRYPTION_KEY=aba3aa8e29b9904d5d8d705230b664c053415c54be20ad13be99af0057dfa23a SERVER_PORT=6989 HTTPS_PORT=5878 -NODE_ENV=development \ No newline at end of file +NODE_ENV=development +# AUTO_SELF_CERT=false \ No newline at end of file From 11869ef42718aad10b8a19926b593806cf82def9 Mon Sep 17 00:00:00 2001 From: tcatlas <23132187+tcatlas@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:29:03 +0000 Subject: [PATCH 2/5] add selfsigned dependency --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 7bc921594..ec41f1ed0 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "openid-client": "^6.8.1", "sequelize": "^6.37.7", "sharp": "^0.34.5", + "selfsigned": "^2.4.1", "speakeasy": "^2.0.0", "sqlite3": "^5.1.7", "ssh2": "^1.17.0", From e4d995977a7002b9001fd3f001a7ca093411752d Mon Sep 17 00:00:00 2001 From: tcatlas <23132187+tcatlas@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:33:00 +0000 Subject: [PATCH 3/5] docs: https env variables --- docs/ssl.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/ssl.md b/docs/ssl.md index e130963f3..def600cdb 100644 --- a/docs/ssl.md +++ b/docs/ssl.md @@ -16,6 +16,13 @@ Nexterm will automatically detect them and start an HTTPS server. You can change the HTTPS port by setting the `HTTPS_PORT` environment variable. +## 📌 Environment Variables + +- `HTTPS_PORT`: HTTPS listener port (default `5878`) +- `SSL_CERT_PATH`: absolute or relative path to `cert.pem` (default `./data/certs/cert.pem`) +- `SSL_KEY_PATH`: absolute or relative path to `key.pem` (default `./data/certs/key.pem`) +- `AUTO_SELF_CERT`: set to `false` to disable auto-generation of self-signed certs + ## 🐳 Docker Setup Add the following to your existing `docker-compose.yml`: From fadbd5d38d901aa467d294e2868c27e20dc7ecae Mon Sep 17 00:00:00 2001 From: tcatlas <23132187+tcatlas@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:58:14 +0000 Subject: [PATCH 4/5] automatic self-signed cert generation --- client/vite.config.js | 85 +++++++++++++++++++++++++++++++++++++++++-- server/index.js | 76 ++++++++++++++++++++++++-------------- server/utils/ssl.js | 47 ++++++++++++++++++++++++ yarn.lock | 20 ++++++++++ 4 files changed, 198 insertions(+), 30 deletions(-) create mode 100644 server/utils/ssl.js diff --git a/client/vite.config.js b/client/vite.config.js index cc0503f72..049cc4bdd 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -2,6 +2,59 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import * as path from "path"; import * as fs from "fs"; +import * as http from "node:http"; +import * as https from "node:https"; +import { createRequire } from "node:module"; + +const ensureUpgradeCallback = (ServerCtor) => { + if (!ServerCtor?.prototype) return; + if (typeof ServerCtor.prototype.shouldUpgradeCallback !== "function") { + Object.defineProperty(ServerCtor.prototype, "shouldUpgradeCallback", { + value: () => true, + writable: true, + configurable: true + }); + } +}; + +ensureUpgradeCallback(http.Server); +ensureUpgradeCallback(https.Server); + +const require = createRequire(import.meta.url); + +const DEFAULT_CERT_PATH = process.env.SSL_CERT_PATH || path.resolve(__dirname, "../data/certs/cert.pem"); +const DEFAULT_KEY_PATH = process.env.SSL_KEY_PATH || path.resolve(__dirname, "../data/certs/key.pem"); + +const loadOrCreateDevCerts = () => { + if (fs.existsSync(DEFAULT_CERT_PATH) && fs.existsSync(DEFAULT_KEY_PATH)) { + return { + cert: fs.readFileSync(DEFAULT_CERT_PATH), + key: fs.readFileSync(DEFAULT_KEY_PATH) + }; + } + + const selfsigned = require("selfsigned"); + fs.mkdirSync(path.dirname(DEFAULT_CERT_PATH), { recursive: true }); + fs.mkdirSync(path.dirname(DEFAULT_KEY_PATH), { recursive: true }); + + const attrs = [{ name: "commonName", value: "localhost" }]; + const pems = selfsigned.generate(attrs, { + days: 3650, + keySize: 2048, + algorithm: "sha256", + extensions: [ + { + name: "subjectAltName", + altNames: [{ type: 2, value: "localhost" }] + } + ] + }); + + fs.writeFileSync(DEFAULT_CERT_PATH, pems.cert, { mode: 0o644 }); + fs.writeFileSync(DEFAULT_KEY_PATH, pems.private, { mode: 0o600 }); + + return { cert: pems.cert, key: pems.private }; +}; const guacamolePlugin = () => { const modulesDir = path.resolve(__dirname, '../vendor/guacamole-client/guacamole-common-js/src/main/webapp/modules'); @@ -23,8 +76,32 @@ const guacamolePlugin = () => { }; } +const fixUpgradeCallbackPlugin = () => ({ + name: "fix-upgrade-callback", + configureServer(server) { + const httpServer = server.httpServer; + if (!httpServer) return; + + const defineUpgradeCallback = () => { + Object.defineProperty(httpServer, "shouldUpgradeCallback", { + configurable: true, + get: () => () => true, + set: () => {} + }); + }; + + defineUpgradeCallback(); + + httpServer.on("upgrade", () => { + if (typeof httpServer.shouldUpgradeCallback !== "function") { + defineUpgradeCallback(); + } + }); + } +}); + export default defineConfig({ - plugins: [guacamolePlugin(), react()], + plugins: [guacamolePlugin(), fixUpgradeCallbackPlugin(), react()], css: { preprocessorOptions: { sass: { @@ -38,10 +115,12 @@ export default defineConfig({ } }, server: { + https: loadOrCreateDevCerts(), proxy: { "/api": { - target: "http://localhost:6989", - ws: true + target: "https://localhost:5878", + ws: true, + secure: false } } } diff --git a/server/index.js b/server/index.js index ce8ee8ecd..d63e57a6b 100644 --- a/server/index.js +++ b/server/index.js @@ -18,20 +18,25 @@ const recordingService = require("./utils/recordingService"); const { generateOpenAPISpec } = require("./openapi"); const { isAdmin } = require("./middlewares/permission"); const logger = require("./utils/logger"); +const { ensureSelfSignedCerts } = require("./utils/ssl"); const { startSourceSyncService, stopSourceSyncService } = require("./utils/sourceSyncService"); const backupService = require("./utils/backupService"); require("./utils/folder"); process.on("uncaughtException", (err) => require("./utils/errorHandling")(err)); -const APP_PORT = process.env.SERVER_PORT || 6989; const HTTPS_PORT = process.env.HTTPS_PORT || 5878; - -const CERTS_DIR = path.join(__dirname, "../data/certs"); -const CERT_PATH = path.join(CERTS_DIR, "cert.pem"); -const KEY_PATH = path.join(CERTS_DIR, "key.pem"); - -const hasSSLCerts = () => fs.existsSync(CERT_PATH) && fs.existsSync(KEY_PATH); +const AUTO_SELF_CERT_ENABLED = process.env.AUTO_SELF_CERT !== "false"; + +const DEFAULT_CERTS_DIR = path.join(__dirname, "../data/certs"); +const CERT_PATH = process.env.SSL_CERT_PATH + ? path.resolve(process.env.SSL_CERT_PATH) + : path.join(DEFAULT_CERTS_DIR, "cert.pem"); +const KEY_PATH = process.env.SSL_KEY_PATH + ? path.resolve(process.env.SSL_KEY_PATH) + : path.join(DEFAULT_CERTS_DIR, "key.pem"); +const CERTS_DIR = path.dirname(CERT_PATH); +const KEY_DIR = path.dirname(KEY_PATH); const app = expressWs(express()).app; @@ -118,26 +123,43 @@ db.authenticate() backupService.start(); - app.listen(APP_PORT, () => - logger.system(`Server listening on port ${APP_PORT}`) - ); - - if (hasSSLCerts()) { - try { - const sslOptions = { - cert: fs.readFileSync(CERT_PATH), - key: fs.readFileSync(KEY_PATH) - }; - - const httpsServer = https.createServer(sslOptions, app); - expressWs(app, httpsServer); - - httpsServer.listen(HTTPS_PORT, () => - logger.system(`HTTPS server listening on port ${HTTPS_PORT}`) - ); - } catch (err) { - logger.error("Failed to start HTTPS server", { error: err.message }); - } + const certStatus = ensureSelfSignedCerts({ + certPath: CERT_PATH, + keyPath: KEY_PATH, + certsDir: CERTS_DIR, + keyDir: KEY_DIR, + autoEnabled: AUTO_SELF_CERT_ENABLED + }); + + if (certStatus.status === "partial") { + logger.error("TLS certificate or key missing. Provide both cert.pem and key.pem in data/certs."); + process.exit(112); + } + + if (certStatus.status === "missing" && certStatus.autoDisabled) { + logger.error("TLS certificates missing and AUTO_SELF_CERT is disabled."); + process.exit(112); + } + + if (certStatus.status === "error") { + process.exit(112); + } + + try { + const sslOptions = { + cert: fs.readFileSync(CERT_PATH), + key: fs.readFileSync(KEY_PATH) + }; + + const httpsServer = https.createServer(sslOptions, app); + expressWs(app, httpsServer); + + httpsServer.listen(HTTPS_PORT, () => + logger.system(`HTTPS server listening on port ${HTTPS_PORT}`) + ); + } catch (err) { + logger.error("Failed to start HTTPS server", { error: err.message }); + process.exit(112); } }); diff --git a/server/utils/ssl.js b/server/utils/ssl.js new file mode 100644 index 000000000..fe5a49da1 --- /dev/null +++ b/server/utils/ssl.js @@ -0,0 +1,47 @@ +const fs = require("fs"); +const selfsigned = require("selfsigned"); +const logger = require("./logger"); + +const TEN_YEARS_IN_DAYS = 3650; + +const ensureSelfSignedCerts = ({ certPath, keyPath, certsDir, keyDir, autoEnabled }) => { + const certExists = fs.existsSync(certPath); + const keyExists = fs.existsSync(keyPath); + + if (certExists && keyExists) return { status: "existing" }; + + if (!certExists && !keyExists) { + if (!autoEnabled) return { status: "missing", autoDisabled: true }; + + try { + if (certsDir) fs.mkdirSync(certsDir, { recursive: true }); + if (keyDir && keyDir !== certsDir) fs.mkdirSync(keyDir, { recursive: true }); + + const attrs = [{ name: "commonName", value: "localhost" }]; + const pems = selfsigned.generate(attrs, { + days: TEN_YEARS_IN_DAYS, + keySize: 2048, + algorithm: "sha256", + extensions: [ + { + name: "subjectAltName", + altNames: [{ type: 2, value: "localhost" }] + } + ] + }); + + fs.writeFileSync(certPath, pems.cert, { mode: 0o644 }); + fs.writeFileSync(keyPath, pems.private, { mode: 0o600 }); + + logger.system("Generated self-signed TLS certificate", { certPath, keyPath }); + return { status: "generated" }; + } catch (error) { + logger.error("Failed to generate self-signed TLS certificate", { error: error.message }); + return { status: "error", error }; + } + } + + return { status: "partial" }; +}; + +module.exports = { ensureSelfSignedCerts }; diff --git a/yarn.lock b/yarn.lock index 3fa2ebcba..936c15cce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1046,6 +1046,13 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== +"@types/node-forge@^1.3.0": + version "1.3.14" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.14.tgz#006c2616ccd65550560c2757d8472eb6d3ecea0b" + integrity sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw== + dependencies: + "@types/node" "*" + "@types/node@*": version "25.0.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.3.tgz#79b9ac8318f373fbfaaf6e2784893efa9701f269" @@ -3398,6 +3405,11 @@ node-fetch@^3.3.0, node-fetch@^3.3.2: fetch-blob "^3.1.4" formdata-polyfill "^4.0.10" +node-forge@^1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751" + integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg== + node-gyp-build@^4.8.4: version "4.8.4" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" @@ -3962,6 +3974,14 @@ seek-bzip@^1.0.5: dependencies: commander "^2.8.1" +selfsigned@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.7.3: version "7.7.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" From af513bce6ef21a3cb2e71e93008757648a11db1e Mon Sep 17 00:00:00 2001 From: tcatlas <23132187+tcatlas@users.noreply.github.com> Date: Fri, 6 Feb 2026 23:17:20 +0000 Subject: [PATCH 5/5] dont generate certs if .env says not to note: in the dev environment the vite backend will fail without a cert --- client/vite.config.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/vite.config.js b/client/vite.config.js index 049cc4bdd..7b24f7b50 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -5,6 +5,7 @@ import * as fs from "fs"; import * as http from "node:http"; import * as https from "node:https"; import { createRequire } from "node:module"; +import dotenv from "dotenv"; const ensureUpgradeCallback = (ServerCtor) => { if (!ServerCtor?.prototype) return; @@ -20,6 +21,9 @@ const ensureUpgradeCallback = (ServerCtor) => { ensureUpgradeCallback(http.Server); ensureUpgradeCallback(https.Server); +dotenv.config({ path: path.resolve(__dirname, "../.env"), override: false }); +dotenv.config({ path: path.resolve(__dirname, ".env"), override: false }); + const require = createRequire(import.meta.url); const DEFAULT_CERT_PATH = process.env.SSL_CERT_PATH || path.resolve(__dirname, "../data/certs/cert.pem"); @@ -33,6 +37,10 @@ const loadOrCreateDevCerts = () => { }; } + if (process.env.AUTO_SELF_CERT === "false") { + throw new Error("Dev HTTPS certs missing and AUTO_SELF_CERT=false"); + } + const selfsigned = require("selfsigned"); fs.mkdirSync(path.dirname(DEFAULT_CERT_PATH), { recursive: true }); fs.mkdirSync(path.dirname(DEFAULT_KEY_PATH), { recursive: true });