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          // 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