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 { Socket } from 'net'; 9import { Duplex } from 'stream'; 10import * as tls from 'tls'; 11 12import { CommandError } from '../../../utils/errors'; 13import { AFCClient } from './client/AFCClient'; 14import { DebugserverClient } from './client/DebugserverClient'; 15import { InstallationProxyClient } from './client/InstallationProxyClient'; 16import { LockdowndClient } from './client/LockdowndClient'; 17import { MobileImageMounterClient } from './client/MobileImageMounterClient'; 18import { ServiceClient } from './client/ServiceClient'; 19import { UsbmuxdClient, UsbmuxdDevice, UsbmuxdPairRecord } from './client/UsbmuxdClient'; 20 21export class ClientManager { 22 private connections: Socket[]; 23 constructor( 24 public pairRecord: UsbmuxdPairRecord, 25 public device: UsbmuxdDevice, 26 private lockdowndClient: LockdowndClient 27 ) { 28 this.connections = [lockdowndClient.socket]; 29 } 30 31 static async create(udid?: string) { 32 const usbmuxClient = new UsbmuxdClient(UsbmuxdClient.connectUsbmuxdSocket()); 33 const device = await usbmuxClient.getDevice(udid); 34 const pairRecord = await usbmuxClient.readPairRecord(device.Properties.SerialNumber); 35 const lockdownSocket = await usbmuxClient.connect(device, 62078); 36 const lockdownClient = new LockdowndClient(lockdownSocket); 37 await lockdownClient.doHandshake(pairRecord); 38 return new ClientManager(pairRecord, device, lockdownClient); 39 } 40 41 async getUsbmuxdClient() { 42 const usbmuxClient = new UsbmuxdClient(UsbmuxdClient.connectUsbmuxdSocket()); 43 this.connections.push(usbmuxClient.socket); 44 return usbmuxClient; 45 } 46 47 async getLockdowndClient() { 48 const usbmuxClient = new UsbmuxdClient(UsbmuxdClient.connectUsbmuxdSocket()); 49 const lockdownSocket = await usbmuxClient.connect(this.device, 62078); 50 const lockdownClient = new LockdowndClient(lockdownSocket); 51 this.connections.push(lockdownClient.socket); 52 return lockdownClient; 53 } 54 55 async getLockdowndClientWithHandshake() { 56 const lockdownClient = await this.getLockdowndClient(); 57 await lockdownClient.doHandshake(this.pairRecord); 58 return lockdownClient; 59 } 60 61 async getAFCClient() { 62 return this.getServiceClient('com.apple.afc', AFCClient); 63 } 64 65 async getInstallationProxyClient() { 66 return this.getServiceClient('com.apple.mobile.installation_proxy', InstallationProxyClient); 67 } 68 69 async getMobileImageMounterClient() { 70 return this.getServiceClient('com.apple.mobile.mobile_image_mounter', MobileImageMounterClient); 71 } 72 73 async getDebugserverClient() { 74 try { 75 // iOS 14 added support for a secure debug service so try to connect to that first 76 return await this.getServiceClient( 77 'com.apple.debugserver.DVTSecureSocketProxy', 78 DebugserverClient 79 ); 80 } catch { 81 // otherwise, fall back to the previous implementation 82 return this.getServiceClient('com.apple.debugserver', DebugserverClient, true); 83 } 84 } 85 86 private async getServiceClient<T extends ServiceClient<any>>( 87 name: string, 88 ServiceType: new (...args: any[]) => T, 89 disableSSL = false 90 ) { 91 const { port: servicePort, enableServiceSSL } = await this.lockdowndClient.startService(name); 92 const usbmuxClient = new UsbmuxdClient(UsbmuxdClient.connectUsbmuxdSocket()); 93 let usbmuxdSocket = await usbmuxClient.connect(this.device, servicePort); 94 95 if (enableServiceSSL) { 96 const tlsOptions: tls.ConnectionOptions = { 97 rejectUnauthorized: false, 98 secureContext: tls.createSecureContext({ 99 secureProtocol: 'TLSv1_method', 100 cert: this.pairRecord.RootCertificate, 101 key: this.pairRecord.RootPrivateKey, 102 }), 103 }; 104 105 // Some services seem to not support TLS/SSL after the initial handshake 106 // More info: https://github.com/libimobiledevice/libimobiledevice/issues/793 107 if (disableSSL) { 108 // According to https://nodejs.org/api/tls.html#tls_tls_connect_options_callback we can 109 // pass any Duplex in to tls.connect instead of a Socket. So we'll use our proxy to keep 110 // the TLS wrapper and underlying usbmuxd socket separate. 111 const proxy: any = new UsbmuxdProxy(usbmuxdSocket); 112 tlsOptions.socket = proxy; 113 114 await new Promise<void>((resolve, reject) => { 115 const timeoutId = setTimeout(() => { 116 reject( 117 new CommandError('APPLE_DEVICE', 'The TLS handshake failed to complete after 5s.') 118 ); 119 }, 5000); 120 tls.connect(tlsOptions, function (this: tls.TLSSocket) { 121 clearTimeout(timeoutId); 122 // After the handshake, we don't need TLS or the proxy anymore, 123 // since we'll just pass in the naked usbmuxd socket to the service client 124 this.destroy(); 125 resolve(); 126 }); 127 }); 128 } else { 129 tlsOptions.socket = usbmuxdSocket; 130 usbmuxdSocket = tls.connect(tlsOptions); 131 } 132 } 133 const client = new ServiceType(usbmuxdSocket); 134 this.connections.push(client.socket); 135 return client; 136 } 137 138 end() { 139 for (const socket of this.connections) { 140 // may already be closed 141 try { 142 socket.end(); 143 } catch {} 144 } 145 } 146} 147 148class UsbmuxdProxy extends Duplex { 149 constructor(private usbmuxdSock: Socket) { 150 super(); 151 152 this.usbmuxdSock.on('data', (data) => { 153 this.push(data); 154 }); 155 } 156 157 _write(chunk: any, encoding: string, callback: (err?: Error) => void) { 158 this.usbmuxdSock.write(chunk); 159 callback(); 160 } 161 162 _read(size: number) { 163 // Stub so we don't error, since we push everything we get from usbmuxd as it comes in. 164 // TODO: better way to do this? 165 } 166 167 _destroy() { 168 this.usbmuxdSock.removeAllListeners(); 169 } 170} 171