UNPKG

domain-info-fetcher

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

629 lines (565 loc) 17.9 kB
import * as https from "https"; import { Socket } from "net"; import * as dns from "dns"; import { WhoisData } from "./src/whois"; // Custom interface for the socket object interface CustomSocket extends Socket { getPeerCertificate(detailed: boolean): CertificateData; } // Interface for certificate data interface CertificateData { subject: { [key: string]: string | string[] }; issuer: { [key: string]: string | string[] }; valid_from: string; valid_to: string; raw?: Buffer; fingerprint?: string; fingerprint256?: string; serialNumber?: string; issuerCertificate?: CertificateData; [key: string]: unknown; } // SSL data structure interface SslData { subject: { [key: string]: string | string[] }; issuer: { [key: string]: string | string[] }; valid: boolean; validFrom: number; validTo: number; // New fields for PEM certificates certificate?: string; intermediateCertificate?: string; rootCertificate?: string; // New field for readable details details?: { issuer: string; subject: string; validFrom: Date; validTo: Date; }; } // DomainInfo interface to describe the return type of fetchDomainInfo function interface DomainInfo { sslData: SslData; serverData: string | undefined; dnsData: | { A: string[]; CNAME: string | null; TXT: string[]; MX: Array<{ exchange: string; priority: number }>; NS: string[]; SOA: dns.SoaRecord | null; } | undefined; httpStatus: number | undefined; // New WHOIS data field for version 2.3.0 whoisData?: WhoisData; } // Options for configuring the fetch request export interface RequestOptions { /** Timeout in milliseconds for HTTP requests */ timeout?: number; /** Custom headers to include in HTTP requests */ headers?: Record<string, string>; /** Whether to follow redirects in HTTP requests */ followRedirects?: boolean; /** Maximum number of redirects to follow */ maxRedirects?: number; } // Default request options const DEFAULT_OPTIONS: RequestOptions = { timeout: 10000, // 10 seconds followRedirects: true, maxRedirects: 5, }; /** * Formats a given domain to `example.com` format. * @param domain The domain to format. * @returns The formatted domain. */ export function formatDomain(domain: string): string { 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. */ export function extractSubdomain(domain: string): string | null { 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. */ export function getRootDomain(domain: string): string { 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. */ export const checkDomain = (domain: string): boolean => { const domainParts = domain.split("."); return domainParts.length > 1 && domainParts[0].length > 0; }; /** * converts a date string to a timestamp * @param dateString * @returns timestamp */ export function dateToTimestamp(dateString: string): number { 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: CertificateData): SslData { const validToTimestamp = dateToTimestamp(cert.valid_to); const validFromTimestamp = dateToTimestamp(cert.valid_from); // Extract human-readable subject and issuer information const subjectCN = typeof cert.subject?.CN === "string" ? cert.subject.CN : typeof cert.subject?.commonName === "string" ? cert.subject.commonName : Array.isArray(cert.subject?.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 cert.issuer?.O === "string" ? cert.issuer.O : typeof cert.issuer?.organizationName === "string" ? cert.issuer.organizationName : typeof cert.issuer?.CN === "string" ? cert.issuer.CN : typeof cert.issuer?.commonName === "string" ? cert.issuer.commonName : Array.isArray(cert.issuer?.O) ? cert.issuer.O.join(", ") : Array.isArray(cert.issuer?.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: string | undefined = undefined; let intermediateCertificate: string | undefined = undefined; let rootCertificate: string | undefined = undefined; try { // The main certificate PEM if (cert.raw) { certificate = `-----BEGIN CERTIFICATE-----\n${cert.raw .toString("base64") .match(/.{1,64}/g) ?.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${intermediate.raw .toString("base64") .match(/.{1,64}/g) ?.join("\n")}\n-----END CERTIFICATE-----`; } // Root certificate (if chain available) if ( intermediate.issuerCertificate && intermediate.issuerCertificate.raw ) { rootCertificate = `-----BEGIN CERTIFICATE-----\n${intermediate.issuerCertificate.raw .toString("base64") .match(/.{1,64}/g) ?.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), }, }; } /** * 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. */ export async function fetchDomainInfo( domain: string, options?: RequestOptions ): Promise<DomainInfo | undefined> { if (!domain) { throw new Error("Domain name cannot be empty"); } if (!checkDomain(domain)) { throw new Error("Invalid domain name format"); } const mergedOptions = { ...DEFAULT_OPTIONS, ...options }; const formattedDomain = formatDomain(domain); // Include WHOIS data in the Promise.all array const [ sslData, serverData, dnsData, httpStatus, whoisData, ] = await Promise.all([ getSslData(formattedDomain, mergedOptions).catch((error) => { // Enhance error message with more specific details let errorMessage = "Could not fetch SSL data for domain " + domain; if (error.code) { errorMessage += ". Error code: " + error.code; } if (error.message) { errorMessage += ". Details: " + error.message; } throw new Error(errorMessage); }), getServerData(formattedDomain, mergedOptions).catch((error) => { // Enhance error message with more specific details let errorMessage = "Could not fetch server data for domain " + domain; if (error.code) { errorMessage += ". Error code: " + error.code; } if (error.message) { errorMessage += ". Details: " + error.message; } throw new Error(errorMessage); }), getDnsData(formattedDomain).catch((error) => { // Enhance error message with more specific details let errorMessage = "Could not fetch DNS data for domain " + domain; if (error.code) { errorMessage += ". Error code: " + error.code; } if (error.message) { errorMessage += ". Details: " + error.message; } throw new Error(errorMessage); }), getHttpStatus(formattedDomain, mergedOptions), // Add WHOIS data fetch, but make it optional import("./src/whois") .then((whoisModule) => whoisModule.getWhoisData(formattedDomain).catch((error) => { // Log the error but don't fail the whole request console.warn(`WHOIS data fetch failed: ${error.message}`); return undefined; }) ) .catch(() => undefined), // Make WHOIS data optional ]); if (!sslData) { throw new Error( "Could not fetch SSL data for domain " + domain + ". The SSL certificate may be invalid or the domain may not support HTTPS." ); } return { sslData, serverData, dnsData, httpStatus, whoisData }; } /** * 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: string, options: RequestOptions = DEFAULT_OPTIONS ): Promise<SslData> { return new Promise((resolve, reject) => { const req = https .request( `https://${domain}`, { method: "HEAD", timeout: options.timeout, headers: options.headers || {}, }, (res) => { const socket = res.socket as CustomSocket; const cert = socket.getPeerCertificate(true); resolve(extractSslData(cert)); socket.destroy(); } ) .on("error", (error) => { 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: string, options: RequestOptions = DEFAULT_OPTIONS ): Promise<string | undefined> { 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 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) => { 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: string ): Promise<{ A: string[]; CNAME: string | null; TXT: string[]; MX: Array<{ exchange: string; priority: number }>; NS: string[]; SOA: dns.SoaRecord | null; }> { 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: string): Promise<string[]> { 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: string): Promise<string | null> { 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: string): Promise<string[]> { 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: string ): Promise<Array<{ exchange: string; priority: number }>> { 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: string): Promise<string[]> { 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: string): Promise<dns.SoaRecord | null> { 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: string, options: RequestOptions = DEFAULT_OPTIONS ): Promise<number> { 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(); }); } // Export WhoisData interface for users export { WhoisData } from "./src/whois";