Skip to content

Commit a316b22

Browse files
feat: add tls options support
1 parent 6150f06 commit a316b22

File tree

6 files changed

+157
-1
lines changed

6 files changed

+157
-1
lines changed

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"hono": "^4.6.10",
5454
"jose": "^6.0.11",
5555
"patch-package": "^8.0.0",
56+
"undici": "^7.12.0",
5657
"ws": "^8.18.0",
5758
"zod": "^3.22.4"
5859
},

src/handlers/handlerUtils.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Context } from 'hono';
2+
import { Agent } from 'undici/types';
23
import {
34
AZURE_OPEN_AI,
45
BEDROCK,
@@ -27,6 +28,7 @@ import { ConditionalRouter } from '../services/conditionalRouter';
2728
import { RouterError } from '../errors/RouterError';
2829
import { GatewayError } from '../errors/GatewayError';
2930
import { HookType } from '../middlewares/hooks/types';
31+
import { getCustomHttpsAgent } from '../utils/fetch';
3032

3133
// Services
3234
import { CacheResponseObject, CacheService } from './services/cacheService';
@@ -177,12 +179,22 @@ export async function constructRequest(
177179
providerMappedHeaders
178180
);
179181

180-
const fetchOptions: RequestInit = {
182+
const fetchOptions: RequestInit & { dispatcher?: Agent } = {
181183
method: requestContext.method,
182184
headers,
183185
...(requestContext.endpoint === 'uploadFile' && { duplex: 'half' }),
184186
};
185187

188+
const { tlsOptions } = requestContext.providerOption;
189+
if (tlsOptions?.ca || tlsOptions?.rejectUnauthorized === false) {
190+
// SECURITY NOTE: The following allows to disable TLS certificate validation
191+
192+
fetchOptions.dispatcher = getCustomHttpsAgent({
193+
rejectUnauthorized: tlsOptions?.rejectUnauthorized,
194+
ca: tlsOptions?.ca,
195+
});
196+
}
197+
186198
const body = constructRequestBody(requestContext, providerMappedHeaders);
187199
if (body) {
188200
fetchOptions.body = body;
@@ -950,6 +962,15 @@ export function constructConfigFromRequestHeaders(
950962
anthropicVersion: requestHeaders[`x-${POWERED_BY}-anthropic-version`],
951963
};
952964

965+
let tlsOptions = undefined;
966+
if (requestHeaders['x-portkey-tls-options']) {
967+
try {
968+
tlsOptions = JSON.parse(requestHeaders['x-portkey-tls-options']);
969+
} catch (e) {
970+
console.warn('Failed to parse x-portkey-tls-options:', e);
971+
}
972+
}
973+
953974
const vertexServiceAccountJson =
954975
requestHeaders[`x-${POWERED_BY}-vertex-service-account-json`];
955976

@@ -1120,6 +1141,7 @@ export function constructConfigFromRequestHeaders(
11201141
...(requestHeaders[`x-${POWERED_BY}-provider`] === FIREWORKS_AI &&
11211142
fireworksConfig),
11221143
...(requestHeaders[`x-${POWERED_BY}-provider`] === CORTEX && cortexConfig),
1144+
tlsOptions,
11231145
};
11241146
}
11251147

src/types/requestBody.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,18 @@ export interface Options {
157157

158158
/** Cortex specific fields */
159159
snowflakeAccount?: string;
160+
161+
/**
162+
* TLS options for outgoing requests. Example:
163+
* {
164+
* ca?: string; // CA certificate(s) in PEM format
165+
* rejectUnauthorized?: boolean;
166+
* }
167+
*/
168+
tlsOptions?: {
169+
ca?: string;
170+
rejectUnauthorized?: boolean;
171+
};
160172
}
161173

162174
/**

src/utils/fetch.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Agent } from 'undici';
2+
3+
/**
4+
* Creates a custom HTTPS agent with SSL configuration options
5+
*
6+
* @param options - Configuration options for the HTTPS agent
7+
* @param options.rejectUnauthorized - Whether to reject unauthorized certificates (default: true)
8+
* @param options.ca - Custom CA certificate (optional)
9+
* @returns HTTPS Agent instance
10+
*/
11+
export function getCustomHttpsAgent(
12+
options: {
13+
rejectUnauthorized?: boolean;
14+
ca?: string | Buffer;
15+
cert?: string | Buffer;
16+
key?: string | Buffer;
17+
} = {}
18+
): Agent {
19+
const { rejectUnauthorized = true, ca } = options || {};
20+
21+
return new Agent({
22+
connect: {
23+
rejectUnauthorized,
24+
...(ca && { ca }),
25+
},
26+
});
27+
}

tests/unit/src/utils/fetch.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Agent } from 'undici';
2+
import { getCustomHttpsAgent } from '../../../../src/utils/fetch';
3+
4+
const mockCa =
5+
'-----BEGIN CERTIFICATE-----\nMOCK_CA_CERT\n-----END CERTIFICATE-----';
6+
7+
function getSymbolValue<T extends object>(
8+
obj: T,
9+
description: string
10+
): unknown {
11+
const symbol = Object.getOwnPropertySymbols(obj).find(
12+
(s) => s.description === description
13+
);
14+
return symbol ? (obj as any)[symbol] : undefined;
15+
}
16+
17+
describe('getCustomHttpsAgent', () => {
18+
describe('default behavior', () => {
19+
it('should create an Agent with rejectUnauthorized true by default', () => {
20+
const agent = getCustomHttpsAgent();
21+
const optionsValue = getSymbolValue(agent, 'options');
22+
23+
expect(agent).toBeInstanceOf(Agent);
24+
expect(optionsValue).toEqual({
25+
connect: { rejectUnauthorized: true },
26+
});
27+
});
28+
});
29+
30+
describe('with custom options', () => {
31+
it('should create an Agent with custom rejectUnauthorized option', () => {
32+
const agent = getCustomHttpsAgent({ rejectUnauthorized: false });
33+
const optionsValue = getSymbolValue(agent, 'options');
34+
35+
expect(agent).toBeInstanceOf(Agent);
36+
expect(optionsValue).toEqual({
37+
connect: { rejectUnauthorized: false },
38+
});
39+
});
40+
41+
it('should create an Agent with custom CA certificate', () => {
42+
const agent = getCustomHttpsAgent({ ca: mockCa });
43+
const optionsValue = getSymbolValue(agent, 'options');
44+
45+
expect(agent).toBeInstanceOf(Agent);
46+
expect(optionsValue).toEqual({
47+
connect: { ca: mockCa, rejectUnauthorized: true },
48+
});
49+
});
50+
});
51+
52+
describe('with Buffer inputs', () => {
53+
it('should create an Agent with Buffer CA certificate', () => {
54+
const mockCaBuffer = Buffer.from(mockCa);
55+
const agent = getCustomHttpsAgent({ ca: mockCaBuffer });
56+
const optionsValue = getSymbolValue(agent, 'options');
57+
58+
expect(agent).toBeInstanceOf(Agent);
59+
expect(optionsValue).toEqual({
60+
connect: { ca: mockCaBuffer, rejectUnauthorized: true },
61+
});
62+
});
63+
});
64+
65+
describe('edge cases', () => {
66+
it('should accept optional options parameter', () => {
67+
// Test that the function can be called without parameters
68+
const agent1 = getCustomHttpsAgent();
69+
expect(agent1).toBeInstanceOf(Agent);
70+
71+
// Test that the function can be called with empty options
72+
const agent2 = getCustomHttpsAgent({});
73+
expect(agent2).toBeInstanceOf(Agent);
74+
75+
// Test that the function can be called with partial options
76+
const agent3 = getCustomHttpsAgent({ rejectUnauthorized: false });
77+
expect(agent3).toBeInstanceOf(Agent);
78+
79+
// Test that the function can be called with null options
80+
const agent4 = getCustomHttpsAgent(null as any);
81+
expect(agent4).toBeInstanceOf(Agent);
82+
});
83+
});
84+
});

0 commit comments

Comments
 (0)