Files
ferrix/slides/dns-lookup.js
T

106 lines
5.0 KiB
JavaScript

// Live lookups on slides.
//
// <form class="dns-lookup"> with <input name="domain"> and a .dns-result:
// asks Google Public DNS over HTTPS for the name's AAAA records
// (https://developers.google.com/speed/public-dns/docs/doh/json) and
// prints the answer the way dig would.
// <form class="ripe-lookup"> with <input name="query"> and a .dns-result:
// asks the RIPE database (https://apps.db.ripe.net/docs/) what it knows
// about an address, prefix or AS number, and prints the objects the way
// whois would.
(function () {
const TYPES = { 1: "A", 5: "CNAME", 28: "AAAA" };
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
async function resolve(name, type) {
const r = await fetch(`https://dns.google/resolve?name=${encodeURIComponent(name)}&type=${type}`, {
headers: { accept: "application/dns-json" },
});
if (!r.ok) throw new Error(`dns.google answered HTTP ${r.status}`);
return r.json();
}
const line = (rr) =>
`<span class="name">${esc(rr.name)}</span> <span class="ttl">${rr.TTL}</span> IN <span class="type">${esc(TYPES[rr.type] || rr.type)}</span> <span class="${rr.type === 28 ? "addr" : "data"}">${esc(rr.data)}</span>`;
async function lookup(form) {
const out = form.querySelector(".dns-result");
const name = form.elements.domain.value.trim().replace(/\.$/, "");
if (!name) return;
out.innerHTML = `<span class="dim">asking dns.google for ${esc(name)} AAAA ...</span>`;
try {
const v6 = await resolve(name, "AAAA");
if (v6.Status === 3) {
out.innerHTML = `<span class="bad">NXDOMAIN</span> <span class="dim">there is no such name</span>`;
return;
}
if (v6.Status !== 0) {
out.innerHTML = `<span class="bad">the resolver answered with status ${v6.Status}</span>`;
return;
}
const answers = v6.Answer || [];
const lines = answers.map(line);
if (!answers.some((rr) => rr.type === 28)) {
// No IPv6 address. Show that this is a real answer, not a failure,
// and what the name does have.
const v4 = await resolve(name, "A");
lines.push(`<span class="dim">no AAAA record: this name has no IPv6 address</span>`);
(v4.Answer || []).filter((rr) => rr.type === 1).forEach((rr) => lines.push(line(rr)));
}
out.innerHTML = lines.join("\n");
} catch (e) {
out.innerHTML = `<span class="bad">lookup failed:</span> ${esc(e.message)} <span class="dim">(is there a network?)</span>`;
}
}
// ---- RIPE database ----------------------------------------------------
// Objects as apps.db.ripe.net shows them: one card per object, one row per
// attribute, references in blue, rows the RIPE NCC manages shaded. The
// API does not say which rows those are, so they are the ones the website
// shades for an assignment: the key, org, status, source, sponsoring-org
// and the RIPE NCC's own maintainers.
const RIPE_MANAGED = new Set(["inetnum", "inet6num", "aut-num", "org", "status", "source", "sponsoring-org"]);
const managed = (a) => RIPE_MANAGED.has(a.name) || (a.name === "mnt-by" && /^RIPE-NCC-/.test(a.value));
async function ripe(form) {
const out = form.querySelector(".ripe-result");
const query = form.elements.query.value.trim();
if (!query) return;
out.innerHTML = `<div class="ripe-msg">Searching the RIPE database for ${esc(query)} ...</div>`;
try {
const url = `https://rest.db.ripe.net/search.json?query-string=${encodeURIComponent(query)}&flags=no-referenced&flags=no-irt&source=ripe`;
const r = await fetch(url, { headers: { accept: "application/json" } });
if (r.status === 404) {
out.innerHTML = `<div class="ripe-msg">No results found for ${esc(query)}.</div>`;
return;
}
if (!r.ok) throw new Error(`rest.db.ripe.net answered HTTP ${r.status}`);
const objects = ((await r.json()).objects || {}).object || [];
out.innerHTML = objects
.map((o) => {
const rows = o.attributes.attribute.map((a) => {
const value = a.link || a.name === o.type ? `<span class="ref">${esc(a.value)}</span>` : esc(a.value);
return `<tr class="${managed(a) ? "managed" : ""}"><th>${esc(a.name)}:</th><td>${value}${a.comment ? `<span class="comment"># ${esc(a.comment)}</span>` : ""}</td></tr>`;
});
return `<div class="ripe-card"><table>${rows.join("")}</table></div>`;
})
.join("");
out.scrollTop = 0;
} catch (e) {
out.innerHTML = `<div class="ripe-msg">Lookup failed: ${esc(e.message)}. Is there a network?</div>`;
}
}
// The answer changes the slide's height; have reveal centre it again.
const relayout = () => window.deck && window.deck.layout();
document.addEventListener("submit", async (e) => {
const form = e.target.closest && e.target.closest("form.dns-lookup, form.ripe-lookup");
if (!form) return;
e.preventDefault();
await (form.classList.contains("ripe-lookup") ? ripe(form) : lookup(form));
relayout();
});
})();