mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
fix(core): Proxy all HTTP traffic instead of only axios (#20614)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { CLI_DIR, EDITOR_UI_DIST_DIR, inE2ETests } from '@/constants';
|
||||
import type { ICredentialsOverwrite } from '@/interfaces';
|
||||
import { inDevelopment, inProduction } from '@n8n/backend-common';
|
||||
import { SecurityConfig } from '@n8n/config';
|
||||
import { Time } from '@n8n/constants';
|
||||
@@ -8,20 +10,18 @@ import express from 'express';
|
||||
import { access as fsAccess } from 'fs/promises';
|
||||
import helmet from 'helmet';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
import { InstanceSettings, installGlobalProxyAgent } from 'n8n-core';
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
import { resolve } from 'path';
|
||||
|
||||
import { AbstractServer } from '@/abstract-server';
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import config from '@/config';
|
||||
import { CLI_DIR, EDITOR_UI_DIST_DIR, inE2ETests } from '@/constants';
|
||||
import { ControllerRegistry } from '@/controller.registry';
|
||||
import { CredentialsOverwrites } from '@/credentials-overwrites';
|
||||
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
||||
import { EventService } from '@/events/event.service';
|
||||
import { LogStreamingEventRelay } from '@/events/relays/log-streaming.event-relay';
|
||||
import type { ICredentialsOverwrite } from '@/interfaces';
|
||||
import { isLdapEnabled } from '@/ldap.ee/helpers.ee';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { handleMfaDisable, isMfaFeatureEnabled } from '@/mfa/helpers';
|
||||
@@ -479,6 +479,8 @@ export class Server extends AbstractServer {
|
||||
} else {
|
||||
this.app.use('/', express.static(staticCacheDir, cacheOptions));
|
||||
}
|
||||
|
||||
installGlobalProxyAgent();
|
||||
}
|
||||
|
||||
protected setupPushServer(): void {
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import type { AddressInfo } from 'net';
|
||||
import nock from 'nock';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { installGlobalProxyAgent, uninstallGlobalProxyAgent } from '../http-proxy';
|
||||
|
||||
interface TestResponse {
|
||||
message: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ProxyRequest {
|
||||
method: string;
|
||||
url: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
async function createMockProxyServer() {
|
||||
const capturedRequests: ProxyRequest[] = [];
|
||||
const server = http.createServer((req, res) => {
|
||||
capturedRequests.push({
|
||||
method: req.method ?? 'GET',
|
||||
url: req.url ?? '',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ message: 'proxied', timestamp: Date.now() }));
|
||||
});
|
||||
|
||||
server.on('connect', (req, clientSocket) => {
|
||||
capturedRequests.push({
|
||||
method: 'CONNECT',
|
||||
url: req.url ?? '',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
||||
clientSocket.end();
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.listen(0, '127.0.0.1', resolve);
|
||||
server.on('error', reject);
|
||||
});
|
||||
|
||||
const address = server.address() as AddressInfo;
|
||||
|
||||
return {
|
||||
server,
|
||||
port: address.port,
|
||||
url: `http://127.0.0.1:${address.port}`,
|
||||
capturedRequests,
|
||||
clearRequests: () => (capturedRequests.length = 0),
|
||||
};
|
||||
}
|
||||
|
||||
async function makeRequest(url: string): Promise<TestResponse> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const urlObj = new URL(url);
|
||||
const httpModule = urlObj.protocol === 'https:' ? https : http;
|
||||
|
||||
const req = httpModule.get(url, { timeout: 5000 }, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => (data += chunk));
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (error) {
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('HTTP Proxy Tests', () => {
|
||||
let proxyServer: Awaited<ReturnType<typeof createMockProxyServer>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
proxyServer = await createMockProxyServer();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await promisify(proxyServer.server.close.bind(proxyServer.server))();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.HTTP_PROXY;
|
||||
delete process.env.HTTPS_PROXY;
|
||||
delete process.env.NO_PROXY;
|
||||
delete process.env.ALL_PROXY;
|
||||
nock.cleanAll();
|
||||
nock.restore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
uninstallGlobalProxyAgent();
|
||||
proxyServer.clearRequests();
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
name: 'should use HTTP_PROXY for HTTP requests',
|
||||
env: { HTTP_PROXY: true },
|
||||
targetUrl: 'http://api.example.com:8080/test',
|
||||
expectProxied: true,
|
||||
},
|
||||
{
|
||||
name: 'should ignore HTTPS_PROXY for HTTP requests',
|
||||
env: { HTTPS_PROXY: true },
|
||||
targetUrl: 'http://api.example.com:8080/test',
|
||||
expectProxied: false,
|
||||
},
|
||||
{
|
||||
name: 'should use ALL_PROXY when specific proxy not set',
|
||||
env: { ALL_PROXY: true },
|
||||
targetUrl: 'http://api.example.com:8080/test',
|
||||
expectProxied: true,
|
||||
},
|
||||
{
|
||||
name: 'should prefer HTTP_PROXY over ALL_PROXY',
|
||||
env: { HTTP_PROXY: true, ALL_PROXY: 'http://unused:8080' },
|
||||
targetUrl: 'http://api.example.com:8080/test',
|
||||
expectProxied: true,
|
||||
},
|
||||
{
|
||||
name: 'should make direct requests when no proxy configured',
|
||||
env: {},
|
||||
targetUrl: 'http://api.example.com:8080/test',
|
||||
expectProxied: false,
|
||||
},
|
||||
{
|
||||
name: 'should bypass proxy for exact hostname match',
|
||||
env: { HTTP_PROXY: true, NO_PROXY: 'api.example.com' },
|
||||
targetUrl: 'http://api.example.com:8080/test',
|
||||
expectProxied: false,
|
||||
},
|
||||
{
|
||||
name: 'should bypass proxy for exact IP match',
|
||||
env: { HTTP_PROXY: true, NO_PROXY: '192.168.1.100' },
|
||||
targetUrl: 'http://192.168.1.100:8080/test',
|
||||
expectProxied: false,
|
||||
},
|
||||
{
|
||||
name: 'should proxy when hostname not in NO_PROXY',
|
||||
env: { HTTP_PROXY: true, NO_PROXY: 'example.com' },
|
||||
targetUrl: 'http://test.local:8080/api',
|
||||
expectProxied: true,
|
||||
},
|
||||
{
|
||||
name: 'should bypass proxy for wildcard subdomain patterns',
|
||||
env: { HTTP_PROXY: true, NO_PROXY: '*.local' },
|
||||
targetUrl: 'http://app.local:8080/api',
|
||||
expectProxied: false,
|
||||
},
|
||||
{
|
||||
name: 'should bypass proxy for nested wildcard patterns',
|
||||
env: { HTTP_PROXY: true, NO_PROXY: '*.example.com' },
|
||||
targetUrl: 'http://api.example.com:8080/data',
|
||||
expectProxied: false,
|
||||
},
|
||||
{
|
||||
name: 'should proxy when wildcard does not match',
|
||||
env: { HTTP_PROXY: true, NO_PROXY: '*.example.com' },
|
||||
targetUrl: 'http://test.local:8080/api',
|
||||
expectProxied: true,
|
||||
},
|
||||
{
|
||||
name: 'should handle multiple NO_PROXY patterns - match first',
|
||||
env: { HTTP_PROXY: true, NO_PROXY: 'localhost,*.local,example.com' },
|
||||
targetUrl: 'http://localhost:8080/test',
|
||||
expectProxied: false,
|
||||
},
|
||||
{
|
||||
name: 'should handle multiple NO_PROXY patterns - match middle',
|
||||
env: { HTTP_PROXY: true, NO_PROXY: 'localhost,*.local,example.com' },
|
||||
targetUrl: 'http://app.local:8080/api',
|
||||
expectProxied: false,
|
||||
},
|
||||
{
|
||||
name: 'should handle multiple NO_PROXY patterns - match last',
|
||||
env: { HTTP_PROXY: true, NO_PROXY: 'localhost,*.local,example.com' },
|
||||
targetUrl: 'http://example.com:8080/data',
|
||||
expectProxied: false,
|
||||
},
|
||||
{
|
||||
name: 'should proxy when none of multiple patterns match',
|
||||
env: { HTTP_PROXY: true, NO_PROXY: 'localhost,*.example.com,test.org' },
|
||||
targetUrl: 'http://app.local:8080/api',
|
||||
expectProxied: true,
|
||||
},
|
||||
{
|
||||
name: 'should respect NO_PROXY with ALL_PROXY',
|
||||
env: { ALL_PROXY: true, NO_PROXY: '*.example.com' },
|
||||
targetUrl: 'http://api.example.com:8080/test',
|
||||
expectProxied: false,
|
||||
},
|
||||
{
|
||||
name: 'should proxy when target not in NO_PROXY list',
|
||||
env: { HTTP_PROXY: true, NO_PROXY: 'localhost,*.internal' },
|
||||
targetUrl: 'http://api.example.com:8080/test',
|
||||
expectProxied: true,
|
||||
},
|
||||
])('$name', async ({ env, targetUrl, expectProxied }) => {
|
||||
if (env.HTTP_PROXY) process.env.HTTP_PROXY = proxyServer.url;
|
||||
if (env.HTTPS_PROXY) process.env.HTTPS_PROXY = proxyServer.url;
|
||||
if (env.ALL_PROXY === true) process.env.ALL_PROXY = proxyServer.url;
|
||||
if (env.ALL_PROXY && env.ALL_PROXY !== true) process.env.ALL_PROXY = env.ALL_PROXY;
|
||||
if (env.NO_PROXY) process.env.NO_PROXY = env.NO_PROXY;
|
||||
|
||||
installGlobalProxyAgent();
|
||||
|
||||
let scope: nock.Scope | undefined;
|
||||
if (!expectProxied) {
|
||||
scope = setupDirectRequestMock(targetUrl);
|
||||
}
|
||||
|
||||
const response = await makeRequest(targetUrl);
|
||||
|
||||
if (expectProxied) {
|
||||
expectProxiedResponse(response);
|
||||
} else {
|
||||
expectDirectResponse(response, scope);
|
||||
}
|
||||
});
|
||||
|
||||
function setupDirectRequestMock(targetUrl: string) {
|
||||
if (!nock.isActive()) nock.activate();
|
||||
const url = new URL(targetUrl);
|
||||
return nock(`${url.protocol}//${url.host}`)
|
||||
.get(url.pathname)
|
||||
.reply(200, { message: 'direct', timestamp: Date.now() });
|
||||
}
|
||||
|
||||
function expectProxiedResponse(response: TestResponse) {
|
||||
expect(response.message).toBe('proxied');
|
||||
expect(proxyServer.capturedRequests.length).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
function expectDirectResponse(response: TestResponse, scope?: nock.Scope) {
|
||||
expect(response.message).toBe('direct');
|
||||
expect(proxyServer.capturedRequests).toHaveLength(0);
|
||||
expect(scope?.isDone()).toBe(true);
|
||||
}
|
||||
});
|
||||
+1
-91
@@ -1,8 +1,5 @@
|
||||
import FormData from 'form-data';
|
||||
import { Agent as HttpAgent } from 'http';
|
||||
import { HttpProxyAgent } from 'http-proxy-agent';
|
||||
import { Agent as HttpsAgent } from 'https';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import type { Agent as HttpsAgent } from 'https';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type {
|
||||
IHttpRequestMethods,
|
||||
@@ -22,7 +19,6 @@ import {
|
||||
applyPaginationRequestData,
|
||||
convertN8nRequestToAxios,
|
||||
createFormDataObject,
|
||||
getAgentWithProxy,
|
||||
httpRequest,
|
||||
invokeAxios,
|
||||
parseRequestObject,
|
||||
@@ -870,90 +866,4 @@ describe('Request Helper Functions', () => {
|
||||
scope.done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgentWithProxy', () => {
|
||||
const baseUrlHttps = 'https://example.com';
|
||||
const baseUrlHttp = 'http://example.com';
|
||||
const proxyUrlHttps = 'http://proxy-for-https.com:8080/';
|
||||
const proxyUrlHttp = 'http://proxy-for-http.com:8080/';
|
||||
|
||||
test('should return a regular HTTP agent when no proxy is set', async () => {
|
||||
const { agent, protocol } = getAgentWithProxy({
|
||||
targetUrl: baseUrlHttp,
|
||||
});
|
||||
expect(protocol).toEqual('http');
|
||||
expect(agent).toBeInstanceOf(HttpAgent);
|
||||
});
|
||||
|
||||
test('should return a regular HTTPS agent when no proxy is set', async () => {
|
||||
const { agent, protocol } = getAgentWithProxy({
|
||||
targetUrl: baseUrlHttps,
|
||||
});
|
||||
expect(protocol).toEqual('https');
|
||||
expect(agent).toBeInstanceOf(HttpsAgent);
|
||||
});
|
||||
|
||||
test('should use a proxyConfig object', async () => {
|
||||
const { agent, protocol } = getAgentWithProxy({
|
||||
targetUrl: baseUrlHttps,
|
||||
proxyConfig: {
|
||||
host: 'proxy-for-https.com',
|
||||
port: 8080,
|
||||
},
|
||||
});
|
||||
expect(protocol).toEqual('https');
|
||||
expect((agent as HttpsProxyAgent<string>).proxy.href).toEqual(proxyUrlHttps);
|
||||
});
|
||||
|
||||
test('should use a proxyConfig string', async () => {
|
||||
const { agent, protocol } = getAgentWithProxy({
|
||||
targetUrl: baseUrlHttps,
|
||||
proxyConfig: proxyUrlHttps,
|
||||
});
|
||||
expect(agent).toBeInstanceOf(HttpsProxyAgent);
|
||||
expect(protocol).toEqual('https');
|
||||
expect((agent as HttpsProxyAgent<string>).proxy.href).toEqual(proxyUrlHttps);
|
||||
});
|
||||
|
||||
describe('environment variables', () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeAll(() => {
|
||||
originalEnv = { ...process.env };
|
||||
process.env.HTTP_PROXY = proxyUrlHttp;
|
||||
process.env.HTTPS_PROXY = proxyUrlHttps;
|
||||
process.env.NO_PROXY = 'should-not-proxy.com';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
test('should proxy http requests (HTTP_PROXY)', async () => {
|
||||
const { agent, protocol } = getAgentWithProxy({
|
||||
targetUrl: baseUrlHttp,
|
||||
});
|
||||
expect(protocol).toEqual('http');
|
||||
expect(agent).toBeInstanceOf(HttpProxyAgent);
|
||||
expect((agent as HttpsProxyAgent<string>).proxy.href).toEqual(proxyUrlHttp);
|
||||
});
|
||||
|
||||
test('should proxy https requests (HTTPS_PROXY)', async () => {
|
||||
const { agent, protocol } = getAgentWithProxy({
|
||||
targetUrl: baseUrlHttps,
|
||||
});
|
||||
expect(protocol).toEqual('https');
|
||||
expect(agent).toBeInstanceOf(HttpsProxyAgent);
|
||||
expect((agent as HttpsProxyAgent<string>).proxy.href).toEqual(proxyUrlHttps);
|
||||
});
|
||||
|
||||
test('should not proxy some hosts based on NO_PROXY', async () => {
|
||||
const { agent, protocol } = getAgentWithProxy({
|
||||
targetUrl: 'https://should-not-proxy.com/foo',
|
||||
});
|
||||
expect(protocol).toEqual('https');
|
||||
expect(agent).toBeInstanceOf(HttpsAgent);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+41
-93
@@ -19,10 +19,8 @@ import type { AxiosError, AxiosHeaders, AxiosRequestConfig, AxiosResponse } from
|
||||
import axios from 'axios';
|
||||
import crypto, { createHmac } from 'crypto';
|
||||
import FormData from 'form-data';
|
||||
import { IncomingMessage, Agent as HttpAgent } from 'http';
|
||||
import { HttpProxyAgent } from 'http-proxy-agent';
|
||||
import { type AgentOptions, Agent as HttpsAgent } from 'https';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { type AgentOptions } from 'https';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import merge from 'lodash/merge';
|
||||
@@ -63,10 +61,10 @@ import type {
|
||||
} from 'n8n-workflow';
|
||||
import type { Token } from 'oauth-1.0a';
|
||||
import clientOAuth1 from 'oauth-1.0a';
|
||||
import proxyFromEnv from 'proxy-from-env';
|
||||
import { stringify } from 'qs';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { createHttpProxyAgent, createHttpsProxyAgent } from '@/http-proxy';
|
||||
import type { IResponseError } from '@/interfaces';
|
||||
|
||||
import { binaryToString } from './binary-helper-functions';
|
||||
@@ -89,95 +87,63 @@ axios.defaults.proxy = false;
|
||||
|
||||
function validateUrl(url?: string): boolean {
|
||||
if (!url) return false;
|
||||
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getUrlFromProxyConfig(proxyConfig: IHttpRequestOptions['proxy'] | string): string | null {
|
||||
if (typeof proxyConfig === 'string') {
|
||||
if (!validateUrl(proxyConfig)) {
|
||||
return null;
|
||||
}
|
||||
return proxyConfig;
|
||||
return validateUrl(proxyConfig) ? proxyConfig : null;
|
||||
}
|
||||
|
||||
if (!proxyConfig?.host) {
|
||||
return null;
|
||||
}
|
||||
if (!proxyConfig?.host) return null;
|
||||
|
||||
const { protocol, host, port, auth } = proxyConfig;
|
||||
const safeProtocol = protocol?.endsWith(':') ? protocol.replace(':', '') : (protocol ?? 'http');
|
||||
const safeProtocol = protocol?.endsWith(':') ? protocol.slice(0, -1) : (protocol ?? 'http');
|
||||
|
||||
try {
|
||||
const url = new URL(`${safeProtocol}://${host}`);
|
||||
|
||||
if (port !== undefined) {
|
||||
url.port = String(port);
|
||||
}
|
||||
|
||||
if (port !== undefined) url.port = String(port);
|
||||
if (auth?.username) {
|
||||
url.username = auth.username;
|
||||
url.password = auth.password ?? '';
|
||||
}
|
||||
|
||||
return url.href;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getTargetUrlFromAxiosConfig(axiosConfig: AxiosRequestConfig): string {
|
||||
const { url, baseURL } = axiosConfig;
|
||||
function buildTargetUrl(url?: string, baseURL?: string): string | undefined {
|
||||
if (!url) return undefined;
|
||||
|
||||
try {
|
||||
return new URL(url ?? '', baseURL).href;
|
||||
return baseURL ? new URL(url, baseURL).href : url;
|
||||
} catch {
|
||||
return '';
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
type AgentInfo = { protocol: 'http' | 'https'; agent: HttpAgent };
|
||||
|
||||
export function getAgentWithProxy({
|
||||
agentOptions,
|
||||
proxyConfig,
|
||||
targetUrl,
|
||||
}: {
|
||||
agentOptions?: AgentOptions;
|
||||
proxyConfig?: IHttpRequestOptions['proxy'] | string;
|
||||
targetUrl: string;
|
||||
}): AgentInfo {
|
||||
// If no proxy is set in config, use HTTP_PROXY/HTTPS_PROXY/ALL_PROXY from env depending on target URL
|
||||
// Also respect NO_PROXY to disable the proxy for certain hosts
|
||||
const proxyUrl = getUrlFromProxyConfig(proxyConfig) ?? proxyFromEnv.getProxyForUrl(targetUrl);
|
||||
const protocol = targetUrl.startsWith('https://') ? 'https' : 'http';
|
||||
|
||||
if (proxyUrl) {
|
||||
const ProxyAgent = protocol === 'http' ? HttpProxyAgent : HttpsProxyAgent;
|
||||
return { protocol, agent: new ProxyAgent(proxyUrl, agentOptions) };
|
||||
}
|
||||
|
||||
const Agent = protocol === 'http' ? HttpAgent : HttpsAgent;
|
||||
return { protocol, agent: new Agent(agentOptions) };
|
||||
}
|
||||
|
||||
const applyAgentToAxiosConfig = (
|
||||
function setAxiosAgents(
|
||||
config: AxiosRequestConfig,
|
||||
{ agent, protocol }: AgentInfo,
|
||||
): AxiosRequestConfig => {
|
||||
if (protocol === 'http') {
|
||||
config.httpAgent = agent;
|
||||
} else {
|
||||
config.httpsAgent = agent;
|
||||
}
|
||||
agentOptions?: AgentOptions,
|
||||
proxyConfig?: IHttpRequestOptions['proxy'] | string,
|
||||
): void {
|
||||
if (config.httpAgent || config.httpsAgent) return;
|
||||
|
||||
return config;
|
||||
};
|
||||
const customProxyUrl = getUrlFromProxyConfig(proxyConfig);
|
||||
|
||||
const targetUrl = buildTargetUrl(config.url, config.baseURL);
|
||||
|
||||
if (!targetUrl) return;
|
||||
|
||||
config.httpAgent = createHttpProxyAgent(customProxyUrl, targetUrl, agentOptions);
|
||||
config.httpsAgent = createHttpsProxyAgent(customProxyUrl, targetUrl, agentOptions);
|
||||
}
|
||||
|
||||
axios.interceptors.request.use((config) => {
|
||||
// If no content-type is set by us, prevent axios from force-setting the content-type to `application/x-www-form-urlencoded`
|
||||
@@ -185,13 +151,7 @@ axios.interceptors.request.use((config) => {
|
||||
config.headers.setContentType(false, false);
|
||||
}
|
||||
|
||||
if (!config.httpsAgent && !config.httpAgent) {
|
||||
const agent = getAgentWithProxy({
|
||||
targetUrl: getTargetUrlFromAxiosConfig(config),
|
||||
});
|
||||
|
||||
applyAgentToAxiosConfig(config, agent);
|
||||
}
|
||||
setAxiosAgents(config);
|
||||
|
||||
return config;
|
||||
});
|
||||
@@ -230,22 +190,21 @@ const getBeforeRedirectFn =
|
||||
(redirectedRequest: Record<string, any>) => {
|
||||
const redirectAgentOptions = {
|
||||
...agentOptions,
|
||||
|
||||
servername: redirectedRequest.hostname,
|
||||
};
|
||||
const redirectAgent = getAgentWithProxy({
|
||||
agentOptions: redirectAgentOptions,
|
||||
proxyConfig,
|
||||
targetUrl: redirectedRequest.href,
|
||||
});
|
||||
const customProxyUrl = getUrlFromProxyConfig(proxyConfig);
|
||||
|
||||
redirectedRequest.agent = redirectAgent.agent;
|
||||
if (redirectAgent.protocol === 'http') {
|
||||
redirectedRequest.agents.http = redirectAgent.agent;
|
||||
} else {
|
||||
redirectedRequest.agents.https = redirectAgent.agent;
|
||||
}
|
||||
// Create both agents and set them
|
||||
const targetUrl = redirectedRequest.href;
|
||||
const httpAgent = createHttpProxyAgent(customProxyUrl, targetUrl, redirectAgentOptions);
|
||||
const httpsAgent = createHttpsProxyAgent(customProxyUrl, targetUrl, redirectAgentOptions);
|
||||
|
||||
redirectedRequest.agent = redirectedRequest.href.startsWith('https://')
|
||||
? httpsAgent
|
||||
: httpAgent;
|
||||
redirectedRequest.agents = { http: httpAgent, https: httpsAgent };
|
||||
|
||||
// Copy auth headers
|
||||
if (axiosConfig.headers?.Authorization) {
|
||||
redirectedRequest.headers.Authorization = axiosConfig.headers.Authorization;
|
||||
}
|
||||
@@ -613,13 +572,7 @@ export async function parseRequestObject(requestObject: IRequestOptions) {
|
||||
axiosConfig.timeout = requestObject.timeout;
|
||||
}
|
||||
|
||||
const agent = getAgentWithProxy({
|
||||
agentOptions,
|
||||
proxyConfig: requestObject.proxy,
|
||||
targetUrl: getTargetUrlFromAxiosConfig(axiosConfig),
|
||||
});
|
||||
|
||||
applyAgentToAxiosConfig(axiosConfig, agent);
|
||||
setAxiosAgents(axiosConfig, agentOptions, requestObject.proxy);
|
||||
|
||||
axiosConfig.beforeRedirect = getBeforeRedirectFn(agentOptions, axiosConfig, requestObject.proxy);
|
||||
|
||||
@@ -799,12 +752,7 @@ export function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): Axios
|
||||
if (n8nRequest.skipSslCertificateValidation === true) {
|
||||
agentOptions.rejectUnauthorized = false;
|
||||
}
|
||||
const agent = getAgentWithProxy({
|
||||
agentOptions,
|
||||
proxyConfig: proxy,
|
||||
targetUrl: getTargetUrlFromAxiosConfig(axiosRequest),
|
||||
});
|
||||
applyAgentToAxiosConfig(axiosRequest, agent);
|
||||
setAxiosAgents(axiosRequest, agentOptions, proxy);
|
||||
|
||||
axiosRequest.beforeRedirect = getBeforeRedirectFn(agentOptions, axiosRequest, n8nRequest.proxy);
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import http from 'http';
|
||||
import { HttpProxyAgent } from 'http-proxy-agent';
|
||||
import https from 'https';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
import proxyFromEnv from 'proxy-from-env';
|
||||
|
||||
type ProxyRequestParameters = Parameters<HttpProxyAgent<string>['addRequest']>;
|
||||
type ProxyClientRequest = ProxyRequestParameters[0];
|
||||
type ProxyRequestOptions = ProxyRequestParameters[1];
|
||||
|
||||
function buildTargetUrl(hostname: string, port: number, protocol: 'http' | 'https'): string {
|
||||
const defaultPort = protocol === 'https' ? 443 : 80;
|
||||
const portSuffix = port === defaultPort ? '' : `:${port}`;
|
||||
return `${protocol}://${hostname}${portSuffix}`;
|
||||
}
|
||||
|
||||
function extractHostInfo(
|
||||
options: http.RequestOptions,
|
||||
defaultPort: number,
|
||||
): { hostname: string; port: number } {
|
||||
const hostname = options.hostname ?? options.host ?? 'localhost';
|
||||
const port =
|
||||
typeof options.port === 'string' ? parseInt(options.port, 10) : (options.port ?? defaultPort);
|
||||
return { hostname: String(hostname), port: Number(port) };
|
||||
}
|
||||
|
||||
function getOrCreateProxyAgent<T extends HttpProxyAgent<string> | HttpsProxyAgent<string>>(
|
||||
cache: Map<string, T>,
|
||||
proxyUrl: string,
|
||||
createAgent: (url: string) => T,
|
||||
): T {
|
||||
let proxyAgent = cache.get(proxyUrl);
|
||||
if (!proxyAgent) {
|
||||
proxyAgent = createAgent(proxyUrl);
|
||||
cache.set(proxyUrl, proxyAgent);
|
||||
}
|
||||
return proxyAgent;
|
||||
}
|
||||
|
||||
function createFallbackAgent<T extends http.Agent | https.Agent>(AgentClass: new () => T): T {
|
||||
return new AgentClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Node.js is working on native HTTP proxy support (as of Node.js 24)
|
||||
* When it is stable we can use it and remove this implementation
|
||||
*
|
||||
* https://nodejs.org/api/http.html#built-in-proxy-support
|
||||
*/
|
||||
class HttpProxyManager extends http.Agent {
|
||||
private readonly proxyAgentCache = new Map<string, HttpProxyAgent<string>>();
|
||||
private readonly fallbackAgent = createFallbackAgent(http.Agent);
|
||||
|
||||
addRequest(req: http.ClientRequest, options: http.RequestOptions) {
|
||||
const { hostname, port } = extractHostInfo(options, 80);
|
||||
const targetUrl = buildTargetUrl(hostname, port, 'http');
|
||||
const proxyUrl = proxyFromEnv.getProxyForUrl(targetUrl);
|
||||
|
||||
if (proxyUrl) {
|
||||
const proxyAgent = getOrCreateProxyAgent(
|
||||
this.proxyAgentCache,
|
||||
proxyUrl,
|
||||
(url) => new HttpProxyAgent(url),
|
||||
);
|
||||
return proxyAgent.addRequest(req as ProxyClientRequest, options as ProxyRequestOptions);
|
||||
}
|
||||
|
||||
return this.fallbackAgent.addRequest(req, options);
|
||||
}
|
||||
}
|
||||
|
||||
class HttpsProxyManager extends https.Agent {
|
||||
private readonly proxyAgentCache = new Map<string, HttpsProxyAgent<string>>();
|
||||
private readonly fallbackAgent = createFallbackAgent(https.Agent);
|
||||
|
||||
addRequest(req: http.ClientRequest, options: https.RequestOptions) {
|
||||
const { hostname, port } = extractHostInfo(options, 443);
|
||||
const targetUrl = buildTargetUrl(hostname, port, 'https');
|
||||
const proxyUrl = proxyFromEnv.getProxyForUrl(targetUrl);
|
||||
|
||||
if (proxyUrl) {
|
||||
const proxyAgent = getOrCreateProxyAgent(
|
||||
this.proxyAgentCache,
|
||||
proxyUrl,
|
||||
(url) => new HttpsProxyAgent(url),
|
||||
);
|
||||
return proxyAgent.addRequest(req, options);
|
||||
}
|
||||
|
||||
return this.fallbackAgent.addRequest(req, options);
|
||||
}
|
||||
}
|
||||
|
||||
export function createHttpProxyAgent(
|
||||
customProxyUrl: string | null = null,
|
||||
targetUrl: string,
|
||||
options?: http.AgentOptions,
|
||||
): http.Agent {
|
||||
const proxyUrl = customProxyUrl ?? proxyFromEnv.getProxyForUrl(targetUrl);
|
||||
|
||||
if (proxyUrl) {
|
||||
return new HttpProxyAgent(proxyUrl, options);
|
||||
}
|
||||
|
||||
return new http.Agent(options);
|
||||
}
|
||||
|
||||
export function createHttpsProxyAgent(
|
||||
customProxyUrl: string | null = null,
|
||||
targetUrl: string,
|
||||
options?: https.AgentOptions,
|
||||
): https.Agent {
|
||||
const proxyUrl = customProxyUrl ?? proxyFromEnv.getProxyForUrl(targetUrl);
|
||||
|
||||
if (proxyUrl) {
|
||||
return new HttpsProxyAgent(proxyUrl, options);
|
||||
}
|
||||
|
||||
return new https.Agent(options);
|
||||
}
|
||||
|
||||
function hasProxyEnvironmentVariables(): boolean {
|
||||
return Boolean(process.env.HTTP_PROXY ?? process.env.HTTPS_PROXY ?? process.env.ALL_PROXY);
|
||||
}
|
||||
|
||||
export function installGlobalProxyAgent(): void {
|
||||
if (hasProxyEnvironmentVariables()) {
|
||||
LoggerProxy.debug('Installing global HTTP proxy agents', {
|
||||
HTTP_PROXY: process.env.HTTP_PROXY,
|
||||
HTTPS_PROXY: process.env.HTTPS_PROXY,
|
||||
NO_PROXY: process.env.NO_PROXY,
|
||||
ALL_PROXY: process.env.ALL_PROXY,
|
||||
});
|
||||
|
||||
http.globalAgent = new HttpProxyManager();
|
||||
https.globalAgent = new HttpsProxyManager();
|
||||
}
|
||||
}
|
||||
|
||||
export function uninstallGlobalProxyAgent(): void {
|
||||
http.globalAgent = new http.Agent();
|
||||
https.globalAgent = new https.Agent();
|
||||
}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import type http from 'http-proxy-agent';
|
||||
import type https from 'https-proxy-agent';
|
||||
|
||||
declare module 'http' {
|
||||
interface Agent {
|
||||
addRequest(req: ClientRequest, options: http.HttpProxyAgentOptions): void;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'https' {
|
||||
interface Agent {
|
||||
addRequest(req: ClientRequest, options: https.HttpsProxyAgentOptions): void;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export * from './html-sandbox';
|
||||
export * from './instance-settings';
|
||||
export * from './nodes-loader';
|
||||
export * from './utils';
|
||||
export * from './http-proxy';
|
||||
export { WorkflowHasIssuesError } from './errors/workflow-has-issues.error';
|
||||
|
||||
export type * from './interfaces';
|
||||
|
||||
Reference in New Issue
Block a user