Files
ferrix/crates/web/static/statusz.html
T
Rayhaan JaufeerallyandClaude Fable 5.1 a137160b48 feature: lab topology, student handout; drop peer routes on session end
Phase 5 of the teaching demo plus two fixes found while exercising the
dialer on the status page.

- Adj-RIB-In: routes learned from a peer are removed when its session ends
  (RFC 4271 §8.2.2), emitting a Withdrawn event per prefix before the
  session Down event. Previously stale routes from dead sessions stayed in
  the received table.
- statusz: poll the peers table every 5 s so dial state (Connecting /
  Backoff and its retry countdown) stays current; those changes carry no
  event.
- docs/lab: docker-compose topology with ferrix (AS65000) and two FRR
  routers. r2 accepts nothing directly from ferrix so student prefixes
  only reach it via r1, then come back with AS path 65002 65001 65000. r1
  applies community-driven policy: NO_EXPORT stops forwarding, 65001:100
  prepends, BLACKHOLE drops. Dockerfile for the daemon.
- docs/lab/HANDOUT.md: student exercises walking through announcement,
  reading the bytes, prepending, NO_EXPORT, action communities,
  blackholing and withdrawal.

The container files have not been executed (no runtime available here);
docs/lab/README.md lists what is most likely to need adjusting.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 23:26:10 +02:00

331 lines
22 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ferrix_bgp /statusz</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='7' fill='%231a73e8'/%3E%3Ctext x='8' y='12' font-size='10' font-family='monospace' fill='white' text-anchor='middle'%3EB%3C/text%3E%3C/svg%3E">
<style>
:root {
--bg: #ffffff; --fg: #202124; --muted: #5f6368; --line: #dadce0;
--head: #f1f3f4; --accent: #1a73e8; --ok: #188038; --warn: #b06000; --err: #c5221f;
--hi: #fef7e0; --mono: "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
--hex-hdr:#e8f0fe; --hex-len:#fce8e6; --hex-attr:#e6f4ea; --hex-nlri:#fef7e0;
}
* { box-sizing: border-box; }
body { margin: 0; font: 13px/1.45 var(--mono); color: var(--fg); background: var(--bg); }
a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; }
header { padding: 8px 16px; border-bottom: 1px solid var(--line); background: var(--head); display: flex; flex-wrap: wrap; gap: 4px 24px; align-items: baseline; }
header h1 { font-size: 15px; margin: 0 8px 0 0; font-weight: bold; }
header .kv { color: var(--muted); } header .kv b { color: var(--fg); font-weight: normal; }
nav { padding: 4px 16px; border-bottom: 1px solid var(--line); color: var(--muted); }
nav a { margin-right: 14px; }
main { padding: 0 16px 40px; max-width: 1400px; }
section { margin-top: 22px; }
h2 { font-size: 13px; margin: 0 0 6px; padding-bottom: 3px; border-bottom: 1px solid var(--line); text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
h2 .count { float: right; text-transform: none; letter-spacing: 0; }
table { border-collapse: collapse; width: 100%; }
th, td { text-align: left; padding: 3px 8px; border-bottom: 1px solid var(--line); vertical-align: top; white-space: nowrap; }
th { background: var(--head); font-weight: normal; color: var(--muted); }
td.wrap { white-space: normal; }
tr.mine { background: var(--hi); }
tr.wire td { background: #fafafa; white-space: normal; }
.empty { color: var(--muted); padding: 6px 8px; }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--err); margin-right: 4px; vertical-align: middle; }
.dot.on { background: var(--ok); }
.tag { display: inline-block; padding: 0 5px; border: 1px solid var(--line); border-radius: 3px; margin: 1px 2px 1px 0; background: #fff; }
.tag.wk { border-color: var(--warn); color: var(--warn); }
.muted { color: var(--muted); } .ok { color: var(--ok); } .err { color: var(--err); } .warn { color: var(--warn); }
form.announce { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 8px 12px; align-items: end; padding: 10px; border: 1px solid var(--line); background: #fafafa; }
form.announce label { display: flex; flex-direction: column; gap: 2px; color: var(--muted); }
form.announce .span2 { grid-column: span 2; }
input, select, button { font: inherit; padding: 3px 6px; border: 1px solid var(--line); border-radius: 3px; background: #fff; color: var(--fg); }
input:focus, select:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
button { cursor: pointer; } button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
button.small { padding: 0 6px; line-height: 18px; } button:disabled { opacity: .5; cursor: default; }
.row { display: flex; gap: 6px; align-items: center; }
#announce-msg { grid-column: 1 / -1; min-height: 1.4em; }
.hex { font-family: var(--mono); word-break: break-all; line-height: 1.7; }
.hex span { padding: 1px 0; } .hex .h { background: var(--hex-hdr); } .hex .l { background: var(--hex-len); } .hex .a { background: var(--hex-attr); } .hex .n { background: var(--hex-nlri); }
.hex .a:nth-of-type(odd) { filter: brightness(.96); }
.legend span { padding: 0 6px; margin-right: 8px; }
.wirebox { margin: 4px 0 8px; padding: 6px 8px; border-left: 3px solid var(--line); }
#event-log { max-height: 260px; overflow-y: auto; border: 1px solid var(--line); padding: 4px 8px; background: #fafafa; }
#event-log div { white-space: nowrap; } #event-log .t { color: var(--muted); margin-right: 8px; }
footer { padding: 10px 16px; color: var(--muted); border-top: 1px solid var(--line); margin-top: 30px; }
@media (prefers-color-scheme: dark) {
:root { --bg:#202124; --fg:#e8eaed; --muted:#9aa0a6; --line:#3c4043; --head:#292a2d; --accent:#8ab4f8; --ok:#81c995; --warn:#fdd663; --err:#f28b82; --hi:#3b3419;
--hex-hdr:#28344b; --hex-len:#4a2a28; --hex-attr:#26382b; --hex-nlri:#3b3419; }
input, select, button, .tag { background: #303134; color: var(--fg); }
tr.wire td, form.announce, #event-log { background: #292a2d; }
}
</style>
</head>
<body>
<header>
<h1>ferrix_bgp</h1>
<span class="kv">router-id <b id="s-router-id">…</b></span>
<span class="kv">AS <b id="s-asn">…</b></span>
<span class="kv">version <b id="s-version">…</b></span>
<span class="kv">uptime <b id="s-uptime">…</b></span>
<span class="kv">sessions <b id="s-sessions">…</b></span>
<span class="kv">local <b id="s-local">…</b></span>
<span class="kv">received <b id="s-received">…</b></span>
<span class="kv"><span class="dot" id="live-dot"></span><span id="live-text">connecting</span></span>
</header>
<nav>
<a href="#peers">peers</a><a href="#announce">announce</a><a href="#local">local routes</a><a href="#received">received routes</a><a href="#events">events</a>
<a href="/metricz">/metricz</a><a href="/api/status">/api/status</a>
</nav>
<main>
<section id="peers">
<h2>Peers <span class="count" id="peers-count"></span></h2>
<table>
<thead><tr><th>name</th><th>peer</th><th>ASN</th><th>state</th><th>local address (next hop)</th><th>established</th><th>uptime</th><th>4-byte ASN</th><th>detail</th></tr></thead>
<tbody id="peers-body"></tbody>
</table>
</section>
<section id="announce">
<h2>Announce a prefix</h2>
<form class="announce" id="announce-form" autocomplete="off">
<label>owner (your name)<input name="owner" id="f-owner" required maxlength="64" placeholder="alice"></label>
<label>prefix<input name="prefix" id="f-prefix" required placeholder="10.100.1.0/24"></label>
<label>origin<select name="origin" id="f-origin"><option>IGP</option><option>EGP</option><option>INCOMPLETE</option></select></label>
<label>AS-path prepends<input name="prepend" id="f-prepend" type="number" min="0" max="16" value="0"></label>
<label>MED (optional)<input name="med" id="f-med" type="number" min="0" max="4294967295" placeholder="none"></label>
<label class="span2">communities <span class="muted">(comma separated, ASN:VALUE or name)</span>
<div class="row"><input name="communities" id="f-communities" placeholder="65000:100, NO_EXPORT" style="flex:1">
<select id="f-wk"><option value="">+ well-known…</option></select></div>
</label>
<label class="span2">large communities <span class="muted">(GLOBAL:LOCAL1:LOCAL2)</span><input name="large_communities" id="f-large" placeholder="65000:1:2"></label>
<div class="row"><button type="submit" class="primary">Announce</button><button type="button" id="f-clear">Clear</button></div>
<div id="announce-msg"></div>
</form>
</section>
<section id="local">
<h2>Local routes <span class="count" id="local-count"></span></h2>
<table>
<thead><tr><th>prefix</th><th>owner</th><th>origin</th><th>AS path</th><th>MED</th><th>communities</th><th>large communities</th><th>age</th><th></th></tr></thead>
<tbody id="local-body"></tbody>
</table>
</section>
<section id="received">
<h2>Received routes (Adj-RIB-In) <span class="count" id="received-count"></span></h2>
<p class="muted" style="margin:0 0 6px">Rows highlighted in yellow carry a prefix that is also announced locally: a route that has travelled through the lab and come back.</p>
<table>
<thead><tr><th>prefix</th><th>from peer</th><th>AS path</th><th>next hop</th><th>origin</th><th>MED</th><th>communities</th><th>large communities</th><th>age</th></tr></thead>
<tbody id="received-body"></tbody>
</table>
</section>
<section id="events">
<h2>Event log</h2>
<div id="event-log"></div>
</section>
</main>
<footer>ferrix_bgp · Adj-RIB-In and local routes are live via <a href="/api/events">/api/events</a> · tables refresh on every event.</footer>
<script>
(() => {
const $ = id => document.getElementById(id);
const esc = s => String(s).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const state = { status: null, peers: [], local: [], received: [], wellKnown: {}, openWire: new Set() };
// ---- helpers -----------------------------------------------------------
const fmtDur = s => {
s = Math.max(0, Math.floor(s));
const d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60), sec = s % 60;
return d ? `${d}d${h}h` : h ? `${h}h${m}m` : m ? `${m}m${sec}s` : `${sec}s`;
};
const now = () => Math.floor(Date.now() / 1000);
const age = unix => fmtDur(now() - unix);
const fmtTime = unix => new Date(unix * 1000).toLocaleTimeString();
const tags = (list, wk) => list.length ? list.map(c => {
const name = wk && state.wellKnown[c];
return `<span class="tag${name ? ' wk' : ''}" title="${esc(c)}">${esc(name ? name : c)}</span>`;
}).join('') : '<span class="muted">—</span>';
const asPath = p => p.length ? p.join(' ') : '<span class="muted">(empty)</span>';
const myOwner = () => $('f-owner').value.trim();
async function getJSON(url) {
const r = await fetch(url);
if (!r.ok) throw new Error(`${url}: ${r.status}`);
return r.json();
}
// ---- renderers ---------------------------------------------------------
function renderStatus() {
const s = state.status; if (!s) return;
$('s-router-id').textContent = s.router_id; $('s-asn').textContent = s.local_asn;
$('s-version').textContent = s.version; $('s-uptime').textContent = fmtDur(s.uptime_secs + (now() - s._at));
$('s-sessions').textContent = state.peers.filter(p => p.state === 'Established').length; $('s-local').textContent = state.local.length;
$('s-received').textContent = state.received.length;
}
const STATE_CLASS = { Established: 'ok', Connecting: 'warn', Backoff: 'err', Idle: 'muted' };
function renderPeers() {
const b = $('peers-body'); $('peers-count').textContent = `${state.peers.filter(p => p.state === 'Established').length} up / ${state.peers.length}`;
if (!state.peers.length) { b.innerHTML = '<tr><td colspan="9" class="empty">No peers configured and none connected.</td></tr>'; return; }
b.innerHTML = state.peers.map(p => {
const up = p.state === 'Established';
const detail = p.state === 'Backoff' ? `${esc(p.last_error || '')}; retry in ${fmtDur(p.retry_at_unix - now())}` : '';
return `<tr>
<td>${esc(p.name ?? '')}${p.name ? '' : '<span class="muted">(inbound)</span>'}</td><td>${esc(p.peer_addr)}</td><td>${p.peer_asn}</td>
<td class="${STATE_CLASS[p.state] || ''}">${esc(p.state)}</td><td>${up ? esc(p.local_addr) : '—'}</td>
<td>${up ? fmtTime(p.established_at_unix) : '—'}</td><td>${up ? age(p.established_at_unix) : '—'}</td><td>${up ? (p.four_octet_asn ? 'yes' : 'no') : '—'}</td>
<td class="wrap muted">${detail}</td></tr>`; }).join('');
}
function renderLocal() {
const b = $('local-body'); $('local-count').textContent = state.local.length;
if (!state.local.length) { b.innerHTML = '<tr><td colspan="9" class="empty">Nothing announced yet.</td></tr>'; return; }
const me = myOwner(), asn = state.status ? state.status.local_asn : '?';
b.innerHTML = state.local.map(r => {
const mine = r.owner === me;
const path = Array(1 + r.prepend).fill(asn).join(' ');
const open = state.openWire.has(r.prefix);
return `<tr class="${mine ? 'mine' : ''}" data-prefix="${esc(r.prefix)}">
<td>${esc(r.prefix)}</td><td>${esc(r.owner)}</td><td>${esc(r.origin)}</td><td>${path}</td>
<td>${r.med ?? '<span class="muted">—</span>'}</td><td class="wrap">${tags(r.communities, true)}</td><td class="wrap">${tags(r.large_communities)}</td>
<td>${age(r.announced_at_unix)}</td>
<td><button class="small" data-act="wire" data-prefix="${esc(r.prefix)}">${open ? 'hide bytes' : 'bytes'}</button>
<button class="small" data-act="withdraw" data-prefix="${esc(r.prefix)}" ${mine ? '' : 'disabled title="only the owner can withdraw"'}>withdraw</button></td></tr>` +
(open ? `<tr class="wire"><td colspan="9" id="wire-${esc(r.prefix)}"><span class="muted">loading…</span></td></tr>` : '');
}).join('');
state.openWire.forEach(loadWire);
}
function renderReceived() {
const b = $('received-body'); $('received-count').textContent = state.received.length;
if (!state.received.length) { b.innerHTML = '<tr><td colspan="9" class="empty">No routes received from peers.</td></tr>'; return; }
const localPrefixes = new Set(state.local.map(r => r.prefix));
b.innerHTML = state.received.map(r => { const s = r.summary; return `<tr class="${localPrefixes.has(r.prefix) ? 'mine' : ''}">
<td>${esc(r.prefix)}</td><td>${esc(r.peer_addr)} <span class="muted">AS${r.peer_asn}</span></td><td>${asPath(s.as_path)}</td>
<td>${esc(s.next_hop ?? '—')}</td><td>${esc(s.origin ?? '—')}</td><td>${s.med ?? '<span class="muted">—</span>'}</td>
<td class="wrap">${tags(s.communities, true)}</td><td class="wrap">${tags(s.large_communities)}</td><td>${age(r.received_at_unix)}</td></tr>`; }).join('');
}
// ---- wire view: split the UPDATE bytes into labelled fields ------------
const ATTR_NAMES = {1:'ORIGIN',2:'AS_PATH',3:'NEXT_HOP',4:'MED',5:'LOCAL_PREF',6:'ATOMIC_AGGREGATE',7:'AGGREGATOR',8:'COMMUNITIES',14:'MP_REACH_NLRI',15:'MP_UNREACH_NLRI',16:'EXT_COMMUNITIES',32:'LARGE_COMMUNITIES'};
function segmentUpdate(hex) {
const bytes = hex.match(/../g) || [], segs = [];
const take = (n, cls, label) => { segs.push({ cls, label, hex: bytes.splice(0, n).join(' ') }); };
take(16, 'h', 'marker'); take(2, 'h', 'length'); take(1, 'h', 'type=UPDATE');
const wlen = parseInt(hex.substr(38, 4), 16); take(2, 'l', `withdrawn len=${wlen}`); if (wlen) take(wlen, 'n', 'withdrawn routes');
const alen = parseInt(bytes.slice(0, 2).join(''), 16); take(2, 'l', `attr len=${alen}`);
let rem = alen;
while (rem > 0) {
const flags = parseInt(bytes[0], 16), type = parseInt(bytes[1], 16), ext = flags & 0x10;
const hdr = ext ? 4 : 3, len = ext ? parseInt(bytes[2] + bytes[3], 16) : parseInt(bytes[2], 16);
take(hdr + len, 'a', `${ATTR_NAMES[type] || 'attr ' + type} (flags 0x${flags.toString(16)}, len ${len})`); rem -= hdr + len;
}
if (bytes.length) take(bytes.length, 'n', 'NLRI');
return segs;
}
async function loadWire(prefix) {
const cell = $(`wire-${prefix}`); if (!cell) return;
try {
const views = await getJSON(`/api/routes/local/wire?prefix=${encodeURIComponent(prefix)}`);
if (!views.length) { cell.innerHTML = '<span class="muted">No established session to send this on.</span>'; return; }
cell.innerHTML = `<div class="legend muted">UPDATE as sent: <span class="h">header</span><span class="l">lengths</span><span class="a">path attributes</span><span class="n">NLRI</span></div>` +
views.map(v => `<div class="wirebox"><div>to <b>${esc(v.peer_addr)}</b> AS${v.peer_asn} · ${v.length} bytes · next hop ${esc(v.next_hop)} · AS path ${asPath(v.summary.as_path)}</div>
<div class="hex">${segmentUpdate(v.hex).map(s => `<span class="${s.cls}" title="${esc(s.label)}">${s.hex}</span>`).join(' ')}</div></div>`).join('');
} catch (e) { cell.innerHTML = `<span class="err">${esc(e.message)}</span>`; }
}
// ---- data loading ------------------------------------------------------
async function loadStatus() { state.status = await getJSON('/api/status'); state.status._at = now(); renderStatus(); }
async function loadPeers() { state.peers = await getJSON('/api/peers'); renderPeers(); renderStatus(); }
async function loadLocal() { state.local = await getJSON('/api/routes/local'); renderLocal(); renderReceived(); renderStatus(); }
async function loadReceived() { state.received = await getJSON('/api/routes/received'); renderReceived(); renderStatus(); }
async function loadAll() { await Promise.all([loadStatus(), loadPeers(), loadLocal(), loadReceived()]); }
// ---- events ------------------------------------------------------------
function logEvent(text, cls) {
const box = $('event-log'), d = document.createElement('div');
d.innerHTML = `<span class="t">${new Date().toLocaleTimeString()}</span><span class="${cls || ''}">${text}</span>`;
box.prepend(d); while (box.children.length > 200) box.lastChild.remove();
}
function describe(ev) {
switch (ev.type) {
case 'session_up': return [`session UP with ${esc(ev.peer.name || ev.peer.peer_addr)} AS${ev.peer.peer_asn}`, 'ok'];
case 'session_down': return [`session DOWN with ${esc(ev.peer_addr)} AS${ev.peer_asn}`, 'err'];
case 'local_announced': return [`announced ${esc(ev.route.prefix)} by ${esc(ev.route.owner)} [${ev.route.communities.map(c => state.wellKnown[c] || c).map(esc).join(' ')}]`, ''];
case 'local_withdrawn': return [`withdrew ${esc(ev.prefix)}`, 'warn'];
case 'received_announced': return [`received ${esc(ev.route.prefix)} from AS${ev.route.peer_asn} path [${ev.route.summary.as_path.join(' ')}]`, ''];
case 'received_withdrawn': return [`peer AS${ev.peer_asn} withdrew ${esc(ev.prefix)}`, 'warn'];
case 'lagged': return [`missed ${ev.missed} events on ${ev.stream}; reloading`, 'err'];
default: return [esc(JSON.stringify(ev)), ''];
}
}
function connect() {
const es = new EventSource('/api/events');
es.onopen = () => { $('live-dot').classList.add('on'); $('live-text').textContent = 'live'; loadAll().catch(console.error); };
es.onerror = () => { $('live-dot').classList.remove('on'); $('live-text').textContent = 'reconnecting'; };
es.addEventListener('bgp', e => {
const ev = JSON.parse(e.data); const [text, cls] = describe(ev); logEvent(text, cls);
const t = ev.type;
if (t.startsWith('session')) loadPeers().catch(console.error);
else if (t.startsWith('local')) loadLocal().catch(console.error);
else if (t.startsWith('received')) loadReceived().catch(console.error);
else loadAll().catch(console.error);
});
}
// ---- form --------------------------------------------------------------
const splitList = s => s.split(',').map(x => x.trim()).filter(Boolean);
function setMsg(text, cls) { const m = $('announce-msg'); m.className = cls || ''; m.textContent = text; }
$('announce-form').addEventListener('submit', async e => {
e.preventDefault();
const body = {
prefix: $('f-prefix').value.trim(), owner: myOwner(), origin: $('f-origin').value,
prepend: Number($('f-prepend').value || 0), communities: splitList($('f-communities').value),
large_communities: splitList($('f-large').value),
};
if ($('f-med').value !== '') body.med = Number($('f-med').value);
try { localStorage.setItem('owner', body.owner); } catch (_) {}
setMsg('announcing…', 'muted');
const r = await fetch('/api/routes/local', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
const j = await r.json().catch(() => ({}));
if (r.ok) { setMsg(`announced ${j.prefix} to ${state.peers.length} peer(s)`, 'ok'); loadLocal().catch(console.error); }
else setMsg(`${r.status}: ${j.error || 'request failed'}`, 'err');
});
$('f-clear').addEventListener('click', () => { ['f-prefix','f-communities','f-large','f-med'].forEach(id => $(id).value = ''); $('f-prepend').value = 0; $('f-origin').value = 'IGP'; setMsg(''); });
$('f-wk').addEventListener('change', e => {
if (!e.target.value) return;
const cur = splitList($('f-communities').value); if (!cur.includes(e.target.value)) cur.push(e.target.value);
$('f-communities').value = cur.join(', '); e.target.value = '';
});
$('f-owner').addEventListener('input', renderLocal);
$('local-body').addEventListener('click', async e => {
const btn = e.target.closest('button[data-act]'); if (!btn) return;
const prefix = btn.dataset.prefix;
if (btn.dataset.act === 'wire') { state.openWire.has(prefix) ? state.openWire.delete(prefix) : state.openWire.add(prefix); renderLocal(); return; }
if (!confirm(`Withdraw ${prefix}?`)) return;
const r = await fetch('/api/routes/local', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ prefix, owner: myOwner() }) });
if (!r.ok) { const j = await r.json().catch(() => ({})); setMsg(`${r.status}: ${j.error || 'withdraw failed'}`, 'err'); }
else { setMsg(`withdrew ${prefix}`, 'warn'); state.openWire.delete(prefix); loadLocal().catch(console.error); }
});
// ---- boot --------------------------------------------------------------
try { $('f-owner').value = localStorage.getItem('owner') || ''; } catch (_) {}
getJSON('/api/communities/well-known').then(list => {
list.forEach(c => { state.wellKnown[c.value] = c.name; const o = document.createElement('option'); o.value = c.name; o.textContent = `${c.name} (${c.value})`; $('f-wk').appendChild(o); });
renderLocal(); renderReceived();
}).catch(console.error);
loadAll().catch(e => logEvent(esc(e.message), 'err'));
connect();
// Dial-state changes (Connecting → Backoff → …) carry no event, so poll
// the peers table; everything else is event-driven and only re-rendered
// here to keep ages ticking.
setInterval(() => { loadPeers().catch(console.error); renderStatus(); renderLocal(); renderReceived(); }, 5000);
})();
</script>
</body>
</html>