matterbridge 3.1.6-dev-20250721-75fab6b → 3.1.7-dev-20250723-aab81fe

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ import { MDNS_MULTICAST_IPV4_ADDRESS, MDNS_MULTICAST_IPV6_ADDRESS, MDNS_MULTICAST_PORT } from './multicast.js';
2
+ import { Mdns } from './mdns.js';
3
+ {
4
+ const mdnsIpv4 = new Mdns('mDNS Server udp4', MDNS_MULTICAST_IPV4_ADDRESS, MDNS_MULTICAST_PORT, 'udp4', true, undefined, '0.0.0.0');
5
+ const mdnsIpv6 = new Mdns('mDNS Server udp6', MDNS_MULTICAST_IPV6_ADDRESS, MDNS_MULTICAST_PORT, 'udp6', true, undefined, '::');
6
+ mdnsIpv4.listNetworkInterfaces();
7
+ function cleanupAndLogAndExit() {
8
+ mdnsIpv4.stop();
9
+ mdnsIpv6.stop();
10
+ mdnsIpv4.logDevices();
11
+ mdnsIpv6.logDevices();
12
+ process.exit(0);
13
+ }
14
+ const queryUdp4 = () => {
15
+ mdnsIpv4.sendQuery([
16
+ { name: '_matter._tcp.local', type: 12, class: 1, unicastResponse: false },
17
+ { name: '_shelly._tcp.local', type: 12, class: 1, unicastResponse: false },
18
+ { name: '_http._tcp.local', type: 12, class: 1, unicastResponse: false },
19
+ { name: '_services._dns-sd._udp.local', type: 12, class: 1, unicastResponse: false },
20
+ ]);
21
+ const ptrRdata = mdnsIpv4.encodeDnsName('matterbridge._http._tcp.local');
22
+ mdnsIpv4.sendResponse('_http._tcp.local', 12, 1, 120, ptrRdata);
23
+ };
24
+ const queryUdp6 = () => {
25
+ mdnsIpv6.sendQuery([
26
+ { name: '_matter._tcp.local', type: 12, class: 1, unicastResponse: true },
27
+ { name: '_shelly._tcp.local', type: 12, class: 1, unicastResponse: true },
28
+ { name: '_http._tcp.local', type: 12, class: 1, unicastResponse: true },
29
+ { name: '_services._dns-sd._udp.local', type: 12, class: 1, unicastResponse: true },
30
+ ]);
31
+ const ptrRdata = mdnsIpv6.encodeDnsName('matterbridge._http._tcp.local');
32
+ mdnsIpv6.sendResponse('_http._tcp.local', 12, 1, 120, ptrRdata);
33
+ };
34
+ process.on('SIGINT', () => {
35
+ cleanupAndLogAndExit();
36
+ });
37
+ mdnsIpv4.start();
38
+ mdnsIpv4.on('ready', (address) => {
39
+ mdnsIpv4.log.info(`mdnsIpv4 server ready on ${address.family} ${address.address}:${address.port}`);
40
+ if (!process.argv.includes('--query'))
41
+ return;
42
+ queryUdp4();
43
+ setInterval(() => {
44
+ queryUdp4();
45
+ }, 10000).unref();
46
+ });
47
+ mdnsIpv6.start();
48
+ mdnsIpv6.on('ready', (address) => {
49
+ mdnsIpv6.log.info(`mdnsIpv6 server ready on ${address.family} ${address.address}:${address.port}`);
50
+ if (!process.argv.includes('--query'))
51
+ return;
52
+ queryUdp6();
53
+ setInterval(() => {
54
+ queryUdp6();
55
+ }, 10000).unref();
56
+ });
57
+ setTimeout(() => {
58
+ cleanupAndLogAndExit();
59
+ }, 600000);
60
+ }
@@ -0,0 +1,595 @@
1
+ import { BLUE, CYAN, db, er, GREEN, idn, MAGENTA, nf, rs } from 'node-ansi-logger';
2
+ import { Multicast } from './multicast.js';
3
+ export var DnsRecordType;
4
+ (function (DnsRecordType) {
5
+ DnsRecordType[DnsRecordType["A"] = 1] = "A";
6
+ DnsRecordType[DnsRecordType["NS"] = 2] = "NS";
7
+ DnsRecordType[DnsRecordType["MD"] = 3] = "MD";
8
+ DnsRecordType[DnsRecordType["MF"] = 4] = "MF";
9
+ DnsRecordType[DnsRecordType["CNAME"] = 5] = "CNAME";
10
+ DnsRecordType[DnsRecordType["SOA"] = 6] = "SOA";
11
+ DnsRecordType[DnsRecordType["MB"] = 7] = "MB";
12
+ DnsRecordType[DnsRecordType["MG"] = 8] = "MG";
13
+ DnsRecordType[DnsRecordType["MR"] = 9] = "MR";
14
+ DnsRecordType[DnsRecordType["NULL"] = 10] = "NULL";
15
+ DnsRecordType[DnsRecordType["WKS"] = 11] = "WKS";
16
+ DnsRecordType[DnsRecordType["PTR"] = 12] = "PTR";
17
+ DnsRecordType[DnsRecordType["HINFO"] = 13] = "HINFO";
18
+ DnsRecordType[DnsRecordType["MINFO"] = 14] = "MINFO";
19
+ DnsRecordType[DnsRecordType["MX"] = 15] = "MX";
20
+ DnsRecordType[DnsRecordType["TXT"] = 16] = "TXT";
21
+ DnsRecordType[DnsRecordType["RP"] = 17] = "RP";
22
+ DnsRecordType[DnsRecordType["AFSDB"] = 18] = "AFSDB";
23
+ DnsRecordType[DnsRecordType["X25"] = 19] = "X25";
24
+ DnsRecordType[DnsRecordType["ISDN"] = 20] = "ISDN";
25
+ DnsRecordType[DnsRecordType["RT"] = 21] = "RT";
26
+ DnsRecordType[DnsRecordType["NSAP"] = 22] = "NSAP";
27
+ DnsRecordType[DnsRecordType["NSAP_PTR"] = 23] = "NSAP_PTR";
28
+ DnsRecordType[DnsRecordType["SIG"] = 24] = "SIG";
29
+ DnsRecordType[DnsRecordType["KEY"] = 25] = "KEY";
30
+ DnsRecordType[DnsRecordType["PX"] = 26] = "PX";
31
+ DnsRecordType[DnsRecordType["GPOS"] = 27] = "GPOS";
32
+ DnsRecordType[DnsRecordType["AAAA"] = 28] = "AAAA";
33
+ DnsRecordType[DnsRecordType["LOC"] = 29] = "LOC";
34
+ DnsRecordType[DnsRecordType["NXT"] = 30] = "NXT";
35
+ DnsRecordType[DnsRecordType["EID"] = 31] = "EID";
36
+ DnsRecordType[DnsRecordType["NIMLOC"] = 32] = "NIMLOC";
37
+ DnsRecordType[DnsRecordType["SRV"] = 33] = "SRV";
38
+ DnsRecordType[DnsRecordType["ATMA"] = 34] = "ATMA";
39
+ DnsRecordType[DnsRecordType["NAPTR"] = 35] = "NAPTR";
40
+ DnsRecordType[DnsRecordType["KX"] = 36] = "KX";
41
+ DnsRecordType[DnsRecordType["CERT"] = 37] = "CERT";
42
+ DnsRecordType[DnsRecordType["A6"] = 38] = "A6";
43
+ DnsRecordType[DnsRecordType["DNAME"] = 39] = "DNAME";
44
+ DnsRecordType[DnsRecordType["SINK"] = 40] = "SINK";
45
+ DnsRecordType[DnsRecordType["OPT"] = 41] = "OPT";
46
+ DnsRecordType[DnsRecordType["APL"] = 42] = "APL";
47
+ DnsRecordType[DnsRecordType["DS"] = 43] = "DS";
48
+ DnsRecordType[DnsRecordType["SSHFP"] = 44] = "SSHFP";
49
+ DnsRecordType[DnsRecordType["IPSECKEY"] = 45] = "IPSECKEY";
50
+ DnsRecordType[DnsRecordType["RRSIG"] = 46] = "RRSIG";
51
+ DnsRecordType[DnsRecordType["NSEC"] = 47] = "NSEC";
52
+ DnsRecordType[DnsRecordType["DNSKEY"] = 48] = "DNSKEY";
53
+ DnsRecordType[DnsRecordType["DHCID"] = 49] = "DHCID";
54
+ DnsRecordType[DnsRecordType["NSEC3"] = 50] = "NSEC3";
55
+ DnsRecordType[DnsRecordType["NSEC3PARAM"] = 51] = "NSEC3PARAM";
56
+ DnsRecordType[DnsRecordType["TLSA"] = 52] = "TLSA";
57
+ DnsRecordType[DnsRecordType["SMIMEA"] = 53] = "SMIMEA";
58
+ DnsRecordType[DnsRecordType["HIP"] = 55] = "HIP";
59
+ DnsRecordType[DnsRecordType["NINFO"] = 56] = "NINFO";
60
+ DnsRecordType[DnsRecordType["RKEY"] = 57] = "RKEY";
61
+ DnsRecordType[DnsRecordType["TALINK"] = 58] = "TALINK";
62
+ DnsRecordType[DnsRecordType["CDS"] = 59] = "CDS";
63
+ DnsRecordType[DnsRecordType["CDNSKEY"] = 60] = "CDNSKEY";
64
+ DnsRecordType[DnsRecordType["OPENPGPKEY"] = 61] = "OPENPGPKEY";
65
+ DnsRecordType[DnsRecordType["CSYNC"] = 62] = "CSYNC";
66
+ DnsRecordType[DnsRecordType["ZONEMD"] = 63] = "ZONEMD";
67
+ DnsRecordType[DnsRecordType["SVCB"] = 64] = "SVCB";
68
+ DnsRecordType[DnsRecordType["HTTPS"] = 65] = "HTTPS";
69
+ DnsRecordType[DnsRecordType["SPF"] = 99] = "SPF";
70
+ DnsRecordType[DnsRecordType["UINFO"] = 100] = "UINFO";
71
+ DnsRecordType[DnsRecordType["UID"] = 101] = "UID";
72
+ DnsRecordType[DnsRecordType["GID"] = 102] = "GID";
73
+ DnsRecordType[DnsRecordType["UNSPEC"] = 103] = "UNSPEC";
74
+ DnsRecordType[DnsRecordType["NID"] = 104] = "NID";
75
+ DnsRecordType[DnsRecordType["L32"] = 105] = "L32";
76
+ DnsRecordType[DnsRecordType["L64"] = 106] = "L64";
77
+ DnsRecordType[DnsRecordType["LP"] = 107] = "LP";
78
+ DnsRecordType[DnsRecordType["EUI48"] = 108] = "EUI48";
79
+ DnsRecordType[DnsRecordType["EUI64"] = 109] = "EUI64";
80
+ DnsRecordType[DnsRecordType["TKEY"] = 249] = "TKEY";
81
+ DnsRecordType[DnsRecordType["TSIG"] = 250] = "TSIG";
82
+ DnsRecordType[DnsRecordType["IXFR"] = 251] = "IXFR";
83
+ DnsRecordType[DnsRecordType["AXFR"] = 252] = "AXFR";
84
+ DnsRecordType[DnsRecordType["MAILB"] = 253] = "MAILB";
85
+ DnsRecordType[DnsRecordType["MAILA"] = 254] = "MAILA";
86
+ DnsRecordType[DnsRecordType["ANY"] = 255] = "ANY";
87
+ DnsRecordType[DnsRecordType["URI"] = 256] = "URI";
88
+ DnsRecordType[DnsRecordType["CAA"] = 257] = "CAA";
89
+ DnsRecordType[DnsRecordType["AVC"] = 258] = "AVC";
90
+ DnsRecordType[DnsRecordType["DOA"] = 259] = "DOA";
91
+ DnsRecordType[DnsRecordType["AMTRELAY"] = 260] = "AMTRELAY";
92
+ DnsRecordType[DnsRecordType["ZONEVERSION"] = 261] = "ZONEVERSION";
93
+ DnsRecordType[DnsRecordType["TA"] = 32768] = "TA";
94
+ DnsRecordType[DnsRecordType["DLV"] = 32769] = "DLV";
95
+ })(DnsRecordType || (DnsRecordType = {}));
96
+ export var DnsClass;
97
+ (function (DnsClass) {
98
+ DnsClass[DnsClass["IN"] = 1] = "IN";
99
+ DnsClass[DnsClass["CH"] = 3] = "CH";
100
+ DnsClass[DnsClass["HS"] = 4] = "HS";
101
+ DnsClass[DnsClass["ANY"] = 255] = "ANY";
102
+ })(DnsClass || (DnsClass = {}));
103
+ export var DnsClassFlag;
104
+ (function (DnsClassFlag) {
105
+ DnsClassFlag[DnsClassFlag["FLUSH"] = 32768] = "FLUSH";
106
+ DnsClassFlag[DnsClassFlag["QU"] = 32768] = "QU";
107
+ })(DnsClassFlag || (DnsClassFlag = {}));
108
+ export class Mdns extends Multicast {
109
+ deviceQueries = new Map();
110
+ deviceResponses = new Map();
111
+ constructor(name, multicastAddress, multicastPort, socketType, reuseAddr = true, interfaceName, interfaceAddress) {
112
+ super(name, multicastAddress, multicastPort, socketType, reuseAddr, interfaceName, interfaceAddress);
113
+ }
114
+ onQuery(rinfo, _query) {
115
+ this.log.debug(`mDNS query received from ${BLUE}${rinfo.family}${db} ${BLUE}${rinfo.address}${db}:${BLUE}${rinfo.port}${db}`);
116
+ }
117
+ onResponse(rinfo, _response) {
118
+ this.log.debug(`mDNS response received from ${BLUE}${rinfo.family}${db} ${BLUE}${rinfo.address}${db}:${BLUE}${rinfo.port}${db}`);
119
+ }
120
+ onMessage(msg, rinfo) {
121
+ this.log.info(`Dgram mDNS server received a mDNS message from ${BLUE}${rinfo.family}${nf} ${BLUE}${rinfo.address}${nf}:${BLUE}${rinfo.port}${nf}`);
122
+ try {
123
+ const result = this.decodeMdnsMessage(msg);
124
+ if (result.qr === 0) {
125
+ this.deviceQueries.set(rinfo.address, { rinfo, query: result });
126
+ this.onQuery(rinfo, result);
127
+ }
128
+ else {
129
+ const ptr = result.answers?.find((record) => record.name === '_shelly._tcp.local' && record.type === 12) ||
130
+ result.answers?.find((record) => record.name === '_http._tcp.local' && record.type === 12) ||
131
+ result.answers?.find((record) => record.type === 12);
132
+ this.deviceResponses.set(rinfo.address, { rinfo, response: result, dataPTR: ptr?.data });
133
+ this.onResponse(rinfo, result);
134
+ }
135
+ this.logMdnsMessage(result);
136
+ }
137
+ catch (error) {
138
+ this.log.error(`Error decoding mDNS message: ${error instanceof Error ? error.message : error}`);
139
+ }
140
+ }
141
+ decodeMdnsMessage(msg) {
142
+ if (msg.length < 12) {
143
+ throw new Error('mDNS message too short');
144
+ }
145
+ const id = msg.readUInt16BE(0);
146
+ const flags = msg.readUInt16BE(2);
147
+ const qr = (flags & 0x8000) >> 15;
148
+ const opcode = (flags & 0x7800) >> 11;
149
+ const aa = Boolean(flags & 0x0400);
150
+ const tc = Boolean(flags & 0x0200);
151
+ const rd = Boolean(flags & 0x0100);
152
+ const ra = Boolean(flags & 0x0080);
153
+ const z = (flags & 0x0070) >> 4;
154
+ const rcode = flags & 0x000f;
155
+ const qdCount = msg.readUInt16BE(4);
156
+ const anCount = msg.readUInt16BE(6);
157
+ const nsCount = msg.readUInt16BE(8);
158
+ const arCount = msg.readUInt16BE(10);
159
+ const mdnsMessage = {
160
+ id,
161
+ qr,
162
+ opcode,
163
+ aa,
164
+ tc,
165
+ rd,
166
+ ra,
167
+ z,
168
+ rcode,
169
+ qdCount,
170
+ anCount,
171
+ nsCount,
172
+ arCount,
173
+ questions: [],
174
+ answers: [],
175
+ authorities: [],
176
+ additionals: [],
177
+ };
178
+ let offset = 12;
179
+ for (let i = 0; i < qdCount; i++) {
180
+ const qnameResult = this.decodeDnsName(msg, offset);
181
+ const qname = qnameResult.name;
182
+ offset = qnameResult.newOffset;
183
+ const qtype = msg.readUInt16BE(offset);
184
+ offset += 2;
185
+ const qclass = msg.readUInt16BE(offset);
186
+ offset += 2;
187
+ mdnsMessage.questions?.push({ name: qname, type: qtype, class: qclass });
188
+ }
189
+ for (let i = 0; i < anCount; i++) {
190
+ const rrResult = this.decodeResourceRecord(msg, offset);
191
+ mdnsMessage.answers?.push(rrResult.record);
192
+ offset = rrResult.newOffset;
193
+ }
194
+ for (let i = 0; i < nsCount; i++) {
195
+ const rrResult = this.decodeResourceRecord(msg, offset);
196
+ mdnsMessage.authorities?.push(rrResult.record);
197
+ offset = rrResult.newOffset;
198
+ }
199
+ for (let i = 0; i < arCount; i++) {
200
+ const rrResult = this.decodeResourceRecord(msg, offset);
201
+ mdnsMessage.additionals?.push(rrResult.record);
202
+ offset = rrResult.newOffset;
203
+ }
204
+ return mdnsMessage;
205
+ }
206
+ decodeDnsName(msg, offset) {
207
+ const labels = [];
208
+ let jumped = false;
209
+ let originalOffset = offset;
210
+ let iterations = 0;
211
+ while (true) {
212
+ if (iterations++ > 1000) {
213
+ throw new Error('Too many iterations while decoding DNS name. Possible malformed message.');
214
+ }
215
+ if (offset >= msg.length) {
216
+ throw new Error('Offset exceeds buffer length while decoding DNS name.');
217
+ }
218
+ const len = msg.readUInt8(offset);
219
+ if (len === 0) {
220
+ offset++;
221
+ break;
222
+ }
223
+ if ((len & 0xc0) === 0xc0) {
224
+ if (offset + 1 >= msg.length) {
225
+ throw new Error('Incomplete pointer encountered while decoding DNS name.');
226
+ }
227
+ const pointer = ((len & 0x3f) << 8) | msg.readUInt8(offset + 1);
228
+ if (!jumped) {
229
+ originalOffset = offset + 2;
230
+ }
231
+ offset = pointer;
232
+ jumped = true;
233
+ continue;
234
+ }
235
+ offset++;
236
+ if (offset + len > msg.length) {
237
+ throw new Error('Label length exceeds buffer bounds while decoding DNS name.');
238
+ }
239
+ labels.push(msg.toString('utf8', offset, offset + len));
240
+ offset += len;
241
+ }
242
+ return { name: labels.join('.'), newOffset: jumped ? originalOffset : offset };
243
+ }
244
+ encodeDnsName(name) {
245
+ const labels = name.split('.');
246
+ const buffers = labels.map((label) => {
247
+ const lenBuf = Buffer.alloc(1);
248
+ lenBuf.writeUInt8(label.length, 0);
249
+ return Buffer.concat([lenBuf, Buffer.from(label)]);
250
+ });
251
+ return Buffer.concat([...buffers, Buffer.from([0])]);
252
+ }
253
+ decodeResourceRecord(msg, offset) {
254
+ const nameResult = this.decodeDnsName(msg, offset);
255
+ const name = nameResult.name;
256
+ offset = nameResult.newOffset;
257
+ const type = msg.readUInt16BE(offset);
258
+ offset += 2;
259
+ const rrclass = msg.readUInt16BE(offset);
260
+ offset += 2;
261
+ const ttl = msg.readUInt32BE(offset);
262
+ offset += 4;
263
+ const rdlength = msg.readUInt16BE(offset);
264
+ offset += 2;
265
+ let data = '';
266
+ if (type === 12) {
267
+ const ptrResult = this.decodeDnsName(msg, offset);
268
+ data = ptrResult.name;
269
+ offset += rdlength;
270
+ }
271
+ else if (type === 16) {
272
+ const txtStrings = [];
273
+ const end = offset + rdlength;
274
+ while (offset < end) {
275
+ const txtLen = msg[offset];
276
+ offset++;
277
+ const txt = msg.slice(offset, offset + txtLen).toString('utf8');
278
+ txtStrings.push(txt);
279
+ offset += txtLen;
280
+ }
281
+ data = txtStrings.join(', ');
282
+ }
283
+ else if (type === 33) {
284
+ const priority = msg.readUInt16BE(offset);
285
+ const weight = msg.readUInt16BE(offset + 2);
286
+ const port = msg.readUInt16BE(offset + 4);
287
+ offset += 6;
288
+ const srvTargetResult = this.decodeDnsName(msg, offset);
289
+ data = JSON.stringify({
290
+ priority,
291
+ weight,
292
+ port,
293
+ target: srvTargetResult.name,
294
+ });
295
+ offset = srvTargetResult.newOffset;
296
+ }
297
+ else if (type === 1) {
298
+ const ipBytes = msg.slice(offset, offset + 4);
299
+ data = Array.from(ipBytes).join('.');
300
+ offset += 4;
301
+ }
302
+ else if (type === 28) {
303
+ const ipBytes = msg.slice(offset, offset + 16);
304
+ const ipv6Parts = [];
305
+ for (let i = 0; i < 16; i += 2) {
306
+ ipv6Parts.push(ipBytes.readUInt16BE(i).toString(16));
307
+ }
308
+ data = ipv6Parts.join(':');
309
+ offset += 16;
310
+ }
311
+ else if (type === 47) {
312
+ const { name: nextDomain, newOffset } = this.decodeDnsName(msg, offset);
313
+ const nextDomainLength = newOffset - offset;
314
+ offset = newOffset;
315
+ const bitmapLength = rdlength - nextDomainLength;
316
+ const bitmapData = msg.slice(offset, offset + bitmapLength);
317
+ const types = [];
318
+ let bitmapOffset = 0;
319
+ while (bitmapOffset < bitmapData.length) {
320
+ const windowBlock = bitmapData[bitmapOffset];
321
+ const windowLength = bitmapData[bitmapOffset + 1];
322
+ bitmapOffset += 2;
323
+ for (let i = 0; i < windowLength; i++) {
324
+ const octet = bitmapData[bitmapOffset + i];
325
+ for (let bit = 0; bit < 8; bit++) {
326
+ if (octet & (0x80 >> bit)) {
327
+ const typeCode = windowBlock * 256 + i * 8 + bit;
328
+ types.push(this.dnsTypeToString(typeCode));
329
+ }
330
+ }
331
+ }
332
+ bitmapOffset += windowLength;
333
+ }
334
+ data = JSON.stringify({
335
+ nextDomain,
336
+ types,
337
+ });
338
+ offset += bitmapLength;
339
+ }
340
+ else {
341
+ data = msg.slice(offset, offset + rdlength).toString('hex');
342
+ offset += rdlength;
343
+ }
344
+ return {
345
+ record: { name, type, class: rrclass, ttl, data },
346
+ newOffset: offset,
347
+ };
348
+ }
349
+ sendQuery(questions) {
350
+ const header = Buffer.alloc(12);
351
+ header.writeUInt16BE(0, 0);
352
+ header.writeUInt16BE(0, 2);
353
+ header.writeUInt16BE(questions.length, 4);
354
+ header.writeUInt16BE(0, 6);
355
+ header.writeUInt16BE(0, 8);
356
+ header.writeUInt16BE(0, 10);
357
+ const questionBuffers = questions.map(({ name, type: qtype, class: qclass, unicastResponse = false }) => {
358
+ const qname = this.encodeDnsName(name);
359
+ const qfields = Buffer.alloc(4);
360
+ qfields.writeUInt16BE(qtype, 0);
361
+ qfields.writeUInt16BE(unicastResponse ? qclass | 32768 : qclass, 2);
362
+ return Buffer.concat([qname, qfields]);
363
+ });
364
+ const query = Buffer.concat([header, ...questionBuffers]);
365
+ const decoded = this.decodeMdnsMessage(query);
366
+ this.logMdnsMessage(decoded);
367
+ this.socket.send(query, 0, query.length, this.multicastPort, this.multicastAddress, (error) => {
368
+ if (error) {
369
+ this.log.error(`Dgram mDNS server failed to send query message: ${error.message}`);
370
+ this.emit('error', error);
371
+ }
372
+ else {
373
+ const names = questions
374
+ .map((q) => `- name ${MAGENTA}${q.name}${db} type ${MAGENTA}${this.dnsTypeToString(q.type)}${db} class ${MAGENTA}${this.dnsQuestionClassToString(q.class)}${db} unicastResponse ${MAGENTA}${q.unicastResponse}${db}`)
375
+ .join('\n');
376
+ this.log.debug(`Dgram mDNS server sent query message:\n${names}`);
377
+ this.emit('sent', query, this.multicastAddress, this.multicastPort);
378
+ }
379
+ });
380
+ }
381
+ sendResponse(name, rtype, rclass, ttl, rdata) {
382
+ const header = Buffer.alloc(12);
383
+ header.writeUInt16BE(0, 0);
384
+ header.writeUInt16BE(0x8400, 2);
385
+ header.writeUInt16BE(0, 4);
386
+ header.writeUInt16BE(1, 6);
387
+ header.writeUInt16BE(0, 8);
388
+ header.writeUInt16BE(0, 10);
389
+ const aname = this.encodeDnsName(name);
390
+ const answerFixed = Buffer.alloc(10);
391
+ answerFixed.writeUInt16BE(rtype, 0);
392
+ answerFixed.writeUInt16BE(rclass, 2);
393
+ answerFixed.writeUInt32BE(ttl, 4);
394
+ answerFixed.writeUInt16BE(rdata.length, 8);
395
+ const answer = Buffer.concat([aname, answerFixed, rdata]);
396
+ const response = Buffer.concat([header, answer]);
397
+ this.socket.send(response, 0, response.length, this.multicastPort, this.multicastAddress, (error) => {
398
+ if (error) {
399
+ this.log.error(`Dgram mDNS server failed to send response message for ${MAGENTA}${name}${er} type ${MAGENTA}${this.dnsTypeToString(rtype)}${er} class ${MAGENTA}${this.dnsResponseClassToString(rclass)}${er} ttl ${MAGENTA}${ttl}${er}: ${error instanceof Error ? error.message : error}`);
400
+ this.emit('error', error);
401
+ }
402
+ else {
403
+ this.log.debug(`Dgram mDNS server sent response message for ${MAGENTA}${name}${db} type ${MAGENTA}${this.dnsTypeToString(rtype)}${db} class ${MAGENTA}${this.dnsResponseClassToString(rclass)}${db} ttl ${MAGENTA}${ttl}${db}`);
404
+ this.emit('sent', response, this.multicastAddress, this.multicastPort);
405
+ }
406
+ });
407
+ }
408
+ dnsTypeToString(type) {
409
+ const typeMap = {
410
+ [1]: 'A',
411
+ [2]: 'NS',
412
+ [3]: 'MD',
413
+ [4]: 'MF',
414
+ [5]: 'CNAME',
415
+ [6]: 'SOA',
416
+ [7]: 'MB',
417
+ [8]: 'MG',
418
+ [9]: 'MR',
419
+ [10]: 'NULL',
420
+ [11]: 'WKS',
421
+ [12]: 'PTR',
422
+ [13]: 'HINFO',
423
+ [14]: 'MINFO',
424
+ [15]: 'MX',
425
+ [16]: 'TXT',
426
+ [17]: 'RP',
427
+ [18]: 'AFSDB',
428
+ [19]: 'X25',
429
+ [20]: 'ISDN',
430
+ [21]: 'RT',
431
+ [22]: 'NSAP',
432
+ [23]: 'NSAP_PTR',
433
+ [24]: 'SIG',
434
+ [25]: 'KEY',
435
+ [26]: 'PX',
436
+ [27]: 'GPOS',
437
+ [28]: 'AAAA',
438
+ [29]: 'LOC',
439
+ [30]: 'NXT',
440
+ [31]: 'EID',
441
+ [32]: 'NIMLOC',
442
+ [33]: 'SRV',
443
+ [34]: 'ATMA',
444
+ [35]: 'NAPTR',
445
+ [36]: 'KX',
446
+ [37]: 'CERT',
447
+ [38]: 'A6',
448
+ [39]: 'DNAME',
449
+ [40]: 'SINK',
450
+ [41]: 'OPT',
451
+ [42]: 'APL',
452
+ [43]: 'DS',
453
+ [44]: 'SSHFP',
454
+ [45]: 'IPSECKEY',
455
+ [46]: 'RRSIG',
456
+ [47]: 'NSEC',
457
+ [48]: 'DNSKEY',
458
+ [49]: 'DHCID',
459
+ [50]: 'NSEC3',
460
+ [51]: 'NSEC3PARAM',
461
+ [52]: 'TLSA',
462
+ [53]: 'SMIMEA',
463
+ [55]: 'HIP',
464
+ [56]: 'NINFO',
465
+ [57]: 'RKEY',
466
+ [58]: 'TALINK',
467
+ [59]: 'CDS',
468
+ [60]: 'CDNSKEY',
469
+ [61]: 'OPENPGPKEY',
470
+ [62]: 'CSYNC',
471
+ [63]: 'ZONEMD',
472
+ [64]: 'SVCB',
473
+ [65]: 'HTTPS',
474
+ [99]: 'SPF',
475
+ [100]: 'UINFO',
476
+ [101]: 'UID',
477
+ [102]: 'GID',
478
+ [103]: 'UNSPEC',
479
+ [104]: 'NID',
480
+ [105]: 'L32',
481
+ [106]: 'L64',
482
+ [107]: 'LP',
483
+ [108]: 'EUI48',
484
+ [109]: 'EUI64',
485
+ [249]: 'TKEY',
486
+ [250]: 'TSIG',
487
+ [251]: 'IXFR',
488
+ [252]: 'AXFR',
489
+ [253]: 'MAILB',
490
+ [254]: 'MAILA',
491
+ [255]: 'ANY',
492
+ [256]: 'URI',
493
+ [257]: 'CAA',
494
+ [258]: 'AVC',
495
+ [259]: 'DOA',
496
+ [260]: 'AMTRELAY',
497
+ [261]: 'ZONEVERSION',
498
+ [32768]: 'TA',
499
+ [32769]: 'DLV',
500
+ };
501
+ return typeMap[type] ?? `TYPE${type}`;
502
+ }
503
+ dnsResponseClassToString(cls) {
504
+ const isFlush = !!(cls & 32768);
505
+ const baseClass = cls & 0x7fff;
506
+ let classStr;
507
+ switch (baseClass) {
508
+ case 1:
509
+ classStr = 'IN';
510
+ break;
511
+ case 3:
512
+ classStr = 'CH';
513
+ break;
514
+ case 4:
515
+ classStr = 'HS';
516
+ break;
517
+ case 255:
518
+ classStr = 'ANY';
519
+ break;
520
+ default:
521
+ classStr = `CLASS${baseClass}`;
522
+ }
523
+ return isFlush ? `${classStr}|FLUSH` : classStr;
524
+ }
525
+ dnsQuestionClassToString(cls) {
526
+ const isQU = !!(cls & 32768);
527
+ const baseClass = cls & 0x7fff;
528
+ let classStr;
529
+ switch (baseClass) {
530
+ case 1:
531
+ classStr = 'IN';
532
+ break;
533
+ case 3:
534
+ classStr = 'CH';
535
+ break;
536
+ case 4:
537
+ classStr = 'HS';
538
+ break;
539
+ case 255:
540
+ classStr = 'ANY';
541
+ break;
542
+ default:
543
+ classStr = `CLASS${baseClass}`;
544
+ }
545
+ return isQU ? `${classStr}|QU` : classStr;
546
+ }
547
+ logMdnsMessage(msg) {
548
+ this.log.info(`Decoded mDNS message: ID ${MAGENTA}${msg.id}${nf}, QR ${GREEN}${msg.qr === 0 ? 'Query' : 'Response'}${nf}, OPCODE ${MAGENTA}${msg.opcode}${nf}, AA ${MAGENTA}${msg.aa}${nf}, TC ${MAGENTA}${msg.tc}${nf}, RD ${MAGENTA}${msg.rd}${nf}, RA ${MAGENTA}${msg.ra}${nf}, Z ${MAGENTA}${msg.z}${nf}, RCODE ${MAGENTA}${msg.rcode}${nf}, QDCount ${MAGENTA}${msg.qdCount}${nf}, ANCount ${MAGENTA}${msg.anCount}${nf}, NSCount ${MAGENTA}${msg.nsCount}${nf}, ARCount ${MAGENTA}${msg.arCount}${nf}`);
549
+ msg.questions?.forEach((question) => {
550
+ this.log.info(`Question: ${CYAN}${question.name}${nf} type ${idn}${this.dnsTypeToString(question.type)}${rs}${nf} class ${CYAN}${this.dnsQuestionClassToString(question.class)}${nf}`);
551
+ });
552
+ msg.answers?.forEach((answer) => {
553
+ this.log.info(`Answer: ${CYAN}${answer.name}${nf} type ${idn}${this.dnsTypeToString(answer.type)}${rs}${nf} class ${CYAN}${this.dnsResponseClassToString(answer.class)}${nf} ttl ${CYAN}${answer.ttl}${nf} data ${CYAN}${answer.data}${nf}`);
554
+ });
555
+ msg.authorities?.forEach((authority) => {
556
+ this.log.info(`Authority: ${CYAN}${authority.name}${nf} type ${idn}${this.dnsTypeToString(authority.type)}${rs}${nf} class ${CYAN}${this.dnsResponseClassToString(authority.class)}${nf} ttl ${CYAN}${authority.ttl}${nf} data ${CYAN}${authority.data}${nf}`);
557
+ });
558
+ msg.additionals?.forEach((additional) => {
559
+ this.log.info(`Additional: ${CYAN}${additional.name}${nf} type ${idn}${this.dnsTypeToString(additional.type)}${rs}${nf} class ${CYAN}${this.dnsResponseClassToString(additional.class)}${nf} ttl ${CYAN}${additional.ttl}${nf} data ${CYAN}${additional.data}${nf}`);
560
+ });
561
+ this.log.info(`---\n`);
562
+ }
563
+ logDevices() {
564
+ this.log.info(`Discovered query devices: ${MAGENTA}${this.deviceQueries.size}${nf}`);
565
+ const deviceQueryArray = Array.from(this.deviceQueries.entries());
566
+ deviceQueryArray.sort(([addressA], [addressB]) => {
567
+ const partsA = addressA.split('.').map(Number);
568
+ const partsB = addressB.split('.').map(Number);
569
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
570
+ const diff = (partsA[i] || 0) - (partsB[i] || 0);
571
+ if (diff !== 0)
572
+ return diff;
573
+ }
574
+ return 0;
575
+ });
576
+ deviceQueryArray.forEach(([rinfo, response]) => {
577
+ this.log.info(`- ${MAGENTA}${rinfo}${nf} family ${BLUE}${response.rinfo.family}${nf} address ${BLUE}${response.rinfo.address}${nf} port ${BLUE}${response.rinfo.port}${nf}`);
578
+ });
579
+ this.log.info(`Discovered response devices: ${MAGENTA}${this.deviceResponses.size}${nf}`);
580
+ const deviceResponseArray = Array.from(this.deviceResponses.entries());
581
+ deviceResponseArray.sort(([addressA], [addressB]) => {
582
+ const partsA = addressA.split(/[:.]/).map((part) => parseInt(part, 16));
583
+ const partsB = addressB.split(/[:.]/).map((part) => parseInt(part, 16));
584
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
585
+ const diff = (partsA[i] || 0) - (partsB[i] || 0);
586
+ if (diff !== 0)
587
+ return diff;
588
+ }
589
+ return 0;
590
+ });
591
+ deviceResponseArray.forEach(([rinfo, response]) => {
592
+ this.log.info(`- ${MAGENTA}${rinfo}${nf} family ${BLUE}${response.rinfo.family}${nf} address ${BLUE}${response.rinfo.address}${nf} port ${BLUE}${response.rinfo.port}${nf} PTR ${GREEN}${response.dataPTR}${nf}`);
593
+ });
594
+ }
595
+ }