Files
Checkmate/server/src/service/infrastructure/networkService.js
T

580 lines
16 KiB
JavaScript
Raw Normal View History

2025-10-28 22:41:05 -07:00
import CacheableLookup from "cacheable-lookup";
2025-04-20 11:29:53 -07:00
const SERVICE_NAME = "NetworkService";
2025-08-15 16:04:43 -07:00
2025-04-20 11:29:53 -07:00
class NetworkService {
static SERVICE_NAME = SERVICE_NAME;
2025-08-15 16:04:43 -07:00
constructor({ axios, got, https, jmespath, GameDig, ping, logger, http, Docker, net, stringService, settingsService }) {
2025-04-20 11:29:53 -07:00
this.TYPE_PING = "ping";
this.TYPE_HTTP = "http";
this.TYPE_PAGESPEED = "pagespeed";
this.TYPE_HARDWARE = "hardware";
this.TYPE_DOCKER = "docker";
this.TYPE_PORT = "port";
2025-08-02 02:04:06 +03:00
this.TYPE_GAME = "game";
2025-04-20 11:29:53 -07:00
this.SERVICE_NAME = SERVICE_NAME;
this.NETWORK_ERROR = 5000;
this.PING_ERROR = 5001;
this.axios = axios;
2025-08-15 16:04:43 -07:00
this.https = https;
this.jmespath = jmespath;
this.GameDig = GameDig;
2025-04-20 11:29:53 -07:00
this.ping = ping;
this.logger = logger;
this.http = http;
this.Docker = Docker;
this.net = net;
this.stringService = stringService;
this.settingsService = settingsService;
2025-10-28 22:41:05 -07:00
const cacheable = new CacheableLookup();
this.got = got.extend({
dnsCache: cacheable,
timeout: {
request: 30000,
},
retry: { limit: 1 },
});
2025-04-20 11:29:53 -07:00
}
2025-08-15 16:04:43 -07:00
// Helper functions
2025-04-20 11:29:53 -07:00
async timeRequest(operation) {
2025-08-15 16:04:43 -07:00
const start = process.hrtime.bigint();
2025-04-20 11:29:53 -07:00
try {
const response = await operation();
2025-10-09 09:33:14 -07:00
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
return { response, responseTime: elapsedMs };
2025-04-20 11:29:53 -07:00
} catch (error) {
2025-10-09 09:33:14 -07:00
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
return { response: null, responseTime: elapsedMs, error };
2025-08-15 16:04:43 -07:00
}
}
// Main entry point
async requestStatus(monitor) {
const type = monitor?.type || "unknown";
switch (type) {
case this.TYPE_PING:
return await this.requestPing(monitor);
case this.TYPE_HTTP:
return await this.requestHttp(monitor);
case this.TYPE_PAGESPEED:
return await this.requestPageSpeed(monitor);
case this.TYPE_HARDWARE:
return await this.requestHardware(monitor);
case this.TYPE_DOCKER:
return await this.requestDocker(monitor);
case this.TYPE_PORT:
return await this.requestPort(monitor);
case this.TYPE_GAME:
return await this.requestGame(monitor);
default:
return await this.handleUnsupportedType(type);
2025-04-20 11:29:53 -07:00
}
}
2025-06-18 10:05:04 +08:00
async requestPing(monitor) {
2025-04-20 11:29:53 -07:00
try {
2025-08-15 16:04:43 -07:00
if (!monitor?.url) {
throw new Error("Monitor URL is required");
}
const rawUrl = monitor.url;
const sanitizedHost = rawUrl
.replace(/^https?:\/\//, "")
.replace(/\/.*$/, "")
.replace(/:.*/, "");
const { response, error } = await this.timeRequest(() => this.ping.promise.probe(sanitizedHost));
2025-04-20 11:29:53 -07:00
2025-08-15 16:04:43 -07:00
if (!response) {
2025-09-08 06:42:36 -07:00
if (error) {
throw error;
}
2025-08-15 16:04:43 -07:00
throw new Error("Ping failed - no result returned");
}
2025-04-20 11:29:53 -07:00
const pingResponse = {
2026-01-13 20:20:00 +00:00
monitorId: monitor.id,
2025-04-20 11:29:53 -07:00
type: "ping",
2025-08-15 16:04:43 -07:00
status: response.alive,
code: 200,
responseTime: response.time,
2025-08-15 16:04:43 -07:00
message: "Success",
2025-04-20 11:29:53 -07:00
payload: response,
};
2025-08-15 16:04:43 -07:00
2025-04-20 11:29:53 -07:00
if (error) {
pingResponse.status = false;
2025-08-15 16:04:43 -07:00
pingResponse.code = 200;
pingResponse.message = "Ping failed";
2025-04-20 11:29:53 -07:00
return pingResponse;
}
return pingResponse;
2025-08-15 16:04:43 -07:00
} catch (err) {
err.service = this.SERVICE_NAME;
err.method = "requestPing";
throw err;
2025-04-20 11:29:53 -07:00
}
}
2025-06-06 14:05:59 +08:00
async requestHttp(monitor) {
2026-01-13 20:20:00 +00:00
const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
2025-08-15 16:04:43 -07:00
const httpResponse = {
2026-01-13 20:20:00 +00:00
monitorId: id,
2025-08-15 16:04:43 -07:00
teamId: teamId,
type,
};
2025-04-20 11:29:53 -07:00
2025-08-15 16:04:43 -07:00
try {
if (!url) {
throw new Error("Monitor URL is required");
}
const config = {
headers: secret ? { Authorization: `Bearer ${secret}` } : undefined,
};
2025-04-20 11:29:53 -07:00
2025-08-15 16:04:43 -07:00
if (ignoreTlsErrors) {
config.agent = {
https: new this.https.Agent({
rejectUnauthorized: false,
}),
};
2025-05-06 10:08:19 -07:00
}
2025-08-15 16:04:43 -07:00
const response = await this.got(url, config);
2025-04-20 11:29:53 -07:00
2025-08-15 16:04:43 -07:00
let payload;
const contentType = response.headers["content-type"];
2025-04-20 11:29:53 -07:00
2025-08-15 16:04:43 -07:00
if (contentType && contentType.includes("application/json")) {
try {
payload = JSON.parse(response.body);
} catch {
payload = response.body;
}
} else {
payload = response.body;
2025-04-20 11:29:53 -07:00
}
2025-08-15 16:04:43 -07:00
httpResponse.code = response.statusCode;
httpResponse.status = response.ok;
httpResponse.message = response.statusMessage;
httpResponse.responseTime = response.timings.phases.total || 0;
httpResponse.payload = payload;
httpResponse.timings = response.timings || {};
2025-04-20 11:29:53 -07:00
2025-08-26 14:54:24 -07:00
if (!expectedValue && !jsonPath) {
2025-04-20 11:29:53 -07:00
return httpResponse;
}
if (expectedValue && !jsonPath) {
2025-08-26 14:54:24 -07:00
let ok = false;
if (matchMethod === "equal") ok = payload === expectedValue;
if (matchMethod === "include" && typeof payload === "string") ok = payload.includes(expectedValue);
if (matchMethod === "regex" && typeof payload === "string") ok = new RegExp(expectedValue).test(payload);
2025-08-26 14:54:24 -07:00
if (ok === true) {
return httpResponse;
} else {
httpResponse.code = 500;
httpResponse.status = false;
2025-09-17 23:33:45 -03:00
httpResponse.message = this.stringService.httpExpectedValueFail;
2025-08-26 14:54:24 -07:00
return httpResponse;
}
}
2025-04-20 11:29:53 -07:00
if (jsonPath) {
const contentType = response.headers["content-type"];
const isJson = contentType?.includes("application/json");
if (!isJson) {
httpResponse.status = false;
httpResponse.message = this.stringService.httpNotJson;
return httpResponse;
}
try {
const extracted = this.jmespath.search(payload, jsonPath);
if (expectedValue) {
let ok = false;
if (matchMethod === "equal") ok = extracted === expectedValue;
if (matchMethod === "include" && typeof extracted === "string") ok = extracted.includes(expectedValue);
if (matchMethod === "regex" && typeof extracted === "string") ok = new RegExp(expectedValue).test(extracted);
if (ok) {
httpResponse.extracted = extracted;
return httpResponse;
} else {
httpResponse.status = false;
httpResponse.code = 500;
2025-09-17 23:33:45 -03:00
httpResponse.message = this.stringService.httpJsonPathFail;
httpResponse.extracted = extracted;
return httpResponse;
}
} else {
const isFalsey = extracted === false || extracted === "false" || extracted === undefined || extracted === null;
if (!isFalsey) {
httpResponse.extracted = extracted;
return httpResponse;
} else {
httpResponse.status = false;
httpResponse.code = 500;
2025-09-17 23:33:45 -03:00
httpResponse.message = this.stringService.httpJsonPathFail;
httpResponse.extracted = extracted;
return httpResponse;
}
}
2025-08-15 16:04:43 -07:00
} catch {
2025-04-20 11:29:53 -07:00
httpResponse.status = false;
httpResponse.message = this.stringService.httpJsonPathError;
return httpResponse;
}
}
2025-08-15 16:04:43 -07:00
return httpResponse;
} catch (err) {
if (err.name === "HTTPError" || err.name === "RequestError") {
httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR;
2025-04-20 11:29:53 -07:00
httpResponse.status = false;
2025-08-15 16:04:43 -07:00
httpResponse.message = err?.response?.statusCode || err.message;
httpResponse.responseTime = err?.timings?.phases?.total || 0;
httpResponse.payload = null;
httpResponse.timings = err?.timings || {};
2025-04-20 11:29:53 -07:00
return httpResponse;
}
2025-08-15 16:04:43 -07:00
err.service = this.SERVICE_NAME;
err.method = "requestHttp";
throw err;
2025-04-20 11:29:53 -07:00
}
}
2025-08-15 16:04:43 -07:00
async requestPageSpeed(monitor) {
2025-04-20 11:29:53 -07:00
try {
2025-06-06 14:05:59 +08:00
const url = monitor.url;
2025-08-15 16:04:43 -07:00
if (!url) {
throw new Error("Monitor URL is required");
}
let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
2025-05-12 10:56:58 -07:00
const dbSettings = await this.settingsService.getDBSettings();
if (dbSettings?.pagespeedApiKey) {
2025-08-15 16:04:43 -07:00
pageSpeedUrl += `&key=${dbSettings.pagespeedApiKey}`;
2025-05-12 10:56:58 -07:00
} else {
2025-05-19 11:23:13 -07:00
this.logger.warn({
2025-08-15 16:04:43 -07:00
message: "PageSpeed API key not found, job not executed",
2025-05-12 10:56:58 -07:00
service: this.SERVICE_NAME,
method: "requestPagespeed",
details: { url },
});
2025-04-20 11:29:53 -07:00
}
2025-08-15 16:04:43 -07:00
return await this.requestHttp({
...monitor,
url: pageSpeedUrl,
});
} catch (err) {
err.service = this.SERVICE_NAME;
err.method = "requestPageSpeed";
throw err;
2025-04-20 11:29:53 -07:00
}
}
2025-06-06 14:05:59 +08:00
async requestHardware(monitor) {
2025-04-20 11:29:53 -07:00
try {
2025-06-06 14:05:59 +08:00
return await this.requestHttp(monitor);
2025-08-15 16:04:43 -07:00
} catch (err) {
err.service = this.SERVICE_NAME;
err.method = "requestHardware";
throw err;
2025-04-20 11:29:53 -07:00
}
}
2025-06-06 14:05:59 +08:00
async requestDocker(monitor) {
2025-04-20 11:29:53 -07:00
try {
2025-08-15 16:04:43 -07:00
if (!monitor.url) {
throw new Error("Monitor URL is required");
}
2025-04-20 11:29:53 -07:00
const docker = new this.Docker({
socketPath: "/var/run/docker.sock",
handleError: true, // Enable error handling
});
2025-08-29 09:22:02 -07:00
const dockerResponse = {
2026-01-13 20:20:00 +00:00
monitorId: monitor.id,
2025-08-29 09:22:02 -07:00
type: monitor.type,
};
2025-04-20 11:29:53 -07:00
const containers = await docker.listContainers({ all: true });
// 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)
let exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput);
// 2. Exact container name match (case-insensitive)
let exactNameMatch = containers.find((c) =>
c.Names.some((name) => {
const cleanName = name.replace(/^\/+/, "").toLowerCase();
return cleanName === normalizedInput;
})
);
// 3. Partial ID match (fallback for backwards compatibility)
let partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput));
// Select container based on priority
let targetContainer = exactIdMatch || exactNameMatch || partialIdMatch;
2025-08-29 09:22:02 -07:00
// Return negative response if no container
if (!targetContainer) {
this.logger.warn({
message: `No container found for "${monitor.url}".`,
service: this.SERVICE_NAME,
method: "requestDocker",
details: { url: monitor.url },
});
dockerResponse.code = 404;
dockerResponse.status = false;
dockerResponse.message = this.stringService.dockerNotFound;
return dockerResponse;
}
2025-08-29 09:22:02 -07:00
// Return negative response if ambiguous matches exist
const matchTypes = [];
if (exactIdMatch) matchTypes.push("exact ID");
if (exactNameMatch) matchTypes.push("exact name");
if (partialIdMatch && !exactIdMatch) matchTypes.push("partial ID");
if (matchTypes.length > 1) {
this.logger.warn({
message: `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`,
service: this.SERVICE_NAME,
method: "requestDocker",
details: { url: monitor.url },
});
dockerResponse.status = 404;
dockerResponse.status = false;
dockerResponse.message = `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`;
return dockerResponse;
2025-04-20 11:29:53 -07:00
}
const container = docker.getContainer(targetContainer.Id);
2025-10-09 09:33:14 -07:00
const { response, responseTime, error } = await this.timeRequest(() => container.inspect());
2025-04-20 11:29:53 -07:00
2025-10-09 09:33:14 -07:00
dockerResponse.responseTime = responseTime;
2025-08-29 09:22:02 -07:00
dockerResponse.status = response?.State?.Status === "running" ? true : false;
dockerResponse.code = 200;
dockerResponse.message = "Docker container status fetched successfully";
2025-04-20 11:29:53 -07:00
if (error) {
dockerResponse.status = false;
dockerResponse.code = error.statusCode || this.NETWORK_ERROR;
2025-07-21 10:34:16 -07:00
dockerResponse.message = error.reason || "Failed to fetch Docker container information";
2025-04-20 11:29:53 -07:00
return dockerResponse;
}
2025-08-15 16:04:43 -07:00
2025-04-20 11:29:53 -07:00
return dockerResponse;
2025-08-15 16:04:43 -07:00
} catch (err) {
err.service = this.SERVICE_NAME;
err.method = "requestDocker";
throw err;
2025-04-20 11:29:53 -07:00
}
}
2025-06-06 14:05:59 +08:00
async requestPort(monitor) {
2025-04-20 11:29:53 -07:00
try {
2025-06-06 14:05:59 +08:00
const { url, port } = monitor;
2025-10-09 09:33:14 -07:00
const { response, responseTime, error } = await this.timeRequest(async () => {
2025-04-20 11:29:53 -07:00
return new Promise((resolve, reject) => {
const socket = this.net.createConnection(
{
host: url,
port,
},
() => {
socket.end();
socket.destroy();
resolve({ success: true });
}
);
socket.setTimeout(5000);
socket.on("timeout", () => {
socket.destroy();
reject(new Error("Connection timeout"));
});
socket.on("error", (err) => {
socket.destroy();
reject(err);
});
});
});
const portResponse = {
2025-08-15 16:04:43 -07:00
code: 200,
status: response.success,
message: this.stringService.portSuccess,
2026-01-13 20:20:00 +00:00
monitorId: monitor.id,
2025-06-06 14:05:59 +08:00
type: monitor.type,
2025-10-09 09:33:14 -07:00
responseTime: responseTime,
2025-04-20 11:29:53 -07:00
};
if (error) {
portResponse.code = this.NETWORK_ERROR;
2025-08-15 16:04:43 -07:00
portResponse.status = false;
2025-04-20 11:29:53 -07:00
portResponse.message = this.stringService.portFail;
return portResponse;
}
return portResponse;
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = "requestTCP";
throw error;
}
}
2025-08-15 16:04:43 -07:00
async requestGame(monitor) {
try {
const { url, port, gameId } = monitor;
const gameResponse = {
code: 200,
status: true,
message: "Success",
2026-01-13 20:20:00 +00:00
monitorId: monitor.id,
2025-08-15 16:04:43 -07:00
type: "game",
};
const state = await this.GameDig.query({
type: gameId,
host: url,
port: port,
}).catch((error) => {
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
method: "requestGame",
details: { url, port, gameId },
});
});
if (!state) {
gameResponse.code = this.NETWORK_ERROR;
gameResponse.status = false;
gameResponse.message = "No response";
return gameResponse;
}
gameResponse.responseTime = state.ping;
gameResponse.payload = state;
return gameResponse;
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = "requestPing";
throw error;
}
}
async handleUnsupportedType(type) {
2025-04-20 11:29:53 -07:00
const err = new Error(`Unsupported type: ${type}`);
err.service = this.SERVICE_NAME;
err.method = "getStatus";
throw err;
}
2025-08-15 16:04:43 -07:00
// Other network requests unrelated to monitoring:
2025-06-19 12:11:43 +08:00
async requestWebhook(type, url, body) {
2025-04-20 11:29:53 -07:00
try {
2025-06-19 12:11:43 +08:00
const response = await this.axios.post(url, body, {
2025-04-20 11:29:53 -07:00
headers: {
"Content-Type": "application/json",
},
});
return {
type: "webhook",
status: true,
code: response.status,
2025-06-19 12:11:43 +08:00
message: `Successfully sent ${type} notification`,
2025-04-20 11:29:53 -07:00
payload: response.data,
};
} catch (error) {
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
method: "requestWebhook",
});
return {
type: "webhook",
status: false,
code: error.response?.status || this.NETWORK_ERROR,
2025-06-19 12:11:43 +08:00
message: `Failed to send ${type} notification`,
2025-04-20 11:29:53 -07:00
payload: error.response?.data,
};
}
}
2025-06-10 13:44:15 +08:00
async requestPagerDuty({ message, routingKey, monitorUrl }) {
try {
const response = await this.axios.post(`https://events.pagerduty.com/v2/enqueue`, {
routing_key: routingKey,
event_action: "trigger",
payload: {
summary: message,
severity: "critical",
source: monitorUrl,
timestamp: new Date().toISOString(),
},
});
if (response?.data?.status !== "success") return false;
return true;
} catch (error) {
error.details = error.response?.data;
error.service = this.SERVICE_NAME;
error.method = "requestPagerDuty";
throw error;
}
}
async requestMatrix({ homeserverUrl, accessToken, roomId, message }) {
try {
const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`;
const body = {
msgtype: "m.text",
body: message,
format: "org.matrix.custom.html",
formatted_body: message,
};
const response = await this.axios.post(url, body, {
headers: {
"Content-Type": "application/json",
},
});
return {
status: true,
code: response.status,
message: "Successfully sent Matrix notification",
};
} catch (error) {
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
method: "requestMatrix",
});
return {
status: false,
code: error.response?.status || this.NETWORK_ERROR,
message: "Failed to send Matrix notification",
payload: error.response?.data,
};
}
}
2025-04-20 11:29:53 -07:00
}
export default NetworkService;