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