/* 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 { spawn } = 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) {
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 =
"<!doctype html>" +
"<html>" +
"<head><title>HOORAY!</title></head>" +
// 'You Win!' used in tests to check we reached this server
"<body>You Win! (by requesting" +
pathName +
")</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() {
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);
}
},
};
var resetLater =
function () {};
resetLater.prototype = {
resp:
null,
onTimeout:
function onTimeout() {
this.resp.stream.reset(
"HTTP_1_1_REQUIRED");
},
};
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;
}
let answers = [];
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")
) {
// add padding to the response, because the client must be able ignore it
answers.push({
name:
".",
type:
"PADDING",
data: Buffer.from(
// PADDING_PADDING_PADDING
"50414444494e475f50414444494e475f50414444494e47",
"hex"
),
});
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;
}
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 =
'<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(Buffer.from([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);
let 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 (let 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");
var received_data =
false;
req.on(
"data",
function receivePostData(chunk) {
received_data =
true;
post_hash.update(chunk.toString());
});
req.on(
"end",
function finishPost() {
let md5 = received_data ? post_hash.digest(
"hex") :
"0";
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);
let rl =
new runlater();
rl.req = req;
rl.resp = res;
setTimeout(executeRunLater, 750, rl);
});
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);
let 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;
}
res.setHeader(
"X-H11Required-Stream-Ok", h11required_header);
}
else if (u.pathname ===
"/h11required_with_content") {
if (req.httpVersionMajor === 2) {
res.setHeader(
"Content-Type",
"text/plain");
res.setHeader(
"Content-Length",
"ok".length);
res.writeHead(200);
res.write(
"ok");
let resetFunc =
new resetLater();
resetFunc.resp = res;
setTimeout(executeRunLater, 1, resetFunc);
return;
}
}
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) {
if (req.httpVersionMajor != 2) {
res.setHeader(
"Connection",
"close");
}
res.writeHead(400);
res.end(
"WRONG CONNECTION, HOMIE!");
return;
}
// Clear these variables so we can run the test again with --verify
didRst =
false;
rstConnection =
null;
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 (let 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 ===
"/hugecontinuedheaders") {
for (let i = 0; i < u.query.size; i++) {
res.setHeader(
"X-Test-Header-" + i,
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".repeat(1024)
);
}
res.writeHead(200);
res.end(content);
return;
}
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_http3.js
else if (u.pathname ===
"/http3-test") {
res.setHeader(
"Cache-Control",
"no-cache");
res.setHeader(
"Alt-Svc",
"h3=" + req.headers[
"x-altsvc"]);
}
// for use with test_http3.js
else if (u.pathname ===
"/http3-test2") {
res.setHeader(
"Cache-Control",
"no-cache");
res.setHeader(
"Alt-Svc",
"h2=foo2.example.com:8000,h3=" + req.headers[
"x-altsvc"]
);
}
else if (u.pathname ===
"/http3-test3") {
res.setHeader(
"Cache-Control",
"no-cache");
res.setHeader(
"Alt-Svc",
"h3-29=" + req.headers[
"x-altsvc"] +
",h3=" + req.headers[
"x-altsvc"]
);
}
// for use with test_trr.js
else if (u.pathname ===
"/dns-cname") {
// asking for cname.example.com
function emitResponse(response, payload) {
let pcontent = createCNameContent(payload);
response.setHeader(
"Content-Type",
"application/dns-message");
response.setHeader(
"Content-Length", pcontent.length);
response.writeHead(200);
response.write(pcontent);
response.end(
"");
}
let payload = Buffer.from(
"");
req.on(
"data",
function receiveData(chunk) {
payload = Buffer.concat([payload, chunk]);
});
req.on(
"end",
function finishedData() {
emitResponse(res, payload);
});
return;
}
else if (u.pathname ==
"/get-doh-req-port-log") {
let rContent = JSON.stringify(gDoHPortsLog);
res.setHeader(
"Content-Type",
"text/plain");
res.setHeader(
"Content-Length", rContent.length);
res.writeHead(400);
res.end(rContent);
return;
}
else if (u.pathname ==
"/reset-doh-request-count") {
gDoHRequestCount = 0;
res.setHeader(
"Content-Type",
"text/plain");
res.setHeader(
"Content-Length",
"ok".length);
res.writeHead(200);
res.write(
"ok");
res.end(
"");
return;
}
else if (u.pathname ==
"/doh") {
let responseIP = u.query.responseIP;
if (!responseIP) {
responseIP =
"5.5.5.5";
}
let redirect = u.query.redirect;
if (redirect) {
responseIP = redirect;
if (u.query.dns) {
res.setHeader(
"Location",
"https://localhost:" +
serverPort +
"/doh?responseIP=" +
responseIP +
"&dns=" +
u.query.dns
);
}
else {
res.setHeader(
"Location",
"https://localhost:" + serverPort + "/doh?responseIP=" + responseIP
);
}
res.writeHead(307);
res.end(
"");
return;
}
if (u.query.auth) {
if (!handleAuth()) {
return;
}
}
if (u.query.noResponse) {
return;
}
if (u.query.push) {
// push.example.org has AAAA entry 2018::2018
let pcontent = dnsPacket.encode({
id: 0,
type:
"response",
flags: dnsPacket.RECURSION_DESIRED,
questions: [{ name:
"push.example.org", type:
"AAAA",
class:
"IN" }],
answers: [
{
name:
"push.example.org",
type:
"AAAA",
ttl: 55,
class:
"IN",
flush:
false,
data:
"2018::2018",
},
],
});
push = res.push({
hostname:
"foo.example.com:" + serverPort,
port: serverPort,
path:
"/dns-pushed-response?dns=AAAAAAABAAAAAAAABHB1c2gHZXhhbXBsZQNvcmcAABwAAQ",
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 = Buffer.from(
"");
function emitResponse(response, requestPayload, decodedPacket, delay) {
let packet = decodedPacket || dnsPacket.decode(requestPayload);
let answer = createDNSAnswer(
response,
packet,
responseIP,
requestPayload
);
if (!answer) {
return;
}
writeDNSResponse(
response,
answer,
delay || getDelayFromPacket(packet, responseType(packet, responseIP)),
"application/dns-message"
);
}
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() {
// parload is empty when we send redirect response.
if (payload.length) {
let packet = dnsPacket.decode(payload);
let delay;
if (u.query.conncycle) {
let name = packet.questions[0].name;
if (name.startsWith(
"newconn")) {
// If we haven't seen a req for this newconn name before,
// or if we've seen one for the same name on the same port,
// synthesize a timeout.
if (
!gDoHNewConnLog[name] ||
gDoHNewConnLog[name] == req.remotePort
) {
delay = 1000;
}
if (!gDoHNewConnLog[name]) {
gDoHNewConnLog[name] = req.remotePort;
}
}
gDoHPortsLog.push([packet.questions[0].name, req.remotePort]);
}
else {
gDoHPortsLog = [];
gDoHNewConnLog = {};
}
emitResponse(res, payload, packet, delay);
}
});
return;
}
else if (u.pathname ===
"/httpssvc") {
let payload = Buffer.from(
"");
req.on(
"data",
function receiveData(chunk) {
payload = Buffer.concat([payload, chunk]);
});
req.on(
"end",
function finishedData() {
let packet = dnsPacket.decode(payload);
let answers = [];
answers.push({
name: packet.questions[0].name,
type: packet.questions[0].type,
ttl: 55,
class:
"IN",
flush:
false,
data: {
priority: 1,
name:
"h3pool",
values: [
{ key:
"alpn", value: [
"h2",
"h3"] },
{ key:
"no-default-alpn" },
{ key:
"port", value: 8888 },
{ key:
"ipv4hint", value:
"1.2.3.4" },
{ key:
"echconfig", value:
"123..." },
{ key:
"ipv6hint", value:
"::1" },
{ key: 30, value:
"somelargestring" },
{ key:
"odoh", value:
"456..." },
],
},
});
answers.push({
name: packet.questions[0].name,
type: packet.questions[0].type,
ttl: 55,
class:
"IN",
flush:
false,
data: {
priority: 2,
name:
".",
values: [
{ key:
"alpn", value:
"h2" },
{ key:
"ipv4hint", value: [
"1.2.3.4",
"5.6.7.8"] },
{ key:
"echconfig", value:
"abc..." },
{ key:
"ipv6hint", value: [
"::1",
"fe80::794f:6d2c:3d5e:7836"] },
{ key:
"odoh", value:
"def..." },
],
},
});
answers.push({
name: packet.questions[0].name,
type: packet.questions[0].type,
ttl: 55,
class:
"IN",
flush:
false,
data: {
priority: 3,
name:
"hello",
values: [],
},
});
let buf = dnsPacket.encode({
type:
"response",
id: packet.id,
flags: dnsPacket.RECURSION_DESIRED,
questions: packet.questions,
answers,
});
res.setHeader(
"Content-Type",
"application/dns-message");
res.setHeader(
"Content-Length", buf.length);
res.writeHead(200);
res.write(buf);
res.end(
"");
});
return;
}
else if (u.pathname ===
"/httpssvc_as_altsvc") {
let payload = Buffer.from(
"");
req.on(
"data",
function receiveData(chunk) {
payload = Buffer.concat([payload, chunk]);
});
req.on(
"end",
function finishedData() {
let packet = dnsPacket.decode(payload);
let answers = [];
if (packet.questions[0].type ==
"HTTPS") {
let priority = 1;
// Set an invalid priority to test the case when receiving a corrupted
// response.
if (packet.questions[0].name ===
"foo.notexisted.com") {
priority = 0;
}
answers.push({
name: packet.questions[0].name,
type: packet.questions[0].type,
ttl: 55,
class:
"IN",
flush:
false,
data: {
priority,
name: packet.questions[0].name,
values: [
{ key:
"alpn", value:
"h2" },
{ key:
"port", value: serverPort },
{ key: 30, value:
"somelargestring" },
],
},
});
}
else {
answers.push({
name: packet.questions[0].name,
type:
"A",
ttl: 55,
flush:
false,
data:
"127.0.0.1",
});
}
let buf = dnsPacket.encode({
type:
"response",
id: packet.id,
flags: dnsPacket.RECURSION_DESIRED,
questions: packet.questions,
answers,
});
res.setHeader(
"Content-Type",
"application/dns-message");
res.setHeader(
"Content-Length", buf.length);
res.writeHead(200);
res.write(buf);
res.end(
"");
});
return;
}
else if (u.pathname ===
"/httpssvc_use_iphint") {
let payload = Buffer.from(
"");
req.on(
"data",
function receiveData(chunk) {
payload = Buffer.concat([payload, chunk]);
});
req.on(
"end",
function finishedData() {
let packet = dnsPacket.decode(payload);
let answers = [];
answers.push({
name: packet.questions[0].name,
type:
"HTTPS",
ttl: 55,
class:
"IN",
flush:
false,
data: {
priority: 1,
name:
".",
values: [
{ key:
"alpn", value:
"h2" },
{ key:
"port", value: serverPort },
{ key:
"ipv4hint", value:
"127.0.0.1" },
],
},
});
let buf = dnsPacket.encode({
type:
"response",
id: packet.id,
flags: dnsPacket.RECURSION_DESIRED,
questions: packet.questions,
answers,
});
res.setHeader(
"Content-Type",
"application/dns-message");
res.setHeader(
"Content-Length", buf.length);
res.writeHead(200);
res.write(buf);
res.end(
"");
});
return;
}
else if (u.pathname ===
"/dns-cname-a") {
let rContent = createCNameARecord();
res.setHeader(
"Content-Type",
"application/dns-message");
res.setHeader(
"Content-Length", rContent.length);
res.writeHead(200);
res.write(rContent);
res.end(
"");
return;
}
else if (u.pathname ===
"/websocket") {
res.setHeader(
"Upgrade",
"websocket");
res.setHeader(
"Connection",
"Upgrade");
var wshash = crypto.createHash(
"sha1");
wshash.update(req.headers[
"sec-websocket-key"]);
wshash.update(
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
let key = wshash.digest(
"base64");
res.setHeader(
"Sec-WebSocket-Accept", key);
res.writeHead(101);
res.end(
"something....");
return;
}
// for use with test_dns_by_type_resolve.js
else if (u.pathname ===
"/txt-dns-push") {
// _esni_push.example.com has A entry 127.0.0.1
let rContent = Buffer.from(
"0000010000010001000000000A5F65736E695F70757368076578616D706C6503636F6D0000010001C00C000100010000003700047F000001",
"hex"
);
// _esni_push.example.com has TXT entry 2062586B67646D39705932556761584D6762586B676347467A63336476636D513D
var pcontent = Buffer.from(
"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", rContent.length);
res.writeHead(200);
res.write(rContent);
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 + '"]');
return;
}
else if (u.pathname ===
"/stale-while-revalidate-loop-test") {
res.setHeader(
"Cache-Control",
"s-maxage=86400, stale-while-revalidate=86400, immutable"
);
res.setHeader(
"Content-Type",
"text/plain; charset=utf-8");
res.setHeader(
"X-Content-Type-Options",
"nosniff");
res.setHeader(
"Content-Length",
"1");
res.writeHead(200,
"OK");
res.end(
"1");
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;
res.setHeader(
"Content-Type",
"text/html");
res.setHeader(
"x-softillegalhpack",
"true");
res.writeHead(200);
res.end(content);
return;
}
else if (u.pathname ===
"/illegalhpackhard") {
// This will cause the compressor to insert an HPACK instruction that will
// cause a session failure.
res.setHeader(
"Content-Type",
"text/html");
res.setHeader(
"x-hardillegalhpack",
"true");
res.writeHead(200);
res.end(content);
return;
}
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 ===
"/immutable-test-expired-with-Expires-header") {
res.setHeader(
"Cache-Control",
"immutable");
res.setHeader(
"Expires",
"Mon, 01 Jan 1990 00:00:00 GMT");
res.setHeader(
"Etag",
"3");
if (req.headers[
"if-none-match"]) {
res.setHeader(
"x-conditional",
"true");
}
}
else if (
u.pathname ===
"/immutable-test-expired-with-last-modified-header"
) {
res.setHeader(
"Cache-Control",
"public, max-age=3600, immutable");
res.setHeader(
"Date",
"Mon, 01 Jan 1990 00:00:00 GMT");
res.setHeader(
"Last-modified",
"Mon, 01 Jan 1990 00:00:00 GMT");
res.setHeader(
"Etag",
"4");
if (req.headers[
"if-none-match"]) {
res.setHeader(
"x-conditional",
"true");
}
}
else if (u.pathname ===
"/origin-4") {
let originList = [];
req.stream.connection.originFrame(originList);
res.setHeader(
"x-client-port", req.remotePort);
}
else if (u.pathname ===
"/origin-6") {
let 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);
const 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");
const 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");
const 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");
const 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 ===
"/redirect_to_http") {
res.setHeader(
"Location",
`http:
//test.httpsrr.redirect.com:${u.query.port}/redirect_to_http?port=${u.query.port}`
);
res.writeHead(307);
res.end(
"");
return;
}
else if (u.pathname ===
"/103_response") {
let link_val = req.headers[
"link-to-set"];
if (link_val) {
res.setHeader(
"link", link_val);
}
res.setHeader(
"something",
"something");
res.writeHead(103);
res.setHeader(
"Content-Type",
"text/plain");
res.setHeader(
"Content-Length",
"12");
res.writeHead(200);
res.write(
"data reached");
res.end();
return;
}
else if (u.pathname.startsWith(
"/invalid_response_header/")) {
// response headers with invalid characters in the name / value (RFC7540 Sec 10.3)
let kind = u.pathname.slice(
"/invalid_response_header/".length);
if (kind ===
"name_spaces") {
res.setHeader(
"With Spaces",
"Hello");
}
else if (kind ===
"value_line_feed") {
res.setHeader(
"invalid-header",
"line\nfeed");
}
else if (kind ===
"value_carriage_return") {
res.setHeader(
"invalid-header",
"carriage\rreturn");
}
else if (kind ===
"value_null") {
res.setHeader(
"invalid-header",
"null\0");
}
res.writeHead(200);
res.end(
"");
return;
}
else if (u.pathname ===
"/origin_header") {
let originHeader = req.headers.origin;
res.setHeader(
"Content-Length", originHeader.length);
res.setHeader(
"Content-Type",
"text/plain");
res.writeHead(200);
res.write(originHeader);
res.end();
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.
});
});
server.on(
"connect",
function (req, clientSocket) {
clientSocket.write(
"HTTP/1.1 404 Not Found\r\nProxy-agent: Node.js-Proxy\r\n\r\n"
);
clientSocket.destroy();
});
function makeid(length) {
var result =
"";
var characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var charactersLength = characters.length;
for (
var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
let globalObjects = {};
var serverPort;
const listen = (serv, envport) => {
if (!serv) {
return Promise.resolve(0);
}
let portSelection = 0;
if (envport !== undefined) {
try {
portSelection = parseInt(envport, 10);
}
catch (e) {
portSelection = -1;
}
}
return new Promise(resolve => {
serv.listen(portSelection,
"0.0.0.0", 2000, () => {
resolve(serv.address().port);
});
});
};
const http = require(
"http");
let httpServer = http.createServer((req, res) => {
if (req.method !=
"POST") {
let u = url.parse(req.url,
true);
if (u.pathname ==
"/test") {
// This path is used to test that the server is working properly
res.writeHead(200);
res.end(
"OK");
return;
}
res.writeHead(405);
res.end(
"Unexpected method: " + req.method);
return;
}
let code =
"";
req.on(
"data",
function receivePostData(chunk) {
code += chunk;
});
req.on(
"end",
function finishPost() {
let u = url.parse(req.url,
true);
if (u.pathname ==
"/fork") {
let id = forkProcess();
computeAndSendBackResponse(id);
return;
}
if (u.pathname ==
"/forkH3Server") {
forkH3Server(u.query.path, u.query.dbPath)
.then(result => {
computeAndSendBackResponse(result);
})
.
catch(error => {
computeAndSendBackResponse(error);
});
return;
}
if (u.pathname.startsWith(
"/kill/")) {
let id = u.pathname.slice(6);
let forked = globalObjects[id];
if (!forked) {
computeAndSendBackResponse(undefined,
new Error(
"could not find id"));
return;
}
new Promise((resolve, reject) => {
forked.resolve = resolve;
forked.reject = reject;
forked.kill();
})
.then(x =>
computeAndSendBackResponse(
undefined,
new Error(`incorrectly resolved ${x}`)
)
)
.
catch(e => {
// We indicate a proper shutdown by resolving with undefined.
if (e && e.toString().match(/child process exit closing code/)) {
e = undefined;
}
computeAndSendBackResponse(undefined, e);
});
return;
}
if (u.pathname.startsWith(
"/execute/")) {
let id = u.pathname.slice(9);
let forked = globalObjects[id];
if (!forked) {
computeAndSendBackResponse(undefined,
new Error(
"could not find id"));
return;
}
new Promise((resolve, reject) => {
forked.resolve = resolve;
forked.reject = reject;
forked.send({ code });
})
.then(x => sendBackResponse(x))
.
catch(e => computeAndSendBackResponse(undefined, e));
}
function computeAndSendBackResponse(evalResult, e) {
let output = { result: evalResult, error:
"", errorStack:
"" };
if (e) {
output.error = e.toString();
output.errorStack = e.stack;
}
sendBackResponse(output);
}
function sendBackResponse(output) {
output = JSON.stringify(output);
res.setHeader(
"Content-Length", output.length);
res.setHeader(
"Content-Type",
"application/json");
res.writeHead(200);
res.write(output);
res.end(
"");
}
});
});
function forkH3Server(serverPath, dbPath) {
const args = [dbPath];
let process = spawn(serverPath, args);
let id = forkProcessInternal(process);
// Return a promise that resolves when we receive data from stdout
return new Promise((resolve, _) => {
process.stdout.on(
"data", data => {
console.log(data.toString());
resolve({ id, output: data.toString().trim() });
});
});
}
function forkProcess() {
let scriptPath = path.resolve(__dirname,
"moz-http2-child.js");
let forked = fork(scriptPath);
return forkProcessInternal(forked);
}
function forkProcessInternal(forked) {
let id = makeid(6);
forked.errors =
"";
globalObjects[id] = forked;
forked.on(
"message", msg => {
if (forked.resolve) {
forked.resolve(msg);
forked.resolve =
null;
}
else {
console.log(
`forked process without handler sent: ${JSON.stringify(msg)}`
);
forked.errors += `forked process without handler sent: ${JSON.stringify(
msg
)}\n`;
}
});
let exitFunction = (code, signal) => {
if (globalObjects[id]) {
delete globalObjects[id];
}
else {
// already called
return;
}
if (!forked.reject) {
console.log(
`child process ${id} closing code: ${code} signal: ${signal}`
);
return;
}
if (forked.errors !=
"") {
forked.reject(forked.errors);
forked.errors =
"";
forked.reject =
null;
return;
}
forked.reject(`child process exit closing code: ${code} signal: ${signal}`);
forked.reject =
null;
};
forked.on(
"error", exitFunction);
forked.on(
"close", exitFunction);
forked.on(
"exit", exitFunction);
return id;
}
Promise.all([
listen(server, process.env.MOZHTTP2_PORT).then(port => (serverPort = port)),
listen(httpServer, process.env.MOZNODE_EXEC_PORT),
]).then(([sPort, nodeExecPort]) => {
console.log(`HTTP2 server listening on ports ${sPort},${nodeExecPort}`);
});