/* 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/. */ // This module is the stateful server side of test_http2.js and is meant // to have node be restarted in between each invocation /* eslint-env node */ var node_http2_root = "../node-http2"; if (process.env.NODE_HTTP2_ROOT) { node_http2_root = process.env.NODE_HTTP2_ROOT; } var http2 = require(node_http2_root); var fs = require("fs"); var url = require("url"); var crypto = require("crypto"); const dnsPacket = require(`${node_http2_root}/../dns-packet`); const ip = require(`${node_http2_root}/../node_ip`); const { fork } = require("child_process"); const path = require("path"); const zlib = require("zlib"); // Hook into the decompression code to log the decompressed name-value pairs var compression_module = node_http2_root + "/lib/protocol/compressor"; var http2_compression = require(compression_module); var HeaderSetDecompressor = http2_compression.HeaderSetDecompressor; var originalRead = HeaderSetDecompressor.prototype.read; var lastDecompressor; var decompressedPairs; HeaderSetDecompressor.prototype.read = function () { if (this != lastDecompressor) { lastDecompressor = this; decompressedPairs = []; } var pair = originalRead.apply(this, arguments); if (pair) { decompressedPairs.push(pair); } return pair; }; var connection_module = node_http2_root + "/lib/protocol/connection"; var http2_connection = require(connection_module); var Connection = http2_connection.Connection; var originalClose = Connection.prototype.close; Connection.prototype.close = function (error, lastId) { if (lastId !== undefined) { this._lastIncomingStream = lastId; } originalClose.apply(this, arguments); }; var framer_module = node_http2_root + "/lib/protocol/framer"; var http2_framer = require(framer_module); var Serializer = http2_framer.Serializer; var originalTransform = Serializer.prototype._transform; var newTransform = function (frame, encoding, done) { if (frame.type == "DATA") { // Insert our empty DATA frame const emptyFrame = {}; emptyFrame.type = "DATA"; emptyFrame.data = Buffer.alloc(0); emptyFrame.flags = []; emptyFrame.stream = frame.stream; var buffers = []; Serializer.DATA(emptyFrame, buffers); Serializer.commonHeader(emptyFrame, buffers); for (var i = 0; i < buffers.length; i++) { this.push(buffers[i]); } // Reset to the original version for later uses Serializer.prototype._transform = originalTransform; } originalTransform.apply(this, arguments); }; function getHttpContent(pathName) { var content = "" + "" + "HOORAY!" + // 'You Win!' used in tests to check we reached this server "You Win! (by requesting" + pathName + ")" + ""; return content; } function generateContent(size) { var content = ""; for (var i = 0; i < size; i++) { content += "0"; } return content; } /* This takes care of responding to the multiplexed request for us */ var m = { mp1res: null, mp2res: null, buf: null, mp1start: 0, mp2start: 0, checkReady() { if (this.mp1res != null && this.mp2res != null) { this.buf = generateContent(30 * 1024); this.mp1start = 0; this.mp2start = 0; this.send(this.mp1res, 0); setTimeout(this.send.bind(this, this.mp2res, 0), 5); } }, send(res, start) { var end = Math.min(start + 1024, this.buf.length); var content = this.buf.substring(start, end); res.write(content); if (end < this.buf.length) { setTimeout(this.send.bind(this, res, end), 10); } else { // Clear these variables so we can run the test again with --verify if (res == this.mp1res) { this.mp1res = null; } else { this.mp2res = null; } res.end(); } }, }; var runlater = function () {}; runlater.prototype = { req: null, resp: null, fin: true, onTimeout: function onTimeout() { this.resp.writeHead(200); if (this.fin) { this.resp.end("It's all good 750ms."); } }, }; var runConnectLater = function () {}; runConnectLater.prototype = { req: null, resp: null, connect: false, onTimeout: function onTimeout() { if (this.connect) { this.resp.writeHead(200); this.connect = true; setTimeout(executeRunLaterCatchError, 50, this); } else { this.resp.end("HTTP/1.1 200\n\r\n\r"); } }, }; var moreData = function () {}; moreData.prototype = { req: null, resp: null, iter: 3, onTimeout: function onTimeout() { // 1mb of data const content = generateContent(1024 * 1024); this.resp.write(content); // 1mb chunk this.iter--; if (!this.iter) { this.resp.end(); } else { setTimeout(executeRunLater, 1, this); } }, }; function executeRunLater(arg) { arg.onTimeout(); } function executeRunLaterCatchError(arg) { arg.onTimeout(); } var h11required_conn = null; var h11required_header = "yes"; var didRst = false; var rstConnection = null; var illegalheader_conn = null; var gDoHPortsLog = []; var gDoHNewConnLog = {}; var gDoHRequestCount = 0; // eslint-disable-next-line complexity function handleRequest(req, res) { var u = ""; if (req.url != undefined) { u = url.parse(req.url, true); } var content = getHttpContent(u.pathname); var push, push1, push1a, push2, push3; // PushService tests. var pushPushServer1, pushPushServer2, pushPushServer3, pushPushServer4; function createCNameContent(payload) { let packet = dnsPacket.decode(payload); if ( packet.questions[0].name == "cname.example.com" && packet.questions[0].type == "A" ) { return dnsPacket.encode({ id: 0, type: "response", flags: dnsPacket.RECURSION_DESIRED, questions: [{ name: packet.questions[0].name, type: "A", class: "IN" }], answers: [ { name: packet.questions[0].name, ttl: 55, type: "CNAME", flush: false, data: "pointing-elsewhere.example.com", }, ], }); } if ( packet.questions[0].name == "pointing-elsewhere.example.com" && packet.questions[0].type == "A" ) { return dnsPacket.encode({ id: 0, type: "response", flags: dnsPacket.RECURSION_DESIRED, questions: [{ name: packet.questions[0].name, type: "A", class: "IN" }], answers: [ { name: packet.questions[0].name, ttl: 55, type: "A", flush: false, data: "99.88.77.66", }, ], }); } return dnsPacket.encode({ id: 0, type: "response", flags: dnsPacket.RECURSION_DESIRED | dnsPacket.rcodes.toRcode("NXDOMAIN"), questions: [ { name: packet.questions[0].name, type: packet.questions[0].type, class: "IN", }, ], answers: [], }); } function createCNameARecord() { // test23 asks for cname-a.example.com // this responds with a CNAME to here.example.com *and* an A record // for here.example.com let rContent; rContent = Buffer.from( "0000" + "0100" + "0001" + // QDCOUNT "0002" + // ANCOUNT "00000000" + // NSCOUNT + ARCOUNT "07636E616D652d61" + // cname-a "076578616D706C6503636F6D00" + // .example.com "00010001" + // question type (A) + question class (IN) // answer record 1 "C00C" + // name pointer to cname-a.example.com "0005" + // type (CNAME) "0001" + // class "00000037" + // TTL "0012" + // RDLENGTH "0468657265" + // here "076578616D706C6503636F6D00" + // .example.com // answer record 2, the A entry for the CNAME above "0468657265" + // here "076578616D706C6503636F6D00" + // .example.com "0001" + // type (A) "0001" + // class "00000037" + // TTL "0004" + // RDLENGTH "09080706", // IPv4 address "hex" ); return rContent; } function responseType(packet, responseIP) { if ( !!packet.questions.length && packet.questions[0].name == "confirm.example.com" && packet.questions[0].type == "NS" ) { return "NS"; } return ip.isV4Format(responseIP) ? "A" : "AAAA"; } function handleAuth() { // There's a Set-Cookie: header in the response for "/dns" , which this // request subsequently would include if the http channel wasn't // anonymous. Thus, if there's a cookie in this request, we know Firefox // mishaved. If there's not, we're fine. if (req.headers.cookie) { res.writeHead(403); res.end("cookie for me, not for you"); return false; } if (req.headers.authorization != "user:password") { res.writeHead(401); res.end("bad boy!"); return false; } return true; } function createDNSAnswer(response, packet, responseIP, requestPayload) { // This shuts down the connection so we can test if the client reconnects if (packet.questions.length && packet.questions[0].name == "closeme.com") { response.stream.connection.close("INTERNAL_ERROR", response.stream.id); return null; } if (packet.questions.length && packet.questions[0].name.endsWith(".pd")) { // Bug 1543811: test edns padding extension. Return whether padding was // included via the first half of the ip address (1.1 vs 2.2) and the // size of the request in the second half of the ip address allowing to // verify that the correct amount of padding was added. if ( !!packet.additionals.length && packet.additionals[0].type == "OPT" && packet.additionals[0].options.some(o => o.type === "PADDING") ) { responseIP = "1.1." + ((requestPayload.length >> 8) & 0xff) + "." + (requestPayload.length & 0xff); } else { responseIP = "2.2." + ((requestPayload.length >> 8) & 0xff) + "." + (requestPayload.length & 0xff); } } if (u.query.corruptedAnswer) { // DNS response header is 12 bytes, we check for this minimum length // at the start of decoding so this is the simplest way to force // a decode error. return "\xFF\xFF\xFF\xFF"; } // Because we send two TRR requests (A and AAAA), skip the first two // requests when testing retry. if (u.query.retryOnDecodeFailure && gDoHRequestCount < 2) { gDoHRequestCount++; return "\xFF\xFF\xFF\xFF"; } function responseData() { if ( !!packet.questions.length && packet.questions[0].name == "confirm.example.com" && packet.questions[0].type == "NS" ) { return "ns.example.com"; } return responseIP; } let answers = []; if ( responseIP != "none" && responseType(packet, responseIP) == packet.questions[0].type ) { answers.push({ name: u.query.hostname ? u.query.hostname : packet.questions[0].name, ttl: 55, type: responseType(packet, responseIP), flush: false, data: responseData(), }); } // for use with test_dns_by_type_resolve.js if (packet.questions[0].type == "TXT") { answers.push({ name: packet.questions[0].name, type: packet.questions[0].type, ttl: 55, class: "IN", flush: false, data: Buffer.from( "62586B67646D39705932556761584D6762586B676347467A63336476636D513D", "hex" ), }); } if (u.query.cnameloop) { answers.push({ name: "cname.example.com", type: "CNAME", ttl: 55, class: "IN", flush: false, data: "pointing-elsewhere.example.com", }); } if (req.headers["accept-language"] || req.headers["user-agent"]) { // If we get this header, don't send back any response. This should // cause the tests to fail. This is easier then actually sending back // the header value into test_trr.js answers = []; } let buf = dnsPacket.encode({ type: "response", id: packet.id, flags: dnsPacket.RECURSION_DESIRED, questions: packet.questions, answers, }); return buf; } function getDelayFromPacket(packet, type) { let delay = 0; if (packet.questions[0].type == "A") { delay = parseInt(u.query.delayIPv4); } else if (packet.questions[0].type == "AAAA") { delay = parseInt(u.query.delayIPv6); } if (u.query.slowConfirm && type == "NS") { delay += 1000; } return delay; } function writeDNSResponse(response, buf, delay, contentType) { function writeResponse(resp, buffer) { resp.setHeader("Set-Cookie", "trackyou=yes; path=/; max-age=100000;"); resp.setHeader("Content-Type", contentType); if (req.headers["accept-encoding"].includes("gzip")) { zlib.gzip(buffer, function (err, result) { resp.setHeader("Content-Encoding", "gzip"); resp.setHeader("Content-Length", result.length); try { resp.writeHead(200); resp.end(result); } catch (e) { // connection was closed by the time we started writing. } }); } else { const output = Buffer.from(buffer, "utf-8"); resp.setHeader("Content-Length", output.length); try { resp.writeHead(200); resp.write(output); resp.end(""); } catch (e) { // connection was closed by the time we started writing. } } } if (delay) { setTimeout( arg => { writeResponse(arg[0], arg[1]); }, delay, [response, buf] ); return; } writeResponse(response, buf); } if (req.httpVersionMajor === 2) { res.setHeader("X-Connection-Http2", "yes"); res.setHeader("X-Http2-StreamId", "" + req.stream.id); } else { res.setHeader("X-Connection-Http2", "no"); } if (u.pathname === "/exit") { res.setHeader("Content-Type", "text/plain"); res.setHeader("Connection", "close"); res.writeHead(200); res.end("ok"); process.exit(); } if (req.method == "CONNECT") { if (req.headers.host == "illegalhpacksoft.example.com:80") { illegalheader_conn = req.stream.connection; res.setHeader("Content-Type", "text/html"); res.setHeader("x-softillegalhpack", "true"); res.writeHead(200); res.end(content); return; } else if (req.headers.host == "illegalhpackhard.example.com:80") { res.setHeader("Content-Type", "text/html"); res.setHeader("x-hardillegalhpack", "true"); res.writeHead(200); res.end(content); return; } else if (req.headers.host == "750.example.com:80") { // This response will mock a response through a proxy to a HTTP server. // After 750ms , a 200 response for the proxy will be sent then // after additional 50ms a 200 response for the HTTP GET request. let rl = new runConnectLater(); rl.req = req; rl.resp = res; setTimeout(executeRunLaterCatchError, 750, rl); return; } else if (req.headers.host == "h11required.com:80") { if (req.httpVersionMajor === 2) { res.stream.reset("HTTP_1_1_REQUIRED"); } return; } } else if (u.pathname === "/750ms") { let rl = new runlater(); rl.req = req; rl.resp = res; setTimeout(executeRunLater, 750, rl); return; } else if (u.pathname === "/750msNoData") { let rl = new runlater(); rl.req = req; rl.resp = res; rl.fin = false; setTimeout(executeRunLater, 750, rl); return; } else if (u.pathname === "/multiplex1" && req.httpVersionMajor === 2) { res.setHeader("Content-Type", "text/plain"); res.writeHead(200); m.mp1res = res; m.checkReady(); return; } else if (u.pathname === "/multiplex2" && req.httpVersionMajor === 2) { res.setHeader("Content-Type", "text/plain"); res.writeHead(200); m.mp2res = res; m.checkReady(); return; } else if (u.pathname === "/header") { var val = req.headers["x-test-header"]; if (val) { res.setHeader("X-Received-Test-Header", val); } } else if (u.pathname === "/doubleheader") { res.setHeader("Content-Type", "text/html"); res.writeHead(200); res.write(content); res.writeHead(200); res.end(); return; } else if (u.pathname === "/cookie_crumbling") { res.setHeader("X-Received-Header-Pairs", JSON.stringify(decompressedPairs)); } else if (u.pathname === "/push") { push = res.push("/push.js"); push.writeHead(200, { "content-type": "application/javascript", pushed: "yes", "content-length": 11, "X-Connection-Http2": "yes", }); push.end("// comments"); content = '