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