/* 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"); const odoh = require(`${node_http2_root}/../odoh-wasm/pkg`); // 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, onTimeout: function onTimeout() { this.resp.writeHead(200); this.resp.end("It's all good 750ms."); }, }; 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(); } var Compressor = http2_compression.Compressor; var HeaderSetCompressor = http2_compression.HeaderSetCompressor; var originalCompressHeaders = Compressor.prototype.compress; function insertSoftIllegalHpack(headers) { var originalCompressed = originalCompressHeaders.apply(this, headers); var illegalLiteral = Buffer.from([ 0x00, // Literal, no index 0x08, // Name: not huffman encoded, 8 bytes long 0x3a, 0x69, 0x6c, 0x6c, 0x65, 0x67, 0x61, 0x6c, // :illegal 0x10, // Value: not huffman encoded, 16 bytes long // REALLY NOT LEGAL 0x52, 0x45, 0x41, 0x4c, 0x4c, 0x59, 0x20, 0x4e, 0x4f, 0x54, 0x20, 0x4c, 0x45, 0x47, 0x41, 0x4c, ]); var newBufferLength = originalCompressed.length + illegalLiteral.length; var concatenated = Buffer.alloc(newBufferLength); originalCompressed.copy(concatenated, 0); illegalLiteral.copy(concatenated, originalCompressed.length); return concatenated; } function insertHardIllegalHpack(headers) { var originalCompressed = originalCompressHeaders.apply(this, headers); // Now we have to add an invalid header var illegalIndexed = HeaderSetCompressor.integer(5000, 7); // The above returns an array of buffers, but there's only one buffer, so // get rid of the array. illegalIndexed = illegalIndexed[0]; // Set the first bit to 1 to signal this is an indexed representation illegalIndexed[0] |= 0x80; var newBufferLength = originalCompressed.length + illegalIndexed.length; var concatenated = Buffer.alloc(newBufferLength); originalCompressed.copy(concatenated, 0); illegalIndexed.copy(concatenated, originalCompressed.length); return concatenated; } var h11required_conn = null; var h11required_header = "yes"; var didRst = false; var rstConnection = null; var illegalheader_conn = null; var cname_confirm = 0; // eslint-disable-next-line complexity function handleRequest(req, res) { // We do this first to ensure nothing goes wonky in our tests that don't want // the headers to have something illegal in them Compressor.prototype.compress = originalCompressHeaders; var 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() { let rContent; if (0 == cname_confirm) { // ... this sends a CNAME back to pointing-elsewhere.example.com rContent = Buffer.from( "00000100000100010000000005636E616D65076578616D706C6503636F6D0000050001C00C0005000100000037002012706F696E74696E672D656C73657768657265076578616D706C6503636F6D00", "hex" ); cname_confirm++; } else { // ... this sends an A 99.88.77.66 entry back for pointing-elsewhere.example.com rContent = Buffer.from( "00000100000100010000000012706F696E74696E672D656C73657768657265076578616D706C6503636F6D0000010001C00C0001000100000037000463584D42", "hex" ); } return rContent; } 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 > 0 && 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) { // This shuts down the connection so we can test if the client reconnects if ( packet.questions.length > 0 && packet.questions[0].name == "closeme.com" ) { response.stream.connection.close("INTERNAL_ERROR", response.stream.id); return null; } function responseData() { if ( packet.questions.length > 0 && 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); resp.writeHead(200); res.end(result); }); } else { resp.setHeader("Content-Length", buffer.length); resp.writeHead(200); resp.write(buffer); resp.end(""); } } 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 (u.pathname === "/750ms") { let rl = new runlater(); rl.req = req; rl.resp = res; 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 = '