const { Duplex } = require('stream');
const { PcapDevice: LiveDeviceCxx } = require('#lib/bindings');
const { pick } = require('#lib/pick');
const { Packet } = require('#lib/packet');
const optionsKeys = [
'capture',
'parse',
'mode',
'direction',
'packetBufferTimeoutMs',
'packetBufferSize',
'snapshotLength',
'nflogGroup',
];
const manualOptionsKeys = ['filter', 'iface'];
const getOptions = obj => pick(obj, ...optionsKeys, ...manualOptionsKeys);
/**
* @typedef {Object} LiveDeviceOptions
* @property {string} [mode] - The mode of the device, either "promiscuous" or "normal".
* @property {string} [direction] - The direction of packet capture, either "inout", "in", or "out".
* @property {number} [packetBufferTimeoutMs] - The packet buffer timeout in milliseconds.
* @property {number} [packetBufferSize] - The size of the packet buffer.
* @property {number} [snapshotLength] - The snapshot length for packet capture.
* @property {number} [nflogGroup] - The NFLOG group.
* @property {string} [iface] - The network interface name.
* @property {string} [filter] - The filter string for packet capture.
*/
/**
* @typedef {Object} DeviceStats
* @property {number} packetsDrop - The number of packets dropped.
* @property {number} packetsDropByInterface - The number of packets dropped by the interface.
* @property {number} packetsRecv - The number of packets received.
*/
/**
* @typedef {Object} InterfaceInfo
* @property {string} name - The name of the network interface.
* @property {string} description - The description of the network interface.
* @property {string} mac - The MAC address of the network interface.
* @property {string} gateway - The gateway address of the network interface.
* @property {number} mtu - The Maximum Transmission Unit size.
* @property {string} linktype - The link type of the network interface.
* @property {string[]} dnsServers - The DNS servers associated with the network interface.
* @property {string[]} addresses - The IP addresses associated with the network interface.
*/
/**
* Duplex stream for capturing and injecting packets on a specific device.
* @extends Duplex
*/
class LiveDevice extends Duplex {
/**
* Creates an instance of LiveDevice.
* @param {LiveDeviceOptions} options - The options for the LiveDevice instance.
*/
constructor(options = {}) {
super({ objectMode: true });
this.options = getOptions(options);
this.isOpen = false;
this.capturing = false;
this.optionsChanged = false;
optionsKeys.forEach(opt => {
Object.defineProperty(this, opt, {
get() {
return this.options[opt];
},
set(val) {
this.optionsChanged = true;
return this.options[opt] = val;
},
});
});
this.options.push = (buffer) => {
if (!this._ifaceCached) {
this._ifaceCached = this.iface;
}
const res = this.push(new Packet({ buffer, iface: this._iface }));
if (!res) {
this.pcapInternal.stopCapture();
this.capturing = false;
}
};
this.pcapInternal = new LiveDeviceCxx(this.options);
}
_construct(callback) {
if (this.optionsChanged) {
this.pcapInternal.setConfig(this.options);
}
if (!this.pcapInternal) {
return callback();
}
try {
this.pcapInternal.open();
} catch (err) {
return callback(err);
}
this.isOpen = true;
if (this.options.filter) {
this.pcapInternal.setFilter(this.options.filter);
}
callback();
}
_read(size) {
if (this.options.capture === false) {
this.push(null);
} else if (!this.capturing) {
this.pcapInternal.startCapture();
this.capturing = true;
}
}
_write(chunk, encoding, callback) {
if (chunk instanceof Packet) {
return this.pcapInternal._write(chunk.buffer, callback);
}
if (Buffer.isBuffer(chunk) || ArrayBuffer.isView(chunk)) {
if (chunk.length > 0) {
return this.pcapInternal._write(chunk, callback);
} else {
callback();
}
} else {
callback(new Error('Invalid argument - expected Packet | Buffer | TypedArray'));
}
}
_writev(chunks, callback) {
return this.pcapInternal._write(chunks.map(e => e.chunk instanceof Packet ? e.chunk.buffer : e.chunk), callback);
}
_destroy(err, callback) {
if (this.pcapInternal) {
this.pcapInternal._destroy();
}
callback(err);
}
_final(callback) {
this.pcapInternal._destroy();
delete this.pcapInternal;
}
/**
* The statistics of the device.
* @throws {Error} If the device is not open.
* @type {DeviceStats}
*/
get stats() {
if (!this.isOpen) {
throw new Error('Device is not open');
}
return this.pcapInternal.stats;
}
/**
* The filter for the device.
* @type {string}
*/
set filter(filter) {
this.options.filter = filter;
if (this.isOpen) {
this.pcapInternal.setFilter(filter);
}
return filter;
}
get filter() {
return this.options.filter;
}
set iface(iface) {
this.options.iface = iface;
}
/**
* The interface information for the device.
* @type {InterfaceInfo}
*/
get iface() {
return this.pcapInternal.interfaceInfo;
}
}
module.exports = { LiveDevice };