File Coverage

blib/lib/Authen/HTTP/Signature/Parser.pm
Criterion Covered Total %
statement 61 64 95.3
branch 17 32 53.1
condition 2 3 66.6
subroutine 9 9 100.0
pod 1 1 100.0
total 90 109 82.5


line stmt bran cond sub pod time code
1             package Authen::HTTP::Signature::Parser;
2              
3 3     3   133298 use strict;
  3         9  
  3         149  
4 3     3   23 use warnings;
  3         6  
  3         96  
5              
6 3     3   3191 use Moo;
  3         131461  
  3         26  
7 3     3   6560 use Authen::HTTP::Signature;
  3         9  
  3         125  
8 3     3   23 use HTTP::Date qw(str2time);
  3         6  
  3         199  
9 3     3   19 use Scalar::Util qw(blessed);
  3         6  
  3         149  
10 3     3   18 use Carp qw(confess);
  3         5  
  3         9732  
11              
12             =head1 NAME
13              
14             Authen::HTTP::Signature::Parser - Parse HTTP signature headers
15              
16             =cut
17              
18             our $VERSION = '0.02';
19              
20             =head1 PURPOSE
21              
22             This class parses a HTTP signature 'Authorization' header (if one exists) from a request
23             object and populates attributes in a L object.
24              
25             =head1 ATTRIBUTES
26              
27             =over
28              
29             =item request
30              
31             The request to be parsed.
32              
33             =back
34              
35             =cut
36              
37             has 'request' => (
38             is => 'rw',
39             isa => sub { confess "'request' must be blessed" unless blessed($_[0]) },
40             predicate => 'has_request',
41             );
42              
43             around BUILDARGS => sub {
44             my $orig = shift;
45             my $class = shift;
46              
47             if ( @_ == 1 ) {
48             unshift @_, "request";
49             }
50              
51             return $class->$orig(@_);
52             };
53              
54             =over
55              
56             =item get_header
57              
58             A call back to get a header from C.
59              
60             =back
61              
62             =cut
63              
64             has 'get_header' => (
65             is => 'rw',
66             isa => sub { die "'get_header' expects a CODE ref\n" unless ref($_[0]) eq "CODE" },
67             predicate => 'has_get_header',
68             default => sub {
69             sub {
70             confess "Didn't get 2 arguments" unless @_ == 2;
71             my $request = shift;
72             confess "'request' isn't blessed" unless blessed $request;
73             my $name = shift;
74              
75             $name eq 'request-line' ?
76             sprintf("%s %s",
77             $request->uri->path_query,
78             $request->protocol)
79             : $request->header($name);
80             };
81             },
82             lazy => 1,
83             );
84              
85             =over
86              
87             =item skew
88              
89             Defaults to 300 seconds in either direction from your clock. If the Date header data is outside of this range,
90             the request is considered invalid.
91              
92             Set this value to 0 to disable skew checks for testing purposes.
93              
94             =back
95              
96             =cut
97              
98             has 'skew' => (
99             is => 'rw',
100             isa => sub { die "$_[0] isn't an integer" unless $_[0] =~ /[0-9]+/ },
101             default => sub { 300 },
102             );
103              
104              
105             =head1 METHOD
106              
107             Errors are fatal.
108              
109             =over
110              
111             =item parse()
112              
113             This method parses signature header components.
114              
115             =back
116              
117             =cut
118              
119             sub parse {
120 2     2 1 31 my $self = shift;
121 2   66     13 my $request = shift || $self->request;
122              
123 2 50       2982 confess "There was no request to parse!" unless $request;
124              
125 2         31 my $sig_str = $self->get_header->($request, 'authorization');
126 2 50       111 confess 'No authorization header value was returned!' unless $sig_str;
127              
128 2         8 $self->_check_skew($request);
129              
130 1         5 my ( $sig_text ) = $sig_str =~ /^(Signature).*$/;
131 1 50       5 confess "does not match required string 'Signature'" unless $sig_text;
132              
133 1         7 my ( $params ) = $sig_str =~ /^Signature\s+(keyId=".*").*$/;
134 1 50       5 confess "No parameters found!" unless $params;
135              
136 1         7 my ( $b64_str ) = $sig_str =~ /^.*"\s+(\S+)$/;
137 1 50       5 confess "No signature data found!" unless $b64_str;
138              
139             # Probably ought to use a module here, but...
140             #
141             # Positive lookbehind and positive lookahead in split
142             # http://www.effectiveperlprogramming.com/blog/1411
143              
144 1         8 my ( $key_id, $algo, $hdrs, $ext ) = split /(?<="),(?=[ahe])/, $params;
145              
146 1         5 $key_id =~ s/^keyId="(.*)"$/$1/;
147 1         5 $algo =~ s/^algorithm="(.*)"$/$1/;
148 1 50       4 $ext =~ s/^ext="(.*)"/$1/ if $ext;
149              
150 1 50       3 confess "No key id found!" unless $key_id;
151 1 50       7 confess "No algorithm found" unless $algo;
152              
153 1         2 my @headers;
154 1 50       6 if ( $hdrs ) {
155 0         0 $hdrs =~ s/^headers="(.*)"$/$1/;
156 0         0 @headers = split / /, $hdrs;
157             }
158              
159 1 50       5 push @headers, "date" unless @headers;
160              
161             # die on duplicate headers
162 1         2 my %h;
163 1         3 foreach my $hdr ( @headers ) {
164 1 50       3 if ( exists $h{$hdr} ) {
165 0         0 confess "Duplicate header '$hdr' found in signature header parameter. Aborting.";
166             }
167 1         4 $h{$hdr}++;
168             }
169              
170             # normalize headers to lower-case
171 1         3 @headers = map { lc } @headers;
  1         4  
172              
173 1 50       25 my $ss = join "\n", map {
174 1         2 $self->get_header->($request, $_)
175             or confess "Couldn't get header value for $_\n" } @headers;
176              
177 1         48 return Authen::HTTP::Signature->new(
178             key_id => $key_id,
179             headers => \@headers,
180             signing_string => $ss,
181             algorithm => $algo,
182             extensions => $ext,
183             signature => $b64_str,
184             request => $request,
185             );
186             }
187              
188             sub _check_skew {
189 2     2   5 my $self = shift;
190              
191 2 100       177 if ( $self->skew ) {
192 1         859 my $request = shift;
193 1 50       6 confess "No request found" unless $request;
194              
195 1         26 my $header_time = str2time($self->get_header->($request, 'date'));
196 1 50       122 confess "No Date header was returned (or could be parsed)" unless $header_time;
197              
198 1         10 my $diff = abs(time - $header_time);
199 1 50       28 if ( $diff >= $self->skew ) {
200 1         35 confess "Request is outside of clock skew tolerance: $diff seconds computed, " . $self->skew . " seconds allowed.\n";
201             }
202             }
203              
204 1         10 return 1;
205              
206             }
207              
208              
209             =head1 SEE ALSO
210              
211             L
212              
213             =cut
214              
215             1;