File Coverage

blib/lib/Sniffer/Connection/HTTP.pm
Criterion Covered Total %
statement 18 142 12.6
branch 0 40 0.0
condition 0 22 0.0
subroutine 6 23 26.0
pod 1 11 9.0
total 25 238 10.5


line stmt bran cond sub pod time code
1             package Sniffer::Connection::HTTP;
2 4     4   26 use strict;
  4         9  
  4         162  
3 4     4   2320 use Sniffer::Connection;
  4         16  
  4         36  
4 4     4   3507 use HTTP::Request;
  4         121501  
  4         162  
5 4     4   3047 use HTTP::Response;
  4         35516  
  4         202  
6              
7             =head1 NAME
8              
9             Sniffer::Connection::HTTP - Callbacks for a HTTP connection
10              
11             =head1 SYNOPSIS
12              
13             You shouldn't use this directly but via L
14             which encapsulates most of this.
15              
16             my $sniffer = Sniffer::Connection::HTTP->new(
17             callbacks => {
18             request => sub { my ($req,$conn) = @_; print $req->uri,"\n" if $req },
19             response => sub { my ($res,$req,$conn) = @_; print $res->code,"\n" },
20             }
21             );
22              
23             # retrieve TCP packet in $tcp, for example via Net::Pcap
24             my $tcp = sniff_tcp_packet;
25              
26             $sniffer->handle_packet($tcp);
27              
28             =cut
29              
30 4     4   35 use base 'Class::Accessor';
  4         9  
  4         502  
31              
32 4     4   32 use vars qw($VERSION);
  4         6  
  4         8480  
33              
34             $VERSION = '0.24';
35              
36             my @callbacks = qw(request response closed log);
37             __PACKAGE__->mk_accessors(qw(tcp_connection sent_buffer recv_buffer _response _response_chunk_size _response_len _request prev_request),
38             @callbacks);
39              
40             sub new {
41 0     0 1   my ($class,%args) = @_;
42              
43 0           my $packet = delete $args{tcp};
44              
45             # Set up dummy callbacks as the default
46 0   0 0     for (@callbacks) { $args{$_} ||= sub {}; };
  0            
47              
48 0           for (qw(sent_buffer recv_buffer)) {
49 0   0       $args{$_} ||= \(my $buffer);
50             };
51              
52 0   0 0     my $tcp_log = delete $args{tcp_log} || sub {};
53              
54 0           my $self = $class->SUPER::new(\%args);
55             $self->tcp_connection(Sniffer::Connection->new(
56             tcp => $packet,
57 0     0     sent_data => sub { $self->sent_data(@_) },
58 0     0     received_data => sub { $self->received_data(@_) },
59       0     closed => sub {},
60 0     0     teardown => sub { $self->closed->($self) },
61 0           log => $tcp_log,
62             ));
63              
64 0           $self;
65             };
66              
67             sub sent_data {
68 0     0 0   my ($self,$data,$conn) = @_;
69 0           $self->flush_received;
70 0           ${$self->{sent_buffer}} .= $data;
  0            
71 0           $self->flush_sent;
72             };
73              
74             sub received_data {
75 0     0 0   my ($self,$data,$conn) = @_;
76 0           $self->flush_sent;
77 0           ${$self->{recv_buffer}} .= $data;
  0            
78             #warn $data;
79 0           $self->flush_received;
80             };
81              
82             sub extract_chunksize {
83 0     0 0   my ($self,$buffer) = @_;
84 0           my $chunksize;
85             #$self->log->("---Extracting from\n$$buffer\n---");
86 0 0         if (! ($$buffer =~ s!^\s*([a-f0-9]+)[ \t]*\r\n!!si)) {
87 0           $self->log->("Extracting chunked size failed.");
88             #$self->log->($$buffer);
89 0           (my $copy = $$buffer) =~ s!\n!\\n\n!gs;
90 0           $copy =~ s!\r!\\r!gs;
91 0           $self->log->($copy);
92             } else {
93 0           $chunksize = hex $1;
94             #$self->log->(sprintf "Found chunked size %s (%s remaining)\n", $chunksize, length $$buffer);
95             #$self->log->(length $$buffer);
96 0           $self->_response_chunk_size($chunksize);
97             };
98             #$self->log->("---Buffer is now\n$$buffer\n---");
99 0           return $chunksize
100             };
101              
102             sub flush_received {
103 0     0 0   my ($self) = @_;
104 0           my $buffer = $self->recv_buffer;
105             #$self->log->($$buffer);
106 0           while ($$buffer) {
107 0 0         if (! (my $res = $self->_response)) {
108             # We need to find something that looks like a valid HTTP request in our stream
109 0 0         if (not $$buffer =~ s!.*^(HTTP/\d\..*? [12345]\d\d\b)!$1!m) {
110             # Need to discard-and-sync
111 0           $$buffer = "";
112             #$self->recv_buffer(undef);
113 0           return;
114             };
115              
116 0 0         if (! ($$buffer =~ s!^(.*?\r?\n\r?\n)!!sm)) {
117             # need more data before header is complete
118 0           $self->log->("Need more header data");
119             #$self->recv_buffer($buffer);
120 0           return;
121             };
122              
123 0           my $h = $1;
124 0           $res = HTTP::Response->parse($h);
125 0           $self->_response($res);
126              
127 0           my $len = $res->header('Content-Length');
128              
129 0           $self->_response_len( $len );
130             };
131              
132 0           my $res = $self->_response;
133 0           my $len = $self->_response_len;
134 0           my $chunksize = $self->_response_chunk_size;
135              
136 0   0       my $te = lc ($res->header('Transfer-Encoding') || '');
137 0 0 0       if ($te and $te eq 'chunked') {
138 0 0         if (! defined $chunksize) {
139 0           $chunksize = $self->extract_chunksize($buffer);
140             };
141              
142 0 0         if (defined $chunksize) {
143             #$self->log->("Chunked size: $chunksize\n");
144             #$self->log->("Got buffer of size " + length $$buffer);
145              
146 0   0       while (defined $chunksize and length $$buffer >= $chunksize) {
147             #$self->log->("Got chunk of size $chunksize");
148             #$self->log->(">>$$buffer<<");
149 0           $self->_response->add_content(substr($$buffer,0,$chunksize));
150             #$self->log->(substr($$buffer,0,$chunksize));
151 0           $$buffer = substr($$buffer,$chunksize);
152 0           $$buffer =~ s!^\r\n!!;
153             #$self->log->(sprintf "Remaining are %s bytes ($$buffer)", length $$buffer);
154              
155 0           $self->_response_chunk_size(undef);
156 0 0         if ($chunksize == 0) {
    0          
157 0           $self->log->("Got chunksize 0, reporting response");
158 0           $self->report_response($res);
159             #$$buffer =~ s!^\r\n!!;
160              
161 0 0         if ($$buffer eq '') {
162 0           return;
163             };
164             } elsif (length $$buffer) {
165             # Get next chunksize, if available
166 0           $chunksize = $self->extract_chunksize($buffer);
167             #$self->log->("Next size is $chunksize");
168             } else {
169             # We've read/received exactly the chunk.
170             };
171              
172             return
173 0 0         if ! defined $chunksize;
174             };
175             };
176             return
177 0           };
178              
179             # Non-chunked handling:
180 0 0 0       if (defined $len and length $$buffer < $len) {
181             # need more data before header is complete
182 0 0         $self->log->(sprintf "Need more response body data (%0.0f%%)\r", 100 * ((length $$buffer) / $len))
183             if $len;
184 0           return;
185             };
186              
187 0 0 0       if (defined $len and $len == 0) {
188             # can only flush at closing of connection
189 0           $self->log->("Would need to collect whole buffer in connection (unimplemented, taking what I've got)" );
190 0           $len = length $$buffer;
191             };
192              
193 0           $self->report_response_buffer($buffer,$len);
194             };
195             };
196              
197             sub report_response_buffer {
198 0     0 0   my ($self,$buffer,$len) = @_;
199 0           my $res = $self->_response;
200              
201 0 0         $len = length $$buffer
202             if (! defined $len);
203              
204 0           $res->content(substr($$buffer,0,$len));
205 0           $self->log->("Response header and content are ready ($len bytes)");
206              
207 0           $$buffer = substr($$buffer,$len);
208 0 0         if (length $$buffer) {
209 0           $self->log->("Leftover data: $$buffer");
210             };
211 0           $self->report_response($res);
212             };
213              
214             sub report_response {
215 0     0 0   my ($self,$res) = @_;
216 0           $self->response->($res,$self->prev_request,$self);
217 0           $self->_response(undef);
218 0           $self->_response_len(undef);
219             };
220              
221             sub flush_sent {
222 0     0 0   my ($self) = @_;
223 0           my $buffer = $self->sent_buffer;
224 0           while ($$buffer) {
225 0 0         if (! (my $req = $self->_request)) {
226             # We need to find something that looks like a valid HTTP request in our stream
227 0           $$buffer =~ s!.*^(GET|POST)!$1!m;
228              
229 0 0         if (! ($$buffer =~ s!^(.*?\r?\n\r?\n)!!sm)) {
230             # need more data before header is complete
231 0           $self->log->("Need more header data");
232             #$self->sent_buffer($buffer);
233 0           return;
234             };
235              
236             # Consider prepending the hostname in front of
237             # the URI for nicer equivalence with HTTP::Proxy?
238              
239 0           $self->log->("Got header");
240 0           my $h = $1;
241 0           $req = HTTP::Request->parse($h);
242              
243 0           my $host;
244             # should be the IP address of some TCP packet if we don't find the header ...
245 0 0         if ($req->header('Host')) {
246 0           $host = $req->header('Host');
247             } else {
248 0           warn "Missing Host: header. Don't know how to determine hostname";
249 0           $host = "???"
250             };
251 0           $req->uri->scheme('http');
252 0           $req->uri->host($host);
253             #$req->uri->port(80); # fix from TCP packet!
254              
255 0           $self->_request($req);
256             };
257              
258 0           my $req = $self->_request;
259 0   0       my $len = $req->header('Content-Length') || 0; # length $$buffer; # not clean
260              
261 0 0         if (length $$buffer < $len) {
262             # need more data before header is complete
263 0           return;
264             };
265              
266 0           $self->_request->content(substr($$buffer,0,$len));
267 0           $self->log->("Request header and content are ready ($len bytes)");
268              
269 0           $self->request->($req,$self);
270              
271 0           $$buffer = substr($$buffer,$len);
272              
273             # Tie request and response together in a better way than serial request->response->request ...
274 0           $self->prev_request($req);
275 0           $self->_request(undef);
276             };
277             };
278              
279             # Delegate some methods
280 0     0 0   sub handle_packet { my $self = shift;$self->tcp_connection->handle_packet(@_); };
  0            
281 0     0 0   sub flow { my $self = shift; return $self->tcp_connection->flow(@_);};
  0            
282 0     0 0   sub last_activity { my $self = shift; $self->tcp_connection->last_activity(@_) }
  0            
283              
284             1;
285              
286             =head1 TODO
287              
288             =over 4
289              
290             =item *
291              
292             Think about pipelined connections. These are not easily massaged into the
293             request/response scheme. Well, maybe they are, with a bit of hidden
294             logic here.
295              
296             =item *
297              
298             Every response accumulates all data in memory instead of
299             giving the user the partial response so it can be written
300             to disk. This should maybe later be improved.
301              
302             =back
303              
304             =head1 BUGS
305              
306             The whole module suite has almost no tests.
307              
308             If you experience problems, I supply me with a complete,
309             relevant packet dump as the included C creates. Even
310             better, supply me with (failing) tests.
311              
312             =head1 AUTHOR
313              
314             Max Maischein (corion@cpan.org)
315              
316             =head1 COPYRIGHT
317              
318             Copyright (C) 2005-2011 Max Maischein. All Rights Reserved.
319              
320             This code is free software; you can redistribute it and/or modify it
321             under the same terms as Perl itself.
322              
323             =cut