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 Debug from 'debug';
9c4ef02aeSEvan Baconimport { Socket } from 'net';
10c4ef02aeSEvan Bacon
11c4ef02aeSEvan Baconimport {
12c4ef02aeSEvan Bacon  ProtocolClient,
13c4ef02aeSEvan Bacon  ProtocolReader,
14c4ef02aeSEvan Bacon  ProtocolReaderCallback,
15c4ef02aeSEvan Bacon  ProtocolReaderFactory,
16c4ef02aeSEvan Bacon  ProtocolWriter,
17c4ef02aeSEvan Bacon} from './AbstractProtocol';
18*8a424bebSJames Ideimport { CommandError } from '../../../../utils/errors';
19c4ef02aeSEvan Bacon
20c4ef02aeSEvan Baconconst debug = Debug('expo:apple-device:protocol:gdb');
21c4ef02aeSEvan Baconconst ACK_SUCCESS = '+'.charCodeAt(0);
22c4ef02aeSEvan Bacon
23c4ef02aeSEvan Baconexport interface GDBMessage {
24c4ef02aeSEvan Bacon  cmd: string;
25c4ef02aeSEvan Bacon  args: string[];
26c4ef02aeSEvan Bacon}
27c4ef02aeSEvan Bacon
28c4ef02aeSEvan Baconexport class GDBProtocolClient extends ProtocolClient<GDBMessage> {
29c4ef02aeSEvan Bacon  constructor(socket: Socket) {
30c4ef02aeSEvan Bacon    super(socket, new ProtocolReaderFactory(GDBProtocolReader), new GDBProtocolWriter());
31c4ef02aeSEvan Bacon  }
32c4ef02aeSEvan Bacon}
33c4ef02aeSEvan Bacon
34c4ef02aeSEvan Baconexport class GDBProtocolReader extends ProtocolReader {
35c4ef02aeSEvan Bacon  constructor(callback: ProtocolReaderCallback) {
36c4ef02aeSEvan Bacon    super(1 /* "Header" is '+' or '-' */, callback);
37c4ef02aeSEvan Bacon  }
38c4ef02aeSEvan Bacon
39c4ef02aeSEvan Bacon  onData(data?: Buffer) {
40c4ef02aeSEvan Bacon    // the GDB protocol does not support body length in its header so we cannot rely on
41c4ef02aeSEvan Bacon    // the parent implementation to determine when a payload is complete
42c4ef02aeSEvan Bacon    try {
43c4ef02aeSEvan Bacon      // if there's data, add it to the existing buffer
44c4ef02aeSEvan Bacon      this.buffer = data ? Buffer.concat([this.buffer, data]) : this.buffer;
45c4ef02aeSEvan Bacon
46c4ef02aeSEvan Bacon      // do we have enough bytes to proceed
47c4ef02aeSEvan Bacon      if (this.buffer.length < this.headerSize) {
48c4ef02aeSEvan Bacon        return; // incomplete header, wait for more
49c4ef02aeSEvan Bacon      }
50c4ef02aeSEvan Bacon
51c4ef02aeSEvan Bacon      // first, check the header
52c4ef02aeSEvan Bacon      if (this.parseHeader(this.buffer) === -1) {
53c4ef02aeSEvan Bacon        // we have a valid header so check the body. GDB packets will always be a leading '$', data bytes,
54c4ef02aeSEvan Bacon        // a trailing '#', and a two digit checksum. minimum valid body is the empty response '$#00'
55c4ef02aeSEvan Bacon        // https://developer.apple.com/library/archive/documentation/DeveloperTools/gdb/gdb/gdb_33.html
56c4ef02aeSEvan Bacon        const packetData = this.buffer.toString().match('\\$.*#[0-9a-f]{2}');
57c4ef02aeSEvan Bacon        if (packetData == null) {
58c4ef02aeSEvan Bacon          return; // incomplete body, wait for more
59c4ef02aeSEvan Bacon        }
60c4ef02aeSEvan Bacon        // extract the body and update the buffer
61c4ef02aeSEvan Bacon        const body = Buffer.from(packetData[0]);
62c4ef02aeSEvan Bacon        this.buffer = this.buffer.slice(this.headerSize + body.length);
63c4ef02aeSEvan Bacon        // parse the payload and recurse if there is more data to process
64c4ef02aeSEvan Bacon        this.callback(this.parseBody(body));
65c4ef02aeSEvan Bacon        if (this.buffer.length) {
66c4ef02aeSEvan Bacon          this.onData();
67c4ef02aeSEvan Bacon        }
68c4ef02aeSEvan Bacon      }
69c4ef02aeSEvan Bacon    } catch (err: any) {
70c4ef02aeSEvan Bacon      this.callback(null, err);
71c4ef02aeSEvan Bacon    }
72c4ef02aeSEvan Bacon  }
73c4ef02aeSEvan Bacon
74c4ef02aeSEvan Bacon  parseHeader(data: Buffer) {
75c4ef02aeSEvan Bacon    if (data[0] !== ACK_SUCCESS) {
76c4ef02aeSEvan Bacon      throw new CommandError('APPLE_DEVICE_GDB', 'Unsuccessful debugserver response');
77c4ef02aeSEvan Bacon    } // TODO: retry?
78c4ef02aeSEvan Bacon    return -1;
79c4ef02aeSEvan Bacon  }
80c4ef02aeSEvan Bacon
81c4ef02aeSEvan Bacon  parseBody(buffer: Buffer) {
82c4ef02aeSEvan Bacon    debug(`Response body: ${buffer.toString()}`);
83c4ef02aeSEvan Bacon    // check for checksum
84c4ef02aeSEvan Bacon    const checksum = buffer.slice(-3).toString();
85c4ef02aeSEvan Bacon    if (checksum.match(/#[0-9a-f]{2}/)) {
86c4ef02aeSEvan Bacon      // remove '$' prefix and checksum
87c4ef02aeSEvan Bacon      const msg = buffer.slice(1, -3).toString();
88c4ef02aeSEvan Bacon      if (validateChecksum(checksum, msg)) {
89c4ef02aeSEvan Bacon        return msg;
90c4ef02aeSEvan Bacon      } else if (msg.startsWith('E')) {
91c4ef02aeSEvan Bacon        if (msg.match(/the device was not, or could not be, unlocked/)) {
92c4ef02aeSEvan Bacon          throw new CommandError('APPLE_DEVICE_LOCKED', 'Device is currently locked.');
93c4ef02aeSEvan Bacon        }
94c4ef02aeSEvan Bacon
95c4ef02aeSEvan Bacon        // Error message from debugserver -- Drop the `E`
96c4ef02aeSEvan Bacon        return msg.slice(1);
97c4ef02aeSEvan Bacon      } else {
98c4ef02aeSEvan Bacon        throw new CommandError(
99c4ef02aeSEvan Bacon          'APPLE_DEVICE_GDB',
100c4ef02aeSEvan Bacon          `Invalid checksum received from debugserver. (checksum: ${checksum}, msg: ${msg})`
101c4ef02aeSEvan Bacon        );
102c4ef02aeSEvan Bacon      }
103c4ef02aeSEvan Bacon    } else {
104c4ef02aeSEvan Bacon      throw new CommandError('APPLE_DEVICE_GDB', "Didn't receive checksum");
105c4ef02aeSEvan Bacon    }
106c4ef02aeSEvan Bacon  }
107c4ef02aeSEvan Bacon}
108c4ef02aeSEvan Bacon
109c4ef02aeSEvan Baconexport class GDBProtocolWriter implements ProtocolWriter {
110c4ef02aeSEvan Bacon  write(socket: Socket, msg: GDBMessage) {
111c4ef02aeSEvan Bacon    const { cmd, args } = msg;
112c4ef02aeSEvan Bacon    debug(`Socket write: ${cmd}, args: ${args}`);
113c4ef02aeSEvan Bacon    // hex encode and concat all args
114c4ef02aeSEvan Bacon    const encodedArgs = args
115c4ef02aeSEvan Bacon      .map((arg) => Buffer.from(arg).toString('hex'))
116c4ef02aeSEvan Bacon      .join()
117c4ef02aeSEvan Bacon      .toUpperCase();
118c4ef02aeSEvan Bacon    const checksumStr = calculateChecksum(cmd + encodedArgs);
119c4ef02aeSEvan Bacon    const formattedCmd = `$${cmd}${encodedArgs}#${checksumStr}`;
120c4ef02aeSEvan Bacon    socket.write(formattedCmd);
121c4ef02aeSEvan Bacon  }
122c4ef02aeSEvan Bacon}
123c4ef02aeSEvan Bacon
124c4ef02aeSEvan Bacon// hex value of (sum of cmd chars mod 256)
125c4ef02aeSEvan Baconfunction calculateChecksum(cmdStr: string) {
126c4ef02aeSEvan Bacon  let checksum = 0;
127c4ef02aeSEvan Bacon  for (let i = 0; i < cmdStr.length; i++) {
128c4ef02aeSEvan Bacon    checksum += cmdStr.charCodeAt(i);
129c4ef02aeSEvan Bacon  }
130c4ef02aeSEvan Bacon  let result = (checksum % 256).toString(16);
131c4ef02aeSEvan Bacon  // pad if necessary
132c4ef02aeSEvan Bacon  if (result.length === 1) {
133c4ef02aeSEvan Bacon    result = `0${result}`;
134c4ef02aeSEvan Bacon  }
135c4ef02aeSEvan Bacon  return result;
136c4ef02aeSEvan Bacon}
137c4ef02aeSEvan Bacon
138c4ef02aeSEvan Baconexport function validateChecksum(checksum: string, msg: string) {
139c4ef02aeSEvan Bacon  // remove '#' from checksum
140c4ef02aeSEvan Bacon  const checksumVal = checksum.startsWith('#') ? checksum.slice(1) : checksum;
141c4ef02aeSEvan Bacon  // remove '$' from msg and calculate its checksum
142c4ef02aeSEvan Bacon  const computedChecksum = calculateChecksum(msg);
143c4ef02aeSEvan Bacon  // debug(`Checksum: ${checksumVal}, computed checksum: ${computedChecksum}`);
144c4ef02aeSEvan Bacon  return checksumVal === computedChecksum;
145c4ef02aeSEvan Bacon}
146