diff --git a/src/rust/lqosd/src/node_manager/js_build/src/circuit.js b/src/rust/lqosd/src/node_manager/js_build/src/circuit.js index 716a44b8..26e32ad3 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/circuit.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/circuit.js @@ -1,6 +1,6 @@ // Obtain URL parameters import {DirectChannel} from "./pubsub/direct_channels"; -import {clearDiv, formatLastSeen, simpleRow, simpleRowHtml, theading} from "./helpers/builders"; +import {clearDiv, formatLastSeen, simpleRow, simpleRowTrustedHtml, theading} from "./helpers/builders"; import { formatRetransmitFraction, formatRtt, @@ -1914,14 +1914,14 @@ function renderTopAsnTab() { row.appendChild(truncatedTrafficCell(rowData.asn_name, "lqos-circuit-traffic-asn-cell")); row.appendChild(truncatedTrafficCell(rowData.asn_country, "lqos-circuit-traffic-country-cell")); - row.appendChild(simpleRowHtml(formatThroughput(rowData.down_bps, plan.down))); - row.appendChild(simpleRowHtml(formatThroughput(rowData.up_bps, plan.up))); - row.appendChild(simpleRowHtml(formatRttNanos(rowData.rtt_down_nanos))); - row.appendChild(simpleRowHtml(formatRttNanos(rowData.rtt_up_nanos))); - row.appendChild(simpleRowHtml(formatQooScore(rowData.qoo_down))); - row.appendChild(simpleRowHtml(formatQooScore(rowData.qoo_up))); - row.appendChild(simpleRowHtml(rowData.retransmit_down_pct > 0 ? formatRetransmitFraction(rowData.retransmit_down_pct) : "-")); - row.appendChild(simpleRowHtml(rowData.retransmit_up_pct > 0 ? formatRetransmitFraction(rowData.retransmit_up_pct) : "-")); + row.appendChild(simpleRowTrustedHtml(formatThroughput(rowData.down_bps, plan.down))); + row.appendChild(simpleRowTrustedHtml(formatThroughput(rowData.up_bps, plan.up))); + row.appendChild(simpleRowTrustedHtml(formatRttNanos(rowData.rtt_down_nanos))); + row.appendChild(simpleRowTrustedHtml(formatRttNanos(rowData.rtt_up_nanos))); + row.appendChild(simpleRowTrustedHtml(formatQooScore(rowData.qoo_down))); + row.appendChild(simpleRowTrustedHtml(formatQooScore(rowData.qoo_up))); + row.appendChild(simpleRowTrustedHtml(rowData.retransmit_down_pct > 0 ? formatRetransmitFraction(rowData.retransmit_down_pct) : "-")); + row.appendChild(simpleRowTrustedHtml(rowData.retransmit_up_pct > 0 ? formatRetransmitFraction(rowData.retransmit_up_pct) : "-")); row.appendChild(simpleRow(scaleNumber(rowData.flow_count))); tbody.appendChild(row); @@ -2003,18 +2003,18 @@ function renderTrafficTab() { row.style.opacity = toNumber(rowData.opacity, 1); row.appendChild(truncatedTrafficCell(rowData.protocol_name, "lqos-circuit-traffic-protocol-cell")); - row.appendChild(simpleRowHtml(formatThroughput(rowData.down_bps, plan.down))); - row.appendChild(simpleRowHtml(formatThroughput(rowData.up_bps, plan.up))); + row.appendChild(simpleRowTrustedHtml(formatThroughput(rowData.down_bps, plan.down))); + row.appendChild(simpleRowTrustedHtml(formatThroughput(rowData.up_bps, plan.up))); row.appendChild(simpleRow(scaleNumber(rowData.bytes_sent_down))); row.appendChild(simpleRow(scaleNumber(rowData.bytes_sent_up))); row.appendChild(simpleRow(scaleNumber(rowData.packets_sent_down))); row.appendChild(simpleRow(scaleNumber(rowData.packets_sent_up))); - row.appendChild(simpleRowHtml(rowData.retransmit_down_pct > 0 ? formatRetransmitFraction(rowData.retransmit_down_pct) : "-")); - row.appendChild(simpleRowHtml(rowData.retransmit_up_pct > 0 ? formatRetransmitFraction(rowData.retransmit_up_pct) : "-")); - row.appendChild(simpleRowHtml(formatRttNanos(rowData.rtt_down_nanos))); - row.appendChild(simpleRowHtml(formatRttNanos(rowData.rtt_up_nanos))); - row.appendChild(simpleRowHtml(formatQooScore(rowData.qoo_down))); - row.appendChild(simpleRowHtml(formatQooScore(rowData.qoo_up))); + row.appendChild(simpleRowTrustedHtml(rowData.retransmit_down_pct > 0 ? formatRetransmitFraction(rowData.retransmit_down_pct) : "-")); + row.appendChild(simpleRowTrustedHtml(rowData.retransmit_up_pct > 0 ? formatRetransmitFraction(rowData.retransmit_up_pct) : "-")); + row.appendChild(simpleRowTrustedHtml(formatRttNanos(rowData.rtt_down_nanos))); + row.appendChild(simpleRowTrustedHtml(formatRttNanos(rowData.rtt_up_nanos))); + row.appendChild(simpleRowTrustedHtml(formatQooScore(rowData.qoo_down))); + row.appendChild(simpleRowTrustedHtml(formatQooScore(rowData.qoo_up))); row.appendChild(truncatedTrafficCell(rowData.asn_name, "lqos-circuit-traffic-asn-cell")); row.appendChild(truncatedTrafficCell(rowData.asn_country, "lqos-circuit-traffic-country-cell")); row.appendChild(simpleRow(rowData.remote_ip)); @@ -2161,7 +2161,10 @@ function initialDevices(circuits) { let name = document.createElement("h5"); name.classList.add("redactable"); - name.innerHTML = " " + circuit.device_name; + const deviceIcon = document.createElement("i"); + deviceIcon.classList.add("fa", "fa-computer"); + name.appendChild(deviceIcon); + name.appendChild(document.createTextNode(` ${circuit.device_name ?? ""}`)); d.appendChild(name); let infoTableWrap = document.createElement("div"); @@ -2181,7 +2184,7 @@ function initialDevices(circuits) { td.classList.add("table-value-cell"); td.classList.add("redactable"); td.colSpan = 2; - td.innerHTML = circuit.mac; + td.textContent = circuit.mac ?? ""; tr.appendChild(td); tbody.appendChild(tr); @@ -2194,7 +2197,7 @@ function initialDevices(circuits) { td = document.createElement("td"); td.classList.add("table-value-cell"); td.colSpan = 2; - td.innerHTML = circuit.comment; + td.textContent = circuit.comment ?? ""; tr2.appendChild(td); tbody.appendChild(tr2); diff --git a/src/rust/lqosd/src/node_manager/js_build/src/config_users.js b/src/rust/lqosd/src/node_manager/js_build/src/config_users.js index f7cd4fc8..a04c109b 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/config_users.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/config_users.js @@ -97,17 +97,22 @@ function loadUsers() { const tbody = $(''); users.forEach(user => { + const actions = $(''); + const editButton = $(' - - `); + .append($('').text(user.username)) + .append($('').text(user.role)) + .append(actions); tbody.append(row); }); diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/circuit_capacity_dash.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/circuit_capacity_dash.js index ea33900d..72ffa0d1 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/circuit_capacity_dash.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/circuit_capacity_dash.js @@ -1,5 +1,5 @@ import {BaseDashlet} from "../lq_js_common/dashboard/base_dashlet"; -import {clearDashDiv, simpleRowHtml, theading} from "../helpers/builders"; +import {clearDashDiv, simpleRowTrustedHtml, theading} from "../helpers/builders"; import {formatRtt, formatPercent} from "../helpers/scaling"; import {redactCell} from "../helpers/redact"; import {TimedCache} from "../lq_js_common/helpers/timed_cache"; @@ -87,9 +87,9 @@ export class CircuitCapacityDash extends BaseDashlet { linkCol.appendChild(link); row.appendChild(linkCol); - row.appendChild(simpleRowHtml(formatPercent(c.capacity[0]*100))); - row.appendChild(simpleRowHtml(formatPercent(c.capacity[1]*100))); - row.appendChild(simpleRowHtml(formatRtt(c.rtt))); + row.appendChild(simpleRowTrustedHtml(formatPercent(c.capacity[0]*100))); + row.appendChild(simpleRowTrustedHtml(formatPercent(c.capacity[1]*100))); + row.appendChild(simpleRowTrustedHtml(formatRtt(c.rtt))); tbody.appendChild(row); count++; diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/ltsShaperStatus.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/ltsShaperStatus.js index 3b12f4fc..d24b787e 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/ltsShaperStatus.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/ltsShaperStatus.js @@ -1,5 +1,5 @@ import {BaseDashlet} from "../lq_js_common/dashboard/base_dashlet"; -import {clearDiv, simpleRow, simpleRowHtml, theading} from "../helpers/builders"; +import {clearDiv, simpleRow, simpleRowTrustedHtml, theading} from "../helpers/builders"; import {get_ws_client} from "../pubsub/ws"; const wsClient = get_ws_client(); @@ -74,7 +74,7 @@ export class LtsShaperStatus extends BaseDashlet { } else if (row.last_seen_seconds_ago > 120) { color = "orange"; } - tr.appendChild(simpleRowHtml(`■`)); + tr.appendChild(simpleRowTrustedHtml(`■`)); tr.appendChild(simpleRow(row.name)); tr.appendChild(simpleRow(row.last_seen_seconds_ago + "s")); tbody.appendChild(tr); diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/top10_downloaders.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/top10_downloaders.js index 52a4f875..c08e162c 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/top10_downloaders.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/top10_downloaders.js @@ -1,4 +1,11 @@ -import {clearDashDiv, simpleRow, simpleRowHtml, theading, TopNTableFromMsgData} from "../helpers/builders"; +import { + clearDashDiv, + simpleLinkRow, + simpleRow, + simpleRowTrustedHtml, + theading, + TopNTableFromMsgData, +} from "../helpers/builders"; import {TimedCache} from "../lq_js_common/helpers/timed_cache"; import {periodNameToSeconds} from "../helpers/time_periods"; import {formatRetransmit, formatRtt} from "../helpers/scaling"; @@ -94,15 +101,19 @@ export class Top10Downloaders extends DashletBaseInsight { //console.log(row); let tr = document.createElement("tr"); tr.classList.add("small"); - tr.appendChild(simpleRowHtml("" + row.circuit_name + "")); + tr.appendChild(simpleLinkRow( + `circuit.html?circuit=${encodeURIComponent(row.circuit_hash ?? "")}`, + row.circuit_name, + true, + )); tr.appendChild(simpleRow(scaleNumber(toNumber(row.bytes_down, 0) * 1000000, 0))); if (row.rtt !== null) { - tr.appendChild(simpleRowHtml(formatRtt(row.rtt))); + tr.appendChild(simpleRowTrustedHtml(formatRtt(row.rtt))); } else { tr.appendChild(simpleRow("-")); } if (row.rxmit !== null) { - tr.appendChild(simpleRowHtml(formatRetransmit(row.rxmit))); + tr.appendChild(simpleRowTrustedHtml(formatRetransmit(row.rxmit))); } else { tr.appendChild(simpleRow("-")); } diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/top10flows_bytes.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/top10flows_bytes.js index c5c10d39..3a633f59 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/top10flows_bytes.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/top10flows_bytes.js @@ -1,4 +1,4 @@ -import {clearDashDiv, simpleRow, simpleRowHtml, theading} from "../helpers/builders"; +import {clearDashDiv, simpleRow, simpleRowTrustedHtml, theading} from "../helpers/builders"; import {scaleNumber, toNumber} from "../lq_js_common/helpers/scaling"; import {formatRtt} from "../helpers/scaling"; import {TrimToFit} from "../lq_js_common/helpers/text_utils"; @@ -202,27 +202,27 @@ export class Top10FlowsBytes extends DashletBaseInsight { tr.classList.add("small"); tr.appendChild(simpleRow(row.circuit_name || "", true)); tr.appendChild(simpleRow(row.protocol)); - tr.appendChild(simpleRowHtml(scaleNumber(row.bytes_down))); - tr.appendChild(simpleRowHtml(scaleNumber(row.bytes_up))); + tr.appendChild(simpleRowTrustedHtml(scaleNumber(row.bytes_down))); + tr.appendChild(simpleRowTrustedHtml(scaleNumber(row.bytes_up))); if (row.rtt_down === null) { - tr.appendChild(simpleRowHtml("-")); + tr.appendChild(simpleRowTrustedHtml("-")); } else { - tr.appendChild(simpleRowHtml(formatRtt(row.rtt_down))); + tr.appendChild(simpleRowTrustedHtml(formatRtt(row.rtt_down))); } if (row.rtt_up === null) { - tr.appendChild(simpleRowHtml("-")); + tr.appendChild(simpleRowTrustedHtml("-")); } else { - tr.appendChild(simpleRowHtml(formatRtt(row.rtt_up))); + tr.appendChild(simpleRowTrustedHtml(formatRtt(row.rtt_up))); } if (row.rxmit_down === null) { - tr.appendChild(simpleRowHtml("-")); + tr.appendChild(simpleRowTrustedHtml("-")); } else { - tr.appendChild(simpleRowHtml(formatRetransmit(row.rxmit_down))); + tr.appendChild(simpleRowTrustedHtml(formatRetransmit(row.rxmit_down))); } if (row.rxmit_up === null) { - tr.appendChild(simpleRowHtml("-")); + tr.appendChild(simpleRowTrustedHtml("-")); } else { - tr.appendChild(simpleRowHtml(formatRetransmit(row.rxmit_up))); + tr.appendChild(simpleRowTrustedHtml(formatRetransmit(row.rxmit_up))); } const asnCell = document.createElement("td"); asnCell.classList.add("lqos-asn-cell"); diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/top_tree_summary.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/top_tree_summary.js index 7eb3e231..cfc47aea 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/top_tree_summary.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/top_tree_summary.js @@ -1,4 +1,4 @@ -import {clearDiv, simpleRowHtml, theading} from "../helpers/builders"; +import {clearDiv, simpleRowTrustedHtml, theading} from "../helpers/builders"; import {formatThroughput, formatRetransmit, formatCakeStat} from "../helpers/scaling"; import {toNumber} from "../lq_js_common/helpers/scaling"; import {DashletBaseInsight} from "./insight_dashlet_base"; @@ -89,8 +89,8 @@ export class TopTreeSummary extends DashletBaseInsight { const tpDown = toNumber(r[1].current_throughput[0], 0) * 8; const tpUp = toNumber(r[1].current_throughput[1], 0) * 8; const maxThroughput = effectiveMax(r[1]); - row.appendChild(simpleRowHtml(formatThroughput(tpDown, maxThroughput[0]))); - row.appendChild(simpleRowHtml(formatThroughput(tpUp, maxThroughput[1]))); + row.appendChild(simpleRowTrustedHtml(formatThroughput(tpDown, maxThroughput[0]))); + row.appendChild(simpleRowTrustedHtml(formatThroughput(tpUp, maxThroughput[1]))); const tcpPacketsDown = retransmitPacketsForNode(r[1], 0); const tcpPacketsUp = retransmitPacketsForNode(r[1], 1); @@ -98,19 +98,19 @@ export class TopTreeSummary extends DashletBaseInsight { const retransmitsUp = toNumber(r[1].current_retransmits[1], 0); if (tcpPacketsDown > 0) { - row.appendChild(simpleRowHtml(formatRetransmit(retransmitsDown / tcpPacketsDown))) + row.appendChild(simpleRowTrustedHtml(formatRetransmit(retransmitsDown / tcpPacketsDown))) } else { - row.appendChild(simpleRowHtml("")); + row.appendChild(simpleRowTrustedHtml("")); } if (tcpPacketsUp > 0) { - row.appendChild(simpleRowHtml(formatRetransmit(retransmitsUp / tcpPacketsUp))) + row.appendChild(simpleRowTrustedHtml(formatRetransmit(retransmitsUp / tcpPacketsUp))) } else { - row.appendChild(simpleRowHtml("")); + row.appendChild(simpleRowTrustedHtml("")); } - row.appendChild(simpleRowHtml(formatCakeStat(r[1].current_marks[0]))) - row.appendChild(simpleRowHtml(formatCakeStat(r[1].current_marks[1]))) - row.appendChild(simpleRowHtml(formatCakeStat(r[1].current_drops[0]))) - row.appendChild(simpleRowHtml(formatCakeStat(r[1].current_drops[1]))) + row.appendChild(simpleRowTrustedHtml(formatCakeStat(r[1].current_marks[0]))) + row.appendChild(simpleRowTrustedHtml(formatCakeStat(r[1].current_marks[1]))) + row.appendChild(simpleRowTrustedHtml(formatCakeStat(r[1].current_drops[0]))) + row.appendChild(simpleRowTrustedHtml(formatCakeStat(r[1].current_drops[1]))) target.appendChild(row); }); } diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/tree_capacity_dash.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/tree_capacity_dash.js index 308d379f..e70567b4 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/tree_capacity_dash.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/tree_capacity_dash.js @@ -1,4 +1,4 @@ -import {clearDashDiv, simpleRowHtml, theading} from "../helpers/builders"; +import {clearDashDiv, simpleRowTrustedHtml, theading} from "../helpers/builders"; import {formatRtt, formatPercent} from "../helpers/scaling"; import {DashletBaseInsight} from "./insight_dashlet_base"; @@ -74,9 +74,9 @@ export class TreeCapacityDash extends DashletBaseInsight { linkCol.appendChild(link); row.appendChild(linkCol); - row.appendChild(simpleRowHtml(formatPercent(down*100))); - row.appendChild(simpleRowHtml(formatPercent(up*100))); - row.appendChild(simpleRowHtml(formatRtt(node.rtt))); + row.appendChild(simpleRowTrustedHtml(formatPercent(down*100))); + row.appendChild(simpleRowTrustedHtml(formatPercent(up*100))); + row.appendChild(simpleRowTrustedHtml(formatRtt(node.rtt))); tbody.appendChild(row); }); diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/worst10_downloaders.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/worst10_downloaders.js index 85c46f07..aebf4643 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/worst10_downloaders.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/worst10_downloaders.js @@ -1,4 +1,11 @@ -import {clearDashDiv, simpleRow, simpleRowHtml, theading, TopNTableFromMsgData} from "../helpers/builders"; +import { + clearDashDiv, + simpleLinkRow, + simpleRow, + simpleRowTrustedHtml, + theading, + TopNTableFromMsgData, +} from "../helpers/builders"; import {TimedCache} from "../lq_js_common/helpers/timed_cache"; import {periodNameToSeconds} from "../helpers/time_periods"; import {formatRetransmit, formatRtt} from "../helpers/scaling"; @@ -94,15 +101,19 @@ export class Worst10Downloaders extends DashletBaseInsight { //console.log(row); let tr = document.createElement("tr"); tr.classList.add("small"); - tr.appendChild(simpleRowHtml("" + row.circuit_name + "")); + tr.appendChild(simpleLinkRow( + `circuit.html?circuit=${encodeURIComponent(row.circuit_hash ?? "")}`, + row.circuit_name, + true, + )); tr.appendChild(simpleRow(scaleNumber(toNumber(row.bytes_down, 0) * 1000000, 0))); if (row.rtt !== null) { - tr.appendChild(simpleRowHtml(formatRtt(row.rtt))); + tr.appendChild(simpleRowTrustedHtml(formatRtt(row.rtt))); } else { tr.appendChild(simpleRow("-")); } if (row.rxmit !== null) { - tr.appendChild(simpleRowHtml(formatRetransmit(row.rxmit))); + tr.appendChild(simpleRowTrustedHtml(formatRetransmit(row.rxmit))); } else { tr.appendChild(simpleRow("-")); } diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/worst10_retransmits.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/worst10_retransmits.js index 7d9d8730..84211db2 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/worst10_retransmits.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/worst10_retransmits.js @@ -1,4 +1,11 @@ -import {clearDashDiv, simpleRow, simpleRowHtml, theading, TopNTableFromMsgData} from "../helpers/builders"; +import { + clearDashDiv, + simpleLinkRow, + simpleRow, + simpleRowTrustedHtml, + theading, + TopNTableFromMsgData, +} from "../helpers/builders"; import {TimedCache} from "../lq_js_common/helpers/timed_cache"; import {periodNameToSeconds} from "../helpers/time_periods"; import {formatRetransmit, formatRtt, retransmitFractionFromSample} from "../helpers/scaling"; @@ -98,15 +105,19 @@ export class Worst10Retransmits extends DashletBaseInsight { //console.log(row); let tr = document.createElement("tr"); tr.classList.add("small"); - tr.appendChild(simpleRowHtml("" + row.circuit_name + "")); + tr.appendChild(simpleLinkRow( + `circuit.html?circuit=${encodeURIComponent(row.circuit_hash ?? "")}`, + row.circuit_name, + true, + )); tr.appendChild(simpleRow(scaleNumber(toNumber(row.bytes_down, 0) * 1000000, 0))); if (row.rtt !== null) { - tr.appendChild(simpleRowHtml(formatRtt(row.rtt))); + tr.appendChild(simpleRowTrustedHtml(formatRtt(row.rtt))); } else { tr.appendChild(simpleRow("-")); } if (row.rxmit !== null) { - tr.appendChild(simpleRowHtml(formatRetransmit(row.rxmit))); + tr.appendChild(simpleRowTrustedHtml(formatRetransmit(row.rxmit))); } else { tr.appendChild(simpleRow("-")); } diff --git a/src/rust/lqosd/src/node_manager/js_build/src/helpers/builders.js b/src/rust/lqosd/src/node_manager/js_build/src/helpers/builders.js index b73bb3cb..320b3791 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/helpers/builders.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/helpers/builders.js @@ -7,6 +7,7 @@ import { import {scaleNanos} from "../lq_js_common/helpers/scaling"; import {redactCell} from "./redact"; import {disposeTooltipsWithin, enableTooltipsWithin} from "../lq_js_common/helpers/tooltips"; +export {safeRelativeHref, simpleLinkRow} from "./safe_dom.mjs"; export function heading5Icon(icon, text) { let h5 = document.createElement("h5"); @@ -42,7 +43,7 @@ export function simpleRow(text, redact=false) { return td; } -export function simpleRowHtml(text) { +export function simpleRowTrustedHtml(text) { let td = document.createElement("td"); td.innerHTML = text; return td; diff --git a/src/rust/lqosd/src/node_manager/js_build/src/helpers/builders.test.mjs b/src/rust/lqosd/src/node_manager/js_build/src/helpers/builders.test.mjs new file mode 100644 index 00000000..89310f50 --- /dev/null +++ b/src/rust/lqosd/src/node_manager/js_build/src/helpers/builders.test.mjs @@ -0,0 +1,75 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import {safeRelativeHref, simpleLinkRow} from "./safe_dom.mjs"; + +class FakeClassList { + constructor() { + this.values = []; + } + + add(...classes) { + this.values.push(...classes); + } + + contains(className) { + return this.values.includes(className); + } +} + +class FakeElement { + constructor(tagName) { + this.tagName = tagName.toUpperCase(); + this.children = []; + this.classList = new FakeClassList(); + this._textContent = ""; + this._href = ""; + } + + appendChild(child) { + this.children.push(child); + } + + set href(value) { + this._href = String(value); + } + + get href() { + return this._href; + } + + set textContent(value) { + this._textContent = String(value); + } + + get textContent() { + return this._textContent; + } +} + +test("safeRelativeHref rejects scriptable URL protocols", () => { + assert.equal(safeRelativeHref("javascript:alert(1)"), "#"); + assert.equal(safeRelativeHref(" data:text/html,boom"), "#"); + assert.equal(safeRelativeHref("https://example.invalid/circuit"), "#"); + assert.equal(safeRelativeHref("//example.invalid/circuit"), "#"); + assert.equal(safeRelativeHref("circuit.html?circuit=123"), "circuit.html?circuit=123"); +}); + +test("simpleLinkRow renders operator text as text content", () => { + globalThis.document = { + createElement: (tagName) => new FakeElement(tagName), + }; + + const row = simpleLinkRow( + "javascript:alert(1)", + "", + true, + ); + const link = row.children[0]; + + assert.equal(row.tagName, "TD"); + assert.equal(link.tagName, "A"); + assert.equal(link.href, "#"); + assert.equal(link.textContent, ""); + assert.equal(link.classList.contains("redactable"), true); +}); diff --git a/src/rust/lqosd/src/node_manager/js_build/src/helpers/safe_dom.mjs b/src/rust/lqosd/src/node_manager/js_build/src/helpers/safe_dom.mjs new file mode 100644 index 00000000..62ed25b6 --- /dev/null +++ b/src/rust/lqosd/src/node_manager/js_build/src/helpers/safe_dom.mjs @@ -0,0 +1,20 @@ +export function safeRelativeHref(href) { + const value = String(href ?? ""); + const trimmed = value.trimStart(); + if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) || trimmed.startsWith("//")) { + return "#"; + } + return value; +} + +export function simpleLinkRow(href, text, redact=false) { + let td = document.createElement("td"); + let link = document.createElement("a"); + link.href = safeRelativeHref(href); + link.textContent = text ?? ""; + if (redact) { + link.classList.add("redactable"); + } + td.appendChild(link); + return td; +} diff --git a/src/rust/lqosd/src/node_manager/js_build/src/template.js b/src/rust/lqosd/src/node_manager/js_build/src/template.js index 62bdb72c..f556c064 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/template.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/template.js @@ -1,4 +1,4 @@ -import {clearDiv} from "./helpers/builders"; +import {clearDiv, safeRelativeHref} from "./helpers/builders"; import {initRedact} from "./helpers/redact"; import {initDayNightMode} from "./helpers/dark_mode"; import {initColorBlind} from "./helpers/colorblind"; @@ -794,11 +794,23 @@ function renderSearchResults(data) { let c = document.createElement("td"); if (item.Circuit !== undefined) { - c.innerHTML = " " + item.Circuit.name + ""; + c.appendChild(searchResultLink( + `/circuit.html?id=${encodeURIComponent(item.Circuit.id ?? "")}`, + ["fa", "fa-user"], + item.Circuit.name, + )); } else if (item.Device !== undefined) { - c.innerHTML = " " + item.Device.name + ""; + c.appendChild(searchResultLink( + `/circuit.html?id=${encodeURIComponent(item.Device.circuit_id ?? "")}`, + ["fa", "fa-computer"], + item.Device.name, + )); } else if (item.Site !== undefined) { - c.innerHTML = " " + item.Site.name + ""; + c.appendChild(searchResultLink( + `/tree.html?parent=${encodeURIComponent(item.Site.idx ?? "")}`, + ["fa", "fa-building"], + item.Site.name, + )); } else { console.log(item); c.innerText = item; @@ -814,6 +826,18 @@ function renderSearchResults(data) { searchResults.appendChild(wrap); } +function searchResultLink(href, iconClasses, label) { + const link = document.createElement("a"); + link.classList.add("nav-link", "redactable"); + link.href = safeRelativeHref(href); + + const icon = document.createElement("i"); + icon.classList.add(...iconClasses); + link.appendChild(icon); + link.appendChild(document.createTextNode(` ${label ?? ""}`)); + return link; +} + function doSearch(search) { if (search.length > 2) { lastSearchTerm = search; diff --git a/src/rust/lqosd/src/node_manager/js_build/src/tree.js b/src/rust/lqosd/src/node_manager/js_build/src/tree.js index e3f4827c..e5e545f8 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/tree.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/tree.js @@ -1,4 +1,4 @@ -import {clearDiv, clientTableHeader, formatLastSeen, simpleRow, simpleRowHtml, theading} from "./helpers/builders"; +import {clearDiv, clientTableHeader, formatLastSeen, simpleRow, simpleRowTrustedHtml, theading} from "./helpers/builders"; import { formatCakeStat, formatCakeStatPercent, formatRetransmit, formatRetransmitFraction, retransmitFractionFromSample, @@ -1870,22 +1870,22 @@ function renderAttachedCircuitsRows(rows) { tr.appendChild(ipCell); tr.appendChild(simpleRow(formatLastSeen(toNumber(circuit.last_seen_nanos, 0)))); - tr.appendChild(simpleRowHtml(formatThroughput(toNumber(circuit.bytes_per_second?.down, 0) * 8, toNumber(circuit.plan_mbps?.down, 0)))); - tr.appendChild(simpleRowHtml(formatThroughput(toNumber(circuit.bytes_per_second?.up, 0) * 8, toNumber(circuit.plan_mbps?.up, 0)))); + tr.appendChild(simpleRowTrustedHtml(formatThroughput(toNumber(circuit.bytes_per_second?.down, 0) * 8, toNumber(circuit.plan_mbps?.down, 0)))); + tr.appendChild(simpleRowTrustedHtml(formatThroughput(toNumber(circuit.bytes_per_second?.up, 0) * 8, toNumber(circuit.plan_mbps?.up, 0)))); if (toNumber(circuit.rtt_current_p50_nanos?.down, 0) > 0) { - tr.appendChild(simpleRowHtml(formatRtt(toNumber(circuit.rtt_current_p50_nanos?.down, 0) / 1_000_000))); + tr.appendChild(simpleRowTrustedHtml(formatRtt(toNumber(circuit.rtt_current_p50_nanos?.down, 0) / 1_000_000))); } else { tr.appendChild(simpleRow("-")); } if (toNumber(circuit.rtt_current_p50_nanos?.up, 0) > 0) { - tr.appendChild(simpleRowHtml(formatRtt(toNumber(circuit.rtt_current_p50_nanos?.up, 0) / 1_000_000))); + tr.appendChild(simpleRowTrustedHtml(formatRtt(toNumber(circuit.rtt_current_p50_nanos?.up, 0) / 1_000_000))); } else { tr.appendChild(simpleRow("-")); } - tr.appendChild(simpleRowHtml(formatRetransmit(retransmitFractionFromSample(circuit.tcp_retransmit_sample?.down)))); - tr.appendChild(simpleRowHtml(formatRetransmit(retransmitFractionFromSample(circuit.tcp_retransmit_sample?.up)))); + tr.appendChild(simpleRowTrustedHtml(formatRetransmit(retransmitFractionFromSample(circuit.tcp_retransmit_sample?.down)))); + tr.appendChild(simpleRowTrustedHtml(formatRetransmit(retransmitFractionFromSample(circuit.tcp_retransmit_sample?.up)))); tbody.appendChild(tr); }); diff --git a/src/rust/lqosd/src/node_manager/js_build/test-node-manager.sh b/src/rust/lqosd/src/node_manager/js_build/test-node-manager.sh index 85f5d6de..613fb5a7 100755 --- a/src/rust/lqosd/src/node_manager/js_build/test-node-manager.sh +++ b/src/rust/lqosd/src/node_manager/js_build/test-node-manager.sh @@ -10,6 +10,7 @@ fi echo "[test-node-manager] Running frontend contract tests..." node --test \ + "${SCRIPT_DIR}/src/helpers/builders.test.mjs" \ "${SCRIPT_DIR}/src/config/shaped_device_wire.test.mjs" \ "${SCRIPT_DIR}/src/config/config_interface_mtu.test.mjs" \ "${SCRIPT_DIR}/src/config/ssl_redirect.test.mjs" \