import { getLocal } from "mockttp"; import * as WebSock from "ws"; const mockServer = getLocal(); const wait_for_open = (socket: WebSock) => { if (socket.readyState === socket.OPEN) return; return new Promise((resolve, reject) => { const maxNumberOfAttempts = 20; const intervalTime = 100; //ms let currentAttempt = 0; const interval = setInterval(() => { if (currentAttempt > maxNumberOfAttempts - 1) { clearInterval(interval); reject(new Error("Maximum number of attempts exceeded")); } else if (socket.readyState === socket.OPEN) { clearInterval(interval); resolve(undefined); } else if (socket.readyState == socket.CLOSED) { console.log(socket.url); reject(new Error("The server socket closed")); } currentAttempt++; }, intervalTime); }); }; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); function waitforhost( url: string, expected_status = 200, interval = 1000, attempts = 10 ): Promise { let count = 1; return new Promise(async (resolve, reject) => { while (count < attempts) { await sleep(interval); try { const response = await fetch(url); if (response.status === expected_status) { resolve(); break; } else { count++; } } catch { count++; console.log(`Still down, trying ${count} of ${attempts}`); } } reject(new Error(`Server is down: ${count} attempts tried`)); }); } (async () => { const proxy_port = process.env.PROXY_PORT; const server_port = process.env.SERVER_PORT; const loopback_port = process.env.LOOPBACK_PORT; // internal only const monitoring_api_port = process.env.MONITORING_API_PORT; if (!proxy_port || !server_port || !monitoring_api_port || !loopback_port) { throw Error("Configuration env variables not set"); } // Expect 403 forbidden await waitforhost("http://127.0.0.1:" + server_port, 403); mockServer.start(+proxy_port); let cached_resp: undefined | string = undefined; const cors_headers = { "access-control-allow-origin": "*", "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS", "access-control-allow-headers": "Content-Type, Authorization", "access-control-max-age": "86400", "access-control-allow-credentials": "true", Origin: "http://127.0.0.1:8080", // Trick the server into thinking that it is accessed through localhost }; await mockServer.forOptions().thenReply(200, "", cors_headers); await mockServer.forPost("/start").thenPassThrough({ beforeRequest: async (req) => { if (cached_resp) { return { response: { body: cached_resp, headers: cors_headers }, }; } // forward the request to the real server return { url: req.url, headers: req.headers, body: await req.body.getDecodedBuffer(), }; }, beforeResponse: async (resp, _req) => { // cache the response cached_resp = (await resp.body.getText()) as string; // set the cors headers, overwriting the prohibitive ones return { headers: { ...resp.headers, ...cors_headers } }; }, }); await mockServer.forPost("/stop").thenReply(200); // forward everything to the real server await mockServer.forUnmatchedRequest().thenPassThrough({ beforeRequest: async (req) => { return { url: req.url }; }, beforeResponse: async (resp, _req) => { return { headers: { ...resp.headers, ...cors_headers } }; }, }); await mockServer .forAnyWebSocket() .thenForwardTo("ws://127.0.0.1:" + loopback_port); let wss = new WebSock.Server({ port: +loopback_port }); // this is append only let httptoolkit_server_messages: String[] = []; let api_server = new WebSock.Server({ port: +monitoring_api_port }); api_server.on("connection", (ws) => { let curr_idx_to_send = 0; const interval = setInterval(() => { while (httptoolkit_server_messages[curr_idx_to_send] != undefined) { if (ws.readyState != ws.OPEN) { clearInterval(interval); ws.close(); break; } ws.send(httptoolkit_server_messages[curr_idx_to_send]); curr_idx_to_send++; } }, 100); }); wss.on("connection", (ws, req) => { let server_session_url = "ws://127.0.0.1:" + server_port + req.url; let httptoolkit_server_session: WebSock; console.log("ws connection:", ws.protocol, typeof ws.protocol); if (ws.protocol) { httptoolkit_server_session = new WebSock( server_session_url, ws.protocol, { joinDuplicateHeaders: true, headers: req.headers } ); } else { httptoolkit_server_session = new WebSock(server_session_url, { joinDuplicateHeaders: true, headers: req.headers, }); } httptoolkit_server_session.onerror = (_) => { ws.close(); httptoolkit_server_session.close(); return; }; httptoolkit_server_session.onopen = () => { console.log("ws connected to the server"); }; httptoolkit_server_session.onmessage = (msg) => { httptoolkit_server_messages.push(msg.data.toString()); ws.send(msg.data); }; // Listening for messages from the client ws.on("message", async (message) => { await wait_for_open(httptoolkit_server_session); httptoolkit_server_session.send(message.toString()); }); // Handling client disconnection ws.on("close", () => { httptoolkit_server_session.close(); }); }); console.log("WS Proxy started"); console.log(mockServer.proxyEnv); })();