├── .jshintrc ├── README.md ├── ansi.js ├── http_session.js ├── http_trace └── package.json /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, 3 | "camelcase": false, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "immed": true, 8 | "indent": 4, 9 | "latedef": "nofunc", 10 | "newcap": false, 11 | "noarg": true, 12 | "nonew": true, 13 | "plusplus": false, 14 | "quotmark": "double", 15 | "regexp": false, 16 | "undef": true, 17 | "unused": true, 18 | "strict": false, 19 | "trailing": true, 20 | "noempty": true, 21 | "globals": { 22 | "console": true, 23 | "Buffer": true, 24 | "setTimeout": true, 25 | "clearTimeout": true, 26 | "setInterval": true, 27 | "clearInterval": true, 28 | "require": false, 29 | "module": false, 30 | "exports": true, 31 | "global": false, 32 | "process": true, 33 | "__dirname": false, 34 | "__filename": false 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## examples/http_trace 2 | 3 | This is a handy program that decodes HTTP and WebSocket traffic. It uses `node_pcap`. Install it with: 4 | 5 | npm install http_trace 6 | 7 | ## Usage `http_trace [options]` 8 | 9 | Capture options: 10 | -i interface name for capture (def: first with an addr) 11 | -f packet filter in pcap-filter(7) syntax (def: all TCP packets) 12 | -b size in MB to buffer between libpcap and app (def: 10) 13 | 14 | HTTP filtering: 15 | Filters are OR-ed together and may be specified more than once. 16 | Show filters are applied first, then ignore filters. 17 | --method show requests with this method 18 | --method-ignore ignore requests with this method 19 | --host show requests with this Host header 20 | --host-ignore ignore requests with this Host header 21 | --url show requests with this URL 22 | --url-ignore ignore requests with this URL 23 | --user-agent show requests with this UA header 24 | --user-agent-ignore ignore requests with this UA header 25 | 26 | HTTP output: 27 | --headers print headers of request and response (def: off) 28 | --bodies print request and response bodies, if any (def: off) 29 | --tcp-verbose display TCP events (def: off) 30 | --no-color disable ANSI colors (def: pretty colors on) 31 | 32 | Examples: 33 | http_trace -f "tcp port 80" 34 | listen for TCP port 80 on the default device 35 | http_trace -i eth1 --method POST 36 | listen on eth1 for all traffic that has an HTTP POST 37 | http_trace --host ranney --headers 38 | matches ranney in Host header and prints req/res headers 39 | 40 | ## Screenshot 41 | 42 | ![http_trace screenshot](http://ranney.com/httptrace.jpg) 43 | 44 | 45 | The TCP tracker in `node_pcap` looks for HTTP at the beginning of every TCP connection. 46 | If found, all captured data on this connection will be fed to node's HTTP parser and events will be generated. 47 | `http_trace` has listeners for these events and will print out some helpful information. 48 | 49 | If a WebSocket upgrade is detected, `http_trace` will start looking for WebSocket messages on that connection. 50 | -------------------------------------------------------------------------------- /ansi.js: -------------------------------------------------------------------------------- 1 | // http://en.wikipedia.org/wiki/ANSI_escape_code 2 | var formats = { 3 | bold: [1, 22], // bright 4 | light: [2, 22], // faint 5 | italic: [3, 23], 6 | underline: [4, 24], // underline single 7 | blink_slow: [5, 25], 8 | blink_fast: [6, 25], 9 | inverse: [7, 27], 10 | conceal: [8, 28], 11 | strikethrough: [9, 29], // crossed-out 12 | // 10 - 20 are font control 13 | underline_double: [21, 24], 14 | black: [30, 39], 15 | red: [31, 39], 16 | green: [32, 39], 17 | yellow: [33, 39], 18 | blue: [34, 39], 19 | magenta: [35, 39], 20 | cyan: [36, 39], 21 | white: [37, 39], 22 | grey: [90, 39] 23 | }; 24 | 25 | var CSI = String.fromCharCode(27) + "["; 26 | 27 | Object.keys(formats).forEach(function (format) { 28 | exports[format] = function fmt(str) { 29 | if (exports.no_color) { 30 | return str; 31 | } 32 | return CSI + formats[format][0] + "m" + str + CSI + formats[format][1] + "m"; 33 | }; 34 | }); 35 | 36 | exports.no_color = false; 37 | -------------------------------------------------------------------------------- /http_session.js: -------------------------------------------------------------------------------- 1 | var HTTPParser = process.binding("http_parser").HTTPParser; // magic alert 2 | var EventEmitter = require("events").EventEmitter; 3 | var inherits = require("util").inherits; 4 | var DNSCache = require("pcap").DNSCache; 5 | var dns_cache = new DNSCache(); 6 | 7 | function HTTPRequest() { 8 | this.start = null; 9 | this.headers = {}; 10 | this.url = null; 11 | this.method = null; 12 | this.body_len = 0; 13 | this.http_version = null; 14 | } 15 | 16 | function HTTPResponse() { 17 | this.start = null; 18 | this.headers = {}; 19 | this.status_code = null; 20 | this.body_len = 0; 21 | this.http_version = null; 22 | } 23 | 24 | function lookup(host_port) { 25 | var parts = host_port.split(":"); 26 | return dns_cache.ptr(parts[0]) + ":" + parts[1]; 27 | } 28 | 29 | // This tracks potentially multiple HTTP requests on a single TCP session 30 | function HTTPSession(tcp_session) { 31 | this.tcp_session = tcp_session; 32 | this.request = new HTTPRequest(); 33 | this.response = new HTTPResponse(); 34 | this.request_parser = new HTTPParser(HTTPParser.REQUEST); 35 | this.response_parser = new HTTPParser(HTTPParser.RESPONSE); 36 | this.request_count = 0; 37 | 38 | this.tcp_session.src_name = lookup(this.tcp_session.src_name); 39 | this.tcp_session.dst_name = lookup(this.tcp_session.dst_name); 40 | 41 | var self = this; 42 | 43 | this.request_parser.url = ""; 44 | // since the HTTP parser is a hot path in node, they use functions as properties instead of 45 | // EventEmitter, which is slightly slower. In order to implement the interface, we need to do the same. 46 | this.request_parser.onHeaders = function (headers, url) { 47 | self.on_req_headers(headers, url); 48 | }; 49 | this.request_parser.onHeadersComplete = function (info) { 50 | self.on_req_headers_complete(info); 51 | }; 52 | this.request_parser.onBody = function (buf, start, len) { 53 | self.on_req_body(buf, start, len); 54 | }; 55 | this.request_parser.onMessageComplete = function () { 56 | self.emit("http request complete", self); 57 | }; 58 | this.response_parser.onHeaders = function (headers) { 59 | self.on_res_headers(headers); 60 | }; 61 | this.response_parser.onHeadersComplete = function(info) { 62 | self.on_res_headers_complete(info); 63 | }; 64 | this.response_parser.onBody = function (buf, start, len) { 65 | self.on_res_body(buf, start, len); 66 | }; 67 | this.response_parser.onMessageComplete = function () { 68 | self.on_res_complete(); 69 | }; 70 | this.tcp_session.on("data send", function (tcp_session, chunk) { 71 | self.on_tcp_data_send(chunk); 72 | }); 73 | this.tcp_session.on("data recv", function (tcp_session, chunk) { 74 | self.on_tcp_data_recv(chunk); 75 | }); 76 | 77 | EventEmitter.call(this); 78 | } 79 | inherits(HTTPSession, EventEmitter); 80 | 81 | HTTPSession.prototype.on_req_headers = function (headers, url) { 82 | this.request_parser.headers = (this.request_parser.headers || []).concat(headers); 83 | this.request_parser.url += url; 84 | }; 85 | 86 | HTTPSession.prototype.on_req_headers_complete = function (info) { 87 | this.request.method = info.method; 88 | this.request.url = info.url || this.request_parser.url; 89 | this.request.http_version = info.versionMajor + "." + info.versionMinor; 90 | 91 | var headers = info.headers || this.request_parser.headers; 92 | for (var i = 0; i < headers.length; i += 2) { 93 | this.request.headers[headers[i]] = headers[i + 1]; 94 | } 95 | 96 | this.request_count++; 97 | this.request.start = Date.now(); 98 | 99 | this.emit("http request", this); 100 | }; 101 | 102 | HTTPSession.prototype.on_req_body = function (buf, start, len) { 103 | this.request.body_len += len; 104 | this.emit("http request body", this, buf.slice(start, start + len)); 105 | }; 106 | 107 | HTTPSession.prototype.on_res_headers = function (headers) { 108 | this.response_parser.headers = (this.response_parser.headers || []).concat(headers); 109 | }; 110 | 111 | HTTPSession.prototype.on_res_headers_complete = function (info) { 112 | this.response.status_code = info.statusCode; 113 | this.response.http_version = info.versionMajor + "." + info.versionMinor; 114 | 115 | var headers = info.headers || this.response_parser.headers; 116 | for (var i = 0; i < headers.length; i += 2) { 117 | this.response.headers[headers[i]] = headers[i + 1]; 118 | } 119 | 120 | // old websocket detect code. It would be nice to enable this again with a modern websocket decoder 121 | // 122 | // if (this.response.status_code === 101 && this.response.headers.Upgrade === "WebSocket") { 123 | // if (this.response.headers["Sec-WebSocket-Location"]) { 124 | // self.setup_websocket_tracking(session, "draft76"); 125 | // } else { 126 | // self.setup_websocket_tracking(session); 127 | // } 128 | // self.emit("websocket upgrade", session, http); 129 | // session.http_detect = false; 130 | // session.websocket_detect = true; 131 | // delete http.response_parser.onMessageComplete; 132 | // } else { 133 | 134 | this.response.start = Date.now(); 135 | 136 | this.emit("http response", this); 137 | }; 138 | 139 | HTTPSession.prototype.on_res_body = function (buf, start, len) { 140 | this.response.body_len += len; 141 | this.emit("http response body", this, buf.slice(start, start + len)); 142 | }; 143 | 144 | HTTPSession.prototype.on_res_complete = function () { 145 | this.emit("http response complete", this); 146 | 147 | this.request = new HTTPRequest(); 148 | this.response = new HTTPResponse(); 149 | }; 150 | 151 | HTTPSession.prototype.on_tcp_data_send = function (chunk) { 152 | try { 153 | this.request_parser.execute(chunk, 0, chunk.length); 154 | } catch (request_err) { 155 | this.emit("http error", this, "send", request_err); 156 | } 157 | // if we are doing websocket, need to feed data to ws parser instead here 158 | }; 159 | 160 | HTTPSession.prototype.on_tcp_data_recv = function (chunk) { 161 | try { 162 | this.response_parser.execute(chunk, 0, chunk.length); 163 | } catch (request_err) { 164 | this.emit("http error", this, "recv", request_err); 165 | } 166 | // if we are doing websocket, need to feed data to ws parser instead here 167 | }; 168 | 169 | module.exports = HTTPSession; 170 | -------------------------------------------------------------------------------- /http_trace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // TODO: 4 | // support -n for inhibiting reverse DNS 5 | // support --group for grouping events from a full req/res cycle 6 | // 7 | 8 | var inspect = require("util").inspect; 9 | var HTTPSession = require("./http_session"); 10 | var node_http = require("http"); 11 | var pcap = require("pcap"); 12 | var pcap_session, ANSI, options = {}; 13 | var ANSI = require("./ansi"); 14 | 15 | function lpad(num, len) { 16 | var str = num.toString(); 17 | 18 | while (str.length < len) { 19 | str = "0" + str; 20 | } 21 | return str; 22 | } 23 | 24 | function format_timestamp(timems) { 25 | var date_obj = new Date(timems); 26 | 27 | return ANSI.blue(lpad(date_obj.getHours(), 2) + ":" + lpad(date_obj.getMinutes(), 2) + ":" + lpad(date_obj.getSeconds(), 2) + "." + 28 | lpad(date_obj.getMilliseconds(), 3)); 29 | } 30 | 31 | function format_hostname(hostname) { 32 | if (/[a-zA-Z]/.test(hostname)) { 33 | var parts = hostname.split(":"); 34 | return ANSI.magenta(parts[0].slice(0, 20) + ":" + parts[1]); 35 | } else { 36 | return ANSI.magenta(hostname); 37 | } 38 | } 39 | 40 | function format_line_start(session, send) { 41 | if (send) { 42 | return format_timestamp(session.current_cap_time) + " " + format_hostname(session.src_name) + " -> " + format_hostname(session.dst_name); 43 | } 44 | return format_timestamp(session.current_cap_time) + " " + format_hostname(session.dst_name) + " -> " + format_hostname(session.src_name); 45 | } 46 | 47 | function format_headers(headers) { 48 | var matched = [], keys = Object.keys(headers); 49 | 50 | if (options.headers) { 51 | matched = keys; 52 | } else if (Array.isArray(options["show-header"])) { 53 | matched = keys.filter(function (key) { 54 | return options["show-header"].some(function (filter) { 55 | return filter.test(key); 56 | }); 57 | }); 58 | } 59 | 60 | if (matched.length === 0) { 61 | return; 62 | } 63 | 64 | console.log(matched.map(function (val) { 65 | if (val === "Cookie") { 66 | var cookie_pairs = headers[val].split("; ").sort(); 67 | return (" " + ANSI.white(val) + ": " + ANSI.grey(cookie_pairs.map(function (pair) { 68 | var parts = pair.split("="); 69 | return parts[0] + ": " + parts[1]; 70 | }).join("\n "))); 71 | } else { 72 | return (" " + ANSI.white(val) + ": " + ANSI.grey(headers[val])); 73 | } 74 | }).join("\n")); 75 | } 76 | 77 | function format_size(size) { 78 | if (size < 1024 * 2) { 79 | return size + "B"; 80 | } else if (size < 1024 * 1024 * 2) { 81 | return (size / 1024).toFixed(2) + "KB"; 82 | } else { 83 | return (size / 1024 / 1024).toFixed(2) + "MB"; 84 | } 85 | } 86 | 87 | function format_json(str) { 88 | var obj; 89 | try { 90 | obj = JSON.parse(str); 91 | } catch (err) { 92 | return str; 93 | } 94 | var keys = Object.keys(obj).sort(); 95 | 96 | return keys.map(function (key) { 97 | if (typeof obj[key] === "object") { 98 | return " " + ANSI.white(key) + inspect(obj[key]); 99 | } else { 100 | return " " + ANSI.white(key) + ": " + ANSI.grey(obj[key]); 101 | } 102 | }).join("\n"); 103 | } 104 | 105 | function usage_die(message) { 106 | if (message) { 107 | console.error(""); 108 | console.error(message); 109 | } 110 | console.error(""); 111 | console.error("usage: http_trace [options]"); 112 | console.error(""); 113 | console.error("Capture options:"); 114 | console.error(" -i interface name for capture (def: first with an addr)"); 115 | console.error(" -f packet filter in pcap-filter(7) syntax (def: all TCP packets)"); 116 | console.error(" -b size in MB to buffer between libpcap and app (def: 10)"); 117 | console.error(""); 118 | console.error("HTTP filtering:"); 119 | console.error(" Filters are RegExps that are OR-ed together and may be specified more than once."); 120 | console.error(" Show filters are applied first, then ignore filters."); 121 | console.error(" --method show requests with this method"); 122 | console.error(" --method-ignore ignore requests with this method"); 123 | console.error(" --host show requests with this Host header"); 124 | console.error(" --host-ignore ignore requests with this Host header"); 125 | console.error(" --url show requests with this URL"); 126 | console.error(" --url-ignore ignore requests with this URL"); 127 | console.error(" --user-agent show requests with this UA header"); 128 | console.error(" --user-agent-ignore ignore requests with this UA header"); 129 | console.error(""); 130 | console.error("HTTP output:"); 131 | console.error(" --headers print all headers of request and response (def: off)"); 132 | console.error(" --show-header print only headers that match regex (def: off)"); 133 | console.error(" --bodies print request and response bodies, if any (def: off)"); 134 | // console.error(" --group group all output for req/res (def: progressive)"); 135 | console.error(" --tcp-verbose display TCP events (def: off)"); 136 | console.error(" --no-color disable ANSI colors (def: pretty colors on)"); 137 | console.error(""); 138 | console.error("Examples:"); 139 | console.error(" http_trace -f \"tcp port 80\""); 140 | console.error(" listen for TCP port 80 on the default device"); 141 | console.error(" http_trace -i eth1 --method POST"); 142 | console.error(" listen on eth1 for all traffic that has an HTTP POST"); 143 | console.error(" http_trace --host ranney --headers"); 144 | console.error(" matches ranney in Host header and prints req/res headers"); 145 | process.exit(1); 146 | } 147 | 148 | function parse_options() { 149 | var argv_slice = process.argv.slice(2), 150 | optnum = 0, opt, optname, 151 | state = "match optname", matches, 152 | valid_options; 153 | 154 | valid_options = { 155 | "i": { multiple: false, has_value: true }, 156 | "f": { multiple: false, has_value: true }, 157 | "b": { multiple: false, has_value: true }, 158 | "method": { multiple: true, has_value: true, regex: true }, 159 | "method-ignore": { multiple: true, has_value: true, regex: true }, 160 | "host": { multiple: true, has_value: true, regex: true }, 161 | "host-ignore": { multiple: true, has_value: true, regex: true }, 162 | "url": { multiple: true, has_value: true, regex: true }, 163 | "url-ignore": { multiple: true, has_value: true, regex: true }, 164 | "user-agent": { multiple: true, has_value: true, regex: true }, 165 | "user-agent-ignore": { multiple: true, has_value: true, regex: true }, 166 | "headers": { multiple: false, has_value: false }, 167 | "show-header": { multiple: true, has_value: true, regex: true }, 168 | "bodies": { multiple: false, has_value: false }, 169 | // "group": { multiple: false, has_value: false }, 170 | "tcp-verbose": { multiple: false, has_value: false }, 171 | "no-color": { multiple: false, has_value: false }, 172 | "help": { multiple: false, has_value: false } 173 | }; 174 | 175 | function set_option(name, value) { 176 | if (valid_options[name].multiple) { 177 | if (valid_options[name].regex) { 178 | value = new RegExp(value); 179 | } 180 | if (options[name] === undefined) { 181 | options[name] = [value]; 182 | } else { 183 | options[name].push(value); 184 | } 185 | } else { 186 | if (options[name] === undefined) { 187 | options[name] = value; 188 | } else { 189 | usage_die("Option " + name + " may only be specified once."); 190 | } 191 | } 192 | } 193 | 194 | while (optnum < argv_slice.length) { 195 | opt = argv_slice[optnum]; 196 | 197 | if (state === "match optname") { 198 | matches = opt.match(/^[\-]{1,2}([^\-].*)/); 199 | if (matches !== null) { 200 | optname = matches[1]; 201 | if (valid_options[optname]) { // if this is a known option 202 | if (valid_options[optname].has_value) { 203 | state = "match optval"; 204 | } else { 205 | set_option(optname, true); 206 | } 207 | } else { 208 | usage_die("Invalid option name: " + optname); 209 | } 210 | } else { 211 | usage_die("bad option name: " + opt); 212 | } 213 | } else if (state === "match optval") { 214 | if (opt[0] !== "-") { 215 | set_option(optname, opt); 216 | state = "match optname"; 217 | } else { 218 | usage_die("bad option value: " + opt); 219 | } 220 | } else { 221 | throw new Error("Unknown state " + state + " in options parser"); 222 | } 223 | 224 | optnum += 1; 225 | } 226 | if (state === "match optval") { 227 | usage_die("Missing option value for " + optname); 228 | } 229 | } 230 | 231 | // TODO - make this way faster 232 | function filter_match(http) { 233 | var show_filters = [], hide_filters = [], show = true; 234 | 235 | options.method && show_filters.push([http.request.method, options.method]); 236 | options.url && show_filters.push([http.request.url, options.url]); 237 | options.host && show_filters.push([http.request.headers.Host, options.host]); 238 | options["user-agent"] && show_filters.push([http.request.headers["User-Agent"], options["user-agent"]]); 239 | 240 | options["method-ignore"] && hide_filters.push([http.request.method, options["method-ignore"]]); 241 | options["url-ignore"] && hide_filters.push([http.request.url, options["url-ignore"]]); 242 | options["host-ignore"] && hide_filters.push([http.request.headers.Host, options["host-ignore"]]); 243 | options["user-agent-ignore"] && hide_filters.push([http.request.headers["User-Agent"], options["user-agent-ignore"]]); 244 | 245 | if (show_filters.length > 0) { 246 | show = show_filters.some(function (filter_pair) { 247 | if (Array.isArray(filter_pair[1])) { 248 | return filter_pair[1].some(function (filter) { 249 | return filter.test(filter_pair[0]); 250 | }); 251 | } 252 | return false; 253 | }); 254 | } // if no show filters, then everything "matches" 255 | 256 | if (hide_filters.length > 0) { 257 | show = !hide_filters.some(function (filter_pair) { 258 | if (typeof filter_pair[1] === "object") { 259 | return filter_pair[1].some(function (filter) { 260 | return filter.test(filter_pair[0]); 261 | }); 262 | } 263 | return false; 264 | }); 265 | } 266 | 267 | return show; 268 | } 269 | 270 | function privs_check() { 271 | if (process.getuid() !== 0) { 272 | console.log(ANSI.bold(ANSI.red("Warning: not running with root privs, which are usually required for raw packet capture."))); 273 | console.log(ANSI.red("Trying to open anyway...")); 274 | } 275 | } 276 | 277 | function start_capture_session() { 278 | if (! options.f) { 279 | // default filter is all IPv4 TCP, which is all we know how to decode right now anyway 280 | options.f = "ip proto \\tcp"; 281 | } 282 | pcap_session = pcap.createSession(options.i, options.f, (options.b * 1024 * 1024)); 283 | console.log("Listening on " + pcap_session.device_name); 284 | } 285 | 286 | function start_drop_watcher() { 287 | // Check for pcap dropped packets on an interval 288 | var first_drop = setInterval(function () { 289 | var stats = pcap_session.stats(); 290 | if (stats.ps_drop > 0) { 291 | console.log(ANSI.bold("pcap dropped packets, need larger buffer or less work to do: " + JSON.stringify(stats))); 292 | clearInterval(first_drop); 293 | setInterval(function () { 294 | console.log(ANSI.bold("pcap dropped packets: " + JSON.stringify(stats))); 295 | }, 5000); 296 | } 297 | }, 1000); 298 | } 299 | 300 | 301 | function binary_body_check(headers) { 302 | var ct = headers["Content-Type"]; 303 | if (ct && (/^(image|video)/.test(ct))) { 304 | return true; 305 | } else { 306 | return false; 307 | } 308 | } 309 | 310 | function setup_listeners() { 311 | var tcp_tracker = new pcap.TCPTracker(); 312 | 313 | pcap_session.on("packet", function (raw_packet) { 314 | var packet = pcap.decode.packet(raw_packet); 315 | tcp_tracker.track_packet(packet); 316 | }); 317 | 318 | // tracker emits sessions, and sessions emit data 319 | tcp_tracker.on("session", function (tcp_session) { 320 | on_tcp_session(tcp_session); 321 | }); 322 | 323 | if (options["tcp-verbose"]) { 324 | tcp_tracker.on("session", function (session) { 325 | if (session.missed_syn) { 326 | console.log(format_line_start(session, true) + " TCP already in progress "); 327 | } else { 328 | console.log(format_line_start(session, true) + " TCP start "); 329 | } 330 | }); 331 | tcp_tracker.on("retransmit", function (session, direction, seqno) { 332 | var line_start; 333 | if (direction === "send") { 334 | line_start = format_line_start(session, true); 335 | } else { 336 | line_start = format_line_start(session, false); 337 | } 338 | console.log(line_start + "TCP retransmit at " + seqno); 339 | }); 340 | tcp_tracker.on("end", function (session) { 341 | console.log(format_line_start(session, true) + " TCP end "); 342 | }); 343 | tcp_tracker.on("reset", function (session) { 344 | // eventually this event will have a direction. Right now, it's only from dst. 345 | console.log(format_line_start(session, false) + " TCP reset "); 346 | }); 347 | tcp_tracker.on("syn retry", function (session) { 348 | console.log(format_line_start(session, true) + " SYN retry"); 349 | }); 350 | } 351 | } 352 | 353 | function on_tcp_session(tcp_session) { 354 | var http_session = new HTTPSession(tcp_session); 355 | 356 | http_session.on("http error", function (session, direction, error) { 357 | console.log("http error ", error.stack); 358 | var line_start; 359 | if (direction === "send") { 360 | line_start = format_line_start(tcp_session, true); 361 | } else { 362 | line_start = format_line_start(tcp_session, false); 363 | } 364 | 365 | console.log(line_start + " HTTP parser error: " + error); 366 | }); 367 | 368 | http_session.on("http request", function (session) { 369 | if (! filter_match(session)) { 370 | return; 371 | } 372 | 373 | var req = session.request; 374 | console.log(format_line_start(tcp_session, true) + " #" + session.request_count + " HTTP " + req.http_version + " request: " + 375 | ANSI.yellow(ANSI.bold(req.method) + " " + req.url)); 376 | 377 | format_headers(req.headers); 378 | req.binary_body = binary_body_check(req.headers); 379 | }); 380 | 381 | http_session.on("http request body", function (session, data) { 382 | if (! filter_match(session)) { 383 | return; 384 | } 385 | 386 | console.log(format_line_start(tcp_session, true) + 387 | " #" + session.request_count + " HTTP " + session.request.http_version + " request body: " + 388 | format_size(data.length)); 389 | 390 | // TODO - need real binary_body check, more than just content-type 391 | if (options.bodies && !session.request.binary_body) { 392 | if (session.request.headers["content-type"].match(/json/)) { 393 | console.log(ANSI.green(data.toString("utf8"))); 394 | } else { 395 | console.log(format_json(data.toString("utf8"))); 396 | } 397 | } 398 | }); 399 | 400 | http_session.on("http request complete", function (session) { 401 | if (! filter_match(session)) { 402 | return; 403 | } 404 | 405 | if (session.request.body_len > 0 || session.request.method !== "GET") { 406 | console.log(format_line_start(tcp_session, true) + 407 | " #" + session.request_count + " HTTP " + session.request.http_version + " request complete " + 408 | format_size(session.request.body_len)); 409 | } 410 | }); 411 | 412 | http_session.on("http response", function (session) { 413 | if (! filter_match(session)) { 414 | return; 415 | } 416 | 417 | var res = session.response; 418 | console.log(format_line_start(tcp_session, false) + " #" + session.request_count + " HTTP " + res.http_version + " response: " + 419 | ANSI.yellow(res.status_code + " " + node_http.STATUS_CODES[res.status_code])); 420 | 421 | format_headers(res.headers); 422 | res.binary_body = binary_body_check(res.headers); 423 | }); 424 | 425 | http_session.on("http response body", function (session, data) { 426 | if (! filter_match(session)) { 427 | return; 428 | } 429 | 430 | console.log(format_line_start(tcp_session, false) + 431 | " #" + session.request_count + " HTTP " + session.response.http_version + " response body: " + 432 | format_size(data.length)); 433 | 434 | if (options.bodies && !session.response.binary_body) { 435 | // TODO - this is not at all what you want for gzipped or binary data 436 | console.log(ANSI.green(data.toString("utf8"))); 437 | } 438 | }); 439 | 440 | http_session.on("http response complete", function (session) { 441 | if (! filter_match(session)) { 442 | return; 443 | } 444 | 445 | console.log(format_line_start(tcp_session, false) + 446 | " #" + session.request_count + " HTTP " + session.response.http_version + " response complete " + 447 | format_size(session.response.body_len) + " " + (Date.now() - session.request.start) + "ms"); 448 | }); 449 | } 450 | 451 | // Make it all go 452 | parse_options(); 453 | if (options["no-color"]) { 454 | ANSI.no_color = true; 455 | } 456 | if (options.help) { 457 | usage_die(); 458 | } 459 | privs_check(); 460 | start_capture_session(); 461 | start_drop_watcher(); 462 | setup_listeners(); 463 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name" : "http_trace", 2 | "version" : "0.5.0", 3 | "description" : "Live HTTP packet capture and protocol decoding", 4 | "author": "Matt Ranney ", 5 | "main": "./http_trace", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/mranney/http_trace.git" 9 | }, 10 | "dependencies": { 11 | "pcap": ">=2.0.0" 12 | }, 13 | "bin": { 14 | "http_trace": "./http_trace" 15 | } 16 | } 17 | --------------------------------------------------------------------------------