@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,1678 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Client = Client;
4
+ // @ts-nocheck
5
+ var net = require('net'), nodeCrypto = require('crypto'), events = require('events'), util = require('util'), fs = require('fs'), dgram = require('dgram');
6
+ const ntp = require('../utils/ntp').default ?? require('../utils/ntp');
7
+ const config = require('../utils/config').default ?? require('../utils/config');
8
+ const nu = require('../utils/numUtil');
9
+ const bplistCreator = require('bplist-creator');
10
+ const bplistParser = require('bplist-parser');
11
+ const LegacySRP = require('./srp').default ?? require('./srp');
12
+ const { SRP, SRPClient, SrpClient } = require('fast-srp-hap');
13
+ const ATVAuthenticator = require('./atvAuthenticator').default ?? require('./atvAuthenticator');
14
+ const tlv = require('../homekit/tlv').default;
15
+ const enc = require('../homekit/encryption').default;
16
+ const Credentials = require('../homekit/credentials').Credentials;
17
+ const { method, attempt } = require('lodash');
18
+ const { hexString2ArrayBuffer } = require('../utils/util');
19
+ const ed25519_js = require('@noble/ed25519');
20
+ const curve25519_js = require('curve25519-js');
21
+ const varint = require('varint');
22
+ const struct = require('python-struct');
23
+ const { default: number } = require('../homekit/number');
24
+ var INFO = -1, OPTIONS = 0, ANNOUNCE = 1, SETUP = 2, RECORD = 3, SETVOLUME = 4, PLAYING = 5, TEARDOWN = 6, CLOSED = 7, SETDAAP = 8, SETART = 9, PAIR_VERIFY_1 = 10, PAIR_VERIFY_2 = 11, OPTIONS2 = 12, AUTH_SETUP = 13, PAIR_PIN_START = 14, PAIR_PIN_SETUP_1 = 15, PAIR_PIN_SETUP_2 = 16, PAIR_PIN_SETUP_3 = 17, PAIR_SETUP_1 = 18, PAIR_SETUP_2 = 19, PAIR_SETUP_3 = 20, PAIR_VERIFY_HAP_1 = 21, PAIR_VERIFY_HAP_2 = 22, SETUP_AP2_1 = 23, SETUP_AP2_2 = 24, SETPEERS = 25, FLUSH = 26, GETVOLUME = 27, SETPROGRESS = 28, OPTIONS3 = 29;
25
+ var rtsp_methods = ["INFO",
26
+ "OPTIONS",
27
+ "ANNOUNCE",
28
+ "SETUP",
29
+ "RECORD",
30
+ "SETVOLUME",
31
+ "PLAYING",
32
+ "TEARDOWN",
33
+ "CLOSED",
34
+ "SETDAAP",
35
+ "SETART",
36
+ "PAIR_VERIFY_1",
37
+ "PAIR_VERIFY_2",
38
+ "OPTIONS2",
39
+ "AUTH_SETUP",
40
+ "PAIR_PIN_START",
41
+ "PAIR_PIN_SETUP_1",
42
+ "PAIR_PIN_SETUP_2",
43
+ "PAIR_PIN_SETUP_3",
44
+ "PAIR_SETUP_1",
45
+ "PAIR_SETUP_2",
46
+ "PAIR_SETUP_3",
47
+ "PAIR_VERIFY_HAP_1",
48
+ "PAIR_VERIFY_HAP_2",
49
+ "SETUP_AP2_1",
50
+ "SETUP_AP2_2",
51
+ "SETPEERS",
52
+ "FLUSH",
53
+ "GETVOLUME",
54
+ "SETPROGRESS",
55
+ "OPTIONS3"
56
+ ];
57
+ function formatLogArg(value) {
58
+ if (Buffer.isBuffer(value)) {
59
+ return `<buffer len=${value.length}>`;
60
+ }
61
+ if (value instanceof Uint8Array) {
62
+ return `<uint8 len=${value.length}>`;
63
+ }
64
+ if (typeof value === 'string') {
65
+ return value;
66
+ }
67
+ try {
68
+ return JSON.stringify(value);
69
+ }
70
+ catch {
71
+ return String(value);
72
+ }
73
+ }
74
+ function logLine(ctx, ...args) {
75
+ if (!args.length) {
76
+ return;
77
+ }
78
+ if (ctx?.log) {
79
+ const formatted = args.map(formatLogArg).join(' ');
80
+ ctx.log('debug', formatted);
81
+ return;
82
+ }
83
+ // eslint-disable-next-line no-console
84
+ console.log(...args);
85
+ }
86
+ function Client(volume, password, audioOut, options) {
87
+ events.EventEmitter.call(this);
88
+ this.audioOut = audioOut;
89
+ this.status = PAIR_VERIFY_1;
90
+ this.socket = null;
91
+ this.cseq = 0;
92
+ this.announceId = null;
93
+ this.activeRemote = nu.randomInt(9).toString().toUpperCase();
94
+ this.dacpId = "04F8191D99BEC6E9";
95
+ this.session = null;
96
+ this.timeout = null;
97
+ this.volume = volume;
98
+ this.progress = 0;
99
+ this.duration = 0;
100
+ this.starttime = 0;
101
+ this.password = password;
102
+ this.passwordTried = false;
103
+ this.requireEncryption = false;
104
+ this.trackInfo = null;
105
+ this.artwork = null;
106
+ this.artworkContentType = null;
107
+ this.callback = null;
108
+ this.controlPort = null;
109
+ this.timingPort = null;
110
+ this.sentFakeProgess = false;
111
+ this.timingDestPort = null;
112
+ this.eventPort = null;
113
+ this.heartBeat = null;
114
+ this.pair_verify_1_verifier = null;
115
+ this.pair_verify_1_signature = null;
116
+ this.code_digest = null;
117
+ this.authSecret = null;
118
+ this.mode = options?.mode ?? 0;
119
+ this.dnstxt = options?.txt ?? [];
120
+ this.alacEncoding = options?.alacEncoding ?? true;
121
+ this.needPassword = options?.needPassword ?? false;
122
+ this.airplay2 = options?.airplay2 ?? false;
123
+ this.needPin = options?.needPin ?? false;
124
+ this.debug = options?.debug ?? false;
125
+ this.transient = options?.transient ?? false;
126
+ this.borkedshp = options?.borkedshp ?? false;
127
+ this.log = options?.log;
128
+ this.logLine = (...args) => logLine(this, ...args);
129
+ this.privateKey = null;
130
+ this.srp = new SRP(2048);
131
+ this.I = '366B4165DD64AD3A';
132
+ this.P = null;
133
+ this.s = null;
134
+ this.B = null;
135
+ this.a = null;
136
+ this.A = null;
137
+ this.M1 = null;
138
+ this.epk = null;
139
+ this.authTag = null;
140
+ this._atv_salt = null;
141
+ this._atv_pub_key = null;
142
+ this._hap_genkey = null;
143
+ this._hap_encrypteddata = null;
144
+ this.pairingId = null;
145
+ this.seed = null;
146
+ this.credentials = null;
147
+ this.event_credentials = null;
148
+ this.verifier_hap_1 = null;
149
+ this.encryptionKey = null;
150
+ this.encryptedChannel = false;
151
+ this.hostip = null;
152
+ this.homekitver = this.transient ? "4" : "3";
153
+ this.metadataReady = false;
154
+ this.connectAttempts = 0;
155
+ this.lastBackoff = 0;
156
+ }
157
+ util.inherits(Client, events.EventEmitter);
158
+ exports.default = { Client };
159
+ Client.prototype.startHandshake = function (udpServers, host, port) {
160
+ this.startTimeout();
161
+ this.hostip = host;
162
+ this.controlPort = udpServers.control.port;
163
+ this.timingPort = udpServers.timing.port;
164
+ this.connectAttempts = 0;
165
+ const connect = () => {
166
+ const attempt = this.connectAttempts ?? 0;
167
+ this.socket = net.connect(port, host, async () => {
168
+ this.clearTimeout();
169
+ this.connectAttempts = 0;
170
+ if (this.needPassword || this.needPin) {
171
+ this.status = PAIR_PIN_START;
172
+ this.sendNextRequest();
173
+ this.startHeartBeat();
174
+ }
175
+ else {
176
+ if (this.mode != 2) {
177
+ if (this.debug)
178
+ this.logLine?.("AUTH_SETUP", "nah");
179
+ this.status = OPTIONS;
180
+ this.sendNextRequest();
181
+ this.startHeartBeat();
182
+ }
183
+ else {
184
+ this.status = AUTH_SETUP;
185
+ if (this.debug)
186
+ this.logLine?.("AUTH_SETUP", "yah");
187
+ this.sendNextRequest();
188
+ this.startHeartBeat();
189
+ }
190
+ }
191
+ });
192
+ let blob = '';
193
+ this.socket.on('data', (data) => {
194
+ if (this.encryptedChannel && this.credentials) {
195
+ // if (this.debug != false) this.logLine?.("incoming", data)
196
+ data = this.credentials.decrypt(data);
197
+ }
198
+ this.clearTimeout();
199
+ /*
200
+ * I wish I could use node's HTTP parser for this...
201
+ * I assume that all responses have empty bodies.
202
+ */
203
+ const rawData = data;
204
+ const dataStr = data.toString();
205
+ blob += dataStr;
206
+ let endIndex = blob.indexOf('\r\n\r\n');
207
+ if (endIndex < 0) {
208
+ return;
209
+ }
210
+ endIndex += 4;
211
+ blob = blob.substring(0, endIndex);
212
+ this.processData(blob, rawData);
213
+ blob = dataStr.substring(endIndex);
214
+ });
215
+ this.socket.on('error', (err) => {
216
+ this.socket = null;
217
+ this.clearTimeout();
218
+ const nextAttempt = (this.connectAttempts ?? 0) + 1;
219
+ this.connectAttempts = nextAttempt;
220
+ const shouldRetry = nextAttempt <= config.rtsp_retry_attempts;
221
+ if (shouldRetry) {
222
+ const baseBackOff = Math.min(config.rtsp_retry_base_ms * Math.pow(2, nextAttempt - 1), config.rtsp_retry_max_ms);
223
+ const jitter = Math.random() * config.rtsp_retry_jitter_ms;
224
+ const backOff = baseBackOff + jitter;
225
+ this.lastBackoff = backOff;
226
+ if (this.debug || config.debug_dump)
227
+ this.logLine?.('rtsp_retry', { attempt: nextAttempt, backOff, code: err?.code });
228
+ setTimeout(() => {
229
+ this.startTimeout();
230
+ connect();
231
+ }, backOff);
232
+ return;
233
+ }
234
+ if (this.debug)
235
+ this.logLine?.(err?.code);
236
+ if (err?.code === 'ECONNREFUSED') {
237
+ if (this.debug)
238
+ this.logLine?.('block');
239
+ this.cleanup('connection_refused');
240
+ }
241
+ else
242
+ this.cleanup('rtsp_socket', err?.code);
243
+ });
244
+ this.socket.on('end', () => {
245
+ if (this.debug) {
246
+ this.logLine?.('rtsp_end', {
247
+ lastStatus: rtsp_methods[this.status + 1] ?? this.status,
248
+ cseq: this.cseq,
249
+ });
250
+ }
251
+ this.connectAttempts = 0;
252
+ this.lastBackoff = 0;
253
+ this.cleanup('disconnected');
254
+ });
255
+ };
256
+ connect();
257
+ };
258
+ Client.prototype.startTimeout = function () {
259
+ if (this.timeout) {
260
+ clearTimeout(this.timeout);
261
+ this.timeout = null;
262
+ }
263
+ this.timeout = setTimeout(() => {
264
+ if (this.debug)
265
+ this.logLine?.('timeout');
266
+ this.cleanup('timeout');
267
+ }, config.rtsp_timeout);
268
+ };
269
+ Client.prototype.clearTimeout = function () {
270
+ if (this.timeout !== null) {
271
+ clearTimeout(this.timeout);
272
+ this.timeout = null;
273
+ }
274
+ };
275
+ Client.prototype.teardown = function () {
276
+ if (this.status === CLOSED) {
277
+ this.emit('end', 'stopped');
278
+ return;
279
+ }
280
+ this.status = TEARDOWN;
281
+ this.sendNextRequest();
282
+ };
283
+ Client.prototype.setVolume = function (volume, callback) {
284
+ if (this.status !== PLAYING)
285
+ return;
286
+ this.volume = volume;
287
+ this.callback = callback;
288
+ this.status = SETVOLUME;
289
+ this.sendNextRequest();
290
+ };
291
+ Client.prototype.setProgress = function (progress, duration, callback) {
292
+ if (this.status !== PLAYING)
293
+ return;
294
+ let normProgress = progress;
295
+ let normDuration = duration;
296
+ if (normDuration > 1000) {
297
+ if (normProgress > 1000) {
298
+ normProgress = Math.round(normProgress / 1000);
299
+ }
300
+ normDuration = Math.round(normDuration / 1000);
301
+ }
302
+ if (normDuration > 0) {
303
+ normProgress = Math.min(Math.max(0, normProgress), normDuration);
304
+ }
305
+ this.progress = normProgress;
306
+ this.duration = normDuration;
307
+ this.callback = callback;
308
+ this.status = SETPROGRESS;
309
+ this.sendNextRequest();
310
+ };
311
+ Client.prototype.setPasscode = async function (passcode) {
312
+ this.password = passcode;
313
+ this.status = this.airplay2 ? PAIR_SETUP_1 : PAIR_PIN_SETUP_1;
314
+ this.sendNextRequest();
315
+ };
316
+ Client.prototype.startHeartBeat = function () {
317
+ if (this.heartBeat) {
318
+ clearInterval(this.heartBeat);
319
+ this.heartBeat = null;
320
+ }
321
+ if (config.rtsp_heartbeat > 0) {
322
+ this.heartBeat = setInterval(() => {
323
+ this.sendHeartBeat(() => {
324
+ //this.logLine?.('HeartBeat sent!');
325
+ });
326
+ }, config.rtsp_heartbeat);
327
+ }
328
+ };
329
+ Client.prototype.sendHeartBeat = function (callback) {
330
+ if (this.status !== PLAYING)
331
+ return;
332
+ this.status = OPTIONS;
333
+ this.callback = callback;
334
+ this.sendNextRequest();
335
+ };
336
+ Client.prototype.setTrackInfo = function (name, artist, album, callback) {
337
+ if (this.status !== PLAYING)
338
+ return;
339
+ if (name != this.trackInfo?.name || artist != this.trackInfo?.artist || album != this.trackInfo?.album) {
340
+ this.starttime = this.audioOut.lastSeq * config.frames_per_packet + 2 * config.sampling_rate;
341
+ }
342
+ this.trackInfo = {
343
+ name: name,
344
+ artist: artist,
345
+ album: album
346
+ };
347
+ this.status = SETDAAP;
348
+ this.callback = callback;
349
+ this.sendNextRequest();
350
+ };
351
+ Client.prototype.setArtwork = function (art, contentType, callback) {
352
+ if (this.status !== PLAYING)
353
+ return;
354
+ if (typeof contentType == 'function') {
355
+ callback = contentType;
356
+ contentType = null;
357
+ }
358
+ if (typeof art == 'string') {
359
+ if (contentType === null) {
360
+ var ext = art.slice(-4);
361
+ if (ext == ".jpg" || ext == "jpeg") {
362
+ contentType = "image/jpeg";
363
+ }
364
+ else if (ext == ".png") {
365
+ contentType = "image/png";
366
+ }
367
+ else if (ext == ".gif") {
368
+ contentType = "image/gif";
369
+ }
370
+ else {
371
+ return this.cleanup('unknown_art_file_ext');
372
+ }
373
+ }
374
+ return fs.readFile(art, (err, data) => {
375
+ if (err !== null) {
376
+ return this.cleanup('invalid_art_file');
377
+ }
378
+ this.setArtwork(data, contentType, callback);
379
+ });
380
+ }
381
+ if (contentType === null)
382
+ return this.cleanup('no_art_content_type');
383
+ this.artworkContentType = contentType;
384
+ this.artwork = art;
385
+ this.status = SETART;
386
+ this.callback = callback;
387
+ this.sendNextRequest();
388
+ };
389
+ Client.prototype.nextCSeq = function () {
390
+ this.cseq += 1;
391
+ return this.cseq;
392
+ };
393
+ Client.prototype.cleanup = function (type, msg) {
394
+ this.emit('end', type, msg);
395
+ this.status = CLOSED;
396
+ this.trackInfo = null;
397
+ this.artwork = null;
398
+ this.artworkContentType = null;
399
+ this.callback = null;
400
+ this.srp = null;
401
+ this.P = null;
402
+ this.s = null;
403
+ this.B = null;
404
+ this.a = null;
405
+ this.A = null;
406
+ this.M1 = null;
407
+ this.epk = null;
408
+ this.authTag = null;
409
+ this._hap_genkey = null;
410
+ this._hap_encrypteddata = null;
411
+ this.seed = null;
412
+ this.credentials = null;
413
+ // this.password = null;
414
+ this.connectAttempts = 0;
415
+ this.removeAllListeners();
416
+ if (this.timeout) {
417
+ clearTimeout(this.timeout);
418
+ this.timeout = null;
419
+ }
420
+ if (this.heartBeat) {
421
+ clearInterval(this.heartBeat);
422
+ this.heartBeat = null;
423
+ }
424
+ if (this.socket) {
425
+ this.socket.destroy();
426
+ this.socket = null;
427
+ }
428
+ if (this.eventsocket) {
429
+ try {
430
+ this.eventsocket.destroy?.();
431
+ this.eventsocket = null;
432
+ }
433
+ catch {
434
+ /* ignore */
435
+ }
436
+ }
437
+ if (this.controlsocket) {
438
+ try {
439
+ this.controlsocket.close();
440
+ }
441
+ catch {
442
+ /* ignore */
443
+ }
444
+ this.controlsocket = null;
445
+ }
446
+ if (this.timingsocket) {
447
+ try {
448
+ this.timingsocket.close();
449
+ }
450
+ catch {
451
+ /* ignore */
452
+ }
453
+ this.timingsocket = null;
454
+ }
455
+ const audioSocket = this.audioSocket;
456
+ if (audioSocket) {
457
+ try {
458
+ audioSocket.close?.();
459
+ audioSocket.destroy?.();
460
+ }
461
+ catch {
462
+ /* ignore */
463
+ }
464
+ this.audioSocket = null;
465
+ }
466
+ };
467
+ function parseResponse(blob) {
468
+ var response = {}, lines = blob.split('\r\n');
469
+ if (lines[0].match(/^Audio-Latency/)) {
470
+ let tmp = lines[0];
471
+ lines[0] = lines[1];
472
+ lines[1] = tmp;
473
+ }
474
+ var codeRes = /(\w+)\/(\S+) (\d+) (.*)/.exec(lines[0]);
475
+ if (!codeRes) {
476
+ response.code = 599;
477
+ response.status = 'UNEXPECTED ' + lines[0];
478
+ return response;
479
+ }
480
+ response.code = parseInt(codeRes[3], 10);
481
+ response.status = codeRes[4];
482
+ var headers = {};
483
+ lines.slice(1).forEach(function (line) {
484
+ var res = /([^:]+):\s*(.*)/.exec(line);
485
+ if (!res)
486
+ return;
487
+ headers[res[1]] = res[2];
488
+ });
489
+ response.headers = headers;
490
+ //this.logLine?.(response);
491
+ return response;
492
+ }
493
+ function parseResponse2(blob, self) {
494
+ var response = {}, lines = blob.split('\r\n');
495
+ // if (self.debug) self.logLine?.(lines);
496
+ if (lines[0].match(/^Audio-Latency/)) {
497
+ let tmp = lines[0];
498
+ lines[0] = lines[1];
499
+ lines[1] = tmp;
500
+ }
501
+ var codeRes = /(\w+)\/(\S+) (\d+) (.*)/.exec(lines[0]);
502
+ if (!codeRes) {
503
+ response.code = 599;
504
+ response.status = 'UNEXPECTED ' + lines[0];
505
+ return response;
506
+ }
507
+ response.code = parseInt(codeRes[3], 10);
508
+ response.status = codeRes[4];
509
+ var headers = {};
510
+ lines.slice(1).forEach(function (line) {
511
+ var res = /([^:]+):\s*(.*)/.exec(line);
512
+ if (!res)
513
+ return;
514
+ headers[res[1]] = res[2];
515
+ });
516
+ response.headers = headers;
517
+ // if (this.debug) this.logLine?.('res: ', response);
518
+ return response;
519
+ }
520
+ function md5(str) {
521
+ var md5sum = nodeCrypto.createHash('md5');
522
+ md5sum.update(str);
523
+ return md5sum.digest('hex').toUpperCase();
524
+ }
525
+ function md5norm(str) {
526
+ var md5sum = nodeCrypto.createHash('md5');
527
+ md5sum.update(str);
528
+ return md5sum.digest('hex');
529
+ }
530
+ Client.prototype.makeHead = function (method, uri, di, clear = false, dimode = null) {
531
+ var head = method + ' ' + uri + ' RTSP/1.0' + '\r\n';
532
+ if (!clear) {
533
+ head += 'CSeq: ' + this.nextCSeq() + '\r\n' +
534
+ 'User-Agent: ' + (this.airplay2 ? "AirPlay/409.16" : config.user_agent) + '\r\n' +
535
+ 'DACP-ID: ' + this.dacpId + '\r\n' +
536
+ (this.session ? 'Session: ' + this.session + '\r\n' : '') +
537
+ 'Active-Remote: ' + this.activeRemote + '\r\n';
538
+ head += 'Client-Instance: ' + this.dacpId + '\r\n';
539
+ }
540
+ ;
541
+ if (di) {
542
+ if (dimode == 'airplay2') {
543
+ var ha1 = md5norm(di.username + ':' + di.realm + ':' + di.password);
544
+ var ha2 = md5norm(method + ':' + uri);
545
+ var diResponse = md5(ha1 + ':' + di.nonce + ':' + ha2);
546
+ }
547
+ else {
548
+ var ha1 = md5(di.username + ':' + di.realm + ':' + di.password);
549
+ var ha2 = md5(method + ':' + uri);
550
+ var diResponse = md5(ha1 + ':' + di.nonce + ':' + ha2);
551
+ }
552
+ head += 'Authorization: Digest ' +
553
+ 'username="' + di.username + '", ' +
554
+ 'realm="' + di.realm + '", ' +
555
+ 'nonce="' + di.nonce + '", ' +
556
+ 'uri="' + uri + '", ' +
557
+ 'response="' + diResponse + '"\r\n';
558
+ }
559
+ return head;
560
+ };
561
+ Client.prototype.makeHeadWithURL = function (method, digestInfo, dimode) {
562
+ return this.makeHead(method, 'rtsp://' + this.socket.address().address + '/' + this.announceId, digestInfo, false, dimode);
563
+ };
564
+ Client.prototype.makeRtpInfo = function () {
565
+ var nextSeq = this.audioOut.lastSeq + 1;
566
+ var rtpSyncTime = nextSeq * config.frames_per_packet + 2 * config.sampling_rate;
567
+ return 'RTP-Info: seq=' + nextSeq + ';rtptime=' + rtpSyncTime + '\r\n';
568
+ };
569
+ Client.prototype.sendNextRequest = async function (di) {
570
+ var request = '';
571
+ var body = '';
572
+ if (this.debug)
573
+ this.logLine?.('Sending request:', rtsp_methods[this.status + 1]);
574
+ switch (this.status) {
575
+ case PAIR_PIN_START:
576
+ this.I = '366B4165DD64AD3A';
577
+ this.P = null;
578
+ this.s = null;
579
+ this.B = null;
580
+ this.a = null;
581
+ this.A = null;
582
+ this.M1 = null;
583
+ this.epk = null;
584
+ this.authTag = null;
585
+ this._atv_salt = null;
586
+ this._atv_pub_key = null;
587
+ this._hap_encrypteddata = null;
588
+ this.seed = null;
589
+ this.pairingId = nodeCrypto.randomUUID();
590
+ this.credentials = null;
591
+ this.verifier_hap_1 = null;
592
+ this.encryptionKey = null;
593
+ request = '';
594
+ if (this.transient && (this.needPin != true) && (this.needPassword != true)) {
595
+ (this.status = PAIR_SETUP_1);
596
+ this.sendNextRequest();
597
+ }
598
+ else if (this.needPin) {
599
+ request += this.makeHead("POST", "/pair-pin-start", "", true);
600
+ if (this.airplay2) {
601
+ request += 'User-Agent: AirPlay/409.16\r\n';
602
+ request += 'Connection: keep-alive\r\n';
603
+ request += 'CSeq: ' + 0 + '\r\n';
604
+ }
605
+ request += 'Content-Length:' + 0 + '\r\n\r\n';
606
+ this.socket.write(Buffer.from(request, 'utf-8'));
607
+ }
608
+ else {
609
+ if (this.password) {
610
+ this.status = this.airplay2 ? PAIR_SETUP_1 : PAIR_PIN_SETUP_1;
611
+ this.sendNextRequest();
612
+ }
613
+ else {
614
+ if (!this.needPassword) {
615
+ this.status = this.airplay2 ? INFO : PAIR_PIN_SETUP_1;
616
+ this.sendNextRequest();
617
+ }
618
+ else {
619
+ this.emit("need_password");
620
+ }
621
+ }
622
+ }
623
+ request = '';
624
+ //}
625
+ break;
626
+ case PAIR_PIN_SETUP_1:
627
+ request = '';
628
+ request += this.makeHead("POST", "/pair-setup-pin", "", true);
629
+ request += 'Content-Type: application/x-apple-binary-plist\r\n';
630
+ let u = bplistCreator({
631
+ user: '366B4165DD64AD3A',
632
+ method: 'pin'
633
+ });
634
+ request += 'Content-Length:' + Buffer.byteLength(u) + '\r\n\r\n';
635
+ this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), u]));
636
+ request = '';
637
+ break;
638
+ case PAIR_PIN_SETUP_2:
639
+ request = '';
640
+ request += this.makeHead("POST", "/pair-setup-pin", "", true);
641
+ request += 'Content-Type: application/x-apple-binary-plist\r\n';
642
+ let u1 = bplistCreator({
643
+ pk: Buffer.from(this.A, 'hex'),
644
+ proof: Buffer.from(this.M1, 'hex')
645
+ });
646
+ request += 'Content-Length:' + Buffer.byteLength(u1) + '\r\n\r\n';
647
+ this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), u1]));
648
+ request = '';
649
+ break;
650
+ case PAIR_PIN_SETUP_3:
651
+ request = '';
652
+ request += this.makeHead("POST", "/pair-setup-pin", "", true);
653
+ request += 'Content-Type: application/x-apple-binary-plist\r\n';
654
+ let u2 = bplistCreator({
655
+ epk: Buffer.from(this.epk, 'hex'),
656
+ authTag: Buffer.from(this.authTag, 'hex')
657
+ });
658
+ request += 'Content-Length:' + Buffer.byteLength(u2) + '\r\n\r\n';
659
+ this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), u2]));
660
+ request = '';
661
+ break;
662
+ case PAIR_VERIFY_1:
663
+ request = '';
664
+ request += this.makeHead("POST", "/pair-verify", "", true);
665
+ request += 'Content-Type: application/octet-stream\r\n';
666
+ this.pair_verify_1_verifier = ATVAuthenticator.verifier(this.authSecret);
667
+ request += 'Content-Length:' + Buffer.byteLength(this.pair_verify_1_verifier.verifierBody) + '\r\n\r\n';
668
+ this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), this.pair_verify_1_verifier.verifierBody]));
669
+ request = '';
670
+ break;
671
+ case PAIR_VERIFY_2:
672
+ request = '';
673
+ request += this.makeHead("POST", "/pair-verify", "", true);
674
+ request += 'Content-Type: application/octet-stream\r\n';
675
+ request += 'Content-Length:' + Buffer.byteLength(this.pair_verify_1_signature) + '\r\n\r\n';
676
+ this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), this.pair_verify_1_signature]));
677
+ request = '';
678
+ // const verifier = ATVAuthenticator.verifier('3c0591f41d1236c9ce5078sscd6fcd42f71f374b8b6dff33fea825366f1c34f828');
679
+ // request += 'Content-Length:' + Buffer.byteLength(verifier.verifierBody) + '\r\n\r\n';
680
+ // this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'),verifier.verifierBody]))
681
+ // request = ''
682
+ break;
683
+ case PAIR_SETUP_1:
684
+ if (this.debug)
685
+ this.logLine?.('loh');
686
+ request = '';
687
+ request += this.makeHead("POST", "/pair-setup", "", true);
688
+ request += 'User-Agent: AirPlay/409.16\r\n';
689
+ request += 'CSeq: ' + this.nextCSeq() + '\r\n';
690
+ request += 'Connection: keep-alive\r\n';
691
+ request += 'X-Apple-HKP: ' + this.homekitver + '\r\n';
692
+ this.logLine?.('rtsp.transient', this.transient);
693
+ if (this.transient == true) {
694
+ this.logLine?.('rtsp.transient', 'uas');
695
+ let ps1 = tlv.encode(tlv.Tag.Sequence, 0x01, tlv.Tag.PairingMethod, 0x00, tlv.Tag.Flags, 0x00000010);
696
+ request += 'Content-Length: ' + Buffer.byteLength(ps1) + '\r\n';
697
+ request += 'Content-Type: application/octet-stream' + '\r\n\r\n';
698
+ this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), ps1]));
699
+ }
700
+ else {
701
+ let ps1 = tlv.encode(tlv.Tag.PairingMethod, 0x00, tlv.Tag.Sequence, 0x01);
702
+ request += 'Content-Length: ' + 6 + '\r\n';
703
+ request += 'Content-Type: application/octet-stream' + '\r\n\r\n';
704
+ this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), ps1]));
705
+ }
706
+ request = '';
707
+ break;
708
+ case PAIR_SETUP_2:
709
+ if (this.debug)
710
+ this.logLine?.('loh2');
711
+ request = '';
712
+ request += this.makeHead("POST", "/pair-setup", "", true);
713
+ request += 'User-Agent: AirPlay/409.16\r\n';
714
+ request += 'CSeq: ' + this.nextCSeq() + '\r\n';
715
+ request += 'Connection: keep-alive\r\n';
716
+ request += 'X-Apple-HKP: ' + this.homekitver + '\r\n';
717
+ request += 'Content-Type: application/octet-stream\r\n';
718
+ let ps2 = tlv.encode(tlv.Tag.Sequence, 0x03, tlv.Tag.PublicKey, this.A, tlv.Tag.Proof, this.M1);
719
+ request += 'Content-Length: ' + Buffer.byteLength(ps2) + '\r\n\r\n';
720
+ this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), ps2]));
721
+ request = '';
722
+ break;
723
+ case PAIR_SETUP_3:
724
+ if (this.debug)
725
+ this.logLine?.('loh3');
726
+ request = '';
727
+ request += this.makeHead("POST", "/pair-setup", "", true);
728
+ request += 'User-Agent: AirPlay/409.16\r\n';
729
+ request += 'CSeq: ' + this.nextCSeq() + '\r\n';
730
+ request += 'Connection: keep-alive\r\n';
731
+ request += 'X-Apple-HKP: ' + this.homekitver + '\r\n';
732
+ request += 'Content-Type: application/octet-stream\r\n';
733
+ this.K = this.srp.computeK();
734
+ this.seed = nodeCrypto.randomBytes(32);
735
+ // let keyPair = ed25519.MakeKeypair(this.seed);
736
+ this.privateKey = ed25519_js.utils.randomPrivateKey();
737
+ let publicKey = await ed25519_js.getPublicKey(this.privateKey);
738
+ // let keyPair = nacl.sign.keyPair.fromSeed(this.seed)
739
+ // let privateKey = keyPair.secretKey;
740
+ // let publicKey = keyPair.publicKey;
741
+ let deviceHash = enc.HKDF("sha512", Buffer.from("Pair-Setup-Controller-Sign-Salt"), this.K, Buffer.from("Pair-Setup-Controller-Sign-Info"), 32);
742
+ let deviceInfo = Buffer.concat([deviceHash, Buffer.from(this.pairingId), publicKey]);
743
+ let deviceSignature = await ed25519_js.sign(deviceInfo, this.privateKey);
744
+ // let deviceSignature = nacl.sign(deviceInfo, privateKey)
745
+ this.encryptionKey = enc.HKDF("sha512", Buffer.from("Pair-Setup-Encrypt-Salt"), this.K, Buffer.from("Pair-Setup-Encrypt-Info"), 32);
746
+ let tlvData = tlv.encode(tlv.Tag.Username, Buffer.from(this.pairingId), tlv.Tag.PublicKey, publicKey, tlv.Tag.Signature, deviceSignature);
747
+ let encryptedTLV = Buffer.concat(enc.encryptAndSeal(tlvData, null, Buffer.from('PS-Msg05'), this.encryptionKey));
748
+ // this.logLine?.("DEBUG: Encrypted Data=" + encryptedTLV.toString('hex'));
749
+ let outerTLV = tlv.encode(tlv.Tag.Sequence, 0x05, tlv.Tag.EncryptedData, encryptedTLV);
750
+ request += 'Content-Length: ' + Buffer.byteLength(outerTLV) + '\r\n\r\n';
751
+ this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), outerTLV]));
752
+ request = '';
753
+ break;
754
+ case PAIR_VERIFY_HAP_1:
755
+ request = '';
756
+ request += this.makeHead("POST", "/pair-verify", "", true);
757
+ request += 'User-Agent: AirPlay/409.16\r\n';
758
+ request += 'CSeq: ' + this.nextCSeq() + '\r\n';
759
+ request += 'Connection: keep-alive\r\n';
760
+ request += 'X-Apple-HKP: ' + this.homekitver + '\r\n';
761
+ request += 'Content-Type: application/octet-stream\r\n';
762
+ let hap1kp = curve25519_js.generateKeyPair(Buffer.alloc(32));
763
+ this.verifyPrivate = Buffer.from(hap1kp.private);
764
+ this.verifyPublic = Buffer.from(hap1kp.public);
765
+ // this.verifyPrivate = Buffer.alloc(32);
766
+ // curve25519.makeSecretKey(this.verifyPrivate);
767
+ // this.verifyPublic = curve25519.derivePublicKey(this.verifyPrivate);
768
+ let encodedData = tlv.encode(tlv.Tag.Sequence, 0x01, tlv.Tag.PublicKey, this.verifyPublic);
769
+ request += 'Content-Length: ' + Buffer.byteLength(encodedData) + '\r\n\r\n';
770
+ this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), encodedData]));
771
+ request = '';
772
+ break;
773
+ case PAIR_VERIFY_HAP_2:
774
+ request = '';
775
+ request += this.makeHead("POST", "/pair-verify", "", true);
776
+ request += 'User-Agent: AirPlay/409.16\r\n';
777
+ request += 'CSeq: ' + this.nextCSeq() + '\r\n';
778
+ request += 'Connection: keep-alive\r\n';
779
+ request += 'X-Apple-HKP: ' + this.homekitver + '\r\n';
780
+ request += 'Content-Type: application/octet-stream\r\n';
781
+ let identifier = tlv.decode(this.verifier_hap_1.pairingData)[tlv.Tag.Username];
782
+ let signature = tlv.decode(this.verifier_hap_1.pairingData)[tlv.Tag.Signature];
783
+ let material = Buffer.concat([this.verifyPublic, Buffer.from(this.credentials.pairingId), this.verifier_hap_1.sessionPublicKey]);
784
+ // let keyPair1 = ed25519.MakeKeypair(this.credentials.encryptionKey);
785
+ // let signed = ed25519.Sign(material, keyPair1);
786
+ // let keyPair1 = ed25519.MakeKeypair(this.credentials.encryptionKey);
787
+ let signed = await ed25519_js.sign(material, this.privateKey);
788
+ this.logLine?.("lengths", this.credentials.encryptionKey.length);
789
+ // let keyPair1 = nacl.sign.keyPair.fromSeed(this.credentials.encryptionKey)
790
+ // let signed = nacl.sign(material, keyPair1.secretKey);
791
+ let plainTLV = tlv.encode(tlv.Tag.Username, Buffer.from(this.credentials.pairingId), tlv.Tag.Signature, signed);
792
+ let encryptedTLV1 = Buffer.concat(enc.encryptAndSeal(plainTLV, null, Buffer.from('PV-Msg03'), this.verifier_hap_1.encryptionKey));
793
+ let pv2 = tlv.encode(tlv.Tag.Sequence, 0x03, tlv.Tag.EncryptedData, encryptedTLV1);
794
+ request += 'Content-Length: ' + Buffer.byteLength(pv2) + '\r\n\r\n';
795
+ this.socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), pv2]));
796
+ request = '';
797
+ break;
798
+ case AUTH_SETUP:
799
+ request = '';
800
+ request += this.makeHead("POST", "/auth-setup", di);
801
+ request += 'Content-Length: ' + 33 + '\r\n\r\n';
802
+ let finalbuffer = Buffer.concat([Buffer.from(request, 'utf-8'),
803
+ Buffer.from([0x01, // unencrypted
804
+ 0x4e, 0xea, 0xd0, 0x4e, 0xa9, 0x2e, 0x47, 0x69,
805
+ 0xd2, 0xe1, 0xfb, 0xd0, 0x96, 0x81, 0xd5, 0x94,
806
+ 0xa8, 0xef, 0x18, 0x45, 0x4a, 0x24, 0xae, 0xaf,
807
+ 0xb3, 0x14, 0x97, 0x0d, 0xa0, 0xb5, 0xa3, 0x49])
808
+ ]);
809
+ if (this.airplay2 != true && this.credentials != null) {
810
+ try {
811
+ this.socket.write(this.credentials.encrypt(finalbuffer));
812
+ }
813
+ catch (e) {
814
+ }
815
+ }
816
+ else {
817
+ this.socket.write(finalbuffer);
818
+ }
819
+ request = '';
820
+ // this.status = OPTIONS;
821
+ // this.sendNextRequest()
822
+ break;
823
+ case OPTIONS:
824
+ request += this.makeHead('OPTIONS', '*', di);
825
+ if (this.airplay2) {
826
+ request += 'User-Agent: AirPlay/409.16\r\n';
827
+ request += 'Connection: keep-alive\r\n';
828
+ }
829
+ request += 'Apple-Challenge: SdX9kFJVxgKVMFof/Znj4Q\r\n\r\n';
830
+ break;
831
+ case OPTIONS2:
832
+ request = '';
833
+ request += this.makeHead('OPTIONS', '*');
834
+ request += this.code_digest;
835
+ this.logLine?.(request);
836
+ this.socket.write(Buffer.from(request, 'utf-8'));
837
+ request = '';
838
+ break;
839
+ case OPTIONS3:
840
+ request = '';
841
+ request += this.makeHead('OPTIONS', '*', di);
842
+ this.logLine?.(request);
843
+ this.socket.write(Buffer.from(request, 'utf-8'));
844
+ request = '';
845
+ break;
846
+ case ANNOUNCE:
847
+ if (this.announceId == null) {
848
+ this.announceId = nu.randomInt(10);
849
+ }
850
+ body =
851
+ 'v=0\r\n' +
852
+ 'o=iTunes ' + this.announceId + ' 0 IN IP4 ' + this.socket.address().address + '\r\n' +
853
+ 's=iTunes\r\n' +
854
+ 'c=IN IP4 ' + this.socket.address().address + '\r\n' +
855
+ 't=0 0\r\n' +
856
+ 'm=audio 0 RTP/AVP 96\r\n';
857
+ if (!this.alacEncoding) {
858
+ body = body + 'a=rtpmap:96 L16/44100/2\r\n' +
859
+ 'a=fmtp:96 352 0 16 40 10 14 2 255 0 0 44100\r\n';
860
+ }
861
+ else {
862
+ body = body + 'a=rtpmap:96 AppleLossless\r\n' +
863
+ 'a=fmtp:96 352 0 16 40 10 14 2 255 0 0 44100\r\n';
864
+ }
865
+ ;
866
+ if (this.requireEncryption) {
867
+ body +=
868
+ 'a=rsaaeskey:' + config.rsa_aeskey_base64 + '\r\n' +
869
+ 'a=aesiv:' + config.iv_base64 + '\r\n';
870
+ }
871
+ request += this.makeHeadWithURL('ANNOUNCE', di);
872
+ request +=
873
+ 'Content-Type: application/sdp\r\n' +
874
+ 'Content-Length: ' + body.length + '\r\n\r\n';
875
+ request += body;
876
+ //this.logLine?.(request);
877
+ break;
878
+ case SETUP:
879
+ request += this.makeHeadWithURL('SETUP', di);
880
+ request +=
881
+ 'Transport: RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;' +
882
+ 'control_port=' + this.controlPort + ';' +
883
+ 'timing_port=' + this.timingPort + '\r\n\r\n';
884
+ //this.logLine?.(request);
885
+ break;
886
+ case INFO:
887
+ request += this.makeHead('GET', '/info', di, true);
888
+ request += 'User-Agent: AirPlay/409.16\r\n';
889
+ request += 'Connection: keep-alive\r\n';
890
+ request += 'CSeq: ' + this.nextCSeq() + '\r\n\r\n';
891
+ if (this.credentials) {
892
+ let enct1x = this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8')]));
893
+ this.socket.write(enct1x);
894
+ request = '';
895
+ }
896
+ //this.logLine?.(request);
897
+ break;
898
+ case SETUP_AP2_1:
899
+ if (this.announceId == null) {
900
+ this.announceId = nu.randomInt(10);
901
+ }
902
+ request += this.makeHeadWithURL('SETUP', di, "airplay2");
903
+ request += 'Content-Type: application/x-apple-binary-plist\r\n';
904
+ // request += 'CSeq: ' + this.nextCSeq() + '\r\n' ;
905
+ // this.timingPort = 32325;
906
+ this.logLine?.('starting ports', this.timingPort, this.controlPort);
907
+ let setap1 = bplistCreator({ deviceID: '2C:61:F3:B6:64:C1',
908
+ sessionUUID: '8EB266BA-B741-40C5-8213-4B7A38DF8773',
909
+ timingPort: this.timingPort,
910
+ timingProtocol: 'NTP'
911
+ // ekey: config.rsa_aeskey_base64,
912
+ // eiv: config.iv_base64
913
+ });
914
+ try {
915
+ this.timingsocket.close();
916
+ }
917
+ catch (e) { }
918
+ try {
919
+ this.timingsocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
920
+ this.timingsocket.on('message', (msg, rinfo) => {
921
+ // only listen and respond on own hosts
922
+ // if (this.hosts.indexOf(rinfo.address) < 0) return;
923
+ var ts1 = msg.readUInt32BE(24);
924
+ var ts2 = msg.readUInt32BE(28);
925
+ var reply = Buffer.alloc(32);
926
+ reply.writeUInt16BE(0x80d3, 0);
927
+ reply.writeUInt16BE(0x0007, 2);
928
+ reply.writeUInt32BE(0x00000000, 4);
929
+ reply.writeUInt32BE(ts1, 8);
930
+ reply.writeUInt32BE(ts2, 12);
931
+ var ntpTime = ntp.timestamp();
932
+ ntpTime.copy(reply, 16);
933
+ ntpTime.copy(reply, 24);
934
+ this.timingsocket.send(reply, 0, reply.length, rinfo.port, rinfo.address);
935
+ this.logLine?.('timing socket pinged', rinfo.port, rinfo.address);
936
+ });
937
+ this.timingsocket.bind(this.timingPort, this.socket.address().address);
938
+ }
939
+ catch (e) { }
940
+ request += 'Content-Length: ' + Buffer.byteLength(setap1) + '\r\n\r\n';
941
+ this.logLine?.(request);
942
+ let s1ct = this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8'), setap1]));
943
+ this.socket.write(s1ct);
944
+ request = '';
945
+ break;
946
+ case SETPEERS:
947
+ request += this.makeHeadWithURL('SETPEERS', di);
948
+ request += 'Content-Type: /peer-list-changed\r\n';
949
+ let speers = bplistCreator([
950
+ this.hostip, this.socket.address().address
951
+ ]);
952
+ this.logLine?.([
953
+ this.hostip, this.socket.address().address
954
+ ]);
955
+ request += 'Content-Length: ' + Buffer.byteLength(speers) + '\r\n\r\n';
956
+ let spct = this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8'), speers]));
957
+ this.socket.write(spct);
958
+ request = '';
959
+ break;
960
+ case FLUSH:
961
+ request += this.makeHeadWithURL('FLUSH', di);
962
+ request += this.makeRtpInfo() + '\r\n';
963
+ let fct = this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8')]));
964
+ this.socket.write(fct);
965
+ request = '';
966
+ break;
967
+ case SETUP_AP2_2:
968
+ if (this.announceId == null) {
969
+ this.announceId = nu.randomInt(10);
970
+ }
971
+ request += this.makeHeadWithURL('SETUP', di);
972
+ request += 'Content-Type: application/x-apple-binary-plist\r\n';
973
+ let setap2 = bplistCreator({ streams: [{ audioFormat: 262144, // PCM/44100/16/2
974
+ audioMode: 'default',
975
+ controlPort: this.controlPort,
976
+ ct: 2,
977
+ isMedia: true,
978
+ latencyMax: 88200,
979
+ latencyMin: 11025,
980
+ shk: Buffer.from(this.credentials.writeKey),
981
+ spf: 352,
982
+ sr: 44100,
983
+ type: 0x60,
984
+ supportsDynamicStreamID: false,
985
+ streamConnectionID: this.announceId
986
+ }] });
987
+ request += 'Content-Length: ' + Buffer.byteLength(setap2) + '\r\n\r\n';
988
+ this.controlsocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
989
+ this.controlsocket.on('message', (msg) => {
990
+ this.logLine?.('controlsocket.data', msg);
991
+ });
992
+ this.controlsocket.bind(this.controlPort, this.socket.address().address);
993
+ let s2ct = this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8'), setap2]));
994
+ this.socket.write(s2ct);
995
+ request = '';
996
+ break;
997
+ case RECORD:
998
+ //this.logLine?.(request);
999
+ if (this.airplay2) {
1000
+ this.event_credentials = new Credentials("sdsds", "", "", "", this.seed);
1001
+ this.event_credentials.writeKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Read-Encryption-Key"), 32);
1002
+ this.event_credentials.readKey = enc.HKDF("sha512", Buffer.from("Events-Salt"), this.srp.computeK(), Buffer.from("Events-Write-Encryption-Key"), 32);
1003
+ this.eventsocket = net.connect(this.eventPort, this.hostip, async () => {
1004
+ });
1005
+ this.eventsocket.on('data', (data) => {
1006
+ if (this.debug) {
1007
+ this.logLine?.('eventsocket.data', data);
1008
+ try {
1009
+ const decrypted = this.event_credentials?.decrypt(data);
1010
+ if (decrypted) {
1011
+ this.logLine?.('eventsocket.data2', decrypted.toString());
1012
+ }
1013
+ }
1014
+ catch (err) {
1015
+ this.logLine?.('eventsocket.decrypt.error', err);
1016
+ }
1017
+ }
1018
+ });
1019
+ this.eventsocket.on('error', (err) => {
1020
+ if (this.debug)
1021
+ this.logLine?.('eventsocket.error', err);
1022
+ });
1023
+ }
1024
+ if (this.airplay2 != null && this.credentials != null) {
1025
+ // this.controlsocket.close();
1026
+ var nextSeq = this.audioOut.lastSeq + 10;
1027
+ var rtpSyncTime = nextSeq * config.frames_per_packet + 2 * config.sampling_rate;
1028
+ request += this.makeHead('RECORD', 'rtsp://' + this.socket.address().address + '/' + this.announceId, di, true);
1029
+ request += 'CSeq: ' + ++this.cseq + '\r\n';
1030
+ request += 'User-Agent: AirPlay/409.16' + '\r\n';
1031
+ request += 'Client-Instance: ' + this.dacpId + '\r\n';
1032
+ request += 'DACP-ID: ' + this.dacpId + '\r\n';
1033
+ request += 'Active-Remote: ' + this.activeRemote + '\r\n';
1034
+ request += 'X-Apple-ProtocolVersion: 1\r\n';
1035
+ request += 'Range: npt=0-\r\n';
1036
+ request += this.makeRtpInfo() + '\r\n';
1037
+ // request += '\r\n';
1038
+ this.logLine?.('ssdas3', request);
1039
+ let rct = this.credentials.encrypt(Buffer.from(request, 'utf-8'));
1040
+ this.socket.write(rct);
1041
+ request = "";
1042
+ }
1043
+ else {
1044
+ request += this.makeHeadWithURL('RECORD', di);
1045
+ request += 'Range: npt=0-\r\n';
1046
+ request += this.makeRtpInfo() + '\r\n';
1047
+ }
1048
+ break;
1049
+ case GETVOLUME:
1050
+ body = "volume\r\n";
1051
+ request += this.makeHeadWithURL('GET_PARAMETER', di);
1052
+ request +=
1053
+ 'Content-Type: text/parameters\r\n' +
1054
+ 'Content-Length: ' + body.length + '\r\n\r\n';
1055
+ if (this.airplay2) {
1056
+ let rct2 = this.credentials.encrypt(Buffer.concat([Buffer.from(request + body, 'utf-8')]));
1057
+ this.socket.write(rct2);
1058
+ request = "";
1059
+ }
1060
+ else {
1061
+ }
1062
+ break;
1063
+ case SETVOLUME:
1064
+ var attenuation = this.volume === 0.0 ?
1065
+ -144.0 :
1066
+ (-30.0) * (100 - this.volume) / 100.0;
1067
+ body = 'volume: ' + attenuation + '\r\n';
1068
+ request += this.makeHeadWithURL('SET_PARAMETER', di);
1069
+ request +=
1070
+ 'Content-Type: text/parameters\r\n' +
1071
+ 'Content-Length: ' + body.length + '\r\n\r\n';
1072
+ request += body;
1073
+ //this.logLine?.(request);
1074
+ break;
1075
+ case SETPROGRESS:
1076
+ function hms(seconds) {
1077
+ const h = Math.floor(seconds / 3600);
1078
+ const m = Math.floor((seconds % 3600) / 60);
1079
+ const s = Math.floor(seconds % 60);
1080
+ return [h, m, s].map((a) => a.toString().padStart(2, '0')).join(':');
1081
+ }
1082
+ let position = this.starttime + (this.progress) * Math.floor((2 * config.sampling_rate) / (config.frames_per_packet / 125) / 0.71);
1083
+ let duration = this.starttime + (this.duration) * Math.floor((2 * config.sampling_rate) / (config.frames_per_packet / 125) / 0.71);
1084
+ if (this.debug) {
1085
+ this.logLine?.('start', this.starttime, 'position', position, 'duration', duration, 'position1', hms(this.progress), 'duration1', hms(this.duration));
1086
+ }
1087
+ body = "progress: " + this.starttime + "/" + position + "/" + duration + '\r\n';
1088
+ request += this.makeHeadWithURL('SET_PARAMETER', di);
1089
+ request +=
1090
+ 'Content-Type: text/parameters\r\n' +
1091
+ 'Content-Length: ' + body.length + '\r\n\r\n';
1092
+ request += body;
1093
+ //this.logLine?.(request);
1094
+ break;
1095
+ case SETDAAP:
1096
+ let daapenc = true;
1097
+ //daapenc = true
1098
+ var name = this.daapEncode('minm', this.trackInfo.name, daapenc);
1099
+ var artist = this.daapEncode('asar', this.trackInfo.artist, daapenc);
1100
+ var album = this.daapEncode('asal', this.trackInfo.album, daapenc);
1101
+ var daapInfo = this.daapEncodeList('mlit', daapenc, name, artist, album);
1102
+ var head = this.makeHeadWithURL('SET_PARAMETER', di);
1103
+ head += this.makeRtpInfo();
1104
+ head +=
1105
+ 'Content-Type: application/x-dmap-tagged\r\n' +
1106
+ 'Content-Length: ' + daapInfo.length + '\r\n\r\n';
1107
+ var buf = Buffer.alloc(head.length);
1108
+ buf.write(head, 0, head.length, 'utf-8');
1109
+ request = Buffer.concat([buf, daapInfo]);
1110
+ //this.logLine?.(request);
1111
+ break;
1112
+ case SETART:
1113
+ var head = this.makeHeadWithURL('SET_PARAMETER', di);
1114
+ head += this.makeRtpInfo();
1115
+ head +=
1116
+ 'Content-Type: ' + this.artworkContentType + '\r\n' +
1117
+ 'Content-Length: ' + this.artwork.length + '\r\n\r\n';
1118
+ var buf = Buffer.alloc(head.length);
1119
+ buf.write(head, 0, head.length, 'utf-8');
1120
+ request = Buffer.concat([buf, this.artwork]);
1121
+ //this.logLine?.(request);
1122
+ if (this.encryptedChannel && this.credentials) {
1123
+ this.socket.write(this.credentials.encrypt(Buffer.concat([request])));
1124
+ }
1125
+ else {
1126
+ this.socket.write(request);
1127
+ }
1128
+ request = '';
1129
+ break;
1130
+ case TEARDOWN:
1131
+ try {
1132
+ this.socket.end(this.makeHead('TEARDOWN', '', di) + '\r\n');
1133
+ }
1134
+ catch (_) { }
1135
+ if (this.debug)
1136
+ this.logLine?.('teardown');
1137
+ this.cleanup('stopped');
1138
+ // return here since the socket is closed
1139
+ return;
1140
+ default:
1141
+ return;
1142
+ }
1143
+ this.startTimeout();
1144
+ if (config.debug_dump) {
1145
+ // eslint-disable-next-line no-console
1146
+ console.debug('rtsp_req', { status: rtsp_methods[this.status] ?? this.status, len: request.length });
1147
+ }
1148
+ if (this.encryptedChannel && this.credentials) {
1149
+ this.socket.write(this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8')])));
1150
+ }
1151
+ else {
1152
+ this.socket.write(request);
1153
+ }
1154
+ };
1155
+ Client.prototype.daapEncodeList = function (field, enc, ...args) {
1156
+ var values = Array.prototype.slice.call(args);
1157
+ var value = Buffer.concat(values);
1158
+ var buf = Buffer.alloc(field.length + 4);
1159
+ buf.write(field, 0, field.length, enc ? 'utf-8' : "ascii");
1160
+ buf.writeUInt32BE(value.byteLength, field.length);
1161
+ return Buffer.concat([buf, value]);
1162
+ };
1163
+ Client.prototype.daapEncode = function (field, value, enc) {
1164
+ var valuebuf = Buffer.from(value, 'utf-8');
1165
+ var buf = Buffer.alloc(field.length + valuebuf.byteLength + 4);
1166
+ buf.write(field, 0, field.length, enc ? 'utf-8' : "ascii");
1167
+ buf.writeUInt32BE(valuebuf.byteLength, field.length);
1168
+ buf.write(value, field.length + 4, valuebuf.byteLength, enc ? 'utf-8' : "ascii");
1169
+ return buf;
1170
+ };
1171
+ Client.prototype.parsePorts = function (headers) {
1172
+ function parsePort(name, transport) {
1173
+ var re = new RegExp(name + '=(\\d+)');
1174
+ var res = re.exec(transport);
1175
+ return res ? parseInt(res[1]) : null;
1176
+ }
1177
+ var transport = String(headers['Transport'] ?? ''), rtspConfig = {
1178
+ audioLatency: parseInt(String(headers['Audio-Latency'] ?? '0'), 10),
1179
+ requireEncryption: this.requireEncryption
1180
+ }, names = ['server_port', 'control_port', 'timing_port'];
1181
+ for (var i = 0; i < names.length; i++) {
1182
+ var name = names[i];
1183
+ var port = parsePort(name, transport);
1184
+ if (port === null) {
1185
+ if (this.debug)
1186
+ this.logLine?.('parseport');
1187
+ // this.cleanup('parse_ports', transport);
1188
+ // return false;
1189
+ rtspConfig[name] = 4533;
1190
+ }
1191
+ else
1192
+ rtspConfig[name] = port;
1193
+ }
1194
+ this.emit('config', rtspConfig);
1195
+ return true;
1196
+ };
1197
+ function parseAuthenticate(auth, field) {
1198
+ var re = new RegExp(field + '="([^"]+)"'), res = re.exec(auth);
1199
+ return res ? res[1] : null;
1200
+ }
1201
+ Client.prototype.processData = function (blob, rawData) {
1202
+ this.logLine?.('Receiving request:', this.hostip, rtsp_methods[this.status + 1]);
1203
+ var response = parseResponse2(blob, this), headers = response.headers || {};
1204
+ if (config.debug_dump) {
1205
+ // eslint-disable-next-line no-console
1206
+ console.debug('rtsp_res', { code: response.code, status: rtsp_methods[this.status + 1], len: rawData.length });
1207
+ }
1208
+ if (this.debug) {
1209
+ try {
1210
+ if ((rawData.toString()).includes("bplist00")) {
1211
+ const buf = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1212
+ const bplist = bplistParser.parseBuffer(buf);
1213
+ this.logLine?.("incoming-res:", JSON.stringify(bplist));
1214
+ }
1215
+ else {
1216
+ this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
1217
+ }
1218
+ }
1219
+ catch {
1220
+ this.logLine?.("incoming-res:", { code: response.code, length: rawData.length });
1221
+ }
1222
+ }
1223
+ if (this.status != OPTIONS && this.status != OPTIONS2 && this.mode == 0) {
1224
+ if (response.code === 401) {
1225
+ if (!this.password) {
1226
+ if (this.debug)
1227
+ this.logLine?.('nopass');
1228
+ if (this.status == OPTIONS3) {
1229
+ this.emit('pair_failed');
1230
+ this.cleanup('no_password');
1231
+ }
1232
+ return;
1233
+ }
1234
+ if (response.code === 455) {
1235
+ return;
1236
+ }
1237
+ if (this.passwordTried) {
1238
+ if (this.debug)
1239
+ this.logLine?.('badpass');
1240
+ this.emit('pair_failed');
1241
+ this.cleanup('bad_password');
1242
+ return;
1243
+ }
1244
+ else
1245
+ this.passwordTried = true;
1246
+ var auth = headers['WWW-Authenticate'];
1247
+ var di = {
1248
+ realm: parseAuthenticate(auth, 'realm'),
1249
+ nonce: parseAuthenticate(auth, 'nonce'),
1250
+ username: 'iTunes',
1251
+ password: this.password
1252
+ };
1253
+ if (this.debug)
1254
+ this.logLine?.();
1255
+ this.sendNextRequest(di);
1256
+ return;
1257
+ }
1258
+ if (response.code === 453) {
1259
+ if (this.debug)
1260
+ this.logLine?.('busy');
1261
+ this.cleanup('busy');
1262
+ return;
1263
+ }
1264
+ if (response.code === 403 && this.status == ANNOUNCE && this.mode == 2) {
1265
+ this.status = AUTH_SETUP;
1266
+ this.sendNextRequest();
1267
+ return;
1268
+ }
1269
+ if (response.code !== 200) {
1270
+ if (this.status != SETVOLUME && (this.status != ANNOUNCE && this.mode == 2) && this.status != SETPEERS && this.status != FLUSH && this.status != RECORD && this.status != GETVOLUME && this.status != SETPROGRESS && this.status != SETDAAP && this.status != SETART) {
1271
+ if ([PAIR_VERIFY_1,
1272
+ PAIR_VERIFY_2,
1273
+ AUTH_SETUP,
1274
+ PAIR_PIN_START,
1275
+ PAIR_PIN_SETUP_1,
1276
+ PAIR_PIN_SETUP_2,
1277
+ PAIR_PIN_SETUP_3].includes(this.status)) {
1278
+ this.emit('pair_failed');
1279
+ }
1280
+ this.cleanup(response.status);
1281
+ return;
1282
+ }
1283
+ }
1284
+ }
1285
+ else if (this.mode == 1) {
1286
+ if (response.code === 401) {
1287
+ if (!this.password) {
1288
+ if (this.debug)
1289
+ this.logLine?.('nopass');
1290
+ this.emit('pair_failed');
1291
+ this.cleanup('no_password');
1292
+ return;
1293
+ }
1294
+ if (this.passwordTried) {
1295
+ if (this.debug)
1296
+ this.logLine?.('badpass');
1297
+ this.emit('pair_failed');
1298
+ this.cleanup('bad_password');
1299
+ return;
1300
+ }
1301
+ else
1302
+ this.passwordTried = true;
1303
+ var auth = headers['WWW-Authenticate'];
1304
+ var di = {
1305
+ realm: parseAuthenticate(auth, 'realm'),
1306
+ nonce: parseAuthenticate(auth, 'nonce'),
1307
+ username: 'iTunes',
1308
+ password: this.password
1309
+ };
1310
+ if (this.debug)
1311
+ this.logLine?.(di);
1312
+ this.sendNextRequest(di);
1313
+ return;
1314
+ }
1315
+ if (response.code === 453) {
1316
+ if (this.debug)
1317
+ this.logLine?.('busy');
1318
+ this.cleanup('busy');
1319
+ return;
1320
+ }
1321
+ if (response.code === 403 && this.status == ANNOUNCE && this.mode == 2) {
1322
+ this.status = AUTH_SETUP;
1323
+ this.sendNextRequest();
1324
+ return;
1325
+ }
1326
+ if (response.code !== 200) {
1327
+ if (this.debug)
1328
+ this.logLine?.(response.status);
1329
+ if (this.status != SETVOLUME && (this.status != ANNOUNCE && this.mode == 2) && this.status != SETPEERS && this.status != FLUSH && this.status != RECORD && this.status != GETVOLUME && this.status != SETPROGRESS && this.status != SETDAAP && this.status != SETART) {
1330
+ if ([PAIR_VERIFY_1,
1331
+ PAIR_VERIFY_2,
1332
+ AUTH_SETUP,
1333
+ PAIR_PIN_START,
1334
+ PAIR_PIN_SETUP_1,
1335
+ PAIR_PIN_SETUP_2,
1336
+ PAIR_PIN_SETUP_3].includes(this.status)) {
1337
+ this.emit('pair_failed');
1338
+ }
1339
+ this.cleanup(response.status);
1340
+ return;
1341
+ }
1342
+ }
1343
+ }
1344
+ // password was accepted (or not needed)
1345
+ this.passwordTried = false;
1346
+ switch (this.status) {
1347
+ case PAIR_PIN_START:
1348
+ if (!this.transient) {
1349
+ this.emit('need_password');
1350
+ }
1351
+ this.status = this.airplay2 ? PAIR_SETUP_1 : PAIR_PIN_SETUP_1;
1352
+ if (!this.transient) {
1353
+ return;
1354
+ }
1355
+ break;
1356
+ case PAIR_PIN_SETUP_1:
1357
+ this.srp = new LegacySRP(2048);
1358
+ this.P = this.password;
1359
+ let bufa = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1360
+ const { pk, salt } = bplistParser.parseBuffer(bufa)[0];
1361
+ this.s = salt.toString('hex');
1362
+ this.B = pk.toString('hex');
1363
+ // SRP: Generate random auth_secret, 'a'; if pairing is successful, it'll be utilized in
1364
+ // subsequent session authentication(s).
1365
+ this.a = nodeCrypto.randomBytes(32).toString('hex');
1366
+ // SRP: Compute A and M1.
1367
+ this.A = this.srp.A(this.a);
1368
+ this.M1 = this.srp.M1(this.I, this.P, this.s, this.a, this.B);
1369
+ this.status = PAIR_PIN_SETUP_2;
1370
+ break;
1371
+ case PAIR_PIN_SETUP_2:
1372
+ const { epk, authTag } = ATVAuthenticator.confirm(this.a, this.srp.K(this.I, this.P, this.s, this.a, this.B));
1373
+ this.epk = epk;
1374
+ this.authTag = authTag;
1375
+ this.status = PAIR_PIN_SETUP_3;
1376
+ break;
1377
+ case PAIR_PIN_SETUP_3:
1378
+ this.status = PAIR_VERIFY_1;
1379
+ this.authSecret = this.a;
1380
+ break;
1381
+ case PAIR_VERIFY_1:
1382
+ let buf1 = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1383
+ this.logLine?.('verify2', Buffer.byteLength(buf1));
1384
+ if (Buffer.byteLength(buf1) != 0) {
1385
+ const atv_pub = buf1.slice(0, 32).toString('hex');
1386
+ const atv_data = buf1.slice(32).toString('hex');
1387
+ const shared = ATVAuthenticator.shared(this.pair_verify_1_verifier.v_pri, atv_pub);
1388
+ const signed = ATVAuthenticator.signed(this.authSecret, this.pair_verify_1_verifier.v_pub, atv_pub);
1389
+ this.pair_verify_1_signature = Buffer.from(Buffer.from([0x00, 0x00, 0x00, 0x00]).toString('hex') +
1390
+ ATVAuthenticator.signature(shared, atv_data, signed), 'hex');
1391
+ if (this.debug)
1392
+ this.logLine?.('verify2', Buffer.byteLength(this.pair_verify_1_signature));
1393
+ this.status = PAIR_VERIFY_2;
1394
+ }
1395
+ else {
1396
+ this.emit('pair_failed');
1397
+ this.cleanup('pair_failed');
1398
+ return;
1399
+ }
1400
+ break;
1401
+ case PAIR_VERIFY_2:
1402
+ this.status = OPTIONS;
1403
+ break;
1404
+ case PAIR_SETUP_1:
1405
+ let buf2 = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1406
+ let databuf1 = tlv.decode(buf2);
1407
+ if (this.debug)
1408
+ this.logLine?.(databuf1);
1409
+ if (databuf1[tlv.Tag.BackOff]) {
1410
+ let backOff = databuf1[tlv.Tag.BackOff];
1411
+ this.logLine?.(backOff);
1412
+ let seconds = backOff.length >= 2 ? Buffer.from(backOff).readInt16LE(0) : 0;
1413
+ this.logLine?.("You've attempt to pair too recently. Try again in " + (seconds) + " seconds.");
1414
+ }
1415
+ if (databuf1[tlv.Tag.ErrorCode]) {
1416
+ let buffer = databuf1[tlv.Tag.ErrorCode];
1417
+ this.logLine?.("Device responded with error code " + Buffer.from(buffer).readIntLE(0, buffer.byteLength) + ". Try rebooting your Apple TV.");
1418
+ }
1419
+ if (databuf1[tlv.Tag.PublicKey]) {
1420
+ this._atv_pub_key = databuf1[tlv.Tag.PublicKey];
1421
+ this._atv_salt = databuf1[tlv.Tag.Salt];
1422
+ this._hap_genkey = nodeCrypto.randomBytes(32);
1423
+ if (this.password == null) {
1424
+ this.password = 3939; // transient
1425
+ }
1426
+ this.srp = new SrpClient(SRP.params.hap, Buffer.from(this._atv_salt), Buffer.from("Pair-Setup"), Buffer.from(this.password.toString()), Buffer.from(this._hap_genkey), true);
1427
+ this.srp.setB(this._atv_pub_key);
1428
+ this.A = this.srp.computeA();
1429
+ this.M1 = this.srp.computeM1();
1430
+ this.status = PAIR_SETUP_2;
1431
+ }
1432
+ else {
1433
+ this.emit('pair_failed');
1434
+ this.cleanup('pair_failed');
1435
+ return;
1436
+ }
1437
+ break;
1438
+ case PAIR_SETUP_2:
1439
+ let buf3 = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1440
+ let databuf2 = tlv.decode(buf3);
1441
+ this.deviceProof = databuf2[tlv.Tag.Proof];
1442
+ if (!this.deviceProof) {
1443
+ this.emit('pair_failed');
1444
+ this.cleanup('pair_failed');
1445
+ return;
1446
+ }
1447
+ // this.logLine?.("DEBUG: Device Proof=" + this.deviceProof.toString('hex'));
1448
+ this.srp.checkM2(this.deviceProof);
1449
+ if (this.transient == true) {
1450
+ this.credentials = new Credentials("sdsds", "", "", "", this.seed);
1451
+ this.credentials.writeKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Write-Encryption-Key"), 32);
1452
+ this.credentials.readKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Read-Encryption-Key"), 32);
1453
+ this.encryptedChannel = true;
1454
+ this.status = SETUP_AP2_1;
1455
+ }
1456
+ else {
1457
+ this.status = PAIR_SETUP_3;
1458
+ }
1459
+ break;
1460
+ case PAIR_SETUP_3:
1461
+ let buf4 = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1462
+ let encryptedData = tlv.decode(buf4)[tlv.Tag.EncryptedData];
1463
+ let cipherText = encryptedData.slice(0, -16);
1464
+ let hmac = encryptedData.slice(-16);
1465
+ let decrpytedData = enc.verifyAndDecrypt(cipherText, hmac, null, Buffer.from('PS-Msg06'), this.encryptionKey);
1466
+ let tlvData = tlv.decode(decrpytedData);
1467
+ this.credentials = new Credentials("sdsds", tlvData[tlv.Tag.Username], this.pairingId, tlvData[tlv.Tag.PublicKey], this.seed);
1468
+ this.status = PAIR_VERIFY_HAP_1;
1469
+ break;
1470
+ case PAIR_VERIFY_HAP_1:
1471
+ let buf5 = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1472
+ let decodedData = tlv.decode(buf5);
1473
+ let sessionPublicKey = decodedData[tlv.Tag.PublicKey];
1474
+ let encryptedData1 = decodedData[tlv.Tag.EncryptedData];
1475
+ if (sessionPublicKey.length != 32) {
1476
+ throw new Error(`sessionPublicKey must be 32 bytes (but was ${sessionPublicKey.length})`);
1477
+ }
1478
+ let cipherText1 = encryptedData1.slice(0, -16);
1479
+ let hmac1 = encryptedData1.slice(-16);
1480
+ // let sharedSecret = curve25519.deriveSharedSecret(this.verifyPrivate, sessionPublicKey);
1481
+ let sharedSecret = curve25519_js.sharedKey(this.verifyPrivate, sessionPublicKey);
1482
+ let encryptionKey = enc.HKDF("sha512", Buffer.from("Pair-Verify-Encrypt-Salt"), sharedSecret, Buffer.from("Pair-Verify-Encrypt-Info"), 32);
1483
+ let decryptedData = enc.verifyAndDecrypt(cipherText1, hmac1, null, Buffer.from('PV-Msg02'), encryptionKey);
1484
+ this.verifier_hap_1 = {
1485
+ sessionPublicKey: sessionPublicKey,
1486
+ sharedSecret: sharedSecret,
1487
+ encryptionKey: encryptionKey,
1488
+ pairingData: decryptedData
1489
+ };
1490
+ this.status = PAIR_VERIFY_HAP_2;
1491
+ this.sharedSecret = sharedSecret;
1492
+ break;
1493
+ case PAIR_VERIFY_HAP_2:
1494
+ let buf6 = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1495
+ this.credentials.readKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.verifier_hap_1.sharedSecret, Buffer.from("Control-Read-Encryption-Key"), 32);
1496
+ this.credentials.writeKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.verifier_hap_1.sharedSecret, Buffer.from("Control-Write-Encryption-Key"), 32);
1497
+ if (this.debug) {
1498
+ this.logLine?.('write', this.credentials.writeKey);
1499
+ }
1500
+ if (this.debug) {
1501
+ this.logLine?.('buf6', buf6);
1502
+ }
1503
+ this.encryptedChannel = true;
1504
+ this.status = SETUP_AP2_1;
1505
+ break;
1506
+ case SETUP_AP2_1:
1507
+ this.logLine?.('timing port parsing');
1508
+ let buf7 = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1509
+ let sa1_bplist = bplistParser.parseBuffer(buf7);
1510
+ this.eventPort = sa1_bplist[0]['eventPort'];
1511
+ if (sa1_bplist[0]['timingPort'])
1512
+ this.timingDestPort = sa1_bplist[0]['timingPort'];
1513
+ this.logLine?.('timing port ok', sa1_bplist[0]['timingPort']);
1514
+ // let rtspConfig1 = {
1515
+ // audioLatency: 50,
1516
+ // requireEncryption: false,
1517
+ // server_port : 22223,
1518
+ // control_port : this.controlPort,
1519
+ // timing_port : this.timingPort,
1520
+ // event_port: this.eventPort,
1521
+ // credentials : this.credentials
1522
+ // }
1523
+ // this.emit('config', rtspConfig1);
1524
+ // this.eventsocket.bind(3003, this.socket.address().address);
1525
+ this.status = SETPEERS;
1526
+ break;
1527
+ case SETUP_AP2_2:
1528
+ let buf8 = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1529
+ let sa2_bplist = bplistParser.parseBuffer(buf8);
1530
+ let rtspConfig = {
1531
+ audioLatency: 50,
1532
+ requireEncryption: false,
1533
+ server_port: sa2_bplist[0]["streams"][0]["dataPort"],
1534
+ control_port: sa2_bplist[0]["streams"][0]["controlPort"],
1535
+ timing_port: this.timingDestPort ? this.timingDestPort : this.timingPort,
1536
+ credentials: this.credentials
1537
+ };
1538
+ this.timingsocket.close();
1539
+ this.controlsocket.close();
1540
+ this.emit('config', rtspConfig);
1541
+ this.logLine?.("goto info");
1542
+ // this.session = 1;
1543
+ this.status = RECORD;
1544
+ // this.emit('ready');
1545
+ break;
1546
+ case SETPEERS:
1547
+ this.status = SETUP_AP2_2;
1548
+ break;
1549
+ case FLUSH:
1550
+ this.status = PLAYING;
1551
+ this.metadataReady = true;
1552
+ this.emit('pair_success');
1553
+ this.session = "1";
1554
+ this.logLine?.("flush");
1555
+ this.emit('ready');
1556
+ // this.logLine?.(sa2_bplist[0]["streams"][0]["controlPort"], sa2_bplist[0]["streams"][0]["dataPort"] )
1557
+ break;
1558
+ case INFO:
1559
+ let buf9 = Buffer.from(rawData).slice(rawData.length - parseInt(headers['Content-Length']), rawData.length);
1560
+ this.status = (this.credentials) ? RECORD : PAIR_SETUP_1;
1561
+ break;
1562
+ case GETVOLUME:
1563
+ this.status = RECORD;
1564
+ break;
1565
+ case AUTH_SETUP:
1566
+ this.status = OPTIONS;
1567
+ break;
1568
+ case OPTIONS:
1569
+ /*
1570
+ * Devices like Apple TV and Zeppelin Air do not support encryption.
1571
+ * Only way of checking that: they do not reply to Apple-Challenge
1572
+ */
1573
+ if (headers['Apple-Response'])
1574
+ this.requireEncryption = true;
1575
+ // this.logLine?.("yeah22332",headers['WWW-Authenticate'],response.code)
1576
+ if (headers['WWW-Authenticate'] != null && response.code === 401) {
1577
+ let auth = headers['WWW-Authenticate'];
1578
+ let realm = parseAuthenticate(auth, 'realm');
1579
+ let nonce = parseAuthenticate(auth, 'nonce');
1580
+ let uri = "*";
1581
+ let user = "iTunes";
1582
+ let methodx = "OPTIONS";
1583
+ let pwd = this.password;
1584
+ let ha1 = md5norm(`${user}:${realm}:${pwd}`);
1585
+ let ha2 = md5norm(`${methodx}:${uri}`);
1586
+ let di_response = md5(`${ha1}:${nonce}:${ha2}`);
1587
+ this.code_digest = `Authorization: Digest username="${user}", realm="${realm}", nonce="${nonce}", uri="${uri}", response="${di_response}" \r\n\r\n`;
1588
+ this.status = OPTIONS2;
1589
+ }
1590
+ else {
1591
+ this.status = this.session ? PLAYING : (this.airplay2 ? PAIR_PIN_START : ANNOUNCE);
1592
+ // if (this.status == ANNOUNCE && response.code === 200){this.emit('pair_success')};
1593
+ }
1594
+ break;
1595
+ case OPTIONS2:
1596
+ /*
1597
+ * Devices like Apple TV and Zeppelin Air do not support encryption.
1598
+ * Only way of checking that: they do not reply to Apple-Challenge
1599
+ */
1600
+ // if(headers['Apple-Response'])
1601
+ // this.requireEncryption = true;
1602
+ if (headers['WWW-Authenticate'] != null && response.code === 401) {
1603
+ let auth = headers['WWW-Authenticate'];
1604
+ let realm = parseAuthenticate(auth, 'realm');
1605
+ let nonce = parseAuthenticate(auth, 'nonce');
1606
+ let uri = "*";
1607
+ let user = "iTunes";
1608
+ let methodx = "OPTIONS";
1609
+ let pwd = this.password;
1610
+ let ha1 = md5(`${user}:${realm}:${pwd}`);
1611
+ let ha2 = md5(`${methodx}:${uri}`);
1612
+ let di_response = md5(`${ha1}:${nonce}:${ha2}`);
1613
+ this.code_digest = `Authorization: Digest username="${user}", realm="${realm}", nonce="${nonce}", uri="${uri}", response="${di_response}" \r\n\r\n`;
1614
+ this.status = OPTIONS3;
1615
+ }
1616
+ else {
1617
+ this.status = this.session ? PLAYING : (this.airplay2 ? SETUP_AP2_1 : ANNOUNCE);
1618
+ // if (this.status == ANNOUNCE && response.code === 200){this.emit('pair_success')}
1619
+ }
1620
+ ;
1621
+ break;
1622
+ case OPTIONS3:
1623
+ this.status = this.session ? PLAYING : (this.airplay2 ? SETUP_AP2_1 : ANNOUNCE);
1624
+ // if (this.status == ANNOUNCE && response.code === 200){this.emit('pair_success')}
1625
+ break;
1626
+ case ANNOUNCE:
1627
+ this.status = (this.airplay2 == true && this.mode == 2) ? PAIR_PIN_START : SETUP;
1628
+ break;
1629
+ case SETUP:
1630
+ this.status = RECORD;
1631
+ this.session = headers['Session'];
1632
+ this.parsePorts(headers);
1633
+ break;
1634
+ case RECORD:
1635
+ this.metadataReady = true;
1636
+ this.emit('pair_success');
1637
+ if (!this.airplay2) {
1638
+ this.session = this.session ?? "1";
1639
+ this.emit('ready');
1640
+ }
1641
+ ;
1642
+ this.status = SETVOLUME;
1643
+ break;
1644
+ case SETVOLUME:
1645
+ if (!this.sentFakeProgess) {
1646
+ this.progress = 10;
1647
+ this.duration = 2000000;
1648
+ this.sentFakeProgess = true;
1649
+ this.status = SETPROGRESS;
1650
+ }
1651
+ else {
1652
+ this.status = PLAYING;
1653
+ }
1654
+ ;
1655
+ break;
1656
+ case SETPROGRESS:
1657
+ // After reporting progress, stay in PLAYING; avoid forcing FLUSH on every update.
1658
+ this.status = PLAYING;
1659
+ break;
1660
+ case SETDAAP:
1661
+ this.status = PLAYING;
1662
+ break;
1663
+ case SETART:
1664
+ this.status = PLAYING;
1665
+ break;
1666
+ }
1667
+ try {
1668
+ if (this.callback != null) {
1669
+ this.callback();
1670
+ }
1671
+ }
1672
+ catch (e) { }
1673
+ this.sendNextRequest();
1674
+ };
1675
+ Client.prototype.parseObject = function (plist) {
1676
+ if (this.debug)
1677
+ this.logLine?.('plist', plist);
1678
+ };