From a316b22ac0649a92480893d96f8c14dc63bca9ac Mon Sep 17 00:00:00 2001 From: arturfromtabnine Date: Thu, 31 Jul 2025 12:49:18 +0200 Subject: [PATCH 1/2] feat: add tls options support --- package-lock.json | 10 ++++ package.json | 1 + src/handlers/handlerUtils.ts | 24 ++++++++- src/types/requestBody.ts | 12 +++++ src/utils/fetch.ts | 27 ++++++++++ tests/unit/src/utils/fetch.test.ts | 84 ++++++++++++++++++++++++++++++ 6 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/utils/fetch.ts create mode 100644 tests/unit/src/utils/fetch.test.ts diff --git a/package-lock.json b/package-lock.json index 01750ed44..43dabe122 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "hono": "^4.6.10", "jose": "^6.0.11", "patch-package": "^8.0.0", + "undici": "^7.12.0", "ws": "^8.18.0", "zod": "^3.22.4" }, @@ -7633,6 +7634,15 @@ "dev": true, "license": "MIT" }, + "node_modules/undici": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.12.0.tgz", + "integrity": "sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/package.json b/package.json index 0fea243ed..c0cbbffe4 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "hono": "^4.6.10", "jose": "^6.0.11", "patch-package": "^8.0.0", + "undici": "^7.12.0", "ws": "^8.18.0", "zod": "^3.22.4" }, diff --git a/src/handlers/handlerUtils.ts b/src/handlers/handlerUtils.ts index d553ba78a..22656194d 100644 --- a/src/handlers/handlerUtils.ts +++ b/src/handlers/handlerUtils.ts @@ -1,4 +1,5 @@ import { Context } from 'hono'; +import { Agent } from 'undici/types'; import { AZURE_OPEN_AI, BEDROCK, @@ -27,6 +28,7 @@ import { ConditionalRouter } from '../services/conditionalRouter'; import { RouterError } from '../errors/RouterError'; import { GatewayError } from '../errors/GatewayError'; import { HookType } from '../middlewares/hooks/types'; +import { getCustomHttpsAgent } from '../utils/fetch'; // Services import { CacheResponseObject, CacheService } from './services/cacheService'; @@ -177,12 +179,22 @@ export async function constructRequest( providerMappedHeaders ); - const fetchOptions: RequestInit = { + const fetchOptions: RequestInit & { dispatcher?: Agent } = { method: requestContext.method, headers, ...(requestContext.endpoint === 'uploadFile' && { duplex: 'half' }), }; + const { tlsOptions } = requestContext.providerOption; + if (tlsOptions?.ca || tlsOptions?.rejectUnauthorized === false) { + // SECURITY NOTE: The following allows to disable TLS certificate validation + + fetchOptions.dispatcher = getCustomHttpsAgent({ + rejectUnauthorized: tlsOptions?.rejectUnauthorized, + ca: tlsOptions?.ca, + }); + } + const body = constructRequestBody(requestContext, providerMappedHeaders); if (body) { fetchOptions.body = body; @@ -950,6 +962,15 @@ export function constructConfigFromRequestHeaders( anthropicVersion: requestHeaders[`x-${POWERED_BY}-anthropic-version`], }; + let tlsOptions = undefined; + if (requestHeaders['x-portkey-tls-options']) { + try { + tlsOptions = JSON.parse(requestHeaders['x-portkey-tls-options']); + } catch (e) { + console.warn('Failed to parse x-portkey-tls-options:', e); + } + } + const vertexServiceAccountJson = requestHeaders[`x-${POWERED_BY}-vertex-service-account-json`]; @@ -1120,6 +1141,7 @@ export function constructConfigFromRequestHeaders( ...(requestHeaders[`x-${POWERED_BY}-provider`] === FIREWORKS_AI && fireworksConfig), ...(requestHeaders[`x-${POWERED_BY}-provider`] === CORTEX && cortexConfig), + tlsOptions, }; } diff --git a/src/types/requestBody.ts b/src/types/requestBody.ts index df048b801..b71edb6ac 100644 --- a/src/types/requestBody.ts +++ b/src/types/requestBody.ts @@ -157,6 +157,18 @@ export interface Options { /** Cortex specific fields */ snowflakeAccount?: string; + + /** + * TLS options for outgoing requests. Example: + * { + * ca?: string; // CA certificate(s) in PEM format + * rejectUnauthorized?: boolean; + * } + */ + tlsOptions?: { + ca?: string; + rejectUnauthorized?: boolean; + }; } /** diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts new file mode 100644 index 000000000..e2fd75741 --- /dev/null +++ b/src/utils/fetch.ts @@ -0,0 +1,27 @@ +import { Agent } from 'undici'; + +/** + * Creates a custom HTTPS agent with SSL configuration options + * + * @param options - Configuration options for the HTTPS agent + * @param options.rejectUnauthorized - Whether to reject unauthorized certificates (default: true) + * @param options.ca - Custom CA certificate (optional) + * @returns HTTPS Agent instance + */ +export function getCustomHttpsAgent( + options: { + rejectUnauthorized?: boolean; + ca?: string | Buffer; + cert?: string | Buffer; + key?: string | Buffer; + } = {} +): Agent { + const { rejectUnauthorized = true, ca } = options || {}; + + return new Agent({ + connect: { + rejectUnauthorized, + ...(ca && { ca }), + }, + }); +} diff --git a/tests/unit/src/utils/fetch.test.ts b/tests/unit/src/utils/fetch.test.ts new file mode 100644 index 000000000..61e54b318 --- /dev/null +++ b/tests/unit/src/utils/fetch.test.ts @@ -0,0 +1,84 @@ +import { Agent } from 'undici'; +import { getCustomHttpsAgent } from '../../../../src/utils/fetch'; + +const mockCa = + '-----BEGIN CERTIFICATE-----\nMOCK_CA_CERT\n-----END CERTIFICATE-----'; + +function getSymbolValue( + obj: T, + description: string +): unknown { + const symbol = Object.getOwnPropertySymbols(obj).find( + (s) => s.description === description + ); + return symbol ? (obj as any)[symbol] : undefined; +} + +describe('getCustomHttpsAgent', () => { + describe('default behavior', () => { + it('should create an Agent with rejectUnauthorized true by default', () => { + const agent = getCustomHttpsAgent(); + const optionsValue = getSymbolValue(agent, 'options'); + + expect(agent).toBeInstanceOf(Agent); + expect(optionsValue).toEqual({ + connect: { rejectUnauthorized: true }, + }); + }); + }); + + describe('with custom options', () => { + it('should create an Agent with custom rejectUnauthorized option', () => { + const agent = getCustomHttpsAgent({ rejectUnauthorized: false }); + const optionsValue = getSymbolValue(agent, 'options'); + + expect(agent).toBeInstanceOf(Agent); + expect(optionsValue).toEqual({ + connect: { rejectUnauthorized: false }, + }); + }); + + it('should create an Agent with custom CA certificate', () => { + const agent = getCustomHttpsAgent({ ca: mockCa }); + const optionsValue = getSymbolValue(agent, 'options'); + + expect(agent).toBeInstanceOf(Agent); + expect(optionsValue).toEqual({ + connect: { ca: mockCa, rejectUnauthorized: true }, + }); + }); + }); + + describe('with Buffer inputs', () => { + it('should create an Agent with Buffer CA certificate', () => { + const mockCaBuffer = Buffer.from(mockCa); + const agent = getCustomHttpsAgent({ ca: mockCaBuffer }); + const optionsValue = getSymbolValue(agent, 'options'); + + expect(agent).toBeInstanceOf(Agent); + expect(optionsValue).toEqual({ + connect: { ca: mockCaBuffer, rejectUnauthorized: true }, + }); + }); + }); + + describe('edge cases', () => { + it('should accept optional options parameter', () => { + // Test that the function can be called without parameters + const agent1 = getCustomHttpsAgent(); + expect(agent1).toBeInstanceOf(Agent); + + // Test that the function can be called with empty options + const agent2 = getCustomHttpsAgent({}); + expect(agent2).toBeInstanceOf(Agent); + + // Test that the function can be called with partial options + const agent3 = getCustomHttpsAgent({ rejectUnauthorized: false }); + expect(agent3).toBeInstanceOf(Agent); + + // Test that the function can be called with null options + const agent4 = getCustomHttpsAgent(null as any); + expect(agent4).toBeInstanceOf(Agent); + }); + }); +}); From 3ef5884cd0c4d135e82bab82d96af2d342f4f965 Mon Sep 17 00:00:00 2001 From: arturfromtabnine Date: Thu, 31 Jul 2025 13:12:35 +0200 Subject: [PATCH 2/2] feat: adjust --- src/utils/fetch.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts index e2fd75741..4c39690e1 100644 --- a/src/utils/fetch.ts +++ b/src/utils/fetch.ts @@ -12,8 +12,6 @@ export function getCustomHttpsAgent( options: { rejectUnauthorized?: boolean; ca?: string | Buffer; - cert?: string | Buffer; - key?: string | Buffer; } = {} ): Agent { const { rejectUnauthorized = true, ca } = options || {};