Add HTTP headers checker and tests
This commit is contained in:
+350
-54
@@ -2,16 +2,21 @@
|
||||
// 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;
|
||||
|
||||
// Update the active tab.
|
||||
// Mark only the selected tab as active.
|
||||
toolTabs.forEach((currentTab) => {
|
||||
currentTab.classList.toggle("active", currentTab === tab);
|
||||
currentTab.classList.toggle(
|
||||
"active",
|
||||
currentTab === tab
|
||||
);
|
||||
});
|
||||
|
||||
// Show only the selected tool panel.
|
||||
@@ -29,58 +34,82 @@ toolTabs.forEach((tab) => {
|
||||
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 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 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.");
|
||||
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.");
|
||||
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`;
|
||||
responseTime.textContent =
|
||||
`${result.responseTimeMs} ms`;
|
||||
redirectCount.textContent = result.redirectCount;
|
||||
checkedAt.textContent = new Date(result.checkedAt).toLocaleString();
|
||||
|
||||
checkedAt.textContent =
|
||||
new Date(result.checkedAt).toLocaleString();
|
||||
|
||||
finalUrl.textContent = result.finalUrl;
|
||||
finalUrl.href = result.finalUrl;
|
||||
|
||||
statusMessage.textContent = "Website check completed.";
|
||||
statusMessage.textContent =
|
||||
"Website check completed.";
|
||||
|
||||
resultPanel.hidden = false;
|
||||
} catch (error) {
|
||||
statusMessage.textContent = error.message;
|
||||
@@ -97,36 +126,52 @@ uptimeForm.addEventListener("submit", async (event) => {
|
||||
// ---------------------------------------------------------
|
||||
|
||||
const dnsForm = document.getElementById("dns-form");
|
||||
const hostnameInput = document.getElementById("hostname-input");
|
||||
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 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");
|
||||
const dnsCheckedAt = document.getElementById(
|
||||
"dns-checked-at"
|
||||
);
|
||||
|
||||
// Convert one DNS record into readable text.
|
||||
function formatDnsRecord(recordType, record) {
|
||||
if (recordType === "MX" && typeof record === "object") {
|
||||
// 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 visible record group.
|
||||
// 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;
|
||||
@@ -137,7 +182,12 @@ function createDnsRecordGroup(recordType, records) {
|
||||
|
||||
records.forEach((record) => {
|
||||
const item = document.createElement("li");
|
||||
item.textContent = formatDnsRecord(recordType, record);
|
||||
|
||||
item.textContent = formatDnsRecord(
|
||||
recordType,
|
||||
record
|
||||
);
|
||||
|
||||
list.appendChild(item);
|
||||
});
|
||||
|
||||
@@ -146,37 +196,54 @@ function createDnsRecordGroup(recordType, records) {
|
||||
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.");
|
||||
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"];
|
||||
const recordOrder = [
|
||||
"A",
|
||||
"AAAA",
|
||||
"CNAME",
|
||||
"MX",
|
||||
"TXT",
|
||||
"NS",
|
||||
];
|
||||
|
||||
// Render each supported record type.
|
||||
recordOrder.forEach((recordType) => {
|
||||
const group = createDnsRecordGroup(
|
||||
recordType,
|
||||
@@ -187,7 +254,9 @@ dnsForm.addEventListener("submit", async (event) => {
|
||||
});
|
||||
|
||||
dnsCheckedAt.textContent =
|
||||
`Checked ${new Date(result.checkedAt).toLocaleString()}`;
|
||||
`Checked ${new Date(
|
||||
result.checkedAt
|
||||
).toLocaleString()}`;
|
||||
|
||||
dnsMessage.textContent = "DNS lookup completed.";
|
||||
dnsResultPanel.hidden = false;
|
||||
@@ -199,11 +268,12 @@ dnsForm.addEventListener("submit", async (event) => {
|
||||
dnsButton.textContent = "Look Up DNS";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// SSL Certificate Checker
|
||||
// ---------------------------------------------------------
|
||||
|
||||
// Get references to the SSL form and result elements.
|
||||
const sslForm = document.getElementById("ssl-form");
|
||||
const sslHostnameInput = document.getElementById(
|
||||
"ssl-hostname-input"
|
||||
@@ -217,16 +287,22 @@ const sslResultPanel = document.getElementById(
|
||||
const sslStatusBadge = document.getElementById(
|
||||
"ssl-status-badge"
|
||||
);
|
||||
const sslHostname = document.getElementById("ssl-hostname");
|
||||
const sslHostname = document.getElementById(
|
||||
"ssl-hostname"
|
||||
);
|
||||
const sslDaysRemaining = document.getElementById(
|
||||
"ssl-days-remaining"
|
||||
);
|
||||
const sslValidFrom = document.getElementById("ssl-valid-from");
|
||||
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 sslCheckedAt = document.getElementById(
|
||||
"ssl-checked-at"
|
||||
);
|
||||
|
||||
const sslSubject = document.getElementById("ssl-subject");
|
||||
const sslIssuer = document.getElementById("ssl-issuer");
|
||||
@@ -234,14 +310,16 @@ const sslFingerprint = document.getElementById(
|
||||
"ssl-fingerprint"
|
||||
);
|
||||
|
||||
const sslAuthorizationErrorBlock = document.getElementById(
|
||||
"ssl-authorization-error-block"
|
||||
);
|
||||
const sslAuthorizationErrorBlock =
|
||||
document.getElementById(
|
||||
"ssl-authorization-error-block"
|
||||
);
|
||||
|
||||
const sslAuthorizationError = document.getElementById(
|
||||
"ssl-authorization-error"
|
||||
);
|
||||
|
||||
// Convert certificate date strings into a readable local date.
|
||||
// Convert a certificate date into local readable text.
|
||||
function formatCertificateDate(value) {
|
||||
if (!value) {
|
||||
return "Unknown";
|
||||
@@ -256,9 +334,12 @@ function formatCertificateDate(value) {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
// Choose readable text for the certificate expiration state.
|
||||
// Convert days remaining into readable text.
|
||||
function getExpirationText(daysRemaining) {
|
||||
if (daysRemaining === null || daysRemaining === undefined) {
|
||||
if (
|
||||
daysRemaining === null ||
|
||||
daysRemaining === undefined
|
||||
) {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
@@ -279,19 +360,22 @@ sslForm.addEventListener("submit", async (event) => {
|
||||
|
||||
const hostname = sslHostnameInput.value.trim();
|
||||
|
||||
// Reset the SSL interface before beginning the request.
|
||||
// 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 }),
|
||||
});
|
||||
|
||||
@@ -299,53 +383,60 @@ sslForm.addEventListener("submit", async (event) => {
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
result.error || "The SSL certificate check failed."
|
||||
result.error ||
|
||||
"The SSL certificate check failed."
|
||||
);
|
||||
}
|
||||
|
||||
// Show whether Node trusted the certificate chain and hostname.
|
||||
// Show whether Node trusted the certificate.
|
||||
if (result.authorized) {
|
||||
sslStatusBadge.textContent = "Valid";
|
||||
sslStatusBadge.className = "status-badge online";
|
||||
sslStatusBadge.className =
|
||||
"status-badge online";
|
||||
} else {
|
||||
sslStatusBadge.textContent = "Not trusted";
|
||||
sslStatusBadge.className = "status-badge offline";
|
||||
sslStatusBadge.className =
|
||||
"status-badge offline";
|
||||
}
|
||||
|
||||
// Populate the certificate summary.
|
||||
sslHostname.textContent = result.hostname;
|
||||
sslDaysRemaining.textContent = getExpirationText(
|
||||
result.daysRemaining
|
||||
);
|
||||
|
||||
sslValidFrom.textContent = formatCertificateDate(
|
||||
result.validFrom
|
||||
);
|
||||
sslValidTo.textContent = formatCertificateDate(
|
||||
result.validTo
|
||||
);
|
||||
sslDaysRemaining.textContent =
|
||||
getExpirationText(result.daysRemaining);
|
||||
|
||||
sslProtocol.textContent = result.protocol || "Unknown";
|
||||
sslCipher.textContent = result.cipher || "Unknown";
|
||||
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
|
||||
);
|
||||
sslCheckedAt.textContent =
|
||||
formatCertificateDate(result.checkedAt);
|
||||
|
||||
sslSubject.textContent =
|
||||
result.subject || "Unknown";
|
||||
|
||||
sslIssuer.textContent =
|
||||
result.issuer || "Unknown";
|
||||
|
||||
// Populate the longer certificate values.
|
||||
sslSubject.textContent = result.subject || "Unknown";
|
||||
sslIssuer.textContent = result.issuer || "Unknown";
|
||||
sslFingerprint.textContent =
|
||||
result.fingerprint256 || "Unavailable";
|
||||
|
||||
// Show the authorization problem only when one exists.
|
||||
// Only show the authorization error when one exists.
|
||||
if (result.authorizationError) {
|
||||
sslAuthorizationError.textContent =
|
||||
result.authorizationError;
|
||||
|
||||
sslAuthorizationErrorBlock.hidden = false;
|
||||
} else {
|
||||
sslAuthorizationError.textContent = "";
|
||||
@@ -354,6 +445,7 @@ sslForm.addEventListener("submit", async (event) => {
|
||||
|
||||
sslMessage.textContent =
|
||||
"Certificate check completed.";
|
||||
|
||||
sslResultPanel.hidden = false;
|
||||
} catch (error) {
|
||||
sslMessage.textContent = error.message;
|
||||
@@ -362,4 +454,208 @@ sslForm.addEventListener("submit", async (event) => {
|
||||
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";
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user