import * as plugins from '../plugins.js'; import { EsclProtocol } from '../scanner/scanner.classes.esclprotocol.js'; import { IppProtocol } from '../printer/printer.classes.ippprotocol.js'; import { cidrToIps, ipRangeToIps, isValidIp, } from '../helpers/helpers.iprange.js'; import type { INetworkScanOptions, INetworkScanResult, INetworkScanDevice, INetworkScanProgress, } from '../interfaces/index.js'; /** * Default ports to probe for device discovery */ const DEFAULT_PORTS = [631, 80, 443, 6566, 9100]; /** * Default scan options */ const DEFAULT_OPTIONS: Required> = { concurrency: 50, timeout: 2000, ports: DEFAULT_PORTS, probeEscl: true, probeIpp: true, probeSane: true, }; /** * Simple concurrency limiter */ class ConcurrencyLimiter { private running = 0; private queue: (() => void)[] = []; constructor(private limit: number) {} async run(fn: () => Promise): Promise { while (this.running >= this.limit) { await new Promise((resolve) => this.queue.push(resolve)); } this.running++; try { return await fn(); } finally { this.running--; const next = this.queue.shift(); if (next) next(); } } } /** * Network Scanner - scans IP ranges for scanners and printers */ export class NetworkScanner extends plugins.events.EventEmitter { private cancelled = false; private scanning = false; /** * Check if a scan is currently in progress */ public get isScanning(): boolean { return this.scanning; } /** * Scan a network range for devices */ public async scan(options: INetworkScanOptions): Promise { if (this.scanning) { throw new Error('A scan is already in progress'); } this.cancelled = false; this.scanning = true; const opts = { ...DEFAULT_OPTIONS, ...options }; const results: INetworkScanResult[] = []; try { // Get list of IPs to scan const ips = this.resolveIps(options); if (ips.length === 0) { throw new Error('No IPs to scan. Provide ipRange, or startIp and endIp.'); } const limiter = new ConcurrencyLimiter(opts.concurrency); let scanned = 0; // Progress tracking const emitProgress = (currentIp?: string) => { const progress: INetworkScanProgress = { total: ips.length, scanned, percentage: Math.round((scanned / ips.length) * 100), currentIp, devicesFound: results.reduce((sum, r) => sum + r.devices.length, 0), }; this.emit('progress', progress); }; emitProgress(); // Scan all IPs with concurrency limit const scanPromises = ips.map((ip) => limiter.run(async () => { if (this.cancelled) return; const devices = await this.probeIp(ip, opts); scanned++; if (devices.length > 0) { const result: INetworkScanResult = { address: ip, devices }; results.push(result); this.emit('device:found', result); } emitProgress(ip); }) ); await Promise.all(scanPromises); if (this.cancelled) { this.emit('cancelled'); } else { this.emit('complete', results); } return results; } finally { this.scanning = false; } } /** * Cancel an ongoing scan */ public async cancel(): Promise { if (this.scanning) { this.cancelled = true; } } /** * Resolve IPs from options (CIDR or start/end range) */ private resolveIps(options: INetworkScanOptions): string[] { if (options.ipRange) { return cidrToIps(options.ipRange); } if (options.startIp && options.endIp) { return ipRangeToIps(options.startIp, options.endIp); } if (options.startIp && !options.endIp) { // Single IP if (isValidIp(options.startIp)) { return [options.startIp]; } } return []; } /** * Probe a single IP address for devices */ private async probeIp( ip: string, opts: Required> ): Promise { const devices: INetworkScanDevice[] = []; const timeout = opts.timeout; // First, do a quick port scan to see which ports are open const openPorts = await this.scanPorts(ip, opts.ports, timeout / 2); if (openPorts.length === 0 || this.cancelled) { return devices; } // Probe each open port for specific protocols const probePromises: Promise[] = []; for (const port of openPorts) { // IPP probe (port 631) if (opts.probeIpp && port === 631) { probePromises.push( this.probeIpp(ip, port, timeout).then((device) => { if (device) devices.push(device); }) ); } // eSCL probe (ports 80, 443) if (opts.probeEscl && (port === 80 || port === 443)) { probePromises.push( this.probeEscl(ip, port, timeout).then((device) => { if (device) devices.push(device); }) ); } // SANE probe (port 6566) if (opts.probeSane && port === 6566) { probePromises.push( this.probeSane(ip, port, timeout).then((device) => { if (device) devices.push(device); }) ); } // JetDirect probe (port 9100) - just mark as raw printer if (port === 9100) { devices.push({ type: 'printer', protocol: 'jetdirect', port: 9100, name: `Raw Printer at ${ip}`, }); } } await Promise.all(probePromises); return devices; } /** * Quick TCP port scan */ private async scanPorts(ip: string, ports: number[], timeout: number): Promise { const openPorts: number[] = []; const scanPromises = ports.map(async (port) => { const isOpen = await this.isPortOpen(ip, port, timeout); if (isOpen) { openPorts.push(port); } }); await Promise.all(scanPromises); return openPorts; } /** * Check if a TCP port is open */ private isPortOpen(ip: string, port: number, timeout: number): Promise { return new Promise((resolve) => { const socket = new plugins.net.Socket(); const timer = setTimeout(() => { socket.destroy(); resolve(false); }, timeout); socket.on('connect', () => { clearTimeout(timer); socket.destroy(); resolve(true); }); socket.on('error', () => { clearTimeout(timer); socket.destroy(); resolve(false); }); socket.connect(port, ip); }); } /** * Probe for IPP printer */ private async probeIpp( ip: string, port: number, timeout: number ): Promise { try { const ipp = new IppProtocol(ip, port); const attrs = await Promise.race([ ipp.getAttributes(), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout) ), ]); if (attrs) { return { type: 'printer', protocol: 'ipp', port, name: `IPP Printer at ${ip}`, model: undefined, }; } } catch { // Not an IPP printer } return null; } /** * Probe for eSCL scanner */ private async probeEscl( ip: string, port: number, timeout: number ): Promise { try { const secure = port === 443; const escl = new EsclProtocol(ip, port, secure); const caps = await Promise.race([ escl.getCapabilities(), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout) ), ]); if (caps) { return { type: 'scanner', protocol: 'escl', port, name: caps.makeAndModel || `eSCL Scanner at ${ip}`, model: caps.makeAndModel, }; } } catch { // Not an eSCL scanner } return null; } /** * Probe for SANE scanner (quick check) */ private async probeSane( ip: string, port: number, timeout: number ): Promise { // SANE probing requires full protocol implementation // For now, just check if port is open and assume it's SANE // A more thorough implementation would send SANE_NET_INIT try { const isOpen = await this.isPortOpen(ip, port, timeout); if (isOpen) { return { type: 'scanner', protocol: 'sane', port, name: `SANE Scanner at ${ip}`, }; } } catch { // Not a SANE scanner } return null; } }