import { Got, HTTPError } from "got"; import ping from "ping"; import net from "net"; import { IMonitor } from "@/db/models/index.js"; import { GotTimings } from "@/db/models/checks/Check.js"; import type { Response } from "got"; import type { ISystemInfo, ICaptureInfo, ILighthouseResult, } from "@/db/models/index.js"; import type { TLSSocket, DetailedPeerCertificate } from "node:tls"; import type { ClientRequest } from "node:http"; import { MonitorType, MonitorStatus } from "@/db/models/monitors/Monitor.js"; import ApiError from "@/utils/ApiError.js"; import { config } from "@/config/index.js"; import CacheableLookup from "cacheable-lookup"; import { getChildLogger } from "@/logger/Logger.js"; const SERVICE_NAME = "NetworkService"; const logger = getChildLogger(SERVICE_NAME); export interface INetworkService { requestHttp: (monitor: IMonitor) => Promise; requestInfrastructure: (monitor: IMonitor) => Promise; requestStatus: (monitor: IMonitor) => Promise; requestPagespeed: (monitor: IMonitor) => Promise; requestPing: (monitor: IMonitor) => Promise; requestPort: (monitor: IMonitor) => Promise; } export interface ICapturePayload { data: ISystemInfo; capture: ICaptureInfo; } export interface ILighthousePayload { lighthouseResult: ILighthouseResult; } export interface StatusResponse { monitorId: string; teamId: string; type: MonitorType; code?: number; status: MonitorStatus; message: string; responseTime: number; timings?: GotTimings; payload?: TPayload; certificateExpiry?: Date | null; } class NetworkService implements INetworkService { public SERVICE_NAME = SERVICE_NAME; private client: Got; private NETWORK_ERROR: number; constructor(got: Got) { this.SERVICE_NAME = SERVICE_NAME; this.NETWORK_ERROR = 5000; const cacheable = new CacheableLookup(); this.client = got.extend({ dnsCache: cacheable, timeout: { request: 30000, }, retry: { limit: 1 }, }); } private buildStatusResponse = ( monitor: IMonitor, response: Response | null, certificateExpiry: Date | null, error: any | null ): StatusResponse => { if (error) { const statusResponse: StatusResponse = { monitorId: monitor._id.toString(), teamId: monitor.teamId.toString(), type: monitor.type, status: "down" as MonitorStatus, code: this.NETWORK_ERROR, message: error.message || "Network error", responseTime: 0, timings: { phases: {} } as GotTimings, certificateExpiry, }; if (error instanceof HTTPError) { statusResponse.code = error?.response?.statusCode || this.NETWORK_ERROR; statusResponse.message = error.message || "HTTP error"; statusResponse.responseTime = error.timings?.phases?.total || 0; statusResponse.timings = error.timings; } return statusResponse; } const statusResponse: StatusResponse = { monitorId: monitor._id.toString(), teamId: monitor.teamId.toString(), type: monitor.type, code: response?.statusCode || this.NETWORK_ERROR, status: response?.ok === true ? "up" : "down", message: response?.statusMessage || "", responseTime: response?.timings?.phases?.total || 0, timings: response?.timings || ({ phases: {} } as GotTimings), certificateExpiry, }; return statusResponse; }; requestHttp = async (monitor: IMonitor) => { try { const url = monitor.url; if (!url) { throw new Error("No URL provided"); } let certificateExpiry: Date | null = null; try { const req: any = this.client(url); req.on("request", (nodeReq: ClientRequest) => { nodeReq.on("socket", (socket: TLSSocket) => { const capture = () => { const cert = socket.getPeerCertificate(true); if (cert && cert.valid_to) { certificateExpiry = new Date(cert.valid_to); } }; capture(); socket.once("secureConnect", capture); }); }); const response: Response = await req; return this.buildStatusResponse( monitor, response, certificateExpiry, null ); } catch (error) { return this.buildStatusResponse( monitor, null, certificateExpiry, error ); } } catch (error) { throw error; } }; requestInfrastructure = async (monitor: IMonitor) => { const url = monitor.url; if (!url) { throw new Error("No URL provided"); } const secret = monitor.secret; if (!secret) { throw new Error("No secret provided for infrastructure monitor"); } let statusResponse: StatusResponse; try { const response: Response | null = await this.client( url, { headers: { Authorization: `Bearer ${secret}` }, responseType: "json", } ); statusResponse = this.buildStatusResponse(monitor, response, null, null); if (!response?.body) { throw new ApiError( "No payload received from infrastructure monitor", 500 ); } statusResponse.payload = response?.body; return statusResponse; } catch (error) { statusResponse = this.buildStatusResponse(monitor, null, null, error); } return statusResponse; }; requestPagespeed = async (monitor: IMonitor) => { const apiKey = config.PAGESPEED_API_KEY; if (!apiKey) { throw new Error("No API key provided for pagespeed monitor"); } const url = monitor.url; if (!url) { throw new Error("No URL provided"); } let statusResponse: StatusResponse; try { const response: Response = await this.client(url); statusResponse = this.buildStatusResponse( monitor, response, null, null ) as StatusResponse; } catch (error) { statusResponse = this.buildStatusResponse(monitor, null, null, error); } const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance&key=${apiKey}`; const pagespeedResponse = await this.client( pagespeedUrl, { responseType: "json", } ); const payload = pagespeedResponse.body; if (payload) { statusResponse.payload = payload; return statusResponse; } else { throw new ApiError("No payload received from pagespeed monitor", 500); } }; requestPing = async (monitor: IMonitor) => { const response = await ping.promise.probe(monitor.url); const status = response?.alive === true ? "up" : "down"; const rawTime = typeof response?.time === "string" ? parseFloat(response.time) : Number(response?.time); const responseTime = Number.isFinite(rawTime) ? rawTime : 0; return { monitorId: monitor._id.toString(), teamId: monitor.teamId.toString(), type: monitor.type, status: status as MonitorStatus, message: "Ping successful", responseTime, timings: { phases: {} } as GotTimings, }; }; requestPort = async (monitor: IMonitor) => { const response = { monitorId: monitor._id.toString(), teamId: monitor.teamId.toString(), type: monitor.type, status: "down" as MonitorStatus, message: "Port check failed", responseTime: 0, timings: { phases: {} } as GotTimings, }; try { const host = monitor.url; const port = monitor.port; if (!port) { throw new Error("No port provided for port monitor"); } const { status, responseTime, message } = await new Promise<{ status: MonitorStatus; responseTime: number; message: string; }>((resolve) => { const start = performance.now(); const socket = net.createConnection({ host, port, }); socket.setTimeout(5000); socket.on("connect", () => { socket.destroy(); const duration = Math.round(performance.now() - start); resolve({ status: "up", responseTime: duration, message: "Port is open", }); }); socket.on("error", (err) => { socket.destroy(); const duration = Math.round(performance.now() - start); resolve({ status: "down", responseTime: duration, message: err.message, }); }); socket.on("timeout", () => { socket.destroy(); const duration = Math.round(performance.now() - start); resolve({ status: "down", responseTime: duration, message: "Port check timed out", }); }); }); response.status = status; response.responseTime = responseTime; response.message = message; } catch (error) { console.error(error); } finally { return response; } }; requestStatus = async (monitor: IMonitor) => { switch (monitor?.type) { case "http": return await this.requestHttp(monitor); // uses GOT case "https": return await this.requestHttp(monitor); // uses GOT case "infrastructure": return await this.requestInfrastructure(monitor); // uses GOT case "pagespeed": return await this.requestPagespeed(monitor); // uses GOT case "ping": return await this.requestPing(monitor); // uses PING case "port": return await this.requestPort(monitor); default: throw new Error("Not implemented"); } }; } export default NetworkService;