@blamejs/core 0.7.18 → 0.7.19
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/CHANGELOG.md +425 -423
- package/README.md +150 -150
- package/bin/blamejs.js +0 -0
- package/index.js +310 -308
- package/lib/api-key.js +660 -660
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt-external.js +365 -0
- package/lib/auth/jwt.js +337 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +628 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -275
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -420
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -293
- package/lib/framework-schema.js +717 -717
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -369
- package/lib/mail.js +981 -981
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/bearer-auth.js +152 -0
- package/lib/middleware/body-parser.js +1170 -1170
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +82 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -111
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +535 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -340
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/ntp-check.js
CHANGED
|
@@ -1,229 +1,229 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Minimal SNTP client for boot-time clock-drift verification.
|
|
4
|
-
*
|
|
5
|
-
* Why: the audit chain's monotonicCounter orders events deterministically
|
|
6
|
-
* even if the wall clock jumps, but recordedAt is the human-readable
|
|
7
|
-
* timestamp auditors will rely on. A clock that's silently off by hours
|
|
8
|
-
* (container with no RTC sync, NTP daemon stopped) makes the audit trail
|
|
9
|
-
* misleading.
|
|
10
|
-
*
|
|
11
|
-
* What this does:
|
|
12
|
-
* - Sends a single SNTPv4 query to a configured server (default
|
|
13
|
-
* pool.ntp.org) over UDP port 123.
|
|
14
|
-
* - Computes drift = (server's transmit timestamp) - (local clock).
|
|
15
|
-
* - Returns the drift in milliseconds.
|
|
16
|
-
*
|
|
17
|
-
* What this does NOT do:
|
|
18
|
-
* - Continuous synchronization (use the OS NTP daemon for that).
|
|
19
|
-
* - Authenticated NTP (NTS, autokey).
|
|
20
|
-
* - Querying multiple servers and taking median (single-shot only).
|
|
21
|
-
*
|
|
22
|
-
* The framework's policy in db.init():
|
|
23
|
-
* - drift |x| < 5min → log info, continue
|
|
24
|
-
* - drift |x| in [5min,1hr) → log warning, continue
|
|
25
|
-
* - drift |x| >= 1hr → log fatal, exit (BLAMEJS_NTP_STRICT=1) or warn
|
|
26
|
-
* - NTP unreachable → log warning, continue (network may not allow UDP/123)
|
|
27
|
-
*/
|
|
28
|
-
var dgram = require("dgram");
|
|
29
|
-
var C = require("./constants");
|
|
30
|
-
|
|
31
|
-
// NTP epoch: 1900-01-01. Unix epoch: 1970-01-01. Offset: 70 years incl. 17
|
|
32
|
-
// leap days = 2,208,988,800 seconds.
|
|
33
|
-
var NTP_TO_UNIX_OFFSET_SECONDS = 2208988800;
|
|
34
|
-
|
|
35
|
-
var DEFAULT_SERVERS = ["pool.ntp.org", "time.cloudflare.com"];
|
|
36
|
-
var DEFAULT_PORT = 123;
|
|
37
|
-
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(3);
|
|
38
|
-
var NTP_PACKET_BYTES = C.BYTES.bytes(48); // RFC 5905 §7.3
|
|
39
|
-
|
|
40
|
-
var DEFAULT_DRIFT_WARN_MS = C.TIME.minutes(5);
|
|
41
|
-
var DEFAULT_DRIFT_FATAL_MS = C.TIME.hours(1);
|
|
42
|
-
|
|
43
|
-
var thresholds = {
|
|
44
|
-
warnMs: DEFAULT_DRIFT_WARN_MS,
|
|
45
|
-
fatalMs: DEFAULT_DRIFT_FATAL_MS,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
function setThresholds(opts) {
|
|
49
|
-
opts = opts || {};
|
|
50
|
-
if (opts.warnMs !== undefined) {
|
|
51
|
-
if (typeof opts.warnMs !== "number" || !isFinite(opts.warnMs) || opts.warnMs < 0) {
|
|
52
|
-
throw new TypeError("ntpCheck.setThresholds: warnMs must be non-negative finite number, got " + JSON.stringify(opts.warnMs));
|
|
53
|
-
}
|
|
54
|
-
thresholds.warnMs = opts.warnMs;
|
|
55
|
-
}
|
|
56
|
-
if (opts.fatalMs !== undefined) {
|
|
57
|
-
if (typeof opts.fatalMs !== "number" || !isFinite(opts.fatalMs) || opts.fatalMs < 0) {
|
|
58
|
-
throw new TypeError("ntpCheck.setThresholds: fatalMs must be non-negative finite number, got " + JSON.stringify(opts.fatalMs));
|
|
59
|
-
}
|
|
60
|
-
thresholds.fatalMs = opts.fatalMs;
|
|
61
|
-
}
|
|
62
|
-
if (thresholds.warnMs > thresholds.fatalMs && thresholds.fatalMs > 0) {
|
|
63
|
-
throw new RangeError("ntpCheck.setThresholds: warnMs (" + thresholds.warnMs +
|
|
64
|
-
") must be <= fatalMs (" + thresholds.fatalMs + ")");
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function getThresholds() {
|
|
69
|
-
return { warnMs: thresholds.warnMs, fatalMs: thresholds.fatalMs };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function _resetThresholdsForTest() {
|
|
73
|
-
thresholds.warnMs = DEFAULT_DRIFT_WARN_MS;
|
|
74
|
-
thresholds.fatalMs = DEFAULT_DRIFT_FATAL_MS;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Query an NTP server once. Resolves with { driftMs, serverTimeMs } or
|
|
79
|
-
* rejects with { code, message } where code is one of:
|
|
80
|
-
* 'ntp/timeout' — server didn't reply within timeoutMs
|
|
81
|
-
* 'ntp/refused' — DNS/connection error
|
|
82
|
-
* 'ntp/bad-reply' — packet structure wrong
|
|
83
|
-
*/
|
|
84
|
-
function querySingle(server, opts) {
|
|
85
|
-
opts = opts || {};
|
|
86
|
-
var port = opts.port || DEFAULT_PORT;
|
|
87
|
-
var timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
88
|
-
|
|
89
|
-
return new Promise(function (resolve, reject) {
|
|
90
|
-
// udp6 for IPv6 literals (`::1`, `fd00::…`), udp4 otherwise. Without
|
|
91
|
-
// this branch a query to an IPv6 NTP host fails with EINVAL because
|
|
92
|
-
// you can't send IPv6 packets through a udp4 socket.
|
|
93
|
-
var family = server.indexOf(":") !== -1 ? "udp6" : "udp4";
|
|
94
|
-
var socket = dgram.createSocket(family);
|
|
95
|
-
var settled = false;
|
|
96
|
-
|
|
97
|
-
function done(err, result) {
|
|
98
|
-
if (settled) return;
|
|
99
|
-
settled = true;
|
|
100
|
-
try { socket.close(); } catch (_e) { /* ignored */ }
|
|
101
|
-
if (err) reject(err); else resolve(result);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
var timer = setTimeout(function () {
|
|
105
|
-
done({ code: "ntp/timeout", message: "no reply from " + server + " within " + timeoutMs + "ms" });
|
|
106
|
-
}, timeoutMs);
|
|
107
|
-
timer.unref();
|
|
108
|
-
|
|
109
|
-
// SNTPv4 client request: NTP_PACKET_BYTES buffer, byte 0 = 0b00_100_011 = 0x23
|
|
110
|
-
// LI=0 (no warning), VN=4, Mode=3 (client). Other bytes zero.
|
|
111
|
-
var req = Buffer.alloc(NTP_PACKET_BYTES);
|
|
112
|
-
req[0] = 0x23;
|
|
113
|
-
var sendTimeMs = Date.now();
|
|
114
|
-
|
|
115
|
-
socket.on("error", function (e) {
|
|
116
|
-
clearTimeout(timer);
|
|
117
|
-
done({ code: "ntp/refused", message: server + ": " + e.message });
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
socket.on("message", function (msg) {
|
|
121
|
-
clearTimeout(timer);
|
|
122
|
-
var receiveTimeMs = Date.now();
|
|
123
|
-
if (!Buffer.isBuffer(msg) || msg.length < NTP_PACKET_BYTES) {
|
|
124
|
-
return done({ code: "ntp/bad-reply", message: "reply too short (" + (msg && msg.length) + " bytes)" });
|
|
125
|
-
}
|
|
126
|
-
// Bytes 40-47 = Transmit Timestamp (NTP epoch seconds.fraction)
|
|
127
|
-
var ntpSeconds = msg.readUInt32BE(40);
|
|
128
|
-
var ntpFraction = msg.readUInt32BE(44);
|
|
129
|
-
var serverUnixSeconds = ntpSeconds - NTP_TO_UNIX_OFFSET_SECONDS;
|
|
130
|
-
var fracMs = Math.round(C.TIME.seconds(ntpFraction / 0x100000000));
|
|
131
|
-
var serverTimeMs = C.TIME.seconds(serverUnixSeconds) + fracMs;
|
|
132
|
-
|
|
133
|
-
// Round-trip-corrected drift: assume the server's reply transmit
|
|
134
|
-
// timestamp is approximately at the midpoint of our send/receive.
|
|
135
|
-
var midpointMs = sendTimeMs + (receiveTimeMs - sendTimeMs) / 2;
|
|
136
|
-
var driftMs = serverTimeMs - midpointMs;
|
|
137
|
-
|
|
138
|
-
done(null, { driftMs: driftMs, serverTimeMs: serverTimeMs, server: server });
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
socket.send(req, 0, req.length, port, server, function (err) {
|
|
142
|
-
if (err) {
|
|
143
|
-
clearTimeout(timer);
|
|
144
|
-
done({ code: "ntp/refused", message: "send to " + server + ": " + err.message });
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Try each server in turn; return the first successful drift measurement.
|
|
152
|
-
* Resolves null if all servers fail (caller decides whether that's fatal).
|
|
153
|
-
*/
|
|
154
|
-
async function checkDrift(opts) {
|
|
155
|
-
opts = opts || {};
|
|
156
|
-
var servers = opts.servers || DEFAULT_SERVERS;
|
|
157
|
-
var lastError = null;
|
|
158
|
-
for (var i = 0; i < servers.length; i++) {
|
|
159
|
-
try {
|
|
160
|
-
return await querySingle(servers[i], opts);
|
|
161
|
-
} catch (e) {
|
|
162
|
-
lastError = e;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return { driftMs: null, error: lastError };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Boot-time check that integrates with the framework's logging policy.
|
|
170
|
-
* Returns a result object with { ok, driftMs, severity, message }.
|
|
171
|
-
* Caller (db.init) decides whether to exit.
|
|
172
|
-
*/
|
|
173
|
-
async function bootCheck(opts) {
|
|
174
|
-
opts = opts || {};
|
|
175
|
-
var result = await checkDrift(opts);
|
|
176
|
-
if (result.driftMs === null) {
|
|
177
|
-
return {
|
|
178
|
-
ok: true, // unreachable NTP isn't a hard failure
|
|
179
|
-
severity: "warning",
|
|
180
|
-
driftMs: null,
|
|
181
|
-
message: "NTP unreachable: " + (result.error && result.error.message) +
|
|
182
|
-
" (continuing — set BLAMEJS_NTP_STRICT=1 to fail closed)",
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
var absMs = Math.abs(result.driftMs);
|
|
186
|
-
var driftStr = (result.driftMs >= 0 ? "+" : "") + result.driftMs + "ms";
|
|
187
|
-
var fatalMs = (opts && typeof opts.driftFatalMs === "number") ? opts.driftFatalMs : thresholds.fatalMs;
|
|
188
|
-
var warnMs = (opts && typeof opts.driftWarnMs === "number") ? opts.driftWarnMs : thresholds.warnMs;
|
|
189
|
-
if (fatalMs > 0 && absMs >= fatalMs) {
|
|
190
|
-
return {
|
|
191
|
-
ok: false,
|
|
192
|
-
severity: "fatal",
|
|
193
|
-
driftMs: result.driftMs,
|
|
194
|
-
server: result.server,
|
|
195
|
-
message: "clock drift " + driftStr + " from " + result.server +
|
|
196
|
-
" (>= " + fatalMs + "ms) — refuse to boot",
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
if (warnMs > 0 && absMs >= warnMs) {
|
|
200
|
-
return {
|
|
201
|
-
ok: true,
|
|
202
|
-
severity: "warning",
|
|
203
|
-
driftMs: result.driftMs,
|
|
204
|
-
server: result.server,
|
|
205
|
-
message: "clock drift " + driftStr + " from " + result.server +
|
|
206
|
-
" (>= " + warnMs + "ms) — investigate",
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
return {
|
|
210
|
-
ok: true,
|
|
211
|
-
severity: "info",
|
|
212
|
-
driftMs: result.driftMs,
|
|
213
|
-
server: result.server,
|
|
214
|
-
message: "clock drift " + driftStr + " from " + result.server,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
module.exports = {
|
|
219
|
-
querySingle: querySingle,
|
|
220
|
-
checkDrift: checkDrift,
|
|
221
|
-
bootCheck: bootCheck,
|
|
222
|
-
setThresholds: setThresholds,
|
|
223
|
-
getThresholds: getThresholds,
|
|
224
|
-
DEFAULT_SERVERS: DEFAULT_SERVERS,
|
|
225
|
-
DEFAULT_DRIFT_WARN_MS: DEFAULT_DRIFT_WARN_MS,
|
|
226
|
-
DEFAULT_DRIFT_FATAL_MS: DEFAULT_DRIFT_FATAL_MS,
|
|
227
|
-
NTP_TO_UNIX_OFFSET_SECONDS: NTP_TO_UNIX_OFFSET_SECONDS,
|
|
228
|
-
_resetThresholdsForTest: _resetThresholdsForTest,
|
|
229
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal SNTP client for boot-time clock-drift verification.
|
|
4
|
+
*
|
|
5
|
+
* Why: the audit chain's monotonicCounter orders events deterministically
|
|
6
|
+
* even if the wall clock jumps, but recordedAt is the human-readable
|
|
7
|
+
* timestamp auditors will rely on. A clock that's silently off by hours
|
|
8
|
+
* (container with no RTC sync, NTP daemon stopped) makes the audit trail
|
|
9
|
+
* misleading.
|
|
10
|
+
*
|
|
11
|
+
* What this does:
|
|
12
|
+
* - Sends a single SNTPv4 query to a configured server (default
|
|
13
|
+
* pool.ntp.org) over UDP port 123.
|
|
14
|
+
* - Computes drift = (server's transmit timestamp) - (local clock).
|
|
15
|
+
* - Returns the drift in milliseconds.
|
|
16
|
+
*
|
|
17
|
+
* What this does NOT do:
|
|
18
|
+
* - Continuous synchronization (use the OS NTP daemon for that).
|
|
19
|
+
* - Authenticated NTP (NTS, autokey).
|
|
20
|
+
* - Querying multiple servers and taking median (single-shot only).
|
|
21
|
+
*
|
|
22
|
+
* The framework's policy in db.init():
|
|
23
|
+
* - drift |x| < 5min → log info, continue
|
|
24
|
+
* - drift |x| in [5min,1hr) → log warning, continue
|
|
25
|
+
* - drift |x| >= 1hr → log fatal, exit (BLAMEJS_NTP_STRICT=1) or warn
|
|
26
|
+
* - NTP unreachable → log warning, continue (network may not allow UDP/123)
|
|
27
|
+
*/
|
|
28
|
+
var dgram = require("dgram");
|
|
29
|
+
var C = require("./constants");
|
|
30
|
+
|
|
31
|
+
// NTP epoch: 1900-01-01. Unix epoch: 1970-01-01. Offset: 70 years incl. 17
|
|
32
|
+
// leap days = 2,208,988,800 seconds.
|
|
33
|
+
var NTP_TO_UNIX_OFFSET_SECONDS = 2208988800;
|
|
34
|
+
|
|
35
|
+
var DEFAULT_SERVERS = ["pool.ntp.org", "time.cloudflare.com"];
|
|
36
|
+
var DEFAULT_PORT = 123;
|
|
37
|
+
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(3);
|
|
38
|
+
var NTP_PACKET_BYTES = C.BYTES.bytes(48); // RFC 5905 §7.3
|
|
39
|
+
|
|
40
|
+
var DEFAULT_DRIFT_WARN_MS = C.TIME.minutes(5);
|
|
41
|
+
var DEFAULT_DRIFT_FATAL_MS = C.TIME.hours(1);
|
|
42
|
+
|
|
43
|
+
var thresholds = {
|
|
44
|
+
warnMs: DEFAULT_DRIFT_WARN_MS,
|
|
45
|
+
fatalMs: DEFAULT_DRIFT_FATAL_MS,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function setThresholds(opts) {
|
|
49
|
+
opts = opts || {};
|
|
50
|
+
if (opts.warnMs !== undefined) {
|
|
51
|
+
if (typeof opts.warnMs !== "number" || !isFinite(opts.warnMs) || opts.warnMs < 0) {
|
|
52
|
+
throw new TypeError("ntpCheck.setThresholds: warnMs must be non-negative finite number, got " + JSON.stringify(opts.warnMs));
|
|
53
|
+
}
|
|
54
|
+
thresholds.warnMs = opts.warnMs;
|
|
55
|
+
}
|
|
56
|
+
if (opts.fatalMs !== undefined) {
|
|
57
|
+
if (typeof opts.fatalMs !== "number" || !isFinite(opts.fatalMs) || opts.fatalMs < 0) {
|
|
58
|
+
throw new TypeError("ntpCheck.setThresholds: fatalMs must be non-negative finite number, got " + JSON.stringify(opts.fatalMs));
|
|
59
|
+
}
|
|
60
|
+
thresholds.fatalMs = opts.fatalMs;
|
|
61
|
+
}
|
|
62
|
+
if (thresholds.warnMs > thresholds.fatalMs && thresholds.fatalMs > 0) {
|
|
63
|
+
throw new RangeError("ntpCheck.setThresholds: warnMs (" + thresholds.warnMs +
|
|
64
|
+
") must be <= fatalMs (" + thresholds.fatalMs + ")");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getThresholds() {
|
|
69
|
+
return { warnMs: thresholds.warnMs, fatalMs: thresholds.fatalMs };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _resetThresholdsForTest() {
|
|
73
|
+
thresholds.warnMs = DEFAULT_DRIFT_WARN_MS;
|
|
74
|
+
thresholds.fatalMs = DEFAULT_DRIFT_FATAL_MS;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Query an NTP server once. Resolves with { driftMs, serverTimeMs } or
|
|
79
|
+
* rejects with { code, message } where code is one of:
|
|
80
|
+
* 'ntp/timeout' — server didn't reply within timeoutMs
|
|
81
|
+
* 'ntp/refused' — DNS/connection error
|
|
82
|
+
* 'ntp/bad-reply' — packet structure wrong
|
|
83
|
+
*/
|
|
84
|
+
function querySingle(server, opts) {
|
|
85
|
+
opts = opts || {};
|
|
86
|
+
var port = opts.port || DEFAULT_PORT;
|
|
87
|
+
var timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
88
|
+
|
|
89
|
+
return new Promise(function (resolve, reject) {
|
|
90
|
+
// udp6 for IPv6 literals (`::1`, `fd00::…`), udp4 otherwise. Without
|
|
91
|
+
// this branch a query to an IPv6 NTP host fails with EINVAL because
|
|
92
|
+
// you can't send IPv6 packets through a udp4 socket.
|
|
93
|
+
var family = server.indexOf(":") !== -1 ? "udp6" : "udp4";
|
|
94
|
+
var socket = dgram.createSocket(family);
|
|
95
|
+
var settled = false;
|
|
96
|
+
|
|
97
|
+
function done(err, result) {
|
|
98
|
+
if (settled) return;
|
|
99
|
+
settled = true;
|
|
100
|
+
try { socket.close(); } catch (_e) { /* ignored */ }
|
|
101
|
+
if (err) reject(err); else resolve(result);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var timer = setTimeout(function () {
|
|
105
|
+
done({ code: "ntp/timeout", message: "no reply from " + server + " within " + timeoutMs + "ms" });
|
|
106
|
+
}, timeoutMs);
|
|
107
|
+
timer.unref();
|
|
108
|
+
|
|
109
|
+
// SNTPv4 client request: NTP_PACKET_BYTES buffer, byte 0 = 0b00_100_011 = 0x23
|
|
110
|
+
// LI=0 (no warning), VN=4, Mode=3 (client). Other bytes zero.
|
|
111
|
+
var req = Buffer.alloc(NTP_PACKET_BYTES);
|
|
112
|
+
req[0] = 0x23;
|
|
113
|
+
var sendTimeMs = Date.now();
|
|
114
|
+
|
|
115
|
+
socket.on("error", function (e) {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
done({ code: "ntp/refused", message: server + ": " + e.message });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
socket.on("message", function (msg) {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
var receiveTimeMs = Date.now();
|
|
123
|
+
if (!Buffer.isBuffer(msg) || msg.length < NTP_PACKET_BYTES) {
|
|
124
|
+
return done({ code: "ntp/bad-reply", message: "reply too short (" + (msg && msg.length) + " bytes)" });
|
|
125
|
+
}
|
|
126
|
+
// Bytes 40-47 = Transmit Timestamp (NTP epoch seconds.fraction)
|
|
127
|
+
var ntpSeconds = msg.readUInt32BE(40);
|
|
128
|
+
var ntpFraction = msg.readUInt32BE(44);
|
|
129
|
+
var serverUnixSeconds = ntpSeconds - NTP_TO_UNIX_OFFSET_SECONDS;
|
|
130
|
+
var fracMs = Math.round(C.TIME.seconds(ntpFraction / 0x100000000));
|
|
131
|
+
var serverTimeMs = C.TIME.seconds(serverUnixSeconds) + fracMs;
|
|
132
|
+
|
|
133
|
+
// Round-trip-corrected drift: assume the server's reply transmit
|
|
134
|
+
// timestamp is approximately at the midpoint of our send/receive.
|
|
135
|
+
var midpointMs = sendTimeMs + (receiveTimeMs - sendTimeMs) / 2;
|
|
136
|
+
var driftMs = serverTimeMs - midpointMs;
|
|
137
|
+
|
|
138
|
+
done(null, { driftMs: driftMs, serverTimeMs: serverTimeMs, server: server });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
socket.send(req, 0, req.length, port, server, function (err) {
|
|
142
|
+
if (err) {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
done({ code: "ntp/refused", message: "send to " + server + ": " + err.message });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Try each server in turn; return the first successful drift measurement.
|
|
152
|
+
* Resolves null if all servers fail (caller decides whether that's fatal).
|
|
153
|
+
*/
|
|
154
|
+
async function checkDrift(opts) {
|
|
155
|
+
opts = opts || {};
|
|
156
|
+
var servers = opts.servers || DEFAULT_SERVERS;
|
|
157
|
+
var lastError = null;
|
|
158
|
+
for (var i = 0; i < servers.length; i++) {
|
|
159
|
+
try {
|
|
160
|
+
return await querySingle(servers[i], opts);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
lastError = e;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { driftMs: null, error: lastError };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Boot-time check that integrates with the framework's logging policy.
|
|
170
|
+
* Returns a result object with { ok, driftMs, severity, message }.
|
|
171
|
+
* Caller (db.init) decides whether to exit.
|
|
172
|
+
*/
|
|
173
|
+
async function bootCheck(opts) {
|
|
174
|
+
opts = opts || {};
|
|
175
|
+
var result = await checkDrift(opts);
|
|
176
|
+
if (result.driftMs === null) {
|
|
177
|
+
return {
|
|
178
|
+
ok: true, // unreachable NTP isn't a hard failure
|
|
179
|
+
severity: "warning",
|
|
180
|
+
driftMs: null,
|
|
181
|
+
message: "NTP unreachable: " + (result.error && result.error.message) +
|
|
182
|
+
" (continuing — set BLAMEJS_NTP_STRICT=1 to fail closed)",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
var absMs = Math.abs(result.driftMs);
|
|
186
|
+
var driftStr = (result.driftMs >= 0 ? "+" : "") + result.driftMs + "ms";
|
|
187
|
+
var fatalMs = (opts && typeof opts.driftFatalMs === "number") ? opts.driftFatalMs : thresholds.fatalMs;
|
|
188
|
+
var warnMs = (opts && typeof opts.driftWarnMs === "number") ? opts.driftWarnMs : thresholds.warnMs;
|
|
189
|
+
if (fatalMs > 0 && absMs >= fatalMs) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
severity: "fatal",
|
|
193
|
+
driftMs: result.driftMs,
|
|
194
|
+
server: result.server,
|
|
195
|
+
message: "clock drift " + driftStr + " from " + result.server +
|
|
196
|
+
" (>= " + fatalMs + "ms) — refuse to boot",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (warnMs > 0 && absMs >= warnMs) {
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
severity: "warning",
|
|
203
|
+
driftMs: result.driftMs,
|
|
204
|
+
server: result.server,
|
|
205
|
+
message: "clock drift " + driftStr + " from " + result.server +
|
|
206
|
+
" (>= " + warnMs + "ms) — investigate",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
severity: "info",
|
|
212
|
+
driftMs: result.driftMs,
|
|
213
|
+
server: result.server,
|
|
214
|
+
message: "clock drift " + driftStr + " from " + result.server,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
querySingle: querySingle,
|
|
220
|
+
checkDrift: checkDrift,
|
|
221
|
+
bootCheck: bootCheck,
|
|
222
|
+
setThresholds: setThresholds,
|
|
223
|
+
getThresholds: getThresholds,
|
|
224
|
+
DEFAULT_SERVERS: DEFAULT_SERVERS,
|
|
225
|
+
DEFAULT_DRIFT_WARN_MS: DEFAULT_DRIFT_WARN_MS,
|
|
226
|
+
DEFAULT_DRIFT_FATAL_MS: DEFAULT_DRIFT_FATAL_MS,
|
|
227
|
+
NTP_TO_UNIX_OFFSET_SECONDS: NTP_TO_UNIX_OFFSET_SECONDS,
|
|
228
|
+
_resetThresholdsForTest: _resetThresholdsForTest,
|
|
229
|
+
};
|