Initial commit

This commit is contained in:
2026-05-28 07:21:47 +00:00
commit 4decb6ec51
9 changed files with 2731 additions and 0 deletions
+306
View File
@@ -0,0 +1,306 @@
const form = document.getElementById("dnsForm");
const domainInput = document.getElementById("domainInput");
const recordDropdown = document.getElementById("recordDropdown");
const recordTrigger = document.getElementById("recordTrigger");
const recordMenu = document.getElementById("recordMenu");
const selectedRecord = document.getElementById("selectedRecord");
const selectedRecordDesc = document.getElementById("selectedRecordDesc");
const checkButton = document.getElementById("checkButton");
const errorBox = document.getElementById("errorBox");
const resultsContainer = document.getElementById("resultsContainer");
const userIpText = document.getElementById("userIpText");
let selectedRecordType = "A";
function showError(message) {
errorBox.style.display = "block";
errorBox.textContent = message;
}
function hideError() {
errorBox.style.display = "none";
errorBox.textContent = "";
}
function formatDnsError(error) {
if (!error) {
return "DNS lookup failed.";
}
if (
error === "ESERVFAIL" ||
error === "SERVFAIL" ||
error.includes("server failed")
) {
return "Destination DNS server did not respond with a valid answer.";
}
if (error.includes("No") && error.includes("record found")) {
return error;
}
if (error.includes("Domain not found")) {
return "Domain was not found.";
}
if (error.includes("timed out") || error.includes("TIMEOUT")) {
return "DNS lookup timed out.";
}
return error;
}
function isPrivateIp(ip) {
if (typeof ip !== "string") return false;
const parts = ip.split(".").map(Number);
if (parts.length !== 4 || parts.some(part => Number.isNaN(part))) {
return false;
}
const a = parts[0];
const b = parts[1];
return (
a === 10 ||
a === 127 ||
(a === 172 && b >= 16 && b <= 31) ||
(a === 192 && b === 168) ||
(a === 169 && b === 254) ||
(a === 100 && b >= 64 && b <= 127)
);
}
async function copyToClipboard(text, button) {
const oldText = button.textContent;
function showCopied() {
button.textContent = "Copied";
button.classList.add("copied");
setTimeout(() => {
button.textContent = oldText;
button.classList.remove("copied");
}, 1200);
}
function showFailed() {
button.textContent = "Failed";
setTimeout(() => {
button.textContent = oldText;
}, 1200);
}
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
showCopied();
return;
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.top = "-9999px";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const successful = document.execCommand("copy");
document.body.removeChild(textarea);
if (successful) {
showCopied();
} else {
showFailed();
}
} catch (err) {
showFailed();
}
}
function createAnswersHtml(result) {
if (result.status !== "success") {
return `<div class="empty">${formatDnsError(result.error)}</div>`;
}
if (!result.answers || result.answers.length === 0) {
return '<div class="empty">No records found.</div>';
}
return result.answers
.map(answer => {
const value =
typeof answer === "object" ? JSON.stringify(answer) : String(answer);
const warning = isPrivateIp(value)
? `<div class="answer-warning">Private / internal IP detected. This domain may be redirected by the local resolver or network.</div>`
: "";
return `
<div class="answer">
<div class="answer-main">
<div class="answer-value">${escapeHtml(value)}</div>
<button type="button" class="copy-button" data-copy="${escapeHtml(value)}">Copy</button>
</div>
${warning}
</div>
`;
})
.join("");
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function createResultCard(item, query) {
const dcText = item.location.datacenter ? ` — ${item.location.datacenter}` : "";
const locationText = `${item.location.country} / ${item.location.city}${dcText}`;
const isSuccess = item.result.status === "success";
return `
<section class="card dns-result-card">
<div class="result-header">
<div>
<div class="location">${escapeHtml(locationText)}</div>
<div class="meta">${escapeHtml(query.domain)} · ${escapeHtml(query.type)}</div>
<div class="source-badge">${escapeHtml(item.location.sourceLabel || item.location.sourceType || "DNS check")}</div>
</div>
<div class="status ${isSuccess ? "success" : "error"}">${isSuccess ? "success" : "error"}</div>
</div>
<div class="answers">
${createAnswersHtml(item.result)}
</div>
<div class="stats">
<div class="stat">Response time: ${item.gatewayTimeMs ?? "-"} ms</div>
</div>
</section>
`;
}
function renderResults(results, query) {
resultsContainer.innerHTML = "";
const ownedResults = results.filter(item => item.location.sourceType === "owned");
const externalResults = results.filter(item => item.location.sourceType === "external");
if (ownedResults.length > 0) {
resultsContainer.insertAdjacentHTML(
"beforeend",
`<div class="result-group-title">Our DNS checks</div>`
);
ownedResults.forEach(item => {
resultsContainer.insertAdjacentHTML("beforeend", createResultCard(item, query));
});
}
if (externalResults.length > 0) {
resultsContainer.insertAdjacentHTML(
"beforeend",
`<div class="result-group-title">External DNS checks</div>`
);
externalResults.forEach(item => {
resultsContainer.insertAdjacentHTML("beforeend", createResultCard(item, query));
});
}
resultsContainer.querySelectorAll(".copy-button").forEach(button => {
button.addEventListener("click", () => {
copyToClipboard(button.dataset.copy, button);
});
});
}
recordTrigger.addEventListener("click", () => {
recordDropdown.classList.toggle("open");
});
recordMenu.querySelectorAll(".select-option").forEach(option => {
option.addEventListener("click", () => {
selectedRecordType = option.dataset.value;
selectedRecord.textContent = option.dataset.value;
selectedRecordDesc.textContent = option.dataset.desc;
recordMenu.querySelectorAll(".select-option").forEach(item => {
item.classList.remove("active");
});
option.classList.add("active");
recordDropdown.classList.remove("open");
});
});
async function loadUserIp() {
if (!userIpText) return;
try {
const response = await fetch("/api/client-ip");
const data = await response.json();
userIpText.textContent = `Your IP: ${data.ip || "Unknown"}`;
} catch (err) {
userIpText.textContent = "Your IP: unavailable";
}
}
loadUserIp();
document.addEventListener("click", event => {
if (!recordDropdown.contains(event.target)) {
recordDropdown.classList.remove("open");
}
});
form.addEventListener("submit", async event => {
event.preventDefault();
hideError();
recordDropdown.classList.remove("open");
const domain = domainInput.value.trim().toLowerCase();
const type = selectedRecordType;
if (!domain) {
showError("Please enter a domain.");
return;
}
checkButton.disabled = true;
checkButton.textContent = "Checking...";
try {
const response = await fetch(
`/api/check?domain=${encodeURIComponent(domain)}&type=${encodeURIComponent(type)}`
);
const data = await response.json();
if (!response.ok || data.status !== "success") {
showError(data.error || "Request failed.");
return;
}
renderResults(data.results, data.query);
} catch (err) {
showError(err.message || "Network error.");
} finally {
checkButton.disabled = false;
checkButton.textContent = "Check DNS";
}
});
+84
View File
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NetworkMaster</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<main class="page">
<div class="container">
<section class="hero">
<div class="badge">DNS propagation checker</div>
<h1>Check DNS records with confidence</h1>
<p class="subtitle">
See how your domain resolves from our own DNS node and trusted external providers.
</p>
</section>
<section class="card search-card">
<form class="form" id="dnsForm">
<input id="domainInput" type="text" placeholder="example.com" autocomplete="off" />
<div class="custom-select" id="recordDropdown">
<button class="select-trigger" type="button" id="recordTrigger">
<span>
<strong id="selectedRecord">A</strong>
<small id="selectedRecordDesc">IPv4 address</small>
</span>
</button>
<div class="select-menu" id="recordMenu">
<button type="button" class="select-option active" data-value="A" data-desc="IPv4 address">
<strong>A</strong>
<small>IPv4 address</small>
</button>
<button type="button" class="select-option" data-value="AAAA" data-desc="IPv6 address">
<strong>AAAA</strong>
<small>IPv6 address</small>
</button>
<button type="button" class="select-option" data-value="NS" data-desc="Name servers">
<strong>NS</strong>
<small>Name servers</small>
</button>
<button type="button" class="select-option" data-value="CNAME" data-desc="Alias record">
<strong>CNAME</strong>
<small>Alias record</small>
</button>
<button type="button" class="select-option" data-value="MX" data-desc="Mail servers">
<strong>MX</strong>
<small>Mail servers</small>
</button>
</div>
</div>
<button id="checkButton" type="submit">Check DNS</button>
</form>
<div class="info-row">
<div class="meta">
Current location: Iran / Tehran
</div>
<div class="meta user-ip" id="userIpText">
Your IP: loading...
</div>
</div>
<div class="error-box" id="errorBox"></div>
</section>
<div class="results-container" id="resultsContainer"></div>
</div>
</main>
<script src="/app.js"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

+518
View File
@@ -0,0 +1,518 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
background:
radial-gradient(circle at top left, rgba(79, 70, 229, 0.16), transparent 32%),
linear-gradient(180deg, #eef4ff 0%, #f8fbff 100%);
color: var(--text);
}
:root {
--bg: #eef4ff;
--card: #ffffff;
--text: #0f172a;
--muted: #64748b;
--primary: #4f46e5;
--primary-hover: #4338ca;
--primary-soft: #eef2ff;
--border: #dbe4f0;
--input-bg: #ffffff;
--success-bg: #dcfce7;
--success-text: #166534;
--error-bg: #fee2e2;
--error-text: #991b1b;
--warning-bg: #fff7ed;
--warning-border: #fed7aa;
--warning-text: #9a3412;
--answer-bg: #f8fafc;
}
body {
margin: 0;
background:
radial-gradient(circle at top left, rgba(79, 70, 229, 0.16), transparent 32%),
linear-gradient(180deg, #eef4ff 0%, #f8fbff 100%);
color: var(--text);
}
.page {
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 980px;
margin: 0 auto;
}
.hero {
text-align: center;
margin-bottom: 32px;
}
.badge {
display: inline-block;
background: var(--primary-soft);
color: var(--primary);
padding: 8px 14px;
border-radius: 999px;
font-size: 14px;
font-weight: 700;
margin-bottom: 16px;
border: 1px solid rgba(79, 70, 229, 0.14);
}
h1 {
font-size: 42px;
margin: 0 0 12px;
letter-spacing: -0.035em;
font-weight: 800;
line-height: 1.1;
}
.subtitle {
color: var(--muted);
font-size: 17px;
margin: 0;
font-weight: 500;
line-height: 1.7;
}
.card {
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(219, 228, 240, 0.85);
border-radius: 24px;
box-shadow: 0 20px 55px rgba(15, 23, 42, 0.08);
padding: 24px;
margin-bottom: 24px;
backdrop-filter: blur(10px);
}
.search-card {
position: relative;
z-index: 100;
}
.form {
display: grid;
grid-template-columns: 1fr 220px 150px;
gap: 12px;
align-items: start;
}
input {
height: 52px;
border-radius: 14px;
border: 1px solid var(--border);
background: var(--input-bg);
font-size: 16px;
padding: 0 16px;
outline: none;
width: 100%;
color: var(--text);
}
input::placeholder {
color: #94a3b8;
}
input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.12);
}
button {
font-family: Arial, sans-serif;
}
#checkButton {
height: 52px;
border-radius: 14px;
border: none;
background: linear-gradient(135deg, var(--primary), #2563eb);
color: white;
cursor: pointer;
font-weight: 700;
font-size: 16px;
padding: 0 16px;
width: 100%;
box-shadow: 0 12px 24px rgba(79, 70, 229, 0.25);
}
#checkButton:hover {
background: linear-gradient(135deg, var(--primary-hover), #1d4ed8);
}
#checkButton:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.custom-select {
position: relative;
height: 52px;
z-index: 50;
}
.select-trigger {
width: 100%;
height: 52px;
background: var(--input-bg);
color: var(--text);
border: 1px solid var(--border);
border-radius: 14px;
padding: 0 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: flex-start;
text-align: left;
}
.select-trigger:hover {
background: #f8faff;
border-color: #b9c7f8;
}
.select-trigger span:first-child {
display: flex;
flex-direction: column;
gap: 3px;
}
.select-trigger strong {
font-size: 15px;
line-height: 1.1;
color: var(--text);
}
.select-trigger small {
font-size: 12px;
line-height: 1.2;
color: var(--muted);
}
.select-menu {
display: none;
position: absolute;
top: 60px;
left: 0;
right: 0;
z-index: 999;
background: #ffffff;
border: 1px solid var(--border);
border-radius: 16px;
padding: 8px;
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.16);
}
.custom-select.open .select-menu {
display: grid;
gap: 6px;
}
.select-option {
width: 100%;
min-height: 54px;
height: auto;
background: transparent;
color: var(--text);
border: none;
border-radius: 12px;
padding: 10px 12px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 3px;
cursor: pointer;
text-align: left;
font-weight: 400;
}
.select-option:hover {
background: #f4f7ff;
}
.select-option.active {
background: var(--primary-soft);
}
.select-option strong {
font-size: 15px;
color: var(--text);
}
.select-option small {
font-size: 12px;
color: var(--muted);
}
.select-option.active strong,
.select-option.active small {
color: var(--primary);
}
.meta {
margin-top: 16px;
color: var(--muted);
font-size: 14px;
}
.result-card {
display: none;
position: relative;
z-index: 1;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 18px;
}
.location {
font-size: 18px;
font-weight: 700;
}
.status {
padding: 7px 12px;
border-radius: 999px;
font-size: 14px;
font-weight: 700;
}
.status.success {
background: var(--success-bg);
color: var(--success-text);
}
.status.error {
background: var(--error-bg);
color: var(--error-text);
}
.answers {
display: grid;
gap: 10px;
}
.answer {
background: var(--answer-bg);
border: 1px solid var(--border);
border-radius: 14px;
padding: 14px 16px;
word-break: break-word;
}
.answer-main {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.answer-value {
font-family: Consolas, monospace;
word-break: break-word;
}
.copy-button {
border: 1px solid var(--border);
background: #ffffff;
color: var(--primary);
border-radius: 10px;
padding: 7px 10px;
cursor: pointer;
font-size: 13px;
font-weight: 700;
flex-shrink: 0;
}
.copy-button:hover {
background: var(--primary-soft);
}
.copy-button.copied {
background: var(--success-bg);
color: var(--success-text);
border-color: var(--success-bg);
}
.answer-warning {
margin-top: 8px;
background: var(--warning-bg);
border: 1px solid var(--warning-border);
color: var(--warning-text);
border-radius: 12px;
padding: 10px 12px;
font-size: 13px;
line-height: 1.5;
}
.empty {
color: var(--muted);
background: var(--answer-bg);
border-radius: 14px;
padding: 16px;
}
.stats {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 18px;
}
.stat {
background: #f1f5f9;
border-radius: 12px;
padding: 10px 12px;
color: #334155;
font-size: 14px;
}
.error-box {
display: none;
background: var(--error-bg);
color: var(--error-text);
border: 1px solid #fecaca;
border-radius: 16px;
padding: 14px 16px;
margin-top: 16px;
}
@media (max-width: 720px) {
h1 {
font-size: 32px;
}
.form {
grid-template-columns: 1fr;
}
.result-header {
align-items: flex-start;
flex-direction: column;
}
.answer-main {
align-items: flex-start;
flex-direction: column;
}
.copy-button {
width: 100%;
}
}
.results-container {
display: grid;
gap: 18px;
}
.result-group-title {
color: var(--muted);
font-size: 14px;
font-weight: 700;
margin: 8px 0 -6px;
}
.dns-result-card {
position: relative;
z-index: 1;
}
.source-badge {
display: inline-block;
margin-top: 8px;
padding: 5px 9px;
border-radius: 999px;
background: var(--primary-soft);
color: var(--primary);
font-size: 12px;
font-weight: 700;
}
.search-card {
position: relative;
z-index: 1000;
}
.results-container {
display: grid;
gap: 18px;
margin-top: 24px;
position: relative;
z-index: 1;
}
.custom-select {
position: relative;
z-index: 2000;
}
.select-menu {
z-index: 3000;
}
.result-group-title {
color: var(--muted);
font-size: 14px;
font-weight: 700;
margin: 8px 0 -6px;
}
.dns-result-card {
position: relative;
z-index: 1;
}
.source-badge {
display: inline-block;
margin-top: 8px;
padding: 5px 9px;
border-radius: 999px;
background: var(--primary-soft);
color: var(--primary);
font-size: 12px;
font-weight: 700;
}
.info-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
}
.info-row .meta {
margin-top: 0;
}
.user-ip {
direction: ltr;
text-align: right;
font-family: Consolas, monospace;
}
@media (max-width: 720px) {
.info-row {
align-items: flex-start;
flex-direction: column; }
.user-ip {
text-align: left;
}
}