├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib └── pcap-parser.js ├── package.json └── test ├── be.pcap ├── malformed.pcap ├── pcap-parser-test.js └── smtp.pcap /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | coverage/ 4 | coverage.* 5 | lib-cov/ 6 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | coverage.* 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 2 | 3 | Breaking API: 4 | 5 | - Creating a parser is now done via the pcapp.parse method rather than 6 | new pcapp.Parser(). Explicitly calling the parse method on the 7 | returned parser is no longer necessary, or possible. All parsing 8 | events are emitted on the next tick. 9 | 10 | ## 0.2.1 11 | 12 | Bug: 13 | 14 | - index.js was not updated to reflect the API change in 0.2.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Near Infinity Corporation 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pcap-parser 2 | 3 | Packet capture (pcap) file parser for Node.js 4 | 5 | ## Installation 6 | 7 | $ npm install pcap-parser 8 | 9 | ## Usage 10 | 11 | ```javascript 12 | var pcapp = require('pcap-parser'); 13 | 14 | var parser = pcapp.parse('/path/to/file.pcap'); 15 | parser.on('packet', function(packet) { 16 | // do your packet processing 17 | }); 18 | ``` 19 | 20 | ## Events 21 | 22 | pcap-parser emits five different events, only some of which you'll 23 | likely care about. Each event is emitted from the parser created with 24 | `pcapp.parse`. The `pcapp.parse` method can be passed a 25 | file path or a readable stream. 26 | 27 | pcap-parser only parses version 2.4 of the libpcap file format in big 28 | or little endian format. Please see 29 | http://wiki.wireshark.org/Development/LibpcapFileFormat for detailed 30 | documentation of the pcap file format. 31 | 32 | ### globalHeader 33 | 34 | Event fired after parsing the global pcap file header. The object passed 35 | to your event listener would look something like 36 | 37 | { 38 | magicNumber: 2712847316, 39 | majorVersion: 2, 40 | minorVersion: 4, 41 | gmtOffset: 0, 42 | timestampAccuracy: 4, 43 | snapshotLength: 65535, 44 | linkLayerType: 1 45 | } 46 | 47 | ### packetHeader 48 | 49 | Event fired after parsing each packet header. The object passed to your 50 | event listener would look something like 51 | 52 | { 53 | timestampSeconds: 1254722767, 54 | timestampMicroseconds: 492060, 55 | capturedLength: 76, 56 | originalLength: 76 57 | } 58 | 59 | ### packetData 60 | 61 | Event fired after parsing each packet's data. The argument passed to the 62 | event listener is simply a buffer containing the packet data. 63 | 64 | ### packet 65 | 66 | Event fired after parsing each packet. The data structure contains both 67 | the header fields and packet data. 68 | 69 | { 70 | header: { 71 | timestampSeconds: 1254722767, 72 | timestampMicroseconds: 492060, 73 | capturedLength: 76, 74 | originalLength: 76 75 | }, 76 | 77 | data: [Buffer] 78 | } 79 | 80 | ### end 81 | 82 | Emitted after all packes in the file or stream have been parsed. There 83 | are no arguments passed to the event listener. 84 | 85 | ### error 86 | 87 | Emitted on any error from the underlying stream. The error object is 88 | passed to the event listener. 89 | 90 | ## License 91 | 92 | (The MIT License) 93 | 94 | Copyright (c) 2012 Near Infinity Corporation 95 | 96 | Permission is hereby granted, free of charge, to any person obtaining 97 | a copy of this software and associated documentation files (the 98 | "Software"), to deal in the Software without restriction, including 99 | without limitation the rights to use, copy, modify, merge, publish, 100 | distribute, sublicense, and/or sell copies of the Software, and to 101 | permit persons to whom the Software is furnished to do so, subject to 102 | the following conditions: 103 | 104 | The above copyright notice and this permission notice shall be 105 | included in all copies or substantial portions of the Software. 106 | 107 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 108 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 109 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 110 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 111 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 112 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 113 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 114 | -------------------------------------------------------------------------------- /lib/pcap-parser.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var events = require('events'); 3 | var fs = require('fs'); 4 | 5 | var GLOBAL_HEADER_LENGTH = 24; //bytes 6 | var PACKET_HEADER_LENGTH = 16; //bytes 7 | 8 | function onError(err) { 9 | this.emit('error', err); 10 | } 11 | 12 | function onEnd() { 13 | this.emit('end'); 14 | } 15 | 16 | function onData(data) { 17 | if (this.errored) { 18 | return; 19 | } 20 | 21 | updateBuffer.call(this, data); 22 | while (this.state.call(this)) {} 23 | } 24 | 25 | function updateBuffer(data) { 26 | if (data === null || data === undefined) { 27 | return; 28 | } 29 | 30 | if (this.buffer === null) { 31 | this.buffer = data; 32 | } else { 33 | var extendedBuffer = new Buffer(this.buffer.length + data.length); 34 | this.buffer.copy(extendedBuffer); 35 | data.copy(extendedBuffer, this.buffer.length); 36 | this.buffer = extendedBuffer; 37 | } 38 | } 39 | 40 | function parseGlobalHeader() { 41 | var buffer = this.buffer; 42 | 43 | if (buffer.length >= GLOBAL_HEADER_LENGTH) { 44 | var msg; 45 | var magicNumber = buffer.toString('hex', 0, 4); 46 | 47 | // determine pcap endianness 48 | if (magicNumber == "a1b2c3d4") { 49 | this.endianness = "BE"; 50 | } else if (magicNumber == "d4c3b2a1") { 51 | this.endianness = "LE"; 52 | } else { 53 | this.errored = true; 54 | this.stream.pause(); 55 | msg = util.format('unknown magic number: %s', magicNumber); 56 | this.emit('error', new Error(msg)); 57 | onEnd.call(this); 58 | return false; 59 | } 60 | 61 | var header = { 62 | magicNumber: buffer['readUInt32' + this.endianness](0, true), 63 | majorVersion: buffer['readUInt16' + this.endianness](4, true), 64 | minorVersion: buffer['readUInt16' + this.endianness](6, true), 65 | gmtOffset: buffer['readInt32' + this.endianness](8, true), 66 | timestampAccuracy: buffer['readUInt32' + this.endianness](12, true), 67 | snapshotLength: buffer['readUInt32' + this.endianness](16, true), 68 | linkLayerType: buffer['readUInt32' + this.endianness](20, true) 69 | }; 70 | 71 | if (header.majorVersion != 2 && header.minorVersion != 4) { 72 | this.errored = true; 73 | this.stream.pause(); 74 | msg = util.format('unsupported version %d.%d. pcap-parser only parses libpcap file format 2.4', header.majorVersion, header.minorVersion); 75 | this.emit('error', new Error(msg)); 76 | onEnd.call(this); 77 | } else { 78 | this.emit('globalHeader', header); 79 | this.buffer = buffer.slice(GLOBAL_HEADER_LENGTH); 80 | this.state = parsePacketHeader; 81 | return true; 82 | } 83 | } 84 | 85 | return false; 86 | } 87 | 88 | function parsePacketHeader() { 89 | var buffer = this.buffer; 90 | 91 | if (buffer.length >= PACKET_HEADER_LENGTH) { 92 | var header = { 93 | timestampSeconds: buffer['readUInt32' + this.endianness](0, true), 94 | timestampMicroseconds: buffer['readUInt32' + this.endianness](4, true), 95 | capturedLength: buffer['readUInt32' + this.endianness](8, true), 96 | originalLength: buffer['readUInt32' + this.endianness](12, true) 97 | }; 98 | 99 | this.currentPacketHeader = header; 100 | this.emit('packetHeader', header); 101 | this.buffer = buffer.slice(PACKET_HEADER_LENGTH); 102 | this.state = parsePacketBody; 103 | return true; 104 | } 105 | 106 | return false; 107 | } 108 | 109 | function parsePacketBody() { 110 | var buffer = this.buffer; 111 | 112 | if (buffer.length >= this.currentPacketHeader.capturedLength) { 113 | var data = buffer.slice(0, this.currentPacketHeader.capturedLength); 114 | 115 | this.emit('packetData', data); 116 | this.emit('packet', { 117 | header: this.currentPacketHeader, 118 | data: data 119 | }); 120 | 121 | this.buffer = buffer.slice(this.currentPacketHeader.capturedLength); 122 | this.state = parsePacketHeader; 123 | return true; 124 | } 125 | 126 | return false; 127 | } 128 | 129 | function Parser(input) { 130 | if (typeof(input) == 'string') { 131 | this.stream = fs.createReadStream(input); 132 | } else { 133 | // assume a ReadableStream 134 | this.stream = input; 135 | } 136 | 137 | this.stream.pause(); 138 | this.stream.on('data', onData.bind(this)); 139 | this.stream.on('error', onError.bind(this)); 140 | this.stream.on('end', onEnd.bind(this)); 141 | 142 | this.buffer = null; 143 | this.state = parseGlobalHeader; 144 | this.endianness = null; 145 | 146 | process.nextTick(this.stream.resume.bind(this.stream)); 147 | } 148 | util.inherits(Parser, events.EventEmitter); 149 | 150 | exports.parse = function (input) { 151 | return new Parser(input); 152 | }; 153 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pcap-parser", 3 | "description": "Packet capture (PCAP) parser for node", 4 | "author": "Jeff Kunkle ", 5 | "keywords": ["pcap", "parser"], 6 | "version": "0.2.1", 7 | "engines": { "node" : ">=0.6.0" }, 8 | "maintainers": [ 9 | { "name": "Jeff Kunkle", "email": "jeff.kunkle@nearinfinity.com" }, 10 | { "name": "Joe Ferner", "email": "joe.ferner@nearinfinity.com" } 11 | ], 12 | "bugs": { "url": "https://github.com/nearinfinity/node-pcap-parser/issues" }, 13 | "license": "MIT", 14 | "repository": { "type": "git", "url": "https://github.com/nearinfinity/node-pcap-parser.git" }, 15 | "devDependencies": { 16 | "vows": "~0.5.13" 17 | }, 18 | "scripts": { 19 | "test": "vows" 20 | }, 21 | "main": "./lib/pcap-parser.js" 22 | } 23 | -------------------------------------------------------------------------------- /test/be.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kunklejr/node-pcap-parser/677be12a02166da44cd22d8acc06af5ec05f1b06/test/be.pcap -------------------------------------------------------------------------------- /test/malformed.pcap: -------------------------------------------------------------------------------- 1 | this is not a valid pcap file. -------------------------------------------------------------------------------- /test/pcap-parser-test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'); 2 | var assert = require('assert'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var pcapp = require('../lib/pcap-parser'); 6 | 7 | vows.describe('pcap-parser').addBatch({ 8 | 'given a bad/malformed pcap file': { 9 | topic: pcapp.parse(fs.createReadStream(path.join(__dirname, 'malformed.pcap'))), 10 | 11 | 'the parser should emit an error event': { 12 | topic: function(parser) { 13 | parser.once('error', this.callback.bind(this, null)); 14 | }, 15 | 16 | 'an error event should have been emitted': function(err) { 17 | assert.isNotNull(err); 18 | } 19 | } 20 | }, 21 | 22 | 'given a readable stream of a little-endian pcap file': { 23 | topic: pcapp.parse(fs.createReadStream(path.join(__dirname, 'smtp.pcap'))), 24 | 25 | 'the parser should emit globalHeader events': { 26 | topic: function(parser) { 27 | parser.once('globalHeader', this.callback.bind(this, null)); 28 | }, 29 | 30 | 'global header values should be correct': function(header) { 31 | assert.isNotNull(header); 32 | assert.equal(header.magicNumber, 2712847316); 33 | assert.equal(header.majorVersion, 2); 34 | assert.equal(header.minorVersion, 4); 35 | assert.equal(header.gmtOffset, 0); 36 | assert.equal(header.timestampAccuracy, 0); 37 | assert.equal(header.snapshotLength, 65535); 38 | assert.equal(header.linkLayerType, 1); 39 | } 40 | }, 41 | 42 | 'the parser should emit packetHeader events': { 43 | topic: function(parser) { 44 | parser.once('packetHeader', this.callback.bind(this, null)); 45 | }, 46 | 47 | 'packet header values should be correct': function(packetHeader) { 48 | assert.isNotNull(packetHeader); 49 | assert.equal(packetHeader.timestampSeconds, 1254722767); 50 | assert.equal(packetHeader.timestampMicroseconds, 492060); 51 | assert.equal(packetHeader.capturedLength, 76); 52 | assert.equal(packetHeader.originalLength, 76); 53 | } 54 | }, 55 | 56 | 'the parser should emit packetData events': { 57 | topic: function(parser) { 58 | parser.once('packetData', this.callback.bind(this, null)); 59 | }, 60 | 61 | 'packet data buffer should not be empty': function(packetData) { 62 | assert.isNotNull(packetData); 63 | assert.equal(packetData.length, 76); 64 | } 65 | }, 66 | 67 | 'the parser should emit packet events': { 68 | topic: function(parser) { 69 | parser.once('packet', this.callback.bind(this, null)); 70 | }, 71 | 72 | 'packet values should be correct': function(packet) { 73 | assert.isNotNull(packet); 74 | assert.isDefined(packet.header); 75 | assert.isDefined(packet.data); 76 | assert.equal(packet.data.length, 76); 77 | assert.equal(packet.header.timestampSeconds, 1254722767); 78 | assert.equal(packet.header.timestampMicroseconds, 492060); 79 | assert.equal(packet.header.capturedLength, 76); 80 | assert.equal(packet.header.originalLength, 76); 81 | } 82 | }, 83 | 84 | 'the parser should emit an end event when finished': { 85 | topic: function(parser) { 86 | parser.on('end', this.callback.bind(this, null)); 87 | }, 88 | 89 | 'it should occur': function() {} 90 | }, 91 | 92 | 'the parser should parse multiple packets': { 93 | topic: function(parser) { 94 | var count = 0; 95 | 96 | parser.on('packet', function(packet) { 97 | count++; 98 | }).on('end', function() { 99 | this.callback(null, count); 100 | }.bind(this)); 101 | }, 102 | 103 | 'it should process 60 packets': function(count) { 104 | assert.equal(count, 60); 105 | } 106 | } 107 | }, 108 | 109 | 'given a readable stream of a big-endian pcap file': { 110 | topic: pcapp.parse(fs.createReadStream(path.join(__dirname, 'be.pcap'))), 111 | 112 | 'the parser should emit globalHeader events': { 113 | topic: function(parser) { 114 | parser.once('globalHeader', this.callback.bind(this, null)); 115 | }, 116 | 117 | 'global header values should be correct': function(header) { 118 | assert.isNotNull(header); 119 | assert.equal(header.magicNumber, 2712847316); 120 | assert.equal(header.majorVersion, 2); 121 | assert.equal(header.minorVersion, 4); 122 | assert.equal(header.gmtOffset, 0); 123 | assert.equal(header.timestampAccuracy, 0); 124 | assert.equal(header.snapshotLength, 9216); 125 | assert.equal(header.linkLayerType, 1); 126 | } 127 | }, 128 | 129 | 'the parser should emit packetHeader events': { 130 | topic: function(parser) { 131 | parser.once('packetHeader', this.callback.bind(this, null)); 132 | }, 133 | 134 | 'packet header values should be correct': function(packetHeader) { 135 | assert.isNotNull(packetHeader); 136 | assert.equal(packetHeader.timestampSeconds, 3064); 137 | assert.equal(packetHeader.timestampMicroseconds, 714590); 138 | assert.equal(packetHeader.capturedLength, 42); 139 | assert.equal(packetHeader.originalLength, 60); 140 | } 141 | }, 142 | 143 | 'the parser should emit packetData events': { 144 | topic: function(parser) { 145 | parser.once('packetData', this.callback.bind(this, null)); 146 | }, 147 | 148 | 'packet data buffer should not be empty': function(packetData) { 149 | assert.isNotNull(packetData); 150 | assert.equal(packetData.length, 42); 151 | } 152 | }, 153 | 154 | 'the parser should emit packet events': { 155 | topic: function(parser) { 156 | parser.once('packet', this.callback.bind(this, null)); 157 | }, 158 | 159 | 'packet values should be correct': function(packet) { 160 | assert.isNotNull(packet); 161 | assert.isDefined(packet.header); 162 | assert.isDefined(packet.data); 163 | assert.equal(packet.data.length, 42); 164 | assert.equal(packet.header.timestampSeconds, 3064); 165 | assert.equal(packet.header.timestampMicroseconds, 714590); 166 | assert.equal(packet.header.capturedLength, 42); 167 | assert.equal(packet.header.originalLength, 60); 168 | } 169 | }, 170 | 171 | 'the parser should emit an end event when finished': { 172 | topic: function(parser) { 173 | parser.on('end', this.callback.bind(this, null)); 174 | }, 175 | 176 | 'it should occur': function() {} 177 | }, 178 | 179 | 'the parser should parse multiple packets': { 180 | topic: function(parser) { 181 | var count = 0; 182 | 183 | parser.on('packet', function(packet) { 184 | count++; 185 | }).on('end', function() { 186 | this.callback(null, count); 187 | }.bind(this)); 188 | }, 189 | 190 | 'it should process 5 packets': function(count) { 191 | assert.equal(count, 5); 192 | } 193 | } 194 | }, 195 | 196 | 'given a path to a pcap file': { 197 | topic: pcapp.parse(path.join(__dirname, 'smtp.pcap')), 198 | 199 | 'the parser should emit all the same events': { 200 | topic: function(parser) { 201 | var events = { 202 | globalHeader: false, 203 | packetHeader: false, 204 | packetData: false, 205 | packet: false 206 | }; 207 | 208 | function ifDone(eventType) { 209 | events[eventType] = true; 210 | if (events.globalHeader && events.packetHeader && events.packetData && events.packet) { 211 | this.callback(null, true); 212 | } 213 | } 214 | 215 | parser.once('globalHeader', ifDone.bind(this, 'globalHeader')); 216 | parser.once('packetHeader', ifDone.bind(this, 'packetHeader')); 217 | parser.once('packetData', ifDone.bind(this, 'packetData')); 218 | parser.once('packet', ifDone.bind(this, 'packet')); 219 | }, 220 | 221 | 'all events should have been emitted': function(confirmation) { 222 | assert.isTrue(confirmation); 223 | } 224 | } 225 | } 226 | }).export(module); 227 | -------------------------------------------------------------------------------- /test/smtp.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kunklejr/node-pcap-parser/677be12a02166da44cd22d8acc06af5ec05f1b06/test/smtp.pcap --------------------------------------------------------------------------------