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