diff --git a/.gitignore b/.gitignore index 759f8bd..faf12b9 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,6 @@ node_modules build #OSX garbage -.DS_Store \ No newline at end of file +.DS_Store +examples/*.pcap +.vscode/ \ No newline at end of file diff --git a/examples/pcap_dump_test.js b/examples/pcap_dump_test.js new file mode 100644 index 0000000..6694472 --- /dev/null +++ b/examples/pcap_dump_test.js @@ -0,0 +1,17 @@ + + + +var pcap = require("../pcap"), + +pcap_dump = new pcap.PcapDumpSession('en0', "ip proto \\tcp",10*1024*1024,"tmp95.pcap",false,5); + +pcap_dump.on('pcap_write_complete_async',function(message){ + console.log("done.....",message); +}); + +pcap_dump.on('pcap_write_error',function(message){ + console.log("pcap_write_error.....",message); +}); + +//pcap_dump.start(); +pcap_dump.startAsyncCapture(); diff --git a/package.json b/package.json index 4ca95cf..46a69a6 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "istanbul": "^0.3.5", "mocha": "^2.1.0", "mocha-sinon": "^1.1.4", + "rewire": "^2.5.2", "should": "^5.0.0", "sinon": "^1.14.1" }, diff --git a/pcap.js b/pcap.js index cd31439..4863ecd 100644 --- a/pcap.js +++ b/pcap.js @@ -6,11 +6,13 @@ var decode = require("./decode").decode; var tcp_tracker = require("./tcp_tracker"); var DNSCache = require("./dns_cache"); var timers = require("timers"); +var pcap_dump = require("./pcap_dump"); exports.decode = decode; exports.TCPTracker = tcp_tracker.TCPTracker; exports.TCPSession = tcp_tracker.TCPSession; exports.DNSCache = DNSCache; +exports.PcapDumpSession = pcap_dump.PcapDumpSession; function PcapSession(is_live, device_name, filter, buffer_size, outfile, is_monitor) { this.is_live = is_live; diff --git a/pcap_binding.cc b/pcap_binding.cc index 06ff64f..0c23abd 100644 --- a/pcap_binding.cc +++ b/pcap_binding.cc @@ -12,6 +12,156 @@ #include "pcap_session.h" using namespace v8; +using Nan::Callback; +using Nan::AsyncQueueWorker; +using Nan::AsyncWorker; +using Nan::Callback; +using Nan::HandleScope; +using Nan::New; +using Nan::Null; +using Nan::To; + +class PcapWorker : public AsyncWorker { + public: + PcapWorker( + Callback *callback, + std::string device, + std::string filter, + int buffer_size, + std::string pcap_output_filename, + int num_packets + ) + : + AsyncWorker(callback), + device(device), + filter(filter), + buffer_size(buffer_size), + pcap_output_filename(pcap_output_filename), + num_packets(num_packets) + {} + ~PcapWorker() {} + + // Executed inside the worker-thread. + // It is not safe to access V8, or V8 data structures + // here, so everything we need for input and output + // should go on `this`. + void Execute () { + if (pcap_lookupnet(device.c_str(), &net, &mask, errbuf) == -1) { + net = 0; + mask = 0; + fprintf(stderr, "warning: %s - this may not actually work\n", errbuf); + SetErrorMessage(errbuf); + return; + } + pcap_handle = pcap_create(device.c_str(), errbuf); + if (pcap_handle == NULL) { + SetErrorMessage(errbuf); + return; + } + + // 64KB is the max IPv4 packet size + if (pcap_set_snaplen(pcap_handle, 65535) != 0) { + SetErrorMessage("error setting snaplen"); + return; + } + + // always use promiscuous mode + if (pcap_set_promisc(pcap_handle, 1) != 0) { + SetErrorMessage("error setting promiscuous mode"); + return; + } + + // Try to set buffer size. Sometimes the OS has a lower limit that it will silently enforce. + if (pcap_set_buffer_size(pcap_handle, buffer_size) != 0) { + SetErrorMessage("error setting buffer size"); + return; + } + + + // set "timeout" on read, even though we are also setting nonblock below. On Linux this is required. + if (pcap_set_timeout(pcap_handle, 1000) != 0) { + SetErrorMessage("error setting read timeout"); + return; + } + + if (pcap_activate(pcap_handle) != 0) { + SetErrorMessage(pcap_geterr(pcap_handle)); + return; + } + if ((pcap_output_filename.size()) > 0) { + pcap_dump_handle = pcap_dump_open(pcap_handle,pcap_output_filename.c_str()); + if (pcap_dump_handle == NULL) { + SetErrorMessage("error opening dump"); + return; + } + } + + + if (filter.size() != 0) { + if (pcap_compile(pcap_handle, &fp, filter.c_str(), 1, net) == -1) { + SetErrorMessage(pcap_geterr(pcap_handle)); + return; + } + + if (pcap_setfilter(pcap_handle, &fp) == -1) { + SetErrorMessage(pcap_geterr(pcap_handle)); + return; + } + + pcap_loop(pcap_handle, num_packets, OnPacketReady, (unsigned char *)pcap_dump_handle); + pcap_freecode(&fp); + /* + * Close the savefile opened in pcap_dump_open(). + */ + pcap_dump_close(pcap_dump_handle); + /* + * Close the packet capture device and free the memory used by the + * packet capture descriptor. + */ + pcap_close(pcap_handle); + } + } + + // Executed when the async work is complete + // this function will be run inside the main event loop + // so it is safe to use V8 again + void HandleOKCallback () { + + Nan::HandleScope scope; + + Local argv[] = { + Nan::Null() + , New(num_packets) + }; + + callback->Call(2, argv); + } + + + +static void OnPacketReady(u_char *s, const struct pcap_pkthdr* pkthdr, const u_char* packet) { + pcap_dump(s, pkthdr, packet); +} + + private: + + std::string device; + std::string filter; + int buffer_size; + std::string pcap_output_filename; + int num_packets; + struct bpf_program fp; + bpf_u_int32 mask; + bpf_u_int32 net; + pcap_t *pcap_handle; + pcap_dumper_t *pcap_dump_handle; + char errbuf[PCAP_ERRBUF_SIZE]; + + + + +}; + // Helper method, convert a sockaddr* (AF_INET or AF_INET6) to a string, and set it as the property // named 'key' in the Address object you pass in. @@ -135,6 +285,57 @@ NAN_METHOD(LibVersion) info.GetReturnValue().Set(Nan::New(pcap_lib_version()).ToLocalChecked()); } +// Asynchronous access to the `Estimate()` function +NAN_METHOD(PcapDumpAsync) { + + if (info.Length() == 8) { + if (!info[0]->IsString()) { + Nan::ThrowTypeError("pcap Open: info[0] must be a String"); + return; + } + if (!info[1]->IsString()) { + Nan::ThrowTypeError("pcap Open: info[1] must be a String"); + return; + } + if (!info[2]->IsInt32()) { + Nan::ThrowTypeError("pcap Open: info[2] must be a Number"); + return; + } + if (!info[3]->IsString()) { + Nan::ThrowTypeError("pcap Open: info[3] must be a String"); + return; + } + if (!info[4]->IsFunction()) { + Nan::ThrowTypeError("pcap Open: info[4] must be a Function"); + return; + } + if (!info[5]->IsBoolean()) { + Nan::ThrowTypeError("pcap Open: info[5] must be a Boolean"); + return; + } + if (!info[6]->IsInt32()) { + Nan::ThrowTypeError("pcap Open: info[6] must be a Number"); + return; + } + if (!info[7]->IsFunction()) { + Nan::ThrowTypeError("pcap Open: info[7] must be a Function"); + return; + } + } else { + Nan::ThrowTypeError("pcap CreatePcapDump: expecting 7 arguments"); + return; + } + Nan::Utf8String device(info[0]->ToString()); + Nan::Utf8String filter(info[1]->ToString()); + int buffer_size = info[2]->Int32Value(); + Nan::Utf8String pcap_output_filename(info[3]->ToString()); + int num_packets = info[6]->Int32Value(); + Callback *callback = new Callback(info[7].As()); + + AsyncQueueWorker(new PcapWorker(callback, std::string(*device),std::string(*filter),buffer_size, std::string(*pcap_output_filename),num_packets)); +} + + void Initialize(Handle exports) { Nan::HandleScope scope; @@ -144,6 +345,7 @@ void Initialize(Handle exports) exports->Set(Nan::New("findalldevs").ToLocalChecked(), Nan::New(FindAllDevs)->GetFunction()); exports->Set(Nan::New("default_device").ToLocalChecked(), Nan::New(DefaultDevice)->GetFunction()); exports->Set(Nan::New("lib_version").ToLocalChecked(), Nan::New(LibVersion)->GetFunction()); + exports->Set(Nan::New("create_pcap_dump_async").ToLocalChecked(), Nan::New(PcapDumpAsync)->GetFunction()); } NODE_MODULE(pcap_binding, Initialize) diff --git a/pcap_dump.js b/pcap_dump.js new file mode 100644 index 0000000..c691d1d --- /dev/null +++ b/pcap_dump.js @@ -0,0 +1,90 @@ +var util = require("util"); +var events = require("events"); +var binding = require("./build/Release/pcap_binding"); + + +function PcapDumpSession(device_name, filter, buffer_size, outfile, is_monitor, number_of_packets_to_be_read) { + + this.device_name = device_name; + this.filter = filter || ""; + this.buffer_size = buffer_size; + this.outfile = outfile || "tmp.pcap"; + this.is_monitor = Boolean(is_monitor); + this.opened = null; + this.packets_read = 0; + this.number_of_packets_to_be_read = number_of_packets_to_be_read || 1; + this.session = new binding.PcapSession(); + + if (typeof this.buffer_size === "number" && !isNaN(this.buffer_size)) { + this.buffer_size = Math.round(this.buffer_size); + } else { + this.buffer_size = 10 * 1024 * 1024; // Default buffer size is 10MB + } + + this.device_name = this.device_name || binding.default_device(); + events.EventEmitter.call(this); +} + +util.inherits(PcapDumpSession, events.EventEmitter); + +exports.lib_version = binding.lib_version(); + +exports.findalldevs = function () { + return binding.findalldevs(); +}; + + +PcapDumpSession.prototype.startAsyncCapture = function () { + + this.opened = true; + binding.create_pcap_dump_async( + this.device_name, + this.filter, + this.buffer_size, + this.outfile, + PcapDumpSession.prototype.on_packet.bind(this), + this.is_monitor, + this.number_of_packets_to_be_read, + PcapDumpSession.prototype.on_pcap_write_complete_async.bind(this) + ); +}; + + +PcapDumpSession.prototype.close = function () { + this.opened = false; + this.session.close(); +}; + +PcapDumpSession.prototype.stats = function () { + return this.session.stats(); +}; + +PcapDumpSession.prototype.on_pcap_write_complete_async = function (err, packet_count) { + + if (err) { + this.emit("pcap_write_error", err); + + } else { + this.emit("pcap_write_complete_async", { + "packets_read": packet_count, + "fileName": this.outfile + }); + } + +}; + + +PcapDumpSession.prototype.on_packet = function (packet) { + this.emit("packet", packet); + +}; + + + + +exports.PcapDumpSession = PcapDumpSession; + + +exports.createPcapDumpSession = function (device, filter, buffer_size, path, monitor, number_of_packets) { + return new PcapDumpSession(device, filter, buffer_size, path, monitor, number_of_packets); +}; \ No newline at end of file diff --git a/spec/pcap_dump.spec.js b/spec/pcap_dump.spec.js new file mode 100644 index 0000000..4366a95 --- /dev/null +++ b/spec/pcap_dump.spec.js @@ -0,0 +1,128 @@ +var + rewire = require("rewire"), + pcap_dump = rewire("../pcap_dump"), + should = require("should"), + sinon = require("sinon"); + +describe("pcap_dump", function() { + beforeEach(function() { + var pcapServiceMock = { + + default_device: function() { + return "en0"; + }, + PcapSession: function() { + return mysessionObject; + }, + findalldevs: function() { + return ["en0", "eth0"]; + }, + /* jshint ignore:start */ + create_pcap_dump_async: function(device_name, filter, buffer_size, outfile, packet_ready, is_monitor, packet_write_complete) { + return "LINKTYPE_ETHERNET"; + } + /* jshint ignore:end */ + }; + var mysessionObject = { + /* jshint ignore:start */ + open_live: function(device_name, filter, buffer_size, outfile, packet_ready, is_monitor) { + return "LINKTYPE_ETHERNET"; + }, + open_offline: function(device_name, filter, buffer_size, outfile, packet_ready, is_monitor) { + + return "LINKTYPE_ETHERNET"; + }, + + fileno: function() { + return "123"; + }, + + stats: function() { + + }, + close: function() { + + } + /* jshint ignore:end */ + }; + pcap_dump.__set__({ + "binding": pcapServiceMock + + }); + + }); + + + + describe("#start should take the default values ", function() { + beforeEach(function() { + this.instance = pcap_dump.createPcapDumpSession(); + }); + it("buffer size should be 10485760 ", function() { + this.instance.startAsyncCapture(); + should(this.instance.buffer_size).be.equal(10485760); + }); + + it("outfile should be tmp.pcap ", function() { + this.instance.startAsyncCapture(); + + should(this.instance.outfile).be.equal("tmp.pcap"); + + }); + + it("number_of_packets_to_be_read should be 1", function() { + this.instance.startAsyncCapture(); + + should(this.instance.number_of_packets_to_be_read).be.equal(1); + + }); + + it("is_monitor should be false", function() { + this.instance.startAsyncCapture(); + + should(this.instance.is_monitor).be.equal(false); + + }); + + it("calls sesssion.close on calling close", function() { + var mock = sinon.mock(this.instance.session); + mock.expects("close").returns(true); + this.instance.close(); + mock.verify(); + mock.restore(); + + }); + + it("calls sesssion.stats on calling stats", function() { + var mock = sinon.mock(this.instance.session); + mock.expects("stats").returns(true); + this.instance.stats(); + mock.verify(); + mock.restore(); + + }); + + + }); + + + describe("#start should values given in ", function() { + beforeEach(function() { + //(device_name, filter, buffer_size, outfile, packet_ready, is_monitor) + this.instance = pcap_dump.createPcapDumpSession("eth1", "dst", 1000, "", "", false); + }); + it("buffer size should be 1000 ", function() { + this.instance.startAsyncCapture(); + should(this.instance.buffer_size).be.equal(1000); + }); + it("findalldevs should return the arrayy ", function() { + this.instance.startAsyncCapture(); + var devices = pcap_dump.findalldevs(); + should(devices[0]).be.equal("en0"); + should(devices[1]).be.equal("eth0"); + + }); + + }); + +}); \ No newline at end of file diff --git a/spec/pcap_test.js b/spec/pcap_test.js new file mode 100644 index 0000000..7958ce2 --- /dev/null +++ b/spec/pcap_test.js @@ -0,0 +1,100 @@ +var + rewire = require("rewire"), + pcap = rewire("../pcap"); + +describe("pcap Tests", function() { + beforeEach(function() { + //is_live, device_name, filter, buffer_size, outfile, is_monitor + var pcapServiceMock = { + + default_device: function() { + return "en0"; + }, + PcapSession: function() { + return mysessionObject; + }, + findalldevs: function(){ + return ["en0","eth0"]; + } + }; + + var mysessionObject = { + /* jshint ignore:start */ + open_live: function(device_name, filter, buffer_size, outfile, packet_ready, is_monitor) { + return "LINKTYPE_ETHERNET"; + }, + open_offline: function(device_name, filter, buffer_size, outfile, packet_ready, is_monitor) { + + return "LINKTYPE_ETHERNET"; + }, + /* jshint ignore:end */ + fileno: function() { + return "123"; + } + + }; + var SocketWatcherMock = function() { + + }; + + /* jshint ignore:start */ + SocketWatcherMock.prototype.set = function(fd, two, three) { + + }; + /* jshint ignore:end */ + SocketWatcherMock.prototype.start = function() { + + }; + + pcap.__set__({ + "binding": pcapServiceMock, + "SocketWatcher": SocketWatcherMock + }); + + }); + + describe("#Initializing with default values ", function() { + + + it("link type should be LINKTYPE_ETHERNET", function() { + this.instance = pcap.createSession("en0", "ip tcp", 1000, "", false); + this.instance.link_type.should.equal("LINKTYPE_ETHERNET"); + + }); + + it(" device_name should be en0", function() { + this.instance = pcap.createSession("en0", "ip tcp", 1000, "", false); + + this.instance.device_name.should.equal("en0"); + + }); + + it(" filter should be ip tcp", function() { + this.instance = pcap.createSession("en0", "ip tcp", 1000, "", false); + this.instance.filter.should.equal("ip tcp"); + + }); + + it(" monitor should be false", function() { + this.instance = pcap.createSession("en0", "ip tcp", 1000, "", false); + this.instance.is_monitor.should.equal(false); + + }); + + it(" buffer_size should be 1000", function() { + this.instance = pcap.createSession("en0", "ip tcp", 1000, "", false); + this.instance.buffer_size.should.equal(1000); + + }); + it(" buffer_size should be 10 * 1024 * 1024 by default", function() { + this.instance = pcap.createSession("en0", "ip tcp", "", "", false); + this.instance.buffer_size.should.equal(10 * 1024 * 1024); + + }); + + + }); + + + +}); \ No newline at end of file diff --git a/tmp.pcap b/tmp.pcap new file mode 100644 index 0000000..a324304 Binary files /dev/null and b/tmp.pcap differ