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   3062 use strict;
  5         11  
  5         169  
3 5     5   27 use warnings;
  5         13  
  5         140  
4 5     5   26 use Carp;
  5         10  
  5         328  
5 5     5   2403 use Digest::HMAC_SHA1;
  5         22252  
  5         218  
6 5     5   1921 use MIME::Base64 ();
  5         2480  
  5         115  
7 5     5   36 use HTTP::Date ();
  5         22  
  5         4756  
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 6731 my ($class, $credentials, $host) = @_;
20 16 50       59 if (ref($credentials) ne 'Amazon::S3::Thin::Credentials') {
21 0         0 croak "credentials object is not given."
22             }
23 16         45 my $self = {
24             credentials => $credentials,
25             host => $host,
26             };
27 16         54 bless $self, $class;
28             }
29              
30             sub sign
31             {
32 3     3 0 13 my ($self, $request) = @_;
33 3 100       13 $request->header(Date => HTTP::Date::time2str(time)) unless $request->header('Date');
34 3 100       221 if (defined $self->{credentials}->session_token) {
35 1         4 $request->header('X-Amz-Security-Token', $self->{credentials}->session_token);
36             }
37 3         77 my $host = $request->uri->host;
38 3         323 my $bucket = substr($host, 0, length($host) - length($self->{host}) - 1);
39 3         13 my $path = $bucket . $request->uri->path;
40 3         72 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         119 , $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 87 my ($self, $method, $path, $headers, $expires) = @_;
51              
52 14         36 my $string_to_sign = $self->string_to_sign( $method, $path, $headers, $expires );
53              
54 14         75 my $hmac = Digest::HMAC_SHA1->new($self->{credentials}->secret_access_key);
55 14         580 $hmac->add($string_to_sign);
56 14         113 return MIME::Base64::encode_base64($hmac->digest, '');
57             }
58              
59             sub string_to_sign {
60 24     24 0 1287 my ($self, $method, $path, $headers, $expires) = @_;
61              
62 24         41 my %interesting_headers = ();
63 24         98 while (my ($key, $value) = each %$headers) {
64 61         114 my $lk = lc $key;
65 61 100 100     406 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         84 $interesting_headers{$lk} = $self->_trim($value);
71             }
72             }
73              
74             # these keys get empty strings if they don't exist
75 24   100     102 $interesting_headers{'content-type'} ||= '';
76 24   100     94 $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       51 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       51 $interesting_headers{'date'} = $expires if $expires;
85              
86 24         51 my $string_to_sign = "$method\n";
87 24         109 foreach my $key (sort keys %interesting_headers) {
88 81 100       253 if ($key =~ /^$AMAZON_HEADER_PREFIX/) {
89 9         28 $string_to_sign .= "$key:$interesting_headers{$key}\n";
90             }
91             else {
92 72         164 $string_to_sign .= "$interesting_headers{$key}\n";
93             }
94             }
95              
96 24         91 $path =~ /^([^?]*)(.*)/;
97 24         74 $string_to_sign .= "/$1";
98 24 100       64 if (! $2) {
99 16         81 return $string_to_sign;
100             }
101              
102 8         15 my $query_string = $2;
103              
104 8         21 my %interesting_subresources = map { $_ => '' } @ordered_subresources;
  128         282  
105              
106 8         48 foreach my $query (split /[&?]/, $query_string) {
107 28         58 $query =~ /^([^=]+)/;
108 28 100       69 if (exists $interesting_subresources{$1}) {
109 12         24 $interesting_subresources{$1} = $query;
110             }
111             }
112 8         17 my $join_char = '?';
113 8         13 foreach my $name (@ordered_subresources) {
114 128 100       228 if ($interesting_subresources{$name}) {
115 12         19 $string_to_sign .= $join_char . $name;
116 12         18 $join_char = '&';
117             }
118             }
119 8         54 return $string_to_sign;
120             }
121              
122             sub _trim {
123 41     41   71 my ($self, $value) = @_;
124 41         113 $value =~ s/^\s+//;
125 41         124 $value =~ s/\s+$//;
126 41         238 return $value;
127             }
128              
129             1;