mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 14:16:43 +00:00
fix: guard self-signed routing request setup
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import * as https from 'https';
|
||||
import { describe, expect, it, mock, spyOn } from 'bun:test';
|
||||
import type { ProxyTarget } from '../../proxy/proxy-target-resolver';
|
||||
|
||||
async function loadRoutingHttpModule() {
|
||||
@@ -22,6 +23,73 @@ describe('routing-strategy-http', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects malformed self-signed HTTPS URLs before arming the request timeout', async () => {
|
||||
const { fetchCliproxyRoutingResponse } = await loadRoutingHttpModule();
|
||||
const originalSetTimeout = globalThis.setTimeout;
|
||||
globalThis.setTimeout = mock(((handler: TimerHandler, timeout?: number) => {
|
||||
void handler;
|
||||
void timeout;
|
||||
return 1 as unknown as ReturnType<typeof setTimeout>;
|
||||
}) as typeof setTimeout);
|
||||
|
||||
const target: ProxyTarget = {
|
||||
host: 'bad host',
|
||||
port: 443,
|
||||
protocol: 'https',
|
||||
allowSelfSigned: true,
|
||||
isRemote: true,
|
||||
};
|
||||
|
||||
try {
|
||||
await expect(fetchCliproxyRoutingResponse(target, 'GET')).rejects.toThrow();
|
||||
expect(globalThis.setTimeout).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
globalThis.setTimeout = originalSetTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
it('clears the request timeout when https.request throws synchronously', async () => {
|
||||
const { fetchCliproxyRoutingResponse } = await loadRoutingHttpModule();
|
||||
const originalSetTimeout = globalThis.setTimeout;
|
||||
const originalClearTimeout = globalThis.clearTimeout;
|
||||
const activeTimers = new Set<number>();
|
||||
|
||||
globalThis.setTimeout = mock(((handler: TimerHandler, timeout?: number) => {
|
||||
void handler;
|
||||
void timeout;
|
||||
const timerId = activeTimers.size + 1;
|
||||
activeTimers.add(timerId);
|
||||
return timerId as unknown as ReturnType<typeof setTimeout>;
|
||||
}) as typeof setTimeout);
|
||||
globalThis.clearTimeout = mock(((timerId?: ReturnType<typeof setTimeout>) => {
|
||||
activeTimers.delete(timerId as unknown as number);
|
||||
}) as typeof clearTimeout);
|
||||
const requestSpy = spyOn(https, 'request').mockImplementation(() => {
|
||||
throw new Error('sync request failure');
|
||||
});
|
||||
|
||||
const target: ProxyTarget = {
|
||||
host: 'proxy.example.com',
|
||||
port: 443,
|
||||
protocol: 'https',
|
||||
allowSelfSigned: true,
|
||||
isRemote: true,
|
||||
};
|
||||
|
||||
try {
|
||||
await expect(fetchCliproxyRoutingResponse(target, 'GET')).rejects.toThrow(
|
||||
'sync request failure'
|
||||
);
|
||||
expect(globalThis.setTimeout).toHaveBeenCalledTimes(1);
|
||||
expect(globalThis.clearTimeout).toHaveBeenCalledTimes(1);
|
||||
expect(activeTimers.size).toBe(0);
|
||||
} finally {
|
||||
requestSpy.mockRestore();
|
||||
globalThis.setTimeout = originalSetTimeout;
|
||||
globalThis.clearTimeout = originalClearTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
it('builds the remote management URL for routing strategy writes', async () => {
|
||||
const { getCliproxyRoutingManagementUrl } = await loadRoutingHttpModule();
|
||||
const target: ProxyTarget = {
|
||||
|
||||
@@ -40,6 +40,8 @@ export async function fetchCliproxyRoutingResponse(
|
||||
}
|
||||
}
|
||||
|
||||
const requestUrl = new URL(url);
|
||||
|
||||
return new Promise<Response>((resolve, reject) => {
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
let settled = false;
|
||||
@@ -51,57 +53,65 @@ export async function fetchCliproxyRoutingResponse(
|
||||
callback();
|
||||
};
|
||||
|
||||
let req: ReturnType<typeof https.request> | undefined;
|
||||
const timeoutId = setTimeout(() => {
|
||||
const error = new Error('Request timeout');
|
||||
req.destroy(error);
|
||||
req?.destroy(error);
|
||||
settle(() => reject(error));
|
||||
}, ROUTING_TIMEOUT_MS);
|
||||
|
||||
const req = https.request(
|
||||
url,
|
||||
{
|
||||
method,
|
||||
headers,
|
||||
agent,
|
||||
timeout: ROUTING_TIMEOUT_MS,
|
||||
},
|
||||
(res) => {
|
||||
let payload = '';
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk) => {
|
||||
payload += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
settle(() =>
|
||||
resolve(
|
||||
new Response(payload, {
|
||||
status: res.statusCode || 500,
|
||||
statusText: res.statusMessage ?? '',
|
||||
headers:
|
||||
typeof res.headers['content-type'] === 'string'
|
||||
? { 'Content-Type': res.headers['content-type'] }
|
||||
: undefined,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
try {
|
||||
req = https.request(
|
||||
requestUrl,
|
||||
{
|
||||
method,
|
||||
headers,
|
||||
agent,
|
||||
timeout: ROUTING_TIMEOUT_MS,
|
||||
},
|
||||
(res) => {
|
||||
let payload = '';
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk) => {
|
||||
payload += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
settle(() =>
|
||||
resolve(
|
||||
new Response(payload, {
|
||||
status: res.statusCode || 500,
|
||||
statusText: res.statusMessage ?? '',
|
||||
headers:
|
||||
typeof res.headers['content-type'] === 'string'
|
||||
? { 'Content-Type': res.headers['content-type'] }
|
||||
: undefined,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
settle(() => reject(error));
|
||||
return;
|
||||
}
|
||||
|
||||
req.on('error', (error) => {
|
||||
const request = req;
|
||||
|
||||
request.on('error', (error) => {
|
||||
settle(() => reject(error));
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
request.on('timeout', () => {
|
||||
const error = new Error('Request timeout');
|
||||
req.destroy(error);
|
||||
request.destroy(error);
|
||||
settle(() => reject(error));
|
||||
});
|
||||
|
||||
if (body) {
|
||||
req.write(JSON.stringify(body));
|
||||
request.write(JSON.stringify(body));
|
||||
}
|
||||
req.end();
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user