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