UNPKG

@xyz/whois

Version:

A powerful TypeScript/JavaScript tool for comprehensive domain analysis, featuring detailed WHOIS data with registration dates, registrars, and domain status. Offers SSL certificate extraction (with PEM support), DNS records, and server details. Includes

639 lines (638 loc) 24.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.checkDomain = void 0; exports.formatDomain = formatDomain; exports.extractSubdomain = extractSubdomain; exports.getRootDomain = getRootDomain; exports.dateToTimestamp = dateToTimestamp; exports.fetchDomainInfo = fetchDomainInfo; const https = __importStar(require("https")); const http = __importStar(require("http")); const dns = __importStar(require("dns")); const logger_1 = __importDefault(require("./src/utils/logger")); // Default request options const DEFAULT_OPTIONS = { timeout: 10000, // 10 seconds followRedirects: true, maxRedirects: 5, }; // Default SSL data structure const DEFAULT_SSL_DATA = { subject: {}, issuer: {}, valid: false, validFrom: 0, validTo: 0, details: { subject: '', issuer: 'No SSL Certificate', validFrom: new Date(0), validTo: new Date(0), }, }; /** * Formats a given domain to `example.com` format. * @param domain The domain to format. * @returns The formatted domain. */ function formatDomain(domain) { return domain .replace(/^(https?:\/\/)?(www\.)?/i, '') .replace(/\/$/, '') .toLowerCase(); } /** * Extracts the subdomain from a given domain. * @param domain The domain to extract the subdomain from. * @returns The subdomain or null if no subdomain is present. */ function extractSubdomain(domain) { const formattedDomain = formatDomain(domain); const parts = formattedDomain.split('.'); // Check if there are more than 2 parts (e.g., sub.example.com) if (parts.length > 2) { return parts[0]; } return null; } /** * Gets the root domain (e.g., example.com) from a domain that may include a subdomain. * @param domain The domain to extract the root domain from. * @returns The root domain. */ function getRootDomain(domain) { const formattedDomain = formatDomain(domain); const parts = formattedDomain.split('.'); // If domain has more than 2 parts, return the last two (e.g., example.com from sub.example.com) if (parts.length > 2) { return parts.slice(-2).join('.'); } return formattedDomain; } /** * Checks if the given domain is valid. * @param domain The domain to check. * @returns True if the domain is valid, false otherwise. */ const checkDomain = (domain) => { const domainParts = domain.split('.'); return domainParts.length > 1 && domainParts[0].length > 0; }; exports.checkDomain = checkDomain; /** * converts a date string to a timestamp * @param dateString * @returns timestamp */ function dateToTimestamp(dateString) { return new Date(dateString).getTime(); } /** * Extracts SSL data from the given certificate. * @param cert The certificate object * @returns An object containing the SSL data. */ function extractSslData(cert) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; const validToTimestamp = dateToTimestamp(cert.valid_to); const validFromTimestamp = dateToTimestamp(cert.valid_from); // Extract human-readable subject and issuer information const subjectCN = typeof ((_a = cert.subject) === null || _a === void 0 ? void 0 : _a.CN) === 'string' ? cert.subject.CN : typeof ((_b = cert.subject) === null || _b === void 0 ? void 0 : _b.commonName) === 'string' ? cert.subject.commonName : Array.isArray((_c = cert.subject) === null || _c === void 0 ? void 0 : _c.CN) ? cert.subject.CN.join(', ') : Object.values(cert.subject || {}) .map((v) => (Array.isArray(v) ? v.join(', ') : v)) .join(', '); // Prioritize Organization (O) for issuer information, falling back to CN if not available const issuerCN = typeof ((_d = cert.issuer) === null || _d === void 0 ? void 0 : _d.O) === 'string' ? cert.issuer.O : typeof ((_e = cert.issuer) === null || _e === void 0 ? void 0 : _e.organizationName) === 'string' ? cert.issuer.organizationName : typeof ((_f = cert.issuer) === null || _f === void 0 ? void 0 : _f.CN) === 'string' ? cert.issuer.CN : typeof ((_g = cert.issuer) === null || _g === void 0 ? void 0 : _g.commonName) === 'string' ? cert.issuer.commonName : Array.isArray((_h = cert.issuer) === null || _h === void 0 ? void 0 : _h.O) ? cert.issuer.O.join(', ') : Array.isArray((_j = cert.issuer) === null || _j === void 0 ? void 0 : _j.CN) ? cert.issuer.CN.join(', ') : Object.values(cert.issuer || {}) .map((v) => (Array.isArray(v) ? v.join(', ') : v)) .join(', '); // Extract certificates in PEM format if available let certificate = undefined; let intermediateCertificate = undefined; let rootCertificate = undefined; try { // The main certificate PEM if (cert.raw) { certificate = `-----BEGIN CERTIFICATE-----\n${(_k = cert.raw .toString('base64') .match(/.{1,64}/g)) === null || _k === void 0 ? void 0 : _k.join('\n')}\n-----END CERTIFICATE-----`; } // Try to extract intermediate and root certificates if available if (cert.issuerCertificate) { const intermediate = cert.issuerCertificate; if (intermediate.raw) { intermediateCertificate = `-----BEGIN CERTIFICATE-----\n${(_l = intermediate.raw .toString('base64') .match(/.{1,64}/g)) === null || _l === void 0 ? void 0 : _l.join('\n')}\n-----END CERTIFICATE-----`; } // Root certificate (if chain available) if (intermediate.issuerCertificate && intermediate.issuerCertificate.raw) { rootCertificate = `-----BEGIN CERTIFICATE-----\n${(_m = intermediate.issuerCertificate.raw .toString('base64') .match(/.{1,64}/g)) === null || _m === void 0 ? void 0 : _m.join('\n')}\n-----END CERTIFICATE-----`; } } } catch { // Silently handle certificate extraction errors // This ensures backward compatibility - if certificate extraction fails, // we'll still return the existing fields } return { // Original fields (for backward compatibility) subject: cert.subject, issuer: cert.issuer, valid: validToTimestamp > Date.now(), validFrom: validFromTimestamp, validTo: validToTimestamp, // New certificate fields certificate, intermediateCertificate, rootCertificate, // Human-readable details details: { subject: subjectCN, issuer: issuerCN, validFrom: new Date(validFromTimestamp), validTo: new Date(validToTimestamp), }, }; } /** * Validates a domain name and throws an appropriate error if invalid */ function validateDomain(domain) { if (!domain) { throw new Error('Domain name cannot be empty'); } if (!(0, exports.checkDomain)(domain)) { throw new Error('Invalid domain name format'); } } /** * Fetches SSL, server, and DNS data for the given domain. * @param domain The domain to fetch the information for. * @param options Optional request configuration * @returns A Promise that resolves to an object containing the SSL, server, and DNS data. */ async function fetchDomainInfo(domain, options = {}) { const { debug = false } = options; const mergedOptions = { ...DEFAULT_OPTIONS, ...options }; validateDomain(domain); const formattedDomain = formatDomain(domain); try { // Initialize result object with default values const result = { sslData: { ...DEFAULT_SSL_DATA, details: { ...DEFAULT_SSL_DATA.details, subject: formattedDomain, issuer: 'No SSL Certificate', validFrom: new Date(0), validTo: new Date(0), }, }, serverData: undefined, dnsData: undefined, httpStatus: undefined, whoisData: undefined, }; // Fetch WHOIS data first since it's most reliable logger_1.default.logInfo(debug, 'Getting WHOIS data', `for domain: ${formattedDomain}`); try { result.whoisData = await Promise.resolve().then(() => __importStar(require('./src/whois'))).then((whoisModule) => whoisModule.getWhoisData(formattedDomain, true, 3, debug)) .catch((error) => { logger_1.default.logWarning(debug, 'WHOIS data fetch', error.message); return undefined; }); } catch (error) { logger_1.default.logError(debug, 'WHOIS data', error); } // Then fetch DNS data logger_1.default.logInfo(debug, 'Getting DNS data', `for domain: ${formattedDomain}`); try { result.dnsData = await getDnsData(formattedDomain); } catch (error) { logger_1.default.logError(debug, 'DNS data', error); } // Finally fetch SSL and server data logger_1.default.logInfo(debug, 'Getting SSL and server data', `for domain: ${formattedDomain}`); try { const [sslData, serverData, httpStatus] = await Promise.all([ getSslData(formattedDomain, mergedOptions), getServerData(formattedDomain, mergedOptions), getHttpStatus(formattedDomain, mergedOptions), ]); result.sslData = sslData; result.serverData = serverData; result.httpStatus = httpStatus; } catch (error) { logger_1.default.logError(debug, 'SSL/server data', error); } return result; } catch (error) { logger_1.default.logError(debug, 'domain info', error); throw error; } } /** * Retrieves SSL data for the given domain. * @param domain The domain to fetch the SSL data for. * @param options Request configuration options * @returns A Promise that resolves to an object containing the SSL data. */ async function getSslData(domain, options = DEFAULT_OPTIONS) { return new Promise((resolve, reject) => { // First try HTTPS with modern protocols const req = https .request(`https://${domain}`, { method: 'HEAD', timeout: options.timeout, headers: options.headers || {}, secureProtocol: 'TLSv1_2_method', // Default to TLS 1.2 }, (res) => { const socket = res.socket; const cert = socket.getPeerCertificate(true); resolve(extractSslData(cert)); socket.destroy(); }) .on('error', (error) => { // If HTTPS fails with EPROTO, try with older protocols if (error.code === 'EPROTO') { const legacyReq = https .request(`https://${domain}`, { method: 'HEAD', timeout: options.timeout, headers: options.headers || {}, secureProtocol: 'TLSv1_method', // Try TLS 1.0 }, (legacyRes) => { const socket = legacyRes.socket; const cert = socket.getPeerCertificate(true); resolve(extractSslData(cert)); socket.destroy(); }) .on('error', (legacyError) => { // If both modern and legacy protocols fail, try HTTP if (legacyError.code === 'EPROTO' || legacyError.code === 'ECONNREFUSED') { const httpReq = http .request(`http://${domain}`, { method: 'HEAD', timeout: options.timeout, headers: options.headers || {}, }, (httpRes) => { var _a; // For HTTP, return a basic SSL data object indicating no SSL resolve({ subject: {}, issuer: {}, valid: false, validFrom: 0, validTo: 0, details: { subject: domain, issuer: 'No SSL Certificate', validFrom: new Date(0), validTo: new Date(0), }, }); (_a = httpRes.socket) === null || _a === void 0 ? void 0 : _a.destroy(); }) .on('error', (httpError) => { reject(httpError); }); httpReq.end(); } else { reject(legacyError); } }); legacyReq.end(); } else if (error.code === 'ECONNREFUSED') { // If connection refused, try HTTP const httpReq = http .request(`http://${domain}`, { method: 'HEAD', timeout: options.timeout, headers: options.headers || {}, }, (httpRes) => { var _a; // For HTTP, return a basic SSL data object indicating no SSL resolve({ subject: {}, issuer: {}, valid: false, validFrom: 0, validTo: 0, details: { subject: domain, issuer: 'No SSL Certificate', validFrom: new Date(0), validTo: new Date(0), }, }); (_a = httpRes.socket) === null || _a === void 0 ? void 0 : _a.destroy(); }) .on('error', (httpError) => { reject(httpError); }); httpReq.end(); } else { reject(error); } }); req.end(); }); } /** * Retrieves server data for the given domain. * @param domain The domain to fetch the server data for. * @param options Request configuration options * @returns A Promise that resolves to a string containing the server data. */ async function getServerData(domain, options = DEFAULT_OPTIONS) { return new Promise((resolve, reject) => { // First try HTTPS const req = https .request(`https://${domain}`, { method: 'HEAD', timeout: options.timeout, headers: options.headers || {}, agent: false, // Disable connection pooling }, (res) => { const serverHeaderValue = res.headers['server']; const result = Array.isArray(serverHeaderValue) ? serverHeaderValue[0] : serverHeaderValue; // Ensure socket is destroyed if (res.socket) { res.socket.destroy(); } resolve(result); }) .on('error', (error) => { // If HTTPS fails, try HTTP if (error.code === 'EPROTO' || error.code === 'ECONNREFUSED') { const httpReq = http .request(`http://${domain}`, { method: 'HEAD', timeout: options.timeout, headers: options.headers || {}, agent: false, // Disable connection pooling }, (httpRes) => { const serverHeaderValue = httpRes.headers['server']; const result = Array.isArray(serverHeaderValue) ? serverHeaderValue[0] : serverHeaderValue; // Ensure socket is destroyed if (httpRes.socket) { httpRes.socket.destroy(); } resolve(result); }) .on('error', (httpError) => { reject(httpError); }); httpReq.end(); } else { reject(error); } }); req.end(); }); } /** * Retrieves DNS data for the given domain. * @param domain The domain to fetch the DNS data for. * @returns A Promise that resolves to an object containing the DNS data. */ async function getDnsData(domain) { const subdomain = extractSubdomain(domain); const rootDomain = getRootDomain(domain); // For a subdomain, we primarily want A and CNAME records of the subdomain itself if (subdomain) { try { const A = await getARecords(domain); const CNAME = await getCNameRecord(domain); // For other DNS records, we typically want the root domain information const TXT = await getTxtRecords(rootDomain); const MX = await getMxRecords(rootDomain); const NS = await getNsRecords(rootDomain); const SOA = await getSoaRecord(rootDomain); return { A, CNAME, TXT, MX, NS, SOA }; } catch (error) { // If looking up the subdomain fails, fall back to the root domain console.error(`Error fetching subdomain DNS data: ${error instanceof Error ? error.message : String(error)}`); console.log(`Falling back to root domain ${rootDomain}`); } } // Standard lookup for root domain or fallback const A = await getARecords(domain); const CNAME = await getCNameRecord(domain); const TXT = await getTxtRecords(domain); const MX = await getMxRecords(domain); const NS = await getNsRecords(domain); const SOA = await getSoaRecord(domain); return { A, CNAME, TXT, MX, NS, SOA }; } /** * Retrieves A records for the given domain. * @param domain The domain to fetch the A records for. * @returns A Promise that resolves to an array of strings containing the A records. */ function getARecords(domain) { return new Promise((resolve, reject) => { dns.resolve4(domain, (error, addresses) => { if (error) { reject(error); } else { resolve(addresses); } }); }); } /** * Retrieves CNAME record for the given domain. * @param domain The domain to fetch the CNAME record for. * @returns A Promise that resolves to a string containing the CNAME record. */ function getCNameRecord(domain) { return new Promise((resolve, reject) => { dns.resolveCname(domain, (error, addresses) => { if (error) { if (error.code === 'ENODATA') { resolve(null); } else { reject(error); } } else { resolve(addresses[0]); } }); }); } /** * Retrieves TXT records for the given domain. * @param domain The domain to fetch the TXT records for. * @returns A Promise that resolves to an array of strings containing the TXT records. */ function getTxtRecords(domain) { return new Promise((resolve, reject) => { dns.resolveTxt(domain, (error, records) => { if (error) { reject(error); } else { const flattenedRecords = records.flat(); resolve(flattenedRecords); } }); }); } /** * Retrieves MX records for the given domain. * @param domain The domain to fetch the MX records for. * @returns A Promise that resolves to an array of objects containing the MX records. */ function getMxRecords(domain) { return new Promise((resolve, reject) => { dns.resolveMx(domain, (error, records) => { if (error) { reject(error); } else { resolve(records); } }); }); } /** * Retrieves NS records for the given domain. * @param domain The domain to fetch the NS records for. * @returns A Promise that resolves to an array of strings containing the NS records. */ function getNsRecords(domain) { return new Promise((resolve, reject) => { dns.resolveNs(domain, (error, records) => { if (error) { reject(error); } else { resolve(records); } }); }); } /** * Retrieves SOA record for the given domain. * @param domain The domain to fetch the SOA record for. * @returns A Promise that resolves to an object containing the SOA record. */ function getSoaRecord(domain) { return new Promise((resolve, reject) => { dns.resolveSoa(domain, (error, record) => { if (error) { if (error.code === 'ENODATA') { resolve(null); } else { reject(error); } } else { resolve(record); } }); }); } /** * Retrieves HTTP status for the given domain. * @param domain The domain to fetch the HTTP status for. * @param options Request configuration options * @returns A Promise that resolves to a number containing the HTTP status. */ async function getHttpStatus(domain, options = DEFAULT_OPTIONS) { return new Promise((resolve, reject) => { const req = https .request(`https://${domain}`, { method: 'HEAD', timeout: options.timeout, headers: options.headers || {}, agent: false, // Disable connection pooling }, (res) => { const statusCode = res.statusCode || 0; // Ensure socket is destroyed if (res.socket) { res.socket.destroy(); } resolve(statusCode); }) .on('error', (error) => { reject(error); }); req.end(); }); }