diff --git a/test/js/node/http2/http2-helpers.cjs b/test/js/node/http2/http2-helpers.cjs new file mode 100644 index 0000000000..a55fce4366 --- /dev/null +++ b/test/js/node/http2/http2-helpers.cjs @@ -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; +}; diff --git a/test/js/node/http2/node-echo-server.fixture.js b/test/js/node/http2/node-echo-server.fixture.js index dd1ce36cf2..289f28421c 100644 --- a/test/js/node/http2/node-echo-server.fixture.js +++ b/test/js/node/http2/node-echo-server.fixture.js @@ -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" })); }); diff --git a/test/js/node/http2/node-http2-memory-leak.js b/test/js/node/http2/node-http2-memory-leak.js index 8abfec7caf..949ade1d49 100644 --- a/test/js/node/http2/node-http2-memory-leak.js +++ b/test/js/node/http2/node-http2-memory-leak.js @@ -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); + }, +); diff --git a/test/js/node/http2/node-http2.test.js b/test/js/node/http2/node-http2.test.js index 71f12033f0..db56f372a0 100644 --- a/test/js/node/http2/node-http2.test.js +++ b/test/js/node/http2/node-http2.test.js @@ -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"); diff --git a/test/js/node/http2/tls-cert.cjs b/test/js/node/http2/tls-cert.cjs new file mode 100644 index 0000000000..82c52b7219 --- /dev/null +++ b/test/js/node/http2/tls-cert.cjs @@ -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 };