|
import { connect } from 'cloudflare:sockets'; |
|
// TCP Health Checker |
|
// Checks to see if specific TCP sockets of servers are still working. |
|
const smsURL = "https://textbelt.com/text"; |
|
const failMessage = "⚠️ TCP Healthcheck FAILED: "; |
|
const clearMessage = "✅ TCP Healthcheck OK: "; |
|
// Returns an Array of hostnames to be healthchecked, or null of none were found |
|
function getHosts(env) { |
|
if(env.hosts) { |
|
const hostsString = String(env.hosts); |
|
if(hostsString.length) |
|
return hostsString.split(","); |
|
} |
|
console.warn("Aborting: Required nvironment variable 'hosts' is missing or lacks hostnames!"); |
|
return null; |
|
} |
|
// Send a text message to the given phone number |
|
function alert(smsNumber, smsKey, message) { |
|
return fetch(smsURL, { |
|
method: "post", |
|
headers: { "Content-Type": "application/json" }, |
|
body: JSON.stringify({ |
|
phone: smsNumber, |
|
message: message, |
|
key: smsKey, |
|
}) |
|
}); |
|
} |
|
// Resolves with true if TCP connection was successful |
|
async function healthcheck(address, port) { |
|
const tcpSocket = connect({ |
|
hostname: address, |
|
port: port, |
|
}); |
|
await tcpSocket.opened; |
|
tcpSocket.close().catch(()=>{}); |
|
return true; |
|
} |
|
// Sets an alert to tripped state and sends an alert message |
|
async function tripAlert(env, name) { |
|
const alertId = `${name}-tripped`; |
|
if(await env.alerts.get(alertId, "json")) |
|
return; |
|
await env.alerts.put(alertId, true); |
|
await alert(env.smsNumber, env.smsKey, failMessage + name); |
|
} |
|
// Sets an alert to cleared state and sends a clear message |
|
async function clearAlert(env, name) { |
|
const alertId = `${name}-tripped`; |
|
if(!(await env.alerts.get(alertId, "json"))) |
|
return; |
|
await env.alerts.put(alertId, false); |
|
await alert(env.smsNumber, env.smsKey, clearMessage + name); |
|
} |
|
// Queues a healthcheck for each given host, and returns an array of Promises which will resolve upon success |
|
function queueHealthChecks(env, hostsArray, sendAlerts = true) { |
|
const checks = []; |
|
let total = 1; |
|
for(const host of hostsArray) { |
|
total++; |
|
const socketAddress = host.trim(); |
|
const hostData = socketAddress.split(":"); |
|
const address = hostData[0]; |
|
const port = Number(hostData[1]); |
|
const queuedCheck = healthcheck(address, port).then(async (success) => { |
|
if(!sendAlerts) |
|
return success; |
|
if(success === true) |
|
await clearAlert(env, socketAddress); |
|
else |
|
await tripAlert(env, socketAddress); |
|
return success; |
|
}).catch(async (err) => { |
|
console.error(err); |
|
if(!sendAlerts) |
|
return err; |
|
await tripAlert(env, socketAddress); |
|
return err; |
|
}); |
|
checks.push(queuedCheck); |
|
} |
|
return checks; |
|
} |
|
// Fetch handler for testing purposes, does not send any alerts |
|
async function fetchHandler(request, env) { |
|
const hosts = getHosts(env); |
|
if(hosts === null || !hosts.length) { |
|
return new Response("Aborting: Required nvironment variable 'hosts' is missing or lacks hostnames!", { |
|
headers: { "Content-Type": "text/plain" } |
|
}); |
|
} |
|
if(!env.alerts) { |
|
console.warn("Aborting: Required KV Binding 'alerts' is missing!") |
|
return; |
|
} |
|
const checks = queueHealthChecks(env, hosts, false); |
|
const handlers = []; |
|
let currentCheck = 0; |
|
for(const check of checks) { |
|
const idString = `Check #${currentCheck} (${hosts[currentCheck].trim()})`; |
|
currentCheck++; |
|
handlers.push(check.then((status) => { |
|
if(status == true) |
|
return `${idString} OK`; |
|
if(status) |
|
return `${idString} FAILED:\n\t${status}`; |
|
return `${idString} FAILED:\n\tFailed to open TCP socket`; |
|
}).catch((err) => { |
|
console.error(err); |
|
return `${idString} FAILED:\n\t${err}`; |
|
})); |
|
} |
|
const results = await Promise.all(handlers); |
|
return new Response(results.join("\n"), { |
|
headers: { "Content-Type": "text/plain" } |
|
}); |
|
} |
|
// Scheduled healthcheck handler which sends alerts upon failure |
|
function scheduledHandler(request, env) { |
|
const hosts = getHosts(env); |
|
if(hosts === null || !hosts.length) |
|
return; |
|
if(!env.alerts) { |
|
console.warn("Aborting: Required KV Binding 'alerts' is missing!") |
|
return; |
|
} |
|
return Promise.all(queueHealthChecks(env, hosts)); |
|
} |
|
export default { |
|
fetch: fetchHandler, |
|
scheduled: scheduledHandler, |
|
}; |