Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

executable file 441 lines (386 sloc) 16.358 kb
#!/usr/bin/env node
/*global process require exports setInterval console */
var sys = require("sys"),
node_http = require('http'),
node_url = require('url'),
pcap = require("pcap"), pcap_session,
ANSI,
options = {};
ANSI = (function () {
// http://en.wikipedia.org/wiki/ANSI_escape_code
var formats = {
bold: [1, 22], // bright
light: [2, 22], // faint
italic: [3, 23],
underline: [4, 24], // underline single
blink_slow: [5, 25],
blink_fast: [6, 25],
inverse: [7, 27],
conceal: [8, 28],
strikethrough: [9, 29], // crossed-out
// 10 - 20 are font control
underline_double: [21, 24],
black: [30, 39],
red: [31, 39],
green: [32, 39],
yellow: [33, 39],
blue: [34, 39],
magenta: [35, 39],
cyan: [36, 39],
white: [37, 39],
grey: [90, 39]
},
CSI = String.fromCharCode(27) + '[';
return function (str, format) {
if (options["no-color"]) {
return str;
}
return CSI + formats[format][0] + 'm' + str + CSI + formats[format][1] + 'm';
};
}());
function lpad(num, len) {
var str = num.toString();
while (str.length < len) {
str = "0" + str;
}
return str;
}
function format_timestamp(timems) {
var date_obj = new Date(timems);
return ANSI(lpad(date_obj.getHours(), 2) + ":" + lpad(date_obj.getMinutes(), 2) + ":" + lpad(date_obj.getSeconds(), 2) + "." +
lpad(date_obj.getMilliseconds(), 3), "blue");
}
function format_hostname(hostname) {
if (/[a-zA-Z]/.test(hostname)) {
var parts = hostname.split(":");
return ANSI(parts[0].split('.')[0] + ":" + parts[1], "magenta");
} else {
return ANSI(hostname, "magenta");
}
}
function format_line_start(ts, src, dst) {
return format_timestamp(ts) + " " + format_hostname(src) + " -> " + format_hostname(dst);
}
function format_headers(headers) {
return Object.keys(headers).map(function (val) {
if (val === "Cookie") {
var cookie_pairs = headers[val].split("; ").sort();
return (" " + ANSI(val, "white") + ": " + ANSI(cookie_pairs.map(function (pair) {
var parts = pair.split('=');
return parts[0] + ": " + parts[1];
}).join("\n "), "grey"));
} else {
return (" " + ANSI(val, "white") + ": " + ANSI(headers[val], "grey"));
}
}).join("\n");
}
function format_size(size) {
if (size < 1024 * 2) {
return size + "B";
} else if (size < 1024 * 1024 * 2) {
return (size / 1024).toFixed(2) + "KB";
} else {
return (size / 1024 / 1024).toFixed(2) + "MB";
}
}
function format_obj(obj) {
var keys = Object.keys(obj).sort();
return keys.map(function (key) {
if (typeof obj[key] === 'object') {
return " " + ANSI(key, "white") + sys.inspect(obj[key]);
} else {
return " " + ANSI(key, "white") + ": " + ANSI(obj[key], "grey");
}
}).join('\n');
}
function usage_die(message) {
if (message) {
sys.error("");
sys.error(message);
}
sys.error("");
sys.error("usage: http_trace [options]");
sys.error("");
sys.error("Capture options:");
sys.error(" -i <interface> interface name for capture (def: first with an addr)");
sys.error(" -f <pcap_filter> packet filter in pcap-filter(7) syntax (def: all TCP packets)");
sys.error(" -b <buffer> size in MB to buffer between libpcap and app (def: 10)");
sys.error("");
sys.error("HTTP filtering:");
sys.error(" Filters are OR-ed together and may be specified more than once.");
sys.error(" --method <regex> filter on method");
sys.error(" --host <regex> filter on Host request header");
sys.error(" --url <regex> filter on URL");
sys.error(" --user-agent <regex> filter on User-Agent request header");
sys.error("");
sys.error("HTTP output:");
sys.error(" --headers print headers of request and response (def: off)");
sys.error(" --bodies print request and response bodies, if any (def: off)");
// sys.error(" --group group all output for req/res (def: progressive)");
sys.error(" --tcp-verbose display TCP events (def: off)");
sys.error(" --no-color disable ANSI colors (def: pretty colors on)");
sys.error("");
sys.error("Examples:");
sys.error(' http_trace -f "tcp port 80"');
sys.error(' listen for TCP port 80 on the default device');
sys.error(' http_trace -i eth1 --method POST');
sys.error(' listen on eth1 for all traffic that has an HTTP POST');
sys.error(' http_trace --host ranney --headers');
sys.error(' matches ranney in Host header and prints req/res headers');
process.exit(1);
}
// Someone else's options parsing library would parse options better, but I fear extra dependencies
function parse_options() {
var argv_slice = process.argv.slice(2),
optnum = 0, opt, optname, optval,
state = "match optname", matches,
valid_options;
valid_options = {
"i": { multiple: false, has_value: true },
"f": { multiple: false, has_value: true },
"b": { multiple: false, has_value: true },
"method": { multiple: true, has_value: true, regex: true },
"host": { multiple: true, has_value: true, regex: true },
"url": { multiple: true, has_value: true, regex: true },
"user-agent": { multiple: true, has_value: true, regex: true },
"headers": { multiple: false, has_value: false },
"bodies": { multiple: false, has_value: false },
// "group": { multiple: false, has_value: false },
"tcp-verbose": { multiple: false, has_value: false },
"no-color": { multiple: false, has_value: false },
"help": { multiple: false, has_value: false }
};
function set_option(name, value) {
if (valid_options[name].multiple) {
if (valid_options[name].regex) {
value = new RegExp(value);
}
if (options[name] === undefined) {
options[name] = [value];
} else {
options[name].push(value);
}
} else {
if (options[name] === undefined) {
options[name] = value;
} else {
usage_die("Option " + name + " may only be specified once.");
}
}
}
while (optnum < argv_slice.length) {
opt = argv_slice[optnum];
if (state === "match optname") {
matches = opt.match(/^[\-]{1,2}([^\-].*)/);
if (matches !== null) {
optname = matches[1];
if (valid_options[optname]) { // if this is a known option
if (valid_options[optname].has_value) {
state = "match optval";
} else {
set_option(optname, true);
}
} else {
usage_die("Invalid option name: " + optname);
}
} else {
usage_die("bad option name: " + opt);
}
} else if (state === "match optval") {
if (opt[0] !== '-') {
set_option(optname, opt);
state = "match optname";
} else {
usage_die("bad option value: " + opt);
}
} else {
throw new Error("Unknown state " + state + " in options parser");
}
optnum += 1;
}
if (state === "match optval") {
usage_die("Missing option value for " + optname);
}
}
function filter_match(http) {
var filters = [
[http.request.method, options.method],
[http.request.url, options.url],
[http.request.headers.Host, options.host],
[http.request.headers["User-Agent"], options["user-agent"]],
], filter_pair_num, filter_index;
if (options.method || options.host || options.url || options["user-agent"]) {
return filters.some(function (filter_pair) {
if (typeof filter_pair[1] === 'object') {
return filter_pair[1].some(function (filter) {
return filter.test(filter_pair[0]);
});
}
return false;
});
} else {
return true; // if no filters, then everything "matches"
}
}
function start_capture_session() {
if (! options.f) {
// default filter is all IPv4 TCP, which is all we know how to decode right now anyway
options.f = "ip proto \\tcp";
}
pcap_session = pcap.createSession(options.i, options.f, (options.b * 1024 * 1024));
console.log("Listening on " + pcap_session.device_name);
}
function start_drop_watcher() {
// Check for pcap dropped packets on an interval
setInterval(function () {
var stats = pcap_session.stats();
if (stats.ps_drop > 0) {
console.log(ANSI("pcap dropped packets, need larger buffer or less work to do: " + sys.inspect(stats), "bold"));
}
}, 2000);
}
function setup_listeners() {
var tcp_tracker = new pcap.TCP_tracker();
// listen for packets, decode them, and feed TCP to the tracker
pcap_session.on('packet', function (raw_packet) {
var packet = pcap.decode.packet(raw_packet);
tcp_tracker.track_packet(packet);
});
if (options["tcp-verbose"]) {
tcp_tracker.on("start", function (session) {
console.log(format_line_start(session.current_cap_time, session.src_name, session.dst_name) +
" TCP start ");
});
tcp_tracker.on("retransmit", function (session, direction, seqno) {
var line_start;
if (direction === "send") {
line_start = format_line_start(session.current_cap_time, session.src_name, session.dst_name);
} else {
line_start = format_line_start(session.current_cap_time, session.dst_name, session.src_name);
}
console.log(line_start + "TCP retransmit at " + seqno);
});
tcp_tracker.on("end", function (session) {
console.log(format_line_start(session.current_cap_time, session.src_name, session.dst_name) +
" TCP end ");
});
tcp_tracker.on("reset", function (session) {
// eventually this event will have a direction. Right now, it's only from dst.
console.log(format_line_start(session.current_cap_time, session.dst_name, session.src_name) +
" TCP reset ");
});
tcp_tracker.on("syn retry", function (session) {
console.log(format_line_start(session.current_cap_time, session.src_name, session.dst_name) +
" SYN retry");
});
}
tcp_tracker.on('http error', function (session, direction, error) {
var line_start;
if (direction === "send") {
line_start = format_line_start(session.current_cap_time, session.src_name, session.dst_name);
} else {
line_start = format_line_start(session.current_cap_time, session.dst_name, session.src_name);
}
console.log(line_start + " HTTP parser error: " + error);
// TODO - probably need to force this request/response to be over at this point
});
tcp_tracker.on('http request', function (session, http) {
if (session.http_request_count) {
session.http_request_count += 1;
} else {
session.http_request_count = 1;
}
if (! filter_match(http)) {
return;
}
console.log(format_line_start(session.current_cap_time, session.src_name, session.dst_name) +
" #" + session.http_request_count + " HTTP " + http.request.http_version + " request: " +
ANSI(ANSI(http.request.method, "bold") + " " + http.request.url, "yellow"));
if (options.headers) {
console.log(format_headers(http.request.headers));
}
});
tcp_tracker.on('http request body', function (session, http, data) {
if (! filter_match(http)) {
return;
}
console.log(format_line_start(session.current_cap_time, session.src_name, session.dst_name) +
" #" + session.http_request_count + " HTTP " + http.request.http_version + " request body: " +
format_size(data.length));
if (options.bodies) {
// TODO - this is not at all what you want for gzipped or binary data
console.log(ANSI(data.toString("utf8"), "green"));
}
});
tcp_tracker.on('http request complete', function (session, http) {
if (! filter_match(http)) {
return;
}
if (http.request.body_len > 0 || http.request.method !== "GET") {
console.log(format_line_start(session.current_cap_time, session.src_name, session.dst_name) +
" #" + session.http_request_count + " HTTP " + http.request.http_version + " request complete " +
format_size(http.request.body_len));
}
});
tcp_tracker.on('http response', function (session, http) {
if (! filter_match(http)) {
return;
}
console.log(format_line_start(session.current_cap_time, session.dst_name, session.src_name) +
" #" + session.http_request_count + " HTTP " + http.response.http_version + " response: " +
ANSI(http.response.status_code + " " + node_http.STATUS_CODES[http.response.status_code], "yellow"));
if (options.headers) {
console.log(format_headers(http.response.headers));
}
});
tcp_tracker.on('http response body', function (session, http, data) {
if (! filter_match(http)) {
return;
}
console.log(format_line_start(session.current_cap_time, session.dst_name, session.src_name) +
" #" + session.http_request_count + " HTTP " + http.response.http_version + " response body: " +
format_size(data.length));
if (options.bodies) {
// TODO - this is not at all what you want for gzipped or binary data
console.log(ANSI(data.toString("utf8"), "green"));
}
});
tcp_tracker.on('http response complete', function (session, http) {
if (! filter_match(http)) {
return;
}
console.log(format_line_start(session.current_cap_time, session.dst_name, session.src_name) +
" #" + session.http_request_count + " HTTP " + http.response.http_version + " response complete " +
format_size(http.response.body_len));
});
tcp_tracker.on('websocket upgrade', function (session, http) {
// TODO - figure out how filters apply to WS
console.log(format_line_start(session.current_cap_time, session.dst_name, session.src_name) +
" WebSocket upgrade " + ANSI(http.response.status_code + " " + node_http.STATUS_CODES[http.response.status_code], "yellow"));
console.log(format_headers(http.response.headers));
});
tcp_tracker.on('websocket message', function (session, dir, message) {
// TODO - figure out how filters apply to WS
var line_start, obj;
if (dir === "send") {
line_start = format_line_start(session.current_cap_time, session.src_name, session.dst_name);
} else {
line_start = format_line_start(session.current_cap_time, session.dst_name, session.src_name);
}
console.log(line_start + " WebSocket message " + format_size(message.length));
try {
obj = JSON.parse(message);
console.log("JSON: " + ANSI(sys.inspect(obj), "green"));
} catch (err) {
console.log(ANSI(message, "green"));
}
});
}
// Make it all go
parse_options();
if (options.help) {
usage_die();
}
start_capture_session();
start_drop_watcher();
setup_listeners();
Jump to Line
Something went wrong with that request. Please try again.