@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.
@@ -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
- for (let attempt = 1; attempt <= retries; attempt++) {
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 < retries) {
24892
+ if (shouldRetry && attempt < effectiveRetries) {
24853
24893
  loggerDahua.warn(
24854
- `[${host}] Dahua Invalid Authority. Retrying ${attempt}/${retries}`
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 < retries) {
24906
+ if (shouldRetry && attempt < effectiveRetries) {
24867
24907
  loggerDahua.warn(
24868
- `[${host}] Dahua Invalid Authority error. Retrying ${attempt}/${retries}`
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
- if (statusCode === 401) {
25295
- loggerDahua.error(`[${camera?.siteName}-${camera?.direction}] 401 Unauthorized - Handshake or Wrong Credentials`);
25296
- console.log(`[${camera?.siteName}-${camera?.direction}] 401 Unauthorized - Handshake or Wrong Credentials`);
25297
- throw new Error("401 Unauthorized - Handshake or Wrong Credentials");
25298
- } else if (statusCode === 403) {
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. Account locked or invalid permissions.`
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 ([400, 500].includes(statusCode)) {
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
- if (errMsg.includes("403") || errMsg.includes("Forbidden")) {
25420
+ const failure = classifyDahuaFailure(errMsg);
25421
+ if (failure === "rejected") {
25379
25422
  loggerDahua.error(
25380
- `[${camera?.siteName}-${camera?.direction}] 403 Forbidden thrown by Dahua client. Account locked or invalid credentials.`
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
- const isAuthError = errMsg.includes("401") || errMsg.includes("Unauthorized") || errMsg.includes("Invalid Authority");
25392
- if (isAuthError) {
25434
+ if (failure === "challenge") {
25393
25435
  authFailureCount++;
25394
25436
  isAuthFailure = true;
25395
25437
  loggerDahua.warn(
25396
- `[${camera?.siteName}-${camera?.direction}] Auth attempt ${authFailureCount}/${MAX_AUTH_RETRIES}`
25438
+ `[${camera?.siteName}-${camera?.direction}] Authentication attempt ${authFailureCount} failed; next attempt in ${dahuaAuthBackoffMs(authFailureCount) / 1e3}s`
25397
25439
  );
25398
- if (authFailureCount >= MAX_AUTH_RETRIES) {
25440
+ if (authFailureCount === DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT) {
25399
25441
  loggerDahua.error(
25400
- `[${camera?.siteName}-${camera?.direction}] Max auth failures reached (${MAX_AUTH_RETRIES}). Wrong credentials.`
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 ? 1e3 : 1e4;
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 = `/cgi-bin/recordUpdater.cgi?action=update&name=${value.mode}&recno=${value.recno}&PlateNumber=${value.plateNumber}&BeginTime=${value.start}&CancelTime=${value.end}&+OpenGate=${_isOpenGate}&MasterOfCar=${value.owner}`;
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: `/cgi-bin/recordUpdater.cgi?action=remove&recno=${value.recno}&name=${value.mode}`
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 = `/cgi-bin/recordUpdater.cgi?action=insert&name=${value.mode}&PlateNumber=${value.plateNumber}&VehicleType=${value.vehicleType}&VehicleColor=${value.vehicleColor}&BeginTime=${value.start}&CancelTime=${value.end}&+OpenGate=${isOpenGateString}&MasterOfCar=${value.owner}`;
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,