diff --git a/README.md b/README.md index 3aab85b..84085fc 100644 --- a/README.md +++ b/README.md @@ -89,10 +89,31 @@ yarn add unbound-control-ts Here's a basic example to demonstrate how to use the library: +Use domain socket: ```ts -import { UnboundControlClient } from 'unbound-control-ts'; +import { UnixUnboundClient } from 'unbound-control-ts'; -const client = new UnboundControlClient('/path/to/unbound-control.sock'); +const client = new UnixUnboundClient('/path/to/unbound-control.sock'); + +(async () => { + try { + const response = await client.status(); + console.log(response); + } catch (error) { + if (error instanceof UnboundError) { + console.error(error.message); + } else { + console.error(error); + } + } +})(); +``` + +Use tcp socket: +```ts +import { TcpUnboundClient } from 'unbound-control-ts'; + +const client = new TcpUnboundClient('localhost', 8953); (async () => { try { @@ -142,7 +163,6 @@ Before you begin, ensure you have the following tools installed on your system: - **Node.js**: Version 16 or later. [Download Node.js](https://nodejs.org/) - **npm**: Comes with Node.js, or install it separately if needed. - **Unbound**: Ensure that `unbound-control` is installed and properly configured. Follow the [Unbound installation guide](https://nlnetlabs.nl/documentation/unbound/) for details. -- **TypeScript**: (Optional) For contributing to or extending the library, TypeScript must be installed globally or as a dev dependency. ### Develop Setup diff --git a/package.json b/package.json index 19d21b3..158ead8 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "test": "jest --detectOpenHandles tests/control.test.ts", "test:it": "jest --detectOpenHandles tests/control.it.test.ts", "snapshot": "jest --detectOpenHandles tests/control.snapshot.test.ts", + "snapshot:update": "jest --detectOpenHandles --updateSnapshot tests/control.snapshot.test.ts", "sample:esm": "node examples/index.mjs", "sample:cjs": "node examples/index.cjs", "sample": "npm run test:esm:node && npm run test:cjs:node" diff --git a/src/client.ts b/src/client.ts index 11112fe..77106c2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,116 +1,21 @@ import { UnboundControl } from "./control"; import { ParseError } from "./error"; +import { Response, TlsConfig, NestedRecord, ValidOption } from "./types"; -/** - * A list of valid configuration options for the `set_option` command. - */ -export type ValidOption = - | "statistics-interval" - | "statistics-cumulative" - | "do-not-query-localhost" - | "harden-short-bufsize" - | "harden-large-queries" - | "harden-glue" - | "harden-dnssec-stripped" - | "harden-below-nxdomain" - | "harden-referral-path" - | "prefetch" - | "prefetch-key" - | "log-queries" - | "hide-identity" - | "hide-version" - | "identity" - | "version" - | "val-log-level" - | "val-log-squelch" - | "ignore-cd-flag" - | "add-holddown" - | "del-holddown" - | "keep-missing" - | "tcp-upstream" - | "ssl-upstream" - | "max-udp-size" - | "ratelimit" - | "ip-ratelimit" - | "cache-max-ttl" - | "cache-min-ttl" - | "cache-max-negative-ttl"; - -export interface Response { - raw: string; - json: any; // eslint-disable-line @typescript-eslint/no-explicit-any -} - -// export interface StatusResponse { -// version: string; -// verbosity: number; -// threads: number; -// modules: string[]; -// uptime: number; -// options: string[]; -// pid: number; -// status: string; -// } - -// export interface StasResponse { -// total: { -// num: { -// queries: number; -// queries_ip_ratelimited: number; -// queries_cookie_valid: number; -// queries_cookie_client: number; -// queries_cookie_invalid: number; -// cachehits: number; -// cachemiss: number; -// prefetch: number; -// queries_timed_out: number; -// expired: number; -// recursivereplies: number; -// }; -// query: { -// queue_time_us: { -// max: number; -// }; -// }; -// requestlist: { -// avg: number; -// max: number; -// overwritten: number; -// exceeded: number; -// current: { -// all: number; -// user: number; -// }; -// }; -// recursion: { -// time: { -// avg: number; -// median: number; -// }; -// }; -// tcpusage: number; -// }; -// time: { -// now: number; -// up: number; -// elapsed: number; -// }; -// } - -export interface NestedRecord { - [key: string]: string | number | string[] | NestedRecord; -} - -export class UnboundControlClient { +abstract class UnboundControlClient { private control: UnboundControl; constructor( - unixSocketName: string | null = null, - // host: string = "127.0.0.1", - // port: number = 8953, - // tlsConfig?: TLSConfig, + unixSocketName: string | null, + host?: string, + port?: number, + tlsConfig?: TlsConfig | null, ) { - this.control = new UnboundControl(unixSocketName); + if (unixSocketName) { + this.control = new UnboundControl(unixSocketName); + } else { + this.control = new UnboundControl(null, host, port, tlsConfig); + } } /** @@ -951,3 +856,16 @@ export class UnboundControlClient { }; } } + +export class UnixUnboundClient extends UnboundControlClient { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(unixSocketName: string) { + super(unixSocketName); + } +} + +export class TcpUnboundClient extends UnboundControlClient { + constructor(host: string, port: number, tlsConfig?: TlsConfig) { + super(null, host, port, tlsConfig); + } +} diff --git a/src/control.ts b/src/control.ts index e6002dc..22ad00d 100644 --- a/src/control.ts +++ b/src/control.ts @@ -1,44 +1,29 @@ import net from "net"; +import tls from "tls"; +import fs from "fs"; import { ConnectionError, CommandError } from "./error"; -// import tls from "tls"; - -/** - * Configuration for the TLS connection. - */ -// export interface TLSConfig { -// /** Certificate file. */ -// cert: string; - -// /** Key file. */ -// key: string; - -// /** CA certificate file. */ -// ca?: string; - -// /** Reject unauthorized connections. If set to false, the server certificate is not verified. */ -// rejectUnauthorized: boolean; -// } +import { TlsConfig } from "./types"; /** * A class to interact with an Unbound control interface via TCP or Unix socket. * Provides methods to establish connections and send commands to the Unbound DNS resolver. */ export class UnboundControl { + /** The path to the Unix domain socket (if applicable). */ + private readonly unixSocketName: string | null; + /** The host address for TCP connections. */ - // private readonly host: string; + private readonly host: string; /** The port number for TCP connections. */ - // private readonly port: number; + private readonly port: number; - /** The path to the Unix domain socket (if applicable). */ - private readonly unixSocketName: string | null; + /** Optional TLS configuration for secure connections. */ + private readonly tlsConfig: TlsConfig | null = null; /** The underlying network socket for communication. */ private socket: net.Socket | null = null; - /** Optional TLS configuration for secure connections. */ - // private readonly tlsConfig: TLSConfig | null = null; - /** * Creates a new instance of the UnboundControl class. * @@ -49,14 +34,14 @@ export class UnboundControl { */ constructor( unixSocketName: string | null = null, - // host: string = "127.0.0.1", - // port: number = 8953, - // tlsConfig?: TLSConfig, + host: string = "localhost", + port: number = 8953, + tlsConfig: TlsConfig | null = null, ) { this.unixSocketName = unixSocketName; - // this.host = host; - // this.port = port; - // this.tlsConfig = tlsConfig || null; + this.host = host; + this.port = port; + this.tlsConfig = tlsConfig; } /** @@ -79,15 +64,21 @@ export class UnboundControl { if (this.unixSocketName !== null) { socket = net.createConnection(this.unixSocketName); } else { - throw new Error("Not implemented"); + 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, + }); + } else { + socket = net.createConnection(this.port, this.host); + } } - // else { - // socket = tls.createConnection({ - // host: this.host, - // port: this.port, - // ...this.tlsConfig, - // }); - // } socket.once("connect", () => { this.socket = socket; diff --git a/src/index.ts b/src/index.ts index 7d7a834..bf7c4b6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export { UnboundControl } from "./control"; -export { UnboundControlClient } from "./client"; +export { UnixUnboundClient, TcpUnboundClient } from "./client"; export { UnboundError, ConnectionError, diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..badaf66 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,113 @@ +/** + * Configuration for the TLS connection. + */ +export interface TlsConfig { + /** Certificate file. */ + cert: string; + + /** Key file. */ + key: string; + + /** CA certificate file. */ + ca?: string; +} + +export interface Response { + raw: string; + json: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +// export interface StatusResponse { +// version: string; +// verbosity: number; +// threads: number; +// modules: string[]; +// uptime: number; +// options: string[]; +// pid: number; +// status: string; +// } + +// export interface StasResponse { +// total: { +// num: { +// queries: number; +// queries_ip_ratelimited: number; +// queries_cookie_valid: number; +// queries_cookie_client: number; +// queries_cookie_invalid: number; +// cachehits: number; +// cachemiss: number; +// prefetch: number; +// queries_timed_out: number; +// expired: number; +// recursivereplies: number; +// }; +// query: { +// queue_time_us: { +// max: number; +// }; +// }; +// requestlist: { +// avg: number; +// max: number; +// overwritten: number; +// exceeded: number; +// current: { +// all: number; +// user: number; +// }; +// }; +// recursion: { +// time: { +// avg: number; +// median: number; +// }; +// }; +// tcpusage: number; +// }; +// time: { +// now: number; +// up: number; +// elapsed: number; +// }; +// } + +export interface NestedRecord { + [key: string]: string | number | string[] | NestedRecord; +} + +/** + * A list of valid configuration options for the `set_option` command. + */ +export type ValidOption = + | "statistics-interval" + | "statistics-cumulative" + | "do-not-query-localhost" + | "harden-short-bufsize" + | "harden-large-queries" + | "harden-glue" + | "harden-dnssec-stripped" + | "harden-below-nxdomain" + | "harden-referral-path" + | "prefetch" + | "prefetch-key" + | "log-queries" + | "hide-identity" + | "hide-version" + | "identity" + | "version" + | "val-log-level" + | "val-log-squelch" + | "ignore-cd-flag" + | "add-holddown" + | "del-holddown" + | "keep-missing" + | "tcp-upstream" + | "ssl-upstream" + | "max-udp-size" + | "ratelimit" + | "ip-ratelimit" + | "cache-max-ttl" + | "cache-min-ttl" + | "cache-max-negative-ttl"; diff --git a/tests/control.it.test.ts b/tests/control.it.test.ts index f390d36..0946502 100644 --- a/tests/control.it.test.ts +++ b/tests/control.it.test.ts @@ -1,4 +1,4 @@ -import { UnboundControlClient } from "../src/index"; +import { UnixUnboundClient } from "../src/index"; import fs from "fs"; import path from "path"; import YAML from "yaml"; @@ -20,14 +20,14 @@ interface TestCase { } describe(`Unix domain socket docker server tests. Unbound version: ${unboundVersion}`, () => { - let client: UnboundControlClient; + let client: UnixUnboundClient; const unixSocketPath = path.join( baseDir, "../unbound-config/unix/socket/unbound.ctl", ); beforeAll(() => { - client = new UnboundControlClient(unixSocketPath); + client = new UnixUnboundClient(unixSocketPath); }); const files = fs @@ -43,7 +43,7 @@ describe(`Unix domain socket docker server tests. Unbound version: ${unboundVers it(`${command} test: ${title}`, async () => { console.log(`Running ${command} test: ${title}`); - const method = client[command as keyof UnboundControlClient].bind( + 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") { diff --git a/tests/control.snapshot.test.ts b/tests/control.snapshot.test.ts index dada18d..a87669b 100644 --- a/tests/control.snapshot.test.ts +++ b/tests/control.snapshot.test.ts @@ -1,4 +1,4 @@ -import { UnboundControlClient } from "../src/index"; +import { UnixUnboundClient } from "../src/index"; import fs from "fs"; import path from "path"; import YAML from "yaml"; @@ -20,14 +20,14 @@ interface TestCase { } describe(`Unix domain socket docker server tests. Unbound version: ${unboundVersion}`, () => { - let client: UnboundControlClient; + let client: UnixUnboundClient; const unixSocketPath = path.join( baseDir, "../unbound-config/unix/socket/unbound.ctl", ); beforeAll(() => { - client = new UnboundControlClient(unixSocketPath); + client = new UnixUnboundClient(unixSocketPath); }); const files = fs @@ -43,7 +43,7 @@ describe(`Unix domain socket docker server tests. Unbound version: ${unboundVers it(`${command} test: ${title}`, async () => { console.log(`Running ${command} test: ${title}`); - const method = client[command as keyof UnboundControlClient].bind( + 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") { diff --git a/tests/control.test.ts b/tests/control.test.ts index c26d6d5..c56ed5a 100644 --- a/tests/control.test.ts +++ b/tests/control.test.ts @@ -1,5 +1,5 @@ import { UnixMockServer, MockServer } from "./mockServer"; -import { UnboundControlClient } from "../src/index"; +import { UnixUnboundClient } from "../src/index"; import fs from "fs"; import path from "path"; import YAML from "yaml"; @@ -27,7 +27,7 @@ interface Response { describe(`Unix domain socket mock server tests. Unbound version: ${unboundVersion}`, () => { let server: MockServer; - let client: UnboundControlClient; + let client: UnixUnboundClient; const unixSocketPath = "/tmp/mock.sock"; beforeAll(() => { @@ -35,7 +35,7 @@ describe(`Unix domain socket mock server tests. Unbound version: ${unboundVersio fs.unlinkSync(unixSocketPath); } server = new UnixMockServer(unixSocketPath); - client = new UnboundControlClient(unixSocketPath); + client = new UnixUnboundClient(unixSocketPath); }); afterEach(async () => { @@ -55,7 +55,7 @@ describe(`Unix domain socket mock server tests. Unbound version: ${unboundVersio it(`${command} test: ${title}`, async () => { server.start(raw); - const method = client[command as keyof UnboundControlClient].bind( + 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") { diff --git a/tests/mockServer.ts b/tests/mockServer.ts index 65d9635..e8e5902 100644 --- a/tests/mockServer.ts +++ b/tests/mockServer.ts @@ -27,7 +27,7 @@ export class UnixMockServer implements MockServer { }); socket.on("end", () => { - socket.destroy(); // 接続を完全に破棄 + socket.destroy(); }); socket.on("error", () => {