diff --git a/server/src/config/services.ts b/server/src/config/services.ts index d7648803f..664f9b2c5 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -145,7 +145,22 @@ export const initializeServices = async ({ const teamsRepository = new MongoTeamsRepository(); const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository(); - const networkService = new NetworkService(axios, got, https, jmespath, GameDig, ping, logger, Docker, net, settingsService, grpc, protoLoader); + const networkService = new NetworkService( + axios, + got, + https, + jmespath, + GameDig as unknown as { + query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }>; + }, + ping, + logger, + Docker, + net, + settingsService, + grpc, + protoLoader + ); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); const notificationMessageBuilder = new NotificationMessageBuilder(); diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index 67a88cb1a..e8a469828 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -7,6 +7,10 @@ import type { PageSpeedStatusPayload, HttpStatusPayload, HardwareStatusPayload, + PingStatusPayload, + DockerStatusPayload, + PortStatusPayload, + GameStatusPayload, } from "@/types/index.js"; import type { AxiosStatic } from "axios"; import { AppError } from "@/utils/AppError.js"; @@ -34,8 +38,25 @@ interface BuildStatusResponseArgs { export interface INetworkService { readonly serviceName: string; - requestStatus(monitor: Monitor): Promise; - requestWebhook(type: string, url: string, body: any): Promise<{ type: string; status: boolean; code: number; message: string; payload?: unknown }>; + requestStatus( + monitor: Monitor + ): Promise< + MonitorStatusResponse< + | PingStatusPayload + | HttpStatusPayload + | PageSpeedStatusPayload + | HardwareStatusPayload + | DockerStatusPayload + | PortStatusPayload + | GameStatusPayload + | GrpcStatusPayload + > + >; + requestWebhook( + type: string, + url: string, + body: unknown + ): Promise<{ type: string; status: boolean; code: number; message: string; payload?: unknown }>; requestPagerDuty(args: { message: string; routingKey: string; monitorUrl: string }): Promise; requestMatrix(args: { homeserverUrl: string; accessToken: string; roomId: string; message: string }): Promise<{ status: boolean; @@ -62,16 +83,16 @@ class NetworkService implements INetworkService { private axios: AxiosStatic; private got: Got; - private https: any; - private jmespath: any; - private GameDig: any; - private ping: any; + private https: typeof import("https"); + private jmespath: { search: (data: unknown, expression: string) => unknown }; + private GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> }; + private ping: typeof import("ping"); private logger: ILogger; - private Docker: any; - private net: any; + private Docker: typeof import("dockerode"); + private net: typeof import("net"); private settingsService: ISettingsService; - private grpc: any; - private protoLoader: any; + private grpc: typeof import("@grpc/grpc-js"); + private protoLoader: typeof import("@grpc/proto-loader"); private buildStatusResponse = ({ monitor, @@ -106,7 +127,7 @@ class NetworkService implements INetworkService { statusResponse.responseTime = error.timings?.phases?.total ?? 0; statusResponse.timings = error.timings; } - return { ...statusResponse, ...(overrides ?? {}) }; + return { ...statusResponse, ...(overrides ?? {}) } as MonitorStatusResponse; } return { @@ -124,22 +145,22 @@ class NetworkService implements INetworkService { expectedValue, extracted, ...(overrides ?? {}), - }; + } as MonitorStatusResponse; }; constructor( axios: AxiosStatic, got: Got, - https: any, - jmespath: any, - GameDig: any, - ping: any, + https: typeof import("https"), + jmespath: { search: (data: unknown, expression: string) => unknown }, + GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> }, + ping: typeof import("ping"), logger: ILogger, - Docker: any, - net: any, + Docker: typeof import("dockerode"), + net: typeof import("net"), settingsService: ISettingsService, - grpc: any, - protoLoader: any + grpc: typeof import("@grpc/grpc-js"), + protoLoader: typeof import("@grpc/proto-loader") ) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; @@ -193,7 +214,20 @@ class NetworkService implements INetworkService { } // Main entry point - async requestStatus(monitor: Monitor): Promise { + async requestStatus( + monitor: Monitor + ): Promise< + MonitorStatusResponse< + | PingStatusPayload + | HttpStatusPayload + | PageSpeedStatusPayload + | HardwareStatusPayload + | DockerStatusPayload + | PortStatusPayload + | GameStatusPayload + | GrpcStatusPayload + > + > { const type = monitor?.type || "unknown"; switch (type) { case this.TYPE_PING: @@ -217,7 +251,7 @@ class NetworkService implements INetworkService { } } - private async requestPing(monitor: Monitor): Promise { + private async requestPing(monitor: Monitor): Promise> { try { if (!monitor?.url) { throw new Error("Monitor URL is required"); @@ -237,30 +271,27 @@ class NetworkService implements INetworkService { throw new Error("Ping failed - no result returned"); } - const pingResponse = this.buildStatusResponse({ + const pingResponse = this.buildStatusResponse({ monitor, - payload: response, + payload: response as PingStatusPayload, overrides: { status: (response as { alive?: boolean })?.alive ?? false, code: 200, message: "Success", responseTime: (response as { time?: number })?.time ?? 0, - payload: response, - }, + payload: response as PingStatusPayload, + } as MonitorStatusResponseOverrides, }); - if (error) { - pingResponse.status = false; - pingResponse.code = this.PING_ERROR; - pingResponse.message = "Ping failed"; - return pingResponse; - } - return pingResponse; - } catch (err: any) { - err.service = this.SERVICE_NAME; - err.method = "requestPing"; - throw err; + } catch (err: unknown) { + const originalMessage = err instanceof Error ? err.message : String(err); + throw new AppError({ + message: originalMessage || "Error performing ping", + service: this.SERVICE_NAME, + method: "requestPing", + details: { url: monitor.url }, + }); } } @@ -279,7 +310,7 @@ class NetworkService implements INetworkService { if (!url) { throw new Error("Monitor URL is required"); } - const config: Record = { + const config: Record = { headers: secret ? { Authorization: `Bearer ${secret}` } : undefined, }; @@ -293,7 +324,7 @@ class NetworkService implements INetworkService { const response = await this.got(url, config); - let payload: any; + let payload: T | string | undefined; const contentType = response.headers["content-type"]; if (contentType && contentType.includes("application/json")) { @@ -381,19 +412,23 @@ class NetworkService implements INetworkService { } } return httpResponse; - } catch (err: any) { - if (err.name === "HTTPError" || err.name === "RequestError") { - httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR; + } catch (err: unknown) { + if (err instanceof HTTPError || err instanceof RequestError) { + httpResponse.code = err.response?.statusCode || this.NETWORK_ERROR; httpResponse.status = false; - httpResponse.message = err?.response?.statusCode || err.message; - httpResponse.responseTime = err?.timings?.phases?.total || 0; + httpResponse.message = String(err.response?.statusCode) || err.message; + httpResponse.responseTime = err.timings?.phases?.total || 0; httpResponse.payload = null; - httpResponse.timings = err?.timings || {}; + httpResponse.timings = err.timings || undefined; return httpResponse; } - err.service = this.SERVICE_NAME; - err.method = "requestHttp"; - throw err; + const originalMessage = err instanceof Error ? err.message : String(err); + throw new AppError({ + message: originalMessage || "Error performing HTTP request", + service: this.SERVICE_NAME, + method: "requestHttp", + details: { url: monitor.url }, + }); } } @@ -419,24 +454,32 @@ class NetworkService implements INetworkService { ...monitor, url: pageSpeedUrl, }); - } catch (err: any) { - err.service = this.SERVICE_NAME; - err.method = "requestPageSpeed"; - throw err; + } catch (err: unknown) { + const originalMessage = err instanceof Error ? err.message : String(err); + throw new AppError({ + message: originalMessage || "Error performing PageSpeed request", + service: this.SERVICE_NAME, + method: "requestPageSpeed", + details: { url: monitor.url }, + }); } } private async requestHardware(monitor: Monitor): Promise> { try { return await this.requestHttp(monitor); - } catch (err: any) { - err.service = this.SERVICE_NAME; - err.method = "requestHardware"; - throw err; + } catch (err: unknown) { + const originalMessage = err instanceof Error ? err.message : String(err); + throw new AppError({ + message: originalMessage || "Error performing hardware request", + service: this.SERVICE_NAME, + method: "requestHardware", + details: { url: monitor.url }, + }); } } - private async requestDocker(monitor: Monitor): Promise { + private async requestDocker(monitor: Monitor): Promise> { try { if (!monitor.url) { throw new Error("Monitor URL is required"); @@ -446,7 +489,7 @@ class NetworkService implements INetworkService { socketPath: "/var/run/docker.sock", }); - const dockerResponse = this.buildStatusResponse({ + const dockerResponse = this.buildStatusResponse({ monitor, overrides: { status: false, @@ -455,17 +498,17 @@ class NetworkService implements INetworkService { }, }); - const containers = await docker.listContainers({ all: true }); + const containers = await docker.listContainers(); // Normalize input: strip leading slashes and convert to lowercase for comparison const normalizedInput = monitor.url.replace(/^\/+/, "").toLowerCase(); // Priority-based matching to avoid ambiguity: // 1. Exact full ID match (64-char) - const exactIdMatch = containers.find((c: any) => c.Id.toLowerCase() === normalizedInput); + const exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput); // 2. Exact container name match (case-insensitive) - const exactNameMatch = containers.find((c: any) => + const exactNameMatch = containers.find((c) => c.Names.some((name: string) => { const cleanName = name.replace(/^\/+/, "").toLowerCase(); return cleanName === normalizedInput; @@ -473,7 +516,7 @@ class NetworkService implements INetworkService { ); // 3. Partial ID match (fallback for backwards compatibility) - const partialIdMatch = containers.find((c: any) => c.Id.toLowerCase().startsWith(normalizedInput)); + const partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput)); // Select container based on priority const targetContainer = exactIdMatch || exactNameMatch || partialIdMatch; @@ -512,9 +555,8 @@ class NetworkService implements INetworkService { } const container = docker.getContainer(targetContainer.Id); - const { response, responseTime, error }: { response?: any; responseTime?: number; error?: any } = await this.timeRequest(() => - container.inspect() - ); + const { response, responseTime, error }: { response?: { State: { Status: string } } | null; responseTime: number; error: unknown } = + await this.timeRequest(() => container.inspect()); dockerResponse.responseTime = responseTime; dockerResponse.status = response?.State?.Status === "running" ? true : false; @@ -523,20 +565,28 @@ class NetworkService implements INetworkService { if (error) { dockerResponse.status = false; - dockerResponse.code = error.statusCode || this.NETWORK_ERROR; - dockerResponse.message = error.reason || "Failed to fetch Docker container information"; + dockerResponse.code = + (error && typeof error === "object" && "statusCode" in error ? (error as { statusCode?: number }).statusCode : undefined) || + this.NETWORK_ERROR; + dockerResponse.message = + (error && typeof error === "object" && "reason" in error ? (error as { reason?: string }).reason : undefined) || + "Failed to fetch Docker container information"; return dockerResponse; } return dockerResponse; - } catch (err: any) { - err.service = this.SERVICE_NAME; - err.method = "requestDocker"; - throw err; + } catch (err: unknown) { + const originalMessage = err instanceof Error ? err.message : String(err); + throw new AppError({ + message: originalMessage || "Error performing Docker request", + service: this.SERVICE_NAME, + method: "requestDocker", + details: { url: monitor.url }, + }); } } - private async requestPort(monitor: Monitor): Promise { + private async requestPort(monitor: Monitor): Promise> { try { const { url, port } = monitor; if (!port) { @@ -563,14 +613,14 @@ class NetworkService implements INetworkService { reject(new Error("Connection timeout")); }); - socket.on("error", (err: any) => { + socket.on("error", (err: unknown) => { socket.destroy(); reject(err); }); }); }); - const portResponse = this.buildStatusResponse({ + const portResponse = this.buildStatusResponse({ monitor, overrides: { code: 200, @@ -588,18 +638,22 @@ class NetworkService implements INetworkService { } return portResponse; - } catch (error: any) { - error.service = this.SERVICE_NAME; - error.method = "requestTCP"; - throw error; + } catch (err: unknown) { + const originalMessage = err instanceof Error ? err.message : String(err); + throw new AppError({ + message: originalMessage || "Error performing port check", + service: this.SERVICE_NAME, + method: "requestPort", + details: { url: monitor.url, port: monitor.port }, + }); } } - private async requestGame(monitor: Monitor): Promise { + private async requestGame(monitor: Monitor): Promise> { try { const { url, port, gameId } = monitor; - const gameResponse = this.buildStatusResponse({ + const gameResponse = this.buildStatusResponse({ monitor, overrides: { code: 200, @@ -612,13 +666,14 @@ class NetworkService implements INetworkService { type: gameId ?? "unknown", host: url, port: port ?? 0, - }).catch((error: any) => { + }).catch((error: unknown): undefined => { this.logger.warn({ - message: error.message, + message: error instanceof Error ? error.message : String(error), service: this.SERVICE_NAME, method: "requestGame", details: { url, port, gameId }, }); + return undefined; }); if (!state) { @@ -628,16 +683,20 @@ class NetworkService implements INetworkService { return gameResponse; } - gameResponse.responseTime = state.ping; - gameResponse.payload = state; + gameResponse.responseTime = state.ping ?? 0; + gameResponse.payload = state as GameStatusPayload; return gameResponse; - } catch (error: any) { - error.service = this.SERVICE_NAME; - error.method = "requestPing"; - throw error; + } catch (err: unknown) { + const originalMessage = err instanceof Error ? err.message : String(err); + throw new AppError({ + message: originalMessage || "Error performing game server check", + service: this.SERVICE_NAME, + method: "requestGame", + details: { url: monitor.url, port: monitor.port, gameId: monitor.gameId }, + }); } } - private async requestGrpc(monitor: Monitor): Promise { + private async requestGrpc(monitor: Monitor): Promise> { try { const { url, port, ignoreTlsErrors } = monitor; const grpcServiceName = monitor.grpcServiceName || ""; @@ -661,7 +720,21 @@ class NetworkService implements INetworkService { defaults: true, oneofs: true, }); - const grpcObject = this.grpc.loadPackageDefinition(packageDefinition); + const grpcObject = this.grpc.loadPackageDefinition(packageDefinition) as unknown as { + grpc: { + health: { + v1: { + Health: new ( + target: string, + credentials: unknown + ) => { + Check: (request: { service: string }, options: { deadline: Date }, callback: (err: unknown, response: unknown) => void) => void; + close: () => void; + }; + }; + }; + }; + }; const healthService = grpcObject.grpc.health.v1.Health; let credentials; @@ -678,7 +751,7 @@ class NetworkService implements INetworkService { const TIMEOUT_MS = 10000; const deadline = new Date(Date.now() + TIMEOUT_MS); - const grpcResponse = this.buildStatusResponse({ + const grpcResponse = this.buildStatusResponse({ monitor, overrides: { status: false, @@ -689,24 +762,30 @@ class NetworkService implements INetworkService { const { response, responseTime, error } = await this.timeRequest(() => { return new Promise((resolve, reject) => { - client.Check({ service: grpcServiceName }, { deadline }, (err: any, response: any) => { + client.Check({ service: grpcServiceName }, { deadline }, (err: unknown, response: unknown) => { client.close(); if (err) { + const grpcErr = err as { code?: number; details?: string; message?: string }; const payload: GrpcStatusPayload = { - grpcStatusCode: err.code ?? -1, - grpcStatusName: this.getGrpcStatusName(err.code), + grpcStatusCode: grpcErr.code ?? -1, + grpcStatusName: this.getGrpcStatusName(grpcErr.code ?? -1), serviceName: grpcServiceName, servingStatus: "UNKNOWN", }; - const grpcError = new AppError({ message: err.details || err.message, service: this.SERVICE_NAME, method: "requestGrpc" }); - (grpcError as any).grpcPayload = payload; - (grpcError as any).grpcCode = err.code; + const grpcError = new AppError({ + message: grpcErr.details || grpcErr.message || "gRPC error", + service: this.SERVICE_NAME, + method: "requestGrpc", + }) as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number }; + grpcError.grpcPayload = payload; + grpcError.grpcCode = grpcErr.code; reject(grpcError); return; } - const servingStatus = response?.status || "UNKNOWN"; + const resp = response as { status?: string } | undefined; + const servingStatus = resp?.status ?? "UNKNOWN"; resolve({ grpcStatusCode: 0, grpcStatusName: "OK", @@ -718,13 +797,13 @@ class NetworkService implements INetworkService { }); if (error) { - const grpcError = error as any; - const payload = grpcError.grpcPayload as GrpcStatusPayload | undefined; + const grpcError = error as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number }; + const payload = grpcError.grpcPayload; grpcResponse.status = false; grpcResponse.code = grpcError.grpcCode ?? this.NETWORK_ERROR; - grpcResponse.message = grpcError.message || "gRPC health check failed"; + grpcResponse.message = grpcError.message ?? "gRPC health check failed"; grpcResponse.responseTime = responseTime; - grpcResponse.payload = payload || null; + grpcResponse.payload = payload ?? null; return grpcResponse; } @@ -740,10 +819,14 @@ class NetworkService implements INetworkService { grpcResponse.payload = grpcPayload; return grpcResponse; - } catch (err: any) { - err.service = this.SERVICE_NAME; - err.method = "requestGrpc"; - throw err; + } catch (err: unknown) { + const originalMessage = err instanceof Error ? err.message : String(err); + throw new AppError({ + message: originalMessage || "Error performing gRPC health check", + service: this.SERVICE_NAME, + method: "requestGrpc", + details: { url: monitor.url, port: monitor.port, grpcServiceName: monitor.grpcServiceName }, + }); } } @@ -782,7 +865,7 @@ class NetworkService implements INetworkService { } // Other network requests unrelated to monitoring: - async requestWebhook(type: string, url: string, body: any) { + async requestWebhook(type: string, url: string, body: unknown) { try { const response = await this.axios.post(url, body, { headers: { @@ -797,19 +880,29 @@ class NetworkService implements INetworkService { message: `Successfully sent ${type} notification`, payload: response.data, }; - } catch (error: any) { + } catch (err: unknown) { this.logger.warn({ - message: error.message, + message: err instanceof Error ? err.message : String(err), service: this.SERVICE_NAME, method: "requestWebhook", }); + if (err && typeof err === "object" && "response" in err) { + const axiosError = err as { response?: { status?: number; data?: unknown } }; + return { + type: "webhook", + status: false, + code: axiosError.response?.status ?? this.NETWORK_ERROR, + message: `Failed to send ${type} notification`, + payload: axiosError.response?.data, + }; + } + return { type: "webhook", status: false, - code: error.response?.status || this.NETWORK_ERROR, + code: this.NETWORK_ERROR, message: `Failed to send ${type} notification`, - payload: error.response?.data, }; } } @@ -829,11 +922,17 @@ class NetworkService implements INetworkService { if (response?.data?.status !== "success") return false; return true; - } catch (error: any) { - error.details = error.response?.data; - error.service = this.SERVICE_NAME; - error.method = "requestPagerDuty"; - throw error; + } catch (err: unknown) { + const originalMessage = err instanceof Error ? err.message : String(err); + + throw new AppError({ + message: originalMessage || "Error sending PagerDuty notification", + service: this.SERVICE_NAME, + method: "requestPagerDuty", + details: { + responseData: err && typeof err === "object" && "response" in err ? (err as { response?: { data?: unknown } }).response?.data : undefined, + }, + }); } } @@ -867,18 +966,35 @@ class NetworkService implements INetworkService { code: response.status, message: "Successfully sent Matrix notification", }; - } catch (error: any) { + } catch (err: unknown) { + if (err instanceof Error) { + this.logger.warn({ + message: err.message, + service: this.SERVICE_NAME, + method: "requestMatrix", + }); + + if (err && typeof err === "object" && "response" in err) { + const axiosError = err as { response?: { status?: number; data?: unknown } }; + return { + status: false, + code: axiosError.response?.status || this.NETWORK_ERROR, + message: "Failed to send Matrix notification", + payload: axiosError.response?.data, + }; + } + } + this.logger.warn({ - message: error.message, + message: String(err), service: this.SERVICE_NAME, method: "requestMatrix", }); return { status: false, - code: error.response?.status || this.NETWORK_ERROR, + code: this.NETWORK_ERROR, message: "Failed to send Matrix notification", - payload: error.response?.data, }; } } diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index 37657cb79..12cd3a77f 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -7,6 +7,12 @@ import type { Check, HardwareStatusPayload, PageSpeedStatusPayload, + PingStatusPayload, + HttpStatusPayload, + DockerStatusPayload, + PortStatusPayload, + GameStatusPayload, + GrpcStatusPayload, CheckSnapshot, MonitorStats, } from "@/types/index.js"; @@ -17,7 +23,17 @@ export interface IStatusService { updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }): Promise; handleIncidentForCheck(check: any, monitor: Monitor, action: any, errorContext?: string): Promise; updateMonitorStatus( - statusResponse: MonitorStatusResponse, + statusResponse: MonitorStatusResponse< + | PingStatusPayload + | HttpStatusPayload + | PageSpeedStatusPayload + | HardwareStatusPayload + | DockerStatusPayload + | PortStatusPayload + | GameStatusPayload + | GrpcStatusPayload + | undefined + >, check: Check ): Promise; } @@ -205,7 +221,17 @@ export class StatusService implements IStatusService { }; updateMonitorStatus = async ( - statusResponse: MonitorStatusResponse, + statusResponse: MonitorStatusResponse< + | PingStatusPayload + | HttpStatusPayload + | PageSpeedStatusPayload + | HardwareStatusPayload + | DockerStatusPayload + | PortStatusPayload + | GameStatusPayload + | GrpcStatusPayload + | undefined + >, check: Check ): Promise => { try {