/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; /* globals require, __dirname, global, Buffer, process */ const fs = require("fs"); const options = { key: fs.readFileSync(__dirname + "/mochitest-cert.key.pem"), cert: fs.readFileSync(__dirname + "/mochitest-cert.pem"), settings: { enableConnectProtocol: true, }, }; const http2 = require("http2"); const http = require("http"); const url = require("url"); const path = require("path"); // This is the path of node-ws when running mochitest locally. let node_ws_root = path.join(__dirname, "../../xpcshell/node-ws"); if (!fs.existsSync(node_ws_root)) { // This path is for running mochitest on try. node_ws_root = path.join(__dirname, "./node_ws"); } const WebSocket = require(`${node_ws_root}/lib/websocket`); let listeningPort = parseInt(process.argv[3].split("=")[1]); let log = function () {}; function handle_h2_non_connect(stream, headers) { const session = stream.session; const uri = new URL( `${headers[":scheme"]}://${headers[":authority"]}${headers[":path"]}` ); const url = uri.toString(); log("REQUEST:", url); log("REQUEST HEADER:", JSON.stringify(headers)); stream.on("close", () => { log("REQUEST STREAM CLOSED:", stream.rstCode); }); stream.on("error", error => { log("RESPONSE STREAM ERROR", error, url, "on session:", session.__id); }); let newHeaders = {}; for (let key in headers) { if (!key.startsWith(":")) { newHeaders[key] = headers[key]; } } const options = { protocol: "http:", hostname: "127.0.0.1", port: 8888, path: headers[":path"], method: headers[":method"], headers: newHeaders, }; log("OPTION:", JSON.stringify(options)); const request = http.request(options); stream.pipe(request); request.on("response", response => { const headers = Object.fromEntries( Object.entries(response.headers).filter( ([key]) => !["connection", "transfer-encoding", "keep-alive"].includes(key) ) ); headers[":status"] = response.statusCode; log("RESPONSE BEGIN", url, headers, "on session:", session.__id); try { stream.respond(headers); response.on("data", data => { log("RESPONSE DATA", data.length, url); stream.write(data); }); response.on("error", error => { log("RESPONSE ERROR", error, url, "on session:", session.__id); stream.close(http2.constants.NGHTTP2_REFUSED_STREAM); }); response.on("end", () => { log("RESPONSE END", url, "on session:", session.__id); stream.end(); }); } catch (exception) { log("RESPONSE EXCEPTION", exception, url, "on session:", session.__id); stream.close(); } }); request.on("error", error => { console.error("REQUEST ERROR", error, url, "on session:", session.__id); try { stream.respond({ ":status": 502, "content-type": "application/proxy-explanation+json", }); stream.end( JSON.stringify({ title: "request error", description: error.toString(), }) ); } catch (exception) { stream.close(); } }); } let server = http2.createSecureServer(options); let session_count = 0; let session_id = 0; server.on("session", session => { session.__id = ++session_id; session.__tunnel_count = 0; ++session_count; if (session_count === 1) { log(`\n\n>>> FIRST SESSION OPENING\n`); } log(`*** NEW SESSION`, session.__id, "( sessions:", session_count, ")"); session.on("error", error => { console.error("SESSION ERROR", session.__id, error); }); session.on("close", () => { --session_count; log(`*** CLOSED SESSION`, session.__id, "( sessions:", session_count, ")"); if (!session_count) { log(`\n\n<<< LAST SESSION CLOSED\n`); } }); }); server.on("stream", (stream, headers) => { if (headers[":method"] === "CONNECT") { stream.respond(); const ws = new WebSocket(null); stream.setNoDelay = () => {}; ws.setSocket(stream, Buffer.from(""), 100 * 1024 * 1024); ws.on("message", data => { ws.send(data); }); } else { handle_h2_non_connect(stream, headers); } }); server.listen(listeningPort); console.log(`Http2 server listening on ports ${server.address().port}`);