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 */
8
9import plist from '@expo/plist';
10import Debug from 'debug';
11import { Socket } from 'net';
12
13import { CommandError } from '../../../../utils/errors';
14import type { ProtocolWriter } from './AbstractProtocol';
15import { PlistProtocolReader, ProtocolClient, ProtocolReaderFactory } from './AbstractProtocol';
16
17const debug = Debug('expo:apple-device:protocol:lockdown');
18export const LOCKDOWN_HEADER_SIZE = 4;
19
20export interface LockdownCommand {
21  Command: string;
22  [key: string]: any;
23}
24
25export interface LockdownResponse {
26  Status: string;
27  [key: string]: any;
28}
29
30export interface LockdownErrorResponse {
31  Error: string;
32  Request?: string;
33  Service?: string;
34}
35
36export interface LockdownRequest {
37  Request: string;
38  [key: string]: any;
39}
40
41function isDefined(val: any) {
42  return typeof val !== 'undefined';
43}
44
45export function isLockdownResponse(resp: any): resp is LockdownResponse {
46  return isDefined(resp.Status);
47}
48
49export function isLockdownErrorResponse(resp: any): resp is LockdownErrorResponse {
50  return isDefined(resp.Error);
51}
52
53export class LockdownProtocolClient<
54  MessageType extends LockdownRequest | LockdownCommand = LockdownRequest
55> extends ProtocolClient<MessageType> {
56  constructor(socket: Socket) {
57    super(socket, new ProtocolReaderFactory(LockdownProtocolReader), new LockdownProtocolWriter());
58  }
59}
60
61export class LockdownProtocolReader extends PlistProtocolReader {
62  constructor(callback: (data: any) => any) {
63    super(LOCKDOWN_HEADER_SIZE, callback);
64  }
65
66  parseHeader(data: Buffer) {
67    return data.readUInt32BE(0);
68  }
69
70  parseBody(data: Buffer) {
71    const resp = super.parseBody(data);
72    debug(`Response: ${JSON.stringify(resp)}`);
73    if (isLockdownErrorResponse(resp)) {
74      if (resp.Error === 'DeviceLocked') {
75        throw new CommandError('APPLE_DEVICE_LOCKED', 'Device is currently locked.');
76      }
77
78      if (resp.Error === 'InvalidService') {
79        let errorMessage = `${resp.Error}: ${resp.Service} (request: ${resp.Request})`;
80        if (resp.Service === 'com.apple.debugserver') {
81          errorMessage +=
82            '\nTry reconnecting your device. You can also debug service logs with `export DEBUG=expo:xdl:ios:*`';
83        }
84        throw new CommandError('APPLE_DEVICE_LOCKDOWN', errorMessage);
85      }
86
87      throw new CommandError('APPLE_DEVICE_LOCKDOWN', resp.Error);
88    }
89    return resp;
90  }
91}
92
93export class LockdownProtocolWriter implements ProtocolWriter {
94  write(socket: Socket, plistData: any) {
95    debug(`socket write: ${JSON.stringify(plistData)}`);
96    const plistMessage = plist.build(plistData);
97    const header = Buffer.alloc(LOCKDOWN_HEADER_SIZE);
98    header.writeUInt32BE(plistMessage.length, 0);
99    socket.write(header);
100    socket.write(plistMessage);
101  }
102}
103