Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Tree: 48a7c112ab
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.