1/**
2 * Copyright (c) 2021 Expo, Inc.
3 * Copyright (c) 2018 Drifty Co.
4 *
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8import plist from '@expo/plist';
9import Debug from 'debug';
10import { Socket, connect } from 'net';
11
12import { ResponseError, ServiceClient } from './ServiceClient';
13import { CommandError } from '../../../../utils/errors';
14import { parsePlistBuffer } from '../../../../utils/plist';
15import { UsbmuxProtocolClient } from '../protocol/UsbmuxProtocol';
16
17const debug = Debug('expo:apple-device:client:usbmuxd');
18
19export interface UsbmuxdDeviceProperties {
20  /** @example 'USB' */
21  ConnectionType: 'USB' | 'Network';
22  /** @example 7 */
23  DeviceID: number;
24  /** @example 339738624 */
25  LocationID?: number;
26  /** @example '00008101-001964A22629003A' */
27  SerialNumber: string;
28  /**
29   * Only available for USB connection.
30   * @example 480000000
31   */
32  ConnectionSpeed?: number;
33  /**
34   * Only available for USB connection.
35   * @example 4776
36   */
37  ProductID?: number;
38  /**
39   * Only available for USB connection.
40   * @example '00008101-001964A22629003A'
41   */
42  UDID?: string;
43  /**
44   * Only available for USB connection.
45   * @example '00008101001964A22629003A'
46   */
47  USBSerialNumber?: string;
48  /**
49   * Only available for Network connection.
50   * @example '08:c7:29:05:f2:30@fe80::ac7:29ff:fe05:f230-supportsRP._apple-mobdev2._tcp.local.'
51   */
52  EscapedFullServiceName?: string;
53  /**
54   * Only available for Network connection.
55   * @example 5
56   */
57  InterfaceIndex?: number;
58  /**
59   * Only available for Network connection.
60   */
61  NetworkAddress?: Buffer;
62}
63
64export interface UsbmuxdDevice {
65  /** @example 7 */
66  DeviceID: number;
67  MessageType: 'Attached'; // TODO: what else?
68  Properties: UsbmuxdDeviceProperties;
69}
70
71export interface UsbmuxdConnectResponse {
72  MessageType: 'Result';
73  Number: number;
74}
75
76export interface UsbmuxdDeviceResponse {
77  DeviceList: UsbmuxdDevice[];
78}
79
80export interface UsbmuxdPairRecordResponse {
81  PairRecordData: Buffer;
82}
83
84export interface UsbmuxdPairRecord {
85  DeviceCertificate: Buffer;
86  EscrowBag: Buffer;
87  HostCertificate: Buffer;
88  HostID: string;
89  HostPrivateKey: Buffer;
90  RootCertificate: Buffer;
91  RootPrivateKey: Buffer;
92  SystemBUID: string;
93  WiFiMACAddress: string;
94}
95
96function isUsbmuxdConnectResponse(resp: any): resp is UsbmuxdConnectResponse {
97  return resp.MessageType === 'Result' && resp.Number !== undefined;
98}
99
100function isUsbmuxdDeviceResponse(resp: any): resp is UsbmuxdDeviceResponse {
101  return resp.DeviceList !== undefined;
102}
103
104function isUsbmuxdPairRecordResponse(resp: any): resp is UsbmuxdPairRecordResponse {
105  return resp.PairRecordData !== undefined;
106}
107
108export class UsbmuxdClient extends ServiceClient<UsbmuxProtocolClient> {
109  constructor(public socket: Socket) {
110    super(socket, new UsbmuxProtocolClient(socket));
111  }
112
113  static connectUsbmuxdSocket(): Socket {
114    debug('connectUsbmuxdSocket');
115    if (process.platform === 'win32') {
116      return connect({ port: 27015, host: 'localhost' });
117    } else {
118      return connect({ path: '/var/run/usbmuxd' });
119    }
120  }
121
122  async connect(device: Pick<UsbmuxdDevice, 'DeviceID'>, port: number): Promise<Socket> {
123    debug(`connect: ${device.DeviceID} on port ${port}`);
124    debug(`connect:device: %O`, device);
125
126    const response = await this.protocolClient.sendMessage({
127      messageType: 'Connect',
128      extraFields: {
129        DeviceID: device.DeviceID,
130        PortNumber: htons(port),
131      },
132    });
133    debug(`connect:device:response: %O`, response);
134
135    if (isUsbmuxdConnectResponse(response) && response.Number === 0) {
136      return this.protocolClient.socket;
137    } else {
138      throw new ResponseError(
139        `There was an error connecting to the USB connected device (id: ${device.DeviceID}, port: ${port})`,
140        response
141      );
142    }
143  }
144
145  async getDevices(): Promise<UsbmuxdDevice[]> {
146    debug('getDevices');
147
148    const resp = await this.protocolClient.sendMessage({
149      messageType: 'ListDevices',
150    });
151
152    if (isUsbmuxdDeviceResponse(resp)) {
153      return resp.DeviceList;
154    } else {
155      throw new ResponseError('Invalid response from getDevices', resp);
156    }
157  }
158
159  async getDevice(udid?: string): Promise<UsbmuxdDevice> {
160    debug(`getDevice ${udid ? 'udid: ' + udid : ''}`);
161    const devices = await this.getDevices();
162
163    if (!devices.length) {
164      throw new CommandError('APPLE_DEVICE_USBMUXD', 'No devices found');
165    }
166
167    if (!udid) {
168      return devices[0];
169    }
170
171    for (const device of devices) {
172      if (device.Properties && device.Properties.SerialNumber === udid) {
173        return device;
174      }
175    }
176
177    throw new CommandError('APPLE_DEVICE_USBMUXD', `No device found (udid: ${udid})`);
178  }
179
180  async readPairRecord(udid: string): Promise<UsbmuxdPairRecord> {
181    debug(`readPairRecord: ${udid}`);
182
183    const resp = await this.protocolClient.sendMessage({
184      messageType: 'ReadPairRecord',
185      extraFields: { PairRecordID: udid },
186    });
187
188    if (isUsbmuxdPairRecordResponse(resp)) {
189      // the pair record can be created as a binary plist
190      const BPLIST_MAGIC = Buffer.from('bplist00');
191      if (BPLIST_MAGIC.compare(resp.PairRecordData, 0, 8) === 0) {
192        debug('Binary plist pair record detected.');
193        return parsePlistBuffer(resp.PairRecordData)[0];
194      } else {
195        // TODO: use parsePlistBuffer
196        return plist.parse(resp.PairRecordData.toString()) as any; // TODO: type guard
197      }
198    } else {
199      throw new ResponseError(
200        `There was an error reading pair record for device (udid: ${udid})`,
201        resp
202      );
203    }
204  }
205}
206
207function htons(n: number): number {
208  return ((n & 0xff) << 8) | ((n >> 8) & 0xff);
209}
210