mirror of
https://github.com/Kong/insomnia.git
synced 2026-07-31 01:37:32 -04:00
fix: propagate system proxy settings to user requests (INS-2943) (#10228)
* fix: propagate system proxy settings to user requests (INS-2943) When proxyEnabled is false in Insomnia Preferences, the request engine (node-libcurl) now falls back to the OS system proxy via Electron's session.defaultSession.resolveProxy(), matching the behavior of Insomnia's internal API calls. Previously, disabling the proxy toggle explicitly set CURLOPT_PROXY to an empty string, which prevented libcurl from using any proxy including the system proxy. This caused user requests to bypass the OS proxy while Insomnia's own network calls (via Chromium's network stack) correctly respected it. Changes: - Add parseResolvedProxy() helper to parse PAC-format proxy strings - Call resolveProxy() inside createConfiguredCurlInstance() when proxy is not explicitly configured - Refactor api.protocol.ts to use the shared helper (removes ~50 lines of duplicated proxy parsing logic) * fix: catch resolveProxy errors and fall back to direct connection * fix: catch resolveProxy errors in api.protocol.ts * fix: remove redundant comment about proxy mode in updateProxy function
This commit is contained in:
@@ -2,12 +2,12 @@ import path from 'node:path';
|
||||
import { Readable } from 'node:stream';
|
||||
import { parse as urlParse } from 'node:url';
|
||||
|
||||
import { Curl, CurlAuth, CurlFeature, CurlProxy, CurlSslOpt, type HeaderInfo } from '@getinsomnia/node-libcurl';
|
||||
import { Curl, CurlAuth, CurlFeature, CurlSslOpt, type HeaderInfo } from '@getinsomnia/node-libcurl';
|
||||
import { app, net, protocol, session } from 'electron';
|
||||
import { services } from 'insomnia-data';
|
||||
|
||||
import { getApiBaseURL } from '../common/constants';
|
||||
import { setDefaultProtocol, shouldBypassProxyForHost } from './network/libcurl-promise';
|
||||
import { parseResolvedProxy, setDefaultProtocol, shouldBypassProxyForHost } from './network/libcurl-promise';
|
||||
import { resolveDbByKey } from './templating-worker-database';
|
||||
|
||||
export interface RegisterProtocolOptions {
|
||||
@@ -54,7 +54,12 @@ export async function registerInsomniaProtocols() {
|
||||
const settings = await services.settings.get();
|
||||
// systemProxy follows the PAC return value format.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file#return_value_format
|
||||
let systemProxyStr = await session.defaultSession.resolveProxy(urlStr);
|
||||
let systemProxyStr: string | undefined;
|
||||
try {
|
||||
systemProxyStr = await session.defaultSession.resolveProxy(urlStr);
|
||||
} catch {
|
||||
// If resolveProxy fails, fall back to direct connection
|
||||
}
|
||||
|
||||
// here we use libcurl to forward the SSE request because the SSE request sent by net.fetch can not be disconnected correctly in some cases
|
||||
// see https://github.com/electron/electron/issues/47097
|
||||
@@ -68,64 +73,12 @@ export async function registerInsomniaProtocols() {
|
||||
|
||||
if (!settings.proxyEnabled) {
|
||||
// follow system proxy
|
||||
if (!systemProxyStr) {
|
||||
// if systemProxy is empty, it means no proxy is used
|
||||
systemProxyStr = 'DIRECT';
|
||||
}
|
||||
|
||||
const proxy = systemProxyStr
|
||||
.trim()
|
||||
.split(/\s*;\s*/g)
|
||||
.find(Boolean);
|
||||
|
||||
// only the first proxy specified will be used
|
||||
const firstProxy = proxy;
|
||||
const parts = firstProxy?.split(/\s+/);
|
||||
|
||||
const proxyType = parts?.[0];
|
||||
|
||||
if (proxyType === 'DIRECT') {
|
||||
curl.setOpt(Curl.option.PROXY, '');
|
||||
const resolved = parseResolvedProxy(systemProxyStr);
|
||||
if (resolved) {
|
||||
curl.setOpt(Curl.option.PROXYTYPE, resolved.proxyType);
|
||||
curl.setOpt(Curl.option.PROXY, resolved.proxyUrl);
|
||||
} else {
|
||||
let unknownProxy = false;
|
||||
let curlOptProxyType = CurlProxy.Http;
|
||||
switch (proxyType) {
|
||||
case 'PROXY': {
|
||||
curlOptProxyType = CurlProxy.Http;
|
||||
break;
|
||||
}
|
||||
case 'HTTP': {
|
||||
curlOptProxyType = CurlProxy.Http;
|
||||
break;
|
||||
}
|
||||
case 'SOCKS': {
|
||||
curlOptProxyType = CurlProxy.Socks4;
|
||||
break;
|
||||
}
|
||||
case 'HTTPS': {
|
||||
curlOptProxyType = CurlProxy.Https;
|
||||
break;
|
||||
}
|
||||
case 'SOCKS4': {
|
||||
curlOptProxyType = CurlProxy.Socks4;
|
||||
break;
|
||||
}
|
||||
case 'SOCKS5': {
|
||||
curlOptProxyType = CurlProxy.Socks5;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// unknown proxy type
|
||||
unknownProxy = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (unknownProxy) {
|
||||
curl.setOpt(Curl.option.PROXY, '');
|
||||
} else if (parts?.[1]) {
|
||||
curl.setOpt(Curl.option.PROXYTYPE, curlOptProxyType);
|
||||
curl.setOpt(Curl.option.PROXY, parts[1]);
|
||||
}
|
||||
curl.setOpt(Curl.option.PROXY, '');
|
||||
}
|
||||
} else {
|
||||
const { protocol, hostname } = urlParse(urlStr);
|
||||
|
||||
@@ -152,14 +152,15 @@ const openCurlConnection = async (
|
||||
try {
|
||||
invariant(options.url, 'URL must be defined');
|
||||
invariant(!options.url.startsWith('file://'), 'Local file URIs are not supported');
|
||||
|
||||
|
||||
const readyStateChannel = `${protocolName}.${request._id}.${REALTIME_EVENTS_CHANNELS.READY_STATE}`;
|
||||
|
||||
const settings = await services.settings.get();
|
||||
const start = performance.now();
|
||||
const clientCertificates = await services.clientCertificate.findByParentId(options.workspaceId);
|
||||
const filteredClientCertificates = filterClientCertificates(clientCertificates, options.url, 'https:');
|
||||
const { curl, debugTimeline } = createConfiguredCurlInstance({
|
||||
|
||||
const { curl, debugTimeline } = await createConfiguredCurlInstance({
|
||||
req: { ...request, cookieJar: options.cookieJar, cookies: [], suppressUserAgent: options.suppressUserAgent },
|
||||
finalUrl: options.url,
|
||||
settings,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
CurlHttpVersion,
|
||||
CurlInfoDebug,
|
||||
CurlNetrc,
|
||||
CurlProxy,
|
||||
CurlSslOpt,
|
||||
} from '@getinsomnia/node-libcurl';
|
||||
import { isValid } from 'date-fns';
|
||||
@@ -123,7 +124,7 @@ export const curlRequest = (options: CurlRequestOptions) =>
|
||||
invariant(!finalUrl.startsWith('file://'), 'Local file URIs are not supported');
|
||||
const caCert = caCertficatePath && (await insecureReadFile(caCertficatePath));
|
||||
|
||||
const { curl, debugTimeline } = createConfiguredCurlInstance({
|
||||
const { curl, debugTimeline } = await createConfiguredCurlInstance({
|
||||
req,
|
||||
finalUrl,
|
||||
settings,
|
||||
@@ -286,7 +287,61 @@ export const curlRequest = (options: CurlRequestOptions) =>
|
||||
}
|
||||
});
|
||||
|
||||
export const createConfiguredCurlInstance = ({
|
||||
/**
|
||||
* Parse a PAC-format proxy string (from Electron's session.resolveProxy) into
|
||||
* a curl proxy URL and type. Returns null when DIRECT.
|
||||
* Format: "PROXY host:port", "HTTPS host:port", "SOCKS host:port", etc.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file#return_value_format
|
||||
*/
|
||||
export function parseResolvedProxy(pacString: string | undefined): { proxyUrl: string; proxyType: CurlProxy } | null {
|
||||
if (!pacString) {
|
||||
return null;
|
||||
}
|
||||
// only the first proxy specified will be used
|
||||
const proxy = pacString
|
||||
.trim()
|
||||
.split(/\s*;\s*/g)
|
||||
.find(Boolean);
|
||||
if (!proxy) {
|
||||
return null;
|
||||
}
|
||||
const parts = proxy.split(/\s+/);
|
||||
const proxyType = parts[0];
|
||||
if (proxyType === 'DIRECT') {
|
||||
return null;
|
||||
}
|
||||
const proxyAddr = parts[1];
|
||||
if (!proxyAddr) {
|
||||
return null;
|
||||
}
|
||||
let curlProxyType: CurlProxy;
|
||||
switch (proxyType) {
|
||||
case 'PROXY':
|
||||
case 'HTTP': {
|
||||
curlProxyType = CurlProxy.Http;
|
||||
break;
|
||||
}
|
||||
case 'HTTPS': {
|
||||
curlProxyType = CurlProxy.Https;
|
||||
break;
|
||||
}
|
||||
case 'SOCKS':
|
||||
case 'SOCKS4': {
|
||||
curlProxyType = CurlProxy.Socks4;
|
||||
break;
|
||||
}
|
||||
case 'SOCKS5': {
|
||||
curlProxyType = CurlProxy.Socks5;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return { proxyUrl: proxyAddr, proxyType: curlProxyType };
|
||||
}
|
||||
|
||||
export const createConfiguredCurlInstance = async ({
|
||||
req,
|
||||
finalUrl,
|
||||
settings,
|
||||
@@ -365,7 +420,21 @@ export const createConfiguredCurlInstance = ({
|
||||
}
|
||||
|
||||
if (!settings.proxyEnabled) {
|
||||
curl.setOpt(Curl.option.PROXY, '');
|
||||
// When proxy is not explicitly configured, fall back to system proxy
|
||||
let resolved: ReturnType<typeof parseResolvedProxy> = null;
|
||||
try {
|
||||
const systemProxy = await electron.session.defaultSession.resolveProxy(finalUrl);
|
||||
resolved = parseResolvedProxy(systemProxy);
|
||||
} catch {
|
||||
// If resolveProxy fails (e.g. invalid URL, session issues), fall back to direct connection
|
||||
}
|
||||
if (resolved) {
|
||||
curl.setOpt(Curl.option.PROXYTYPE, resolved.proxyType);
|
||||
curl.setOpt(Curl.option.PROXY, resolved.proxyUrl);
|
||||
debugTimeline.push({ value: `Using system proxy: ${resolved.proxyUrl}`, name: 'Text', timestamp: Date.now() });
|
||||
} else {
|
||||
curl.setOpt(Curl.option.PROXY, '');
|
||||
}
|
||||
} else {
|
||||
const { protocol, hostname } = urlParse(req.url);
|
||||
const { httpProxy, httpsProxy, noProxy } = settings;
|
||||
|
||||
@@ -27,10 +27,10 @@ async function updateProxy() {
|
||||
}
|
||||
|
||||
// Set proxy rules in the main session https://www.electronjs.org/docs/latest/api/structures/proxy-config
|
||||
// no mode here — it overrides proxyRules ('system' ignores them)
|
||||
await session.defaultSession.setProxy({
|
||||
proxyRules: proxyRules.join(';'),
|
||||
proxyBypassRules: noProxy ?? '',
|
||||
mode: 'fixed_servers',
|
||||
});
|
||||
return;
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user