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