//@ts-ignore import * as netstat from "node-netstat"; import { Event, Emitter } from "@coder/events"; export interface PortScanner { readonly ports: ReadonlyArray; readonly onAdded: Event>; readonly onRemoved: Event>; dispose(): void; } /** * Creates a disposable port scanner. * Will scan local ports and emit events when ports are added or removed. * Currently only scans TCP ports. */ export const createPortScanner = (scanInterval: number = 250): PortScanner => { const ports = new Map(); const addEmitter = new Emitter(); const removeEmitter = new Emitter(); const scan = (onCompleted: (err?: Error) => void): void => { const scanTime = Date.now(); const added: number[] = []; netstat({ done: (err: Error): void => { const removed: number[] = []; ports.forEach((value, key) => { if (value !== scanTime) { // Remove port removed.push(key); ports.delete(key); } }); if (removed.length > 0) { removeEmitter.emit(removed); } if (added.length > 0) { addEmitter.emit(added); } onCompleted(err); }, filter: { state: "LISTEN", }, }, (data: { readonly protocol: string; readonly local: { readonly port: number; readonly address: string; }; }) => { // https://en.wikipedia.org/wiki/Registered_port if (data.local.port <= 1023 || data.local.port >= 49151) { return; } // Only forward TCP ports if (!data.protocol.startsWith("tcp")) { return; } if (!ports.has(data.local.port)) { added.push(data.local.port); } ports.set(data.local.port, scanTime); }); }; let lastTimeout: NodeJS.Timer | undefined; let disposed: boolean = false; const doInterval = (): void => { scan(() => { if (disposed) { return; } lastTimeout = setTimeout(doInterval, scanInterval); }); }; doInterval(); return { get ports(): number[] { return Array.from(ports.keys()); }, get onAdded(): Event { return addEmitter.event; }, get onRemoved(): Event { return removeEmitter.event; }, dispose(): void { if (typeof lastTimeout !== "undefined") { clearTimeout(lastTimeout); } disposed = true; }, }; };