Skip to content

Commit

Permalink
Initial RDAP/WHOIS lookup logic
Browse files Browse the repository at this point in the history
  • Loading branch information
MattIPv4 committed Feb 2, 2021
0 parents commit 22e3e64
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.idea
.DS_Store
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "web-whois",
"version": "0.0.1",
"description": "Perform RDAP/WHOIS lookups over HTTP",
"main": "src/index.js",
"keywords": [],
"repository": {
"type": "git",
"url": "git+https://github.com/MattIPv4/web-whois.git"
},
"author": "Matt (IPv4) Cowley",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/MattIPv4/web-whois/issues"
},
"homepage": "https://github.com/MattIPv4/web-whois#readme",
"dependencies": {
"node-fetch": "^2.6.1"
}
}
19 changes: 19 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const rdapLookup = require('./rdap');
const whoisLookup = require('./whois');

module.exports = async (query, combine = false) => {
// Do the RDAP lookup
const rdap = await rdapLookup(query);

// If we have RDAP data and don't need to combine, return it
if (rdap && !combine) return rdap;

// Doo the WHOIS lokkup
const whois = await whoisLookup(query);

// Combine if we can and need to (preferring RDAP)
if (combine && (rdap || whois)) return { ...(whois || {}), ...(rdap || {}) };

// Return just the WHOIS data
return whois;
};
88 changes: 88 additions & 0 deletions src/rdap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const fetch = require('node-fetch');
const { consistentResultObj } = require('./util');

// Unique values in an array, comma-separated
const uniqueCommaSep = arr => [...new Set(arr)].join(', ');

// Find RDAP data entities that match a name
const findEntities = (name, data) => data.entities && data.entities.filter(entity =>
entity.roles.map(role => role.trim().toLowerCase()).includes(name));

// Find a specific vcard for an RDAP entity
const entityVcard = (entity, vcard) => {
if (entity && entity.vcardArray && Array.isArray(entity.vcardArray) && entity.vcardArray.length > 1) {
const entityFn = entity.vcardArray[1].find(card => card[0] === vcard);
if (entityFn && Array.isArray(entityFn) && entityFn.length > 3) return entityFn[3];
}
};

// Find a vcard name/handle for an RDAP entity
const findEntityName = (name, data) => {
const entities = findEntities(name, data);
if (!entities) return;

return uniqueCommaSep(entities.map(entity => entityVcard(entity, 'fn') || (entity && entity.handle)) || []);
};

// Find a vcard email address for an RDAP entity
const findEntityEmail = (name, data) => {
const entities = findEntities(name, data);
if (!entities) return;

return uniqueCommaSep(entities.map(entity => entityVcard(entity, 'email')) || []);
};

// Get a JS Date for a specific RDAP data event
const findEventDate = (name, data) => {
if (!data.events) return;
const event = data.events.find(event => event.eventAction.trim().toLowerCase() === name);
if (!event || !event.eventDate) return;
return new Date(event.eventDate);
};

// Find the ASN information from the RDAP data
const findAsn = data => uniqueCommaSep((data.arin_originas0_originautnums || []).map(asn => asn.toString()));

// Format RDAP CIDR data
const formatCidr = cidr => cidr && (cidr.v4prefix || cidr.v6prefix) && cidr.length
? (cidr.v4prefix || cidr.v6prefix) + '/' + cidr.length.toString()
: undefined;

// Find the CIDR blocks from the RDAP data
const findCidr = data => uniqueCommaSep((data.cidr0_cidrs || []).map(formatCidr).filter(cidr => cidr !== undefined));

// Find the abuse email in the RDAP data
const findAbuseEmail = data => {
const directAbuse = findEntityEmail('abuse', data);
if (directAbuse) return directAbuse;

const registrarEntities = findEntities('registrar', data);
if (!registrarEntities) return;

return findEntityEmail('abuse', { entities: registrarEntities.map(entity => entity.entities).flat(1) });
};

module.exports = async query => {
const resp = await fetch(`https://rdap.cloud/api/v1/${query}`);
const rawData = await resp.json().catch(() => false);
const data = rawData && rawData.results && rawData.results[query];

// Ensure the data is there
if (!data || !data.success || !data.data)
return false;

// Find the useful information for us
const result = consistentResultObj({
name: data.data.name,
registrant: findEntityName('registrant', data.data),
asn: findAsn(data.data),
registrar: findEntityName('registrar', data.data),
registration: findEventDate('registration', data.data),
expiration: findEventDate('expiration', data.data),
cidr: findCidr(data.data),
abuse: findAbuseEmail(data.data),
});

// Return false if we found nothing, otherwise return the data
return Object.values(result).every(x => x === undefined) ? false : result;
};
10 changes: 10 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports.consistentResultObj = data => ({
name: data.name || undefined,
registrant: data.registrant || undefined,
asn: data.asn || undefined,
registrar: data.registrar || undefined,
registration: data.registration || undefined,
expiration: data.expiration || undefined,
cidr: data.cidr || undefined,
abuse: data.abuse || undefined,
});
88 changes: 88 additions & 0 deletions src/whois.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const fetch = require('node-fetch');
const { consistentResultObj } = require('./util');

const parseWhois = text => {
// RegExp parts
const reLinebreak = '\\r\\n';
const reWhitespace = `[^\\S${reLinebreak}]`;
const reKey = '([a-zA-Z\\s]+):';
const reText = `([^${reLinebreak}]+)`;
const reLineStart = `^${reWhitespace}*${reKey}`;
const reLineEnd = `${reWhitespace}+${reText}$`;

// The RegExps to be used
const reSingleLine = `${reLineStart}${reLineEnd}`;
const regExpSingleLineGm = new RegExp(reSingleLine, 'gm');
const regExpSingleLine = new RegExp(reSingleLine);
const reSplitLine = `${reLineStart}[${reLinebreak}]+${reLineEnd}`;
const regExpSplitLineGm = new RegExp(reSplitLine, 'gm');
const regExpSplitLine = new RegExp(reSplitLine);

// Find the matches in the string
const singleLineMatches = text.match(regExpSingleLineGm) || [];
const splitLineMatches = text.match(regExpSplitLineGm) || [];
const matches = [];

// All single line matches are valid
for (const rawMatch of singleLineMatches) {
const match = rawMatch.trim().match(regExpSingleLine);
matches.push({
key: match[1],
value: match[2],
});
}

// Split line matches that don't include a single line match are valid
for (const rawMatch of splitLineMatches) {
if (singleLineMatches.map(singleLineMatch => rawMatch.includes(singleLineMatch)).includes(true))
continue;

const match = rawMatch.trim().match(regExpSplitLine);
matches.push({
key: match[1],
value: match[2],
});
}

// Return the final parsed data
return matches;
};

// Find an attribute value from the WHOIS data
const findAttribute = (name, data) => {
const entry = data.find(entry => entry.key.trim().toLowerCase() === name);
return entry && entry.value && `${entry.value}`.trim();
};

// Find a JS Date for an attribute from the WHOIS data
const findAttributeDate = (name, data) => {
const attribute = findAttribute(name, data);
if (!attribute) return;
return new Date(attribute);
};

module.exports = async query => {
const resp = await fetch(`https://whoisjs.com/api/v1/${query}`);
const rawData = await resp.json().catch(() => false);

// Ensure the data is there
if (!rawData || !rawData.success || !rawData.raw)
return false;

// Parse ourselves
const data = parseWhois(rawData.raw);
if (!data)
return false;

// Find the useful information for us
const result = consistentResultObj({
registrant: findAttribute('registrant', data),
registrar: findAttribute('registrar', data) || findAttribute('organisation', data),
registration: findAttributeDate('creation date', data) || findAttributeDate('registered on', data),
expiration: findAttributeDate('registry expiry date', data) || findAttributeDate('expiry date', data),
abuse: findAttribute('registrar abuse contact email', data),
});

// Return false if we found nothing, otherwise return the data
return Object.values(result).every(x => x === undefined) ? false : result;
};

0 comments on commit 22e3e64

Please sign in to comment.