/* 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/. */ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs"; /* globals require, __dirname, global, Buffer, process */ class BaseNodeHTTPServerCode { static globalHandler(req, resp) { let path = new URL(req.url, "https://example.com").pathname; let handler = global.path_handlers[path]; if (handler) { return handler(req, resp); } // Didn't find a handler for this path. let response = `

404 Path not found: ${path}

`; resp.setHeader("Content-Type", "text/html"); resp.setHeader("Content-Length", response.length); resp.writeHead(404); resp.end(response); return undefined; } } class ADB { static async stopForwarding(port) { return this.forwardPort(port, true); } static async forwardPort(port, remove = false) { if (!process.env.MOZ_ANDROID_DATA_DIR) { // Not android, or we don't know how to do the forwarding return true; } // When creating a server on Android we must make sure that the port // is forwarded from the host machine to the emulator. let adb_path = "adb"; if (process.env.MOZ_FETCHES_DIR) { adb_path = `${process.env.MOZ_FETCHES_DIR}/android-sdk-linux/platform-tools/adb`; } let command = `${adb_path} reverse tcp:${port} tcp:${port}`; if (remove) { command = `${adb_path} reverse --remove tcp:${port}`; return true; } try { await new Promise((resolve, reject) => { const { exec } = require("child_process"); exec(command, (error, stdout, stderr) => { if (error) { console.log(`error: ${error.message}`); reject(error); } else if (stderr) { console.log(`stderr: ${stderr}`); reject(stderr); } else { // console.log(`stdout: ${stdout}`); resolve(); } }); }); } catch (error) { console.log(`Command failed: ${error}`); return false; } return true; } static async listenAndForwardPort(server, port) { let retryCount = 0; const maxRetries = 10; while (retryCount < maxRetries) { await server.listen(port); let serverPort = server.address().port; let res = await ADB.forwardPort(serverPort); if (res) { return serverPort; } retryCount++; console.log( `Port forwarding failed. Retrying (${retryCount}/${maxRetries})...` ); server.close(); // eslint-disable-next-line no-undef await new Promise(resolve => setTimeout(resolve, 500)); } return -1; } } class BaseNodeServer { protocol() { return this._protocol; } version() { return this._version; } origin() { return `${this.protocol()}://localhost:${this.port()}`; } port() { return this._port; } domain() { return `localhost`; } static async installCert(filename) { if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { // Can't install cert from content process. return; } let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( Ci.nsIX509CertDB ); function readFile(file) { let fstream = Cc[ "@mozilla.org/network/file-input-stream;1" ].createInstance(Ci.nsIFileInputStream); fstream.init(file, -1, 0, 0); let data = NetUtil.readInputStreamToString(fstream, fstream.available()); fstream.close(); return data; } // Find the root directory that contains netwerk/ let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); let rootDir = currentDir.clone(); // XXX(valentin) The certs are stored in netwerk/test/unit // Walk up until the dir contains netwerk/ // This is hacky, but the alternative would also require // us to walk up the path to the root dir. while (rootDir) { let netwerkDir = rootDir.clone(); netwerkDir.append("netwerk"); if (netwerkDir.exists() && netwerkDir.isDirectory()) { break; } let parent = rootDir.parent; if (!parent || parent.equals(rootDir)) { // Reached filesystem root, fallback to current directory rootDir = currentDir; break; } rootDir = parent; } let certFile = rootDir.clone(); certFile.append("netwerk"); certFile.append("test"); certFile.append("unit"); certFile.append(filename); try { let pem = readFile(certFile) .replace(/-----BEGIN CERTIFICATE-----/, "") .replace(/-----END CERTIFICATE-----/, "") .replace(/[\r\n]/g, ""); certdb.addCertFromBase64(pem, "CTu,u,u"); } catch (e) { let errStr = e.toString(); console.log(`Error installing cert ${errStr}`); if (errStr.includes("0x805a1fe8")) { // Can't install the cert without a profile // Let's show an error, otherwise this will be difficult to diagnose. console.log( `!!! BaseNodeServer.installCert > Make sure your unit test calls do_get_profile()` ); } } } /// Stops the server async stop() { if (this.processId) { await this.execute(`ADB.stopForwarding(${this.port()})`); await NodeServer.kill(this.processId); this.processId = undefined; } } /// Executes a command in the context of the node server async execute(command) { return NodeServer.execute(this.processId, command); } /// @path : string - the path on the server that we're handling. ex: /path /// @handler : function(req, resp, url) - function that processes request and /// emits a response. async registerPathHandler(path, handler) { return this.execute( `global.path_handlers["${path}"] = ${handler.toString()}` ); } } // HTTP class NodeHTTPServerCode extends BaseNodeHTTPServerCode { static async startServer(port) { const http = require("http"); global.server = http.createServer(BaseNodeHTTPServerCode.globalHandler); let serverPort = await ADB.listenAndForwardPort(global.server, port); return serverPort; } } export class NodeHTTPServer extends BaseNodeServer { _protocol = "http"; _version = "http/1.1"; /// Starts the server /// @port - default 0 /// when provided, will attempt to listen on that port. async start(port = 0) { this.processId = await NodeServer.fork(); await this.execute(BaseNodeHTTPServerCode); await this.execute(NodeHTTPServerCode); await this.execute(ADB); this._port = await this.execute(`NodeHTTPServerCode.startServer(${port})`); await this.execute(`global.path_handlers = {};`); } } // HTTPS class NodeHTTPSServerCode extends BaseNodeHTTPServerCode { static async startServer(port) { const fs = require("fs"); const options = { key: fs.readFileSync(__dirname + "/http2-cert.key"), cert: fs.readFileSync(__dirname + "/http2-cert.pem"), maxHeaderSize: 128 * 1024, }; const https = require("https"); global.server = https.createServer( options, BaseNodeHTTPServerCode.globalHandler ); let serverPort = await ADB.listenAndForwardPort(global.server, port); return serverPort; } } export class NodeHTTPSServer extends BaseNodeServer { _protocol = "https"; _version = "http/1.1"; /// Starts the server /// @port - default 0 /// when provided, will attempt to listen on that port. async start(port = 0) { if (!this._skipCert) { await BaseNodeServer.installCert("http2-ca.pem"); } this.processId = await NodeServer.fork(); await this.execute(BaseNodeHTTPServerCode); await this.execute(NodeHTTPSServerCode); await this.execute(ADB); this._port = await this.execute(`NodeHTTPSServerCode.startServer(${port})`); await this.execute(`global.path_handlers = {};`); } } // HTTP2 class NodeHTTP2ServerCode extends BaseNodeHTTPServerCode { static async startServer(port) { const fs = require("fs"); const options = { key: fs.readFileSync(__dirname + "/http2-cert.key"), cert: fs.readFileSync(__dirname + "/http2-cert.pem"), }; const http2 = require("http2"); global.server = http2.createSecureServer( options, BaseNodeHTTPServerCode.globalHandler ); global.sessionCount = 0; global.sessions = new Set(); global.server.on("session", session => { global.sessions.add(session); session.on("close", () => { global.sessions.delete(session); }); global.sessionCount++; }); let serverPort = await ADB.listenAndForwardPort(global.server, port); return serverPort; } static sessionCount() { return global.sessionCount; } } export class NodeHTTP2Server extends BaseNodeServer { _protocol = "https"; _version = "h2"; /// Starts the server /// @port - default 0 /// when provided, will attempt to listen on that port. async start(port = 0) { if (!this._skipCert) { await BaseNodeServer.installCert("http2-ca.pem"); } this.processId = await NodeServer.fork(); await this.execute(BaseNodeHTTPServerCode); await this.execute(NodeHTTP2ServerCode); await this.execute(ADB); this._port = await this.execute(`NodeHTTP2ServerCode.startServer(${port})`); await this.execute(`global.path_handlers = {};`); } async sessionCount() { let count = this.execute(`NodeHTTP2ServerCode.sessionCount()`); return count; } } // Base HTTP proxy class BaseProxyCode { static proxyHandler(req, res) { if (req.url.startsWith("/")) { res.writeHead(405); res.end(); return; } let url = new URL(req.url); const http = require("http"); let preq = http .request( { method: req.method, path: url.pathname, port: url.port, host: url.hostname, protocol: url.protocol, }, proxyresp => { res.writeHead( proxyresp.statusCode, proxyresp.statusMessage, proxyresp.headers ); proxyresp.on("data", chunk => { if (!res.writableEnded) { res.write(chunk); } }); proxyresp.on("end", () => { res.end(); }); } ) .on("error", e => { console.log(`sock err: ${e}`); }); if (req.method != "POST") { preq.end(); } else { req.on("data", chunk => { if (!preq.writableEnded) { preq.write(chunk); } }); req.on("end", () => preq.end()); } } static onConnect(req, clientSocket, head) { if (global.connect_handler) { global.connect_handler(req, clientSocket, head); return; } const net = require("net"); // Connect to an origin server const { port, hostname } = new URL(`https://${req.url}`); const serverSocket = net .connect( { port: port || 443, host: hostname, family: 4, // Specifies to use IPv4 }, () => { clientSocket.write( "HTTP/1.1 200 Connection Established\r\n" + "Proxy-agent: Node.js-Proxy\r\n" + "\r\n" ); serverSocket.write(head); serverSocket.pipe(clientSocket); clientSocket.pipe(serverSocket); } ) .on("error", e => { console.log("error" + e); // The socket will error out when we kill the connection // just ignore it. }); clientSocket.on("error", e => { console.log("client error" + e); // Sometimes we got ECONNRESET error on windows platform. // Ignore it for now. }); } } class BaseHTTPProxy extends BaseNodeServer { registerFilter() { const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); this.filter = new NodeProxyFilter( this.protocol(), "localhost", this.port(), 0 ); pps.registerFilter(this.filter, 10); } unregisterFilter() { const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); if (this.filter) { pps.unregisterFilter(this.filter); this.filter = undefined; } } /// Stops the server async stop() { this.unregisterFilter(); await super.stop(); } async registerConnectHandler(handler) { return this.execute(`global.connect_handler = ${handler.toString()}`); } } // HTTP1 Proxy export class NodeProxyFilter { constructor(type, host, port, flags) { this._type = type; this._host = host; this._port = port; this._flags = flags; this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]); } applyFilter(uri, pi, cb) { const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); cb.onProxyFilterResult( pps.newProxyInfo( this._type, this._host, this._port, "", "", this._flags, 1000, null ) ); } } export class Http3ProxyFilter { constructor(host, port, flags, pathTemplate, auth) { this._host = host; this._port = port; this._flags = flags; this._pathTemplate = pathTemplate; this._auth = auth; this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]); } applyFilter(uri, pi, cb) { const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); cb.onProxyFilterResult( pps.newMASQUEProxyInfo( this._host, this._port, this._pathTemplate, this._auth, "", this._flags, 1000, null ) ); } } class HTTPProxyCode { static async startServer(port) { const http = require("http"); global.proxy = http.createServer(BaseProxyCode.proxyHandler); global.proxy.on("connect", BaseProxyCode.onConnect); let proxyPort = await ADB.listenAndForwardPort(global.proxy, port); return proxyPort; } } export class NodeHTTPProxyServer extends BaseHTTPProxy { _protocol = "http"; /// Starts the server /// @port - default 0 /// when provided, will attempt to listen on that port. async start(port = 0) { this.processId = await NodeServer.fork(); await this.execute(BaseProxyCode); await this.execute(HTTPProxyCode); await this.execute(ADB); await this.execute(`global.connect_handler = null;`); this._port = await this.execute(`HTTPProxyCode.startServer(${port})`); this.registerFilter(); } } // HTTPS proxy class HTTPSProxyCode { static async startServer(port) { const fs = require("fs"); const options = { key: fs.readFileSync(__dirname + "/proxy-cert.key"), cert: fs.readFileSync(__dirname + "/proxy-cert.pem"), }; const https = require("https"); global.proxy = https.createServer(options, BaseProxyCode.proxyHandler); global.proxy.on("connect", BaseProxyCode.onConnect); let proxyPort = await ADB.listenAndForwardPort(global.proxy, port); return proxyPort; } } export class NodeHTTPSProxyServer extends BaseHTTPProxy { _protocol = "https"; /// Starts the server /// @port - default 0 /// when provided, will attempt to listen on that port. async start(port = 0) { if (!this._skipCert) { await BaseNodeServer.installCert("proxy-ca.pem"); } this.processId = await NodeServer.fork(); await this.execute(BaseProxyCode); await this.execute(HTTPSProxyCode); await this.execute(ADB); await this.execute(`global.connect_handler = null;`); this._port = await this.execute(`HTTPSProxyCode.startServer(${port})`); this.registerFilter(); } } // HTTP2 proxy class HTTP2ProxyCode { static async startServer(port, auth, maxConcurrentStreams) { const fs = require("fs"); const options = { key: fs.readFileSync(__dirname + "/proxy-cert.key"), cert: fs.readFileSync(__dirname + "/proxy-cert.pem"), settings: { maxConcurrentStreams, }, }; const http2 = require("http2"); global.proxy = http2.createSecureServer(options); global.socketCounts = {}; this.setupProxy(auth); let proxyPort = await ADB.listenAndForwardPort(global.proxy, port); return proxyPort; } static setupProxy(auth) { if (!global.proxy) { throw new Error("proxy is null"); } global.proxy.on("stream", (stream, headers) => { if (headers[":scheme"] === "http") { const http = require("http"); let url = new URL( `${headers[":scheme"]}://${headers[":authority"]}${headers[":path"]}` ); let req = http .request( { method: headers[":method"], path: headers[":path"], port: url.port, host: url.hostname, protocol: url.protocol, }, proxyresp => { let proxyheaders = Object.assign({}, proxyresp.headers); // Filter out some prohibited headers. ["connection", "transfer-encoding", "keep-alive"].forEach( prop => { delete proxyheaders[prop]; } ); try { stream.respond( Object.assign( { ":status": proxyresp.statusCode }, proxyheaders ) ); } catch (e) { // The channel may have been closed already. if ( e.code !== "ERR_HTTP2_INVALID_STREAM" && !e.message.includes("The stream has been destroyed") ) { throw e; } } proxyresp.on("data", chunk => { if (stream.writable) { stream.write(chunk); } }); proxyresp.on("end", () => { stream.end(); }); } ) .on("error", e => { console.log(`sock err: ${e}`); }); if (headers[":method"] != "POST") { req.end(); } else { stream.on("data", chunk => { if (!req.writableEnded) { req.write(chunk); } }); stream.on("end", () => req.end()); } return; } if (headers[":method"] !== "CONNECT") { // Only accept CONNECT requests try { stream.respond({ ":status": 405 }); } catch (e) { if ( e.code !== "ERR_HTTP2_INVALID_STREAM" && !e.message.includes("The stream has been destroyed") ) { throw e; } } stream.end(); return; } const authorization_token = headers["proxy-authorization"]; if (auth && !authorization_token) { try { stream.respond({ ":status": 407, "proxy-authenticate": "Basic realm='foo'", }); } catch (e) { if ( e.code !== "ERR_HTTP2_INVALID_STREAM" && !e.message.includes("The stream has been destroyed") ) { throw e; } } stream.end(); return; } const target = headers[":authority"]; const { port } = new URL(`https://${target}`); const net = require("net"); const socket = net.connect(port, "127.0.0.1", () => { try { global.socketCounts[socket.remotePort] = (global.socketCounts[socket.remotePort] || 0) + 1; try { stream.respond({ ":status": 200 }); } catch (e) { if ( e.code !== "ERR_HTTP2_INVALID_STREAM" && !e.message.includes("The stream has been destroyed") ) { throw e; } } socket.pipe(stream); stream.pipe(socket); } catch (exception) { console.log(exception); stream.close(); } }); const http2 = require("http2"); socket.on("error", error => { const status = error.errno == "ENOTFOUND" ? 404 : 502; try { // If we already sent headers when the socket connected // then sending the status again would throw. if (!stream.sentHeaders) { try { stream.respond({ ":status": status }); } catch (e) { if ( e.code !== "ERR_HTTP2_INVALID_STREAM" && !e.message.includes("The stream has been destroyed") ) { throw e; } } } stream.end(); } catch (exception) { stream.close(http2.constants.NGHTTP2_CONNECT_ERROR); } }); stream.on("close", () => { socket.end(); }); socket.on("close", () => { stream.close(); }); stream.on("end", () => { socket.end(); }); stream.on("aborted", () => { socket.end(); }); stream.on("error", error => { console.log("RESPONSE STREAM ERROR", error); }); }); } static socketCount(port) { return global.socketCounts[port]; } } export class NodeHTTP2ProxyServer extends BaseHTTPProxy { _protocol = "https"; /// Starts the server /// @port - default 0 /// when provided, will attempt to listen on that port. async start(port = 0, auth, maxConcurrentStreams = 100) { await this.startWithoutProxyFilter(port, auth, maxConcurrentStreams); this.registerFilter(); } async startWithoutProxyFilter(port = 0, auth, maxConcurrentStreams = 100) { if (!this._skipCert) { await BaseNodeServer.installCert("proxy-ca.pem"); } this.processId = await NodeServer.fork(); await this.execute(BaseProxyCode); await this.execute(HTTP2ProxyCode); await this.execute(ADB); await this.execute(`global.connect_handler = null;`); this._port = await this.execute( `HTTP2ProxyCode.startServer(${port}, ${auth}, ${maxConcurrentStreams})` ); } async socketCount(port) { let count = await this.execute(`HTTP2ProxyCode.socketCount(${port})`); return count; } } // websocket server class NodeWebSocketServerCode extends BaseNodeHTTPServerCode { static messageHandler(data, ws) { if (global.wsInputHandler) { global.wsInputHandler(data, ws); return; } ws.send("test"); } static async startServer(port) { const fs = require("fs"); const options = { key: fs.readFileSync(__dirname + "/http2-cert.key"), cert: fs.readFileSync(__dirname + "/http2-cert.pem"), }; const https = require("https"); global.server = https.createServer( options, BaseNodeHTTPServerCode.globalHandler ); let node_ws_root = `${__dirname}/../node-ws`; const WS = require(`${node_ws_root}/lib/websocket`); WS.Server = require(`${node_ws_root}/lib/websocket-server`); global.webSocketServer = new WS.Server({ server: global.server }); global.webSocketServer.on("connection", function connection(ws) { ws.on("message", data => NodeWebSocketServerCode.messageHandler(data, ws) ); }); let serverPort = await ADB.listenAndForwardPort(global.server, port); return serverPort; } } export class NodeWebSocketServer extends BaseNodeServer { _protocol = "wss"; /// Starts the server /// @port - default 0 /// when provided, will attempt to listen on that port. async start(port = 0) { if (!this._skipCert) { await BaseNodeServer.installCert("http2-ca.pem"); } this.processId = await NodeServer.fork(); await this.execute(BaseNodeHTTPServerCode); await this.execute(NodeWebSocketServerCode); await this.execute(ADB); this._port = await this.execute( `NodeWebSocketServerCode.startServer(${port})` ); await this.execute(`global.path_handlers = {};`); await this.execute(`global.wsInputHandler = null;`); } async registerMessageHandler(handler) { return this.execute(`global.wsInputHandler = ${handler.toString()}`); } } // websocket http2 server // This code is inspired by // https://github.com/szmarczak/http2-wrapper/blob/master/examples/ws/server.js class NodeWebSocketHttp2ServerCode extends BaseNodeHTTPServerCode { static async startServer(port, fallbackToH1) { const fs = require("fs"); const options = { key: fs.readFileSync(__dirname + "/http2-cert.key"), cert: fs.readFileSync(__dirname + "/http2-cert.pem"), settings: { enableConnectProtocol: !fallbackToH1, allowHTTP1: fallbackToH1, }, }; const http2 = require("http2"); global.h2Server = http2.createSecureServer(options); let node_ws_root = `${__dirname}/../node-ws`; const WS = require(`${node_ws_root}/lib/websocket`); global.h2Server.on("stream", (stream, headers) => { if (headers[":method"] === "CONNECT") { try { stream.respond(); } catch (e) { if ( e.code !== "ERR_HTTP2_INVALID_STREAM" && !e.message.includes("The stream has been destroyed") ) { throw e; } } const ws = new WS(null); stream.setNoDelay = () => {}; ws.setSocket(stream, Buffer.from(""), 100 * 1024 * 1024); ws.on("message", data => { if (global.wsInputHandler) { global.wsInputHandler(data, ws); return; } ws.send("test"); }); } else { try { stream.respond(); } catch (e) { if ( e.code !== "ERR_HTTP2_INVALID_STREAM" && !e.message.includes("The stream has been destroyed") ) { throw e; } } stream.end("ok"); } }); let serverPort = await ADB.listenAndForwardPort(global.h2Server, port); return serverPort; } } export class NodeWebSocketHttp2Server extends BaseNodeServer { _protocol = "h2ws"; /// Starts the server /// @port - default 0 /// when provided, will attempt to listen on that port. async start(port = 0, fallbackToH1 = false) { if (!this._skipCert) { await BaseNodeServer.installCert("http2-ca.pem"); } this.processId = await NodeServer.fork(); await this.execute(BaseNodeHTTPServerCode); await this.execute(NodeWebSocketHttp2ServerCode); await this.execute(ADB); this._port = await this.execute( `NodeWebSocketHttp2ServerCode.startServer(${port}, ${fallbackToH1})` ); await this.execute(`global.path_handlers = {};`); await this.execute(`global.wsInputHandler = null;`); } async registerMessageHandler(handler) { return this.execute(`global.wsInputHandler = ${handler.toString()}`); } } // Helper functions export async function with_node_servers(arrayOfClasses, asyncClosure) { for (let s of arrayOfClasses) { let server = new s(); await server.start(); await asyncClosure(server); await server.stop(); } } export class WebSocketConnection { constructor() { this._openPromise = new Promise(resolve => { this._openCallback = resolve; }); this._stopPromise = new Promise(resolve => { this._stopCallback = resolve; }); this._msgPromise = new Promise(resolve => { this._msgCallback = resolve; }); this._proxyAvailablePromise = new Promise(resolve => { this._proxyAvailCallback = resolve; }); this._messages = []; this._ws = null; } get QueryInterface() { return ChromeUtils.generateQI([ "nsIWebSocketListener", "nsIProtocolProxyCallback", ]); } onAcknowledge() {} onBinaryMessageAvailable(aContext, aMsg) { this._messages.push(aMsg); this._msgCallback(); } onMessageAvailable() {} onServerClose() {} onWebSocketListenerStart() {} onStart() { this._openCallback(); } onStop(aContext, aStatusCode) { this._stopCallback({ status: aStatusCode }); this._ws = null; } onProxyAvailable(req, chan, proxyInfo) { if (proxyInfo) { this._proxyAvailCallback({ type: proxyInfo.type }); } else { this._proxyAvailCallback({}); } } static makeWebSocketChan() { let chan = Cc["@mozilla.org/network/protocol;1?name=wss"].createInstance( Ci.nsIWebSocketChannel ); chan.initLoadInfo( null, // aLoadingNode Services.scriptSecurityManager.getSystemPrincipal(), null, // aTriggeringPrincipal Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, Ci.nsIContentPolicy.TYPE_WEBSOCKET ); return chan; } // Returns a promise that resolves when the websocket channel is opened. open(url) { this._ws = WebSocketConnection.makeWebSocketChan(); let uri = Services.io.newURI(url); this._ws.asyncOpen(uri, url, {}, 0, this, null); return this._openPromise; } // Closes the inner websocket. code and reason arguments are optional. close(code, reason) { this._ws.close(code || Ci.nsIWebSocketChannel.CLOSE_NORMAL, reason || ""); } // Sends a message to the server. send(msg) { this._ws.sendMsg(msg); } // Returns a promise that resolves when the channel's onStop is called. // Promise resolves with an `{status}` object, where status is the // result passed to onStop. finished() { return this._stopPromise; } getProxyInfo() { return this._proxyAvailablePromise; } // Returned promise resolves with an array of received messages // If messages have been received in the the past before calling // receiveMessages, the promise will immediately resolve. Otherwise // it will resolve when the first message is received. async receiveMessages() { await this._msgPromise; this._msgPromise = new Promise(resolve => { this._msgCallback = resolve; }); let messages = this._messages; this._messages = []; return messages; } } export class HTTP3Server { protocol() { return "https"; } version() { return "h3"; } origin() { return `${this.protocol()}://localhost:${this.port()}`; } port() { return this._port; } masque_proxy_port() { return this._masque_proxy_port; } no_response_port() { return this._no_response_port; } domain() { return `localhost`; } /// Stops the server async stop() { if (this.processId) { await NodeServer.kill(this.processId); this.processId = undefined; } } async start(path, dbPath) { let result = await NodeServer.sendCommand( "", `/forkH3Server?path=${path}&dbPath=${dbPath}` ); this.processId = result.id; /* eslint-disable no-control-regex */ const regex = /HTTP3 server listening on ports (\d+), (\d+), (\d+), (\d+), (\d+) and (\d+). EchConfig is @([\x00-\x7F]+)@/; // Execute the regex on the input string let match = regex.exec(result.output); if (match) { // Extract the ports as an array of numbers let ports = match.slice(1, 7).map(Number); this._port = ports[0]; this._no_response_port = ports[4]; this._masque_proxy_port = ports[5]; return ports[0]; } return undefined; } } export class NodeServer { // Executes command in the context of a node server. // See handler in moz-http2.js // // Example use: // let id = NodeServer.fork(); // id is a random string // await NodeServer.execute(id, `"hello"`) // > "hello" // await NodeServer.execute(id, `(() => "hello")()`) // > "hello" // await NodeServer.execute(id, `(() => var_defined_on_server)()`) // > "0" // await NodeServer.execute(id, `var_defined_on_server`) // > "0" // function f(param) { if (param) return param; return "bla"; } // await NodeServer.execute(id, f); // Defines the function on the server // await NodeServer.execute(id, `f()`) // executes defined function // > "bla" // let result = await NodeServer.execute(id, `f("test")`); // > "test" // await NodeServer.kill(id); // shuts down the server // Forks a new node server using moz-http2-child.js as a starting point static fork() { return this.sendCommand("", "/fork"); } // Executes command in the context of the node server indicated by `id` static execute(id, command) { return this.sendCommand(command, `/execute/${id}`); } // Shuts down the server static kill(id) { return this.sendCommand("", `/kill/${id}`); } // Issues a request to the node server (handler defined in moz-http2.js) // This method should not be called directly. static sendCommand(command, path) { let h2Port = Services.env.get("MOZNODE_EXEC_PORT"); if (!h2Port) { throw new Error("Could not find MOZNODE_EXEC_PORT"); } let req = new XMLHttpRequest({ mozAnon: true, mozSystem: true }); const serverIP = AppConstants.platform == "android" ? "10.0.2.2" : "127.0.0.1"; // eslint-disable-next-line @microsoft/sdl/no-insecure-url req.open("POST", `http://${serverIP}:${h2Port}${path}`); req.channel.QueryInterface(Ci.nsIHttpChannelInternal).bypassProxy = true; req.channel.loadFlags |= Ci.nsIChannel.LOAD_BYPASS_URL_CLASSIFIER; // Prevent HTTPS-Only Mode from upgrading the request. req.channel.loadInfo.httpsOnlyStatus |= Ci.nsILoadInfo.HTTPS_ONLY_EXEMPT; // Allow deprecated HTTP request from SystemPrincipal req.channel.loadInfo.allowDeprecatedSystemRequests = true; // Passing a function to NodeServer.execute will define that function // in node. It can be called in a later execute command. let isFunction = function (obj) { return !!(obj && obj.constructor && obj.call && obj.apply); }; let payload = command; if (isFunction(command)) { payload = `${command.name} = ${command.toString()};`; } return new Promise((resolve, reject) => { req.onload = () => { let x = null; if (req.statusText != "OK") { reject(`XHR request failed: ${req.statusText}`); return; } try { x = JSON.parse(req.responseText); } catch (e) { reject(`Failed to parse ${req.responseText} - ${e}`); return; } if (x.error) { let e = new Error(x.error, "", 0); e.stack = x.errorStack; reject(e); return; } resolve(x.result); }; req.onerror = e => { reject(e); }; req.send(payload.toString()); }); } }