@apocaliss92/nodelink-js 0.6.0 → 0.6.2

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.
@@ -3,8 +3,8 @@ import {
3
3
  BaichuanRtspServer,
4
4
  ReolinkBaichuanApi,
5
5
  autoDetectDeviceType
6
- } from "../chunk-7HSTETZR.js";
7
- import "../chunk-XDVBNZGR.js";
6
+ } from "../chunk-D4TKRGUP.js";
7
+ import "../chunk-IQVVVSXO.js";
8
8
  import {
9
9
  __require
10
10
  } from "../chunk-MZUSWKF3.js";
package/dist/index.cjs CHANGED
@@ -268,6 +268,41 @@ var init_crypto = __esm({
268
268
  });
269
269
 
270
270
  // src/protocol/xml.ts
271
+ function xmlTextRe(tag) {
272
+ let re = xmlTextReCache.get(tag);
273
+ if (re === void 0) {
274
+ re = new RegExp(`<${tag}>([^<]*)</${tag}>`);
275
+ xmlTextReCache.set(tag, re);
276
+ }
277
+ return re;
278
+ }
279
+ function xmlTagRe(tag) {
280
+ let re = xmlTagReCache.get(tag);
281
+ if (re === void 0) {
282
+ re = new RegExp(`<${tag}>[^<]*</${tag}>`);
283
+ xmlTagReCache.set(tag, re);
284
+ }
285
+ return re;
286
+ }
287
+ function xmlNestedRe(parent, child) {
288
+ const key = `${parent}\0${child}`;
289
+ let re = xmlNestedReCache.get(key);
290
+ if (re === void 0) {
291
+ re = new RegExp(
292
+ `(<${parent}[^>]*>[\\s\\S]*?<${child}>)[^<]*(</${child}>[\\s\\S]*?</${parent}>)`
293
+ );
294
+ xmlNestedReCache.set(key, re);
295
+ }
296
+ return re;
297
+ }
298
+ function xmlStreamBlockRe(streamTag) {
299
+ let re = xmlStreamBlockReCache.get(streamTag);
300
+ if (re === void 0) {
301
+ re = new RegExp(`(<${streamTag}[^>]*>)([\\s\\S]*?)(</${streamTag}>)`);
302
+ xmlStreamBlockReCache.set(streamTag, re);
303
+ }
304
+ return re;
305
+ }
271
306
  function xmlEscape(text) {
272
307
  if (text === void 0 || text === null || typeof text !== "string") {
273
308
  const error = new Error(
@@ -388,8 +423,7 @@ function buildPreviewStopXmlV11(params) {
388
423
  </body>`;
389
424
  }
390
425
  function getXmlText(xml, tagName) {
391
- const re = new RegExp(`<${tagName}>([^<]*)</${tagName}>`);
392
- const m = re.exec(xml);
426
+ const m = xmlTextRe(tagName).exec(xml);
393
427
  return m?.[1];
394
428
  }
395
429
  function buildPtzControlXml(channelId, command, speed) {
@@ -493,13 +527,12 @@ ${xml}`;
493
527
  function applyXmlTagPatch(xml, tag, value) {
494
528
  if (value === void 0) return xml;
495
529
  const v = typeof value === "boolean" ? value ? 1 : 0 : value;
496
- const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
497
- return xml.replace(re, `<${tag}>${v}</${tag}>`);
530
+ return xml.replace(xmlTagRe(tag), `<${tag}>${v}</${tag}>`);
498
531
  }
499
532
  function upsertXmlTag(xml, tag, value) {
500
533
  if (value === void 0) return xml;
501
534
  const v = typeof value === "boolean" ? value ? 1 : 0 : value;
502
- const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
535
+ const re = xmlTagRe(tag);
503
536
  if (re.test(xml)) {
504
537
  return xml.replace(re, `<${tag}>${v}</${tag}>`);
505
538
  }
@@ -508,16 +541,11 @@ function upsertXmlTag(xml, tag, value) {
508
541
  function patchNestedTag(xml, parent, child, value) {
509
542
  if (value === void 0) return xml;
510
543
  const v = typeof value === "boolean" ? value ? 1 : 0 : value;
511
- const re = new RegExp(
512
- `(<${parent}[^>]*>[\\s\\S]*?<${child}>)[^<]*(</${child}>[\\s\\S]*?</${parent}>)`
513
- );
514
- return xml.replace(re, `$1${v}$2`);
544
+ return xml.replace(xmlNestedRe(parent, child), `$1${v}$2`);
515
545
  }
516
546
  function applyStreamPatch(xml, streamTag, patch) {
517
547
  if (!patch) return xml;
518
- const re = new RegExp(
519
- `(<${streamTag}[^>]*>)([\\s\\S]*?)(</${streamTag}>)`
520
- );
548
+ const re = xmlStreamBlockRe(streamTag);
521
549
  return xml.replace(re, (_match, open, body, close) => {
522
550
  let next = body;
523
551
  if (patch.audio !== void 0) {
@@ -547,10 +575,9 @@ function applyStreamPatch(xml, streamTag, patch) {
547
575
  next = upsertXmlTag(next, "encoderProfile", patch.encoderProfile);
548
576
  }
549
577
  if (patch.gop !== void 0) {
550
- const gopBlockRe = /(<gop[^>]*>)([\s\S]*?)(<\/gop>)/;
551
- if (gopBlockRe.test(next)) {
578
+ if (GOP_BLOCK_RE.test(next)) {
552
579
  next = next.replace(
553
- gopBlockRe,
580
+ GOP_BLOCK_RE,
554
581
  (_m, gOpen, gBody, gClose) => `${gOpen}${applyXmlTagPatch(gBody, "cur", patch.gop)}${gClose}`
555
582
  );
556
583
  } else {
@@ -579,10 +606,15 @@ function buildAbilityInfoExtensionXml(username) {
579
606
  <token>system, streaming, PTZ, IO, security, replay, disk, network, alarm, record, video, image</token>
580
607
  </Extension>`;
581
608
  }
582
- var XML_HEADER;
609
+ var xmlTextReCache, xmlTagReCache, xmlNestedReCache, GOP_BLOCK_RE, xmlStreamBlockReCache, XML_HEADER;
583
610
  var init_xml = __esm({
584
611
  "src/protocol/xml.ts"() {
585
612
  "use strict";
613
+ xmlTextReCache = /* @__PURE__ */ new Map();
614
+ xmlTagReCache = /* @__PURE__ */ new Map();
615
+ xmlNestedReCache = /* @__PURE__ */ new Map();
616
+ GOP_BLOCK_RE = /(<gop[^>]*>)([\s\S]*?)(<\/gop>)/;
617
+ xmlStreamBlockReCache = /* @__PURE__ */ new Map();
586
618
  XML_HEADER = `<?xml version="1.0" encoding="UTF-8" ?>`;
587
619
  }
588
620
  });
@@ -8625,35 +8657,88 @@ function decodeHeader(buf) {
8625
8657
  return { header, headerLen, messageKey };
8626
8658
  }
8627
8659
  var BaichuanFrameParser = class {
8660
+ /** Retained-but-unconsumed contiguous bytes from previous push() calls. */
8628
8661
  buffer = Buffer.alloc(0);
8662
+ /** Chunks received since the last materialization, not yet concatenated. */
8663
+ pending = [];
8664
+ /** Total bytes held in `pending` (kept in sync to avoid re-summing). */
8665
+ pendingLen = 0;
8666
+ /**
8667
+ * Total contiguous bytes (`buffer` + `pending`) required before the next
8668
+ * parse attempt can make progress. While buffered bytes stay below this,
8669
+ * incoming chunks are merely stashed in `pending` with no copy. This is
8670
+ * the mechanism that turns the worst case (a large frame fragmented over
8671
+ * many small TCP chunks) from O(n²) into O(n): we concatenate once, when
8672
+ * enough bytes have arrived, instead of on every chunk.
8673
+ *
8674
+ * Starts at 4 — the minimum needed to inspect the magic header.
8675
+ */
8676
+ needed = 4;
8677
+ /**
8678
+ * Collapse `this.buffer` + all `pending` chunks into a single contiguous
8679
+ * buffer. The retained leftover is copied at most once per materialize(),
8680
+ * and materialize() only runs when `needed` bytes are available — so a
8681
+ * fragmented frame is assembled with a single concat, not one per chunk.
8682
+ */
8683
+ materialize() {
8684
+ if (this.pendingLen === 0) return;
8685
+ if (this.buffer.length === 0 && this.pending.length === 1) {
8686
+ this.buffer = this.pending[0];
8687
+ } else {
8688
+ const parts = this.buffer.length === 0 ? this.pending : [this.buffer, ...this.pending];
8689
+ this.buffer = Buffer.concat(parts);
8690
+ }
8691
+ this.pending = [];
8692
+ this.pendingLen = 0;
8693
+ }
8694
+ /** Total buffered bytes, whether materialized or still pending. */
8695
+ get available() {
8696
+ return this.buffer.length + this.pendingLen;
8697
+ }
8629
8698
  push(chunk) {
8630
8699
  if (chunk.length === 0) return [];
8631
- const c = chunk;
8632
- this.buffer = this.buffer.length === 0 ? c : Buffer.concat([this.buffer, c]);
8700
+ this.pending.push(chunk);
8701
+ this.pendingLen += chunk.length;
8702
+ if (this.available < this.needed) return [];
8703
+ this.materialize();
8633
8704
  const out = [];
8634
8705
  while (true) {
8635
- if (this.buffer.length < 4) break;
8706
+ if (this.buffer.length < 4) {
8707
+ this.needed = 4;
8708
+ break;
8709
+ }
8636
8710
  if (!this.buffer.subarray(0, 4).equals(BC_MAGIC) && !this.buffer.subarray(0, 4).equals(BC_MAGIC_REV)) {
8637
8711
  const idx = this.buffer.indexOf(BC_MAGIC);
8638
8712
  const idxRev = this.buffer.indexOf(BC_MAGIC_REV);
8639
8713
  const next = idx === -1 ? idxRev : idxRev === -1 ? idx : Math.min(idx, idxRev);
8640
8714
  if (next === -1) {
8641
8715
  this.buffer = this.buffer.subarray(Math.max(0, this.buffer.length - 3));
8716
+ this.needed = 4;
8642
8717
  break;
8643
8718
  }
8644
8719
  this.buffer = this.buffer.subarray(next);
8645
- if (this.buffer.length < 20) break;
8720
+ if (this.buffer.length < 20) {
8721
+ this.needed = 20;
8722
+ break;
8723
+ }
8724
+ }
8725
+ if (this.buffer.length < 20) {
8726
+ this.needed = 20;
8727
+ break;
8646
8728
  }
8647
- if (this.buffer.length < 20) break;
8648
8729
  let headerInfo;
8649
8730
  try {
8650
8731
  headerInfo = decodeHeader(this.buffer);
8651
8732
  } catch {
8733
+ this.needed = 24;
8652
8734
  break;
8653
8735
  }
8654
8736
  const { header, headerLen, messageKey } = headerInfo;
8655
8737
  const frameLen = headerLen + header.bodyLen;
8656
- if (this.buffer.length < frameLen) break;
8738
+ if (this.buffer.length < frameLen) {
8739
+ this.needed = frameLen;
8740
+ break;
8741
+ }
8657
8742
  const raw = this.buffer.subarray(0, frameLen);
8658
8743
  const body = raw.subarray(headerLen);
8659
8744
  let extLen = 0;
@@ -8665,6 +8750,7 @@ var BaichuanFrameParser = class {
8665
8750
  const payload = body.subarray(extLen);
8666
8751
  out.push({ header, body, extension, payload, messageKey, raw });
8667
8752
  this.buffer = this.buffer.subarray(frameLen);
8753
+ this.needed = 4;
8668
8754
  }
8669
8755
  return out;
8670
8756
  }
@@ -9047,12 +9133,28 @@ async function getServerBinding(uid, options = {}) {
9047
9133
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
9048
9134
  const logger = options.logger;
9049
9135
  if (typeof fetchImpl !== "function") {
9050
- logger?.debug?.(
9136
+ logger?.log?.(
9051
9137
  `[server-binding] global fetch unavailable; skipping cloud lookup`
9052
9138
  );
9053
9139
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9054
9140
  return void 0;
9055
9141
  }
9142
+ try {
9143
+ const apiHostname = new URL(baseUrl).hostname;
9144
+ const dns2 = await import("dns/promises");
9145
+ const answers = await dns2.lookup(apiHostname, { family: 4, all: true });
9146
+ const sinkholed = answers.find(
9147
+ (a) => a.address?.startsWith("127.") || a.address === "0.0.0.0" || a.address?.startsWith("10.") || a.address?.startsWith("192.168.") || /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(a.address ?? "")
9148
+ );
9149
+ if (sinkholed) {
9150
+ logger?.log?.(
9151
+ `[server-binding] ${uid}: DNS for ${apiHostname} resolves to ${sinkholed.address} (sinkhole / /etc/hosts override). Cloud directory unreachable \u2014 falling back to the 22-hostname P2P sweep. Whitelist ${apiHostname} to enable.`
9152
+ );
9153
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9154
+ return void 0;
9155
+ }
9156
+ } catch {
9157
+ }
9056
9158
  const url = `${baseUrl}/devices/${encodeURIComponent(uid)}/server-binding?language=${encodeURIComponent(language)}`;
9057
9159
  const controller = new AbortController();
9058
9160
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -9063,8 +9165,8 @@ async function getServerBinding(uid, options = {}) {
9063
9165
  headers: { Accept: "application/json" }
9064
9166
  });
9065
9167
  if (!res.ok) {
9066
- logger?.debug?.(
9067
- `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText}`
9168
+ logger?.log?.(
9169
+ `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
9068
9170
  );
9069
9171
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9070
9172
  return void 0;
@@ -9072,8 +9174,15 @@ async function getServerBinding(uid, options = {}) {
9072
9174
  const json = await res.json();
9073
9175
  const parsed = parseServerBindingResponse(json);
9074
9176
  if (!parsed) {
9075
- logger?.debug?.(
9076
- `[server-binding] ${uid}: response shape did not match expectations`
9177
+ logger?.log?.(
9178
+ `[server-binding] ${uid}: response shape did not match expectations (Reolink schema change?)`
9179
+ );
9180
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9181
+ return void 0;
9182
+ }
9183
+ if (parsed.availableZones.length === 0) {
9184
+ logger?.log?.(
9185
+ `[server-binding] ${uid}: cloud returned 0 zones \u2014 UID not registered with Reolink cloud (or wrong region)`
9077
9186
  );
9078
9187
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9079
9188
  return void 0;
@@ -9092,9 +9201,23 @@ async function getServerBinding(uid, options = {}) {
9092
9201
  );
9093
9202
  return parsed;
9094
9203
  } catch (e) {
9095
- logger?.debug?.(
9096
- `[server-binding] ${uid}: ${e?.message ?? String(e)}`
9097
- );
9204
+ const msg = e?.message ?? String(e);
9205
+ const errName = e?.name;
9206
+ if (errName === "AbortError" || msg.includes("aborted")) {
9207
+ logger?.log?.(
9208
+ `[server-binding] ${uid}: timed out after ${timeoutMs}ms (cloud unreachable)`
9209
+ );
9210
+ } else if (msg.includes("ENOTFOUND") || msg.includes("EAI_AGAIN")) {
9211
+ logger?.log?.(
9212
+ `[server-binding] ${uid}: DNS failed (${msg}) \u2014 apis.reolink.com may be blocked at resolver`
9213
+ );
9214
+ } else if (msg.includes("ECONNREFUSED") || msg.includes("EHOSTUNREACH") || msg.includes("ENETUNREACH")) {
9215
+ logger?.log?.(
9216
+ `[server-binding] ${uid}: network unreachable (${msg}) \u2014 cloud port blocked`
9217
+ );
9218
+ } else {
9219
+ logger?.log?.(`[server-binding] ${uid}: fetch failed \u2014 ${msg}`);
9220
+ }
9098
9221
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9099
9222
  return void 0;
9100
9223
  } finally {
@@ -9530,12 +9653,19 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9530
9653
  const tid = (Math.floor(Math.random() * 2147483647) | 0) >>> 0;
9531
9654
  const xml = buildC2mQ({ uid });
9532
9655
  const pkt = encodeDiscoveryPacket(tid, xml);
9656
+ const counters = { sentBytes: 0, rxBytes: 0 };
9533
9657
  return await new Promise((resolve, reject) => {
9534
9658
  const deadline = setTimeout(() => {
9535
9659
  cleanup();
9536
- reject(new Error(`P2P UID lookup timeout (${dest.host}:${dest.port})`));
9660
+ const err = new Error(
9661
+ `P2P UID lookup timeout (${dest.host}:${dest.port}) \u2014 sent=${counters.sentBytes}B rx=${counters.rxBytes}B`
9662
+ );
9663
+ err.sentBytes = counters.sentBytes;
9664
+ err.rxBytes = counters.rxBytes;
9665
+ reject(err);
9537
9666
  }, timeoutMs);
9538
9667
  const onMsg = (msg) => {
9668
+ counters.rxBytes += msg.length;
9539
9669
  try {
9540
9670
  const p = decodeBcUdpPacket(msg);
9541
9671
  if (p.kind !== "discovery") return;
@@ -9543,13 +9673,19 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9543
9673
  const qr = parseM2cQr(p.xml);
9544
9674
  if (!qr?.reg || !qr?.relay) return;
9545
9675
  cleanup();
9546
- resolve({ reg: qr.reg, relay: qr.relay });
9676
+ resolve({
9677
+ reg: qr.reg,
9678
+ relay: qr.relay,
9679
+ sentBytes: counters.sentBytes,
9680
+ rxBytes: counters.rxBytes
9681
+ });
9547
9682
  } catch {
9548
9683
  }
9549
9684
  };
9550
9685
  const send = () => {
9551
9686
  try {
9552
9687
  sock.send(pkt, dest.port, dest.host);
9688
+ counters.sentBytes += pkt.length;
9553
9689
  } catch {
9554
9690
  }
9555
9691
  };