import * as https from "https"; import { Socket } from "net"; import * as dns from "dns"; import type { WhoisData } from "./src/whois"; import { formatDomain, extractSubdomain, getRootDomain, checkDomain, dateToTimestamp, } from "./src/utils"; // 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; /** 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. */ // formatDomain moved to ./src/utils and imported above /** * 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. */ // extractSubdomain moved to ./src/utils and imported above /** * 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. */ // getRootDomain moved to ./src/utils and imported above /** * Checks if the given domain is valid. * @param domain The domain to check. * @returns True if the domain is valid, false otherwise. */ // checkDomain moved to ./src/utils and imported above /** * converts a date string to a timestamp * @param dateString * @returns timestamp */ // dateToTimestamp moved to ./src/utils and imported above /** * 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 { if (!domain) { throw new Error("Domain name cannot be empty"); } if (!checkDomain(domain)) { throw new Error("Invalid domain name"); } 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 { 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 { 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 { 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 { 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 { 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> { 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 { 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 { 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 { 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 type { WhoisData } from "./src/whois"; export { formatDomain, extractSubdomain, getRootDomain, checkDomain, dateToTimestamp, } from "./src/utils";