Spaces:
Sleeping
Sleeping
| import { serve } from "https://deno.land/std@0.208.0/http/server.ts"; | |
| const HOST = Deno.env.get("HOST") ?? "0.0.0.0"; | |
| const PORT = Number(Deno.env.get("PORT") ?? "7860"); | |
| const DOH = (Deno.env.get("DOH") ?? "cloudflare-dns.com") | |
| .replace(/^https?:\/\//, "") | |
| .split("/")[0]; | |
| const CUSTOM_PATH = (Deno.env.get("DOH_PATH") ?? "").trim().replace(/^\/+|\/+$/g, ""); | |
| function isDnsQueryPath(p: string): boolean { | |
| if (!CUSTOM_PATH || CUSTOM_PATH === "dns-query") return p === "/dns-query"; | |
| return p === `/${CUSTOM_PATH}`; | |
| } | |
| function cors(h = new Headers()) { | |
| h.set("access-control-allow-origin", "*"); | |
| h.set("access-control-allow-methods", "GET,POST,OPTIONS"); | |
| h.set("access-control-allow-headers", "*"); | |
| return h; | |
| } | |
| const JSON_HEADERS: HeadersInit = { | |
| accept: "application/dns-json", | |
| "user-agent": "HongShi-DoH/edge", | |
| }; | |
| const BIN_HEADERS: HeadersInit = { | |
| accept: "application/dns-message", | |
| "user-agent": "HongShi-DoH/edge", | |
| }; | |
| const mime: Record<string, string> = { | |
| 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", | |
| jpg: "image/jpeg", | |
| jpeg: "image/jpeg", | |
| svg: "image/svg+xml", | |
| ico: "image/x-icon", | |
| txt: "text/plain; charset=utf-8", | |
| wasm: "application/wasm", | |
| webp: "image/webp", | |
| woff2: "font/woff2", | |
| }; | |
| async function serveFile(path: string) { | |
| try { | |
| const data = await Deno.readFile(path); | |
| const ext = path.split(".").pop()?.toLowerCase() ?? "txt"; | |
| const ct = mime[ext] ?? "application/octet-stream"; | |
| return new Response(data, { headers: cors(new Headers({ "content-type": ct })) }); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function safeJoinPublic(urlPath: string) { | |
| const clean = urlPath.replace(/^\/+/, "").replace(/\.\.+/g, ""); | |
| return `./public/${clean}`; | |
| } | |
| async function dohBinaryProxy(req: Request) { | |
| const url = new URL(req.url); | |
| const upstream = `https://${DOH}/dns-query` + (url.search || ""); | |
| const isGet = url.searchParams.has("dns"); | |
| const isPost = | |
| req.method === "POST" && | |
| (req.headers.get("content-type") || "").startsWith("application/dns-message"); | |
| if (!isGet && !isPost) { | |
| return new Response("Bad Request", { status: 400, headers: cors() }); | |
| } | |
| const init: RequestInit = isGet | |
| ? { headers: BIN_HEADERS } | |
| : { | |
| method: "POST", | |
| headers: { "content-type": "application/dns-message" }, | |
| body: await req.arrayBuffer(), | |
| }; | |
| const r = await fetch(upstream, init); | |
| return new Response(r.body, { | |
| status: r.status, | |
| headers: cors(new Headers({ "content-type": "application/dns-message" })), | |
| }); | |
| } | |
| async function resolveJson(name: string) { | |
| const mk = (t: string) => | |
| `https://${DOH}/dns-query?name=${encodeURIComponent(name)}&type=${t}`; | |
| const [a, aaaa, ns] = await Promise.all([ | |
| fetch(mk("A"), { headers: JSON_HEADERS }).then((r) => r.json()).catch(() => null), | |
| fetch(mk("AAAA"), { headers: JSON_HEADERS }).then((r) => r.json()).catch(() => null), | |
| fetch(mk("NS"), { headers: JSON_HEADERS }).then((r) => r.json()).catch(() => null), | |
| ]); | |
| return { A: a?.Answer ?? [], AAAA: aaaa?.Answer ?? [], NS: ns?.Answer ?? [] }; | |
| } | |
| async function handler(req: Request) { | |
| const url = new URL(req.url); | |
| const p = url.pathname; | |
| if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: cors() }); | |
| if (req.method === "GET" && p === "/") { | |
| const home = await serveFile("./public/index.html"); | |
| if (home) return home; | |
| const html = `<!doctype html><meta charset=utf-8><title>HongShi-DoH</title> | |
| <h1>HongShi-DoH</h1> | |
| <p><a href='/ui/'>UI</a> 路 <a href='/meta'>/meta</a> 路 <a href='/ip'>/ip</a> 路 <a href='/resolve?name=example.com'>/resolve</a></p>`; | |
| return new Response(html, { | |
| headers: cors(new Headers({ "content-type": "text/html; charset=utf-8" })), | |
| }); | |
| } | |
| if (req.method === "GET" && (p === "/ui" || p === "/ui/")) { | |
| if (p === "/ui") return new Response("", { status: 302, headers: { Location: "/ui/" } }); | |
| const ui = await serveFile("./public/ui/index.html"); | |
| return ui ?? new Response("Not Found", { status: 404, headers: cors() }); | |
| } | |
| if (req.method === "GET" && p.startsWith("/ui/")) { | |
| const file = safeJoinPublic(p.replace(/^\/ui\//, "ui/")); | |
| const res = await serveFile(file); | |
| return res ?? new Response("Not Found", { status: 404, headers: cors() }); | |
| } | |
| if (req.method === "GET") { | |
| const file = safeJoinPublic(p); | |
| const res = await serveFile(file); | |
| if (res) return res; | |
| } | |
| if (req.method === "GET" && p === "/meta") { | |
| const effectivePath = (!CUSTOM_PATH || CUSTOM_PATH === "dns-query") | |
| ? "/dns-query" | |
| : `/${CUSTOM_PATH}`; | |
| const body = { | |
| doh: DOH, | |
| doh_path: CUSTOM_PATH || "dns-query", | |
| effective_path: effectivePath, | |
| platform: "hf-space", | |
| env: ["HOST", "PORT", "DOH", "DOH_PATH"] | |
| .filter((k) => Deno.env.get(k)) | |
| .reduce((o, k) => { | |
| o[k] = Deno.env.get(k)!; | |
| return o; | |
| }, {} as Record<string, string>), | |
| }; | |
| return new Response(JSON.stringify(body), { | |
| headers: cors(new Headers({ "content-type": "application/json" })), | |
| }); | |
| } | |
| if (req.method === "GET" && p === "/host") { | |
| const body = { hostname: Deno.env.get("HOSTNAME") ?? null, platform: "hf-space" }; | |
| return new Response(JSON.stringify(body), { | |
| headers: cors(new Headers({ "content-type": "application/json" })), | |
| }); | |
| } | |
| if (req.method === "GET" && p === "/ip") { | |
| const h = req.headers; | |
| const ip = | |
| h.get("x-real-ip") ?? | |
| h.get("cf-connecting-ip") ?? | |
| h.get("fly-client-ip") ?? | |
| ((h.get("x-forwarded-for") || "").split(",")[0].trim() || ""); | |
| const body = { ip, source: { platform: "hf-space", provider: "headers", enriched: false } }; | |
| return new Response(JSON.stringify(body), { | |
| headers: cors(new Headers({ "content-type": "application/json" })), | |
| }); | |
| } | |
| if (req.method === "GET" && p === "/resolve") { | |
| const name = url.searchParams.get("name"); | |
| if (!name) { | |
| return new Response(JSON.stringify({ ok: false, err: "Missing name" }), { | |
| status: 400, | |
| headers: cors(new Headers({ "content-type": "application/json" })), | |
| }); | |
| } | |
| const data = await resolveJson(name); | |
| return new Response(JSON.stringify({ ok: true, name, records: data }), { | |
| headers: cors(new Headers({ "content-type": "application/json" })), | |
| }); | |
| } | |
| if ((req.method === "GET" || req.method === "POST") && isDnsQueryPath(p)) { | |
| return dohBinaryProxy(req); | |
| } | |
| return new Response("Not Found", { status: 404, headers: cors() }); | |
| } | |
| console.log( | |
| `[HongShi-DoH] listening on http://${HOST}:${PORT} (upstream DoH: ${DOH}, path: ${ | |
| (!CUSTOM_PATH || CUSTOM_PATH === "dns-query") ? "/dns-query" : `/${CUSTOM_PATH}` | |
| })`, | |
| ); | |
| serve(handler, { hostname: HOST, port: PORT }); | |