дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(settings): HTTP Proxy settings for the IDE's outgoing connections (#194)
* feat(settings): HTTP Proxy settings for the IDE's outgoing connections Add Appearance & Behavior › System Settings › HTTP Proxy with three modes: No proxy, Auto-detect proxy settings (the default; environment proxy as before) and Manual proxy configuration (HTTP or SOCKS, host and port, a comma-separated "No proxy for" list with `*` wildcards, optional proxy authentication). "Check connection…" GETs a URL with the settings as edited and reports the status or the error. - ide-core: `ProxySettings` plus routing (no-proxy globs, loopback always direct, credentials percent-encoded), the current proxy for the process, curl arguments and child-process variables. - ide-http: `client::build` / `client::shared` is the one HTTP client constructor; the HTTP Client requests and the update check go through it. - ide-tools: curl downloads (tools, JDKs, debug adapters, agent registry, remote server) follow the setting. - A manual proxy reaches child processes (language servers, debug adapters, terminals, git) as HTTP_PROXY / HTTPS_PROXY / ALL_PROXY / NO_PROXY. - The proxy password is kept in the owner-only saved passwords file (`proxy-password.json` in the app data directory), not in settings.toml. Co-Authored-By: Claude <[email protected]> * fix(host): Allow the Unix-only post-start test helpers on Windows Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
Цей коміт міститься в:
1 джерело
c8e3fe9d2e
коміт
9d4e778c7b
28 files changed
+1343
-65
No files matched your search
@@ -4685,5 +4685,28 @@
|
||||
"Code insight needs the index; it can't be stopped": "代码洞察依赖索引,无法停止",
|
||||
"Waiting for other tasks": "正在等待其他任务",
|
||||
"This task can't be cancelled": "此任务无法取消",
|
||||
"Cancelled": "已取消"
|
||||
"Cancelled": "已取消",
|
||||
"No proxy": "不使用代理",
|
||||
"Auto-detect proxy settings": "自动检测代理设置",
|
||||
"Uses the system proxy: the HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables.": "使用系统代理:HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY 环境变量。",
|
||||
"Manual proxy configuration": "手动配置代理",
|
||||
"Proxy": "代理",
|
||||
"Proxy type": "代理类型",
|
||||
"Host name": "主机名",
|
||||
"Port number": "端口号",
|
||||
"No proxy for": "不对以下主机使用代理",
|
||||
"Comma-separated host names; * matches any characters, e.g. *.example.com, 192.168.*": "以逗号分隔的主机名;* 匹配任意字符,例如 *.example.com, 192.168.*",
|
||||
"Proxy authentication": "代理身份验证",
|
||||
"Login": "登录名",
|
||||
"Connection successful: {status}": "连接成功:{status}",
|
||||
"Problem with connection: {message}": "连接出现问题:{message}",
|
||||
"Check connection…": "检查连接…",
|
||||
"Check connection": "检查连接",
|
||||
"Enter any URL to check connection to:": "输入要检查连接的任意 URL:",
|
||||
"Check": "检查",
|
||||
"HTTP Proxy": "HTTP 代理",
|
||||
"Proxy host is empty": "代理主机为空",
|
||||
"Invalid proxy host": "代理主机无效",
|
||||
"Proxy port must be between 1 and 65535": "代理端口必须介于 1 和 65535 之间",
|
||||
"Proxy login is empty": "代理登录名为空"
|
||||
}
|
||||
@@ -155,6 +155,7 @@ const UNTRANSLATED = new Set<string>([
|
||||
"GOPATH",
|
||||
"GOROOT",
|
||||
"HEAD",
|
||||
"HTTP",
|
||||
"JDK",
|
||||
"JDK 1.4.x",
|
||||
"JDK 5 - 8",
|
||||
@@ -162,6 +163,7 @@ const UNTRANSLATED = new Set<string>([
|
||||
"NULL",
|
||||
"ORDER BY",
|
||||
"SDK",
|
||||
"SOCKS",
|
||||
"SSH/SSL",
|
||||
"SSL",
|
||||
"TODO",
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { validateSettings } from "@/features/settings/model/schema";
|
||||
import { DEFAULT_PROXY, proxyProblem, validateProxy, withProxyKind, type ProxySettingsValid } from "./proxySettings";
|
||||
|
||||
const manual = (patch: Partial<ProxySettingsValid> = {}): ProxySettingsValid => ({
|
||||
...DEFAULT_PROXY,
|
||||
mode: "manual",
|
||||
host: "proxy.corp",
|
||||
port: 3128,
|
||||
...patch,
|
||||
});
|
||||
|
||||
describe("validateProxy", () => {
|
||||
it("defaults to auto-detect", () => {
|
||||
expect(validateProxy(undefined)).toEqual(DEFAULT_PROXY);
|
||||
expect(DEFAULT_PROXY.mode).toBe("auto");
|
||||
expect(validateSettings({}).proxy).toEqual(DEFAULT_PROXY);
|
||||
});
|
||||
|
||||
it("keeps valid values", () => {
|
||||
const raw = {
|
||||
mode: "manual",
|
||||
kind: "socks",
|
||||
host: "10.0.0.1",
|
||||
port: 1081,
|
||||
noProxy: "*.example.com, 192.168.*",
|
||||
authenticate: true,
|
||||
login: "me",
|
||||
password: "pw",
|
||||
};
|
||||
expect(validateProxy(raw)).toEqual(raw);
|
||||
expect(validateProxy({ mode: "none" }).mode).toBe("none");
|
||||
});
|
||||
|
||||
it("falls back on malformed values", () => {
|
||||
const p = validateProxy({ mode: "pac", kind: "ftp", host: 7, port: 70000, authenticate: "yes", login: null });
|
||||
expect(p).toEqual(DEFAULT_PROXY);
|
||||
expect(validateProxy({ kind: "socks", port: 0 }).port).toBe(1080);
|
||||
expect(validateProxy({ port: 80.5 }).port).toBe(80);
|
||||
expect(validateProxy([]).mode).toBe("auto");
|
||||
});
|
||||
});
|
||||
|
||||
describe("withProxyKind", () => {
|
||||
it("moves a default port along and keeps a custom one", () => {
|
||||
expect(withProxyKind(manual({ port: 80 }), "socks")).toMatchObject({ kind: "socks", port: 1080 });
|
||||
expect(withProxyKind(manual({ kind: "socks", port: 1080 }), "http").port).toBe(80);
|
||||
expect(withProxyKind(manual({ port: 3128 }), "socks").port).toBe(3128);
|
||||
});
|
||||
});
|
||||
|
||||
describe("proxyProblem", () => {
|
||||
it("checks only a manual configuration", () => {
|
||||
expect(proxyProblem({ ...DEFAULT_PROXY, host: "" })).toBeNull();
|
||||
expect(proxyProblem({ ...DEFAULT_PROXY, mode: "none" })).toBeNull();
|
||||
expect(proxyProblem(manual())).toBeNull();
|
||||
});
|
||||
|
||||
it("reports a missing or invalid host, port and login", () => {
|
||||
expect(proxyProblem(manual({ host: " " }))).toBe("Proxy host is empty");
|
||||
expect(proxyProblem(manual({ host: "http://proxy" }))).toBe("Invalid proxy host");
|
||||
expect(proxyProblem(manual({ host: "user@proxy" }))).toBe("Invalid proxy host");
|
||||
expect(proxyProblem(manual({ port: 0 }))).toBe("Proxy port must be between 1 and 65535");
|
||||
expect(proxyProblem(manual({ authenticate: true, login: "" }))).toBe("Proxy login is empty");
|
||||
expect(proxyProblem(manual({ authenticate: true, login: "me", password: "" }))).toBeNull();
|
||||
expect(proxyProblem(manual({ authenticate: false, login: "" }))).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* System Settings › HTTP Proxy as the UI consumes it (mirrors
|
||||
* `ide-core::proxy_settings` defaults). Normalisation never throws.
|
||||
*/
|
||||
export type ProxyModeValid = "none" | "auto" | "manual";
|
||||
export type ProxyKindValid = "http" | "socks";
|
||||
|
||||
export interface ProxySettingsValid {
|
||||
mode: ProxyModeValid;
|
||||
kind: ProxyKindValid;
|
||||
host: string;
|
||||
port: number;
|
||||
/** Comma-separated host globs reached directly (`*.example.com, 192.168.*`). */
|
||||
noProxy: string;
|
||||
authenticate: boolean;
|
||||
login: string;
|
||||
/** Kept in the saved passwords by the backend, not in settings.toml. */
|
||||
password: string;
|
||||
}
|
||||
|
||||
export const PROXY_DEFAULT_PORT: Record<ProxyKindValid, number> = {
|
||||
http: 80,
|
||||
socks: 1080,
|
||||
};
|
||||
export const PROXY_PORT_LIMITS = { min: 1, max: 65535 } as const;
|
||||
|
||||
export const DEFAULT_PROXY: ProxySettingsValid = {
|
||||
mode: "auto",
|
||||
kind: "http",
|
||||
host: "",
|
||||
port: PROXY_DEFAULT_PORT.http,
|
||||
noProxy: "",
|
||||
authenticate: false,
|
||||
login: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
type Obj = Record<string, unknown>;
|
||||
const obj = (v: unknown): Obj => (v && typeof v === "object" && !Array.isArray(v) ? (v as Obj) : {});
|
||||
const str = (v: unknown) => (typeof v === "string" ? v : "");
|
||||
|
||||
export function validateProxy(raw: unknown): ProxySettingsValid {
|
||||
const p = obj(raw);
|
||||
const mode: ProxyModeValid = p.mode === "none" || p.mode === "manual" ? p.mode : "auto";
|
||||
const kind: ProxyKindValid = p.kind === "socks" ? "socks" : "http";
|
||||
const port =
|
||||
typeof p.port === "number" &&
|
||||
Number.isInteger(p.port) &&
|
||||
p.port >= PROXY_PORT_LIMITS.min &&
|
||||
p.port <= PROXY_PORT_LIMITS.max
|
||||
? p.port
|
||||
: PROXY_DEFAULT_PORT[kind];
|
||||
return {
|
||||
mode,
|
||||
kind,
|
||||
host: str(p.host),
|
||||
port,
|
||||
noProxy: str(p.noProxy),
|
||||
authenticate: p.authenticate === true,
|
||||
login: str(p.login),
|
||||
password: str(p.password),
|
||||
};
|
||||
}
|
||||
|
||||
/** Switching HTTP ⇄ SOCKS moves a default port along. */
|
||||
export function withProxyKind(p: ProxySettingsValid, kind: ProxyKindValid): ProxySettingsValid {
|
||||
const port = p.port === PROXY_DEFAULT_PORT[p.kind] ? PROXY_DEFAULT_PORT[kind] : p.port;
|
||||
return { ...p, kind, port };
|
||||
}
|
||||
|
||||
/** What keeps a manual configuration from working (as the backend checks it), or `null`. */
|
||||
export function proxyProblem(p: ProxySettingsValid): string | null {
|
||||
if (p.mode !== "manual") return null;
|
||||
const host = p.host.trim();
|
||||
if (!host) return "Proxy host is empty";
|
||||
if (/[/@\s]/.test(host)) return "Invalid proxy host";
|
||||
if (p.port < PROXY_PORT_LIMITS.min || p.port > PROXY_PORT_LIMITS.max) {
|
||||
return "Proxy port must be between 1 and 65535";
|
||||
}
|
||||
if (p.authenticate && !p.login.trim()) return "Proxy login is empty";
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача