File Coverage

blib/lib/Amazon/CloudFront/Thin.pm
Criterion Covered Total %
statement 91 93 97.8
branch 10 14 71.4
condition 5 11 45.4
subroutine 21 21 100.0
pod 3 3 100.0
total 130 142 91.5


line stmt bran cond sub pod time code
1             package Amazon::CloudFront::Thin;
2 5     5   151434 use strict;
  5         14  
  5         132  
3 5     5   22 use warnings;
  5         8  
  5         131  
4 5     5   744312 use URI ();
  5         30456  
  5         94  
5 5     5   30 use URI::Escape ();
  5         8  
  5         72  
6 5     5   23 use Carp ();
  5         8  
  5         1208  
7 5     5   24815 use HTTP::Headers ();
  5         36395  
  5         129  
8 5     5   40770 use HTTP::Date ();
  5         24597  
  5         138  
9 5     5   3488 use HTTP::Request ();
  5         61052  
  5         122  
10 5     5   3530 use Digest::SHA ();
  5         14202  
  5         5528  
11              
12             our $VERSION = '0.03';
13              
14             sub new {
15 13     13 1 12446 my ($class, @extra) = @_;
16 13         23 my $args;
17 13         27 my $self = {};
18              
19 13 100       50 if (@extra == 1) {
20 5 50       81 Carp::croak 'please provide a hash or hash reference to new()'
21             unless ref $extra[0] eq 'HASH';
22 5         18 $args = $extra[0];
23             }
24             else {
25 8 50       29 Carp::croak 'please provide a hash or hash reference to new()'
26             unless @extra % 2 == 0;
27 8         28 $args = {@extra};
28             }
29              
30 13         37 foreach my $key (qw(aws_access_key_id aws_secret_access_key distribution_id)) {
31 29 100       81 if (exists $args->{$key}) {
32 21         73 $self->{$key} = $args->{$key};
33             }
34             else {
35 8         1458 Carp::croak "argument '$key' missing on call to new()";
36             }
37             }
38 5         13 bless $self, $class;
39 5   66     41 my $ua = $args->{ua} || _default_ua();
40 5         35 $self->ua($ua);
41              
42 5         47 return $self;
43             }
44              
45             sub _default_ua {
46 3     3   2140 require LWP::UserAgent;
47 3         1581069 my $ua = LWP::UserAgent->new(
48             keep_alive => 10,
49             # requests_redirectable => [qw(GET HEAD DELETE PUT)]
50             );
51 3         557807 $ua->timeout(10);
52 3         86 $ua->env_proxy;
53 3         87056 return $ua;
54             }
55              
56             sub ua {
57 10     10 1 33 my ($self, $ua) = @_;
58 10 100       65 $self->{_ua} = $ua if ($ua);
59 10         104 return $self->{_ua};
60             }
61              
62             sub create_invalidation {
63 1     1 1 9 my ($self, @paths) = @_;
64 1 50 33     13 if (@paths == 1 && ref $paths[0] && ref $paths[0] eq 'ARRAY') {
      33        
65 0         0 @paths = @{$paths[0]};
  0         0  
66             }
67              
68 1         3 my $time = time;
69              
70             my $url = URI->new(
71             'https://cloudfront.amazonaws.com/2015-04-17/distribution/'
72 1         12 . $self->{distribution_id} . '/invalidation'
73             );
74              
75 1         9354 my $content = _create_xml_payload(\@paths, $time);
76              
77             # Amazon unfortunately does not comply with RFC 1123 for the
78             # 'date' header, requiring instead that it gets written in
79             # ISO-8601 format. Since HTTP::Headers does the right thing
80             # for date(), we set the ISO-8601 date in "X-Amz-Date" instead.
81 1         6 my ($formatted_date, $formatted_time) = _format_date($time);
82 1         10 my $http_headers = HTTP::Headers->new(
83             'Content-Length' => length $content,
84             'Content-Type' => 'text/xml',
85             'Host' => $url->host,
86             'X-Amz-Date' => $formatted_date . 'T' . $formatted_time . 'Z',
87             );
88              
89             $http_headers->header(
90             Authorization => 'AWS4-HMAC-SHA256 Credential='
91             . $self->{aws_access_key_id} . '/' . _cloudfront_scope($formatted_date)
92             . ', SignedHeaders=' . _signed_headers($http_headers)
93             . ', Signature='
94             . _calculate_signature(
95             $self->{aws_secret_access_key},
96 1         311 $url,
97             $http_headers,
98             $content
99             )
100             );
101              
102 1         42 my $request = HTTP::Request->new('POST', $url, $http_headers, $content);
103 1         206 return $self->ua->request($request);
104             }
105              
106             sub _cloudfront_scope {
107 3     3   8 my ($date) = @_;
108 3         37 return sprintf("%s/us-east-1/cloudfront/aws4_request", $date);
109             }
110              
111             sub _format_date {
112 6     6   1508 my ($time) = @_;
113 6         37 my @date = gmtime $time;
114 6         14 $date[5] += 1900; # fix the year
115 6         15 $date[4] += 1; # fix the month
116              
117             return (
118 6         52 sprintf('%d%02d%02d', @date[5,4,3]), # YYYYMMDD
119             sprintf('%02d%02d%02d', @date[2,1,0]) # hhmmss
120             );
121             }
122              
123             sub _calculate_signature {
124 1     1   46 my ($aws_secret_access_key, $url, $headers, $content) = @_;
125              
126 1         4 my $canonical_request = _create_canonical_request($url, $headers, $content);
127 1         62 my $string_to_sign = _create_string_to_sign($headers, $canonical_request);
128              
129 1         7 my ($date) = _format_date(
130             HTTP::Date::str2time($headers->header('X-Amz-Date'))
131             );
132 1         6 return _create_signature($aws_secret_access_key, $string_to_sign, $date);
133             }
134              
135             sub _create_canonical_request {
136 2     2   373 my ($url, $headers, $content) = @_;
137              
138             # http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
139 2         12 my @sorted_header_names = sort $headers->header_field_names();
140             return
141             "POST\n"
142             . ($url->path || '/') . "\n\n"
143             . join( "\n", map {
144 2   50     89 lc($_) . ':' . $headers->header($_)
  8         208  
145             } @sorted_header_names
146             ) . "\n\n"
147             . _signed_headers($headers) . "\n"
148             . Digest::SHA::sha256_hex($content)
149             ;
150             }
151              
152             sub _signed_headers {
153 3     3   65 my ($headers) = @_;
154 3         12 return join(';' => map lc, sort $headers->header_field_names());
155             }
156              
157             sub _create_string_to_sign {
158 2     2   1363 my ($headers, $canonical_request) = @_;
159              
160 2         11 my ($formatted_date, $formatted_time) = _format_date(
161             HTTP::Date::str2time($headers->header('X-Amz-Date'))
162             );
163              
164             return
165 2         15 "AWS4-HMAC-SHA256\n"
166             . $formatted_date . 'T' . $formatted_time . "Z\n"
167             . _cloudfront_scope($formatted_date) . "\n"
168             . Digest::SHA::sha256_hex($canonical_request)
169             ;
170             }
171              
172             sub _create_signature {
173 2     2   706 my ($aws_secret_access_key, $string_to_sign, $date) = @_;
174              
175 2         100 return Digest::SHA::hmac_sha256_hex(
176             $string_to_sign, Digest::SHA::hmac_sha256(
177             'aws4_request', Digest::SHA::hmac_sha256(
178             'cloudfront', Digest::SHA::hmac_sha256(
179             'us-east-1', Digest::SHA::hmac_sha256($date, 'AWS4' . $aws_secret_access_key)
180             )
181             )
182             )
183             );
184             }
185              
186             sub _create_xml_payload {
187 2     2   2591973 my ($paths, $identifier) = @_;
188 2         6 my $total_paths = scalar @$paths;
189 2         6 my $path_content;
190 2         9 foreach my $path (@$paths) {
191             # leading '/' is required:
192             # http://docs.aws.amazon.com/AmazonCloudFront/latest/APIReference/InvalidationBatchDatatype.html
193 3 50       20 $path = '/' . $path unless index($path, '/') == 0;
194             # we wrap paths on CDATA so we don't have to escape them
195 3         14 $path_content .= ''
196             }
197 2         15 return qq{$total_paths$path_content$identifier};
198             }
199              
200             42;
201             __END__