File Coverage

blib/lib/Amazon/S3/Thin/Signer/V2.pm
Criterion Covered Total %
statement 72 73 98.6
branch 18 20 90.0
condition 13 13 100.0
subroutine 11 11 100.0
pod 0 4 0.0
total 114 121 94.2


line stmt bran cond sub pod time code
1             package Amazon::S3::Thin::Signer::V2;
2 5     5   2183 use strict;
  5         9  
  5         128  
3 5     5   21 use warnings;
  5         7  
  5         113  
4 5     5   22 use Carp;
  5         7  
  5         230  
5 5     5   1866 use Digest::HMAC_SHA1;
  5         18564  
  5         170  
6 5     5   1535 use MIME::Base64 ();
  5         2156  
  5         95  
7 5     5   26 use HTTP::Date ();
  5         17  
  5         3852  
8              
9             my $AMAZON_HEADER_PREFIX = 'x-amz-';
10              
11             # reserved subresources such as acl or torrent
12             our @ordered_subresources = qw(
13             acl delete lifecycle location logging notification partNumber policy
14             requestPayment torrent uploadId uploads versionId versioning versions
15             website
16             );
17              
18             sub new {
19 16     16 0 5153 my ($class, $credentials, $host) = @_;
20 16 50       53 if (ref($credentials) ne 'Amazon::S3::Thin::Credentials') {
21 0         0 croak "credentials object is not given."
22             }
23 16         36 my $self = {
24             credentials => $credentials,
25             host => $host,
26             };
27 16         47 bless $self, $class;
28             }
29              
30             sub sign
31             {
32 3     3 0 16 my ($self, $request) = @_;
33 3 100       10 $request->header(Date => HTTP::Date::time2str(time)) unless $request->header('Date');
34 3 100       182 if (defined $self->{credentials}->session_token) {
35 1         5 $request->header('X-Amz-Security-Token', $self->{credentials}->session_token);
36             }
37 3         57 my $host = $request->uri->host;
38 3         257 my $bucket = substr($host, 0, length($host) - length($self->{host}) - 1);
39 3         9 my $path = $bucket . $request->uri->path;
40 3         56 my $signature = $self->calculate_signature( $request->method, $path, $request->headers );
41             $request->header(
42             Authorization => sprintf("AWS %s:%s"
43             , $self->{credentials}->access_key_id,
44 3         86 , $signature));
45             }
46              
47             # generate a canonical string for the given parameters. expires is optional and is
48             # only used by query string authentication.
49             sub calculate_signature {
50 14     14 0 70 my ($self, $method, $path, $headers, $expires) = @_;
51              
52 14         25 my $string_to_sign = $self->string_to_sign( $method, $path, $headers, $expires );
53              
54 14         53 my $hmac = Digest::HMAC_SHA1->new($self->{credentials}->secret_access_key);
55 14         455 $hmac->add($string_to_sign);
56 14         88 return MIME::Base64::encode_base64($hmac->digest, '');
57             }
58              
59             sub string_to_sign {
60 24     24 0 1028 my ($self, $method, $path, $headers, $expires) = @_;
61              
62 24         36 my %interesting_headers = ();
63 24         88 while (my ($key, $value) = each %$headers) {
64 61         95 my $lk = lc $key;
65 61 100 100     347 if ( $lk eq 'content-md5'
      100        
      100        
66             or $lk eq 'content-type'
67             or $lk eq 'date'
68             or $lk =~ /^$AMAZON_HEADER_PREFIX/)
69             {
70 41         67 $interesting_headers{$lk} = $self->_trim($value);
71             }
72             }
73              
74             # these keys get empty strings if they don't exist
75 24   100     76 $interesting_headers{'content-type'} ||= '';
76 24   100     72 $interesting_headers{'content-md5'} ||= '';
77              
78             # x-amz-date becomes date if it exists
79             $interesting_headers{'date'} = delete $interesting_headers{'x-amz-date'}
80 24 100       44 if exists $interesting_headers{'x-amz-date'};
81              
82             # if you're using expires for query string auth, then it trumps date
83             # (and x-amz-date)
84 24 50       36 $interesting_headers{'date'} = $expires if $expires;
85              
86 24         59 my $string_to_sign = "$method\n";
87 24         86 foreach my $key (sort keys %interesting_headers) {
88 81 100       201 if ($key =~ /^$AMAZON_HEADER_PREFIX/) {
89 9         20 $string_to_sign .= "$key:$interesting_headers{$key}\n";
90             }
91             else {
92 72         135 $string_to_sign .= "$interesting_headers{$key}\n";
93             }
94             }
95              
96 24         72 $path =~ /^([^?]*)(.*)/;
97 24         54 $string_to_sign .= "/$1";
98 24 100       54 if (! $2) {
99 16         63 return $string_to_sign;
100             }
101              
102 8         17 my $query_string = $2;
103              
104 8         15 my %interesting_subresources = map { $_ => '' } @ordered_subresources;
  128         195  
105              
106 8         36 foreach my $query (split /[&?]/, $query_string) {
107 28         48 $query =~ /^([^=]+)/;
108 28 100       54 if (exists $interesting_subresources{$1}) {
109 12         19 $interesting_subresources{$1} = $query;
110             }
111             }
112 8         11 my $join_char = '?';
113 8         14 foreach my $name (@ordered_subresources) {
114 128 100       191 if ($interesting_subresources{$name}) {
115 12         17 $string_to_sign .= $join_char . $name;
116 12         15 $join_char = '&';
117             }
118             }
119 8         53 return $string_to_sign;
120             }
121              
122             sub _trim {
123 41     41   61 my ($self, $value) = @_;
124 41         86 $value =~ s/^\s+//;
125 41         102 $value =~ s/\s+$//;
126 41         137 return $value;
127             }
128              
129             1;