import * as plugins from './detector.plugins.js'; import { ServiceType } from './detector.interfaces.js'; import type { IDetectorResult, IDetectorOptions } from './detector.interfaces.js'; export class Detector { private smartnetworkInstance = new plugins.smartnetwork.SmartNetwork(); /** * Check if a port is active - simple boolean version for backward compatibility */ public async isActiveSimple(urlArg: string): Promise { const result = await this.isActive(urlArg); return result.isActive; } /** * Check if a port is active with detailed results */ public async isActive(urlArg: string, options?: IDetectorOptions): Promise { const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(urlArg); if (parsedUrl.hostname === 'localhost') { console.log(`detector target is localhost on port ${parsedUrl.port}`); const portUnused = await this.smartnetworkInstance.isLocalPortUnused( parseInt(parsedUrl.port, 10), ); const portAvailable = !portUnused; const result: IDetectorResult = { isActive: portAvailable }; if (options?.detectServiceType) { if (portAvailable) { const serviceType = await this.detectType(urlArg); result.serviceType = serviceType; } else { result.serviceType = ServiceType.UNKNOWN; } } return result; } else { console.log(`detector target is remote domain ${parsedUrl.host} on port ${parsedUrl.port}`); const portAvailable = await this.smartnetworkInstance.isRemotePortAvailable( parsedUrl.host, parseInt(parsedUrl.port, 10), ); const result: IDetectorResult = { isActive: portAvailable }; if (options?.detectServiceType) { if (portAvailable) { const serviceType = await this.detectType(urlArg); result.serviceType = serviceType; } else { result.serviceType = ServiceType.UNKNOWN; } } return result; } } public async detectType(urlArg: string): Promise { const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(urlArg); // Handle different URL schemes and default ports let port = parseInt(parsedUrl.port, 10); if (isNaN(port)) { // Set default ports based on scheme const defaultPorts: { [key: string]: number } = { 'http': 80, 'https': 443, 'ssh': 22, 'ftp': 21, 'smtp': 25, 'mysql': 3306, 'postgresql': 5432, 'mongodb': 27017, 'redis': 6379 }; const scheme = parsedUrl.protocol.replace(':', '').toLowerCase(); port = defaultPorts[scheme] || 80; } const hostname = parsedUrl.hostname; // Check common ports first const commonPorts: { [key: number]: ServiceType } = { 80: ServiceType.HTTP, 443: ServiceType.HTTPS, 22: ServiceType.SSH, 21: ServiceType.FTP, 25: ServiceType.SMTP, 110: ServiceType.POP3, 143: ServiceType.IMAP, 3306: ServiceType.MYSQL, 5432: ServiceType.POSTGRESQL, 27017: ServiceType.MONGODB, 6379: ServiceType.REDIS }; if (commonPorts[port]) { // Verify the service is actually what we expect const verified = await this.verifyServiceType(hostname, port, commonPorts[port]); if (verified) { return commonPorts[port]; } } // Try to detect service by banner/protocol return await this.detectServiceByProtocol(hostname, port); } private async verifyServiceType(hostname: string, port: number, expectedType: ServiceType): Promise { try { switch (expectedType) { case ServiceType.HTTP: case ServiceType.HTTPS: return await this.checkHttpService(hostname, port, expectedType === ServiceType.HTTPS); case ServiceType.SSH: return await this.checkSshService(hostname, port); default: return true; // For now, trust the port number for other services } } catch (error) { return false; } } private async detectServiceByProtocol(hostname: string, port: number): Promise { // Try HTTPS first if (await this.checkHttpService(hostname, port, true)) { return ServiceType.HTTPS; } // Try HTTP if (await this.checkHttpService(hostname, port, false)) { return ServiceType.HTTP; } // Try SSH if (await this.checkSshService(hostname, port)) { return ServiceType.SSH; } // Try to get banner for other services const banner = await this.getBanner(hostname, port); if (banner) { return this.identifyServiceByBanner(banner); } return ServiceType.UNKNOWN; } private async checkHttpService(hostname: string, port: number, isHttps: boolean): Promise { return new Promise((resolve) => { const protocol = isHttps ? plugins.https : plugins.http; const options = { hostname, port, method: 'HEAD', timeout: 5000, rejectUnauthorized: false // Accept self-signed certificates }; const req = protocol.request(options, () => { resolve(true); }); req.on('error', () => { resolve(false); }); req.on('timeout', () => { req.destroy(); resolve(false); }); req.end(); }); } private async checkSshService(hostname: string, port: number): Promise { return new Promise((resolve) => { const socket = new plugins.net.Socket(); socket.setTimeout(5000); socket.on('data', (data) => { const banner = data.toString(); socket.destroy(); // SSH banners typically start with "SSH-" resolve(banner.startsWith('SSH-')); }); socket.on('error', () => { resolve(false); }); socket.on('timeout', () => { socket.destroy(); resolve(false); }); socket.connect(port, hostname); }); } private async getBanner(hostname: string, port: number): Promise { return new Promise((resolve) => { const socket = new plugins.net.Socket(); let banner = ''; socket.setTimeout(5000); socket.on('data', (data) => { banner += data.toString(); socket.destroy(); resolve(banner); }); socket.on('error', () => { resolve(null); }); socket.on('timeout', () => { socket.destroy(); resolve(banner || null); }); socket.connect(port, hostname); }); } private identifyServiceByBanner(banner: string): ServiceType { const bannerLower = banner.toLowerCase(); if (bannerLower.includes('ssh')) return ServiceType.SSH; if (bannerLower.includes('ftp')) return ServiceType.FTP; if (bannerLower.includes('smtp')) return ServiceType.SMTP; if (bannerLower.includes('pop3')) return ServiceType.POP3; if (bannerLower.includes('imap')) return ServiceType.IMAP; if (bannerLower.includes('mysql')) return ServiceType.MYSQL; if (bannerLower.includes('postgresql')) return ServiceType.POSTGRESQL; if (bannerLower.includes('mongodb')) return ServiceType.MONGODB; if (bannerLower.includes('redis')) return ServiceType.REDIS; return ServiceType.UNKNOWN; } }