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