@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 +20 -0
- package/README.md +298 -0
- package/bin/cli.js +138 -0
- package/lib/usbmux.ts +542 -0
- package/package.json +66 -0
- package/test/tests.cjs +328 -0
- package/tsconfig.json +16 -0
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 <sterlingdemille@gmail.com>
|
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
|
+
}
|