Files

1033 lines
30 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const FileUtils = ChromeUtils.importESModule(
"resource://gre/modules/FileUtils.sys.mjs"
).FileUtils;
const { PrivateBrowsingUtils } = ChromeUtils.importESModule(
"resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
);
const gDashboard = Cc["@mozilla.org/network/dashboard;1"].getService(
Ci.nsIDashboard
);
const gDirServ = Cc["@mozilla.org/file/directory_service;1"].getService(
Ci.nsIDirectoryServiceProvider
);
const gNetLinkSvc =
Cc["@mozilla.org/network/network-link-service;1"] &&
Cc["@mozilla.org/network/network-link-service;1"].getService(
Ci.nsINetworkLinkService
);
const gCertDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
const gSSLTokensHasher = Cc["@mozilla.org/security/hash;1"].createInstance(
Ci.nsICryptoHash
);
const gRequestNetworkingData = {
http: gDashboard.requestHttpConnections,
sockets: gDashboard.requestSockets,
dns: gDashboard.requestDNSInfo,
websockets: gDashboard.requestWebsocketConnections,
altsvc: gDashboard.requestAltSvcCache,
ssltokens: gDashboard.requestSSLTokensCache,
dnslookuptool: () => {},
networkid: displayNetworkID,
};
const gDashboardCallbacks = {
http: displayHttp,
sockets: displaySockets,
dns: displayDns,
websockets: displayWebsockets,
altsvc: displayAltSvc,
ssltokens: displaySSLTokensCache,
};
const REFRESH_INTERVAL_MS = 3000;
const gIsPrivateBrowsing = PrivateBrowsingUtils.isWindowPrivate(window);
function isPrivateBrowsingEntry(originAttributesSuffix) {
return !!ChromeUtils.CreateOriginAttributesFromOriginSuffix(
originAttributesSuffix
).privateBrowsingId;
}
function col(element) {
let col = document.createElement("td");
let content = document.createTextNode(element);
col.appendChild(content);
return col;
}
function displayHttp(data) {
let cont = document.getElementById("http_content");
let parent = cont.parentNode;
let new_cont = document.createElement("tbody");
new_cont.setAttribute("id", "http_content");
for (let i = 0; i < data.connections.length; i++) {
if (
!gIsPrivateBrowsing &&
isPrivateBrowsingEntry(data.connections[i].originAttributesSuffix)
) {
continue;
}
let row = document.createElement("tr");
row.appendChild(col(data.connections[i].host));
row.appendChild(col(data.connections[i].port));
row.appendChild(col(data.connections[i].httpVersion));
row.appendChild(col(data.connections[i].ssl));
row.appendChild(col(data.connections[i].active.length));
row.appendChild(col(data.connections[i].idle.length));
row.appendChild(col(data.connections[i].originAttributesSuffix));
new_cont.appendChild(row);
}
parent.replaceChild(new_cont, cont);
}
function displaySockets(data) {
let cont = document.getElementById("sockets_content");
let parent = cont.parentNode;
let new_cont = document.createElement("tbody");
new_cont.setAttribute("id", "sockets_content");
for (let i = 0; i < data.sockets.length; i++) {
if (
!gIsPrivateBrowsing &&
isPrivateBrowsingEntry(data.sockets[i].originAttributesSuffix)
) {
continue;
}
let row = document.createElement("tr");
row.appendChild(col(data.sockets[i].host));
row.appendChild(col(data.sockets[i].port));
row.appendChild(col(data.sockets[i].type));
row.appendChild(col(data.sockets[i].active));
row.appendChild(col(data.sockets[i].sent));
row.appendChild(col(data.sockets[i].received));
row.appendChild(col(data.sockets[i].originAttributesSuffix));
new_cont.appendChild(row);
}
parent.replaceChild(new_cont, cont);
}
function displayDns(data) {
let suffixContent = document.getElementById("dns_suffix_content");
let suffixParent = suffixContent.parentNode;
let suffixes = [];
try {
suffixes = gNetLinkSvc.dnsSuffixList; // May throw
} catch (e) {}
let suffix_tbody = document.createElement("tbody");
suffix_tbody.id = "dns_suffix_content";
for (let suffix of suffixes) {
let row = document.createElement("tr");
row.appendChild(col(suffix));
suffix_tbody.appendChild(row);
}
suffixParent.replaceChild(suffix_tbody, suffixContent);
let trr_url_tbody = document.createElement("tbody");
trr_url_tbody.id = "dns_trr_url";
let trr_url = document.createElement("tr");
trr_url.appendChild(col(Services.dns.currentTrrURI));
trr_url.appendChild(col(Services.dns.currentTrrMode));
trr_url_tbody.appendChild(trr_url);
let prevURL = document.getElementById("dns_trr_url");
prevURL.parentNode.replaceChild(trr_url_tbody, prevURL);
let cont = document.getElementById("dns_content");
let parent = cont.parentNode;
let new_cont = document.createElement("tbody");
new_cont.setAttribute("id", "dns_content");
for (let i = 0; i < data.entries.length; i++) {
// TODO: Will be supported in bug 1889387.
if (data.entries[i].type != Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT) {
continue;
}
if (
!gIsPrivateBrowsing &&
isPrivateBrowsingEntry(data.entries[i].originAttributesSuffix)
) {
continue;
}
let row = document.createElement("tr");
row.appendChild(col(data.entries[i].hostname));
row.appendChild(col(data.entries[i].family));
row.appendChild(col(data.entries[i].trr));
let column = document.createElement("td");
for (let j = 0; j < data.entries[i].hostaddr.length; j++) {
column.appendChild(document.createTextNode(data.entries[i].hostaddr[j]));
column.appendChild(document.createElement("br"));
}
row.appendChild(column);
row.appendChild(col(data.entries[i].expiration));
row.appendChild(col(data.entries[i].originAttributesSuffix));
row.appendChild(col(data.entries[i].flags));
new_cont.appendChild(row);
}
parent.replaceChild(new_cont, cont);
}
function displayWebsockets(data) {
let cont = document.getElementById("websockets_content");
let parent = cont.parentNode;
let new_cont = document.createElement("tbody");
new_cont.setAttribute("id", "websockets_content");
for (let i = 0; i < data.websockets.length; i++) {
let row = document.createElement("tr");
row.appendChild(col(data.websockets[i].hostport));
row.appendChild(col(data.websockets[i].encrypted));
row.appendChild(col(data.websockets[i].msgsent));
row.appendChild(col(data.websockets[i].msgreceived));
row.appendChild(col(data.websockets[i].sentsize));
row.appendChild(col(data.websockets[i].receivedsize));
new_cont.appendChild(row);
}
parent.replaceChild(new_cont, cont);
}
function formatTTL(seconds) {
if (seconds <= 0) {
return "expired";
}
let parts = [];
let d = Math.floor(seconds / 86400);
if (d) {
parts.push(`${d}d`);
}
let h = Math.floor((seconds % 86400) / 3600);
if (h) {
parts.push(`${h}h`);
}
let m = Math.floor((seconds % 3600) / 60);
if (m) {
parts.push(`${m}m`);
}
let s = seconds % 60;
if (s || !parts.length) {
parts.push(`${s}s`);
}
return parts.join(" ");
}
function displayAltSvc(data) {
let cont = document.getElementById("altsvc_content");
let parent = cont.parentNode;
let new_cont = document.createElement("tbody");
new_cont.setAttribute("id", "altsvc_content");
for (let i = 0; i < data.entries.length; i++) {
let entry = data.entries[i];
if (
!gIsPrivateBrowsing &&
isPrivateBrowsingEntry(entry.originAttributesSuffix)
) {
continue;
}
let row = document.createElement("tr");
let scheme = entry.https ? "https" : "http";
row.appendChild(col(`${scheme}://${entry.originHost}:${entry.originPort}`));
row.appendChild(col(`${entry.alternateHost}:${entry.alternatePort}`));
row.appendChild(col(entry.alpn));
row.appendChild(col(entry.validated));
row.appendChild(col(formatTTL(entry.ttl)));
row.appendChild(col(entry.originAttributesSuffix));
new_cont.appendChild(row);
}
parent.replaceChild(new_cont, cont);
}
function splitSSLTokensCacheKey(key) {
let caretIndex = key.indexOf("^");
return caretIndex < 0
? { host: key, isolationKey: "" }
: { host: key.slice(0, caretIndex), isolationKey: key.slice(caretIndex) };
}
// Cheap non-cryptographic fingerprint, used so grouping/caching doesn't need
// to stringify entire (potentially multi-KB) DER byte arrays.
function sslTokensBytesFingerprint(bytes) {
if (!bytes || !bytes.length) {
return "";
}
gSSLTokensHasher.init(Ci.nsICryptoHash.SHA256);
gSSLTokensHasher.update(bytes, bytes.length);
return gSSLTokensHasher.finish(/* aASCII */ true);
}
// Multiple tickets are often issued for the same host in one handshake and
// carry identical metadata apart from the token itself; group them so the
// table shows one row per distinct (host, cert) combination.
function sslTokensGroupKey(entry) {
return JSON.stringify([
entry.key,
entry.evStatus,
entry.certificateTransparencyStatus,
entry.overridableErrorCategory,
entry.builtInRoot,
sslTokensBytesFingerprint(entry.serverCertDER),
entry.succeededCertChainDER?.map(sslTokensBytesFingerprint),
entry.handshakeCertDER?.map(sslTokensBytesFingerprint),
]);
}
function sslTokensCompressionSavedPercent(
compressedLength,
decompressedLength
) {
return decompressedLength
? Math.round(100 * (1 - compressedLength / decompressedLength))
: null;
}
// Sums the length fields used throughout the SSL tokens UI, e.g. across a
// group's tokens or across the whole cache.
function sumSSLTokensLengths(entries) {
let totalToken = 0;
let totalCompressed = 0;
let totalDecompressed = 0;
for (let entry of entries) {
totalToken += entry.tokenLength;
totalCompressed += entry.compressedLength;
totalDecompressed += entry.decompressedLength;
}
return { totalToken, totalCompressed, totalDecompressed };
}
function countSSLTokensCerts(entries) {
let distinctCerts = new Map();
let references = 0;
let totalBytes = 0;
let updateCertStats = der => {
if (!der || !der.length) {
return;
}
references++;
totalBytes += der.length;
distinctCerts.set(sslTokensBytesFingerprint(der), der.length);
};
for (let entry of entries) {
updateCertStats(entry.serverCertDER);
entry.succeededCertChainDER?.forEach(updateCertStats);
entry.handshakeCertDER?.forEach(updateCertStats);
}
let distinctBytes = 0;
for (let bytes of distinctCerts.values()) {
distinctBytes += bytes;
}
return {
distinct: distinctCerts.size,
references,
distinctBytes,
totalBytes,
};
}
// expirationTime is a PRTime (microseconds since the Unix epoch).
function sslTokensExpiryDate(expirationTime) {
return new Date(expirationTime / 1000);
}
function isDateExpired(date) {
return date.getTime() <= Date.now();
}
// Builds a "N% saved" element with full byte-count details in a tooltip, or
// null if there's nothing to show (decompressedLength is falsy).
function buildSSLTokensCompressionDetails(
tagName,
tokenLength,
decompressedLength,
compressedLength
) {
let saved = sslTokensCompressionSavedPercent(
compressedLength,
decompressedLength
);
if (saved === null) {
return null;
}
let el = document.createElement(tagName);
el.textContent = `${saved}%`;
el.setAttribute("data-l10n-attrs", "title");
document.l10n.setAttributes(
el,
"about-networking-ssl-tokens-compression-details",
{ tokenLength, decompressedLength, compressedLength }
);
return el;
}
function groupSSLTokensCacheEntries(entries) {
let groups = new Map();
for (let entry of entries) {
let groupKey = sslTokensGroupKey(entry);
if (!groups.has(groupKey)) {
groups.set(groupKey, []);
}
groups.get(groupKey).push(entry);
}
return [...groups.entries()];
}
function addSSLTokensFlagItem(ul, labelL10nId, value) {
let li = document.createElement("li");
let label = document.createElement("span");
document.l10n.setAttributes(label, labelL10nId);
li.appendChild(label);
li.appendChild(document.createTextNode(`: ${value}`));
ul.appendChild(li);
}
// <details> elements are rebuilt from scratch on every refresh, which would
// otherwise reset any collapsible the user had opened; restore prior state
// by key (cache key + which collapsible) instead.
function restoreSSLTokensDetailsState(details, key, kind, openKeys) {
details.dataset.tokenKey = key;
details.dataset.kind = kind;
if (openKeys.has(`${key}|${kind}`)) {
details.open = true;
}
}
// certDB.constructX509 takes ownership of neither its input nor output;
// failures (e.g. malformed DER) are reported as an empty subject/commonName.
// Keyed by a fingerprint of the DER bytes rather than the cache key, since
// the same cert is commonly reused across many entries/refreshes.
const gSSLTokensCertInfoCache = new Map();
// Full DN strings (e.g. "CN=example.com,O=Example Inc,C=US") are noisy, so
// callers show commonName and leave subject for a tooltip.
function sslTokensCertInfo(certDER) {
let fingerprint = sslTokensBytesFingerprint(certDER);
if (gSSLTokensCertInfoCache.has(fingerprint)) {
return gSSLTokensCertInfoCache.get(fingerprint);
}
let info;
try {
let cert = gCertDB.constructX509(certDER);
info = { subject: cert.subjectName, commonName: cert.commonName };
} catch (e) {
info = { subject: "", commonName: "" };
}
gSSLTokensCertInfoCache.set(fingerprint, info);
return info;
}
// Parsing every cert in a chain is only useful once the user actually looks
// at it, so defer it until the collapsible is expanded (or immediately, if
// it's being restored already-open across a refresh).
function addSSLTokensCertList(
parent,
chainDER,
labelL10nId,
key,
kind,
openKeys
) {
if (!chainDER || !chainDER.length) {
return;
}
let details = document.createElement("details");
restoreSSLTokensDetailsState(details, key, kind, openKeys);
let summary = document.createElement("summary");
document.l10n.setAttributes(summary, labelL10nId, { count: chainDER.length });
details.appendChild(summary);
let ul = document.createElement("ul");
details.appendChild(ul);
let populated = false;
let populate = () => {
if (populated) {
return;
}
populated = true;
for (let certDER of chainDER) {
let li = document.createElement("li");
let { subject, commonName } = sslTokensCertInfo(certDER);
li.textContent = commonName || subject;
li.title = subject;
ul.appendChild(li);
}
};
if (details.open) {
populate();
} else {
details.addEventListener("toggle", () => {
if (details.open) {
populate();
}
});
}
parent.appendChild(details);
}
function addSSLTokensIcon(td, iconSrc, labelL10nId) {
let icon = document.createElement("img");
icon.className = "ssltokens-icon";
icon.src = iconSrc;
icon.setAttribute("data-l10n-attrs", "alt, title");
document.l10n.setAttributes(icon, labelL10nId);
td.appendChild(icon);
}
function addSSLTokensRestoredIcon(td, entry) {
addSSLTokensIcon(
td,
entry.restored
? "chrome://global/skin/icons/arrow-counterclockwise-16.svg"
: "chrome://global/skin/icons/plus.svg",
entry.restored
? "about-networking-ssl-tokens-restored"
: "about-networking-ssl-tokens-new"
);
}
function addSSLTokensExpiredIcon(td) {
addSSLTokensIcon(
td,
"chrome://global/skin/icons/warning-fill-12.svg",
"about-networking-ssl-tokens-expired"
);
}
function buildSSLTokensStatusCell(entry, expired) {
let td = document.createElement("td");
addSSLTokensRestoredIcon(td, entry);
if (expired) {
addSSLTokensExpiredIcon(td);
}
return td;
}
function buildSSLTokensHostCell(host) {
let td = document.createElement("td");
td.className = "ssltokens-key";
td.textContent = host;
return td;
}
// The isolation key is a leading "^" (see OriginAttributes::CreateSuffix)
// followed by a "&"-joined, URL-encoded key=value list; partitionKey is by
// far the most common attribute, so drop its redundant label.
function formatSSLTokensIsolationKey(isolationKey) {
if (!isolationKey) {
return "";
}
let params = new URLSearchParams(isolationKey.slice(1));
let parts = [];
for (let [key, value] of params) {
parts.push(key == "partitionKey" ? value : `${key}=${value}`);
}
return parts.join(" ");
}
function buildSSLTokensIsolationKeyCell(isolationKey) {
let td = document.createElement("td");
td.className = "ssltokens-key";
td.textContent = formatSSLTokensIsolationKey(isolationKey);
td.title = isolationKey;
return td;
}
function buildSSLTokensExpiresCell(expires) {
let td = document.createElement("td");
td.textContent = expires.toLocaleString();
return td;
}
function buildSSLTokensTokensCell(group, key, openKeys) {
let td = document.createElement("td");
let { totalToken, totalCompressed, totalDecompressed } =
sumSSLTokensLengths(group);
let details = document.createElement("details");
restoreSSLTokensDetailsState(details, key, "tokens", openKeys);
let summary = document.createElement("summary");
document.l10n.setAttributes(
summary,
"about-networking-ssl-tokens-token-list",
{
count: group.length,
}
);
if (group.length > 1) {
summary.className = "ssltokens-token-count-multiple";
}
details.appendChild(summary);
let ul = document.createElement("ul");
for (let entry of group) {
let li = document.createElement("li");
addSSLTokensRestoredIcon(li, entry);
if (isDateExpired(sslTokensExpiryDate(entry.expirationTime))) {
addSSLTokensExpiredIcon(li);
}
li.appendChild(document.createTextNode(` #${entry.tokenId}`));
let savedEl = buildSSLTokensCompressionDetails(
"span",
entry.tokenLength,
entry.decompressedLength,
entry.compressedLength
);
if (savedEl) {
li.appendChild(document.createTextNode(" — "));
li.appendChild(savedEl);
}
ul.appendChild(li);
}
details.appendChild(ul);
td.appendChild(details);
let groupSavedEl = buildSSLTokensCompressionDetails(
"div",
totalToken,
totalDecompressed,
totalCompressed
);
if (groupSavedEl) {
td.appendChild(groupSavedEl);
}
return td;
}
function buildSSLTokensCertificateCell(entry, groupKey, openKeys) {
let td = document.createElement("td");
if (entry.serverCertDER && entry.serverCertDER.length) {
let { subject: fullSubject, commonName } = sslTokensCertInfo(
entry.serverCertDER
);
let subject = document.createElement("div");
subject.textContent = commonName || fullSubject;
subject.title = fullSubject;
td.appendChild(subject);
}
let flagsDetails = document.createElement("details");
restoreSSLTokensDetailsState(flagsDetails, groupKey, "flags", openKeys);
let flagsSummary = document.createElement("summary");
document.l10n.setAttributes(flagsSummary, "about-networking-flags");
flagsDetails.appendChild(flagsSummary);
let flagsList = document.createElement("ul");
let flagItems = [
["about-networking-ssl-tokens-ev-status", entry.evStatus],
[
"about-networking-ssl-tokens-ct-status",
entry.certificateTransparencyStatus,
],
[
"about-networking-ssl-tokens-overridable-error",
entry.overridableErrorCategory,
],
[
"about-networking-ssl-tokens-built-in-root",
entry.builtInRoot < 0 ? "unknown" : !!entry.builtInRoot,
],
];
for (let [labelL10nId, value] of flagItems) {
addSSLTokensFlagItem(flagsList, labelL10nId, value);
}
flagsDetails.appendChild(flagsList);
td.appendChild(flagsDetails);
addSSLTokensCertList(
td,
entry.succeededCertChainDER,
"about-networking-ssl-tokens-cert-chain",
groupKey,
"chain",
openKeys
);
addSSLTokensCertList(
td,
entry.handshakeCertDER,
"about-networking-ssl-tokens-handshake-certs",
groupKey,
"handshake",
openKeys
);
return td;
}
function buildSSLTokensSummary(data) {
let container = document.createElement("div");
container.id = "ssltokens_summary";
container.className = "ssltokens-summary";
let { totalCompressed, totalDecompressed } = sumSSLTokensLengths(
data.entries
);
let countSpan = document.createElement("span");
document.l10n.setAttributes(
countSpan,
"about-networking-ssl-tokens-summary-count",
{ count: data.entries.length }
);
container.appendChild(countSpan);
let totalExpired = data.entries.filter(entry =>
isDateExpired(sslTokensExpiryDate(entry.expirationTime))
).length;
if (totalExpired) {
let expiredSpan = document.createElement("span");
document.l10n.setAttributes(
expiredSpan,
"about-networking-ssl-tokens-summary-expired",
{ count: totalExpired }
);
container.appendChild(expiredSpan);
}
let saved = sslTokensCompressionSavedPercent(
totalCompressed,
totalDecompressed
);
if (saved !== null) {
let compressionSpan = document.createElement("span");
document.l10n.setAttributes(
compressionSpan,
"about-networking-ssl-tokens-summary-compression",
{
decompressedLength: totalDecompressed,
compressedLength: totalCompressed,
saved,
}
);
container.appendChild(compressionSpan);
}
let certs = countSSLTokensCerts(data.entries);
if (certs.references) {
let certsSpan = document.createElement("span");
document.l10n.setAttributes(
certsSpan,
"about-networking-ssl-tokens-summary-certs",
{
distinct: certs.distinct,
references: certs.references,
distinctBytes: certs.distinctBytes,
totalBytes: certs.totalBytes,
}
);
container.appendChild(certsSpan);
}
// network.ssl_tokens_cache_capacity is in kilobytes.
let capacityBytes =
Services.prefs.getIntPref("network.ssl_tokens_cache_capacity") * 1024;
let percent = capacityBytes
? Math.round((100 * totalCompressed) / capacityBytes)
: 0;
let capacitySpan = document.createElement("span");
document.l10n.setAttributes(
capacitySpan,
"about-networking-ssl-tokens-summary-capacity",
{
used: Math.round(totalCompressed / 1024),
capacity: Math.round(capacityBytes / 1024),
percent,
}
);
container.appendChild(capacitySpan);
let progress = document.createElement("progress");
progress.max = capacityBytes;
progress.value = totalCompressed;
container.appendChild(progress);
return container;
}
function displaySSLTokensCache(data) {
let summary = document.getElementById("ssltokens_summary");
summary.parentNode.replaceChild(buildSSLTokensSummary(data), summary);
let cont = document.getElementById("ssltokens_content");
let openKeys = new Set();
for (let details of cont.querySelectorAll("details[open]")) {
openKeys.add(`${details.dataset.tokenKey}|${details.dataset.kind}`);
}
let new_cont = document.createElement("tbody");
new_cont.id = "ssltokens_content";
for (let [groupKey, group] of groupSSLTokensCacheEntries(data.entries)) {
// Represent the group by its longest-lived (latest-expiring) token.
let entry = group.reduce((a, b) =>
b.expirationTime > a.expirationTime ? b : a
);
let { host, isolationKey } = splitSSLTokensCacheKey(entry.key);
// Match the other about:networking tabs: don't show private-browsing
// entries as rows unless this page is itself a private window. The
// summary above still counts them (buildSSLTokensSummary receives the
// full, unfiltered data.entries).
if (!gIsPrivateBrowsing && isPrivateBrowsingEntry(isolationKey)) {
continue;
}
let expires = sslTokensExpiryDate(entry.expirationTime);
let expired = isDateExpired(expires);
let row = document.createElement("tr");
row.appendChild(buildSSLTokensStatusCell(entry, expired));
row.appendChild(buildSSLTokensHostCell(host));
row.appendChild(buildSSLTokensIsolationKeyCell(isolationKey));
row.appendChild(buildSSLTokensTokensCell(group, groupKey, openKeys));
row.appendChild(buildSSLTokensExpiresCell(expires));
row.appendChild(buildSSLTokensCertificateCell(entry, groupKey, openKeys));
new_cont.appendChild(row);
}
cont.parentNode.replaceChild(new_cont, cont);
}
function displayNetworkID() {
try {
let linkIsUp = gNetLinkSvc.isLinkUp;
let linkStatusKnown = gNetLinkSvc.linkStatusKnown;
let networkID = gNetLinkSvc.networkID;
document.getElementById("networkid_isUp").innerText = linkIsUp;
document.getElementById("networkid_statusKnown").innerText =
linkStatusKnown;
document.getElementById("networkid_id").innerText = networkID;
} catch (e) {
document.getElementById("networkid_isUp").innerText = "<unknown>";
document.getElementById("networkid_statusKnown").innerText = "<unknown>";
document.getElementById("networkid_id").innerText = "<unknown>";
}
}
function requestAllNetworkingData() {
for (let id in gRequestNetworkingData) {
requestNetworkingDataForTab(id);
}
}
function requestNetworkingDataForTab(id) {
gRequestNetworkingData[id](gDashboardCallbacks[id]);
}
let gInited = false;
function init() {
if (gInited) {
return;
}
gInited = true;
requestAllNetworkingData();
let autoRefresh = document.getElementById("autorefcheck");
if (autoRefresh.checked) {
setAutoRefreshInterval(autoRefresh);
}
autoRefresh.addEventListener("click", function () {
let refrButton = document.getElementById("refreshButton");
if (this.checked) {
setAutoRefreshInterval(this);
refrButton.disabled = "disabled";
} else {
clearInterval(this.interval);
refrButton.disabled = null;
}
});
let refr = document.getElementById("refreshButton");
refr.addEventListener("click", requestAllNetworkingData);
if (document.getElementById("autorefcheck").checked) {
refr.disabled = "disabled";
}
// Event delegation on #categories element
let menu = document.getElementById("categories");
menu.addEventListener("click", function click(e) {
if (e.target && e.target.parentNode == menu) {
show(e.target);
}
});
let clearHTTPCache = document.getElementById("clearHTTPCache");
clearHTTPCache.addEventListener("click", async function () {
Services.cache2.clear();
});
let dnsLookupButton = document.getElementById("dnsLookupButton");
dnsLookupButton.addEventListener("click", function () {
doLookup();
});
let hostInput = document.getElementById("host");
hostInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
doLookup();
}
});
let clearDNSCache = document.getElementById("clearDNSCache");
clearDNSCache.addEventListener("click", function () {
Services.dns.clearCache(true);
});
if (location.hash) {
let sectionButton = document.getElementById(
"category-" + location.hash.substring(1)
);
if (sectionButton) {
sectionButton.click();
}
}
}
function show(button) {
let current_tab = document.querySelector(".active");
let category = button.getAttribute("id").substring("category-".length);
let content = document.getElementById(category);
if (current_tab == content) {
return;
}
current_tab.classList.remove("active");
current_tab.hidden = true;
content.classList.add("active");
content.hidden = false;
let current_button = document.querySelector("[selected=true]");
current_button.removeAttribute("selected");
button.setAttribute("selected", "true");
let autoRefresh = document.getElementById("autorefcheck");
if (autoRefresh.checked) {
clearInterval(autoRefresh.interval);
setAutoRefreshInterval(autoRefresh);
}
let title = document.getElementById("sectionTitle");
title.textContent = button.children[0].textContent;
location.hash = category;
}
function setAutoRefreshInterval(checkBox) {
let active_tab = document.querySelector(".active");
checkBox.interval = setInterval(function () {
requestNetworkingDataForTab(active_tab.id);
}, REFRESH_INTERVAL_MS);
}
// We use the pageshow event instead of onload. This is needed because sometimes
// the page is loaded via session-restore/bfcache. In such cases we need to call
// init() to keep the page behaviour consistent with the ticked checkboxes.
// Mostly the issue is with the autorefresh checkbox.
window.addEventListener("pageshow", function () {
init();
});
function doLookup() {
let host = document.getElementById("host").value;
if (host) {
try {
gDashboard.requestDNSLookup(host, displayDNSLookup);
} catch (e) {}
try {
gDashboard.requestDNSHTTPSRRLookup(host, displayHTTPSRRLookup);
} catch (e) {}
}
}
function displayDNSLookup(data) {
let cont = document.getElementById("dnslookuptool_content");
let parent = cont.parentNode;
let new_cont = document.createElement("tbody");
new_cont.setAttribute("id", "dnslookuptool_content");
if (data.answer) {
for (let address of data.address) {
let row = document.createElement("tr");
row.appendChild(col(address));
new_cont.appendChild(row);
}
} else {
new_cont.appendChild(col(data.error));
}
parent.replaceChild(new_cont, cont);
}
function displayHTTPSRRLookup(data) {
let cont = document.getElementById("https_rr_content");
let parent = cont.parentNode;
let new_cont = document.createElement("tbody");
new_cont.setAttribute("id", "https_rr_content");
if (data.answer) {
for (let record of data.records) {
let row = document.createElement("tr");
let alpn = record.alpn ? `alpn="${record.alpn.alpn}" ` : "";
let noDefaultAlpn = record.noDefaultAlpn ? "noDefaultAlpn " : "";
let port = record.port ? `port="${record.port.port}" ` : "";
let echConfig = record.echConfig
? `echConfig="${record.echConfig.echConfig}" `
: "";
let ODoHConfig = record.ODoHConfig
? `odoh="${record.ODoHConfig.ODoHConfig}" `
: "";
let ipv4hint = "";
let ipv6hint = "";
if (record.ipv4Hint) {
let ipv4Str = "";
for (let addr of record.ipv4Hint.address) {
ipv4Str += `${addr}, `;
}
// Remove ", " at the end.
ipv4Str = ipv4Str.slice(0, -2);
ipv4hint = `ipv4hint="${ipv4Str}" `;
}
if (record.ipv6Hint) {
let ipv6Str = "";
for (let addr of record.ipv6Hint.address) {
ipv6Str += `${addr}, `;
}
// Remove ", " at the end.
ipv6Str = ipv6Str.slice(0, -2);
ipv6hint = `ipv6hint="${ipv6Str}" `;
}
let str = `${record.priority} ${record.targetName} `;
str += `(${alpn}${noDefaultAlpn}${port}`;
str += `${ipv4hint}${echConfig}${ipv6hint}`;
str += `${ODoHConfig})`;
row.appendChild(col(str));
new_cont.appendChild(row);
}
} else {
new_cont.appendChild(col(data.error));
}
parent.replaceChild(new_cont, cont);
}