mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Release v3.8.41 (#5327)
Release v3.8.41 — 52 commits since v3.8.40 (19 CHANGELOG bullets, 11 contributors). All gating CI green: Unit×8, Coverage×8, Vitest, Package Artifact, Quality Ratchet, CodeQL, Lint, Docs Sync (Strict), Node 24/26 compat, E2E×9, Integration, Electron smoke. Advisory checks overridden (main unprotected): PR Test Policy = test-masking heuristic on the cumulative 52-commit assert delta (legitimate dead-code-sweep removals + consolidations, reviewed per-PR); SonarCloud/SonarQube = new-code maintainability/coverage quality gate (CodeQL/Semgrep/Security/npm-audit/Dependabot all clean — not a security finding).
This commit is contained in:
committed by
GitHub
parent
7c23dab64d
commit
78f09c8d9f
@@ -149,6 +149,12 @@ const EXTRA_MODULE_ENTRIES = [
|
||||
src: ["scripts", "dev", "webdav-handler.mjs"],
|
||||
dest: ["webdav-handler.mjs"],
|
||||
},
|
||||
{
|
||||
// #5242: opt-in HTTPS/TLS resolver (server-ws.mjs dependency).
|
||||
label: "tls-options (server-ws.mjs dependency)",
|
||||
src: ["scripts", "dev", "tls-options.mjs"],
|
||||
dest: ["tls-options.mjs"],
|
||||
},
|
||||
{
|
||||
label: "runtime-env script",
|
||||
src: ["scripts", "build", "runtime-env.mjs"],
|
||||
|
||||
@@ -44,6 +44,7 @@ export const APP_STAGING_ALLOWED_EXACT_PATHS: string[] = [
|
||||
"peer-stamp.mjs",
|
||||
"responses-ws-proxy.mjs",
|
||||
"scripts/dev/sync-env.mjs",
|
||||
"scripts/dev/tls-options.mjs",
|
||||
"server.js",
|
||||
"server-ws.mjs",
|
||||
"webdav-handler.mjs",
|
||||
@@ -111,6 +112,9 @@ export const PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS: string[] = [
|
||||
"scripts/build/sync-env.mjs",
|
||||
"scripts/dev/responses-ws-proxy.mjs",
|
||||
"scripts/dev/sync-env.mjs",
|
||||
// #5361: imported at runtime by bin/cli/commands/serve.mjs + the standalone
|
||||
// server wrapper for opt-in native HTTPS/TLS serving (kept dependency-light).
|
||||
"scripts/dev/tls-options.mjs",
|
||||
"scripts/postinstall.mjs",
|
||||
"src/shared/utils/nodeRuntimeSupport.ts",
|
||||
];
|
||||
|
||||
@@ -36,6 +36,55 @@ export function calibrateHeapFallbackMb(totalmemBytes) {
|
||||
return Math.min(4096, Math.max(512, target));
|
||||
}
|
||||
|
||||
const MAX_OLD_SPACE_FLAG = "--max-old-space-size";
|
||||
|
||||
/**
|
||||
* True when the caller already pinned the V8 heap via NODE_OPTIONS
|
||||
* (`--max-old-space-size=…`). Used to decide whether `omniroute serve` may
|
||||
* append/inject the calibrated default — a user-set value must always win.
|
||||
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
|
||||
*/
|
||||
export function envHasExplicitHeapFlag(env = process.env) {
|
||||
return String(env?.NODE_OPTIONS || "").includes(MAX_OLD_SPACE_FLAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the NODE_OPTIONS string for the spawned server, preserving any flags
|
||||
* the user already exported. #5238: `omniroute serve` used to UNCONDITIONALLY
|
||||
* overwrite NODE_OPTIONS with the calibrated `--max-old-space-size`, silently
|
||||
* discarding a user-set `NODE_OPTIONS=--max-old-space-size=8192` (reporter set
|
||||
* 8192 and still OOM'd at ~505MB). Mirrors the Electron (electron/main.js) and
|
||||
* standalone (scripts/dev/run-standalone.mjs) launchers:
|
||||
* - if NODE_OPTIONS already contains `--max-old-space-size`, keep it as-is
|
||||
* (the user's value wins);
|
||||
* - otherwise append the calibrated `--max-old-space-size=<memoryLimit>` to
|
||||
* the existing NODE_OPTIONS, preserving unrelated flags (e.g.
|
||||
* `--enable-source-maps`).
|
||||
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
|
||||
* @param {number} memoryLimit — calibrated V8 heap ceiling (MB)
|
||||
* @returns {string} the NODE_OPTIONS value to pass to the child process
|
||||
*/
|
||||
export function buildServerNodeOptions(env = process.env, memoryLimit) {
|
||||
const existing = String(env?.NODE_OPTIONS || "").trim();
|
||||
if (existing.includes(MAX_OLD_SPACE_FLAG)) return existing;
|
||||
return `${existing} ${MAX_OLD_SPACE_FLAG}=${memoryLimit}`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the leading `node` CLI args that pin the V8 heap. When the user already
|
||||
* pinned the heap via NODE_OPTIONS, return `[]` so we do NOT inject a
|
||||
* conflicting/shadowing CLI `--max-old-space-size` (CLI args override
|
||||
* NODE_OPTIONS, which would re-introduce #5238). Otherwise return the calibrated
|
||||
* flag — NODE_OPTIONS already carries the same value, so this stays redundant
|
||||
* (identical value), never conflicting.
|
||||
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
|
||||
* @param {number} memoryLimit — calibrated V8 heap ceiling (MB)
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function buildNodeHeapArgs(env = process.env, memoryLimit) {
|
||||
return envHasExplicitHeapFlag(env) ? [] : [`${MAX_OLD_SPACE_FLAG}=${memoryLimit}`];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [fromEnv]
|
||||
* Defaults to process.env. Pass bootstrap `merged` so project `.env` PORT applies before spawn.
|
||||
|
||||
@@ -5,11 +5,23 @@ import { createResponsesWsProxy } from "./responses-ws-proxy.mjs";
|
||||
import { ensurePeerStampToken, wrapRequestListenerWithPeerStamp } from "./peer-stamp.mjs";
|
||||
import { maybeHandleWebdav } from "./webdav-handler.mjs";
|
||||
import methodGuard from "./http-method-guard.cjs";
|
||||
import { resolveTlsOptions, createServerListener } from "./tls-options.mjs";
|
||||
|
||||
const originalCreateServer = http.createServer.bind(http);
|
||||
const proxiesByPort = new Map();
|
||||
const { wrapRequestListenerWithMethodGuard } = methodGuard;
|
||||
|
||||
// Opt-in native HTTPS (#5242). Resolved once at boot: when both OMNIROUTE_TLS_CERT
|
||||
// and OMNIROUTE_TLS_KEY point at readable files we terminate TLS on the same
|
||||
// listener Next binds to (so WS `upgrade` / request wrappers keep working over
|
||||
// TLS). Absent or misconfigured → null → identical plain-HTTP behavior as before.
|
||||
const tlsOptions = resolveTlsOptions(process.env);
|
||||
if (tlsOptions) {
|
||||
console.log(
|
||||
`[omniroute][tls] HTTPS enabled — terminating TLS with cert=${tlsOptions.certPath}`
|
||||
);
|
||||
}
|
||||
|
||||
process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID();
|
||||
// Per-process secret proving the trusted peer-IP stamp came from this server.
|
||||
ensurePeerStampToken();
|
||||
@@ -107,7 +119,10 @@ http.createServer = function createServerWithResponsesWs(...args) {
|
||||
);
|
||||
}
|
||||
|
||||
const server = originalCreateServer(...args);
|
||||
// When TLS is configured, return an https.Server (terminating TLS on the same
|
||||
// listener); otherwise the original http.Server. The downstream .on/.addListener
|
||||
// patches below apply identically to both (https.Server extends http.Server).
|
||||
const server = createServerListener(args, tlsOptions, { createHttp: originalCreateServer });
|
||||
const originalOn = server.on.bind(server);
|
||||
const originalAddListener = server.addListener.bind(server);
|
||||
|
||||
|
||||
92
scripts/dev/tls-options.mjs
Normal file
92
scripts/dev/tls-options.mjs
Normal file
@@ -0,0 +1,92 @@
|
||||
// scripts/dev/tls-options.mjs
|
||||
//
|
||||
// Pure, dependency-light helpers for OmniRoute's opt-in native HTTPS/TLS serving
|
||||
// (#5242, Bug 1C). Kept side-effect-free and free of heavy imports so it can be
|
||||
// imported both by the CLI (bin/cli/commands/serve.mjs) and by the standalone
|
||||
// server wrapper (standalone-server-ws.mjs), and unit-tested in isolation.
|
||||
//
|
||||
// TLS is strictly opt-in: when neither cert nor key is provided the server
|
||||
// behaves EXACTLY as before (plain HTTP). A misconfiguration (only one of the
|
||||
// pair, or an unreadable path) NEVER crashes the server — it logs a warning and
|
||||
// falls back to HTTP, preserving today's behavior and loopback/security posture.
|
||||
|
||||
import fs from "node:fs";
|
||||
import http from "node:http";
|
||||
import https from "node:https";
|
||||
|
||||
/**
|
||||
* Resolve TLS options from environment variables.
|
||||
*
|
||||
* Reads `OMNIROUTE_TLS_CERT` and `OMNIROUTE_TLS_KEY` (filesystem paths). Returns
|
||||
* `{ cert, key }` (file contents) only when BOTH are provided and readable.
|
||||
* Otherwise returns `null` so the caller serves plain HTTP — never throwing.
|
||||
*
|
||||
* @param {NodeJS.ProcessEnv} [env=process.env]
|
||||
* @param {{ readFileSync?: typeof fs.readFileSync, warn?: (msg: string) => void }} [deps]
|
||||
* @returns {{ cert: Buffer|string, key: Buffer|string, certPath: string, keyPath: string } | null}
|
||||
*/
|
||||
export function resolveTlsOptions(
|
||||
env = process.env,
|
||||
{ readFileSync = fs.readFileSync, warn = (m) => console.warn(m) } = {}
|
||||
) {
|
||||
const certPath = typeof env?.OMNIROUTE_TLS_CERT === "string" ? env.OMNIROUTE_TLS_CERT.trim() : "";
|
||||
const keyPath = typeof env?.OMNIROUTE_TLS_KEY === "string" ? env.OMNIROUTE_TLS_KEY.trim() : "";
|
||||
|
||||
// Neither provided → plain HTTP, no warning (the common, default case).
|
||||
if (!certPath && !keyPath) return null;
|
||||
|
||||
// Only one of the pair → never half-enable TLS. Warn + fall back to HTTP.
|
||||
if (!certPath || !keyPath) {
|
||||
warn(
|
||||
`[omniroute][tls] HTTPS not enabled: both OMNIROUTE_TLS_CERT and OMNIROUTE_TLS_KEY ` +
|
||||
`are required (only ${certPath ? "cert" : "key"} provided). Serving HTTP.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Both provided → read them. A bad/unreadable path falls back to HTTP rather
|
||||
// than crashing the server over a TLS misconfiguration.
|
||||
try {
|
||||
const cert = readFileSync(certPath);
|
||||
const key = readFileSync(keyPath);
|
||||
return { cert, key, certPath, keyPath };
|
||||
} catch (err) {
|
||||
warn(
|
||||
`[omniroute][tls] HTTPS not enabled: could not read TLS cert/key ` +
|
||||
`(${err?.code || err?.message || String(err)}). Serving HTTP.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create either an `https.Server` (when `tlsOptions` is provided) or an
|
||||
* `http.Server` (when it is `null`), forwarding the same request-listener /
|
||||
* options arguments the caller would otherwise pass to `http.createServer`.
|
||||
*
|
||||
* When TLS is enabled, any leading options object is merged with `cert`/`key`
|
||||
* and the trailing request listener is preserved, so existing wiring (WebSocket
|
||||
* `upgrade` handling, request wrappers) keeps working over TLS unchanged.
|
||||
*
|
||||
* @param {any[]} args - the args originally passed to http.createServer (options?, listener?)
|
||||
* @param {{ cert: Buffer|string, key: Buffer|string } | null} tlsOptions
|
||||
* @param {{ createHttp?: Function, createHttps?: Function }} [deps]
|
||||
* @returns {import("node:http").Server | import("node:https").Server}
|
||||
*/
|
||||
export function createServerListener(
|
||||
args,
|
||||
tlsOptions,
|
||||
{ createHttp = http.createServer.bind(http), createHttps = https.createServer.bind(https) } = {}
|
||||
) {
|
||||
const argList = Array.isArray(args) ? args : args === undefined ? [] : [args];
|
||||
|
||||
if (!tlsOptions) return createHttp(...argList);
|
||||
|
||||
const { cert, key } = tlsOptions;
|
||||
const lastFnIdx = argList.map((a) => typeof a === "function").lastIndexOf(true);
|
||||
const listener = lastFnIdx >= 0 ? argList[lastFnIdx] : undefined;
|
||||
const baseOpts =
|
||||
argList[0] && typeof argList[0] === "object" && !Buffer.isBuffer(argList[0]) ? argList[0] : {};
|
||||
const merged = { ...baseOpts, cert, key };
|
||||
return listener ? createHttps(merged, listener) : createHttps(merged);
|
||||
}
|
||||
Reference in New Issue
Block a user