Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ENCRYPTION_KEY=aba3aa8e29b9904d5d8d705230b664c053415c54be20ad13be99af0057dfa23a
SERVER_PORT=6989
HTTPS_PORT=5878
NODE_ENV=development
NODE_ENV=development
# AUTO_SELF_CERT=false
93 changes: 90 additions & 3 deletions client/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,67 @@ 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";
import dotenv from "dotenv";

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);

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");
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)
};
}

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 });

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');
Expand All @@ -23,8 +84,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: {
Expand All @@ -38,10 +123,12 @@ export default defineConfig({
}
},
server: {
https: loadOrCreateDevCerts(),
proxy: {
"/api": {
target: "http://localhost:6989",
ws: true
target: "https://localhost:5878",
ws: true,
secure: false
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions docs/ssl.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
76 changes: 49 additions & 27 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
});

Expand Down
47 changes: 47 additions & 0 deletions server/utils/ssl.js
Original file line number Diff line number Diff line change
@@ -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 };
20 changes: 20 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down