@collight/dns-sd
Version:
A DNS Service Discovery implementation in modern TypeScript
1 lines • 11.1 kB
Source Map (JSON)
{"version":3,"file":"DiscoveredService.cjs","sources":["../../../src/browser/DiscoveredService.ts"],"sourcesContent":["import { RemoteInfo } from 'dgram';\nimport { Answer, StringAnswer } from 'dns-packet';\nimport { ResponsePacket } from 'multicast-dns';\n\nimport { decodeTXT, nameEquals, ServiceType } from '../utils';\n\n// MARK: DiscoveredService\n/**\n * Represents a discovered mDNS/DNS-SD service instance, as defined in the DNS-Based Service Discovery specification.\n *\n * A `DiscoveredService` is constructed from multiple DNS resource records typically found in an mDNS response:\n * - A {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1 | PTR record} that points to the instance's FQDN.\n * - An {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.2 | SRV record} that provides the target host and port.\n * - A {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6 | TXT record} containing key-value metadata.\n * - One or more {@link https://datatracker.ietf.org/doc/html/rfc6762#section-5.4 | A or AAAA records} for IP addresses.\n * - Optionally, additional {@link https://datatracker.ietf.org/doc/html/rfc6763#section-7.1 | subtype PTR records} for selective discovery.\n *\n * All records are typically retrieved from a single mDNS response.\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4 | RFC 6763 § 4: Service Instances}\n * @see {@link https://datatracker.ietf.org/doc/html/rfc2782 | RFC 2782 - DNS SRV Records}\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6762 | RFC 6762 - Multicast DNS}\n */\nexport class DiscoveredService {\n /**\n * Creates a list of `DiscoveredService` instances from a DNS response packet.\n *\n * This function filters out records with expired TTL, finds matching PTR records for the given service name,\n * and builds structured `DiscoveredService` objects by resolving their associated SRV, TXT, A/AAAA, and subtype PTR records.\n *\n * @internal\n * @param packet - The full mDNS response packet containing DNS records.\n * @param referer - Network information about the source of the packet.\n * @returns An array of discovered service instances matching the given name.\n */\n static fromResponse(packet: ResponsePacket, referer: RemoteInfo): DiscoveredService[] {\n const answers: Answer[] = [];\n\n // Collect all relevant resource records with valid TTL (ignores goodbye messages)\n for (const answer of [...packet.answers, ...packet.additionals]) {\n if ('ttl' in answer && answer.ttl !== undefined && answer.ttl > 0) {\n answers.push(answer);\n }\n }\n\n const services: DiscoveredService[] = [];\n\n // Find matching PTR records and construct DiscoveredService instances\n for (const ptr of answers) {\n if (ptr.type === 'PTR') {\n const service = this.fromPTR(ptr, answers, referer);\n if (service) {\n services.push(service);\n }\n }\n }\n\n return services;\n }\n\n /**\n * Constructs a `DiscoveredService` instance from a PTR record and its associated resource records.\n *\n * This method finds the corresponding SRV record for service connection details, optional TXT records\n * for metadata, subtype PTR records for selective discovery, and A/AAAA records for resolving IP addresses.\n *\n * @internal\n * @param ptr - The PTR record pointing to a service instance.\n * @param answers - A list of resource records from the DNS response to search through.\n * @param referer - The network information of the source of the response.\n * @returns A `DiscoveredService` instance or `null` if a valid SRV record is not found.\n */\n private static fromPTR(ptr: StringAnswer, answers: Answer[], referer: RemoteInfo): DiscoveredService | null {\n let service: DiscoveredService | undefined;\n\n // Look for the SRV record that defines host and port\n for (const answer of answers) {\n if (answer.type === 'SRV' && nameEquals(answer.name, ptr.data)) {\n const fqdn = answer.name;\n const name = fqdn.split('.')[0];\n if (name === undefined) {\n console.error('Invalid service name', name);\n return null;\n }\n\n const host = answer.data.target;\n const port = answer.data.port;\n\n /** @remarks Only the TTL of the PTR record is used for simplicity and compatibility */\n service = new DiscoveredService(name, fqdn, host, port, referer, ptr.ttl, Date.now());\n }\n }\n\n if (!service) return null;\n\n // Attach TXT metadata if available\n for (const answer of answers) {\n if (answer.type === 'TXT' && nameEquals(answer.name, ptr.data)) {\n service.txt = decodeTXT(answer.data, /* binary */ false);\n service.binaryTxt = decodeTXT(answer.data, /* binary */ true);\n service.rawTxt = answer.data;\n }\n }\n\n // Collect any advertised subtypes (via _sub PTR records)\n for (const answer of answers) {\n if (answer.type === 'PTR' && nameEquals(answer.data, ptr.data) && answer.name.includes('._sub')) {\n const types = ServiceType.fromString(answer.name);\n if (types.subtype !== undefined) {\n service.subtypes.push(types.subtype);\n }\n }\n }\n\n // Resolve host IP addresses (A/AAAA)\n for (const answer of answers) {\n if ((answer.type === 'A' || answer.type === 'AAAA') && service.host && nameEquals(answer.name, service.host)) {\n service.addresses.push(answer.data);\n }\n }\n\n return service;\n }\n\n /**\n * The registered service type (e.g., _http, _ipp).\n * @example \"ipp\"\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.2}\n */\n type?: string;\n /**\n * Network protocol used by the service (_tcp or _udp).\n * @example \"tcp\"\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.2}\n */\n protocol?: string;\n /**\n * Subtypes advertised by the service for selective discovery.\n * @example [\"scanner\", \"fax\"]\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-7.1}\n */\n subtypes: string[] = [];\n\n /**\n * Resolved IPv4/IPv6 addresses of the service host.\n * @example [\"192.168.1.42\", \"fe80::1c2a:3bff:fe4e:1234\"]\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-5.4}\n */\n addresses: string[] = [];\n\n /**\n * Decoded TXT record key-value pairs.\n * @example { \"note\": \"Office printer\", \"paper\": \"A4\" }\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6}\n */\n txt?: Record<string, string>;\n /**\n * Decoded binary TXT record key-value pairs.\n * @example { \"note\": <Buffer 04 6e 6f 74 65 0b 4f 66 69 63 65 20 50 72 69 6e 74 65 72> }\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6}\n */\n binaryTxt?: Record<string, Buffer>;\n /**\n * Raw TXT record data as received over the network.\n * @example <Buffer 04 6e 6f 74 65 0b 4f 66 66 69 63 65 20 50 72 69 6e 74 65 72>\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6}\n */\n rawTxt?: string | Buffer | (string | Buffer)[];\n\n /**\n * Timer for checking TTL expiration.\n */\n ttlTimer?: NodeJS.Timeout;\n\n private constructor(\n /**\n * Short instance name of the service (first label of the FQDN).\n * @example \"MyPrinter\"\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.1}\n */\n readonly name: string,\n /**\n * Fully qualified domain name (e.g., MyPrinter._ipp._tcp.local).\n * @example \"MyPrinter._ipp._tcp.local\"\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.1}\n */\n readonly fqdn: string,\n /**\n * Hostname of the machine offering the service (SRV record target).\n * @example \"MyPrinter.local\"\n * @see {@link https://datatracker.ietf.org/doc/html/rfc2782}\n */\n readonly host: string,\n /**\n * TCP or UDP port on which the service is running.\n * @example 631\n * @see {@link https://datatracker.ietf.org/doc/html/rfc2782}\n */\n readonly port: number,\n /**\n * Network info about where the service response came from.\n * @example { address: \"192.168.1.101\", family: \"IPv4\", port: 5353, size: 412 }\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-5}\n */\n readonly referer: RemoteInfo,\n\n /**\n * Time-to-live value in seconds.\n * @example 120\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-10}\n */\n readonly ttl: number | undefined,\n /**\n * Last time the service was seen (in milliseconds since the Unix epoch).\n * @example 1634567890000\n */\n readonly lastSeen: number,\n ) {\n const serviceType = ServiceType.fromString(fqdn.split('.').slice(1, -1).join('.'));\n this.type = serviceType.name;\n this.protocol = serviceType.protocol;\n }\n\n get expired(): boolean {\n return this.ttl !== undefined && Date.now() > this.lastSeen + this.ttl * 1000;\n }\n}\n"],"names":["DiscoveredService","name","fqdn","host","port","referer","ttl","lastSeen","serviceType","ServiceType","packet","answers","answer","services","ptr","service","nameEquals","decodeTXT","types"],"mappings":"0QAuBO,MAAMA,CAAkB,CAuJrB,YAMGC,EAMAC,EAMAC,EAMAC,EAMAC,EAOAC,EAKAC,EACT,CArCS,KAAA,KAAAN,EAMA,KAAA,KAAAC,EAMA,KAAA,KAAAC,EAMA,KAAA,KAAAC,EAMA,KAAA,QAAAC,EAOA,KAAA,IAAAC,EAKA,KAAA,SAAAC,EA3EX,KAAA,SAAqB,CAAA,EAOrB,KAAA,UAAsB,CAAA,EAsEpB,MAAMC,EAAcC,EAAAA,YAAY,WAAWP,EAAK,MAAM,GAAG,EAAE,MAAM,EAAG,EAAE,EAAE,KAAK,GAAG,CAAC,EACjF,KAAK,KAAOM,EAAY,KACxB,KAAK,SAAWA,EAAY,QAC9B,CA1LA,OAAO,aAAaE,EAAwBL,EAA0C,CACpF,MAAMM,EAAoB,CAAA,EAG1B,UAAWC,IAAU,CAAC,GAAGF,EAAO,QAAS,GAAGA,EAAO,WAAW,EACxD,QAASE,GAAUA,EAAO,MAAQ,QAAaA,EAAO,IAAM,GAC9DD,EAAQ,KAAKC,CAAM,EAIvB,MAAMC,EAAgC,CAAA,EAGtC,UAAWC,KAAOH,EAChB,GAAIG,EAAI,OAAS,MAAO,CACtB,MAAMC,EAAU,KAAK,QAAQD,EAAKH,EAASN,CAAO,EAC9CU,GACFF,EAAS,KAAKE,CAAO,CAEzB,CAGF,OAAOF,CACT,CAcA,OAAe,QAAQC,EAAmBH,EAAmBN,EAA+C,CAC1G,IAAIU,EAGJ,UAAWH,KAAUD,EACnB,GAAIC,EAAO,OAAS,OAASI,EAAAA,WAAWJ,EAAO,KAAME,EAAI,IAAI,EAAG,CAC9D,MAAMZ,EAAOU,EAAO,KACdX,EAAOC,EAAK,MAAM,GAAG,EAAE,CAAC,EAC9B,GAAID,IAAS,OACX,eAAQ,MAAM,uBAAwBA,CAAI,EACnC,KAGT,MAAME,EAAOS,EAAO,KAAK,OACnBR,EAAOQ,EAAO,KAAK,KAGzBG,EAAU,IAAIf,EAAkBC,EAAMC,EAAMC,EAAMC,EAAMC,EAASS,EAAI,IAAK,KAAK,IAAA,CAAK,CACtF,CAGF,GAAI,CAACC,EAAS,OAAO,KAGrB,UAAWH,KAAUD,EACfC,EAAO,OAAS,OAASI,EAAAA,WAAWJ,EAAO,KAAME,EAAI,IAAI,IAC3DC,EAAQ,IAAME,EAAAA,UAAUL,EAAO,KAAmB,EAAA,EAClDG,EAAQ,UAAYE,EAAAA,UAAUL,EAAO,KAAmB,EAAA,EACxDG,EAAQ,OAASH,EAAO,MAK5B,UAAWA,KAAUD,EACnB,GAAIC,EAAO,OAAS,OAASI,EAAAA,WAAWJ,EAAO,KAAME,EAAI,IAAI,GAAKF,EAAO,KAAK,SAAS,OAAO,EAAG,CAC/F,MAAMM,EAAQT,EAAAA,YAAY,WAAWG,EAAO,IAAI,EAC5CM,EAAM,UAAY,QACpBH,EAAQ,SAAS,KAAKG,EAAM,OAAO,CAEvC,CAIF,UAAWN,KAAUD,GACdC,EAAO,OAAS,KAAOA,EAAO,OAAS,SAAWG,EAAQ,MAAQC,EAAAA,WAAWJ,EAAO,KAAMG,EAAQ,IAAI,GACzGA,EAAQ,UAAU,KAAKH,EAAO,IAAI,EAItC,OAAOG,CACT,CAqGA,IAAI,SAAmB,CACrB,OAAO,KAAK,MAAQ,QAAa,KAAK,MAAQ,KAAK,SAAW,KAAK,IAAM,GAC3E,CACF"}