diff --git a/proxy/api/index.js b/proxy/api/index.js index 94bc364..3d86390 100644 --- a/proxy/api/index.js +++ b/proxy/api/index.js @@ -1,11 +1,156 @@ // This is a Node.js implementation of status monitoring +const location = '' + const express = require('express') +const net = require('net') const app = express() const port = 3000 -app.get('/', (req, res) => { - res.send('Hello World!') +async function getWorkerLocation() { + const res = await fetch('https://cloudflare.com/cdn-cgi/trace') + const text = await res.text() + + const colo = /^colo=(.*)$/m.exec(text)?.[1] + return colo +} + +const fetchTimeout = (url, ms, options = {}) => { + const controller = new AbortController() + const promise = fetch(url, { signal: controller.signal, ...options }) + const timeout = setTimeout(() => controller.abort(), ms) + return promise.finally(() => clearTimeout(timeout)) +} + +function withTimeout(millis, promise) { + const timeout = new Promise((resolve, reject) => + setTimeout(() => reject(new Error(`Promise timed out after ${millis}ms`)), millis) + ) + + return Promise.race([promise, timeout]) +} + + +// TODO: More code reuse here +async function getStatus(monitor) { + let status = { + ping: 0, + up: false, + err: 'Unknown', + } + + const startTime = Date.now() + + if (monitor.method === 'TCP_PING') { + // TCP port endpoint monitor + try { + // This is not a real https connection, but we need to add a dummy `https://` to parse the hostname & port + const parsed = new URL("https://" + monitor.target) + + const client = await net.createConnection( + { host: parsed.hostname, port: Number(parsed.port) }, + () => { + console.log(`${monitor.name} connected to ${monitor.target}`) + status.ping = Date.now() - startTime + status.up = true + status.err = '' + client.end() + } + ) + + // const socket = connect({ hostname: parsed.hostname, port: Number(parsed.port) }) + + // // Now we have an `opened` promise! + // // @ts-ignore + // await withTimeout(monitor.timeout || 10000, socket.opened) + // await socket.close() + + + } catch (e) { + console.log(`${monitor.name} errored with ${e.name}: ${e.message}`) + if (e.message.includes('timed out')) { + status.ping = monitor.timeout || 10000 + } + status.up = false + status.err = e.name + ': ' + e.message + } + } else { + // HTTP endpoint monitor + try { + const response = await fetchTimeout(monitor.target, monitor.timeout || 10000, { + method: monitor.method, + headers: monitor.headers, + body: monitor.body, + }) + + console.log(`${monitor.name} responded with ${response.status}`) + status.ping = Date.now() - startTime + + if (monitor.expectedCodes) { + if (!monitor.expectedCodes.includes(response.status)) { + console.log(`${monitor.name} expected ${monitor.expectedCodes}, got ${response.status}`) + status.up = false + status.err = `Expected codes: ${JSON.stringify(monitor.expectedCodes)}, Got: ${response.status + }` + return status + } + } else { + if (response.status < 200 || response.status > 299) { + console.log(`${monitor.name} expected 2xx, got ${response.status}`) + status.up = false + status.err = `Expected codes: 2xx, Got: ${response.status}` + return status + } + } + + if (monitor.responseKeyword || monitor.responseForbiddenKeyword) { + // Only read response body if we have a keyword to check + const responseBody = await response.text() + + // MUST contain responseKeyword + if (monitor.responseKeyword && !responseBody.includes(monitor.responseKeyword)) { + console.log(`${monitor.name} expected keyword ${monitor.responseKeyword}, not found in response (truncated to 100 chars): ${responseBody.slice(0, 100)}`) + status.up = false + status.err = "HTTP response doesn't contain the configured keyword" + return status + } + + // MUST NOT contain responseForbiddenKeyword + if (monitor.responseForbiddenKeyword && responseBody.includes(monitor.responseForbiddenKeyword)) { + console.log(`${monitor.name} forbidden keyword ${monitor.responseForbiddenKeyword}, found in response (truncated to 100 chars): ${responseBody.slice(0, 100)}`) + status.up = false + status.err = "HTTP response contains the configured forbidden keyword" + return status + } + } + + status.up = true + status.err = '' + } catch (e) { + console.log(`${monitor.name} errored with ${e.name}: ${e.message}`) + if (e.name === 'AbortError') { + status.ping = monitor.timeout || 10000 + status.up = false + status.err = `Timeout after ${status.ping}ms` + } else { + status.up = false + status.err = e.name + ': ' + e.message + } + } + } + + return status +} + +app.use(express.json()); + +app.post('/', (req, res) => { + res.json( + { + location: getWorkerLocation(), + status: getStatus(req.body), + } + ) }) app.listen(port, () => {