fix(jira): honor no_proxy for electron requests #6324 (#8177)

Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
This commit is contained in:
felix bear 2026-06-09 22:00:43 +09:00 committed by GitHub
parent d844559c6f
commit 1c53656889
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 241 additions and 3 deletions

View file

@ -18,7 +18,7 @@ export const sendJiraRequest = ({
jiraCfg: JiraCfg;
}): void => {
const mainWin = getWin();
const agent = createProxyAwareAgent(jiraCfg?.isAllowSelfSignedCertificate);
const agent = createProxyAwareAgent(url, jiraCfg?.isAllowSelfSignedCertificate);
fetch(url, {
...requestInit,

View file

@ -0,0 +1,168 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const Module = require('node:module');
const { Agent: HttpsAgent } = require('node:https');
const { HttpsProxyAgent } = require('https-proxy-agent');
require('ts-node/register/transpile-only');
const originalModuleLoad = Module._load;
const originalEnv = { ...process.env };
const proxyAgentModulePath = path.resolve(__dirname, 'proxy-agent.ts');
const resetModule = () => {
delete require.cache[proxyAgentModulePath];
};
const clearProxyEnv = () => {
delete process.env.HTTPS_PROXY;
delete process.env.https_proxy;
delete process.env.HTTP_PROXY;
delete process.env.http_proxy;
delete process.env.NO_PROXY;
delete process.env.no_proxy;
};
const installMocks = () => {
Module._load = function patchedLoad(request, parent, isMain) {
if (request === 'electron-log/main') {
return {
log: () => {},
};
}
return originalModuleLoad.call(this, request, parent, isMain);
};
};
const loadProxyAgentModule = () => {
resetModule();
return require(proxyAgentModulePath);
};
test.beforeEach(() => {
clearProxyEnv();
installMocks();
resetModule();
});
test.afterEach(() => {
Module._load = originalModuleLoad;
process.env = { ...originalEnv };
resetModule();
});
test('returns undefined when no proxy or self-signed handling is configured', () => {
const { createProxyAwareAgent } = loadProxyAgentModule();
assert.equal(
createProxyAwareAgent('https://jira.internal.example/rest/api'),
undefined,
);
});
test('creates a proxy agent from HTTPS_PROXY', () => {
process.env.HTTPS_PROXY = 'http://proxy.example:8080';
const { createProxyAwareAgent } = loadProxyAgentModule();
const agent = createProxyAwareAgent('https://jira.external.example/rest/api');
assert.ok(agent instanceof HttpsProxyAgent);
assert.equal(agent.proxy.href, 'http://proxy.example:8080/');
});
test('bypasses proxy for an exact NO_PROXY host match', () => {
process.env.HTTPS_PROXY = 'http://proxy.example:8080';
process.env.NO_PROXY = 'jira.internal.example';
const { createProxyAwareAgent } = loadProxyAgentModule();
assert.equal(
createProxyAwareAgent('https://jira.internal.example/rest/api'),
undefined,
);
});
test('bypasses proxy for subdomains of a bare NO_PROXY domain', () => {
process.env.HTTPS_PROXY = 'http://proxy.example:8080';
process.env.NO_PROXY = 'internal.example';
const { createProxyAwareAgent } = loadProxyAgentModule();
assert.equal(
createProxyAwareAgent('https://jira.internal.example/rest/api'),
undefined,
);
assert.ok(
createProxyAwareAgent('https://notinternal.example/rest/api') instanceof
HttpsProxyAgent,
);
});
test('honors lowercase no_proxy and leading-dot suffix matches', () => {
process.env.HTTPS_PROXY = 'http://proxy.example:8080';
process.env.no_proxy = '.internal.example';
const { createProxyAwareAgent } = loadProxyAgentModule();
assert.equal(
createProxyAwareAgent('https://jira.internal.example/rest/api'),
undefined,
);
assert.equal(createProxyAwareAgent('https://internal.example/rest/api'), undefined);
});
test('honors wildcard NO_PROXY entries', () => {
process.env.HTTPS_PROXY = 'http://proxy.example:8080';
process.env.NO_PROXY = '*';
const { createProxyAwareAgent } = loadProxyAgentModule();
assert.equal(
createProxyAwareAgent('https://jira.external.example/rest/api'),
undefined,
);
});
test('honors wildcard suffix NO_PROXY entries', () => {
process.env.HTTPS_PROXY = 'http://proxy.example:8080';
process.env.NO_PROXY = '*.corp.example';
const { createProxyAwareAgent } = loadProxyAgentModule();
assert.equal(createProxyAwareAgent('https://jira.corp.example/rest/api'), undefined);
});
test('requires NO_PROXY port entries to match the request port', () => {
process.env.HTTPS_PROXY = 'http://proxy.example:8080';
process.env.NO_PROXY = 'jira.internal.example:8443';
const { createProxyAwareAgent } = loadProxyAgentModule();
assert.equal(
createProxyAwareAgent('https://jira.internal.example:8443/rest/api'),
undefined,
);
assert.ok(
createProxyAwareAgent('https://jira.internal.example:443/rest/api') instanceof
HttpsProxyAgent,
);
});
test('keeps self-signed handling when NO_PROXY bypasses the proxy', () => {
process.env.HTTPS_PROXY = 'http://proxy.example:8080';
process.env.NO_PROXY = 'jira.internal.example';
const { createProxyAwareAgent } = loadProxyAgentModule();
const agent = createProxyAwareAgent('https://jira.internal.example/rest/api', true);
assert.ok(agent instanceof HttpsAgent);
assert.equal(agent.options.rejectUnauthorized, false);
});
test('keeps proxy handling for non-matching NO_PROXY entries', () => {
process.env.HTTPS_PROXY = 'http://proxy.example:8080';
process.env.NO_PROXY = 'other.internal.example,.corp.example';
const { createProxyAwareAgent } = loadProxyAgentModule();
const agent = createProxyAwareAgent('https://jira.internal.example/rest/api', true);
assert.ok(agent instanceof HttpsProxyAgent);
assert.equal(agent.proxy.href, 'http://proxy.example:8080/');
assert.equal(agent.options.rejectUnauthorized, false);
});

View file

@ -13,11 +13,80 @@ export const getProxyUrl = (): string | undefined =>
process.env.http_proxy ||
undefined;
const getNoProxy = (): string | undefined =>
process.env.NO_PROXY || process.env.no_proxy || undefined;
const getUrlPort = (url: URL): string =>
url.port || (url.protocol === 'http:' ? '80' : url.protocol === 'https:' ? '443' : '');
const parseNoProxyEntry = (
rawEntry: string,
): { host: string; port?: string } | undefined => {
const entry = rawEntry.trim().toLowerCase();
if (!entry) {
return undefined;
}
const withoutProtocol = entry.replace(/^[a-z][a-z\d+.-]*:\/\//, '');
const withoutPath = withoutProtocol.split('/')[0];
const portMatch = withoutPath.match(/:(\d+)$/);
const port = portMatch?.[1];
const host = (port ? withoutPath.slice(0, -port.length - 1) : withoutPath).replace(
/^\[(.*)]$/,
'$1',
);
return host ? { host, port } : undefined;
};
export const isNoProxyMatch = (requestUrl: string, noProxy = getNoProxy()): boolean => {
if (!noProxy) {
return false;
}
let url: URL;
try {
url = new URL(requestUrl);
} catch {
return false;
}
const targetHost = url.hostname.toLowerCase();
const targetPort = getUrlPort(url);
return noProxy
.split(/[,\s]+/)
.map(parseNoProxyEntry)
.filter((entry): entry is { host: string; port?: string } => !!entry)
.some(({ host, port }) => {
if (host === '*') {
return true;
}
if (port && port !== targetPort) {
return false;
}
if (host.startsWith('*.')) {
const suffix = host.slice(1);
return targetHost.endsWith(suffix);
}
if (host.startsWith('.')) {
return targetHost === host.slice(1) || targetHost.endsWith(host);
}
return targetHost === host || targetHost.endsWith(`.${host}`);
});
};
/**
* Builds a node-fetchcompatible HTTPS agent that respects:
* 1. Proxy environment variables (HTTPS_PROXY / HTTP_PROXY)
* 2. An opt-in flag to accept self-signed certificates on the **target** server
* 2. NO_PROXY / no_proxy bypasses for the current request URL
* 3. An opt-in flag to accept self-signed certificates on the **target** server
*
* @param requestUrl The URL about to be requested.
* @param allowSelfSigned When `true`, TLS certificate errors on both the
* proxy **and** the target connection are ignored.
* This is intentional the user opted in via the
@ -26,11 +95,12 @@ export const getProxyUrl = (): string | undefined =>
* a proxy nor self-signed handling is needed.
*/
export const createProxyAwareAgent = (
requestUrl: string,
allowSelfSigned = false,
): HttpsProxyAgent<string> | HttpsAgent | undefined => {
const proxyUrl = getProxyUrl();
if (proxyUrl) {
if (proxyUrl && !isNoProxyMatch(requestUrl)) {
log(`Using proxy.${allowSelfSigned ? ' (self-signed certs allowed)' : ''}`);
const agent = new HttpsProxyAgent(proxyUrl, {