Files
2026-07-23 05:53:14 -04:00

661 lines
16 KiB
JavaScript

// ---------------------------------------------------------
// Tool tab navigation
// ---------------------------------------------------------
// Find all tool buttons and tool panels.
const toolTabs = document.querySelectorAll(".tool-tab");
const toolPanels = document.querySelectorAll(".tool-panel");
// Switch tools when a tab button is clicked.
toolTabs.forEach((tab) => {
tab.addEventListener("click", () => {
const selectedToolId = tab.dataset.tool;
// Mark only the selected tab as active.
toolTabs.forEach((currentTab) => {
currentTab.classList.toggle(
"active",
currentTab === tab
);
});
// Show only the selected tool panel.
toolPanels.forEach((panel) => {
panel.hidden = panel.id !== selectedToolId;
});
});
});
// ---------------------------------------------------------
// Website Status Checker
// ---------------------------------------------------------
const uptimeForm = document.getElementById("uptime-form");
const urlInput = document.getElementById("url-input");
const checkButton = document.getElementById("check-button");
const statusMessage = document.getElementById(
"status-message"
);
const resultPanel = document.getElementById("result-panel");
const onlineBadge = document.getElementById("online-badge");
const statusCode = document.getElementById("status-code");
const responseTime = document.getElementById(
"response-time"
);
const redirectCount = document.getElementById(
"redirect-count"
);
const checkedAt = document.getElementById("checked-at");
const finalUrl = document.getElementById("final-url");
// Handle Website Status form submissions.
uptimeForm.addEventListener("submit", async (event) => {
event.preventDefault();
const url = urlInput.value.trim();
// Reset the interface before checking.
statusMessage.textContent = "Checking website...";
statusMessage.className = "status-message";
resultPanel.hidden = true;
checkButton.disabled = true;
checkButton.textContent = "Checking...";
try {
const response = await fetch("/api/uptime", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ url }),
});
const result = await response.json();
// Handle HTTP-level errors.
if (!response.ok) {
throw new Error(
result.error || "The request failed."
);
}
// Handle unreachable or blocked destinations.
if (!result.online) {
throw new Error(
result.error ||
"The website could not be reached."
);
}
// Fill in the successful result.
onlineBadge.textContent = "Online";
onlineBadge.className = "status-badge online";
statusCode.textContent = result.statusCode;
responseTime.textContent =
`${result.responseTimeMs} ms`;
redirectCount.textContent = result.redirectCount;
checkedAt.textContent =
new Date(result.checkedAt).toLocaleString();
finalUrl.textContent = result.finalUrl;
finalUrl.href = result.finalUrl;
statusMessage.textContent =
"Website check completed.";
resultPanel.hidden = false;
} catch (error) {
statusMessage.textContent = error.message;
statusMessage.className = "status-message error";
} finally {
checkButton.disabled = false;
checkButton.textContent = "Check Website";
}
});
// ---------------------------------------------------------
// DNS Lookup
// ---------------------------------------------------------
const dnsForm = document.getElementById("dns-form");
const hostnameInput = document.getElementById(
"hostname-input"
);
const dnsButton = document.getElementById("dns-button");
const dnsMessage = document.getElementById("dns-message");
const dnsResultPanel = document.getElementById(
"dns-result-panel"
);
const dnsHostname = document.getElementById(
"dns-hostname"
);
const dnsRecords = document.getElementById("dns-records");
const dnsCheckedAt = document.getElementById(
"dns-checked-at"
);
// Convert one DNS record into readable text.
function formatDnsRecord(recordType, record) {
// MX records are objects containing an exchange and priority.
if (
recordType === "MX" &&
typeof record === "object" &&
record !== null
) {
return `${record.exchange} (priority ${record.priority})`;
}
return String(record);
}
// Build one DNS record section.
function createDnsRecordGroup(recordType, records) {
const group = document.createElement("section");
group.className = "dns-record-group";
const heading = document.createElement("h4");
heading.textContent = recordType;
group.appendChild(heading);
// Show an empty state when no records exist.
if (!Array.isArray(records) || records.length === 0) {
const emptyMessage = document.createElement("p");
emptyMessage.className = "empty-record";
emptyMessage.textContent = "No records found.";
group.appendChild(emptyMessage);
return group;
}
const list = document.createElement("ul");
list.className = "dns-record-list";
records.forEach((record) => {
const item = document.createElement("li");
item.textContent = formatDnsRecord(
recordType,
record
);
list.appendChild(item);
});
group.appendChild(list);
return group;
}
// Handle DNS form submissions.
dnsForm.addEventListener("submit", async (event) => {
event.preventDefault();
const hostname = hostnameInput.value.trim();
// Reset the DNS interface before the request.
dnsMessage.textContent = "Looking up DNS records...";
dnsMessage.className = "status-message";
dnsResultPanel.hidden = true;
dnsButton.disabled = true;
dnsButton.textContent = "Looking Up...";
try {
const response = await fetch("/api/dns", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ hostname }),
});
const result = await response.json();
if (!response.ok) {
throw new Error(
result.error || "The DNS lookup failed."
);
}
dnsHostname.textContent = result.hostname;
// Remove any records from the previous lookup.
dnsRecords.replaceChildren();
const recordOrder = [
"A",
"AAAA",
"CNAME",
"MX",
"TXT",
"NS",
];
// Render each supported record type.
recordOrder.forEach((recordType) => {
const group = createDnsRecordGroup(
recordType,
result.records[recordType]
);
dnsRecords.appendChild(group);
});
dnsCheckedAt.textContent =
`Checked ${new Date(
result.checkedAt
).toLocaleString()}`;
dnsMessage.textContent = "DNS lookup completed.";
dnsResultPanel.hidden = false;
} catch (error) {
dnsMessage.textContent = error.message;
dnsMessage.className = "status-message error";
} finally {
dnsButton.disabled = false;
dnsButton.textContent = "Look Up DNS";
}
});
// ---------------------------------------------------------
// SSL Certificate Checker
// ---------------------------------------------------------
const sslForm = document.getElementById("ssl-form");
const sslHostnameInput = document.getElementById(
"ssl-hostname-input"
);
const sslButton = document.getElementById("ssl-button");
const sslMessage = document.getElementById("ssl-message");
const sslResultPanel = document.getElementById(
"ssl-result-panel"
);
const sslStatusBadge = document.getElementById(
"ssl-status-badge"
);
const sslHostname = document.getElementById(
"ssl-hostname"
);
const sslDaysRemaining = document.getElementById(
"ssl-days-remaining"
);
const sslValidFrom = document.getElementById(
"ssl-valid-from"
);
const sslValidTo = document.getElementById("ssl-valid-to");
const sslProtocol = document.getElementById("ssl-protocol");
const sslCipher = document.getElementById("ssl-cipher");
const sslAddress = document.getElementById("ssl-address");
const sslCheckedAt = document.getElementById(
"ssl-checked-at"
);
const sslSubject = document.getElementById("ssl-subject");
const sslIssuer = document.getElementById("ssl-issuer");
const sslFingerprint = document.getElementById(
"ssl-fingerprint"
);
const sslAuthorizationErrorBlock =
document.getElementById(
"ssl-authorization-error-block"
);
const sslAuthorizationError = document.getElementById(
"ssl-authorization-error"
);
// Convert a certificate date into local readable text.
function formatCertificateDate(value) {
if (!value) {
return "Unknown";
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return date.toLocaleString();
}
// Convert days remaining into readable text.
function getExpirationText(daysRemaining) {
if (
daysRemaining === null ||
daysRemaining === undefined
) {
return "Unknown";
}
if (daysRemaining < 0) {
return `${Math.abs(daysRemaining)} days expired`;
}
if (daysRemaining === 0) {
return "Expires today";
}
return `${daysRemaining} days`;
}
// Handle SSL certificate form submissions.
sslForm.addEventListener("submit", async (event) => {
event.preventDefault();
const hostname = sslHostnameInput.value.trim();
// Reset the SSL interface before the request.
sslMessage.textContent = "Checking certificate...";
sslMessage.className = "status-message";
sslResultPanel.hidden = true;
sslButton.disabled = true;
sslButton.textContent = "Checking...";
try {
const response = await fetch("/api/ssl", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ hostname }),
});
const result = await response.json();
if (!response.ok) {
throw new Error(
result.error ||
"The SSL certificate check failed."
);
}
// Show whether Node trusted the certificate.
if (result.authorized) {
sslStatusBadge.textContent = "Valid";
sslStatusBadge.className =
"status-badge online";
} else {
sslStatusBadge.textContent = "Not trusted";
sslStatusBadge.className =
"status-badge offline";
}
sslHostname.textContent = result.hostname;
sslDaysRemaining.textContent =
getExpirationText(result.daysRemaining);
sslValidFrom.textContent =
formatCertificateDate(result.validFrom);
sslValidTo.textContent =
formatCertificateDate(result.validTo);
sslProtocol.textContent =
result.protocol || "Unknown";
sslCipher.textContent =
result.cipher || "Unknown";
sslAddress.textContent = result.resolvedAddress
? `${result.resolvedAddress} (IPv${result.addressFamily})`
: "Unknown";
sslCheckedAt.textContent =
formatCertificateDate(result.checkedAt);
sslSubject.textContent =
result.subject || "Unknown";
sslIssuer.textContent =
result.issuer || "Unknown";
sslFingerprint.textContent =
result.fingerprint256 || "Unavailable";
// Only show the authorization error when one exists.
if (result.authorizationError) {
sslAuthorizationError.textContent =
result.authorizationError;
sslAuthorizationErrorBlock.hidden = false;
} else {
sslAuthorizationError.textContent = "";
sslAuthorizationErrorBlock.hidden = true;
}
sslMessage.textContent =
"Certificate check completed.";
sslResultPanel.hidden = false;
} catch (error) {
sslMessage.textContent = error.message;
sslMessage.className = "status-message error";
} finally {
sslButton.disabled = false;
sslButton.textContent = "Check Certificate";
}
});
// ---------------------------------------------------------
// HTTP Headers Checker
// ---------------------------------------------------------
const headersForm = document.getElementById(
"headers-form"
);
const headersUrlInput = document.getElementById(
"headers-url-input"
);
const headersButton = document.getElementById(
"headers-button"
);
const headersMessage = document.getElementById(
"headers-message"
);
const headersResultPanel = document.getElementById(
"headers-result-panel"
);
const headersScoreBadge = document.getElementById(
"headers-score-badge"
);
const headersStatus = document.getElementById(
"headers-status"
);
const headersRedirects = document.getElementById(
"headers-redirects"
);
const headersScore = document.getElementById(
"headers-score"
);
const headersCheckedAt = document.getElementById(
"headers-checked-at"
);
const headersFinalUrl = document.getElementById(
"headers-final-url"
);
const securityHeaderResults = document.getElementById(
"security-header-results"
);
const allHeaderResults = document.getElementById(
"all-header-results"
);
// Build one visible security-header result.
function createSecurityHeaderItem(header) {
const item = document.createElement("article");
item.className = "security-header-item";
const heading = document.createElement("div");
heading.className = "security-header-heading";
const name = document.createElement("strong");
name.textContent = header.name;
const status = document.createElement("span");
status.className = header.present
? "header-status present"
: "header-status missing";
status.textContent = header.present
? "Present"
: "Missing";
heading.append(name, status);
const purpose = document.createElement("p");
purpose.className = "security-header-purpose";
purpose.textContent = header.purpose;
item.append(heading, purpose);
// Show the raw header value when present.
if (header.present && header.value) {
const value = document.createElement("p");
value.className = "security-header-value";
const code = document.createElement("code");
code.textContent = header.value;
value.appendChild(code);
item.appendChild(value);
}
return item;
}
// Build one raw response-header row.
function createResponseHeaderItem(name, value) {
const item = document.createElement("div");
item.className = "response-header-item";
const nameElement = document.createElement("div");
nameElement.className = "response-header-name";
nameElement.textContent = name;
const valueElement = document.createElement("div");
valueElement.className = "response-header-value";
valueElement.textContent = value;
item.append(nameElement, valueElement);
return item;
}
// Handle HTTP header form submissions.
headersForm.addEventListener("submit", async (event) => {
event.preventDefault();
const url = headersUrlInput.value.trim();
// Reset the interface before checking.
headersMessage.textContent =
"Checking response headers...";
headersMessage.className = "status-message";
headersResultPanel.hidden = true;
headersButton.disabled = true;
headersButton.textContent = "Checking...";
try {
const response = await fetch("/api/headers", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ url }),
});
const result = await response.json();
if (!response.ok) {
throw new Error(
result.error ||
"The HTTP header check failed."
);
}
// Fill in the summary information.
headersStatus.textContent =
`${result.statusCode} ${
result.statusText || ""
}`.trim();
headersRedirects.textContent =
result.redirectCount;
headersScore.textContent =
`${result.presentSecurityHeaderCount} of ` +
`${result.totalSecurityHeaderCount}`;
headersScoreBadge.textContent =
`${result.presentSecurityHeaderCount}/` +
`${result.totalSecurityHeaderCount}`;
headersCheckedAt.textContent =
new Date(result.checkedAt).toLocaleString();
headersFinalUrl.textContent = result.finalUrl;
headersFinalUrl.href = result.finalUrl;
// Remove old security header results.
securityHeaderResults.replaceChildren();
result.securityHeaders.forEach((header) => {
securityHeaderResults.appendChild(
createSecurityHeaderItem(header)
);
});
// Remove old raw response headers.
allHeaderResults.replaceChildren();
// Render all response headers alphabetically.
Object.entries(result.headers)
.sort(([nameA], [nameB]) =>
nameA.localeCompare(nameB)
)
.forEach(([name, value]) => {
allHeaderResults.appendChild(
createResponseHeaderItem(name, value)
);
});
headersMessage.textContent =
"Header check completed.";
headersResultPanel.hidden = false;
} catch (error) {
headersMessage.textContent = error.message;
headersMessage.className = "status-message error";
} finally {
headersButton.disabled = false;
headersButton.textContent = "Check Headers";
}
});