Skip to content

Commit 22e3e64

Browse files
committed
Initial RDAP/WHOIS lookup logic
0 parents  commit 22e3e64

File tree

7 files changed

+241
-0
lines changed

7 files changed

+241
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
.idea
3+
.DS_Store

package-lock.json

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "web-whois",
3+
"version": "0.0.1",
4+
"description": "Perform RDAP/WHOIS lookups over HTTP",
5+
"main": "src/index.js",
6+
"keywords": [],
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/MattIPv4/web-whois.git"
10+
},
11+
"author": "Matt (IPv4) Cowley",
12+
"license": "Apache-2.0",
13+
"bugs": {
14+
"url": "https://github.com/MattIPv4/web-whois/issues"
15+
},
16+
"homepage": "https://github.com/MattIPv4/web-whois#readme",
17+
"dependencies": {
18+
"node-fetch": "^2.6.1"
19+
}
20+
}

src/index.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const rdapLookup = require('./rdap');
2+
const whoisLookup = require('./whois');
3+
4+
module.exports = async (query, combine = false) => {
5+
// Do the RDAP lookup
6+
const rdap = await rdapLookup(query);
7+
8+
// If we have RDAP data and don't need to combine, return it
9+
if (rdap && !combine) return rdap;
10+
11+
// Doo the WHOIS lokkup
12+
const whois = await whoisLookup(query);
13+
14+
// Combine if we can and need to (preferring RDAP)
15+
if (combine && (rdap || whois)) return { ...(whois || {}), ...(rdap || {}) };
16+
17+
// Return just the WHOIS data
18+
return whois;
19+
};

src/rdap.js

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
const fetch = require('node-fetch');
2+
const { consistentResultObj } = require('./util');
3+
4+
// Unique values in an array, comma-separated
5+
const uniqueCommaSep = arr => [...new Set(arr)].join(', ');
6+
7+
// Find RDAP data entities that match a name
8+
const findEntities = (name, data) => data.entities && data.entities.filter(entity =>
9+
entity.roles.map(role => role.trim().toLowerCase()).includes(name));
10+
11+
// Find a specific vcard for an RDAP entity
12+
const entityVcard = (entity, vcard) => {
13+
if (entity && entity.vcardArray && Array.isArray(entity.vcardArray) && entity.vcardArray.length > 1) {
14+
const entityFn = entity.vcardArray[1].find(card => card[0] === vcard);
15+
if (entityFn && Array.isArray(entityFn) && entityFn.length > 3) return entityFn[3];
16+
}
17+
};
18+
19+
// Find a vcard name/handle for an RDAP entity
20+
const findEntityName = (name, data) => {
21+
const entities = findEntities(name, data);
22+
if (!entities) return;
23+
24+
return uniqueCommaSep(entities.map(entity => entityVcard(entity, 'fn') || (entity && entity.handle)) || []);
25+
};
26+
27+
// Find a vcard email address for an RDAP entity
28+
const findEntityEmail = (name, data) => {
29+
const entities = findEntities(name, data);
30+
if (!entities) return;
31+
32+
return uniqueCommaSep(entities.map(entity => entityVcard(entity, 'email')) || []);
33+
};
34+
35+
// Get a JS Date for a specific RDAP data event
36+
const findEventDate = (name, data) => {
37+
if (!data.events) return;
38+
const event = data.events.find(event => event.eventAction.trim().toLowerCase() === name);
39+
if (!event || !event.eventDate) return;
40+
return new Date(event.eventDate);
41+
};
42+
43+
// Find the ASN information from the RDAP data
44+
const findAsn = data => uniqueCommaSep((data.arin_originas0_originautnums || []).map(asn => asn.toString()));
45+
46+
// Format RDAP CIDR data
47+
const formatCidr = cidr => cidr && (cidr.v4prefix || cidr.v6prefix) && cidr.length
48+
? (cidr.v4prefix || cidr.v6prefix) + '/' + cidr.length.toString()
49+
: undefined;
50+
51+
// Find the CIDR blocks from the RDAP data
52+
const findCidr = data => uniqueCommaSep((data.cidr0_cidrs || []).map(formatCidr).filter(cidr => cidr !== undefined));
53+
54+
// Find the abuse email in the RDAP data
55+
const findAbuseEmail = data => {
56+
const directAbuse = findEntityEmail('abuse', data);
57+
if (directAbuse) return directAbuse;
58+
59+
const registrarEntities = findEntities('registrar', data);
60+
if (!registrarEntities) return;
61+
62+
return findEntityEmail('abuse', { entities: registrarEntities.map(entity => entity.entities).flat(1) });
63+
};
64+
65+
module.exports = async query => {
66+
const resp = await fetch(`https://rdap.cloud/api/v1/${query}`);
67+
const rawData = await resp.json().catch(() => false);
68+
const data = rawData && rawData.results && rawData.results[query];
69+
70+
// Ensure the data is there
71+
if (!data || !data.success || !data.data)
72+
return false;
73+
74+
// Find the useful information for us
75+
const result = consistentResultObj({
76+
name: data.data.name,
77+
registrant: findEntityName('registrant', data.data),
78+
asn: findAsn(data.data),
79+
registrar: findEntityName('registrar', data.data),
80+
registration: findEventDate('registration', data.data),
81+
expiration: findEventDate('expiration', data.data),
82+
cidr: findCidr(data.data),
83+
abuse: findAbuseEmail(data.data),
84+
});
85+
86+
// Return false if we found nothing, otherwise return the data
87+
return Object.values(result).every(x => x === undefined) ? false : result;
88+
};

src/util.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports.consistentResultObj = data => ({
2+
name: data.name || undefined,
3+
registrant: data.registrant || undefined,
4+
asn: data.asn || undefined,
5+
registrar: data.registrar || undefined,
6+
registration: data.registration || undefined,
7+
expiration: data.expiration || undefined,
8+
cidr: data.cidr || undefined,
9+
abuse: data.abuse || undefined,
10+
});

src/whois.js

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
const fetch = require('node-fetch');
2+
const { consistentResultObj } = require('./util');
3+
4+
const parseWhois = text => {
5+
// RegExp parts
6+
const reLinebreak = '\\r\\n';
7+
const reWhitespace = `[^\\S${reLinebreak}]`;
8+
const reKey = '([a-zA-Z\\s]+):';
9+
const reText = `([^${reLinebreak}]+)`;
10+
const reLineStart = `^${reWhitespace}*${reKey}`;
11+
const reLineEnd = `${reWhitespace}+${reText}$`;
12+
13+
// The RegExps to be used
14+
const reSingleLine = `${reLineStart}${reLineEnd}`;
15+
const regExpSingleLineGm = new RegExp(reSingleLine, 'gm');
16+
const regExpSingleLine = new RegExp(reSingleLine);
17+
const reSplitLine = `${reLineStart}[${reLinebreak}]+${reLineEnd}`;
18+
const regExpSplitLineGm = new RegExp(reSplitLine, 'gm');
19+
const regExpSplitLine = new RegExp(reSplitLine);
20+
21+
// Find the matches in the string
22+
const singleLineMatches = text.match(regExpSingleLineGm) || [];
23+
const splitLineMatches = text.match(regExpSplitLineGm) || [];
24+
const matches = [];
25+
26+
// All single line matches are valid
27+
for (const rawMatch of singleLineMatches) {
28+
const match = rawMatch.trim().match(regExpSingleLine);
29+
matches.push({
30+
key: match[1],
31+
value: match[2],
32+
});
33+
}
34+
35+
// Split line matches that don't include a single line match are valid
36+
for (const rawMatch of splitLineMatches) {
37+
if (singleLineMatches.map(singleLineMatch => rawMatch.includes(singleLineMatch)).includes(true))
38+
continue;
39+
40+
const match = rawMatch.trim().match(regExpSplitLine);
41+
matches.push({
42+
key: match[1],
43+
value: match[2],
44+
});
45+
}
46+
47+
// Return the final parsed data
48+
return matches;
49+
};
50+
51+
// Find an attribute value from the WHOIS data
52+
const findAttribute = (name, data) => {
53+
const entry = data.find(entry => entry.key.trim().toLowerCase() === name);
54+
return entry && entry.value && `${entry.value}`.trim();
55+
};
56+
57+
// Find a JS Date for an attribute from the WHOIS data
58+
const findAttributeDate = (name, data) => {
59+
const attribute = findAttribute(name, data);
60+
if (!attribute) return;
61+
return new Date(attribute);
62+
};
63+
64+
module.exports = async query => {
65+
const resp = await fetch(`https://whoisjs.com/api/v1/${query}`);
66+
const rawData = await resp.json().catch(() => false);
67+
68+
// Ensure the data is there
69+
if (!rawData || !rawData.success || !rawData.raw)
70+
return false;
71+
72+
// Parse ourselves
73+
const data = parseWhois(rawData.raw);
74+
if (!data)
75+
return false;
76+
77+
// Find the useful information for us
78+
const result = consistentResultObj({
79+
registrant: findAttribute('registrant', data),
80+
registrar: findAttribute('registrar', data) || findAttribute('organisation', data),
81+
registration: findAttributeDate('creation date', data) || findAttributeDate('registered on', data),
82+
expiration: findAttributeDate('registry expiry date', data) || findAttributeDate('expiry date', data),
83+
abuse: findAttribute('registrar abuse contact email', data),
84+
});
85+
86+
// Return false if we found nothing, otherwise return the data
87+
return Object.values(result).every(x => x === undefined) ? false : result;
88+
};

0 commit comments

Comments
 (0)