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