Permalink
Switch branches/tags
Nothing to show
Find file
f655d9b Jan 31, 2015
executable file 463 lines (405 sloc) 16.9 KB
#!/usr/bin/env node
// TODO:
// support -n for inhibiting reverse DNS
// support --group for grouping events from a full req/res cycle
//
var inspect = require("util").inspect;
var HTTPSession = require("./http_session");
var node_http = require("http");
var pcap = require("pcap");
var pcap_session, ANSI, options = {};
var ANSI = require("./ansi");
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.blue(lpad(date_obj.getHours(), 2) + ":" + lpad(date_obj.getMinutes(), 2) + ":" + lpad(date_obj.getSeconds(), 2) + "." +
lpad(date_obj.getMilliseconds(), 3));
}
function format_hostname(hostname) {
if (/[a-zA-Z]/.test(hostname)) {
var parts = hostname.split(":");
return ANSI.magenta(parts[0].slice(0, 20) + ":" + parts[1]);
} else {
return ANSI.magenta(hostname);
}
}
function format_line_start(session, send) {
if (send) {
return format_timestamp(session.current_cap_time) + " " + format_hostname(session.src_name) + " -> " + format_hostname(session.dst_name);
}
return format_timestamp(session.current_cap_time) + " " + format_hostname(session.dst_name) + " -> " + format_hostname(session.src_name);
}
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.white(val) + ": " + ANSI.grey(cookie_pairs.map(function (pair) {
var parts = pair.split("=");
return parts[0] + ": " + parts[1];
}).join("\n ")));
} else {
return (" " + ANSI.white(val) + ": " + ANSI.grey(headers[val]));
}
}).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_json(str) {
var obj;
try {
obj = JSON.parse(str);
} catch (err) {
return str;
}
var keys = Object.keys(obj).sort();
return keys.map(function (key) {
if (typeof obj[key] === "object") {
return " " + ANSI.white(key) + inspect(obj[key]);
} else {
return " " + ANSI.white(key) + ": " + ANSI.grey(obj[key]);
}
}).join("\n");
}
function usage_die(message) {
if (message) {
console.error("");
console.error(message);
}
console.error("");
console.error("usage: http_trace [options]");
console.error("");
console.error("Capture options:");
console.error(" -i <interface> interface name for capture (def: first with an addr)");
console.error(" -f <pcap_filter> packet filter in pcap-filter(7) syntax (def: all TCP packets)");
console.error(" -b <buffer> size in MB to buffer between libpcap and app (def: 10)");
console.error("");
console.error("HTTP filtering:");
console.error(" Filters are RegExps that are OR-ed together and may be specified more than once.");
console.error(" Show filters are applied first, then ignore filters.");
console.error(" --method <regex> show requests with this method");
console.error(" --method-ignore <regex> ignore requests with this method");
console.error(" --host <regex> show requests with this Host header");
console.error(" --host-ignore <regex> ignore requests with this Host header");
console.error(" --url <regex> show requests with this URL");
console.error(" --url-ignore <regex> ignore requests with this URL");
console.error(" --user-agent <regex> show requests with this UA header");
console.error(" --user-agent-ignore <regex> ignore requests with this UA header");
console.error("");
console.error("HTTP output:");
console.error(" --headers print all headers of request and response (def: off)");
console.error(" --show-header print only headers that match regex (def: off)");
console.error(" --bodies print request and response bodies, if any (def: off)");
// console.error(" --group group all output for req/res (def: progressive)");
console.error(" --tcp-verbose display TCP events (def: off)");
console.error(" --no-color disable ANSI colors (def: pretty colors on)");
console.error("");
console.error("Examples:");
console.error(" http_trace -f \"tcp port 80\"");
console.error(" listen for TCP port 80 on the default device");
console.error(" http_trace -i eth1 --method POST");
console.error(" listen on eth1 for all traffic that has an HTTP POST");
console.error(" http_trace --host ranney --headers");
console.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,
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);
}
}
// TODO - make this way faster
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.bold(ANSI.red("Warning: not running with root privs, which are usually required for raw packet capture.")));
console.log(ANSI.red("Trying to open anyway..."));
}
}
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.bold("pcap dropped packets, need larger buffer or less work to do: " + JSON.stringify(stats)));
clearInterval(first_drop);
setInterval(function () {
console.log(ANSI.bold("pcap dropped packets: " + JSON.stringify(stats)));
}, 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.TCPTracker();
pcap_session.on("packet", function (raw_packet) {
var packet = pcap.decode.packet(raw_packet);
tcp_tracker.track_packet(packet);
});
// tracker emits sessions, and sessions emit data
tcp_tracker.on("session", function (tcp_session) {
on_tcp_session(tcp_session);
});
if (options["tcp-verbose"]) {
tcp_tracker.on("session", function (session) {
if (session.missed_syn) {
console.log(format_line_start(session, true) + " TCP already in progress ");
} else {
console.log(format_line_start(session, true) + " TCP start ");
}
});
tcp_tracker.on("retransmit", function (session, direction, seqno) {
var line_start;
if (direction === "send") {
line_start = format_line_start(session, true);
} else {
line_start = format_line_start(session, false);
}
console.log(line_start + "TCP retransmit at " + seqno);
});
tcp_tracker.on("end", function (session) {
console.log(format_line_start(session, true) + " 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, false) + " TCP reset ");
});
tcp_tracker.on("syn retry", function (session) {
console.log(format_line_start(session, true) + " SYN retry");
});
}
}
function on_tcp_session(tcp_session) {
var http_session = new HTTPSession(tcp_session);
http_session.on("http error", function (session, direction, error) {
console.log("http error ", error.stack);
var line_start;
if (direction === "send") {
line_start = format_line_start(tcp_session, true);
} else {
line_start = format_line_start(tcp_session, false);
}
console.log(line_start + " HTTP parser error: " + error);
});
http_session.on("http request", function (session) {
if (! filter_match(session)) {
return;
}
var req = session.request;
console.log(format_line_start(tcp_session, true) + " #" + session.request_count + " HTTP " + req.http_version + " request: " +
ANSI.yellow(ANSI.bold(req.method) + " " + req.url));
format_headers(req.headers);
req.binary_body = binary_body_check(req.headers);
});
http_session.on("http request body", function (session, data) {
if (! filter_match(session)) {
return;
}
console.log(format_line_start(tcp_session, true) +
" #" + session.request_count + " HTTP " + session.request.http_version + " request body: " +
format_size(data.length));
// TODO - need real binary_body check, more than just content-type
if (options.bodies && !session.request.binary_body) {
if (session.request.headers["content-type"].match(/json/)) {
console.log(ANSI.green(data.toString("utf8")));
} else {
console.log(format_json(data.toString("utf8")));
}
}
});
http_session.on("http request complete", function (session) {
if (! filter_match(session)) {
return;
}
if (session.request.body_len > 0 || session.request.method !== "GET") {
console.log(format_line_start(tcp_session, true) +
" #" + session.request_count + " HTTP " + session.request.http_version + " request complete " +
format_size(session.request.body_len));
}
});
http_session.on("http response", function (session) {
if (! filter_match(session)) {
return;
}
var res = session.response;
console.log(format_line_start(tcp_session, false) + " #" + session.request_count + " HTTP " + res.http_version + " response: " +
ANSI.yellow(res.status_code + " " + node_http.STATUS_CODES[res.status_code]));
format_headers(res.headers);
res.binary_body = binary_body_check(res.headers);
});
http_session.on("http response body", function (session, data) {
if (! filter_match(session)) {
return;
}
console.log(format_line_start(tcp_session, false) +
" #" + session.request_count + " HTTP " + session.response.http_version + " response body: " +
format_size(data.length));
if (options.bodies && !session.response.binary_body) {
// TODO - this is not at all what you want for gzipped or binary data
console.log(ANSI.green(data.toString("utf8")));
}
});
http_session.on("http response complete", function (session) {
if (! filter_match(session)) {
return;
}
console.log(format_line_start(tcp_session, false) +
" #" + session.request_count + " HTTP " + session.response.http_version + " response complete " +
format_size(session.response.body_len) + " " + (Date.now() - session.request.start) + "ms");
});
}
// Make it all go
parse_options();
if (options["no-color"]) {
ANSI.no_color = true;
}
if (options.help) {
usage_die();
}
privs_check();
start_capture_session();
start_drop_watcher();
setup_listeners();