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 { AFCClient } from './client/AFCClient'; 13import { DebugserverClient } from './client/DebugserverClient'; 14import { InstallationProxyClient } from './client/InstallationProxyClient'; 15import { LockdowndClient } from './client/LockdowndClient'; 16import { MobileImageMounterClient } from './client/MobileImageMounterClient'; 17import { ServiceClient } from './client/ServiceClient'; 18import { UsbmuxdClient, UsbmuxdDevice, UsbmuxdPairRecord } from './client/UsbmuxdClient'; 19import { CommandError } from '../../../utils/errors'; 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 // Avoid using `secureProtocol` fixing the socket to a single TLS version. 100 // Newer Node versions might not support older TLS versions. 101 // By using the default `minVersion` and `maxVersion` options, 102 // The socket will automatically use the appropriate TLS version. 103 // See: https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions 104 cert: this.pairRecord.RootCertificate, 105 key: this.pairRecord.RootPrivateKey, 106 }), 107 }; 108 109 // Some services seem to not support TLS/SSL after the initial handshake 110 // More info: https://github.com/libimobiledevice/libimobiledevice/issues/793 111 if (disableSSL) { 112 // According to https://nodejs.org/api/tls.html#tls_tls_connect_options_callback we can 113 // pass any Duplex in to tls.connect instead of a Socket. So we'll use our proxy to keep 114 // the TLS wrapper and underlying usbmuxd socket separate. 115 const proxy: any = new UsbmuxdProxy(usbmuxdSocket); 116 tlsOptions.socket = proxy; 117 118 await new Promise<void>((resolve, reject) => { 119 const timeoutId = setTimeout(() => { 120 reject( 121 new CommandError('APPLE_DEVICE', 'The TLS handshake failed to complete after 5s.') 122 ); 123 }, 5000); 124 tls.connect(tlsOptions, function (this: tls.TLSSocket) { 125 clearTimeout(timeoutId); 126 // After the handshake, we don't need TLS or the proxy anymore, 127 // since we'll just pass in the naked usbmuxd socket to the service client 128 this.destroy(); 129 resolve(); 130 }); 131 }); 132 } else { 133 tlsOptions.socket = usbmuxdSocket; 134 usbmuxdSocket = tls.connect(tlsOptions); 135 } 136 } 137 const client = new ServiceType(usbmuxdSocket); 138 this.connections.push(client.socket); 139 return client; 140 } 141 142 end() { 143 for (const socket of this.connections) { 144 // may already be closed 145 try { 146 socket.end(); 147 } catch {} 148 } 149 } 150} 151 152class UsbmuxdProxy extends Duplex { 153 constructor(private usbmuxdSock: Socket) { 154 super(); 155 156 this.usbmuxdSock.on('data', (data) => { 157 this.push(data); 158 }); 159 } 160 161 _write(chunk: any, encoding: string, callback: (err?: Error) => void) { 162 this.usbmuxdSock.write(chunk); 163 callback(); 164 } 165 166 _read(size: number) { 167 // Stub so we don't error, since we push everything we get from usbmuxd as it comes in. 168 // TODO: better way to do this? 169 } 170 171 _destroy() { 172 this.usbmuxdSock.removeAllListeners(); 173 } 174} 175