• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

homebridge / HAP-NodeJS / 9957884830

11 Jul 2024 02:14AM UTC coverage: 64.289%. First build
9957884830

push

github

web-flow
v1.0.0 (#1042)

## v1.0.0 (2024-07-10)

### Breaking Changes

- **The minimum Node.js version required is now `v18`.**
- **Important notice:** Because of the cleanup of the Deprecated code,
you will need to migrate you code base.
    - Remove the long-deprecated init().
- Deprecate Core, BridgedCore, legacy Camera characteristics. (#1058)
(@hjdhjd)
- For deprecated `Core` and `BridgedCore` see:
https://github.com/homebridge/HAP-NodeJS/wiki/Deprecation-of-Core-and-BridgeCore
    - Legacy code deprecation cleanup. (#1059) (@hjdhjd)
- For deprecated `storagePath` switch to
`HAPStorage.setCustomStoragePath`, `AudioCodec` switch to
`AudioStreamingCodec`, `VideoCodec` switch to
`H264CodecParameters`,`StreamAudioParams` switch to
`AudioStreamingOptions`, `StreamVideoParams` switch to
`VideoStreamingOptions`,`cameraSource` switch to `CameraController`.
- Others deprecated code to highlight removed: `useLegacyAdvertiser`,
`AccessoryLoader`.
- Fix: Naming for Characteristic.ProgramMode has been corrected from
`PROGRAM_SCHEDULED_MANUAL_MODE_` to `PROGRAM_SCHEDULED_MANUAL_MODE`

### Fixed

- Fix: Build Issues (#1041) (@NorthernMan54)
- Fix: Ensure data is only transmitted on open and ready connections.
(#1051) (@hjdhjd)
- Fix: Ensure we check names using the full UTF-8 character set. (#1052)
(@hjdhjd)
- Fix: ConfiguredName (#1049) (@donavanbecker)
- Fix: Manufacturer looking at checkName but should look at checkValue.
(#1053) (@donavanbecker)

### Other Changes

- Implement warning messages for invalid characters in names (#1009)
(@NorthernMan54)
- Mitigate event emitter "memory leak" warnings when a significant
number of camera streaming events occur simultaneously (#1037) (@hjdhjd)
- AdaptiveLightingController fix & improvement (#1038) (@Shaquu)
- Minor fixes to recording logging and one change in logging. (#1040)
(@hjdhjd)
- Bridged core and core cleanup (#1048) (@Shaquu)
- Increase snapshot handler warning timeout ... (continued)

1362 of 2513 branches covered (54.2%)

Branch coverage included in aggregate %.

28 of 51 new or added lines in 10 files covered. (54.9%)

6219 of 9279 relevant lines covered (67.02%)

310.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

19.74
/src/lib/datastream/DataStreamServer.ts
1
import assert from "assert";
18✔
2
import crypto from "crypto";
18✔
3
import createDebug from "debug";
18✔
4
import { EventEmitter, EventEmitter as NodeEventEmitter } from "events";
18✔
5
import net, { Socket } from "net";
18✔
6
import { HAPConnection, HAPConnectionEvent } from "../util/eventedhttp";
7

8
import * as hapCrypto from "../util/hapCrypto";
18✔
9
import { DataStreamParser, DataStreamReader, DataStreamWriter, Int64 } from "./DataStreamParser";
18✔
10

11

12
const debug = createDebug("HAP-NodeJS:DataStream:Server");
18✔
13

14
/**
15
 * @group HomeKit Data Streams (HDS)
16
 */
17
export type PreparedDataStreamSession = {
18

19
  connection: HAPConnection, // reference to the hap session which created the request
20

21
  accessoryToControllerEncryptionKey: Buffer,
22
  controllerToAccessoryEncryptionKey: Buffer,
23
  accessoryKeySalt: Buffer,
24

25
  port?: number,
26

27
  connectTimeout?: NodeJS.Timeout, // 10s timer
28

29
}
30

31
/**
32
 * @group HomeKit Data Streams (HDS)
33
 */
34
export type PrepareSessionCallback = (error?: Error, preparedSession?: PreparedDataStreamSession) => void;
35
/**
36
 * @group HomeKit Data Streams (HDS)
37
 */
38
export type EventHandler = (message: Record<any, any>) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
39
/**
40
 * @group HomeKit Data Streams (HDS)
41
 */
42
export type RequestHandler = (id: number, message: Record<any, any>) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
43
/**
44
 * @group HomeKit Data Streams (HDS)
45
 */
46
export type ResponseHandler = (
47
  error: Error | undefined,
48
  status: HDSStatus | undefined,
49
  message: Record<any, any>) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
50
/**
51
 * @group HomeKit Data Streams (HDS)
52
 */
53
export type GlobalEventHandler = (
54
  connection: DataStreamConnection,
55
  message: Record<any, any>) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
56
/**
57
 * @group HomeKit Data Streams (HDS)
58
 */
59
export type GlobalRequestHandler = (
60
  connection: DataStreamConnection, id: number,
61
  message: Record<any, any>) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
62

63
/**
64
 * @group HomeKit Data Streams (HDS)
65
 */
66
export interface DataStreamProtocolHandler {
67
  eventHandler?: Record<string, EventHandler>;
68
  requestHandler?: Record<string, RequestHandler>;
69
}
70

71
/**
72
 * @group HomeKit Data Streams (HDS)
73
 */
74
export const enum Protocols { // a collection of currently known protocols
18✔
75
  CONTROL = "control",
18✔
76
  TARGET_CONTROL = "targetControl",
18✔
77
  DATA_SEND = "dataSend",
18✔
78
}
79

80
/**
81
 * @group HomeKit Data Streams (HDS)
82
 */
83
export const enum Topics { // a collection of currently known topics grouped by their protocol
18✔
84
  // control
85
  HELLO = "hello",
18✔
86

87
  // targetControl
88
  WHOAMI = "whoami",
18✔
89

90
  // dataSend
91
  OPEN = "open",
18✔
92
  DATA = "data",
18✔
93
  ACK = "ack",
18✔
94
  CLOSE = "close",
18✔
95
}
96

97
/**
98
 * @group HomeKit Data Streams (HDS)
99
 */
100
export enum HDSStatus {
18✔
101
  // noinspection JSUnusedGlobalSymbols
102
  SUCCESS = 0,
18✔
103
  OUT_OF_MEMORY = 1,
18✔
104
  TIMEOUT = 2,
18✔
105
  HEADER_ERROR = 3,
18✔
106
  PAYLOAD_ERROR = 4,
18✔
107
  MISSING_PROTOCOL = 5,
18✔
108
  PROTOCOL_SPECIFIC_ERROR = 6,
18✔
109
}
110

111
/**
112
 * @group HomeKit Data Streams (HDS)
113
 */
114
export const enum HDSProtocolSpecificErrorReason { // close reason used in the dataSend protocol
18✔
115
  // noinspection JSUnusedGlobalSymbols
116
  NORMAL = 0,
18✔
117
  NOT_ALLOWED = 1,
18✔
118
  BUSY = 2,
18✔
119
  CANCELLED = 3,
18✔
120
  UNSUPPORTED = 4,
18✔
121
  UNEXPECTED_FAILURE = 5,
18✔
122
  TIMEOUT = 6,
18✔
123
  BAD_DATA = 7,
18✔
124
  PROTOCOL_ERROR = 8,
18✔
125
  INVALID_CONFIGURATION = 9,
18✔
126
}
127

128
/**
129
 * An error indicating a protocol level HDS error.
130
 * E.g. it may be used to encode a {@link HDSStatus.PROTOCOL_SPECIFIC_ERROR} in the {@link Protocols.DATA_SEND} protocol.
131
 * @group HomeKit Data Streams (HDS)
132
 */
133
export class HDSProtocolError extends Error {
18✔
134
  reason: HDSProtocolSpecificErrorReason;
135

136
  /**
137
   * Initializes a new `HDSProtocolError`
138
   * @param reason - The {@link HDSProtocolSpecificErrorReason}.
139
   *  Values MUST NOT be {@link HDSProtocolSpecificErrorReason.NORMAL}.
140
   */
141
  constructor(reason: HDSProtocolSpecificErrorReason) {
142
    super("HDSProtocolError: " + reason);
×
143
    assert(reason !== HDSProtocolSpecificErrorReason.NORMAL, "Cannot initialize a HDSProtocolError with NORMAL!");
×
144
    this.reason = reason;
×
145
  }
146
}
147

148

149
const enum ServerState {
18✔
150
  UNINITIALIZED, // server socket hasn't been created
18✔
151
  BINDING, // server is created and is currently trying to bind
18✔
152
  LISTENING, // server is created and currently listening for new connections
18✔
153
  CLOSING,
18✔
154
}
155

156
const enum ConnectionState {
18✔
157
  UNIDENTIFIED,
18✔
158
  EXPECTING_HELLO,
18✔
159
  READY,
18✔
160
  CLOSING,
18✔
161
  CLOSED,
18✔
162
}
163

164
/**
165
 * @group HomeKit Data Streams (HDS)
166
 */
167
export type HDSFrame = {
168
  header: Buffer,
169
  cipheredPayload: Buffer,
170
  authTag: Buffer,
171
  plaintextPayload?: Buffer,
172
}
173

174
/**
175
 * @group HomeKit Data Streams (HDS)
176
 */
177
export const enum MessageType {
18✔
178
  EVENT = 1,
18✔
179
  REQUEST = 2,
18✔
180
  RESPONSE = 3,
18✔
181
}
182

183
/**
184
 * @group HomeKit Data Streams (HDS)
185
 */
186
export type DataStreamMessage = {
187
  type: MessageType,
188

189
  protocol: string,
190
  topic: string,
191
  id?: number, // for requests and responses
192
  status?: HDSStatus, // for responses
193

194
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
195
  message: Record<any, any>,
196
}
197

198
/**
199
 * @group HomeKit Data Streams (HDS)
200
 */
201
export const enum DataStreamServerEvent {
18✔
202
  /**
203
   * This event is emitted when a new client socket is received. At this point we have no idea to what
204
   * hap session this connection will be matched.
205
   */
206
  CONNECTION_OPENED = "connection-opened",
18✔
207
  /**
208
   * This event is emitted when the socket of a connection gets closed.
209
   */
210
  CONNECTION_CLOSED = "connection-closed",
18✔
211
}
212

213
/**
214
 * @group HomeKit Data Streams (HDS)
215
 */
216
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
217
export declare interface DataStreamServer {
218
  on(event: "connection-opened", listener: (connection: DataStreamConnection) => void): this;
219
  on(event: "connection-closed", listener: (connection: DataStreamConnection) => void): this;
220

221
  emit(event: "connection-opened", connection: DataStreamConnection): boolean;
222
  emit(event: "connection-closed", connection: DataStreamConnection): boolean;
223
}
224

225
/**
226
 * DataStreamServer which listens for incoming tcp connections and handles identification of new connections
227
 * @group HomeKit Data Streams (HDS)
228
 */
229
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
230
export class DataStreamServer extends EventEmitter {
18✔
231

232
  static readonly version = "1.0";
18✔
233

234
  private state: ServerState = ServerState.UNINITIALIZED;
219✔
235

236
  private static accessoryToControllerInfo = Buffer.from("HDS-Read-Encryption-Key");
18✔
237
  private static controllerToAccessoryInfo = Buffer.from("HDS-Write-Encryption-Key");
18✔
238

239
  private tcpServer?: net.Server;
240
  private tcpPort?: number;
241

242
  preparedSessions: PreparedDataStreamSession[] = [];
219✔
243
  private readonly connections: DataStreamConnection[] = [];
219✔
244
  private removeListenersOnceClosed = false;
219✔
245

246
  private readonly internalEventEmitter: NodeEventEmitter = new NodeEventEmitter(); // used for message event and message request handlers
219✔
247

248
  public constructor() {
249
    super();
219✔
250
  }
251

252
  /**
253
   * Registers a new event handler to handle incoming event messages.
254
   * The handler is only called for a connection if for the give protocol no ProtocolHandler
255
   * was registered on the connection level.
256
   *
257
   * @param protocol - name of the protocol to register the handler for
258
   * @param event - name of the event (also referred to as topic. See {@link Topics} for some known ones)
259
   * @param handler - function to be called for every occurring event
260
   */
261
  public onEventMessage(protocol: string | Protocols, event: string | Topics, handler: GlobalEventHandler): this {
262
    this.internalEventEmitter.on(protocol + "-e-" + event, handler);
×
263
    return this;
×
264
  }
265

266
  /**
267
   * Removes a registered event handler.
268
   *
269
   * @param protocol - name of the protocol to unregister the handler for
270
   * @param event - name of the event (also referred to as topic. See {@link Topics} for some known ones)
271
   * @param handler - registered event handler
272
   */
273
  public removeEventHandler(protocol: string | Protocols, event: string | Topics, handler: GlobalEventHandler): this {
274
    this.internalEventEmitter.removeListener(protocol + "-e-" + event, handler);
×
275
    return this;
×
276
  }
277

278
  /**
279
   * Registers a new request handler to handle incoming request messages.
280
   * The handler is only called for a connection if for the give protocol no ProtocolHandler
281
   * was registered on the connection level.
282
   *
283
   * @param protocol - name of the protocol to register the handler for
284
   * @param request - name of the request (also referred to as topic. See {@link Topics} for some known ones)
285
   * @param handler - function to be called for every occurring request
286
   */
287
  public onRequestMessage(protocol: string | Protocols, request: string | Topics, handler: GlobalRequestHandler): this {
288
    this.internalEventEmitter.on(protocol + "-r-" + request, handler);
219✔
289
    return this;
219✔
290
  }
291

292
  /**
293
   * Removes a registered request handler.
294
   *
295
   * @param protocol - name of the protocol to unregister the handler for
296
   * @param request - name of the request (also referred to as topic. See {@link Topics} for some known ones)
297
   * @param handler - registered request handler
298
   */
299
  public removeRequestHandler(protocol: string | Protocols, request: string | Topics, handler: GlobalRequestHandler): this {
300
    this.internalEventEmitter.removeListener(protocol + "-r-" + request, handler);
×
301
    return this;
×
302
  }
303

304
  public prepareSession(connection: HAPConnection, controllerKeySalt: Buffer, callback: PrepareSessionCallback): void {
305
    debug("Preparing for incoming HDS connection from %s", connection.sessionID);
×
306
    const accessoryKeySalt = crypto.randomBytes(32);
×
307
    const salt = Buffer.concat([controllerKeySalt, accessoryKeySalt]);
×
308

309
    const accessoryToControllerEncryptionKey = hapCrypto.HKDF(
×
310
      "sha512",
311
      salt,
312
      connection.encryption!.sharedSecret,
313
      DataStreamServer.accessoryToControllerInfo,
314
      32,
315
    );
316
    const controllerToAccessoryEncryptionKey = hapCrypto.HKDF(
×
317
      "sha512",
318
      salt,
319
      connection.encryption!.sharedSecret,
320
      DataStreamServer.controllerToAccessoryInfo,
321
      32,
322
    );
323

324
    const preparedSession: PreparedDataStreamSession = {
×
325
      connection: connection,
326
      accessoryToControllerEncryptionKey: accessoryToControllerEncryptionKey,
327
      controllerToAccessoryEncryptionKey: controllerToAccessoryEncryptionKey,
328
      accessoryKeySalt: accessoryKeySalt,
329
      connectTimeout: setTimeout(() => this.timeoutPreparedSession(preparedSession), 10000),
×
330
    };
331
    preparedSession.connectTimeout!.unref();
×
332
    this.preparedSessions.push(preparedSession);
×
333

334
    this.checkTCPServerEstablished(preparedSession, (error) => {
×
335
      if (error) {
×
336
        callback(error);
×
337
      } else {
338
        callback(undefined, preparedSession);
×
339
      }
340
    });
341
  }
342

343
  private timeoutPreparedSession(preparedSession: PreparedDataStreamSession) {
344
    debug("Prepared HDS session timed out out since no connection was opened for 10 seconds (%s)", preparedSession.connection.sessionID);
×
345
    const index = this.preparedSessions.indexOf(preparedSession);
×
346
    if (index >= 0) {
×
347
      this.preparedSessions.splice(index, 1);
×
348
    }
349

350
    this.checkCloseable();
×
351
  }
352

353
  private checkTCPServerEstablished(preparedSession: PreparedDataStreamSession, callback: (error?: Error) => void) {
354
    switch (this.state) {
×
355
    case ServerState.UNINITIALIZED:
356
      debug("Starting up TCP server.");
×
357
      this.tcpServer = net.createServer();
×
358

359
      this.tcpServer.once("listening", this.listening.bind(this, preparedSession, callback));
×
360
      this.tcpServer.on("connection", this.onConnection.bind(this));
×
361
      this.tcpServer.on("close", this.closed.bind(this));
×
362

363
      this.tcpServer.listen();
×
364
      this.state = ServerState.BINDING;
×
365
      break;
×
366
    case ServerState.BINDING:
367
      debug("TCP server already running. Waiting for it to bind.");
×
368
      this.tcpServer!.once("listening", this.listening.bind(this, preparedSession, callback));
×
369
      break;
×
370
    case ServerState.LISTENING:
371
      debug("Instructing client to connect to already running TCP server");
×
372
      preparedSession.port = this.tcpPort;
×
373
      callback();
×
374
      break;
×
375
    case ServerState.CLOSING:
376
      debug("TCP socket is currently closing. Trying again when server is fully closed and opening a new one then.");
×
377
      this.tcpServer!.once("close", () => setTimeout(() => this.checkTCPServerEstablished(preparedSession, callback), 10));
×
378
      break;
×
379
    }
380
  }
381

382
  private listening(preparedSession: PreparedDataStreamSession, callback: (error?: Error) => void) {
383
    this.state = ServerState.LISTENING;
×
384

385
    const address = this.tcpServer!.address();
×
386
    if (address && typeof address !== "string") { // address is only typeof string when listening to a pipe or unix socket
×
387
      this.tcpPort = address.port;
×
388
      preparedSession.port = address.port;
×
389

390
      debug("TCP server is now listening for new data stream connections on port %s", address.port);
×
391
      callback();
×
392
    }
393
  }
394

395
  private onConnection(socket: Socket) {
396
    debug("[%s] New DataStream connection was established", socket.remoteAddress);
×
397
    // eslint-disable-next-line @typescript-eslint/no-use-before-define
398
    const connection = new DataStreamConnection(socket);
×
399

400
    connection.on(DataStreamConnectionEvent.IDENTIFICATION, this.handleSessionIdentification.bind(this, connection));
×
401
    connection.on(DataStreamConnectionEvent.HANDLE_MESSAGE_GLOBALLY, this.handleMessageGlobally.bind(this, connection));
×
402
    connection.on(DataStreamConnectionEvent.CLOSED, this.connectionClosed.bind(this, connection));
×
403

404
    this.connections.push(connection);
×
405

406
    this.emit(DataStreamServerEvent.CONNECTION_OPENED, connection);
×
407
  }
408

409
  private handleSessionIdentification(connection: DataStreamConnection, firstFrame: HDSFrame, callback: IdentificationCallback) {
410
    let identifiedSession: PreparedDataStreamSession | undefined = undefined;
×
411
    for (let i = 0; i < this.preparedSessions.length; i++) {
×
412
      const preparedSession = this.preparedSessions[i];
×
413

414
      // if we successfully decrypt the first frame with this key we know to which session this connection belongs
415
      if (connection.decryptHDSFrame(firstFrame, preparedSession.controllerToAccessoryEncryptionKey)) {
×
416
        identifiedSession = preparedSession;
×
417
        break;
×
418
      }
419
    }
420

421
    callback(identifiedSession);
×
422

423
    if (identifiedSession) {
×
424
      debug("[%s] Connection was successfully identified (linked with sessionId: %s)", connection.remoteAddress, identifiedSession.connection.sessionID);
×
425
      const index = this.preparedSessions.indexOf(identifiedSession);
×
426
      if (index >= 0) {
×
427
        this.preparedSessions.splice(index, 1);
×
428
      }
429

430
      clearTimeout(identifiedSession.connectTimeout!);
×
431
      identifiedSession.connectTimeout = undefined;
×
432

433
      // we have currently no experience with data stream connections, maybe it would be good to index active connections
434
      // by their hap sessionId in order to clear out old but still open connections when the controller opens a new one
435
      // on the other hand the keepAlive should handle that also :thinking:
436
    } else { // we looped through all session and didn't find anything
437
      debug("[%s] Could not identify connection. Terminating.", connection.remoteAddress);
×
438
      connection.close(); // disconnecting since first message was not a valid hello
×
439
    }
440
  }
441

442
  private handleMessageGlobally(connection: DataStreamConnection, message: DataStreamMessage) {
443
    assert.notStrictEqual(message.type, MessageType.RESPONSE); // responses can't physically get here
×
444

445
    let separator = "";
×
446
    const args = [];
×
447
    if (message.type === MessageType.EVENT) {
×
448
      separator = "-e-";
×
449
    } else if (message.type === MessageType.REQUEST) {
×
450
      separator = "-r-";
×
451
      args.push(message.id!);
×
452
    }
453
    args.push(message.message);
×
454

455
    let hadListeners;
456
    try {
×
457
      hadListeners = this.internalEventEmitter.emit(message.protocol + separator + message.topic, connection, ...args);
×
458
    } catch (error) {
459
      hadListeners = true;
×
460
      debug("[%s] Error occurred while dispatching handler for HDS message: %o", connection.remoteAddress, message);
×
461
      debug(error.stack);
×
462
    }
463

464
    if (!hadListeners) {
×
465
      debug("[%s] WARNING no handler was found for message: %o", connection.remoteAddress, message);
×
466
    }
467
  }
468

469
  private connectionClosed(connection: DataStreamConnection) {
470
    debug("[%s] DataStream connection closed", connection.remoteAddress);
×
471

472
    this.connections.splice(this.connections.indexOf(connection), 1);
×
473
    this.emit(DataStreamServerEvent.CONNECTION_CLOSED, connection);
×
474

475
    this.checkCloseable();
×
476

477
    if (this.state === ServerState.CLOSING && this.removeListenersOnceClosed && this.connections.length === 0) {
×
478
      this.removeAllListeners(); // see this.destroy()
×
479
    }
480
  }
481

482
  private checkCloseable() {
483
    if (this.connections.length === 0 && this.preparedSessions.length === 0 && this.state < ServerState.CLOSING) {
×
484
      debug("Last connection disconnected. Closing the server now.");
×
485

486
      this.state = ServerState.CLOSING;
×
487
      this.tcpServer!.close();
×
488
    }
489
  }
490

491
  /**
492
   * This method will fully stop the DataStreamServer
493
   */
494
  public destroy(): void {
495
    if (this.state > ServerState.UNINITIALIZED && this.state < ServerState.CLOSING) {
6!
496
      this.tcpServer!.close();
×
497
      for (const connection of this.connections) {
×
498
        connection.close();
×
499
      }
500
    }
501

502
    this.state = ServerState.CLOSING;
6✔
503

504
    this.removeListenersOnceClosed = true;
6✔
505
    this.internalEventEmitter.removeAllListeners();
6✔
506
  }
507

508
  private closed() {
509
    this.tcpServer = undefined;
×
510
    this.tcpPort = undefined;
×
511

512
    this.state = ServerState.UNINITIALIZED;
×
513
  }
514

515
}
516

517
/**
518
 * @group HomeKit Data Streams (HDS)
519
 */
520
export type IdentificationCallback = (identifiedSession?: PreparedDataStreamSession) => void;
521

522
/**
523
 * @group HomeKit Data Streams (HDS)
524
 */
525
export const enum DataStreamConnectionEvent {
18✔
526
  /**
527
   * This event is emitted when the first HDSFrame is received from a new connection.
528
   * The connection expects the handler to identify the connection by trying to match the decryption keys.
529
   * If identification was successful the PreparedDataStreamSession should be supplied to the callback,
530
   * otherwise undefined should be supplied.
531
   */
532
  IDENTIFICATION = "identification",
18✔
533
  /**
534
   * This event is emitted when no handler could be found for the given protocol of an event or request message.
535
   */
536
  HANDLE_MESSAGE_GLOBALLY = "handle-message-globally",
18✔
537
  /**
538
   * This event is emitted when the socket of the connection was closed.
539
   */
540
  CLOSED = "closed",
18✔
541
}
542

543
/**
544
 * @group HomeKit Data Streams (HDS)
545
 */
546
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
547
export declare interface DataStreamConnection {
548
  on(event: "identification", listener: (frame: HDSFrame, callback: IdentificationCallback) => void): this;
549
  on(event: "handle-message-globally", listener: (message: DataStreamMessage) => void): this;
550
  on(event: "closed", listener: () => void): this;
551

552
  emit(event: "identification", frame: HDSFrame, callback: IdentificationCallback): boolean;
553
  emit(event: "handle-message-globally", message: DataStreamMessage): boolean;
554
  emit(event: "closed"): boolean;
555
}
556

557
/**
558
 * @group HomeKit Data Streams (HDS)
559
 */
560
export const enum HDSConnectionErrorType {
18✔
561
  ILLEGAL_STATE = 1,
18✔
562
  CLOSED_SOCKET = 2,
18✔
563
  MAX_PAYLOAD_LENGTH = 3,
18✔
564
}
565

566
/**
567
 * @group HomeKit Data Streams (HDS)
568
 */
569
export class HDSConnectionError extends Error {
18✔
570
  readonly type: HDSConnectionErrorType;
571

572
  constructor(message: string, type: HDSConnectionErrorType) {
573
    super(message);
×
574
    this.type = type;
×
575
  }
576
}
577

578
/**
579
 * DataStream connection which holds any necessary state information, encryption and decryption keys, manages
580
 * protocol handlers and also handles sending and receiving of data stream frames.
581
 *
582
 * @group HomeKit Data Streams (HDS)
583
 */
584
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
585
export class DataStreamConnection extends EventEmitter {
18✔
586

587
  private static readonly MAX_PAYLOAD_LENGTH = 0b11111111111111111111;
18✔
588

589
  private socket: Socket;
590
  private connection?: HAPConnection; // reference to the hap connection. is present when state > UNIDENTIFIED
591
  readonly remoteAddress: string;
592
  /*
593
        Since our DataStream server does only listen on one port and this port is supplied to every client
594
        which wants to connect, we do not really know which client is who when we receive a tcp connection.
595
        Thus, we find the correct PreparedDataStreamSession object by testing the encryption keys of all available
596
        prepared sessions. Then we can reference this hds connection with the correct hap connection and mark it as identified.
597
     */
598
  private state: ConnectionState = ConnectionState.UNIDENTIFIED;
×
599

600
  private accessoryToControllerEncryptionKey?: Buffer;
601
  private controllerToAccessoryEncryptionKey?: Buffer;
602

603
  private accessoryToControllerNonce: number;
604
  private readonly accessoryToControllerNonceBuffer: Buffer;
605
  private controllerToAccessoryNonce: number;
606
  private readonly controllerToAccessoryNonceBuffer: Buffer;
607

608
  private frameBuffer?: Buffer; // used to store incomplete HDS frames
609

610
  private readonly hapConnectionClosedListener: () => void;
611
  private protocolHandlers: Record<string, DataStreamProtocolHandler> = {}; // used to store protocolHandlers identified by their protocol name
×
612

613
  private responseHandlers: Record<number, ResponseHandler> = {}; // used to store responseHandlers indexed by their respective requestId
×
614
  private responseTimers: Record<number, NodeJS.Timeout> = {}; // used to store response timeouts indexed by their respective requestId
×
615

616
  private helloTimer?: NodeJS.Timeout;
617

618
  constructor(socket: Socket) {
619
    super();
×
620
    this.socket = socket;
×
621
    this.remoteAddress = socket.remoteAddress!;
×
622

623
    this.socket.setNoDelay(true); // disable Nagle algorithm
×
624
    this.socket.setKeepAlive(true);
×
625

626
    this.accessoryToControllerNonce = 0;
×
627
    this.accessoryToControllerNonceBuffer = Buffer.alloc(8);
×
628
    this.controllerToAccessoryNonce = 0;
×
629
    this.controllerToAccessoryNonceBuffer = Buffer.alloc(8);
×
630

631
    this.hapConnectionClosedListener = this.onHAPSessionClosed.bind(this);
×
632

633
    this.addProtocolHandler(Protocols.CONTROL, {
×
634
      requestHandler: {
635
        [Topics.HELLO]: this.handleHello.bind(this),
636
      },
637
    });
638

639
    this.helloTimer = setTimeout(() => {
×
640
      debug("[%s] Hello message did not arrive in time. Killing the connection", this.remoteAddress);
×
641
      this.close();
×
642
    }, 10000);
643

644
    this.socket.on("data", this.onSocketData.bind(this));
×
645
    this.socket.on("error", this.onSocketError.bind(this));
×
646
    this.socket.on("close", this.onSocketClose.bind(this));
×
647

648
    // this is to mitigate the event emitter "memory leak warning".
649
    // e.g. with HSV there might be multiple cameras subscribing to the CLOSE event. one subscription for
650
    // every active recording stream on a camera. The default limit of 10 might be easily reached.
651
    // Setting a high limit isn't the prefect solution, but will avoid false positives but ensures that
652
    // a warning is still be printed if running long enough.
653
    this.setMaxListeners(100);
×
654
  }
655

656
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
657
  private handleHello(id: number, message: Record<any, any>) {
658
    // that hello is indeed the _first_ message received is verified in onSocketData(...)
659
    debug("[%s] Received hello message from client: %o", this.remoteAddress, message);
×
660

661
    clearTimeout(this.helloTimer!);
×
662
    this.helloTimer = undefined;
×
663

664
    this.state = ConnectionState.READY;
×
665

666
    this.sendResponse(Protocols.CONTROL, Topics.HELLO, id);
×
667
  }
668

669
  /**
670
   * Registers a new protocol handler to handle incoming messages.
671
   * The same protocol cannot be registered multiple times.
672
   *
673
   * @param protocol - name of the protocol to register the handler for
674
   * @param protocolHandler - object to be registered as protocol handler
675
   */
676
  addProtocolHandler(protocol: string | Protocols, protocolHandler: DataStreamProtocolHandler): boolean {
677
    if (this.protocolHandlers[protocol] !== undefined) {
×
678
      return false;
×
679
    }
680

681
    this.protocolHandlers[protocol] = protocolHandler;
×
682
    return true;
×
683
  }
684

685
  /**
686
   * Removes a protocol handler if it is registered.
687
   *
688
   * @param protocol - name of the protocol to unregister the handler for
689
   * @param protocolHandler - object which will be unregistered
690
   */
691
  removeProtocolHandler(protocol: string | Protocols, protocolHandler: DataStreamProtocolHandler): void {
692
    const current = this.protocolHandlers[protocol];
×
693

694
    if (current === protocolHandler) {
×
695
      delete this.protocolHandlers[protocol];
×
696
    }
697
  }
698

699
  /**
700
   * Sends a new event message to the connected client.
701
   *
702
   * @param protocol - name of the protocol
703
   * @param event - name of the event (also referred to as topic. See {@link Topics} for some known ones)
704
   * @param message - message dictionary which gets sent along the event
705
   */
706
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
707
  sendEvent(protocol: string | Protocols, event: string | Topics, message: Record<any, any> = {}): void {
×
708
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
709
    const header: Record<any, any> = {};
×
710
    header.protocol = protocol;
×
711
    header.event = event;
×
712

NEW
713
    if (this.state === ConnectionState.READY) {
×
NEW
714
      this.sendHDSFrame(header, message);
×
715
    }
716
  }
717

718
  /**
719
   * Sends a new request message to the connected client.
720
   *
721
   * @param protocol - name of the protocol
722
   * @param request - name of the request (also referred to as topic. See {@link Topics} for some known ones)
723
   * @param message - message dictionary which gets sent along the request
724
   * @param callback - handler which gets supplied with an error object if the response didn't
725
   *                   arrive in time or the status and the message dictionary from the response
726
   */
727
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
728
  sendRequest(protocol: string | Protocols, request: string | Topics, message: Record<any, any> = {}, callback: ResponseHandler): void {
×
729
    let requestId: number;
730
    do { // generate unused requestId
×
731
      // currently writing int64 to data stream is not really supported, so 32-bit int will be the max
732
      requestId = Math.floor(Math.random() * 4294967295);
×
733
    } while (this.responseHandlers[requestId] !== undefined);
734

735
    this.responseHandlers[requestId] = callback;
×
736
    this.responseTimers[requestId] = setTimeout(() => {
×
737
      // we did not receive a response => close socket
738
      this.close();
×
739

740
      const handler = this.responseHandlers[requestId];
×
741

742
      delete this.responseHandlers[requestId];
×
743
      delete this.responseTimers[requestId];
×
744

745
      // handler should be able to clean up their stuff
746
      handler(new Error("timeout"), undefined, {});
×
747
    }, 10000); // 10s timer
748

749
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
750
    const header: Record<any, any> = {};
×
751
    header.protocol = protocol;
×
752
    header.request = request;
×
753
    header.id = new Int64(requestId);
×
754

755
    this.sendHDSFrame(header, message);
×
756
  }
757

758
  /**
759
   * Send a new response message to a received request message to the client.
760
   *
761
   * @param protocol - name of the protocol
762
   * @param response - name of the response (also referred to as topic. See {@link Topics} for some known ones)
763
   * @param id - id from the request, to associate the response to the request
764
   * @param status - status indication if the request was successful. A status of zero indicates success.
765
   * @param message - message dictionary which gets sent along the response
766
   */
767
  sendResponse(
768
    protocol: string | Protocols,
769
    response: string | Topics,
770
    id: number,
771
    status: HDSStatus = HDSStatus.SUCCESS,
×
772
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
773
    message: Record<any, any> = {},
×
774
  ): void {
775
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
776
    const header: Record<any, any> = {};
×
777
    header.protocol = protocol;
×
778
    header.response = response;
×
779
    header.id = new Int64(id);
×
780
    header.status = new Int64(status);
×
781

782
    this.sendHDSFrame(header, message);
×
783
  }
784

785
  private onSocketData(data: Buffer) {
786
    if (this.state >= ConnectionState.CLOSING) {
×
787
      return;
×
788
    }
789

790
    let frameIndex = 0;
×
791
    const frames: HDSFrame[] = this.decodeHDSFrames(data);
×
792
    if (frames.length === 0) { // not enough data
×
793
      return;
×
794
    }
795

796
    if (this.state === ConnectionState.UNIDENTIFIED) {
×
797
      // at the beginning we are only interested in trying to decrypt the first frame in order to test decryption keys
798
      const firstFrame = frames[frameIndex++];
×
799
      this.emit(DataStreamConnectionEvent.IDENTIFICATION, firstFrame, (identifiedSession?: PreparedDataStreamSession) => {
×
800
        if (identifiedSession) {
×
801
          // horray, we found our connection
802
          this.connection = identifiedSession.connection;
×
803
          this.accessoryToControllerEncryptionKey = identifiedSession.accessoryToControllerEncryptionKey;
×
804
          this.controllerToAccessoryEncryptionKey = identifiedSession.controllerToAccessoryEncryptionKey;
×
805
          this.state = ConnectionState.EXPECTING_HELLO;
×
806

807
          // below listener is removed in .close()
808
          this.connection.setMaxListeners(this.connection.getMaxListeners() + 1);
×
809
          this.connection.on(HAPConnectionEvent.CLOSED, this.hapConnectionClosedListener); // register close listener
×
810

811
          debug("[%s] Registering CLOSED handler to HAP connection. Connection currently has %d close handlers!",
×
812
            this.remoteAddress, this.connection.listeners(HAPConnectionEvent.CLOSED).length);
813
        }
814
      });
815

816
      if (this.state === ConnectionState.UNIDENTIFIED) {
×
817
        // did not find a prepared connection, server already closed this connection; nothing to do here
818
        return;
×
819
      }
820
    }
821

822
    for (; frameIndex < frames.length; frameIndex++) { // decrypt all remaining frames
×
823
      if (!this.decryptHDSFrame(frames[frameIndex])) {
×
824
        debug("[%s] HDS frame decryption or authentication failed. Connection will be terminated!", this.remoteAddress);
×
825
        this.close();
×
826
        return;
×
827
      }
828
    }
829

830
    const messages: DataStreamMessage[] = this.decodePayloads(frames); // decode contents of payload
×
831

832
    if (this.state === ConnectionState.EXPECTING_HELLO) {
×
833
      const firstMessage = messages[0];
×
834

835
      if (firstMessage.protocol !== Protocols.CONTROL || firstMessage.type !== MessageType.REQUEST || firstMessage.topic !== Topics.HELLO) {
×
836
        // first message is not the expected hello request
837
        debug("[%s] First message received was not the expected hello message. Instead got: %o", this.remoteAddress, firstMessage);
×
838
        this.close();
×
839
        return;
×
840
      }
841
    }
842

843
    messages.forEach(message => {
×
844
      if (message.type === MessageType.RESPONSE) {
×
845
        // protocol and topic are currently not tested here; just assumed they are correct;
846
        // probably they are as the requestId is unique per connection no matter what protocol is used
847
        const responseHandler = this.responseHandlers[message.id!];
×
848
        const responseTimer = this.responseTimers[message.id!];
×
849

850
        if (responseTimer) {
×
851
          clearTimeout(responseTimer);
×
852
          delete this.responseTimers[message.id!];
×
853
        }
854

855
        if (!responseHandler) {
×
856
          // we got a response to a request we did not send; we ignore it for now, since nobody will be hurt
857
          debug("WARNING we received a response to a request we have not sent: %o", message);
×
858
          return;
×
859
        }
860

861
        try {
×
862
          responseHandler(undefined, message.status!, message.message);
×
863
        } catch (error) {
864
          debug("[%s] Error occurred while dispatching response handler for HDS message: %o", this.remoteAddress, message);
×
865
          debug(error.stack);
×
866
        }
867
        delete this.responseHandlers[message.id!];
×
868
      } else {
869
        const handler = this.protocolHandlers[message.protocol];
×
870
        if (handler === undefined) {
×
871
          // send message to the server to check if there are some global handlers for it
872
          this.emit(DataStreamConnectionEvent.HANDLE_MESSAGE_GLOBALLY, message);
×
873
          return;
×
874
        }
875

876
        if (message.type === MessageType.EVENT) {
×
877
          let eventHandler: EventHandler;
878
          if (!handler.eventHandler || !(eventHandler = handler.eventHandler[message.topic])) {
×
879
            debug("[%s] WARNING no event handler was found for message: %o", this.remoteAddress, message);
×
880
            return;
×
881
          }
882

883
          try {
×
884
            eventHandler(message.message);
×
885
          } catch (error) {
886
            debug("[%s] Error occurred while dispatching event handler for HDS message: %o", this.remoteAddress, message);
×
887
            debug(error.stack);
×
888
          }
889
        } else if (message.type === MessageType.REQUEST) {
×
890
          let requestHandler: RequestHandler;
891
          if (!handler.requestHandler || !(requestHandler = handler.requestHandler[message.topic])) {
×
892
            debug("[%s] WARNING no request handler was found for message: %o", this.remoteAddress, message);
×
893
            return;
×
894
          }
895

896
          try {
×
897
            requestHandler(message.id!, message.message);
×
898
          } catch (error) {
899
            debug("[%s] Error occurred while dispatching request handler for HDS message: %o", this.remoteAddress, message);
×
900
            debug(error.stack);
×
901
          }
902
        } else {
903
          debug("[%s] Encountered unknown message type with id %d", this.remoteAddress, message.type);
×
904
        }
905
      }
906
    });
907
  }
908

909
  private decodeHDSFrames(data: Buffer) {
910
    if (this.frameBuffer !== undefined) {
×
911
      data = Buffer.concat([this.frameBuffer, data]);
×
912
      this.frameBuffer = undefined;
×
913
    }
914

915
    const totalBufferLength = data.length;
×
916
    const frames: HDSFrame[] = [];
×
917

918
    for (let frameBegin = 0; frameBegin < totalBufferLength;) {
×
919
      if (frameBegin + 4 > totalBufferLength) {
×
920
        // we don't have enough data in the buffer for the next header
921
        this.frameBuffer = data.slice(frameBegin);
×
922
        break;
×
923
      }
924

925
      const payloadType = data.readUInt8(frameBegin); // type defining structure of payload; 8-bit; currently expected to be 1
×
926
      const payloadLength = data.readUIntBE(frameBegin + 1, 3); // read 24-bit big-endian uint length field
×
927

928
      if (payloadLength > DataStreamConnection.MAX_PAYLOAD_LENGTH) {
×
929
        debug("[%s] Connection send payload with size bigger than the maximum allow for data stream", this.remoteAddress);
×
930
        this.close();
×
931
        return [];
×
932
      }
933

934
      const remainingBufferLength = totalBufferLength - frameBegin - 4; // subtract 4 for payloadType (1-byte) and payloadLength (3-byte)
×
935
      // check if the data from this frame is already there (payload + 16-byte authTag)
936
      if (payloadLength + 16 > remainingBufferLength) {
×
937
        // Frame is fragmented, so we wait until we receive more
938
        this.frameBuffer = data.slice(frameBegin);
×
939
        break;
×
940
      }
941

942
      const payloadBegin = frameBegin + 4;
×
943
      const authTagBegin = payloadBegin + payloadLength;
×
944

945
      const header = data.slice(frameBegin, payloadBegin); // header is also authenticated using authTag
×
946
      const cipheredPayload = data.slice(payloadBegin, authTagBegin);
×
947
      const plaintextPayload = Buffer.alloc(payloadLength);
×
948
      const authTag = data.slice(authTagBegin, authTagBegin + 16);
×
949

950
      frameBegin = authTagBegin + 16; // move to next frame
×
951

952
      if (payloadType === 1) {
×
953
        const hdsFrame: HDSFrame = {
×
954
          header: header,
955
          cipheredPayload: cipheredPayload,
956
          authTag: authTag,
957
        };
958
        frames.push(hdsFrame);
×
959
      } else {
960
        debug("[%s] Encountered unknown payload type %d for payload: %s", this.remoteAddress, plaintextPayload.toString("hex"));
×
961
      }
962
    }
963

964
    return frames;
×
965
  }
966

967
  /**
968
   * @private file-private API
969
   */
970
  decryptHDSFrame(frame: HDSFrame, keyOverwrite?: Buffer): boolean {
971
    hapCrypto.writeUInt64LE(this.controllerToAccessoryNonce, this.controllerToAccessoryNonceBuffer, 0); // update nonce buffer
×
972

973
    const key = keyOverwrite || this.controllerToAccessoryEncryptionKey!;
×
974
    try {
×
975
      frame.plaintextPayload = hapCrypto.chacha20_poly1305_decryptAndVerify(key, this.controllerToAccessoryNonceBuffer,
×
976
        frame.header, frame.cipheredPayload, frame.authTag);
977
      this.controllerToAccessoryNonce++; // we had a successful encryption, increment the nonce
×
978
      return true;
×
979
    } catch (error) {
980
      // frame decryption or authentication failed. Could happen when our guess for a PreparedDataStreamSession is wrong
981
      return false;
×
982
    }
983
  }
984

985
  private decodePayloads(frames: HDSFrame[]) {
986
    const messages: DataStreamMessage[] = [];
×
987

988
    frames.forEach(frame => {
×
989
      const payload = frame.plaintextPayload;
×
990
      if (!payload) {
×
991
        throw new HDSConnectionError("Reached illegal state. Encountered HDSFrame with wasn't decrypted yet!", HDSConnectionErrorType.ILLEGAL_STATE);
×
992
      }
993

994
      const headerLength = payload.readUInt8(0);
×
995
      const messageLength = payload.length - headerLength - 1;
×
996

997
      const headerBegin = 1;
×
998
      const messageBegin = headerBegin + headerLength;
×
999

1000
      const headerPayload = new DataStreamReader(payload.slice(headerBegin, headerBegin + headerLength));
×
1001
      const messagePayload = new DataStreamReader(payload.slice(messageBegin, messageBegin + messageLength));
×
1002

1003
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
1004
      let headerDictionary: Record<any, any>;
1005
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
1006
      let messageDictionary: Record<any, any>;
1007
      try {
×
1008
        headerDictionary = DataStreamParser.decode(headerPayload);
×
1009
        headerPayload.finished();
×
1010
      } catch (error) {
1011
        debug("[%s] Failed to decode header payload: %s", this.remoteAddress, error.message);
×
1012
        return;
×
1013
      }
1014

1015
      try {
×
1016
        messageDictionary = DataStreamParser.decode(messagePayload);
×
1017
        messagePayload.finished();
×
1018
      } catch (error) {
1019
        debug("[%s] Failed to decode message payload: %s (header: %o)", this.remoteAddress, error.message, headerDictionary);
×
1020
        return;
×
1021
      }
1022

1023
      let type: MessageType;
1024
      const protocol: string = headerDictionary.protocol;
×
1025
      let topic: string;
1026
      let id: number | undefined = undefined;
×
1027
      let status: HDSStatus | undefined = undefined;
×
1028

1029
      if (headerDictionary.event !== undefined) {
×
1030
        type = MessageType.EVENT;
×
1031
        topic = headerDictionary.event;
×
1032
      } else if (headerDictionary.request !== undefined) {
×
1033
        type = MessageType.REQUEST;
×
1034
        topic = headerDictionary.request;
×
1035
        id = headerDictionary.id;
×
1036
      } else if (headerDictionary.response !== undefined) {
×
1037
        type = MessageType.RESPONSE;
×
1038
        topic = headerDictionary.response;
×
1039
        id = headerDictionary.id;
×
1040
        status = headerDictionary.status;
×
1041
      } else {
1042
        debug("[%s] Encountered unknown payload header format: %o (message: %o)", this.remoteAddress, headerDictionary, messageDictionary);
×
1043
        return;
×
1044
      }
1045

1046
      const message: DataStreamMessage = {
×
1047
        type: type,
1048
        protocol: protocol,
1049
        topic: topic,
1050
        id: id,
1051
        status: status,
1052
        message: messageDictionary,
1053
      };
1054
      messages.push(message);
×
1055
    });
1056

1057
    return messages;
×
1058
  }
1059

1060
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1061
  private sendHDSFrame(header: Record<any, any>, message: Record<any, any>) {
1062
    if (this.state >= ConnectionState.CLOSING) {
×
1063
      throw new HDSConnectionError("Cannot send message on closing/closed socket!", HDSConnectionErrorType.CLOSED_SOCKET);
×
1064
    }
1065

1066
    const headerWriter = new DataStreamWriter();
×
1067
    const messageWriter = new DataStreamWriter();
×
1068

1069
    DataStreamParser.encode(header, headerWriter);
×
1070
    DataStreamParser.encode(message, messageWriter);
×
1071

1072

1073
    const payloadHeaderBuffer = Buffer.alloc(1);
×
1074
    payloadHeaderBuffer.writeUInt8(headerWriter.length(), 0);
×
1075
    const payloadBuffer = Buffer.concat([payloadHeaderBuffer, headerWriter.getData(), messageWriter.getData()]);
×
1076
    if (payloadBuffer.length > DataStreamConnection.MAX_PAYLOAD_LENGTH) {
×
1077
      throw new HDSConnectionError(
×
1078
        "Tried sending payload with length larger than the maximum allowed for data stream",
1079
        HDSConnectionErrorType.MAX_PAYLOAD_LENGTH,
1080
      );
1081
    }
1082

1083
    const frameTypeBuffer = Buffer.alloc(1);
×
1084
    frameTypeBuffer.writeUInt8(1, 0);
×
1085
    let frameLengthBuffer = Buffer.alloc(4);
×
1086
    frameLengthBuffer.writeUInt32BE(payloadBuffer.length, 0);
×
1087
    frameLengthBuffer = frameLengthBuffer.slice(1, 4); // a bit hacky but the only real way to write 24-bit int in node
×
1088

1089
    const frameHeader = Buffer.concat([frameTypeBuffer, frameLengthBuffer]);
×
1090

1091
    hapCrypto.writeUInt64LE(this.accessoryToControllerNonce++, this.accessoryToControllerNonceBuffer);
×
1092
    const encrypted = hapCrypto.chacha20_poly1305_encryptAndSeal(
×
1093
      this.accessoryToControllerEncryptionKey!,
1094
      this.accessoryToControllerNonceBuffer,
1095
      frameHeader,
1096
      payloadBuffer,
1097
    );
1098

1099
    this.socket.write(Buffer.concat([frameHeader, encrypted.ciphertext, encrypted.authTag]));
×
1100

1101
    /* Useful for debugging outgoing packages and detecting encoding errors
1102
        console.log("SENT DATA: " + payloadBuffer.toString("hex"));
1103
        const frame: HDSFrame = {
1104
            header: frameHeader,
1105
            plaintextPayload: payloadBuffer,
1106
            cipheredPayload: cipheredPayload,
1107
            authTag: authTag,
1108
        };
1109
        const sentMessage = this.decodePayloads([frame])[0];
1110
        console.log("Sent message: " + JSON.stringify(sentMessage, null, 4));
1111
        //*/
1112
  }
1113

1114
  close(): void { // closing socket by sending FIN packet; incoming data will be ignored from that point on
1115
    if (this.state >= ConnectionState.CLOSING) {
×
1116
      return; // connection is already closing/closed
×
1117
    }
1118

1119
    this.state = ConnectionState.CLOSING;
×
1120
    this.socket.end();
×
1121
  }
1122

1123
  isConsideredClosed(): boolean {
1124
    return this.state >= ConnectionState.CLOSING;
×
1125
  }
1126

1127
  private onHAPSessionClosed() {
1128
    // If the hap connection is closed it is probably also a good idea to close the data stream connection
1129
    debug("[%s] HAP connection disconnected. Also closing DataStream connection now.", this.remoteAddress);
×
1130
    this.close();
×
1131
  }
1132

1133
  private onSocketError(error: Error) {
1134
    debug("[%s] Encountered socket error: %s", this.remoteAddress, error.message);
×
1135
    // onSocketClose will be called next
1136
  }
1137

1138
  private onSocketClose() {
1139
    // this instance is now considered completely dead
1140
    this.state = ConnectionState.CLOSED;
×
1141
    this.emit(DataStreamConnectionEvent.CLOSED);
×
1142

1143
    this.connection?.removeListener(HAPConnectionEvent.CLOSED, this.hapConnectionClosedListener);
×
1144
    this.connection?.setMaxListeners(this.connection.getMaxListeners() - 1);
×
1145
    this.removeAllListeners();
×
1146
  }
1147

1148
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc