NodeServer.sys.mjs (36606B)
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/. */ 4 5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 6 import { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs"; 7 8 /* globals require, __dirname, global, Buffer, process */ 9 10 class BaseNodeHTTPServerCode { 11 static globalHandler(req, resp) { 12 let path = new URL(req.url, "https://example.com").pathname; 13 let handler = global.path_handlers[path]; 14 if (handler) { 15 return handler(req, resp); 16 } 17 18 // Didn't find a handler for this path. 19 let response = `<h1> 404 Path not found: ${path}</h1>`; 20 resp.setHeader("Content-Type", "text/html"); 21 resp.setHeader("Content-Length", response.length); 22 resp.writeHead(404); 23 resp.end(response); 24 return undefined; 25 } 26 } 27 28 class ADB { 29 static async stopForwarding(port) { 30 return this.forwardPort(port, true); 31 } 32 33 static async forwardPort(port, remove = false) { 34 if (!process.env.MOZ_ANDROID_DATA_DIR) { 35 // Not android, or we don't know how to do the forwarding 36 return true; 37 } 38 // When creating a server on Android we must make sure that the port 39 // is forwarded from the host machine to the emulator. 40 let adb_path = "adb"; 41 if (process.env.MOZ_FETCHES_DIR) { 42 adb_path = `${process.env.MOZ_FETCHES_DIR}/android-sdk-linux/platform-tools/adb`; 43 } 44 45 let command = `${adb_path} reverse tcp:${port} tcp:${port}`; 46 if (remove) { 47 command = `${adb_path} reverse --remove tcp:${port}`; 48 return true; 49 } 50 51 try { 52 await new Promise((resolve, reject) => { 53 const { exec } = require("child_process"); 54 exec(command, (error, stdout, stderr) => { 55 if (error) { 56 console.log(`error: ${error.message}`); 57 reject(error); 58 } else if (stderr) { 59 console.log(`stderr: ${stderr}`); 60 reject(stderr); 61 } else { 62 // console.log(`stdout: ${stdout}`); 63 resolve(); 64 } 65 }); 66 }); 67 } catch (error) { 68 console.log(`Command failed: ${error}`); 69 return false; 70 } 71 72 return true; 73 } 74 75 static async listenAndForwardPort(server, port) { 76 let retryCount = 0; 77 const maxRetries = 10; 78 79 while (retryCount < maxRetries) { 80 await server.listen(port); 81 let serverPort = server.address().port; 82 let res = await ADB.forwardPort(serverPort); 83 84 if (res) { 85 return serverPort; 86 } 87 88 retryCount++; 89 console.log( 90 `Port forwarding failed. Retrying (${retryCount}/${maxRetries})...` 91 ); 92 server.close(); 93 // eslint-disable-next-line no-undef 94 await new Promise(resolve => setTimeout(resolve, 500)); 95 } 96 97 return -1; 98 } 99 } 100 101 class BaseNodeServer { 102 protocol() { 103 return this._protocol; 104 } 105 version() { 106 return this._version; 107 } 108 origin() { 109 return `${this.protocol()}://localhost:${this.port()}`; 110 } 111 port() { 112 return this._port; 113 } 114 domain() { 115 return `localhost`; 116 } 117 118 static async installCert(filename) { 119 if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { 120 // Can't install cert from content process. 121 return; 122 } 123 let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( 124 Ci.nsIX509CertDB 125 ); 126 127 function readFile(file) { 128 let fstream = Cc[ 129 "@mozilla.org/network/file-input-stream;1" 130 ].createInstance(Ci.nsIFileInputStream); 131 fstream.init(file, -1, 0, 0); 132 let data = NetUtil.readInputStreamToString(fstream, fstream.available()); 133 fstream.close(); 134 return data; 135 } 136 137 // Find the root directory that contains netwerk/ 138 let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); 139 let rootDir = currentDir.clone(); 140 141 // XXX(valentin) The certs are stored in netwerk/test/unit 142 // Walk up until the dir contains netwerk/ 143 // This is hacky, but the alternative would also require 144 // us to walk up the path to the root dir. 145 while (rootDir) { 146 let netwerkDir = rootDir.clone(); 147 netwerkDir.append("netwerk"); 148 if (netwerkDir.exists() && netwerkDir.isDirectory()) { 149 break; 150 } 151 let parent = rootDir.parent; 152 if (!parent || parent.equals(rootDir)) { 153 // Reached filesystem root, fallback to current directory 154 rootDir = currentDir; 155 break; 156 } 157 rootDir = parent; 158 } 159 160 let certFile = rootDir.clone(); 161 certFile.append("netwerk"); 162 certFile.append("test"); 163 certFile.append("unit"); 164 certFile.append(filename); 165 166 try { 167 let pem = readFile(certFile) 168 .replace(/-----BEGIN CERTIFICATE-----/, "") 169 .replace(/-----END CERTIFICATE-----/, "") 170 .replace(/[\r\n]/g, ""); 171 certdb.addCertFromBase64(pem, "CTu,u,u"); 172 } catch (e) { 173 let errStr = e.toString(); 174 console.log(`Error installing cert ${errStr}`); 175 if (errStr.includes("0x805a1fe8")) { 176 // Can't install the cert without a profile 177 // Let's show an error, otherwise this will be difficult to diagnose. 178 console.log( 179 `!!! BaseNodeServer.installCert > Make sure your unit test calls do_get_profile()` 180 ); 181 } 182 } 183 } 184 185 /// Stops the server 186 async stop() { 187 if (this.processId) { 188 await this.execute(`ADB.stopForwarding(${this.port()})`); 189 await NodeServer.kill(this.processId); 190 this.processId = undefined; 191 } 192 } 193 194 /// Executes a command in the context of the node server 195 async execute(command) { 196 return NodeServer.execute(this.processId, command); 197 } 198 199 /// @path : string - the path on the server that we're handling. ex: /path 200 /// @handler : function(req, resp, url) - function that processes request and 201 /// emits a response. 202 async registerPathHandler(path, handler) { 203 return this.execute( 204 `global.path_handlers["${path}"] = ${handler.toString()}` 205 ); 206 } 207 } 208 209 // HTTP 210 211 class NodeHTTPServerCode extends BaseNodeHTTPServerCode { 212 static async startServer(port) { 213 const http = require("http"); 214 global.server = http.createServer(BaseNodeHTTPServerCode.globalHandler); 215 216 let serverPort = await ADB.listenAndForwardPort(global.server, port); 217 return serverPort; 218 } 219 } 220 221 export class NodeHTTPServer extends BaseNodeServer { 222 _protocol = "http"; 223 _version = "http/1.1"; 224 /// Starts the server 225 /// @port - default 0 226 /// when provided, will attempt to listen on that port. 227 async start(port = 0) { 228 this.processId = await NodeServer.fork(); 229 230 await this.execute(BaseNodeHTTPServerCode); 231 await this.execute(NodeHTTPServerCode); 232 await this.execute(ADB); 233 this._port = await this.execute(`NodeHTTPServerCode.startServer(${port})`); 234 await this.execute(`global.path_handlers = {};`); 235 } 236 } 237 238 // HTTPS 239 240 class NodeHTTPSServerCode extends BaseNodeHTTPServerCode { 241 static async startServer(port) { 242 const fs = require("fs"); 243 const options = { 244 key: fs.readFileSync(__dirname + "/http2-cert.key"), 245 cert: fs.readFileSync(__dirname + "/http2-cert.pem"), 246 maxHeaderSize: 128 * 1024, 247 }; 248 const https = require("https"); 249 global.server = https.createServer( 250 options, 251 BaseNodeHTTPServerCode.globalHandler 252 ); 253 254 let serverPort = await ADB.listenAndForwardPort(global.server, port); 255 return serverPort; 256 } 257 } 258 259 export class NodeHTTPSServer extends BaseNodeServer { 260 _protocol = "https"; 261 _version = "http/1.1"; 262 /// Starts the server 263 /// @port - default 0 264 /// when provided, will attempt to listen on that port. 265 async start(port = 0) { 266 if (!this._skipCert) { 267 await BaseNodeServer.installCert("http2-ca.pem"); 268 } 269 this.processId = await NodeServer.fork(); 270 271 await this.execute(BaseNodeHTTPServerCode); 272 await this.execute(NodeHTTPSServerCode); 273 await this.execute(ADB); 274 this._port = await this.execute(`NodeHTTPSServerCode.startServer(${port})`); 275 await this.execute(`global.path_handlers = {};`); 276 } 277 } 278 279 // HTTP2 280 281 class NodeHTTP2ServerCode extends BaseNodeHTTPServerCode { 282 static async startServer(port) { 283 const fs = require("fs"); 284 const options = { 285 key: fs.readFileSync(__dirname + "/http2-cert.key"), 286 cert: fs.readFileSync(__dirname + "/http2-cert.pem"), 287 }; 288 const http2 = require("http2"); 289 global.server = http2.createSecureServer( 290 options, 291 BaseNodeHTTPServerCode.globalHandler 292 ); 293 294 global.sessionCount = 0; 295 global.sessions = new Set(); 296 global.server.on("session", session => { 297 global.sessions.add(session); 298 session.on("close", () => { 299 global.sessions.delete(session); 300 }); 301 global.sessionCount++; 302 }); 303 304 let serverPort = await ADB.listenAndForwardPort(global.server, port); 305 return serverPort; 306 } 307 308 static sessionCount() { 309 return global.sessionCount; 310 } 311 } 312 313 export class NodeHTTP2Server extends BaseNodeServer { 314 _protocol = "https"; 315 _version = "h2"; 316 /// Starts the server 317 /// @port - default 0 318 /// when provided, will attempt to listen on that port. 319 async start(port = 0) { 320 if (!this._skipCert) { 321 await BaseNodeServer.installCert("http2-ca.pem"); 322 } 323 this.processId = await NodeServer.fork(); 324 325 await this.execute(BaseNodeHTTPServerCode); 326 await this.execute(NodeHTTP2ServerCode); 327 await this.execute(ADB); 328 this._port = await this.execute(`NodeHTTP2ServerCode.startServer(${port})`); 329 await this.execute(`global.path_handlers = {};`); 330 } 331 332 async sessionCount() { 333 let count = this.execute(`NodeHTTP2ServerCode.sessionCount()`); 334 return count; 335 } 336 } 337 338 // Base HTTP proxy 339 340 class BaseProxyCode { 341 static proxyHandler(req, res) { 342 if (req.url.startsWith("/")) { 343 res.writeHead(405); 344 res.end(); 345 return; 346 } 347 348 let url = new URL(req.url); 349 const http = require("http"); 350 let preq = http 351 .request( 352 { 353 method: req.method, 354 path: url.pathname, 355 port: url.port, 356 host: url.hostname, 357 protocol: url.protocol, 358 }, 359 proxyresp => { 360 res.writeHead( 361 proxyresp.statusCode, 362 proxyresp.statusMessage, 363 proxyresp.headers 364 ); 365 proxyresp.on("data", chunk => { 366 if (!res.writableEnded) { 367 res.write(chunk); 368 } 369 }); 370 proxyresp.on("end", () => { 371 res.end(); 372 }); 373 } 374 ) 375 .on("error", e => { 376 console.log(`sock err: ${e}`); 377 }); 378 if (req.method != "POST") { 379 preq.end(); 380 } else { 381 req.on("data", chunk => { 382 if (!preq.writableEnded) { 383 preq.write(chunk); 384 } 385 }); 386 req.on("end", () => preq.end()); 387 } 388 } 389 390 static onConnect(req, clientSocket, head) { 391 if (global.connect_handler) { 392 global.connect_handler(req, clientSocket, head); 393 return; 394 } 395 const net = require("net"); 396 // Connect to an origin server 397 const { port, hostname } = new URL(`https://${req.url}`); 398 const serverSocket = net 399 .connect( 400 { 401 port: port || 443, 402 host: hostname, 403 family: 4, // Specifies to use IPv4 404 }, 405 () => { 406 clientSocket.write( 407 "HTTP/1.1 200 Connection Established\r\n" + 408 "Proxy-agent: Node.js-Proxy\r\n" + 409 "\r\n" 410 ); 411 serverSocket.write(head); 412 serverSocket.pipe(clientSocket); 413 clientSocket.pipe(serverSocket); 414 } 415 ) 416 .on("error", e => { 417 console.log("error" + e); 418 // The socket will error out when we kill the connection 419 // just ignore it. 420 }); 421 422 clientSocket.on("error", e => { 423 console.log("client error" + e); 424 // Sometimes we got ECONNRESET error on windows platform. 425 // Ignore it for now. 426 }); 427 } 428 } 429 430 class BaseHTTPProxy extends BaseNodeServer { 431 registerFilter() { 432 const pps = 433 Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); 434 this.filter = new NodeProxyFilter( 435 this.protocol(), 436 "localhost", 437 this.port(), 438 0 439 ); 440 pps.registerFilter(this.filter, 10); 441 } 442 443 unregisterFilter() { 444 const pps = 445 Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); 446 if (this.filter) { 447 pps.unregisterFilter(this.filter); 448 this.filter = undefined; 449 } 450 } 451 452 /// Stops the server 453 async stop() { 454 this.unregisterFilter(); 455 await super.stop(); 456 } 457 458 async registerConnectHandler(handler) { 459 return this.execute(`global.connect_handler = ${handler.toString()}`); 460 } 461 } 462 463 // HTTP1 Proxy 464 465 export class NodeProxyFilter { 466 constructor(type, host, port, flags) { 467 this._type = type; 468 this._host = host; 469 this._port = port; 470 this._flags = flags; 471 this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]); 472 } 473 applyFilter(uri, pi, cb) { 474 const pps = 475 Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); 476 cb.onProxyFilterResult( 477 pps.newProxyInfo( 478 this._type, 479 this._host, 480 this._port, 481 "", 482 "", 483 this._flags, 484 1000, 485 null 486 ) 487 ); 488 } 489 } 490 491 export class Http3ProxyFilter { 492 constructor(host, port, flags, masqueTemplate, auth) { 493 this._host = host; 494 this._port = port; 495 this._flags = flags; 496 this._masqueTemplate = masqueTemplate; 497 this._auth = auth; 498 this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]); 499 } 500 applyFilter(uri, pi, cb) { 501 const pps = 502 Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); 503 cb.onProxyFilterResult( 504 pps.newMASQUEProxyInfo( 505 this._host, 506 this._port, 507 this._masqueTemplate, 508 this._auth, 509 "", 510 this._flags, 511 1000, 512 null 513 ) 514 ); 515 } 516 } 517 518 class HTTPProxyCode { 519 static async startServer(port) { 520 const http = require("http"); 521 global.proxy = http.createServer(BaseProxyCode.proxyHandler); 522 global.proxy.on("connect", BaseProxyCode.onConnect); 523 524 let proxyPort = await ADB.listenAndForwardPort(global.proxy, port); 525 return proxyPort; 526 } 527 } 528 529 export class NodeHTTPProxyServer extends BaseHTTPProxy { 530 _protocol = "http"; 531 /// Starts the server 532 /// @port - default 0 533 /// when provided, will attempt to listen on that port. 534 async start(port = 0) { 535 this.processId = await NodeServer.fork(); 536 537 await this.execute(BaseProxyCode); 538 await this.execute(HTTPProxyCode); 539 await this.execute(ADB); 540 await this.execute(`global.connect_handler = null;`); 541 this._port = await this.execute(`HTTPProxyCode.startServer(${port})`); 542 543 this.registerFilter(); 544 } 545 } 546 547 // HTTPS proxy 548 549 class HTTPSProxyCode { 550 static async startServer(port) { 551 const fs = require("fs"); 552 const options = { 553 key: fs.readFileSync(__dirname + "/proxy-cert.key"), 554 cert: fs.readFileSync(__dirname + "/proxy-cert.pem"), 555 }; 556 const https = require("https"); 557 global.proxy = https.createServer(options, BaseProxyCode.proxyHandler); 558 global.proxy.on("connect", BaseProxyCode.onConnect); 559 560 let proxyPort = await ADB.listenAndForwardPort(global.proxy, port); 561 return proxyPort; 562 } 563 } 564 565 export class NodeHTTPSProxyServer extends BaseHTTPProxy { 566 _protocol = "https"; 567 /// Starts the server 568 /// @port - default 0 569 /// when provided, will attempt to listen on that port. 570 async start(port = 0) { 571 if (!this._skipCert) { 572 await BaseNodeServer.installCert("proxy-ca.pem"); 573 } 574 this.processId = await NodeServer.fork(); 575 576 await this.execute(BaseProxyCode); 577 await this.execute(HTTPSProxyCode); 578 await this.execute(ADB); 579 await this.execute(`global.connect_handler = null;`); 580 this._port = await this.execute(`HTTPSProxyCode.startServer(${port})`); 581 582 this.registerFilter(); 583 } 584 } 585 586 // HTTP2 proxy 587 588 class HTTP2ProxyCode { 589 static async startServer(port, auth, maxConcurrentStreams) { 590 const fs = require("fs"); 591 const options = { 592 key: fs.readFileSync(__dirname + "/proxy-cert.key"), 593 cert: fs.readFileSync(__dirname + "/proxy-cert.pem"), 594 settings: { 595 maxConcurrentStreams, 596 }, 597 }; 598 const http2 = require("http2"); 599 global.proxy = http2.createSecureServer(options); 600 global.socketCounts = {}; 601 this.setupProxy(auth); 602 603 let proxyPort = await ADB.listenAndForwardPort(global.proxy, port); 604 return proxyPort; 605 } 606 607 static setupProxy(auth) { 608 if (!global.proxy) { 609 throw new Error("proxy is null"); 610 } 611 612 global.proxy.on("stream", (stream, headers) => { 613 if (headers[":scheme"] === "http") { 614 const http = require("http"); 615 let url = new URL( 616 `${headers[":scheme"]}://${headers[":authority"]}${headers[":path"]}` 617 ); 618 let req = http 619 .request( 620 { 621 method: headers[":method"], 622 path: headers[":path"], 623 port: url.port, 624 host: url.hostname, 625 protocol: url.protocol, 626 }, 627 proxyresp => { 628 let proxyheaders = Object.assign({}, proxyresp.headers); 629 // Filter out some prohibited headers. 630 ["connection", "transfer-encoding", "keep-alive"].forEach( 631 prop => { 632 delete proxyheaders[prop]; 633 } 634 ); 635 try { 636 stream.respond( 637 Object.assign( 638 { ":status": proxyresp.statusCode }, 639 proxyheaders 640 ) 641 ); 642 } catch (e) { 643 // The channel may have been closed already. 644 if ( 645 e.code !== "ERR_HTTP2_INVALID_STREAM" && 646 !e.message.includes("The stream has been destroyed") 647 ) { 648 throw e; 649 } 650 } 651 proxyresp.on("data", chunk => { 652 if (stream.writable) { 653 stream.write(chunk); 654 } 655 }); 656 proxyresp.on("end", () => { 657 stream.end(); 658 }); 659 } 660 ) 661 .on("error", e => { 662 console.log(`sock err: ${e}`); 663 }); 664 665 if (headers[":method"] != "POST") { 666 req.end(); 667 } else { 668 stream.on("data", chunk => { 669 if (!req.writableEnded) { 670 req.write(chunk); 671 } 672 }); 673 stream.on("end", () => req.end()); 674 } 675 return; 676 } 677 if (headers[":method"] !== "CONNECT") { 678 // Only accept CONNECT requests 679 try { 680 stream.respond({ ":status": 405 }); 681 } catch (e) { 682 if ( 683 e.code !== "ERR_HTTP2_INVALID_STREAM" && 684 !e.message.includes("The stream has been destroyed") 685 ) { 686 throw e; 687 } 688 } 689 stream.end(); 690 return; 691 } 692 693 const authorization_token = headers["proxy-authorization"]; 694 if (auth && !authorization_token) { 695 try { 696 stream.respond({ 697 ":status": 407, 698 "proxy-authenticate": "Basic realm='foo'", 699 }); 700 } catch (e) { 701 if ( 702 e.code !== "ERR_HTTP2_INVALID_STREAM" && 703 !e.message.includes("The stream has been destroyed") 704 ) { 705 throw e; 706 } 707 } 708 stream.end(); 709 return; 710 } 711 712 const target = headers[":authority"]; 713 const { port } = new URL(`https://${target}`); 714 const net = require("net"); 715 const socket = net.connect(port, "127.0.0.1", () => { 716 try { 717 global.socketCounts[socket.remotePort] = 718 (global.socketCounts[socket.remotePort] || 0) + 1; 719 try { 720 stream.respond({ ":status": 200 }); 721 } catch (e) { 722 if ( 723 e.code !== "ERR_HTTP2_INVALID_STREAM" && 724 !e.message.includes("The stream has been destroyed") 725 ) { 726 throw e; 727 } 728 } 729 socket.pipe(stream); 730 stream.pipe(socket); 731 } catch (exception) { 732 console.log(exception); 733 stream.close(); 734 } 735 }); 736 const http2 = require("http2"); 737 socket.on("error", error => { 738 const status = error.errno == "ENOTFOUND" ? 404 : 502; 739 try { 740 // If we already sent headers when the socket connected 741 // then sending the status again would throw. 742 if (!stream.sentHeaders) { 743 try { 744 stream.respond({ ":status": status }); 745 } catch (e) { 746 if ( 747 e.code !== "ERR_HTTP2_INVALID_STREAM" && 748 !e.message.includes("The stream has been destroyed") 749 ) { 750 throw e; 751 } 752 } 753 } 754 stream.end(); 755 } catch (exception) { 756 stream.close(http2.constants.NGHTTP2_CONNECT_ERROR); 757 } 758 }); 759 stream.on("close", () => { 760 socket.end(); 761 }); 762 socket.on("close", () => { 763 stream.close(); 764 }); 765 stream.on("end", () => { 766 socket.end(); 767 }); 768 stream.on("aborted", () => { 769 socket.end(); 770 }); 771 stream.on("error", error => { 772 console.log("RESPONSE STREAM ERROR", error); 773 }); 774 }); 775 } 776 777 static socketCount(port) { 778 return global.socketCounts[port]; 779 } 780 } 781 782 export class NodeHTTP2ProxyServer extends BaseHTTPProxy { 783 _protocol = "https"; 784 /// Starts the server 785 /// @port - default 0 786 /// when provided, will attempt to listen on that port. 787 async start(port = 0, auth, maxConcurrentStreams = 100) { 788 await this.startWithoutProxyFilter(port, auth, maxConcurrentStreams); 789 this.registerFilter(); 790 } 791 792 async startWithoutProxyFilter(port = 0, auth, maxConcurrentStreams = 100) { 793 if (!this._skipCert) { 794 await BaseNodeServer.installCert("proxy-ca.pem"); 795 } 796 this.processId = await NodeServer.fork(); 797 798 await this.execute(BaseProxyCode); 799 await this.execute(HTTP2ProxyCode); 800 await this.execute(ADB); 801 await this.execute(`global.connect_handler = null;`); 802 this._port = await this.execute( 803 `HTTP2ProxyCode.startServer(${port}, ${auth}, ${maxConcurrentStreams})` 804 ); 805 } 806 807 async socketCount(port) { 808 let count = await this.execute(`HTTP2ProxyCode.socketCount(${port})`); 809 return count; 810 } 811 } 812 813 // websocket server 814 815 class NodeWebSocketServerCode extends BaseNodeHTTPServerCode { 816 static messageHandler(data, ws) { 817 if (global.wsInputHandler) { 818 global.wsInputHandler(data, ws); 819 return; 820 } 821 822 ws.send("test"); 823 } 824 825 static async startServer(port) { 826 const fs = require("fs"); 827 const options = { 828 key: fs.readFileSync(__dirname + "/http2-cert.key"), 829 cert: fs.readFileSync(__dirname + "/http2-cert.pem"), 830 }; 831 const https = require("https"); 832 global.server = https.createServer( 833 options, 834 BaseNodeHTTPServerCode.globalHandler 835 ); 836 837 let node_ws_root = `${__dirname}/../node-ws`; 838 const WS = require(`${node_ws_root}/lib/websocket`); 839 WS.Server = require(`${node_ws_root}/lib/websocket-server`); 840 global.webSocketServer = new WS.Server({ server: global.server }); 841 global.webSocketServer.on("connection", function connection(ws) { 842 ws.on("message", data => 843 NodeWebSocketServerCode.messageHandler(data, ws) 844 ); 845 }); 846 847 let serverPort = await ADB.listenAndForwardPort(global.server, port); 848 return serverPort; 849 } 850 } 851 852 export class NodeWebSocketServer extends BaseNodeServer { 853 _protocol = "wss"; 854 /// Starts the server 855 /// @port - default 0 856 /// when provided, will attempt to listen on that port. 857 async start(port = 0) { 858 if (!this._skipCert) { 859 await BaseNodeServer.installCert("http2-ca.pem"); 860 } 861 this.processId = await NodeServer.fork(); 862 863 await this.execute(BaseNodeHTTPServerCode); 864 await this.execute(NodeWebSocketServerCode); 865 await this.execute(ADB); 866 this._port = await this.execute( 867 `NodeWebSocketServerCode.startServer(${port})` 868 ); 869 await this.execute(`global.path_handlers = {};`); 870 await this.execute(`global.wsInputHandler = null;`); 871 } 872 873 async registerMessageHandler(handler) { 874 return this.execute(`global.wsInputHandler = ${handler.toString()}`); 875 } 876 } 877 878 // unencrypted websocket server 879 880 class NodeWebSocketPlainServerCode extends BaseNodeHTTPServerCode { 881 static async startServer(port) { 882 const http = require("http"); 883 global.server = http.createServer(BaseNodeHTTPServerCode.globalHandler); 884 885 let node_ws_root = `${__dirname}/../node-ws`; 886 const WS = require(`${node_ws_root}/lib/websocket`); 887 WS.Server = require(`${node_ws_root}/lib/websocket-server`); 888 global.webSocketServer = new WS.Server({ server: global.server }); 889 global.webSocketServer.on("connection", function connection(ws) { 890 ws.on("message", data => 891 NodeWebSocketServerCode.messageHandler(data, ws) 892 ); 893 }); 894 895 let serverPort = await ADB.listenAndForwardPort(global.server, port); 896 return serverPort; 897 } 898 } 899 900 export class NodeWebSocketPlainServer extends BaseNodeServer { 901 _protocol = "ws"; 902 /// Starts the server 903 /// @port - default 0 904 /// when provided, will attempt to listen on that port. 905 async start(port = 0) { 906 this.processId = await NodeServer.fork(); 907 908 await this.execute(BaseNodeHTTPServerCode); 909 await this.execute(NodeWebSocketServerCode); 910 await this.execute(NodeWebSocketPlainServerCode); 911 await this.execute(ADB); 912 this._port = await this.execute( 913 `NodeWebSocketPlainServerCode.startServer(${port})` 914 ); 915 await this.execute(`global.path_handlers = {};`); 916 await this.execute(`global.wsInputHandler = null;`); 917 } 918 919 async registerMessageHandler(handler) { 920 return this.execute(`global.wsInputHandler = ${handler.toString()}`); 921 } 922 } 923 924 // websocket http2 server 925 // This code is inspired by 926 // https://github.com/szmarczak/http2-wrapper/blob/master/examples/ws/server.js 927 class NodeWebSocketHttp2ServerCode extends BaseNodeHTTPServerCode { 928 static async startServer(port, fallbackToH1) { 929 const fs = require("fs"); 930 const options = { 931 key: fs.readFileSync(__dirname + "/http2-cert.key"), 932 cert: fs.readFileSync(__dirname + "/http2-cert.pem"), 933 settings: { 934 enableConnectProtocol: !fallbackToH1, 935 allowHTTP1: fallbackToH1, 936 }, 937 }; 938 const http2 = require("http2"); 939 global.h2Server = http2.createSecureServer(options); 940 941 let node_ws_root = `${__dirname}/../node-ws`; 942 const WS = require(`${node_ws_root}/lib/websocket`); 943 944 global.h2Server.on("stream", (stream, headers) => { 945 if (headers[":method"] === "CONNECT") { 946 try { 947 stream.respond(); 948 } catch (e) { 949 if ( 950 e.code !== "ERR_HTTP2_INVALID_STREAM" && 951 !e.message.includes("The stream has been destroyed") 952 ) { 953 throw e; 954 } 955 } 956 957 const ws = new WS(null); 958 stream.setNoDelay = () => {}; 959 ws.setSocket(stream, Buffer.from(""), 100 * 1024 * 1024); 960 961 ws.on("message", data => { 962 if (global.wsInputHandler) { 963 global.wsInputHandler(data, ws); 964 return; 965 } 966 967 ws.send("test"); 968 }); 969 } else { 970 try { 971 stream.respond(); 972 } catch (e) { 973 if ( 974 e.code !== "ERR_HTTP2_INVALID_STREAM" && 975 !e.message.includes("The stream has been destroyed") 976 ) { 977 throw e; 978 } 979 } 980 stream.end("ok"); 981 } 982 }); 983 984 let serverPort = await ADB.listenAndForwardPort(global.h2Server, port); 985 return serverPort; 986 } 987 } 988 989 export class NodeWebSocketHttp2Server extends BaseNodeServer { 990 _protocol = "wss"; 991 /// Starts the server 992 /// @port - default 0 993 /// when provided, will attempt to listen on that port. 994 async start(port = 0, fallbackToH1 = false) { 995 if (!this._skipCert) { 996 await BaseNodeServer.installCert("http2-ca.pem"); 997 } 998 this.processId = await NodeServer.fork(); 999 1000 await this.execute(BaseNodeHTTPServerCode); 1001 await this.execute(NodeWebSocketHttp2ServerCode); 1002 await this.execute(ADB); 1003 this._port = await this.execute( 1004 `NodeWebSocketHttp2ServerCode.startServer(${port}, ${fallbackToH1})` 1005 ); 1006 await this.execute(`global.path_handlers = {};`); 1007 await this.execute(`global.wsInputHandler = null;`); 1008 } 1009 1010 async registerMessageHandler(handler) { 1011 return this.execute(`global.wsInputHandler = ${handler.toString()}`); 1012 } 1013 } 1014 1015 // Helper functions 1016 1017 export async function with_node_servers(arrayOfClasses, asyncClosure) { 1018 for (let s of arrayOfClasses) { 1019 let server = new s(); 1020 await server.start(); 1021 await asyncClosure(server); 1022 await server.stop(); 1023 } 1024 } 1025 1026 export class WebSocketConnection { 1027 constructor() { 1028 this._openPromise = new Promise(resolve => { 1029 this._openCallback = resolve; 1030 }); 1031 1032 this._stopPromise = new Promise(resolve => { 1033 this._stopCallback = resolve; 1034 }); 1035 1036 this._msgPromise = new Promise(resolve => { 1037 this._msgCallback = resolve; 1038 }); 1039 1040 this._proxyAvailablePromise = new Promise(resolve => { 1041 this._proxyAvailCallback = resolve; 1042 }); 1043 1044 this._messages = []; 1045 this._ws = null; 1046 } 1047 1048 get QueryInterface() { 1049 return ChromeUtils.generateQI([ 1050 "nsIWebSocketListener", 1051 "nsIProtocolProxyCallback", 1052 ]); 1053 } 1054 1055 onAcknowledge() {} 1056 onBinaryMessageAvailable(aContext, aMsg) { 1057 this._messages.push(aMsg); 1058 this._msgCallback(); 1059 } 1060 onMessageAvailable() {} 1061 onServerClose() {} 1062 onWebSocketListenerStart() {} 1063 onStart() { 1064 this._openCallback(); 1065 } 1066 onStop(aContext, aStatusCode) { 1067 this._stopCallback({ status: aStatusCode }); 1068 this._ws = null; 1069 } 1070 onProxyAvailable(req, chan, proxyInfo) { 1071 if (proxyInfo) { 1072 this._proxyAvailCallback({ type: proxyInfo.type }); 1073 } else { 1074 this._proxyAvailCallback({}); 1075 } 1076 } 1077 1078 static makeWebSocketChan(url) { 1079 let protocol = url.startsWith("wss:") ? "wss" : "ws"; 1080 let chan = Cc[ 1081 `@mozilla.org/network/protocol;1?name=${protocol}` 1082 ].createInstance(Ci.nsIWebSocketChannel); 1083 chan.initLoadInfo( 1084 null, // aLoadingNode 1085 Services.scriptSecurityManager.getSystemPrincipal(), 1086 null, // aTriggeringPrincipal 1087 Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, 1088 Ci.nsIContentPolicy.TYPE_WEBSOCKET 1089 ); 1090 return chan; 1091 } 1092 // Returns a promise that resolves when the websocket channel is opened. 1093 open(url) { 1094 this._ws = WebSocketConnection.makeWebSocketChan(url); 1095 let uri = Services.io.newURI(url); 1096 this._ws.asyncOpen(uri, url, {}, 0, this, null); 1097 return this._openPromise; 1098 } 1099 // Closes the inner websocket. code and reason arguments are optional. 1100 close(code, reason) { 1101 this._ws.close(code || Ci.nsIWebSocketChannel.CLOSE_NORMAL, reason || ""); 1102 } 1103 // Sends a message to the server. 1104 send(msg) { 1105 this._ws.sendMsg(msg); 1106 } 1107 // Returns a promise that resolves when the channel's onStop is called. 1108 // Promise resolves with an `{status}` object, where status is the 1109 // result passed to onStop. 1110 finished() { 1111 return this._stopPromise; 1112 } 1113 getProxyInfo() { 1114 return this._proxyAvailablePromise; 1115 } 1116 1117 // Returned promise resolves with an array of received messages 1118 // If messages have been received in the the past before calling 1119 // receiveMessages, the promise will immediately resolve. Otherwise 1120 // it will resolve when the first message is received. 1121 async receiveMessages() { 1122 await this._msgPromise; 1123 this._msgPromise = new Promise(resolve => { 1124 this._msgCallback = resolve; 1125 }); 1126 let messages = this._messages; 1127 this._messages = []; 1128 return messages; 1129 } 1130 } 1131 1132 export class HTTP3Server { 1133 protocol() { 1134 return "https"; 1135 } 1136 version() { 1137 return "h3"; 1138 } 1139 origin() { 1140 return `${this.protocol()}://localhost:${this.port()}`; 1141 } 1142 port() { 1143 return this._port; 1144 } 1145 masque_proxy_port() { 1146 return this._masque_proxy_port; 1147 } 1148 no_response_port() { 1149 return this._no_response_port; 1150 } 1151 domain() { 1152 return `localhost`; 1153 } 1154 1155 /// Stops the server 1156 async stop() { 1157 if (this.processId) { 1158 await NodeServer.kill(this.processId); 1159 this.processId = undefined; 1160 } 1161 } 1162 1163 async start(path, dbPath) { 1164 let result = await NodeServer.sendCommand( 1165 "", 1166 `/forkH3Server?path=${path}&dbPath=${dbPath}` 1167 ); 1168 this.processId = result.id; 1169 1170 /* eslint-disable no-control-regex */ 1171 const regex = 1172 /HTTP3 server listening on ports (\d+), (\d+), (\d+), (\d+), (\d+) and (\d+). EchConfig is @([\x00-\x7F]+)@/; 1173 1174 // Execute the regex on the input string 1175 let match = regex.exec(result.output); 1176 1177 if (match) { 1178 // Extract the ports as an array of numbers 1179 let ports = match.slice(1, 7).map(Number); 1180 this._port = ports[0]; 1181 this._no_response_port = ports[4]; 1182 this._masque_proxy_port = ports[5]; 1183 return ports[0]; 1184 } 1185 1186 return undefined; 1187 } 1188 } 1189 1190 export class NodeServer { 1191 // Executes command in the context of a node server. 1192 // See handler in moz-http2.js 1193 // 1194 // Example use: 1195 // let id = NodeServer.fork(); // id is a random string 1196 // await NodeServer.execute(id, `"hello"`) 1197 // > "hello" 1198 // await NodeServer.execute(id, `(() => "hello")()`) 1199 // > "hello" 1200 // await NodeServer.execute(id, `(() => var_defined_on_server)()`) 1201 // > "0" 1202 // await NodeServer.execute(id, `var_defined_on_server`) 1203 // > "0" 1204 // function f(param) { if (param) return param; return "bla"; } 1205 // await NodeServer.execute(id, f); // Defines the function on the server 1206 // await NodeServer.execute(id, `f()`) // executes defined function 1207 // > "bla" 1208 // let result = await NodeServer.execute(id, `f("test")`); 1209 // > "test" 1210 // await NodeServer.kill(id); // shuts down the server 1211 1212 // Forks a new node server using moz-http2-child.js as a starting point 1213 static fork() { 1214 return this.sendCommand("", "/fork"); 1215 } 1216 // Executes command in the context of the node server indicated by `id` 1217 static execute(id, command) { 1218 return this.sendCommand(command, `/execute/${id}`); 1219 } 1220 // Shuts down the server 1221 static kill(id) { 1222 return this.sendCommand("", `/kill/${id}`); 1223 } 1224 1225 // Issues a request to the node server (handler defined in moz-http2.js) 1226 // This method should not be called directly. 1227 static sendCommand(command, path) { 1228 let h2Port = Services.env.get("MOZNODE_EXEC_PORT"); 1229 if (!h2Port) { 1230 throw new Error("Could not find MOZNODE_EXEC_PORT"); 1231 } 1232 1233 let req = new XMLHttpRequest({ mozAnon: true, mozSystem: true }); 1234 const serverIP = 1235 AppConstants.platform == "android" ? "10.0.2.2" : "127.0.0.1"; 1236 // eslint-disable-next-line @microsoft/sdl/no-insecure-url 1237 req.open("POST", `http://${serverIP}:${h2Port}${path}`); 1238 req.channel.QueryInterface(Ci.nsIHttpChannelInternal).bypassProxy = true; 1239 req.channel.loadFlags |= Ci.nsIChannel.LOAD_BYPASS_URL_CLASSIFIER; 1240 // Prevent HTTPS-Only Mode from upgrading the request. 1241 req.channel.loadInfo.httpsOnlyStatus |= Ci.nsILoadInfo.HTTPS_ONLY_EXEMPT; 1242 // Allow deprecated HTTP request from SystemPrincipal 1243 req.channel.loadInfo.allowDeprecatedSystemRequests = true; 1244 1245 // Passing a function to NodeServer.execute will define that function 1246 // in node. It can be called in a later execute command. 1247 let isFunction = function (obj) { 1248 return !!(obj && obj.constructor && obj.call && obj.apply); 1249 }; 1250 let payload = command; 1251 if (isFunction(command)) { 1252 payload = `${command.name} = ${command.toString()};`; 1253 } 1254 1255 return new Promise((resolve, reject) => { 1256 req.onload = () => { 1257 let x = null; 1258 1259 if (req.statusText != "OK") { 1260 reject(`XHR request failed: ${req.statusText}`); 1261 return; 1262 } 1263 1264 try { 1265 x = JSON.parse(req.responseText); 1266 } catch (e) { 1267 reject(`Failed to parse ${req.responseText} - ${e}`); 1268 return; 1269 } 1270 1271 if (x.error) { 1272 let e = new Error(x.error, "", 0); 1273 e.stack = x.errorStack; 1274 reject(e); 1275 return; 1276 } 1277 resolve(x.result); 1278 }; 1279 req.onerror = e => { 1280 reject(e); 1281 }; 1282 1283 req.send(payload.toString()); 1284 }); 1285 } 1286 }