|
export default { |
|
async fetch(request, env, ctx) { |
|
try { |
|
const srvName = "_factorio._udp.example.com"; // CHANGE ME: Requires a DNS SRV Record, See https://wiki.factorio.com/Multiplayer#DNS_SRV_Records. |
|
const dohBase = "https://1.1.1.1/dns-query"; |
|
|
|
// --- helpers --- |
|
const doh = async (name, type) => { |
|
const url = `${dohBase}?name=${encodeURIComponent(name)}&type=${encodeURIComponent(type)}`; |
|
const res = await fetch(url, { |
|
headers: { "accept": "application/dns-json" }, |
|
// Keep DoH results hot for a short time on the edge |
|
cf: { cacheTtl: 20, cacheEverything: true } |
|
}); |
|
if (!res.ok) throw new Error(`DoH ${type} query failed (${res.status})`); |
|
return res.json(); |
|
}; |
|
|
|
const pickSrv = (answers) => { |
|
// SRV RDATA: "priority weight port target" |
|
// Prefer lowest priority; if tie, highest weight. |
|
const parsed = answers |
|
.filter(a => a.type === 33 && typeof a.data === "string") |
|
.map(a => { |
|
const [priStr, wtStr, portStr, ...targetParts] = a.data.trim().split(/\s+/); |
|
const target = targetParts.join(" ").replace(/\.+$/, ""); // strip trailing dot |
|
return { |
|
priority: parseInt(priStr, 10), |
|
weight: parseInt(wtStr, 10), |
|
port: parseInt(portStr, 10), |
|
target |
|
}; |
|
}); |
|
|
|
if (parsed.length === 0) throw new Error("No SRV answers found"); |
|
parsed.sort((a, b) => a.priority - b.priority || b.weight - a.weight); |
|
return parsed[0]; |
|
}; |
|
|
|
const firstA = (answers) => { |
|
const a = answers.find(a => a.type === 1 && a.data); |
|
return a?.data ?? null; |
|
}; |
|
|
|
const firstAAAA = (answers) => { |
|
const aaaa = answers.find(a => a.type === 28 && aaaa?.data); // will fail; fix below |
|
return aaaa?.data ?? null; |
|
}; |
|
|
|
// --- 1) SRV lookup --- |
|
const srvResp = await doh(srvName, "SRV"); |
|
if (!srvResp.Answer || !Array.isArray(srvResp.Answer)) { |
|
throw new Error("SRV lookup returned no Answer"); |
|
} |
|
const srv = pickSrv(srvResp.Answer); |
|
|
|
// --- 2) Resolve target to IP (A first, then AAAA) --- |
|
const aResp = await doh(srv.target, "A"); |
|
let ip = (aResp.Answer && Array.isArray(aResp.Answer)) ? firstA(aResp.Answer) : null; |
|
|
|
if (!ip) { |
|
const aaaaResp = await doh(srv.target, "AAAA"); |
|
const answers = (aaaaResp.Answer && Array.isArray(aaaaResp.Answer)) ? aaaaResp.Answer : []; |
|
const aaaaRec = answers.find(x => x.type === 28 && x.data); |
|
ip = aaaaRec?.data ?? null; |
|
} |
|
if (!ip) throw new Error(`Could not resolve IP for ${srv.target}`); |
|
|
|
// Wrap IPv6 literal for host:port |
|
const hostForUrl = ip.includes(":") ? `[${ip}]` : ip; |
|
|
|
// --- 3) Query Factorio get-game-details --- |
|
const detailsUrl = `https://multiplayer.factorio.com/get-game-details/${hostForUrl}:${srv.port}`; |
|
const detailsRes = await fetch(detailsUrl, { |
|
// Factorio endpoint is public; set a small cache |
|
cf: { cacheTtl: 10, cacheEverything: true } |
|
}); |
|
|
|
// Pass through status & body; add friendly CORS |
|
const body = await detailsRes.text(); |
|
const headers = new Headers(detailsRes.headers); |
|
headers.set("access-control-allow-origin", "*"); |
|
headers.set("access-control-allow-methods", "GET, OPTIONS"); |
|
headers.set("access-control-allow-headers", "Content-Type"); |
|
headers.set("cache-control", "public, max-age=10"); |
|
|
|
return new Response(body, { status: detailsRes.status, headers }); |
|
} catch (err) { |
|
return new Response( |
|
JSON.stringify({ error: String(err?.message ?? err) }, null, 2), |
|
{ |
|
status: 502, |
|
headers: { |
|
"content-type": "application/json", |
|
"access-control-allow-origin": "*" |
|
} |
|
} |
|
); |
|
} |
|
} |
|
}; |