File Coverage

blib/lib/Amazon/S3/SignedURLGenerator.pm
Criterion Covered Total %
statement 18 75 24.0
branch 0 26 0.0
condition 0 25 0.0
subroutine 6 12 50.0
pod 0 2 0.0
total 24 140 17.1


line stmt bran cond sub pod time code
1             package Amazon::S3::SignedURLGenerator;
2              
3 1     1   46811 use strict;
  1         9  
  1         26  
4 1     1   5 use warnings;
  1         1  
  1         34  
5             our $VERSION = '0.02';
6              
7 1     1   4 use Carp;
  1         2  
  1         46  
8 1     1   263 use URI::Escape;
  1         1135  
  1         48  
9 1     1   246 use Digest::HMAC_SHA1;
  1         3411  
  1         36  
10 1     1   244 use MIME::Base64 qw(encode_base64);
  1         446  
  1         723  
11              
12             sub new {
13 0     0 0   my $proto = shift;
14 0   0       my $class = ref($proto) || $proto;
15              
16 0 0         my %args = scalar(@_) % 2 ? %{$_[0]} : @_;
  0            
17 0 0         $args{aws_access_key_id} or croak 'aws_access_key_id is required';
18 0 0         $args{aws_secret_access_key} or croak 'aws_secret_access_key is required';
19              
20 0   0       $args{prefix} ||= 'https://s3.amazonaws.com';
21 0   0       $args{expires} ||= 3600;
22              
23 0           $args{prefix} =~ s/\/$//; # remove last /
24              
25 0           return bless \%args, $class;
26             }
27              
28             sub generate_url {
29 0     0 0   my ($self, $method, $path, $headers) = @_;
30              
31 0           $path =~ s/^\///;
32 0   0       $headers ||= {};
33 0   0       my $expires = $headers->{expires} || (time() + $self->{expires});
34              
35 0           my $x_path = $path;
36 0 0         if ($self->{prefix} =~ '//(.*)\.s3') {
37 0           $x_path = $1 . '/' . $path;
38             }
39              
40 0           my $canonical_string = __canonical_string($method, $x_path, $headers, $expires);
41 0           my $encoded_canonical = __encode($self->{aws_secret_access_key}, $canonical_string, 1);
42 0 0         if (index($path, '?') == -1) {
43 0           return "$self->{prefix}/$path?Signature=$encoded_canonical&Expires=$expires&AWSAccessKeyId=$self->{aws_access_key_id}";
44             } else {
45 0           return "$self->{prefix}/$path&Signature=$encoded_canonical&Expires=$expires&AWSAccessKeyId=$self->{aws_access_key_id}";
46             }
47             }
48              
49             our $AMAZON_HEADER_PREFIX = 'x-amz-';
50             our $METADATA_PREFIX = 'x-amz-meta-';
51              
52             sub __trim {
53 0     0     my ($value) = @_;
54              
55 0           $value =~ s/^\s+//;
56 0           $value =~ s/\s+$//;
57 0           return $value;
58             }
59              
60             # generate a canonical string for the given parameters. expires is optional and is
61             # only used by query string authentication.
62             sub __canonical_string {
63 0     0     my ($method, $path, $headers, $expires) = @_;
64 0           my %interesting_headers = ();
65 0           while (my ($key, $value) = each %$headers) {
66 0           my $lk = lc $key;
67 0 0 0       if (
      0        
      0        
68             $lk eq 'content-md5' or
69             $lk eq 'content-type' or
70             $lk eq 'date' or
71             $lk =~ /^$AMAZON_HEADER_PREFIX/
72             ) {
73 0           $interesting_headers{$lk} = __trim($value);
74             }
75             }
76              
77             # these keys get empty strings if they don't exist
78 0   0       $interesting_headers{'content-type'} ||= '';
79 0   0       $interesting_headers{'content-md5'} ||= '';
80              
81             # just in case someone used this. it's not necessary in this lib.
82 0 0         $interesting_headers{'date'} = '' if $interesting_headers{'x-amz-date'};
83              
84             # if you're using expires for query string auth, then it trumps date
85             # (and x-amz-date)
86 0 0         $interesting_headers{'date'} = $expires if $expires;
87              
88 0           my $buf = "$method\n";
89 0           foreach my $key (sort keys %interesting_headers) {
90 0 0         if ($key =~ /^$AMAZON_HEADER_PREFIX/) {
91 0           $buf .= "$key:$interesting_headers{$key}\n";
92             } else {
93 0           $buf .= "$interesting_headers{$key}\n";
94             }
95             }
96              
97             # don't include anything after the first ? in the resource...
98 0           $path =~ /^([^?]*)/;
99 0           $buf .= "/$1";
100              
101             # ...unless there is an acl or torrent parameter
102 0 0         if ($path =~ /[&?]acl($|=|&)/) {
    0          
    0          
103 0           $buf .= '?acl';
104             } elsif ($path =~ /[&?]torrent($|=|&)/) {
105 0           $buf .= '?torrent';
106             } elsif ($path =~ /[&?]logging($|=|&)/) {
107 0           $buf .= '?logging';
108             }
109              
110 0           return $buf;
111             }
112              
113             # finds the hmac-sha1 hash of the canonical string and the aws secret access key and then
114             # base64 encodes the result (optionally urlencoding after that).
115             sub __encode {
116 0     0     my ($aws_secret_access_key, $str, $urlencode) = @_;
117 0           my $hmac = Digest::HMAC_SHA1->new($aws_secret_access_key);
118 0           $hmac->add($str);
119 0           my $b64 = encode_base64($hmac->digest, '');
120 0 0         if ($urlencode) {
121 0           return __urlencode($b64);
122             } else {
123 0           return $b64;
124             }
125             }
126              
127             sub __urlencode {
128 0     0     my ($unencoded) = @_;
129 0           return uri_escape($unencoded, '^A-Za-z0-9_-');
130             }
131              
132             1;
133             __END__