From 61778bdba8787a13ccb47a4c64583f5d00d4146d Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 8 Jun 2025 07:04:35 +0000 Subject: [PATCH] feat(ops-server): implement TypedRouter integration and modular handler classes --- readme.opsserver.md | 33 ++- test/test.opsserver-api.ts | 83 ++++++ ts/classes.dcrouter.ts | 2 + ts/opsserver/classes.opsserver.ts | 41 ++- ts/opsserver/handlers/admin.handler.ts | 148 ++++++++++ ts/opsserver/handlers/config.handler.ts | 143 +++++++++ ts/opsserver/handlers/index.ts | 5 + ts/opsserver/handlers/logs.handler.ts | 195 ++++++++++++ ts/opsserver/handlers/security.handler.ts | 208 +++++++++++++ ts/opsserver/handlers/stats.handler.ts | 344 ++++++++++++++++++++++ 10 files changed, 1188 insertions(+), 14 deletions(-) create mode 100644 test/test.opsserver-api.ts create mode 100644 ts/opsserver/handlers/admin.handler.ts create mode 100644 ts/opsserver/handlers/config.handler.ts create mode 100644 ts/opsserver/handlers/index.ts create mode 100644 ts/opsserver/handlers/logs.handler.ts create mode 100644 ts/opsserver/handlers/security.handler.ts create mode 100644 ts/opsserver/handlers/stats.handler.ts diff --git a/readme.opsserver.md b/readme.opsserver.md index 31d6431..dda5b2c 100644 --- a/readme.opsserver.md +++ b/readme.opsserver.md @@ -70,14 +70,14 @@ ts_interfaces/ - **Health Check** - [x] `IReq_GetHealthStatus` - Service health monitoring -### Phase 2: Backend Implementation +### Phase 2: Backend Implementation ✓ -#### 2.1 Enhance OpsServer (`ts/opsserver/classes.opsserver.ts`) +#### 2.1 Enhance OpsServer (`ts/opsserver/classes.opsserver.ts`) ✓ -- [ ] Add TypedRouter initialization -- [ ] Use TypedServer's built-in typedrouter -- [ ] CORS is already handled by TypedServer -- [ ] Add handler registration method +- [x] Add TypedRouter initialization +- [x] Use TypedServer's built-in typedrouter +- [x] CORS is already handled by TypedServer +- [x] Add handler registration method ```typescript // Example structure following cloudly pattern @@ -122,15 +122,15 @@ TypedServer (built-in typedrouter at /typedrequest) This allows clean separation of concerns while keeping all handlers accessible through the single `/typedrequest` endpoint. -#### 2.2 Create Handler Classes +#### 2.2 Create Handler Classes ✓ Create modular handlers in `ts/opsserver/handlers/`: -- [ ] `stats.handler.ts` - Server and performance statistics -- [ ] `email.handler.ts` - Email-related operations -- [ ] `dns.handler.ts` - DNS management statistics -- [ ] `security.handler.ts` - Security and reputation metrics -- [ ] `config.handler.ts` - Configuration management +- [x] `stats.handler.ts` - Server and performance statistics +- [x] `security.handler.ts` - Security and reputation metrics +- [x] `config.handler.ts` - Configuration management +- [x] `logs.handler.ts` - Log retrieval and streaming +- [x] `admin.handler.ts` - Authentication and session management Each handler should: - Have its own typedrouter that gets added to OpsServer's router @@ -298,10 +298,17 @@ Create modular components in `ts_web/elements/components/`: - Added `@api.global/typedrequest-interfaces` dependency - All interfaces compile successfully +- **Phase 2: Backend Implementation** - TypedRouter integration and handlers + - Enhanced OpsServer with hierarchical TypedRouter structure + - Created all handler classes with proper TypedHandler registration + - Implemented mock data responses for all endpoints + - Fixed all TypeScript compilation errors + - VirtualStream used for log streaming with Uint8Array encoding + ### Next Steps -- Phase 2: Backend Implementation - Enhance OpsServer and create handlers - Phase 3: Frontend State Management - Set up Smartstate - Phase 4: Frontend Integration - Create API clients and update dashboard +- Phase 5: Create modular UI components --- diff --git a/test/test.opsserver-api.ts b/test/test.opsserver-api.ts new file mode 100644 index 0000000..9ff57bb --- /dev/null +++ b/test/test.opsserver-api.ts @@ -0,0 +1,83 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { DcRouter } from '../ts/index.js'; +import { TypedRequest } from '@api.global/typedrequest'; +import * as interfaces from '../ts_interfaces/index.js'; + +let testDcRouter: DcRouter; + +tap.test('should start DCRouter with OpsServer', async () => { + testDcRouter = new DcRouter({ + // Minimal config for testing + }); + + await testDcRouter.start(); + expect(testDcRouter.opsServer).toBeInstanceOf(Object); +}); + +tap.test('should respond to health status request', async () => { + const healthRequest = new TypedRequest( + 'http://localhost:3000/typedrequest', + 'getHealthStatus' + ); + + const response = await healthRequest.fire({ + detailed: false + }); + + expect(response).toHaveProperty('health'); + expect(response.health.healthy).toBeTrue(); + expect(response.health.services).toHaveProperty('OpsServer'); +}); + +tap.test('should respond to server statistics request', async () => { + const statsRequest = new TypedRequest( + 'http://localhost:3000/typedrequest', + 'getServerStatistics' + ); + + const response = await statsRequest.fire({ + includeHistory: false + }); + + expect(response).toHaveProperty('stats'); + expect(response.stats).toHaveProperty('uptime'); + expect(response.stats).toHaveProperty('cpuUsage'); + expect(response.stats).toHaveProperty('memoryUsage'); +}); + +tap.test('should respond to configuration request', async () => { + const configRequest = new TypedRequest( + 'http://localhost:3000/typedrequest', + 'getConfiguration' + ); + + const response = await configRequest.fire({}); + + expect(response).toHaveProperty('config'); + expect(response.config).toHaveProperty('email'); + expect(response.config).toHaveProperty('dns'); + expect(response.config).toHaveProperty('proxy'); + expect(response.config).toHaveProperty('security'); +}); + +tap.test('should handle log retrieval request', async () => { + const logsRequest = new TypedRequest( + 'http://localhost:3000/typedrequest', + 'getRecentLogs' + ); + + const response = await logsRequest.fire({ + limit: 10 + }); + + expect(response).toHaveProperty('logs'); + expect(response).toHaveProperty('total'); + expect(response).toHaveProperty('hasMore'); + expect(response.logs).toBeArray(); +}); + +tap.test('should stop DCRouter', async () => { + await testDcRouter.stop(); +}); + +export default tap.start(); \ No newline at end of file diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts index 76aa234..40885e5 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -134,6 +134,8 @@ export class DcRouter { public storageManager: StorageManager; public opsServer: OpsServer; + // TypedRouter for API endpoints + public typedrouter = new plugins.typedrequest.TypedRouter(); // Environment access private qenv = new plugins.qenv.Qenv('./', '.nogit/'); diff --git a/ts/opsserver/classes.opsserver.ts b/ts/opsserver/classes.opsserver.ts index 4cd41e3..2423908 100644 --- a/ts/opsserver/classes.opsserver.ts +++ b/ts/opsserver/classes.opsserver.ts @@ -1,13 +1,27 @@ import type DcRouter from '../classes.dcrouter.js'; import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; +import * as handlers from './handlers/index.js'; export class OpsServer { public dcRouterRef: DcRouter; public server: plugins.typedserver.utilityservers.UtilityWebsiteServer; + + // TypedRouter for OpsServer-specific handlers + public typedrouter = new plugins.typedrequest.TypedRouter(); + + // Handler instances + private adminHandler: handlers.AdminHandler; + private configHandler: handlers.ConfigHandler; + private logsHandler: handlers.LogsHandler; + private securityHandler: handlers.SecurityHandler; + private statsHandler: handlers.StatsHandler; constructor(dcRouterRefArg: DcRouter) { this.dcRouterRef = dcRouterRefArg; + + // Add our typedrouter to the dcRouter's main typedrouter + this.dcRouterRef.typedrouter.addTypedRouter(this.typedrouter); } public async start() { @@ -16,9 +30,34 @@ export class OpsServer { feedMetadata: null, serveDir: paths.distServe, }); + + // The server has a built-in typedrouter at /typedrequest + // Add the main dcRouter typedrouter to the server's typedrouter + this.server.typedrouter.addTypedRouter(this.dcRouterRef.typedrouter); + + // Set up handlers + this.setupHandlers(); await this.server.start(3000); } + + /** + * Set up all TypedRequest handlers + */ + private setupHandlers(): void { + // Instantiate all handlers - they self-register with the typedrouter + this.adminHandler = new handlers.AdminHandler(this); + this.configHandler = new handlers.ConfigHandler(this); + this.logsHandler = new handlers.LogsHandler(this); + this.securityHandler = new handlers.SecurityHandler(this); + this.statsHandler = new handlers.StatsHandler(this); + + console.log('✅ OpsServer TypedRequest handlers initialized'); + } - public async stop() {} + public async stop() { + if (this.server) { + await this.server.stop(); + } + } } \ No newline at end of file diff --git a/ts/opsserver/handlers/admin.handler.ts b/ts/opsserver/handlers/admin.handler.ts new file mode 100644 index 0000000..adcd29f --- /dev/null +++ b/ts/opsserver/handlers/admin.handler.ts @@ -0,0 +1,148 @@ +import * as plugins from '../../plugins.js'; +import type { OpsServer } from '../classes.opsserver.js'; +import * as interfaces from '../../../ts_interfaces/index.js'; + +export class AdminHandler { + public typedrouter = new plugins.typedrequest.TypedRouter(); + + // Simple in-memory session storage (in production, use proper session management) + private sessions = new Map(); + + constructor(private opsServerRef: OpsServer) { + // Add this handler's router to the parent + this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); + this.registerHandlers(); + } + + private registerHandlers(): void { + // Admin Login Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'adminLoginWithUsernameAndPassword', + async (dataArg, toolsArg) => { + try { + // TODO: Implement proper authentication + // For now, use a simple hardcoded check + if (dataArg.username === 'admin' && dataArg.password === 'admin') { + const token = plugins.uuid.v4(); + const identity: interfaces.data.IIdentity = { + token, + expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours + permissions: ['admin'], + }; + + // Store session + this.sessions.set(token, { + identity, + createdAt: Date.now(), + lastAccess: Date.now(), + }); + + // Clean up old sessions + this.cleanupSessions(); + + return { + identity, + }; + } else { + return {}; + } + } catch (error) { + return {}; + } + } + ) + ); + + // Admin Logout Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'adminLogout', + async (dataArg, toolsArg) => { + if (dataArg.identity?.token && this.sessions.has(dataArg.identity.token)) { + this.sessions.delete(dataArg.identity.token); + return { + success: true, + }; + } else { + return { + success: false, + }; + } + } + ) + ); + + // Verify Identity Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'verifyIdentity', + async (dataArg, toolsArg) => { + if (!dataArg.identity?.token) { + return { + valid: false, + }; + } + + const session = this.sessions.get(dataArg.identity.token); + if (session && session.identity.expiresAt > Date.now()) { + // Update last access + session.lastAccess = Date.now(); + + return { + valid: true, + identity: session.identity, + }; + } else { + // Clean up expired session + if (session) { + this.sessions.delete(dataArg.identity.token); + } + return { + valid: false, + }; + } + } + ) + ); + } + + /** + * Clean up expired sessions (older than 24 hours) + */ + private cleanupSessions(): void { + const now = Date.now(); + const maxAge = 24 * 60 * 60 * 1000; // 24 hours + + for (const [token, session] of this.sessions.entries()) { + if (now - session.lastAccess > maxAge) { + this.sessions.delete(token); + } + } + } + + /** + * Create a guard for authentication + * This can be used by other handlers to protect endpoints + */ + public createAuthGuard() { + return async (dataArg: { identity?: interfaces.data.IIdentity }) => { + if (!dataArg.identity?.token) { + return false; + } + + const session = this.sessions.get(dataArg.identity.token); + if (session && session.identity.expiresAt > Date.now()) { + // Update last access + session.lastAccess = Date.now(); + return true; + } + + return false; + }; + } +} \ No newline at end of file diff --git a/ts/opsserver/handlers/config.handler.ts b/ts/opsserver/handlers/config.handler.ts new file mode 100644 index 0000000..87cad89 --- /dev/null +++ b/ts/opsserver/handlers/config.handler.ts @@ -0,0 +1,143 @@ +import * as plugins from '../../plugins.js'; +import type { OpsServer } from '../classes.opsserver.js'; +import * as interfaces from '../../../ts_interfaces/index.js'; + +export class ConfigHandler { + public typedrouter = new plugins.typedrequest.TypedRouter(); + + constructor(private opsServerRef: OpsServer) { + // Add this handler's router to the parent + this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); + this.registerHandlers(); + } + + private registerHandlers(): void { + // Get Configuration Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getConfiguration', + async (dataArg, toolsArg) => { + const config = await this.getConfiguration(dataArg.section); + return { + config, + section: dataArg.section, + }; + } + ) + ); + + // Update Configuration Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'updateConfiguration', + async (dataArg, toolsArg) => { + try { + const updatedConfig = await this.updateConfiguration(dataArg.section, dataArg.config); + return { + updated: true, + config: updatedConfig, + }; + } catch (error) { + return { + updated: false, + config: null, + }; + } + } + ) + ); + } + + private async getConfiguration(section?: string): Promise<{ + email: { + enabled: boolean; + ports: number[]; + maxMessageSize: number; + rateLimits: { + perMinute: number; + perHour: number; + perDay: number; + }; + }; + dns: { + enabled: boolean; + port: number; + nameservers: string[]; + caching: boolean; + ttl: number; + }; + proxy: { + enabled: boolean; + httpPort: number; + httpsPort: number; + maxConnections: number; + }; + security: { + blockList: string[]; + rateLimit: boolean; + spamDetection: boolean; + tlsRequired: boolean; + }; + }> { + const dcRouter = this.opsServerRef.dcRouterRef; + + return { + email: { + enabled: !!dcRouter.emailServer, + ports: dcRouter.emailServer ? [25, 465, 587, 2525] : [], + maxMessageSize: 10 * 1024 * 1024, // 10MB default + rateLimits: { + perMinute: 10, + perHour: 100, + perDay: 1000, + }, + }, + dns: { + enabled: !!dcRouter.dnsServer, + port: 53, + nameservers: dcRouter.options.dnsNsDomains || [], + caching: true, + ttl: 300, + }, + proxy: { + enabled: !!dcRouter.smartProxy, + httpPort: 80, + httpsPort: 443, + maxConnections: 1000, + }, + security: { + blockList: [], + rateLimit: true, + spamDetection: true, + tlsRequired: false, + }, + }; + } + + private async updateConfiguration(section: string, config: any): Promise { + // TODO: Implement actual configuration updates + // This would involve: + // 1. Validating the configuration changes + // 2. Applying them to the running services + // 3. Persisting them to storage + // 4. Potentially restarting affected services + + // For now, just validate and return the config + if (section === 'email' && config.maxMessageSize && config.maxMessageSize < 1024) { + throw new Error('Maximum message size must be at least 1KB'); + } + + if (section === 'dns' && config.ttl && (config.ttl < 0 || config.ttl > 86400)) { + throw new Error('DNS TTL must be between 0 and 86400 seconds'); + } + + if (section === 'proxy' && config.maxConnections && config.maxConnections < 1) { + throw new Error('Maximum connections must be at least 1'); + } + + // In a real implementation, apply the changes here + // For now, return the current configuration + const currentConfig = await this.getConfiguration(section); + return currentConfig; + } +} \ No newline at end of file diff --git a/ts/opsserver/handlers/index.ts b/ts/opsserver/handlers/index.ts new file mode 100644 index 0000000..f867286 --- /dev/null +++ b/ts/opsserver/handlers/index.ts @@ -0,0 +1,5 @@ +export * from './admin.handler.js'; +export * from './config.handler.js'; +export * from './logs.handler.js'; +export * from './security.handler.js'; +export * from './stats.handler.js'; \ No newline at end of file diff --git a/ts/opsserver/handlers/logs.handler.ts b/ts/opsserver/handlers/logs.handler.ts new file mode 100644 index 0000000..9989dc4 --- /dev/null +++ b/ts/opsserver/handlers/logs.handler.ts @@ -0,0 +1,195 @@ +import * as plugins from '../../plugins.js'; +import type { OpsServer } from '../classes.opsserver.js'; +import * as interfaces from '../../../ts_interfaces/index.js'; + +export class LogsHandler { + public typedrouter = new plugins.typedrequest.TypedRouter(); + + constructor(private opsServerRef: OpsServer) { + // Add this handler's router to the parent + this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); + this.registerHandlers(); + } + + private registerHandlers(): void { + // Get Recent Logs Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getRecentLogs', + async (dataArg, toolsArg) => { + const logs = await this.getRecentLogs( + dataArg.level, + dataArg.category, + dataArg.limit || 100, + dataArg.offset || 0, + dataArg.search, + dataArg.timeRange + ); + + return { + logs, + total: logs.length, // TODO: Implement proper total count + hasMore: false, // TODO: Implement proper pagination + }; + } + ) + ); + + // Get Log Stream Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getLogStream', + async (dataArg, toolsArg) => { + // Create a virtual stream for log streaming + const virtualStream = new plugins.typedrequest.VirtualStream(); + + // Set up log streaming + const streamLogs = this.setupLogStream( + virtualStream, + dataArg.filters?.level, + dataArg.filters?.category, + dataArg.follow + ); + + // Start streaming + streamLogs.start(); + + // VirtualStream handles cleanup automatically + + return { + logStream: virtualStream as any, // Cast to IVirtualStream interface + }; + } + ) + ); + } + + private async getRecentLogs( + level?: 'error' | 'warn' | 'info' | 'debug', + category?: 'smtp' | 'dns' | 'security' | 'system' | 'email', + limit: number = 100, + offset: number = 0, + search?: string, + timeRange?: '1h' | '6h' | '24h' | '7d' | '30d' + ): Promise> { + // TODO: Implement actual log retrieval from storage or logger + // For now, return mock data + const mockLogs: Array<{ + timestamp: number; + level: 'debug' | 'info' | 'warn' | 'error'; + category: 'smtp' | 'dns' | 'security' | 'system' | 'email'; + message: string; + metadata?: any; + }> = []; + + const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email']; + const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug']; + const now = Date.now(); + + // Generate some mock log entries + for (let i = 0; i < 50; i++) { + const mockCategory = categories[Math.floor(Math.random() * categories.length)]; + const mockLevel = levels[Math.floor(Math.random() * levels.length)]; + + // Filter by requested criteria + if (level && mockLevel !== level) continue; + if (category && mockCategory !== category) continue; + + mockLogs.push({ + timestamp: now - (i * 60000), // 1 minute apart + level: mockLevel, + category: mockCategory, + message: `Sample log message ${i} from ${mockCategory}`, + metadata: { + requestId: plugins.uuid.v4(), + }, + }); + } + + // Apply pagination + return mockLogs.slice(offset, offset + limit); + } + + private setupLogStream( + virtualStream: plugins.typedrequest.VirtualStream, + levelFilter?: string[], + categoryFilter?: string[], + follow: boolean = true + ): { + start: () => void; + stop: () => void; + } { + let intervalId: NodeJS.Timeout | null = null; + let logIndex = 0; + + const start = () => { + if (!follow) { + // Send existing logs and close + this.getRecentLogs( + levelFilter?.[0] as any, + categoryFilter?.[0] as any, + 100, + 0 + ).then(logs => { + logs.forEach(log => { + const logData = JSON.stringify(log); + const encoder = new TextEncoder(); + virtualStream.sendData(encoder.encode(logData)); + }); + // VirtualStream doesn't have end() method - it closes automatically + }); + return; + } + + // For follow mode, simulate real-time log streaming + intervalId = setInterval(() => { + const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email']; + const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug']; + + const mockCategory = categories[Math.floor(Math.random() * categories.length)]; + const mockLevel = levels[Math.floor(Math.random() * levels.length)]; + + // Filter by requested criteria + if (levelFilter && !levelFilter.includes(mockLevel)) return; + if (categoryFilter && !categoryFilter.includes(mockCategory)) return; + + const logEntry = { + timestamp: Date.now(), + level: mockLevel, + category: mockCategory, + message: `Real-time log ${logIndex++} from ${mockCategory}`, + metadata: { + requestId: plugins.uuid.v4(), + }, + }; + + const logData = JSON.stringify(logEntry); + const encoder = new TextEncoder(); + virtualStream.sendData(encoder.encode(logData)); + }, 2000); // Send a log every 2 seconds + + // TODO: Hook into actual logger events + // logger.on('log', (logEntry) => { + // if (matchesCriteria(logEntry, level, service)) { + // virtualStream.sendData(formatLogEntry(logEntry)); + // } + // }); + }; + + const stop = () => { + if (intervalId) { + clearInterval(intervalId); + intervalId = null; + } + // TODO: Unhook from logger events + }; + + return { start, stop }; + } +} \ No newline at end of file diff --git a/ts/opsserver/handlers/security.handler.ts b/ts/opsserver/handlers/security.handler.ts new file mode 100644 index 0000000..24f70d2 --- /dev/null +++ b/ts/opsserver/handlers/security.handler.ts @@ -0,0 +1,208 @@ +import * as plugins from '../../plugins.js'; +import type { OpsServer } from '../classes.opsserver.js'; +import * as interfaces from '../../../ts_interfaces/index.js'; + +export class SecurityHandler { + public typedrouter = new plugins.typedrequest.TypedRouter(); + + constructor(private opsServerRef: OpsServer) { + // Add this handler's router to the parent + this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); + this.registerHandlers(); + } + + private registerHandlers(): void { + // Security Metrics Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getSecurityMetrics', + async (dataArg, toolsArg) => { + const metrics = await this.collectSecurityMetrics(); + return { + metrics: { + blockedIPs: metrics.blockedIPs, + reputationScores: metrics.reputationScores, + spamDetected: metrics.spamDetection.detected, + malwareDetected: metrics.malwareDetected, + phishingDetected: metrics.phishingDetected, + authenticationFailures: metrics.authFailures, + suspiciousActivities: metrics.suspiciousActivities, + }, + trends: dataArg.includeDetails ? { + spam: metrics.trends.spam, + malware: metrics.trends.malware, + phishing: metrics.trends.phishing, + } : undefined, + }; + } + ) + ); + + // Active Connections Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getActiveConnections', + async (dataArg, toolsArg) => { + const connections = await this.getActiveConnections(dataArg.protocol, dataArg.state); + const connectionInfos: interfaces.data.IConnectionInfo[] = connections.map(conn => ({ + id: conn.id, + remoteAddress: conn.source.ip, + localAddress: conn.destination.ip, + startTime: conn.startTime, + protocol: conn.type === 'http' ? 'https' : conn.type as any, + state: conn.status as any, + bytesReceived: Math.floor(conn.bytesTransferred / 2), + bytesSent: Math.floor(conn.bytesTransferred / 2), + })); + + const summary = { + total: connectionInfos.length, + byProtocol: connectionInfos.reduce((acc, conn) => { + acc[conn.protocol] = (acc[conn.protocol] || 0) + 1; + return acc; + }, {} as { [protocol: string]: number }), + byState: connectionInfos.reduce((acc, conn) => { + acc[conn.state] = (acc[conn.state] || 0) + 1; + return acc; + }, {} as { [state: string]: number }), + }; + + return { + connections: connectionInfos, + summary, + }; + } + ) + ); + + // Rate Limit Status Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getRateLimitStatus', + async (dataArg, toolsArg) => { + const status = await this.getRateLimitStatus(dataArg.domain, dataArg.ip); + const limits: interfaces.data.IRateLimitInfo[] = status.limits.map(limit => ({ + domain: limit.identifier, + currentRate: limit.current, + limit: limit.limit, + remaining: limit.limit - limit.current, + resetTime: limit.resetAt, + blocked: limit.status === 'limited', + })); + + return { + limits, + globalLimit: dataArg.includeBlocked ? { + current: limits.reduce((sum, l) => sum + l.currentRate, 0), + limit: 1000, // Global limit + remaining: 1000 - limits.reduce((sum, l) => sum + l.currentRate, 0), + } : undefined, + }; + } + ) + ); + } + + private async collectSecurityMetrics(): Promise<{ + blockedIPs: string[]; + reputationScores: { [domain: string]: number }; + spamDetection: { + detected: number; + falsePositives: number; + }; + malwareDetected: number; + phishingDetected: number; + authFailures: number; + suspiciousActivities: number; + trends: { + spam: Array<{ timestamp: number; value: number }>; + malware: Array<{ timestamp: number; value: number }>; + phishing: Array<{ timestamp: number; value: number }>; + }; + }> { + // TODO: Implement actual security metrics collection + return { + blockedIPs: [], + reputationScores: {}, + spamDetection: { + detected: 0, + falsePositives: 0, + }, + malwareDetected: 0, + phishingDetected: 0, + authFailures: 0, + suspiciousActivities: 0, + trends: { + spam: [], + malware: [], + phishing: [], + }, + }; + } + + private async getActiveConnections( + protocol?: 'http' | 'https' | 'smtp' | 'smtps', + state?: string + ): Promise> { + const connections: Array<{ + id: string; + type: 'http' | 'smtp' | 'dns'; + source: { + ip: string; + port: number; + country?: string; + }; + destination: { + ip: string; + port: number; + service?: string; + }; + startTime: number; + bytesTransferred: number; + status: 'active' | 'idle' | 'closing'; + }> = []; + + // TODO: Implement actual connection tracking + // This would collect from: + // - SmartProxy connections + // - Email server connections + // - DNS server connections + + return connections; + } + + private async getRateLimitStatus( + domain?: string, + ip?: string + ): Promise<{ + limits: Array<{ + identifier: string; + type: 'ip' | 'domain' | 'email'; + limit: number; + current: number; + resetAt: number; + status: 'ok' | 'warning' | 'limited'; + }>; + }> { + // TODO: Implement actual rate limit status collection + return { + limits: [], + }; + } +} \ No newline at end of file diff --git a/ts/opsserver/handlers/stats.handler.ts b/ts/opsserver/handlers/stats.handler.ts new file mode 100644 index 0000000..431e840 --- /dev/null +++ b/ts/opsserver/handlers/stats.handler.ts @@ -0,0 +1,344 @@ +import * as plugins from '../../plugins.js'; +import type { OpsServer } from '../classes.opsserver.js'; +import * as interfaces from '../../../ts_interfaces/index.js'; + +export class StatsHandler { + public typedrouter = new plugins.typedrequest.TypedRouter(); + + constructor(private opsServerRef: OpsServer) { + // Add this handler's router to the parent + this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); + this.registerHandlers(); + } + + private registerHandlers(): void { + // Server Statistics Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getServerStatistics', + async (dataArg, toolsArg) => { + const stats = await this.collectServerStats(); + return { + stats: { + uptime: stats.uptime, + startTime: Date.now() - (stats.uptime * 1000), + memoryUsage: stats.memoryUsage, + cpuUsage: stats.cpuUsage, + activeConnections: stats.activeConnections, + totalConnections: stats.totalConnections, + }, + history: dataArg.includeHistory ? stats.history : undefined, + }; + } + ) + ); + + // Email Statistics Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getEmailStatistics', + async (dataArg, toolsArg) => { + const emailServer = this.opsServerRef.dcRouterRef.emailServer; + if (!emailServer) { + return { + stats: { + sent: 0, + received: 0, + bounced: 0, + queued: 0, + failed: 0, + averageDeliveryTime: 0, + deliveryRate: 0, + bounceRate: 0, + }, + }; + } + + const stats = await this.collectEmailStats(); + return { + stats: { + sent: stats.sentToday, + received: stats.receivedToday, + bounced: Math.floor(stats.sentToday * stats.bounceRate / 100), + queued: stats.queueSize, + failed: 0, + averageDeliveryTime: 0, + deliveryRate: stats.deliveryRate, + bounceRate: stats.bounceRate, + }, + domainBreakdown: dataArg.includeDetails ? stats.domainBreakdown : undefined, + }; + } + ) + ); + + // DNS Statistics Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getDnsStatistics', + async (dataArg, toolsArg) => { + const dnsServer = this.opsServerRef.dcRouterRef.dnsServer; + if (!dnsServer) { + return { + stats: { + totalQueries: 0, + cacheHits: 0, + cacheMisses: 0, + cacheHitRate: 0, + activeDomains: 0, + averageResponseTime: 0, + queryTypes: {}, + }, + }; + } + + const stats = await this.collectDnsStats(); + return { + stats: { + totalQueries: stats.totalQueries, + cacheHits: stats.cacheHits, + cacheMisses: stats.cacheMisses, + cacheHitRate: stats.cacheHitRate, + activeDomains: stats.topDomains.length, + averageResponseTime: 0, + queryTypes: stats.queryTypes, + }, + domainBreakdown: dataArg.includeQueryTypes ? stats.domainBreakdown : undefined, + }; + } + ) + ); + + // Queue Status Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getQueueStatus', + async (dataArg, toolsArg) => { + const emailServer = this.opsServerRef.dcRouterRef.emailServer; + const queues: interfaces.data.IQueueStatus[] = []; + + if (emailServer) { + const status = await this.getQueueStatus(); + queues.push({ + name: dataArg.queueName || 'default', + size: status.pending, + processing: status.active, + failed: status.failed, + retrying: status.retrying, + averageProcessingTime: 0, + }); + } + + return { + queues, + totalItems: queues.reduce((sum, q) => sum + q.size + q.processing + q.failed + q.retrying, 0), + }; + } + ) + ); + + // Health Status Handler + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getHealthStatus', + async (dataArg, toolsArg) => { + const health = await this.checkHealthStatus(); + return { + health: { + healthy: health.healthy, + uptime: process.uptime(), + services: health.services.reduce((acc, service) => { + acc[service.name] = { + status: service.status, + message: service.message, + lastCheck: Date.now(), + }; + return acc; + }, {} as any), + version: '2.12.0', // TODO: Get from package.json + }, + }; + } + ) + ); + } + + private async collectServerStats(): Promise<{ + uptime: number; + cpuUsage: { + user: number; + system: number; + }; + memoryUsage: interfaces.data.IServerStats['memoryUsage']; + requestsPerSecond: number; + activeConnections: number; + totalConnections: number; + history: Array<{ + timestamp: number; + value: number; + }>; + }> { + const uptime = process.uptime(); + const memUsage = process.memoryUsage(); + const totalMem = plugins.os.totalmem(); + const freeMem = plugins.os.freemem(); + const usedMem = totalMem - freeMem; + + // Get CPU usage (simplified - in production would use proper monitoring) + const cpuUsage = plugins.os.loadavg()[0] * 100 / plugins.os.cpus().length; + + // TODO: Implement proper request tracking + const requestsPerSecond = 0; + const activeConnections = 0; + const totalConnections = 0; + + return { + uptime, + cpuUsage: { + user: cpuUsage * 0.7, // Approximate user CPU + system: cpuUsage * 0.3, // Approximate system CPU + }, + memoryUsage: { + heapUsed: memUsage.heapUsed, + heapTotal: memUsage.heapTotal, + external: memUsage.external, + rss: memUsage.rss, + }, + requestsPerSecond, + activeConnections, + totalConnections, + history: [], // TODO: Implement history tracking + }; + } + + private async collectEmailStats(): Promise<{ + sentToday: number; + receivedToday: number; + bounceRate: number; + deliveryRate: number; + queueSize: number; + domainBreakdown?: { [domain: string]: interfaces.data.IEmailStats }; + }> { + // TODO: Implement actual email statistics collection + return { + sentToday: 0, + receivedToday: 0, + bounceRate: 0, + deliveryRate: 100, + queueSize: 0, + }; + } + + private async collectDnsStats(): Promise<{ + queriesPerSecond: number; + totalQueries: number; + cacheHits: number; + cacheMisses: number; + cacheHitRate: number; + topDomains: Array<{ + domain: string; + count: number; + }>; + queryTypes: { [key: string]: number }; + domainBreakdown?: { [domain: string]: interfaces.data.IDnsStats }; + }> { + // TODO: Implement actual DNS statistics collection + return { + queriesPerSecond: 0, + totalQueries: 0, + cacheHits: 0, + cacheMisses: 0, + cacheHitRate: 0, + topDomains: [], + queryTypes: {}, + }; + } + + private async getQueueStatus(): Promise<{ + pending: number; + active: number; + failed: number; + retrying: number; + items: Array<{ + id: string; + recipient: string; + subject: string; + status: string; + attempts: number; + nextRetry?: number; + }>; + }> { + // TODO: Implement actual queue status collection + return { + pending: 0, + active: 0, + failed: 0, + retrying: 0, + items: [], + }; + } + + private async checkHealthStatus(): Promise<{ + healthy: boolean; + services: Array<{ + name: string; + status: 'healthy' | 'degraded' | 'unhealthy'; + message?: string; + }>; + checks: Array<{ + name: string; + passed: boolean; + message?: string; + }>; + }> { + const services: Array<{ + name: string; + status: 'healthy' | 'degraded' | 'unhealthy'; + message?: string; + }> = []; + + // Check HTTP Proxy + if (this.opsServerRef.dcRouterRef.smartProxy) { + services.push({ + name: 'HTTP/HTTPS Proxy', + status: 'healthy', + }); + } + + // Check Email Server + if (this.opsServerRef.dcRouterRef.emailServer) { + services.push({ + name: 'Email Server', + status: 'healthy', + }); + } + + // Check DNS Server + if (this.opsServerRef.dcRouterRef.dnsServer) { + services.push({ + name: 'DNS Server', + status: 'healthy', + }); + } + + // Check OpsServer + services.push({ + name: 'OpsServer', + status: 'healthy', + }); + + const healthy = services.every(s => s.status === 'healthy'); + + return { + healthy, + services, + checks: [ + { + name: 'Memory Usage', + passed: process.memoryUsage().heapUsed < (plugins.os.totalmem() * 0.9), + message: 'Memory usage within limits', + }, + ], + }; + } +} \ No newline at end of file