diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index 49be48ef542f5..e9271c24456b5 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -9,6 +9,7 @@ export * from './rest_spec'; export interface IngestManagerConfigType { enabled: boolean; registryUrl?: string; + registryProxyUrl?: string; agents: { enabled: boolean; tlsCheckDisabled: boolean; diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 70685cf818b08..ad2eecc0bb057 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -31,6 +31,7 @@ export const config: PluginConfigDescriptor = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), registryUrl: schema.maybe(schema.uri()), + registryProxyUrl: schema.maybe(schema.uri()), agents: schema.object({ enabled: schema.boolean({ defaultValue: true }), tlsCheckDisabled: schema.boolean({ defaultValue: false }), diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/proxy.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/proxy.test.ts new file mode 100644 index 0000000000000..d6e60eb11b230 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/proxy.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { getProxyAgent, getProxyAgentOptions } from './proxy'; + +describe('getProxyAgent', () => { + test('return HttpsProxyAgent for https proxy url', () => { + const agent = getProxyAgent({ + proxyUrl: 'https://proxyhost', + targetUrl: 'https://targethost', + }); + expect(agent instanceof HttpsProxyAgent).toBeTruthy(); + }); + + test('return HttpProxyAgent for http proxy url', () => { + const agent = getProxyAgent({ + proxyUrl: 'http://proxyhost', + targetUrl: 'http://targethost', + }); + expect(agent instanceof HttpProxyAgent).toBeTruthy(); + }); +}); + +describe('getProxyAgentOptions', () => { + test('return url only for https', () => { + const httpsProxy = 'https://12.34.56.78:910'; + + const optionsA = getProxyAgentOptions({ + proxyUrl: httpsProxy, + targetUrl: 'https://targethost', + }); + expect(optionsA).toEqual({ + headers: { Host: 'targethost' }, + host: '12.34.56.78', + port: 910, + protocol: 'https:', + rejectUnauthorized: undefined, + }); + + const optionsB = getProxyAgentOptions({ + proxyUrl: httpsProxy, + targetUrl: 'https://example.com/?a=b&c=d', + }); + expect(optionsB).toEqual({ + headers: { Host: 'example.com' }, + host: '12.34.56.78', + port: 910, + protocol: 'https:', + rejectUnauthorized: undefined, + }); + + // given http value and https proxy + const optionsC = getProxyAgentOptions({ + proxyUrl: httpsProxy, + targetUrl: 'http://example.com/?a=b&c=d', + }); + expect(optionsC).toEqual({ + headers: { Host: 'example.com' }, + host: '12.34.56.78', + port: 910, + protocol: 'https:', + rejectUnauthorized: undefined, + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/proxy.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/proxy.ts new file mode 100644 index 0000000000000..ae1c13761052b --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/proxy.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import HttpProxyAgent from 'http-proxy-agent'; +import HttpsProxyAgent, { + HttpsProxyAgent as IHttpsProxyAgent, + HttpsProxyAgentOptions, +} from 'https-proxy-agent'; +import { appContextService } from '../../index'; +export interface RegistryProxySettings { + proxyUrl: string; + proxyHeaders?: Record; + proxyRejectUnauthorizedCertificates?: boolean; +} + +type ProxyAgent = IHttpsProxyAgent | HttpProxyAgent; +type GetProxyAgentParams = RegistryProxySettings & { targetUrl: string }; + +export function getRegistryProxyUrl(): string | undefined { + const proxyUrl = appContextService.getConfig()?.registryProxyUrl; + return proxyUrl; +} + +export function getProxyAgent(options: GetProxyAgentParams): ProxyAgent { + const isHttps = options.targetUrl.startsWith('https:'); + const agentOptions = isHttps && getProxyAgentOptions(options); + const agent: ProxyAgent = isHttps + ? // @ts-expect-error ts(7009) HttpsProxyAgent isn't a class so TS complains about using `new` + new HttpsProxyAgent(agentOptions) + : new HttpProxyAgent(options.proxyUrl); + + return agent; +} + +export function getProxyAgentOptions(options: GetProxyAgentParams): HttpsProxyAgentOptions { + const endpointParsed = new URL(options.targetUrl); + const proxyParsed = new URL(options.proxyUrl); + + return { + host: proxyParsed.hostname, + port: Number(proxyParsed.port), + protocol: proxyParsed.protocol, + // The headers to send + headers: options.proxyHeaders || { + // the proxied URL's host is put in the header instead of the server's actual host + Host: endpointParsed.host, + }, + // do not fail on invalid certs if value is false + rejectUnauthorized: options.proxyRejectUnauthorizedCertificates, + }; +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.ts index e549d6b1f71aa..2b9c349565790 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.ts @@ -4,17 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import fetch, { FetchError, Response } from 'node-fetch'; +import fetch, { FetchError, Response, RequestInit } from 'node-fetch'; import pRetry from 'p-retry'; import { streamToString } from './streams'; +import { appContextService } from '../../app_context'; import { RegistryError, RegistryConnectionError, RegistryResponseError } from '../../../errors'; +import { getProxyAgent, getRegistryProxyUrl } from './proxy'; type FailedAttemptErrors = pRetry.FailedAttemptError | FetchError | Error; // not sure what to call this function, but we're not exporting it async function registryFetch(url: string) { - const response = await fetch(url); - + const response = await fetch(url, getFetchOptions(url)); if (response.ok) { return response; } else { @@ -81,3 +82,17 @@ function isFetchError(error: FailedAttemptErrors): error is FetchError { function isSystemError(error: FailedAttemptErrors): boolean { return isFetchError(error) && error.type === 'system'; } + +export function getFetchOptions(targetUrl: string): RequestInit | undefined { + const proxyUrl = getRegistryProxyUrl(); + if (!proxyUrl) { + return undefined; + } + + const logger = appContextService.getLogger(); + logger.debug(`Using ${proxyUrl} as proxy for ${targetUrl}`); + + return { + agent: getProxyAgent({ proxyUrl, targetUrl }), + }; +}