199 lines
5.7 KiB
JavaScript
199 lines
5.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from 'node:fs';
|
|
import test from "node:test";
|
|
|
|
const {
|
|
buildSharedProxyInfo,
|
|
} = await import("../../dist/server/sharedProxy.js");
|
|
const {
|
|
createSharedProxyRoute,
|
|
} = await import('../../dist/server/http/routes/sharedProxyRoute.js');
|
|
|
|
function response() {
|
|
return {
|
|
writeHead(status, headers) {
|
|
this.status = status;
|
|
this.headers = headers;
|
|
},
|
|
end(payload) {
|
|
this.rawPayload = payload;
|
|
this.payload = JSON.parse(payload);
|
|
},
|
|
};
|
|
}
|
|
|
|
function createHarness(overrides = {}) {
|
|
const events = [];
|
|
const refreshError = overrides.refreshError;
|
|
const route = createSharedProxyRoute({
|
|
appMode: overrides.appMode ?? 'gateway',
|
|
proxyPort: overrides.proxyPort ?? 8080,
|
|
sharedProxyHost: overrides.sharedProxyHost ?? '',
|
|
refreshRuntime: async () => {
|
|
events.push('refresh');
|
|
if (refreshError) throw refreshError;
|
|
return { running: overrides.running ?? true };
|
|
},
|
|
});
|
|
return { events, route };
|
|
}
|
|
|
|
test("gateway shared proxy info exposes host and socks proxy when running", () => {
|
|
const info = buildSharedProxyInfo({
|
|
appMode: "gateway",
|
|
proxyPort: 8080,
|
|
running: true,
|
|
hostHeader: "192.168.50.111:3456",
|
|
});
|
|
|
|
assert.equal(info.available, true);
|
|
assert.deepEqual(info.proxy, {
|
|
host: "192.168.50.111",
|
|
port: 8080,
|
|
protocol: "socks5",
|
|
httpUrl: "http://192.168.50.111:8080",
|
|
socksUrl: "socks5://192.168.50.111:8080",
|
|
});
|
|
});
|
|
|
|
test('shared proxy builder preserves host precedence, IPv6, port and unavailable quirks', () => {
|
|
const configured = buildSharedProxyInfo({
|
|
appMode: 'gateway',
|
|
proxyPort: '9000px',
|
|
running: true,
|
|
hostHeader: 'ignored.example:1234',
|
|
sharedProxyHost: ' proxy.example ',
|
|
});
|
|
assert.deepEqual(configured.proxy, {
|
|
host: 'proxy.example',
|
|
port: 9000,
|
|
protocol: 'socks5',
|
|
httpUrl: 'http://proxy.example:9000',
|
|
socksUrl: 'socks5://proxy.example:9000',
|
|
});
|
|
|
|
const ipv6 = buildSharedProxyInfo({
|
|
appMode: 'gateway',
|
|
proxyPort: 1080,
|
|
running: true,
|
|
hostHeader: '[2001:db8::1]:443',
|
|
});
|
|
assert.equal(ipv6.proxy.host, '2001:db8::1');
|
|
|
|
assert.equal(buildSharedProxyInfo({
|
|
appMode: 'gateway',
|
|
proxyPort: 1080,
|
|
running: true,
|
|
hostHeader: '',
|
|
}).available, '');
|
|
assert.equal(buildSharedProxyInfo({
|
|
appMode: 'gateway',
|
|
proxyPort: 65536,
|
|
running: true,
|
|
hostHeader: 'gateway.local',
|
|
}).available, false);
|
|
});
|
|
|
|
test('shared proxy route refreshes once before reading Host and preserves raw JSON response', async () => {
|
|
const harness = createHarness();
|
|
const res = response();
|
|
const headers = {};
|
|
Object.defineProperty(headers, 'host', {
|
|
get() {
|
|
harness.events.push('host');
|
|
return '192.168.50.111:3456';
|
|
},
|
|
});
|
|
|
|
assert.equal(await harness.route.handle({
|
|
method: 'GET',
|
|
url: '/api/shared-proxy',
|
|
headers,
|
|
}, res), true);
|
|
assert.deepEqual(harness.events, ['refresh', 'host']);
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
|
|
assert.equal(res.rawPayload.endsWith('\n'), false);
|
|
assert.deepEqual(res.payload, {
|
|
success: true,
|
|
available: true,
|
|
mode: 'gateway',
|
|
proxy: {
|
|
host: '192.168.50.111',
|
|
port: 8080,
|
|
protocol: 'socks5',
|
|
httpUrl: 'http://192.168.50.111:8080',
|
|
socksUrl: 'socks5://192.168.50.111:8080',
|
|
},
|
|
});
|
|
});
|
|
|
|
test('shared proxy route still refreshes for client and unavailable responses', async () => {
|
|
for (const options of [
|
|
{ appMode: 'client' },
|
|
{ running: false },
|
|
{ proxyPort: 'invalid' },
|
|
]) {
|
|
const harness = createHarness(options);
|
|
const res = response();
|
|
await harness.route.handle({
|
|
method: 'GET',
|
|
url: '/api/shared-proxy',
|
|
headers: { host: 'gateway.local' },
|
|
}, res);
|
|
assert.deepEqual(harness.events, ['refresh']);
|
|
assert.equal(res.payload.available, false);
|
|
assert.equal(res.payload.proxy, null);
|
|
}
|
|
});
|
|
|
|
test('shared proxy route exact guard avoids refresh and errors propagate unchanged', async () => {
|
|
for (const [method, url] of [
|
|
['POST', '/api/shared-proxy'],
|
|
['GET', '/api/shared-proxy?source=client'],
|
|
['GET', '/api/shared-proxy/'],
|
|
['GET', '/api/other'],
|
|
]) {
|
|
const harness = createHarness();
|
|
assert.equal(await harness.route.handle({ method, url, headers: {} }, response()), false);
|
|
assert.deepEqual(harness.events, []);
|
|
}
|
|
|
|
const refreshError = new Error('refresh failed');
|
|
const brokenRefresh = createHarness({ refreshError });
|
|
await assert.rejects(
|
|
brokenRefresh.route.handle({
|
|
method: 'GET',
|
|
url: '/api/shared-proxy',
|
|
headers: { host: 'gateway.local' },
|
|
}, response()),
|
|
(error) => error === refreshError,
|
|
);
|
|
|
|
const builderError = new Error('builder failed');
|
|
const brokenBuilder = createHarness({
|
|
sharedProxyHost: { toString() { throw builderError; } },
|
|
});
|
|
await assert.rejects(
|
|
brokenBuilder.route.handle({
|
|
method: 'GET',
|
|
url: '/api/shared-proxy',
|
|
headers: { host: 'gateway.local' },
|
|
}, response()),
|
|
(error) => error === builderError,
|
|
);
|
|
});
|
|
|
|
test('shared proxy route is the sole HTTP owner', () => {
|
|
const index = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
|
|
const route = readFileSync(
|
|
new URL('../../src/server/http/routes/sharedProxyRoute.ts', import.meta.url),
|
|
'utf8',
|
|
);
|
|
assert.match(index, /createSharedProxyRoute\(\{/);
|
|
assert.match(index, /sharedProxyRoute\.handle\(req, res\)/);
|
|
assert.doesNotMatch(index, /['"]\/api\/shared-proxy['"]|buildSharedProxyInfo/);
|
|
assert.match(route, /req\.url !== '\/api\/shared-proxy'/);
|
|
assert.match(route, /buildSharedProxyInfo\(\{/);
|
|
});
|