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