Add DNS lookup tool and tests

This commit is contained in:
Keith Casko
2026-07-23 02:24:31 -04:00
parent dd5092d352
commit 28ccd40686
9 changed files with 666 additions and 30 deletions
+58
View File
@@ -0,0 +1,58 @@
// Maximum length allowed by the DNS hostname standard.
const MAX_HOSTNAME_LENGTH = 253;
// Validate and normalize a submitted hostname.
function validateHostname(value) {
// Make sure the submitted value is text and is not empty.
if (typeof value !== "string" || value.trim() === "") {
throw new Error("A hostname is required.");
}
// Remove surrounding spaces and normalize uppercase letters.
let hostname = value.trim().toLowerCase();
// Allow users to paste a complete website URL.
// Example: https://example.com/page becomes example.com.
if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
try {
hostname = new URL(hostname).hostname.toLowerCase();
} catch {
throw new Error("Enter a valid hostname or website URL.");
}
}
// Remove a trailing period from a fully qualified hostname.
// Example: example.com. becomes example.com.
if (hostname.endsWith(".")) {
hostname = hostname.slice(0, -1);
}
// DNS hostnames cannot exceed 253 characters.
if (hostname.length > MAX_HOSTNAME_LENGTH) {
throw new Error("The hostname is too long.");
}
// Require normal DNS labels separated by periods.
// Letters, numbers, and interior hyphens are allowed.
const hostnamePattern =
/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
if (!hostnamePattern.test(hostname)) {
throw new Error("Enter a valid public hostname, such as example.com.");
}
// Block local-only hostnames.
if (
hostname === "localhost" ||
hostname.endsWith(".localhost") ||
hostname.endsWith(".local")
) {
throw new Error("Local hostnames are not allowed.");
}
return hostname;
}
module.exports = {
validateHostname,
};
+26
View File
@@ -0,0 +1,26 @@
const express = require("express");
const { lookupDns } = require("../services/dnsService");
const { validateHostname } = require("../middleware/validateHostname");
const { uptimeRateLimiter } = require("../middleware/rateLimit");
const router = express.Router();
// POST /api/dns
router.post("/", uptimeRateLimiter, async (req, res) => {
try {
// Validate and normalize the submitted hostname.
const hostname = validateHostname(req.body.hostname);
// Perform the DNS lookup.
const result = await lookupDns(hostname);
return res.json(result);
} catch (error) {
return res.status(400).json({
error: error.message || "The DNS lookup failed.",
});
}
});
module.exports = router;
+6
View File
@@ -7,6 +7,9 @@ const path = require("path");
// Import the uptime API route.
const uptimeRouter = require("./routes/uptime");
// Import the DNS lookup API route.
const dnsRouter = require("./routes/dns");
// Create the Express application.
const app = express();
@@ -23,6 +26,9 @@ app.use(express.static(path.join(__dirname, "..", "public")));
// Handle website uptime checks.
app.use("/api/uptime", uptimeRouter);
// Handle DNS lookup requests.
app.use("/api/dns", dnsRouter);
// Simple health-check route.
app.get("/health", (req, res) => {
res.json({
+65
View File
@@ -0,0 +1,65 @@
// Import Node's built-in DNS promise API.
const dns = require("dns").promises;
// Record types supported by the first DNS Lookup version.
const RECORD_TYPES = ["A", "AAAA", "CNAME", "MX", "TXT", "NS"];
// Errors that usually mean a record type simply does not exist.
const EMPTY_RECORD_ERRORS = new Set([
"ENODATA",
"ENOTFOUND",
"ENOENT",
"ENODOMAIN",
]);
// Look up one DNS record type without crashing the entire request.
async function resolveRecord(hostname, recordType) {
try {
const records = await dns.resolve(hostname, recordType);
// TXT records arrive as arrays of text fragments.
// Join each record into one readable string.
if (recordType === "TXT") {
return records.map((parts) => parts.join(""));
}
return records;
} catch (error) {
// A domain may have an A record but no MX, TXT, or CNAME record.
// That is normal, so return an empty array.
if (EMPTY_RECORD_ERRORS.has(error.code)) {
return [];
}
throw error;
}
}
// Look up all supported DNS record types.
async function lookupDns(hostname) {
const results = {};
// Run all record lookups at the same time.
const lookups = RECORD_TYPES.map(async (recordType) => {
try {
results[recordType] = await resolveRecord(hostname, recordType);
} catch {
// One failed record type should not erase successful results.
results[recordType] = [];
}
});
await Promise.all(lookups);
return {
hostname,
records: results,
checkedAt: new Date().toISOString(),
};
}
module.exports = {
lookupDns,
resolveRecord,
RECORD_TYPES,
};