netkitty
Version:
Network tools kit
305 lines (269 loc) • 10.5 kB
text/typescript
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
}
}