Files
DNS-Propagation-Checker/index.php
Kim Vinberg aKa Uldtot d0f505afd5 On branch main
Initial commit

 Changes to be committed:
	new file:   api.php
	new file:   index.php
	new file:   readme.md
2026-01-25 14:05:51 +01:00

325 lines
11 KiB
PHP

<!doctype html>
<html lang="da">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DNS Propagation Checker</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.glass {
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.skeleton {
background: linear-gradient(90deg, rgba(255, 255, 255, .05), rgba(255, 255, 255, .12), rgba(255, 255, 255, .05));
background-size: 200% 100%;
animation: shimmer 1.2s infinite;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
td,
th {
padding-top: .45rem !important;
padding-bottom: .45rem !important;
}
</style>
</head>
<body class="min-h-screen bg-slate-950 text-slate-100">
<!-- Header -->
<header class="border-b border-white/10 bg-white/5 glass">
<div class="mx-auto max-w-6xl px-4 py-6">
<div class="flex flex-col gap-3">
<h1 class="text-2xl font-semibold tracking-tight text-slate-100">
DNS Propagation Checker
</h1>
<p class="text-sm leading-relaxed text-slate-300">
This tool performs global DNS lookups using public DNS resolvers in multiple countries.
Results are shown in a compact overview with TTL and record data displayed directly, making it easy to compare DNS responses across locations.
A resolver is only marked as OK when it actually returns valid DNS answers.
</p>
<div class="flex flex-wrap gap-2 text-xs">
<span class="rounded-full border border-white/10 bg-slate-950/30 px-2 py-1 text-slate-300">
🔒 Privacy-friendly · No data logging
</span>
</div>
</div>
</div>
</header>
<!-- Main content -->
<div class="mx-auto max-w-6xl px-4 py-8">
<!-- Controls -->
<div class="mt-6 rounded-2xl border border-white/10 bg-white/5 p-4 glass">
<form id="form" class="grid grid-cols-1 md:grid-cols-12 gap-3 items-end">
<div class="md:col-span-5">
<label class="text-sm text-slate-300">Domain</label>
<input id="domain" required class="w-full rounded-xl border border-white/10 bg-slate-950/50 px-3 py-2.5" placeholder="example.com" />
</div>
<div class="md:col-span-2">
<label class="text-sm text-slate-300">Type</label>
<select id="type" class="w-full rounded-xl border border-white/10 bg-slate-950/50 px-3 py-2.5">
<option>A</option>
<option>AAAA</option>
<!--<option>CNAME</option> -->
<option>MX</option>
<option>NS</option>
<option>TXT</option>
<!-- <option>SOA</option> -->
<!-- <option>SRV</option> -->
<!-- <option>CAA</option> -->
<!-- <option>PTR</option> -->
</select>
</div>
<div class="md:col-span-5">
<label class="text-sm text-slate-300">Expected value (optional)</label>
<input id="expected" class="w-full rounded-xl border border-white/10 bg-slate-950/50 px-3 py-2.5" placeholder="93.184.216.34" />
</div>
<div class="md:col-span-12 flex flex-wrap gap-3 mt-2 items-center justify-between">
<button id="submitBtn" class="inline-flex items-center justify-center gap-2 rounded-lg
bg-slate-200 text-slate-900
px-4 py-2 text-sm font-medium
hover:bg-white
focus:outline-none focus:ring-2 focus:ring-white/40
disabled:opacity-50 disabled:cursor-not-allowed
transition">
🔎 Run lookup
</button>
<div class="flex items-center gap-4 text-sm text-slate-300">
<label><input id="onlyErrors" type="checkbox"> Errors only</label>
<label><input id="onlyMismatches" type="checkbox"> Mismatches only</label>
<input id="filterText" class="rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm" placeholder="Filter…" />
</div>
</div>
</form>
</div>
<!-- Summary -->
<div id="summaryBadges" class="mt-4 flex flex-wrap gap-2"></div>
<!-- Results -->
<div class="mt-4 rounded-2xl border border-white/10 bg-white/5 glass overflow-hidden">
<table class="min-w-full text-sm">
<thead class="bg-slate-950/70 border-b border-white/10">
<tr class="text-left text-slate-300">
<th class="px-4">Resolver</th>
<th class="px-4">Status</th>
<th class="px-4">Time</th>
<th class="px-4">TTL</th>
<th class="px-4">Data</th>
<th class="px-4">Match</th>
<th class="px-4">Detaljer</th>
</tr>
</thead>
<tbody id="rows">
<tr>
<td colspan="7" class="px-4 py-4 text-slate-400">Run lookup to see results…</td>
</tr>
</tbody>
</table>
</div>
</div>
<footer class="mt-10 border-t border-white/10 bg-white/5 text-center">
<div class="max-w-7xl mx-auto px-4 py-4 text-xs text-slate-400">
&copy; <span id="year"></span> <a href="https://dicm.dk/">dicm.dk</a>
</div>
</footer>
<script>
// Auto-opdater årstal
document.getElementById('year').textContent = new Date().getFullYear();
</script>
<script>
function flagFromISO2(code) {
if (!code || code.length !== 2) return "";
const cc = code.toUpperCase();
// regional indicator symbols
const A = 0x1F1E6;
const first = cc.charCodeAt(0) - 65 + A;
const second = cc.charCodeAt(1) - 65 + A;
return String.fromCodePoint(first) + String.fromCodePoint(second);
}
const $ = id => document.getElementById(id);
const rowsEl = $("rows");
const summaryEl = $("summaryBadges");
let lastResponse = null;
/* ---------- helpers ---------- */
function hasAnswers(it) {
return Array.isArray(it?.answers) && it.answers.length > 0;
}
function effectiveStatus(it) {
if (it?.status === "OK" && !hasAnswers(it)) return "NO_RESULT";
return it?.status || "ERROR";
}
function badge(text, cls) {
return `<span class="rounded-full border px-2 py-0.5 text-xs ${cls}">${text}</span>`;
}
function summarizeData(answers) {
if (!answers || !answers.length) return {
ttl: "",
data: ""
};
return {
ttl: answers[0].ttl ?? "",
data: answers.map(a => a.data).filter(Boolean).slice(0, 3).join(" <br><br> ")
};
}
/* ---------- rendering ---------- */
function renderSummary(resp) {
const results = resp.results || [];
const ok = results.filter(r => effectiveStatus(r) === "OK").length;
const err = results.length - ok;
const expected = !!resp.expected;
const matches = expected ? results.filter(r => r.matched === true && effectiveStatus(r) === "OK").length : 0;
const mismatches = expected ? results.filter(r => r.matched === false && effectiveStatus(r) === "OK").length : 0;
summaryEl.innerHTML = [
badge(`${results.length} resolvers`, "border-white/10 bg-white/5"),
badge(`${ok} OK`, "border-emerald-400/30 bg-emerald-400/10 text-emerald-200"),
badge(`${err} errors`, err ? "border-rose-400/30 bg-rose-400/10 text-rose-200" : "border-white/10 bg-white/5"),
expected ? badge(`${matches} matches`, "border-emerald-400/30 bg-emerald-400/10 text-emerald-200") : "",
expected ? badge(`${mismatches} mismatches`, mismatches ? "border-amber-400/30 bg-amber-400/10 text-amber-200" : "border-white/10 bg-white/5") : ""
].join("");
}
function escapeHtml(str) {
return String(str ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function renderRows(resp) {
lastResponse = resp;
renderSummary(resp);
const filter = $("filterText").value.toLowerCase();
const onlyErrors = $("onlyErrors").checked;
const onlyMismatches = $("onlyMismatches").checked && resp.expected;
const items = resp.results.filter(it => {
const st = effectiveStatus(it);
if (onlyErrors && st === "OK") return false;
if (onlyMismatches && it.matched !== false) return false;
if (!filter) return true;
return JSON.stringify(it).toLowerCase().includes(filter);
});
if (!items.length) {
rowsEl.innerHTML = `<tr><td colspan="7" class="px-4 py-4 text-slate-400">Ingen resultater</td></tr>`;
return;
}
rowsEl.innerHTML = items.map(it => {
const st = effectiveStatus(it);
const statusBadge =
st === "OK" ? badge("OK", "border-emerald-400/30 bg-emerald-400/10 text-emerald-200") :
st === "NO RESULT" ? badge("NO RESULT", "border-rose-400/30 bg-rose-400/10 text-rose-200") :
badge("ERROR", "border-rose-400/30 bg-rose-400/10 text-rose-200");
const {
ttl,
data
} = summarizeData(it.answers);
const match =
resp.expected ?
it.matched === true ? badge("match", "border-emerald-400/30 bg-emerald-400/10 text-emerald-200") :
it.matched === false ? badge("no", "border-amber-400/30 bg-amber-400/10 text-amber-200") :
"" :
"";
const flag = flagFromISO2(it.countryCode);
const country = it.country || "";
const city = it.city ? ` · ${it.city}` : "";
return `
<tr class="border-b border-white/5 hover:bg-white/5">
<td class="px-4 font-semibold"> <div class="font-semibold text-slate-100">
${flag ? flag + " " : ""}${escapeHtml(country)} — ${escapeHtml(it.resolver)}
</div>
<div class="text-xs text-slate-400">${escapeHtml(it.city || "")}</div></td>
<td class="px-4">${statusBadge}</td>
<td class="px-4">${it.timeMs ?? "—"}ms</td>
<td class="px-4">${ttl}</td>
<td class="px-4 mono max-w-[520px]">${data}</td>
<td class="px-4">${match}</td>
<td class="px-4">
<details>
<summary class="cursor-pointer text-xs underline">vis</summary>
<pre class="mono text-xs mt-2 max-h-56 overflow-auto">${JSON.stringify(it, null, 2)}</pre>
</details>
</td>
</tr>
`;
}).join("");
}
/* ---------- events ---------- */
$("form").addEventListener("submit", async e => {
e.preventDefault();
rowsEl.innerHTML = `<tr><td colspan="7" class="px-4 py-4">⏳ Running</td></tr>`;
const url = new URL("./api.php", location.href);
url.searchParams.set("domain", $("domain").value);
url.searchParams.set("type", $("type").value);
if ($("expected").value.trim()) url.searchParams.set("expected", $("expected").value.trim());
const r = await fetch(url);
const data = await r.json();
renderRows(data);
});
["filterText", "onlyErrors", "onlyMismatches"].forEach(id =>
$(id).addEventListener("input", () => lastResponse && renderRows(lastResponse))
);
</script>
</body>
</html>