|
1 | 1 | // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
2 | 2 | // SPDX-License-Identifier: Apache-2.0 |
3 | 3 |
|
| 4 | +import { EventEmitter } from "node:events"; |
| 5 | +import fs from "node:fs"; |
4 | 6 | import http from "node:http"; |
| 7 | +import http2 from "node:http2"; |
5 | 8 | import type { AddressInfo } from "node:net"; |
| 9 | +import os from "node:os"; |
| 10 | +import path from "node:path"; |
6 | 11 |
|
7 | | -import { afterEach, describe, expect, it } from "vitest"; |
| 12 | +import { afterEach, describe, expect, it, vi } from "vitest"; |
8 | 13 |
|
9 | | -import { isGatewayHttpReady } from "./gateway-http-readiness"; |
| 14 | +import { isDockerDriverGatewayHttpReady, isGatewayHttpReady } from "./gateway-http-readiness"; |
10 | 15 |
|
11 | 16 | const servers: http.Server[] = []; |
12 | 17 |
|
@@ -84,3 +89,95 @@ describe("isGatewayHttpReady abort handling", () => { |
84 | 89 | await expect(probe).resolves.toBe(false); |
85 | 90 | }); |
86 | 91 | }); |
| 92 | + |
| 93 | +describe("isDockerDriverGatewayHttpReady TLS servername", () => { |
| 94 | + const tlsDirs: string[] = []; |
| 95 | + |
| 96 | + afterEach(() => { |
| 97 | + for (const dir of tlsDirs.splice(0)) { |
| 98 | + fs.rmSync(dir, { force: true, recursive: true }); |
| 99 | + } |
| 100 | + }); |
| 101 | + |
| 102 | + function writeLocalTlsDir(): string { |
| 103 | + const tlsDir = fs.mkdtempSync(path.join(os.tmpdir(), "gateway-tls-")); |
| 104 | + tlsDirs.push(tlsDir); |
| 105 | + fs.writeFileSync(path.join(tlsDir, "ca.crt"), "ca"); |
| 106 | + fs.mkdirSync(path.join(tlsDir, "client")); |
| 107 | + fs.writeFileSync(path.join(tlsDir, "client", "tls.crt"), "cert"); |
| 108 | + fs.writeFileSync(path.join(tlsDir, "client", "tls.key"), "key"); |
| 109 | + return tlsDir; |
| 110 | + } |
| 111 | + |
| 112 | + function healthySessionStub(): http2.ClientHttp2Session { |
| 113 | + const stream = new EventEmitter() as EventEmitter & { |
| 114 | + close: () => void; |
| 115 | + end: (payload?: Buffer) => void; |
| 116 | + }; |
| 117 | + stream.close = () => undefined; |
| 118 | + stream.end = () => { |
| 119 | + setImmediate(() => { |
| 120 | + stream.emit("response", { |
| 121 | + [http2.constants.HTTP2_HEADER_STATUS]: 200, |
| 122 | + [http2.constants.HTTP2_HEADER_CONTENT_TYPE]: "application/grpc", |
| 123 | + "grpc-status": "0", |
| 124 | + }); |
| 125 | + stream.emit("end"); |
| 126 | + }); |
| 127 | + }; |
| 128 | + const client = new EventEmitter() as EventEmitter & { |
| 129 | + close: () => void; |
| 130 | + request: () => typeof stream; |
| 131 | + }; |
| 132 | + client.close = () => undefined; |
| 133 | + client.request = () => stream; |
| 134 | + return client as unknown as http2.ClientHttp2Session; |
| 135 | + } |
| 136 | + |
| 137 | + function spyOnHttp2Connect() { |
| 138 | + return vi.spyOn(http2, "connect").mockImplementation(() => healthySessionStub()); |
| 139 | + } |
| 140 | + |
| 141 | + function connectOptionsFrom( |
| 142 | + connect: ReturnType<typeof spyOnHttp2Connect>, |
| 143 | + ): Record<string, unknown> { |
| 144 | + expect(connect).toHaveBeenCalledTimes(1); |
| 145 | + return connect.mock.calls[0]?.[1] as Record<string, unknown>; |
| 146 | + } |
| 147 | + |
| 148 | + it("omits servername for an IP-literal gateway host, which Node 25 rejects as a TLS ServerName (#7527)", async () => { |
| 149 | + vi.stubEnv("OPENSHELL_LOCAL_TLS_DIR", writeLocalTlsDir()); |
| 150 | + const connect = spyOnHttp2Connect(); |
| 151 | + |
| 152 | + await expect( |
| 153 | + isDockerDriverGatewayHttpReady(1_000, "https://127.0.0.1:8080/openshell.v1.OpenShell/Health"), |
| 154 | + ).resolves.toBe(true); |
| 155 | + |
| 156 | + expect(connectOptionsFrom(connect)).not.toHaveProperty("servername"); |
| 157 | + }); |
| 158 | + |
| 159 | + it("omits servername for a bracketed IPv6 gateway host (#7527)", async () => { |
| 160 | + vi.stubEnv("OPENSHELL_LOCAL_TLS_DIR", writeLocalTlsDir()); |
| 161 | + const connect = spyOnHttp2Connect(); |
| 162 | + |
| 163 | + await expect( |
| 164 | + isDockerDriverGatewayHttpReady(1_000, "https://[::1]:8080/openshell.v1.OpenShell/Health"), |
| 165 | + ).resolves.toBe(true); |
| 166 | + |
| 167 | + expect(connectOptionsFrom(connect)).not.toHaveProperty("servername"); |
| 168 | + }); |
| 169 | + |
| 170 | + it("keeps servername for a DNS gateway hostname (#7527)", async () => { |
| 171 | + vi.stubEnv("OPENSHELL_LOCAL_TLS_DIR", writeLocalTlsDir()); |
| 172 | + const connect = spyOnHttp2Connect(); |
| 173 | + |
| 174 | + await expect( |
| 175 | + isDockerDriverGatewayHttpReady( |
| 176 | + 1_000, |
| 177 | + "https://host.openshell.internal:8080/openshell.v1.OpenShell/Health", |
| 178 | + ), |
| 179 | + ).resolves.toBe(true); |
| 180 | + |
| 181 | + expect(connectOptionsFrom(connect).servername).toBe("host.openshell.internal"); |
| 182 | + }); |
| 183 | +}); |
0 commit comments