@apocaliss92/nodelink-js 0.6.2 → 0.6.4
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/{chunk-D4TKRGUP.js → chunk-UL34MR4L.js} +153 -28
- package/dist/chunk-UL34MR4L.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +144 -27
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +155 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -2
- package/dist/index.d.ts +59 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-D4TKRGUP.js.map +0 -1
|
@@ -689,6 +689,7 @@ function readCache(uid, now) {
|
|
|
689
689
|
}
|
|
690
690
|
async function getServerBinding(uid, options = {}) {
|
|
691
691
|
if (!uid || typeof uid !== "string") return void 0;
|
|
692
|
+
uid = uid.toUpperCase();
|
|
692
693
|
const now = Date.now();
|
|
693
694
|
const cached = readCache(uid, now);
|
|
694
695
|
if (cached?.kind === "ok") return cached.response;
|
|
@@ -731,8 +732,14 @@ async function getServerBinding(uid, options = {}) {
|
|
|
731
732
|
headers: { Accept: "application/json" }
|
|
732
733
|
});
|
|
733
734
|
if (!res.ok) {
|
|
735
|
+
let bodyPreview;
|
|
736
|
+
try {
|
|
737
|
+
const text = await res.text();
|
|
738
|
+
bodyPreview = text.slice(0, 512).replace(/\s+/g, " ").trim();
|
|
739
|
+
} catch {
|
|
740
|
+
}
|
|
734
741
|
logger?.log?.(
|
|
735
|
-
`[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
|
|
742
|
+
`[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}` + (bodyPreview ? ` \u2014 body=${bodyPreview}` : "")
|
|
736
743
|
);
|
|
737
744
|
cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
|
|
738
745
|
return void 0;
|
|
@@ -848,6 +855,68 @@ function parseServerBindingResponse(raw) {
|
|
|
848
855
|
}
|
|
849
856
|
|
|
850
857
|
// src/bcudp/BcUdpStream.ts
|
|
858
|
+
async function probeEgressForHost(destHost, destPort) {
|
|
859
|
+
return await new Promise((resolve, reject) => {
|
|
860
|
+
const probe = dgram.createSocket("udp4");
|
|
861
|
+
let settled = false;
|
|
862
|
+
const finish = (err, out) => {
|
|
863
|
+
if (settled) return;
|
|
864
|
+
settled = true;
|
|
865
|
+
try {
|
|
866
|
+
probe.close();
|
|
867
|
+
} catch {
|
|
868
|
+
}
|
|
869
|
+
if (err || !out) reject(err ?? new Error("egress probe failed"));
|
|
870
|
+
else resolve(out);
|
|
871
|
+
};
|
|
872
|
+
probe.on("error", (e) => finish(e));
|
|
873
|
+
try {
|
|
874
|
+
probe.connect(destPort, destHost, () => {
|
|
875
|
+
try {
|
|
876
|
+
const a = probe.address();
|
|
877
|
+
if (typeof a === "string") return finish(new Error("probe address is string"));
|
|
878
|
+
finish(void 0, {
|
|
879
|
+
localAddress: a.address,
|
|
880
|
+
localPort: a.port
|
|
881
|
+
});
|
|
882
|
+
} catch (e) {
|
|
883
|
+
finish(e);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
} catch (e) {
|
|
887
|
+
finish(e);
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
function isSameSubnetAsAnyLocalIface(destHost, srcInfo) {
|
|
892
|
+
if (!/^\d+\.\d+\.\d+\.\d+$/.test(destHost)) return "unknown";
|
|
893
|
+
const dest = destHost.split(".").map((s) => Number(s));
|
|
894
|
+
if (dest.some((n) => !Number.isFinite(n) || n < 0 || n > 255))
|
|
895
|
+
return "unknown";
|
|
896
|
+
const ifaces = networkInterfaces();
|
|
897
|
+
let ownerSubnet;
|
|
898
|
+
for (const name of Object.keys(ifaces)) {
|
|
899
|
+
const entries = ifaces[name];
|
|
900
|
+
if (!entries) continue;
|
|
901
|
+
for (const e of entries) {
|
|
902
|
+
if (e.family !== "IPv4" || e.internal) continue;
|
|
903
|
+
if (e.address !== srcInfo.localAddress) continue;
|
|
904
|
+
const addr = e.address.split(".").map((s) => Number(s));
|
|
905
|
+
const mask = e.netmask.split(".").map((s) => Number(s));
|
|
906
|
+
if (addr.length !== 4 || mask.length !== 4 || addr.some((n) => !Number.isFinite(n)) || mask.some((n) => !Number.isFinite(n)))
|
|
907
|
+
continue;
|
|
908
|
+
ownerSubnet = { addr, mask };
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
if (ownerSubnet) break;
|
|
912
|
+
}
|
|
913
|
+
if (!ownerSubnet) return "unknown";
|
|
914
|
+
for (let i = 0; i < 4; i++) {
|
|
915
|
+
if ((ownerSubnet.addr[i] & ownerSubnet.mask[i]) !== (dest[i] & ownerSubnet.mask[i]))
|
|
916
|
+
return "mismatch";
|
|
917
|
+
}
|
|
918
|
+
return "same";
|
|
919
|
+
}
|
|
851
920
|
var AckLatency = class {
|
|
852
921
|
currentValues = [];
|
|
853
922
|
lastReceiveTime = null;
|
|
@@ -926,6 +995,16 @@ function isUnroutableForP2P(ip) {
|
|
|
926
995
|
var P2P_LOOKUP_PORT = 9999;
|
|
927
996
|
var P2P_MAX_WAIT_MS = 15e3;
|
|
928
997
|
var P2P_RESEND_WAIT_MS = 500;
|
|
998
|
+
var inflightP2pLookups = /* @__PURE__ */ new Map();
|
|
999
|
+
var cachedP2pLookups = /* @__PURE__ */ new Map();
|
|
1000
|
+
var negCachedP2pLookups = /* @__PURE__ */ new Map();
|
|
1001
|
+
var P2P_LOOKUP_CACHE_TTL_MS = 3e4;
|
|
1002
|
+
var P2P_LOOKUP_NEG_CACHE_TTL_MS = 15e3;
|
|
1003
|
+
function _clearP2pLookupDedupForTests() {
|
|
1004
|
+
inflightP2pLookups.clear();
|
|
1005
|
+
cachedP2pLookups.clear();
|
|
1006
|
+
negCachedP2pLookups.clear();
|
|
1007
|
+
}
|
|
929
1008
|
var BcUdpStream = class extends EventEmitter {
|
|
930
1009
|
opts;
|
|
931
1010
|
/**
|
|
@@ -1014,31 +1093,14 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1014
1093
|
});
|
|
1015
1094
|
sock.on("error", (e) => this.emit("error", e));
|
|
1016
1095
|
sock.on("close", () => this.emit("close"));
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
await new Promise((resolve, reject) => {
|
|
1026
|
-
sock.once("error", reject);
|
|
1027
|
-
sock.bind(port, "0.0.0.0", () => {
|
|
1028
|
-
sock.removeListener("error", reject);
|
|
1029
|
-
resolve();
|
|
1030
|
-
});
|
|
1031
|
-
});
|
|
1032
|
-
bound = true;
|
|
1033
|
-
break;
|
|
1034
|
-
} catch {
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
if (!bound) {
|
|
1038
|
-
await new Promise(
|
|
1039
|
-
(resolve) => sock.bind(0, "0.0.0.0", () => resolve())
|
|
1040
|
-
);
|
|
1041
|
-
}
|
|
1096
|
+
await new Promise((resolve, reject) => {
|
|
1097
|
+
const onErr = (e) => reject(e);
|
|
1098
|
+
sock.once("error", onErr);
|
|
1099
|
+
sock.bind(0, "0.0.0.0", () => {
|
|
1100
|
+
sock.removeListener("error", onErr);
|
|
1101
|
+
resolve();
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
1042
1104
|
if (this.opts.mode === "direct") {
|
|
1043
1105
|
this.remote = { host: this.opts.host, port: this.opts.port };
|
|
1044
1106
|
this.clientId = this.opts.clientId;
|
|
@@ -1109,6 +1171,49 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1109
1171
|
this.remote = { host: connected.rhost, port: connected.rport };
|
|
1110
1172
|
}
|
|
1111
1173
|
async p2pUidLookup(sock, uid) {
|
|
1174
|
+
const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
|
|
1175
|
+
const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
|
|
1176
|
+
const cached = cachedP2pLookups.get(uid);
|
|
1177
|
+
if (cached && cached.expires > Date.now()) {
|
|
1178
|
+
log(
|
|
1179
|
+
`UID=${shortUid} cached lookup hit (relay=${cached.result.relay.ip}:${cached.result.relay.port})`
|
|
1180
|
+
);
|
|
1181
|
+
return cached.result;
|
|
1182
|
+
}
|
|
1183
|
+
const negCached = negCachedP2pLookups.get(uid);
|
|
1184
|
+
if (negCached && negCached.expires > Date.now()) {
|
|
1185
|
+
const remaining = negCached.expires - Date.now();
|
|
1186
|
+
log(
|
|
1187
|
+
`UID=${shortUid} negative-cache hit (fail-fast, retry in ${Math.ceil(remaining / 1e3)}s)`
|
|
1188
|
+
);
|
|
1189
|
+
throw negCached.error;
|
|
1190
|
+
}
|
|
1191
|
+
const inflight = inflightP2pLookups.get(uid);
|
|
1192
|
+
if (inflight) {
|
|
1193
|
+
log(`UID=${shortUid} sharing in-flight lookup with concurrent race lane`);
|
|
1194
|
+
return await inflight;
|
|
1195
|
+
}
|
|
1196
|
+
const work = this._doP2pUidLookupWork(sock, uid);
|
|
1197
|
+
inflightP2pLookups.set(uid, work);
|
|
1198
|
+
try {
|
|
1199
|
+
const result = await work;
|
|
1200
|
+
cachedP2pLookups.set(uid, {
|
|
1201
|
+
result,
|
|
1202
|
+
expires: Date.now() + P2P_LOOKUP_CACHE_TTL_MS
|
|
1203
|
+
});
|
|
1204
|
+
return result;
|
|
1205
|
+
} catch (e) {
|
|
1206
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
1207
|
+
negCachedP2pLookups.set(uid, {
|
|
1208
|
+
error: err,
|
|
1209
|
+
expires: Date.now() + P2P_LOOKUP_NEG_CACHE_TTL_MS
|
|
1210
|
+
});
|
|
1211
|
+
throw err;
|
|
1212
|
+
} finally {
|
|
1213
|
+
inflightP2pLookups.delete(uid);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
async _doP2pUidLookupWork(sock, uid) {
|
|
1112
1217
|
const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
|
|
1113
1218
|
const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
|
|
1114
1219
|
const t0 = Date.now();
|
|
@@ -1576,6 +1681,23 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1576
1681
|
log(
|
|
1577
1682
|
`local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
|
|
1578
1683
|
);
|
|
1684
|
+
if (directHost && localMode === "local-direct") {
|
|
1685
|
+
try {
|
|
1686
|
+
const egress = await probeEgressForHost(directHost, ports[0] ?? 2015);
|
|
1687
|
+
const sameSubnet = isSameSubnetAsAnyLocalIface(directHost, egress);
|
|
1688
|
+
if (sameSubnet === "mismatch") {
|
|
1689
|
+
log(
|
|
1690
|
+
`WARN: kernel-chosen source IP ${egress.localAddress} is NOT in the same subnet as ${directHost}. Some Reolink battery cams silently drop discovery packets with off-subnet source IPs. If discovery fails, check your routing table \u2014 likely a Tailscale / VPN / secondary NIC stealing the default route.`
|
|
1691
|
+
);
|
|
1692
|
+
} else {
|
|
1693
|
+
log(
|
|
1694
|
+
`egress for ${directHost} \u2192 src=${egress.localAddress}` + (sameSubnet === "same" ? ` (same subnet \u2713)` : ` (subnet relationship unknown)`)
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
} catch (e) {
|
|
1698
|
+
this.emit("debug", "egress_probe_failed", e);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1579
1701
|
let bytesSent = 0;
|
|
1580
1702
|
let pktsRecv = 0;
|
|
1581
1703
|
sock.on("message", () => {
|
|
@@ -24583,7 +24705,7 @@ function selectViableUdpMethods(hasUid, methods = ALL_UDP_DISCOVERY_METHODS) {
|
|
|
24583
24705
|
return methods.filter((m) => m === "local-direct");
|
|
24584
24706
|
}
|
|
24585
24707
|
function normalizeUid(uid) {
|
|
24586
|
-
const v = uid?.trim();
|
|
24708
|
+
const v = uid?.trim().toUpperCase();
|
|
24587
24709
|
return v ? v : void 0;
|
|
24588
24710
|
}
|
|
24589
24711
|
function maskUid(uid) {
|
|
@@ -25304,7 +25426,10 @@ export {
|
|
|
25304
25426
|
encodeHeader,
|
|
25305
25427
|
decodeHeader,
|
|
25306
25428
|
BaichuanFrameParser,
|
|
25429
|
+
probeEgressForHost,
|
|
25430
|
+
isSameSubnetAsAnyLocalIface,
|
|
25307
25431
|
isUnroutableForP2P,
|
|
25432
|
+
_clearP2pLookupDedupForTests,
|
|
25308
25433
|
BcUdpStream,
|
|
25309
25434
|
asLogger,
|
|
25310
25435
|
createNullLogger,
|
|
@@ -25372,4 +25497,4 @@ export {
|
|
|
25372
25497
|
tcpReachabilityProbe,
|
|
25373
25498
|
autoDetectDeviceType
|
|
25374
25499
|
};
|
|
25375
|
-
//# sourceMappingURL=chunk-
|
|
25500
|
+
//# sourceMappingURL=chunk-UL34MR4L.js.map
|