@basmilius/apple-common 0.8.2 → 0.9.2
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.
- package/dist/index.d.mts +199 -77
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +670 -36
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -5
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { randomBytes, randomFillSync, randomUUID } from "node:crypto";
|
|
2
|
-
import
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { Socket, createConnection } from "node:net";
|
|
3
6
|
import { createInterface } from "node:readline";
|
|
7
|
+
import { createSocket } from "node:dgram";
|
|
8
|
+
import { networkInterfaces } from "node:os";
|
|
4
9
|
import { EventEmitter } from "node:events";
|
|
5
|
-
import { Socket } from "node:net";
|
|
6
10
|
import { NTP, OPack, TLV8 } from "@basmilius/apple-encoding";
|
|
7
11
|
import { Chacha20, Curve25519, Ed25519, hkdf } from "@basmilius/apple-encryption";
|
|
8
12
|
import { SRP, SrpClient } from "fast-srp-hap";
|
|
9
|
-
import { createSocket } from "node:dgram";
|
|
10
|
-
import { networkInterfaces } from "node:os";
|
|
11
13
|
|
|
14
|
+
//#region \0rolldown/runtime.js
|
|
15
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
12
18
|
//#region ../../node_modules/.bun/uuid@13.0.0/node_modules/uuid/dist-node/stringify.js
|
|
13
19
|
const byteToHex = [];
|
|
14
20
|
for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1));
|
|
@@ -53,6 +59,178 @@ function v4(options, buf, offset) {
|
|
|
53
59
|
return _v4(options, buf, offset);
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/airplayFeatures.ts
|
|
64
|
+
const AirPlayFeatureFlags = {
|
|
65
|
+
SupportsAirPlayVideoV1: 1n << 0n,
|
|
66
|
+
SupportsAirPlayPhoto: 1n << 1n,
|
|
67
|
+
SupportsAirPlayVideoFairPlay: 1n << 2n,
|
|
68
|
+
SupportsAirPlayVideoVolumeControl: 1n << 3n,
|
|
69
|
+
SupportsAirPlayVideoHTTPLiveStreams: 1n << 4n,
|
|
70
|
+
SupportsAirPlaySlideShow: 1n << 5n,
|
|
71
|
+
SupportsAirPlayScreen: 1n << 7n,
|
|
72
|
+
SupportsAirPlayAudio: 1n << 9n,
|
|
73
|
+
AudioRedundant: 1n << 11n,
|
|
74
|
+
Authentication_4: 1n << 14n,
|
|
75
|
+
MetadataFeatures_0: 1n << 15n,
|
|
76
|
+
MetadataFeatures_1: 1n << 16n,
|
|
77
|
+
MetadataFeatures_2: 1n << 17n,
|
|
78
|
+
AudioFormats_0: 1n << 18n,
|
|
79
|
+
AudioFormats_1: 1n << 19n,
|
|
80
|
+
AudioFormats_2: 1n << 20n,
|
|
81
|
+
AudioFormats_3: 1n << 21n,
|
|
82
|
+
Authentication_1: 1n << 23n,
|
|
83
|
+
Authentication_8: 1n << 26n,
|
|
84
|
+
SupportsLegacyPairing: 1n << 27n,
|
|
85
|
+
HasUnifiedAdvertiserInfo: 1n << 30n,
|
|
86
|
+
IsCarPlay: 1n << 32n,
|
|
87
|
+
SupportsAirPlayVideoPlayQueue: 1n << 33n,
|
|
88
|
+
SupportsAirPlayFromCloud: 1n << 34n,
|
|
89
|
+
SupportsTLS_PSK: 1n << 35n,
|
|
90
|
+
SupportsUnifiedMediaControl: 1n << 38n,
|
|
91
|
+
SupportsBufferedAudio: 1n << 40n,
|
|
92
|
+
SupportsPTP: 1n << 41n,
|
|
93
|
+
SupportsScreenMultiCodec: 1n << 42n,
|
|
94
|
+
SupportsSystemPairing: 1n << 43n,
|
|
95
|
+
IsAPValeriaScreenSender: 1n << 44n,
|
|
96
|
+
SupportsHKPairingAndAccessControl: 1n << 46n,
|
|
97
|
+
SupportsCoreUtilsPairingAndEncryption: 1n << 48n,
|
|
98
|
+
SupportsAirPlayVideoV2: 1n << 49n,
|
|
99
|
+
MetadataFeatures_3: 1n << 50n,
|
|
100
|
+
SupportsUnifiedPairSetupAndMFi: 1n << 51n,
|
|
101
|
+
SupportsSetPeersExtendedMessage: 1n << 52n,
|
|
102
|
+
SupportsAPSync: 1n << 54n,
|
|
103
|
+
SupportsWoL: 1n << 55n,
|
|
104
|
+
SupportsWoL2: 1n << 56n,
|
|
105
|
+
SupportsHangdogRemoteControl: 1n << 58n,
|
|
106
|
+
SupportsAudioStreamConnectionSetup: 1n << 59n,
|
|
107
|
+
SupportsAudioMetadataControl: 1n << 60n,
|
|
108
|
+
SupportsRFC2198Redundancy: 1n << 61n
|
|
109
|
+
};
|
|
110
|
+
const PASSWORD_BIT = 128n;
|
|
111
|
+
const LEGACY_PAIRING_BIT = 512n;
|
|
112
|
+
const PIN_REQUIRED_BIT = 8n;
|
|
113
|
+
const parseFeatures = (features) => {
|
|
114
|
+
const parts = features.split(",").map((part) => part.trim());
|
|
115
|
+
if (parts.length === 1) return BigInt(parts[0]);
|
|
116
|
+
if (parts.length === 2) {
|
|
117
|
+
const low = BigInt(parts[0]);
|
|
118
|
+
return BigInt(parts[1]) << 32n | low;
|
|
119
|
+
}
|
|
120
|
+
throw new Error(`Invalid features format: ${features}`);
|
|
121
|
+
};
|
|
122
|
+
const hasFeatureFlag = (features, flag) => (features & flag) !== 0n;
|
|
123
|
+
const describeFlags = (features) => {
|
|
124
|
+
const result = [];
|
|
125
|
+
for (const [name, flag] of Object.entries(AirPlayFeatureFlags)) if (hasFeatureFlag(features, flag)) result.push(name);
|
|
126
|
+
return result;
|
|
127
|
+
};
|
|
128
|
+
const getProtocolVersion = (txt) => {
|
|
129
|
+
const featuresStr = txt.features ?? txt.ft;
|
|
130
|
+
if (!featuresStr) return 1;
|
|
131
|
+
const features = parseFeatures(featuresStr);
|
|
132
|
+
if (hasFeatureFlag(features, AirPlayFeatureFlags.SupportsUnifiedMediaControl)) return 2;
|
|
133
|
+
if (hasFeatureFlag(features, AirPlayFeatureFlags.SupportsCoreUtilsPairingAndEncryption)) return 2;
|
|
134
|
+
return 1;
|
|
135
|
+
};
|
|
136
|
+
const getPairingRequirement = (txt) => {
|
|
137
|
+
const featuresStr = txt.features ?? txt.ft;
|
|
138
|
+
if (!featuresStr) return "none";
|
|
139
|
+
const features = parseFeatures(featuresStr);
|
|
140
|
+
const sf = txt.sf ? BigInt(txt.sf) : 0n;
|
|
141
|
+
if (hasFeatureFlag(features, AirPlayFeatureFlags.SupportsHKPairingAndAccessControl)) return "homekit";
|
|
142
|
+
if ((sf & PIN_REQUIRED_BIT) !== 0n) return "pin";
|
|
143
|
+
if (hasFeatureFlag(features, AirPlayFeatureFlags.SupportsSystemPairing)) return "transient";
|
|
144
|
+
if ((sf & LEGACY_PAIRING_BIT) !== 0n) return "pin";
|
|
145
|
+
return "none";
|
|
146
|
+
};
|
|
147
|
+
const isPasswordRequired = (txt) => {
|
|
148
|
+
if (txt.pw === "true") return true;
|
|
149
|
+
return ((txt.sf ? BigInt(txt.sf) : 0n) & PASSWORD_BIT) !== 0n;
|
|
150
|
+
};
|
|
151
|
+
const isRemoteControlSupported = (txt) => {
|
|
152
|
+
const featuresStr = txt.features ?? txt.ft;
|
|
153
|
+
if (!featuresStr) return false;
|
|
154
|
+
return hasFeatureFlag(parseFeatures(featuresStr), AirPlayFeatureFlags.SupportsHangdogRemoteControl);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/storage.ts
|
|
159
|
+
const credentialKey = (deviceId, protocol) => `${deviceId}:${protocol}`;
|
|
160
|
+
const serializeCredentials = (credentials) => ({
|
|
161
|
+
accessoryIdentifier: credentials.accessoryIdentifier,
|
|
162
|
+
accessoryLongTermPublicKey: credentials.accessoryLongTermPublicKey.toString("base64"),
|
|
163
|
+
pairingId: credentials.pairingId.toString("base64"),
|
|
164
|
+
publicKey: credentials.publicKey.toString("base64"),
|
|
165
|
+
secretKey: credentials.secretKey.toString("base64")
|
|
166
|
+
});
|
|
167
|
+
const deserializeCredentials = (stored) => ({
|
|
168
|
+
accessoryIdentifier: stored.accessoryIdentifier,
|
|
169
|
+
accessoryLongTermPublicKey: Buffer.from(stored.accessoryLongTermPublicKey, "base64"),
|
|
170
|
+
pairingId: Buffer.from(stored.pairingId, "base64"),
|
|
171
|
+
publicKey: Buffer.from(stored.publicKey, "base64"),
|
|
172
|
+
secretKey: Buffer.from(stored.secretKey, "base64")
|
|
173
|
+
});
|
|
174
|
+
const createEmptyData = () => ({
|
|
175
|
+
version: 1,
|
|
176
|
+
devices: {},
|
|
177
|
+
credentials: {}
|
|
178
|
+
});
|
|
179
|
+
var Storage = class {
|
|
180
|
+
#data = createEmptyData();
|
|
181
|
+
get data() {
|
|
182
|
+
return this.#data;
|
|
183
|
+
}
|
|
184
|
+
setData(data) {
|
|
185
|
+
this.#data = data;
|
|
186
|
+
}
|
|
187
|
+
getDevice(identifier) {
|
|
188
|
+
return this.#data.devices[identifier];
|
|
189
|
+
}
|
|
190
|
+
setDevice(identifier, device) {
|
|
191
|
+
this.#data.devices[identifier] = device;
|
|
192
|
+
}
|
|
193
|
+
removeDevice(identifier) {
|
|
194
|
+
delete this.#data.devices[identifier];
|
|
195
|
+
for (const key of Object.keys(this.#data.credentials)) if (key.startsWith(`${identifier}:`)) delete this.#data.credentials[key];
|
|
196
|
+
}
|
|
197
|
+
listDevices() {
|
|
198
|
+
return Object.values(this.#data.devices);
|
|
199
|
+
}
|
|
200
|
+
getCredentials(deviceId, protocol) {
|
|
201
|
+
const stored = this.#data.credentials[credentialKey(deviceId, protocol)];
|
|
202
|
+
if (!stored) return;
|
|
203
|
+
return deserializeCredentials(stored);
|
|
204
|
+
}
|
|
205
|
+
setCredentials(deviceId, protocol, credentials) {
|
|
206
|
+
this.#data.credentials[credentialKey(deviceId, protocol)] = serializeCredentials(credentials);
|
|
207
|
+
}
|
|
208
|
+
removeCredentials(deviceId, protocol) {
|
|
209
|
+
delete this.#data.credentials[credentialKey(deviceId, protocol)];
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
var JsonStorage = class extends Storage {
|
|
213
|
+
#path;
|
|
214
|
+
constructor(path) {
|
|
215
|
+
super();
|
|
216
|
+
this.#path = path ?? join(process.env.HOME ?? process.env.USERPROFILE ?? ".", ".config", "apple-protocols", "storage.json");
|
|
217
|
+
}
|
|
218
|
+
async load() {
|
|
219
|
+
if (!existsSync(this.#path)) return;
|
|
220
|
+
const raw = readFileSync(this.#path, "utf-8");
|
|
221
|
+
const json = JSON.parse(raw);
|
|
222
|
+
if (json.version === 1) this.setData(json);
|
|
223
|
+
}
|
|
224
|
+
async save() {
|
|
225
|
+
mkdirSync(dirname(this.#path), { recursive: true });
|
|
226
|
+
writeFileSync(this.#path, JSON.stringify(this.data, null, 2), "utf-8");
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var MemoryStorage = class extends Storage {
|
|
230
|
+
async load() {}
|
|
231
|
+
async save() {}
|
|
232
|
+
};
|
|
233
|
+
|
|
56
234
|
//#endregion
|
|
57
235
|
//#region src/cli.ts
|
|
58
236
|
async function prompt(message) {
|
|
@@ -77,23 +255,452 @@ const AIRPLAY_SERVICE = "_airplay._tcp.local";
|
|
|
77
255
|
const COMPANION_LINK_SERVICE = "_companion-link._tcp.local";
|
|
78
256
|
const RAOP_SERVICE = "_raop._tcp.local";
|
|
79
257
|
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/mdns.ts
|
|
260
|
+
const MDNS_ADDRESS = "224.0.0.251";
|
|
261
|
+
const MDNS_PORT = 5353;
|
|
262
|
+
const QUERY_ID = 13823;
|
|
263
|
+
const SERVICES_PER_MSG = 3;
|
|
264
|
+
const QueryType = {
|
|
265
|
+
A: 1,
|
|
266
|
+
PTR: 12,
|
|
267
|
+
TXT: 16,
|
|
268
|
+
AAAA: 28,
|
|
269
|
+
SRV: 33,
|
|
270
|
+
ANY: 255
|
|
271
|
+
};
|
|
272
|
+
const encodeQName = (name) => {
|
|
273
|
+
const parts = [];
|
|
274
|
+
const labels = splitServiceName(name);
|
|
275
|
+
for (const label of labels) {
|
|
276
|
+
const encoded = Buffer.from(label, "utf-8");
|
|
277
|
+
if (encoded.byteLength > 63) parts.push(Buffer.from([63]), encoded.subarray(0, 63));
|
|
278
|
+
else parts.push(Buffer.from([encoded.byteLength]), encoded);
|
|
279
|
+
}
|
|
280
|
+
parts.push(Buffer.from([0]));
|
|
281
|
+
return Buffer.concat(parts);
|
|
282
|
+
};
|
|
283
|
+
const splitServiceName = (name) => {
|
|
284
|
+
const match = name.match(/\._[a-z]+\._(?:tcp|udp)\.local$/);
|
|
285
|
+
if (match) return [name.substring(0, match.index), ...match[0].substring(1).split(".")];
|
|
286
|
+
return name.split(".");
|
|
287
|
+
};
|
|
288
|
+
const encodeDnsHeader = (header) => {
|
|
289
|
+
const buf = Buffer.allocUnsafe(12);
|
|
290
|
+
buf.writeUInt16BE(header.id, 0);
|
|
291
|
+
buf.writeUInt16BE(header.flags, 2);
|
|
292
|
+
buf.writeUInt16BE(header.qdcount, 4);
|
|
293
|
+
buf.writeUInt16BE(header.ancount, 6);
|
|
294
|
+
buf.writeUInt16BE(header.nscount, 8);
|
|
295
|
+
buf.writeUInt16BE(header.arcount, 10);
|
|
296
|
+
return buf;
|
|
297
|
+
};
|
|
298
|
+
const encodeDnsQuestion = (name, qtype, unicastResponse = false) => {
|
|
299
|
+
const qname = encodeQName(name);
|
|
300
|
+
const suffix = Buffer.allocUnsafe(4);
|
|
301
|
+
suffix.writeUInt16BE(qtype, 0);
|
|
302
|
+
suffix.writeUInt16BE(unicastResponse ? 32769 : 1, 2);
|
|
303
|
+
return Buffer.concat([qname, suffix]);
|
|
304
|
+
};
|
|
305
|
+
const createQueryPackets = (services, qtype = QueryType.PTR, unicastResponse = false) => {
|
|
306
|
+
const packets = [];
|
|
307
|
+
for (let i = 0; i < services.length; i += SERVICES_PER_MSG) {
|
|
308
|
+
const chunk = services.slice(i, i + SERVICES_PER_MSG);
|
|
309
|
+
const questions = chunk.map((s) => encodeDnsQuestion(s, qtype, unicastResponse));
|
|
310
|
+
const header = encodeDnsHeader({
|
|
311
|
+
id: QUERY_ID,
|
|
312
|
+
flags: 0,
|
|
313
|
+
qdcount: chunk.length,
|
|
314
|
+
ancount: 0,
|
|
315
|
+
nscount: 0,
|
|
316
|
+
arcount: 0
|
|
317
|
+
});
|
|
318
|
+
packets.push(Buffer.concat([header, ...questions]));
|
|
319
|
+
}
|
|
320
|
+
return packets;
|
|
321
|
+
};
|
|
322
|
+
const decodeQName = (buf, offset) => {
|
|
323
|
+
const labels = [];
|
|
324
|
+
let currentOffset = offset;
|
|
325
|
+
let jumped = false;
|
|
326
|
+
let returnOffset = offset;
|
|
327
|
+
while (currentOffset < buf.byteLength) {
|
|
328
|
+
const length = buf[currentOffset];
|
|
329
|
+
if (length === 0) {
|
|
330
|
+
if (!jumped) returnOffset = currentOffset + 1;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
if ((length & 192) === 192) {
|
|
334
|
+
const pointer = (length & 63) << 8 | buf[currentOffset + 1];
|
|
335
|
+
if (!jumped) returnOffset = currentOffset + 2;
|
|
336
|
+
currentOffset = pointer;
|
|
337
|
+
jumped = true;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
currentOffset++;
|
|
341
|
+
labels.push(buf.toString("utf-8", currentOffset, currentOffset + length));
|
|
342
|
+
currentOffset += length;
|
|
343
|
+
if (!jumped) returnOffset = currentOffset;
|
|
344
|
+
}
|
|
345
|
+
return [labels.join("."), returnOffset];
|
|
346
|
+
};
|
|
347
|
+
const decodeDnsHeader = (buf) => ({
|
|
348
|
+
id: buf.readUInt16BE(0),
|
|
349
|
+
flags: buf.readUInt16BE(2),
|
|
350
|
+
qdcount: buf.readUInt16BE(4),
|
|
351
|
+
ancount: buf.readUInt16BE(6),
|
|
352
|
+
nscount: buf.readUInt16BE(8),
|
|
353
|
+
arcount: buf.readUInt16BE(10)
|
|
354
|
+
});
|
|
355
|
+
const decodeQuestion = (buf, offset) => {
|
|
356
|
+
const [qname, newOffset] = decodeQName(buf, offset);
|
|
357
|
+
return [{
|
|
358
|
+
qname,
|
|
359
|
+
qtype: buf.readUInt16BE(newOffset),
|
|
360
|
+
qclass: buf.readUInt16BE(newOffset + 2)
|
|
361
|
+
}, newOffset + 4];
|
|
362
|
+
};
|
|
363
|
+
const decodeTxtRecord = (buf, offset, length) => {
|
|
364
|
+
const properties = {};
|
|
365
|
+
let pos = offset;
|
|
366
|
+
const end = offset + length;
|
|
367
|
+
while (pos < end) {
|
|
368
|
+
const strLen = buf[pos];
|
|
369
|
+
pos++;
|
|
370
|
+
if (strLen === 0 || pos + strLen > end) break;
|
|
371
|
+
const str = buf.toString("utf-8", pos, pos + strLen);
|
|
372
|
+
pos += strLen;
|
|
373
|
+
const eqIndex = str.indexOf("=");
|
|
374
|
+
if (eqIndex >= 0) properties[str.substring(0, eqIndex)] = str.substring(eqIndex + 1);
|
|
375
|
+
else properties[str] = "";
|
|
376
|
+
}
|
|
377
|
+
return properties;
|
|
378
|
+
};
|
|
379
|
+
const decodeSrvRecord = (buf, offset) => {
|
|
380
|
+
const priority = buf.readUInt16BE(offset);
|
|
381
|
+
const weight = buf.readUInt16BE(offset + 2);
|
|
382
|
+
const port = buf.readUInt16BE(offset + 4);
|
|
383
|
+
const [target] = decodeQName(buf, offset + 6);
|
|
384
|
+
return {
|
|
385
|
+
priority,
|
|
386
|
+
weight,
|
|
387
|
+
port,
|
|
388
|
+
target
|
|
389
|
+
};
|
|
390
|
+
};
|
|
391
|
+
const decodeResource = (buf, offset) => {
|
|
392
|
+
const [qname, nameEnd] = decodeQName(buf, offset);
|
|
393
|
+
const qtype = buf.readUInt16BE(nameEnd);
|
|
394
|
+
const qclass = buf.readUInt16BE(nameEnd + 2);
|
|
395
|
+
const ttl = buf.readUInt32BE(nameEnd + 4);
|
|
396
|
+
const rdLength = buf.readUInt16BE(nameEnd + 8);
|
|
397
|
+
const rdOffset = nameEnd + 10;
|
|
398
|
+
let rdata;
|
|
399
|
+
switch (qtype) {
|
|
400
|
+
case QueryType.A:
|
|
401
|
+
rdata = `${buf[rdOffset]}.${buf[rdOffset + 1]}.${buf[rdOffset + 2]}.${buf[rdOffset + 3]}`;
|
|
402
|
+
break;
|
|
403
|
+
case QueryType.AAAA: {
|
|
404
|
+
const parts = [];
|
|
405
|
+
for (let i = 0; i < 8; i++) parts.push(buf.readUInt16BE(rdOffset + i * 2).toString(16));
|
|
406
|
+
rdata = parts.join(":");
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case QueryType.PTR: {
|
|
410
|
+
const [name] = decodeQName(buf, rdOffset);
|
|
411
|
+
rdata = name;
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
case QueryType.SRV:
|
|
415
|
+
rdata = decodeSrvRecord(buf, rdOffset);
|
|
416
|
+
break;
|
|
417
|
+
case QueryType.TXT:
|
|
418
|
+
rdata = decodeTxtRecord(buf, rdOffset, rdLength);
|
|
419
|
+
break;
|
|
420
|
+
default: rdata = buf.subarray(rdOffset, rdOffset + rdLength);
|
|
421
|
+
}
|
|
422
|
+
return [{
|
|
423
|
+
qname,
|
|
424
|
+
qtype,
|
|
425
|
+
qclass,
|
|
426
|
+
ttl,
|
|
427
|
+
rdata
|
|
428
|
+
}, rdOffset + rdLength];
|
|
429
|
+
};
|
|
430
|
+
const decodeDnsResponse = (buf) => {
|
|
431
|
+
const header = decodeDnsHeader(buf);
|
|
432
|
+
let offset = 12;
|
|
433
|
+
for (let i = 0; i < header.qdcount; i++) {
|
|
434
|
+
const [, newOffset] = decodeQuestion(buf, offset);
|
|
435
|
+
offset = newOffset;
|
|
436
|
+
}
|
|
437
|
+
const answers = [];
|
|
438
|
+
for (let i = 0; i < header.ancount; i++) {
|
|
439
|
+
const [record, newOffset] = decodeResource(buf, offset);
|
|
440
|
+
answers.push(record);
|
|
441
|
+
offset = newOffset;
|
|
442
|
+
}
|
|
443
|
+
for (let i = 0; i < header.nscount; i++) {
|
|
444
|
+
const [, newOffset] = decodeResource(buf, offset);
|
|
445
|
+
offset = newOffset;
|
|
446
|
+
}
|
|
447
|
+
const resources = [];
|
|
448
|
+
for (let i = 0; i < header.arcount; i++) {
|
|
449
|
+
const [record, newOffset] = decodeResource(buf, offset);
|
|
450
|
+
resources.push(record);
|
|
451
|
+
offset = newOffset;
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
header,
|
|
455
|
+
answers,
|
|
456
|
+
resources
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
var ServiceCollector = class {
|
|
460
|
+
#ptrMap = /* @__PURE__ */ new Map();
|
|
461
|
+
#srvMap = /* @__PURE__ */ new Map();
|
|
462
|
+
#txtMap = /* @__PURE__ */ new Map();
|
|
463
|
+
#addressMap = /* @__PURE__ */ new Map();
|
|
464
|
+
addRecords(answers, resources) {
|
|
465
|
+
for (const record of [...answers, ...resources]) switch (record.qtype) {
|
|
466
|
+
case QueryType.PTR: {
|
|
467
|
+
const existing = this.#ptrMap.get(record.qname);
|
|
468
|
+
if (existing) existing.add(record.rdata);
|
|
469
|
+
else this.#ptrMap.set(record.qname, new Set([record.rdata]));
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
case QueryType.SRV:
|
|
473
|
+
this.#srvMap.set(record.qname, record.rdata);
|
|
474
|
+
break;
|
|
475
|
+
case QueryType.TXT:
|
|
476
|
+
this.#txtMap.set(record.qname, record.rdata);
|
|
477
|
+
break;
|
|
478
|
+
case QueryType.A:
|
|
479
|
+
this.#addressMap.set(record.qname, record.rdata);
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
get services() {
|
|
484
|
+
const results = [];
|
|
485
|
+
for (const [serviceType, instanceNames] of this.#ptrMap) for (const instanceQName of instanceNames) {
|
|
486
|
+
const srv = this.#srvMap.get(instanceQName);
|
|
487
|
+
if (!srv || srv.port === 0) continue;
|
|
488
|
+
const address = this.#addressMap.get(srv.target);
|
|
489
|
+
if (!address) continue;
|
|
490
|
+
const txt = this.#txtMap.get(instanceQName) ?? {};
|
|
491
|
+
const typeIndex = instanceQName.indexOf("._");
|
|
492
|
+
const name = typeIndex >= 0 ? instanceQName.substring(0, typeIndex) : instanceQName;
|
|
493
|
+
if (!results.some((s) => s.name === name && s.type === serviceType)) results.push({
|
|
494
|
+
name,
|
|
495
|
+
type: serviceType,
|
|
496
|
+
address,
|
|
497
|
+
port: srv.port,
|
|
498
|
+
properties: txt
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return results;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
const WAKE_PORTS$1 = [
|
|
505
|
+
7e3,
|
|
506
|
+
3689,
|
|
507
|
+
49152,
|
|
508
|
+
32498
|
|
509
|
+
];
|
|
510
|
+
const knock = (address) => {
|
|
511
|
+
const promises = WAKE_PORTS$1.map((port) => new Promise((resolve) => {
|
|
512
|
+
const socket = createConnection({
|
|
513
|
+
host: address,
|
|
514
|
+
port,
|
|
515
|
+
timeout: 500
|
|
516
|
+
});
|
|
517
|
+
socket.on("connect", () => {
|
|
518
|
+
socket.destroy();
|
|
519
|
+
resolve();
|
|
520
|
+
});
|
|
521
|
+
socket.on("error", () => {
|
|
522
|
+
socket.destroy();
|
|
523
|
+
resolve();
|
|
524
|
+
});
|
|
525
|
+
socket.on("timeout", () => {
|
|
526
|
+
socket.destroy();
|
|
527
|
+
resolve();
|
|
528
|
+
});
|
|
529
|
+
}));
|
|
530
|
+
return Promise.all(promises).then(() => {});
|
|
531
|
+
};
|
|
532
|
+
const unicast = (hosts, services, timeout = 4) => {
|
|
533
|
+
return new Promise((resolve) => {
|
|
534
|
+
const queries = createQueryPackets(services);
|
|
535
|
+
const collector = new ServiceCollector();
|
|
536
|
+
const sockets = [];
|
|
537
|
+
let resolved = false;
|
|
538
|
+
const finish = () => {
|
|
539
|
+
if (resolved) return;
|
|
540
|
+
resolved = true;
|
|
541
|
+
clearInterval(interval);
|
|
542
|
+
for (const socket of sockets) try {
|
|
543
|
+
socket.close();
|
|
544
|
+
} catch {}
|
|
545
|
+
resolve(collector.services);
|
|
546
|
+
};
|
|
547
|
+
for (const host of hosts) {
|
|
548
|
+
const socket = createSocket("udp4");
|
|
549
|
+
sockets.push(socket);
|
|
550
|
+
socket.on("message", (data) => {
|
|
551
|
+
try {
|
|
552
|
+
const response = decodeDnsResponse(data);
|
|
553
|
+
collector.addRecords(response.answers, response.resources);
|
|
554
|
+
} catch {}
|
|
555
|
+
});
|
|
556
|
+
socket.on("error", () => {});
|
|
557
|
+
}
|
|
558
|
+
let interval;
|
|
559
|
+
Promise.all(hosts.map((h) => knock(h))).then(() => {
|
|
560
|
+
const sendQueries = () => {
|
|
561
|
+
for (let i = 0; i < hosts.length; i++) for (const query of queries) sockets[i]?.send(query, MDNS_PORT, hosts[i]);
|
|
562
|
+
};
|
|
563
|
+
sendQueries();
|
|
564
|
+
interval = setInterval(sendQueries, 1e3);
|
|
565
|
+
setTimeout(finish, timeout * 1e3);
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
const multicast = (services, timeout = 4) => {
|
|
570
|
+
return new Promise((resolve) => {
|
|
571
|
+
const collector = new ServiceCollector();
|
|
572
|
+
const queries = createQueryPackets(services);
|
|
573
|
+
const sockets = [];
|
|
574
|
+
let resolved = false;
|
|
575
|
+
let interval;
|
|
576
|
+
const finish = () => {
|
|
577
|
+
if (resolved) return;
|
|
578
|
+
resolved = true;
|
|
579
|
+
clearInterval(interval);
|
|
580
|
+
for (const socket of sockets) try {
|
|
581
|
+
socket.close();
|
|
582
|
+
} catch {}
|
|
583
|
+
resolve(collector.services);
|
|
584
|
+
};
|
|
585
|
+
const onMessage = (data) => {
|
|
586
|
+
try {
|
|
587
|
+
const response = decodeDnsResponse(data);
|
|
588
|
+
collector.addRecords(response.answers, response.resources);
|
|
589
|
+
} catch {}
|
|
590
|
+
};
|
|
591
|
+
const addSocket = (address, port) => {
|
|
592
|
+
return new Promise((resolveSocket) => {
|
|
593
|
+
const socket = createSocket({
|
|
594
|
+
type: "udp4",
|
|
595
|
+
reuseAddr: true
|
|
596
|
+
});
|
|
597
|
+
socket.on("message", onMessage);
|
|
598
|
+
socket.on("error", () => {
|
|
599
|
+
resolveSocket(null);
|
|
600
|
+
});
|
|
601
|
+
socket.bind(port, address ?? "", () => {
|
|
602
|
+
if (address) try {
|
|
603
|
+
socket.setMulticastInterface(address);
|
|
604
|
+
socket.addMembership(MDNS_ADDRESS, address);
|
|
605
|
+
} catch {}
|
|
606
|
+
else try {
|
|
607
|
+
socket.addMembership(MDNS_ADDRESS);
|
|
608
|
+
} catch {}
|
|
609
|
+
sockets.push(socket);
|
|
610
|
+
resolveSocket(socket);
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
};
|
|
614
|
+
const getPrivateAddresses = () => {
|
|
615
|
+
try {
|
|
616
|
+
const { networkInterfaces } = __require("node:os");
|
|
617
|
+
const interfaces = networkInterfaces();
|
|
618
|
+
const addresses = [];
|
|
619
|
+
for (const nets of Object.values(interfaces)) for (const net of nets) if (net.family === "IPv4" && net.internal === false) addresses.push(net.address);
|
|
620
|
+
return addresses;
|
|
621
|
+
} catch {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
const setup = async () => {
|
|
626
|
+
await addSocket(null, MDNS_PORT);
|
|
627
|
+
for (const address of getPrivateAddresses()) await addSocket(address, 0);
|
|
628
|
+
if (sockets.length === 0) {
|
|
629
|
+
resolve([]);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const sendQueries = () => {
|
|
633
|
+
for (const socket of sockets) for (const query of queries) try {
|
|
634
|
+
socket.send(query, MDNS_PORT, MDNS_ADDRESS);
|
|
635
|
+
} catch {}
|
|
636
|
+
};
|
|
637
|
+
sendQueries();
|
|
638
|
+
interval = setInterval(sendQueries, 1e3);
|
|
639
|
+
setTimeout(finish, timeout * 1e3);
|
|
640
|
+
};
|
|
641
|
+
setup();
|
|
642
|
+
});
|
|
643
|
+
};
|
|
644
|
+
|
|
80
645
|
//#endregion
|
|
81
646
|
//#region src/discovery.ts
|
|
647
|
+
const CACHE_TTL = 3e4;
|
|
648
|
+
const WAKE_PORTS = [
|
|
649
|
+
7e3,
|
|
650
|
+
3689,
|
|
651
|
+
49152,
|
|
652
|
+
32498
|
|
653
|
+
];
|
|
654
|
+
const toDiscoveryResult = (service) => {
|
|
655
|
+
const txt = service.properties;
|
|
656
|
+
const featuresStr = txt.features ?? txt.ft;
|
|
657
|
+
const model = txt.model ?? txt.am ?? "";
|
|
658
|
+
const protocol = service.type.includes("._tcp") ? "tcp" : "udp";
|
|
659
|
+
const hostname = service.name.replace(/\s+/g, "-");
|
|
660
|
+
return {
|
|
661
|
+
id: `${hostname}.local`,
|
|
662
|
+
fqdn: `${hostname}.local`,
|
|
663
|
+
address: service.address,
|
|
664
|
+
modelName: model,
|
|
665
|
+
familyName: null,
|
|
666
|
+
txt,
|
|
667
|
+
features: featuresStr ? tryParseFeatures(featuresStr) : void 0,
|
|
668
|
+
service: {
|
|
669
|
+
port: service.port,
|
|
670
|
+
protocol,
|
|
671
|
+
type: service.type
|
|
672
|
+
},
|
|
673
|
+
packet: null
|
|
674
|
+
};
|
|
675
|
+
};
|
|
676
|
+
const tryParseFeatures = (features) => {
|
|
677
|
+
try {
|
|
678
|
+
return parseFeatures(features);
|
|
679
|
+
} catch {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
};
|
|
82
683
|
var Discovery = class Discovery {
|
|
684
|
+
static #cache = /* @__PURE__ */ new Map();
|
|
83
685
|
#service;
|
|
84
686
|
constructor(service) {
|
|
85
687
|
this.#service = service;
|
|
86
688
|
}
|
|
87
|
-
async find() {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
689
|
+
async find(useCache = true) {
|
|
690
|
+
if (useCache) {
|
|
691
|
+
const cached = Discovery.#cache.get(this.#service);
|
|
692
|
+
if (cached && cached.expiresAt > Date.now()) return cached.results;
|
|
693
|
+
}
|
|
694
|
+
const mapped = (await multicast([this.#service], 4)).map(toDiscoveryResult);
|
|
695
|
+
Discovery.#cache.set(this.#service, {
|
|
696
|
+
results: mapped,
|
|
697
|
+
expiresAt: Date.now() + CACHE_TTL
|
|
698
|
+
});
|
|
699
|
+
return mapped;
|
|
93
700
|
}
|
|
94
701
|
async findUntil(id, tries = 10, timeout = 1e3) {
|
|
95
702
|
while (tries > 0) {
|
|
96
|
-
const devices = await this.find();
|
|
703
|
+
const devices = await this.find(false);
|
|
97
704
|
const device = devices.find((device) => device.id === id);
|
|
98
705
|
if (device) return device;
|
|
99
706
|
console.log();
|
|
@@ -102,7 +709,57 @@ var Discovery = class Discovery {
|
|
|
102
709
|
tries--;
|
|
103
710
|
await waitFor(timeout);
|
|
104
711
|
}
|
|
105
|
-
throw new Error("Device not found after
|
|
712
|
+
throw new Error("Device not found after several tries, aborting.");
|
|
713
|
+
}
|
|
714
|
+
static clearCache() {
|
|
715
|
+
Discovery.#cache.clear();
|
|
716
|
+
}
|
|
717
|
+
static async wake(address) {
|
|
718
|
+
const promises = WAKE_PORTS.map((port) => new Promise((resolve) => {
|
|
719
|
+
const socket = createConnection({
|
|
720
|
+
host: address,
|
|
721
|
+
port,
|
|
722
|
+
timeout: 500
|
|
723
|
+
});
|
|
724
|
+
socket.on("connect", () => {
|
|
725
|
+
socket.destroy();
|
|
726
|
+
resolve();
|
|
727
|
+
});
|
|
728
|
+
socket.on("error", () => {
|
|
729
|
+
socket.destroy();
|
|
730
|
+
resolve();
|
|
731
|
+
});
|
|
732
|
+
socket.on("timeout", () => {
|
|
733
|
+
socket.destroy();
|
|
734
|
+
resolve();
|
|
735
|
+
});
|
|
736
|
+
}));
|
|
737
|
+
await Promise.all(promises);
|
|
738
|
+
}
|
|
739
|
+
static async discoverAll() {
|
|
740
|
+
const allServices = await multicast([
|
|
741
|
+
AIRPLAY_SERVICE,
|
|
742
|
+
COMPANION_LINK_SERVICE,
|
|
743
|
+
RAOP_SERVICE
|
|
744
|
+
], 4);
|
|
745
|
+
const devices = /* @__PURE__ */ new Map();
|
|
746
|
+
for (const service of allServices) {
|
|
747
|
+
const result = toDiscoveryResult(service);
|
|
748
|
+
const existing = devices.get(result.id);
|
|
749
|
+
if (existing) {
|
|
750
|
+
if (service.type === AIRPLAY_SERVICE) existing.airplay = result;
|
|
751
|
+
else if (service.type === COMPANION_LINK_SERVICE) existing.companionLink = result;
|
|
752
|
+
else if (service.type === RAOP_SERVICE) existing.raop = result;
|
|
753
|
+
} else devices.set(result.id, {
|
|
754
|
+
id: result.id,
|
|
755
|
+
name: result.fqdn,
|
|
756
|
+
address: result.address,
|
|
757
|
+
airplay: service.type === AIRPLAY_SERVICE ? result : void 0,
|
|
758
|
+
companionLink: service.type === COMPANION_LINK_SERVICE ? result : void 0,
|
|
759
|
+
raop: service.type === RAOP_SERVICE ? result : void 0
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
return [...devices.values()];
|
|
106
763
|
}
|
|
107
764
|
static airplay() {
|
|
108
765
|
return new Discovery(AIRPLAY_SERVICE);
|
|
@@ -114,29 +771,6 @@ var Discovery = class Discovery {
|
|
|
114
771
|
return new Discovery(RAOP_SERVICE);
|
|
115
772
|
}
|
|
116
773
|
};
|
|
117
|
-
function generateId(result) {
|
|
118
|
-
if (!result?.packet) return null;
|
|
119
|
-
const { answers = [], additionals = [] } = result.packet;
|
|
120
|
-
const allRecords = [...answers, ...additionals];
|
|
121
|
-
const srvRecord = allRecords.find((record) => record.type === "SRV");
|
|
122
|
-
if (srvRecord?.rdata?.target) return srvRecord.rdata.target;
|
|
123
|
-
if (result.address) {
|
|
124
|
-
const addressRecord = allRecords.find((record) => (record.type === "A" || record.type === "AAAA") && record.rdata === result.address);
|
|
125
|
-
if (addressRecord?.name) return addressRecord.name;
|
|
126
|
-
}
|
|
127
|
-
const aRecord = allRecords.find((record) => record.type === "A");
|
|
128
|
-
if (aRecord?.name) return aRecord.name;
|
|
129
|
-
if (result.modelName) return `${result.modelName.replace(/\s+/g, "-").replace(/[^a-zA-Z0-9-]/g, "")}.local`;
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
function getTxt(result) {
|
|
133
|
-
if (!result.packet) return {};
|
|
134
|
-
const { answers = [], additionals = [] } = result.packet;
|
|
135
|
-
const records = [...answers, ...additionals];
|
|
136
|
-
const txt = {};
|
|
137
|
-
for (const record of records) if (record.type === "TXT" && record.rdata) Object.assign(txt, record.rdata);
|
|
138
|
-
return txt;
|
|
139
|
-
}
|
|
140
774
|
|
|
141
775
|
//#endregion
|
|
142
776
|
//#region src/symbols.ts
|
|
@@ -841,5 +1475,5 @@ function splitUInt53(number) {
|
|
|
841
1475
|
}
|
|
842
1476
|
|
|
843
1477
|
//#endregion
|
|
844
|
-
export { AIRPLAY_SERVICE, AIRPLAY_TRANSIENT_PIN, AccessoryPair, AccessoryVerify, COMPANION_LINK_SERVICE, Connection, Context, Discovery, ENCRYPTION, EncryptionAwareConnection, EncryptionState, HTTP_TIMEOUT, RAOP_SERVICE, TimingServer, generateActiveRemoteId, generateDacpId, generateSessionId, getLocalIP, getMacAddress, prompt, randomInt32, randomInt64, reporter, uint16ToBE, uint53ToLE, v4 as uuid, waitFor };
|
|
1478
|
+
export { AIRPLAY_SERVICE, AIRPLAY_TRANSIENT_PIN, AccessoryPair, AccessoryVerify, AirPlayFeatureFlags, COMPANION_LINK_SERVICE, Connection, Context, Discovery, ENCRYPTION, EncryptionAwareConnection, EncryptionState, HTTP_TIMEOUT, JsonStorage, MemoryStorage, RAOP_SERVICE, Storage, TimingServer, describeFlags, generateActiveRemoteId, generateDacpId, generateSessionId, getLocalIP, getMacAddress, getPairingRequirement, getProtocolVersion, hasFeatureFlag, isPasswordRequired, isRemoteControlSupported, multicast as mdnsMulticast, unicast as mdnsUnicast, parseFeatures, prompt, randomInt32, randomInt64, reporter, uint16ToBE, uint53ToLE, v4 as uuid, waitFor };
|
|
845
1479
|
//# sourceMappingURL=index.mjs.map
|