// Flight Lookup — Node proxy + static server // Serves the static site and proxies lookups to the OpenSky Network API. // OpenSky gives live ADS-B positions (no scheduled status), so a lookup // returns the flight's current position if it's airborne, otherwise a // "not airborne" result. Mock data is used as a fallback when OpenSky // has no live match for the callsign. const http = require("http"); const fs = require("fs"); const path = require("path"); const PORT = process.env.PORT || 9000; const ROOT = __dirname; // ---- OpenSky credentials (from credentials.json) ---- let OPENSKY_USER = ""; let OPENSKY_PASS = ""; try { const creds = JSON.parse(fs.readFileSync(path.join(ROOT, "credentials.json"), "utf8")); OPENSKY_USER = creds.clientId || ""; OPENSKY_PASS = creds.clientSecret || ""; } catch (e) { console.warn("Could not read credentials.json — OpenSky lookups will use anonymous access."); } const OPENSKY_BASE = "https://opensky-network.org"; // ---- MIME types for static files ---- const MIME = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".png": "image/png", ".svg": "image/svg+xml", ".ico": "image/x-icon", ".webmanifest": "application/manifest+json", }; function sendJson(res, status, obj) { const body = JSON.stringify(obj); res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", }); res.end(body); } function serveStatic(req, res, urlPath) { let filePath = urlPath === "/" ? "/index.html" : urlPath; filePath = path.normalize(filePath).replace(/^(\.\.[/\\])+/, ""); const full = path.join(ROOT, filePath); if (!full.startsWith(ROOT)) { res.writeHead(403); return res.end("Forbidden"); } fs.readFile(full, (err, data) => { if (err) { res.writeHead(404, { "Content-Type": "text/plain" }); return res.end("Not found"); } const ext = path.extname(full).toLowerCase(); res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream" }); res.end(data); }); } // ---- OpenSky call ---- function openSkyFetch(pathname) { const auth = Buffer.from(`${OPENSKY_USER}:${OPENSKY_PASS}`).toString("base64"); const headers = { "User-Agent": "flight-lookup/1.0 (personal project)" }; if (OPENSKY_USER) headers["Authorization"] = "Basic " + auth; return fetch(OPENSKY_BASE + pathname, { headers }); } // OpenSky states/all returns an array of arrays. Index map: // 0 0: icao24, 1: callsign, 2: origin_country, 3: time_position, 4: last_seen, // 5: longitude, 6: latitude, 7: baro_altitude, 8: on_ground, 9: velocity, // 10: true_track, 11: vertical_rate, 12: sensors, 13: geo_altitude, // 14: squawk, 15: spi, 16: position_source function normalizeState(s) { return { icao24: s[0], callsign: (s[1] || "").trim(), country: s[2], lastSeen: s[4], lon: s[5], lat: s[6], baroAlt: s[7], onGround: s[8], velocity: s[9], track: s[10], vertRate: s[11], geoAlt: s[13], squawk: s[14], }; } // Find a live state matching the callsign. OpenSky callsigns are the // flight number (e.g. "AA100"). We match on the numeric flight number and // prefer an exact callsign match. function findFlight(states, airlineCode, flightNumber) { const num = String(flightNumber); let exact = null; let numeric = null; for (const s of states) { const cs = s.callsign.toUpperCase(); if (!cs) continue; // Exact: callsign equals "AA100" if (cs === `${airlineCode}${num}`) { exact = s; break; } // Numeric: callsign ends with the flight number (e.g. "AAL100", "100") if (!numeric && cs.endsWith(num) && cs.length >= num.length) { numeric = s; } } return exact || numeric; } async function handleLookup(req, res, query) { const code = (query.get("code") || "").toUpperCase(); const num = (query.get("num") || "").trim(); if (!code || !num) { return sendJson(res, 400, { error: "code and num query params required" }); } let states = []; let source = "opensky"; let openSkyError = null; try { const r = await openSkyFetch("/api/states/all"); if (!r.ok) { openSkyError = `OpenSky HTTP ${r.status}`; } else { const data = await r.json(); states = (data.states || []).map(normalizeState).filter((s) => s.callsign); } } catch (e) { openSkyError = String(e.message || e); } const match = states.length ? findFlight(states, code, num) : null; if (match) { const now = Date.now(); const lastSeenMs = match.lastSeen ? match.lastSeen * 1000 : now; const altitudeFt = match.geoAlt != null ? Math.round(match.geoAlt * 3.28084) : null; return sendJson(res, 200, { source: "opensky", live: true, airlineCode: code, flightNumber: num, callsign: match.callsign, country: match.country, status: match.onGround ? "onGround" : "airborne", position: { lat: match.lat, lon: match.lon, altitudeFt, velocityKt: match.velocity != null ? Math.round(match.velocity * 1.94384) : null, trackDeg: match.track != null ? Math.round(match.track) : null, vertRateFpm: match.vertRate != null ? Math.round(match.vertRate * 196.85) : null, }, lastSeen: lastSeenMs, squawk: match.squawk, icao24: match.icao24, }); } // No live match — return a "not airborne" result so the UI can show // a friendly message (and optionally fall back to mock). return sendJson(res, 200, { source: openSkyError ? "none" : "opensky", live: false, airlineCode: code, flightNumber: num, error: openSkyError || "notAirborne", message: openSkyError ? `OpenSky unavailable: ${openSkyError}` : "No live position found for this flight right now.", }); } const server = http.createServer((req, res) => { const url = new URL(req.url, `http://${req.headers.host || "localhost"}`); if (url.pathname === "/api/lookup") { handleLookup(req, res, url.searchParams).catch((e) => { sendJson(res, 500, { error: String(e.message || e) }); }); return; } if (url.pathname === "/api/health") { return sendJson(res, 200, { ok: true, opensky: Boolean(OPENSKY_USER) }); } serveStatic(req, res, url.pathname); }); server.listen(PORT, "0.0.0.0", () => { console.log(`Flight Lookup running at http://0.0.0.0:${PORT}`); console.log(`OpenSky auth: ${OPENSKY_USER ? "configured" : "anonymous"}`); });