UNPKG

netkitty

Version:
305 lines (269 loc) 10.5 kB
import {GetNetworkInterfaces} from './GetNetworkInterfaces' import {INetworkInterface} from './interfaces/INetworkInterface' import {ICaptureOptions} from './interfaces/ICaptureOptions' import {existsSync, rmSync, writeFileSync} from 'node:fs' import {GetDeviceCaptureTemporaryFilename} from '../GetDeviceCaptureTemporaryFilename' import {ChildProcess} from 'node:child_process' import path from 'node:path' import isElectron from 'is-electron' import * as childProcess from 'child_process' import {utilityProcess, UtilityProcess} from 'electron' import {PipeServer} from '../pipe/PipeServer' import {PipeClientSocket} from '../pipe/PipeClientSocket' import {randomInt} from 'crypto' import {cp, rm} from 'node:fs/promises' import EventEmitter from 'events' import {DeviceNotFoundError} from '../../errors/DeviceNotFoundError' import {tmpdir} from 'node:os' import {IPcapPacketInfo} from '../pcap/interfaces/IPcapPacketInfo' import {GeneratePCAPHeader} from '../pcap/PCAPGenerator' export class Capture extends EventEmitter { readonly #workerModule: string = path.resolve(__dirname, './workers/CaptureWorker.js') readonly #device: string readonly #tmpDir: string readonly #temporaryFilename: string readonly #pipeServer: PipeServer #hasWorker: boolean = false #worker: ChildProcess | UtilityProcess | undefined #workerId: string | undefined #workerSocket: PipeClientSocket | undefined #workerDestroying: boolean = false #started: boolean = false #paused: boolean = false #operating: boolean = false #filter: string = '' #count: number = 0 public get filter(): string { return this.#filter } public get temporaryFilename(): string { return this.#temporaryFilename } public get count(): number { return this.#count } /** * Constructor * @param options */ constructor(options: ICaptureOptions) { super() this.#workerModule = options.workerModule ? options.workerModule : this.#workerModule this.#device = options.device this.#filter = options.filter ? options.filter : '' this.#tmpDir = options.tmpDir ? options.tmpDir : path.resolve(tmpdir(), 'netkitty-tmp') if (!options.workerModule) { //Use origin worker module, check capture device is available if (!GetNetworkInterfaces().filter((availableDevice: INetworkInterface): boolean => availableDevice.name === this.#device).length) throw new DeviceNotFoundError(`Device ${this.#device} not found`) } this.#temporaryFilename = options.temporaryFilename ? options.temporaryFilename : GetDeviceCaptureTemporaryFilename(this.#device, this.#tmpDir) this.#pipeServer = new PipeServer() this.cleanTemporaryFile() writeFileSync(this.#temporaryFilename, GeneratePCAPHeader()) } /** * Create and initialize capture worker * @protected */ protected createCaptureWorker(): void { this.#hasWorker = true this.#workerId = `CW_${Date.now().toString(16)}${randomInt(32).toString(16)}` const env: Record<string, string> = { captureWorkerId: this.#workerId, captureDevice: this.#device, captureFilter: this.#filter, captureTemporaryFilename: this.#temporaryFilename, socketPath: this.#pipeServer.socketPath } this.getWorkerSocket().then((socket: PipeClientSocket): void => { socket.on('packet', (packetInfo: IPcapPacketInfo): void => { this.emit('rawPacket', packetInfo.index, packetInfo.packet, packetInfo.seconds, packetInfo.microseconds) this.emit('packet', packetInfo) this.#count += 1 }) }) this.#worker = isElectron() ? utilityProcess.fork(this.#workerModule, [], {env: env}) : childProcess.fork(this.#workerModule, [], {env: env}) } /** * Destroy capture worker * @protected */ protected async destroyCaptureWorker(): Promise<void> { if (!this.#hasWorker) return if (this.#workerDestroying) { while (this.#workerDestroying) await new Promise(resolve => setTimeout(resolve, 10)) return } this.#workerDestroying = true await new Promise<void>(resolve => { if (this.#workerSocket && this.#worker) { const disconnectHandler: () => void = (): void => { clearTimeout(notifyExitTimeout) return resolve() } this.#workerSocket.once('disconnect', disconnectHandler) this.#workerSocket.notify('exit') const notifyExitTimeout: NodeJS.Timeout = setTimeout((): void => { this.#workerSocket?.off('disconnect', disconnectHandler) this.#worker?.kill('SIGINT') return resolve() }, 10000) } }) this.#workerSocket = undefined this.#worker = undefined this.#workerId = undefined this.#workerDestroying = false this.#hasWorker = false } /** * Get worker socket * @protected */ protected async getWorkerSocket(): Promise<PipeClientSocket> { return new Promise(resolve => { if (!this.#workerSocket) { this.#pipeServer.once('connect', (clientSocket: PipeClientSocket): void => { this.#workerSocket = clientSocket return resolve(this.#workerSocket) }) } else { return resolve(this.#workerSocket) } }) } /** * Clean resources before start capture * @param removeTmpFile * @protected */ protected async cleanResources(removeTmpFile: boolean = true): Promise<void> { //Clean cached capture data if (existsSync(this.#temporaryFilename) && removeTmpFile) this.cleanTemporaryFile() //Clean old worker resources if (this.#worker && this.#workerSocket) await this.destroyCaptureWorker() } /** * Clean temporary pcap file * @protected */ protected cleanTemporaryFile(): void { rmSync(this.#temporaryFilename, { recursive: true, force: true }) } /** * Wait other operation done * @protected */ protected async waitOperationDone(): Promise<void> { while (this.#operating) await new Promise(resolve => setTimeout(resolve, 10)) } /** * Start capture packets */ public async start(): Promise<void> { if (this.#paused) return await this.resume() if (this.#started) return await this.waitOperationDone() this.#operating = true try { this.#started = true await this.cleanResources(false) this.createCaptureWorker() const workerSocket: PipeClientSocket = await this.getWorkerSocket() await workerSocket.invoke('start') } catch (e) { this.#started = false this.#operating = false throw e } this.#operating = false } /** * Stop capture packets */ public async stop(): Promise<void> { if (!this.#started) return if (this.#workerDestroying) return await this.waitOperationDone() this.#operating = true const workerSocket: PipeClientSocket = await this.getWorkerSocket() await workerSocket.invoke('stop') while (await workerSocket.invoke('count') !== this.count) await new Promise(resolve => setTimeout(resolve, 10)) await this.destroyCaptureWorker() this.#started = false this.#operating = false } /** * Pause capture packets */ public async pause(): Promise<void> { if (!this.#started) return if (!this.#hasWorker) return if (this.#workerDestroying) return await this.waitOperationDone() this.#operating = true const workerSocket: PipeClientSocket = await this.getWorkerSocket() await workerSocket.invoke('stop') while (await workerSocket.invoke('count') !== this.count) await new Promise(resolve => setTimeout(resolve, 10)) this.#paused = true this.#operating = false } /** * Resume capture packets */ public async resume(): Promise<void> { if (!this.#started) return if (!this.#hasWorker) return if (this.#workerDestroying) return await this.waitOperationDone() this.#operating = true const workerSocket: PipeClientSocket = await this.getWorkerSocket() await workerSocket.invoke('start') this.#paused = false this.#operating = false } /** * Set capture filter * @param filter */ public async setFilter(filter: string): Promise<void> { if (!this.#started) return if (!this.#hasWorker) return if (this.#workerDestroying) return await this.waitOperationDone() this.#operating = true const workerSocket: PipeClientSocket = await this.getWorkerSocket() await workerSocket.invoke('setFilter', filter) this.#filter = filter this.#operating = false } /** * Save captured pcap to destination * @param destination */ public async saveTo(destination: string): Promise<void> { await cp(this.temporaryFilename, destination) } /** * Dispose capture */ public async dispose(): Promise<void> { await this.stop() await this.cleanResources() } public on(eventName: 'packet', listener: (pcapPacketInfo: IPcapPacketInfo) => void): this public on(eventName: 'rawPacket', listener: (index: number, packet: string, seconds: number, microseconds: number) => void): this public on(eventName: string, listener: (...args: any[]) => void): this { super.on(eventName, listener) return this } public once(eventName: 'packet', listener: (pcapPacketInfo: IPcapPacketInfo) => void): this public once(eventName: 'rawPacket', listener: (index: number, packet: string, seconds: number, microseconds: number) => void): this public once(eventName: string, listener: (...args: any[]) => void): this { super.once(eventName, listener) return this } }