File Coverage

blib/lib/HTTP/Lint.pm
Criterion Covered Total %
statement 61 66 92.4
branch 38 58 65.5
condition 57 86 66.2
subroutine 12 13 92.3
pod 4 7 57.1
total 172 230 74.7


line stmt bran cond sub pod time code
1 2     2   94223 use strict;
  2         5  
  2         68  
2 2     2   11 use warnings;
  2         3  
  2         96  
3              
4             package HTTP::Lint;
5              
6             =head1 NAME
7              
8             HTTP::Lint - Check HTTP messages and transactions for protocol violations
9              
10             =head1 SYNOPSIS
11              
12             use HTTP::Lint qw/http_lint/;
13             use HTTP::Request;
14             use HTTP::Response;
15              
16             my $request = parse HTTP::Request ($q);
17             my $response = parse HTTP::Request ($r);
18              
19             # Check request
20             warn $_ foreach http_lint ($request);
21              
22             # Check response, treat warnings as fatal
23             foreach http_lint ($response) {
24             die $_ if ref $_ eq 'HTTP::Lint::Error';
25             warn $_ if ref $_ eq 'HTTP::Lint::Warning';
26             }
27              
28             # Construct a transaction and check it
29             $response->request ($request);
30             warn $_ foreach http_lint ($response);
31              
32             =head1 DESCRIPTION
33              
34             B checks for protocol violation and suspicious or
35             ambigious stuff in HTTP messages and transactions. It produces
36             errors and warning, loosely corresponsing to MUST and SHOULD
37             clauses in RFC2616 (HTTP/1.1 specification).
38              
39             =cut
40              
41 2     2   12 use Scalar::Util qw/blessed/;
  2         8  
  2         207  
42 2     2   10 use Exporter qw/import/;
  2         11  
  2         3918  
43             our @EXPORT_OK = qw/http_lint request_lint response_lint transaction_lint/;
44              
45             # These are only used internally, no need for POD documentation
46             @HTTP::Lint::Warning::ISA = @HTTP::Lint::Error::ISA
47             = ('HTTP::Lint::Message');
48 17     17 0 91 sub message { [ shift, \@_ ] }
49 9     9 0 682 sub error { bless message (@_), 'HTTP::Lint::Error'; }
50 8     8 0 358 sub warning { bless message (@_), 'HTTP::Lint::Warning'; }
51              
52             =head1 SUBROUTINES
53              
54             =over 4
55              
56             =item B [MESSAGE]
57              
58             Checks an instance of a subclass of L:
59             a L or a L. If a L
60             is given, and it contains a valid B associated,
61             the request is checked too and a transaction check is done to
62             check whether the response is appropriate for the request.
63              
64             Result of the call is an array of arrayrefs blessed with
65             L or L package.
66             The first element of the message is the message string,
67             the second one is the arrayref of section numbers that refer
68             to B:
69              
70             bless [ '418 Response from what is not a teapot',
71             [ 666,1,2,3 ] ], 'HTTP::Lint::Error';
72              
73             You can stringify the message or call the method B
74             to pretty-format the message.
75              
76             =cut
77              
78             sub http_lint
79             {
80 7     7 1 8918 my $message = shift;
81 7         13 my @return;
82              
83 7 50       43 return @return unless blessed $message;
84 7 50       44 if ($message->isa ('HTTP::Response')) {
85 7         25 push @return, response_lint ($message);
86 7 50       22 if ($message->request) {
87 7         74 push @return, transaction_lint ($message->request, $message);
88 7         20 $message = $message->request;
89             }
90             }
91 7 50       84 if ($message->isa ('HTTP::Request')) {
92 7         19 push @return, request_lint ($message);
93             }
94              
95 7         88 return @return;
96             }
97              
98             =item B [REQUEST]
99              
100             Only check a L.
101              
102             The return value follows the same rules as of B.
103              
104             =cut
105              
106             sub request_lint
107             {
108 7     7 1 10 my $request = shift;
109 7         10 my @return;
110              
111             # http://www.w3.org/Protocols/HTTP/1.1/rfc2616bis/issues/#i19
112 7 100 100     23 push @return, error $request->method.' request with non-empty body'
113             if $request->method =~ /^(GET|HEAD|DELETE)$/
114             and $request->content;
115 7 100 100     121 push @return, error 'HTTP/1.1 request without Host header' => 9
      100        
116             if ($request->protocol || 'HTTP/1.0') eq 'HTTP/1.1'
117             and not defined $request->header ('Host');
118 7 100       138 push @return, warning 'Missing Accept header' => 14,1
119             unless $request->header ('Accept');
120              
121 7         253 return @return;
122             }
123              
124             =item B [REQUEST]
125              
126             Only check a L.
127              
128             The return value follows the same rules as of B.
129              
130             =cut
131              
132             sub response_lint
133             {
134 7     7 1 13 my $response = shift;
135 7         10 my @return;
136              
137 7 50 66     45 push @return, error 'Length does not correspond to actual content size' => 4,4
138             if defined $response->content_length
139             and $response->content_length != length ($response->content);
140 7 100 100     393 push @return, error 'HTTP/1.1 non-close response without given size' => 19,6,2
      100        
      66        
      100        
      50        
      66        
141             if ($response->protocol || 'HTTP/1.0') eq 'HTTP/1.1'
142             and not $response->code == 204
143             and not $response->code == 304
144             and not defined $response->content_length
145             and ($response->header ('Transfer-Encoding') || '') ne 'chunked';
146 7 100 100     188 push @return, warning 'Missing media type', 7,2,1
147             if $response->content
148             and not defined $response->header ('Content-Type');
149 7 50 66     233 push @return, error $response->code.' response with content', 10,2,5
150             if $response->content
151             and $response->code =~ /^[23]04$/;
152 7 100 66     140 push @return, error 'Location missing for a '.$response->code.' response' => 10,2,2
153             if $response->code =~ /^(201|3\d\d)$/
154             and not defined $response->header ('Location');
155 7 100 66     80 push @return, error 'WWW-Authenticate header missing for a 401 response' => 14,47
156             if $response->code == 401
157             and not defined $response->header ('WWW-Authenticate');
158 7 50 33     79 push @return, error 'Proxy-Authenticate header missing for a 407 response' => 10,4,8
159             if $response->code == 407
160             and not defined $response->header ('Proxy-Authenticate');
161 7 100 66     222 push @return, warning 'Retry-After header missing for a 503 response' => 10,5,4
162             if $response->code == 503
163             and not defined $response->header ('Retry-After');
164 7 50       75 push @return, warning 'Undefined Refresh header is present'
165             if defined $response->header ('Refresh');
166 7 100 66     404 push @return, error '405 without allowed methods specified' => 10,4,6
167             if $response->code == 405
168             and not defined $response->header ('Allow');
169 7 0 33     59 push @return, error 'Partial content lacks correct range specification' => 10,2,7
      0        
      33        
170             if $response->code eq 206
171             and not $response->header ('Content-Range')
172             and not ($response->header ('Content-Type') || '') eq 'multipart/byteranges';
173 7 100 66     74 push @return, warning 'Missing Date header' => 14,18
174             if $response->code =~ /^2\d\d$/
175             and not $response->header ('Date');
176              
177 7         53 return @return;
178             }
179              
180             =item B [REQUEST] [RESPONSE]
181              
182             Only check a relation between L and L.
183              
184             The return value follows the same rules as of B.
185              
186             =cut
187              
188             sub transaction_lint
189             {
190 7     7 1 67 my $request = shift;
191 7         12 my $response = shift;
192 7         7 my @return;
193              
194 7 100 100     25 push @return, warning 'HTTP/1.1 response for a HTTP/1.0 request' => 3,1
      100        
      100        
195             if ($request->protocol || 'HTTP/1.0') eq 'HTTP/1.0'
196             and ($response->protocol || 'HTTP/1.0') eq 'HTTP/1.1';
197 7 100 100     112 push @return, warning 'Action with side effects conducted for a '.$request->method.' request' => 13,9
198             if $request->method =~ /^(GET|HEAD|TRACE|OPTIONS)$/
199             and $response->code == 201;
200 7 100 66     101 push @return, error 'HEAD response with non-empty body' => 4,3
201             if $request->method eq 'HEAD'
202             and $response->content;
203 7 50 0     69 push @return, warning 'TRACE response with wrong content type' => 9,8
      33        
204             if $request->method eq 'TRACE'
205             and ($response->header ('Content-Type') || '') ne 'message/http';
206 7 50 33     72 push @return, error 'Partial content returned despite not being asked for' => 14,35,2
207             if $response->code eq 206
208             and not defined $request->header ('Range');
209 7 50 33     69 push @return, error 'Server demands length despite being given it' => 10,4,12
210             if $response->code eq 411
211             and $request->header ('Content-Length');
212 7 50 33     80 push @return, error 'Server complains about bad range without range being requested' => 10,4,17
213             if $response->code eq 416
214             and not $request->header ('Range');
215              
216 7         68 return @return;
217             }
218              
219             package HTTP::Lint::Message;
220              
221 2         16 use overload fallback => 1,
222 2     2   1673 '""' => \&pretty;
  2         1192  
223              
224             sub pretty
225             {
226 0     0     my $self = shift;
227 0 0         $self->isa ('HTTP::Lint::Message')
228             or die 'Not a HTTP::Lint::Message';
229 0           return (ref $self eq 'HTTP::Lint::Error' ? 'ERROR: ' : 'WARNING: ').
230             $self->[0].
231 0 0         (@{$self->[1]} ? ' [RFC2616: '.join ('.', @{$self->[1]}).']': '');
  0 0          
232             }
233              
234             =back
235              
236             =head1 SEE ALSO
237              
238             =over
239              
240             =item *
241              
242             L -- HTTP/1.1 protoocl specification
243              
244             =item *
245              
246             L -- Ambigious stuff in RFC2616
247              
248             =item *
249              
250             L -- Object representation of a HTTP message
251              
252             =back
253              
254             =head1 BUGS
255              
256             Probably many!
257              
258             The set of checks is very incomplete and some are likely wrong and produce
259             false positives.
260              
261             Contributions, patches and bug reports are more than welcome.
262              
263             =head1 COPYRIGHT
264              
265             Copyright 2011, Lubomir Rintel
266              
267             This program is free software; you can redistribute it and/or modify it
268             under the same terms as Perl itself.
269              
270             =head1 AUTHOR
271              
272             Lubomir Rintel C
273              
274             =cut
275              
276             1;