Compare commits

..

8 Commits

Author SHA1 Message Date
2dbdf2d2b1 v5.4.3
Some checks failed
Docker (tags) / security (push) Has been cancelled
Docker (tags) / test (push) Has been cancelled
Docker (tags) / release (push) Has been cancelled
Docker (tags) / metadata (push) Has been cancelled
2026-02-14 09:25:59 +00:00
383e0adc23 fix(dependencies): bump @push.rocks/smartproxy to ^25.1.0 2026-02-14 09:25:59 +00:00
d7789f5a44 v5.4.2
Some checks failed
Docker (tags) / security (push) Has been cancelled
Docker (tags) / test (push) Has been cancelled
Docker (tags) / release (push) Has been cancelled
Docker (tags) / metadata (push) Has been cancelled
2026-02-13 23:16:25 +00:00
2638990667 fix(dcrouter): improve domain pattern matching to support routing-glob and wildcard patterns and use matching logic when resolving routes 2026-02-13 23:16:25 +00:00
c33ecdc26f v5.4.1
Some checks failed
Docker (tags) / security (push) Has been cancelled
Docker (tags) / test (push) Has been cancelled
Docker (tags) / release (push) Has been cancelled
Docker (tags) / metadata (push) Has been cancelled
2026-02-13 22:03:23 +00:00
b033d80927 fix(network,dcrouter): Always register SmartProxy certificate event handlers and include total bytes + improved connection metrics in network stats/UI 2026-02-13 22:03:23 +00:00
cf5d616769 v5.4.0
Some checks failed
Docker (tags) / security (push) Has been cancelled
Docker (tags) / test (push) Has been cancelled
Docker (tags) / release (push) Has been cancelled
Docker (tags) / metadata (push) Has been cancelled
2026-02-13 21:37:52 +00:00
8e722f5ab6 feat(certificates): include certificate source/issuer and Rust-side status checks; pass eventComms into certProvisionFunction and record expiry information 2026-02-13 21:37:52 +00:00
11 changed files with 142 additions and 87 deletions

View File

@@ -1,5 +1,36 @@
# Changelog
## 2026-02-14 - 5.4.3 - fix(dependencies)
bump @push.rocks/smartproxy to ^25.1.0
- Updated @push.rocks/smartproxy from ^25.0.0 to ^25.1.0 in package.json
## 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)
include certificate source/issuer and Rust-side status checks; pass eventComms into certProvisionFunction and record expiry information
- bump @push.rocks/smartproxy dependency to ^25.0.0
- add optional 'source' field to certificate status and propagate event.source when certificates are issued, renewed, or failed
- change smartProxy.certProvisionFunction signature to accept eventComms; use it to log attempts, set source and expiryDate, and fall back to http-01 on DNS-01 failure
- make buildCertificateOverview async and query smartProxy.getCertificateStatus for a route when event-based status is unknown
- improve logging to include certificate source and more contextual messages
## 2026-02-13 - 5.3.0 - feat(certificates)
add certificate overview and reprovisioning in ops UI and API; track SmartProxy certificate events

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "5.3.0",
"version": "5.4.3",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -49,7 +49,7 @@
"@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartproxy": "^24.0.0",
"@push.rocks/smartproxy": "^25.1.0",
"@push.rocks/smartradius": "^1.1.1",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10",

10
pnpm-lock.yaml generated
View File

@@ -75,8 +75,8 @@ importers:
specifier: ^4.2.3
version: 4.2.3
'@push.rocks/smartproxy':
specifier: ^24.0.0
version: 24.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
specifier: ^25.1.0
version: 25.1.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
'@push.rocks/smartradius':
specifier: ^1.1.1
version: 1.1.1
@@ -1040,8 +1040,8 @@ packages:
'@push.rocks/smartpromise@4.2.3':
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
'@push.rocks/smartproxy@24.0.0':
resolution: {integrity: sha512-xSz6mrV59xmuiuaBgej6Fq611r9+Ay0ad2XiZAP/XGrkWykgQNeDZqzAq8dadmaCqO/3bVfH/mXlEYaKrDyTYA==}
'@push.rocks/smartproxy@25.1.0':
resolution: {integrity: sha512-VA2Vu4ne1XJRF7lbd2Z70WPnYjcaSE6q3fmLCXXNfeRAsOw28xWidgQaJer/64G8HWZb0M6ygYB0jZ3ac3WJ2Q==}
'@push.rocks/smartpuppeteer@2.0.5':
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
@@ -6441,7 +6441,7 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@24.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
'@push.rocks/smartproxy@25.1.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
dependencies:
'@push.rocks/lik': 6.2.2
'@push.rocks/smartacme': 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '5.3.0',
version: '5.4.3',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -189,6 +189,7 @@ export class DcRouter {
domain: string;
expiryDate?: string;
issuedAt?: string;
source?: string;
error?: string;
}>();
@@ -450,9 +451,14 @@ export class DcRouter {
});
await this.smartAcme.start();
smartProxyConfig.certProvisionFunction = async (domain: string) => {
smartProxyConfig.certProvisionFunction = async (domain, eventComms) => {
try {
eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
eventComms.setSource('smartacme-dns-01');
const cert = await this.smartAcme.getCertificateForDomain(domain);
if (cert.validUntil) {
eventComms.setExpiryDate(new Date(cert.validUntil));
}
return {
id: cert.id,
domainName: cert.domainName,
@@ -463,7 +469,7 @@ export class DcRouter {
csr: cert.csr,
};
} catch (err) {
console.error(`[DcRouter] SmartAcme DNS-01 failed for ${domain}, falling back to http-01:`, err.message);
eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${err.message}, falling back to http-01`);
return 'http01';
}
};
@@ -485,39 +491,41 @@ export class DcRouter {
console.error('[DcRouter] Error stack:', err.stack);
});
if (acmeConfig) {
this.smartProxy.on('certificate-issued', (event) => {
console.log(`[DcRouter] Certificate issued for ${event.domain}, expires ${event.expiryDate}`);
const routeName = this.findRouteNameForDomain(event.domain);
if (routeName) {
this.certificateStatusMap.set(routeName, {
status: 'valid', domain: event.domain,
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
});
}
});
// Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
const routeName = this.findRouteNameForDomain(event.domain);
if (routeName) {
this.certificateStatusMap.set(routeName, {
status: 'valid', domain: event.domain,
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
source: event.source,
});
}
});
this.smartProxy.on('certificate-renewed', (event) => {
console.log(`[DcRouter] Certificate renewed for ${event.domain}, expires ${event.expiryDate}`);
const routeName = this.findRouteNameForDomain(event.domain);
if (routeName) {
this.certificateStatusMap.set(routeName, {
status: 'valid', domain: event.domain,
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
});
}
});
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
const routeName = this.findRouteNameForDomain(event.domain);
if (routeName) {
this.certificateStatusMap.set(routeName, {
status: 'valid', domain: event.domain,
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
source: event.source,
});
}
});
this.smartProxy.on('certificate-failed', (event) => {
console.error(`[DcRouter] Certificate failed for ${event.domain}:`, event.error);
const routeName = this.findRouteNameForDomain(event.domain);
if (routeName) {
this.certificateStatusMap.set(routeName, {
status: 'failed', domain: event.domain, error: event.error,
});
}
});
}
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error);
const routeName = this.findRouteNameForDomain(event.domain);
if (routeName) {
this.certificateStatusMap.set(routeName, {
status: 'failed', domain: event.domain, error: event.error,
source: event.source,
});
}
});
// Start SmartProxy
console.log('[DcRouter] Starting SmartProxy...');
@@ -666,24 +674,25 @@ export class DcRouter {
* @returns Whether the domain matches the pattern
*/
private isDomainMatch(domain: string, pattern: string): boolean {
// Normalize inputs
domain = domain.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('*.')) {
const patternSuffix = pattern.slice(2); // Remove the "*." prefix
// Check if domain ends with the pattern suffix and has at least one character before it
return domain.endsWith(patternSuffix) && domain.length > patternSuffix.length;
const suffix = pattern.slice(2);
if (domain === suffix) return true;
return domain.endsWith(suffix) && domain.length > suffix.length;
}
// No match
return false;
}
@@ -697,7 +706,9 @@ export class DcRouter {
const routeDomains = Array.isArray(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;
}

View File

@@ -16,7 +16,7 @@ export class CertificateHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
'getCertificateOverview',
async (dataArg) => {
const certificates = this.buildCertificateOverview();
const certificates = await this.buildCertificateOverview();
const summary = this.buildSummary(certificates);
return { certificates, summary };
}
@@ -34,7 +34,7 @@ export class CertificateHandler {
);
}
private buildCertificateOverview(): interfaces.requests.ICertificateInfo[] {
private async buildCertificateOverview(): Promise<interfaces.requests.ICertificateInfo[]> {
const dcRouter = this.opsServerRef.dcRouterRef;
const smartProxy = dcRouter.smartProxy;
if (!smartProxy) return [];
@@ -82,14 +82,26 @@ export class CertificateHandler {
expiryDate = eventStatus.expiryDate;
issuedAt = eventStatus.issuedAt;
error = eventStatus.error;
if (eventStatus.source) {
issuer = eventStatus.source;
}
}
// Try to get Rust-side certificate data
try {
// getCertificateStatus is async but we're in a sync context
// We'll rely on event-based data primarily
} catch {
// Ignore errors from Rust bridge
// Try Rust-side certificate status if no event data
if (status === 'unknown') {
try {
const rustStatus = await smartProxy.getCertificateStatus(route.name);
if (rustStatus) {
if (rustStatus.expiryDate) expiryDate = rustStatus.expiryDate;
if (rustStatus.issuer) issuer = rustStatus.issuer;
if (rustStatus.issuedAt) issuedAt = rustStatus.issuedAt;
if (rustStatus.status === 'valid' || rustStatus.status === 'expired') {
status = rustStatus.status;
}
}
} catch {
// Rust bridge may not support this command yet — ignore
}
}
// Compute status from expiry date if we have one and status is still valid/unknown

View File

@@ -251,26 +251,21 @@ export class StatsHandler {
if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) {
promises.push(
this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats().then(stats => {
const connectionDetails: interfaces.data.IConnectionDetails[] = [];
stats.connectionsByIP.forEach((count, ip) => {
connectionDetails.push({
remoteAddress: ip,
protocol: 'https' as any,
state: 'established' as any,
startTime: Date.now(),
bytesIn: 0,
bytesOut: 0,
});
});
(async () => {
const stats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
const serverStats = await this.collectServerStats();
metrics.network = {
totalBandwidth: {
in: stats.throughputRate.bytesInPerSecond,
out: stats.throughputRate.bytesOutPerSecond,
},
activeConnections: stats.connectionsByIP.size,
connectionDetails: connectionDetails.slice(0, 50), // Limit to 50 connections
totalBytes: {
in: stats.totalDataTransferred.bytesIn,
out: stats.totalDataTransferred.bytesOut,
},
activeConnections: serverStats.activeConnections,
connectionDetails: [],
topEndpoints: stats.topIPs.map(ip => ({
endpoint: ip.ip,
requests: ip.count,
@@ -280,7 +275,7 @@ export class StatsHandler {
},
})),
};
})
})()
);
}

View File

@@ -116,6 +116,10 @@ export interface INetworkMetrics {
in: number;
out: number;
};
totalBytes?: {
in: number;
out: number;
};
activeConnections: number;
connectionDetails: IConnectionDetails[];
topEndpoints: Array<{

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '5.3.0',
version: '5.4.3',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -47,6 +47,7 @@ export interface INetworkState {
connections: interfaces.data.IConnectionInfo[];
connectionsByIP: { [ip: string]: number };
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
totalBytes: { in: number; out: number };
topIPs: Array<{ ip: string; count: number }>;
lastUpdated: number;
isLoading: boolean;
@@ -144,6 +145,7 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
connections: [],
connectionsByIP: {},
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
totalBytes: { in: 0, out: 0 },
topIPs: [],
lastUpdated: 0,
isLoading: false,
@@ -421,6 +423,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
connections: connectionsResponse.connections,
connectionsByIP,
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 || [],
lastUpdated: Date.now(),
isLoading: false,
@@ -790,6 +795,7 @@ async function dispatchCombinedRefreshAction() {
bytesInPerSecond: network.totalBandwidth.in,
bytesOutPerSecond: network.totalBandwidth.out
},
totalBytes: network.totalBytes || { in: 0, out: 0 },
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
lastUpdated: Date.now(),
isLoading: false,
@@ -805,6 +811,7 @@ async function dispatchCombinedRefreshAction() {
bytesInPerSecond: network.totalBandwidth.in,
bytesOutPerSecond: network.totalBandwidth.out
},
totalBytes: network.totalBytes || { in: 0, out: 0 },
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
lastUpdated: Date.now(),
isLoading: false,
@@ -845,13 +852,6 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
refreshInterval = setInterval(() => {
// Use combined refresh action for efficiency
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);
}
} else {

View File

@@ -426,6 +426,7 @@ export class OpsViewNetwork extends DeesElement {
type: 'number',
icon: 'download',
color: '#22c55e',
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`,
},
{
id: 'throughputOut',
@@ -435,6 +436,7 @@ export class OpsViewNetwork extends DeesElement {
type: 'number',
icon: 'upload',
color: '#8b5cf6',
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
},
];