1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /* import-globals-from head_cache.js */
8 /* import-globals-from head_cookies.js */
9 /* import-globals-from head_channels.js */
11 const { NodeServer } = ChromeUtils.importESModule(
12 "resource://testing-common/httpd.sys.mjs"
14 const { AppConstants } = ChromeUtils.importESModule(
15 "resource://gre/modules/AppConstants.sys.mjs"
18 /* globals require, __dirname, global, Buffer, process */
20 class BaseNodeHTTPServerCode {
21 static globalHandler(req, resp) {
22 let path = new URL(req.url, "http://example.com").pathname;
23 let handler = global.path_handlers[path];
25 return handler(req, resp);
28 // Didn't find a handler for this path.
29 let response = `<h1> 404 Path not found: ${path}</h1>`;
30 resp.setHeader("Content-Type", "text/html");
31 resp.setHeader("Content-Length", response.length);
39 static async stopForwarding(port) {
40 return this.forwardPort(port, true);
43 static async forwardPort(port, remove = false) {
44 if (!process.env.MOZ_ANDROID_DATA_DIR) {
45 // Not android, or we don't know how to do the forwarding
48 // When creating a server on Android we must make sure that the port
49 // is forwarded from the host machine to the emulator.
51 if (process.env.MOZ_FETCHES_DIR) {
52 adb_path = `${process.env.MOZ_FETCHES_DIR}/android-sdk-linux/platform-tools/adb`;
55 let command = `${adb_path} reverse tcp:${port} tcp:${port}`;
57 command = `${adb_path} reverse --remove tcp:${port}`;
62 await new Promise((resolve, reject) => {
63 const { exec } = require("child_process");
64 exec(command, (error, stdout, stderr) => {
66 console.log(`error: ${error.message}`);
69 console.log(`stderr: ${stderr}`);
72 // console.log(`stdout: ${stdout}`);
78 console.log(`Command failed: ${error}`);
85 static async listenAndForwardPort(server, port) {
87 const maxRetries = 10;
89 while (retryCount < maxRetries) {
90 await server.listen(port);
91 let serverPort = server.address().port;
92 let res = await ADB.forwardPort(serverPort);
100 `Port forwarding failed. Retrying (${retryCount}/${maxRetries})...`
103 // eslint-disable-next-line no-undef
104 await new Promise(resolve => setTimeout(resolve, 500));
111 class BaseNodeServer {
113 return this._protocol;
116 return this._version;
119 return `${this.protocol()}://localhost:${this.port()}`;
130 if (this.processId) {
131 await this.execute(`ADB.stopForwarding(${this.port()})`);
132 await NodeServer.kill(this.processId);
133 this.processId = undefined;
137 /// Executes a command in the context of the node server
138 async execute(command) {
139 return NodeServer.execute(this.processId, command);
142 /// @path : string - the path on the server that we're handling. ex: /path
143 /// @handler : function(req, resp, url) - function that processes request and
144 /// emits a response.
145 async registerPathHandler(path, handler) {
147 `global.path_handlers["${path}"] = ${handler.toString()}`
154 class NodeHTTPServerCode extends BaseNodeHTTPServerCode {
155 static async startServer(port) {
156 const http = require("http");
157 global.server = http.createServer(BaseNodeHTTPServerCode.globalHandler);
159 let serverPort = await ADB.listenAndForwardPort(global.server, port);
164 class NodeHTTPServer extends BaseNodeServer {
166 _version = "http/1.1";
167 /// Starts the server
168 /// @port - default 0
169 /// when provided, will attempt to listen on that port.
170 async start(port = 0) {
171 this.processId = await NodeServer.fork();
173 await this.execute(BaseNodeHTTPServerCode);
174 await this.execute(NodeHTTPServerCode);
175 await this.execute(ADB);
176 this._port = await this.execute(`NodeHTTPServerCode.startServer(${port})`);
177 await this.execute(`global.path_handlers = {};`);
183 class NodeHTTPSServerCode extends BaseNodeHTTPServerCode {
184 static async startServer(port) {
185 const fs = require("fs");
187 key: fs.readFileSync(__dirname + "/http2-cert.key"),
188 cert: fs.readFileSync(__dirname + "/http2-cert.pem"),
190 const https = require("https");
191 global.server = https.createServer(
193 BaseNodeHTTPServerCode.globalHandler
196 let serverPort = await ADB.listenAndForwardPort(global.server, port);
201 class NodeHTTPSServer extends BaseNodeServer {
203 _version = "http/1.1";
204 /// Starts the server
205 /// @port - default 0
206 /// when provided, will attempt to listen on that port.
207 async start(port = 0) {
208 this.processId = await NodeServer.fork();
210 await this.execute(BaseNodeHTTPServerCode);
211 await this.execute(NodeHTTPSServerCode);
212 await this.execute(ADB);
213 this._port = await this.execute(`NodeHTTPSServerCode.startServer(${port})`);
214 await this.execute(`global.path_handlers = {};`);
220 class NodeHTTP2ServerCode extends BaseNodeHTTPServerCode {
221 static async startServer(port) {
222 const fs = require("fs");
224 key: fs.readFileSync(__dirname + "/http2-cert.key"),
225 cert: fs.readFileSync(__dirname + "/http2-cert.pem"),
227 const http2 = require("http2");
228 global.server = http2.createSecureServer(
230 BaseNodeHTTPServerCode.globalHandler
233 global.sessionCount = 0;
234 global.server.on("session", () => {
235 global.sessionCount++;
238 let serverPort = await ADB.listenAndForwardPort(global.server, port);
242 static sessionCount() {
243 return global.sessionCount;
247 class NodeHTTP2Server extends BaseNodeServer {
250 /// Starts the server
251 /// @port - default 0
252 /// when provided, will attempt to listen on that port.
253 async start(port = 0) {
254 this.processId = await NodeServer.fork();
256 await this.execute(BaseNodeHTTPServerCode);
257 await this.execute(NodeHTTP2ServerCode);
258 await this.execute(ADB);
259 this._port = await this.execute(`NodeHTTP2ServerCode.startServer(${port})`);
260 await this.execute(`global.path_handlers = {};`);
263 async sessionCount() {
264 let count = this.execute(`NodeHTTP2ServerCode.sessionCount()`);
271 class BaseProxyCode {
272 static proxyHandler(req, res) {
273 if (req.url.startsWith("/")) {
279 let url = new URL(req.url);
280 const http = require("http");
288 protocol: url.protocol,
292 proxyresp.statusCode,
293 proxyresp.statusMessage,
296 proxyresp.on("data", chunk => {
297 if (!res.writableEnded) {
301 proxyresp.on("end", () => {
307 console.log(`sock err: ${e}`);
309 if (req.method != "POST") {
312 req.on("data", chunk => {
313 if (!preq.writableEnded) {
317 req.on("end", () => preq.end());
321 static onConnect(req, clientSocket, head) {
322 if (global.connect_handler) {
323 global.connect_handler(req, clientSocket, head);
326 const net = require("net");
327 // Connect to an origin server
328 const { port, hostname } = new URL(`https://${req.url}`);
329 const serverSocket = net
334 family: 4, // Specifies to use IPv4
338 "HTTP/1.1 200 Connection Established\r\n" +
339 "Proxy-agent: Node.js-Proxy\r\n" +
342 serverSocket.write(head);
343 serverSocket.pipe(clientSocket);
344 clientSocket.pipe(serverSocket);
348 console.log("error" + e);
349 // The socket will error out when we kill the connection
353 clientSocket.on("error", e => {
354 console.log("client error" + e);
355 // Sometimes we got ECONNRESET error on windows platform.
356 // Ignore it for now.
361 class BaseHTTPProxy extends BaseNodeServer {
364 Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
365 this.filter = new NodeProxyFilter(
371 pps.registerFilter(this.filter, 10);
372 registerCleanupFunction(() => {
373 this.unregisterFilter();
379 Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
381 pps.unregisterFilter(this.filter);
382 this.filter = undefined;
388 this.unregisterFilter();
392 async registerConnectHandler(handler) {
393 return this.execute(`global.connect_handler = ${handler.toString()}`);
399 class NodeProxyFilter {
400 constructor(type, host, port, flags) {
405 this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]);
407 applyFilter(uri, pi, cb) {
409 Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
410 cb.onProxyFilterResult(
425 class HTTPProxyCode {
426 static async startServer(port) {
427 const http = require("http");
428 global.proxy = http.createServer(BaseProxyCode.proxyHandler);
429 global.proxy.on("connect", BaseProxyCode.onConnect);
431 let proxyPort = await ADB.listenAndForwardPort(global.proxy, port);
436 class NodeHTTPProxyServer extends BaseHTTPProxy {
438 /// Starts the server
439 /// @port - default 0
440 /// when provided, will attempt to listen on that port.
441 async start(port = 0) {
442 this.processId = await NodeServer.fork();
444 await this.execute(BaseProxyCode);
445 await this.execute(HTTPProxyCode);
446 await this.execute(ADB);
447 await this.execute(`global.connect_handler = null;`);
448 this._port = await this.execute(`HTTPProxyCode.startServer(${port})`);
450 this.registerFilter();
456 class HTTPSProxyCode {
457 static async startServer(port) {
458 const fs = require("fs");
460 key: fs.readFileSync(__dirname + "/proxy-cert.key"),
461 cert: fs.readFileSync(__dirname + "/proxy-cert.pem"),
463 const https = require("https");
464 global.proxy = https.createServer(options, BaseProxyCode.proxyHandler);
465 global.proxy.on("connect", BaseProxyCode.onConnect);
467 let proxyPort = await ADB.listenAndForwardPort(global.proxy, port);
472 class NodeHTTPSProxyServer extends BaseHTTPProxy {
474 /// Starts the server
475 /// @port - default 0
476 /// when provided, will attempt to listen on that port.
477 async start(port = 0) {
478 this.processId = await NodeServer.fork();
480 await this.execute(BaseProxyCode);
481 await this.execute(HTTPSProxyCode);
482 await this.execute(ADB);
483 await this.execute(`global.connect_handler = null;`);
484 this._port = await this.execute(`HTTPSProxyCode.startServer(${port})`);
486 this.registerFilter();
492 class HTTP2ProxyCode {
493 static async startServer(port, auth) {
494 const fs = require("fs");
496 key: fs.readFileSync(__dirname + "/proxy-cert.key"),
497 cert: fs.readFileSync(__dirname + "/proxy-cert.pem"),
499 const http2 = require("http2");
500 global.proxy = http2.createSecureServer(options);
501 global.socketCounts = {};
502 this.setupProxy(auth);
504 let proxyPort = await ADB.listenAndForwardPort(global.proxy, port);
508 static setupProxy(auth) {
510 throw new Error("proxy is null");
513 global.proxy.on("stream", (stream, headers) => {
514 if (headers[":scheme"] === "http") {
515 const http = require("http");
517 `${headers[":scheme"]}://${headers[":authority"]}${headers[":path"]}`
522 method: headers[":method"],
523 path: headers[":path"],
526 protocol: url.protocol,
529 let proxyheaders = Object.assign({}, proxyresp.headers);
530 // Filter out some prohibited headers.
531 ["connection", "transfer-encoding", "keep-alive"].forEach(
533 delete proxyheaders[prop];
539 { ":status": proxyresp.statusCode },
544 // The channel may have been closed already.
545 if (e.message != "The stream has been destroyed") {
549 proxyresp.on("data", chunk => {
550 if (stream.writable) {
554 proxyresp.on("end", () => {
560 console.log(`sock err: ${e}`);
563 if (headers[":method"] != "POST") {
566 stream.on("data", chunk => {
567 if (!req.writableEnded) {
571 stream.on("end", () => req.end());
575 if (headers[":method"] !== "CONNECT") {
576 // Only accept CONNECT requests
577 stream.respond({ ":status": 405 });
582 const authorization_token = headers["proxy-authorization"];
583 if (auth && !authorization_token) {
586 "proxy-authenticate": "Basic realm='foo'",
592 const target = headers[":authority"];
593 const { port } = new URL(`https://${target}`);
594 const net = require("net");
595 const socket = net.connect(port, "127.0.0.1", () => {
597 global.socketCounts[socket.remotePort] =
598 (global.socketCounts[socket.remotePort] || 0) + 1;
599 stream.respond({ ":status": 200 });
602 } catch (exception) {
603 console.log(exception);
607 const http2 = require("http2");
608 socket.on("error", error => {
609 const status = error.errno == "ENOTFOUND" ? 404 : 502;
611 // If we already sent headers when the socket connected
612 // then sending the status again would throw.
613 if (!stream.sentHeaders) {
614 stream.respond({ ":status": status });
617 } catch (exception) {
618 stream.close(http2.constants.NGHTTP2_CONNECT_ERROR);
621 stream.on("close", () => {
624 socket.on("close", () => {
627 stream.on("end", () => {
630 stream.on("aborted", () => {
633 stream.on("error", error => {
634 console.log("RESPONSE STREAM ERROR", error);
639 static socketCount(port) {
640 return global.socketCounts[port];
644 class NodeHTTP2ProxyServer extends BaseHTTPProxy {
646 /// Starts the server
647 /// @port - default 0
648 /// when provided, will attempt to listen on that port.
649 async start(port = 0, auth) {
650 this.processId = await NodeServer.fork();
652 await this.execute(BaseProxyCode);
653 await this.execute(HTTP2ProxyCode);
654 await this.execute(ADB);
655 await this.execute(`global.connect_handler = null;`);
656 this._port = await this.execute(
657 `HTTP2ProxyCode.startServer(${port}, ${auth})`
660 this.registerFilter();
663 async socketCount(port) {
664 let count = this.execute(`HTTP2ProxyCode.socketCount(${port})`);
671 class NodeWebSocketServerCode extends BaseNodeHTTPServerCode {
672 static messageHandler(data, ws) {
673 if (global.wsInputHandler) {
674 global.wsInputHandler(data, ws);
681 static async startServer(port) {
682 const fs = require("fs");
684 key: fs.readFileSync(__dirname + "/http2-cert.key"),
685 cert: fs.readFileSync(__dirname + "/http2-cert.pem"),
687 const https = require("https");
688 global.server = https.createServer(
690 BaseNodeHTTPServerCode.globalHandler
693 let node_ws_root = `${__dirname}/../node-ws`;
694 const WebSocket = require(`${node_ws_root}/lib/websocket`);
695 WebSocket.Server = require(`${node_ws_root}/lib/websocket-server`);
696 global.webSocketServer = new WebSocket.Server({ server: global.server });
697 global.webSocketServer.on("connection", function connection(ws) {
698 ws.on("message", data =>
699 NodeWebSocketServerCode.messageHandler(data, ws)
703 let serverPort = await ADB.listenAndForwardPort(global.server, port);
708 class NodeWebSocketServer extends BaseNodeServer {
710 /// Starts the server
711 /// @port - default 0
712 /// when provided, will attempt to listen on that port.
713 async start(port = 0) {
714 this.processId = await NodeServer.fork();
716 await this.execute(BaseNodeHTTPServerCode);
717 await this.execute(NodeWebSocketServerCode);
718 await this.execute(ADB);
719 this._port = await this.execute(
720 `NodeWebSocketServerCode.startServer(${port})`
722 await this.execute(`global.path_handlers = {};`);
723 await this.execute(`global.wsInputHandler = null;`);
726 async registerMessageHandler(handler) {
727 return this.execute(`global.wsInputHandler = ${handler.toString()}`);
731 // websocket http2 server
732 // This code is inspired by
733 // https://github.com/szmarczak/http2-wrapper/blob/master/examples/ws/server.js
734 class NodeWebSocketHttp2ServerCode extends BaseNodeHTTPServerCode {
735 static async startServer(port) {
736 const fs = require("fs");
738 key: fs.readFileSync(__dirname + "/http2-cert.key"),
739 cert: fs.readFileSync(__dirname + "/http2-cert.pem"),
741 enableConnectProtocol: true,
744 const http2 = require("http2");
745 global.h2Server = http2.createSecureServer(options);
747 let node_ws_root = `${__dirname}/../node-ws`;
748 const WebSocket = require(`${node_ws_root}/lib/websocket`);
750 global.h2Server.on("stream", (stream, headers) => {
751 if (headers[":method"] === "CONNECT") {
754 const ws = new WebSocket(null);
755 stream.setNoDelay = () => {};
756 ws.setSocket(stream, Buffer.from(""), 100 * 1024 * 1024);
758 ws.on("message", data => {
759 if (global.wsInputHandler) {
760 global.wsInputHandler(data, ws);
772 let serverPort = await ADB.listenAndForwardPort(global.h2Server, port);
777 class NodeWebSocketHttp2Server extends BaseNodeServer {
779 /// Starts the server
780 /// @port - default 0
781 /// when provided, will attempt to listen on that port.
782 async start(port = 0) {
783 this.processId = await NodeServer.fork();
785 await this.execute(BaseNodeHTTPServerCode);
786 await this.execute(NodeWebSocketHttp2ServerCode);
787 await this.execute(ADB);
788 this._port = await this.execute(
789 `NodeWebSocketHttp2ServerCode.startServer(${port})`
791 await this.execute(`global.path_handlers = {};`);
792 await this.execute(`global.wsInputHandler = null;`);
795 async registerMessageHandler(handler) {
796 return this.execute(`global.wsInputHandler = ${handler.toString()}`);
802 async function with_node_servers(arrayOfClasses, asyncClosure) {
803 for (let s of arrayOfClasses) {
804 let server = new s();
805 await server.start();
806 registerCleanupFunction(async () => {
809 await asyncClosure(server);
814 // nsITLSServerSocket needs a certificate with a corresponding private key
815 // available. xpcshell tests can import the test file "client-cert.p12" using
816 // the password "password", resulting in a certificate with the common name
817 // "Test End-entity" being available with a corresponding private key.
818 function getTestServerCertificate() {
819 const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
822 const certFile = do_get_file("client-cert.p12");
823 certDB.importPKCS12File(certFile, "password");
824 for (const cert of certDB.getCerts()) {
825 if (cert.commonName == "Test End-entity") {
832 class WebSocketConnection {
834 this._openPromise = new Promise(resolve => {
835 this._openCallback = resolve;
838 this._stopPromise = new Promise(resolve => {
839 this._stopCallback = resolve;
842 this._msgPromise = new Promise(resolve => {
843 this._msgCallback = resolve;
846 this._proxyAvailablePromise = new Promise(resolve => {
847 this._proxyAvailCallback = resolve;
854 get QueryInterface() {
855 return ChromeUtils.generateQI([
856 "nsIWebSocketListener",
857 "nsIProtocolProxyCallback",
862 onBinaryMessageAvailable(aContext, aMsg) {
863 this._messages.push(aMsg);
866 onMessageAvailable() {}
868 onWebSocketListenerStart() {}
870 this._openCallback();
872 onStop(aContext, aStatusCode) {
873 this._stopCallback({ status: aStatusCode });
876 onProxyAvailable(req, chan, proxyInfo) {
878 this._proxyAvailCallback({ type: proxyInfo.type });
880 this._proxyAvailCallback({});
884 static makeWebSocketChan() {
885 let chan = Cc["@mozilla.org/network/protocol;1?name=wss"].createInstance(
886 Ci.nsIWebSocketChannel
889 null, // aLoadingNode
890 Services.scriptSecurityManager.getSystemPrincipal(),
891 null, // aTriggeringPrincipal
892 Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
893 Ci.nsIContentPolicy.TYPE_WEBSOCKET
897 // Returns a promise that resolves when the websocket channel is opened.
899 this._ws = WebSocketConnection.makeWebSocketChan();
900 let uri = Services.io.newURI(url);
901 this._ws.asyncOpen(uri, url, {}, 0, this, null);
902 return this._openPromise;
904 // Closes the inner websocket. code and reason arguments are optional.
905 close(code, reason) {
906 this._ws.close(code || Ci.nsIWebSocketChannel.CLOSE_NORMAL, reason || "");
908 // Sends a message to the server.
910 this._ws.sendMsg(msg);
912 // Returns a promise that resolves when the channel's onStop is called.
913 // Promise resolves with an `{status}` object, where status is the
914 // result passed to onStop.
916 return this._stopPromise;
919 return this._proxyAvailablePromise;
922 // Returned promise resolves with an array of received messages
923 // If messages have been received in the the past before calling
924 // receiveMessages, the promise will immediately resolve. Otherwise
925 // it will resolve when the first message is received.
926 async receiveMessages() {
927 await this._msgPromise;
928 this._msgPromise = new Promise(resolve => {
929 this._msgCallback = resolve;
931 let messages = this._messages;