[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
255
electron/bridges/httpNetworkProxyBridge.cjs
Normal file
255
electron/bridges/httpNetworkProxyBridge.cjs
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* HTTP network proxy bridge — applies app-level proxy for Chromium sessions
|
||||
* (electron.net.fetch used by Google/OneDrive/GitHub OAuth) and Node HTTP stacks
|
||||
* (AI providerHandlers, WebDAV, S3, electron-updater).
|
||||
*
|
||||
* Not related to SSH ProxyJump / ProxyCommand vault profiles.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const DEFAULT_SETTINGS = Object.freeze({
|
||||
mode: "system",
|
||||
url: "",
|
||||
bypass: "<local>",
|
||||
});
|
||||
|
||||
const PROXY_ENV_KEYS = [
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"NO_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"no_proxy",
|
||||
];
|
||||
|
||||
const VALID_MODES = new Set(["system", "direct", "custom"]);
|
||||
|
||||
function asTrimmedString(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip userinfo from proxy URLs without rewriting the rest of the string.
|
||||
* Electron proxyRules does not support credentials. Avoid `new URL()` so
|
||||
* incomplete drafts like `http://127.0.0.1:` keep the trailing colon.
|
||||
*/
|
||||
function sanitizeProxyUrl(proxyUrl) {
|
||||
const trimmed = asTrimmedString(proxyUrl);
|
||||
if (!trimmed) return "";
|
||||
// Strip userinfo for both scheme URLs and scheme-less drafts.
|
||||
return trimmed.replace(/^([a-z][a-z0-9+.-]*:\/\/)?([^/?#]*@)/i, "$1");
|
||||
}
|
||||
|
||||
function normalizeProxySettingsPayload(raw) {
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
|
||||
const modeRaw = asTrimmedString(raw.mode);
|
||||
const mode = VALID_MODES.has(modeRaw) ? modeRaw : "system";
|
||||
const url = sanitizeProxyUrl(raw.url);
|
||||
const bypass = asTrimmedString(raw.bypass) || DEFAULT_SETTINGS.bypass;
|
||||
|
||||
if (mode === "system") {
|
||||
return { mode: "system", url: "", bypass: DEFAULT_SETTINGS.bypass };
|
||||
}
|
||||
if (mode === "direct") {
|
||||
return { mode: "direct", url: "", bypass: DEFAULT_SETTINGS.bypass };
|
||||
}
|
||||
// Keep custom+empty as a draft so the settings UI can show URL fields.
|
||||
return { mode: "custom", url, bypass };
|
||||
}
|
||||
|
||||
/** Effective mode for Electron/env apply: empty custom acts like system. */
|
||||
function effectiveApplySettings(settings) {
|
||||
if (settings.mode === "custom" && !settings.url) {
|
||||
return { mode: "system", url: "", bypass: DEFAULT_SETTINGS.bypass };
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
function buildElectronProxyConfigFromPayload(raw) {
|
||||
const settings = effectiveApplySettings(normalizeProxySettingsPayload(raw));
|
||||
if (settings.mode === "direct") return { mode: "direct" };
|
||||
if (settings.mode === "custom") {
|
||||
return {
|
||||
mode: "fixed_servers",
|
||||
proxyRules: settings.url,
|
||||
proxyBypassRules: settings.bypass || DEFAULT_SETTINGS.bypass,
|
||||
};
|
||||
}
|
||||
return { mode: "system" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip userinfo from a proxy URL before writing to process.env so local
|
||||
* terminals / spawned tools do not inherit proxy credentials.
|
||||
* (normalizeProxySettingsPayload already strips credentials; this is defense in depth.)
|
||||
*/
|
||||
function sanitizeProxyUrlForEnv(proxyUrl) {
|
||||
return sanitizeProxyUrl(proxyUrl);
|
||||
}
|
||||
|
||||
/** Snapshot of env values before this feature last wrote them (WeakMap by env object). */
|
||||
const ownedEnvSnapshots = new WeakMap();
|
||||
let applyGeneration = 0;
|
||||
let applyChain = Promise.resolve();
|
||||
|
||||
function captureEnvSnapshot(env) {
|
||||
const snapshot = {};
|
||||
for (const key of PROXY_ENV_KEYS) {
|
||||
snapshot[key] = Object.prototype.hasOwnProperty.call(env, key) ? env[key] : undefined;
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function restoreEnvSnapshot(env, snapshot) {
|
||||
if (!snapshot) return;
|
||||
for (const key of PROXY_ENV_KEYS) {
|
||||
if (snapshot[key] === undefined) {
|
||||
delete env[key];
|
||||
} else {
|
||||
env[key] = snapshot[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply or clear Node proxy env vars on `env` (defaults to process.env).
|
||||
*
|
||||
* - custom (with URL): snapshot prior values once, then write redacted proxy URL/bypass
|
||||
* - direct: snapshot prior values once (if needed), then clear proxy env
|
||||
* - system / empty custom: restore the snapshot captured before this feature mutated env
|
||||
*
|
||||
* Credentials in the proxy URL are stripped before writing to env so child
|
||||
* processes (local terminals) do not inherit user:pass.
|
||||
*/
|
||||
function applyNodeProxyEnv(raw, env = process.env) {
|
||||
const settings = effectiveApplySettings(normalizeProxySettingsPayload(raw));
|
||||
|
||||
if (settings.mode === "system") {
|
||||
const snapshot = ownedEnvSnapshots.get(env);
|
||||
if (snapshot) {
|
||||
restoreEnvSnapshot(env, snapshot);
|
||||
ownedEnvSnapshots.delete(env);
|
||||
}
|
||||
return { applied: false, settings };
|
||||
}
|
||||
|
||||
if (!ownedEnvSnapshots.has(env)) {
|
||||
ownedEnvSnapshots.set(env, captureEnvSnapshot(env));
|
||||
}
|
||||
|
||||
if (settings.mode === "direct") {
|
||||
for (const key of PROXY_ENV_KEYS) {
|
||||
delete env[key];
|
||||
}
|
||||
return { applied: true, settings };
|
||||
}
|
||||
|
||||
const safeUrl = sanitizeProxyUrlForEnv(settings.url);
|
||||
env.HTTP_PROXY = safeUrl;
|
||||
env.HTTPS_PROXY = safeUrl;
|
||||
env.NO_PROXY = settings.bypass || "";
|
||||
env.http_proxy = safeUrl;
|
||||
env.https_proxy = safeUrl;
|
||||
env.no_proxy = settings.bypass || "";
|
||||
return { applied: true, settings };
|
||||
}
|
||||
|
||||
function resetProxyEnvOwnershipForTests() {
|
||||
// WeakMap cannot be cleared; tests use fresh env objects.
|
||||
applyGeneration = 0;
|
||||
applyChain = Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone `baseEnv` for local terminal / PTY children, restoring the proxy env
|
||||
* values that existed before this feature mutated process.env.
|
||||
*
|
||||
* App-level Direct/Custom modes intentionally rewrite process.env for Node
|
||||
* HTTP clients (AI, updater). Terminals should keep the user's launch-time
|
||||
* HTTP(S)_PROXY / NO_PROXY instead of inheriting those app-only overrides.
|
||||
*/
|
||||
function buildTerminalProcessEnv(baseEnv = process.env) {
|
||||
const env = { ...baseEnv };
|
||||
const snapshot = ownedEnvSnapshots.get(baseEnv) || ownedEnvSnapshots.get(process.env);
|
||||
if (snapshot) {
|
||||
restoreEnvSnapshot(env, snapshot);
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
let currentSettings = { ...DEFAULT_SETTINGS };
|
||||
|
||||
function getCurrentProxySettings() {
|
||||
return { ...currentSettings };
|
||||
}
|
||||
|
||||
async function applyHttpNetworkProxy(raw, deps = {}) {
|
||||
const settings = normalizeProxySettingsPayload(raw);
|
||||
const electronConfig = buildElectronProxyConfigFromPayload(settings);
|
||||
const generation = ++applyGeneration;
|
||||
const env = deps.env || process.env;
|
||||
|
||||
// Serialize setProxy so an older slow call cannot overwrite a newer one.
|
||||
const previous = applyChain;
|
||||
let release;
|
||||
applyChain = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
|
||||
try {
|
||||
await previous.catch(() => {});
|
||||
if (generation !== applyGeneration) {
|
||||
return { success: false, settings, electronConfig, superseded: true };
|
||||
}
|
||||
|
||||
const session =
|
||||
deps.session ||
|
||||
deps.electronModule?.session?.defaultSession ||
|
||||
null;
|
||||
|
||||
if (session?.setProxy) {
|
||||
await session.setProxy(electronConfig);
|
||||
}
|
||||
|
||||
if (generation !== applyGeneration) {
|
||||
return { success: false, settings, electronConfig, superseded: true };
|
||||
}
|
||||
|
||||
currentSettings = settings;
|
||||
applyNodeProxyEnv(settings, env);
|
||||
return { success: true, settings, electronConfig };
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Electron.IpcMain} ipcMain
|
||||
* @param {import('electron')=} electronModule
|
||||
*/
|
||||
function registerHandlers(ipcMain, electronModule) {
|
||||
ipcMain.handle("netcatty:networkProxy:set", async (_event, payload) => {
|
||||
return applyHttpNetworkProxy(payload, { electronModule });
|
||||
});
|
||||
|
||||
ipcMain.handle("netcatty:networkProxy:get", async () => {
|
||||
return { settings: getCurrentProxySettings() };
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_SETTINGS,
|
||||
normalizeProxySettingsPayload,
|
||||
buildElectronProxyConfigFromPayload,
|
||||
applyNodeProxyEnv,
|
||||
applyHttpNetworkProxy,
|
||||
buildTerminalProcessEnv,
|
||||
getCurrentProxySettings,
|
||||
resetProxyEnvOwnershipForTests,
|
||||
sanitizeProxyUrlForEnv,
|
||||
registerHandlers,
|
||||
};
|
||||
Reference in New Issue
Block a user