moz-http2.js (27601B)
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 // This module is the stateful server side of test_http2.js and is meant 6 // to have node be restarted in between each invocation 7 8 /* eslint-env node */ 9 10 var node_http2_root = "../node-http2"; 11 if (process.env.NODE_HTTP2_ROOT) { 12 node_http2_root = process.env.NODE_HTTP2_ROOT; 13 } 14 var http2 = require(node_http2_root); 15 var fs = require("fs"); 16 var url = require("url"); 17 var crypto = require("crypto"); 18 const ip = require(`${node_http2_root}/../node_ip`); 19 const { fork } = require("child_process"); 20 const { spawn } = require("child_process"); 21 const path = require("path"); 22 23 // Hook into the decompression code to log the decompressed name-value pairs 24 var compression_module = node_http2_root + "/lib/protocol/compressor"; 25 var http2_compression = require(compression_module); 26 var HeaderSetDecompressor = http2_compression.HeaderSetDecompressor; 27 var originalRead = HeaderSetDecompressor.prototype.read; 28 var lastDecompressor; 29 var decompressedPairs; 30 HeaderSetDecompressor.prototype.read = function () { 31 if (this != lastDecompressor) { 32 lastDecompressor = this; 33 decompressedPairs = []; 34 } 35 var pair = originalRead.apply(this, arguments); 36 if (pair) { 37 decompressedPairs.push(pair); 38 } 39 return pair; 40 }; 41 42 var connection_module = node_http2_root + "/lib/protocol/connection"; 43 var http2_connection = require(connection_module); 44 var Connection = http2_connection.Connection; 45 var originalClose = Connection.prototype.close; 46 Connection.prototype.close = function (error, lastId) { 47 if (lastId !== undefined) { 48 this._lastIncomingStream = lastId; 49 } 50 51 originalClose.apply(this, arguments); 52 }; 53 54 var framer_module = node_http2_root + "/lib/protocol/framer"; 55 var http2_framer = require(framer_module); 56 var Serializer = http2_framer.Serializer; 57 var originalTransform = Serializer.prototype._transform; 58 var newTransform = function (frame) { 59 if (frame.type == "DATA") { 60 // Insert our empty DATA frame 61 const emptyFrame = {}; 62 emptyFrame.type = "DATA"; 63 emptyFrame.data = Buffer.alloc(0); 64 emptyFrame.flags = []; 65 emptyFrame.stream = frame.stream; 66 var buffers = []; 67 Serializer.DATA(emptyFrame, buffers); 68 Serializer.commonHeader(emptyFrame, buffers); 69 for (var i = 0; i < buffers.length; i++) { 70 this.push(buffers[i]); 71 } 72 73 // Reset to the original version for later uses 74 Serializer.prototype._transform = originalTransform; 75 } 76 originalTransform.apply(this, arguments); 77 }; 78 79 function getHttpContent(pathName) { 80 var content = 81 "<!doctype html>" + 82 "<html>" + 83 "<head><title>HOORAY!</title></head>" + 84 // 'You Win!' used in tests to check we reached this server 85 "<body>You Win! (by requesting" + 86 pathName + 87 ")</body>" + 88 "</html>"; 89 return content; 90 } 91 92 function generateContent(size) { 93 var content = ""; 94 for (var i = 0; i < size; i++) { 95 content += "0"; 96 } 97 return content; 98 } 99 100 /* This takes care of responding to the multiplexed request for us */ 101 var m = { 102 mp1res: null, 103 mp2res: null, 104 buf: null, 105 mp1start: 0, 106 mp2start: 0, 107 108 checkReady() { 109 if (this.mp1res != null && this.mp2res != null) { 110 this.buf = generateContent(30 * 1024); 111 this.mp1start = 0; 112 this.mp2start = 0; 113 this.send(this.mp1res, 0); 114 setTimeout(this.send.bind(this, this.mp2res, 0), 5); 115 } 116 }, 117 118 send(res, start) { 119 var end = Math.min(start + 1024, this.buf.length); 120 var content = this.buf.substring(start, end); 121 res.write(content); 122 if (end < this.buf.length) { 123 setTimeout(this.send.bind(this, res, end), 10); 124 } else { 125 // Clear these variables so we can run the test again with --verify 126 if (res == this.mp1res) { 127 this.mp1res = null; 128 } else { 129 this.mp2res = null; 130 } 131 res.end(); 132 } 133 }, 134 }; 135 136 var runlater = function () {}; 137 runlater.prototype = { 138 req: null, 139 resp: null, 140 fin: true, 141 142 onTimeout: function onTimeout() { 143 this.resp.writeHead(200); 144 if (this.fin) { 145 this.resp.end("It's all good 750ms."); 146 } 147 }, 148 }; 149 150 var runConnectLater = function () {}; 151 runConnectLater.prototype = { 152 req: null, 153 resp: null, 154 connect: false, 155 156 onTimeout: function onTimeout() { 157 if (this.connect) { 158 this.resp.writeHead(200); 159 this.connect = true; 160 setTimeout(executeRunLaterCatchError, 50, this); 161 } else { 162 this.resp.end("HTTP/1.1 200\n\r\n\r"); 163 } 164 }, 165 }; 166 167 var moreData = function () {}; 168 moreData.prototype = { 169 req: null, 170 resp: null, 171 iter: 3, 172 173 onTimeout: function onTimeout() { 174 // 1mb of data 175 const content = generateContent(1024 * 1024); 176 this.resp.write(content); // 1mb chunk 177 this.iter--; 178 if (!this.iter) { 179 this.resp.end(); 180 } else { 181 setTimeout(executeRunLater, 1, this); 182 } 183 }, 184 }; 185 186 var resetLater = function () {}; 187 resetLater.prototype = { 188 resp: null, 189 190 onTimeout: function onTimeout() { 191 this.resp.stream.reset("HTTP_1_1_REQUIRED"); 192 }, 193 }; 194 195 function executeRunLater(arg) { 196 arg.onTimeout(); 197 } 198 199 function executeRunLaterCatchError(arg) { 200 arg.onTimeout(); 201 } 202 203 var h11required_conn = null; 204 var h11required_header = "yes"; 205 var didRst = false; 206 var rstConnection = null; 207 var illegalheader_conn = null; 208 209 // eslint-disable-next-line complexity 210 function handleRequest(req, res) { 211 var u = ""; 212 if (req.url != undefined) { 213 u = url.parse(req.url, true); 214 } 215 var content = getHttpContent(u.pathname); 216 var push; 217 218 if (req.httpVersionMajor === 2) { 219 res.setHeader("X-Connection-Http2", "yes"); 220 res.setHeader("X-Http2-StreamId", "" + req.stream.id); 221 } else { 222 res.setHeader("X-Connection-Http2", "no"); 223 } 224 225 if (u.pathname === "/exit") { 226 res.setHeader("Content-Type", "text/plain"); 227 res.setHeader("Connection", "close"); 228 res.writeHead(200); 229 res.end("ok"); 230 process.exit(); 231 } 232 233 if (req.method == "CONNECT") { 234 if (req.headers.host == "illegalhpacksoft.example.com:80") { 235 illegalheader_conn = req.stream.connection; 236 res.setHeader("Content-Type", "text/html"); 237 res.setHeader("x-softillegalhpack", "true"); 238 res.writeHead(200); 239 res.end(content); 240 return; 241 } else if (req.headers.host == "illegalhpackhard.example.com:80") { 242 res.setHeader("Content-Type", "text/html"); 243 res.setHeader("x-hardillegalhpack", "true"); 244 res.writeHead(200); 245 res.end(content); 246 return; 247 } else if (req.headers.host == "750.example.com:80") { 248 // This response will mock a response through a proxy to a HTTP server. 249 // After 750ms , a 200 response for the proxy will be sent then 250 // after additional 50ms a 200 response for the HTTP GET request. 251 let rl = new runConnectLater(); 252 rl.req = req; 253 rl.resp = res; 254 setTimeout(executeRunLaterCatchError, 750, rl); 255 return; 256 } else if (req.headers.host == "h11required.com:80") { 257 if (req.httpVersionMajor === 2) { 258 res.stream.reset("HTTP_1_1_REQUIRED"); 259 } 260 return; 261 } 262 } else if (u.pathname === "/750ms") { 263 let rl = new runlater(); 264 rl.req = req; 265 rl.resp = res; 266 setTimeout(executeRunLater, 750, rl); 267 return; 268 } else if (u.pathname === "/750msNoData") { 269 let rl = new runlater(); 270 rl.req = req; 271 rl.resp = res; 272 rl.fin = false; 273 setTimeout(executeRunLater, 750, rl); 274 return; 275 } else if (u.pathname === "/multiplex1" && req.httpVersionMajor === 2) { 276 res.setHeader("Content-Type", "text/plain"); 277 res.writeHead(200); 278 m.mp1res = res; 279 m.checkReady(); 280 return; 281 } else if (u.pathname === "/multiplex2" && req.httpVersionMajor === 2) { 282 res.setHeader("Content-Type", "text/plain"); 283 res.writeHead(200); 284 m.mp2res = res; 285 m.checkReady(); 286 return; 287 } else if (u.pathname === "/header") { 288 var val = req.headers["x-test-header"]; 289 if (val) { 290 res.setHeader("X-Received-Test-Header", val); 291 } 292 } else if (u.pathname === "/doubleheader") { 293 res.setHeader("Content-Type", "text/html"); 294 res.writeHead(200); 295 res.write(content); 296 res.writeHead(200); 297 res.end(); 298 return; 299 } else if (u.pathname === "/cookie_crumbling") { 300 res.setHeader("X-Received-Header-Pairs", JSON.stringify(decompressedPairs)); 301 } else if (u.pathname === "/big") { 302 content = generateContent(128 * 1024); 303 var hash = crypto.createHash("md5"); 304 hash.update(content); 305 let md5 = hash.digest("hex"); 306 res.setHeader("X-Expected-MD5", md5); 307 } else if (u.pathname === "/huge") { 308 content = generateContent(1024); 309 res.setHeader("Content-Type", "text/plain"); 310 res.writeHead(200); 311 // 1mb of data 312 for (let i = 0; i < 1024 * 1; i++) { 313 res.write(content); // 1kb chunk 314 } 315 res.end(); 316 return; 317 } else if (u.pathname === "/post" || u.pathname === "/patch") { 318 if (req.method != "POST" && req.method != "PATCH") { 319 res.writeHead(405); 320 res.end("Unexpected method: " + req.method); 321 return; 322 } 323 324 var post_hash = crypto.createHash("md5"); 325 var received_data = false; 326 req.on("data", function receivePostData(chunk) { 327 received_data = true; 328 post_hash.update(chunk.toString()); 329 }); 330 req.on("end", function finishPost() { 331 let md5 = received_data ? post_hash.digest("hex") : "0"; 332 res.setHeader("X-Calculated-MD5", md5); 333 res.writeHead(200); 334 res.end(content); 335 }); 336 337 return; 338 } else if (u.pathname === "/750msPost") { 339 if (req.method != "POST") { 340 res.writeHead(405); 341 res.end("Unexpected method: " + req.method); 342 return; 343 } 344 345 var accum = 0; 346 req.on("data", function receivePostData(chunk) { 347 accum += chunk.length; 348 }); 349 req.on("end", function finishPost() { 350 res.setHeader("X-Recvd", accum); 351 let rl = new runlater(); 352 rl.req = req; 353 rl.resp = res; 354 setTimeout(executeRunLater, 750, rl); 355 }); 356 357 return; 358 } else if (u.pathname === "/h11required_stream") { 359 if (req.httpVersionMajor === 2) { 360 h11required_conn = req.stream.connection; 361 res.stream.reset("HTTP_1_1_REQUIRED"); 362 return; 363 } 364 } else if (u.pathname === "/bigdownload") { 365 res.setHeader("Content-Type", "text/html"); 366 res.writeHead(200); 367 368 let rl = new moreData(); 369 rl.req = req; 370 rl.resp = res; 371 setTimeout(executeRunLater, 1, rl); 372 return; 373 } else if (u.pathname === "/h11required_session") { 374 if (req.httpVersionMajor === 2) { 375 if (h11required_conn !== req.stream.connection) { 376 h11required_header = "no"; 377 } 378 res.stream.connection.close("HTTP_1_1_REQUIRED", res.stream.id - 2); 379 return; 380 } 381 res.setHeader("X-H11Required-Stream-Ok", h11required_header); 382 } else if (u.pathname === "/h11required_with_content") { 383 if (req.httpVersionMajor === 2) { 384 res.setHeader("Content-Type", "text/plain"); 385 res.setHeader("Content-Length", "ok".length); 386 res.writeHead(200); 387 res.write("ok"); 388 let resetFunc = new resetLater(); 389 resetFunc.resp = res; 390 setTimeout(executeRunLater, 1, resetFunc); 391 return; 392 } 393 } else if (u.pathname === "/rstonce") { 394 if (!didRst && req.httpVersionMajor === 2) { 395 didRst = true; 396 rstConnection = req.stream.connection; 397 req.stream.reset("REFUSED_STREAM"); 398 return; 399 } 400 401 if (rstConnection === null || rstConnection !== req.stream.connection) { 402 if (req.httpVersionMajor != 2) { 403 res.setHeader("Connection", "close"); 404 } 405 res.writeHead(400); 406 res.end("WRONG CONNECTION, HOMIE!"); 407 return; 408 } 409 410 // Clear these variables so we can run the test again with --verify 411 didRst = false; 412 rstConnection = null; 413 414 if (req.httpVersionMajor != 2) { 415 res.setHeader("Connection", "close"); 416 } 417 res.writeHead(200); 418 res.end("It's all good."); 419 return; 420 } else if (u.pathname === "/continuedheaders") { 421 var pushRequestHeaders = { "x-pushed-request": "true" }; 422 var pushResponseHeaders = { 423 "content-type": "text/plain", 424 "content-length": "2", 425 "X-Connection-Http2": "yes", 426 }; 427 var pushHdrTxt = 428 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 429 var pullHdrTxt = pushHdrTxt.split("").reverse().join(""); 430 for (let i = 0; i < 265; i++) { 431 pushRequestHeaders["X-Push-Test-Header-" + i] = pushHdrTxt; 432 res.setHeader("X-Pull-Test-Header-" + i, pullHdrTxt); 433 } 434 push = res.push({ 435 hostname: "localhost:" + serverPort, 436 port: serverPort, 437 path: "/continuedheaders/push", 438 method: "GET", 439 headers: pushRequestHeaders, 440 }); 441 push.writeHead(200, pushResponseHeaders); 442 push.end("ok"); 443 } else if (u.pathname === "/hugecontinuedheaders") { 444 for (let i = 0; i < u.query.size; i++) { 445 res.setHeader( 446 "X-Test-Header-" + i, 447 "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".repeat(1024) 448 ); 449 } 450 res.writeHead(200); 451 res.end(content); 452 return; 453 } else if (u.pathname === "/altsvc1") { 454 if ( 455 req.httpVersionMajor != 2 || 456 req.scheme != "http" || 457 req.headers["alt-used"] != "foo.example.com:" + serverPort 458 ) { 459 res.writeHead(400); 460 res.end("WHAT?"); 461 return; 462 } 463 // test the alt svc frame for use with altsvc2 464 res.altsvc( 465 "foo.example.com", 466 serverPort, 467 "h2", 468 3600, 469 req.headers["x-redirect-origin"] 470 ); 471 } else if (u.pathname === "/altsvc2") { 472 if ( 473 req.httpVersionMajor != 2 || 474 req.scheme != "http" || 475 req.headers["alt-used"] != "foo.example.com:" + serverPort 476 ) { 477 res.writeHead(400); 478 res.end("WHAT?"); 479 return; 480 } 481 } 482 483 // for use with test_altsvc.js 484 else if (u.pathname === "/altsvc-test") { 485 res.setHeader("Cache-Control", "no-cache"); 486 res.setHeader("Alt-Svc", "h2=" + req.headers["x-altsvc"]); 487 } 488 // for use with test_http3.js 489 else if (u.pathname === "/http3-test") { 490 res.setHeader("Cache-Control", "no-cache"); 491 res.setHeader("Alt-Svc", "h3=" + req.headers["x-altsvc"]); 492 } 493 // for use with test_http3.js 494 else if (u.pathname === "/http3-test2") { 495 res.setHeader("Cache-Control", "no-cache"); 496 res.setHeader( 497 "Alt-Svc", 498 "h2=foo2.example.com:8000,h3=" + req.headers["x-altsvc"] 499 ); 500 } else if (u.pathname === "/http3-test3") { 501 res.setHeader("Cache-Control", "no-cache"); 502 res.setHeader( 503 "Alt-Svc", 504 "h3-29=" + req.headers["x-altsvc"] + ",h3=" + req.headers["x-altsvc"] 505 ); 506 } else if (u.pathname === "/websocket") { 507 res.setHeader("Upgrade", "websocket"); 508 res.setHeader("Connection", "Upgrade"); 509 var wshash = crypto.createHash("sha1"); 510 wshash.update(req.headers["sec-websocket-key"]); 511 wshash.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); 512 let key = wshash.digest("base64"); 513 res.setHeader("Sec-WebSocket-Accept", key); 514 res.writeHead(101); 515 res.end("something...."); 516 return; 517 } else if (u.pathname === "/.well-known/http-opportunistic") { 518 res.setHeader("Cache-Control", "no-cache"); 519 res.setHeader("Content-Type", "application/json"); 520 res.writeHead(200, "OK"); 521 res.end('["http://' + req.headers.host + '"]'); 522 return; 523 } else if (u.pathname === "/stale-while-revalidate-loop-test") { 524 res.setHeader( 525 "Cache-Control", 526 "s-maxage=86400, stale-while-revalidate=86400, immutable" 527 ); 528 res.setHeader("Content-Type", "text/plain; charset=utf-8"); 529 res.setHeader("X-Content-Type-Options", "nosniff"); 530 res.setHeader("Content-Length", "1"); 531 res.writeHead(200, "OK"); 532 res.end("1"); 533 return; 534 } else if (u.pathname === "/illegalhpacksoft") { 535 // This will cause the compressor to compress a header that is not legal, 536 // but only affects the stream, not the session. 537 illegalheader_conn = req.stream.connection; 538 res.setHeader("Content-Type", "text/html"); 539 res.setHeader("x-softillegalhpack", "true"); 540 res.writeHead(200); 541 res.end(content); 542 return; 543 } else if (u.pathname === "/illegalhpackhard") { 544 // This will cause the compressor to insert an HPACK instruction that will 545 // cause a session failure. 546 res.setHeader("Content-Type", "text/html"); 547 res.setHeader("x-hardillegalhpack", "true"); 548 res.writeHead(200); 549 res.end(content); 550 return; 551 } else if (u.pathname === "/illegalhpack_validate") { 552 if (req.stream.connection === illegalheader_conn) { 553 res.setHeader("X-Did-Goaway", "no"); 554 } else { 555 res.setHeader("X-Did-Goaway", "yes"); 556 } 557 // Fall through to the default response behavior 558 } else if (u.pathname === "/foldedheader") { 559 res.setHeader("X-Folded-Header", "this is\n folded"); 560 // Fall through to the default response behavior 561 } else if (u.pathname === "/emptydata") { 562 // Overwrite the original transform with our version that will insert an 563 // empty DATA frame at the beginning of the stream response, then fall 564 // through to the default response behavior. 565 Serializer.prototype._transform = newTransform; 566 } 567 568 // for use with test_immutable.js 569 else if (u.pathname === "/immutable-test-without-attribute") { 570 res.setHeader("Cache-Control", "max-age=100000"); 571 res.setHeader("Etag", "1"); 572 if (req.headers["if-none-match"]) { 573 res.setHeader("x-conditional", "true"); 574 } 575 // default response from here 576 } else if (u.pathname === "/immutable-test-with-attribute") { 577 res.setHeader("Cache-Control", "max-age=100000, immutable"); 578 res.setHeader("Etag", "2"); 579 if (req.headers["if-none-match"]) { 580 res.setHeader("x-conditional", "true"); 581 } 582 // default response from here 583 } else if (u.pathname === "/immutable-test-expired-with-Expires-header") { 584 res.setHeader("Cache-Control", "immutable"); 585 res.setHeader("Expires", "Mon, 01 Jan 1990 00:00:00 GMT"); 586 res.setHeader("Etag", "3"); 587 588 if (req.headers["if-none-match"]) { 589 res.setHeader("x-conditional", "true"); 590 } 591 } else if ( 592 u.pathname === "/immutable-test-expired-with-last-modified-header" 593 ) { 594 res.setHeader("Cache-Control", "public, max-age=3600, immutable"); 595 res.setHeader("Date", "Mon, 01 Jan 1990 00:00:00 GMT"); 596 res.setHeader("Last-modified", "Mon, 01 Jan 1990 00:00:00 GMT"); 597 res.setHeader("Etag", "4"); 598 599 if (req.headers["if-none-match"]) { 600 res.setHeader("x-conditional", "true"); 601 } 602 } else if (u.pathname === "/statusphrase") { 603 // Fortunately, the node-http2 API is dumb enough to allow this right on 604 // through, so we can easily test rejecting this on gecko's end. 605 res.writeHead("200 OK"); 606 res.end(content); 607 return; 608 } else if (u.pathname === "/doublypushed") { 609 content = "not pushed"; 610 } else if (u.pathname === "/diskcache") { 611 content = "this was pulled via h2"; 612 } 613 614 // For test_header_Server_Timing.js 615 else if (u.pathname === "/server-timing") { 616 res.setHeader("Content-Type", "text/plain"); 617 res.setHeader("Content-Length", "12"); 618 res.setHeader("Trailer", "Server-Timing"); 619 res.setHeader( 620 "Server-Timing", 621 "metric; dur=123.4; desc=description, metric2; dur=456.78; desc=description1" 622 ); 623 res.write("data reached"); 624 res.addTrailers({ 625 "Server-Timing": 626 "metric3; dur=789.11; desc=description2, metric4; dur=1112.13; desc=description3", 627 }); 628 res.end(); 629 return; 630 } else if (u.pathname === "/103_response") { 631 let link_val = req.headers["link-to-set"]; 632 if (link_val) { 633 res.setHeader("link", link_val); 634 } 635 res.setHeader("something", "something"); 636 res.writeHead(103); 637 638 res.setHeader("Content-Type", "text/plain"); 639 res.setHeader("Content-Length", "12"); 640 res.writeHead(200); 641 res.write("data reached"); 642 res.end(); 643 return; 644 } else if (u.pathname.startsWith("/invalid_response_header/")) { 645 // response headers with invalid characters in the name / value (RFC7540 Sec 10.3) 646 let kind = u.pathname.slice("/invalid_response_header/".length); 647 if (kind === "name_spaces") { 648 res.setHeader("With Spaces", "Hello"); 649 } else if (kind === "value_line_feed") { 650 res.setHeader("invalid-header", "line\nfeed"); 651 } else if (kind === "value_carriage_return") { 652 res.setHeader("invalid-header", "carriage\rreturn"); 653 } else if (kind === "value_null") { 654 res.setHeader("invalid-header", "null\0"); 655 } 656 657 res.writeHead(200); 658 res.end(""); 659 return; 660 } 661 662 res.setHeader("Content-Type", "text/html"); 663 if (req.httpVersionMajor != 2) { 664 res.setHeader("Connection", "close"); 665 } 666 res.writeHead(200); 667 res.end(content); 668 } 669 670 // Set up the SSL certs for our server - this server has a cert for foo.example.com 671 // signed by netwerk/tests/unit/http2-ca.pem 672 var options = { 673 key: fs.readFileSync(__dirname + "/http2-cert.key"), 674 cert: fs.readFileSync(__dirname + "/http2-cert.pem"), 675 }; 676 677 if (process.env.HTTP2_LOG !== undefined) { 678 var log_module = node_http2_root + "/test/util"; 679 options.log = require(log_module).createLogger("server"); 680 } 681 682 var server = http2.createServer(options, handleRequest); 683 684 server.on("connection", function (socket) { 685 socket.on("error", function () { 686 // Ignoring SSL socket errors, since they usually represent a connection that was tore down 687 // by the browser because of an untrusted certificate. And this happens at least once, when 688 // the first test case if done. 689 }); 690 }); 691 692 server.on("connect", function (req, clientSocket) { 693 clientSocket.write( 694 "HTTP/1.1 404 Not Found\r\nProxy-agent: Node.js-Proxy\r\n\r\n" 695 ); 696 clientSocket.destroy(); 697 }); 698 699 function makeid(length) { 700 var result = ""; 701 var characters = 702 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 703 var charactersLength = characters.length; 704 for (var i = 0; i < length; i++) { 705 result += characters.charAt(Math.floor(Math.random() * charactersLength)); 706 } 707 return result; 708 } 709 710 let globalObjects = {}; 711 var serverPort; 712 713 const listen = (serv, envport) => { 714 if (!serv) { 715 return Promise.resolve(0); 716 } 717 718 let portSelection = 0; 719 if (envport !== undefined) { 720 try { 721 portSelection = parseInt(envport, 10); 722 } catch (e) { 723 portSelection = -1; 724 } 725 } 726 return new Promise(resolve => { 727 serv.listen(portSelection, "0.0.0.0", 2000, () => { 728 resolve(serv.address().port); 729 }); 730 }); 731 }; 732 733 const http = require("http"); 734 let httpServer = http.createServer((req, res) => { 735 if (req.method != "POST") { 736 let u = url.parse(req.url, true); 737 if (u.pathname == "/test") { 738 // This path is used to test that the server is working properly 739 res.writeHead(200); 740 res.end("OK"); 741 return; 742 } 743 res.writeHead(405); 744 res.end("Unexpected method: " + req.method); 745 return; 746 } 747 748 let code = ""; 749 req.on("data", function receivePostData(chunk) { 750 code += chunk; 751 }); 752 req.on("end", function finishPost() { 753 let u = url.parse(req.url, true); 754 if (u.pathname == "/fork") { 755 let id = forkProcess(); 756 computeAndSendBackResponse(id); 757 return; 758 } 759 760 if (u.pathname == "/forkH3Server") { 761 forkH3Server(u.query.path, u.query.dbPath) 762 .then(result => { 763 computeAndSendBackResponse(result); 764 }) 765 .catch(error => { 766 computeAndSendBackResponse(error); 767 }); 768 return; 769 } 770 771 if (u.pathname.startsWith("/kill/")) { 772 let id = u.pathname.slice(6); 773 let forked = globalObjects[id]; 774 if (!forked) { 775 computeAndSendBackResponse(undefined, new Error("could not find id")); 776 return; 777 } 778 779 new Promise((resolve, reject) => { 780 forked.resolve = resolve; 781 forked.reject = reject; 782 forked.kill(); 783 }) 784 .then(x => 785 computeAndSendBackResponse( 786 undefined, 787 new Error(`incorrectly resolved ${x}`) 788 ) 789 ) 790 .catch(e => { 791 // We indicate a proper shutdown by resolving with undefined. 792 if (e && e.toString().match(/child process exit closing code/)) { 793 e = undefined; 794 } 795 computeAndSendBackResponse(undefined, e); 796 }); 797 return; 798 } 799 800 if (u.pathname.startsWith("/execute/")) { 801 let id = u.pathname.slice(9); 802 let forked = globalObjects[id]; 803 if (!forked) { 804 computeAndSendBackResponse(undefined, new Error("could not find id")); 805 return; 806 } 807 808 let messageId = makeid(6); 809 new Promise((resolve, reject) => { 810 forked.messageHandlers[messageId] = { resolve, reject }; 811 forked.send({ code, messageId }); 812 }) 813 .then(x => sendBackResponse(x)) 814 .catch(e => computeAndSendBackResponse(undefined, e)); 815 } 816 817 function computeAndSendBackResponse(evalResult, e) { 818 let output = { result: evalResult, error: "", errorStack: "" }; 819 if (e) { 820 output.error = e.toString(); 821 output.errorStack = e.stack; 822 } 823 sendBackResponse(output); 824 } 825 826 function sendBackResponse(output) { 827 output = JSON.stringify(output); 828 829 res.setHeader("Content-Length", output.length); 830 res.setHeader("Content-Type", "application/json"); 831 res.writeHead(200); 832 res.write(output); 833 res.end(""); 834 } 835 }); 836 }); 837 838 function forkH3Server(serverPath, dbPath) { 839 const args = [dbPath]; 840 let process = spawn(serverPath, args); 841 let id = forkProcessInternal(process); 842 // Return a promise that resolves when we receive data from stdout 843 return new Promise((resolve, _) => { 844 process.stdout.on("data", data => { 845 console.log(data.toString()); 846 resolve({ id, output: data.toString().trim() }); 847 }); 848 }); 849 } 850 851 function forkProcess() { 852 let scriptPath = path.resolve(__dirname, "moz-http2-child.js"); 853 let forked = fork(scriptPath); 854 return forkProcessInternal(forked); 855 } 856 857 function forkProcessInternal(forked) { 858 let id = makeid(6); 859 forked.errors = ""; 860 forked.messageHandlers = {}; 861 globalObjects[id] = forked; 862 forked.on("message", msg => { 863 if (msg.messageId && forked.messageHandlers[msg.messageId]) { 864 let handler = forked.messageHandlers[msg.messageId]; 865 delete forked.messageHandlers[msg.messageId]; 866 handler.resolve(msg); 867 } else { 868 console.log( 869 `forked process without handler sent: ${JSON.stringify(msg)}` 870 ); 871 forked.errors += `forked process without handler sent: ${JSON.stringify( 872 msg 873 )}\n`; 874 } 875 }); 876 877 let exitFunction = (code, signal) => { 878 if (globalObjects[id]) { 879 delete globalObjects[id]; 880 } else { 881 // already called 882 return; 883 } 884 885 let errorMsg = `child process exit closing code: ${code} signal: ${signal}`; 886 if (forked.errors != "") { 887 errorMsg = forked.errors; 888 forked.errors = ""; 889 } 890 891 // Handle /kill/ case where forked.reject is set 892 if (forked.reject) { 893 forked.reject(errorMsg); 894 forked.reject = null; 895 forked.resolve = null; 896 } 897 898 if (Object.keys(forked.messageHandlers).length === 0) { 899 return; 900 } 901 902 for (let messageId in forked.messageHandlers) { 903 forked.messageHandlers[messageId].reject(errorMsg); 904 } 905 forked.messageHandlers = {}; 906 }; 907 908 forked.on("error", exitFunction); 909 forked.on("close", exitFunction); 910 forked.on("exit", exitFunction); 911 912 return id; 913 } 914 915 Promise.all([ 916 listen(server, process.env.MOZHTTP2_PORT).then(port => (serverPort = port)), 917 listen(httpServer, process.env.MOZNODE_EXEC_PORT), 918 ]).then(([sPort, nodeExecPort]) => { 919 console.log(`HTTP2 server listening on ports ${sPort},${nodeExecPort}`); 920 });