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