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