344 lines
9.9 KiB
TypeScript
344 lines
9.9 KiB
TypeScript
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<StatusResponse>;
|
|
requestInfrastructure: (monitor: IMonitor) => Promise<StatusResponse>;
|
|
requestStatus: (monitor: IMonitor) => Promise<StatusResponse>;
|
|
requestPagespeed: (monitor: IMonitor) => Promise<StatusResponse>;
|
|
requestPing: (monitor: IMonitor) => Promise<StatusResponse>;
|
|
requestPort: (monitor: IMonitor) => Promise<StatusResponse>;
|
|
}
|
|
|
|
export interface ICapturePayload {
|
|
data: ISystemInfo;
|
|
capture: ICaptureInfo;
|
|
}
|
|
|
|
export interface ILighthousePayload {
|
|
lighthouseResult: ILighthouseResult;
|
|
}
|
|
|
|
export interface StatusResponse<TPayload = unknown> {
|
|
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 = <T>(
|
|
monitor: IMonitor,
|
|
response: Response<T> | null,
|
|
certificateExpiry: Date | null,
|
|
error: any | null
|
|
): StatusResponse<T> => {
|
|
if (error) {
|
|
const statusResponse: StatusResponse<T> = {
|
|
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<T> = {
|
|
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<ICapturePayload>;
|
|
try {
|
|
const response: Response<ICapturePayload> | 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<ILighthousePayload>;
|
|
|
|
try {
|
|
const response: Response = await this.client(url);
|
|
statusResponse = this.buildStatusResponse(
|
|
monitor,
|
|
response,
|
|
null,
|
|
null
|
|
) as StatusResponse<ILighthousePayload>;
|
|
} 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<ILighthousePayload>(
|
|
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;
|