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