@ciphore/radiocli 0.2.4 → 0.2.6

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