UNPKG

@collight/dns-sd

Version:

A DNS Service Discovery implementation in modern TypeScript

1 lines 7.74 kB
{"version":3,"file":"MDNSServer.cjs","sources":["../../../src/utils/MDNSServer.ts"],"sourcesContent":["import { EventEmitter } from 'events';\nimport { RecordType, SrvAnswer, StringAnswer, TxtAnswer } from 'dns-packet';\nimport deepEqual from 'fast-deep-equal';\nimport mDNS, { MulticastDNS, QueryPacket } from 'multicast-dns';\n\nimport { nameEquals } from './dns-utils';\n\n/**\n * Represents a DNS Resource Record relevant for mDNS responses.\n */\nexport type MDNSRecord = StringAnswer | TxtAnswer | SrvAnswer;\n\ntype EventMap = {\n responded: [packet: mDNS.ResponseOutgoingPacket, error: Error | null, bytes?: number];\n};\n\n// MARK: MDNSServer\n/**\n * An mDNS server that responds to DNS queries on the local network.\n *\n * Maintains a registry of published records and automatically responds\n * to queries using the multicast-dns protocol.\n *\n * Emits:\n * - `responded` when a response is sent to a query\n *\n * @example\n * ```ts\n * const server = new MDNSServer({}, console.error)\n * server.register([{ name: '_http._tcp.local', type: 'PTR', data: 'MyService._http._tcp.local' }])\n * ```\n *\n * @see https://datatracker.ietf.org/doc/html/rfc6762\n */\nexport class MDNSServer extends EventEmitter<EventMap> {\n mdns: MulticastDNS;\n\n private typeToRecords = new Map<RecordType, MDNSRecord[]>();\n\n /**\n * @param options - Configuration passed to the underlying `multicast-dns` instance.\n */\n constructor(options: mDNS.Options) {\n super();\n\n this.mdns = mDNS(options);\n this.mdns.setMaxListeners(0);\n this.mdns.on('query', q => this.respond(q));\n }\n\n /**\n * Registers a list of DNS records to be used in future responses.\n *\n * Duplicate records (based on type, name, and deep equality of data) are ignored.\n *\n * @param records - An array of mDNS records to register.\n */\n register(records: MDNSRecord[]): void {\n function isDuplicate(a: MDNSRecord, b: MDNSRecord): boolean {\n return a.type === b.type && a.name === b.name && deepEqual(a.data, b.data);\n }\n\n for (const record of records) {\n const type = record.type;\n const records = this.typeToRecords.get(type) ?? [];\n if (records.every(r => !isDuplicate(r, record))) {\n records.push(record);\n }\n this.typeToRecords.set(type, records);\n }\n }\n\n /**\n * Unregisters a set of previously registered DNS records.\n *\n * Matching is done by `record.name` only (not deeply).\n *\n * @param records - Records to remove from the server registry.\n */\n unregister(records: MDNSRecord[]): void {\n for (const record of records) {\n const type = record.type;\n const records = this.typeToRecords.get(type) ?? [];\n const filtered = records.filter(r => r.name !== record.name);\n if (filtered.length > 0) {\n this.typeToRecords.set(type, filtered);\n } else {\n this.typeToRecords.delete(type);\n }\n }\n }\n\n // MARK: private\n /**\n * Returns all records of a given type matching a name.\n *\n * If the name is not fully qualified (no dot), only the first segment of the record name is compared.\n *\n * @param type - DNS record type.\n * @param name - Name to match against.\n * @returns Matching records.\n */\n private getRecordsOf(type: RecordType, name: string): MDNSRecord[] {\n const records: MDNSRecord[] = [];\n for (const record of this.typeToRecords.get(type) ?? []) {\n const targetName = name.includes('.') ? record.name : record.name.split('.')[0];\n if (targetName !== undefined && nameEquals(targetName, name)) {\n records.push(record);\n }\n }\n return records;\n }\n\n /**\n * Responds to incoming mDNS queries with matching registered records.\n * Handles `ANY` queries and populates additional records for `PTR → SRV → A/AAAA + TXT` dependencies,\n *\n * @param query - The incoming mDNS query packet.\n *\n * @see {@link https://www.rfc-editor.org/rfc/rfc6763#section-12}\n */\n private respond(query: QueryPacket): void {\n for (const question of query.questions) {\n const queryType = question.type as RecordType | 'ANY';\n const queryName = question.name;\n\n let answers: MDNSRecord[];\n if (queryType === 'ANY') {\n answers = Array.from(this.typeToRecords.keys()).flatMap(type => this.getRecordsOf(type, queryName));\n } else {\n answers = this.getRecordsOf(queryType, queryName);\n }\n\n if (answers.length === 0) continue;\n\n const additionals: MDNSRecord[] = [];\n if (queryType !== 'ANY') {\n for (const answer of answers) {\n // Add SRV and TXT records if PTR is present\n if (answer.type === 'PTR') {\n additionals.push(...this.getRecordsOf('SRV', answer.data));\n additionals.push(...this.getRecordsOf('TXT', answer.data));\n }\n }\n\n // Collect unique SRV targets to enrich with A/AAAA\n const targets = new Set<string>();\n for (const record of additionals) {\n if (record.type === 'SRV') {\n const target = record.data.target;\n targets.add(target);\n }\n }\n\n for (const target of targets) {\n additionals.push(...this.getRecordsOf('A', target));\n additionals.push(...this.getRecordsOf('AAAA', target));\n }\n }\n\n const packet: mDNS.ResponseOutgoingPacket = { answers, additionals };\n this.mdns.respond(packet, (...args) => {\n this.emit('responded', packet, ...args);\n });\n }\n }\n}\n"],"names":["MDNSServer","EventEmitter","options","mDNS","q","records","isDuplicate","a","b","deepEqual","record","type","filtered","name","targetName","nameEquals","query","question","queryType","queryName","answers","additionals","answer","targets","target","packet","args"],"mappings":"+LAkCO,MAAMA,UAAmBC,EAAAA,YAAuB,CAQrD,YAAYC,EAAuB,CACjC,MAAA,EANF,KAAQ,kBAAoB,IAQ1B,KAAK,KAAOC,EAAKD,CAAO,EACxB,KAAK,KAAK,gBAAgB,CAAC,EAC3B,KAAK,KAAK,GAAG,WAAc,KAAK,QAAQE,CAAC,CAAC,CAC5C,CASA,SAASC,EAA6B,CACpC,SAASC,EAAYC,EAAeC,EAAwB,CAC1D,OAAOD,EAAE,OAASC,EAAE,MAAQD,EAAE,OAASC,EAAE,MAAQC,EAAUF,EAAE,KAAMC,EAAE,IAAI,CAC3E,CAEA,UAAWE,KAAUL,EAAS,CAC5B,MAAMM,EAAOD,EAAO,KACdL,EAAU,KAAK,cAAc,IAAIM,CAAI,GAAK,CAAA,EAC5CN,EAAQ,MAAM,GAAK,CAACC,EAAY,EAAGI,CAAM,CAAC,GAC5CL,EAAQ,KAAKK,CAAM,EAErB,KAAK,cAAc,IAAIC,EAAMN,CAAO,CACtC,CACF,CASA,WAAWA,EAA6B,CACtC,UAAWK,KAAUL,EAAS,CAC5B,MAAMM,EAAOD,EAAO,KAEdE,GADU,KAAK,cAAc,IAAID,CAAI,GAAK,CAAA,GACvB,UAAY,EAAE,OAASD,EAAO,IAAI,EACvDE,EAAS,OAAS,EACpB,KAAK,cAAc,IAAID,EAAMC,CAAQ,EAErC,KAAK,cAAc,OAAOD,CAAI,CAElC,CACF,CAYQ,aAAaA,EAAkBE,EAA4B,CACjE,MAAMR,EAAwB,CAAA,EAC9B,UAAWK,KAAU,KAAK,cAAc,IAAIC,CAAI,GAAK,GAAI,CACvD,MAAMG,EAAaD,EAAK,SAAS,GAAG,EAAIH,EAAO,KAAOA,EAAO,KAAK,MAAM,GAAG,EAAE,CAAC,EAC1EI,IAAe,QAAaC,EAAAA,WAAWD,EAAYD,CAAI,GACzDR,EAAQ,KAAKK,CAAM,CAEvB,CACA,OAAOL,CACT,CAUQ,QAAQW,EAA0B,CACxC,UAAWC,KAAYD,EAAM,UAAW,CACtC,MAAME,EAAYD,EAAS,KACrBE,EAAYF,EAAS,KAE3B,IAAIG,EAOJ,GANIF,IAAc,MAChBE,EAAU,MAAM,KAAK,KAAK,cAAc,KAAA,CAAM,EAAE,QAAQT,GAAQ,KAAK,aAAaA,EAAMQ,CAAS,CAAC,EAElGC,EAAU,KAAK,aAAaF,EAAWC,CAAS,EAG9CC,EAAQ,SAAW,EAAG,SAE1B,MAAMC,EAA4B,CAAA,EAClC,GAAIH,IAAc,MAAO,CACvB,UAAWI,KAAUF,EAEfE,EAAO,OAAS,QAClBD,EAAY,KAAK,GAAG,KAAK,aAAa,MAAOC,EAAO,IAAI,CAAC,EACzDD,EAAY,KAAK,GAAG,KAAK,aAAa,MAAOC,EAAO,IAAI,CAAC,GAK7D,MAAMC,MAAc,IACpB,UAAWb,KAAUW,EACnB,GAAIX,EAAO,OAAS,MAAO,CACzB,MAAMc,EAASd,EAAO,KAAK,OAC3Ba,EAAQ,IAAIC,CAAM,CACpB,CAGF,UAAWA,KAAUD,EACnBF,EAAY,KAAK,GAAG,KAAK,aAAa,IAAKG,CAAM,CAAC,EAClDH,EAAY,KAAK,GAAG,KAAK,aAAa,OAAQG,CAAM,CAAC,CAEzD,CAEA,MAAMC,EAAsC,CAAE,QAAAL,EAAS,YAAAC,CAAA,EACvD,KAAK,KAAK,QAAQI,EAAQ,IAAIC,IAAS,CACrC,KAAK,KAAK,YAAaD,EAAQ,GAAGC,CAAI,CACxC,CAAC,CACH,CACF,CACF"}