mirror of
https://github.com/by-sonic/tglock.git
synced 2026-09-20 01:22:47 +03:00
197 lines
7.2 KiB
JavaScript
197 lines
7.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import test from "node:test";
|
|
import vm from "node:vm";
|
|
|
|
const source = await readFile(new URL("./tglock-worker.js", import.meta.url), "utf8");
|
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
function deferred() {
|
|
let resolve, reject;
|
|
const promise = new Promise((a, b) => { resolve = a; reject = b; });
|
|
return { promise, resolve, reject };
|
|
}
|
|
|
|
// Execute the actual standalone deployment file, replacing only Cloudflare's
|
|
// platform primitives. No test-only implementation of the relay is used.
|
|
async function fixture({ opened = Promise.resolve(), write, timeout = false } = {}) {
|
|
let readController;
|
|
const readable = new ReadableStream({ start(controller) { readController = controller; } });
|
|
const reads = { reject(error) { readController.error(error); } };
|
|
const closed = deferred();
|
|
const writes = [];
|
|
const events = new Map();
|
|
const state = { connects: 0, accepted: false, socketCloses: 0, sent: [], closes: [] };
|
|
const server = {
|
|
accept() { state.accepted = true; },
|
|
addEventListener(name, callback) { events.set(name, callback); },
|
|
close(code, reason) { state.closes.push({ code, reason }); },
|
|
send(chunk) { state.sent.push(Array.from(chunk)); },
|
|
};
|
|
const socket = {
|
|
opened, closed: closed.promise,
|
|
writable: { getWriter: () => ({
|
|
ready: Promise.resolve(),
|
|
async write(chunk) { writes.push(Array.from(chunk)); if (write) await write(chunk); },
|
|
}) },
|
|
readable,
|
|
async close() {
|
|
state.socketCloses++;
|
|
try { readController.close(); } catch { /* already closed or errored */ }
|
|
closed.resolve();
|
|
},
|
|
};
|
|
const context = vm.createContext({
|
|
URL, Uint8Array, ArrayBuffer, setTimeout: timeout ? (fn) => setTimeout(fn, 0) : setTimeout, clearTimeout,
|
|
Response: class {
|
|
constructor(body, options) { this.body = body; Object.assign(this, options); }
|
|
},
|
|
WebSocketPair: class { constructor() { this[0] = {}; this[1] = server; } },
|
|
});
|
|
const sockets = new vm.SyntheticModule(["connect"], function () {
|
|
this.setExport("connect", (address) => {
|
|
state.connects++;
|
|
state.address = address;
|
|
return socket;
|
|
});
|
|
}, { context });
|
|
const module = new vm.SourceTextModule(source, { context });
|
|
await module.link((name) => {
|
|
assert.equal(name, "cloudflare:sockets");
|
|
return sockets;
|
|
});
|
|
await module.evaluate();
|
|
return {
|
|
...state,
|
|
state, writes, reads, closed, socket,
|
|
download(chunk) { readController.enqueue(chunk); },
|
|
eof() { readController.close(); },
|
|
message(data) { events.get("message")({ data }); },
|
|
event(name) { events.get(name)({}); },
|
|
fetch(path = "/apiws?dst=149.154.167.51&dc=2", headers = {}, env = {}) {
|
|
return module.namespace.default.fetch(new Request(`https://example.workers.dev${path}`, {
|
|
headers: { Upgrade: "websocket", "Sec-WebSocket-Protocol": "binary", ...headers },
|
|
}), env);
|
|
},
|
|
};
|
|
}
|
|
|
|
test("HTTP deployment check does not claim Telegram connectivity", async () => {
|
|
const f = await fixture();
|
|
assert.equal((await f.fetch("/apiws", { Upgrade: "" })).status, 426);
|
|
assert.equal(f.state.connects, 0);
|
|
});
|
|
|
|
test("reject unauthorized destinations, token and subprotocol before opening TCP", async () => {
|
|
const f = await fixture();
|
|
assert.equal((await f.fetch("/wrong")).status, 404);
|
|
assert.equal((await f.fetch("/apiws?dst=127.0.0.1")).status, 403);
|
|
assert.equal((await f.fetch(undefined, {}, { TGLOCK_TOKEN: "test-token" })).status, 403);
|
|
assert.equal((await f.fetch(undefined, { "Sec-WebSocket-Protocol": "chat" })).status, 400);
|
|
assert.equal(f.state.connects, 0);
|
|
});
|
|
|
|
test("wait for upstream TCP before accepting WebSocket", async () => {
|
|
const opened = deferred();
|
|
const f = await fixture({ opened: opened.promise });
|
|
const response = f.fetch();
|
|
await tick();
|
|
assert.equal(f.state.accepted, false);
|
|
opened.resolve();
|
|
const result = await response;
|
|
assert.equal(result.status, 101);
|
|
assert.equal(result.headers["Sec-WebSocket-Protocol"], "binary");
|
|
assert.equal(f.state.accepted, true);
|
|
f.event("close");
|
|
});
|
|
|
|
test("failed upstream returns HTTP 502, never a successful tunnel", async () => {
|
|
const f = await fixture({ opened: Promise.reject(new Error("unreachable")) });
|
|
assert.equal((await f.fetch()).status, 502);
|
|
assert.equal(f.state.accepted, false);
|
|
assert.equal(f.state.socketCloses, 1);
|
|
});
|
|
|
|
test("upstream timeout returns 504 and cancels socket", async () => {
|
|
const f = await fixture({ opened: new Promise(() => {}), timeout: true });
|
|
assert.equal((await f.fetch()).status, 504);
|
|
assert.equal(f.state.accepted, false);
|
|
assert.equal(f.state.socketCloses, 1);
|
|
});
|
|
|
|
test("serialize binary writes, preserve bytes and view offsets", async () => {
|
|
const firstWrite = deferred();
|
|
let calls = 0;
|
|
const f = await fixture({ write: async () => { if (++calls === 1) await firstWrite.promise; } });
|
|
await f.fetch();
|
|
f.message(new Uint8Array([1, 2]).buffer);
|
|
f.message(new Uint8Array([99, 3, 4, 88]).subarray(1, 3));
|
|
await tick();
|
|
assert.deepEqual(f.writes, [[1, 2]]);
|
|
firstWrite.resolve();
|
|
await tick();
|
|
assert.deepEqual(f.writes, [[1, 2], [3, 4]]);
|
|
f.event("close");
|
|
});
|
|
|
|
test("overflow closes socket and discards queued writes", async () => {
|
|
const f = await fixture();
|
|
await f.fetch();
|
|
f.message(new Uint8Array(1024 * 1024).buffer);
|
|
f.message(new Uint8Array([1]).buffer);
|
|
await tick();
|
|
assert.equal(f.state.closes[0].code, 1009);
|
|
assert.equal(f.state.socketCloses, 1);
|
|
assert.equal(f.writes.length, 0);
|
|
});
|
|
|
|
test("empty-message floods have a bounded queue too", async () => {
|
|
const f = await fixture();
|
|
await f.fetch();
|
|
for (let i = 0; i < 257; i++) f.message(new ArrayBuffer(0));
|
|
await tick();
|
|
assert.equal(f.state.closes[0].code, 1009);
|
|
assert.equal(f.state.socketCloses, 1);
|
|
});
|
|
|
|
test("text and write failures get diagnostic close codes", async () => {
|
|
const f = await fixture();
|
|
await f.fetch();
|
|
f.message("not binary");
|
|
assert.equal(f.state.closes[0].code, 1003);
|
|
const failing = await fixture({ write: async () => { throw new Error("reset"); } });
|
|
await failing.fetch();
|
|
failing.message(new Uint8Array([1]).buffer);
|
|
await tick();
|
|
assert.equal(failing.state.closes[0].code, 1011);
|
|
assert.equal(failing.state.socketCloses, 1);
|
|
});
|
|
|
|
test("read errors propagate and close both socket directions", async () => {
|
|
const f = await fixture();
|
|
await f.fetch();
|
|
f.reads.reject(new Error("reset"));
|
|
await tick();
|
|
assert.equal(f.state.closes[0].code, 1011);
|
|
assert.equal(f.state.socketCloses, 1);
|
|
});
|
|
|
|
test("Telegram bytes reach WebSocket unchanged and EOF closes both directions", async () => {
|
|
const f = await fixture();
|
|
await f.fetch();
|
|
f.download(new Uint8Array([0, 255, 128]));
|
|
f.download(new Uint8Array([1, 2]));
|
|
f.eof();
|
|
await tick();
|
|
assert.deepEqual(f.state.sent, [[0, 255, 128], [1, 2]]);
|
|
assert.equal(f.state.closes[0].code, 1000);
|
|
assert.equal(f.state.socketCloses, 1);
|
|
});
|
|
|
|
test("CDN worker keeps the requested DC203 destination", async () => {
|
|
const f = await fixture();
|
|
await f.fetch("/apiws?dst=91.105.192.100&dc=203");
|
|
assert.equal(f.state.address.hostname, "91.105.192.100");
|
|
assert.equal(f.state.address.port, 443);
|
|
f.event("close");
|
|
});
|