Gestire le query WHOIS in Node.js
WHOIS è uno dei protocolli più vecchi ancora in circolazione su Internet: una connessione TCP, una riga di testo inviata al server, un blocco di testo restituito in risposta. Nonostante la semplicità, scrivere un client WHOIS affidabile è meno banale di quanto sembri. I server non concordano sul formato delle risposte, sulla codifica dei caratteri, sui limiti di frequenza delle richieste e perfino su quale server sia quello giusto da interrogare. In questo articolo costruiamo passo dopo passo un client WHOIS completo in Node.js, senza dipendenze esterne, partendo da una query elementare fino ad arrivare a un piccolo servizio HTTP con cache, gestione dei referral e limitazione delle richieste.
Prima di cominciare è utile inquadrare lo stato attuale del protocollo. Il 28 gennaio 2025 ICANN ha fatto decadere per registry e registrar dei domini generici (gTLD) l'obbligo contrattuale di offrire il servizio WHOIS sulla porta 43, indicando RDAP come protocollo di riferimento. Questo non significa che WHOIS sia scomparso: molti registry di primo livello nazionali (ccTLD), come quelli di .it e .de, continuano a usarlo, e diversi registry generici, tra cui quello del .com, mantengono attivo il servizio. Un client WHOIS ben scritto resta quindi uno strumento utile, a patto di progettarlo tenendo conto che alcuni server potrebbero non rispondere più.
Come funziona il protocollo
Il protocollo è descritto nella RFC 3912, un documento di poche pagine. Il client apre una connessione TCP verso la porta 43 del server, invia la query terminata dalla sequenza \r\n (carriage return seguito da line feed) e legge tutto ciò che il server restituisce finché quest'ultimo non chiude la connessione. Non esiste un'intestazione che indichi la lunghezza della risposta, non esiste un formato strutturato e non esiste nemmeno una codifica dei caratteri dichiarata: la chiusura della connessione è l'unico segnale che la risposta è completa.
Da queste poche regole derivano tutte le difficoltà pratiche che affronteremo:
- poiché la fine della risposta coincide con la chiusura del socket, un server lento o bloccato può tenere aperta la connessione indefinitamente, quindi servono timeout;
- poiché non c'è un limite dichiarato, un server malevolo o malfunzionante potrebbe inviare quantità arbitrarie di dati, quindi serve un tetto alla dimensione della risposta;
- poiché la codifica non è dichiarata, bisogna tentare una decodifica e prevedere un ripiego;
- poiché ogni server formatta il testo a modo suo, l'interpretazione dei campi richiede euristiche.
C'è poi la questione di quale server interrogare. Ogni dominio di primo livello ha il proprio server WHOIS, e per alcuni registry, come quello del .com, la risposta del registry contiene un riferimento al server WHOIS del registrar presso cui il dominio è stato registrato, che può fornire dati più dettagliati. Il punto di partenza naturale è whois.iana.org, il server di IANA, che per qualsiasi dominio restituisce il record del relativo TLD con l'indicazione del server da contattare.
Una prima query con il modulo net
Il modulo node:net fornisce tutto ciò che serve per aprire una connessione TCP. Una prima versione, volutamente essenziale, si limita a connettersi al server del registry .com, inviare il nome di dominio e stampare la risposta.
import net from 'node:net';
const socket = net.createConnection({ host: 'whois.verisign-grs.com', port: 43 }, () => {
// La query è il nome di dominio seguito da CRLF
socket.write('example.com\r\n');
});
const chunks = [];
// I dati arrivano a blocchi: li accumuliamo fino alla chiusura
socket.on('data', (chunk) => chunks.push(chunk));
socket.on('end', () => {
console.log(Buffer.concat(chunks).toString('utf8'));
});
socket.on('error', (err) => {
console.error(`Errore: ${err.message}`);
});
Eseguendo lo script si ottiene una risposta simile alla seguente, qui abbreviata:
Domain Name: EXAMPLE.COM
Registry Domain ID: 2336799_DOMAIN_COM-VRSN
Registrar WHOIS Server: whois.iana.org
Registrar URL: http://res-dom.iana.org
Updated Date: 2024-08-14T07:01:34Z
Creation Date: 1995-08-14T04:00:00Z
Registry Expiry Date: 2025-08-13T04:00:00Z
Registrar: RESERVED-Internet Assigned Numbers Authority
Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited
Name Server: A.IANA-SERVERS.NET
Name Server: B.IANA-SERVERS.NET
>>> Last update of whois database: 2026-10-16T16:20:11Z <<<
Il codice funziona, ma ha tutti i difetti elencati in precedenza: nessun timeout, nessun limite alla dimensione, decodifica UTF-8 data per scontata e nessuna validazione dell'input. Accumulare i blocchi in un array e concatenarli alla fine, invece di convertirli in stringa uno alla volta, è invece già la scelta corretta: un carattere UTF-8 multibyte potrebbe trovarsi spezzato tra due blocchi, e decodificare i frammenti separatamente produrrebbe caratteri corrotti.
Una funzione di query robusta
Trasformiamo l'esempio in una funzione riutilizzabile che restituisce una Promise. La funzione deve gestire due tipi di timeout distinti. Il primo è un timeout di inattività, che scatta quando il server smette di inviare dati per un certo intervallo; il metodo socket.setTimeout() serve esattamente a questo. Il secondo è una scadenza complessiva: un server che invia un byte ogni nove secondi non farebbe mai scattare un timeout di inattività di dieci secondi, ma terrebbe occupata la connessione per un tempo inaccettabile.
// whois/query.js
import net from 'node:net';
export const WHOIS_PORT = 43;
export const DEFAULT_TIMEOUT_MS = 10_000;
export const MAX_RESPONSE_BYTES = 1024 * 1024;
export class WhoisError extends Error {
constructor(message, { server, cause } = {}) {
super(message, { cause });
this.name = 'WhoisError';
this.server = server;
}
}
export function queryServer(server, query, options = {}) {
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const deadlineMs = options.deadlineMs ?? timeoutMs * 3;
const maxBytes = options.maxBytes ?? MAX_RESPONSE_BYTES;
return new Promise((resolve, reject) => {
const chunks = [];
let received = 0;
let settled = false;
const socket = net.createConnection({ host: server, port: WHOIS_PORT });
// Scadenza complessiva, indipendente dall'attività del socket
const deadline = setTimeout(() => {
finish(new WhoisError(`Scadenza di ${deadlineMs} ms superata`, { server }));
}, deadlineMs);
// Chiude tutto una sola volta, qualunque sia l'evento che arriva per primo
function finish(err, value) {
if (settled) return;
settled = true;
clearTimeout(deadline);
socket.destroy();
if (err) {
reject(err);
} else {
resolve(value);
}
}
// Timeout di inattività: nessun dato ricevuto per timeoutMs
socket.setTimeout(timeoutMs, () => {
finish(new WhoisError(`Nessuna risposta per ${timeoutMs} ms`, { server }));
});
socket.once('connect', () => {
socket.write(`${query}\r\n`);
});
socket.on('data', (chunk) => {
received += chunk.length;
if (received > maxBytes) {
finish(new WhoisError(`Risposta oltre il limite di ${maxBytes} byte`, { server }));
return;
}
chunks.push(chunk);
});
socket.once('end', () => {
finish(null, decodeResponse(Buffer.concat(chunks)));
});
socket.once('error', (err) => {
finish(new WhoisError(`Errore di rete: ${err.message}`, { server, cause: err }));
});
});
}
export function decodeResponse(buffer) {
// Si tenta prima UTF-8 in modalità rigorosa, poi si ripiega su Latin-1
try {
return new TextDecoder('utf-8', { fatal: true }).decode(buffer);
} catch {
return new TextDecoder('latin1').decode(buffer);
}
}
La funzione finish() è il cuore della gestione degli errori. In un socket di Node.js più eventi possono arrivare in sequenza: un timeout seguito da un errore, un errore seguito dalla chiusura, il superamento del limite seguito da altri blocchi di dati già in coda. Il flag settled garantisce che la Promise venga risolta o rifiutata una sola volta e che il socket venga distrutto in ogni caso, evitando perdite di risorse.
Per la decodifica usiamo TextDecoder con l'opzione fatal: true: se i byte non costituiscono UTF-8 valido, il decoder solleva un'eccezione invece di sostituire i caratteri non validi con il simbolo di rimpiazzo. In quel caso ripieghiamo su Latin-1, che è la codifica storicamente più diffusa tra i server WHOIS che non usano UTF-8. Latin-1 ha la proprietà di poter decodificare qualsiasi sequenza di byte, quindi il ripiego non fallisce mai.
Normalizzare e validare il dominio
Prima di inviare qualsiasi cosa a un server remoto bisogna validare l'input. Il motivo non è solo la correttezza: una query WHOIS è una riga di testo terminata da \r\n, e un input contenente a sua volta caratteri di fine riga permetterebbe di iniettare query aggiuntive. La validazione deve quindi garantire che la stringa inviata contenga solo caratteri ammessi in un nome di dominio.
C'è poi il tema dei nomi di dominio internazionalizzati (IDN). Un dominio come caffè.it va convertito nella forma ASCII detta Punycode, in questo caso xn--caff-8oa.it, perché è in quella forma che i registry lo conoscono. Node.js offre la funzione domainToASCII() nel modulo node:url, che effettua la conversione secondo lo standard WHATWG e restituisce una stringa vuota se il dominio non è valido.
// whois/domain.js
import { domainToASCII } from 'node:url';
import { WhoisError } from './query.js';
const ASCII_DOMAIN = /^[a-z0-9.-]+$/;
export function normalizeDomain(input) {
// Rimuove spazi, maiuscole e l'eventuale punto finale
const trimmed = String(input ?? '').trim().toLowerCase().replace(/\.$/, '');
// domainToASCII() rimuoverebbe in silenzio tabulazioni e fine riga: li rifiutiamo prima
if (/[\u0000-\u0020\u007f]/.test(trimmed)) {
throw new WhoisError(`Dominio non valido: ${input}`);
}
const ascii = domainToASCII(trimmed);
if (!ascii || !ASCII_DOMAIN.test(ascii)) {
throw new WhoisError(`Dominio non valido: ${input}`);
}
const labels = ascii.split('.');
if (labels.length < 2 || ascii.length > 253) {
throw new WhoisError(`Dominio non valido: ${input}`);
}
// Ogni etichetta deve avere da 1 a 63 caratteri e non iniziare o finire con un trattino
for (const label of labels) {
if (label.length === 0 || label.length > 63 || label.startsWith('-') || label.endsWith('-')) {
throw new WhoisError(`Etichetta non valida nel dominio: ${input}`);
}
}
return ascii;
}
export function topLevelDomain(domain) {
return domain.slice(domain.lastIndexOf('.') + 1);
}
Il primo controllo merita una spiegazione. Seguendo lo standard WHATWG sugli URL, domainToASCII() elimina tabulazioni e caratteri di fine riga invece di segnalarli come errore: l'input a.com\r\nx diventerebbe silenziosamente a.comx, un dominio diverso da quello richiesto. Per questo i caratteri di controllo e gli spazi vengono rifiutati esplicitamente prima della conversione. Il controllo con l'espressione regolare ASCII_DOMAIN dopo la conversione completa la protezione contro l'iniezione: qualunque cosa contenga l'input originale, la stringa che arriva al server può contenere solo lettere minuscole, cifre, punti e trattini.
Seguire i referral
Con la funzione di query e la normalizzazione pronte possiamo affrontare la ricerca del server corretto. Il percorso tipico ha tre tappe:
- si interroga
whois.iana.orgcon il dominio e si legge il camporefer:della risposta, che indica il server del registry del TLD; - si interroga il server del registry;
- se la risposta del registry contiene il campo
Registrar WHOIS Server:, si interroga anche il server del registrar.
Questo schema richiede alcune cautele. Alcuni registrar indicano il proprio server in forma di URL invece che di hostname, altri indicano sé stessi creando un ciclo, altri ancora indicano server che dopo la dismissione dell'obbligo contrattuale non rispondono più. Il client deve quindi ripulire il valore trovato, ricordare i server già visitati, imporre un numero massimo di passaggi e, soprattutto, non considerare fallita l'intera ricerca se fallisce solo l'ultimo passaggio verso il registrar: i dati del registry sono già un risultato valido.
Un'ulteriore ottimizzazione consiste nel memorizzare l'associazione tra TLD e server del registry. La risposta di IANA per .com sarà sempre la stessa, quindi non ha senso interrogare IANA a ogni ricerca.
Infine, alcuni server interpretano la query in modo particolare. Il server di Verisign per .com e .net, ad esempio, cerca la stringa anche tra i nomi dei name server registrati; anteponendo la parola chiave domain alla query si limita la ricerca ai soli domini. Gestiamo queste particolarità con una piccola tabella di modelli di query.
// whois/lookup.js
import { queryServer, WhoisError } from './query.js';
import { normalizeDomain, topLevelDomain } from './domain.js';
export const IANA_SERVER = 'whois.iana.org';
const MAX_HOPS = 4;
// Modelli di query per i server che richiedono una sintassi particolare
const QUERY_TEMPLATES = {
'whois.verisign-grs.com': (domain) => `domain ${domain}`,
};
const REFERRAL_PATTERNS = [
/^\s*refer:\s*(\S+)/im,
/^\s*whois:\s*(\S+)/im,
/^\s*Registrar WHOIS Server:\s*(\S+)/im,
];
export function buildQuery(server, domain) {
const template = QUERY_TEMPLATES[server];
return template ? template(domain) : domain;
}
export function findReferral(text) {
for (const pattern of REFERRAL_PATTERNS) {
const match = text.match(pattern);
if (match) {
const host = cleanHost(match[1]);
if (host) return host;
}
}
return null;
}
function cleanHost(value) {
// Alcuni registrar indicano un URL invece di un hostname
const host = value
.replace(/^[a-z]+:\/\//i, '')
.replace(/[/:].*$/, '')
.toLowerCase();
return /^[a-z0-9.-]+\.[a-z]{2,}$/.test(host) ? host : null;
}
export async function lookup(input, options = {}) {
const domain = normalizeDomain(input);
const tld = topLevelDomain(domain);
const query = options.query ?? queryServer;
const tldServers = options.tldServers ?? new Map();
const chain = [];
const visited = new Set();
let server = options.server ?? tldServers.get(tld) ?? IANA_SERVER;
while (server && chain.length < MAX_HOPS && !visited.has(server)) {
visited.add(server);
let text;
try {
text = await query(server, buildQuery(server, domain), options);
} catch (err) {
// Se abbiamo già una risposta dal registry, un registrar muto non è fatale
const hasRegistryAnswer = chain.some((step) => step.server !== IANA_SERVER);
if (hasRegistryAnswer) {
chain.push({ server, error: err.message });
break;
}
throw err;
}
chain.push({ server, text });
const next = findReferral(text);
// Memorizza il server del registry indicato da IANA per questo TLD
if (server === IANA_SERVER && next) {
tldServers.set(tld, next);
}
server = next;
}
const answers = chain.filter((step) => step.text && step.server !== IANA_SERVER);
if (answers.length === 0) {
throw new WhoisError(`Nessun server WHOIS trovato per ${domain}`, { server: IANA_SERVER });
}
return { domain, chain, text: answers.at(-1).text };
}
La funzione lookup() accetta tramite le opzioni una funzione query alternativa e una mappa tldServers condivisa. Questa scelta rende il codice testabile, perché nei test si può sostituire la rete con una funzione che restituisce risposte prefabbricate, e prepara il terreno per il client con cache e limitazione delle richieste che costruiremo più avanti.
Il valore restituito contiene l'intera catena dei server interrogati, comprese le eventuali risposte di errore, e il testo dell'ultima risposta utile. Conservare la catena è prezioso in fase di diagnosi: quando un dominio sembra non avere dati, sapere quali server sono stati contattati e cosa hanno risposto fa risparmiare molto tempo.
Interpretare la risposta
Le risposte WHOIS non hanno un formato standard, ma la maggior parte dei server usa righe nella forma Chiave: valore. Alcuni registry, come quello italiano, organizzano invece i dati in blocchi: una riga di intestazione senza due punti, come Nameservers, seguita da righe rientrate che appartengono a quel blocco, fino alla successiva riga vuota. Un parser pragmatico può gestire entrambi i casi con poche regole:
- le righe vuote chiudono il blocco corrente;
- le righe che iniziano con
%,#o>>>sono commenti o note e vengono ignorate; - una riga senza due punti, se non siamo in un blocco, apre un nuovo blocco; se siamo in un blocco, è un valore di quel blocco;
- una riga con due punti è una coppia chiave-valore; se il valore è vuoto apre un blocco, se siamo in un blocco la chiave viene prefissata con il nome del blocco.
// whois/parse.js
const COMMENT_PREFIXES = ['%', '#', '>>>'];
export function parseWhois(text) {
const fields = new Map();
let section = null;
const add = (key, value) => {
if (!fields.has(key)) fields.set(key, []);
fields.get(key).push(value);
};
for (const rawLine of text.split(/\r?\n/)) {
const line = rawLine.trim();
// Una riga vuota chiude il blocco corrente
if (line === '') {
section = null;
continue;
}
if (COMMENT_PREFIXES.some((prefix) => line.startsWith(prefix))) continue;
const separator = line.indexOf(':');
if (separator <= 0) {
// Senza due punti: valore del blocco corrente oppure intestazione di un nuovo blocco
if (section) {
add(section, line);
} else {
section = line.toLowerCase();
}
continue;
}
const key = line.slice(0, separator).trim().toLowerCase();
const value = line.slice(separator + 1).trim();
if (value === '') {
section = key;
continue;
}
add(section ? `${section}.${key}` : key, value);
}
return fields;
}
Il risultato è una mappa in cui ogni chiave è associata a un array di valori, perché campi come Name Server o Domain Status compaiono più volte. Su questa mappa costruiamo un record con i campi più comuni, usando per ciascuno un elenco di alias che copre le varianti usate dai diversi registry.
// whois/record.js
import { parseWhois } from './parse.js';
const FIELD_ALIASES = {
registrar: ['registrar', 'registrar.organization', 'registrar.name', 'sponsoring registrar'],
createdAt: ['creation date', 'created', 'created on', 'registered on', 'registration time'],
updatedAt: ['updated date', 'last update', 'last updated', 'changed', 'modified'],
expiresAt: [
'registry expiry date',
'registrar registration expiration date',
'expiry date',
'expire date',
'expire',
'expires',
'expiration time',
],
nameServers: ['name server', 'nameservers', 'nserver', 'nameservers.name'],
status: ['domain status', 'status', 'state'],
};
const NOT_FOUND_PATTERNS = [
/^no match for/im,
/^not found/im,
/^status:\s*(available|free)\b/im,
/no data found/i,
/no entries found/i,
/domain not found/i,
];
function first(fields, aliases) {
for (const alias of aliases) {
const values = fields.get(alias);
if (values?.length) return values[0];
}
return null;
}
function all(fields, aliases) {
const result = new Set();
for (const alias of aliases) {
for (const value of fields.get(alias) ?? []) {
// Alcuni server aggiungono indirizzi IP o URL dopo il valore: teniamo solo il primo token
result.add(value.split(/\s+/)[0].toLowerCase());
}
}
return [...result];
}
export function toRecord(domain, text) {
const available = NOT_FOUND_PATTERNS.some((pattern) => pattern.test(text));
const fields = parseWhois(text);
return {
domain,
available,
registrar: first(fields, FIELD_ALIASES.registrar),
createdAt: first(fields, FIELD_ALIASES.createdAt),
updatedAt: first(fields, FIELD_ALIASES.updatedAt),
expiresAt: first(fields, FIELD_ALIASES.expiresAt),
nameServers: all(fields, FIELD_ALIASES.nameServers),
status: all(fields, FIELD_ALIASES.status),
};
}
Il rilevamento della disponibilità avviene sul testo grezzo, prima del parsing, perché i messaggi che indicano un dominio libero sono spesso commenti o frasi libere che il parser scarterebbe. Va detto con chiarezza che si tratta di un'euristica: la risposta WHOIS non è una garanzia di disponibilità, e la verifica definitiva resta quella effettuata dal registrar al momento della registrazione. Le date, inoltre, vengono restituite come stringhe: i formati variano da registry a registry e convertirle in oggetti Date senza conoscere il formato specifico rischia di introdurre errori silenziosi.
Cache e limitazione delle richieste
I server WHOIS applicano limiti di frequenza spesso non documentati, e superarli può portare a risposte vuote, messaggi di errore o al blocco temporaneo dell'indirizzo IP. Un client destinato a un uso continuativo deve quindi fare tre cose: memorizzare i risultati per un certo tempo, evitare di eseguire contemporaneamente la stessa ricerca più volte e distanziare le richieste verso uno stesso server.
Per distanziare le richieste usiamo una tecnica di prenotazione degli intervalli: per ogni server memorizziamo il primo istante libero e, a ogni richiesta, prenotiamo quell'istante spostando in avanti il successivo. Poiché in Node.js il codice JavaScript viene eseguito su un solo thread, la prenotazione avviene in modo atomico anche con molte richieste concorrenti.
// whois/client.js
import { queryServer } from './query.js';
import { lookup } from './lookup.js';
import { normalizeDomain } from './domain.js';
import { toRecord } from './record.js';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export class WhoisClient {
#cache = new Map();
#inFlight = new Map();
#nextSlot = new Map();
#tldServers = new Map();
constructor({ ttlMs = 60 * 60 * 1000, minIntervalMs = 1000, ...queryOptions } = {}) {
this.ttlMs = ttlMs;
this.minIntervalMs = minIntervalMs;
this.queryOptions = queryOptions;
}
async #throttle(server) {
const now = Date.now();
const slot = Math.max(now, this.#nextSlot.get(server) ?? 0);
// Prenota l'intervallo prima di attendere, così le richieste concorrenti si mettono in fila
this.#nextSlot.set(server, slot + this.minIntervalMs);
if (slot > now) await sleep(slot - now);
}
#query = async (server, query) => {
await this.#throttle(server);
return queryServer(server, query, this.queryOptions);
};
async lookup(input, { fresh = false } = {}) {
const domain = normalizeDomain(input);
const cached = this.#cache.get(domain);
if (!fresh && cached && cached.expiresAt > Date.now()) {
return cached.value;
}
// Se la stessa ricerca è già in corso, si riusa la stessa Promise
if (this.#inFlight.has(domain)) {
return this.#inFlight.get(domain);
}
const promise = (async () => {
const result = await lookup(domain, {
query: this.#query,
tldServers: this.#tldServers,
});
const value = {
...toRecord(domain, result.text),
servers: result.chain.map((step) => step.server),
raw: result.text,
};
this.#cache.set(domain, { value, expiresAt: Date.now() + this.ttlMs });
return value;
})();
this.#inFlight.set(domain, promise);
try {
return await promise;
} finally {
this.#inFlight.delete(domain);
}
}
}
La mappa #inFlight risolve un problema che la sola cache non copre: se dieci richieste per lo stesso dominio arrivano nello stesso istante, la cache è vuota per tutte e dieci, e senza deduplicazione partirebbero dieci ricerche identiche. Con la mappa delle ricerche in corso, la prima richiesta avvia la ricerca e le altre nove attendono la stessa Promise.
Questa cache vive nella memoria del processo, il che va bene per un servizio singolo. Se il servizio gira su più istanze, la cache e soprattutto il limitatore di frequenza vanno spostati in un archivio condiviso come Redis, altrimenti ogni istanza rispetterà il limite per conto proprio e il server WHOIS vedrà un traffico moltiplicato.
Un piccolo servizio HTTP
Per completare il quadro esponiamo il client tramite un servizio HTTP minimale basato sul modulo node:http. Il servizio risponde a richieste come GET /whois?domain=example.com con un documento JSON e distingue gli errori di input dagli errori dei server remoti.
// server.js
import http from 'node:http';
import { WhoisClient } from './whois/client.js';
import { WhoisError } from './whois/query.js';
const client = new WhoisClient({ timeoutMs: 8000, minIntervalMs: 1500 });
function sendJson(res, status, body) {
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(body));
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, 'http://localhost');
if (req.method !== 'GET' || url.pathname !== '/whois') {
sendJson(res, 404, { error: 'Risorsa non trovata' });
return;
}
const domain = url.searchParams.get('domain');
if (!domain) {
sendJson(res, 400, { error: 'Parametro domain mancante' });
return;
}
try {
const record = await client.lookup(domain, { fresh: url.searchParams.has('fresh') });
const { raw, ...summary } = record;
sendJson(res, 200, url.searchParams.has('raw') ? record : summary);
} catch (err) {
if (err instanceof WhoisError && !err.server) {
// Errore di validazione dell'input
sendJson(res, 422, { error: err.message });
return;
}
// Errore del server remoto: rete, timeout o risposta non valida
sendJson(res, 502, { error: err.message, server: err.server ?? null });
}
});
server.listen(3000, () => {
console.log('Servizio WHOIS in ascolto su http://localhost:3000');
});
Si può provare il servizio con curl:
curl 'http://localhost:3000/whois?domain=example.com'
curl 'http://localhost:3000/whois?domain=caff%C3%A8.it&raw'
La distinzione tra il codice 422 e il codice 502 non è un dettaglio: chi consuma il servizio deve poter capire se il problema è nella richiesta che ha inviato, e quindi è inutile riprovare, oppure in un server remoto, e quindi riprovare più tardi potrebbe funzionare. In questo esempio la distinzione si basa sulla presenza della proprietà server nell'errore, che la funzione di query imposta sempre e la normalizzazione mai.
Considerazioni finali: RDAP, privacy e buone pratiche
Il client che abbiamo costruito copre i casi d'uso più comuni, ma è importante conoscerne i limiti. Per i domini generici, RDAP è ormai il protocollo di riferimento: funziona su HTTPS, restituisce JSON strutturato e prevede un meccanismo standard di individuazione del server tramite il registro di bootstrap pubblicato da IANA. Un'evoluzione naturale di questo client è tentare prima RDAP, usando la funzione fetch integrata in Node.js, e ripiegare su WHOIS solo per i TLD che non offrono un servizio RDAP.
Dal punto di vista dei dati, dopo l'entrata in vigore del GDPR la maggior parte dei registry e dei registrar oscura le informazioni personali degli intestatari. È normale trovare nei campi relativi al registrante diciture come REDACTED FOR PRIVACY: il client non deve considerarle un errore, e chi progetta funzionalità basate su WHOIS non dovrebbe contare sulla disponibilità di nomi, indirizzi email o numeri di telefono.
Infine, qualche regola di buon comportamento. Le condizioni d'uso dei server WHOIS vietano generalmente l'interrogazione massiva e l'uso dei dati per finalità di marketing; i limiti di frequenza vanno rispettati con margine; e i risultati vanno memorizzati in cache il più a lungo possibile compatibilmente con lo scopo. Un client educato è anche un client che funziona meglio, perché viene bloccato meno spesso.