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