All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good
- Refactored WebSocketScanner to support both worker-based and in-process scanning, allowing for better performance based on the environment. - Introduced a new WebSocketScannerWorker class to handle scanning tasks, improving modularity and separation of concerns. - Added detailed event handling for service discovery and scan progress, enhancing real-time feedback during network scans. - Implemented graceful shutdown and cleanup of workers, ensuring resource management and stability during scanning operations. - Enhanced error handling and logging throughout the scanning process for better visibility and debugging.
241 lines
7.2 KiB
JavaScript
241 lines
7.2 KiB
JavaScript
import { EventEmitter } from 'events';
|
|
import os from 'os';
|
|
import { Worker } from 'worker_threads';
|
|
import { WebSocketScannerWorker } from './websocketScannerWorker.js';
|
|
|
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
const workerUrl = new URL('./websocketScannerWorker.js', import.meta.url);
|
|
|
|
export class WebSocketScanner extends EventEmitter {
|
|
constructor(options = {}) {
|
|
super();
|
|
this.scanning = false;
|
|
this.stopped = false;
|
|
this.workers = [];
|
|
this.inProcessWorker = null;
|
|
this.useWorkers = !isBun;
|
|
this.maxThreads = this.useWorkers
|
|
? (options.maxThreads || os.cpus().length)
|
|
: Math.max(1, Math.min(options.maxThreads || 16, 24));
|
|
this.totalIPs = 0;
|
|
this.scannedIPs = 0;
|
|
}
|
|
|
|
/**
|
|
* Scans the local network for services on a specified port.
|
|
* When loop is true, repeats until stopScan() is called.
|
|
*/
|
|
async scanNetwork(port, protocol, objectType = 'printer', { loop = false } = {}) {
|
|
this.stopped = false;
|
|
this.scanning = true;
|
|
let foundServices = [];
|
|
|
|
try {
|
|
do {
|
|
await this.cleanupWorkers();
|
|
if (this.stopped) {
|
|
break;
|
|
}
|
|
|
|
foundServices = await this.scanOnce(port, protocol, objectType);
|
|
|
|
if (this.stopped) {
|
|
break;
|
|
}
|
|
|
|
this.emit('scanPassComplete', foundServices);
|
|
} while (loop && !this.stopped);
|
|
} finally {
|
|
await this.cleanupWorkers();
|
|
this.scanning = false;
|
|
}
|
|
|
|
return foundServices;
|
|
}
|
|
|
|
async scanOnce(port, protocol, objectType) {
|
|
this.scannedIPs = 0;
|
|
const foundServices = [];
|
|
|
|
const { startIP, endIP } = this.getLocalNetworkRange();
|
|
const start = this.ipToNumber(startIP);
|
|
const end = this.ipToNumber(endIP);
|
|
|
|
this.totalIPs = end - start + 1;
|
|
|
|
if (this.useWorkers) {
|
|
await this.scanWithWorkers(start, end, port, protocol, objectType, foundServices);
|
|
} else {
|
|
await this.scanInProcess(startIP, endIP, port, protocol, objectType, foundServices);
|
|
}
|
|
|
|
await this.cleanupWorkers();
|
|
return foundServices;
|
|
}
|
|
|
|
handleWorkerMessage(message, foundServices) {
|
|
if (this.stopped) {
|
|
return;
|
|
}
|
|
|
|
switch (message.type) {
|
|
case 'serviceFound':
|
|
foundServices.push({ ip: message.ip, hostname: message.hostname });
|
|
this.emit('serviceFound', { ip: message.ip, hostname: message.hostname });
|
|
break;
|
|
case 'scanProgress':
|
|
this.scannedIPs += message.increment;
|
|
this.emit('scanProgress', {
|
|
currentIP: message.currentIP,
|
|
progress: (this.scannedIPs / this.totalIPs) * 100
|
|
});
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
async scanInProcess(startIP, endIP, port, protocol, objectType, foundServices) {
|
|
const worker = new WebSocketScannerWorker((message) => {
|
|
this.handleWorkerMessage(message, foundServices);
|
|
});
|
|
this.inProcessWorker = worker;
|
|
|
|
await worker.scanRange(startIP, endIP, port, protocol, objectType, {
|
|
concurrency: this.maxThreads
|
|
});
|
|
}
|
|
|
|
async scanWithWorkers(start, end, port, protocol, objectType, foundServices) {
|
|
const ipsPerWorker = Math.ceil(this.totalIPs / this.maxThreads);
|
|
const workerPromises = [];
|
|
|
|
for (let i = 0; i < this.maxThreads; i++) {
|
|
if (this.stopped) {
|
|
break;
|
|
}
|
|
|
|
const workerStart = start + (i * ipsPerWorker);
|
|
const workerEnd = Math.min(workerStart + ipsPerWorker - 1, end);
|
|
|
|
if (workerStart > end) {
|
|
break;
|
|
}
|
|
|
|
const worker = new Worker(workerUrl);
|
|
this.workers.push(worker);
|
|
|
|
const workerPromise = new Promise((resolve) => {
|
|
let settled = false;
|
|
const finish = (results) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
resolve(results || []);
|
|
};
|
|
|
|
worker.on('message', (message) => {
|
|
if (this.stopped) {
|
|
finish([]);
|
|
return;
|
|
}
|
|
|
|
if (message.type === 'scanComplete') {
|
|
finish(message.results);
|
|
return;
|
|
}
|
|
|
|
this.handleWorkerMessage(message, foundServices);
|
|
});
|
|
worker.on('error', () => finish([]));
|
|
worker.on('exit', () => finish([]));
|
|
});
|
|
|
|
worker.postMessage({
|
|
type: 'scan',
|
|
startIP: this.numberToIP(workerStart),
|
|
endIP: this.numberToIP(workerEnd),
|
|
port,
|
|
protocol,
|
|
objectType
|
|
});
|
|
|
|
workerPromises.push(workerPromise);
|
|
}
|
|
|
|
await Promise.all(workerPromises);
|
|
}
|
|
|
|
async cleanupWorkers() {
|
|
if (this.inProcessWorker) {
|
|
this.inProcessWorker.stop();
|
|
this.inProcessWorker = null;
|
|
}
|
|
|
|
const workers = this.workers.splice(0, this.workers.length);
|
|
await Promise.all(workers.map((worker) => this.terminateWorker(worker)));
|
|
}
|
|
|
|
terminateWorker(worker) {
|
|
return Promise.race([
|
|
Promise.resolve().then(() => worker.terminate()),
|
|
new Promise((resolve) => setTimeout(resolve, 500))
|
|
]).catch(() => {});
|
|
}
|
|
|
|
getLocalNetworkRange() {
|
|
const interfaces = os.networkInterfaces();
|
|
let localIP = null;
|
|
let subnetMask = null;
|
|
|
|
for (const name of Object.keys(interfaces)) {
|
|
for (const iface of interfaces[name]) {
|
|
const isIPv4 = iface.family === 'IPv4' || iface.family === 4;
|
|
if (isIPv4 && !iface.internal) {
|
|
localIP = iface.address;
|
|
subnetMask = iface.netmask;
|
|
break;
|
|
}
|
|
}
|
|
if (localIP) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!localIP) {
|
|
throw new Error('Could not determine local network IP address');
|
|
}
|
|
|
|
const ipNum = this.ipToNumber(localIP);
|
|
const maskNum = this.ipToNumber(subnetMask);
|
|
const networkNum = ipNum & maskNum;
|
|
const broadcastNum = networkNum | (~maskNum >>> 0);
|
|
|
|
return {
|
|
startIP: this.numberToIP(networkNum + 1),
|
|
endIP: this.numberToIP(broadcastNum - 1)
|
|
};
|
|
}
|
|
|
|
ipToNumber(ip) {
|
|
return ip.split('.')
|
|
.reduce((acc, octet) => (acc << 8) + parseInt(octet), 0) >>> 0;
|
|
}
|
|
|
|
numberToIP(num) {
|
|
return [
|
|
(num >>> 24) & 255,
|
|
(num >>> 16) & 255,
|
|
(num >>> 8) & 255,
|
|
num & 255
|
|
].join('.');
|
|
}
|
|
|
|
async stopScan() {
|
|
this.stopped = true;
|
|
this.scanning = false;
|
|
await this.cleanupWorkers();
|
|
}
|
|
}
|