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