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