@limrun/ui 0.9.0-rc.5 → 0.9.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/components/device-install/device-install-dialog.d.ts +5 -0
- package/dist/components/device-install/index.d.ts +2 -0
- package/dist/core/device-install/apple/client.d.ts +17 -0
- package/dist/core/device-install/apple/crypto.d.ts +20 -0
- package/dist/core/device-install/apple/gsa-srp.d.ts +26 -0
- package/dist/core/device-install/apple/index.d.ts +5 -0
- package/dist/core/device-install/apple/provisioning.d.ts +161 -0
- package/dist/core/device-install/apple/relay.d.ts +29 -0
- package/dist/core/device-install/index.d.ts +4 -0
- package/dist/core/device-install/operations/index.d.ts +6 -0
- package/dist/core/device-install/operations/limbuild-client.d.ts +28 -0
- package/dist/core/device-install/operations/operations.d.ts +32 -0
- package/dist/core/device-install/operations/relay-client.d.ts +25 -0
- package/dist/core/device-install/operations/relay-protocol.d.ts +27 -0
- package/dist/core/device-install/operations/usbmux.d.ts +32 -0
- package/dist/core/device-install/operations/webusb.d.ts +21 -0
- package/dist/core/device-install/storage/browser-storage.d.ts +44 -0
- package/dist/core/device-install/storage/index.d.ts +1 -0
- package/dist/core/device-install/types.d.ts +48 -0
- package/dist/device-install/index.cjs +1 -0
- package/dist/device-install/index.d.ts +3 -0
- package/dist/device-install/index.js +78 -0
- package/dist/device-install/react.cjs +1 -0
- package/dist/device-install/react.d.ts +1 -0
- package/dist/device-install/react.js +4 -0
- package/dist/device-install-dialog-86RDdoK9.js +2 -0
- package/dist/device-install-dialog-CnyDWf0q.mjs +462 -0
- package/dist/device-install-dialog.css +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/use-device-install.d.ts +73 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +495 -502
- package/dist/use-device-install-CbGVvwPp.js +31 -0
- package/dist/use-device-install-j1Gekpl4.mjs +13623 -0
- package/package.json +15 -2
- package/src/components/device-install/device-install-dialog.css +325 -0
- package/src/components/device-install/device-install-dialog.tsx +513 -0
- package/src/components/device-install/index.ts +2 -0
- package/src/core/device-install/apple/client.ts +152 -0
- package/src/core/device-install/apple/crypto.ts +202 -0
- package/src/core/device-install/apple/gsa-srp.ts +127 -0
- package/src/core/device-install/apple/index.ts +5 -0
- package/src/core/device-install/apple/provisioning.ts +298 -0
- package/src/core/device-install/apple/relay.ts +221 -0
- package/src/core/device-install/index.ts +4 -0
- package/src/core/device-install/operations/index.ts +6 -0
- package/src/core/device-install/operations/limbuild-client.ts +104 -0
- package/src/core/device-install/operations/operations.ts +217 -0
- package/src/core/device-install/operations/relay-client.ts +255 -0
- package/src/core/device-install/operations/relay-protocol.ts +71 -0
- package/src/core/device-install/operations/usbmux.ts +270 -0
- package/src/core/device-install/operations/webusb-dom.d.ts +54 -0
- package/src/core/device-install/operations/webusb.ts +105 -0
- package/src/core/device-install/storage/browser-storage.ts +263 -0
- package/src/core/device-install/storage/index.ts +1 -0
- package/src/core/device-install/types.ts +65 -0
- package/src/device-install/index.ts +3 -0
- package/src/device-install/react.ts +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-device-install.ts +1210 -0
- package/src/index.ts +2 -0
- package/vite.config.ts +6 -2
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type { DeviceHello, DeviceInstallLog, PairRecordPayload } from '../types';
|
|
2
|
+
import { decodeFrame, decodeJson, encodeFrame, encodeJson, RelayMessageType } from './relay-protocol';
|
|
3
|
+
import {
|
|
4
|
+
openStream,
|
|
5
|
+
receiveStreamData,
|
|
6
|
+
sendStreamData,
|
|
7
|
+
type UsbmuxSession,
|
|
8
|
+
type UsbmuxStream,
|
|
9
|
+
} from './usbmux';
|
|
10
|
+
|
|
11
|
+
type OpenStreamPayload = {
|
|
12
|
+
port: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ProgressPayload = {
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class RelayClient {
|
|
20
|
+
private socket?: WebSocket;
|
|
21
|
+
private streams = new Map<number, UsbmuxStream>();
|
|
22
|
+
private frameQueue = Promise.resolve();
|
|
23
|
+
private closed = false;
|
|
24
|
+
private pairRecordWaiter?: {
|
|
25
|
+
resolve: (record: PairRecordPayload) => void;
|
|
26
|
+
reject: (error: Error) => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly webSocketUrl: string,
|
|
31
|
+
private readonly session: UsbmuxSession,
|
|
32
|
+
private readonly deviceHello: DeviceHello,
|
|
33
|
+
private readonly log: DeviceInstallLog,
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
async connect() {
|
|
37
|
+
const socket = new WebSocket(this.webSocketUrl);
|
|
38
|
+
socket.binaryType = 'arraybuffer';
|
|
39
|
+
this.socket = socket;
|
|
40
|
+
socket.onclose = () => {
|
|
41
|
+
this.closed = true;
|
|
42
|
+
this.log('Relay socket closed');
|
|
43
|
+
if (this.pairRecordWaiter) {
|
|
44
|
+
this.pairRecordWaiter.reject(new Error('Relay socket closed'));
|
|
45
|
+
this.pairRecordWaiter = undefined;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
await new Promise<void>((resolve, reject) => {
|
|
49
|
+
socket.onopen = () => resolve();
|
|
50
|
+
socket.onerror = () => reject(new Error('WebSocket connection failed'));
|
|
51
|
+
});
|
|
52
|
+
socket.onmessage = (event) => {
|
|
53
|
+
const data = event.data instanceof ArrayBuffer ? new Uint8Array(event.data) : new Uint8Array();
|
|
54
|
+
this.enqueueFrame(data);
|
|
55
|
+
};
|
|
56
|
+
await this.send({
|
|
57
|
+
type: RelayMessageType.DeviceHello,
|
|
58
|
+
requestId: 0,
|
|
59
|
+
streamId: 0,
|
|
60
|
+
payload: encodeJson(this.deviceHello),
|
|
61
|
+
});
|
|
62
|
+
this.log('Connected WebSocket relay');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async startPairing() {
|
|
66
|
+
const recordPromise = new Promise<PairRecordPayload>((resolve, reject) => {
|
|
67
|
+
this.pairRecordWaiter = { resolve, reject };
|
|
68
|
+
});
|
|
69
|
+
await this.send({
|
|
70
|
+
type: RelayMessageType.StartPairing,
|
|
71
|
+
requestId: 0,
|
|
72
|
+
streamId: 0,
|
|
73
|
+
payload: encodeJson({}),
|
|
74
|
+
});
|
|
75
|
+
this.log('Pairing requested');
|
|
76
|
+
return recordPromise;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async startInstall(pairRecord: PairRecordPayload) {
|
|
80
|
+
await this.send({
|
|
81
|
+
type: RelayMessageType.StartInstall,
|
|
82
|
+
requestId: 0,
|
|
83
|
+
streamId: 0,
|
|
84
|
+
payload: encodeJson(pairRecord),
|
|
85
|
+
});
|
|
86
|
+
this.log('Installation requested');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
close() {
|
|
90
|
+
this.closed = true;
|
|
91
|
+
this.socket?.close();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private enqueueFrame(data: Uint8Array) {
|
|
95
|
+
this.frameQueue = this.frameQueue.then(() => this.handleFrame(decodeFrame(data))).catch((error) => {
|
|
96
|
+
this.log('Relay frame handling failed', error instanceof Error ? error.message : String(error));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async handleFrame(frame: ReturnType<typeof decodeFrame>) {
|
|
101
|
+
switch (frame.type) {
|
|
102
|
+
case RelayMessageType.OpenStream:
|
|
103
|
+
await this.handleOpenStream(
|
|
104
|
+
frame.requestId,
|
|
105
|
+
frame.streamId,
|
|
106
|
+
decodeJson<OpenStreamPayload>(frame.payload).port,
|
|
107
|
+
);
|
|
108
|
+
break;
|
|
109
|
+
case RelayMessageType.StreamData: {
|
|
110
|
+
const stream = this.streams.get(frame.streamId);
|
|
111
|
+
if (!stream) throw new Error(`Unknown stream ${frame.streamId}`);
|
|
112
|
+
await sendStreamData(stream, frame.payload);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case RelayMessageType.StreamClose:
|
|
116
|
+
this.streams.delete(frame.streamId);
|
|
117
|
+
break;
|
|
118
|
+
case RelayMessageType.InstallProgress:
|
|
119
|
+
this.log(formatInstallProgress(decodeJson<ProgressPayload>(frame.payload).message));
|
|
120
|
+
break;
|
|
121
|
+
case RelayMessageType.Error:
|
|
122
|
+
this.handleError(frame.payload);
|
|
123
|
+
break;
|
|
124
|
+
case RelayMessageType.PairRecordReady:
|
|
125
|
+
this.handlePairRecordReady(decodeJson<PairRecordPayload>(frame.payload));
|
|
126
|
+
break;
|
|
127
|
+
case RelayMessageType.Ping:
|
|
128
|
+
await this.send({
|
|
129
|
+
type: RelayMessageType.Pong,
|
|
130
|
+
requestId: frame.requestId,
|
|
131
|
+
streamId: 0,
|
|
132
|
+
payload: new Uint8Array(),
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private handleError(payload: Uint8Array) {
|
|
139
|
+
const message = decodeServerError(payload);
|
|
140
|
+
this.log('Server error', message);
|
|
141
|
+
if (this.pairRecordWaiter) {
|
|
142
|
+
this.pairRecordWaiter.reject(new Error(message));
|
|
143
|
+
this.pairRecordWaiter = undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private handlePairRecordReady(record: PairRecordPayload) {
|
|
148
|
+
this.log('Pair record received', record.udid);
|
|
149
|
+
this.pairRecordWaiter?.resolve(record);
|
|
150
|
+
this.pairRecordWaiter = undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async handleOpenStream(requestId: number, streamId: number, port: number) {
|
|
154
|
+
try {
|
|
155
|
+
const stream = await openStream(this.session, port);
|
|
156
|
+
this.streams.set(streamId, stream);
|
|
157
|
+
await this.send({
|
|
158
|
+
type: RelayMessageType.OpenResult,
|
|
159
|
+
requestId,
|
|
160
|
+
streamId,
|
|
161
|
+
payload: encodeJson({ ok: true }),
|
|
162
|
+
});
|
|
163
|
+
this.log(`Opened device stream ${streamId} to port ${port}`);
|
|
164
|
+
void this.pumpDeviceToServer(streamId, stream);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
this.log(`Open device stream ${streamId} failed`, error instanceof Error ? error.message : String(error));
|
|
167
|
+
await this.send({
|
|
168
|
+
type: RelayMessageType.OpenResult,
|
|
169
|
+
requestId,
|
|
170
|
+
streamId,
|
|
171
|
+
payload: encodeJson({
|
|
172
|
+
ok: false,
|
|
173
|
+
error: error instanceof Error ? error.message : String(error),
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async pumpDeviceToServer(streamId: number, stream: UsbmuxStream) {
|
|
180
|
+
try {
|
|
181
|
+
for (;;) {
|
|
182
|
+
const data = await receiveStreamData(stream);
|
|
183
|
+
if (this.closed) return;
|
|
184
|
+
await this.send({
|
|
185
|
+
type: RelayMessageType.StreamData,
|
|
186
|
+
requestId: 0,
|
|
187
|
+
streamId,
|
|
188
|
+
payload: data,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
this.log(`Device stream ${streamId} closed`, error instanceof Error ? error.message : String(error));
|
|
193
|
+
await this.send({
|
|
194
|
+
type: RelayMessageType.StreamClose,
|
|
195
|
+
requestId: 0,
|
|
196
|
+
streamId,
|
|
197
|
+
payload: encodeJson({
|
|
198
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private async send(frame: Parameters<typeof encodeFrame>[0]) {
|
|
205
|
+
if (this.closed) return;
|
|
206
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
207
|
+
this.closed = true;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.socket.send(encodeFrame(frame));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function decodeServerError(payload: Uint8Array) {
|
|
215
|
+
const text = new TextDecoder().decode(payload);
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(text) as { error?: string };
|
|
218
|
+
return userFacingServerError(parsed.error ?? text);
|
|
219
|
+
} catch {
|
|
220
|
+
return userFacingServerError(text);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function userFacingServerError(message: string) {
|
|
225
|
+
return message.replace(/libimobiledevice/g, '').trim();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function formatInstallProgress(message: string) {
|
|
229
|
+
const prefix = 'install status: ';
|
|
230
|
+
if (!message.startsWith(prefix)) {
|
|
231
|
+
return message;
|
|
232
|
+
}
|
|
233
|
+
const xml = message.slice(prefix.length);
|
|
234
|
+
const doc = new DOMParser().parseFromString(xml, 'application/xml');
|
|
235
|
+
const dict = doc.querySelector('plist > dict');
|
|
236
|
+
if (!dict) {
|
|
237
|
+
return message;
|
|
238
|
+
}
|
|
239
|
+
const values = readPlistDict(dict);
|
|
240
|
+
const status = values.Status ?? 'Unknown';
|
|
241
|
+
const percent = values.PercentComplete ? `${values.PercentComplete}% ` : '';
|
|
242
|
+
return `Install progress: ${percent}${status}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function readPlistDict(dict: Element) {
|
|
246
|
+
const result: Record<string, string> = {};
|
|
247
|
+
const children = Array.from(dict.children);
|
|
248
|
+
for (let index = 0; index < children.length; index += 2) {
|
|
249
|
+
const key = children[index];
|
|
250
|
+
const value = children[index + 1];
|
|
251
|
+
if (!key || key.tagName !== 'key' || !value) continue;
|
|
252
|
+
result[key.textContent ?? ''] = value.textContent ?? '';
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const RELAY_PROTOCOL_VERSION = 1;
|
|
2
|
+
export const RELAY_HEADER_BYTES = 16;
|
|
3
|
+
|
|
4
|
+
export const RelayMessageType = {
|
|
5
|
+
DeviceHello: 1,
|
|
6
|
+
OpenStream: 2,
|
|
7
|
+
OpenResult: 3,
|
|
8
|
+
StreamData: 4,
|
|
9
|
+
StreamClose: 5,
|
|
10
|
+
InstallProgress: 6,
|
|
11
|
+
Error: 7,
|
|
12
|
+
Ping: 8,
|
|
13
|
+
Pong: 9,
|
|
14
|
+
StartPairing: 10,
|
|
15
|
+
StartInstall: 11,
|
|
16
|
+
PairRecordReady: 12,
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export type RelayMessageType = (typeof RelayMessageType)[keyof typeof RelayMessageType];
|
|
20
|
+
|
|
21
|
+
export type RelayFrame = {
|
|
22
|
+
type: RelayMessageType;
|
|
23
|
+
requestId: number;
|
|
24
|
+
streamId: number;
|
|
25
|
+
payload: Uint8Array;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function encodeFrame(frame: RelayFrame) {
|
|
29
|
+
const result = new Uint8Array(RELAY_HEADER_BYTES + frame.payload.byteLength);
|
|
30
|
+
const view = new DataView(result.buffer);
|
|
31
|
+
view.setUint8(0, RELAY_PROTOCOL_VERSION);
|
|
32
|
+
view.setUint8(1, frame.type);
|
|
33
|
+
view.setUint8(2, 0);
|
|
34
|
+
view.setUint8(3, 0);
|
|
35
|
+
view.setUint32(4, frame.requestId);
|
|
36
|
+
view.setUint32(8, frame.streamId);
|
|
37
|
+
view.setUint32(12, frame.payload.byteLength);
|
|
38
|
+
result.set(frame.payload, RELAY_HEADER_BYTES);
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function decodeFrame(data: Uint8Array): RelayFrame {
|
|
43
|
+
if (data.byteLength < RELAY_HEADER_BYTES) {
|
|
44
|
+
throw new Error(`Relay frame too short: ${data.byteLength}`);
|
|
45
|
+
}
|
|
46
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
47
|
+
const version = view.getUint8(0);
|
|
48
|
+
if (version !== RELAY_PROTOCOL_VERSION) {
|
|
49
|
+
throw new Error(`Unsupported relay protocol version ${version}`);
|
|
50
|
+
}
|
|
51
|
+
const payloadLength = view.getUint32(12);
|
|
52
|
+
if (data.byteLength !== RELAY_HEADER_BYTES + payloadLength) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Relay frame length mismatch: got ${data.byteLength}, expected ${RELAY_HEADER_BYTES + payloadLength}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
type: view.getUint8(1) as RelayMessageType,
|
|
59
|
+
requestId: view.getUint32(4),
|
|
60
|
+
streamId: view.getUint32(8),
|
|
61
|
+
payload: data.slice(RELAY_HEADER_BYTES),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function encodeJson(value: unknown) {
|
|
66
|
+
return new TextEncoder().encode(JSON.stringify(value));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function decodeJson<T>(payload: Uint8Array): T {
|
|
70
|
+
return JSON.parse(new TextDecoder().decode(payload)) as T;
|
|
71
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/// <reference path="./webusb-dom.d.ts" />
|
|
2
|
+
|
|
3
|
+
import type { UsbmuxCandidate } from './webusb';
|
|
4
|
+
import { getBulkEndpoints, transferIn, transferOutWithZlp } from './webusb';
|
|
5
|
+
|
|
6
|
+
const PROTO_VERSION = 0;
|
|
7
|
+
const PROTO_SETUP = 2;
|
|
8
|
+
const PROTO_TCP = 6;
|
|
9
|
+
const MAGIC = 0xfeedface;
|
|
10
|
+
const FLAG_SYN = 0x02;
|
|
11
|
+
const FLAG_RST = 0x04;
|
|
12
|
+
const FLAG_ACK = 0x10;
|
|
13
|
+
const FIRST_SPORT = 49152;
|
|
14
|
+
|
|
15
|
+
export type UsbmuxSession = {
|
|
16
|
+
device: USBDevice;
|
|
17
|
+
candidate: UsbmuxCandidate;
|
|
18
|
+
inEndpoint: ReturnType<typeof getBulkEndpoints>['inEndpoint'];
|
|
19
|
+
outEndpoint: ReturnType<typeof getBulkEndpoints>['outEndpoint'];
|
|
20
|
+
muxVersion: number;
|
|
21
|
+
txSeq: number;
|
|
22
|
+
rxSeq: number;
|
|
23
|
+
nextSport: number;
|
|
24
|
+
streams: Map<string, UsbmuxStream>;
|
|
25
|
+
writeChain: Promise<void>;
|
|
26
|
+
closed: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type UsbmuxStream = {
|
|
30
|
+
session: UsbmuxSession;
|
|
31
|
+
sport: number;
|
|
32
|
+
dport: number;
|
|
33
|
+
seq: number;
|
|
34
|
+
ack: number;
|
|
35
|
+
queue: Uint8Array[];
|
|
36
|
+
waiters: Array<(value: Uint8Array) => void>;
|
|
37
|
+
error?: Error;
|
|
38
|
+
opened: Promise<void>;
|
|
39
|
+
resolveOpened: () => void;
|
|
40
|
+
rejectOpened: (error: Error) => void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export async function createUsbmuxSession(device: USBDevice, candidate: UsbmuxCandidate) {
|
|
44
|
+
const { inEndpoint, outEndpoint } = getBulkEndpoints(candidate);
|
|
45
|
+
const versionPayload = new Uint8Array(12);
|
|
46
|
+
const versionView = new DataView(versionPayload.buffer);
|
|
47
|
+
versionView.setUint32(0, 2);
|
|
48
|
+
versionView.setUint32(4, 0);
|
|
49
|
+
versionView.setUint32(8, 0);
|
|
50
|
+
await transferOutWithZlp(device, outEndpoint, buildV1Packet(PROTO_VERSION, versionPayload));
|
|
51
|
+
const versionPacket = await readPacket(device, inEndpoint, 1);
|
|
52
|
+
const version = new DataView(versionPacket.payload.buffer, versionPacket.payload.byteOffset).getUint32(0);
|
|
53
|
+
const session: UsbmuxSession = {
|
|
54
|
+
device,
|
|
55
|
+
candidate,
|
|
56
|
+
inEndpoint,
|
|
57
|
+
outEndpoint,
|
|
58
|
+
muxVersion: version,
|
|
59
|
+
txSeq: 0,
|
|
60
|
+
rxSeq: 0xffff,
|
|
61
|
+
nextSport: FIRST_SPORT,
|
|
62
|
+
streams: new Map(),
|
|
63
|
+
writeChain: Promise.resolve(),
|
|
64
|
+
closed: false,
|
|
65
|
+
};
|
|
66
|
+
if (version >= 2) {
|
|
67
|
+
await sendMux(session, PROTO_SETUP, new Uint8Array([0x07]));
|
|
68
|
+
}
|
|
69
|
+
void readLoop(session);
|
|
70
|
+
return session;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function openStream(session: UsbmuxSession, port: number) {
|
|
74
|
+
if (session.closed) {
|
|
75
|
+
throw new Error('usbmux session is closed.');
|
|
76
|
+
}
|
|
77
|
+
let resolveOpened!: () => void;
|
|
78
|
+
let rejectOpened!: (error: Error) => void;
|
|
79
|
+
const stream: UsbmuxStream = {
|
|
80
|
+
session,
|
|
81
|
+
sport: session.nextSport++,
|
|
82
|
+
dport: port,
|
|
83
|
+
seq: 0,
|
|
84
|
+
ack: 0,
|
|
85
|
+
queue: [],
|
|
86
|
+
waiters: [],
|
|
87
|
+
opened: new Promise<void>((resolve, reject) => {
|
|
88
|
+
resolveOpened = resolve;
|
|
89
|
+
rejectOpened = reject;
|
|
90
|
+
}),
|
|
91
|
+
resolveOpened,
|
|
92
|
+
rejectOpened,
|
|
93
|
+
};
|
|
94
|
+
session.streams.set(streamKey(port, stream.sport), stream);
|
|
95
|
+
await sendTcp(stream, FLAG_SYN, new Uint8Array());
|
|
96
|
+
await stream.opened;
|
|
97
|
+
return stream;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function sendStreamData(stream: UsbmuxStream, bytes: Uint8Array) {
|
|
101
|
+
if (stream.session.closed) {
|
|
102
|
+
throw new Error('usbmux session is closed.');
|
|
103
|
+
}
|
|
104
|
+
await sendTcp(stream, FLAG_ACK, bytes);
|
|
105
|
+
stream.seq += bytes.byteLength;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function receiveStreamData(stream: UsbmuxStream) {
|
|
109
|
+
if (stream.queue.length > 0) {
|
|
110
|
+
return stream.queue.shift()!;
|
|
111
|
+
}
|
|
112
|
+
if (stream.error) {
|
|
113
|
+
throw stream.error;
|
|
114
|
+
}
|
|
115
|
+
return new Promise<Uint8Array>((resolve) => {
|
|
116
|
+
stream.waiters.push(resolve);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function closeUsbmuxSession(session: UsbmuxSession) {
|
|
121
|
+
session.closed = true;
|
|
122
|
+
for (const stream of session.streams.values()) {
|
|
123
|
+
stream.error = new Error('usbmux session closed');
|
|
124
|
+
stream.rejectOpened(stream.error);
|
|
125
|
+
while (stream.waiters.length > 0) {
|
|
126
|
+
stream.waiters.shift()!(new Uint8Array());
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
session.streams.clear();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function sendTcp(stream: UsbmuxStream, flags: number, payload: Uint8Array) {
|
|
133
|
+
const tcp = new Uint8Array(20 + payload.byteLength);
|
|
134
|
+
const view = new DataView(tcp.buffer);
|
|
135
|
+
view.setUint16(0, stream.sport);
|
|
136
|
+
view.setUint16(2, stream.dport);
|
|
137
|
+
view.setUint32(4, stream.seq);
|
|
138
|
+
view.setUint32(8, stream.ack);
|
|
139
|
+
view.setUint8(12, 0x50);
|
|
140
|
+
view.setUint8(13, flags);
|
|
141
|
+
view.setUint16(14, 512);
|
|
142
|
+
tcp.set(payload, 20);
|
|
143
|
+
await sendMux(stream.session, PROTO_TCP, tcp);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function sendMux(session: UsbmuxSession, protocol: number, payload: Uint8Array) {
|
|
147
|
+
if (session.closed) {
|
|
148
|
+
throw new Error('usbmux session is closed.');
|
|
149
|
+
}
|
|
150
|
+
session.writeChain = session.writeChain.then(async () => {
|
|
151
|
+
if (session.closed) return;
|
|
152
|
+
const packet =
|
|
153
|
+
session.muxVersion >= 2
|
|
154
|
+
? buildV2Packet(protocol, payload, session.txSeq++, session.rxSeq)
|
|
155
|
+
: buildV1Packet(protocol, payload);
|
|
156
|
+
await transferOutWithZlp(session.device, session.outEndpoint, packet);
|
|
157
|
+
});
|
|
158
|
+
return session.writeChain;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function readLoop(session: UsbmuxSession) {
|
|
162
|
+
try {
|
|
163
|
+
for (;;) {
|
|
164
|
+
if (session.closed) return;
|
|
165
|
+
const packet = await readPacket(session.device, session.inEndpoint, session.muxVersion);
|
|
166
|
+
if (session.closed) return;
|
|
167
|
+
if (packet.rxSeq !== undefined) {
|
|
168
|
+
session.rxSeq = packet.rxSeq;
|
|
169
|
+
}
|
|
170
|
+
if (packet.protocol !== PROTO_TCP) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const tcp = parseTcp(packet.payload);
|
|
174
|
+
const stream = session.streams.get(streamKey(tcp.sport, tcp.dport));
|
|
175
|
+
if (!stream) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (tcp.flags & FLAG_RST) {
|
|
179
|
+
stream.error = new Error(`Device reset stream ${stream.dport}`);
|
|
180
|
+
stream.rejectOpened(stream.error);
|
|
181
|
+
while (stream.waiters.length > 0) {
|
|
182
|
+
stream.waiters.shift()!(new Uint8Array());
|
|
183
|
+
}
|
|
184
|
+
session.streams.delete(streamKey(stream.dport, stream.sport));
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if ((tcp.flags & (FLAG_SYN | FLAG_ACK)) === (FLAG_SYN | FLAG_ACK)) {
|
|
188
|
+
stream.seq += 1;
|
|
189
|
+
stream.ack = tcp.seq + 1;
|
|
190
|
+
await sendTcp(stream, FLAG_ACK, new Uint8Array());
|
|
191
|
+
stream.resolveOpened();
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (tcp.payload.byteLength === 0) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
stream.ack = tcp.seq + tcp.payload.byteLength;
|
|
198
|
+
await sendTcp(stream, FLAG_ACK, new Uint8Array());
|
|
199
|
+
if (stream.waiters.length > 0) {
|
|
200
|
+
stream.waiters.shift()!(tcp.payload);
|
|
201
|
+
} else {
|
|
202
|
+
stream.queue.push(tcp.payload);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (!session.closed) {
|
|
207
|
+
closeUsbmuxSession(session);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function readPacket(device: USBDevice, endpoint: UsbmuxSession['inEndpoint'], version: number) {
|
|
213
|
+
for (;;) {
|
|
214
|
+
const bytes = await transferIn(device, endpoint);
|
|
215
|
+
const packet = parseMux(bytes, version);
|
|
216
|
+
if (version === 1 && packet.protocol !== PROTO_VERSION) continue;
|
|
217
|
+
return packet;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildV1Packet(protocol: number, payload: Uint8Array) {
|
|
222
|
+
const bytes = new Uint8Array(8 + payload.byteLength);
|
|
223
|
+
const view = new DataView(bytes.buffer);
|
|
224
|
+
view.setUint32(0, protocol);
|
|
225
|
+
view.setUint32(4, bytes.byteLength);
|
|
226
|
+
bytes.set(payload, 8);
|
|
227
|
+
return bytes;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function buildV2Packet(protocol: number, payload: Uint8Array, txSeq: number, rxSeq: number) {
|
|
231
|
+
const bytes = new Uint8Array(16 + payload.byteLength);
|
|
232
|
+
const view = new DataView(bytes.buffer);
|
|
233
|
+
view.setUint32(0, protocol);
|
|
234
|
+
view.setUint32(4, bytes.byteLength);
|
|
235
|
+
view.setUint32(8, MAGIC);
|
|
236
|
+
view.setUint16(12, txSeq);
|
|
237
|
+
view.setUint16(14, rxSeq);
|
|
238
|
+
bytes.set(payload, 16);
|
|
239
|
+
return bytes;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function parseMux(bytes: Uint8Array, version: number) {
|
|
243
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
244
|
+
const protocol = view.getUint32(0);
|
|
245
|
+
const length = view.getUint32(4);
|
|
246
|
+
const headerSize = version >= 2 ? 16 : 8;
|
|
247
|
+
return {
|
|
248
|
+
protocol,
|
|
249
|
+
length,
|
|
250
|
+
rxSeq: version >= 2 ? view.getUint16(14) : undefined,
|
|
251
|
+
payload: bytes.slice(headerSize, length),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function parseTcp(bytes: Uint8Array) {
|
|
256
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
257
|
+
const dataOffset = (view.getUint8(12) >> 4) * 4;
|
|
258
|
+
return {
|
|
259
|
+
sport: view.getUint16(0),
|
|
260
|
+
dport: view.getUint16(2),
|
|
261
|
+
seq: view.getUint32(4),
|
|
262
|
+
ack: view.getUint32(8),
|
|
263
|
+
flags: view.getUint8(13),
|
|
264
|
+
payload: bytes.slice(dataOffset),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function streamKey(devicePort: number, hostPort: number) {
|
|
269
|
+
return `${devicePort}:${hostPort}`;
|
|
270
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
type USBTransferStatus = 'ok' | 'stall' | 'babble';
|
|
2
|
+
|
|
3
|
+
interface USBInTransferResult {
|
|
4
|
+
data?: DataView;
|
|
5
|
+
status: USBTransferStatus;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface USBOutTransferResult {
|
|
9
|
+
bytesWritten: number;
|
|
10
|
+
status: USBTransferStatus;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface USBDevice {
|
|
14
|
+
vendorId: number;
|
|
15
|
+
productId: number;
|
|
16
|
+
productName?: string;
|
|
17
|
+
manufacturerName?: string;
|
|
18
|
+
serialNumber?: string;
|
|
19
|
+
opened: boolean;
|
|
20
|
+
configuration: { configurationValue: number } | null;
|
|
21
|
+
configurations: Array<{
|
|
22
|
+
configurationValue: number;
|
|
23
|
+
interfaces: Array<{
|
|
24
|
+
interfaceNumber: number;
|
|
25
|
+
alternates: Array<{
|
|
26
|
+
alternateSetting: number;
|
|
27
|
+
interfaceClass: number;
|
|
28
|
+
interfaceSubclass: number;
|
|
29
|
+
interfaceProtocol: number;
|
|
30
|
+
endpoints: Array<{
|
|
31
|
+
endpointNumber: number;
|
|
32
|
+
direction: 'in' | 'out';
|
|
33
|
+
type: string;
|
|
34
|
+
packetSize: number;
|
|
35
|
+
}>;
|
|
36
|
+
}>;
|
|
37
|
+
}>;
|
|
38
|
+
}>;
|
|
39
|
+
open(): Promise<void>;
|
|
40
|
+
close(): Promise<void>;
|
|
41
|
+
selectConfiguration(configurationValue: number): Promise<void>;
|
|
42
|
+
claimInterface(interfaceNumber: number): Promise<void>;
|
|
43
|
+
releaseInterface(interfaceNumber: number): Promise<void>;
|
|
44
|
+
selectAlternateInterface(interfaceNumber: number, alternateSetting: number): Promise<void>;
|
|
45
|
+
transferIn(endpointNumber: number, length: number): Promise<USBInTransferResult>;
|
|
46
|
+
transferOut(endpointNumber: number, data: Uint8Array): Promise<USBOutTransferResult>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Navigator {
|
|
50
|
+
usb?: {
|
|
51
|
+
requestDevice(options: { filters: Array<{ vendorId: number }> }): Promise<USBDevice>;
|
|
52
|
+
getDevices(): Promise<USBDevice[]>;
|
|
53
|
+
};
|
|
54
|
+
}
|