@irdk/usbmux 0.2.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Sterling DeMille <sterlingdemille@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,298 @@
1
+ # node-usbmux
2
+
3
+ Node-usbmux is an iOS usbmuxd client library inspired by [tcprelay.py](https://github.com/rcg4u/iphonessh)
4
+
5
+
6
+ ## What is usbmuxd?
7
+
8
+ All USB communication with iOS devices (including communication from iTunes) is handled by the usbmux daemon. When a device is plugged in, usbmuxd connects to it and acts as a middleman for communicating with the device, multiplexing TCP like connections to sockets on the device. (USB multiplexer = usbmux)
9
+
10
+
11
+ ## What does node-usbmux do?
12
+
13
+ Node-usbmux provides tcp connections to sockets on iOS devices via USB.
14
+
15
+ Installed globally, node-usbmux's CLI lets you create TCP relays that tunnel traffic from localhost through USB to the device. (useful for accessing ssh or veency over usb)
16
+
17
+ The obvious advantages of a USB connection over a wifi connection are speed, not having to be on the same network, device doesn't need to be unlocked to maintain connection, etc.
18
+
19
+
20
+ ## Install
21
+
22
+ ```
23
+ npm install [-g] usbmux
24
+ ```
25
+
26
+ Prerequisites: iTunes or [libimobiledevice](http://www.libimobiledevice.org/)
27
+
28
+
29
+ ## CLI Usage
30
+
31
+ Node-usbmux adds the `irelay` command:
32
+
33
+ ```sh
34
+ # Relay localhost:2222 to port 22 on device through USB
35
+ # (so you can ssh root@localhost -p 2222)
36
+ irelay 22:2222
37
+
38
+ # Relay multiple port pairs, extra verbose mode
39
+ irelay 22:2222 1234:1234 -vv
40
+
41
+ # Specify a device with -u, --udid option
42
+ irelay 22:2222 1234:1234 --udid=12345abcde12345abcde12345abcde12345abcde
43
+
44
+ # Show UDIDs of attached devices
45
+ irelay listen
46
+
47
+ # More info
48
+ irelay --help
49
+ ```
50
+
51
+
52
+ ## Module Usage
53
+
54
+ ```javascript
55
+ var usbmux = require('usbmux');
56
+
57
+ // usbmux.Relay()
58
+ // usbmux.createListener()
59
+ // usbmux.getTunnel()
60
+ // usbmux.devices
61
+
62
+ ```
63
+
64
+ ### new usbmux.Relay(devicePort, relayPort[, options])
65
+ Create a tcp relay that pipes a local port to an attached iOS device port.
66
+
67
+ - devicePort {integer} - Destination port on device
68
+ - relayPort {integer} - Local port to start tcp server on
69
+ - options {object}
70
+ - options.timeout - Search time (in ms) before emitting warning
71
+ - options.udid - UDID of specific device to connect to
72
+
73
+ ```javascript
74
+ // Ex:
75
+ var relay = new usbmux.Relay(22, 2222)
76
+ .on('error', function(err) {})
77
+ .on('ready', function(udid) {
78
+ // A USB device is connected and ready to be relayed to
79
+ })
80
+ ...
81
+
82
+ // you can stop the relay when done
83
+ relay.stop();
84
+ ```
85
+
86
+ ##### EVENTS:
87
+
88
+ **error** - _err {Error}_ <br/>
89
+ Fires when there is an error:
90
+ - with the relay's TCP server (like EADDRINUSE), or
91
+ - from usbmuxd
92
+
93
+ **warning** - _err {Error}_ <br/>
94
+ When a relay starts it will check for connected devices. If there isn't a device ready within the time limit (default 1s), the relay will issue a warning (but will continue to search for and use connected devices).
95
+
96
+ **ready** - _UDID {string}_ <br/>
97
+ Fires when a connected device is first detected by the relay
98
+
99
+ **attached** - _UDID {string}_ <br/>
100
+ Fires when a USB device is attached (or first detected)
101
+
102
+ **detached** - _UDID {string}_ <br/>
103
+ Fires when a USB device is detached
104
+
105
+ **connect** <br/>
106
+ Fires when a new connection is made to the relay's TCP server
107
+
108
+ **disconnect** <br/>
109
+ Fires when a connection to the relay's TCP server is ended
110
+
111
+ **close** <br/>
112
+ Fires when the relay's TCP server is closes (the net.Server event)
113
+
114
+
115
+ ### usbmux.createListener()
116
+ Connects to usbmuxd and listens for iOS devices <br/>
117
+ Returns a normal net.Socket connection with two added events:
118
+
119
+ ```javascript
120
+ // Ex:
121
+ var listener = new usbmux.createListener()
122
+ .on('error', function(err) {})
123
+ .on('attached', function(udid) {})
124
+ .on('detached', function(udid) {});
125
+
126
+ // listener is just a net.Socket connection to usbmuxd
127
+ assert(listener instanceof net.Socket);
128
+ listener.end();
129
+ ```
130
+
131
+ ##### EVENTS:
132
+
133
+ **attached** - _UDID {string}_ <br/>
134
+ Fires when a USB device is attached (or first detected)
135
+
136
+ **detached** - _UDID {string}_ <br/>
137
+ Fires when a USB device is detached
138
+
139
+
140
+ ### usbmux.getTunnel(devicePort[, options])
141
+ Get a tunneled connection to port on device within a timeout period <br/>
142
+ Returns a promise that resolves a net.Socket connection to the requested port
143
+
144
+ - devicePort {integer} - Destination port on device
145
+ - options {object}
146
+ - options.timeout - Search time (in ms) before failing with error
147
+ - options.udid - UDID of specific device to connect to
148
+
149
+ ```javascript
150
+ // Ex:
151
+ usbmux.getTunnel(1234)
152
+ .then(function(tunnel) {
153
+ // tunnel is just a net.Socket connection to the device port
154
+ // you can write / .on('data') it like normal
155
+ assert(tunnel instanceof net.Socket);
156
+ tunnel.write('hello');
157
+ })
158
+ .catch(function(err) {
159
+ console.err(err);
160
+ // "Tunnel failed, Err #3: Port isn't available or open"
161
+ });
162
+ ```
163
+
164
+
165
+ ### usbmux.devices
166
+ Currently connected USB devices, keyed by UDIDs
167
+
168
+ ```javascript
169
+ // Ex:
170
+ listener.on('attached', function(udid) {
171
+ console.log(usbmux.devices[udid]);
172
+ });
173
+
174
+ // {
175
+ // ConnectionType: 'USB',
176
+ // DeviceID: 19,
177
+ // LocationID: 0,
178
+ // ProductID: 4776,
179
+ // SerialNumber: '22226dd59aaac687f555f8521f8ffddac32d394b'
180
+ // }
181
+ ```
182
+
183
+
184
+ ## Tests
185
+
186
+ ```
187
+ npm test
188
+ ```
189
+
190
+ Some of the tests require an attached device since that was easier than implementing an entire mock usbmuxd. These tests connect to device port 22 by default, but you can set a different port with the env var `TESTPORT`.
191
+
192
+
193
+ ## How does usbmuxd work?
194
+
195
+ Usbmuxd operates over TCP and accepts two different requests: `listen` and `connect`.
196
+
197
+ A `listen` request asks usbmuxd to turn the current tcp connection into a dedicated notification pipe, sending notifications about devices as they are attached and detached.
198
+
199
+ A `connect` request asks usbmuxd to turn the current tcp connection into a tunneled connection to a port on the device. Connect requests need a DeviceID, which you get from the listener notifications.
200
+
201
+ Each request must be sent in a new tcp connection, i.e. you can't send a listen request and a connect request in the same connection. Because of this, you'll always need at least two connections open, one listening for device status and one actually connecting to devices.
202
+
203
+
204
+ ## Usbmuxd protocol
205
+
206
+ Usbmux messages are composed of a header and a payload plist.
207
+
208
+ There used to be a [binary](https://www.theiphonewiki.com/wiki/Usbmux) version of the protocol, but it isn't used anymore. There is no documentation for usbmuxd, so this understanding is borrowed from looking at the implementation in [tcprelay.py](https://github.com/rcg4u/iphonessh).
209
+
210
+ ##### Header:
211
+
212
+ Four 32-bit unsigned LE integers (in order):
213
+ <br/> _Length:_ length of the header + plist (16 + plist.length)
214
+ <br/> _Version:_ 0 for binary version, 1 for plist version
215
+ <br/> _Request:_ always 8 (taken from tcprelay.py)
216
+ <br/> _Tag:_ always 1 (taken from tcprelay.py)
217
+
218
+ ##### Listen:
219
+
220
+ ```plist
221
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
222
+ <plist version="1.0">
223
+ <dict>
224
+ <key>MessageType</key>
225
+ <string>Listen</string>
226
+ <key>ClientVersionString</key>
227
+ <string>node-usbmux</string>
228
+ <key>ProgName</key>
229
+ <string>node-usbmux</string>
230
+ </dict>
231
+ </plist>
232
+ ```
233
+
234
+ ##### Connect:
235
+
236
+ ```plist
237
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
238
+ <plist version="1.0">
239
+ <dict>
240
+ <key>MessageType</key>
241
+ <string>Connect</string>
242
+ <key>ClientVersionString</key>
243
+ <string>node-usbmux</string>
244
+ <key>ProgName</key>
245
+ <string>node-usbmux</string>
246
+ <key>DeviceID</key>
247
+ <integer>3</integer>
248
+ <key>PortNumber</key>
249
+ <integer>5632</integer>
250
+ </dict>
251
+ </plist>
252
+ ```
253
+
254
+ It's important to note that the PortNumber must be byte-swapped to be network-endian. So port 22 in this example ends up being sent as 5632.
255
+
256
+ ##### Response:
257
+
258
+ ```plist
259
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
260
+ <plist version="1.0">
261
+ <dict>
262
+ <key>MessageType</key>
263
+ <string>Result</string>
264
+ <key>Number</key>
265
+ <integer>0</integer>
266
+ </dict>
267
+ </plist>
268
+ ```
269
+
270
+ The `Number` field indicates status. 0 is success, other numbers indicate an error:
271
+ - 0: Success
272
+ - 2: Device requested isn't connected
273
+ - 3: Port requested isn't available \ open
274
+ - 5: Malformed request
275
+
276
+
277
+ ## License
278
+
279
+ The MIT License (MIT)
280
+
281
+ Copyright (c) 2015 Sterling DeMille &lt;sterlingdemille@gmail.com&gt;
282
+
283
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
284
+ this software and associated documentation files (the "Software"), to deal in
285
+ the Software without restriction, including without limitation the rights to
286
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
287
+ the Software, and to permit persons to whom the Software is furnished to do so,
288
+ subject to the following conditions:
289
+
290
+ The above copyright notice and this permission notice shall be included in all
291
+ copies or substantial portions of the Software.
292
+
293
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
294
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
295
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
296
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
297
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
298
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/bin/cli.js ADDED
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ import yargs from "yargs";
3
+ import { hideBin } from "yargs/helpers";
4
+
5
+ import * as usbmux from "../dist/usbmux.js";
6
+
7
+ var argv = yargs(hideBin(process.argv))
8
+ .usage("Usage: irelay <port:port> [options]")
9
+ .demand(1)
10
+ .option("u", {
11
+ alias: "udid",
12
+ describe: "Specify device to connect to by UDID",
13
+ type: "string",
14
+ })
15
+ .command("listen", "Listen for attached devices")
16
+ .option("v", {
17
+ alias: "verbose",
18
+ describe: "Output debugging info",
19
+ count: "verbose",
20
+ })
21
+ .help("h")
22
+ .alias("h", "help")
23
+ .example("irelay 22:2222", "Pipe localhost:2222 to port 22 on device")
24
+ .example("irelay 22:2222 1234:1234", "Pipe multiple ports")
25
+ .example("irelay listen", "Show UDIDs of attached devices").argv;
26
+
27
+ /**
28
+ * Shows debugging info only for verbosity = 1
29
+ * @param {*...} arguments
30
+ */
31
+ function info() {
32
+ if (argv.verbose !== 1) return;
33
+ var args = Array.prototype.slice.call(arguments);
34
+ if (!args[args.length - 1]) args.pop(); // if last arg is undefined, remove it
35
+ console.log.apply(console, args);
36
+ }
37
+
38
+ /**
39
+ * Panic!
40
+ * @param {*...} arguments
41
+ */
42
+ function panic() {
43
+ console.error("");
44
+ console.error.apply(console, arguments);
45
+ console.error("");
46
+ process.exit();
47
+ }
48
+
49
+ /**
50
+ * Error handler for listener and relay
51
+ * @param {Error} err
52
+ */
53
+ function onErr(err) {
54
+ // local port is in use
55
+ if (err.code === "EADDRINUSE") {
56
+ panic("Local port is in use \nFailing...");
57
+ }
58
+ // usbmux not there
59
+ if (err.code === "ECONNREFUSED" || err.code === "EADDRNOTAVAIL") {
60
+ panic("Usbmuxd not found at", usbmux.address, "\nFailing...");
61
+ }
62
+ // other
63
+ panic("%s \nFailing...", err);
64
+ }
65
+
66
+ /**
67
+ * Listen for and report connected devices
68
+ */
69
+ function listenForDevices() {
70
+ console.log("Listening for connected devices... \n");
71
+
72
+ usbmux
73
+ .createListener()
74
+ .on("error", onErr)
75
+ .on("attached", function (udid) {
76
+ console.log("Device found: ", udid);
77
+ })
78
+ .on("detached", function (udid) {
79
+ console.log("Device removed: ", udid);
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Parse port arg string into array of ints (ie, '22:2222' -> [22, 2222])
85
+ * @param {string} arg
86
+ * @return {integer[]}
87
+ */
88
+ function parsePorts(arg) {
89
+ // coerce and split
90
+ var ports = ("" + arg).split(":");
91
+ if (ports.length !== 2) {
92
+ panic("Error parsing ports.");
93
+ }
94
+
95
+ // parse ints
96
+ var devicePort = Number(ports[0] === "" ? NaN : ports[0]),
97
+ relayPort = Number(ports[1] === "" ? NaN : ports[1]);
98
+
99
+ // NaN check em
100
+ if (devicePort !== devicePort || relayPort !== relayPort) {
101
+ panic("Error parsing ports.");
102
+ }
103
+
104
+ return [devicePort, relayPort];
105
+ }
106
+
107
+ /**
108
+ * Start a new relay from a pair of given ports
109
+ * @param {integer[]} portPair - [devicePort, relayPort]
110
+ */
111
+ function startRelay(portPair) {
112
+ var devicePort = portPair[0],
113
+ relayPort = portPair[1];
114
+
115
+ console.log(
116
+ "Starting relay from local port: %s -> device port: %s",
117
+ relayPort,
118
+ devicePort,
119
+ );
120
+
121
+ new usbmux.Relay(devicePort, relayPort, { udid: argv.udid })
122
+ .on("error", onErr)
123
+ .on("warning", console.log.bind(console, "Warning: device not found..."))
124
+ .on("ready", info.bind(this, "Device ready: "))
125
+ .on("attached", info.bind(this, "Device attached: "))
126
+ .on("detached", info.bind(this, "Device detached: "))
127
+ .on("connect", info.bind(this, "New connection to relay started."))
128
+ .on("disconnect", info.bind(this, "Connection to relay closed."))
129
+ .on("close", info.bind(this, "Relay has closed."));
130
+ }
131
+
132
+ // Set debugging env vars if extra verbose (needs to be set before requiring)
133
+ if (argv.verbose >= 2) process.env.DEBUG = "usbmux:*";
134
+
135
+ // Either listen or start relays
136
+ argv._[0] === "listen"
137
+ ? listenForDevices()
138
+ : argv._.map(parsePorts).forEach(startRelay);
package/lib/usbmux.ts ADDED
@@ -0,0 +1,542 @@
1
+ import net from "node:net";
2
+ import { EventEmitter } from "events";
3
+
4
+ import plist from "plist";
5
+
6
+ /**
7
+ * Debugging
8
+ * set with DEBUG=usbmux:* env variable
9
+ *
10
+ * on windows cmd set with: cmd /C "SET DEBUG=usbmux:* && node script.js"
11
+ */
12
+ import bug from "debug";
13
+ const debug = {
14
+ relay: bug("usbmux:relay"),
15
+ listen: bug("usbmux:listen"),
16
+ connect: bug("usbmux:connect"),
17
+ };
18
+
19
+ type Device = {
20
+ ConnectionType: string;
21
+ DeviceID: DeviceId;
22
+ LocationID: number;
23
+ ProductID: number;
24
+ SerialNumber: string;
25
+ };
26
+ /**
27
+ * Keep track of connected devices
28
+ *
29
+ * Maps device UDID to device properties, ie:
30
+ * '22226dd59aaac687f555f8521f8ffddac32d394b': {
31
+ * ConnectionType: 'USB',
32
+ * DeviceID: 19,
33
+ * LocationID: 0,
34
+ * ProductID: 4776,
35
+ * SerialNumber: '22226dd59aaac687f555f8521f8ffddac32d394b'
36
+ * }
37
+ *
38
+ * Devices are added and removed to this obj only by createListener()
39
+ *
40
+ */
41
+ export const devices: Record<string, Device> = {};
42
+
43
+ /**
44
+ * usbmuxd address
45
+ *
46
+ * OSX usbmuxd listens on a unix socket at /var/run/usbmuxd
47
+ * Windows usbmuxd listens on port 27015
48
+ *
49
+ * libimobiledevice[1] looks like it operates at /var/run/usbmuxd too, but if
50
+ * your usbmuxd is listening somewhere else you'll need to set this manually.
51
+ *
52
+ * [1] github.com/libimobiledevice/usbmuxd
53
+ */
54
+ export const address =
55
+ process.platform === "win32"
56
+ ? { port: 27015, family: 4 }
57
+ : { path: "/var/run/usbmuxd" };
58
+
59
+ /**
60
+ * Exposes methods for dealing with usbmuxd protocol messages (send/receive)
61
+ *
62
+ * The usbmuxd message protocol has 2 versions. V1 doesn't look like its used
63
+ * anymore. V2 is a header + plist format like this:
64
+ *
65
+ * Header:
66
+ * UInt32LE Length - is the length of the header + plist (16 + plist.length)
67
+ * UInt32LE Version - is 0 for binary version, 1 for plist version
68
+ * UInt32LE Request - is always 8, for plist? from rcg4u/iphonessh
69
+ * UInt32LE Tag - is always 1, ? from rcg4u/iphonessh
70
+ *
71
+ * Plist:
72
+ * <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
73
+ * "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
74
+ * <plist version="1.0">
75
+ * <dict>
76
+ * <key>MessageType</key>
77
+ * <string>Listen</string>
78
+ * <key>ClientVersionString</key>
79
+ * <string>node-usbmux</string>
80
+ * <key>ProgName</key>
81
+ * <string>node-usbmux</string>
82
+ * </dict>
83
+ * </plist>
84
+ *
85
+ * References:
86
+ * - https://github.com/rcg4u/iphonessh
87
+ * - https://www.theiphonewiki.com/wiki/Usbmux (binary protocol)
88
+ */
89
+
90
+ type DeviceId = number;
91
+ type UsbMuxPlist =
92
+ | {
93
+ MessageType: "Result";
94
+ Number: number;
95
+ }
96
+ | {
97
+ MessageType: "Attached";
98
+ Number: number;
99
+ Properties: Device;
100
+ }
101
+ | { MessageType: "Detached"; Number: number; DeviceID: DeviceId };
102
+
103
+ export const protocol = (function () {
104
+ /**
105
+ * Pack a request object into a buffer for usbmuxd
106
+ */
107
+ function pack(payload_obj: plist.PlistValue): Buffer {
108
+ const payload_plist = plist.build(payload_obj);
109
+ const payload_buf = Buffer.from(payload_plist);
110
+
111
+ var header = {
112
+ len: payload_buf.length + 16,
113
+ version: 1,
114
+ request: 8,
115
+ tag: 1,
116
+ };
117
+
118
+ const header_buf = Buffer.alloc(16);
119
+ header_buf.fill(0);
120
+ header_buf.writeUInt32LE(header.len, 0);
121
+ header_buf.writeUInt32LE(header.version, 4);
122
+ header_buf.writeUInt32LE(header.request, 8);
123
+ header_buf.writeUInt32LE(header.tag, 12);
124
+
125
+ return Buffer.concat([header_buf, payload_buf]);
126
+ }
127
+
128
+ /**
129
+ * Swap endianness of a 16bit value
130
+ */
131
+ function byteSwap16(val: number) {
132
+ return ((val & 0xff) << 8) | ((val >> 8) & 0xff);
133
+ }
134
+
135
+ /**
136
+ * Listen request
137
+ */
138
+ const listen: Buffer = pack({
139
+ MessageType: "Listen",
140
+ ClientVersionString: "node-usbmux",
141
+ ProgName: "node-usbmux",
142
+ });
143
+
144
+ /**
145
+ * Connect request
146
+ *
147
+ * Note: PortNumber must be network-endian, so it gets byte swapped here
148
+ */
149
+ function connect(deviceID: DeviceId, port: number): Buffer {
150
+ return pack({
151
+ MessageType: "Connect",
152
+ ClientVersionString: "node-usbmux",
153
+ ProgName: "node-usbmux",
154
+ DeviceID: deviceID,
155
+ PortNumber: byteSwap16(port),
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Creates a function that will parse messages from data events
161
+ *
162
+ * net.Socket data events sometimes break up the incoming message across
163
+ * multiple events, making it necessary to combine them. This parser function
164
+ * assembles messages using the length given in the message header and calls
165
+ * the onComplete callback as new messages are assembled. Sometime multiple
166
+ * messages will be within a single data buffer too.
167
+ */
168
+ function makeParser(
169
+ onComplete: (msg: UsbMuxPlist) => void,
170
+ ): (data: Buffer) => void {
171
+ // Store status (remaining message length & msg text) of partial messages
172
+ // across multiple calls to the parse function
173
+ let len: number, msg: string;
174
+
175
+ return function parse(data: Buffer) {
176
+ // Check if this data represents a new incoming message or is part of an
177
+ // existing partially completed message
178
+ if (!len) {
179
+ // The length of the message's body is the total length (the first
180
+ // UInt32LE in the header) minus the length of header itself (16)
181
+ len = data.readUInt32LE(0) - 16;
182
+ msg = "";
183
+
184
+ // If there is data beyond the header then continue adding data to msg
185
+ data = data.subarray(16);
186
+ if (!data.length) return;
187
+ }
188
+
189
+ // Add in data until our remaining length is used up
190
+ var body = data.subarray(0, len);
191
+ msg += body;
192
+ len -= body.length;
193
+
194
+ // If msg is finished, convert plist to obj and run callback
195
+ if (len === 0) onComplete(plist.parse(msg) as UsbMuxPlist);
196
+
197
+ // If there is any data left over that means there is another message
198
+ // so we need to run this parse fct again using the rest of the data
199
+ data = data.subarray(body.length);
200
+ if (data.length) parse(data);
201
+ };
202
+ }
203
+
204
+ // Exposed methods
205
+ return {
206
+ listen: listen,
207
+ connect: connect,
208
+ makeParser: makeParser,
209
+ };
210
+ })();
211
+
212
+ /**
213
+ * Custom usbmuxd error
214
+ *
215
+ * There's no documentation for usbmuxd responses, but I think I've figured
216
+ * out these result numbers:
217
+ * 0 - Success
218
+ * 2 - Device requested isn't connected
219
+ * 3 - Port requested isn't available \ open
220
+ * 5 - Malformed request
221
+ */
222
+ export class UsbmuxdError extends Error {
223
+ number: number;
224
+
225
+ constructor(message: string, number: number) {
226
+ if (number) {
227
+ message += ", Err #" + number;
228
+ }
229
+ if (number === 2) message += ": Device isn't connected";
230
+ if (number === 3) message += ": Port isn't available or open";
231
+ if (number === 5) message += ": Malformed request";
232
+ super(message);
233
+ if (number) {
234
+ this.number = number;
235
+ }
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Connects to usbmuxd and listens for ios devices
241
+ *
242
+ * This connection stays open, listening as devices are plugged/unplugged and
243
+ * cant be upgraded into a tcp tunnel. You have to start a second connection
244
+ * with connect() to actually make tunnel.
245
+ *
246
+ * @return {net.Socket} - Socket with 2 bolted on events, attached & detached:
247
+ *
248
+ * Fires when devices are plugged in or first found by the listener
249
+ * @event net.Socket#attached
250
+ * @type {string} - UDID
251
+ *
252
+ * Fires when devices are unplugged
253
+ * @event net.Socket#detached
254
+ * @type {string} - UDID
255
+ */
256
+ export function createListener(): net.Socket {
257
+ const conn = net.connect(address);
258
+ const req = protocol.listen;
259
+
260
+ /**
261
+ * Handle complete messages from usbmuxd
262
+ * @function
263
+ */
264
+ const parse = protocol.makeParser(function onMsgComplete(msg) {
265
+ debug.listen("Response: \n%o", msg);
266
+
267
+ // first response always acknowledges / denies the request:
268
+ if (msg.MessageType === "Result" && msg.Number !== 0) {
269
+ conn.emit("error", new UsbmuxdError("Listen failed", msg.Number));
270
+ conn.end();
271
+ }
272
+
273
+ // subsequent responses report on connected device status:
274
+ if (msg.MessageType === "Attached") {
275
+ devices[msg.Properties.SerialNumber] = msg.Properties;
276
+ conn.emit("attached", msg.Properties.SerialNumber);
277
+ }
278
+
279
+ if (msg.MessageType === "Detached") {
280
+ // given msg.DeviceID, find matching device and remove it
281
+ Object.keys(devices).forEach(function (key) {
282
+ if (devices[key].DeviceID === msg.DeviceID) {
283
+ conn.emit("detached", devices[key].SerialNumber);
284
+ delete devices[key];
285
+ }
286
+ });
287
+ }
288
+ });
289
+
290
+ debug.listen("Request: \n%s", req.subarray(16).toString());
291
+
292
+ conn.on("data", parse);
293
+ process.nextTick(function () {
294
+ conn.write(req);
295
+ });
296
+
297
+ return conn;
298
+ }
299
+
300
+ /**
301
+ * Connects to a device through usbmuxd for a tunneled tcp connection
302
+ */
303
+ export function connect(
304
+ deviceID: DeviceId,
305
+ devicePort: number,
306
+ ): Promise<net.Socket> {
307
+ return new Promise(function (resolve, reject) {
308
+ const conn = net.connect(address),
309
+ req = protocol.connect(deviceID, devicePort);
310
+
311
+ /**
312
+ * Handle complete messages from usbmuxd
313
+ * @function
314
+ */
315
+ var parse = protocol.makeParser(function onMsgComplete(msg) {
316
+ debug.connect("Response: \n%o", msg);
317
+
318
+ if (msg.MessageType === "Result" && msg.Number === 0) {
319
+ conn.removeListener("data", parse);
320
+ resolve(conn);
321
+ return;
322
+ }
323
+
324
+ // anything other response means it failed
325
+ reject(new UsbmuxdError("Tunnel failed", msg.Number));
326
+ conn.end();
327
+ });
328
+
329
+ debug.connect("Request: \n%s", req.subarray(16).toString());
330
+
331
+ conn.on("data", parse);
332
+ process.nextTick(function () {
333
+ conn.write(req);
334
+ });
335
+ });
336
+ }
337
+
338
+ type RelayOpts = {
339
+ timeout?: number;
340
+ udid?: string;
341
+ };
342
+ /**
343
+ * Creates a new tcp relay to a port on connected usb device
344
+ *
345
+ * @constructor
346
+ * @param {integer} devicePort - Port to connect to on device
347
+ * @param {integer} relayPort - Local port that will listen as relay
348
+ * @param {object} [opts] - Options
349
+ * @param {integer} [opts.timeout=1000] - Search time (ms) before warning
350
+ * @param {string} [opts.udid] - UDID of specific device to connect to
351
+ *
352
+ * @public
353
+ */
354
+ export class Relay extends EventEmitter {
355
+ _devicePort: number;
356
+ _relayPort: number;
357
+ _udid?: string;
358
+ constructor(devicePort: number, relayPort: number, opts: RelayOpts) {
359
+ super();
360
+ this._devicePort = devicePort;
361
+ this._relayPort = relayPort;
362
+
363
+ opts = opts || {};
364
+ this._udid = opts.udid;
365
+
366
+ this._startListener(opts.timeout || 1000);
367
+ this._startServer();
368
+ }
369
+ /**
370
+ * Stops the relay
371
+ */
372
+ stop = function () {
373
+ this._listener.end();
374
+ this._server.close();
375
+ };
376
+ /**
377
+ * Debugging wrapper for emits
378
+ */
379
+ _emit = function (event: string, data: any) {
380
+ debug.relay("Emit: %s", event + (data ? ", Data: " + data : ""));
381
+ this.emit(event, data);
382
+ };
383
+ /**
384
+ * Starts a usbmuxd listener
385
+ *
386
+ * Relay will start searching for connected devices and issue a warning if a
387
+ * device is not found within the timeout. If/when a device is found, it will
388
+ * emit a ready event.
389
+ *
390
+ * Listener events (attach, detach, error) are passed through as relay events.
391
+ */
392
+ _startListener = function (timeout: number) {
393
+ var _this = this;
394
+
395
+ var timer = setTimeout(function () {
396
+ // no UDID was given and no devices found yet
397
+ if (!_this._udid && !Object.keys(devices).length) {
398
+ _this._emit("warning", new Error("No devices connected"));
399
+ }
400
+ // UDID was given, but that device is not connected
401
+ if (_this._udid && !devices[_this._udid]) {
402
+ _this._emit("warning", new Error("Requested device not connected"));
403
+ }
404
+ }, timeout || 1000);
405
+
406
+ function readyCheck(udid: string) {
407
+ if (_this._udid && _this._udid !== udid) return;
408
+ _this._emit("ready", udid);
409
+ _this._listener.removeListener("attached", readyCheck);
410
+ clearTimeout(timer);
411
+ }
412
+
413
+ this._listener = createListener()
414
+ .on("attached", readyCheck)
415
+ .on("attached", _this._emit.bind(this, "attached"))
416
+ .on("detached", _this._emit.bind(this, "detached"))
417
+ .on("error", _this._emit.bind(this, "error"));
418
+ };
419
+ /**
420
+ * Start local TCP server that will pipe to the usbmuxd tunnel
421
+ *
422
+ * Server events (close and error) are passed through as relay events.
423
+ */
424
+ _startServer = function () {
425
+ var _this = this;
426
+ this._server = net
427
+ .createServer(this._handler.bind(this))
428
+ .on("close", _this._emit.bind(this, "close"))
429
+ .on("error", function (err) {
430
+ _this._listener.end();
431
+ _this._emit("error", err);
432
+ })
433
+ .listen(this._relayPort);
434
+ };
435
+ /**
436
+ * Handle & pipe connections from local server
437
+ *
438
+ * Fires error events and connection begin / disconnect events
439
+ */
440
+ _handler = function (conn: net.Socket) {
441
+ // emit error if there are no devices connected
442
+ if (!Object.keys(devices).length) {
443
+ this._emit("error", new Error("No devices connected"));
444
+ conn.end();
445
+ return;
446
+ }
447
+
448
+ // emit error if a udid was specified but that device isn't connected
449
+ if (this._udid && !devices[this._udid]) {
450
+ this._emit("error", new Error("Requested device not connected"));
451
+ conn.end();
452
+ return;
453
+ }
454
+
455
+ // Use specified device or choose one from available devices
456
+ var _this = this,
457
+ udid = this._udid || Object.keys(devices)[0],
458
+ deviceID = devices[udid].DeviceID;
459
+
460
+ connect(deviceID, this._devicePort)
461
+ .then(function (tunnel) {
462
+ // pipe connection & tunnel together
463
+ conn.pipe(tunnel).pipe(conn);
464
+
465
+ _this._emit("connect");
466
+
467
+ conn.on("end", function () {
468
+ _this._emit("disconnect");
469
+ tunnel.end();
470
+ conn.end();
471
+ });
472
+
473
+ conn.on("error", function () {
474
+ tunnel.end();
475
+ conn.end();
476
+ });
477
+ })
478
+ .catch(function (err) {
479
+ _this._emit("error", err);
480
+ conn.end();
481
+ });
482
+ };
483
+ }
484
+
485
+ /**
486
+ * Find a device (specified or not) within a timeout
487
+ *
488
+ * Usbmuxd has IDs it assigned to devices as they are plugged in. The IDs
489
+ * change as devices are unpplugged and plugged back in, so even if we have a
490
+ * UDID we need to get the current ID from usbmuxd before we can connect.
491
+ */
492
+ export function findDevice(opts: RelayOpts): Promise<number> {
493
+ return new Promise(function (resolve, reject) {
494
+ var listener = createListener();
495
+ opts = opts || {};
496
+
497
+ var timer = setTimeout(function () {
498
+ listener.end();
499
+ opts.udid
500
+ ? reject(new Error("Requested device not connected"))
501
+ : reject(new Error("No devices connected"));
502
+ }, opts.timeout || 1000);
503
+
504
+ listener.on("attached", function (udid) {
505
+ if (opts.udid && opts.udid !== udid) return;
506
+ listener.end();
507
+ clearTimeout(timer);
508
+ resolve(devices[udid].DeviceID);
509
+ });
510
+ });
511
+ }
512
+
513
+ /**
514
+ * Get a tunneled connection to a device (specified or not) within a timeout
515
+ */
516
+ export async function getTunnel(
517
+ devicePort: number,
518
+ opts: RelayOpts,
519
+ ): Promise<net.Socket> {
520
+ opts = opts || {};
521
+ let udid: string;
522
+ let deviceID: DeviceId;
523
+
524
+ // If UDID was specified and that device's DeviceID is known, connect to it
525
+ if (opts.udid && devices[opts.udid]) {
526
+ deviceID = devices[opts.udid].DeviceID;
527
+ return connect(deviceID, devicePort);
528
+ }
529
+
530
+ // If no UDID given, connect to any known device
531
+ // (random because no key order, but there's probably only 1 option anyways)
532
+ if (!opts.udid && Object.keys(devices).length) {
533
+ udid = Object.keys(devices)[0];
534
+ deviceID = devices[udid].DeviceID;
535
+ return connect(deviceID, devicePort);
536
+ }
537
+
538
+ // - Try to find and connect to requested the device (given opts.UDID),
539
+ // - or find and connect to any device (no opts.UDID given)
540
+ const deviceID_2 = await findDevice(opts);
541
+ return await connect(deviceID_2, devicePort);
542
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@irdk/usbmux",
3
+ "version": "0.2.0",
4
+ "description": "iOS usbmuxd client library",
5
+ "main": "./dist/usbmux.cjs",
6
+ "module": "./dist/usbmux.js",
7
+ "types": "./dist/usbmux.d.ts",
8
+ "type": "module",
9
+ "bin": {
10
+ "irelay": "./bin/cli.js"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "tsup": {
16
+ "entry": [
17
+ "./lib/usbmux.ts"
18
+ ],
19
+ "target": [
20
+ "node18",
21
+ "es2020"
22
+ ],
23
+ "splitting": false,
24
+ "sourcemap": true,
25
+ "clean": true,
26
+ "dts": true,
27
+ "format": [
28
+ "cjs",
29
+ "esm"
30
+ ]
31
+ },
32
+ "scripts": {
33
+ "test": "./node_modules/.bin/mocha ./test/tests.cjs",
34
+ "build": "tsup",
35
+ "dev": "tsup --watch"
36
+ },
37
+ "keywords": [
38
+ "usbmuxd",
39
+ "ios",
40
+ "irelay"
41
+ ],
42
+ "author": {
43
+ "name": "Sterling DeMille",
44
+ "email": "sterlingdemille+npm@gmail.com"
45
+ },
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/demille/node-usbmux.git"
50
+ },
51
+ "dependencies": {
52
+ "debug": "^4.3.6",
53
+ "plist": "^3.1.0",
54
+ "yargs": "^17.7.2"
55
+ },
56
+ "devDependencies": {
57
+ "@types/debug": "^4.1.12",
58
+ "@types/plist": "^3.0.5",
59
+ "mocha": "^10.7.0",
60
+ "rewire": "^7.0.0",
61
+ "should": "^13.2.3",
62
+ "tsup": "^8.0.1",
63
+ "typescript": "^5.8.2"
64
+ },
65
+ "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
66
+ }
package/test/tests.cjs ADDED
@@ -0,0 +1,328 @@
1
+ var net = require("net"),
2
+ should = require("should"),
3
+ rewire = require("rewire");
4
+
5
+ var usbmux = rewire("../dist/usbmux.cjs"),
6
+ protocol = usbmux.__get__("protocol");
7
+
8
+ // tests connect to port 22 on attached ios device (or set env var)
9
+ var port = process.env.TESTPORT || 22;
10
+
11
+ //
12
+ // TESTS AND EXPECTED RESULTS
13
+ //
14
+
15
+ var tests = {
16
+ // for testing protocol.listen
17
+ listen:
18
+ "gwEAAAEAAAAIAAAAAQAAADw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+CjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPgo8cGxpc3QgdmVyc2lvbj0iMS4wIj4KICA8ZGljdD4KICAgIDxrZXk+TWVzc2FnZVR5cGU8L2tleT4KICAgIDxzdHJpbmc+TGlzdGVuPC9zdHJpbmc+CiAgICA8a2V5PkNsaWVudFZlcnNpb25TdHJpbmc8L2tleT4KICAgIDxzdHJpbmc+bm9kZS11c2JtdXg8L3N0cmluZz4KICAgIDxrZXk+UHJvZ05hbWU8L2tleT4KICAgIDxzdHJpbmc+bm9kZS11c2JtdXg8L3N0cmluZz4KICA8L2RpY3Q+CjwvcGxpc3Q+",
19
+
20
+ // for testing
21
+ // - protocol.connect(12, 22)
22
+ // - protocol.connect(21, 1234)
23
+ connect: {
24
+ _12_22:
25
+ "7AEAAAEAAAAIAAAAAQAAADw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+CjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPgo8cGxpc3QgdmVyc2lvbj0iMS4wIj4KICA8ZGljdD4KICAgIDxrZXk+TWVzc2FnZVR5cGU8L2tleT4KICAgIDxzdHJpbmc+Q29ubmVjdDwvc3RyaW5nPgogICAgPGtleT5DbGllbnRWZXJzaW9uU3RyaW5nPC9rZXk+CiAgICA8c3RyaW5nPm5vZGUtdXNibXV4PC9zdHJpbmc+CiAgICA8a2V5PlByb2dOYW1lPC9rZXk+CiAgICA8c3RyaW5nPm5vZGUtdXNibXV4PC9zdHJpbmc+CiAgICA8a2V5PkRldmljZUlEPC9rZXk+CiAgICA8aW50ZWdlcj4xMjwvaW50ZWdlcj4KICAgIDxrZXk+UG9ydE51bWJlcjwva2V5PgogICAgPGludGVnZXI+NTYzMjwvaW50ZWdlcj4KICA8L2RpY3Q+CjwvcGxpc3Q+",
26
+ _21_1234:
27
+ "7QEAAAEAAAAIAAAAAQAAADw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+CjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPgo8cGxpc3QgdmVyc2lvbj0iMS4wIj4KICA8ZGljdD4KICAgIDxrZXk+TWVzc2FnZVR5cGU8L2tleT4KICAgIDxzdHJpbmc+Q29ubmVjdDwvc3RyaW5nPgogICAgPGtleT5DbGllbnRWZXJzaW9uU3RyaW5nPC9rZXk+CiAgICA8c3RyaW5nPm5vZGUtdXNibXV4PC9zdHJpbmc+CiAgICA8a2V5PlByb2dOYW1lPC9rZXk+CiAgICA8c3RyaW5nPm5vZGUtdXNibXV4PC9zdHJpbmc+CiAgICA8a2V5PkRldmljZUlEPC9rZXk+CiAgICA8aW50ZWdlcj4yMTwvaW50ZWdlcj4KICAgIDxrZXk+UG9ydE51bWJlcjwva2V5PgogICAgPGludGVnZXI+NTM3NjQ8L2ludGVnZXI+CiAgPC9kaWN0Pgo8L3BsaXN0Pg==",
28
+ },
29
+
30
+ // for testing the message parser
31
+ makeParser: {
32
+ // a confirmation message
33
+ confirmation: {
34
+ msg: {
35
+ MessageType: "Result",
36
+ Number: 0,
37
+ },
38
+ data: new Buffer(
39
+ "JgEAAAEAAAAIAAAAAQAAADw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+CjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPgo8cGxpc3QgdmVyc2lvbj0iMS4wIj4KPGRpY3Q+Cgk8a2V5Pk1lc3NhZ2VUeXBlPC9rZXk+Cgk8c3RyaW5nPlJlc3VsdDwvc3RyaW5nPgoJPGtleT5OdW1iZXI8L2tleT4KCTxpbnRlZ2VyPjA8L2ludGVnZXI+CjwvZGljdD4KPC9wbGlzdD4K",
40
+ "base64",
41
+ ),
42
+ },
43
+
44
+ // a device report message
45
+ device: {
46
+ msg: {
47
+ DeviceID: 7,
48
+ MessageType: "Attached",
49
+ Properties: {
50
+ ConnectionType: "USB",
51
+ DeviceID: 7,
52
+ LocationID: 0,
53
+ ProductID: 4776,
54
+ SerialNumber: "22226dd59068c222f46522221f8222da222d394b",
55
+ },
56
+ },
57
+ data: new Buffer(
58
+ "qAIAAAAAAAAAAAAAAAAAADw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+CjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPgo8cGxpc3QgdmVyc2lvbj0iMS4wIj4KICA8ZGljdD4KICAgIDxrZXk+RGV2aWNlSUQ8L2tleT4KICAgIDxpbnRlZ2VyPjc8L2ludGVnZXI+CiAgICA8a2V5Pk1lc3NhZ2VUeXBlPC9rZXk+CiAgICA8c3RyaW5nPkF0dGFjaGVkPC9zdHJpbmc+CiAgICA8a2V5PlByb3BlcnRpZXM8L2tleT4KICAgIDxkaWN0PgogICAgICA8a2V5PkNvbm5lY3Rpb25UeXBlPC9rZXk+CiAgICAgIDxzdHJpbmc+VVNCPC9zdHJpbmc+CiAgICAgIDxrZXk+RGV2aWNlSUQ8L2tleT4KICAgICAgPGludGVnZXI+NzwvaW50ZWdlcj4KICAgICAgPGtleT5Mb2NhdGlvbklEPC9rZXk+CiAgICAgIDxpbnRlZ2VyPjA8L2ludGVnZXI+CiAgICAgIDxrZXk+UHJvZHVjdElEPC9rZXk+CiAgICAgIDxpbnRlZ2VyPjQ3NzY8L2ludGVnZXI+CiAgICAgIDxrZXk+U2VyaWFsTnVtYmVyPC9rZXk+CiAgICAgIDxzdHJpbmc+MjIyMjZkZDU5MDY4YzIyMmY0NjUyMjIyMWY4MjIyZGEyMjJkMzk0Yjwvc3RyaW5nPgogICAgPC9kaWN0PgogIDwvZGljdD4KPC9wbGlzdD4=",
59
+ "base64",
60
+ ),
61
+ },
62
+ },
63
+ };
64
+
65
+ //
66
+ // RUNNER
67
+ //
68
+
69
+ // Test the building and parsing of usbmuxd messages
70
+ //
71
+ describe("protocol", function () {
72
+ // Test packing a listen request obj -> a usbmuxd msg with header
73
+ // Comparing against a known working example
74
+ //
75
+ describe(".listen", function () {
76
+ it("matches test buffer", function () {
77
+ protocol.listen.toString("base64").should.be.eql(tests.listen);
78
+ });
79
+ });
80
+
81
+ // Test packing a connect request obj -> a usbmuxd msg with header
82
+ // Compare against known working examples
83
+ //
84
+ describe(".connect()", function () {
85
+ it("connect(12, 22)", function () {
86
+ protocol
87
+ .connect(12, 22)
88
+ .toString("base64")
89
+ .should.be.eql(tests.connect._12_22);
90
+ });
91
+ it("connect(21, 1234)", function () {
92
+ protocol
93
+ .connect(21, 1234)
94
+ .toString("base64")
95
+ .should.be.eql(tests.connect._21_1234);
96
+ });
97
+ });
98
+
99
+ // Test parse function, comparing known data & messages
100
+ //
101
+ // This is probably the most important test because there's a few cases that
102
+ // need to be handled and everything breaks if the messages don't get parsed
103
+ // correctly.
104
+ //
105
+ describe(".parse()", function () {
106
+ //
107
+ // Case 1: a whole message
108
+ //
109
+
110
+ /**
111
+ * Test conversion of one data buf -> one usbmuxd message
112
+ *
113
+ * @param {string} messageType - 'confirmation' or 'device'
114
+ * @param {Function} done
115
+ */
116
+ function one_data_to_one_msg(messageType, done) {
117
+ var test = tests.makeParser[messageType].data,
118
+ expected = tests.makeParser[messageType].msg;
119
+
120
+ var build = protocol.makeParser(function (msg) {
121
+ msg.should.eql(expected);
122
+ done();
123
+ });
124
+
125
+ build(test);
126
+ }
127
+
128
+ describe("where 1 data event makes up 1 complete msg", function () {
129
+ it("confirmation msg", function (done) {
130
+ one_data_to_one_msg("confirmation", done);
131
+ });
132
+
133
+ it("device report msg", function (done) {
134
+ one_data_to_one_msg("device", done);
135
+ });
136
+ });
137
+
138
+ //
139
+ // Case 2: messages broken up across data events
140
+ //
141
+
142
+ /**
143
+ * Test conversion of multiple data buf -> one usbmuxd message
144
+ *
145
+ * @param {string} messageType - 'confirmation' or 'device'
146
+ * @param {integer} numSegments - Number of segments to break data buf into
147
+ * @param {Function} done
148
+ */
149
+ function more_datas_to_one_msg(messageType, numSegments, done) {
150
+ var test = tests.makeParser[messageType].data,
151
+ expected = tests.makeParser[messageType].msg;
152
+
153
+ var build = protocol.makeParser(function (msg) {
154
+ msg.should.eql(expected);
155
+ done();
156
+ });
157
+
158
+ for (var i = 0; i < numSegments; i++) {
159
+ var previous = (test.length / numSegments) * i,
160
+ next = (test.length / numSegments) * (i + 1);
161
+
162
+ build(test.slice(previous, next));
163
+ }
164
+ }
165
+
166
+ describe("where >1 data events make up 1 complete msg", function () {
167
+ it("confirmation msg (split in 2)", function (done) {
168
+ more_datas_to_one_msg("confirmation", 2, done);
169
+ });
170
+
171
+ it("device report msg (split in 3)", function (done) {
172
+ more_datas_to_one_msg("device", 3, done);
173
+ });
174
+ });
175
+
176
+ //
177
+ // Case 3: multiple messages (w/ headers) in a data event
178
+ //
179
+
180
+ /**
181
+ * Call cb after getting called n times
182
+ *
183
+ * Accumulates return values in array each time its called, then passes to cb
184
+ *
185
+ * @param {integer} n - Number of times to call
186
+ * @param {Function} done
187
+ */
188
+ function callAfter(n, cb) {
189
+ var count = 0,
190
+ acc = [];
191
+
192
+ return function (item) {
193
+ count++;
194
+ acc.push(item);
195
+ if (count === n) cb(acc);
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Test conversion of one data buf -> multiple usbmuxd messages
201
+ *
202
+ * @param {string[]} messageTypes - [] of 'confirmation's or 'device's
203
+ * @param {Function} done
204
+ */
205
+ function one_data_to_many_msg(messageTypes, done) {
206
+ var test = Buffer.concat(
207
+ messageTypes.map(function (messageType) {
208
+ return tests.makeParser[messageType].data;
209
+ }),
210
+ );
211
+
212
+ var expected = messageTypes.map(function (messageType) {
213
+ return tests.makeParser[messageType].msg;
214
+ });
215
+
216
+ var onFinished = callAfter(messageTypes.length, function (msgs) {
217
+ msgs.should.eql(expected);
218
+ done();
219
+ });
220
+
221
+ var build = protocol.makeParser(onFinished);
222
+
223
+ build(test);
224
+ }
225
+
226
+ describe("where 1 data event makes up >1 complete msg", function () {
227
+ it("confirmation + report", function (done) {
228
+ one_data_to_many_msg(["confirmation", "device"], done);
229
+ });
230
+
231
+ it("report + report", function (done) {
232
+ one_data_to_many_msg(["device", "device"], done);
233
+ });
234
+
235
+ it("confirmation + report + report", function (done) {
236
+ one_data_to_many_msg(["confirmation", "device", "device"], done);
237
+ });
238
+ });
239
+ });
240
+ });
241
+
242
+ // From here on testing depends on a real plugged in device; its just easier to
243
+ // test the methods directly than mock out a tcp connection with fake buffers.
244
+ //
245
+ // These methods make up the core, so if they pass the rest of the module is
246
+ // probably in good shape.
247
+ //
248
+
249
+ describe("createListener()", function () {
250
+ it("fires attached event when a device is plugged in", function (done) {
251
+ const listener = usbmux
252
+ .createListener()
253
+ .on("error", done)
254
+ .on("attached", function () {
255
+ listener.end();
256
+ done();
257
+ });
258
+ });
259
+ });
260
+
261
+ describe("connect()", function () {
262
+ it("resolves a tunneled connection (id from above)", function (done) {
263
+ const listener = usbmux
264
+ .createListener()
265
+ .on("error", done)
266
+ .once("attached", function (udid) {
267
+ const deviceID = usbmux.devices[udid].DeviceID;
268
+ usbmux
269
+ .__get__("connect")(deviceID, port)
270
+ .then(function (tunnel) {
271
+ tunnel.should.be.instanceof(net.Socket);
272
+ tunnel.end();
273
+ listener.end();
274
+ done();
275
+ })
276
+ .catch(done);
277
+ });
278
+ });
279
+ });
280
+
281
+ describe("getTunnel()", function () {
282
+ describe("resolves a tunneled connection", function () {
283
+ it("with the udid option", function (done) {
284
+ const listener = usbmux
285
+ .createListener()
286
+ .on("error", done)
287
+ .once("attached", function (udid) {
288
+ usbmux
289
+ .getTunnel(port, { udid: udid })
290
+ .then(function (tunnel) {
291
+ tunnel.should.be.instanceof(net.Socket);
292
+ tunnel.end();
293
+ listener.end();
294
+ done();
295
+ })
296
+ .catch(done);
297
+ });
298
+ });
299
+
300
+ it("and without udid option", function (done) {
301
+ // emptying out device cache to test that case too
302
+ usbmux.__set__("devices", {});
303
+
304
+ usbmux
305
+ .getTunnel(port)
306
+ .then(function (tunnel) {
307
+ tunnel.should.be.instanceof(net.Socket);
308
+ tunnel.end();
309
+ done();
310
+ })
311
+ .catch(done);
312
+ });
313
+ });
314
+ });
315
+
316
+ describe("Relay()", function () {
317
+ it("has a server, a listener, and fires a ready event", function (done) {
318
+ var relay = new usbmux.Relay(port, 2222)
319
+ .on("error", done)
320
+ .on("warning", done)
321
+ .on("ready", function () {
322
+ relay._server.should.instanceof(net.Server);
323
+ relay._listener.should.instanceof(net.Socket);
324
+ relay.stop();
325
+ done();
326
+ });
327
+ });
328
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "esModuleInterop": true,
4
+ "allowSyntheticDefaultImports": true,
5
+ "target": "es6",
6
+ "noImplicitAny": true,
7
+ "moduleResolution": "node",
8
+ "sourceMap": true,
9
+ "outDir": "dist",
10
+ "baseUrl": ".",
11
+ "paths": {
12
+ "*": ["node_modules/*"]
13
+ }
14
+ },
15
+ "include": ["lib/**/*"]
16
+ }