Deflake http2 tests (#10682)

This commit is contained in:
Jarred Sumner
2024-04-30 09:50:30 -07:00
committed by GitHub
parent 18261046ee
commit efd4e15f4c
5 changed files with 334 additions and 215 deletions

View File

@@ -0,0 +1,36 @@
const path = require("path");
module.exports.TLS_CERT = require("./tls-cert.cjs").TLS_CERT;
module.exports.TLS_OPTIONS = require("./tls-cert.cjs").TLS_OPTIONS;
const nodeExecutable = typeof Bun !== "undefined" ? Bun.which("node") : "node";
exports.nodeEchoServer = async function nodeEchoServer() {
if (!nodeExecutable) throw new Error("node executable not found");
const subprocess = require("child_process").spawn(
nodeExecutable,
[path.join(__dirname, "node-echo-server.fixture.js")],
{
stdout: "pipe",
stderr: "inherit",
stdin: "inherit",
},
);
const { promise, resolve, reject } = Promise.withResolvers();
subprocess.unref();
subprocess.stdout.setEncoding("utf8");
var data = "";
function readData(chunk) {
data += chunk;
try {
const address = JSON.parse(data);
const url = `https://${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`;
subprocess.stdout.off("data", readData);
resolve({ address, url, subprocess });
} catch (e) {
console.error(e);
}
}
subprocess.stdout.on("data", readData);
return await promise;
};

View File

@@ -1,36 +1,79 @@
const http2 = require("http2");
const TLS_CERT = {
key: "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+7odzr3yIYewR\nNRGIubF5hzT7Bym2dDab4yhaKf5drL+rcA0J15BM8QJ9iSmL1ovg7x35Q2MBKw3r\nl/Yyy3aJS8whZTUze522El72iZbdNbS+oH6GxB2gcZB6hmUehPjHIUH4icwPdwVU\neR6fB7vkfDddLXe0Tb4qsO1EK8H0mr5PiQSXfj39Yc1QHY7/gZ/xeSrt/6yn0oH9\nHbjF2XLSL2j6cQPKEayartHN0SwzwLi0eWSzcziVPSQV7c6Lg9UuIHbKlgOFzDpc\np1p1lRqv2yrT25im/dS6oy9XX+p7EfZxqeqpXX2fr5WKxgnzxI3sW93PG8FUIDHt\nnUsoHX3RAgMBAAECggEAAckMqkn+ER3c7YMsKRLc5bUE9ELe+ftUwfA6G+oXVorn\nE+uWCXGdNqI+TOZkQpurQBWn9IzTwv19QY+H740cxo0ozZVSPE4v4czIilv9XlVw\n3YCNa2uMxeqp76WMbz1xEhaFEgn6ASTVf3hxYJYKM0ljhPX8Vb8wWwlLONxr4w4X\nOnQAB5QE7i7LVRsQIpWKnGsALePeQjzhzUZDhz0UnTyGU6GfC+V+hN3RkC34A8oK\njR3/Wsjahev0Rpb+9Pbu3SgTrZTtQ+srlRrEsDG0wVqxkIk9ueSMOHlEtQ7zYZsk\nlX59Bb8LHNGQD5o+H1EDaC6OCsgzUAAJtDRZsPiZEQKBgQDs+YtVsc9RDMoC0x2y\nlVnP6IUDXt+2UXndZfJI3YS+wsfxiEkgK7G3AhjgB+C+DKEJzptVxP+212hHnXgr\n1gfW/x4g7OWBu4IxFmZ2J/Ojor+prhHJdCvD0VqnMzauzqLTe92aexiexXQGm+WW\nwRl3YZLmkft3rzs3ZPhc1G2X9QKBgQDOQq3rrxcvxSYaDZAb+6B/H7ZE4natMCiz\nLx/cWT8n+/CrJI2v3kDfdPl9yyXIOGrsqFgR3uhiUJnz+oeZFFHfYpslb8KvimHx\nKI+qcVDcprmYyXj2Lrf3fvj4pKorc+8TgOBDUpXIFhFDyM+0DmHLfq+7UqvjU9Hs\nkjER7baQ7QKBgQDTh508jU/FxWi9RL4Jnw9gaunwrEt9bxUc79dp+3J25V+c1k6Q\nDPDBr3mM4PtYKeXF30sBMKwiBf3rj0CpwI+W9ntqYIwtVbdNIfWsGtV8h9YWHG98\nJ9q5HLOS9EAnogPuS27walj7wL1k+NvjydJ1of+DGWQi3aQ6OkMIegap0QKBgBlR\nzCHLa5A8plG6an9U4z3Xubs5BZJ6//QHC+Uzu3IAFmob4Zy+Lr5/kITlpCyw6EdG\n3xDKiUJQXKW7kluzR92hMCRnVMHRvfYpoYEtydxcRxo/WS73SzQBjTSQmicdYzLE\ntkLtZ1+ZfeMRSpXy0gR198KKAnm0d2eQBqAJy0h9AoGBAM80zkd+LehBKq87Zoh7\ndtREVWslRD1C5HvFcAxYxBybcKzVpL89jIRGKB8SoZkF7edzhqvVzAMP0FFsEgCh\naClYGtO+uo+B91+5v2CCqowRJUGfbFOtCuSPR7+B3LDK8pkjK2SQ0mFPUfRA5z0z\nNVWtC0EYNBTRkqhYtqr3ZpUc\n-----END PRIVATE KEY-----\n",
cert: "-----BEGIN CERTIFICATE-----\nMIIDrzCCApegAwIBAgIUHaenuNcUAu0tjDZGpc7fK4EX78gwDQYJKoZIhvcNAQEL\nBQAwaTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh\nbmNpc2NvMQ0wCwYDVQQKDARPdmVuMREwDwYDVQQLDAhUZWFtIEJ1bjETMBEGA1UE\nAwwKc2VydmVyLWJ1bjAeFw0yMzA5MDYyMzI3MzRaFw0yNTA5MDUyMzI3MzRaMGkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNj\nbzENMAsGA1UECgwET3ZlbjERMA8GA1UECwwIVGVhbSBCdW4xEzARBgNVBAMMCnNl\ncnZlci1idW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+7odzr3yI\nYewRNRGIubF5hzT7Bym2dDab4yhaKf5drL+rcA0J15BM8QJ9iSmL1ovg7x35Q2MB\nKw3rl/Yyy3aJS8whZTUze522El72iZbdNbS+oH6GxB2gcZB6hmUehPjHIUH4icwP\ndwVUeR6fB7vkfDddLXe0Tb4qsO1EK8H0mr5PiQSXfj39Yc1QHY7/gZ/xeSrt/6yn\n0oH9HbjF2XLSL2j6cQPKEayartHN0SwzwLi0eWSzcziVPSQV7c6Lg9UuIHbKlgOF\nzDpcp1p1lRqv2yrT25im/dS6oy9XX+p7EfZxqeqpXX2fr5WKxgnzxI3sW93PG8FU\nIDHtnUsoHX3RAgMBAAGjTzBNMCwGA1UdEQQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQ\nAAAAAAAAAAAAAAAAAAAAATAdBgNVHQ4EFgQUF3y/su4J/8ScpK+rM2LwTct6EQow\nDQYJKoZIhvcNAQELBQADggEBAGWGWp59Bmrk3Gt0bidFLEbvlOgGPWCT9ZrJUjgc\nhY44E+/t4gIBdoKOSwxo1tjtz7WsC2IYReLTXh1vTsgEitk0Bf4y7P40+pBwwZwK\naeIF9+PC6ZoAkXGFRoyEalaPVQDBg/DPOMRG9OH0lKfen9OGkZxmmjRLJzbyfAhU\noI/hExIjV8vehcvaJXmkfybJDYOYkN4BCNqPQHNf87ZNdFCb9Zgxwp/Ou+47J5k4\n5plQ+K7trfKXG3ABMbOJXNt1b0sH8jnpAsyHY4DLEQqxKYADbXsr3YX/yy6c0eOo\nX2bHGD1+zGsb7lGyNyoZrCZ0233glrEM4UxmvldBcWwOWfk=\n-----END CERTIFICATE-----\n",
};
const fs = require("fs");
const { TLS_CERT, TLS_OPTIONS } = require("./tls-cert.cjs");
const server = http2.createSecureServer({
...TLS_CERT,
ca: TLS_CERT.cert,
rejectUnauthorized: false,
});
const setCookie = ["a=b", "c=d; Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly", "e=f"];
server.on("stream", (stream, headers, flags) => {
stream.respond({
"content-type": "text/html",
":status": 200,
"set-cookie": setCookie,
});
// errors here are not useful the test should handle on the client side
stream.on("error", err => console.error(err));
if (headers["x-wait-trailer"]) {
const response = { headers, flags };
stream.respond({
"content-type": "text/html",
":status": 200,
"set-cookie": setCookie,
});
stream.on("trailers", (headers, flags) => {
stream.end(JSON.stringify({ ...response, trailers: headers }));
});
} else if (headers["x-no-echo"]) {
let byteLength = 0;
stream.on("data", chunk => {
byteLength += chunk.length;
});
stream.respond({
"content-type": "application/json",
":status": 200,
});
stream.on("end", () => {
stream.end(JSON.stringify(byteLength));
});
} else {
stream.end(JSON.stringify({ headers, flags }));
// Store the request information, excluding pseudo-headers in the header echo
const requestData = {
method: headers[":method"],
path: headers[":path"],
headers: Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])),
body: [],
url: `${baseurl}${headers[":path"]}`,
};
// Collect data from the stream
stream.on("data", chunk => {
requestData.body.push(chunk);
});
// Once all data is received, echo it back
stream.on("end", () => {
if (requestData.body.length > 0) {
requestData.data = Buffer.concat(requestData.body).toString();
try {
requestData.json = JSON.parse(requestData.data); // Convert buffer array to string
} catch (e) {}
}
stream.respond({
"content-type": "application/json",
":status": 200,
// Set security and cache-control headers
"cache-control": "no-store",
"x-content-type-options": "nosniff",
"set-cookie": setCookie,
});
stream.end(JSON.stringify(requestData));
});
}
});
let baseurl = "https://localhost:";
server.listen(0, "localhost");
server.on("listening", () => {
process.stdout.write(JSON.stringify(server.address()));
const { port, address, family } = server.address();
baseurl = `https://localhost:${port}`;
process.stdout.write(JSON.stringify({ port, address: "localhost", family: "IPv4" }));
});

View File

@@ -1,107 +1,141 @@
import { heapStats } from "bun:jsc";
import http2 from "http2";
import path from "path";
// This file is meant to be able to run in node and bun
const http2 = require("http2");
const { TLS_OPTIONS, nodeEchoServer } = require("./http2-helpers.cjs");
function getHeapStats() {
return heapStats().objectTypeCounts;
if (globalThis.Bun) {
const heapStats = require("bun:jsc").heapStats;
return heapStats().objectTypeCounts;
} else {
return {
objectTypeCounts: {
H2FrameParser: 0,
TLSSocket: 0,
},
};
}
}
const gc = globalThis.gc || globalThis.Bun?.gc || (() => {});
const sleep = dur => new Promise(resolve => setTimeout(resolve, dur));
const nodeExecutable = Bun.which("node");
if (!nodeExecutable) {
console.log("No node executable found");
process.exit(99); // 99 no node executable
}
async function nodeEchoServer() {
const subprocess = Bun.spawn([nodeExecutable, path.join(import.meta.dir, "node-echo-server.fixture.js")], {
stdout: "pipe",
});
const reader = subprocess.stdout.getReader();
const data = await reader.read();
const decoder = new TextDecoder("utf-8");
const address = JSON.parse(decoder.decode(data.value));
const url = `https://${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`;
return { address, url, subprocess };
}
// X iterations should be enough to detect a leak
const ITERATIONS = 50;
const ITERATIONS = 20;
// lets send a bigish payload
const PAYLOAD = Buffer.from("a".repeat(128 * 1024));
const PAYLOAD = Buffer.from("BUN".repeat((1024 * 128) / 3));
const MULTIPLEX = 50;
const info = await nodeEchoServer();
async function main() {
let info;
let tls;
async function runRequests(iterations) {
for (let j = 0; j < iterations; j++) {
let client = http2.connect(info.url, { rejectUnauthorized: false });
let promises = [];
// 100 multiplex POST connections per iteration
for (let i = 0; i < 100; i++) {
const { promise, resolve, reject } = Promise.withResolvers();
const req = client.request({ ":path": "/post", ":method": "POST" });
let got_response = false;
req.on("response", () => {
got_response = true;
});
if (process.env.HTTP2_SERVER_INFO) {
info = JSON.parse(process.env.HTTP2_SERVER_INFO);
} else {
info = await nodeEchoServer();
console.log("Starting server", info.url);
}
req.setEncoding("utf8");
req.on("end", () => {
if (got_response) {
resolve();
} else {
reject(new Error("no response"));
}
});
req.write(PAYLOAD);
req.end();
promises.push(promise);
if (process.env.HTTP2_SERVER_TLS) {
tls = JSON.parse(process.env.HTTP2_SERVER_TLS);
} else {
tls = TLS_OPTIONS;
}
async function runRequests(iterations) {
for (let j = 0; j < iterations; j++) {
let client = http2.connect(info.url, tls);
let promises = [];
for (let i = 0; i < MULTIPLEX; i++) {
const { promise, resolve, reject } = Promise.withResolvers();
const req = client.request({ ":path": "/post", ":method": "POST", "x-no-echo": "1" });
req.setEncoding("utf8");
req.on("response", (headers, flags) => {
req.on("data", chunk => {
if (JSON.parse(chunk) !== PAYLOAD.length) {
console.log("Got wrong data", chunk);
reject(new Error("wrong data"));
return;
}
resolve();
});
});
req.end(PAYLOAD, err => {
if (err) reject(err);
});
promises.push(promise);
}
try {
await Promise.all(promises);
} catch (e) {
console.log(e);
}
try {
client.close();
} catch (e) {
console.log(e);
}
client = null;
promises = null;
gc(true);
}
await Promise.all(promises);
client.close();
client = null;
promises = null;
Bun.gc(true);
}
try {
const startStats = getHeapStats();
// warm up
await runRequests(ITERATIONS);
await sleep(10);
gc(true);
// take a baseline
const baseline = process.memoryUsage.rss();
console.error("Initial memory usage", (baseline / 1024 / 1024) | 0, "MB");
// run requests
await runRequests(ITERATIONS);
await sleep(10);
gc(true);
// take an end snapshot
const end = process.memoryUsage.rss();
const delta = end - baseline;
const deltaMegaBytes = (delta / 1024 / 1024) | 0;
console.error("Memory delta", deltaMegaBytes, "MB");
// we executed 100 requests per iteration, memory usage should not go up by 10 MB
if (deltaMegaBytes > 20) {
console.log("Too many bodies leaked", deltaMegaBytes);
process.exit(1);
}
const endStats = getHeapStats();
info?.subprocess?.kill?.();
// check for H2FrameParser leaks
const pendingH2Parsers = (endStats.H2FrameParser || 0) - (startStats.H2FrameParser || 0);
if (pendingH2Parsers > 5) {
console.log("Too many pending H2FrameParsers", pendingH2Parsers);
process.exit(pendingH2Parsers);
}
// check for TLSSocket leaks
const pendingTLSSockets = (endStats.TLSSocket || 0) - (startStats.TLSSocket || 0);
if (pendingTLSSockets > 5) {
console.log("Too many pending TLSSockets", pendingTLSSockets);
process.exit(pendingTLSSockets);
}
process.exit(0);
} catch (err) {
console.log(err);
info?.subprocess?.kill?.();
process.exit(99); // 99 exception
}
}
try {
const startStats = getHeapStats();
// warm up
await runRequests(ITERATIONS);
await Bun.sleep(10);
Bun.gc(true);
// take a baseline
const baseline = process.memoryUsage.rss();
// run requests
await runRequests(ITERATIONS);
await Bun.sleep(10);
Bun.gc(true);
// take an end snapshot
const end = process.memoryUsage.rss();
const delta = end - baseline;
const bodiesLeaked = delta / PAYLOAD.length;
// we executed 10 requests per iteration
if (bodiesLeaked > ITERATIONS) {
console.log("Too many bodies leaked", bodiesLeaked);
process.exit(1);
}
const endStats = getHeapStats();
info.subprocess.kill();
// check for H2FrameParser leaks
const pendingH2Parsers = (endStats.H2FrameParser || 0) - (startStats.H2FrameParser || 0);
if (pendingH2Parsers > 5) {
console.log("Too many pending H2FrameParsers", pendingH2Parsers);
process.exit(pendingH2Parsers);
}
// check for TLSSocket leaks
const pendingTLSSockets = (endStats.TLSSocket || 0) - (startStats.TLSSocket || 0);
if (pendingTLSSockets > 5) {
console.log("Too many pending TLSSockets", pendingTLSSockets);
process.exit(pendingTLSSockets);
}
process.exit(0);
} catch (err) {
console.log(err);
info.subprocess.kill();
process.exit(99); // 99 exception
}
main().then(
() => {},
err => {
console.error(err);
process.exit(99);
},
);

View File

@@ -8,25 +8,21 @@ import fs from "node:fs";
import { bunExe, bunEnv } from "harness";
import { tmpdir } from "node:os";
import http2utils from "./helpers";
const TLS_CERT = {
key: "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+7odzr3yIYewR\nNRGIubF5hzT7Bym2dDab4yhaKf5drL+rcA0J15BM8QJ9iSmL1ovg7x35Q2MBKw3r\nl/Yyy3aJS8whZTUze522El72iZbdNbS+oH6GxB2gcZB6hmUehPjHIUH4icwPdwVU\neR6fB7vkfDddLXe0Tb4qsO1EK8H0mr5PiQSXfj39Yc1QHY7/gZ/xeSrt/6yn0oH9\nHbjF2XLSL2j6cQPKEayartHN0SwzwLi0eWSzcziVPSQV7c6Lg9UuIHbKlgOFzDpc\np1p1lRqv2yrT25im/dS6oy9XX+p7EfZxqeqpXX2fr5WKxgnzxI3sW93PG8FUIDHt\nnUsoHX3RAgMBAAECggEAAckMqkn+ER3c7YMsKRLc5bUE9ELe+ftUwfA6G+oXVorn\nE+uWCXGdNqI+TOZkQpurQBWn9IzTwv19QY+H740cxo0ozZVSPE4v4czIilv9XlVw\n3YCNa2uMxeqp76WMbz1xEhaFEgn6ASTVf3hxYJYKM0ljhPX8Vb8wWwlLONxr4w4X\nOnQAB5QE7i7LVRsQIpWKnGsALePeQjzhzUZDhz0UnTyGU6GfC+V+hN3RkC34A8oK\njR3/Wsjahev0Rpb+9Pbu3SgTrZTtQ+srlRrEsDG0wVqxkIk9ueSMOHlEtQ7zYZsk\nlX59Bb8LHNGQD5o+H1EDaC6OCsgzUAAJtDRZsPiZEQKBgQDs+YtVsc9RDMoC0x2y\nlVnP6IUDXt+2UXndZfJI3YS+wsfxiEkgK7G3AhjgB+C+DKEJzptVxP+212hHnXgr\n1gfW/x4g7OWBu4IxFmZ2J/Ojor+prhHJdCvD0VqnMzauzqLTe92aexiexXQGm+WW\nwRl3YZLmkft3rzs3ZPhc1G2X9QKBgQDOQq3rrxcvxSYaDZAb+6B/H7ZE4natMCiz\nLx/cWT8n+/CrJI2v3kDfdPl9yyXIOGrsqFgR3uhiUJnz+oeZFFHfYpslb8KvimHx\nKI+qcVDcprmYyXj2Lrf3fvj4pKorc+8TgOBDUpXIFhFDyM+0DmHLfq+7UqvjU9Hs\nkjER7baQ7QKBgQDTh508jU/FxWi9RL4Jnw9gaunwrEt9bxUc79dp+3J25V+c1k6Q\nDPDBr3mM4PtYKeXF30sBMKwiBf3rj0CpwI+W9ntqYIwtVbdNIfWsGtV8h9YWHG98\nJ9q5HLOS9EAnogPuS27walj7wL1k+NvjydJ1of+DGWQi3aQ6OkMIegap0QKBgBlR\nzCHLa5A8plG6an9U4z3Xubs5BZJ6//QHC+Uzu3IAFmob4Zy+Lr5/kITlpCyw6EdG\n3xDKiUJQXKW7kluzR92hMCRnVMHRvfYpoYEtydxcRxo/WS73SzQBjTSQmicdYzLE\ntkLtZ1+ZfeMRSpXy0gR198KKAnm0d2eQBqAJy0h9AoGBAM80zkd+LehBKq87Zoh7\ndtREVWslRD1C5HvFcAxYxBybcKzVpL89jIRGKB8SoZkF7edzhqvVzAMP0FFsEgCh\naClYGtO+uo+B91+5v2CCqowRJUGfbFOtCuSPR7+B3LDK8pkjK2SQ0mFPUfRA5z0z\nNVWtC0EYNBTRkqhYtqr3ZpUc\n-----END PRIVATE KEY-----\n",
cert: "-----BEGIN CERTIFICATE-----\nMIIDrzCCApegAwIBAgIUHaenuNcUAu0tjDZGpc7fK4EX78gwDQYJKoZIhvcNAQEL\nBQAwaTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh\nbmNpc2NvMQ0wCwYDVQQKDARPdmVuMREwDwYDVQQLDAhUZWFtIEJ1bjETMBEGA1UE\nAwwKc2VydmVyLWJ1bjAeFw0yMzA5MDYyMzI3MzRaFw0yNTA5MDUyMzI3MzRaMGkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNj\nbzENMAsGA1UECgwET3ZlbjERMA8GA1UECwwIVGVhbSBCdW4xEzARBgNVBAMMCnNl\ncnZlci1idW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+7odzr3yI\nYewRNRGIubF5hzT7Bym2dDab4yhaKf5drL+rcA0J15BM8QJ9iSmL1ovg7x35Q2MB\nKw3rl/Yyy3aJS8whZTUze522El72iZbdNbS+oH6GxB2gcZB6hmUehPjHIUH4icwP\ndwVUeR6fB7vkfDddLXe0Tb4qsO1EK8H0mr5PiQSXfj39Yc1QHY7/gZ/xeSrt/6yn\n0oH9HbjF2XLSL2j6cQPKEayartHN0SwzwLi0eWSzcziVPSQV7c6Lg9UuIHbKlgOF\nzDpcp1p1lRqv2yrT25im/dS6oy9XX+p7EfZxqeqpXX2fr5WKxgnzxI3sW93PG8FU\nIDHtnUsoHX3RAgMBAAGjTzBNMCwGA1UdEQQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQ\nAAAAAAAAAAAAAAAAAAAAATAdBgNVHQ4EFgQUF3y/su4J/8ScpK+rM2LwTct6EQow\nDQYJKoZIhvcNAQELBQADggEBAGWGWp59Bmrk3Gt0bidFLEbvlOgGPWCT9ZrJUjgc\nhY44E+/t4gIBdoKOSwxo1tjtz7WsC2IYReLTXh1vTsgEitk0Bf4y7P40+pBwwZwK\naeIF9+PC6ZoAkXGFRoyEalaPVQDBg/DPOMRG9OH0lKfen9OGkZxmmjRLJzbyfAhU\noI/hExIjV8vehcvaJXmkfybJDYOYkN4BCNqPQHNf87ZNdFCb9Zgxwp/Ou+47J5k4\n5plQ+K7trfKXG3ABMbOJXNt1b0sH8jnpAsyHY4DLEQqxKYADbXsr3YX/yy6c0eOo\nX2bHGD1+zGsb7lGyNyoZrCZ0233glrEM4UxmvldBcWwOWfk=\n-----END CERTIFICATE-----\n",
};
import { afterAll, describe, beforeAll, it, test, expect } from "vitest";
import { TLS_OPTIONS, nodeEchoServer, TLS_CERT } from "./http2-helpers";
const nodeExecutable = which("node");
async function nodeEchoServer() {
if (!nodeExecutable) throw new Error("node executable not found");
const subprocess = Bun.spawn([nodeExecutable, path.join(import.meta.dir, "node-echo-server.fixture.js")], {
stdout: "pipe",
});
const reader = subprocess.stdout.getReader();
const data = await reader.read();
const decoder = new TextDecoder("utf-8");
const address = JSON.parse(decoder.decode(data.value));
const url = `https://${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`;
return { address, url, subprocess };
}
let nodeEchoServer_;
let HTTPS_SERVER;
beforeAll(async () => {
nodeEchoServer_ = await nodeEchoServer();
HTTPS_SERVER = nodeEchoServer_.url;
});
afterAll(async () => {
nodeEchoServer_.subprocess?.kill?.(9);
});
async function nodeDynamicServer(test_name, code) {
if (!nodeExecutable) throw new Error("node executable not found");
@@ -47,7 +43,10 @@ server.on("listening", () => {
const subprocess = Bun.spawn([nodeExecutable, file_name], {
stdout: "pipe",
stdin: "inherit",
stderr: "inherit",
});
subprocess.unref();
const reader = subprocess.stdout.getReader();
const data = await reader.read();
const decoder = new TextDecoder("utf-8");
@@ -58,6 +57,9 @@ server.on("listening", () => {
function doHttp2Request(url, headers, payload, options, request_options) {
const { promise, resolve, reject: promiseReject } = Promise.withResolvers();
if (url.startsWith(HTTPS_SERVER)) {
options = { ...(options || {}), rejectUnauthorized: true, ...TLS_OPTIONS };
}
const client = options ? http2.connect(url, options) : http2.connect(url);
client.on("error", promiseReject);
@@ -93,8 +95,7 @@ function doHttp2Request(url, headers, payload, options, request_options) {
function doMultiplexHttp2Request(url, requests) {
const { promise, resolve, reject: promiseReject } = Promise.withResolvers();
const client = http2.connect(url);
const client = http2.connect(url, TLS_OPTIONS);
client.on("error", promiseReject);
function reject(err) {
@@ -139,30 +140,30 @@ function doMultiplexHttp2Request(url, requests) {
describe("Client Basics", () => {
// we dont support server yet but we support client
it("should be able to send a GET request", async () => {
const result = await doHttp2Request("https://httpbin.org", { ":path": "/get", "test-header": "test-value" });
const result = await doHttp2Request(HTTPS_SERVER, { ":path": "/get", "test-header": "test-value" });
let parsed;
expect(() => (parsed = JSON.parse(result.data))).not.toThrow();
expect(parsed.url).toBe("https://httpbin.org/get");
expect(parsed.headers["Test-Header"]).toBe("test-value");
expect(parsed.url).toBe(`${HTTPS_SERVER}/get`);
expect(parsed.headers["test-header"]).toBe("test-value");
});
it("should be able to send a POST request", async () => {
const payload = JSON.stringify({ "hello": "bun" });
const result = await doHttp2Request(
"https://httpbin.org",
HTTPS_SERVER,
{ ":path": "/post", "test-header": "test-value", ":method": "POST" },
payload,
);
let parsed;
expect(() => (parsed = JSON.parse(result.data))).not.toThrow();
expect(parsed.url).toBe("https://httpbin.org/post");
expect(parsed.headers["Test-Header"]).toBe("test-value");
expect(parsed.url).toBe(`${HTTPS_SERVER}/post`);
expect(parsed.headers["test-header"]).toBe("test-value");
expect(parsed.json).toEqual({ "hello": "bun" });
expect(parsed.data).toEqual(payload);
});
it("should be able to send data using end", async () => {
const payload = JSON.stringify({ "hello": "bun" });
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect("https://httpbin.org");
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
client.on("error", reject);
const req = client.request({ ":path": "/post", "test-header": "test-value", ":method": "POST" });
let response_headers = null;
@@ -182,13 +183,13 @@ describe("Client Basics", () => {
const result = await promise;
let parsed;
expect(() => (parsed = JSON.parse(result.data))).not.toThrow();
expect(parsed.url).toBe("https://httpbin.org/post");
expect(parsed.headers["Test-Header"]).toBe("test-value");
expect(parsed.url).toBe(`${HTTPS_SERVER}/post`);
expect(parsed.headers["test-header"]).toBe("test-value");
expect(parsed.json).toEqual({ "hello": "bun" });
expect(parsed.data).toEqual(payload);
});
it("should be able to mutiplex GET requests", async () => {
const results = await doMultiplexHttp2Request("https://httpbin.org", [
const results = await doMultiplexHttp2Request(HTTPS_SERVER, [
{ headers: { ":path": "/get" } },
{ headers: { ":path": "/get" } },
{ headers: { ":path": "/get" } },
@@ -199,11 +200,11 @@ describe("Client Basics", () => {
for (let i = 0; i < results.length; i++) {
let parsed;
expect(() => (parsed = JSON.parse(results[i].data))).not.toThrow();
expect(parsed.url).toBe("https://httpbin.org/get");
expect(parsed.url).toBe(`${HTTPS_SERVER}/get`);
}
});
it("should be able to mutiplex POST requests", async () => {
const results = await doMultiplexHttp2Request("https://httpbin.org", [
const results = await doMultiplexHttp2Request(HTTPS_SERVER, [
{ headers: { ":path": "/post", ":method": "POST" }, payload: JSON.stringify({ "request": 1 }) },
{ headers: { ":path": "/post", ":method": "POST" }, payload: JSON.stringify({ "request": 2 }) },
{ headers: { ":path": "/post", ":method": "POST" }, payload: JSON.stringify({ "request": 3 }) },
@@ -214,7 +215,7 @@ describe("Client Basics", () => {
for (let i = 0; i < results.length; i++) {
let parsed;
expect(() => (parsed = JSON.parse(results[i].data))).not.toThrow();
expect(parsed.url).toBe("https://httpbin.org/post");
expect(parsed.url).toBe(`${HTTPS_SERVER}/post`);
expect([1, 2, 3, 4, 5]).toContain(parsed.json?.request);
}
});
@@ -508,7 +509,7 @@ describe("Client Basics", () => {
});
it("headers cannot be bigger than 65536 bytes", async () => {
try {
await doHttp2Request("https://bun.sh", { ":path": "/", "test-header": "A".repeat(90000) });
await doHttp2Request(HTTPS_SERVER, { ":path": "/", "test-header": "A".repeat(90000) });
expect("unreachable").toBe(true);
} catch (err) {
expect(err.code).toBe("ERR_HTTP2_STREAM_ERROR");
@@ -517,7 +518,7 @@ describe("Client Basics", () => {
});
it("should be destroyed after close", async () => {
const { promise, resolve, reject: promiseReject } = Promise.withResolvers();
const client = http2.connect("https://httpbin.org/get");
const client = http2.connect(`${HTTPS_SERVER}/get`, TLS_OPTIONS);
client.on("error", promiseReject);
client.on("close", resolve);
function reject(err) {
@@ -537,7 +538,7 @@ describe("Client Basics", () => {
});
it("should be destroyed after destroy", async () => {
const { promise, resolve, reject: promiseReject } = Promise.withResolvers();
const client = http2.connect("https://httpbin.org/get");
const client = http2.connect(`${HTTPS_SERVER}/get`, TLS_OPTIONS);
client.on("error", promiseReject);
client.on("close", resolve);
function reject(err) {
@@ -556,21 +557,21 @@ describe("Client Basics", () => {
expect(client.destroyed).toBe(true);
});
it("should fail to connect over HTTP/1.1", async () => {
const tls = {
...TLS_CERT,
ca: TLS_CERT.cert,
};
const tls = TLS_CERT;
const server = Bun.serve({
port: 0,
hostname: "127.0.0.1",
tls,
tls: {
...tls,
ca: TLS_CERT.ca,
},
fetch() {
return new Response("hello");
},
});
const url = `https://127.0.0.1:${server.port}`;
try {
await doHttp2Request(url, { ":path": "/" }, null, tls);
await doHttp2Request(url, { ":path": "/" }, null, TLS_OPTIONS);
expect("unreachable").toBe(true);
} catch (err) {
expect(err.code).toBe("ERR_HTTP2_ERROR");
@@ -599,12 +600,13 @@ describe("Client Basics", () => {
.connect(
{
rejectUnauthorized: false,
host: "httpbin.org",
port: 443,
host: new URL(HTTPS_SERVER).hostname,
port: new URL(HTTPS_SERVER).port,
ALPNProtocols: ["h2"],
...TLS_OPTIONS,
},
() => {
doHttp2Request("https://httpbin.org/get", { ":path": "/get" }, null, {
doHttp2Request(`${HTTPS_SERVER}/get`, { ":path": "/get" }, null, {
createConnection: () => {
return new JSSocket(socket);
},
@@ -615,20 +617,20 @@ describe("Client Basics", () => {
const result = await promise;
let parsed;
expect(() => (parsed = JSON.parse(result.data))).not.toThrow();
expect(parsed.url).toBe("https://httpbin.org/get");
expect(parsed.url).toBe(`${HTTPS_SERVER}/get`);
socket.destroy();
});
it("close callback", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect(`https://httpbin.org/get`);
const client = http2.connect(`${HTTPS_SERVER}/get`, TLS_OPTIONS);
client.on("error", reject);
client.close(resolve);
await promise;
expect(client.destroyed).toBe(true);
});
it("is possibel to abort request", async () => {
it("is possible to abort request", async () => {
const abortController = new AbortController();
const promise = doHttp2Request("https://httpbin.org/get", { ":path": "/get" }, null, null, {
const promise = doHttp2Request(`${HTTPS_SERVER}/get`, { ":path": "/get" }, null, null, {
signal: abortController.signal,
});
abortController.abort();
@@ -643,7 +645,7 @@ describe("Client Basics", () => {
it("aborted event should work with abortController", async () => {
const abortController = new AbortController();
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect("https://www.example.com");
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
client.on("error", reject);
const req = client.request({ ":path": "/" }, { signal: abortController.signal });
req.on("aborted", resolve);
@@ -662,7 +664,7 @@ describe("Client Basics", () => {
});
it("aborted event should work with aborted signal", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect("https://www.example.com");
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
client.on("error", reject);
const req = client.request({ ":path": "/" }, { signal: AbortSignal.abort() });
req.on("aborted", resolve);
@@ -680,7 +682,7 @@ describe("Client Basics", () => {
});
it("endAfterHeaders should work", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect("https://www.example.com");
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
client.on("error", reject);
const req = client.request({ ":path": "/" });
req.endAfterHeaders = true;
@@ -703,7 +705,7 @@ describe("Client Basics", () => {
});
it("state should work", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect("https://www.example.com");
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
client.on("error", reject);
const req = client.request({ ":path": "/", "test-header": "test-value" });
{
@@ -811,7 +813,7 @@ describe("Client Basics", () => {
});
it("ping events should work", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect("https://www.example.com");
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
client.on("error", reject);
client.on("connect", () => {
client.ping(Buffer.from("12345678"), (err, duration, payload) => {
@@ -838,7 +840,7 @@ describe("Client Basics", () => {
});
it("ping without events should work", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect("https://www.example.com");
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
client.on("error", reject);
client.on("connect", () => {
client.ping((err, duration, payload) => {
@@ -864,7 +866,7 @@ describe("Client Basics", () => {
});
it("ping with wrong payload length events should error", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect("https://www.example.com");
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
client.on("error", resolve);
client.on("connect", () => {
client.ping(Buffer.from("oops"), (err, duration, payload) => {
@@ -882,7 +884,7 @@ describe("Client Basics", () => {
});
it("ping with wrong payload type events should throw", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect("https://www.example.com");
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
client.on("error", resolve);
client.on("connect", () => {
try {
@@ -901,7 +903,7 @@ describe("Client Basics", () => {
});
it("stream event should work", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect("https://www.example.com");
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
client.on("error", reject);
client.on("stream", stream => {
resolve(stream);
@@ -914,7 +916,7 @@ describe("Client Basics", () => {
});
it("should wait request to be sent before closing", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect("https://www.example.com");
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
client.on("error", reject);
const req = client.request({ ":path": "/" });
let response_headers = null;
@@ -928,54 +930,46 @@ describe("Client Basics", () => {
expect(response_headers[":status"]).toBe(200);
});
it("wantTrailers should work", async () => {
const info = await nodeEchoServer();
try {
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect(info.url, {
...TLS_CERT,
ca: TLS_CERT.cert,
});
client.on("error", reject);
const headers = { ":path": "/", ":method": "POST", "x-wait-trailer": "true" };
const req = client.request(headers, {
waitForTrailers: true,
});
req.setEncoding("utf8");
let response_headers;
req.on("response", headers => {
response_headers = headers;
});
let trailers = { "x-trailer": "hello" };
req.on("wantTrailers", () => {
req.sendTrailers(trailers);
});
let data = "";
req.on("data", chunk => {
data += chunk;
client.close();
});
req.on("error", reject);
req.on("end", () => {
resolve({ data, headers: response_headers });
client.close();
});
req.end("hello");
const response = await promise;
let parsed;
expect(() => (parsed = JSON.parse(response.data))).not.toThrow();
expect(parsed.headers[":method"]).toEqual(headers[":method"]);
expect(parsed.headers[":path"]).toEqual(headers[":path"]);
expect(parsed.headers["x-wait-trailer"]).toEqual(headers["x-wait-trailer"]);
expect(parsed.trailers).toEqual(trailers);
expect(response.headers[":status"]).toBe(200);
expect(response.headers["set-cookie"]).toEqual([
"a=b",
"c=d; Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly",
"e=f",
]);
} finally {
info.subprocess.kill();
}
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
client.on("error", reject);
const headers = { ":path": "/", ":method": "POST", "x-wait-trailer": "true" };
const req = client.request(headers, {
waitForTrailers: true,
});
req.setEncoding("utf8");
let response_headers;
req.on("response", headers => {
response_headers = headers;
});
let trailers = { "x-trailer": "hello" };
req.on("wantTrailers", () => {
req.sendTrailers(trailers);
});
let data = "";
req.on("data", chunk => {
data += chunk;
client.close();
});
req.on("error", reject);
req.on("end", () => {
resolve({ data, headers: response_headers });
client.close();
});
req.end("hello");
const response = await promise;
let parsed;
expect(() => (parsed = JSON.parse(response.data))).not.toThrow();
expect(parsed.headers[":method"]).toEqual(headers[":method"]);
expect(parsed.headers[":path"]).toEqual(headers[":path"]);
expect(parsed.headers["x-wait-trailer"]).toEqual(headers["x-wait-trailer"]);
expect(parsed.trailers).toEqual(trailers);
expect(response.headers[":status"]).toBe(200);
expect(response.headers["set-cookie"]).toEqual([
"a=b",
"c=d; Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly",
"e=f",
]);
});
it("should not leak memory", () => {
@@ -984,10 +978,13 @@ describe("Client Basics", () => {
env: {
...bunEnv,
BUN_JSC_forceRAMSize: (1024 * 1024 * 64).toString("10"),
HTTP2_SERVER_INFO: JSON.stringify(nodeEchoServer_),
HTTP2_SERVER_TLS: JSON.stringify(TLS_OPTIONS),
},
stderr: "inherit",
stdin: "inherit",
stdout: "inherit",
});
expect(stdout.toString("utf-8")).toBeEmpty();
expect(exitCode).toBe(0);
}, 100000);
@@ -1048,7 +1045,7 @@ describe("Client Basics", () => {
}
});
it("should not be able to write on socket", async () => {
const server = await nodeEchoServer();
const server = nodeEchoServer_;
try {
const client = http2.connect(server.url);
client.socket.write("hello");

View File

@@ -0,0 +1,9 @@
var TLS_CERT = {
key: "\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGYP2mk8DRbJiI\nZFISwazTu3cwihJo42JROYgUkCvjHssijs86zIX0DYYSsQjw8dgU7PGFGdnEthTu\nMj9FyJpovX/o72/FPCrJtPPr2/ztlIWmOvKzxjA1f4OicKT0gVxIMca/ZjtXQ6G3\nR0fkL+dEPc7aJNvG2p+DIlGPtqN0PH9ktVtOAzGDEMtxculOmzCF2+auaukK5R1B\njTpHAJIRzwQFuwXz8LhSA4v5tSNlNkpIeIjwOM3writFBChVoDm/TPey4HeQ6Psy\nfjzBYg1EnWXJT2aDAtQJQmCcpoLt3R02HztZGlPF37UQK2JiujEruQ81WBmLTauI\nh4JVVOB7AgMBAAECggEARk31VuWiPhYYcK3tEEynLVqQwRkNsTJ0k4iqG2+EvjcZ\nkqO9+X6mMnngfBtVqd5rz+6xIZSpwrcs78XK+rY/UsNl422H1QSfvWBt2bbbCm/K\ndcEKZn/kcfFA+36kVyrJQ6SwZQCcIy8KzuNqLQp1EZA+EL1jTjQIt/afpSj7AKQY\nVlWHMXRf38WRGAo4w5jMMzM0Kw9Kn1U5Nx6WD7FcXo4uRhmxF/0aRzCXobWPcYwL\nBFbOJjEK0jEV1pNkGATUJ0NsgnHWRAuvQj7z0noSt6jXCSBZFjkf+j9AUmxuGOD+\nyargqTgINX/WYpIEd9Fr6p4vVBnA5coCVdGawFgvkQKBgQDl5CxUNeo9SRS7BeoF\njo+Ivo0VLQJWb9U8HpG1UTc0gjGitgV4dQ/6C4OFlzUF69gIGbzGf5AVIvbzCJUT\nAuV6BpGdMfexVRms8p3ktZ1dRDxN6wPICAfCLNV8aOp2p9f9ZFSWnm8M8oGMrHo+\nKgj0f19FNXOMbKcDncj+ZMLc3QKBgQDc6KE3KW3U8a+EVUcOfvnHnEi4FeeSuykA\n/KCvGww5m5QoDy+e5F4VgFWrOnobPERk9tTGPiR9P5juNxXtzecT+Ug4Bxxk6NXy\n2tK4RoR1m/NTw1Hr3xp7CodFqdE/sDeb/M253lnfnp2JSp+J6ddavb4XTj+J+PIA\neH1NW9PRNwKBgQCfbvYbVOTlqehZqElbnzoGQPjBRdzIK3j738t3ryKVJPHdgVUb\n7DuvUwrcvDgGqkDBpW/ZTiCTuBMCC+KvM6QIU8Pq+/tnHbjXy88bDaVcSHV2KFYQ\nBRm0XbmVNYHd1puh3VIYvzoPBaQ49mk08ZwSTL+61M4VBklx5Zy+aQ0HdQKBgCrD\nwgnatE9n5jF5DMNqo1IYGB/C5cyK/NobDcQ4OTqhuqGypuZckTYaXPtD28WP+jGN\ncw1ZlFjGygU7lrwtgxFjza5C+iUyydA0ulxAEn5uDUHm6uH9k7PECwHaaQ6qP2ms\nG+tidwWKQDcGwjHBmhYP60+5ryU3kymyKZejMjMrAoGBAMgk1COWDKaYDd5fWnBP\nyejYV1tPLAW83s66DMDXMZWpTbw5sKEvxERJL3HUGrxD0bRRBSG7wA2RjA9JouUN\njzHIFSCuJi9ZNJoHA9RV4UtpMTdMWg05nj/izoNXnYfW7LA4Yo3R7BqKDXIN/W3u\nR9MHcOTB0jPnKj9dcSdU5nXy\n-----END PRIVATE KEY-----",
cert: "\n-----BEGIN CERTIFICATE-----\nMIIDCTCCAfGgAwIBAgIUAbpwqNLdxKrf8ScWim6lPc59R8gwDQYJKoZIhvcNAQEL\nBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDQzMDA1NDk0M1oXDTI0MDUz\nMDA1NDk0M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEAxmD9ppPA0WyYiGRSEsGs07t3MIoSaONiUTmIFJAr4x7L\nIo7POsyF9A2GErEI8PHYFOzxhRnZxLYU7jI/RciaaL1/6O9vxTwqybTz69v87ZSF\npjrys8YwNX+DonCk9IFcSDHGv2Y7V0Oht0dH5C/nRD3O2iTbxtqfgyJRj7ajdDx/\nZLVbTgMxgxDLcXLpTpswhdvmrmrpCuUdQY06RwCSEc8EBbsF8/C4UgOL+bUjZTZK\nSHiI8DjN8K4rRQQoVaA5v0z3suB3kOj7Mn48wWINRJ1lyU9mgwLUCUJgnKaC7d0d\nNh87WRpTxd+1ECtiYroxK7kPNVgZi02riIeCVVTgewIDAQABo1MwUTAdBgNVHQ4E\nFgQUT42TrSl9k+7K3zYA32cnubSY07UwHwYDVR0jBBgwFoAUT42TrSl9k+7K3zYA\n32cnubSY07UwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAljpg\nWyKu4HIAdCwvAbplaiwKlVO1KGb8a2/FJ3zcUiipX1q2dDDrrPsjuWXQN7/NrmuT\n65zUAJSYJZZyw5GB1oQD96YsQq2B4Y2s9jx5H/e3W4ys5YJRZRU4cFJLSHD07x5O\n7E9mT5HoyF0fy1/7XnKRwfYMsUhe/kRkM2XbT8d5ZRPesNKPWVb7Pv6BYWXcewSB\n/vnEr2vtjLY6J3WxYPwY3ocC8K1vNk103zkwX37suXu65U3rWBiFiqUWCZf+8IFo\nNRKv5Gva03SnZ7I9pnJQv7igk21wxgG9/y5R8Kg7Fpkcqx3ph3CMsxbjhgX7pmYj\nV9Amy+loH/ocj6HpYQ==\n-----END CERTIFICATE-----",
ca: "",
};
var TLS_OPTIONS = { ca: TLS_CERT.cert };
module.exports = { TLS_CERT, TLS_OPTIONS };