Improved web RDAP parser, fixes #203

This commit is contained in:
Pinga 2025-02-21 06:16:11 +02:00
parent c923aaceb0
commit e3589af8aa

View file

@ -13,14 +13,12 @@ $c['branding'] = isset($c['branding']) ? $c['branding'] : false;
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Domain Lookup</title> <title>Domain Lookup</title>
<style> <style>
/* Resetting and base styles */
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
/* Improved font settings using system fonts */
body { body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #fff; background-color: #fff;
@ -50,7 +48,6 @@ $c['branding'] = isset($c['branding']) ? $c['branding'] : false;
margin-bottom: 20px; margin-bottom: 20px;
} }
/* Input field styling with improved typography */
input[type="text"] { input[type="text"] {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
@ -67,21 +64,18 @@ $c['branding'] = isset($c['branding']) ? $c['branding'] : false;
outline: none; outline: none;
} }
/* General link styles */
a { a {
color: #007BFF; color: #007BFF;
text-decoration: none; text-decoration: none;
transition: color 0.3s ease, text-decoration 0.3s ease; transition: color 0.3s ease, text-decoration 0.3s ease;
} }
/* Hover and focus states for links */
a:hover, a:hover,
a:focus { a:focus {
color: #0056b3; color: #0056b3;
text-decoration: underline; text-decoration: underline;
} }
/* CAPTCHA container styling */
.captcha-container { .captcha-container {
display: flex; display: flex;
align-items: center; align-items: center;
@ -111,7 +105,6 @@ $c['branding'] = isset($c['branding']) ? $c['branding'] : false;
outline: none; outline: none;
} }
/* Button styling */
.buttons { .buttons {
display: flex; display: flex;
gap: 10px; gap: 10px;
@ -134,7 +127,6 @@ $c['branding'] = isset($c['branding']) ? $c['branding'] : false;
transform: scale(1.05); transform: scale(1.05);
} }
/* Result display area */
pre { pre {
white-space: pre-wrap; white-space: pre-wrap;
white-space: -moz-pre-wrap; white-space: -moz-pre-wrap;
@ -154,7 +146,6 @@ $c['branding'] = isset($c['branding']) ? $c['branding'] : false;
font-size: 1rem; font-size: 1rem;
} }
/* Footer styling */
footer { footer {
margin-top: 40px; margin-top: 40px;
font-size: 0.9rem; font-size: 0.9rem;
@ -320,9 +311,10 @@ $c['branding'] = isset($c['branding']) ? $c['branding'] : false;
if (data.error) { if (data.error) {
console.error('Error:', data.error); console.error('Error:', data.error);
document.getElementById('result').innerText = 'Error: ' + data.error; document.getElementById('result').innerText = 'Error: ' + data.error;
document.getElementById('bottom').style.display = 'block';
} else { } else {
// Parse and display RDAP data // Parse and display RDAP data
let output = parseRdapResponse(data); let output = parseRDAP(data);
document.getElementById('result').innerText = output; document.getElementById('result').innerText = output;
document.getElementById('bottom').style.display = 'block'; document.getElementById('bottom').style.display = 'block';
if (captchaInput && !captchaInput.disabled) { if (captchaInput && !captchaInput.disabled) {
@ -333,113 +325,176 @@ $c['branding'] = isset($c['branding']) ? $c['branding'] : false;
}) })
.catch(error => { .catch(error => {
console.error('Error:', error); // Log the error to the console console.error('Error:', error); // Log the error to the console
document.getElementById('result').innerText = 'Error: ' + error.message; // Display the error message on the page document.getElementById('result').innerText = 'Error: ' + error.message;
document.getElementById('bottom').style.display = 'block';
}); });
}); });
}); });
function parseRdapResponse(data) { /**
let output = ''; * Flattens the "entities" field.
* The RDAP JSON sometimes nests arrays of entities.
// Domain Name and Status */
output += 'Domain Name: ' + (data.ldhName || 'N/A') + '\n'; function flattenEntities(entities) {
output += 'Status: ' + (data.status ? data.status.join(', ') : 'N/A') + '\n\n'; let flat = [];
entities.forEach(item => {
// Parsing entities for specific roles like registrar and registrant if (Array.isArray(item)) {
if (data.entities && data.entities.length > 0) { flat = flat.concat(item);
data.entities.forEach(entity => { } else if (typeof item === "object" && item !== null) {
output += parseEntity(entity); flat.push(item);
}); // If an entity contains a nested entities array (for example, abuse contacts inside registrar)
if (item.entities && Array.isArray(item.entities)) {
flat = flat.concat(flattenEntities(item.entities));
}
} }
});
// Nameservers return flat;
if (data.nameservers && data.nameservers.length > 0) {
output += 'Nameservers:\n';
data.nameservers.forEach(ns => {
output += ' - ' + ns.ldhName + '\n';
});
output += '\n';
}
// SecureDNS Details
if (data.secureDNS) {
output += 'SecureDNS:\n';
output += ' - Delegation Signed: ' + (data.secureDNS.delegationSigned ? 'Yes' : 'No') + '\n';
output += ' - Zone Signed: ' + (data.secureDNS.zoneSigned ? 'Yes' : 'No') + '\n\n';
}
// Events (like registration, expiration dates)
if (data.events && data.events.length > 0) {
output += 'Events:\n';
data.events.forEach(event => {
output += ' - ' + event.eventAction + ': ' + new Date(event.eventDate).toLocaleString() + '\n';
});
output += '\n';
}
// Domain Status and Notices
if (data.notices && data.notices.length > 0) {
output += 'Notices:\n';
data.notices.forEach(notice => {
output += ' - ' + (notice.title || 'Notice') + ': ' + notice.description.join(' ') + '\n';
});
}
return output;
} }
function parseEntity(entity) { /**
let output = ''; * Helper to extract a vCard field value by key from a vcardArray.
*/
if (entity.roles) { function getVCardValue(vcardArray, key) {
output += entity.roles.join(', ').toUpperCase() + ' Contact:\n'; if (!vcardArray || vcardArray.length < 2) return null;
if (entity.vcardArray && entity.vcardArray.length > 1) { const props = vcardArray[1];
output += parseVcard(entity.vcardArray[1]); const field = props.find(item => item[0] === key);
} return field ? field[3] : null;
if (entity.roles.includes('registrar') && entity.publicIds) {
output += ' IANA ID: ' + entity.publicIds.map(id => id.identifier).join(', ') + '\n';
}
if (entity.roles.includes('abuse') && entity.vcardArray) {
const emailEntry = entity.vcardArray[1].find(entry => entry[0] === 'email');
if (emailEntry) {
output += ' Abuse Email: ' + emailEntry[3] + '\n';
}
}
output += '\n';
}
if (entity.entities && entity.entities.length > 0) {
entity.entities.forEach(subEntity => {
output += parseEntity(subEntity);
});
}
return output;
} }
function parseVcard(vcard) { /**
let vcardOutput = ''; * Main parser: Takes the RDAP JSON object and returns a WHOIS-style text output.
vcard.forEach(entry => { */
switch (entry[0]) { function parseRDAP(data) {
case 'fn': let output = "";
vcardOutput += ' Name: ' + entry[3] + '\n';
break; // Domain basic details
case 'adr': output += `Domain Name: ${(data.ldhName || "N/A").toUpperCase()}\n`;
if (Array.isArray(entry[3]) && entry[3].length > 0) { output += `Domain ID: ${data.handle || "N/A"}\n\n`;
const addressParts = entry[3];
vcardOutput += ' Address: ' + addressParts.join(', ') + '\n'; // Domain status
} if (data.status && data.status.length) {
break; output += "Status:\n";
case 'email': data.status.forEach(s => {
vcardOutput += ' Email: ' + entry[3] + '\n'; output += ` - ${s}\n`;
break;
case 'tel':
vcardOutput += ' Phone: ' + entry[3] + '\n';
break;
}
}); });
return vcardOutput; output += "\n";
}
// Events (e.g., registration, expiration, last update)
if (data.events && data.events.length) {
output += "Events:\n";
data.events.forEach(event => {
// Capitalize event action for display
const action = event.eventAction.charAt(0).toUpperCase() + event.eventAction.slice(1);
output += ` ${action}: ${event.eventDate}\n`;
});
output += "\n";
}
// Nameservers
if (data.nameservers && data.nameservers.length) {
output += "Nameservers:\n";
data.nameservers.forEach(ns => {
output += ` - ${ns.ldhName || "N/A"}\n`;
});
output += "\n";
}
// Secure DNS info
if (data.secureDNS) {
output += "Secure DNS:\n";
output += ` Zone Signed: ${data.secureDNS.zoneSigned}\n`;
output += ` Delegation Signed: ${data.secureDNS.delegationSigned}\n\n`;
}
// Flatten all entities (registrar, registrant, admin, tech, billing, etc.)
let allEntities = data.entities ? flattenEntities(data.entities) : [];
// Registrar
const registrar = allEntities.find(ent => ent.roles && ent.roles.includes("registrar"));
if (registrar) {
const regName = getVCardValue(registrar.vcardArray, "fn") || "N/A";
output += `Registrar: ${regName}\n`;
let ianaId = "";
if (registrar.publicIds && Array.isArray(registrar.publicIds)) {
const ianaObj = registrar.publicIds.find(pub => pub.type === "IANA Registrar ID");
if (ianaObj) {
ianaId = ianaObj.identifier;
}
}
output += `IANA ID: ${ianaId}\n\n`;
// Look for nested abuse contact within the registrar entity
if (registrar.entities && Array.isArray(registrar.entities)) {
const abuseContact = flattenEntities(registrar.entities).find(ent => ent.roles && ent.roles.includes("abuse"));
if (abuseContact) {
const abuseName = getVCardValue(abuseContact.vcardArray, "fn") || "N/A";
const abuseEmail = getVCardValue(abuseContact.vcardArray, "email") || "N/A";
const abuseTel = getVCardValue(abuseContact.vcardArray, "tel") || "N/A";
output += "Registrar Abuse Contact:\n";
output += ` Name: ${abuseName}\n`;
output += ` Email: ${abuseEmail}\n`;
output += ` Phone: ${abuseTel}\n`;
}
}
output += "\n";
}
// Process other roles: registrant, admin, tech, billing
const rolesToShow = ["registrant", "admin", "tech", "billing"];
rolesToShow.forEach(role => {
// Filter entities by role
const ents = allEntities.filter(ent => ent.roles && ent.roles.includes(role));
if (ents.length) {
ents.forEach(ent => {
const name = getVCardValue(ent.vcardArray, "fn") || "N/A";
output += `${role.charAt(0).toUpperCase() + role.slice(1)} Contact: ${name}\n`;
output += ` Handle: ${ent.handle || "N/A"}\n`;
// Optionally, include organization and address if available
const org = getVCardValue(ent.vcardArray, "org");
if (org) {
output += ` Organization: ${org}\n`;
}
// You can add more fields as needed (e.g., email, phone)
const email = getVCardValue(ent.vcardArray, "email");
if (email) {
output += ` Email: ${email}\n`;
}
const tel = getVCardValue(ent.vcardArray, "tel");
if (tel) {
output += ` Phone: ${tel}\n`;
}
const address = getVCardValue(ent.vcardArray, "adr");
if (address) {
// Since the address is an array, filter out any empty parts and join them
const addrStr = Array.isArray(address) ? address.filter(part => part && part.trim()).join(', ') : address;
output += ` Address: ${addrStr}\n`;
}
output += "\n";
});
}
});
// Notices
if (data.notices && data.notices.length) {
output += "Notices:\n";
data.notices.forEach(notice => {
if (notice.title) {
output += ` ${notice.title}\n`;
}
if (notice.description && Array.isArray(notice.description)) {
notice.description.forEach(desc => {
output += ` ${desc}\n`;
});
}
output += "\n";
});
}
return output;
} }
</script> </script>
</body> </body>
</html> </html>