mirror of
https://github.com/certd/certd.git
synced 2026-06-10 18:57:33 +08:00
perf(settings): 新增NO_PROXY代理排除配置
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { expect } from "chai";
|
||||
import { createAxiosService, HttpClient, setGlobalHeaders } from "./util.request.js";
|
||||
import { createAgent, createAxiosService, getGlobalAgents, HttpClient, isNoProxyMatched, setGlobalHeaders, setGlobalProxy } from "./util.request.js";
|
||||
import { ILogger } from "./util.log.js";
|
||||
|
||||
const testLogger = {
|
||||
debug() {},
|
||||
info() {},
|
||||
error() {},
|
||||
} as unknown as ILogger;
|
||||
@@ -10,6 +11,9 @@ const testLogger = {
|
||||
describe("util.request", () => {
|
||||
afterEach(() => {
|
||||
setGlobalHeaders({});
|
||||
setGlobalProxy({});
|
||||
delete process.env.NO_PROXY;
|
||||
delete process.env.no_proxy;
|
||||
});
|
||||
|
||||
it("should merge global headers without overriding request headers", async () => {
|
||||
@@ -50,4 +54,122 @@ describe("util.request", () => {
|
||||
request: "request",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set no_proxy environment variables", () => {
|
||||
setGlobalProxy({
|
||||
httpProxy: "http://127.0.0.1:1080",
|
||||
httpsProxy: "http://127.0.0.1:1080",
|
||||
noProxy: "localhost,*.internal.example.com",
|
||||
});
|
||||
|
||||
expect(process.env.NO_PROXY).to.equal("localhost,*.internal.example.com");
|
||||
expect(process.env.no_proxy).to.equal("localhost,*.internal.example.com");
|
||||
});
|
||||
|
||||
it("should normalize multiline no_proxy environment variables", () => {
|
||||
setGlobalProxy({
|
||||
noProxy: "localhost\n127.0.0.1, 192.168.*\n*.internal.example.com",
|
||||
});
|
||||
|
||||
expect(process.env.NO_PROXY).to.equal("localhost,127.0.0.1,192.168.*,*.internal.example.com");
|
||||
expect(process.env.no_proxy).to.equal("localhost,127.0.0.1,192.168.*,*.internal.example.com");
|
||||
});
|
||||
|
||||
it("should not change environment variables when creating agents", () => {
|
||||
process.env.HTTP_PROXY = "http://old-http-proxy";
|
||||
process.env.HTTPS_PROXY = "http://old-https-proxy";
|
||||
process.env.NO_PROXY = "old.local";
|
||||
|
||||
createAgent({
|
||||
httpProxy: "http://127.0.0.1:1080",
|
||||
httpsProxy: "http://127.0.0.1:1081",
|
||||
});
|
||||
|
||||
expect(process.env.HTTP_PROXY).to.equal("http://old-http-proxy");
|
||||
expect(process.env.HTTPS_PROXY).to.equal("http://old-https-proxy");
|
||||
expect(process.env.NO_PROXY).to.equal("old.local");
|
||||
});
|
||||
|
||||
it("should bypass global proxy when request host matches no_proxy", async () => {
|
||||
setGlobalProxy({
|
||||
httpProxy: "http://127.0.0.1:1080",
|
||||
httpsProxy: "http://127.0.0.1:1080",
|
||||
noProxy: "localhost,.internal.example.com",
|
||||
});
|
||||
|
||||
const globalAgents = getGlobalAgents();
|
||||
const http = createAxiosService({ logger: testLogger }) as HttpClient;
|
||||
const res = await http.request({
|
||||
url: "https://api.internal.example.com",
|
||||
method: "get",
|
||||
logReq: false,
|
||||
logRes: false,
|
||||
adapter: async config => {
|
||||
return {
|
||||
config,
|
||||
data: {
|
||||
usesGlobalHttpAgent: config.httpAgent === globalAgents.httpAgent,
|
||||
usesGlobalHttpsAgent: config.httpsAgent === globalAgents.httpsAgent,
|
||||
},
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(res).to.deep.equal({
|
||||
usesGlobalHttpAgent: false,
|
||||
usesGlobalHttpsAgent: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should bypass custom request proxy when request host matches no_proxy", async () => {
|
||||
setGlobalProxy({
|
||||
noProxy: ".internal.example.com",
|
||||
});
|
||||
|
||||
const http = createAxiosService({ logger: testLogger }) as HttpClient;
|
||||
const res = await http.request({
|
||||
url: "https://api.internal.example.com",
|
||||
method: "get",
|
||||
httpProxy: "http://127.0.0.1:1080",
|
||||
logReq: false,
|
||||
logRes: false,
|
||||
adapter: async config => {
|
||||
return {
|
||||
config,
|
||||
data: {
|
||||
httpAgent: config.httpAgent?.constructor?.name,
|
||||
httpsAgent: config.httpsAgent?.constructor?.name,
|
||||
},
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(res).to.deep.equal({
|
||||
httpAgent: "Agent",
|
||||
httpsAgent: "Agent",
|
||||
});
|
||||
});
|
||||
|
||||
it("should match no_proxy rules", () => {
|
||||
expect(isNoProxyMatched("*", { hostname: "api.example.com", port: "" })).to.equal(true);
|
||||
expect(isNoProxyMatched("api.example.com", { hostname: "api.example.com", port: "" })).to.equal(true);
|
||||
expect(isNoProxyMatched("example.com", { hostname: "api.example.com", port: "" })).to.equal(true);
|
||||
expect(isNoProxyMatched(".example.com", { hostname: "api.example.com", port: "" })).to.equal(true);
|
||||
expect(isNoProxyMatched("*.example.com", { hostname: "api.example.com", port: "" })).to.equal(true);
|
||||
expect(isNoProxyMatched("127.0.0.1", { hostname: "127.0.0.1", port: "" })).to.equal(true);
|
||||
expect(isNoProxyMatched("192.168.*", { hostname: "192.168.1.10", port: "" })).to.equal(true);
|
||||
expect(isNoProxyMatched("192.168.*", { hostname: "192.169.1.10", port: "" })).to.equal(false);
|
||||
expect(isNoProxyMatched("[::1]", { hostname: "::1", port: "" })).to.equal(true);
|
||||
expect(isNoProxyMatched("[::1]:8443", { hostname: "::1", port: "8443" })).to.equal(true);
|
||||
expect(isNoProxyMatched("api.example.com:8443", { hostname: "api.example.com", port: "8443" })).to.equal(true);
|
||||
expect(isNoProxyMatched("api.example.com:8443", { hostname: "api.example.com", port: "443" })).to.equal(false);
|
||||
expect(isNoProxyMatched("127.0.0.1", { hostname: "127.0.0.2", port: "" })).to.equal(false);
|
||||
expect(isNoProxyMatched(".example.com", { hostname: "example.org", port: "" })).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,11 +82,24 @@ export class HttpError extends Error {
|
||||
export const HttpCommonError = HttpError;
|
||||
|
||||
let defaultAgents = createAgent();
|
||||
const directAgents = createAgent();
|
||||
let defaultProxyOptions: GlobalProxyOptions = {};
|
||||
let defaultHeaders: Record<string, string> = {};
|
||||
|
||||
export function setGlobalProxy(opts: { httpProxy?: string; httpsProxy?: string }) {
|
||||
export type GlobalProxyOptions = {
|
||||
httpProxy?: string;
|
||||
httpsProxy?: string;
|
||||
noProxy?: string;
|
||||
};
|
||||
|
||||
export function setGlobalProxy(opts: GlobalProxyOptions) {
|
||||
logger.info("setGlobalProxy:", opts);
|
||||
defaultAgents = createAgent(opts);
|
||||
defaultProxyOptions = { ...opts };
|
||||
defaultAgents = createAgent({
|
||||
httpProxy: opts.httpProxy,
|
||||
httpsProxy: opts.httpsProxy,
|
||||
});
|
||||
setProxyEnvironment(opts);
|
||||
}
|
||||
|
||||
export function getGlobalAgents() {
|
||||
@@ -137,21 +150,25 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
|
||||
if (config.timeout == null) {
|
||||
config.timeout = 15000;
|
||||
}
|
||||
let agents = defaultAgents;
|
||||
if (config.skipSslVerify || config.httpProxy) {
|
||||
let rejectUnauthorized = true;
|
||||
const bypassProxy = shouldBypassProxy(config, defaultProxyOptions.noProxy);
|
||||
const useCustomProxy = !!config.httpProxy && !bypassProxy;
|
||||
let agents = bypassProxy ? directAgents : defaultAgents;
|
||||
if (bypassProxy) {
|
||||
logger.info("命中no_proxy配置,跳过代理:", config.url);
|
||||
}
|
||||
if (config.skipSslVerify || useCustomProxy) {
|
||||
const agentOptions: any = {};
|
||||
if (config.skipSslVerify) {
|
||||
logger.info("忽略接口请求的SSL校验");
|
||||
rejectUnauthorized = false;
|
||||
agentOptions.rejectUnauthorized = false;
|
||||
}
|
||||
const proxy: any = {};
|
||||
if (config.httpProxy) {
|
||||
if (useCustomProxy) {
|
||||
logger.info("使用自定义http代理:", config.httpProxy);
|
||||
proxy.httpProxy = config.httpProxy;
|
||||
proxy.httpsProxy = config.httpProxy;
|
||||
agentOptions.httpProxy = config.httpProxy;
|
||||
agentOptions.httpsProxy = config.httpProxy;
|
||||
}
|
||||
|
||||
agents = createAgent({ rejectUnauthorized, ...proxy } as any);
|
||||
agents = createAgent(agentOptions);
|
||||
}
|
||||
|
||||
delete config.skipSslVerify;
|
||||
@@ -354,7 +371,7 @@ export type CreateAgentOptions = {
|
||||
httpsProxy?: string;
|
||||
} & nodeHttp.AgentOptions;
|
||||
export function createAgent(opts: CreateAgentOptions = {}) {
|
||||
opts = merge(
|
||||
const { httpProxy, httpsProxy, ...agentOptions } = merge(
|
||||
{
|
||||
autoSelectFamily: true,
|
||||
autoSelectFamilyAttemptTimeout: 1000,
|
||||
@@ -364,29 +381,19 @@ export function createAgent(opts: CreateAgentOptions = {}) {
|
||||
);
|
||||
|
||||
let httpAgent, httpsAgent;
|
||||
const httpProxy = opts.httpProxy;
|
||||
if (httpProxy) {
|
||||
process.env.HTTP_PROXY = httpProxy;
|
||||
process.env.http_proxy = httpProxy;
|
||||
logger.info("use httpProxy:", httpProxy);
|
||||
httpAgent = new HttpProxyAgent(httpProxy, opts as any);
|
||||
merge(httpAgent.options, opts);
|
||||
httpAgent = new HttpProxyAgent(httpProxy, agentOptions as any);
|
||||
merge(httpAgent.options, agentOptions);
|
||||
} else {
|
||||
process.env.HTTP_PROXY = "";
|
||||
process.env.http_proxy = "";
|
||||
httpAgent = new nodeHttp.Agent(opts);
|
||||
httpAgent = new nodeHttp.Agent(agentOptions);
|
||||
}
|
||||
const httpsProxy = opts.httpsProxy;
|
||||
if (httpsProxy) {
|
||||
process.env.HTTPS_PROXY = httpsProxy;
|
||||
process.env.https_proxy = httpsProxy;
|
||||
logger.info("use httpsProxy:", httpsProxy);
|
||||
httpsAgent = new HttpsProxyAgent(httpsProxy, opts as any);
|
||||
merge(httpsAgent.options, opts);
|
||||
httpsAgent = new HttpsProxyAgent(httpsProxy, agentOptions as any);
|
||||
merge(httpsAgent.options, agentOptions);
|
||||
} else {
|
||||
process.env.HTTPS_PROXY = "";
|
||||
process.env.https_proxy = "";
|
||||
httpsAgent = new https.Agent(opts);
|
||||
httpsAgent = new https.Agent(agentOptions);
|
||||
}
|
||||
return {
|
||||
httpAgent,
|
||||
@@ -394,6 +401,145 @@ export function createAgent(opts: CreateAgentOptions = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function setProxyEnvironment(opts: GlobalProxyOptions = {}) {
|
||||
setEnvValue("HTTP_PROXY", opts.httpProxy);
|
||||
setEnvValue("http_proxy", opts.httpProxy);
|
||||
setEnvValue("HTTPS_PROXY", opts.httpsProxy);
|
||||
setEnvValue("https_proxy", opts.httpsProxy);
|
||||
const noProxy = normalizeNoProxyText(opts.noProxy);
|
||||
setEnvValue("NO_PROXY", noProxy);
|
||||
setEnvValue("no_proxy", noProxy);
|
||||
}
|
||||
|
||||
function setEnvValue(key: string, value?: string) {
|
||||
process.env[key] = value || "";
|
||||
}
|
||||
|
||||
function shouldBypassProxy(config: AxiosRequestConfig, noProxy?: string) {
|
||||
if (!noProxy) {
|
||||
return false;
|
||||
}
|
||||
const target = getRequestTarget(config);
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
return splitNoProxyRules(noProxy).some(item => isNoProxyMatched(item, target));
|
||||
}
|
||||
|
||||
function getRequestTarget(config: AxiosRequestConfig) {
|
||||
try {
|
||||
const baseURL = config.baseURL || undefined;
|
||||
const url = new URL(config.url || "", baseURL);
|
||||
return {
|
||||
hostname: normalizeHost(url.hostname),
|
||||
port: url.port,
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isNoProxyMatched(rule: string, target: { hostname: string; port: string }) {
|
||||
if (rule === "*") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const normalizedRule = normalizeNoProxyRule(rule);
|
||||
if (!normalizedRule.host) {
|
||||
return false;
|
||||
}
|
||||
if (normalizedRule.port && normalizedRule.port !== target.port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const host = normalizeHost(target.hostname);
|
||||
if (normalizedRule.host.includes("*")) {
|
||||
return wildcardHostMatched(normalizedRule.host, host);
|
||||
}
|
||||
if (normalizedRule.host.startsWith("*.")) {
|
||||
const suffix = normalizedRule.host.substring(1);
|
||||
return host.endsWith(suffix);
|
||||
}
|
||||
if (normalizedRule.host.startsWith(".")) {
|
||||
return host === normalizedRule.host.substring(1) || host.endsWith(normalizedRule.host);
|
||||
}
|
||||
return host === normalizedRule.host || host.endsWith(`.${normalizedRule.host}`);
|
||||
}
|
||||
|
||||
function normalizeNoProxyRule(rule: string) {
|
||||
let value = rule.trim().toLowerCase();
|
||||
if (value.includes("://")) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return {
|
||||
host: normalizeHost(url.hostname),
|
||||
port: url.port,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
host: "",
|
||||
port: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let port = "";
|
||||
if (value.startsWith("[")) {
|
||||
const closeIndex = value.indexOf("]");
|
||||
const host = value.substring(1, closeIndex);
|
||||
const rest = value.substring(closeIndex + 1);
|
||||
if (rest.startsWith(":")) {
|
||||
port = rest.substring(1);
|
||||
}
|
||||
return {
|
||||
host: normalizeHost(host),
|
||||
port,
|
||||
};
|
||||
}
|
||||
|
||||
const colonCount = (value.match(/:/g) || []).length;
|
||||
const portIndex = value.lastIndexOf(":");
|
||||
if (colonCount === 1 && portIndex > -1) {
|
||||
port = value.substring(portIndex + 1);
|
||||
value = value.substring(0, portIndex);
|
||||
}
|
||||
return {
|
||||
host: normalizeHost(value),
|
||||
port,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHost(host: string) {
|
||||
let value = host.trim().toLowerCase();
|
||||
if (value.startsWith("[") && value.endsWith("]")) {
|
||||
value = value.substring(1, value.length - 1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function wildcardHostMatched(rule: string, host: string) {
|
||||
const pattern = rule.split("*").map(escapeRegExp).join(".*");
|
||||
return new RegExp(`^${pattern}$`).test(host);
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function normalizeNoProxyText(noProxy?: string) {
|
||||
return splitNoProxyRules(noProxy).join(",");
|
||||
}
|
||||
|
||||
function splitNoProxyRules(noProxy?: string) {
|
||||
if (!noProxy) {
|
||||
return [];
|
||||
}
|
||||
return noProxy
|
||||
.split(/[,\s]+/)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export async function download(req: { http: HttpClient; config: HttpRequestConfig; savePath: string; logger: ILogger }) {
|
||||
const { http, config, savePath, logger } = req;
|
||||
return safePromise((resolve, reject) => {
|
||||
|
||||
@@ -81,6 +81,7 @@ export class SysPrivateSettings extends BaseSettings {
|
||||
|
||||
httpsProxy? = '';
|
||||
httpProxy? = '';
|
||||
noProxy? = '';
|
||||
commonHeaders?: string = '';
|
||||
|
||||
reverseProxies?: Record<string, string> = {};
|
||||
|
||||
@@ -165,6 +165,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
||||
const opts = {
|
||||
httpProxy: privateSetting.httpProxy,
|
||||
httpsProxy: privateSetting.httpsProxy,
|
||||
noProxy: privateSetting.noProxy,
|
||||
};
|
||||
setGlobalProxy(opts);
|
||||
setGlobalHeaders(this.parseKeyValueText(privateSetting.commonHeaders));
|
||||
|
||||
@@ -111,6 +111,9 @@ export default {
|
||||
httpsProxyPlaceholder: "http://192.168.1.2:18010/",
|
||||
saveThenTestTitle: "Save first, then click test",
|
||||
httpsProxyHelper: "Usually both proxies are the same, save first then test",
|
||||
noProxy: "Proxy Bypass",
|
||||
noProxyPlaceholder: "localhost,127.0.0.1,.example.com,192.168.*",
|
||||
noProxyHelper: "Configure NO_PROXY. Separate entries with commas, spaces, or line breaks; matched requests bypass the proxy. \nExample: localhost,127.0.0.1,.example.com,192.168.*",
|
||||
dualStackNetwork: "Dual Stack Network",
|
||||
ipv4Priority: "IPv4 Priority",
|
||||
ipv6Priority: "IPv6 Priority",
|
||||
|
||||
@@ -108,6 +108,9 @@ export default {
|
||||
httpsProxyPlaceholder: "http://192.168.1.2:18010/",
|
||||
saveThenTestTitle: "保存后,再点击测试",
|
||||
httpsProxyHelper: "一般这两个代理填一样的,保存后再测试",
|
||||
noProxy: "代理排除",
|
||||
noProxyPlaceholder: "localhost,127.0.0.1,.example.com,192.168.*",
|
||||
noProxyHelper: "配置NO_PROXY,多个地址可用英文逗号、空格或换行分隔,命中的请求将不走代理\n例如:localhost,127.0.0.1,.example.com,192.168.*",
|
||||
dualStackNetwork: "双栈网络",
|
||||
ipv4Priority: "IPV4优先",
|
||||
ipv6Priority: "IPV6优先",
|
||||
|
||||
@@ -105,6 +105,7 @@ export type InviteSetting = {
|
||||
export type SysPrivateSetting = {
|
||||
httpProxy?: string;
|
||||
httpsProxy?: string;
|
||||
noProxy?: string;
|
||||
commonHeaders?: string;
|
||||
reverseProxies?: any;
|
||||
dnsResultOrder?: string;
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
<div class="helper">{{ t("certd.httpsProxyHelper") }}</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('certd.noProxy')" :name="['private', 'noProxy']">
|
||||
<a-textarea v-model:value="formState.private.noProxy" :placeholder="t('certd.noProxyPlaceholder')" rows="3" />
|
||||
<div class="helper">{{ t("certd.noProxyHelper") }}</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('certd.sys.setting.environmentVars')" :name="['private', 'environmentVars']">
|
||||
<a-textarea v-model:value="formState.private.environmentVars" :placeholder="environmentVarsExample" rows="4" />
|
||||
<div class="helper">{{ t("certd.sys.setting.environmentVarsHelper") }}</div>
|
||||
|
||||
Reference in New Issue
Block a user