1
# This program is copyright 2009-2011 Percona Inc.
2
# Feedback and improvements are welcome.
4
# THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
5
# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
6
# MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
8
# This program is free software; you can redistribute it and/or modify it under
9
# the terms of the GNU General Public License as published by the Free Software
10
# Foundation, version 2; OR the Perl Artistic License. On UNIX and similar
11
# systems, you can issue `man perlgpl' or `man perlartistic' to read these
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
16
# Place, Suite 330, Boston, MA 02111-1307 USA.
17
# ###########################################################################
18
# HTTPProtocolParser package $Revision: 5811 $
19
# ###########################################################################
21
# Package: HTTPProtocolParser
22
# HTTPProtocolParser parses HTTP traffic from tcpdump files.
24
package HTTPProtocolParser;
25
use base 'ProtocolParser';
28
use warnings FATAL => 'all';
29
use English qw(-no_match_vars);
30
use constant MKDEBUG => $ENV{MKDEBUG} || 0;
32
# server is the "host:port" of the sever being watched. It's auto-guessed if
35
my ( $class, %args ) = @_;
36
my $self = $class->SUPER::new(
43
# Handles a packet from the server given the state of the session. Returns an
44
# event if one was ready to be created, otherwise returns nothing.
45
sub _packet_from_server {
46
my ( $self, $packet, $session, $misc ) = @_;
47
die "I need a packet" unless $packet;
48
die "I need a session" unless $session;
50
MKDEBUG && _d('Packet is from server; client state:', $session->{state});
52
# If there's no session state, then we're catching a server response
54
if ( !$session->{state} ) {
55
MKDEBUG && _d('Ignoring mid-stream server response');
59
if ( $session->{out_of_order} ) {
60
# We're waiting for the header so we can get the content length.
61
# Once we know this, we can determine how many out of order packets
62
# we need to complete the request, then order them and re-process.
63
my ($line1, $content);
64
if ( !$session->{have_header} ) {
65
($line1, $content) = $self->_parse_header(
66
$session, $packet->{data}, $packet->{data_len});
69
$session->{have_header} = 1;
70
$packet->{content_len} = length $content;
71
MKDEBUG && _d('Got out of order header with',
72
$packet->{content_len}, 'bytes of content');
74
my $have_len = $packet->{content_len} || $packet->{data_len};
75
map { $have_len += $_->{data_len} }
76
@{$session->{packets}};
77
$session->{have_all_packets}
78
= 1 if $session->{attribs}->{bytes}
79
&& $have_len >= $session->{attribs}->{bytes};
80
MKDEBUG && _d('Have', $have_len, 'of', $session->{attribs}->{bytes});
84
# Assume that the server is returning only one value.
85
# TODO: make it handle multiple.
86
if ( $session->{state} eq 'awaiting reply' ) {
88
# Save this early because we may return early if the packets
89
# are being received out of order. Also, save it only once
90
# in case we re-process packets if they're out of order.
91
$session->{start_reply} = $packet->{ts} unless $session->{start_reply};
93
# Get first line of header and first chunk of contents/data.
94
my ($line1, $content) = $self->_parse_header($session, $packet->{data},
97
# The reponse, when in order, is text header followed by data.
98
# If there's no line1, then we didn't get the text header first
99
# which means we're getting the response in out of order packets.
101
$session->{out_of_order} = 1; # alert parent
102
$session->{have_all_packets} = 0;
106
# First line should be: version code phrase
107
# E.g.: HTTP/1.1 200 OK
108
my ($version, $code, $phrase) = $line1 =~ m/(\S+)/g;
109
$session->{attribs}->{Status_code} = $code;
110
MKDEBUG && _d('Status code for last', $session->{attribs}->{arg},
111
'request:', $session->{attribs}->{Status_code});
113
my $content_len = $content ? length $content : 0;
114
MKDEBUG && _d('Got', $content_len, 'bytes of content');
115
if ( $session->{attribs}->{bytes}
116
&& $content_len < $session->{attribs}->{bytes} ) {
117
$session->{data_len} = $session->{attribs}->{bytes};
118
$session->{buff} = $content;
119
$session->{buff_left} = $session->{attribs}->{bytes} - $content_len;
120
MKDEBUG && _d('Contents not complete,', $session->{buff_left},
122
$session->{state} = 'recving content';
126
elsif ( $session->{state} eq 'recving content' ) {
127
if ( $session->{buff} ) {
128
MKDEBUG && _d('Receiving content,', $session->{buff_left},
132
MKDEBUG && _d('Contents received');
136
warn "Server response in unknown state";
140
MKDEBUG && _d('Creating event, deleting session');
141
$session->{end_reply} = $session->{ts_max} || $packet->{ts};
142
my $event = $self->make_event($session, $packet);
143
delete $self->{sessions}->{$session->{client}}; # http is stateless!
147
# Handles a packet from the client given the state of the session.
148
sub _packet_from_client {
149
my ( $self, $packet, $session, $misc ) = @_;
150
die "I need a packet" unless $packet;
151
die "I need a session" unless $session;
153
MKDEBUG && _d('Packet is from client; state:', $session->{state});
156
if ( ($session->{state} || '') =~ m/awaiting / ) {
157
MKDEBUG && _d('More client headers:', $packet->{data});
161
if ( !$session->{state} ) {
162
$session->{state} = 'awaiting reply';
163
my ($line1, undef) = $self->_parse_header($session, $packet->{data}, $packet->{data_len});
164
# First line should be: request page version
165
# E.g.: GET /foo.html HTTP/1.1
166
my ($request, $page, $version) = $line1 =~ m/(\S+)/g;
167
if ( !$request || !$page ) {
168
MKDEBUG && _d("Didn't get a request or page:", $request, $page);
171
$request = lc $request;
172
my $vh = $session->{attribs}->{Virtual_host} || '';
173
my $arg = "$request $vh$page";
174
MKDEBUG && _d('arg:', $arg);
176
if ( $request eq 'get' || $request eq 'post' ) {
177
@{$session->{attribs}}{qw(arg)} = ($arg);
180
MKDEBUG && _d("Don't know how to handle a", $request, "request");
184
$session->{start_request} = $packet->{ts};
185
$session->{attribs}->{host} = $packet->{src_host};
186
$session->{attribs}->{pos_in_log} = $packet->{pos_in_log};
187
$session->{attribs}->{ts} = $packet->{ts};
191
die "Probably multiple GETs from client before a server response?";
198
my ( $self, $session, $data, $len, $no_recurse ) = @_;
199
die "I need data" unless $data;
200
my ($header, $content) = split(/\r\n\r\n/, $data);
201
my ($line1, $header_vals) = $header =~ m/\A(\S+ \S+ .+?)\r\n(.+)?/s;
202
MKDEBUG && _d('HTTP header:', $line1);
203
return unless $line1;
205
if ( !$header_vals ) {
206
MKDEBUG && _d('No header vals');
207
return $line1, undef;
210
foreach my $val ( split(/\r\n/, $header_vals) ) {
212
# Capture and save any useful header values.
213
MKDEBUG && _d('HTTP header:', $val);
214
if ( $val =~ m/^Content-Length/i ) {
215
($session->{attribs}->{bytes}) = $val =~ /: (\d+)/;
216
MKDEBUG && _d('Saved Content-Length:', $session->{attribs}->{bytes});
218
if ( $val =~ m/Content-Encoding/i ) {
219
($session->{compressed}) = $val =~ /: (\w+)/;
220
MKDEBUG && _d('Saved Content-Encoding:', $session->{compressed});
222
if ( $val =~ m/^Host/i ) {
223
# The "host" attribute is already taken, so we call this "domain".
224
($session->{attribs}->{Virtual_host}) = $val =~ /: (\S+)/;
225
MKDEBUG && _d('Saved Host:', ($session->{attribs}->{Virtual_host}));
228
return $line1, $content;
232
my ($package, undef, $line) = caller 0;
233
@_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
234
map { defined $_ ? $_ : 'undef' }
236
print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
241
# ###########################################################################
242
# End HTTPProtocolParser package
243
# ###########################################################################