mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 12:14:19 +00:00
fix: guard self-signed routing request setup
This commit is contained in:
1 parent
d49bbdaec3
commit
5807328cb5
2 files changed
+115
-37
No files matched your search
@@ -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';
|
import type { ProxyTarget } from '../../proxy/proxy-target-resolver';
|
||||||
|
|
||||||
async function loadRoutingHttpModule() {
|
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 () => {
|
it('builds the remote management URL for routing strategy writes', async () => {
|
||||||
const { getCliproxyRoutingManagementUrl } = await loadRoutingHttpModule();
|
const { getCliproxyRoutingManagementUrl } = await loadRoutingHttpModule();
|
||||||
const target: ProxyTarget = {
|
const target: ProxyTarget = {
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export async function fetchCliproxyRoutingResponse(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestUrl = new URL(url);
|
||||||
|
|
||||||
return new Promise<Response>((resolve, reject) => {
|
return new Promise<Response>((resolve, reject) => {
|
||||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||||
let settled = false;
|
let settled = false;
|
||||||
@@ -51,57 +53,65 @@ export async function fetchCliproxyRoutingResponse(
|
|||||||
callback();
|
callback();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let req: ReturnType<typeof https.request> | undefined;
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
const error = new Error('Request timeout');
|
const error = new Error('Request timeout');
|
||||||
req.destroy(error);
|
req?.destroy(error);
|
||||||
settle(() => reject(error));
|
settle(() => reject(error));
|
||||||
}, ROUTING_TIMEOUT_MS);
|
}, ROUTING_TIMEOUT_MS);
|
||||||
|
|
||||||
const req = https.request(
|
try {
|
||||||
url,
|
req = https.request(
|
||||||
{
|
requestUrl,
|
||||||
method,
|
{
|
||||||
headers,
|
method,
|
||||||
agent,
|
headers,
|
||||||
timeout: ROUTING_TIMEOUT_MS,
|
agent,
|
||||||
},
|
timeout: ROUTING_TIMEOUT_MS,
|
||||||
(res) => {
|
},
|
||||||
let payload = '';
|
(res) => {
|
||||||
res.setEncoding('utf8');
|
let payload = '';
|
||||||
res.on('data', (chunk) => {
|
res.setEncoding('utf8');
|
||||||
payload += chunk;
|
res.on('data', (chunk) => {
|
||||||
});
|
payload += chunk;
|
||||||
res.on('end', () => {
|
});
|
||||||
settle(() =>
|
res.on('end', () => {
|
||||||
resolve(
|
settle(() =>
|
||||||
new Response(payload, {
|
resolve(
|
||||||
status: res.statusCode || 500,
|
new Response(payload, {
|
||||||
statusText: res.statusMessage ?? '',
|
status: res.statusCode || 500,
|
||||||
headers:
|
statusText: res.statusMessage ?? '',
|
||||||
typeof res.headers['content-type'] === 'string'
|
headers:
|
||||||
? { 'Content-Type': res.headers['content-type'] }
|
typeof res.headers['content-type'] === 'string'
|
||||||
: undefined,
|
? { '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));
|
settle(() => reject(error));
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('timeout', () => {
|
request.on('timeout', () => {
|
||||||
const error = new Error('Request timeout');
|
const error = new Error('Request timeout');
|
||||||
req.destroy(error);
|
request.destroy(error);
|
||||||
settle(() => reject(error));
|
settle(() => reject(error));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (body) {
|
if (body) {
|
||||||
req.write(JSON.stringify(body));
|
request.write(JSON.stringify(body));
|
||||||
}
|
}
|
||||||
req.end();
|
request.end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user