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 plist from '@expo/plist'; 9import Debug from 'debug'; 10import { Socket } from 'net'; 11 12import { CommandError } from '../../../../utils/errors'; 13import { parsePlistBuffer } from '../../../../utils/plist'; 14 15const BPLIST_MAGIC = Buffer.from('bplist00'); 16const debug = Debug('expo:apple-device:protocol'); 17 18export class ProtocolClientError extends CommandError { 19 constructor(msg: string, public error: Error, public protocolMessage?: any) { 20 super(msg); 21 } 22} 23 24export type ProtocolReaderCallback = (resp: any, err?: Error) => void; 25 26export class ProtocolReaderFactory<T> { 27 constructor(private ProtocolReader: new (callback: ProtocolReaderCallback) => T) {} 28 29 create(callback: (resp: any, err?: Error) => void): T { 30 return new this.ProtocolReader(callback); 31 } 32} 33 34export abstract class ProtocolReader { 35 protected body!: Buffer; // TODO: ! -> ? 36 protected bodyLength!: number; // TODO: ! -> ? 37 protected buffer = Buffer.alloc(0); 38 constructor(protected headerSize: number, protected callback: ProtocolReaderCallback) { 39 this.onData = this.onData.bind(this); 40 } 41 42 /** Returns length of body, or -1 if header doesn't contain length */ 43 protected abstract parseHeader(data: Buffer): number; 44 protected abstract parseBody(data: Buffer): any; 45 46 onData(data?: Buffer) { 47 try { 48 // if there's data, add it on to existing buffer 49 this.buffer = data ? Buffer.concat([this.buffer, data]) : this.buffer; 50 // we haven't gotten the body length from the header yet 51 if (!this.bodyLength) { 52 if (this.buffer.length < this.headerSize) { 53 // partial header, wait for rest 54 return; 55 } 56 this.bodyLength = this.parseHeader(this.buffer); 57 // move on to body 58 this.buffer = this.buffer.slice(this.headerSize); 59 if (!this.buffer.length) { 60 // only got header, wait for body 61 return; 62 } 63 } 64 if (this.buffer.length < this.bodyLength) { 65 // wait for rest of body 66 return; 67 } 68 69 if (this.bodyLength === -1) { 70 this.callback(this.parseBody(this.buffer)); 71 this.buffer = Buffer.alloc(0); 72 } else { 73 this.body = this.buffer.slice(0, this.bodyLength); 74 this.bodyLength -= this.body.length; 75 if (!this.bodyLength) { 76 this.callback(this.parseBody(this.body)); 77 } 78 this.buffer = this.buffer.slice(this.body.length); 79 // There are multiple messages here, call parse again 80 if (this.buffer.length) { 81 this.onData(); 82 } 83 } 84 } catch (err: any) { 85 this.callback(null, err); 86 } 87 } 88} 89 90export abstract class PlistProtocolReader extends ProtocolReader { 91 protected parseBody(body: Buffer) { 92 if (BPLIST_MAGIC.compare(body, 0, 8) === 0) { 93 return parsePlistBuffer(body); 94 } else { 95 return plist.parse(body.toString('utf8')); 96 } 97 } 98} 99 100export interface ProtocolWriter { 101 write(sock: Socket, msg: any): void; 102} 103 104export abstract class ProtocolClient<MessageType = any> { 105 constructor( 106 public socket: Socket, 107 protected readerFactory: ProtocolReaderFactory<ProtocolReader>, 108 protected writer: ProtocolWriter 109 ) {} 110 111 sendMessage<ResponseType = any>(msg: MessageType): Promise<ResponseType>; 112 sendMessage<CallbackType = void, ResponseType = any>( 113 msg: MessageType, 114 callback: (response: ResponseType, resolve: any, reject: any) => void 115 ): Promise<CallbackType>; 116 sendMessage<CallbackType = void, ResponseType = any>( 117 msg: MessageType, 118 callback?: (response: ResponseType, resolve: any, reject: any) => void 119 ): Promise<CallbackType | ResponseType> { 120 const onError = (error: Error) => { 121 debug('Unexpected protocol socket error encountered: %s', error); 122 throw new ProtocolClientError( 123 `Unexpected protocol error encountered: ${error.message}`, 124 error, 125 msg 126 ); 127 }; 128 129 return new Promise<ResponseType | CallbackType>((resolve, reject) => { 130 const reader = this.readerFactory.create(async (response: ResponseType, error?: Error) => { 131 if (error) { 132 reject(error); 133 return; 134 } 135 if (callback) { 136 callback( 137 response, 138 (value: any) => { 139 this.socket.removeListener('data', reader.onData); 140 this.socket.removeListener('error', onError); 141 resolve(value); 142 }, 143 reject 144 ); 145 } else { 146 this.socket.removeListener('data', reader.onData); 147 this.socket.removeListener('error', onError); 148 resolve(response); 149 } 150 }); 151 this.socket.on('data', reader.onData); 152 this.socket.on('error', onError); 153 this.writer.write(this.socket, msg); 154 }); 155 } 156} 157