UNPKG

@collight/dns-sd

Version:

A DNS Service Discovery implementation in modern TypeScript

1 lines 12.3 kB
{"version":3,"file":"Service.cjs","sources":["../../../src/registry/Service.ts"],"sourcesContent":["import { EventEmitter } from 'events';\nimport os from 'os';\nimport { SrvAnswer, StringAnswer, TxtAnswer } from 'dns-packet';\n\nimport { encodeTXT, MDNSRecord, ServiceType } from '../utils';\nimport { Registry } from './Registry';\n\n// MARK: ServiceOptions\n/**\n * Options used to configure a service instance.\n */\nexport interface ServiceOptions {\n /**\n * The protocol used by the service, typically \"tcp\" or \"udp\".\n * @default 'tcp'\n */\n protocol?: string;\n /**\n * The service type (e.g., \"http\", \"ipp\").\n * Used to form the full service type domain.\n */\n type: string;\n /**\n * Optional list of subtype identifiers for selective discovery.\n */\n subtypes?: string[];\n /**\n * The instance name of the service.\n * This will be sanitized by replacing dots with dashes.\n * @example 'MyPrinter'\n */\n name: string;\n\n /**\n * The hostname of the machine offering the service.\n * @default os.hostname()\n */\n host?: string;\n /**\n * The port number on which the service is listening.\n */\n port: number;\n\n /**\n * Optional TXT record key-value pairs to advertise service metadata.\n */\n txt?: Record<string, string | number | boolean | Buffer>;\n\n /**\n * The TTL (time to live) in seconds for the all records.\n * @remarks The same TTL is set to all records (PTR, SRV, TXT, A, AAAA) for simplicity and compatibility.\n * @default 4500\n */\n ttl?: number;\n\n /**\n * Whether to perform probing to detect name conflicts before publishing.\n * @default true\n */\n probe?: boolean;\n /**\n * Whether to automatically resolve name conflicts by appending a number to the service name.\n * @default true\n */\n probeAutoResolve?: boolean;\n\n /**\n * Whether to disable publishing IPv6 addresses for this service.\n * @default false\n */\n disableIPv6?: boolean;\n}\n\ninterface ServiceEventMap {\n up: [];\n down: [];\n}\n\n// MARK: Service\n/**\n * Represents a service registered on the local network via mDNS/DNS-SD.\n *\n * This class manages the service's DNS resource records such as PTR, SRV, TXT, A, and AAAA,\n * constructed according to the DNS-Based Service Discovery specification.\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763 | RFC 6763 - DNS-Based Service Discovery}\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6762 | RFC 6762 - Multicast DNS}\n */\nexport class Service extends EventEmitter<ServiceEventMap> implements Required<ServiceOptions> {\n static TLD = '.local';\n\n protocol: string;\n type: string;\n name: string;\n subtypes: string[];\n\n host: string;\n port: number;\n\n txt: Record<string, string | number | boolean | Buffer>;\n\n ttl: number;\n\n probe = true;\n probeAutoResolve = true;\n disableIPv6: boolean;\n\n fqdn: string;\n\n started = false;\n published = false;\n destroyed = false;\n\n registry?: Registry;\n\n constructor(options: ServiceOptions) {\n super();\n\n if (options.port <= 0 || options.port > 65535) {\n throw new Error('Invalid port number');\n }\n\n this.protocol = options.protocol ?? 'tcp';\n this.type = new ServiceType(options.type, this.protocol).toString();\n this.subtypes = options.subtypes ?? [];\n this.name = options.name.split('.').join('-');\n\n this.host = options.host ?? os.hostname();\n this.port = options.port;\n\n this.txt = options.txt ?? {};\n\n this.ttl = options.ttl ?? 4500;\n\n this.probe = options.probe ?? true;\n this.probeAutoResolve = options.probeAutoResolve ?? true;\n\n this.disableIPv6 = options.disableIPv6 ?? false;\n\n this.fqdn = `${this.name}.${this.type}${Service.TLD}`;\n }\n\n /**\n * Starts the registered service, initiating its publication on the network.\n *\n * If the service has already been started, this method does nothing.\n */\n async start(): Promise<void> {\n if (!this.started) {\n this.started = true;\n await this.registry?.onServiceStart(this);\n }\n }\n\n /**\n * Stops the registered service and removes it from the network.\n */\n async stop(): Promise<void> {\n if (this.started) {\n this.started = false;\n await this.registry?.onServiceStop(this);\n }\n }\n\n /**\n * Generates all DNS resource records representing this service instance.\n *\n * This includes:\n * - PTR record for the service type\n * - SRV record with host and port\n * - TXT record with metadata\n * - PTR record advertising the service type in the _services._dns-sd._udp.local domain\n * - PTR records for any declared subtypes\n * - A and AAAA records for each network interface address (unless IPv6 is disabled)\n *\n * @returns An array of DNS resource records suitable for publishing via mDNS.\n */\n getRecords(): MDNSRecord[] {\n const records: MDNSRecord[] = [\n this.getRecordPTR(),\n this.getRecordSRV(),\n this.getRecordTXT(),\n this.getRecordPTRServiceTypeEnumeration(),\n ];\n\n // Handle subtypes\n for (const subtype of this.subtypes) {\n records.push(this.getRecordPTRSubtype(subtype));\n }\n\n // Create record per interface address\n const ifaces = Object.values(os.networkInterfaces());\n for (const iface of ifaces) {\n for (const addr of iface ?? []) {\n if (addr.internal || addr.mac === '00:00:00:00:00:00') {\n continue;\n }\n if (addr.family === 'IPv4') {\n records.push(this.getRecordA(addr.address));\n } else {\n if (this.disableIPv6) continue;\n records.push(this.getRecordAAAA(addr.address));\n }\n }\n }\n\n // Return all records\n return records;\n }\n\n // MARK: private\n /**\n * Constructs the main PTR record pointing to this instance's FQDN.\n *\n * This advertises the presence of a service instance under the service type domain.\n *\n * @returns A PTR record for service instance discovery.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1 | RFC 6763 §4.1 - Structured Service Instance Names}\n */\n private getRecordPTR(): StringAnswer {\n return {\n type: 'PTR',\n name: `${this.type}${Service.TLD}`,\n ttl: this.ttl,\n data: this.fqdn,\n };\n }\n\n /**\n * Constructs a subtype PTR record to support selective instance discovery by subtype.\n *\n * This record maps from a subtype-specific domain to the instance FQDN.\n *\n * @param subtype - The subtype identifier (e.g., \"printer\").\n * @returns A subtype-specific PTR record.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-7.1 | RFC 6763 §7.1 - Selective Instance Enumeration (Subtypes)}\n */\n private getRecordPTRSubtype(subtype: string): StringAnswer {\n return {\n type: 'PTR',\n name: `_${subtype}._sub.${this.type}${Service.TLD}`,\n ttl: this.ttl,\n data: `${this.name}.${this.type}${Service.TLD}`,\n };\n }\n\n /**\n * Constructs a PTR record under the special _services._dns-sd._udp.local name.\n *\n * This enables enumeration of available service types on the local network.\n *\n * @returns A PTR record pointing to this service type.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-9 | RFC 6763 §9 - Service Type Enumeration}\n */\n private getRecordPTRServiceTypeEnumeration(): StringAnswer {\n return {\n type: 'PTR',\n name: `_services._dns-sd._udp${Service.TLD}`,\n ttl: this.ttl,\n data: `${this.type}${Service.TLD}`,\n };\n }\n\n /**\n * Constructs an SRV record that contains the target host and port of this service.\n *\n * This enables clients to locate the host and communication endpoint.\n *\n * @returns An SRV record for this service instance.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1 | RFC 6763 §4.1 - Structured Service Instance Names}\n * @see {@link https://datatracker.ietf.org/doc/html/rfc2782 | RFC 2782 - A DNS RR for specifying the location of services (DNS SRV)}\n */\n private getRecordSRV(): SrvAnswer {\n return {\n type: 'SRV',\n name: this.fqdn,\n ttl: this.ttl,\n data: {\n port: this.port,\n target: this.host,\n },\n };\n }\n\n /**\n * Constructs a TXT record carrying metadata key-value pairs about the service.\n *\n * Keys should be lowercase and limited in length. Empty value fields are valid.\n *\n * @returns A TXT record representing service metadata.\n *\n * The name of a TXT record is the same name as the SRV record\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6 | RFC 6763 §6 - Data Syntax for DNS-SD TXT Records}\n */\n private getRecordTXT(): TxtAnswer {\n return {\n type: 'TXT',\n name: this.fqdn,\n ttl: this.ttl,\n data: encodeTXT(this.txt),\n };\n }\n\n /**\n * Constructs an A record mapping the hostname to an IPv4 address.\n *\n * @param ip - The IPv4 address associated with the service host.\n * @returns An A record for IPv4 address resolution.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-6 | RFC 6762 §6 - Responding}\n */\n private getRecordA(ip: string): StringAnswer {\n return {\n type: 'A',\n name: this.host,\n ttl: this.ttl,\n data: ip,\n };\n }\n\n /**\n * Constructs an AAAA record mapping the hostname to an IPv6 address.\n *\n * @param ip - The IPv6 address associated with the service host.\n * @returns An AAAA record for IPv6 address resolution.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-6 | RFC 6762 §6 - Responding}\n */\n private getRecordAAAA(ip: string): StringAnswer {\n return {\n type: 'AAAA',\n name: this.host,\n ttl: this.ttl,\n data: ip,\n };\n }\n}\n"],"names":["_Service","EventEmitter","options","ServiceType","os","records","subtype","ifaces","iface","addr","encodeTXT","ip","Service"],"mappings":"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"}