@mega-yfue/eufy-sdk 0.2.0 → 0.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -15139,6 +15139,7 @@ var LIVE_START_RETRANSMIT_MS = 150;
15139
15139
  var LIVE_START_ACK_DEADLINE_MS = 3e3;
15140
15140
  var SEQUENCE_LOOKBACK = 32768;
15141
15141
  var STALE_RETRANSMIT_DEPTH = 1024;
15142
+ var PUNCH_PROBE_SOCKETS = 7;
15142
15143
  var MAX_TRACED_DATAGRAM_GAPS = 8;
15143
15144
  var CMD_SET_PAYLOAD = 1350;
15144
15145
  var CMD_NOTIFY_PAYLOAD = 1351;
@@ -15200,8 +15201,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
15200
15201
  lookupTimer;
15201
15202
  heartbeatTimer;
15202
15203
  connectTimer;
15203
- /** Our own bound host:port, self-reported inside LOOKUP_WITH_KEY requests (see sendLookups). */
15204
- selfAddress;
15204
+ /** Extra sockets registered alongside {@link socket} while connecting; see {@link PUNCH_PROBE_SOCKETS}. */
15205
+ probeSockets = [];
15206
+ /** Cloud lookup inputs resolved once for a connection. */
15207
+ cloudLookup;
15208
+ /** Local outbound IPv4 reported inside LOOKUP_WITH_KEY requests. */
15209
+ selfHost;
15205
15210
  /** In-flight multi-datagram frame per data channel (see onData). */
15206
15211
  pendingByDataType = /* @__PURE__ */ new Map();
15207
15212
  /** Last datagram sequence number seen per dataType — used to detect a lost/reordered datagram
@@ -15496,7 +15501,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
15496
15501
  }
15497
15502
  const socket = dgram.createSocket("udp4");
15498
15503
  this.socket = socket;
15499
- socket.on("message", (msg, rinfo) => this.onMessage(msg, rinfo));
15504
+ socket.on("message", (msg, rinfo) => this.onMessage(msg, rinfo, socket));
15500
15505
  socket.on("error", (e) => this.emit("error", e));
15501
15506
  await new Promise((resolve) => {
15502
15507
  socket.bind(() => {
@@ -15507,15 +15512,20 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
15507
15512
  resolve();
15508
15513
  });
15509
15514
  });
15510
- const boundPort = socket.address().port;
15511
15515
  void _P2PSession.detectLocalIp().then((host) => {
15512
15516
  if (host && !this.closed)
15513
- this.selfAddress = { host, port: boundPort };
15517
+ this.selfHost = host;
15514
15518
  });
15519
+ const { dskKey, cloudAddresses } = this.cfg;
15520
+ this.cloudLookup = dskKey && cloudAddresses?.length ? { key: dskKey, addresses: cloudAddresses } : void 0;
15521
+ if (this.cloudLookup)
15522
+ await this.startPunchProbes();
15523
+ if (this.closed)
15524
+ return;
15515
15525
  this.trace({
15516
15526
  phase: "lookup-channels",
15517
15527
  local: !this.cfg.noBroadcast || this.cfg.localAddress !== void 0,
15518
- cloud: this.cfg.dskKey !== void 0 && (this.cfg.cloudAddresses?.length ?? 0) > 0
15528
+ cloud: this.cloudLookup !== void 0
15519
15529
  });
15520
15530
  this.sendLookups();
15521
15531
  this.lookupTimer = setInterval(() => this.sendLookups(), LOOKUP_RETRY_MS);
@@ -15593,14 +15603,20 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
15593
15603
  this.send({ host: "255.255.255.255", port: LOCAL_LOOKUP_PORT }, RequestMessageType.LOCAL_LOOKUP, localPayload);
15594
15604
  if (this.cfg.localAddress)
15595
15605
  this.send({ host: this.cfg.localAddress, port: LOCAL_LOOKUP_PORT }, RequestMessageType.LOCAL_LOOKUP, localPayload);
15596
- if (this.cfg.dskKey && this.cfg.cloudAddresses?.length) {
15597
- const payload = this.selfAddress ? buildLookupWithKeyPayload(this.cfg.p2pDid, this.selfAddress.host, this.selfAddress.port, this.cfg.dskKey) : buildLookupWithKeyPayload2(this.cfg.p2pDid, this.cfg.dskKey);
15598
- const type = this.selfAddress ? RequestMessageType.LOOKUP_WITH_KEY : RequestMessageType.LOOKUP_WITH_KEY2;
15599
- for (const addr of this.cfg.cloudAddresses)
15600
- this.send(addr, type, payload);
15601
- this.logger.debug(`[p2p] ${this.cfg.stationSn} sendLookups: cloud -> ${this.cfg.cloudAddresses.map((a) => `${a.host}:${a.port}`).join(", ")} self=${this.selfAddress?.host}:${this.selfAddress?.port}`);
15606
+ const lookup2 = this.cloudLookup;
15607
+ if (lookup2) {
15608
+ for (const socket of [this.socket, ...this.probeSockets])
15609
+ this.sendCloudLookup(socket, lookup2);
15610
+ this.logger.debug(`[p2p] ${this.cfg.stationSn} sendLookups: cloud -> ${lookup2.addresses.map((a) => `${a.host}:${a.port}`).join(", ")} self=${this.selfHost ?? "unknown"}`);
15602
15611
  }
15603
15612
  }
15613
+ /** Register a bound socket's own port with each configured cloud lookup address. */
15614
+ sendCloudLookup(socket, lookup2) {
15615
+ const payload = this.selfHost ? buildLookupWithKeyPayload(this.cfg.p2pDid, this.selfHost, socket.address().port, lookup2.key) : buildLookupWithKeyPayload2(this.cfg.p2pDid, lookup2.key);
15616
+ const type = this.selfHost ? RequestMessageType.LOOKUP_WITH_KEY : RequestMessageType.LOOKUP_WITH_KEY2;
15617
+ for (const addr of lookup2.addresses)
15618
+ this.send(addr, type, payload, socket);
15619
+ }
15604
15620
  /**
15605
15621
  * Route one inbound UDP datagram by its message type, tracing every non-DATA one and any type this session
15606
15622
  * does not model.
@@ -15612,28 +15628,30 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
15612
15628
  * recur across later probes of it. Correlate an unmodelled type against a WORKING session before reading it
15613
15629
  * as a cause.
15614
15630
  */
15615
- onMessage(msg, rinfo) {
15631
+ onMessage(msg, rinfo, socket = this.socket) {
15632
+ if (!socket)
15633
+ return;
15616
15634
  if (!hasHeader(msg, ResponseMessageType.DATA)) {
15617
15635
  this.logger.debug(`[p2p] ${this.cfg.stationSn} <<< ${rinfo.address}:${rinfo.port} header=${msg.subarray(0, 2).toString("hex")} len=${msg.length}`);
15618
15636
  }
15619
15637
  if (hasHeader(msg, ResponseMessageType.LOCAL_LOOKUP_RESP)) {
15620
15638
  if (!this.connected)
15621
- this.beginCheckCam({ host: rinfo.address, port: rinfo.port });
15639
+ this.beginCheckCam({ host: rinfo.address, port: rinfo.port }, socket);
15622
15640
  } else if (hasHeader(msg, ResponseMessageType.LOOKUP_ADDR) || hasHeader(msg, ResponseMessageType.LOOKUP_ADDR2)) {
15623
15641
  if (!this.connected) {
15624
15642
  const addr = parseLookupAddr(msg);
15625
15643
  this.logger.debug(`[p2p] ${this.cfg.stationSn} LOOKUP_ADDR -> ${addr.host}:${addr.port}`);
15626
15644
  if (addr.host !== "0.0.0.0")
15627
- this.beginCheckCam(addr);
15645
+ this.beginCheckCam(addr, socket);
15628
15646
  }
15629
15647
  } else if (hasHeader(msg, ResponseMessageType.CAM_ID) || hasHeader(msg, ResponseMessageType.TURN_SERVER_CAM_ID)) {
15630
- this.onConnected({ host: rinfo.address, port: rinfo.port });
15648
+ this.onConnected({ host: rinfo.address, port: rinfo.port }, socket);
15631
15649
  } else if (hasHeader(msg, ResponseMessageType.PONG)) {
15632
15650
  this.lastPongData = msg.length > 4 ? msg.subarray(4) : void 0;
15633
15651
  this.lastPongAt = Date.now();
15634
15652
  this.pathStaleTraced = false;
15635
15653
  } else if (hasHeader(msg, ResponseMessageType.PING)) {
15636
- this.send({ host: rinfo.address, port: rinfo.port }, RequestMessageType.PONG);
15654
+ this.send({ host: rinfo.address, port: rinfo.port }, RequestMessageType.PONG, void 0, socket);
15637
15655
  } else if (hasHeader(msg, ResponseMessageType.ACK)) {
15638
15656
  this.onAck(msg);
15639
15657
  } else if (hasHeader(msg, ResponseMessageType.DATA)) {
@@ -15643,17 +15661,67 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
15643
15661
  this.logger.debug(`[p2p] ${this.cfg.stationSn} UNHANDLED payload hex: ${msg.toString("hex")}`);
15644
15662
  }
15645
15663
  }
15646
- beginCheckCam(addr) {
15664
+ beginCheckCam(addr, socket) {
15647
15665
  this.logger.debug(`[p2p] ${this.cfg.stationSn} beginCheckCam -> ${addr.host}:${addr.port} (+/-3)`);
15648
15666
  const payload = buildCheckCamPayload(this.cfg.p2pDid);
15649
- this.send(addr, RequestMessageType.CHECK_CAM, payload);
15667
+ this.send(addr, RequestMessageType.CHECK_CAM, payload, socket);
15650
15668
  for (let p = addr.port - 3; p <= addr.port + 3; p++)
15651
15669
  if (p !== addr.port && p > 0)
15652
- this.send({ host: addr.host, port: p }, RequestMessageType.CHECK_CAM, payload);
15670
+ this.send({ host: addr.host, port: p }, RequestMessageType.CHECK_CAM, payload, socket);
15671
+ }
15672
+ /** Bind additional UDP source ports before the first cloud lookup. */
15673
+ async startPunchProbes() {
15674
+ await Promise.all(Array.from({ length: PUNCH_PROBE_SOCKETS }, async () => {
15675
+ const probe = dgram.createSocket("udp4");
15676
+ probe.on("error", (error) => this.logger.warn("[p2p] lookup socket error", error.message));
15677
+ probe.on("message", (msg, rinfo) => this.onMessage(msg, rinfo, probe));
15678
+ this.probeSockets.push(probe);
15679
+ await new Promise((resolve) => {
15680
+ let settled = false;
15681
+ const finish = (bound) => {
15682
+ if (settled)
15683
+ return;
15684
+ settled = true;
15685
+ probe.off("error", onError);
15686
+ probe.off("close", onClose);
15687
+ if (!bound) {
15688
+ this.probeSockets = this.probeSockets.filter((socket) => socket !== probe);
15689
+ try {
15690
+ probe.close();
15691
+ } catch {
15692
+ }
15693
+ }
15694
+ resolve();
15695
+ };
15696
+ const onError = () => finish(false);
15697
+ const onClose = () => finish(false);
15698
+ probe.once("error", onError);
15699
+ probe.once("close", onClose);
15700
+ probe.bind(() => finish(true));
15701
+ });
15702
+ }));
15653
15703
  }
15654
- onConnected(addr) {
15704
+ /** Close all lookup sockets except the one that completed the handshake. */
15705
+ stopPunchProbes(winner) {
15706
+ for (const probe of this.probeSockets) {
15707
+ if (probe === winner)
15708
+ continue;
15709
+ probe.removeAllListeners("message");
15710
+ probe.close();
15711
+ }
15712
+ this.probeSockets = [];
15713
+ }
15714
+ onConnected(addr, socket) {
15655
15715
  if (this.connected)
15656
15716
  return;
15717
+ this.stopPunchProbes(socket);
15718
+ if (this.socket !== socket) {
15719
+ this.socket?.removeAllListeners();
15720
+ this.socket?.close();
15721
+ socket.removeAllListeners("error");
15722
+ socket.on("error", (error) => this.emit("error", error));
15723
+ this.socket = socket;
15724
+ }
15657
15725
  this.connected = true;
15658
15726
  this.connectedAtMs = Date.now();
15659
15727
  this.connecting = false;
@@ -16540,11 +16608,11 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16540
16608
  }
16541
16609
  this.emit("data", frame);
16542
16610
  }
16543
- send(addr, type, payload) {
16544
- if (!this.socket)
16611
+ send(addr, type, payload, socket = this.socket) {
16612
+ if (!socket)
16545
16613
  return;
16546
16614
  const packet = frameMessage(type, payload);
16547
- this.socket.send(packet, addr.port, addr.host, (err) => {
16615
+ socket.send(packet, addr.port, addr.host, (err) => {
16548
16616
  if (err)
16549
16617
  this.logger.warn(`[p2p] send err ${addr.host}:${addr.port}`, err.message);
16550
16618
  });
@@ -16553,6 +16621,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16553
16621
  if (this.closed)
16554
16622
  return;
16555
16623
  this.closed = true;
16624
+ this.stopPunchProbes();
16556
16625
  this.connectionGeneration += 1;
16557
16626
  this.level2Key = void 0;
16558
16627
  this.level2Seq = 0;