@serve.zone/dcrouter 10.1.0 → 10.1.2
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_serve/bundle.js +554 -554
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +17 -2
- package/dist_ts/monitoring/classes.metricsmanager.js +11 -5
- package/dist_ts/opsserver/handlers/logs.handler.js +9 -3
- package/dist_ts/security/classes.contentscanner.d.ts +4 -0
- package/dist_ts/security/classes.contentscanner.js +7 -1
- package/dist_ts/security/classes.ipreputationchecker.d.ts +10 -0
- package/dist_ts/security/classes.ipreputationchecker.js +31 -6
- package/dist_ts/security/classes.securitylogger.d.ts +4 -0
- package/dist_ts/security/classes.securitylogger.js +7 -1
- package/dist_ts/storage/classes.storagemanager.d.ts +1 -0
- package/dist_ts/storage/classes.storagemanager.js +7 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/ops-view-apitokens.js +3 -3
- package/dist_ts_web/elements/ops-view-logs.js +8 -1
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +19 -2
- package/ts/monitoring/classes.metricsmanager.ts +10 -5
- package/ts/opsserver/handlers/logs.handler.ts +8 -4
- package/ts/security/classes.contentscanner.ts +8 -1
- package/ts/security/classes.ipreputationchecker.ts +33 -6
- package/ts/security/classes.securitylogger.ts +8 -1
- package/ts/storage/classes.storagemanager.ts +6 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/ops-view-apitokens.ts +2 -2
- package/ts_web/elements/ops-view-logs.ts +7 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serve.zone/dcrouter",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "10.1.
|
|
4
|
+
"version": "10.1.2",
|
|
5
5
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@git.zone/tsrun": "^2.0.1",
|
|
25
25
|
"@git.zone/tstest": "^3.1.8",
|
|
26
26
|
"@git.zone/tswatch": "^3.2.0",
|
|
27
|
-
"@types/node": "^25.3.
|
|
27
|
+
"@types/node": "^25.3.3"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@api.global/typedrequest": "^3.2.6",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@push.rocks/smartradius": "^1.1.1",
|
|
54
54
|
"@push.rocks/smartrequest": "^5.0.1",
|
|
55
55
|
"@push.rocks/smartrx": "^3.0.10",
|
|
56
|
-
"@push.rocks/smartstate": "^2.
|
|
56
|
+
"@push.rocks/smartstate": "^2.1.1",
|
|
57
57
|
"@push.rocks/smartunique": "^3.0.9",
|
|
58
58
|
"@serve.zone/catalog": "^2.5.0",
|
|
59
59
|
"@serve.zone/interfaces": "^5.3.0",
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { MetricsManager } from './monitoring/index.js';
|
|
|
23
23
|
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
|
|
24
24
|
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
|
25
25
|
import { RouteConfigManager, ApiTokenManager } from './config/index.js';
|
|
26
|
+
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
|
|
26
27
|
|
|
27
28
|
export interface IDcRouterOptions {
|
|
28
29
|
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
|
@@ -956,6 +957,7 @@ export class DcRouter {
|
|
|
956
957
|
// Stop cache database after other services (they may need it during shutdown)
|
|
957
958
|
if (this.cacheDb) {
|
|
958
959
|
await this.cacheDb.stop().catch(err => logger.log('error', 'Error stopping CacheDb', { error: String(err) }));
|
|
960
|
+
CacheDb.resetInstance();
|
|
959
961
|
}
|
|
960
962
|
|
|
961
963
|
// Clear backoff cache in cert scheduler
|
|
@@ -979,6 +981,11 @@ export class DcRouter {
|
|
|
979
981
|
this.apiTokenManager = undefined;
|
|
980
982
|
this.certificateStatusMap.clear();
|
|
981
983
|
|
|
984
|
+
// Reset security singletons to allow GC
|
|
985
|
+
SecurityLogger.resetInstance();
|
|
986
|
+
ContentScanner.resetInstance();
|
|
987
|
+
IPReputationChecker.resetInstance();
|
|
988
|
+
|
|
982
989
|
logger.log('info', 'All DcRouter services stopped');
|
|
983
990
|
} catch (error) {
|
|
984
991
|
logger.log('error', 'Error during DcRouter shutdown', { error: String(error) });
|
|
@@ -1363,15 +1370,25 @@ export class DcRouter {
|
|
|
1363
1370
|
return;
|
|
1364
1371
|
}
|
|
1365
1372
|
|
|
1373
|
+
// Prevent uncaught exception from socket 'error' events
|
|
1374
|
+
socket.on('error', (err) => {
|
|
1375
|
+
logger.log('error', `DNS socket error: ${err.message}`);
|
|
1376
|
+
if (!socket.destroyed) {
|
|
1377
|
+
socket.destroy();
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1366
1381
|
logger.log('debug', 'DNS socket handler: passing socket to DnsServer');
|
|
1367
|
-
|
|
1382
|
+
|
|
1368
1383
|
try {
|
|
1369
1384
|
// Use the built-in socket handler from smartdns
|
|
1370
1385
|
// This handles HTTP/2, DoH protocol, etc.
|
|
1371
1386
|
await (this.dnsServer as any).handleHttpsSocket(socket);
|
|
1372
1387
|
} catch (error) {
|
|
1373
1388
|
logger.log('error', `DNS socket handler error: ${error.message}`);
|
|
1374
|
-
socket.
|
|
1389
|
+
if (!socket.destroyed) {
|
|
1390
|
+
socket.destroy();
|
|
1391
|
+
}
|
|
1375
1392
|
}
|
|
1376
1393
|
};
|
|
1377
1394
|
}
|
|
@@ -111,6 +111,15 @@ export class MetricsManager {
|
|
|
111
111
|
this.securityMetrics.lastResetDate = currentDate;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// Prune old query timestamps (keep last 5 minutes)
|
|
115
|
+
const fiveMinutesAgo = Date.now() - 300000;
|
|
116
|
+
const idx = this.dnsMetrics.queryTimestamps.findIndex(ts => ts >= fiveMinutesAgo);
|
|
117
|
+
if (idx > 0) {
|
|
118
|
+
this.dnsMetrics.queryTimestamps = this.dnsMetrics.queryTimestamps.slice(idx);
|
|
119
|
+
} else if (idx === -1) {
|
|
120
|
+
this.dnsMetrics.queryTimestamps = [];
|
|
121
|
+
}
|
|
122
|
+
|
|
114
123
|
// Prune old time-series buckets every minute (don't wait for lazy query)
|
|
115
124
|
this.pruneOldBuckets();
|
|
116
125
|
}, 60000); // Check every minute
|
|
@@ -427,13 +436,9 @@ export class MetricsManager {
|
|
|
427
436
|
this.dnsMetrics.cacheMisses++;
|
|
428
437
|
}
|
|
429
438
|
|
|
430
|
-
// Track query timestamp
|
|
439
|
+
// Track query timestamp (pruning moved to resetInterval to avoid O(n) per query)
|
|
431
440
|
this.dnsMetrics.queryTimestamps.push(Date.now());
|
|
432
441
|
|
|
433
|
-
// Keep only timestamps from last 5 minutes
|
|
434
|
-
const fiveMinutesAgo = Date.now() - 300000;
|
|
435
|
-
this.dnsMetrics.queryTimestamps = this.dnsMetrics.queryTimestamps.filter(ts => ts >= fiveMinutesAgo);
|
|
436
|
-
|
|
437
442
|
// Track response time if provided
|
|
438
443
|
if (responseTimeMs) {
|
|
439
444
|
this.dnsMetrics.responseTimes.push(responseTimeMs);
|
|
@@ -318,11 +318,15 @@ export class LogsHandler {
|
|
|
318
318
|
try {
|
|
319
319
|
// Use a timeout to detect hung streams (sendData can hang if the
|
|
320
320
|
// VirtualStream's keepAlive loop has ended)
|
|
321
|
+
let timeoutHandle: ReturnType<typeof setTimeout>;
|
|
321
322
|
await Promise.race([
|
|
322
|
-
virtualStream.sendData(encoder.encode(logData))
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
),
|
|
323
|
+
virtualStream.sendData(encoder.encode(logData)).then((result) => {
|
|
324
|
+
clearTimeout(timeoutHandle);
|
|
325
|
+
return result;
|
|
326
|
+
}),
|
|
327
|
+
new Promise<never>((_, reject) => {
|
|
328
|
+
timeoutHandle = setTimeout(() => reject(new Error('stream send timeout')), 10_000);
|
|
329
|
+
}),
|
|
326
330
|
]);
|
|
327
331
|
} catch {
|
|
328
332
|
// Stream closed, errored, or timed out — clean up
|
|
@@ -182,7 +182,14 @@ export class ContentScanner {
|
|
|
182
182
|
}
|
|
183
183
|
return ContentScanner.instance;
|
|
184
184
|
}
|
|
185
|
-
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Reset the singleton instance (for shutdown/testing)
|
|
188
|
+
*/
|
|
189
|
+
public static resetInstance(): void {
|
|
190
|
+
ContentScanner.instance = undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
186
193
|
/**
|
|
187
194
|
* Scan an email for malicious content
|
|
188
195
|
* @param email The email to scan
|
|
@@ -65,6 +65,8 @@ export class IPReputationChecker {
|
|
|
65
65
|
private reputationCache: LRUCache<string, IReputationResult>;
|
|
66
66
|
private options: Required<IIPReputationOptions>;
|
|
67
67
|
private storageManager?: any; // StorageManager instance
|
|
68
|
+
private saveCacheTimer: ReturnType<typeof setTimeout> | null = null;
|
|
69
|
+
private static readonly SAVE_CACHE_DEBOUNCE_MS = 30_000;
|
|
68
70
|
|
|
69
71
|
// Default DNSBL servers
|
|
70
72
|
private static readonly DEFAULT_DNSBL_SERVERS = [
|
|
@@ -143,7 +145,20 @@ export class IPReputationChecker {
|
|
|
143
145
|
}
|
|
144
146
|
return IPReputationChecker.instance;
|
|
145
147
|
}
|
|
146
|
-
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Reset the singleton instance (for shutdown/testing)
|
|
151
|
+
*/
|
|
152
|
+
public static resetInstance(): void {
|
|
153
|
+
if (IPReputationChecker.instance) {
|
|
154
|
+
if (IPReputationChecker.instance.saveCacheTimer) {
|
|
155
|
+
clearTimeout(IPReputationChecker.instance.saveCacheTimer);
|
|
156
|
+
IPReputationChecker.instance.saveCacheTimer = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
IPReputationChecker.instance = undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
147
162
|
/**
|
|
148
163
|
* Check an IP address's reputation
|
|
149
164
|
* @param ip IP address to check
|
|
@@ -213,12 +228,9 @@ export class IPReputationChecker {
|
|
|
213
228
|
// Update cache with result
|
|
214
229
|
this.reputationCache.set(ip, result);
|
|
215
230
|
|
|
216
|
-
//
|
|
231
|
+
// Schedule debounced cache save if enabled
|
|
217
232
|
if (this.options.enableLocalCache) {
|
|
218
|
-
|
|
219
|
-
this.saveCache().catch(error => {
|
|
220
|
-
logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
|
|
221
|
-
});
|
|
233
|
+
this.debouncedSaveCache();
|
|
222
234
|
}
|
|
223
235
|
|
|
224
236
|
// Log the reputation check
|
|
@@ -447,6 +459,21 @@ export class IPReputationChecker {
|
|
|
447
459
|
});
|
|
448
460
|
}
|
|
449
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Schedule a debounced cache save (at most once per SAVE_CACHE_DEBOUNCE_MS)
|
|
464
|
+
*/
|
|
465
|
+
private debouncedSaveCache(): void {
|
|
466
|
+
if (this.saveCacheTimer) {
|
|
467
|
+
return; // already scheduled
|
|
468
|
+
}
|
|
469
|
+
this.saveCacheTimer = setTimeout(() => {
|
|
470
|
+
this.saveCacheTimer = null;
|
|
471
|
+
this.saveCache().catch(error => {
|
|
472
|
+
logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
|
|
473
|
+
});
|
|
474
|
+
}, IPReputationChecker.SAVE_CACHE_DEBOUNCE_MS);
|
|
475
|
+
}
|
|
476
|
+
|
|
450
477
|
/**
|
|
451
478
|
* Save cache to disk or storage manager
|
|
452
479
|
*/
|
|
@@ -83,7 +83,14 @@ export class SecurityLogger {
|
|
|
83
83
|
}
|
|
84
84
|
return SecurityLogger.instance;
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reset the singleton instance (for shutdown/testing)
|
|
89
|
+
*/
|
|
90
|
+
public static resetInstance(): void {
|
|
91
|
+
SecurityLogger.instance = undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
/**
|
|
88
95
|
* Log a security event
|
|
89
96
|
* @param event The security event to log
|
|
@@ -30,6 +30,7 @@ export type StorageBackend = 'filesystem' | 'custom' | 'memory';
|
|
|
30
30
|
* Provides unified key-value storage with multiple backend support
|
|
31
31
|
*/
|
|
32
32
|
export class StorageManager {
|
|
33
|
+
private static readonly MAX_MEMORY_ENTRIES = 10_000;
|
|
33
34
|
private backend: StorageBackend;
|
|
34
35
|
private memoryStore: Map<string, string> = new Map();
|
|
35
36
|
private config: IStorageConfig;
|
|
@@ -227,6 +228,11 @@ export class StorageManager {
|
|
|
227
228
|
|
|
228
229
|
case 'memory': {
|
|
229
230
|
this.memoryStore.set(key, value);
|
|
231
|
+
// Evict oldest entries if memory store exceeds limit
|
|
232
|
+
while (this.memoryStore.size > StorageManager.MAX_MEMORY_ENTRIES) {
|
|
233
|
+
const firstKey = this.memoryStore.keys().next().value;
|
|
234
|
+
this.memoryStore.delete(firstKey);
|
|
235
|
+
}
|
|
230
236
|
break;
|
|
231
237
|
}
|
|
232
238
|
|
|
@@ -154,7 +154,7 @@ export class OpsViewApiTokens extends DeesElement {
|
|
|
154
154
|
},
|
|
155
155
|
{
|
|
156
156
|
name: 'Roll',
|
|
157
|
-
iconName: 'lucide:
|
|
157
|
+
iconName: 'lucide:rotate-cw',
|
|
158
158
|
type: ['inRow', 'contextmenu'] as any,
|
|
159
159
|
actionFunc: async (actionData: any) => {
|
|
160
160
|
const token = actionData.item as interfaces.data.IApiTokenInfo;
|
|
@@ -306,7 +306,7 @@ export class OpsViewApiTokens extends DeesElement {
|
|
|
306
306
|
},
|
|
307
307
|
{
|
|
308
308
|
name: 'Roll Token',
|
|
309
|
-
iconName: 'lucide:
|
|
309
|
+
iconName: 'lucide:rotate-cw',
|
|
310
310
|
action: async (modalArg: any) => {
|
|
311
311
|
await modalArg.destroy();
|
|
312
312
|
try {
|
|
@@ -76,8 +76,15 @@ export class OpsViewLogs extends DeesElement {
|
|
|
76
76
|
// Wait for xterm terminal to finish initializing (CDN load)
|
|
77
77
|
if (!chartLog.terminalReady) {
|
|
78
78
|
await new Promise<void>((resolve) => {
|
|
79
|
+
let attempts = 0;
|
|
80
|
+
const maxAttempts = 200; // 200 * 50ms = 10 seconds
|
|
79
81
|
const check = () => {
|
|
80
82
|
if (chartLog.terminalReady) { resolve(); return; }
|
|
83
|
+
if (++attempts >= maxAttempts) {
|
|
84
|
+
console.warn('ops-view-logs: terminal ready timeout after 10s');
|
|
85
|
+
resolve(); // resolve gracefully to avoid blocking
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
81
88
|
setTimeout(check, 50);
|
|
82
89
|
};
|
|
83
90
|
check();
|