File Coverage

lib/App/MtAws/GlacierRequest.pm
Criterion Covered Total %
statement 209 342 61.1
branch 75 184 40.7
condition 27 47 57.4
subroutine 25 37 67.5
pod 0 17 0.0
total 336 627 53.5


line stmt bran cond sub pod time code
1             # mt-aws-glacier - Amazon Glacier sync client
2             # Copyright (C) 2012-2014 Victor Efimov
3             # http://mt-aws.com (also http://vs-dev.com) vs@vs-dev.com
4             # License: GPLv3
5             #
6             # This file is part of "mt-aws-glacier"
7             #
8             # mt-aws-glacier is free software: you can redistribute it and/or modify
9             # it under the terms of the GNU General Public License as published by
10             # the Free Software Foundation, either version 3 of the License, or
11             # (at your option) any later version.
12             #
13             # mt-aws-glacier is distributed in the hope that it will be useful,
14             # but WITHOUT ANY WARRANTY; without even the implied warranty of
15             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16             # GNU General Public License for more details.
17             #
18             # You should have received a copy of the GNU General Public License
19             # along with this program. If not, see <http://www.gnu.org/licenses/>.
20              
21             package App::MtAws::GlacierRequest;
22              
23             our $VERSION = '1.114_2';
24              
25 18     18   8637 use strict;
  18         33  
  18         490  
26 18     18   58 use warnings;
  18         21  
  18         476  
27 18     18   66 use utf8;
  18         25  
  18         99  
28 18     18   1857 use POSIX;
  18         10404  
  18         129  
29 18     18   31643 use LWP 5.803;
  18         1408  
  18         425  
30 18     18   80 use LWP::UserAgent;
  18         20  
  18         354  
31 18     18   66 use HTTP::Request;
  18         19  
  18         1423  
32 18     18   8611 use Digest::SHA qw/hmac_sha256 hmac_sha256_hex sha256_hex/;
  18         45358  
  18         1427  
33 18     18   7367 use App::MtAws::MetaData;
  18         50  
  18         1219  
34 18     18   112 use App::MtAws::Utils;
  18         39  
  18         2303  
35 18     18   88 use App::MtAws::Exceptions;
  18         30  
  18         1190  
36 18     18   10658 use App::MtAws::HttpSegmentWriter;
  18         55  
  18         728  
37 18     18   8992 use App::MtAws::SHAHash qw/large_sha256_hex/;
  18         35  
  18         755  
38 18     18   76 use Carp;
  18         21  
  18         70188  
39              
40             sub new
41             {
42 69     69 0 110658 my ($class, $options) = @_;
43 69         142 my $self = {};
44 69         126 bless $self, $class;
45              
46 69   66     1274 defined($self->{$_} = $options->{$_})||confess $_ for (qw/region key secret protocol timeout/);
47 65   66     353 defined($options->{$_}) and $self->{$_} = $options->{$_} for (qw/vault token/); # TODO: validate vault later
48              
49 65 100       492 confess unless $self->{protocol} =~ /^https?$/; # we check external data here, even if it's verified in the beginning, especially if it's used to construct URL
50 64   50     373 $self->{service} ||= 'glacier';
51 64         153 $self->{account_id} = '-';
52 64         247 $self->{host} = "$self->{service}.$self->{region}.amazonaws.com";
53              
54 64         152 $self->{headers} = [];
55              
56 64         240 $self->add_header('Host', $self->{host});
57 64 50       258 $self->add_header('x-amz-glacier-version', '2012-06-01') if $self->{service} eq 'glacier';
58 64 100       167 $self->add_header('x-amz-security-token', $self->{token}) if defined $self->{token};
59              
60 64         147 return $self;
61             }
62              
63             sub add_header
64             {
65 131     131 0 193 my ($self, $name, $value) = @_;
66 131         124 push @{$self->{headers}}, { name => $name, value => $value};
  131         467  
67             }
68              
69             sub create_multipart_upload
70             {
71 1     1 0 10 my ($self, $partsize, $relfilename, $mtime) = @_;
72              
73 1 50       4 defined($relfilename)||confess;
74 1 50       3 defined($mtime)||confess;
75 1 50       3 $partsize||confess;
76              
77 1         4 $self->{url} = "/$self->{account_id}/vaults/$self->{vault}/multipart-uploads";
78 1         1 $self->{method} = 'POST';
79              
80 1         4 $self->add_header('x-amz-part-size', $partsize);
81              
82             # currently meat_encode only returns undef if filename is too big
83 1 50       7 defined($self->{description} = App::MtAws::MetaData::meta_encode($relfilename, $mtime)) or
84             die exception 'file_name_too_big' =>
85             "Either relative filename %string filename% is too big to store in Amazon Glacier metadata. ".
86             "(Limit is about 700 ASCII characters or 350 2-byte UTF-8 characters) or file modification time %string mtime% out of range".
87             "(Only years from 1000 to 9999 are supported)",
88             filename => $relfilename, mtime => $mtime; # TODO: more clear error
89 0         0 $self->add_header('x-amz-archive-description', $self->{description});
90              
91 0         0 my $resp = $self->perform_lwp();
92 0 0       0 return $resp ? $resp->header('x-amz-multipart-upload-id') : undef;
93             }
94              
95             sub upload_part
96             {
97 0     0 0 0 my ($self, $uploadid, $dataref, $offset, $part_final_hash) = @_;
98              
99 0 0       0 $uploadid||confess;
100 0 0       0 ($self->{dataref} = $dataref)||confess;
101 0 0       0 defined($offset)||confess;
102 0 0       0 ($self->{part_final_hash} = $part_final_hash)||confess;
103              
104 0         0 $self->_calc_data_hash;
105              
106 0         0 $self->{url} = "/$self->{account_id}/vaults/$self->{vault}/multipart-uploads/$uploadid";
107 0         0 $self->{method} = 'PUT';
108 0         0 $self->add_header('Content-Type', 'application/octet-stream');
109 0         0 $self->add_header('Content-Length', length(${$self->{dataref}}));
  0         0  
110 0         0 $self->add_header('x-amz-content-sha256', $self->{data_sha256});
111 0         0 $self->add_header('x-amz-sha256-tree-hash', $self->{part_final_hash});
112 0         0 my ($start, $end) = ($offset, $offset+length(${$self->{dataref}})-1 );
  0         0  
113 0         0 $self->add_header('Content-Range', "bytes ${start}-${end}/*");
114              
115 0         0 my $resp = $self->perform_lwp();
116 0 0       0 return $resp ? 1 : undef;
117             }
118              
119              
120             sub finish_multipart_upload
121             {
122 0     0 0 0 my ($self, $uploadid, $size, $treehash) = @_;
123              
124 0 0       0 $uploadid||confess;
125 0 0       0 $size||confess;
126 0 0       0 $treehash||confess;
127              
128 0         0 $self->{url} = "/$self->{account_id}/vaults/$self->{vault}/multipart-uploads/$uploadid";
129 0         0 $self->{method} = 'POST';
130 0         0 $self->add_header('x-amz-sha256-tree-hash', $treehash);
131 0         0 $self->add_header('x-amz-archive-size', $size);
132              
133 0         0 my $resp = $self->perform_lwp();
134 0 0       0 return $resp ? $resp->header('x-amz-archive-id') : undef;
135             }
136              
137              
138             sub delete_archive
139             {
140 0     0 0 0 my ($self, $archive_id) = @_;
141              
142 0 0       0 $archive_id||confess;
143              
144 0         0 $self->{url} = "/$self->{account_id}/vaults/$self->{vault}/archives/$archive_id";
145 0         0 $self->{method} = 'DELETE';
146              
147 0         0 my $resp = $self->perform_lwp();
148 0 0       0 return $resp ? 1 : undef;
149             }
150              
151              
152             sub retrieve_archive
153             {
154 0     0 0 0 my ($self, $archive_id) = @_;
155              
156 0 0       0 $archive_id||confess;
157              
158 0         0 $self->add_header('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8');
159 0         0 $self->{url} = "/$self->{account_id}/vaults/$self->{vault}/jobs";
160 0         0 $self->{method} = 'POST';
161              
162             # add "SNSTopic": "sometopic"
163             # no Test::Tabs
164 0         0 my $body = <<"END";
165             {
166             "Type": "archive-retrieval",
167             "ArchiveId": "$archive_id"
168             }
169             END
170              
171             # use Test::Tabs
172 0         0 $self->{dataref} = \$body;
173              
174 0         0 my $resp = $self->perform_lwp();
175 0 0       0 return $resp ? $resp->header('x-amz-job-id') : undef;
176             }
177              
178             sub retrieve_inventory
179             {
180 0     0 0 0 my ($self, $format) = @_;
181              
182 0 0       0 $format or confess;
183              
184 0 0       0 if ($format eq 'json') {
    0          
185 0         0 $format = 'JSON';
186             } elsif ($format eq 'csv') {
187 0         0 $format = 'CSV';
188             } else {
189 0         0 confess "unknown inventory format $format";
190             }
191              
192 0         0 $self->add_header('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8');
193 0         0 $self->{url} = "/$self->{account_id}/vaults/$self->{vault}/jobs";
194 0         0 $self->{method} = 'POST';
195              
196 0         0 my $job_meta = App::MtAws::MetaData::meta_job_encode(META_JOB_TYPE_FULL);
197              
198             # add "SNSTopic": "sometopic"
199             # no Test::Tabs
200 0         0 my $body = <<"END";
201             {
202             "Type": "inventory-retrieval",
203             "Description": "$job_meta",
204             "Format": "$format"
205             }
206             END
207             # use Test::Tabs
208 0         0 $self->{dataref} = \$body;
209              
210 0         0 my $resp = $self->perform_lwp();
211 0 0       0 return $resp ? $resp->header('x-amz-job-id') : undef;
212             }
213              
214             sub retrieval_fetch_job
215             {
216 0     0 0 0 my ($self, $marker) = @_;
217              
218 0         0 $self->{url} = "/$self->{account_id}/vaults/$self->{vault}/jobs";
219              
220 0         0 $self->{params} = { completed => 'true' };
221 0 0       0 $self->{params}->{marker} = $marker if defined($marker);
222              
223 0         0 $self->{method} = 'GET';
224              
225 0         0 my $resp = $self->perform_lwp();
226 0         0 return $resp->decoded_content; # TODO: return reference?
227             }
228              
229              
230             # TODO: rename
231             sub retrieval_download_job
232             {
233 1     1 0 2 my ($self, $jobid, $relfilename, $tempfile, $size, $journal_treehash) = @_;
234              
235 1 50       2 $journal_treehash||confess;
236 1 50       7 $jobid||confess;
237 1 50       5 defined($tempfile)||confess;
238 1 50       3 defined($relfilename)||confess;
239 1 50       2 $size or confess "no size";
240              
241 1         5 $self->{url} = "/$self->{account_id}/vaults/$self->{vault}/jobs/$jobid/output";
242              
243 1         2 $self->{expected_size} = $size;
244 1         9 $self->{writer} = App::MtAws::HttpFileWriter->new(tempfile => $tempfile);
245              
246 1         2 $self->{method} = 'GET';
247              
248 1         2 my $resp = $self->perform_lwp();
249 1 50       8 my $reported_th = $resp->header('x-amz-sha256-tree-hash') or confess;
250              
251 1         35 $self->{writer}->treehash->calc_tree();
252 1         3 my $th = $self->{writer}->treehash->get_final_hash();
253              
254 1 50       6 $reported_th eq $th or
255             die exception 'treehash_mismatch_full' =>
256             'TreeHash for received file %string filename% (full file) does not match. '.
257             'TreeHash reported by server: %reported%, Calculated TreeHash: %calculated%, TreeHash from Journal: %journal_treehash%',
258             calculated => $th, reported => $reported_th, journal_treehash => $journal_treehash, filename => $relfilename;
259              
260 1 50       4 $reported_th eq $journal_treehash or
261             die exception 'treehash_mismatch_journal' =>
262             'TreeHash for received file %string filename% (full file) does not match TreeHash in journal. '.
263             'TreeHash reported by server: %reported%, Calculated TreeHash: %calculated%, TreeHash from Journal: %journal_treehash%',
264             calculated => $th, reported => $reported_th, journal_treehash => $journal_treehash, filename => $relfilename;
265              
266 1 50       4 return $resp ? 1 : undef;
267             }
268              
269             sub segment_download_job
270             {
271 0     0 0 0 my ($self, $jobid, $tempfile, $filename, $position, $size) = @_;
272              
273 0 0       0 $jobid||confess;
274 0 0       0 defined($position) or confess "no position";
275 0 0       0 $size or confess "no size";
276 0 0       0 defined($filename)||confess;
277              
278 0         0 $self->{url} = "/$self->{account_id}/vaults/$self->{vault}/jobs/$jobid/output";
279              
280 0         0 $self->{expected_size} = $size;
281 0         0 $self->{writer} = App::MtAws::HttpSegmentWriter->new(tempfile => $tempfile, position => $position, filename => $filename);
282              
283 0         0 $self->{method} = 'GET';
284 0         0 my $end_position = $position + $size - 1;
285 0         0 $self->add_header('Range', "bytes=$position-$end_position");
286              
287 0         0 my $resp = $self->perform_lwp();
288 0 0 0     0 $resp && $resp->code == 206 or confess;
289              
290 0 0       0 my $reported_th = $resp->header('x-amz-sha256-tree-hash') or confess;
291 0         0 $self->{writer}->treehash->calc_tree();
292 0         0 my $th = $self->{writer}->treehash->get_final_hash();
293              
294 0 0       0 $reported_th eq $th or
295             die exception 'treehash_mismatch_segment' =>
296             'TreeHash for received segment of file %string filename% (position %position%, size %size%) does not match. '.
297             'TreeHash reported by server %reported%, Calculated TreeHash %calculated%',
298             calculated => $th, reported => $reported_th, filename => $filename, position => $position, size => $size;
299             # TODO: better report relative filename
300              
301 0         0 my ($start, $end, $len) = $resp->header('Content-Range') =~ m!bytes\s+(\d+)\-(\d+)\/(\d+)!;
302              
303 0 0 0     0 confess unless defined($start) && defined($end) && $len;
      0        
304 0 0       0 confess unless $end >= $start;
305 0 0       0 confess unless $position == $start;
306 0 0       0 confess unless $end_position == $end;
307              
308 0 0       0 return $resp ? 1 : undef; # $resp->decoded_content is undefined here as content_file used
309             }
310              
311             sub retrieval_download_to_memory
312             {
313 0     0 0 0 my ($self, $jobid) = @_;
314              
315 0 0       0 $jobid||confess;
316              
317 0         0 $self->{url} = "/$self->{account_id}/vaults/$self->{vault}/jobs/$jobid/output";
318 0         0 $self->{method} = 'GET';
319              
320 0         0 my $resp = $self->perform_lwp();
321              
322 0 0       0 $resp or confess;
323              
324 0         0 my $itype = do {
325 0         0 my $ct = $resp->content_type;
326 0 0       0 if ($ct eq 'text/csv') {
    0          
327 0         0 INVENTORY_TYPE_CSV
328             } elsif ($ct eq 'application/json') {
329 0         0 INVENTORY_TYPE_JSON
330             } else {
331 0         0 confess "Unknown mime-type $ct";
332             }
333             };
334 0         0 return ($resp->content, $itype);
335             }
336              
337             sub create_vault
338             {
339 0     0 0 0 my ($self, $vault_name) = @_;
340              
341 0 0       0 confess unless defined($vault_name);
342              
343 0         0 $self->{url} = "/$self->{account_id}/vaults/$vault_name";
344 0         0 $self->{method} = 'PUT';
345              
346 0         0 my $resp = $self->perform_lwp();
347 0 0       0 return $resp ? $resp->header('x-amzn-RequestId') : undef;
348             }
349              
350             sub delete_vault
351             {
352 0     0 0 0 my ($self, $vault_name) = @_;
353              
354 0 0       0 confess unless defined($vault_name);
355              
356 0         0 $self->{url} = "/$self->{account_id}/vaults/$vault_name";
357 0         0 $self->{method} = 'DELETE';
358              
359 0         0 my $resp = $self->perform_lwp();
360 0 0       0 return $resp ? $resp->header('x-amzn-RequestId') : undef;
361             }
362              
363              
364              
365             sub _calc_data_hash
366             {
367 0     0   0 my ($self) = @_;
368              
369 0 0       0 if (length(${$self->{dataref}}) <= 1048576) {
  0         0  
370 0         0 $self->{data_sha256} = $self->{part_final_hash};
371             } else {
372 0         0 $self->{data_sha256} = large_sha256_hex(${$self->{dataref}});
  0         0  
373             }
374             }
375              
376              
377             sub _sign
378             {
379 65     65   101 my ($self) = @_;
380              
381 65         122 my $now = time();
382              
383 65         122 $self->{last_request_time} = $now; # we use same timestamp when writing to journal
384              
385 65         4866 my $date8601 = strftime("%Y%m%dT%H%M%SZ", gmtime($now));
386 65         3128 my $datestr = strftime("%Y%m%d", gmtime($now));
387              
388              
389             $self->{req_headers} = [
390 65         416 { name => 'x-amz-date', value => $date8601 },
391             ];
392              
393              
394             # getting canonical URL
395              
396 65         126 my @all_headers = sort { $a->{name} cmp $b->{name} } (@{$self->{headers}}, @{$self->{req_headers}});
  195         415  
  65         157  
  65         281  
397              
398              
399 65         156 my $canonical_headers = join ("\n", map { lc($_->{name}).":".trim($_->{value}) } @all_headers);
  195         555  
400 65         139 my $signed_headers = join (';', map { lc($_->{name}) } @all_headers);
  195         382  
401              
402             my $bodyhash = $self->{data_sha256} ?
403             $self->{data_sha256} :
404 65 100       682 ( $self->{dataref} ? large_sha256_hex(${$self->{dataref}}) : sha256_hex('') );
  12 50       77  
405              
406 65 50       286 $self->{params_s} = $self->{params} ? join ('&', map { "$_=$self->{params}->{$_}" } sort keys %{$self->{params}}) : ""; # TODO: proper URI encode
  0         0  
  0         0  
407 65         96 my $canonical_query_string = $self->{params_s};
408              
409 65         245 my $canonical_url = join("\n", $self->{method}, $self->{url}, $canonical_query_string, $canonical_headers, "", $signed_headers, $bodyhash);
410 65         512 my $canonical_url_hash = sha256_hex($canonical_url);
411              
412              
413             # /getting canonical URL
414              
415 65         238 my $credentials = "$datestr/$self->{region}/$self->{service}/aws4_request";
416              
417 65         174 my $string_to_sign = join("\n", "AWS4-HMAC-SHA256", $date8601, $credentials, $canonical_url_hash);
418              
419 65         192 my ($kSigning, $kSigning_hex) = get_signature_key($self->{secret}, $datestr, $self->{region}, $self->{service});
420 65         653 my $signature = hmac_sha256_hex($string_to_sign, $kSigning);
421              
422              
423              
424 65         308 my $auth = "AWS4-HMAC-SHA256 Credential=$self->{key}/$credentials, SignedHeaders=$signed_headers, Signature=$signature";
425              
426 65         71 push @{$self->{req_headers}}, { name => 'Authorization', value => $auth};
  65         344  
427             }
428              
429              
430 23     23   942 sub _max_retries { 100 }
431 0     0   0 sub _sleep($) { sleep shift }
432              
433             sub throttle
434             {
435 111     111 0 1215 my ($i) = @_;
436 111 100       213 if ($i <= 5) {
    100          
    100          
    100          
437 16         61 _sleep 1;
438             } elsif ($i <= 10) {
439 5         8 _sleep 5;
440             } elsif ($i <= 20) {
441 10         11 _sleep 15;
442             } elsif ($i <= 50) {
443 30         31 _sleep 60
444             } else {
445 50         54 _sleep 180;
446             }
447             }
448              
449             sub perform_lwp
450             {
451 57     57   5044 my ($self) = @_;
452              
453 57         171 for my $i (1.._max_retries) {
454 65         1214 undef $self->{last_retry_reason};
455 65         216 $self->_sign();
456              
457 65         437 my $ua = LWP::UserAgent->new(timeout => $self->{timeout});
458 65 50       16728 $ua->protocols_allowed ( [ 'https' ] ) if $self->{protocol} eq 'https'; # Lets hard code this.
459 65         345 $ua->agent("mt-aws-glacier/${App::MtAws::VERSION} (http://mt-aws.com/) "); # use of App::MtAws::VERSION_MATURITY produce warning
460 62         3072 my $req = undef;
461 62         243 my $url = $self->{protocol} ."://$self->{host}$self->{url}";
462 62 100       231 $url = $self->{protocol} ."://$ENV{MTGLACIER_FAKE_HOST}$self->{url}" if $ENV{MTGLACIER_FAKE_HOST};
463 62 50       185 if ($self->{protocol} eq 'https') {
464 0 0       0 if ($ENV{MTGLACIER_FAKE_HOST}) {
465 0         0 $ua->ssl_opts( verify_hostname => 0, SSL_verify_mode=>0); #Hostname mismatch causes LWP to error.
466             } else {
467 0         0 $ua->ssl_opts( verify_hostname => 1, SSL_verify_mode=>1);
468             }
469             }
470 62 50       164 $url .= "?$self->{params_s}" if $self->{params_s};
471 62 100       321 if ($self->{method} eq 'PUT') {
    100          
    100          
    50          
472 3         29 $req = HTTP::Request->new(PUT => $url, undef, $self->{dataref});
473             } elsif ($self->{method} eq 'POST') {
474 3 50       16 if ($self->{dataref}) {
475 3         11 $req = HTTP::Request->new(POST => $url, [Content_Type => 'form-data'], ${$self->{dataref}});
  3         29  
476             } else {
477 0         0 $req = HTTP::Request->new(POST => $url );
478             }
479             } elsif ($self->{method} eq 'DELETE') {
480 3         26 $req = HTTP::Request->new(DELETE => $url);
481             } elsif ($self->{method} eq 'GET') {
482 53         287 $req = HTTP::Request->new(GET => $url);
483             } else {
484 0         0 confess;
485             }
486 62         18628 for ( @{$self->{headers}}, @{$self->{req_headers}} ) {
  62         142  
  62         160  
487 248         9061 $req->header( $_->{name}, $_->{value} );
488             }
489 62         2096 my $resp = undef;
490              
491 62         101 my $t0 = time();
492 62 50 33     357 if ($self->{content_file} && $self->{writer}) {
    50          
    100          
493 0         0 confess "content_file and writer at same time";
494             } elsif ($self->{content_file}) {
495 0         0 $resp = $ua->request($req, $self->{content_file});
496             } elsif ($self->{writer}) {
497 11         26 my $size = undef;
498             $resp = $ua->request($req, sub {
499 2934 100   2934   674273 unless (defined($size)) {
500 6 50 33     76 if ($_[1] && $_[1]->isa('HTTP::Response')) {
501 6         24 $size = $_[1]->content_length;
502 6 100 100     228 if (!$size || ($self->{expected_size} && $size != $self->{expected_size})) {
      66        
503 2         18 die exception
504             wrong_file_size_in_journal =>
505             'Wrong Content-Length received from server, probably wrong file size in Journal or wrong server';
506             }
507 4         32 $self->{writer}->reinit($size);
508             } else {
509             # we should "confess" here, but we cant, only exceptions propogated
510 0         0 die exception "unknow_error" => "Unknown error, probably LWP version is too old";
511             }
512             }
513 2932         7484 $self->{writer}->add_data($_[0]);
514 2932         3294 1;
515 11         122 });
516             } else {
517 51         176 $resp = $ua->request($req);
518             }
519 62         77742 my $dt = time()-$t0;
520              
521 62 100 100     181 if (($resp->code eq '500') && $resp->header('Client-Warning') && ($resp->header('Client-Warning') eq 'Internal response')) {
    100 66        
    100 100        
    100 66        
    100          
522 3         285 print "PID $$ HTTP connection problem (timeout?). Will retry ($dt seconds spent for request)\n";
523 3         8 $self->{last_retry_reason} = 'Internal response';
524 3         9 throttle($i);
525             } elsif ($resp->code =~ /^(500|408)$/) {
526 6         205 print "PID $$ HTTP ".$resp->code." This might be normal. Will retry ($dt seconds spent for request)\n";
527 6         57 $self->{last_retry_reason} = $resp->code;
528 6         36 throttle($i);
529             } elsif (defined($resp->header('X-Died')) && (get_exception($resp->header('X-Died')))) {
530 2         12 die $resp->header('X-Died'); # propogate our own exceptions
531             } elsif (defined($resp->header('X-Died')) && length($resp->header('X-Died'))) {
532 3         233 print "PID $$ HTTP connection problem. Will retry ($dt seconds spent for request)\n";
533 3         9 $self->{last_retry_reason} = 'X-Died';
534 3         12 throttle($i);
535             } elsif ($resp->code =~ /^2\d\d$/) {
536 24 100 100     2387 if ($self->{writer}) {
    100          
537 4         25 my ($c, $reason) = $self->{writer}->finish();
538 4 100       18 if ($c eq 'retry') {
    50          
539 2         62 print "PID $$ HTTP $reason. Will retry ($dt seconds spent for request)\n";
540 2         7 $self->{last_retry_reason} = $reason;
541 2         9 throttle($i);
542             } elsif ($c ne 'ok') {
543 0         0 confess;
544             } else {
545 2         63 return $resp;
546             }
547             } elsif (defined($resp->content_length) && $resp->content_length != length($resp->content)){
548 4         378 print "PID $$ HTTP Unexpected end of data. Will retry ($dt seconds spent for request)\n";
549 4         13 $self->{last_retry_reason}='Unexpected end of data';
550 4         21 throttle($i);
551             } else {
552 16         647 return $resp;
553             }
554             } else {
555 24 100       2743 if ($resp->code =~ /^40[03]$/) {
556 7 100 66     92 if ($resp->content_type && $resp->content_type eq 'application/json') {
557 5         345 my $json = JSON::XS->new->allow_nonref;
558 5         12 my $scalar = eval { $json->decode( $resp->content ); }; # we assume content always in utf8
  5         30  
559 5 50       135 if (defined $scalar) {
560 5         14 my $code = $scalar->{code};
561 5         8 my $type = $scalar->{type};
562 5         13 my $message = $scalar->{message};
563 5 50       19 if ($code eq 'ThrottlingException') {
564 5         183 print "PID $$ ThrottlingException. Will retry ($dt seconds spent for request)\n";
565 5         15 $self->{last_retry_reason} = 'ThrottlingException';
566 5         20 throttle($i);
567 5         190 next;
568             }
569             }
570             }
571             }
572 19         240 print STDERR "Error:\n";
573 19         72 print STDERR dump_request_response($req, $resp);
574 19         77 die exception 'http_unexpected_reply' => 'Unexpected reply from remote server';
575             }
576             }
577 15         402 die exception 'too_many_tries' => "Request was not successful after "._max_retries." retries";
578             }
579              
580              
581              
582             sub get_signature_key
583             {
584 65     65 0 125 my ($secret, $date, $region, $service) = @_;
585 65         88 my $kSecret = $secret;
586 65         750 my $kDate = hmac_sha256($date, "AWS4".$kSecret);
587 65         567 my $kRegion = hmac_sha256($region, $kDate);
588 65         526 my $kService = hmac_sha256($service, $kRegion);
589 65         507 my $kSigning = hmac_sha256("aws4_request", $kService);
590 65         504 my $kSigning_hex = hmac_sha256_hex($kService, "aws4_request");
591              
592 65         205 return ($kSigning, $kSigning_hex);
593             }
594              
595             sub trim
596             {
597 195     195 0 251 my ($s) = @_;
598 195         1153 $s =~ s/\s*\Z//gsi;
599 195         573 $s =~ s/\A\s*//gsi;
600 195         648 $s;
601             }
602              
603              
604             1;
605             __END__