@lox-audioserver/node-airplay-sender 0.4.0

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.
Files changed (75) hide show
  1. package/README.md +93 -0
  2. package/dist/core/ap2_test.d.ts +1 -0
  3. package/dist/core/ap2_test.js +8 -0
  4. package/dist/core/atv.d.ts +16 -0
  5. package/dist/core/atv.js +215 -0
  6. package/dist/core/atvAuthenticator.d.ts +30 -0
  7. package/dist/core/atvAuthenticator.js +134 -0
  8. package/dist/core/audioOut.d.ts +43 -0
  9. package/dist/core/audioOut.js +220 -0
  10. package/dist/core/deviceAirtunes.d.ts +76 -0
  11. package/dist/core/deviceAirtunes.js +536 -0
  12. package/dist/core/devices.d.ts +50 -0
  13. package/dist/core/devices.js +221 -0
  14. package/dist/core/index.d.ts +56 -0
  15. package/dist/core/index.js +144 -0
  16. package/dist/core/rtsp.d.ts +12 -0
  17. package/dist/core/rtsp.js +1678 -0
  18. package/dist/core/srp.d.ts +14 -0
  19. package/dist/core/srp.js +128 -0
  20. package/dist/core/udpServers.d.ts +44 -0
  21. package/dist/core/udpServers.js +244 -0
  22. package/dist/esm/core/ap2_test.js +8 -0
  23. package/dist/esm/core/atv.js +215 -0
  24. package/dist/esm/core/atvAuthenticator.js +134 -0
  25. package/dist/esm/core/audioOut.js +220 -0
  26. package/dist/esm/core/deviceAirtunes.js +536 -0
  27. package/dist/esm/core/devices.js +221 -0
  28. package/dist/esm/core/index.js +144 -0
  29. package/dist/esm/core/rtsp.js +1678 -0
  30. package/dist/esm/core/srp.js +128 -0
  31. package/dist/esm/core/udpServers.js +244 -0
  32. package/dist/esm/homekit/credentials.js +109 -0
  33. package/dist/esm/homekit/encryption.js +82 -0
  34. package/dist/esm/homekit/number.js +47 -0
  35. package/dist/esm/homekit/tlv.js +97 -0
  36. package/dist/esm/index.js +310 -0
  37. package/dist/esm/package.json +1 -0
  38. package/dist/esm/utils/alac.js +62 -0
  39. package/dist/esm/utils/alacEncoder.js +34 -0
  40. package/dist/esm/utils/circularBuffer.js +132 -0
  41. package/dist/esm/utils/config.js +49 -0
  42. package/dist/esm/utils/http.js +148 -0
  43. package/dist/esm/utils/ntp.js +56 -0
  44. package/dist/esm/utils/numUtil.js +17 -0
  45. package/dist/esm/utils/packetPool.js +52 -0
  46. package/dist/esm/utils/util.js +9 -0
  47. package/dist/homekit/credentials.d.ts +31 -0
  48. package/dist/homekit/credentials.js +109 -0
  49. package/dist/homekit/encryption.d.ts +12 -0
  50. package/dist/homekit/encryption.js +82 -0
  51. package/dist/homekit/number.d.ts +7 -0
  52. package/dist/homekit/number.js +47 -0
  53. package/dist/homekit/tlv.d.ts +25 -0
  54. package/dist/homekit/tlv.js +97 -0
  55. package/dist/index.d.ts +121 -0
  56. package/dist/index.js +310 -0
  57. package/dist/utils/alac.d.ts +9 -0
  58. package/dist/utils/alac.js +62 -0
  59. package/dist/utils/alacEncoder.d.ts +14 -0
  60. package/dist/utils/alacEncoder.js +34 -0
  61. package/dist/utils/circularBuffer.d.ts +32 -0
  62. package/dist/utils/circularBuffer.js +132 -0
  63. package/dist/utils/config.d.ts +42 -0
  64. package/dist/utils/config.js +49 -0
  65. package/dist/utils/http.d.ts +19 -0
  66. package/dist/utils/http.js +148 -0
  67. package/dist/utils/ntp.d.ts +21 -0
  68. package/dist/utils/ntp.js +56 -0
  69. package/dist/utils/numUtil.d.ts +5 -0
  70. package/dist/utils/numUtil.js +17 -0
  71. package/dist/utils/packetPool.d.ts +25 -0
  72. package/dist/utils/packetPool.js +52 -0
  73. package/dist/utils/util.d.ts +2 -0
  74. package/dist/utils/util.js +9 -0
  75. package/package.json +71 -0
@@ -0,0 +1,14 @@
1
+ declare class SRP {
2
+ group: number;
3
+ N: any;
4
+ g: any;
5
+ constructor(group: number);
6
+ u(A: string, B: string): any;
7
+ k(): string;
8
+ x(I: string, P: string, s: string): string;
9
+ S(B: string, k: string, x: string, a: string, u: string): string;
10
+ A(a: string): string;
11
+ K(I: string, P: string, s: string, a: string, B: string): string;
12
+ M1(I: string, P: string, s: string, a: string, B: string): string;
13
+ }
14
+ export default SRP;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const bigInt = require('big-integer');
4
+ const sha1 = require('js-sha1');
5
+ const memoize = require('lodash/memoize');
6
+ const util_1 = require("../utils/util");
7
+ // ...
8
+ const groups = {
9
+ 1024: {
10
+ N: new bigInt('EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C' +
11
+ '9C256576D674DF7496EA81D3383B4813D692C6E0E0D5D8E250B98BE4' +
12
+ '8E495C1D6089DAD15DC7D7B46154D6B6CE8EF4AD69B15D4982559B29' +
13
+ '7BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA9A' +
14
+ 'FD5138FE8376435B9FC61D2FC0EB06E3', 16),
15
+ g: new bigInt(2)
16
+ },
17
+ 2048: {
18
+ N: new bigInt('AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC319294' +
19
+ '3DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310D' +
20
+ 'CD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FB' +
21
+ 'D5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF74' +
22
+ '7359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A' +
23
+ '436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D' +
24
+ '5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E73' +
25
+ '03CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB6' +
26
+ '94B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F' +
27
+ '9E4AFF73', 16),
28
+ g: new bigInt(2)
29
+ }
30
+ };
31
+ // ...
32
+ class SRP {
33
+ group;
34
+ N;
35
+ g;
36
+ constructor(group) {
37
+ this.group = group;
38
+ const groupConfig = groups[group];
39
+ if (!groupConfig) {
40
+ throw new Error(`SRP group ${group} is not supported`);
41
+ }
42
+ this.N = groupConfig.N;
43
+ this.g = groupConfig.g;
44
+ this.A = memoize(this.A.bind(this));
45
+ this.K = memoize(this.K.bind(this));
46
+ }
47
+ // ...
48
+ // Private.
49
+ u(A, B) {
50
+ const A_buf = (0, util_1.hexString2ArrayBuffer)(A);
51
+ const B_buf = (0, util_1.hexString2ArrayBuffer)(B);
52
+ const result = new Uint8Array(A_buf.byteLength + B_buf.byteLength);
53
+ result.set(A_buf);
54
+ result.set(B_buf, A_buf.byteLength);
55
+ return sha1(result);
56
+ }
57
+ k() {
58
+ const padded_g = '0'.repeat((this.group / 4) - 1) + this.g.toString(16);
59
+ const N_buf = (0, util_1.hexString2ArrayBuffer)(this.N.toString(16));
60
+ const g_buf = (0, util_1.hexString2ArrayBuffer)(padded_g);
61
+ const result = new Uint8Array(N_buf.byteLength + g_buf.byteLength);
62
+ result.set(N_buf);
63
+ result.set(g_buf, N_buf.byteLength);
64
+ return sha1(result);
65
+ }
66
+ x(I, P, s) {
67
+ const s_buf = (0, util_1.hexString2ArrayBuffer)(s.toLowerCase());
68
+ const I_P_buf = (0, util_1.hexString2ArrayBuffer)(sha1(I + ':' + P));
69
+ const result = new Uint8Array(s_buf.byteLength + I_P_buf.byteLength);
70
+ result.set(s_buf);
71
+ result.set(I_P_buf, s_buf.byteLength);
72
+ return sha1(result);
73
+ }
74
+ ;
75
+ S(B, k, x, a, u) {
76
+ const Bn = new bigInt(B, 16);
77
+ const kn = new bigInt(k, 16);
78
+ const xn = new bigInt(x, 16);
79
+ const an = new bigInt(a, 16);
80
+ const un = new bigInt(u, 16);
81
+ return Bn.add(this.N.multiply(kn)).subtract(this.g.modPow(xn, this.N).multiply(kn)).mod(this.N)
82
+ .modPow(an.add(un.multiply(xn)), this.N)
83
+ .toString(16);
84
+ }
85
+ // ...
86
+ // Public.
87
+ A(a) {
88
+ return this.g.modPow(new bigInt(a, 16), this.N).toString(16);
89
+ }
90
+ K(I, P, s, a, B) {
91
+ const k = this.k();
92
+ const x = this.x(I, P, s);
93
+ const u = this.u(this.A(a), B);
94
+ const S = this.S(B, k, x, a, u);
95
+ // ...
96
+ const S_buf = (0, util_1.hexString2ArrayBuffer)(S);
97
+ let hash1 = new Uint8Array(S_buf.byteLength + 4);
98
+ hash1.set(S_buf);
99
+ hash1.set([0x00, 0x00, 0x00, 0x00], S_buf.byteLength);
100
+ let hash2 = new Uint8Array(S_buf.byteLength + 4);
101
+ hash2.set(S_buf);
102
+ hash2.set([0x00, 0x00, 0x00, 0x01], S_buf.byteLength);
103
+ return sha1(hash1) + sha1(hash2);
104
+ }
105
+ M1(I, P, s, a, B) {
106
+ // M1 = H( H(N) ^ H(g) | H(I) | s | PAD(A) | PAD(B) | K )
107
+ const A = this.A(a);
108
+ const K = this.K(I, P, s, a, B);
109
+ const hN = new Uint8Array(sha1.arrayBuffer((0, util_1.hexString2ArrayBuffer)(this.N.toString(16))));
110
+ const hg = new Uint8Array(sha1.arrayBuffer([this.g.toString(16)]));
111
+ const hN_hg = new Uint8Array(20);
112
+ for (let i = 0; i < 20; i++) {
113
+ hN_hg[i] = hN[i] ^ hg[i];
114
+ }
115
+ const hI = sha1.arrayBuffer(I);
116
+ // ...
117
+ return sha1.create()
118
+ .update(hN_hg)
119
+ .update(hI)
120
+ .update((0, util_1.hexString2ArrayBuffer)(s))
121
+ .update((0, util_1.hexString2ArrayBuffer)(A))
122
+ .update((0, util_1.hexString2ArrayBuffer)(B))
123
+ .update((0, util_1.hexString2ArrayBuffer)(K))
124
+ .hex();
125
+ }
126
+ }
127
+ // ...
128
+ exports.default = SRP;
@@ -0,0 +1,44 @@
1
+ import { EventEmitter } from 'node:events';
2
+ type ControlSyncTarget = {
3
+ host: string;
4
+ controlPort: number;
5
+ };
6
+ type SenderReportCounters = {
7
+ rtpTimestamp: number;
8
+ ntp: Buffer;
9
+ packetCount: number;
10
+ octetCount: number;
11
+ };
12
+ type ReceiverReport = {
13
+ ssrc?: number;
14
+ };
15
+ type ExtendedReport = {
16
+ ntp?: Buffer;
17
+ ssrc?: number;
18
+ lastRr?: number;
19
+ delaySinceLastRr?: number;
20
+ };
21
+ /**
22
+ * Manages control/timing UDP sockets used by RAOP for resend requests and clock sync.
23
+ * Binds ports for both endpoints and emits events with socket info.
24
+ */
25
+ export default class UDPServers extends EventEmitter {
26
+ private status;
27
+ private readonly control;
28
+ private readonly timing;
29
+ private readonly hosts;
30
+ /**
31
+ * Bind control + timing sockets for a host and emit `ports` when ready.
32
+ */
33
+ bind(host: string): void;
34
+ /** Close sockets and reset state. */
35
+ close(): void;
36
+ /**
37
+ * Send an RTCP sync packet to a receiver to align playback.
38
+ */
39
+ sendControlSync(seq: number, dev: ControlSyncTarget, tsOffsetFrames?: number, sr?: SenderReportCounters, rr?: ReceiverReport, xr?: ExtendedReport): void;
40
+ private buildSenderReport;
41
+ private buildReceiverReport;
42
+ private buildExtendedReport;
43
+ }
44
+ export {};
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_dgram_1 = __importDefault(require("node:dgram"));
7
+ const node_events_1 = require("node:events");
8
+ const async_1 = __importDefault(require("async"));
9
+ const config_1 = __importDefault(require("../utils/config"));
10
+ const numUtil_1 = require("../utils/numUtil");
11
+ const ntp_1 = __importDefault(require("../utils/ntp"));
12
+ const UNBOUND = 0;
13
+ const BINDING = 1;
14
+ const BOUND = 2;
15
+ /**
16
+ * Manages control/timing UDP sockets used by RAOP for resend requests and clock sync.
17
+ * Binds ports for both endpoints and emits events with socket info.
18
+ */
19
+ class UDPServers extends node_events_1.EventEmitter {
20
+ status = UNBOUND;
21
+ control = { socket: null, port: null, name: 'control' };
22
+ timing = { socket: null, port: null, name: 'timing' };
23
+ hosts = [];
24
+ /**
25
+ * Bind control + timing sockets for a host and emit `ports` when ready.
26
+ */
27
+ bind(host) {
28
+ this.hosts.push(host);
29
+ if (this.status === BOUND) {
30
+ process.nextTick(() => {
31
+ this.emit('ports', null, this.control, this.timing);
32
+ });
33
+ return;
34
+ }
35
+ if (this.status === BINDING) {
36
+ return;
37
+ }
38
+ this.status = BINDING;
39
+ this.timing.socket = node_dgram_1.default.createSocket({ type: 'udp4', reuseAddr: true });
40
+ this.timing.socket.on('message', (msg, rinfo) => {
41
+ if (!this.hosts.includes(rinfo.address))
42
+ return;
43
+ const ts1 = msg.readUInt32BE(24);
44
+ const ts2 = msg.readUInt32BE(28);
45
+ const reply = Buffer.alloc(32);
46
+ reply.writeUInt16BE(0x80d3, 0);
47
+ reply.writeUInt16BE(0x0007, 2);
48
+ reply.writeUInt32BE(0x00000000, 4);
49
+ reply.writeUInt32BE(ts1, 8);
50
+ reply.writeUInt32BE(ts2, 12);
51
+ const ntpTime = ntp_1.default.timestamp();
52
+ ntpTime.copy(reply, 16);
53
+ ntpTime.copy(reply, 24);
54
+ this.timing.socket?.send(reply, 0, reply.length, rinfo.port, rinfo.address);
55
+ });
56
+ this.control.socket = node_dgram_1.default.createSocket({ type: 'udp4', reuseAddr: true });
57
+ this.control.socket.on('message', (msg, rinfo) => {
58
+ if (!this.hosts.includes(rinfo.address))
59
+ return;
60
+ const resendRequested = msg.readUInt8(1) === (0x80 | 0x55);
61
+ if (resendRequested) {
62
+ const missedSeq = msg.readUInt16BE(4);
63
+ const count = msg.readUInt16BE(6);
64
+ this.emit('resendRequested', missedSeq, count);
65
+ }
66
+ });
67
+ if (process.platform !== 'darwin') {
68
+ this.control.socket.on('error', (err) => {
69
+ this.emit('ports', err);
70
+ });
71
+ this.timing.socket.on('error', (err) => {
72
+ this.emit('ports', err);
73
+ });
74
+ this.control.socket.bind(0, () => {
75
+ this.control.port = this.control.socket?.address().port ?? null;
76
+ });
77
+ this.timing.socket.bind(0, () => {
78
+ this.timing.port = this.timing.socket?.address().port ?? null;
79
+ });
80
+ const interval = setInterval(() => {
81
+ if (this.timing.port != null && this.control.port != null) {
82
+ clearInterval(interval);
83
+ this.status = BOUND;
84
+ this.emit('ports', null, this.control, this.timing);
85
+ }
86
+ }, 100);
87
+ return;
88
+ }
89
+ const toBind = [this.control, this.timing];
90
+ let currentPort = config_1.default.udp_default_port;
91
+ async_1.default.whilst((cb) => cb(null, toBind.length > 0), (cb) => {
92
+ const nextPort = toBind[0];
93
+ nextPort.socket?.once('error', (err) => {
94
+ if (err.code === 'EADDRINUSE') {
95
+ currentPort += 1;
96
+ cb();
97
+ }
98
+ else {
99
+ cb(err);
100
+ }
101
+ });
102
+ nextPort.socket?.once('listening', () => {
103
+ toBind.shift();
104
+ nextPort.port = currentPort;
105
+ currentPort += 1;
106
+ cb();
107
+ });
108
+ nextPort.socket?.bind(currentPort);
109
+ }, (err) => {
110
+ if (err) {
111
+ this.close();
112
+ this.emit('ports', err);
113
+ }
114
+ else {
115
+ this.status = BOUND;
116
+ this.emit('ports', null, this.control, this.timing);
117
+ }
118
+ });
119
+ }
120
+ /** Close sockets and reset state. */
121
+ close() {
122
+ try {
123
+ this.status = UNBOUND;
124
+ this.timing.socket?.close();
125
+ this.timing.socket = null;
126
+ this.control.socket?.close();
127
+ this.control.socket = null;
128
+ }
129
+ catch {
130
+ // ignore
131
+ }
132
+ }
133
+ /**
134
+ * Send an RTCP sync packet to a receiver to align playback.
135
+ */
136
+ sendControlSync(seq, dev, tsOffsetFrames = 0, sr, rr, xr) {
137
+ if (this.status !== BOUND || !this.control.socket)
138
+ return;
139
+ const packet = Buffer.alloc(20);
140
+ packet.writeUInt16BE(0x80d4, 0);
141
+ packet.writeUInt16BE(0x0007, 2);
142
+ packet.writeUInt32BE((0, numUtil_1.low32)((seq + tsOffsetFrames) * config_1.default.frames_per_packet), 4);
143
+ const ntpTime = ntp_1.default.timestamp();
144
+ ntpTime.copy(packet, 8);
145
+ packet.writeUInt32BE((0, numUtil_1.low32)((seq + tsOffsetFrames) * config_1.default.frames_per_packet + config_1.default.sampling_rate * 2), 16);
146
+ const delay = Math.max(0, config_1.default.control_sync_base_delay_ms +
147
+ Math.random() * config_1.default.control_sync_jitter_ms);
148
+ setTimeout(() => {
149
+ if (config_1.default.debug_dump) {
150
+ // eslint-disable-next-line no-console
151
+ console.debug('rtcp_sync', { seq, tsOffsetFrames, host: dev.host, port: dev.controlPort });
152
+ }
153
+ this.control.socket?.send(packet, 0, packet.length, dev.controlPort, dev.host);
154
+ if (sr && config_1.default.send_rtcp_sr) {
155
+ const srPacket = this.buildSenderReport(sr);
156
+ if (config_1.default.debug_dump) {
157
+ // eslint-disable-next-line no-console
158
+ console.debug('rtcp_sr', { ssrc: config_1.default.device_magic, rtp: sr.rtpTimestamp, packets: sr.packetCount });
159
+ }
160
+ this.control.socket?.send(srPacket, 0, srPacket.length, dev.controlPort, dev.host);
161
+ }
162
+ if (config_1.default.send_rtcp_rr) {
163
+ const rrPacket = this.buildReceiverReport(rr);
164
+ if (config_1.default.debug_dump) {
165
+ // eslint-disable-next-line no-console
166
+ console.debug('rtcp_rr', { ssrc: config_1.default.device_magic });
167
+ }
168
+ this.control.socket?.send(rrPacket, 0, rrPacket.length, dev.controlPort, dev.host);
169
+ }
170
+ if (xr && config_1.default.send_rtcp_xr) {
171
+ const xrPacket = this.buildExtendedReport(xr);
172
+ if (config_1.default.debug_dump) {
173
+ // eslint-disable-next-line no-console
174
+ console.debug('rtcp_xr', { ssrc: config_1.default.device_magic });
175
+ }
176
+ this.control.socket?.send(xrPacket, 0, xrPacket.length, dev.controlPort, dev.host);
177
+ }
178
+ }, delay);
179
+ }
180
+ buildSenderReport(counters) {
181
+ const sr = Buffer.alloc(28);
182
+ // V=2, P=0, RC=0
183
+ sr.writeUInt8(0x80, 0);
184
+ // PT=200 (SR)
185
+ sr.writeUInt8(200, 1);
186
+ // length in 32-bit words minus 1 -> 6 words (28 bytes) => 6
187
+ sr.writeUInt16BE(6, 2);
188
+ // SSRC
189
+ sr.writeUInt32BE(config_1.default.device_magic, 4);
190
+ // NTP timestamp
191
+ counters.ntp.copy(sr, 8, 0, 8);
192
+ // RTP timestamp
193
+ sr.writeUInt32BE((0, numUtil_1.low32)(counters.rtpTimestamp), 16);
194
+ // packet count
195
+ sr.writeUInt32BE((0, numUtil_1.low32)(counters.packetCount), 20);
196
+ // octet count
197
+ sr.writeUInt32BE((0, numUtil_1.low32)(counters.octetCount), 24);
198
+ return sr;
199
+ }
200
+ buildReceiverReport(rr) {
201
+ const packet = Buffer.alloc(8);
202
+ // V=2, P=0, RC=0
203
+ packet.writeUInt8(0x80, 0);
204
+ // PT=201 (RR)
205
+ packet.writeUInt8(201, 1);
206
+ // length = 1 (8 bytes / 4 - 1)
207
+ packet.writeUInt16BE(1, 2);
208
+ // SSRC
209
+ packet.writeUInt32BE(rr?.ssrc ?? config_1.default.device_magic, 4);
210
+ return packet;
211
+ }
212
+ buildExtendedReport(xr) {
213
+ // XR with RRT (Receiver Reference Time) and optional DLRR.
214
+ const rrtBlock = Buffer.alloc(12);
215
+ // BT=4 (RRT), reserved+block length=2 (8 octets following header)
216
+ rrtBlock.writeUInt8(4, 0);
217
+ rrtBlock.writeUInt8(0, 1);
218
+ rrtBlock.writeUInt16BE(2, 2);
219
+ (xr?.ntp ?? ntp_1.default.timestamp()).copy(rrtBlock, 4, 0, 8);
220
+ let dlrrBlock = null;
221
+ if (typeof xr?.lastRr === 'number' || typeof xr?.delaySinceLastRr === 'number') {
222
+ dlrrBlock = Buffer.alloc(12);
223
+ // BT=5 (DLRR)
224
+ dlrrBlock.writeUInt8(5, 0);
225
+ dlrrBlock.writeUInt8(0, 1);
226
+ dlrrBlock.writeUInt16BE(3, 2); // block length (3 words = 12 bytes following header)
227
+ dlrrBlock.writeUInt32BE(xr?.lastRr ?? 0, 4);
228
+ dlrrBlock.writeUInt32BE(xr?.delaySinceLastRr ?? 0, 8);
229
+ }
230
+ const blocks = dlrrBlock ? Buffer.concat([rrtBlock, dlrrBlock]) : rrtBlock;
231
+ const packet = Buffer.alloc(8 + blocks.length);
232
+ // V=2, P=0, RC=0
233
+ packet.writeUInt8(0x80, 0);
234
+ // PT=207 (XR)
235
+ packet.writeUInt8(207, 1);
236
+ // length in 32-bit words minus 1
237
+ packet.writeUInt16BE(packet.length / 4 - 1, 2);
238
+ // SSRC
239
+ packet.writeUInt32BE(xr?.ssrc ?? config_1.default.device_magic, 4);
240
+ blocks.copy(packet, 8);
241
+ return packet;
242
+ }
243
+ }
244
+ exports.default = UDPServers;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const number_1 = __importDefault(require("../homekit/number"));
7
+ const u = number_1.default.UInt16toBufferBE(287);
8
+ console.log(Buffer.concat([u.slice(1), u.slice(0, 1)]));
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const crypto_1 = __importDefault(require("crypto"));
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const bplist_creator_1 = __importDefault(require("bplist-creator"));
9
+ const bplist_parser_1 = __importDefault(require("bplist-parser"));
10
+ const srp_1 = __importDefault(require("./srp"));
11
+ const atvAuthenticator_1 = __importDefault(require("./atvAuthenticator"));
12
+ const http_1 = __importDefault(require("../utils/http"));
13
+ // ...
14
+ // Configuration.
15
+ const loadConfig = (configFilePath) => !fs_1.default.existsSync(configFilePath) ? null : JSON.parse(fs_1.default.readFileSync(configFilePath, 'utf8'));
16
+ const saveConfig = (configFilePath, config) => fs_1.default.writeFileSync(configFilePath, JSON.stringify(config, null, '\t'));
17
+ // ...
18
+ class ATV {
19
+ addr;
20
+ port;
21
+ httpClient;
22
+ auth_secret;
23
+ constructor(addr, port) {
24
+ this.addr = addr;
25
+ this.port = port || 7000;
26
+ this.httpClient = (0, http_1.default)();
27
+ this.auth_secret = null;
28
+ }
29
+ // ...
30
+ auth(configFilePath, authenticator) {
31
+ async function auth(owner) {
32
+ await owner.httpClient.connect(owner.addr, owner.port);
33
+ let conf = loadConfig(configFilePath);
34
+ const authSecret = conf && typeof conf['auth_secret'] === 'string' ? conf['auth_secret'] : null;
35
+ if (!authSecret) {
36
+ // a pairing does not exist and must be performed.
37
+ // ...
38
+ // SRP parameters.
39
+ const srp = new srp_1.default(2048);
40
+ const I = '366B4165DD64AD3A';
41
+ let P;
42
+ let s;
43
+ let B;
44
+ let a;
45
+ let A;
46
+ let M1;
47
+ await owner.httpClient.request('POST', '/pair-pin-start')
48
+ .then(() => authenticator())
49
+ .then(pin => {
50
+ P = pin;
51
+ return owner.httpClient.request('POST', '/pair-setup-pin', {
52
+ 'Content-Type': 'application/x-apple-binary-plist'
53
+ }, (0, bplist_creator_1.default)({
54
+ user: '366B4165DD64AD3A',
55
+ method: 'pin'
56
+ }));
57
+ })
58
+ .then((res) => {
59
+ const { pk, salt } = bplist_parser_1.default.parseBuffer(res.body)[0];
60
+ s = salt.toString('hex');
61
+ B = pk.toString('hex');
62
+ // SRP: Generate random auth_secret, 'a'; if pairing is successful, it'll be utilized in
63
+ // subsequent session authentication(s).
64
+ a = crypto_1.default.randomBytes(32).toString('hex');
65
+ // SRP: Compute A and M1.
66
+ A = srp.A(a);
67
+ M1 = srp.M1(I, P, s, a, B);
68
+ return owner.httpClient.request('POST', '/pair-setup-pin', {
69
+ 'Content-Type': 'application/x-apple-binary-plist'
70
+ }, (0, bplist_creator_1.default)({
71
+ pk: Buffer.from(A, 'hex'),
72
+ proof: Buffer.from(M1, 'hex')
73
+ }));
74
+ })
75
+ .then(() => {
76
+ // confirm the auth secret (a).
77
+ const { epk, authTag } = atvAuthenticator_1.default.confirm(a, srp.K(I, P, s, a, B));
78
+ // complete pair-setup-pin by registering the auth secret with the target device.
79
+ return owner.httpClient.request('POST', '/pair-setup-pin', {
80
+ 'Content-Type': 'application/x-apple-binary-plist'
81
+ }, (0, bplist_creator_1.default)({
82
+ epk: Buffer.from(epk, 'hex'),
83
+ authTag: Buffer.from(authTag, 'hex')
84
+ }));
85
+ })
86
+ .then(() => {
87
+ // save the auth secret for subsequent session authentication(s).
88
+ if (!conf) {
89
+ conf = {};
90
+ }
91
+ conf['auth_secret'] = a;
92
+ saveConfig(configFilePath, conf);
93
+ });
94
+ }
95
+ // ...
96
+ // Authenticate session with the target device using existing pairing information.
97
+ const verifier = atvAuthenticator_1.default.verifier(conf?.['auth_secret'] ?? '');
98
+ return owner.httpClient.request('POST', '/pair-verify', {
99
+ 'Content-Type': 'application/octet-stream'
100
+ }, verifier.verifierBody)
101
+ .then((res) => {
102
+ const atv_pub = res.body.slice(0, 32).toString('hex');
103
+ const atv_data = res.body.slice(32).toString('hex');
104
+ const shared = atvAuthenticator_1.default.shared(verifier.v_pri, atv_pub);
105
+ const signed = atvAuthenticator_1.default.signed(conf?.['auth_secret'] ?? '', verifier.v_pub, atv_pub);
106
+ const signature = Buffer.from(Buffer.from([0x00, 0x00, 0x00, 0x00]).toString('hex') +
107
+ atvAuthenticator_1.default.signature(shared, atv_data, signed), 'hex');
108
+ return owner.httpClient.request('POST', '/pair-verify', {
109
+ 'Content-Type': 'application/octet-stream'
110
+ }, signature);
111
+ });
112
+ }
113
+ return auth(this);
114
+ }
115
+ authSecret() {
116
+ return this.auth_secret;
117
+ }
118
+ authSimple(authenticator) {
119
+ async function auth(owner) {
120
+ await owner.httpClient.connect(owner.addr, owner.port);
121
+ const conf = null;
122
+ if (!conf || !conf['auth_secret']) {
123
+ // a pairing does not exist and must be performed.
124
+ // ...
125
+ // SRP parameters.
126
+ const srp = new srp_1.default(2048);
127
+ const I = '366B4165DD64AD3A';
128
+ let P;
129
+ let s;
130
+ let B;
131
+ let a;
132
+ let A;
133
+ let M1;
134
+ await owner.httpClient.request('POST', '/pair-pin-start')
135
+ .then(() => authenticator())
136
+ .then(pin => {
137
+ P = pin;
138
+ return owner.httpClient.request('POST', '/pair-setup-pin', {
139
+ 'Content-Type': 'application/x-apple-binary-plist'
140
+ }, (0, bplist_creator_1.default)({
141
+ user: '366B4165DD64AD3A',
142
+ method: 'pin'
143
+ }));
144
+ })
145
+ .then((res) => {
146
+ const { pk, salt } = bplist_parser_1.default.parseBuffer(res.body)[0];
147
+ s = salt.toString('hex');
148
+ B = pk.toString('hex');
149
+ // SRP: Generate random auth_secret, 'a'; if pairing is successful, it'll be utilized in
150
+ // subsequent session authentication(s).
151
+ a = crypto_1.default.randomBytes(32).toString('hex');
152
+ // SRP: Compute A and M1.
153
+ A = srp.A(a);
154
+ M1 = srp.M1(I, P, s, a, B);
155
+ return owner.httpClient.request('POST', '/pair-setup-pin', {
156
+ 'Content-Type': 'application/x-apple-binary-plist'
157
+ }, (0, bplist_creator_1.default)({
158
+ pk: Buffer.from(A, 'hex'),
159
+ proof: Buffer.from(M1, 'hex')
160
+ }));
161
+ }).then(() => {
162
+ // confirm the auth secret (a).
163
+ const { epk, authTag } = atvAuthenticator_1.default.confirm(a, srp.K(I, P, s, a, B));
164
+ // complete pair-setup-pin by registering the auth secret with the target device.
165
+ return owner.httpClient.request('POST', '/pair-setup-pin', {
166
+ 'Content-Type': 'application/x-apple-binary-plist'
167
+ }, (0, bplist_creator_1.default)({
168
+ epk: Buffer.from(epk, 'hex'),
169
+ authTag: Buffer.from(authTag, 'hex')
170
+ }));
171
+ })
172
+ .then(() => {
173
+ // save the auth secret for subsequent session authentication(s).
174
+ owner.auth_secret = a;
175
+ });
176
+ }
177
+ }
178
+ return auth(this);
179
+ }
180
+ verifySimple(secret) {
181
+ // ...
182
+ // Authenticate session with the target device using existing pairing information.
183
+ const verifier = atvAuthenticator_1.default.verifier(secret);
184
+ return this.httpClient.request('POST', '/pair-verify', {
185
+ 'Content-Type': 'application/octet-stream',
186
+ }, verifier.verifierBody)
187
+ .then((res) => {
188
+ const atv_pub = res.body.slice(0, 32).toString('hex');
189
+ const atv_data = res.body.slice(32).toString('hex');
190
+ const shared = atvAuthenticator_1.default.shared(verifier.v_pri, atv_pub);
191
+ const signed = atvAuthenticator_1.default.signed(secret, verifier.v_pub, atv_pub);
192
+ const signature = Buffer.from(Buffer.from([0x00, 0x00, 0x00, 0x00]).toString('hex') +
193
+ atvAuthenticator_1.default.signature(shared, atv_data, signed), 'hex');
194
+ return this.httpClient.request('POST', '/pair-verify', {
195
+ 'Content-Type': 'application/octet-stream',
196
+ }, signature);
197
+ });
198
+ }
199
+ play(videoUrl) {
200
+ return this.httpClient.request('POST', '/play', {
201
+ 'Content-Type': 'application/x-apple-binary-plist'
202
+ }, (0, bplist_creator_1.default)({
203
+ 'Content-Location': videoUrl,
204
+ 'Start-Location': 0
205
+ }));
206
+ }
207
+ stop() {
208
+ return this.httpClient.request('POST', '/stop');
209
+ }
210
+ close() {
211
+ this.httpClient.close();
212
+ }
213
+ }
214
+ // ...
215
+ exports.default = ATV;