@basmilius/apple-common 0.8.1 → 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 +1478 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -5
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,1479 @@
|
|
|
1
|
-
import
|
|
2
|
-
`)),t--,await D(n)}throw Error(`Device not found after serveral tries, aborting.`)}static airplay(){return new e(A)}static companionLink(){return new e(j)}static raop(){return new e(M)}};function P(e){if(!e?.packet)return null;let{answers:t=[],additionals:n=[]}=e.packet,r=[...t,...n],i=r.find(e=>e.type===`SRV`);if(i?.rdata?.target)return i.rdata.target;if(e.address){let t=r.find(t=>(t.type===`A`||t.type===`AAAA`)&&t.rdata===e.address);if(t?.name)return t.name}let a=r.find(e=>e.type===`A`);return a?.name?a.name:e.modelName?`${e.modelName.replace(/\s+/g,`-`).replace(/[^a-zA-Z0-9-]/g,``)}.local`:null}function ee(e){if(!e.packet)return{};let{answers:t=[],additionals:n=[]}=e.packet,r=[...t,...n],i={};for(let e of r)e.type===`TXT`&&e.rdata&&Object.assign(i,e.rdata);return i}const F=Symbol(),I={resolve:()=>{},reject:e=>{}};var L=class extends a{get address(){return this.#e}get context(){return this.#n}get port(){return this.#t}get isConnected(){return this.#u===`connected`}get state(){if(this.#u===`closing`||this.#u===`failed`)return this.#u;if(!this.#l)return`disconnected`;switch(this.#l.readyState){case`opening`:return`connecting`;case`open`:return`connected`;default:return this.#u}}#e;#t;#n;#r=!1;#i=0;#a=3;#o=!0;#s=3e3;#c;#l;#u;#d;constructor(e,t,n){super(),this.#e=t,this.#t=n,this.#n=e,this.#u=`disconnected`}async connect(){if(this.#u!==`connected`){if(this.#u===`connecting`)throw Error(`A connection is already being established.`);return this.#o=!0,this.#i=0,this.#f()}}destroy(){this.#l?.destroy()}async disconnect(){if(this.#c&&=(clearTimeout(this.#c),void 0),this.#o=!1,!(!this.#l||this.#u===`disconnected`))return new Promise(e=>{this.#u=`closing`,this.#l.once(`close`,()=>{this.#p(),e()}),this.#l.end()})}debug(e){return this.#r=e,this}retry(e,t=3e3){return this.#a=e,this.#s=t,this}write(e){if(!this.#l||this.state!==`connected`||!this.#l.writable){this.emit(`error`,Error(`Cannot write to a disconnected connection.`));return}this.#l.write(e,e=>{e&&(this.#n.logger.error(`Failed to write data to socket.`),this.emit(`error`,e))})}async#f(){return new Promise((e,t)=>{this.#u=`connecting`,this.#d={resolve:e,reject:t},this.#l?.removeAllListeners(),this.#l=void 0,this.#l=new o,this.#l.setNoDelay(!0),this.#l.setTimeout(1e4),this.#l.on(`close`,this.#h.bind(this)),this.#l.on(`connect`,this.#g.bind(this)),this.#l.on(`data`,this.#_.bind(this)),this.#l.on(`end`,this.#v.bind(this)),this.#l.on(`error`,this.#y.bind(this)),this.#l.on(`timeout`,this.#b.bind(this)),this.#n.logger.net(`Connecting to ${this.#e}:${this.#t}...`),this.#l.connect({host:this.#e,port:this.#t,keepAlive:!0})})}#p(){this.#c&&=(clearTimeout(this.#c),void 0),this.#l&&=(this.#l.removeAllListeners(),this.#l.destroy(),void 0),this.#u=`disconnected`,this.#d=void 0}#m(e){if(!this.#o||this.#i>=this.#a){this.#u=`failed`,this.#d?.reject(e),this.#d=void 0;return}this.#c&&=(clearTimeout(this.#c),void 0),this.#i++,this.#n.logger.net(`Retry attempt ${this.#i} / ${this.#a} in ${this.#s}ms...`);let{resolve:t,reject:n}=this.#d??I;this.#p(),this.#c=setTimeout(async()=>{this.#c=void 0;try{this.#d={resolve:t,reject:n},await this.#f(),t()}catch{}},this.#s)}#h(e){let t=this.#u===`connected`;this.#u!==`closing`&&(this.#u=`disconnected`,this.#n.logger.net(`Connection closed (${e?`with error`:`normally`}).`)),this.emit(`close`,e),t&&this.#o&&e&&this.#m(Error(`Connection closed unexpectedly.`))}#g(){this.#u=`connected`,this.#i=0,this.#l.setKeepAlive(!0,1e4),this.#l.setTimeout(0),this.emit(`connect`),this.#d?.resolve(),this.#d=void 0}#_(e){if(this.#r){let t=Math.min(e.byteLength,64);this.#n.logger.debug(`Received ${e.byteLength} bytes of data.`),this.#n.logger.debug(`hex=${e.subarray(0,t).toString(`hex`)}`),this.#n.logger.debug(`ascii=${e.toString(`ascii`).replace(/[^\x20-\x7E]/g,`.`).substring(0,t)}`)}this.emit(`data`,e)}#v(){this.emit(`end`)}#y(e){this.#n.logger.error(`Connection error: ${e.message}`),this.listenerCount(`error`)>0?this.emit(`error`,e):this.#n.logger.warn(`No error handler registered. This is likely a bug.`,this.constructor.name,`#onError`),this.#u===`connecting`?this.#m(e):this.#u=`failed`}#b(){this.#n.logger.error(`Connection timed out.`);let e=Error(`Connection timed out.`);this.emit(`timeout`),this.#u===`connecting`?this.#m(e):(this.#u=`failed`,this.#l?.destroy())}},R=class extends L{get isEncrypted(){return!!this[F]}[F];enableEncryption(e,t){this[F]=new z(e,t)}},z=class{readKey;readCount;writeKey;writeCount;constructor(e,t){this.readCount=0,this.readKey=e,this.writeCount=0,this.writeKey=t}},B=class{get id(){return this.#e}get label(){return this.#t}#e;#t;constructor(e){this.#e=e,this.#t=`\u001b[36m[${e}]\u001b[39m`}debug(...e){H(this.#t,...e)}error(...e){U(this.#t,...e)}info(...e){W(this.#t,...e)}net(...e){G(this.#t,...e)}raw(...e){K(this.#t,...e)}warn(...e){q(this.#t,...e)}},V=class{#e=[];all(){this.#e=[`debug`,`error`,`info`,`net`,`raw`,`warn`]}disable(e){this.#e.includes(e)&&this.#e.splice(this.#e.indexOf(e),1)}enable(e){this.#e.includes(e)||this.#e.push(e)}isEnabled(e){return this.#e.includes(e)}};function H(...e){J.isEnabled(`debug`)&&console.debug(`\x1B[36m[debug]\x1B[39m`,...e)}function U(...e){J.isEnabled(`error`)&&console.error(`\x1B[31m[error]\x1B[39m`,...e)}function W(...e){J.isEnabled(`info`)&&console.info(`\x1B[32m[info]\x1B[39m`,...e)}function G(...e){J.isEnabled(`net`)&&console.info(`\x1B[33m[net]\x1B[39m`,...e)}function K(...e){J.isEnabled(`raw`)&&console.log(`\x1B[34m[raw]\x1B[39m`,...e)}function q(...e){J.isEnabled(`warn`)&&console.warn(`\x1B[33m[warn]\x1B[39m`,...e)}const J=new V;var Y=class{get deviceId(){return this.#e}get logger(){return this.#t}#e;#t;constructor(e){this.#e=e,this.#t=new B(e)}},X=class{get context(){return this.#e}#e;constructor(e){this.#e=e}tlv(e){let t=l.decode(e);return t.has(l.Value.Error)&&l.bail(t),this.#e.logger.raw(`Decoded TLV`,t),t}},Z=class extends X{#e;#t;#n;#r;#i;#a;constructor(e,t){super(e),this.#e=`basmilius/apple-protocols`,this.#t=Buffer.from(T().toUpperCase()),this.#n=t}async start(){let e=f.generateKeyPair();this.#r=Buffer.from(e.publicKey),this.#i=Buffer.from(e.secretKey)}async pin(e){let t=await this.m1(),n=await this.m2(t,await e()),r=await this.m3(n),i=await this.m4(r),a=await this.m5(i),o=await this.m6(i,a);if(!o)throw Error(`Pairing failed, could not get accessory keys.`);return o}async transient(){let e=await this.m1([[l.Value.Flags,l.Flags.TransientPairing]]),t=await this.m2(e),n=await this.m3(t),r=await this.m4(n),i=p({hash:`sha512`,key:r.sharedSecret,length:32,salt:Buffer.from(`Control-Salt`),info:Buffer.from(`Control-Read-Encryption-Key`)}),a=p({hash:`sha512`,key:r.sharedSecret,length:32,salt:Buffer.from(`Control-Salt`),info:Buffer.from(`Control-Write-Encryption-Key`)});return{pairingId:this.#t,sharedSecret:r.sharedSecret,accessoryToControllerKey:i,controllerToAccessoryKey:a}}async m1(e=[]){let t=await this.#n(`m1`,l.encode([[l.Value.Method,l.Method.PairSetup],[l.Value.State,l.State.M1],...e])),n=this.tlv(t);return{publicKey:n.get(l.Value.PublicKey),salt:n.get(l.Value.Salt)}}async m2(e,t=O){let n=await m.genKey(32);return this.#a=new h(m.params.hap,e.salt,Buffer.from(`Pair-Setup`),Buffer.from(t),n,!0),this.#a.setB(e.publicKey),{publicKey:this.#a.computeA(),proof:this.#a.computeM1()}}async m3(e){let t=await this.#n(`m3`,l.encode([[l.Value.State,l.State.M3],[l.Value.PublicKey,e.publicKey],[l.Value.Proof,e.proof]]));return{serverProof:this.tlv(t).get(l.Value.Proof)}}async m4(e){return this.#a.checkM2(e.serverProof),{sharedSecret:this.#a.computeK()}}async m5(e){let t=p({hash:`sha512`,key:e.sharedSecret,length:32,salt:Buffer.from(`Pair-Setup-Controller-Sign-Salt`,`utf8`),info:Buffer.from(`Pair-Setup-Controller-Sign-Info`,`utf8`)}),n=p({hash:`sha512`,key:e.sharedSecret,length:32,salt:Buffer.from(`Pair-Setup-Encrypt-Salt`,`utf8`),info:Buffer.from(`Pair-Setup-Encrypt-Info`,`utf8`)}),r=Buffer.concat([t,this.#t,this.#r]),i=f.sign(r,this.#i),a=l.encode([[l.Value.Identifier,this.#t],[l.Value.PublicKey,this.#r],[l.Value.Signature,Buffer.from(i)],[l.Value.Name,c.encode({name:this.#e})]]),{authTag:o,ciphertext:s}=u.encrypt(n,Buffer.from(`PS-Msg05`),null,a),d=Buffer.concat([s,o]),m=await this.#n(`m5`,l.encode([[l.Value.State,l.State.M5],[l.Value.EncryptedData,d]])),h=this.tlv(m).get(l.Value.EncryptedData),g=h.subarray(0,-16);return{authTag:h.subarray(-16),data:g,sessionKey:n}}async m6(e,t){let n=u.decrypt(t.sessionKey,Buffer.from(`PS-Msg06`),null,t.data,t.authTag),r=l.decode(n),i=r.get(l.Value.Identifier),a=r.get(l.Value.PublicKey),o=r.get(l.Value.Signature),s=p({hash:`sha512`,key:e.sharedSecret,length:32,salt:Buffer.from(`Pair-Setup-Accessory-Sign-Salt`),info:Buffer.from(`Pair-Setup-Accessory-Sign-Info`)}),c=Buffer.concat([s,i,a]);if(!f.verify(c,o,a))throw Error(`Invalid accessory signature.`);return{accessoryIdentifier:i.toString(),accessoryLongTermPublicKey:a,pairingId:this.#t,publicKey:this.#r,secretKey:this.#i}}},Q=class extends X{#e;#t;constructor(e,t){super(e),this.#e=d.generateKeyPair(),this.#t=t}async start(e){let t=await this.#n(),n=await this.#r(e.accessoryIdentifier,e.accessoryLongTermPublicKey,t);return await this.#i(e.pairingId,e.secretKey,n),await this.#a(n,e.pairingId)}async#n(){let e=await this.#t(`m1`,l.encode([[l.Value.State,l.State.M1],[l.Value.PublicKey,Buffer.from(this.#e.publicKey)]])),t=this.tlv(e),n=t.get(l.Value.PublicKey);return{encryptedData:t.get(l.Value.EncryptedData),serverPublicKey:n}}async#r(e,t,n){let r=Buffer.from(d.generateSharedSecKey(this.#e.secretKey,n.serverPublicKey)),i=p({hash:`sha512`,key:r,length:32,salt:Buffer.from(`Pair-Verify-Encrypt-Salt`),info:Buffer.from(`Pair-Verify-Encrypt-Info`)}),a=n.encryptedData.subarray(0,-16),o=n.encryptedData.subarray(-16),s=u.decrypt(i,Buffer.from(`PV-Msg02`),null,a,o),c=l.decode(s),m=c.get(l.Value.Identifier),h=c.get(l.Value.Signature);if(m.toString()!==e)throw Error(`Invalid accessory identifier. Expected ${m.toString()} to be ${e}.`);let g=Buffer.concat([n.serverPublicKey,m,this.#e.publicKey]);if(!f.verify(g,h,t))throw Error(`Invalid accessory signature.`);return{serverEphemeralPublicKey:n.serverPublicKey,sessionKey:i,sharedSecret:r}}async#i(e,t,n){let r=Buffer.concat([this.#e.publicKey,e,n.serverEphemeralPublicKey]),i=Buffer.from(f.sign(r,t)),a=l.encode([[l.Value.Identifier,e],[l.Value.Signature,i]]),{authTag:o,ciphertext:s}=u.encrypt(n.sessionKey,Buffer.from(`PV-Msg03`),null,a),c=Buffer.concat([s,o]);return await this.#t(`m3`,l.encode([[l.Value.State,l.State.M3],[l.Value.EncryptedData,c]])),{}}async#a(e,t){return{accessoryToControllerKey:Buffer.alloc(0),controllerToAccessoryKey:Buffer.alloc(0),pairingId:t,sharedSecret:e.sharedSecret}}},te=class{get port(){return this.#n}#e;#t;#n=0;constructor(){this.#e=new B(`timing-server`),this.#t=g(`udp4`),this.#t.on(`connect`,this.#r.bind(this)),this.#t.on(`error`,this.#i.bind(this)),this.#t.on(`message`,this.#o.bind(this))}close(){this.#t.close(),this.#n=0}listen(){return new Promise((e,t)=>{this.#t.once(`error`,t),this.#t.once(`listening`,()=>{this.#t.removeListener(`error`,t),this.#a(),e()}),this.#t.bind(0,e)})}#r(){this.#t.setRecvBufferSize(16384),this.#t.setSendBufferSize(16384)}#i(e){this.#e.error(`Timing server error`,e)}#a(){let{port:e}=this.#t.address();this.#n=e}#o(e,t){try{let n=s.decode(e),r=s.now(),[i,a]=s.parts(r);this.#e.info(`Timing server ntp=${r} receivedSeconds=${i} receivedFraction=${a}`);let o=s.encode({proto:n.proto,type:211,seqno:n.seqno,padding:0,reftime_sec:n.sendtime_sec,reftime_frac:n.sendtime_frac,recvtime_sec:i,recvtime_frac:a,sendtime_sec:i,sendtime_frac:a});this.#t.send(o,t.port,t.address,e=>{e&&this.#e.warn(`Timing server failed to send response to ${t.address}:${t.port}`,e)})}catch(n){this.#e.warn(`Timing server received malformed packet (${e.length} bytes) from ${t.address}:${t.port}`,n)}}};function ne(){return Math.floor(Math.random()*2**32).toString(10)}function re(){return Math.floor(Math.random()*2**64).toString(16).toUpperCase()}function ie(){return Math.floor(Math.random()*2**32).toString(10)}function $(){let e=_();for(let t of Object.values(e))if(t){for(let e of t)if(!(e.internal||e.family!==`IPv4`)&&e.address&&e.address!==`127.0.0.1`)return e.address}return null}function ae(){let e=_();for(let t of Object.values(e))if(t){for(let e of t)if(!(e.internal||e.family!==`IPv4`)&&e.mac&&e.mac!==`00:00:00:00:00:00`)return e.mac.toUpperCase()}return`00:00:00:00:00:00`}function oe(){return e(4).readUInt32BE(0)}function se(){return e(8).readBigUint64LE(0)}function ce(e){let t=Buffer.allocUnsafe(2);return t.writeUInt16BE(e,0),t}function le(e){let[t,n]=ue(e),r=Buffer.allocUnsafe(8);return r.writeUInt32LE(n,0),r.writeUInt32LE(t,4),r}function ue(e){let t=4294967295;if(e<=-1||e>9007199254740991)throw Error(`Number out of range.`);if(Math.floor(e)!==e)throw Error(`Number is not an integer.`);let n=0,r=e&4294967295,i=r<0?(e&2147483647)+2147483648:r;return e>t&&(n=(e-i)/(t+1)),[n,i]}export{A as AIRPLAY_SERVICE,O as AIRPLAY_TRANSIENT_PIN,Z as AccessoryPair,Q as AccessoryVerify,j as COMPANION_LINK_SERVICE,L as Connection,Y as Context,N as Discovery,F as ENCRYPTION,R as EncryptionAwareConnection,z as EncryptionState,k as HTTP_TIMEOUT,M as RAOP_SERVICE,te as TimingServer,ne as generateActiveRemoteId,re as generateDacpId,ie as generateSessionId,$ as getLocalIP,ae as getMacAddress,E as prompt,oe as randomInt32,se as randomInt64,J as reporter,ce as uint16ToBE,le as uint53ToLE,T as uuid,D as waitFor};
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { randomBytes, randomFillSync, randomUUID } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { Socket, createConnection } from "node:net";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
import { createSocket } from "node:dgram";
|
|
8
|
+
import { networkInterfaces } from "node:os";
|
|
9
|
+
import { EventEmitter } from "node:events";
|
|
10
|
+
import { NTP, OPack, TLV8 } from "@basmilius/apple-encoding";
|
|
11
|
+
import { Chacha20, Curve25519, Ed25519, hkdf } from "@basmilius/apple-encryption";
|
|
12
|
+
import { SRP, SrpClient } from "fast-srp-hap";
|
|
13
|
+
|
|
14
|
+
//#region \0rolldown/runtime.js
|
|
15
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region ../../node_modules/.bun/uuid@13.0.0/node_modules/uuid/dist-node/stringify.js
|
|
19
|
+
const byteToHex = [];
|
|
20
|
+
for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1));
|
|
21
|
+
function unsafeStringify(arr, offset = 0) {
|
|
22
|
+
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region ../../node_modules/.bun/uuid@13.0.0/node_modules/uuid/dist-node/rng.js
|
|
27
|
+
const rnds8Pool = new Uint8Array(256);
|
|
28
|
+
let poolPtr = rnds8Pool.length;
|
|
29
|
+
function rng() {
|
|
30
|
+
if (poolPtr > rnds8Pool.length - 16) {
|
|
31
|
+
randomFillSync(rnds8Pool);
|
|
32
|
+
poolPtr = 0;
|
|
33
|
+
}
|
|
34
|
+
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region ../../node_modules/.bun/uuid@13.0.0/node_modules/uuid/dist-node/native.js
|
|
39
|
+
var native_default = { randomUUID };
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region ../../node_modules/.bun/uuid@13.0.0/node_modules/uuid/dist-node/v4.js
|
|
43
|
+
function _v4(options, buf, offset) {
|
|
44
|
+
options = options || {};
|
|
45
|
+
const rnds = options.random ?? options.rng?.() ?? rng();
|
|
46
|
+
if (rnds.length < 16) throw new Error("Random bytes length must be >= 16");
|
|
47
|
+
rnds[6] = rnds[6] & 15 | 64;
|
|
48
|
+
rnds[8] = rnds[8] & 63 | 128;
|
|
49
|
+
if (buf) {
|
|
50
|
+
offset = offset || 0;
|
|
51
|
+
if (offset < 0 || offset + 16 > buf.length) throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
|
|
52
|
+
for (let i = 0; i < 16; ++i) buf[offset + i] = rnds[i];
|
|
53
|
+
return buf;
|
|
54
|
+
}
|
|
55
|
+
return unsafeStringify(rnds);
|
|
56
|
+
}
|
|
57
|
+
function v4(options, buf, offset) {
|
|
58
|
+
if (native_default.randomUUID && !buf && !options) return native_default.randomUUID();
|
|
59
|
+
return _v4(options, buf, offset);
|
|
60
|
+
}
|
|
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
|
+
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/cli.ts
|
|
236
|
+
async function prompt(message) {
|
|
237
|
+
const cli = createInterface({
|
|
238
|
+
input: process.stdin,
|
|
239
|
+
output: process.stdout
|
|
240
|
+
});
|
|
241
|
+
const answer = await new Promise((resolve) => cli.question(`${message}: `, resolve));
|
|
242
|
+
cli.close();
|
|
243
|
+
return answer;
|
|
244
|
+
}
|
|
245
|
+
async function waitFor(ms) {
|
|
246
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/const.ts
|
|
251
|
+
const AIRPLAY_TRANSIENT_PIN = "3939";
|
|
252
|
+
const HTTP_TIMEOUT = 6e3;
|
|
253
|
+
const SOCKET_TIMEOUT = 1e4;
|
|
254
|
+
const AIRPLAY_SERVICE = "_airplay._tcp.local";
|
|
255
|
+
const COMPANION_LINK_SERVICE = "_companion-link._tcp.local";
|
|
256
|
+
const RAOP_SERVICE = "_raop._tcp.local";
|
|
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
|
+
|
|
645
|
+
//#endregion
|
|
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
|
+
};
|
|
683
|
+
var Discovery = class Discovery {
|
|
684
|
+
static #cache = /* @__PURE__ */ new Map();
|
|
685
|
+
#service;
|
|
686
|
+
constructor(service) {
|
|
687
|
+
this.#service = service;
|
|
688
|
+
}
|
|
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;
|
|
700
|
+
}
|
|
701
|
+
async findUntil(id, tries = 10, timeout = 1e3) {
|
|
702
|
+
while (tries > 0) {
|
|
703
|
+
const devices = await this.find(false);
|
|
704
|
+
const device = devices.find((device) => device.id === id);
|
|
705
|
+
if (device) return device;
|
|
706
|
+
console.log();
|
|
707
|
+
console.log(`Device not found, retrying in ${timeout}ms...`);
|
|
708
|
+
console.log(devices.map((d) => ` ● ${d.id} (${d.fqdn})`).join("\n"));
|
|
709
|
+
tries--;
|
|
710
|
+
await waitFor(timeout);
|
|
711
|
+
}
|
|
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()];
|
|
763
|
+
}
|
|
764
|
+
static airplay() {
|
|
765
|
+
return new Discovery(AIRPLAY_SERVICE);
|
|
766
|
+
}
|
|
767
|
+
static companionLink() {
|
|
768
|
+
return new Discovery(COMPANION_LINK_SERVICE);
|
|
769
|
+
}
|
|
770
|
+
static raop() {
|
|
771
|
+
return new Discovery(RAOP_SERVICE);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
//#endregion
|
|
776
|
+
//#region src/symbols.ts
|
|
777
|
+
const ENCRYPTION = Symbol();
|
|
778
|
+
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/connection.ts
|
|
781
|
+
const NOOP_PROMISE_HANDLER = {
|
|
782
|
+
resolve: () => {},
|
|
783
|
+
reject: (_) => {}
|
|
784
|
+
};
|
|
785
|
+
var Connection = class extends EventEmitter {
|
|
786
|
+
get address() {
|
|
787
|
+
return this.#address;
|
|
788
|
+
}
|
|
789
|
+
get context() {
|
|
790
|
+
return this.#context;
|
|
791
|
+
}
|
|
792
|
+
get port() {
|
|
793
|
+
return this.#port;
|
|
794
|
+
}
|
|
795
|
+
get isConnected() {
|
|
796
|
+
return this.#state === "connected";
|
|
797
|
+
}
|
|
798
|
+
get state() {
|
|
799
|
+
if (this.#state === "closing" || this.#state === "failed") return this.#state;
|
|
800
|
+
if (!this.#socket) return "disconnected";
|
|
801
|
+
switch (this.#socket.readyState) {
|
|
802
|
+
case "opening": return "connecting";
|
|
803
|
+
case "open": return "connected";
|
|
804
|
+
default: return this.#state;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
#address;
|
|
808
|
+
#port;
|
|
809
|
+
#context;
|
|
810
|
+
#debug = false;
|
|
811
|
+
#retryAttempt = 0;
|
|
812
|
+
#retryAttempts = 3;
|
|
813
|
+
#retryEnabled = true;
|
|
814
|
+
#retryInterval = 3e3;
|
|
815
|
+
#retryTimeout;
|
|
816
|
+
#socket;
|
|
817
|
+
#state;
|
|
818
|
+
#connectPromise;
|
|
819
|
+
constructor(context, address, port) {
|
|
820
|
+
super();
|
|
821
|
+
this.#address = address;
|
|
822
|
+
this.#port = port;
|
|
823
|
+
this.#context = context;
|
|
824
|
+
this.#state = "disconnected";
|
|
825
|
+
}
|
|
826
|
+
async connect() {
|
|
827
|
+
if (this.#state === "connected") return;
|
|
828
|
+
if (this.#state === "connecting") throw new Error("A connection is already being established.");
|
|
829
|
+
this.#retryEnabled = true;
|
|
830
|
+
this.#retryAttempt = 0;
|
|
831
|
+
return this.#attemptConnect();
|
|
832
|
+
}
|
|
833
|
+
destroy() {
|
|
834
|
+
this.#socket?.destroy();
|
|
835
|
+
}
|
|
836
|
+
async disconnect() {
|
|
837
|
+
if (this.#retryTimeout) {
|
|
838
|
+
clearTimeout(this.#retryTimeout);
|
|
839
|
+
this.#retryTimeout = void 0;
|
|
840
|
+
}
|
|
841
|
+
this.#retryEnabled = false;
|
|
842
|
+
if (!this.#socket || this.#state === "disconnected") return;
|
|
843
|
+
return new Promise((resolve) => {
|
|
844
|
+
this.#state = "closing";
|
|
845
|
+
this.#socket.once("close", () => {
|
|
846
|
+
this.#cleanup();
|
|
847
|
+
resolve();
|
|
848
|
+
});
|
|
849
|
+
this.#socket.end();
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
debug(enabled) {
|
|
853
|
+
this.#debug = enabled;
|
|
854
|
+
return this;
|
|
855
|
+
}
|
|
856
|
+
retry(attempts, interval = 3e3) {
|
|
857
|
+
this.#retryAttempts = attempts;
|
|
858
|
+
this.#retryInterval = interval;
|
|
859
|
+
return this;
|
|
860
|
+
}
|
|
861
|
+
write(data) {
|
|
862
|
+
if (!this.#socket || this.state !== "connected" || !this.#socket.writable) {
|
|
863
|
+
this.emit("error", /* @__PURE__ */ new Error("Cannot write to a disconnected connection."));
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
this.#socket.write(data, (err) => {
|
|
867
|
+
if (!err) return;
|
|
868
|
+
this.#context.logger.error("Failed to write data to socket.");
|
|
869
|
+
this.emit("error", err);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
async #attemptConnect() {
|
|
873
|
+
return new Promise((resolve, reject) => {
|
|
874
|
+
this.#state = "connecting";
|
|
875
|
+
this.#connectPromise = {
|
|
876
|
+
resolve,
|
|
877
|
+
reject
|
|
878
|
+
};
|
|
879
|
+
this.#socket?.removeAllListeners();
|
|
880
|
+
this.#socket = void 0;
|
|
881
|
+
this.#socket = new Socket();
|
|
882
|
+
this.#socket.setNoDelay(true);
|
|
883
|
+
this.#socket.setTimeout(SOCKET_TIMEOUT);
|
|
884
|
+
this.#socket.on("close", this.#onClose.bind(this));
|
|
885
|
+
this.#socket.on("connect", this.#onConnect.bind(this));
|
|
886
|
+
this.#socket.on("data", this.#onData.bind(this));
|
|
887
|
+
this.#socket.on("end", this.#onEnd.bind(this));
|
|
888
|
+
this.#socket.on("error", this.#onError.bind(this));
|
|
889
|
+
this.#socket.on("timeout", this.#onTimeout.bind(this));
|
|
890
|
+
this.#context.logger.net(`Connecting to ${this.#address}:${this.#port}...`);
|
|
891
|
+
this.#socket.connect({
|
|
892
|
+
host: this.#address,
|
|
893
|
+
port: this.#port,
|
|
894
|
+
keepAlive: true
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
#cleanup() {
|
|
899
|
+
if (this.#retryTimeout) {
|
|
900
|
+
clearTimeout(this.#retryTimeout);
|
|
901
|
+
this.#retryTimeout = void 0;
|
|
902
|
+
}
|
|
903
|
+
if (this.#socket) {
|
|
904
|
+
this.#socket.removeAllListeners();
|
|
905
|
+
this.#socket.destroy();
|
|
906
|
+
this.#socket = void 0;
|
|
907
|
+
}
|
|
908
|
+
this.#state = "disconnected";
|
|
909
|
+
this.#connectPromise = void 0;
|
|
910
|
+
}
|
|
911
|
+
#scheduleRetry(err) {
|
|
912
|
+
if (!this.#retryEnabled || this.#retryAttempt >= this.#retryAttempts) {
|
|
913
|
+
this.#state = "failed";
|
|
914
|
+
this.#connectPromise?.reject(err);
|
|
915
|
+
this.#connectPromise = void 0;
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (this.#retryTimeout) {
|
|
919
|
+
clearTimeout(this.#retryTimeout);
|
|
920
|
+
this.#retryTimeout = void 0;
|
|
921
|
+
}
|
|
922
|
+
this.#retryAttempt++;
|
|
923
|
+
this.#context.logger.net(`Retry attempt ${this.#retryAttempt} / ${this.#retryAttempts} in ${this.#retryInterval}ms...`);
|
|
924
|
+
const { resolve, reject } = this.#connectPromise ?? NOOP_PROMISE_HANDLER;
|
|
925
|
+
this.#cleanup();
|
|
926
|
+
this.#retryTimeout = setTimeout(async () => {
|
|
927
|
+
this.#retryTimeout = void 0;
|
|
928
|
+
try {
|
|
929
|
+
this.#connectPromise = {
|
|
930
|
+
resolve,
|
|
931
|
+
reject
|
|
932
|
+
};
|
|
933
|
+
await this.#attemptConnect();
|
|
934
|
+
resolve();
|
|
935
|
+
} catch (retryErr) {}
|
|
936
|
+
}, this.#retryInterval);
|
|
937
|
+
}
|
|
938
|
+
#onClose(hadError) {
|
|
939
|
+
const wasConnected = this.#state === "connected";
|
|
940
|
+
if (this.#state !== "closing") {
|
|
941
|
+
this.#state = "disconnected";
|
|
942
|
+
this.#context.logger.net(`Connection closed (${hadError ? "with error" : "normally"}).`);
|
|
943
|
+
}
|
|
944
|
+
this.emit("close", hadError);
|
|
945
|
+
if (wasConnected && this.#retryEnabled && hadError) this.#scheduleRetry(/* @__PURE__ */ new Error("Connection closed unexpectedly."));
|
|
946
|
+
}
|
|
947
|
+
#onConnect() {
|
|
948
|
+
this.#state = "connected";
|
|
949
|
+
this.#retryAttempt = 0;
|
|
950
|
+
this.#socket.setKeepAlive(true, 1e4);
|
|
951
|
+
this.#socket.setTimeout(0);
|
|
952
|
+
this.emit("connect");
|
|
953
|
+
this.#connectPromise?.resolve();
|
|
954
|
+
this.#connectPromise = void 0;
|
|
955
|
+
}
|
|
956
|
+
#onData(data) {
|
|
957
|
+
if (this.#debug) {
|
|
958
|
+
const cutoff = Math.min(data.byteLength, 64);
|
|
959
|
+
this.#context.logger.debug(`Received ${data.byteLength} bytes of data.`);
|
|
960
|
+
this.#context.logger.debug(`hex=${data.subarray(0, cutoff).toString("hex")}`);
|
|
961
|
+
this.#context.logger.debug(`ascii=${data.toString("ascii").replace(/[^\x20-\x7E]/g, ".").substring(0, cutoff)}`);
|
|
962
|
+
}
|
|
963
|
+
this.emit("data", data);
|
|
964
|
+
}
|
|
965
|
+
#onEnd() {
|
|
966
|
+
this.emit("end");
|
|
967
|
+
}
|
|
968
|
+
#onError(err) {
|
|
969
|
+
this.#context.logger.error(`Connection error: ${err.message}`);
|
|
970
|
+
if (this.listenerCount("error") > 0) this.emit("error", err);
|
|
971
|
+
else this.#context.logger.warn("No error handler registered. This is likely a bug.", this.constructor.name, "#onError");
|
|
972
|
+
if (this.#state === "connecting") this.#scheduleRetry(err);
|
|
973
|
+
else this.#state = "failed";
|
|
974
|
+
}
|
|
975
|
+
#onTimeout() {
|
|
976
|
+
this.#context.logger.error("Connection timed out.");
|
|
977
|
+
const err = /* @__PURE__ */ new Error("Connection timed out.");
|
|
978
|
+
this.emit("timeout");
|
|
979
|
+
if (this.#state === "connecting") this.#scheduleRetry(err);
|
|
980
|
+
else {
|
|
981
|
+
this.#state = "failed";
|
|
982
|
+
this.#socket?.destroy();
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
var EncryptionAwareConnection = class extends Connection {
|
|
987
|
+
get isEncrypted() {
|
|
988
|
+
return !!this[ENCRYPTION];
|
|
989
|
+
}
|
|
990
|
+
[ENCRYPTION];
|
|
991
|
+
enableEncryption(readKey, writeKey) {
|
|
992
|
+
this[ENCRYPTION] = new EncryptionState(readKey, writeKey);
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
var EncryptionState = class {
|
|
996
|
+
readKey;
|
|
997
|
+
readCount;
|
|
998
|
+
writeKey;
|
|
999
|
+
writeCount;
|
|
1000
|
+
constructor(readKey, writeKey) {
|
|
1001
|
+
this.readCount = 0;
|
|
1002
|
+
this.readKey = readKey;
|
|
1003
|
+
this.writeCount = 0;
|
|
1004
|
+
this.writeKey = writeKey;
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
//#endregion
|
|
1009
|
+
//#region src/reporter.ts
|
|
1010
|
+
var Logger = class {
|
|
1011
|
+
get id() {
|
|
1012
|
+
return this.#id;
|
|
1013
|
+
}
|
|
1014
|
+
get label() {
|
|
1015
|
+
return this.#label;
|
|
1016
|
+
}
|
|
1017
|
+
#id;
|
|
1018
|
+
#label;
|
|
1019
|
+
constructor(id) {
|
|
1020
|
+
this.#id = id;
|
|
1021
|
+
this.#label = `\u001b[36m[${id}]\u001b[39m`;
|
|
1022
|
+
}
|
|
1023
|
+
debug(...data) {
|
|
1024
|
+
debug(this.#label, ...data);
|
|
1025
|
+
}
|
|
1026
|
+
error(...data) {
|
|
1027
|
+
error(this.#label, ...data);
|
|
1028
|
+
}
|
|
1029
|
+
info(...data) {
|
|
1030
|
+
info(this.#label, ...data);
|
|
1031
|
+
}
|
|
1032
|
+
net(...data) {
|
|
1033
|
+
net(this.#label, ...data);
|
|
1034
|
+
}
|
|
1035
|
+
raw(...data) {
|
|
1036
|
+
raw(this.#label, ...data);
|
|
1037
|
+
}
|
|
1038
|
+
warn(...data) {
|
|
1039
|
+
warn(this.#label, ...data);
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
var Reporter = class {
|
|
1043
|
+
#enabled = [];
|
|
1044
|
+
all() {
|
|
1045
|
+
this.#enabled = [
|
|
1046
|
+
"debug",
|
|
1047
|
+
"error",
|
|
1048
|
+
"info",
|
|
1049
|
+
"net",
|
|
1050
|
+
"raw",
|
|
1051
|
+
"warn"
|
|
1052
|
+
];
|
|
1053
|
+
}
|
|
1054
|
+
disable(group) {
|
|
1055
|
+
if (this.#enabled.includes(group)) this.#enabled.splice(this.#enabled.indexOf(group), 1);
|
|
1056
|
+
}
|
|
1057
|
+
enable(group) {
|
|
1058
|
+
if (!this.#enabled.includes(group)) this.#enabled.push(group);
|
|
1059
|
+
}
|
|
1060
|
+
isEnabled(group) {
|
|
1061
|
+
return this.#enabled.includes(group);
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
function debug(...data) {
|
|
1065
|
+
reporter.isEnabled("debug") && console.debug(`\u001b[36m[debug]\u001b[39m`, ...data);
|
|
1066
|
+
}
|
|
1067
|
+
function error(...data) {
|
|
1068
|
+
reporter.isEnabled("error") && console.error(`\u001b[31m[error]\u001b[39m`, ...data);
|
|
1069
|
+
}
|
|
1070
|
+
function info(...data) {
|
|
1071
|
+
reporter.isEnabled("info") && console.info(`\u001b[32m[info]\u001b[39m`, ...data);
|
|
1072
|
+
}
|
|
1073
|
+
function net(...data) {
|
|
1074
|
+
reporter.isEnabled("net") && console.info(`\u001b[33m[net]\u001b[39m`, ...data);
|
|
1075
|
+
}
|
|
1076
|
+
function raw(...data) {
|
|
1077
|
+
reporter.isEnabled("raw") && console.log(`\u001b[34m[raw]\u001b[39m`, ...data);
|
|
1078
|
+
}
|
|
1079
|
+
function warn(...data) {
|
|
1080
|
+
reporter.isEnabled("warn") && console.warn(`\u001b[33m[warn]\u001b[39m`, ...data);
|
|
1081
|
+
}
|
|
1082
|
+
const reporter = new Reporter();
|
|
1083
|
+
|
|
1084
|
+
//#endregion
|
|
1085
|
+
//#region src/context.ts
|
|
1086
|
+
var Context = class {
|
|
1087
|
+
get deviceId() {
|
|
1088
|
+
return this.#deviceId;
|
|
1089
|
+
}
|
|
1090
|
+
get logger() {
|
|
1091
|
+
return this.#logger;
|
|
1092
|
+
}
|
|
1093
|
+
#deviceId;
|
|
1094
|
+
#logger;
|
|
1095
|
+
constructor(deviceId) {
|
|
1096
|
+
this.#deviceId = deviceId;
|
|
1097
|
+
this.#logger = new Logger(deviceId);
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
//#endregion
|
|
1102
|
+
//#region src/pairing.ts
|
|
1103
|
+
var BasePairing = class {
|
|
1104
|
+
get context() {
|
|
1105
|
+
return this.#context;
|
|
1106
|
+
}
|
|
1107
|
+
#context;
|
|
1108
|
+
constructor(context) {
|
|
1109
|
+
this.#context = context;
|
|
1110
|
+
}
|
|
1111
|
+
tlv(buffer) {
|
|
1112
|
+
const data = TLV8.decode(buffer);
|
|
1113
|
+
if (data.has(TLV8.Value.Error)) TLV8.bail(data);
|
|
1114
|
+
this.#context.logger.raw("Decoded TLV", data);
|
|
1115
|
+
return data;
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
var AccessoryPair = class extends BasePairing {
|
|
1119
|
+
#name;
|
|
1120
|
+
#pairingId;
|
|
1121
|
+
#requestHandler;
|
|
1122
|
+
#publicKey;
|
|
1123
|
+
#secretKey;
|
|
1124
|
+
#srp;
|
|
1125
|
+
constructor(context, requestHandler) {
|
|
1126
|
+
super(context);
|
|
1127
|
+
this.#name = "basmilius/apple-protocols";
|
|
1128
|
+
this.#pairingId = Buffer.from(v4().toUpperCase());
|
|
1129
|
+
this.#requestHandler = requestHandler;
|
|
1130
|
+
}
|
|
1131
|
+
async start() {
|
|
1132
|
+
const keyPair = Ed25519.generateKeyPair();
|
|
1133
|
+
this.#publicKey = Buffer.from(keyPair.publicKey);
|
|
1134
|
+
this.#secretKey = Buffer.from(keyPair.secretKey);
|
|
1135
|
+
}
|
|
1136
|
+
async pin(askPin) {
|
|
1137
|
+
const m1 = await this.m1();
|
|
1138
|
+
const m2 = await this.m2(m1, await askPin());
|
|
1139
|
+
const m3 = await this.m3(m2);
|
|
1140
|
+
const m4 = await this.m4(m3);
|
|
1141
|
+
const m5 = await this.m5(m4);
|
|
1142
|
+
const m6 = await this.m6(m4, m5);
|
|
1143
|
+
if (!m6) throw new Error("Pairing failed, could not get accessory keys.");
|
|
1144
|
+
return m6;
|
|
1145
|
+
}
|
|
1146
|
+
async transient() {
|
|
1147
|
+
const m1 = await this.m1([[TLV8.Value.Flags, TLV8.Flags.TransientPairing]]);
|
|
1148
|
+
const m2 = await this.m2(m1);
|
|
1149
|
+
const m3 = await this.m3(m2);
|
|
1150
|
+
const m4 = await this.m4(m3);
|
|
1151
|
+
const accessoryToControllerKey = hkdf({
|
|
1152
|
+
hash: "sha512",
|
|
1153
|
+
key: m4.sharedSecret,
|
|
1154
|
+
length: 32,
|
|
1155
|
+
salt: Buffer.from("Control-Salt"),
|
|
1156
|
+
info: Buffer.from("Control-Read-Encryption-Key")
|
|
1157
|
+
});
|
|
1158
|
+
const controllerToAccessoryKey = hkdf({
|
|
1159
|
+
hash: "sha512",
|
|
1160
|
+
key: m4.sharedSecret,
|
|
1161
|
+
length: 32,
|
|
1162
|
+
salt: Buffer.from("Control-Salt"),
|
|
1163
|
+
info: Buffer.from("Control-Write-Encryption-Key")
|
|
1164
|
+
});
|
|
1165
|
+
return {
|
|
1166
|
+
pairingId: this.#pairingId,
|
|
1167
|
+
sharedSecret: m4.sharedSecret,
|
|
1168
|
+
accessoryToControllerKey,
|
|
1169
|
+
controllerToAccessoryKey
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
async m1(additionalTlv = []) {
|
|
1173
|
+
const response = await this.#requestHandler("m1", TLV8.encode([
|
|
1174
|
+
[TLV8.Value.Method, TLV8.Method.PairSetup],
|
|
1175
|
+
[TLV8.Value.State, TLV8.State.M1],
|
|
1176
|
+
...additionalTlv
|
|
1177
|
+
]));
|
|
1178
|
+
const data = this.tlv(response);
|
|
1179
|
+
return {
|
|
1180
|
+
publicKey: data.get(TLV8.Value.PublicKey),
|
|
1181
|
+
salt: data.get(TLV8.Value.Salt)
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
async m2(m1, pin = AIRPLAY_TRANSIENT_PIN) {
|
|
1185
|
+
const srpKey = await SRP.genKey(32);
|
|
1186
|
+
this.#srp = new SrpClient(SRP.params.hap, m1.salt, Buffer.from("Pair-Setup"), Buffer.from(pin), srpKey, true);
|
|
1187
|
+
this.#srp.setB(m1.publicKey);
|
|
1188
|
+
return {
|
|
1189
|
+
publicKey: this.#srp.computeA(),
|
|
1190
|
+
proof: this.#srp.computeM1()
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
async m3(m2) {
|
|
1194
|
+
const response = await this.#requestHandler("m3", TLV8.encode([
|
|
1195
|
+
[TLV8.Value.State, TLV8.State.M3],
|
|
1196
|
+
[TLV8.Value.PublicKey, m2.publicKey],
|
|
1197
|
+
[TLV8.Value.Proof, m2.proof]
|
|
1198
|
+
]));
|
|
1199
|
+
return { serverProof: this.tlv(response).get(TLV8.Value.Proof) };
|
|
1200
|
+
}
|
|
1201
|
+
async m4(m3) {
|
|
1202
|
+
this.#srp.checkM2(m3.serverProof);
|
|
1203
|
+
return { sharedSecret: this.#srp.computeK() };
|
|
1204
|
+
}
|
|
1205
|
+
async m5(m4) {
|
|
1206
|
+
const iosDeviceX = hkdf({
|
|
1207
|
+
hash: "sha512",
|
|
1208
|
+
key: m4.sharedSecret,
|
|
1209
|
+
length: 32,
|
|
1210
|
+
salt: Buffer.from("Pair-Setup-Controller-Sign-Salt", "utf8"),
|
|
1211
|
+
info: Buffer.from("Pair-Setup-Controller-Sign-Info", "utf8")
|
|
1212
|
+
});
|
|
1213
|
+
const sessionKey = hkdf({
|
|
1214
|
+
hash: "sha512",
|
|
1215
|
+
key: m4.sharedSecret,
|
|
1216
|
+
length: 32,
|
|
1217
|
+
salt: Buffer.from("Pair-Setup-Encrypt-Salt", "utf8"),
|
|
1218
|
+
info: Buffer.from("Pair-Setup-Encrypt-Info", "utf8")
|
|
1219
|
+
});
|
|
1220
|
+
const deviceInfo = Buffer.concat([
|
|
1221
|
+
iosDeviceX,
|
|
1222
|
+
this.#pairingId,
|
|
1223
|
+
this.#publicKey
|
|
1224
|
+
]);
|
|
1225
|
+
const signature = Ed25519.sign(deviceInfo, this.#secretKey);
|
|
1226
|
+
const innerTlv = TLV8.encode([
|
|
1227
|
+
[TLV8.Value.Identifier, this.#pairingId],
|
|
1228
|
+
[TLV8.Value.PublicKey, this.#publicKey],
|
|
1229
|
+
[TLV8.Value.Signature, Buffer.from(signature)],
|
|
1230
|
+
[TLV8.Value.Name, OPack.encode({ name: this.#name })]
|
|
1231
|
+
]);
|
|
1232
|
+
const { authTag, ciphertext } = Chacha20.encrypt(sessionKey, Buffer.from("PS-Msg05"), null, innerTlv);
|
|
1233
|
+
const encrypted = Buffer.concat([ciphertext, authTag]);
|
|
1234
|
+
const response = await this.#requestHandler("m5", TLV8.encode([[TLV8.Value.State, TLV8.State.M5], [TLV8.Value.EncryptedData, encrypted]]));
|
|
1235
|
+
const encryptedDataRaw = this.tlv(response).get(TLV8.Value.EncryptedData);
|
|
1236
|
+
const encryptedData = encryptedDataRaw.subarray(0, -16);
|
|
1237
|
+
return {
|
|
1238
|
+
authTag: encryptedDataRaw.subarray(-16),
|
|
1239
|
+
data: encryptedData,
|
|
1240
|
+
sessionKey
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
async m6(m4, m5) {
|
|
1244
|
+
const data = Chacha20.decrypt(m5.sessionKey, Buffer.from("PS-Msg06"), null, m5.data, m5.authTag);
|
|
1245
|
+
const tlv = TLV8.decode(data);
|
|
1246
|
+
const accessoryIdentifier = tlv.get(TLV8.Value.Identifier);
|
|
1247
|
+
const accessoryLongTermPublicKey = tlv.get(TLV8.Value.PublicKey);
|
|
1248
|
+
const accessorySignature = tlv.get(TLV8.Value.Signature);
|
|
1249
|
+
const accessoryX = hkdf({
|
|
1250
|
+
hash: "sha512",
|
|
1251
|
+
key: m4.sharedSecret,
|
|
1252
|
+
length: 32,
|
|
1253
|
+
salt: Buffer.from("Pair-Setup-Accessory-Sign-Salt"),
|
|
1254
|
+
info: Buffer.from("Pair-Setup-Accessory-Sign-Info")
|
|
1255
|
+
});
|
|
1256
|
+
const accessoryInfo = Buffer.concat([
|
|
1257
|
+
accessoryX,
|
|
1258
|
+
accessoryIdentifier,
|
|
1259
|
+
accessoryLongTermPublicKey
|
|
1260
|
+
]);
|
|
1261
|
+
if (!Ed25519.verify(accessoryInfo, accessorySignature, accessoryLongTermPublicKey)) throw new Error("Invalid accessory signature.");
|
|
1262
|
+
return {
|
|
1263
|
+
accessoryIdentifier: accessoryIdentifier.toString(),
|
|
1264
|
+
accessoryLongTermPublicKey,
|
|
1265
|
+
pairingId: this.#pairingId,
|
|
1266
|
+
publicKey: this.#publicKey,
|
|
1267
|
+
secretKey: this.#secretKey
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
var AccessoryVerify = class extends BasePairing {
|
|
1272
|
+
#ephemeralKeyPair;
|
|
1273
|
+
#requestHandler;
|
|
1274
|
+
constructor(context, requestHandler) {
|
|
1275
|
+
super(context);
|
|
1276
|
+
this.#ephemeralKeyPair = Curve25519.generateKeyPair();
|
|
1277
|
+
this.#requestHandler = requestHandler;
|
|
1278
|
+
}
|
|
1279
|
+
async start(credentials) {
|
|
1280
|
+
const m1 = await this.#m1();
|
|
1281
|
+
const m2 = await this.#m2(credentials.accessoryIdentifier, credentials.accessoryLongTermPublicKey, m1);
|
|
1282
|
+
await this.#m3(credentials.pairingId, credentials.secretKey, m2);
|
|
1283
|
+
return await this.#m4(m2, credentials.pairingId);
|
|
1284
|
+
}
|
|
1285
|
+
async #m1() {
|
|
1286
|
+
const response = await this.#requestHandler("m1", TLV8.encode([[TLV8.Value.State, TLV8.State.M1], [TLV8.Value.PublicKey, Buffer.from(this.#ephemeralKeyPair.publicKey)]]));
|
|
1287
|
+
const data = this.tlv(response);
|
|
1288
|
+
const serverPublicKey = data.get(TLV8.Value.PublicKey);
|
|
1289
|
+
return {
|
|
1290
|
+
encryptedData: data.get(TLV8.Value.EncryptedData),
|
|
1291
|
+
serverPublicKey
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
async #m2(localAccessoryIdentifier, longTermPublicKey, m1) {
|
|
1295
|
+
const sharedSecret = Buffer.from(Curve25519.generateSharedSecKey(this.#ephemeralKeyPair.secretKey, m1.serverPublicKey));
|
|
1296
|
+
const sessionKey = hkdf({
|
|
1297
|
+
hash: "sha512",
|
|
1298
|
+
key: sharedSecret,
|
|
1299
|
+
length: 32,
|
|
1300
|
+
salt: Buffer.from("Pair-Verify-Encrypt-Salt"),
|
|
1301
|
+
info: Buffer.from("Pair-Verify-Encrypt-Info")
|
|
1302
|
+
});
|
|
1303
|
+
const encryptedData = m1.encryptedData.subarray(0, -16);
|
|
1304
|
+
const encryptedTag = m1.encryptedData.subarray(-16);
|
|
1305
|
+
const data = Chacha20.decrypt(sessionKey, Buffer.from("PV-Msg02"), null, encryptedData, encryptedTag);
|
|
1306
|
+
const tlv = TLV8.decode(data);
|
|
1307
|
+
const accessoryIdentifier = tlv.get(TLV8.Value.Identifier);
|
|
1308
|
+
const accessorySignature = tlv.get(TLV8.Value.Signature);
|
|
1309
|
+
if (accessoryIdentifier.toString() !== localAccessoryIdentifier) throw new Error(`Invalid accessory identifier. Expected ${accessoryIdentifier.toString()} to be ${localAccessoryIdentifier}.`);
|
|
1310
|
+
const accessoryInfo = Buffer.concat([
|
|
1311
|
+
m1.serverPublicKey,
|
|
1312
|
+
accessoryIdentifier,
|
|
1313
|
+
this.#ephemeralKeyPair.publicKey
|
|
1314
|
+
]);
|
|
1315
|
+
if (!Ed25519.verify(accessoryInfo, accessorySignature, longTermPublicKey)) throw new Error("Invalid accessory signature.");
|
|
1316
|
+
return {
|
|
1317
|
+
serverEphemeralPublicKey: m1.serverPublicKey,
|
|
1318
|
+
sessionKey,
|
|
1319
|
+
sharedSecret
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
async #m3(pairingId, secretKey, m2) {
|
|
1323
|
+
const iosDeviceInfo = Buffer.concat([
|
|
1324
|
+
this.#ephemeralKeyPair.publicKey,
|
|
1325
|
+
pairingId,
|
|
1326
|
+
m2.serverEphemeralPublicKey
|
|
1327
|
+
]);
|
|
1328
|
+
const iosDeviceSignature = Buffer.from(Ed25519.sign(iosDeviceInfo, secretKey));
|
|
1329
|
+
const innerTlv = TLV8.encode([[TLV8.Value.Identifier, pairingId], [TLV8.Value.Signature, iosDeviceSignature]]);
|
|
1330
|
+
const { authTag, ciphertext } = Chacha20.encrypt(m2.sessionKey, Buffer.from("PV-Msg03"), null, innerTlv);
|
|
1331
|
+
const encrypted = Buffer.concat([ciphertext, authTag]);
|
|
1332
|
+
await this.#requestHandler("m3", TLV8.encode([[TLV8.Value.State, TLV8.State.M3], [TLV8.Value.EncryptedData, encrypted]]));
|
|
1333
|
+
return {};
|
|
1334
|
+
}
|
|
1335
|
+
async #m4(m2, pairingId) {
|
|
1336
|
+
return {
|
|
1337
|
+
accessoryToControllerKey: Buffer.alloc(0),
|
|
1338
|
+
controllerToAccessoryKey: Buffer.alloc(0),
|
|
1339
|
+
pairingId,
|
|
1340
|
+
sharedSecret: m2.sharedSecret
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
//#endregion
|
|
1346
|
+
//#region src/timing.ts
|
|
1347
|
+
var TimingServer = class {
|
|
1348
|
+
get port() {
|
|
1349
|
+
return this.#port;
|
|
1350
|
+
}
|
|
1351
|
+
#logger;
|
|
1352
|
+
#socket;
|
|
1353
|
+
#port = 0;
|
|
1354
|
+
constructor() {
|
|
1355
|
+
this.#logger = new Logger("timing-server");
|
|
1356
|
+
this.#socket = createSocket("udp4");
|
|
1357
|
+
this.#socket.on("connect", this.#onConnect.bind(this));
|
|
1358
|
+
this.#socket.on("error", this.#onError.bind(this));
|
|
1359
|
+
this.#socket.on("message", this.#onMessage.bind(this));
|
|
1360
|
+
}
|
|
1361
|
+
close() {
|
|
1362
|
+
this.#socket.close();
|
|
1363
|
+
this.#port = 0;
|
|
1364
|
+
}
|
|
1365
|
+
listen() {
|
|
1366
|
+
return new Promise((resolve, reject) => {
|
|
1367
|
+
this.#socket.once("error", reject);
|
|
1368
|
+
this.#socket.once("listening", () => {
|
|
1369
|
+
this.#socket.removeListener("error", reject);
|
|
1370
|
+
this.#onListening();
|
|
1371
|
+
resolve();
|
|
1372
|
+
});
|
|
1373
|
+
this.#socket.bind(0, resolve);
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
#onConnect() {
|
|
1377
|
+
this.#socket.setRecvBufferSize(16384);
|
|
1378
|
+
this.#socket.setSendBufferSize(16384);
|
|
1379
|
+
}
|
|
1380
|
+
#onError(err) {
|
|
1381
|
+
this.#logger.error("Timing server error", err);
|
|
1382
|
+
}
|
|
1383
|
+
#onListening() {
|
|
1384
|
+
const { port } = this.#socket.address();
|
|
1385
|
+
this.#port = port;
|
|
1386
|
+
}
|
|
1387
|
+
#onMessage(data, info) {
|
|
1388
|
+
try {
|
|
1389
|
+
const request = NTP.decode(data);
|
|
1390
|
+
const ntp = NTP.now();
|
|
1391
|
+
const [receivedSeconds, receivedFraction] = NTP.parts(ntp);
|
|
1392
|
+
this.#logger.info(`Timing server ntp=${ntp} receivedSeconds=${receivedSeconds} receivedFraction=${receivedFraction}`);
|
|
1393
|
+
const response = NTP.encode({
|
|
1394
|
+
proto: request.proto,
|
|
1395
|
+
type: 211,
|
|
1396
|
+
seqno: request.seqno,
|
|
1397
|
+
padding: 0,
|
|
1398
|
+
reftime_sec: request.sendtime_sec,
|
|
1399
|
+
reftime_frac: request.sendtime_frac,
|
|
1400
|
+
recvtime_sec: receivedSeconds,
|
|
1401
|
+
recvtime_frac: receivedFraction,
|
|
1402
|
+
sendtime_sec: receivedSeconds,
|
|
1403
|
+
sendtime_frac: receivedFraction
|
|
1404
|
+
});
|
|
1405
|
+
this.#socket.send(response, info.port, info.address, (err) => {
|
|
1406
|
+
if (!err) return;
|
|
1407
|
+
this.#logger.warn(`Timing server failed to send response to ${info.address}:${info.port}`, err);
|
|
1408
|
+
});
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
this.#logger.warn(`Timing server received malformed packet (${data.length} bytes) from ${info.address}:${info.port}`, err);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
//#endregion
|
|
1416
|
+
//#region src/utils.ts
|
|
1417
|
+
function generateActiveRemoteId() {
|
|
1418
|
+
return Math.floor(Math.random() * 2 ** 32).toString(10);
|
|
1419
|
+
}
|
|
1420
|
+
function generateDacpId() {
|
|
1421
|
+
return Math.floor(Math.random() * 2 ** 64).toString(16).toUpperCase();
|
|
1422
|
+
}
|
|
1423
|
+
function generateSessionId() {
|
|
1424
|
+
return Math.floor(Math.random() * 2 ** 32).toString(10);
|
|
1425
|
+
}
|
|
1426
|
+
function getLocalIP() {
|
|
1427
|
+
const interfaces = networkInterfaces();
|
|
1428
|
+
for (const iface of Object.values(interfaces)) {
|
|
1429
|
+
if (!iface) continue;
|
|
1430
|
+
for (const net of iface) {
|
|
1431
|
+
if (net.internal || net.family !== "IPv4") continue;
|
|
1432
|
+
if (net.address && net.address !== "127.0.0.1") return net.address;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
function getMacAddress() {
|
|
1438
|
+
const interfaces = networkInterfaces();
|
|
1439
|
+
for (const iface of Object.values(interfaces)) {
|
|
1440
|
+
if (!iface) continue;
|
|
1441
|
+
for (const net of iface) {
|
|
1442
|
+
if (net.internal || net.family !== "IPv4") continue;
|
|
1443
|
+
if (net.mac && net.mac !== "00:00:00:00:00:00") return net.mac.toUpperCase();
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return "00:00:00:00:00:00";
|
|
1447
|
+
}
|
|
1448
|
+
function randomInt32() {
|
|
1449
|
+
return randomBytes(4).readUInt32BE(0);
|
|
1450
|
+
}
|
|
1451
|
+
function randomInt64() {
|
|
1452
|
+
return randomBytes(8).readBigUint64LE(0);
|
|
1453
|
+
}
|
|
1454
|
+
function uint16ToBE(value) {
|
|
1455
|
+
const buffer = Buffer.allocUnsafe(2);
|
|
1456
|
+
buffer.writeUInt16BE(value, 0);
|
|
1457
|
+
return buffer;
|
|
1458
|
+
}
|
|
1459
|
+
function uint53ToLE(value) {
|
|
1460
|
+
const [upper, lower] = splitUInt53(value);
|
|
1461
|
+
const buffer = Buffer.allocUnsafe(8);
|
|
1462
|
+
buffer.writeUInt32LE(lower, 0);
|
|
1463
|
+
buffer.writeUInt32LE(upper, 4);
|
|
1464
|
+
return buffer;
|
|
1465
|
+
}
|
|
1466
|
+
function splitUInt53(number) {
|
|
1467
|
+
const MAX_UINT32 = 4294967295;
|
|
1468
|
+
if (number <= -1 || number > 9007199254740991) throw new Error("Number out of range.");
|
|
1469
|
+
if (Math.floor(number) !== number) throw new Error("Number is not an integer.");
|
|
1470
|
+
let upper = 0;
|
|
1471
|
+
const signbit = number & 4294967295;
|
|
1472
|
+
const lower = signbit < 0 ? (number & 2147483647) + 2147483648 : signbit;
|
|
1473
|
+
if (number > MAX_UINT32) upper = (number - lower) / (MAX_UINT32 + 1);
|
|
1474
|
+
return [upper, lower];
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
//#endregion
|
|
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 };
|
|
3
1479
|
//# sourceMappingURL=index.mjs.map
|