File Coverage

blib/lib/WebService/S3/Tiny.pm
Criterion Covered Total %
statement 63 63 100.0
branch 6 6 100.0
condition 20 31 64.5
subroutine 16 16 100.0
pod 10 10 100.0
total 115 126 91.2


line stmt bran cond sub pod time code
1             package WebService::S3::Tiny 0.002;
2              
3 3     3   263475 use strict;
  3         24  
  3         95  
4 3     3   15 use warnings;
  3         6  
  3         85  
5              
6 3     3   15 use Carp;
  3         5  
  3         173  
7 3     3   1735 use Digest::SHA qw/hmac_sha256 hmac_sha256_hex sha256_hex/;
  3         10001  
  3         311  
8 3     3   2431 use HTTP::Tiny 0.014;
  3         181402  
  3         3994  
9              
10             my %url_enc = map { chr, sprintf '%%%02X', $_ } 0..255;
11              
12             sub new {
13 3     3 1 373 my ( $class, %args ) = @_;
14              
15 3   50     17 $args{access_key} // croak '"access_key" is required';
16 3   50     12 $args{host} // croak '"host" is required';
17 3   100     20 $args{region} //= 'us-east-1';
18 3   50     12 $args{secret_key} // croak '"secret_key" is requried';
19 3   100     17 $args{service} //= 's3';
20 3   33     33 $args{ua} //= HTTP::Tiny->new;
21              
22 3         368 bless \%args, $class;
23             }
24              
25 1     1 1 11 sub delete_bucket { $_[0]->request( 'DELETE', $_[1], undef, undef, $_[2] ) }
26 1     1 1 1003 sub get_bucket { $_[0]->request( 'GET', $_[1], undef, undef, $_[2], $_[3] ) }
27 1     1 1 802 sub head_bucket { $_[0]->request( 'HEAD', $_[1], undef, undef, $_[2] ) }
28 1     1 1 748 sub put_bucket { $_[0]->request( 'PUT', $_[1], undef, undef, $_[2] ) }
29 1     1 1 697 sub delete_object { $_[0]->request( 'DELETE', $_[1], $_[2], undef, $_[3] ) }
30 1     1 1 704 sub get_object { $_[0]->request( 'GET', $_[1], $_[2], undef, $_[3], $_[4] ) }
31 1     1 1 692 sub head_object { $_[0]->request( 'HEAD', $_[1], $_[2], undef, $_[3] ) }
32 2     2 1 689 sub put_object { $_[0]->request( 'PUT', $_[1], $_[2], $_[3], $_[4] ) }
33              
34             sub request {
35 30     30 1 42482 my ( $self, $method, $bucket, $object, $content, $headers, $query ) = @_;
36              
37 30   50     102 $headers //= {};
38              
39             # Lowercase header keys.
40 30         94 %$headers = map { lc, $headers->{$_} } keys %$headers;
  68         253  
41              
42 30   100     160 $query = HTTP::Tiny->www_form_urlencode( $query // {} );
43              
44 30   66     1133 utf8::encode my $path = _normalize_path( join '/', '', $bucket, $object // () );
45              
46 30         104 $path =~ s|([^A-Za-z0-9\-\._~/])|$url_enc{$1}|g;
47              
48 30         96 $headers->{host} = $self->{host} =~ s|^https?://||r;
49              
50 30         116 my ( $s, $m, $h, $d, $M, $y ) = gmtime;
51              
52 30         393 my $time = $headers->{'x-amz-date'} = sprintf '%d%02d%02dT%02d%02d%02dZ',
53             $y + 1900, $M + 1, $d, $h, $m, $s;
54              
55 30         80 my $date = substr $time, 0, 8;
56              
57             # Prefer user supplied checksums.
58 30   50     389 my $sha = $headers->{'x-amz-content-sha256'} //= sha256_hex $content // '';
      33        
59              
60 30         75 my $creq_headers = '';
61              
62 30         204 for my $k ( sort keys %$headers ) {
63 100         214 my $v = $headers->{$k};
64              
65 100         185 $creq_headers .= "\n$k:";
66              
67 100 100       1100 $creq_headers .= join ',',
68             map s/\s+/ /gr =~ s/^\s+|\s+$//gr,
69             map split(/\n/), ref $v ? @$v : $v;
70             }
71              
72 30         131 my $signed_headers = join ';', sort keys %$headers;
73              
74 30         169 utf8::encode my $creq = "$method\n$path\n$query$creq_headers\n\n$signed_headers\n$sha";
75              
76 30         85 my $cred_scope = "$date/$self->{region}/$self->{service}/aws4_request";
77              
78             my $sig = hmac_sha256_hex(
79             "AWS4-HMAC-SHA256\n$time\n$cred_scope\n" . sha256_hex($creq),
80             hmac_sha256(
81             aws4_request => hmac_sha256(
82             $self->{service} => hmac_sha256(
83             $self->{region},
84 30         1583 hmac_sha256( $date, "AWS4$self->{secret_key}" ),
85             ),
86             ),
87             ),
88             );
89              
90 30         191 $headers->{authorization} = join(
91             ', ',
92             "AWS4-HMAC-SHA256 Credential=$self->{access_key}/$cred_scope",
93             "SignedHeaders=$signed_headers",
94             "Signature=$sig",
95             );
96              
97             # HTTP::Tiny doesn't like us providing our own host header, but we have to
98             # sign it, so let's hope HTTP::Tiny calculates the same value as us :-S
99 30         69 delete $headers->{host};
100              
101             $self->{ua}->request(
102 30         196 $method => "$self->{host}$path?$query",
103             { content => $content, headers => $headers },
104             );
105             }
106              
107             sub _normalize_path {
108 30     30   119 my @old_parts = split m(/), $_[0], -1;
109 30         53 my @new_parts;
110              
111 30         157 for ( 0 .. $#old_parts ) {
112 101         193 my $part = $old_parts[$_];
113              
114 101 100 100     493 if ( $part eq '..' ) {
    100 66        
115 3         6 pop @new_parts;
116             }
117             elsif ( $part ne '.' && ( length $part || $_ == $#old_parts ) ) {
118 34         95 push @new_parts, $part;
119             }
120             }
121              
122 30         160 '/' . join '/', @new_parts;
123             }
124              
125             1;