Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
24 changes: 23 additions & 1 deletion src/handlers/handlerUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Context } from 'hono';
import { Agent } from 'undici/types';
import {
AZURE_OPEN_AI,
BEDROCK,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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`];

Expand Down Expand Up @@ -1120,6 +1141,7 @@ export function constructConfigFromRequestHeaders(
...(requestHeaders[`x-${POWERED_BY}-provider`] === FIREWORKS_AI &&
fireworksConfig),
...(requestHeaders[`x-${POWERED_BY}-provider`] === CORTEX && cortexConfig),
tlsOptions,
};
}

Expand Down
12 changes: 12 additions & 0 deletions src/types/requestBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}

/**
Expand Down
25 changes: 25 additions & 0 deletions src/utils/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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;
} = {}
): Agent {
const { rejectUnauthorized = true, ca } = options || {};

return new Agent({
connect: {
rejectUnauthorized,
...(ca && { ca }),
},
});
}
84 changes: 84 additions & 0 deletions tests/unit/src/utils/fetch.test.ts
Original file line number Diff line number Diff line change
@@ -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<T extends object>(
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);
});
});
});