As presented. The palette is light on request of the reviewer; code blocks and byte dumps keep a dark ground so the byte colours still work. The OPEN walk lights the hovered bytes as an inverted block and its table cells are dark text on a tint of their group colour. build.py inlines reveal.js, the stylesheets, scripts and Markdown sections into slides/dist/hands-on-bgp.html for uploading. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
141 lines
6.5 KiB
JavaScript
141 lines
6.5 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) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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();
|
|
});
|
|
})();
|
|
|
|
// Byte walks: hovering (or tabbing to) a field row lights its bytes in the
|
|
// dump above it, and hovering bytes in the dump lights their row and scrolls
|
|
// it into view, so the dump can be used as the index while presenting.
|
|
(function () {
|
|
const pair = (el) => {
|
|
const walk = el.closest(".bytewalk");
|
|
if (!walk) return [];
|
|
const i = el.dataset.i;
|
|
return [walk.querySelector(`.bw-dump span[data-i="${i}"]`), walk.querySelector(`.bw-row[data-i="${i}"]`)].filter(Boolean);
|
|
};
|
|
const target = (e) => e.target.closest && e.target.closest(".bw-row, .bw-dump span[data-i]");
|
|
const on = (e) => {
|
|
const el = target(e);
|
|
if (!el) return;
|
|
const both = pair(el);
|
|
both.forEach((x) => x.classList.add("on"));
|
|
if (el.matches(".bw-dump span")) {
|
|
// Scroll the box itself, and instantly: scrollIntoView would also
|
|
// move the slide, and Chrome drops smooth scrolls inside the
|
|
// transformed slide that reveal positions.
|
|
const row = both.find((x) => x.classList.contains("bw-row"));
|
|
const box = row && row.closest(".bw-rows");
|
|
if (box) box.scrollTop = row.offsetTop - box.clientHeight / 2 + row.offsetHeight / 2;
|
|
}
|
|
};
|
|
const off = (e) => {
|
|
const el = target(e);
|
|
if (el) pair(el).forEach((x) => x.classList.remove("on"));
|
|
};
|
|
document.addEventListener("mouseover", on);
|
|
document.addEventListener("focusin", on);
|
|
document.addEventListener("mouseout", off);
|
|
document.addEventListener("focusout", off);
|
|
})();
|