Refactor VPN proxy client implementation
This commit is contained in:
@@ -1,9 +1,42 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from "node:test";
|
||||
|
||||
const {
|
||||
buildSharedProxyInfo,
|
||||
} = await import("../../src/server/sharedProxy.js");
|
||||
} = 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({
|
||||
@@ -22,3 +55,144 @@ test("gateway shared proxy info exposes host and socks proxy when running", () =
|
||||
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\(\{/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user