https://github.com/mozilla/gecko-dev
Raw File
Tip revision: 52568d10660dfb0ce950f6983b20228374602efe authored by Nick Thomas on 14 November 2019, 03:15:51 UTC
Bug 1595632 - don't hardcode TC root url for release start emails, r=Callek a=NPOTB
Tip revision: 52568d1
moz-http2.js
/* 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

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 net = require('net');
var url = require('url');
var crypto = require('crypto');
const dnsPacket = require(`${node_http2_root}/../dns-packet`);
const ip = require(`${node_http2_root}/../node-ip`);

let http2_internal = null;
try {
  http2_internal = require('http2');
} catch (_) {
  // silently ignored
}

// 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
    emptyFrame = {};
    emptyFrame.type = 'DATA';
    emptyFrame.data = new Buffer(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(path) {
  var content = '<!doctype html>' +
                '<html>' +
                '<head><title>HOORAY!</title></head>' +
                // 'You Win!' used in tests to check we reached this server
                '<body>You Win! (by requesting' + path + ')</body>' +
                '</html>';
  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: function() {
    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: function(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 {
      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
    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 = new Buffer([
      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 = new Buffer(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 = new Buffer(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 ns_confirm = 0;
var cname_confirm = 0;

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;

  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') {
    var 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 = '<head> <script src="push.js"/></head>body text';
  }

  else if (u.pathname === "/push.js") {
    content = '// comments';
    res.setHeader("pushed", "no");
  }

  else if (u.pathname === "/push2") {
    push = res.push('/push2.js');
    push.writeHead(200, {
      'content-type': 'application/javascript',
      'pushed' : 'yes',
      // no content-length
      'X-Connection-Http2': 'yes'
    });
    push.end('// comments');
    content = '<head> <script src="push2.js"/></head>body text';
  }

  else if (u.pathname === "/push5") {
    push = res.push('/push5.js');
    push.writeHead(200, {
      'content-type': 'application/javascript',
      'pushed' : 'yes',
      // no content-length
      'X-Connection-Http2': 'yes'
    });
    content = generateContent(1024 * 150);
    push.write(content);
    push.end();
    content = '<head> <script src="push5.js"/></head>body text';
  }

  else if (u.pathname === "/pushapi1") {
    push1 = res.push(
        { hostname: 'localhost:' + serverPort, port: serverPort, path : '/pushapi1/1', method : 'GET',
          headers: {'x-pushed-request': 'true', 'x-foo' : 'bar'}});
    push1.writeHead(200, {
      'pushed' : 'yes',
      'content-length' : 1,
      'subresource' : '1',
      'X-Connection-Http2': 'yes'
      });
    push1.end('1');

    push1a = res.push(
        { hostname: 'localhost:' + serverPort, port: serverPort, path : '/pushapi1/1', method : 'GET',
          headers: {'x-foo' : 'bar', 'x-pushed-request': 'true'}});
    push1a.writeHead(200, {
      'pushed' : 'yes',
      'content-length' : 1,
      'subresource' : '1a',
      'X-Connection-Http2': 'yes'
    });
    push1a.end('1');

    push2 = res.push(
        { hostname: 'localhost:' + serverPort, port: serverPort, path : '/pushapi1/2', method : 'GET',
          headers: {'x-pushed-request': 'true'}});
    push2.writeHead(200, {
      'pushed' : 'yes',
      'subresource' : '2',
      'content-length' : 1,
      'X-Connection-Http2': 'yes'
    });
    push2.end('2');

    push3 = res.push(
        { hostname: 'localhost:' + serverPort, port: serverPort, path : '/pushapi1/3', method : 'GET',
          headers: {'x-pushed-request': 'true', 'Accept-Encoding' : 'br'}});
    push3.writeHead(200, {
      'pushed' : 'yes',
      'content-length' : 6,
      'subresource' : '3',
      'content-encoding' : 'br',
      'X-Connection-Http2': 'yes'
    });
    push3.end(new Buffer([0x8b, 0x00, 0x80, 0x33, 0x0a, 0x03])); // '3\n'

    content = '0';
  }

  else if (u.pathname === "/big") {
    content = generateContent(128 * 1024);
    var hash = crypto.createHash('md5');
    hash.update(content);
    var md5 = hash.digest('hex');
    res.setHeader("X-Expected-MD5", md5);
  }

  else if (u.pathname === "/huge") {
    content = generateContent(1024);
    res.setHeader('Content-Type', 'text/plain');
    res.writeHead(200);
    // 1mb of data
    for (var i = 0; i < (1024 * 1); i++) {
      res.write(content); // 1kb chunk
    }
    res.end();
    return;
  }

  else if (u.pathname === "/post" || u.pathname === "/patch") {
    if (req.method != "POST" && req.method != "PATCH") {
      res.writeHead(405);
      res.end('Unexpected method: ' + req.method);
      return;
    }

    var post_hash = crypto.createHash('md5');
    req.on('data', function receivePostData(chunk) {
      post_hash.update(chunk.toString());
    });
    req.on('end', function finishPost() {
      var md5 = post_hash.digest('hex');
      res.setHeader('X-Calculated-MD5', md5);
      res.writeHead(200);
      res.end(content);
    });

    return;
  }

  else if (u.pathname === "/750msPost") {
    if (req.method != "POST") {
      res.writeHead(405);
      res.end('Unexpected method: ' + req.method);
      return;
    }

    var accum = 0;
    req.on('data', function receivePostData(chunk) {
      accum += chunk.length;
    });
    req.on('end', function finishPost() {
      res.setHeader('X-Recvd', accum);
      var rl = new runlater();
      rl.req = req;
      rl.resp = res;
      setTimeout(executeRunLater, 750, rl);
      return;
    });

    return;
  }

  else if (u.pathname === "/h11required_stream") {
    if (req.httpVersionMajor === 2) {
      h11required_conn = req.stream.connection;
      res.stream.reset('HTTP_1_1_REQUIRED');
      return;
    }
  }

  else if (u.pathname === "/bigdownload") {

    res.setHeader('Content-Type', 'text/html');
    res.writeHead(200);

    var rl = new moreData();
    rl.req = req;
    rl.resp = res;
    setTimeout(executeRunLater, 1, rl);
    return;
  }

  else if (u.pathname === "/h11required_session") {
    if (req.httpVersionMajor === 2) {
      if (h11required_conn !== req.stream.connection) {
        h11required_header = "no";
      }
      res.stream.connection.close('HTTP_1_1_REQUIRED', res.stream.id - 2);
      return;
    } else {
      res.setHeader('X-H11Required-Stream-Ok', h11required_header);
    }
  }

  else if (u.pathname === "/rstonce") {
    if (!didRst && req.httpVersionMajor === 2) {
      didRst = true;
      rstConnection = req.stream.connection;
      req.stream.reset('REFUSED_STREAM');
      return;
    }

    if (rstConnection === null ||
        rstConnection !== req.stream.connection) {
      res.setHeader('Connection', 'close');
      res.writeHead(400);
      res.end("WRONG CONNECTION, HOMIE!");
      return;
    }

    if (req.httpVersionMajor != 2) {
      res.setHeader('Connection', 'close');
    }
    res.writeHead(200);
    res.end("It's all good.");
    return;
  }

  else if (u.pathname === "/continuedheaders") {
    var pushRequestHeaders = {'x-pushed-request': 'true'};
    var pushResponseHeaders = {'content-type': 'text/plain',
                               'content-length': '2',
                               'X-Connection-Http2': 'yes'};
    var pushHdrTxt = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    var pullHdrTxt = pushHdrTxt.split('').reverse().join('');
    for (var i = 0; i < 265; i++) {
      pushRequestHeaders['X-Push-Test-Header-' + i] = pushHdrTxt;
      res.setHeader('X-Pull-Test-Header-' + i, pullHdrTxt);
    }
    push = res.push({
      hostname: 'localhost:' + serverPort,
      port: serverPort,
      path: '/continuedheaders/push',
      method: 'GET',
      headers: pushRequestHeaders
    });
    push.writeHead(200, pushResponseHeaders);
    push.end("ok");
  }

  else if (u.pathname === "/altsvc1") {
    if (req.httpVersionMajor != 2 ||
      req.scheme != "http" ||
      req.headers['alt-used'] != ("foo.example.com:" + serverPort)) {
      res.writeHead(400);
      res.end("WHAT?");
      return;
    }
    // test the alt svc frame for use with altsvc2
    res.altsvc("foo.example.com", serverPort, "h2", 3600, req.headers['x-redirect-origin']);
  }

  else if (u.pathname === "/altsvc2") {
    if (req.httpVersionMajor != 2 ||
      req.scheme != "http" ||
      req.headers['alt-used'] != ("foo.example.com:" + serverPort)) {
      res.writeHead(400);
      res.end("WHAT?");
      return;
   }
  }

  // for use with test_altsvc.js
  else if (u.pathname === "/altsvc-test") {
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Alt-Svc', 'h2=' + req.headers['x-altsvc']);
  }
  // for use with test_trr.js
  else if (u.pathname === "/dns-cname") {
    // asking for cname.example.com
    var content;
    if(0 == cname_confirm) {
      // ... this sends a CNAME back to pointing-elsewhere.example.com
      content = new Buffer("00000100000100010000000005636E616D65076578616D706C6503636F6D0000050001C00C0005000100000037002012706F696E74696E672D656C73657768657265076578616D706C6503636F6D00", "hex");
      cname_confirm++;
    }
    else {
      // ... this sends an A 99.88.77.66 entry back for pointing-elsewhere.example.com
      content = new Buffer("00000100000100010000000012706F696E74696E672D656C73657768657265076578616D706C6503636F6D0000010001C00C0001000100000037000463584D42", "hex");
    }
    res.setHeader('Content-Type', 'application/dns-message');
    res.setHeader('Content-Length', content.length);
    res.writeHead(200);
    res.write(content);
    res.end("");
    return;

  }
  else if (u.pathname == "/doh") {
    ns_confirm = 0; // back to first reply for dns-confirm
    cname_confirm = 0; // back to first reply for dns-cname

    let responseIP = u.query["responseIP"];
    if (!responseIP) {
      responseIP = "5.5.5.5";
    }

    if (u.query["auth"]) {
      // 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;
      }
      if (req.headers['authorization'] != "user:password") {
        res.writeHead(401);
        res.end("bad boy!");
        return;
      }
    }

    if (u.query["push"]) {
      // push.example.com has AAAA entry 2018::2018
      var pcontent= new Buffer("0000010000010001000000000470757368076578616D706C6503636F6D00001C0001C00C001C000100000037001020180000000000000000000000002018", "hex");
      push = res.push({
        hostname: 'foo.example.com:' + serverPort,
        port: serverPort,
        path: '/dns-pushed-response?dns=AAAAAAABAAAAAAAABHB1c2gHZXhhbXBsZQNjb20AABwAAQ',
        method: 'GET',
        headers: {
          'accept' : 'application/dns-message'
        }
      });
      push.writeHead(200, {
        'content-type': 'application/dns-message',
        'pushed' : 'yes',
        'content-length' : pcontent.length,
        'X-Connection-Http2': 'yes'
      });
      push.end(pcontent);
    }

    let payload = new Buffer("");

    function emitResponse(response, requestPayload) {
      let packet = dnsPacket.decode(requestPayload);

      let buf = dnsPacket.encode({
        type: 'query',
        id: packet.id,
        flags: dnsPacket.RECURSION_DESIRED,
        questions: packet.questions,
        answers: [{
          name: packet.questions[0].name,
          ttl: 55,
          type: ip.isV4Format(responseIP) ? "A" : "AAAA",
          flush: false,
          data: responseIP,
        }],
      });

      response.setHeader('Content-Length', buf.length);
      response.setHeader('Set-Cookie', 'trackyou=yes; path=/; max-age=100000;');
      response.setHeader('Content-Type', 'application/dns-message');
      response.writeHead(200);
      response.write(buf);
      response.end("");
      return;
    }

    if (u.query["dns"]) {
      payload = Buffer.from(u.query["dns"], 'base64');
      emitResponse(res, payload);
      return;
    }

    req.on('data', function receiveData(chunk) {
      payload = Buffer.concat([payload, chunk]);
    });
    req.on('end', function finishedData() {
      emitResponse(res, payload);
      return;
    });
    return;
  }
  else if (u.pathname === "/dns-cname-a") {
    // test23 asks for cname-a.example.com
    // this responds with a CNAME to here.example.com *and* an A record
    // for here.example.com
    var content;

    content = new Buffer("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");
    res.setHeader('Content-Type', 'application/dns-message');
    res.setHeader('Content-Length', content.length);
    res.writeHead(200);
    res.write(content);
    res.end("");
    return;

  }
  else if (u.pathname === "/dns-cname-loop") {
    // asking for cname.example.com
    var content;
    // ... this always sends a CNAME back to pointing-elsewhere.example.com. Loop time!
    content = new Buffer("00000100000100010000000005636E616D65076578616D706C6503636F6D0000050001C00C0005000100000037002012706F696E74696E672D656C73657768657265076578616D706C65C01A00", "hex");
    res.setHeader('Content-Type', 'application/dns-message');
    res.setHeader('Content-Length', content.length);
    res.writeHead(200);
    res.write(content);
    res.end("");
    return;

  }
  else if (u.pathname === "/dns-ns") {
    // confirm.example.com has NS entry ns.example.com
    var content= new Buffer("00000100000100010000000007636F6E6669726D076578616D706C6503636F6D0000020001C00C00020001000000370012026E73076578616D706C6503636F6D010A00", "hex");
    res.setHeader('Content-Type', 'application/dns-message');
    res.setHeader('Content-Length', content.length);
    res.writeHead(200);
    res.write(content);
    res.end("");
    return;
  }
  else if (u.pathname === '/dns-750ms') {
    // it's just meant to be this slow - the test doesn't care about the actual response
    return;
  }
  // for use with test_trr.js
  else if (u.pathname === "/dns-confirm") {
    if (0 == ns_confirm) {
      // confirm.example.com has NS entry ns.example.com
      var content= new Buffer("00000100000100010000000007636F6E6669726D076578616D706C6503636F6D0000020001C00C00020001000000370012026E73076578616D706C6503636F6D010A00", "hex");
      ns_confirm++;
    } else if (2 >= ns_confirm) {
      // next response: 10b-100.example.com has AAAA entry 1::FFFF

      // we expect two requests for this name (A + AAAA), respond identically
      // for both and expect the client to reject the wrong one
      var content= new Buffer("000001000001000100000000" + "073130622d313030" +
                              "076578616D706C6503636F6D00001C0001C00C001C00010000003700100001000000000000000000000000FFFF", "hex");
      ns_confirm++;
    } else {
      // everything else is just wrong
      return;
    }
    res.setHeader('Content-Type', 'application/dns-message');
    res.setHeader('Content-Length', content.length);
    res.writeHead(200);
    res.write(content);
    res.end("");
    return;
  }
  // for use with test_esni_dns_fetch.js
  else if (u.pathname === "/esni-dns") {
    content = new Buffer("0000" +
                         "8180" +
                         "0001" + // QDCOUNT
                         "0001" + // ANCOUNT
                         "00000000" + // NSCOUNT + ARCOUNT
                         "055F65736E69076578616D706C6503636F6D00" + // _esni.example.com
                         "00100001" + // question type (TXT) + question class (IN)

                         "C00C" + // name pointer to .example.com
                         "0010" + // type (TXT)
                         "0001" + // class
                         "00000037" + // TTL
                         "0021" + // RDLENGTH
                         "2062586B67646D39705932556761584D6762586B676347467A63336476636D513D", // esni keys.
                         "hex");

    res.setHeader('Content-Type', 'application/dns-message');
    res.setHeader('Content-Length', content.length);
    res.writeHead(200);
    res.write(content);
    res.end("");
    return;
  }

  // for use with test_esni_dns_fetch.js
  else if (u.pathname === "/esni-dns-push") {
    // _esni_push.example.com has A entry 127.0.0.1
    var content= new Buffer("0000010000010001000000000A5F65736E695F70757368076578616D706C6503636F6D0000010001C00C000100010000003700047F000001", "hex");

    // _esni_push.example.com has TXT entry 2062586B67646D39705932556761584D6762586B676347467A63336476636D513D
    var pcontent= new Buffer("0000818000010001000000000A5F65736E695F70757368076578616D706C6503636F6D0000100001C00C001000010000003700212062586B67646D39705932556761584D6762586B676347467A63336476636D513D", "hex");

    push = res.push({
      hostname: 'foo.example.com:' + serverPort,
      port: serverPort,
      path: '/dns-pushed-response?dns=AAABAAABAAAAAAABCl9lc25pX3B1c2gHZXhhbXBsZQNjb20AABAAAQAAKRAAAAAAAAAIAAgABAABAAA',
      method: 'GET',
      headers: {
        'accept' : 'application/dns-message'
      }
    });
    push.writeHead(200, {
      'content-type': 'application/dns-message',
      'pushed' : 'yes',
      'content-length' : pcontent.length,
      'X-Connection-Http2': 'yes'
    });
    push.end(pcontent);
    res.setHeader('Content-Type', 'application/dns-message');
    res.setHeader('Content-Length', content.length);
    res.writeHead(200);
    res.write(content);
    res.end("");
    return;
  }

  else if (u.pathname === "/.well-known/http-opportunistic") {
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Content-Type', 'application/json');
    res.writeHead(200, "OK");
    res.end('{"http://' + req.headers['host'] + '": { "tls-ports": [' + serverPort + '] }}');
    return;
  }

  // for PushService tests.
  else if (u.pathname === "/pushSubscriptionSuccess/subscribe") {
    res.setHeader("Location",
                  'https://localhost:' + serverPort + '/pushSubscriptionSuccesss');
    res.setHeader("Link",
                  '</pushEndpointSuccess>; rel="urn:ietf:params:push", ' +
                  '</receiptPushEndpointSuccess>; rel="urn:ietf:params:push:receipt"');
    res.writeHead(201, "OK");
    res.end("");
    return;
  }

  else if (u.pathname === "/pushSubscriptionSuccesss") {
    // do nothing.
    return;
  }

  else if (u.pathname === "/pushSubscriptionMissingLocation/subscribe") {
    res.setHeader("Link",
                  '</pushEndpointMissingLocation>; rel="urn:ietf:params:push", ' +
                  '</receiptPushEndpointMissingLocation>; rel="urn:ietf:params:push:receipt"');
    res.writeHead(201, "OK");
    res.end("");
    return;
  }

  else if (u.pathname === "/pushSubscriptionMissingLink/subscribe") {
    res.setHeader("Location",
                  'https://localhost:' + serverPort + '/subscriptionMissingLink');
    res.writeHead(201, "OK");
    res.end("");
    return;
  }

  else if (u.pathname === "/pushSubscriptionLocationBogus/subscribe") {
    res.setHeader("Location", '1234');
    res.setHeader("Link",
                  '</pushEndpointLocationBogus; rel="urn:ietf:params:push", ' +
                  '</receiptPushEndpointLocationBogus>; rel="urn:ietf:params:push:receipt"');
    res.writeHead(201, "OK");
    res.end("");
    return;
  }

  else if (u.pathname === "/pushSubscriptionMissingLink1/subscribe") {
    res.setHeader("Location",
                  'https://localhost:' + serverPort + '/subscriptionMissingLink1');
    res.setHeader("Link",
                  '</receiptPushEndpointMissingLink1>; rel="urn:ietf:params:push:receipt"');
    res.writeHead(201, "OK");
    res.end("");
    return;
  }

  else if (u.pathname === "/pushSubscriptionMissingLink2/subscribe") {
    res.setHeader("Location",
                  'https://localhost:' + serverPort + '/subscriptionMissingLink2');
    res.setHeader("Link",
                  '</pushEndpointMissingLink2>; rel="urn:ietf:params:push"');
    res.writeHead(201, "OK");
    res.end("");
    return;
  }

  else if (u.pathname === "/subscriptionMissingLink2") {
    // do nothing.
    return;
  }

  else if (u.pathname === "/pushSubscriptionNot201Code/subscribe") {
    res.setHeader("Location",
                  'https://localhost:' + serverPort + '/subscriptionNot2xxCode');
    res.setHeader("Link",
                  '</pushEndpointNot201Code>; rel="urn:ietf:params:push", ' +
                  '</receiptPushEndpointNot201Code>; rel="urn:ietf:params:push:receipt"');
    res.writeHead(200, "OK");
    res.end("");
    return;
  }

  else if (u.pathname ==="/pushNotifications/subscription1") {
    pushPushServer1 = res.push(
      { hostname: 'localhost:' + serverPort, port: serverPort,
        path : '/pushNotificationsDeliver1', method : 'GET',
        headers: { 'Encryption-Key': 'keyid="notification1"; dh="BO_tgGm-yvYAGLeRe16AvhzaUcpYRiqgsGOlXpt0DRWDRGGdzVLGlEVJMygqAUECarLnxCiAOHTP_znkedrlWoU"',
                   'Encryption': 'keyid="notification1";salt="uAZaiXpOSfOLJxtOCZ09dA"',
                   'Content-Encoding': 'aesgcm128',
                 }
      });
    pushPushServer1.writeHead(200, {
      'subresource' : '1'
      });

    pushPushServer1.end('370aeb3963f12c4f12bf946bd0a7a9ee7d3eaff8f7aec62b530fc25cfa', 'hex');
    return;
  }

  else if (u.pathname ==="/pushNotifications/subscription2") {
    pushPushServer2 = res.push(
      { hostname: 'localhost:' + serverPort, port: serverPort,
        path : '/pushNotificationsDeliver3', method : 'GET',
        headers: { 'Encryption-Key': 'keyid="notification2"; dh="BKVdQcgfncpNyNWsGrbecX0zq3eHIlHu5XbCGmVcxPnRSbhjrA6GyBIeGdqsUL69j5Z2CvbZd-9z1UBH0akUnGQ"',
                   'Encryption': 'keyid="notification2";salt="vFn3t3M_k42zHBdpch3VRw"',
                   'Content-Encoding': 'aesgcm128',
                 }
      });
    pushPushServer2.writeHead(200, {
      'subresource' : '1'
      });

    pushPushServer2.end('66df5d11daa01e5c802ff97cdf7f39684b5bf7c6418a5cf9b609c6826c04b25e403823607ac514278a7da945', 'hex');
    return;
  }

  else if (u.pathname ==="/pushNotifications/subscription3") {
    pushPushServer3 = res.push(
      { hostname: 'localhost:' + serverPort, port: serverPort,
        path : '/pushNotificationsDeliver3', method : 'GET',
        headers: { 'Encryption-Key': 'keyid="notification3";dh="BD3xV_ACT8r6hdIYES3BJj1qhz9wyv7MBrG9vM2UCnjPzwE_YFVpkD-SGqE-BR2--0M-Yf31wctwNsO1qjBUeMg"',
                   'Encryption': 'keyid="notification3"; salt="DFq188piWU7osPBgqn4Nlg"; rs=24',
                   'Content-Encoding': 'aesgcm128',
                 }
      });
    pushPushServer3.writeHead(200, {
      'subresource' : '1'
      });

    pushPushServer3.end('2caaeedd9cf1059b80c58b6c6827da8ff7de864ac8bea6d5775892c27c005209cbf9c4de0c3fbcddb9711d74eaeebd33f7275374cb42dd48c07168bc2cc9df63e045ce2d2a2408c66088a40c', 'hex');
    return;
  }

  else if (u.pathname == "/pushNotifications/subscription4") {
    pushPushServer4 = res.push(
      { hostname: 'localhost:' + serverPort, port: serverPort,
        path : '/pushNotificationsDeliver4', method : 'GET',
        headers: { 'Crypto-Key': 'keyid="notification4";dh="BJScXUUTcs7D8jJWI1AOxSgAKkF7e56ay4Lek52TqDlWo1yGd5czaxFWfsuP4j7XNWgGYm60-LKpSUMlptxPFVQ"',
                   'Encryption': 'keyid="notification4"; salt="sn9p2QqF3V6KBclda8vx7w"',
                   'Content-Encoding': 'aesgcm',
                 }
      });
    pushPushServer4.writeHead(200, {
      'subresource' : '1'
      });

    pushPushServer4.end('9eba7ba6192544a39bd9e9b58e702d0748f1776b27f6616cdc55d29ed5a015a6db8f2dd82cd5751a14315546194ff1c18458ab91eb36c9760ccb042670001fd9964557a079553c3591ee131ceb259389cfffab3ab873f873caa6a72e87d262b8684c3260e5940b992234deebf57a9ff3a8775742f3cbcb152d249725a28326717e19cce8506813a155eff5df9bdba9e3ae8801d3cc2b7e7f2f1b6896e63d1fdda6f85df704b1a34db7b2dd63eba11ede154300a318c6f83c41a3d32356a196e36bc905b99195fd91ae4ff3f545c42d17f1fdc1d5bd2bf7516d0765e3a859fffac84f46160b79cedda589f74c25357cf6988cd8ba83867ebd86e4579c9d3b00a712c77fcea3b663007076e21f9819423faa830c2176ff1001c1690f34be26229a191a938517', 'hex');
    return;
  }

  else if ((u.pathname === "/pushNotificationsDeliver1") ||
           (u.pathname === "/pushNotificationsDeliver2") ||
           (u.pathname === "/pushNotificationsDeliver3")) {
    res.writeHead(410, "GONE");
    res.end("");
    return;
  }

  else if (u.pathname === "/illegalhpacksoft") {
    // This will cause the compressor to compress a header that is not legal,
    // but only affects the stream, not the session.
    illegalheader_conn = req.stream.connection;
    Compressor.prototype.compress = insertSoftIllegalHpack;
    // Fall through to the default response behavior
  }

  else if (u.pathname === "/illegalhpackhard") {
    // This will cause the compressor to insert an HPACK instruction that will
    // cause a session failure.
    Compressor.prototype.compress = insertHardIllegalHpack;
    // Fall through to default response behavior
  }

  else if (u.pathname === "/illegalhpack_validate") {
    if (req.stream.connection === illegalheader_conn) {
      res.setHeader('X-Did-Goaway', 'no');
    } else {
      res.setHeader('X-Did-Goaway', 'yes');
    }
    // Fall through to the default response behavior
  }

  else if (u.pathname === "/foldedheader") {
    res.setHeader('X-Folded-Header', 'this is\n folded');
    // Fall through to the default response behavior
  }

  else if (u.pathname === "/emptydata") {
    // Overwrite the original transform with our version that will insert an
    // empty DATA frame at the beginning of the stream response, then fall
    // through to the default response behavior.
    Serializer.prototype._transform = newTransform;
  }

  // for use with test_immutable.js
  else if (u.pathname === "/immutable-test-without-attribute") {
    res.setHeader('Cache-Control', 'max-age=100000');
    res.setHeader('Etag', '1');
    if (req.headers["if-none-match"]) {
      res.setHeader("x-conditional", "true");
    }
    // default response from here
  }
  else if (u.pathname === "/immutable-test-with-attribute") {
    res.setHeader('Cache-Control', 'max-age=100000, immutable');
    res.setHeader('Etag', '2');
    if (req.headers["if-none-match"]) {
      res.setHeader("x-conditional", "true");
    }
    // default response from here
  }
  else if (u.pathname === "/origin-4") {
   var originList = [ ];
   req.stream.connection.originFrame(originList);
   res.setHeader("x-client-port", req.remotePort);
  }
  else if (u.pathname === "/origin-6") {
   var originList = [ "https://alt1.example.com:" + serverPort,
                      "https://alt2.example.com:" + serverPort,
                      "https://bar.example.com:" + serverPort ];
   req.stream.connection.originFrame(originList);
   res.setHeader("x-client-port", req.remotePort);
  }
  else if (u.pathname === "/origin-11-a") {
    res.setHeader("x-client-port", req.remotePort);

    pushb = res.push(
        { hostname: 'foo.example.com:' + serverPort, port: serverPort, path : '/origin-11-b', method : 'GET',
          headers: {'x-pushed-request': 'true', 'x-foo' : 'bar'}});
    pushb.writeHead(200, {
      'pushed' : 'yes',
      'content-length' : 1
      });
    pushb.end('1');

    pushc = res.push(
        { hostname: 'bar.example.com:' + serverPort, port: serverPort, path : '/origin-11-c', method : 'GET',
          headers: {'x-pushed-request': 'true', 'x-foo' : 'bar'}});
    pushc.writeHead(200, {
      'pushed' : 'yes',
      'content-length' : 1
      });
    pushc.end('1');

    pushd = res.push(
        { hostname: 'madeup.example.com:' + serverPort, port: serverPort, path : '/origin-11-d', method : 'GET',
          headers: {'x-pushed-request': 'true', 'x-foo' : 'bar'}});
    pushd.writeHead(200, {
      'pushed' : 'yes',
      'content-length' : 1
      });
    pushd.end('1');

    pushe = res.push(
        { hostname: 'alt1.example.com:' + serverPort, port: serverPort, path : '/origin-11-e', method : 'GET',
          headers: {'x-pushed-request': 'true', 'x-foo' : 'bar'}});
    pushe.writeHead(200, {
      'pushed' : 'yes',
      'content-length' : 1
      });
    pushe.end('1');
  }
  else if (u.pathname.substring(0,8) === "/origin-") { // test_origin.js coalescing
    res.setHeader("x-client-port", req.remotePort);
  }

  else if (u.pathname === "/statusphrase") {
    // Fortunately, the node-http2 API is dumb enough to allow this right on
    // through, so we can easily test rejecting this on gecko's end.
    res.writeHead("200 OK");
    res.end(content);
    return;
  }

  else if (u.pathname === "/doublepush") {
    push1 = res.push('/doublypushed');
    push1.writeHead(200, {
      'content-type': 'text/plain',
      'pushed' : 'yes',
      'content-length' : 6,
      'X-Connection-Http2': 'yes'
    });
    push1.end('pushed');

    push2 = res.push('/doublypushed');
    push2.writeHead(200, {
      'content-type': 'text/plain',
      'pushed' : 'yes',
      'content-length' : 6,
      'X-Connection-Http2': 'yes'
    });
    push2.end('pushed');
  }

  else if (u.pathname === "/doublypushed") {
    content = 'not pushed';
  }

  else if (u.pathname === "/diskcache") {
    content = "this was pulled via h2";
  }

  else if (u.pathname === "/pushindisk") {
    var pushedContent = "this was pushed via h2";
    push = res.push('/diskcache');
    push.writeHead(200, {
      'content-type': 'text/html',
      'pushed' : 'yes',
      'content-length' : pushedContent.length,
      'X-Connection-Http2': 'yes'
    });
    push.end(pushedContent);
  }

  // For test_header_Server_Timing.js
  else if (u.pathname === "/server-timing") {
    res.setHeader('Content-Type', 'text/plain');
    res.setHeader('Content-Length', '12');
    res.setHeader('Trailer', 'Server-Timing');
    res.setHeader('Server-Timing', 'metric; dur=123.4; desc=description, metric2; dur=456.78; desc=description1');
    res.write('data reached');
    res.addTrailers({'Server-Timing': 'metric3; dur=789.11; desc=description2, metric4; dur=1112.13; desc=description3'});
    res.end();
    return;
  }

  else if (u.pathname === "/proxy-session-counter") {
    // Incremented with every newly created session on the proxy
    res.end(proxy_session_count.toString());
    return;
  }

  res.setHeader('Content-Type', 'text/html');
  if (req.httpVersionMajor != 2) {
    res.setHeader('Connection', 'close');
  }
  res.writeHead(200);
  res.end(content);
}

// Set up the SSL certs for our server - this server has a cert for foo.example.com
// signed by netwerk/tests/unit/http2-ca.pem
var options = {
  key: fs.readFileSync(__dirname + '/http2-cert.key'),
  cert: fs.readFileSync(__dirname + '/http2-cert.pem'),
};

if (process.env.HTTP2_LOG !== undefined) {
  var log_module = node_http2_root + "/test/util";
  options.log = require(log_module).createLogger('server')
}

var server = http2.createServer(options, handleRequest);

server.on('connection', function(socket) {
  socket.on('error', function() {
    // Ignoring SSL socket errors, since they usually represent a connection that was tore down
    // by the browser because of an untrusted certificate. And this happens at least once, when
    // the first test case if done.
  });
});


var proxy = http2_internal ? http2_internal.createSecureServer(options) : null;
var proxy_session_count = 0;

if (http2_internal) {
  proxy.on('session', () => {
    // Can be queried directly on the h2 server under "/proxy-session-counter"
    ++proxy_session_count;
  });

  proxy.on('stream', (stream, headers) => {
    if (headers[':method'] !== 'CONNECT') {
      // Only accept CONNECT requests
      stream.close(http2.constants.NGHTTP2_REFUSED_STREAM);
      return;
    }

    const target = headers[':authority'];

    const authorization_token = headers['proxy-authorization'];
    if ('authorization-token' != authorization_token || target == '407.example.com:443') {
      stream.respond({ ':status': 407 });
      // Deliberately send no Proxy-Authenticate header
      stream.end();
      return;
    }
    if (target == '404.example.com:443') {
      // 404 Not Found, a response code that a proxy should return when the host can't be found
      stream.respond({ ':status': 404 });
      stream.end();
      return;
    }
    if (target == '429.example.com:443') {
      // 429 Too Many Requests, a response code that a proxy should return when receiving too many requests
      stream.respond({ ':status': 429 });
      stream.end();
      return;
    }
    if (target == '502.example.com:443') {
      // 502 Bad Gateway, a response code mostly resembling immediate connection error
      stream.respond({ ':status': 502 });
      stream.end();
      return;
    }
    if (target == '504.example.com:443') {
      // 504 Gateway Timeout, did not receive a timely response from an upstream server
      stream.respond({ ':status': 504 });
      stream.end();
      return;
    }

    const socket = net.connect(serverPort, '127.0.0.1', () => {
      try {
        stream.respond({ ':status': 200 });
        socket.pipe(stream);
        stream.pipe(socket);
      } catch (exception) {
        stream.close(http2.constants.NGHTTP2_STREAM_CLOSED);
      }
    });
    socket.on('error', (error) => {
      throw `Unxpected error when conneting the HTTP/2 server from the HTTP/2 proxy during CONNECT handling: '${error}'`;
    });
  });
}

var serverPort;

const listen = (server, envport) => {
  if (!server) {
    return Promise.resolve(0);
  }

  let portSelection = 0;
  if (envport !== undefined) {
    try {
      portSelection = parseInt(envport, 10);
    } catch (e) {
      portSelection = -1;
    }
  }
  return new Promise(resolve => {
    server.listen(portSelection, "0.0.0.0", 200, () => {
      resolve(server.address().port);
    });
  });
}

Promise.all([
  listen(server, process.env.MOZHTTP2_PORT).then(port => serverPort = port),
  listen(proxy, process.env.MOZHTTP2_PROXY_PORT)
]).then(([serverPort, proxyPort]) => {
  console.log(`HTTP2 server listening on ports ${serverPort},${proxyPort}`);
});
back to top