From a03a85c4a6ef1dea19f5467bf48115dec991943c Mon Sep 17 00:00:00 2001 From: tsutsu3 Date: Fri, 29 Nov 2024 00:03:30 +0900 Subject: [PATCH] Support tcp socket --- src/client.ts | 10 +++--- src/control.ts | 52 ++++++++++++++++++++---------- tests/control.test.ts | 73 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 111 insertions(+), 24 deletions(-) diff --git a/src/client.ts b/src/client.ts index 77106c2..f75766f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -6,15 +6,15 @@ abstract class UnboundControlClient { private control: UnboundControl; constructor( - unixSocketName: string | null, + unixSocketName?: string, host?: string, port?: number, - tlsConfig?: TlsConfig | null, + tlsConfig?: TlsConfig, ) { if (unixSocketName) { this.control = new UnboundControl(unixSocketName); } else { - this.control = new UnboundControl(null, host, port, tlsConfig); + this.control = new UnboundControl(undefined, host, port, tlsConfig); } } @@ -865,7 +865,7 @@ export class UnixUnboundClient extends UnboundControlClient { } export class TcpUnboundClient extends UnboundControlClient { - constructor(host: string, port: number, tlsConfig?: TlsConfig) { - super(null, host, port, tlsConfig); + constructor(host: string, port: number, tlsConfig: TlsConfig) { + super(undefined, host, port, tlsConfig); } } diff --git a/src/control.ts b/src/control.ts index 22ad00d..5fda53b 100644 --- a/src/control.ts +++ b/src/control.ts @@ -10,7 +10,7 @@ import { TlsConfig } from "./types"; */ export class UnboundControl { /** The path to the Unix domain socket (if applicable). */ - private readonly unixSocketName: string | null; + private readonly unixSocketName?: string; /** The host address for TCP connections. */ private readonly host: string; @@ -19,10 +19,10 @@ export class UnboundControl { private readonly port: number; /** Optional TLS configuration for secure connections. */ - private readonly tlsConfig: TlsConfig | null = null; + private readonly tlsConfig?: TlsConfig; /** The underlying network socket for communication. */ - private socket: net.Socket | null = null; + private socket?: net.Socket | null; /** * Creates a new instance of the UnboundControl class. @@ -33,10 +33,10 @@ export class UnboundControl { * @param tlsConfig - Optional TLS configuration for secure connections. */ constructor( - unixSocketName: string | null = null, + unixSocketName?: string, host: string = "localhost", port: number = 8953, - tlsConfig: TlsConfig | null = null, + tlsConfig?: TlsConfig, ) { this.unixSocketName = unixSocketName; this.host = host; @@ -61,22 +61,40 @@ export class UnboundControl { return new Promise((resolve, reject) => { let socket: net.Socket; - if (this.unixSocketName !== null) { + if (this.unixSocketName) { socket = net.createConnection(this.unixSocketName); } else { if (this.tlsConfig) { - socket = tls.connect({ - host: this.host, - port: this.port, - rejectUnauthorized: this.tlsConfig.ca ? true : false, - cert: fs.readFileSync(this.tlsConfig.cert), - key: fs.readFileSync(this.tlsConfig.key), - ca: this.tlsConfig.ca - ? fs.readFileSync(this.tlsConfig.ca) - : undefined, - }); + // Connect via TLS + socket = tls.connect( + { + host: this.host, + port: this.port, + rejectUnauthorized: !!this.tlsConfig.ca, + cert: fs.readFileSync(this.tlsConfig.cert), + key: fs.readFileSync(this.tlsConfig.key), + ca: this.tlsConfig.ca + ? fs.readFileSync(this.tlsConfig.ca) + : undefined, + }, + () => { + const tlsSocket = socket as tls.TLSSocket; + if (tlsSocket.authorized || !this.tlsConfig?.ca) { + resolve(tlsSocket); + } else { + reject( + new ConnectionError( + `TLS authorization failed: ${tlsSocket.authorizationError}`, + ), + ); + } + }, + ); } else { - socket = net.createConnection(this.port, this.host); + // Connect via plain TCP + socket = net.createConnection(this.port, this.host, () => { + resolve(socket); + }); } } diff --git a/tests/control.test.ts b/tests/control.test.ts index c56ed5a..5ea00df 100644 --- a/tests/control.test.ts +++ b/tests/control.test.ts @@ -1,8 +1,9 @@ -import { UnixMockServer, MockServer } from "./mockServer"; -import { UnixUnboundClient } from "../src/index"; import fs from "fs"; import path from "path"; import YAML from "yaml"; +import { UnixMockServer, TcpTlsMockServer, MockServer } from "./mockServer"; +import { UnixUnboundClient, TcpUnboundClient } from "../src/index"; +import { TlsConfig } from "../src/types"; const baseDir = path.resolve(__dirname); const unboundVersion = process.env.UNBOUND_VERSION || "1.22.0"; @@ -84,3 +85,71 @@ describe(`Unix domain socket mock server tests. Unbound version: ${unboundVersio } } }); + +describe(`TCP socket docker server tests. Unbound version: ${unboundVersion}`, () => { + let server: MockServer; + let client: TcpUnboundClient; + const keyPath = path.join(baseDir, "./key/unbound_control.key"); + const certPath = path.join(baseDir, "./key/unbound_control.pem"); + const tlsConfig: TlsConfig = { + cert: certPath, + key: keyPath, + }; + + const options = { + key: fs.readFileSync(path.join(baseDir, "./key/unbound_server.key")), + cert: fs.readFileSync(path.join(baseDir, "./key/unbound_server.pem")), + requestCert: false, + }; + + beforeAll(() => { + server = new TcpTlsMockServer("localhost", 8953, options); + client = new TcpUnboundClient("localhost", 8953, tlsConfig); + }); + + afterEach(async () => { + await server.stop(); + }); + + const files = fs + .readdirSync(dataDir) + .filter((file) => file.endsWith(".yaml")); + + for (const file of files) { + const command = file.replace(".yaml", ""); + const fileContent = fs.readFileSync(path.join(dataDir, file), "utf-8"); + const contents = YAML.parse(fileContent) as ParsedData; + + for (const { title, options, raw, expected, exception } of contents.data) { + it(`${command} test: ${title}`, async () => { + server.start(raw); + + const method = client[command as keyof UnixUnboundClient].bind( + client, + ) as (...args: any[]) => Promise; // eslint-disable-line @typescript-eslint/no-explicit-any + if (typeof method !== "function") { + throw new Error(`Invalid command: ${command}`); + } + + const args = + options !== undefined + ? Array.isArray(options) + ? options + : [options] + : []; + + let result; + + if (exception) { + await expect(method.apply(client, args)).rejects.toThrow(exception); + } else { + result = await method.apply(client, args); + expect(result.raw).toEqual(raw); + expect(result.json).toEqual(expected); + } + + await client.disconnect(); + }); + } + } +});