@7365admin1/core 3.32.2-staging.75 → 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-credential-logging.md +19 -0
- package/.changeset/dahua-spec-defects.md +20 -0
- package/dist/index.js +99 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +99 -31
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/dahua-protocol.util.test.mjs +217 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Stop logging camera and mailer credentials
|
|
6
|
+
|
|
7
|
+
Six places wrote a secret or a resident's details to stdout on an ordinary code
|
|
8
|
+
path. `getTrafficJunction` dumped the whole `TSiteCamera` object — including the
|
|
9
|
+
camera's `username` and plaintext `password` — on every reconnect, and
|
|
10
|
+
`addPlateNumber` logged its argument, which carries the same password plus a
|
|
11
|
+
resident's plate number and owner name. `checkOutBySiteAndPlate` logged the
|
|
12
|
+
matched visitor transaction. `siteCameraRepo.updateById` logged the re-read
|
|
13
|
+
camera document, and `vehicleService.add` logged the whole array of a site's
|
|
14
|
+
cameras, both with credentials in them. `sendEmail` printed `MAILER_PASSWORD`
|
|
15
|
+
itself on every message sent.
|
|
16
|
+
|
|
17
|
+
All now log identifiers, counts and outcomes only, through the module's logger
|
|
18
|
+
rather than `console.log`; the mailer line is removed outright because it
|
|
19
|
+
carried nothing but the secret.
|
|
@@ -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
|
@@ -22700,7 +22700,6 @@ async function sendEmail({ to, subject, text, html }) {
|
|
|
22700
22700
|
pass: MAILER_PASSWORD
|
|
22701
22701
|
}
|
|
22702
22702
|
});
|
|
22703
|
-
console.log(MAILER_EMAIL, MAILER_PASSWORD, MAILER_TRANSPORT_HOST, MAILER_TRANSPORT_PORT);
|
|
22704
22703
|
const mailOptions = {
|
|
22705
22704
|
from: `Seven 365 <${MAILER_EMAIL}>`,
|
|
22706
22705
|
to,
|
|
@@ -24778,6 +24777,45 @@ function shouldAlertOnFailure(consecutiveFailures) {
|
|
|
24778
24777
|
return (consecutiveFailures - CAMERA_ALERT_EVERY_AFTER) % CAMERA_ALERT_EVERY_AFTER === 0;
|
|
24779
24778
|
}
|
|
24780
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
|
+
|
|
24781
24819
|
// src/services/dahua.service.ts
|
|
24782
24820
|
var cameraRegistry = /* @__PURE__ */ new Map();
|
|
24783
24821
|
var _savedOnDetected;
|
|
@@ -24823,7 +24861,8 @@ async function useDahuaDigestWithRetry({
|
|
|
24823
24861
|
retryDelayMs = 0
|
|
24824
24862
|
}) {
|
|
24825
24863
|
let lastError;
|
|
24826
|
-
|
|
24864
|
+
const effectiveRetries = Math.min(retries, DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT);
|
|
24865
|
+
for (let attempt = 1; attempt <= effectiveRetries; attempt++) {
|
|
24827
24866
|
try {
|
|
24828
24867
|
const effectiveTimeout = streaming ? 0 : timeout;
|
|
24829
24868
|
const response = await useDahuaDigest({
|
|
@@ -24850,9 +24889,9 @@ async function useDahuaDigestWithRetry({
|
|
|
24850
24889
|
}
|
|
24851
24890
|
const errorText = streaming ? `HTTP ${statusCode}` : getDahuaResponseText(response);
|
|
24852
24891
|
const shouldRetry = isDahuaInvalidAuthority(errorText);
|
|
24853
|
-
if (shouldRetry && attempt <
|
|
24892
|
+
if (shouldRetry && attempt < effectiveRetries) {
|
|
24854
24893
|
loggerDahua.warn(
|
|
24855
|
-
`[${host}] Dahua Invalid Authority. Retrying ${attempt}/${
|
|
24894
|
+
`[${host}] Dahua Invalid Authority. Retrying ${attempt}/${effectiveRetries}`
|
|
24856
24895
|
);
|
|
24857
24896
|
if (retryDelayMs > 0) {
|
|
24858
24897
|
await sleep(retryDelayMs);
|
|
@@ -24864,9 +24903,9 @@ async function useDahuaDigestWithRetry({
|
|
|
24864
24903
|
lastError = error;
|
|
24865
24904
|
const message = error?.message || String(error);
|
|
24866
24905
|
const shouldRetry = isDahuaInvalidAuthority(message);
|
|
24867
|
-
if (shouldRetry && attempt <
|
|
24906
|
+
if (shouldRetry && attempt < effectiveRetries) {
|
|
24868
24907
|
loggerDahua.warn(
|
|
24869
|
-
`[${host}] Dahua Invalid Authority error. Retrying ${attempt}/${
|
|
24908
|
+
`[${host}] Dahua Invalid Authority error. Retrying ${attempt}/${effectiveRetries}`
|
|
24870
24909
|
);
|
|
24871
24910
|
if (retryDelayMs > 0) {
|
|
24872
24911
|
await sleep(retryDelayMs);
|
|
@@ -24970,7 +25009,9 @@ function useDahuaService() {
|
|
|
24970
25009
|
return;
|
|
24971
25010
|
try {
|
|
24972
25011
|
const result = await _checkOutBySiteAndPlate(site, plateNumber2);
|
|
24973
|
-
|
|
25012
|
+
loggerDahua.info(
|
|
25013
|
+
`checkOutBySiteAndPlate matched=${Boolean(result)} type=${result?.type ?? "none"}`
|
|
25014
|
+
);
|
|
24974
25015
|
if (onDetected2 && result?.type != "resident" && result?.site) {
|
|
24975
25016
|
onDetected2({ reload: true, site: result?.site?.toString(), siteName: camera?.siteName, cameraDirection: camera?.direction, direction });
|
|
24976
25017
|
}
|
|
@@ -25269,11 +25310,12 @@ function useDahuaService() {
|
|
|
25269
25310
|
);
|
|
25270
25311
|
}
|
|
25271
25312
|
async function getTrafficJunction(camera, signal, onDetected) {
|
|
25272
|
-
|
|
25313
|
+
loggerDahua.info(
|
|
25314
|
+
`getTrafficJunction starting for camera ${camera?._id} [${camera?.siteName}-${camera?.direction}] host ${camera?.host}`
|
|
25315
|
+
);
|
|
25273
25316
|
let authFailureCount = 0;
|
|
25274
25317
|
let connectionFailureCount = 0;
|
|
25275
25318
|
let alertedFailureCount = 0;
|
|
25276
|
-
const MAX_AUTH_RETRIES = 10;
|
|
25277
25319
|
while (!signal.aborted) {
|
|
25278
25320
|
let bufferQueue = null;
|
|
25279
25321
|
let response = null;
|
|
@@ -25288,23 +25330,26 @@ function useDahuaService() {
|
|
|
25288
25330
|
streaming: true
|
|
25289
25331
|
});
|
|
25290
25332
|
const statusCode = getDahuaStatusCode(response);
|
|
25291
|
-
|
|
25292
|
-
|
|
25293
|
-
|
|
25294
|
-
throw new Error("401 Unauthorized -
|
|
25295
|
-
} 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") {
|
|
25296
25338
|
loggerDahua.error(
|
|
25297
|
-
`[${camera?.siteName}-${camera?.direction}] 403 Forbidden
|
|
25339
|
+
`[${camera?.siteName}-${camera?.direction}] 403 Forbidden - credentials rejected by the device. Not retrying.`
|
|
25298
25340
|
);
|
|
25299
25341
|
if (onDetected) {
|
|
25300
25342
|
onDetected({
|
|
25301
25343
|
site: camera?.site,
|
|
25302
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.
|
|
25303
25348
|
messagePermanent: cameraCredentialsMessage(camera)
|
|
25304
25349
|
});
|
|
25305
25350
|
}
|
|
25306
25351
|
return;
|
|
25307
|
-
} else if (
|
|
25352
|
+
} else if (verdict === "fatal") {
|
|
25308
25353
|
loggerDahua.error(`[${camera?.siteName}-${camera?.direction}] Connection error: ${statusCode}`);
|
|
25309
25354
|
if (onDetected) {
|
|
25310
25355
|
onDetected({ site: camera?.site, messagePermanent: cameraNotAuthorisedMessage(camera) });
|
|
@@ -25372,9 +25417,10 @@ function useDahuaService() {
|
|
|
25372
25417
|
break;
|
|
25373
25418
|
}
|
|
25374
25419
|
const errMsg = String(error?.message || error);
|
|
25375
|
-
|
|
25420
|
+
const failure = classifyDahuaFailure(errMsg);
|
|
25421
|
+
if (failure === "rejected") {
|
|
25376
25422
|
loggerDahua.error(
|
|
25377
|
-
`[${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.`
|
|
25378
25424
|
);
|
|
25379
25425
|
if (onDetected) {
|
|
25380
25426
|
onDetected({
|
|
@@ -25385,16 +25431,15 @@ function useDahuaService() {
|
|
|
25385
25431
|
}
|
|
25386
25432
|
return;
|
|
25387
25433
|
}
|
|
25388
|
-
|
|
25389
|
-
if (isAuthError) {
|
|
25434
|
+
if (failure === "challenge") {
|
|
25390
25435
|
authFailureCount++;
|
|
25391
25436
|
isAuthFailure = true;
|
|
25392
25437
|
loggerDahua.warn(
|
|
25393
|
-
`[${camera?.siteName}-${camera?.direction}]
|
|
25438
|
+
`[${camera?.siteName}-${camera?.direction}] Authentication attempt ${authFailureCount} failed; next attempt in ${dahuaAuthBackoffMs(authFailureCount) / 1e3}s`
|
|
25394
25439
|
);
|
|
25395
|
-
if (authFailureCount
|
|
25440
|
+
if (authFailureCount === DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT) {
|
|
25396
25441
|
loggerDahua.error(
|
|
25397
|
-
`[${camera?.siteName}-${camera?.direction}]
|
|
25442
|
+
`[${camera?.siteName}-${camera?.direction}] ${authFailureCount} consecutive authentication failures. Slowing down and alerting the operator.`
|
|
25398
25443
|
);
|
|
25399
25444
|
if (onDetected) {
|
|
25400
25445
|
onDetected({
|
|
@@ -25403,7 +25448,6 @@ function useDahuaService() {
|
|
|
25403
25448
|
messagePermanent: cameraCredentialsMessage(camera)
|
|
25404
25449
|
});
|
|
25405
25450
|
}
|
|
25406
|
-
return;
|
|
25407
25451
|
}
|
|
25408
25452
|
} else {
|
|
25409
25453
|
authFailureCount = 0;
|
|
@@ -25443,7 +25487,7 @@ function useDahuaService() {
|
|
|
25443
25487
|
}
|
|
25444
25488
|
}
|
|
25445
25489
|
if (!signal.aborted) {
|
|
25446
|
-
const waitMs = isAuthFailure ?
|
|
25490
|
+
const waitMs = isAuthFailure ? dahuaAuthBackoffMs(authFailureCount) : 1e4;
|
|
25447
25491
|
await new Promise((res) => setTimeout(res, waitMs));
|
|
25448
25492
|
}
|
|
25449
25493
|
}
|
|
@@ -25456,7 +25500,9 @@ function useDahuaService() {
|
|
|
25456
25500
|
return match ? match[1] : null;
|
|
25457
25501
|
}
|
|
25458
25502
|
async function addPlateNumber(value) {
|
|
25459
|
-
|
|
25503
|
+
loggerDahua.info(
|
|
25504
|
+
`addPlateNumber called for host ${value?.host} mode ${value?.mode}`
|
|
25505
|
+
);
|
|
25460
25506
|
let recno = null;
|
|
25461
25507
|
const validation = import_joi40.default.object({
|
|
25462
25508
|
host: import_joi40.default.string().required(),
|
|
@@ -25601,7 +25647,15 @@ function useDahuaService() {
|
|
|
25601
25647
|
const openGateString = String(value.isOpenGate);
|
|
25602
25648
|
_isOpenGate = openGateString ? openGateString : "true";
|
|
25603
25649
|
}
|
|
25604
|
-
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
|
+
});
|
|
25605
25659
|
try {
|
|
25606
25660
|
const response = await useDahuaDigest({
|
|
25607
25661
|
host: value.host,
|
|
@@ -25634,7 +25688,10 @@ function useDahuaService() {
|
|
|
25634
25688
|
host: value.host,
|
|
25635
25689
|
username: value.username,
|
|
25636
25690
|
password: value.password,
|
|
25637
|
-
endpoint:
|
|
25691
|
+
endpoint: buildPlateRemoveEndpoint({
|
|
25692
|
+
recno: value.recno,
|
|
25693
|
+
mode: value.mode
|
|
25694
|
+
})
|
|
25638
25695
|
});
|
|
25639
25696
|
return response;
|
|
25640
25697
|
} catch (error2) {
|
|
@@ -25690,7 +25747,16 @@ function useDahuaService() {
|
|
|
25690
25747
|
value.vehicleColor = String(value.vehicleColor ?? "").substring(0, 31) || "unknown";
|
|
25691
25748
|
const _openGate = String(value.isOpenGate);
|
|
25692
25749
|
const isOpenGateString = _openGate && _openGate !== "undefined" ? _openGate : "true";
|
|
25693
|
-
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
|
+
});
|
|
25694
25760
|
try {
|
|
25695
25761
|
const response = await useDahuaDigest({
|
|
25696
25762
|
host: value.host,
|
|
@@ -26116,7 +26182,7 @@ function useSiteCameraRepo() {
|
|
|
26116
26182
|
}
|
|
26117
26183
|
if (result?._id) {
|
|
26118
26184
|
const result2 = await findOne({ _id: new import_mongodb46.ObjectId(result._id) }, void 0, { session });
|
|
26119
|
-
|
|
26185
|
+
import_node_server_utils74.logger.info(`Site camera ${result2?._id} re-read after update; reconnecting listener`);
|
|
26120
26186
|
if (result2?._id) {
|
|
26121
26187
|
const { listenToCamera } = useDahuaService();
|
|
26122
26188
|
await listenToCamera(result2);
|
|
@@ -27642,7 +27708,9 @@ function useVehicleService() {
|
|
|
27642
27708
|
siteCameras.push(...siteCameraReq.items);
|
|
27643
27709
|
page++;
|
|
27644
27710
|
} while (page <= pages);
|
|
27645
|
-
|
|
27711
|
+
import_node_server_utils78.logger.info(
|
|
27712
|
+
`addVehicle resolved ${siteCameras.length} site camera(s) for site ${value?.site}`
|
|
27713
|
+
);
|
|
27646
27714
|
}
|
|
27647
27715
|
for (const plateNumber of plateNumbers) {
|
|
27648
27716
|
const vehicleValue = {
|