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