-
-
Notifications
You must be signed in to change notification settings - Fork 180
Expand file tree
/
Copy pathusbmux.ts
More file actions
586 lines (524 loc) · 17.2 KB
/
Copy pathusbmux.ts
File metadata and controls
586 lines (524 loc) · 17.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import * as net from 'net';
import { EventEmitter } from 'events';
import * as Q from 'q';
import * as plist from 'plist';
/**
* Keep track of connected devices
*
* Maps device UDID to device properties, ie:
* '22226dd59aaac687f555f8521f8ffddac32d394b': {
* ConnectionType: 'USB',
* DeviceID: 19,
* LocationID: 0,
* ProductID: 4776,
* SerialNumber: '22226dd59aaac687f555f8521f8ffddac32d394b'
* }
*
* Devices are added and removed to this obj only by createListener()
*
* @public
*/
const devices: { [udid: string]: any } = {};
/**
* usbmuxd address
*
* OSX usbmuxd listens on a unix socket at /var/run/usbmuxd
* Windows usbmuxd listens on port 27015
*
* libimobiledevice[1] looks like it operates at /var/run/usbmuxd too, but if
* your usbmuxd is listening somewhere else you'll need to set this manually.
*
* [1] github.com/libimobiledevice/usbmuxd
*
* @public
*/
let address: { port?: number; path?: string } =
process.platform === 'win32' ? { port: 27015 } : { path: '/var/run/usbmuxd' };
/**
* Exposes methods for dealing with usbmuxd protocol messages (send/receive)
*
* The usbmuxd message protocol has 2 versions. V1 doesn't look like its used
* anymore. V2 is a header + plist format like this:
*
* Header:
* UInt32LE Length - is the length of the header + plist (16 + plist.length)
* UInt32LE Version - is 0 for binary version, 1 for plist version
* UInt32LE Request - is always 8, for plist? from rcg4u/iphonessh
* UInt32LE Tag - is always 1, ? from rcg4u/iphonessh
*
* Plist:
* <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
* "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
* <plist version="1.0">
* <dict>
* <key>MessageType</key>
* <string>Listen</string>
* <key>ClientVersionString</key>
* <string>node-usbmux</string>
* <key>ProgName</key>
* <string>node-usbmux</string>
* </dict>
* </plist>
*
* References:
* - https://github.com/rcg4u/iphonessh
* - https://www.theiphonewiki.com/wiki/Usbmux (binary protocol)
*/
const protocol = (() => {
/**
* Pack a request object into a buffer for usbmuxd
*
* @param {object} payload_obj
* @return {Buffer}
*/
function pack(payload_obj: any): Buffer {
const payload_plist = plist.build(payload_obj);
const payload_buf = Buffer.from(payload_plist);
const header = {
len: payload_buf.length + 16,
version: 1,
request: 8,
tag: 1,
};
const header_buf = Buffer.alloc(16);
header_buf.fill(0);
header_buf.writeUInt32LE(header.len, 0);
header_buf.writeUInt32LE(header.version, 4);
header_buf.writeUInt32LE(header.request, 8);
header_buf.writeUInt32LE(header.tag, 12);
return Buffer.concat([header_buf, payload_buf]);
}
/**
* Swap endianness of a 16bit value
*/
function byteSwap16(val: number): number {
return ((val & 0xff) << 8) | ((val >> 8) & 0xff);
}
/**
* Listen request
* @type {Buffer}
*/
const listen = pack({
MessageType: 'Listen',
ClientVersionString: 'node-usbmux',
ProgName: 'node-usbmux',
});
/**
* Connect request
*
* Note: PortNumber must be network-endian, so it gets byte swapped here
*
* @param {integer} deviceID
* @param {integer} port
* @return {Buffer}
*/
function connect(deviceID: number, port: number): Buffer {
return pack({
MessageType: 'Connect',
ClientVersionString: 'node-usbmux',
ProgName: 'node-usbmux',
DeviceID: deviceID,
PortNumber: byteSwap16(port),
});
}
/**
* Creates a function that will parse messages from data events
*
* net.Socket data events sometimes break up the incoming message across
* multiple events, making it necessary to combine them. This parser function
* assembles messages using the length given in the message header and calls
* the onComplete callback as new messages are assembled. Sometime multiple
* messages will be within a single data buffer too.
*
* @param {makeParserCb} onComplete - Called as new msgs are assembled
* @return {function} - Parser function
*
* @callback makeParserCb
* @param {object} - msg object converted from plist
*/
function makeParser(onComplete: (msg: any) => void): (data: Buffer) => void {
// Store status (remaining message length & msg text) of partial messages
// across multiple calls to the parse function
let len: number, msg: string;
/**
* @param {Buffer} data - From a socket's data event
*/
return function parse(data: Buffer): void {
// Check if this data represents a new incoming message or is part of an
// existing partially completed message
if (!len) {
// The length of the message's body is the total length (the first
// UInt32LE in the header) minus the length of the header itself (16)
len = data.readUInt32LE(0) - 16;
msg = '';
// If there is data beyond the header then continue adding data to msg
data = data.slice(16);
if (!data.length) return;
}
// Add in data until our remaining length is used up
const body = data.slice(0, len);
msg += body.toString();
len -= body.length;
// If msg is finished, convert plist to obj and run callback
if (len === 0) onComplete(plist.parse(msg));
// If there is any data left over that means there is another message
// so we need to run this parse fct again using the rest of the data
data = data.slice(body.length);
if (data.length) parse(data);
};
}
// Exposed methods
return {
listen: listen,
connect: connect,
makeParser: makeParser,
};
})();
/**
* Custom usbmuxd error
*
* There's no documentation for usbmuxd responses, but I think I've figured
* out these result numbers:
* 0 - Success
* 2 - Device requested isn't connected
* 3 - Port requested isn't available \ open
* 5 - Malformed request
*
* @param {string} message - Error message
* @param {integer} [number] - Error number given from usbmuxd response
*/
class UsbmuxdError extends Error {
number?: number;
constructor(message: string, number?: number) {
super(message);
this.name = 'UsbmuxdError';
if (number) {
this.number = number;
this.message += ', Err #' + number;
}
if (number === 2) this.message += ': Device is not connected';
if (number === 3) this.message += ': Port is not available or open';
if (number === 5) this.message += ': Malformed request';
}
}
/**
* Connects to usbmuxd and listens for ios devices
*
* This connection stays open, listening as devices are plugged/unplugged and
* cant be upgraded into a tcp tunnel. You have to start a second connection
* with connect() to actually make tunnel.
*
* @return {net.Socket} - Socket with 2 bolted on events, attached & detached:
*
* Fires when devices are plugged in or first found by the listener
* @event net.Socket#attached
* @type {string} - UDID
*
* Fires when devices are unplugged
* @event net.Socket#detached
* @type {string} - UDID
*
* @public
*/
function createListener(): net.Socket {
const conn = net.connect(address);
const req = protocol.listen;
/**
* Handle complete messages from usbmuxd
* @function
*/
const parse = protocol.makeParser(function onMsgComplete(msg) {
// first response always acknowledges / denies the request:
if (msg.MessageType === 'Result' && msg.Number !== 0) {
conn.emit('error', new UsbmuxdError('Listen failed', msg.Number));
conn.end();
}
// subsequent responses report on connected device status:
console.log(msg);
if (msg.MessageType === 'Attached') {
devices[msg.Properties.SerialNumber] = msg.Properties;
conn.emit('attached', msg.Properties.SerialNumber);
}
if (msg.MessageType === 'Detached') {
// given msg.DeviceID, find matching device and remove it
Object.keys(devices).forEach(function (key) {
if (devices[key].DeviceID === msg.DeviceID) {
conn.emit('detached', devices[key].SerialNumber);
delete devices[key];
}
});
}
});
conn.on('data', parse);
process.nextTick(function () {
conn.write(req);
});
return conn;
}
/**
* Connects to a device through usbmuxd for a tunneled tcp connection
*
* @param {string} deviceID - Target device's usbmuxd ID
* @param {integer} devicePort - Port on ios device to connect to
* @return {Q.promise}
* - resolves {net.Socket} - Tunneled tcp connection to device
* - rejects {Error}
*/
function connectToDevice(deviceID: string, devicePort: number): Q.Promise<net.Socket> {
return Q.Promise(function (resolve, reject) {
const conn = net.connect(address);
const req = protocol.connect(parseInt(deviceID), devicePort);
/**
* Handle complete messages from usbmuxd
* @function
*/
const parse = protocol.makeParser(function onMsgComplete(msg) {
debug.connect('Response: \n%o', msg);
if (msg.MessageType === 'Result' && msg.Number === 0) {
conn.removeListener('data', parse);
resolve(conn);
return;
}
// anything other response means it failed
reject(new UsbmuxdError('Tunnel failed', msg.Number));
conn.end();
});
debug.connect('Request: \n%s', req.slice(16).toString());
conn.on('data', parse);
process.nextTick(function () {
conn.write(req);
});
});
}
/**
* Creates a new tcp relay to a port on connected usb device
*
* @constructor
* @param {integer} devicePort - Port to connect to on device
* @param {integer} relayPort - Local port that will listen as relay
* @param {object} [opts] - Options
* @param {integer} [opts.timeout=1000] - Search time (ms) before warning
* @param {string} [opts.udid] - UDID of specific device to connect to
*
* @public
*/
class Relay extends EventEmitter {
private _devicePort: number;
private _relayPort: number;
private _udid?: string;
private _listener: net.Socket | null = null;
private _server: net.Server | null = null;
constructor(devicePort: number, relayPort: number, opts?: { timeout?: number; udid?: string }) {
super();
this._devicePort = devicePort;
this._relayPort = relayPort;
opts = opts || {};
this._udid = opts.udid;
this._startListener(opts.timeout);
this._startServer();
}
/**
* Stops the relay
*/
stop(): void {
if (this._listener) this._listener.end();
if (this._server) this._server.close();
}
/**
* Debugging wrapper for emits
*
* @param {string} event
* @param {*} [data]
*/
private _emit(event: string, data?: any): void {
debug.relay('Emit: %s', event + (data ? ', Data: ' + data : ''));
this.emit(event, data);
}
/**
* Starts a usbmuxd listener
*
* Relay will start searching for connected devices and issue a warning if a
* device is not found within the timeout. If/when a device is found, it will
* emit a ready event.
*
* Listener events (attach, detach, error) are passed through as relay events.
*
* @param {integer} [timeout=1000] - Search time (ms) before warning
*/
private _startListener(timeout?: number): void {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
const timer = setTimeout(function () {
// no UDID was given and no devices found yet
if (!_this._udid && !Object.keys(devices).length) {
_this._emit('warning', new Error('No devices connected'));
}
// UDID was given, but that device is not connected
if (_this._udid && !devices[_this._udid]) {
_this._emit('warning', new Error('Requested device not connected'));
}
}, timeout || 1000);
function readyCheck(udid: string): void {
if (_this._udid && _this._udid !== udid) return;
_this._emit('ready', udid);
if (_this._listener) _this._listener.removeListener('attached', readyCheck);
clearTimeout(timer);
}
this._listener = createListener()
.on('attached', readyCheck)
.on('attached', _this._emit.bind(this, 'attached'))
.on('detached', _this._emit.bind(this, 'detached'))
.on('error', _this._emit.bind(this, 'error'));
}
/**
* Start local TCP server that will pipe to the usbmuxd tunnel
*
* Server events (close and error) are passed through as relay events.
*/
private _startServer(): void {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
this._server = net
.createServer(this._handler.bind(this))
.on('close', _this._emit.bind(this, 'close'))
.on('error', function (err) {
if (_this._listener) _this._listener.end();
_this._emit('error', err);
})
.listen(this._relayPort);
}
/**
* Handle & pipe connections from local server
*
* Fires error events and connection begin / disconnect events
*
* @param {net.Socket} conn - The local connection socket
*/
private _handler(conn: net.Socket): void {
// emit error if there are no devices connected
if (!Object.keys(devices).length) {
this._emit('error', new Error('No devices connected'));
conn.end();
return;
}
// emit error if a udid was specified but that device isn't connected
if (this._udid && !devices[this._udid]) {
this._emit('error', new Error('Requested device not connected'));
conn.end();
return;
}
// Use specified device or choose one from available devices
const udid = this._udid || Object.keys(devices)[0];
const deviceID = devices[udid].DeviceID;
connectToDevice(deviceID.toString(), this._devicePort)
.then(function (tunnel) {
// pipe connection & tunnel together
conn.pipe(tunnel).pipe(conn);
_this._emit('connect');
conn.on('end', function () {
_this._emit('disconnect');
tunnel.end();
conn.end();
});
conn.on('error', function () {
tunnel.end();
conn.end();
});
})
.catch(function (err) {
_this._emit('error', err);
conn.end();
});
}
}
/**
* Find a device (specified or not) within a timeout
*
* Usbmuxd has IDs it assigned to devices as they are plugged in. The IDs
* change as devices are unplugged and plugged back in, so even if we have a
* UDID we need to get the current ID from usbmuxd before we can connect.
*
* @param {object} [opts] - Options
* @param {integer} [opts.timeout=1000] - Search time (in ms) before failing
* @param {string} [opts.udid] - UDID of a specific device to find
* @return {Q.promise}
* - resolves {integer} - DeviceID from usbmuxd needed for a connect request
* - rejects {Error}
*/
function findDevice(opts?: { timeout?: number; udid?: string }): Q.Promise<number> {
return Q.Promise(function (resolve, reject) {
const listener = createListener();
opts = opts || {};
const timer = setTimeout(function () {
listener.end();
opts && opts.udid
? reject(new Error('Requested device not connected'))
: reject(new Error('No devices connected'));
}, opts.timeout || 1000);
listener.on('attached', function (udid: string) {
if (opts && opts.udid && opts.udid !== udid) return;
listener.end();
clearTimeout(timer);
resolve(devices[udid].DeviceID);
});
});
}
/**
* Get a tunneled connection to a device (specified or not) within a timeout
*
* @param {integer} devicePort - Port to connect to on device
* @param {object} [opts] - Options
* @param {integer} [opts.timeout=1000] - Search time (in ms) before failing
* @param {string} [opts.udid] - UDID of specific device to connect to
* @return {Q.promise}
* - resolves {net.Socket} - Tunneled connection to device
* - rejects {Error}
*
* @public
*/
function getTunnel(
devicePort: number,
opts?: { timeout?: number; udid?: string },
): Q.Promise<net.Socket> {
opts = opts || {};
let udid, deviceID;
// If UDID was specified and that device's DeviceID is known, connect to it
if (opts.udid && devices[opts.udid]) {
deviceID = devices[opts.udid].DeviceID;
return connectToDevice(deviceID.toString(), devicePort);
}
// If no UDID given, connect to any known device
// (random because no key order, but there's probably only 1 option anyways)
if (!opts.udid && Object.keys(devices).length) {
udid = Object.keys(devices)[0];
deviceID = devices[udid].DeviceID;
return connectToDevice(deviceID.toString(), devicePort);
}
// - Try to find and connect to requested the device (given opts.UDID),
// - or find and connect to any device (no opts.UDID given)
return findDevice(opts).then(function (deviceID) {
return connectToDevice(deviceID.toString(), devicePort);
});
}
//
// EXPORTS
//
export = {
devices: devices,
Relay: Relay,
getTunnel: getTunnel,
createListener: createListener,
};
// getter and setter for usbmuxd address
Object.defineProperty(module.exports, 'address', {
get: function () {
return address;
},
set: function (newAddress: { port?: number; path?: string }) {
if (newAddress.port !== undefined || newAddress.path !== undefined) {
address = newAddress;
}
},
});