Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7789f5a44 | |||
| 2638990667 | |||
| c33ecdc26f | |||
| b033d80927 |
17
changelog.md
17
changelog.md
@@ -1,5 +1,22 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-13 - 5.4.2 - fix(dcrouter)
|
||||||
|
improve domain pattern matching to support routing-glob and wildcard patterns and use matching logic when resolving routes
|
||||||
|
|
||||||
|
- Support routing-glob patterns beginning with '*' (e.g. *example.com) to match base domain, wildcard form, and subdomains
|
||||||
|
- Treat standard wildcard patterns ('*.example.com') as matching both the base domain (example.com) and its subdomains
|
||||||
|
- Use isDomainMatch when resolving routes instead of exact array includes to allow pattern matching
|
||||||
|
- Normalize domain and pattern to lowercase and simplify equality checks
|
||||||
|
|
||||||
|
## 2026-02-13 - 5.4.1 - fix(network,dcrouter)
|
||||||
|
Always register SmartProxy certificate event handlers and include total bytes + improved connection metrics in network stats/UI
|
||||||
|
|
||||||
|
- Always register SmartProxy 'certificate-issued', 'certificate-renewed', and 'certificate-failed' handlers (previously only registered when acmeConfig was present) so certificate events are processed regardless of provisioning path.
|
||||||
|
- Add totalBytes (in/out) to network stats and propagate it through ts_interfaces and app state so total data transferred is available to the UI.
|
||||||
|
- Combine metricsManager.getNetworkStats with collectServerStats to compute activeConnections and adjust connectionDetails/TopEndpoints handling.
|
||||||
|
- Update ops UI to display totalBytes in throughput cards and remove a redundant network-specific auto-refresh fetch.
|
||||||
|
- Type and state updates: ts_interfaces/data/stats.ts and ts_web/appstate.ts updated with totalBytes and initialization/default mapping adjusted.
|
||||||
|
|
||||||
## 2026-02-13 - 5.4.0 - feat(certificates)
|
## 2026-02-13 - 5.4.0 - feat(certificates)
|
||||||
include certificate source/issuer and Rust-side status checks; pass eventComms into certProvisionFunction and record expiry information
|
include certificate source/issuer and Rust-side status checks; pass eventComms into certProvisionFunction and record expiry information
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "5.4.0",
|
"version": "5.4.2",
|
||||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '5.4.0',
|
version: '5.4.2',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -491,42 +491,41 @@ export class DcRouter {
|
|||||||
console.error('[DcRouter] Error stack:', err.stack);
|
console.error('[DcRouter] Error stack:', err.stack);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (acmeConfig) {
|
// Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
|
||||||
this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
||||||
console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||||
const routeName = this.findRouteNameForDomain(event.domain);
|
const routeName = this.findRouteNameForDomain(event.domain);
|
||||||
if (routeName) {
|
if (routeName) {
|
||||||
this.certificateStatusMap.set(routeName, {
|
this.certificateStatusMap.set(routeName, {
|
||||||
status: 'valid', domain: event.domain,
|
status: 'valid', domain: event.domain,
|
||||||
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
||||||
source: event.source,
|
source: event.source,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
||||||
console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||||
const routeName = this.findRouteNameForDomain(event.domain);
|
const routeName = this.findRouteNameForDomain(event.domain);
|
||||||
if (routeName) {
|
if (routeName) {
|
||||||
this.certificateStatusMap.set(routeName, {
|
this.certificateStatusMap.set(routeName, {
|
||||||
status: 'valid', domain: event.domain,
|
status: 'valid', domain: event.domain,
|
||||||
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
||||||
source: event.source,
|
source: event.source,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
|
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
|
||||||
console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error);
|
console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error);
|
||||||
const routeName = this.findRouteNameForDomain(event.domain);
|
const routeName = this.findRouteNameForDomain(event.domain);
|
||||||
if (routeName) {
|
if (routeName) {
|
||||||
this.certificateStatusMap.set(routeName, {
|
this.certificateStatusMap.set(routeName, {
|
||||||
status: 'failed', domain: event.domain, error: event.error,
|
status: 'failed', domain: event.domain, error: event.error,
|
||||||
source: event.source,
|
source: event.source,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Start SmartProxy
|
// Start SmartProxy
|
||||||
console.log('[DcRouter] Starting SmartProxy...');
|
console.log('[DcRouter] Starting SmartProxy...');
|
||||||
@@ -675,24 +674,25 @@ export class DcRouter {
|
|||||||
* @returns Whether the domain matches the pattern
|
* @returns Whether the domain matches the pattern
|
||||||
*/
|
*/
|
||||||
private isDomainMatch(domain: string, pattern: string): boolean {
|
private isDomainMatch(domain: string, pattern: string): boolean {
|
||||||
// Normalize inputs
|
|
||||||
domain = domain.toLowerCase();
|
domain = domain.toLowerCase();
|
||||||
pattern = pattern.toLowerCase();
|
pattern = pattern.toLowerCase();
|
||||||
|
|
||||||
// Check for exact match
|
if (domain === pattern) return true;
|
||||||
if (domain === pattern) {
|
|
||||||
return true;
|
// Routing-glob: *example.com matches example.com, sub.example.com, *.example.com
|
||||||
|
if (pattern.startsWith('*') && !pattern.startsWith('*.')) {
|
||||||
|
const baseDomain = pattern.slice(1); // *nevermind.cloud → nevermind.cloud
|
||||||
|
if (domain === baseDomain || domain === `*.${baseDomain}`) return true;
|
||||||
|
if (domain.endsWith(baseDomain) && domain.length > baseDomain.length) return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for wildcard match (*.example.com)
|
// Standard wildcard: *.example.com matches sub.example.com and example.com
|
||||||
if (pattern.startsWith('*.')) {
|
if (pattern.startsWith('*.')) {
|
||||||
const patternSuffix = pattern.slice(2); // Remove the "*." prefix
|
const suffix = pattern.slice(2);
|
||||||
|
if (domain === suffix) return true;
|
||||||
// Check if domain ends with the pattern suffix and has at least one character before it
|
return domain.endsWith(suffix) && domain.length > suffix.length;
|
||||||
return domain.endsWith(patternSuffix) && domain.length > patternSuffix.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No match
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,7 +706,9 @@ export class DcRouter {
|
|||||||
const routeDomains = Array.isArray(route.match.domains)
|
const routeDomains = Array.isArray(route.match.domains)
|
||||||
? route.match.domains
|
? route.match.domains
|
||||||
: [route.match.domains];
|
: [route.match.domains];
|
||||||
if (routeDomains.includes(domain)) return route.name;
|
for (const pattern of routeDomains) {
|
||||||
|
if (this.isDomainMatch(domain, pattern)) return route.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,26 +251,21 @@ export class StatsHandler {
|
|||||||
|
|
||||||
if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) {
|
if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
promises.push(
|
promises.push(
|
||||||
this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats().then(stats => {
|
(async () => {
|
||||||
const connectionDetails: interfaces.data.IConnectionDetails[] = [];
|
const stats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
||||||
stats.connectionsByIP.forEach((count, ip) => {
|
const serverStats = await this.collectServerStats();
|
||||||
connectionDetails.push({
|
|
||||||
remoteAddress: ip,
|
|
||||||
protocol: 'https' as any,
|
|
||||||
state: 'established' as any,
|
|
||||||
startTime: Date.now(),
|
|
||||||
bytesIn: 0,
|
|
||||||
bytesOut: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
metrics.network = {
|
metrics.network = {
|
||||||
totalBandwidth: {
|
totalBandwidth: {
|
||||||
in: stats.throughputRate.bytesInPerSecond,
|
in: stats.throughputRate.bytesInPerSecond,
|
||||||
out: stats.throughputRate.bytesOutPerSecond,
|
out: stats.throughputRate.bytesOutPerSecond,
|
||||||
},
|
},
|
||||||
activeConnections: stats.connectionsByIP.size,
|
totalBytes: {
|
||||||
connectionDetails: connectionDetails.slice(0, 50), // Limit to 50 connections
|
in: stats.totalDataTransferred.bytesIn,
|
||||||
|
out: stats.totalDataTransferred.bytesOut,
|
||||||
|
},
|
||||||
|
activeConnections: serverStats.activeConnections,
|
||||||
|
connectionDetails: [],
|
||||||
topEndpoints: stats.topIPs.map(ip => ({
|
topEndpoints: stats.topIPs.map(ip => ({
|
||||||
endpoint: ip.ip,
|
endpoint: ip.ip,
|
||||||
requests: ip.count,
|
requests: ip.count,
|
||||||
@@ -280,7 +275,7 @@ export class StatsHandler {
|
|||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
})
|
})()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ export interface INetworkMetrics {
|
|||||||
in: number;
|
in: number;
|
||||||
out: number;
|
out: number;
|
||||||
};
|
};
|
||||||
|
totalBytes?: {
|
||||||
|
in: number;
|
||||||
|
out: number;
|
||||||
|
};
|
||||||
activeConnections: number;
|
activeConnections: number;
|
||||||
connectionDetails: IConnectionDetails[];
|
connectionDetails: IConnectionDetails[];
|
||||||
topEndpoints: Array<{
|
topEndpoints: Array<{
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '5.4.0',
|
version: '5.4.2',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export interface INetworkState {
|
|||||||
connections: interfaces.data.IConnectionInfo[];
|
connections: interfaces.data.IConnectionInfo[];
|
||||||
connectionsByIP: { [ip: string]: number };
|
connectionsByIP: { [ip: string]: number };
|
||||||
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
|
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
|
||||||
|
totalBytes: { in: number; out: number };
|
||||||
topIPs: Array<{ ip: string; count: number }>;
|
topIPs: Array<{ ip: string; count: number }>;
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@@ -144,6 +145,7 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
|||||||
connections: [],
|
connections: [],
|
||||||
connectionsByIP: {},
|
connectionsByIP: {},
|
||||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
|
totalBytes: { in: 0, out: 0 },
|
||||||
topIPs: [],
|
topIPs: [],
|
||||||
lastUpdated: 0,
|
lastUpdated: 0,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -421,6 +423,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
connections: connectionsResponse.connections,
|
connections: connectionsResponse.connections,
|
||||||
connectionsByIP,
|
connectionsByIP,
|
||||||
throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
|
totalBytes: networkStatsResponse.totalDataTransferred
|
||||||
|
? { in: networkStatsResponse.totalDataTransferred.bytesIn, out: networkStatsResponse.totalDataTransferred.bytesOut }
|
||||||
|
: { in: 0, out: 0 },
|
||||||
topIPs: networkStatsResponse.topIPs || [],
|
topIPs: networkStatsResponse.topIPs || [],
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -790,6 +795,7 @@ async function dispatchCombinedRefreshAction() {
|
|||||||
bytesInPerSecond: network.totalBandwidth.in,
|
bytesInPerSecond: network.totalBandwidth.in,
|
||||||
bytesOutPerSecond: network.totalBandwidth.out
|
bytesOutPerSecond: network.totalBandwidth.out
|
||||||
},
|
},
|
||||||
|
totalBytes: network.totalBytes || { in: 0, out: 0 },
|
||||||
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -805,6 +811,7 @@ async function dispatchCombinedRefreshAction() {
|
|||||||
bytesInPerSecond: network.totalBandwidth.in,
|
bytesInPerSecond: network.totalBandwidth.in,
|
||||||
bytesOutPerSecond: network.totalBandwidth.out
|
bytesOutPerSecond: network.totalBandwidth.out
|
||||||
},
|
},
|
||||||
|
totalBytes: network.totalBytes || { in: 0, out: 0 },
|
||||||
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -845,13 +852,6 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
|
|||||||
refreshInterval = setInterval(() => {
|
refreshInterval = setInterval(() => {
|
||||||
// Use combined refresh action for efficiency
|
// Use combined refresh action for efficiency
|
||||||
dispatchCombinedRefreshAction();
|
dispatchCombinedRefreshAction();
|
||||||
|
|
||||||
// If network view is active, also ensure we have fresh network data
|
|
||||||
const currentView = uiStatePart.getState().activeView;
|
|
||||||
if (currentView === 'network') {
|
|
||||||
// Network view needs more frequent updates, fetch directly
|
|
||||||
networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
|
|
||||||
}
|
|
||||||
}, uiState.refreshInterval);
|
}, uiState.refreshInterval);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -426,6 +426,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'download',
|
icon: 'download',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
|
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'throughputOut',
|
id: 'throughputOut',
|
||||||
@@ -435,6 +436,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'upload',
|
icon: 'upload',
|
||||||
color: '#8b5cf6',
|
color: '#8b5cf6',
|
||||||
|
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user