import net from 'node:net'; import * as services from '../../db/repos/monitored_services.js'; import { log } from '../../log.js'; export const NAME = 'discover.lan'; // Common homelab web/service ports to probe. const PORTS = [80, 81, 443, 2424, 3000, 3001, 5000, 5055, 6767, 6875, 7878, 8000, 8006, 8080, 8081, 8096, 8123, 8265, 8384, 8443, 8989, 9000, 9090, 9696, 11434, 19999, 32400, 60072]; const HTTPS_PORTS = new Set([443, 8443, 8006]); function tcpOpen(host, port, timeoutMs = 350) { return new Promise(resolve => { const sock = net.connect({ host, port }); let done = false; const finish = (ok) => { if (!done) { done = true; sock.destroy(); resolve(ok); } }; sock.setTimeout(timeoutMs); sock.on('connect', () => finish(true)); sock.on('timeout', () => finish(false)); sock.on('error', () => finish(false)); }); } async function httpTitle(url) { try { const res = await fetch(url, { redirect: 'manual', signal: AbortSignal.timeout(2500) }); let title = ''; if (res.status >= 200 && res.status < 400) { const html = await res.text().catch(() => ''); const m = html.match(/([^<]{1,80})/i); title = m ? m[1].trim().replace(/\s+/g, ' ') : ''; } return { code: res.status, title }; } catch { return null; } } // Test seam. let _tcp = tcpOpen, _http = httpTitle; export function _setProbes({ tcp, http } = {}) { _tcp = tcp || tcpOpen; _http = http || httpTitle; } async function mapPool(items, concurrency, fn) { const out = new Array(items.length); let i = 0; await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, async () => { while (i < items.length) { const idx = i++; out[idx] = await fn(items[idx]); } })); return out; } export async function handler(job) { const subnet = job?.data?.subnet || process.env.DISCOVER_SUBNET || '192.168.1'; const targets = []; for (let h = 1; h <= 254; h++) for (const port of PORTS) targets.push({ host: `${subnet}.${h}`, port }); // 1) TCP sweep → live host:ports const open = (await mapPool(targets, 120, async (t) => (await _tcp(t.host, t.port)) ? t : null)).filter(Boolean); // 2) HTTP-probe each, build + upsert discovered candidates (no-clobber in the repo) let added = 0; for (const { host, port } of open) { const scheme = HTTPS_PORTS.has(port) ? 'https' : 'http'; const url = `${scheme}://${host}:${port}`; const probe = await _http(url); const name = (probe && probe.title) || `${host}:${port}`; const id = `disc-${host.replace(/\./g, '-')}-${port}`; const check = scheme === 'https' ? { type: 'tcp' } : { type: 'http' }; const r = await services.upsertDiscovered({ id, name, category: 'other', host, url, check }); if (r) added++; } log.info({ open: open.length, added }, 'lan discovery complete'); return { open: open.length, added }; }