netkitty
Version:
Network tools kit
307 lines (271 loc) • 9.83 kB
text/typescript
import EventEmitter from 'events'
import {FileHandle, FileReadResult, open} from 'node:fs/promises'
import {ReadStream, WriteStream} from 'node:fs'
import DuplexPair from 'duplexpair'
import {PcapParser} from './PcapParser'
import {IPcapPacketInfo} from './interfaces/IPcapPacketInfo'
export interface IPcapReaderOptions {
filename: string
watch?: boolean
chunkSize?: number
onPacket?: (pcapPacketInfo: IPcapPacketInfo) => Promise<void> | void
onStart?: () => Promise<void> | void
onStop?: () => Promise<void> | void
onDone?: () => Promise<void> | void
onError?: (err: Error) => void
}
export class PcapReader extends EventEmitter {
protected readonly filename: string
protected readonly watch: boolean
protected readonly chunkSize: number = 1518 * 10
protected duplexPair: DuplexPair
protected parser: PcapParser
protected index: number = 0
protected offset: number = 0
protected readDone: boolean = false
protected readBufferFileHandle: FileHandle | null = null
protected onPacket?: (pcapPacketInfo: IPcapPacketInfo) => Promise<void> | void
protected onStart?: () => Promise<void> | void
protected onStop?: () => Promise<void> | void
protected onDone?: () => Promise<void> | void
protected onError?: (err: Error) => void
protected paused: boolean = false
protected get readStream(): ReadStream {
return this.duplexPair.socket2
}
protected get writeStream(): WriteStream {
return this.duplexPair.socket1
}
constructor(options: IPcapReaderOptions) {
super()
this.filename = options.filename
this.watch = !!options.watch
this.chunkSize = options.chunkSize ? options.chunkSize : this.chunkSize
if (options.onPacket) this.onPacket = options.onPacket
if (options.onStart) this.onStart = options.onStart
if (options.onStop) this.onStop = options.onStop
if (options.onDone) this.onDone = options.onDone
if (options.onError) this.onError = options.onError
}
/**
* Pause read
* @protected
*/
protected pauseRead(): void {
this.readStream.pause()
this.paused = true
}
/**
* Resume read
* @protected
*/
protected resumeRead(): void {
this.readStream.resume()
this.paused = false
}
/**
* Initialize reader
* @protected
*/
protected initReader(): void {
this.duplexPair = new DuplexPair()
this.parser = PcapParser.parse(this.readStream)
this.parser
.on('packet', async (pcapPacketInfo: IPcapPacketInfo): Promise<void> => {
this.index = pcapPacketInfo.index
this.emit('packet', pcapPacketInfo)
if (this.onPacket) {
this.pauseRead()
await this.onPacket(pcapPacketInfo)
this.resumeRead()
}
})
.on('error', (err: Error): void => {
this.emit('error', err)
if (this.onError) this.onError(err)
})
}
/**
* Reset reader
* @protected
*/
protected async reset(): Promise<void> {
await this.stop()
this.parser?.removeAllListeners()
this.index = 0
this.offset = 0
this.paused = false
this.initReader()
}
/**
* Initialize read packet file handle
* @protected
*/
protected async initReadPacketFileHandle(): Promise<FileHandle> {
return await open(this.filename, 'r')
}
/**
* Initialize read buffer file handle
* @protected
*/
protected async initReadBufferFileHandle(): Promise<FileHandle> {
if (!this.readBufferFileHandle) this.readBufferFileHandle = await open(this.filename, 'r')
return this.readBufferFileHandle
}
/**
* Read file buffer
* @protected
*/
protected async readBuffer(): Promise<boolean> {
const readBufferFileHandle: FileHandle = await this.initReadBufferFileHandle()
try {
const result: FileReadResult<Buffer> = await readBufferFileHandle.read({
buffer: Buffer.alloc(this.chunkSize),
offset: 0,
length: this.chunkSize,
position: this.offset
})
if (result.bytesRead <= 0) return true
const data: Buffer = Buffer.alloc(result.bytesRead)
result.buffer.copy(data, 0, 0, result.bytesRead)
this.offset += result.bytesRead
this.writeStream.write(data)
return false
} catch (e) {
return true
}
}
/**
* Read file continually
* @protected
*/
protected continualRead(): void {
process.nextTick(async (): Promise<void> => {
let read: boolean = true
this.once('stop', (): boolean => read = false)
let isReachEnd: boolean = false
while (read || !isReachEnd) {
if (this.paused) {
await new Promise(resolve => setTimeout(resolve, 1))
continue
}
isReachEnd = await this.readBuffer()
if (isReachEnd) {
await new Promise(resolve => setTimeout(resolve, 1))
}
}
await this.readBufferFileHandle?.close()
this.readBufferFileHandle = null
this.readDone = true
this.emit('done')
if (this.onDone) await this.onDone()
})
}
/**
* Read the file straight
* @protected
*/
protected straightRead(): void {
process.nextTick(async (): Promise<void> => {
let isReachEnd: boolean = false
let stopped: boolean = false
this.once('stop', (): boolean => stopped = true)
while (!isReachEnd && !stopped) {
if (this.paused) {
await new Promise(resolve => setTimeout(resolve, 1))
continue
}
isReachEnd = await this.readBuffer()
}
this.readDone = true
if (!stopped) await this.stop()
await this.readBufferFileHandle?.close()
this.readBufferFileHandle = null
this.emit('done')
if (this.onDone) await this.onDone()
})
}
/**
* Read packet data by record's offset and record's length
* @param offset
* @param length
*/
public async readPacket(offset: number, length: number): Promise<Buffer> {
const fileHandle: FileHandle = await this.initReadPacketFileHandle()
const recordHeaderLength: number = 16
const packetLength: number = length - recordHeaderLength
const result: FileReadResult<Buffer> = await fileHandle.read({
buffer: Buffer.alloc(packetLength),
offset: 0,
length: packetLength,
position: offset + recordHeaderLength
})
await fileHandle.close()
return result.buffer
}
/**
* Start reading pcap
*/
public async start(): Promise<void> {
await this.reset()
this.emit('start')
if (this.onStart) await this.onStart()
if (this.watch) {
this.continualRead()
} else {
this.straightRead()
}
}
/**
* Stop reading pcap
*/
public async stop(): Promise<void> {
if (this.parser) {
this.emit('stop')
if (!this.readDone) await new Promise(resolve => this.once('done', resolve))
if (this.onStop) await this.onStop()
}
if (this.duplexPair) {
await new Promise<void>(resolve => {
if (this.writeStream.closed) return resolve()
this.writeStream.once('close', () => resolve())
this.writeStream.once('error', err => !!err)
this.writeStream.destroy()
})
await new Promise<void>(resolve => {
if (this.readStream.closed) return resolve()
this.readStream.once('close', () => resolve())
this.readStream.once('error', err => !!err)
this.readStream.destroy()
})
}
}
/**
* Close pcap reader
*/
public async close(): Promise<void> {
await this.stop()
this.emit('close')
this.removeAllListeners()
}
public on(eventName: 'packet', listener: (pcapPacketInfo: IPcapPacketInfo) => void): this
public on(eventName: 'done', listener: (...args: any[]) => void): this
public on(eventName: 'start', listener: (...args: any[]) => void): this
public on(eventName: 'stop', listener: (...args: any[]) => void): this
public on(eventName: 'close', listener: (...args: any[]) => void): this
public on(eventName: 'error', listener: (error: Error) => 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: 'done', listener: (...args: any[]) => void): this
public once(eventName: 'start', listener: (...args: any[]) => void): this
public once(eventName: 'stop', listener: (...args: any[]) => void): this
public once(eventName: 'close', listener: (...args: any[]) => void): this
public once(eventName: 'error', listener: (error: Error) => void): this
public once(eventName: string, listener: (...args: any[]) => void): this {
super.once(eventName, listener)
return this
}
}