doh / server.ts
hongshi-files's picture
Upload 13 files
ab85026 verified
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 });