File Coverage

blib/lib/Amazon/S3/Thin/Signer/V4.pm
Criterion Covered Total %
statement 66 66 100.0
branch 6 6 100.0
condition 8 9 88.8
subroutine 12 12 100.0
pod 2 3 66.6
total 94 96 97.9


line stmt bran cond sub pod time code
1             package Amazon::S3::Thin::Signer::V4;
2              
3             =head1 NAME
4              
5             Amazon::S3::Thin::Signer::V4 - AWS Version 4 Signer
6              
7             =head1 SYNOPSIS
8              
9             # create a client object
10             my $s3client = Amazon::S3::Thin->new({
11             aws_access_key_id => $aws_access_key_id,
12             aws_secret_access_key => $secret_access_key,
13             });
14              
15             # create a signer
16             my $signer = Amazon::S3::Thin::Signer::V4->new($s3client);
17              
18             # create a request
19             my $request = HTTP::Request->new(...);
20              
21             # sign the request using the client keys
22             $signer->sign($request);
23              
24             =head1 DESCRIPTION
25              
26             This module creates objects that can sign AWS requests using signature version
27             4, as implemented by the L module.
28              
29             =cut
30              
31 8     8   1511 use strict;
  8         20  
  8         235  
32 8     8   35 use warnings;
  8         12  
  8         205  
33 8     8   3211 use AWS::Signature4;
  8         156635  
  8         239  
34 8     8   55 use Digest::SHA ();
  8         17  
  8         103  
35 8     8   640 use JSON::PP ();
  8         12731  
  8         106  
36 8     8   2083 use MIME::Base64 ();
  8         2803  
  8         181  
37 8     8   53 use POSIX 'strftime';
  8         30  
  8         56  
38              
39             sub new {
40 16     16 0 9256 my ($class, $credentials, $region) = @_;
41 16         47 my $self = {
42             credentials => $credentials,
43             region => $region,
44             };
45 16         74 bless $self, $class;
46             }
47              
48             =head1 METHODS
49              
50             =head2 sign($request)
51              
52             Signs supplied L object, adding required AWS headers.
53              
54             =cut
55              
56             sub sign
57             {
58 18     18 1 49 my ($self, $request) = @_;
59 18         41 my $signer = $self->signer;
60 18 100       354 if (defined $self->{credentials}->session_token) {
61 1         4 $request->header('X-Amz-Security-Token', $self->{credentials}->session_token);
62             }
63 18         120 my $digest = Digest::SHA::sha256_hex($request->content);
64 18         370 $request->header('X-Amz-Content-SHA256', $digest);
65 18         1071 $signer->sign($request, $self->{region}, $digest);
66 18         16653 $request;
67             }
68              
69             =head2 signer
70              
71             Returns an L object for signing requests
72              
73             =cut
74              
75             sub signer
76             {
77 25     25 1 39 my $self = shift;
78             AWS::Signature4->new(
79             -access_key => $self->{credentials}->access_key_id,
80             -secret_key => $self->{credentials}->secret_access_key,
81 25         127 );
82             }
83              
84             # This method is written referencing these botocore's implementations:
85             # https://github.com/boto/botocore/blob/00c4cadcf0996ef77a3a01b158f15c8fced9909b/botocore/signers.py#L602-L714
86             # https://github.com/boto/botocore/blob/00c4cadcf0996ef77a3a01b158f15c8fced9909b/botocore/signers.py#L459-L528
87             # https://github.com/boto/botocore/blob/00c4cadcf0996ef77a3a01b158f15c8fced9909b/botocore/auth.py#L585-L628
88             sub _generate_presigned_post {
89 6     6   14 my ($self, $bucket, $key, $fields, $conditions, $expires_in) = @_;
90              
91             # $fields is arrayref of key/value pairs. The order of the fields is important because AWS says "please check the order of the fields"...
92 6   100     21 $fields ||= [];
93 6   100     20 $conditions ||= [];
94 6   100     18 $expires_in ||= 3600;
95              
96 6         10 my $t = time;
97 6         327 my $datetime = strftime('%Y%m%dT%H%M%SZ', gmtime($t));
98 6         144 my $expiration = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($t + $expires_in));
99              
100 6         22 my $signer = $self->signer;
101 6         133 my ($date) = $datetime =~ /^(\d+)T/;
102 6         19 my $credential = $signer->access_key . '/' . $date . '/' . $self->{region} . '/s3/aws4_request';
103              
104 6         52 push @$conditions, {bucket => $bucket};
105              
106 6         13 push @$fields, key => $key;
107 6 100       19 if ($key =~ /\$\{filename\}$/) {
108 1         4 push @$conditions, ['starts-with' => '$key', substr($key, 0, -11)];
109             } else {
110 5         10 push @$conditions, {key => $key};
111             }
112              
113 6         10 push @$fields, 'x-amz-algorithm' => 'AWS4-HMAC-SHA256';
114 6         13 push @$fields, 'x-amz-credential' => $credential;
115 6         9 push @$fields, 'x-amz-date' => $datetime;
116              
117 6         14 push @$conditions, {'x-amz-algorithm' => 'AWS4-HMAC-SHA256'};
118 6         13 push @$conditions, {'x-amz-credential' => $credential};
119 6         14 push @$conditions, {'x-amz-date' => $datetime};
120              
121 6         19 my $session_token = $self->{credentials}->session_token;
122 6 100       12 if (defined $session_token) {
123 1         3 push @$fields, 'x-amz-security-token' => $session_token;
124 1         4 push @$conditions, {'x-amz-security-token' => $session_token};
125             }
126              
127 6         32 my $policy = $self->_encode_policy({
128             expiration => $expiration,
129             conditions => $conditions,
130             });
131 6         2625 push @$fields, policy => $policy;
132              
133             my $signing_key = $signer->signing_key(
134             $signer->secret_key,
135             's3',
136             $self->{region},
137 6         19 $date,
138             );
139 6         273 push @$fields, 'x-amz-signature' => Digest::SHA::hmac_sha256_hex($policy, $signing_key);
140              
141 6         103 return $fields;
142             }
143              
144             my $_JSON;
145             sub _encode_policy {
146 6     6   11 my $self = shift;
147 6   66     33 return MIME::Base64::encode_base64(
148             ($_JSON ||= JSON::PP->new->utf8->canonical)->encode(@_),
149             ''
150             );
151             }
152              
153             1;
154              
155             =head1 LICENSE
156              
157             Copyright (C) 2016, Robert Showalter
158              
159             This library is free software; you can redistribute it and/or modify it under
160             the same terms as Perl itself.
161              
162             =head1 AUTHOR
163              
164             Robert Showalter
165              
166             =head1 SEE ALSO
167              
168             L