@7365admin1/core 3.32.2-staging.76 → 3.32.2-staging.77
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/.changeset/dahua-spec-defects.md +20 -0
- package/dist/index.js +86 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +86 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/dahua-protocol.util.test.mjs +217 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Fix three Dahua HTTP API defects found by checking our integration against the
|
|
6
|
+
vendor specification (V3.37):
|
|
7
|
+
|
|
8
|
+
- The ANPR reconnect loop retried authentication ten times at one-second
|
|
9
|
+
intervals. The device locks the account for 1800 seconds after three failed
|
|
10
|
+
logins in thirty, so our own recovery could take ANPR down at a site for half
|
|
11
|
+
an hour. Authentication now backs off well past the device's counting window,
|
|
12
|
+
and a network fault is distinguished from an authentication failure.
|
|
13
|
+
- 401 and 403 were handled backwards. Per spec §3.4, 401 is the digest
|
|
14
|
+
challenge (retry) and 403 is a credentials rejection (stop). We treated 403 as
|
|
15
|
+
an account lock and killed the listener, and counted 401s toward a "wrong
|
|
16
|
+
password" warning.
|
|
17
|
+
- `updatePlateNumber`, `bulkInsertPlateNumber` and `removePlateNumber`
|
|
18
|
+
interpolated values into the CGI query string unencoded, against the spec's
|
|
19
|
+
stated RFC 3986 requirement. A plate or owner name containing a space or "&"
|
|
20
|
+
broke the request or appended extra parameters to it.
|
package/dist/index.js
CHANGED
|
@@ -24777,6 +24777,45 @@ function shouldAlertOnFailure(consecutiveFailures) {
|
|
|
24777
24777
|
return (consecutiveFailures - CAMERA_ALERT_EVERY_AFTER) % CAMERA_ALERT_EVERY_AFTER === 0;
|
|
24778
24778
|
}
|
|
24779
24779
|
|
|
24780
|
+
// src/utils/dahua-protocol.util.ts
|
|
24781
|
+
var DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT = 2;
|
|
24782
|
+
var AUTH_BACKOFF_MS = [6e4, 3e5, 9e5];
|
|
24783
|
+
function dahuaAuthBackoffMs(consecutiveAuthFailures) {
|
|
24784
|
+
const index = Math.min(
|
|
24785
|
+
Math.max(consecutiveAuthFailures, 1),
|
|
24786
|
+
AUTH_BACKOFF_MS.length
|
|
24787
|
+
);
|
|
24788
|
+
return AUTH_BACKOFF_MS[index - 1];
|
|
24789
|
+
}
|
|
24790
|
+
function classifyDahuaStatus(statusCode) {
|
|
24791
|
+
if (statusCode === 401)
|
|
24792
|
+
return "challenge";
|
|
24793
|
+
if (statusCode === 403)
|
|
24794
|
+
return "rejected";
|
|
24795
|
+
if (statusCode === 400 || statusCode === 500)
|
|
24796
|
+
return "fatal";
|
|
24797
|
+
return "proceed";
|
|
24798
|
+
}
|
|
24799
|
+
function classifyDahuaFailure(message) {
|
|
24800
|
+
const text = String(message ?? "");
|
|
24801
|
+
if (/\b403\b|forbidden/i.test(text))
|
|
24802
|
+
return "rejected";
|
|
24803
|
+
if (/\b401\b|unauthorized|invalid authority|digest/i.test(text)) {
|
|
24804
|
+
return "challenge";
|
|
24805
|
+
}
|
|
24806
|
+
return "transient";
|
|
24807
|
+
}
|
|
24808
|
+
var enc = (value) => encodeURIComponent(String(value));
|
|
24809
|
+
function buildPlateUpdateEndpoint(value) {
|
|
24810
|
+
return `/cgi-bin/recordUpdater.cgi?action=update&name=${enc(value.mode)}&recno=${enc(value.recno)}&PlateNumber=${enc(value.plateNumber)}&BeginTime=${enc(value.start)}&CancelTime=${enc(value.end)}&+OpenGate=${value.openGate}&MasterOfCar=${enc(value.owner)}`;
|
|
24811
|
+
}
|
|
24812
|
+
function buildPlateInsertEndpoint(value) {
|
|
24813
|
+
return `/cgi-bin/recordUpdater.cgi?action=insert&name=${enc(value.mode)}&PlateNumber=${enc(value.plateNumber)}&VehicleType=${enc(value.vehicleType)}&VehicleColor=${enc(value.vehicleColor)}&BeginTime=${enc(value.start)}&CancelTime=${enc(value.end)}&+OpenGate=${value.openGate}&MasterOfCar=${enc(value.owner)}`;
|
|
24814
|
+
}
|
|
24815
|
+
function buildPlateRemoveEndpoint(value) {
|
|
24816
|
+
return `/cgi-bin/recordUpdater.cgi?action=remove&recno=${enc(value.recno)}&name=${enc(value.mode)}`;
|
|
24817
|
+
}
|
|
24818
|
+
|
|
24780
24819
|
// src/services/dahua.service.ts
|
|
24781
24820
|
var cameraRegistry = /* @__PURE__ */ new Map();
|
|
24782
24821
|
var _savedOnDetected;
|
|
@@ -24822,7 +24861,8 @@ async function useDahuaDigestWithRetry({
|
|
|
24822
24861
|
retryDelayMs = 0
|
|
24823
24862
|
}) {
|
|
24824
24863
|
let lastError;
|
|
24825
|
-
|
|
24864
|
+
const effectiveRetries = Math.min(retries, DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT);
|
|
24865
|
+
for (let attempt = 1; attempt <= effectiveRetries; attempt++) {
|
|
24826
24866
|
try {
|
|
24827
24867
|
const effectiveTimeout = streaming ? 0 : timeout;
|
|
24828
24868
|
const response = await useDahuaDigest({
|
|
@@ -24849,9 +24889,9 @@ async function useDahuaDigestWithRetry({
|
|
|
24849
24889
|
}
|
|
24850
24890
|
const errorText = streaming ? `HTTP ${statusCode}` : getDahuaResponseText(response);
|
|
24851
24891
|
const shouldRetry = isDahuaInvalidAuthority(errorText);
|
|
24852
|
-
if (shouldRetry && attempt <
|
|
24892
|
+
if (shouldRetry && attempt < effectiveRetries) {
|
|
24853
24893
|
loggerDahua.warn(
|
|
24854
|
-
`[${host}] Dahua Invalid Authority. Retrying ${attempt}/${
|
|
24894
|
+
`[${host}] Dahua Invalid Authority. Retrying ${attempt}/${effectiveRetries}`
|
|
24855
24895
|
);
|
|
24856
24896
|
if (retryDelayMs > 0) {
|
|
24857
24897
|
await sleep(retryDelayMs);
|
|
@@ -24863,9 +24903,9 @@ async function useDahuaDigestWithRetry({
|
|
|
24863
24903
|
lastError = error;
|
|
24864
24904
|
const message = error?.message || String(error);
|
|
24865
24905
|
const shouldRetry = isDahuaInvalidAuthority(message);
|
|
24866
|
-
if (shouldRetry && attempt <
|
|
24906
|
+
if (shouldRetry && attempt < effectiveRetries) {
|
|
24867
24907
|
loggerDahua.warn(
|
|
24868
|
-
`[${host}] Dahua Invalid Authority error. Retrying ${attempt}/${
|
|
24908
|
+
`[${host}] Dahua Invalid Authority error. Retrying ${attempt}/${effectiveRetries}`
|
|
24869
24909
|
);
|
|
24870
24910
|
if (retryDelayMs > 0) {
|
|
24871
24911
|
await sleep(retryDelayMs);
|
|
@@ -25276,7 +25316,6 @@ function useDahuaService() {
|
|
|
25276
25316
|
let authFailureCount = 0;
|
|
25277
25317
|
let connectionFailureCount = 0;
|
|
25278
25318
|
let alertedFailureCount = 0;
|
|
25279
|
-
const MAX_AUTH_RETRIES = 10;
|
|
25280
25319
|
while (!signal.aborted) {
|
|
25281
25320
|
let bufferQueue = null;
|
|
25282
25321
|
let response = null;
|
|
@@ -25291,23 +25330,26 @@ function useDahuaService() {
|
|
|
25291
25330
|
streaming: true
|
|
25292
25331
|
});
|
|
25293
25332
|
const statusCode = getDahuaStatusCode(response);
|
|
25294
|
-
|
|
25295
|
-
|
|
25296
|
-
|
|
25297
|
-
throw new Error("401 Unauthorized -
|
|
25298
|
-
} else if (
|
|
25333
|
+
const verdict = classifyDahuaStatus(statusCode);
|
|
25334
|
+
if (verdict === "challenge") {
|
|
25335
|
+
loggerDahua.warn(`[${camera?.siteName}-${camera?.direction}] 401 - digest challenge not completed; will re-authenticate after backoff.`);
|
|
25336
|
+
throw new Error("401 Unauthorized - digest challenge not completed");
|
|
25337
|
+
} else if (verdict === "rejected") {
|
|
25299
25338
|
loggerDahua.error(
|
|
25300
|
-
`[${camera?.siteName}-${camera?.direction}] 403 Forbidden
|
|
25339
|
+
`[${camera?.siteName}-${camera?.direction}] 403 Forbidden - credentials rejected by the device. Not retrying.`
|
|
25301
25340
|
);
|
|
25302
25341
|
if (onDetected) {
|
|
25303
25342
|
onDetected({
|
|
25304
25343
|
site: camera?.site,
|
|
25305
25344
|
direction: camera?.direction,
|
|
25345
|
+
// Superseded #1803's own wording: cameraCredentialsMessage already
|
|
25346
|
+
// says the login was refused and where to correct it, and no longer
|
|
25347
|
+
// claims a ten-minute lockout.
|
|
25306
25348
|
messagePermanent: cameraCredentialsMessage(camera)
|
|
25307
25349
|
});
|
|
25308
25350
|
}
|
|
25309
25351
|
return;
|
|
25310
|
-
} else if (
|
|
25352
|
+
} else if (verdict === "fatal") {
|
|
25311
25353
|
loggerDahua.error(`[${camera?.siteName}-${camera?.direction}] Connection error: ${statusCode}`);
|
|
25312
25354
|
if (onDetected) {
|
|
25313
25355
|
onDetected({ site: camera?.site, messagePermanent: cameraNotAuthorisedMessage(camera) });
|
|
@@ -25375,9 +25417,10 @@ function useDahuaService() {
|
|
|
25375
25417
|
break;
|
|
25376
25418
|
}
|
|
25377
25419
|
const errMsg = String(error?.message || error);
|
|
25378
|
-
|
|
25420
|
+
const failure = classifyDahuaFailure(errMsg);
|
|
25421
|
+
if (failure === "rejected") {
|
|
25379
25422
|
loggerDahua.error(
|
|
25380
|
-
`[${camera?.siteName}-${camera?.direction}] 403 Forbidden thrown by Dahua client
|
|
25423
|
+
`[${camera?.siteName}-${camera?.direction}] 403 Forbidden thrown by Dahua client - credentials rejected. Not retrying.`
|
|
25381
25424
|
);
|
|
25382
25425
|
if (onDetected) {
|
|
25383
25426
|
onDetected({
|
|
@@ -25388,16 +25431,15 @@ function useDahuaService() {
|
|
|
25388
25431
|
}
|
|
25389
25432
|
return;
|
|
25390
25433
|
}
|
|
25391
|
-
|
|
25392
|
-
if (isAuthError) {
|
|
25434
|
+
if (failure === "challenge") {
|
|
25393
25435
|
authFailureCount++;
|
|
25394
25436
|
isAuthFailure = true;
|
|
25395
25437
|
loggerDahua.warn(
|
|
25396
|
-
`[${camera?.siteName}-${camera?.direction}]
|
|
25438
|
+
`[${camera?.siteName}-${camera?.direction}] Authentication attempt ${authFailureCount} failed; next attempt in ${dahuaAuthBackoffMs(authFailureCount) / 1e3}s`
|
|
25397
25439
|
);
|
|
25398
|
-
if (authFailureCount
|
|
25440
|
+
if (authFailureCount === DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT) {
|
|
25399
25441
|
loggerDahua.error(
|
|
25400
|
-
`[${camera?.siteName}-${camera?.direction}]
|
|
25442
|
+
`[${camera?.siteName}-${camera?.direction}] ${authFailureCount} consecutive authentication failures. Slowing down and alerting the operator.`
|
|
25401
25443
|
);
|
|
25402
25444
|
if (onDetected) {
|
|
25403
25445
|
onDetected({
|
|
@@ -25406,7 +25448,6 @@ function useDahuaService() {
|
|
|
25406
25448
|
messagePermanent: cameraCredentialsMessage(camera)
|
|
25407
25449
|
});
|
|
25408
25450
|
}
|
|
25409
|
-
return;
|
|
25410
25451
|
}
|
|
25411
25452
|
} else {
|
|
25412
25453
|
authFailureCount = 0;
|
|
@@ -25446,7 +25487,7 @@ function useDahuaService() {
|
|
|
25446
25487
|
}
|
|
25447
25488
|
}
|
|
25448
25489
|
if (!signal.aborted) {
|
|
25449
|
-
const waitMs = isAuthFailure ?
|
|
25490
|
+
const waitMs = isAuthFailure ? dahuaAuthBackoffMs(authFailureCount) : 1e4;
|
|
25450
25491
|
await new Promise((res) => setTimeout(res, waitMs));
|
|
25451
25492
|
}
|
|
25452
25493
|
}
|
|
@@ -25606,7 +25647,15 @@ function useDahuaService() {
|
|
|
25606
25647
|
const openGateString = String(value.isOpenGate);
|
|
25607
25648
|
_isOpenGate = openGateString ? openGateString : "true";
|
|
25608
25649
|
}
|
|
25609
|
-
const endpoint =
|
|
25650
|
+
const endpoint = buildPlateUpdateEndpoint({
|
|
25651
|
+
mode: value.mode,
|
|
25652
|
+
recno: value.recno,
|
|
25653
|
+
plateNumber: value.plateNumber,
|
|
25654
|
+
start: value.start,
|
|
25655
|
+
end: value.end,
|
|
25656
|
+
owner: value.owner,
|
|
25657
|
+
openGate: _isOpenGate
|
|
25658
|
+
});
|
|
25610
25659
|
try {
|
|
25611
25660
|
const response = await useDahuaDigest({
|
|
25612
25661
|
host: value.host,
|
|
@@ -25639,7 +25688,10 @@ function useDahuaService() {
|
|
|
25639
25688
|
host: value.host,
|
|
25640
25689
|
username: value.username,
|
|
25641
25690
|
password: value.password,
|
|
25642
|
-
endpoint:
|
|
25691
|
+
endpoint: buildPlateRemoveEndpoint({
|
|
25692
|
+
recno: value.recno,
|
|
25693
|
+
mode: value.mode
|
|
25694
|
+
})
|
|
25643
25695
|
});
|
|
25644
25696
|
return response;
|
|
25645
25697
|
} catch (error2) {
|
|
@@ -25695,7 +25747,16 @@ function useDahuaService() {
|
|
|
25695
25747
|
value.vehicleColor = String(value.vehicleColor ?? "").substring(0, 31) || "unknown";
|
|
25696
25748
|
const _openGate = String(value.isOpenGate);
|
|
25697
25749
|
const isOpenGateString = _openGate && _openGate !== "undefined" ? _openGate : "true";
|
|
25698
|
-
const endpoint =
|
|
25750
|
+
const endpoint = buildPlateInsertEndpoint({
|
|
25751
|
+
mode: value.mode,
|
|
25752
|
+
plateNumber: value.plateNumber,
|
|
25753
|
+
vehicleType: value.vehicleType,
|
|
25754
|
+
vehicleColor: value.vehicleColor,
|
|
25755
|
+
start: value.start,
|
|
25756
|
+
end: value.end,
|
|
25757
|
+
owner: value.owner,
|
|
25758
|
+
openGate: isOpenGateString
|
|
25759
|
+
});
|
|
25699
25760
|
try {
|
|
25700
25761
|
const response = await useDahuaDigest({
|
|
25701
25762
|
host: value.host,
|