diff --git a/client/src/App.js b/client/src/App.js
index 85be7dc..a48ded1 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -129,6 +129,22 @@ function Dashboard() {
/>
+
+
+
+
+
=4.0.0"
+ }
+ },
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
@@ -2197,6 +2208,27 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/package.json b/package.json
index 13f188d..dace783 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,9 @@
"node-cron": "^3.0.3",
"axios": "^1.6.2",
"nodemailer": "^6.9.7",
- "dotenv": "^16.3.1"
+ "dotenv": "^16.3.1",
+ "ping": "^0.4.4",
+ "ws": "^8.17.1"
},
"devDependencies": {
"nodemon": "^3.0.2",
diff --git a/server/database.js b/server/database.js
index baad159..a810620 100644
--- a/server/database.js
+++ b/server/database.js
@@ -41,4 +41,12 @@ db.exec(`
CREATE INDEX IF NOT EXISTS idx_incidents_resource_id ON incidents(resource_id);
`);
+// Lightweight migration: add details column for per-check metadata if missing
+try {
+ db.prepare("ALTER TABLE checks ADD COLUMN details TEXT").run();
+ // Column added
+} catch (err) {
+ // Ignore if the column already exists
+}
+
module.exports = db;
diff --git a/server/monitorService.js b/server/monitorService.js
index 3de0fbd..ecfe799 100644
--- a/server/monitorService.js
+++ b/server/monitorService.js
@@ -1,26 +1,77 @@
const axios = require('axios');
+const net = require('net');
+const tls = require('tls');
+const dns = require('dns').promises;
+const WebSocket = require('ws');
+const ping = require('ping');
const db = require('./database');
+const DEFAULT_PORTS = {
+ http: 80,
+ https: 443,
+ tcp: 80,
+ tls: 443,
+ websocket: 80,
+};
+
+function parseHostPort(urlString, fallbackPort) {
+ try {
+ const u = new URL(urlString.startsWith('http') || urlString.startsWith('ws') ? urlString : `http://${urlString}`);
+ return { host: u.hostname, port: u.port ? Number(u.port) : fallbackPort };
+ } catch (err) {
+ // Fallback if url is just host:port
+ const [host, port] = urlString.split(':');
+ return { host, port: port ? Number(port) : fallbackPort };
+ }
+}
+
class MonitorService {
async checkResource(resource) {
+ const type = (resource.type || 'http').toLowerCase();
const startTime = Date.now();
- let result = {
+ const base = {
resource_id: resource.id,
status: 'down',
response_time: null,
status_code: null,
error_message: null,
+ details: null,
};
+ switch (type) {
+ case 'http':
+ case 'https':
+ case 'health':
+ return this.checkHttp(resource, startTime, base);
+ case 'tcp':
+ return this.checkTcp(resource, startTime, base);
+ case 'tls':
+ return this.checkTls(resource, startTime, base);
+ case 'dns':
+ return this.checkDns(resource, startTime, base);
+ case 'websocket':
+ case 'ws':
+ case 'wss':
+ return this.checkWebSocket(resource, startTime, base);
+ case 'icmp':
+ case 'ping':
+ return this.checkIcmp(resource, startTime, base);
+ default:
+ // Fallback to HTTP if unknown
+ return this.checkHttp(resource, startTime, base);
+ }
+ }
+
+ async checkHttp(resource, startTime, base) {
+ let result = { ...base };
try {
const response = await axios.get(resource.url, {
timeout: resource.timeout,
- validateStatus: () => true, // Don't throw on any status
+ validateStatus: () => true,
+ maxRedirects: 5,
});
-
const responseTime = Date.now() - startTime;
const isUp = response.status >= 200 && response.status < 400;
-
result = {
...result,
status: isUp ? 'up' : 'down',
@@ -32,21 +83,168 @@ class MonitorService {
result.error_message = error.message;
result.response_time = Date.now() - startTime;
}
-
return result;
}
+ async checkTcp(resource, startTime, base) {
+ const { host, port } = parseHostPort(resource.url, DEFAULT_PORTS.tcp);
+ return new Promise(resolve => {
+ const socket = new net.Socket();
+ let finished = false;
+
+ const done = (status, error_message = null) => {
+ if (finished) return;
+ finished = true;
+ socket.destroy();
+ resolve({
+ ...base,
+ status,
+ response_time: Date.now() - startTime,
+ status_code: null,
+ error_message,
+ details: JSON.stringify({ host, port }),
+ });
+ };
+
+ socket.setTimeout(resource.timeout || 5000);
+ socket.once('connect', () => done('up'));
+ socket.once('timeout', () => done('down', 'TCP timeout'));
+ socket.once('error', (err) => done('down', err.message));
+ socket.connect(port, host);
+ });
+ }
+
+ async checkTls(resource, startTime, base) {
+ const { host, port } = parseHostPort(resource.url, DEFAULT_PORTS.tls);
+ return new Promise(resolve => {
+ const socket = tls.connect({ host, port, servername: host, timeout: resource.timeout || 5000 }, () => {
+ const cert = socket.getPeerCertificate();
+ const now = Date.now();
+ const expires = new Date(cert.valid_to).getTime();
+ const daysRemaining = Math.round((expires - now) / (1000 * 60 * 60 * 24));
+ const validHost = cert.subjectaltname ? cert.subjectaltname.includes(host) : true;
+
+ const status = daysRemaining > 0 && validHost ? 'up' : 'down';
+ resolve({
+ ...base,
+ status,
+ response_time: Date.now() - startTime,
+ error_message: status === 'up' ? null : 'TLS invalid or expired',
+ details: JSON.stringify({
+ issuer: cert.issuer?.CN,
+ subject: cert.subject?.CN,
+ valid_to: cert.valid_to,
+ days_remaining: daysRemaining,
+ host_matches: validHost,
+ }),
+ });
+ socket.end();
+ });
+
+ socket.on('error', err => {
+ resolve({
+ ...base,
+ status: 'down',
+ response_time: Date.now() - startTime,
+ error_message: err.message,
+ });
+ });
+
+ socket.setTimeout(resource.timeout || 5000, () => {
+ resolve({
+ ...base,
+ status: 'down',
+ response_time: Date.now() - startTime,
+ error_message: 'TLS timeout',
+ });
+ socket.destroy();
+ });
+ });
+ }
+
+ async checkDns(resource, startTime, base) {
+ const { host } = parseHostPort(resource.url, DEFAULT_PORTS.http);
+ try {
+ const records = await dns.lookup(host, { all: true });
+ return {
+ ...base,
+ status: 'up',
+ response_time: Date.now() - startTime,
+ details: JSON.stringify({ answers: records }),
+ };
+ } catch (err) {
+ return {
+ ...base,
+ status: 'down',
+ response_time: Date.now() - startTime,
+ error_message: err.message,
+ };
+ }
+ }
+
+ async checkWebSocket(resource, startTime, base) {
+ return new Promise(resolve => {
+ let finished = false;
+
+ const done = (status, error_message = null) => {
+ if (finished) return;
+ finished = true;
+ resolve({
+ ...base,
+ status,
+ response_time: Date.now() - startTime,
+ error_message,
+ });
+ };
+
+ try {
+ const ws = new WebSocket(resource.url, { handshakeTimeout: resource.timeout || 5000 });
+ ws.on('open', () => {
+ done('up');
+ ws.close();
+ });
+ ws.on('error', (err) => done('down', err.message));
+ ws.on('close', () => done('up'));
+ } catch (err) {
+ done('down', err.message);
+ }
+ });
+ }
+
+ async checkIcmp(resource, startTime, base) {
+ const { host } = parseHostPort(resource.url, DEFAULT_PORTS.http);
+ try {
+ const res = await ping.promise.probe(host, { timeout: (resource.timeout || 5000) / 1000 });
+ const status = res.alive ? 'up' : 'down';
+ return {
+ ...base,
+ status,
+ response_time: res.time || Date.now() - startTime,
+ error_message: res.alive ? null : res.output,
+ details: JSON.stringify({ packetLoss: res.packetLoss, time: res.time }),
+ };
+ } catch (err) {
+ return {
+ ...base,
+ status: 'down',
+ response_time: Date.now() - startTime,
+ error_message: err.message,
+ };
+ }
+ }
+
saveCheck(check) {
const stmt = db.prepare(`
- INSERT INTO checks (resource_id, status, response_time, status_code, error_message)
- VALUES (?, ?, ?, ?, ?)
+ INSERT INTO checks (resource_id, status, response_time, status_code, error_message, details)
+ VALUES (?, ?, ?, ?, ?, ?)
`);
stmt.run(
check.resource_id,
check.status,
check.response_time,
check.status_code,
- check.error_message
+ check.error_message,
+ check.details || null
);
}