Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Tree: 0e7ce8f4de
Fetching contributors…

Cannot retrieve contributors at this time

executable file 512 lines (447 sloc) 19.43 kB
#!/usr/bin/env node
/*global process require exports setInterval console */
// TODO:
// support -n for inhibiting reverse DNS
// support --group for grouping events from a full req/res cycle
//
var util;
try {
util = require("util");
} catch (err) {
util = require("sys");
}
var 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) {
var matched = [], keys = Object.keys(headers);
if (options.headers) {
matched = keys;
} else if (Array.isArray(options["show-header"])) {
matched = keys.filter(function (key) {
return options["show-header"].some(function (filter) {
return filter.test(key);
});
});
}
if (matched.length === 0) {
return;
}
console.log(matched.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") + util.inspect(obj[key]);
} else {
return " " + ANSI(key, "white") + ": " + ANSI(obj[key], "grey");
}
}).join('\n');
}
function usage_die(message) {
if (message) {
util.error("");
util.error(message);
}
util.error("");
util.error("usage: http_trace [options]");
util.error("");
util.error("Capture options:");
util.error(" -i <interface> interface name for capture (def: first with an addr)");
util.error(" -f <pcap_filter> packet filter in pcap-filter(7) syntax (def: all TCP packets)");
util.error(" -b <buffer> size in MB to buffer between libpcap and app (def: 10)");
util.error("");
util.error("HTTP filtering:");
util.error(" Filters are RegExps that are OR-ed together and may be specified more than once.");
util.error(" Show filters are applied first, then ignore filters.");
util.error(" --method <regex> show requests with this method");
util.error(" --method-ignore <regex> ignore requests with this method");
util.error(" --host <regex> show requests with this Host header");
util.error(" --host-ignore <regex> ignore requests with this Host header");
util.error(" --url <regex> show requests with this URL");
util.error(" --url-ignore <regex> ignore requests with this URL");
util.error(" --user-agent <regex> show requests with this UA header");
util.error(" --user-agent-ignore <regex> ignore requests with this UA header");
util.error("");
util.error("HTTP output:");
util.error(" --headers print all headers of request and response (def: off)");
util.error(" --show-header print only headers that match regex (def: off)");
util.error(" --bodies print request and response bodies, if any (def: off)");
// util.error(" --group group all output for req/res (def: progressive)");
util.error(" --tcp-verbose display TCP events (def: off)");
util.error(" --no-color disable ANSI colors (def: pretty colors on)");
util.error("");
util.error("Examples:");
util.error(' http_trace -f "tcp port 80"');
util.error(' listen for TCP port 80 on the default device');
util.error(' http_trace -i eth1 --method POST');
util.error(' listen on eth1 for all traffic that has an HTTP POST');
util.error(' http_trace --host ranney --headers');
util.error(' matches ranney in Host header and prints req/res headers');
process.exit(1);
}
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 },
"method-ignore": { multiple: true, has_value: true, regex: true },
"host": { multiple: true, has_value: true, regex: true },
"host-ignore": { multiple: true, has_value: true, regex: true },
"url": { multiple: true, has_value: true, regex: true },
"url-ignore": { multiple: true, has_value: true, regex: true },
"user-agent": { multiple: true, has_value: true, regex: true },
"user-agent-ignore": { multiple: true, has_value: true, regex: true },
"headers": { multiple: false, has_value: false },
"show-header": { multiple: true, has_value: true, regex: true },
"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 show_filters = [], hide_filters = [], show = true;
options.method && show_filters.push([http.request.method, options.method]);
options.url && show_filters.push([http.request.url, options.url]);
options.host && show_filters.push([http.request.headers.Host, options.host]);
options["user-agent"] && show_filters.push([http.request.headers["User-Agent"], options["user-agent"]]);
options["method-ignore"] && hide_filters.push([http.request.method, options["method-ignore"]]);
options["url-ignore"] && hide_filters.push([http.request.url, options["url-ignore"]]);
options["host-ignore"] && hide_filters.push([http.request.headers.Host, options["host-ignore"]]);
options["user-agent-ignore"] && hide_filters.push([http.request.headers["User-Agent"], options["user-agent-ignore"]]);
if (show_filters.length > 0) {
show = show_filters.some(function (filter_pair) {
if (Array.isArray(filter_pair[1])) {
return filter_pair[1].some(function (filter) {
return filter.test(filter_pair[0]);
});
}
return false;
});
} // if no show filters, then everything "matches"
if (hide_filters.length > 0) {
show = !hide_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;
});
}
return show;
}
function privs_check() {
if (process.getuid() !== 0) {
console.log(ANSI(ANSI("Warning: not running with root privs, which are usually required for raw packet capture.", "red"), "bold"));
console.log(ANSI("Trying to open anyway...", "red"));
}
}
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
var first_drop = 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: " + util.inspect(stats), "bold"));
clearInterval(first_drop);
setInterval(function () {
console.log(ANSI("pcap dropped packets: " + util.inspect(stats), "bold"));
}, 5000);
}
}, 1000);
}
function binary_body_check(headers) {
var ct = headers["Content-Type"];
if (ct && (/^(image|video)/.test(ct))) {
return true;
} else {
return false;
}
}
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"));
format_headers(http.request.headers);
http.request.binary_body = binary_body_check(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 && !http.request.binary_body) {
// TODO - this is not at all what you want for gzipped 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"));
format_headers(http.response.headers);
http.response.binary_body = binary_body_check(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 && !http.response.binary_body) {
// 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"));
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(util.inspect(obj), "green"));
} catch (err) {
console.log(ANSI(message, "green"));
}
});
}
// Make it all go
parse_options();
if (options.help) {
usage_die();
}
privs_check();
start_capture_session();
start_drop_watcher();
setup_listeners();
Jump to Line
Something went wrong with that request. Please try again.