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.001;
2              
3 3     3   220359 use strict;
  3         23  
  3         83  
4 3     3   16 use warnings;
  3         6  
  3         83  
5              
6 3     3   18 use Carp;
  3         7  
  3         182  
7 3     3   1599 use Digest::SHA qw/hmac_sha256 hmac_sha256_hex sha256_hex/;
  3         9119  
  3         246  
8 3     3   1989 use HTTP::Tiny;
  3         142016  
  3         2969  
9              
10             my %url_enc = map { chr, sprintf '%%%02X', $_ } 0..255;
11              
12             sub new {
13 3     3 1 310 my ( $class, %args ) = @_;
14              
15 3   50     14 $args{access_key} // croak '"access_key" is required';
16 3   50     12 $args{host} // croak '"host" is required';
17 3   100     15 $args{region} //= 'us-east-1';
18 3   50     9 $args{secret_key} // croak '"secret_key" is requried';
19 3   100     15 $args{service} //= 's3';
20 3   33     39 $args{ua} //= HTTP::Tiny->new;
21              
22 3         318 bless \%args, $class;
23             }
24              
25 1     1 1 9 sub delete_bucket { $_[0]->request( 'DELETE', $_[1], undef, undef, $_[2] ) }
26 1     1 1 840 sub get_bucket { $_[0]->request( 'GET', $_[1], undef, undef, $_[2], $_[3] ) }
27 1     1 1 608 sub head_bucket { $_[0]->request( 'HEAD', $_[1], undef, undef, $_[2] ) }
28 1     1 1 578 sub put_bucket { $_[0]->request( 'PUT', $_[1], undef, undef, $_[2] ) }
29 1     1 1 568 sub delete_object { $_[0]->request( 'DELETE', $_[1], $_[2], undef, $_[3] ) }
30 1     1 1 566 sub get_object { $_[0]->request( 'GET', $_[1], $_[2], undef, $_[3], $_[4] ) }
31 1     1 1 576 sub head_object { $_[0]->request( 'HEAD', $_[1], $_[2], undef, $_[3] ) }
32 2     2 1 591 sub put_object { $_[0]->request( 'PUT', $_[1], $_[2], $_[3], $_[4] ) }
33              
34             sub request {
35 30     30 1 31295 my ( $self, $method, $bucket, $object, $content, $headers, $query ) = @_;
36              
37 30   50     72 $headers //= {};
38              
39             # Lowercase header keys.
40 30         66 %$headers = map { lc, $headers->{$_} } keys %$headers;
  68         185  
41              
42 30   100     127 $query = HTTP::Tiny->www_form_urlencode( $query // {} );
43              
44 30   66     872 utf8::encode my $path = _normalize_path( join '/', '', $bucket, $object // () );
45              
46 30         68 $path =~ s|([^A-Za-z0-9\-\._~/])|$url_enc{$1}|g;
47              
48 30         73 $headers->{host} = $self->{host} =~ s|^https?://||r;
49              
50 30         74 my ( $s, $m, $h, $d, $M, $y ) = gmtime;
51              
52 30         292 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         62 my $date = substr $time, 0, 8;
56              
57             # Prefer user supplied checksums.
58 30   50     257 my $sha = $headers->{'x-amz-content-sha256'} //= sha256_hex $content // '';
      33        
59              
60 30         46 my $creq_headers = '';
61              
62 30         120 for my $k ( sort keys %$headers ) {
63 100         147 my $v = $headers->{$k};
64              
65 100         140 $creq_headers .= "\n$k:";
66              
67 100 100       793 $creq_headers .= join ',',
68             map s/\s+/ /gr =~ s/^\s+|\s+$//gr,
69             map split(/\n/), ref $v ? @$v : $v;
70             }
71              
72 30         99 my $signed_headers = join ';', sort keys %$headers;
73              
74 30         120 utf8::encode my $creq = "$method\n$path\n$query$creq_headers\n\n$signed_headers\n$sha";
75              
76 30         64 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         976 hmac_sha256( $date, "AWS4$self->{secret_key}" ),
85             ),
86             ),
87             ),
88             );
89              
90 30         123 $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         56 delete $headers->{host};
100              
101             $self->{ua}->request(
102 30         139 $method => "$self->{host}$path?$query",
103             { content => $content, headers => $headers },
104             );
105             }
106              
107             sub _normalize_path {
108 30     30   94 my @old_parts = split m(/), $_[0], -1;
109 30         42 my @new_parts;
110              
111 30         78 for ( 0 .. $#old_parts ) {
112 101         138 my $part = $old_parts[$_];
113              
114 101 100 100     375 if ( $part eq '..' ) {
    100 66        
115 3         7 pop @new_parts;
116             }
117             elsif ( $part ne '.' && ( length $part || $_ == $#old_parts ) ) {
118 34         70 push @new_parts, $part;
119             }
120             }
121              
122 30         117 '/' . join '/', @new_parts;
123             }
124              
125             1;