File Coverage

blib/lib/Amazon/Signature4/Lite.pm
Criterion Covered Total %
statement 76 76 100.0
branch 18 18 100.0
condition 17 24 70.8
subroutine 10 10 100.0
pod 3 3 100.0
total 124 131 94.6


line stmt bran cond sub pod time code
1             package Amazon::Signature4::Lite;
2              
3 2     2   343539 use strict;
  2         3  
  2         68  
4 2     2   7 use warnings;
  2         3  
  2         165  
5              
6 2     2   925 use Digest::SHA qw(sha256_hex hmac_sha256 hmac_sha256_hex);
  2         7346  
  2         256  
7 2     2   1238 use MIME::Base64 qw(encode_base64);
  2         1902  
  2         134  
8 2     2   859 use POSIX qw(strftime);
  2         15434  
  2         9  
9 2     2   3822 use URI::Escape qw(uri_escape_utf8);
  2         3823  
  2         2785  
10              
11             our $VERSION = '1.0.0';
12              
13             my @SERVICE_URL_PATTERNS = (
14             qr/(s3)[.]amazonaws[.]com\z/xsm,
15             qr/(s3)[.]([^.]+)[.]amazonaws[.]com\z/xsm,
16             qr/(s3)[.][^.]+[.]([^.]+)[.]amazonaws[.]com\z/xsm,
17             qr/(s3)[-][^.]+[.].+[.]([^.]+)[.]amazonaws[.]com\z/xsm,
18             qr/^([[:alnum:]-]+)[.]([^.]+)[.]amazonaws[.]com\z/xsm, # service.region.amazonaws.com
19             qr/^([[:alnum:]-]+)[.]amazonaws[.]com\z/xsm, # service.amazonaws.com (no region)
20             );
21              
22             ########################################################################
23             sub new {
24             ########################################################################
25 15     15 1 244422 my ( $class, %args ) = @_;
26              
27 15 100       60 die "access_key is required\n" if !$args{access_key};
28 14 100       35 die "secret_key is required\n" if !$args{secret_key};
29 13 100       39 die "region is required\n" if !$args{region};
30              
31             return bless {
32             access_key => $args{access_key},
33             secret_key => $args{secret_key},
34             session_token => $args{session_token},
35             region => $args{region},
36 12   100     95 service => $args{service} // 's3',
37             }, $class;
38             }
39              
40             ########################################################################
41             sub parse_service_url {
42             ########################################################################
43 6     6 1 12194 my ( $class_or_self, %args ) = @_;
44              
45 6         20 my ( $host, $service ) = @args{qw(host service)};
46 6         16 my ( $region, $default_region ) = @args{qw(region default_region)};
47              
48 6 100 66     29 if ( !$service || !$region ) {
49 5         15 for my $pattern (@SERVICE_URL_PATTERNS) {
50 16 100       99 if ( $host =~ $pattern ) {
51 4         14 $service = $1;
52 4   66     26 $region = $2 || $region || $default_region;
53 4         10 last;
54             }
55             }
56             }
57              
58 6   66     22 $region ||= $default_region;
59              
60 6         35 return ( $host, $service, $region );
61             }
62              
63             ########################################################################
64             sub sign {
65             ########################################################################
66 10     10 1 1127 my ( $self, %args ) = @_;
67              
68 10   50     40 my $method = uc( $args{method} // 'GET' );
69 10 100       33 my $url = $args{url} or die "url is required\n";
70 9   100     36 my $headers = $args{headers} // {};
71 9   100     72 my $payload = $args{payload} // q{};
72              
73             # parse url into components
74 9         88 my ( $scheme, $host, $path, $query ) = $url =~ m{\A(https?)://([^/?#]+)([^?#]*)(?:[?]([^#]*))?\z}xsm;
75              
76 9   50     26 $path //= '/';
77 9   100     41 $query //= q{};
78              
79             # timestamps
80 9   33     25 my $now = $args{time} // time;
81 9         352 my $datetime = strftime( '%Y%m%dT%H%M%SZ', gmtime($now) );
82 9         50 my ($date) = $datetime =~ /\A(\d{8})/xsm;
83              
84             # payload hash
85 9 100       122 my $payload_hash = sha256_hex( ref $payload ? ${$payload} : $payload );
  1         10  
86              
87             # canonical headers - must include host and x-amz-date at minimum
88             my %sign_headers = (
89 9         17 %{$headers},
  9         48  
90             'host' => $host,
91             'x-amz-date' => $datetime,
92             'x-amz-content-sha256' => $payload_hash,
93             );
94              
95             $sign_headers{'x-amz-security-token'} = $self->{session_token}
96 9 100       30 if $self->{session_token};
97              
98             # sort and build canonical headers string
99 9         48 my @header_keys = sort { lc($a) cmp lc($b) } keys %sign_headers;
  29         75  
100 9         21 my $canon_headers = join q{}, map { lc($_) . ':' . $sign_headers{$_} . "\n" } @header_keys;
  30         80  
101 9         29 my $signed_headers = join ';', map { lc($_) } @header_keys;
  30         53  
102              
103             # canonical query string - sort by encoded key then encoded value
104 9         38 my $canon_query = q{};
105 9 100       65 if ($query) {
106             my @pairs = map {
107 3         25 join '=', map { uri_escape_utf8($_) } split /=/xsm, $_, 2
  6         45  
108             }
109 1         4 sort { $a cmp $b }
  3         5  
110             split /&/xsm, $query;
111 1         14 $canon_query = join '&', @pairs;
112             }
113              
114             # canonical request
115 9         25 my $canon_request = join "\n", $method, _encode_path($path), $canon_query, $canon_headers, $signed_headers, $payload_hash;
116              
117             # credential scope
118 9         149 my $service = $self->{service};
119 9         15 my $region = $self->{region};
120 9         20 my $scope = "$date/$region/$service/aws4_request";
121              
122             # string to sign
123 9         77 my $string_to_sign = join "\n", 'AWS4-HMAC-SHA256', $datetime, $scope, sha256_hex($canon_request);
124              
125             # signing key - HMAC chain
126 9         17 my $signing_key = hmac_sha256( 'aws4_request',
127 9         209 hmac_sha256( $service, hmac_sha256( $region, hmac_sha256( $date, "AWS4${\$self->{secret_key}}" ) ) ) );
128              
129             # signature
130 9         76 my $signature = hmac_sha256_hex( $string_to_sign, $signing_key );
131              
132             # authorization header
133             my $authorization = sprintf
134             'AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s',
135 9         23 $self->{access_key}, $scope, $signed_headers, $signature;
136              
137             # return merged headers ready for HTTP::Tiny
138 9         79 return { %sign_headers, 'Authorization' => $authorization, };
139             }
140              
141             ########################################################################
142             sub _encode_path {
143             ########################################################################
144 16     16   8876 my ($path) = @_;
145              
146             # encode each segment individually, preserving slashes
147 16         56 return join '/', map { uri_escape_utf8($_) } split m{/}xsm, $path, -1;
  47         645  
148             }
149              
150             1;
151              
152             __END__