File Coverage

blib/lib/Amazon/SQS/Simple/Base.pm
Criterion Covered Total %
statement 73 159 45.9
branch 7 34 20.5
condition 5 19 26.3
subroutine 20 26 76.9
pod 0 1 0.0
total 105 239 43.9


line stmt bran cond sub pod time code
1             package Amazon::SQS::Simple::Base;
2              
3 2     2   6 use strict;
  2         2  
  2         42  
4 2     2   4 use warnings;
  2         2  
  2         39  
5 2     2   6 use Carp qw( croak carp );
  2         2  
  2         76  
6 2     2   814 use Digest::HMAC_SHA1;
  2         8114  
  2         82  
7 2     2   12 use Digest::SHA qw(hmac_sha256 sha256);
  2         2  
  2         78  
8 2     2   1086 use LWP::UserAgent;
  2         82539  
  2         98  
9 2     2   1162 use MIME::Base64;
  2         1133  
  2         126  
10 2     2   11 use URI::Escape;
  2         3  
  2         82  
11 2     2   1332 use XML::Simple;
  2         17360  
  2         18  
12 2     2   182 use HTTP::Date;
  2         3  
  2         103  
13 2     2   1043 use HTTP::Request::Common;
  2         3290  
  2         103  
14 2     2   854 use AWS::Signature4;
  2         29444  
  2         81  
15 2     2   16 use POSIX qw(strftime);
  2         3  
  2         15  
16 2     2   1487 use Encode qw(encode);
  2         19721  
  2         184  
17 2     2   1281 use Data::Dumper;
  2         11705  
  2         156  
18 2     2   1256 use VM::EC2::Security::CredentialCache;
  2         915363  
  2         65  
19              
20 2     2   13 use base qw(Exporter);
  2         2  
  2         182  
21              
22             use constant ({
23 2         2333 SQS_VERSION_2012_11_05 => '2012-11-05',
24             BASE_ENDPOINT => 'http://sqs.us-east-1.amazonaws.com',
25             DEF_MAX_GET_MSG_SIZE => 4096, # Messages larger than this size will use a POST request.
26             MAX_RETRIES => 4,
27 2     2   8 });
  2         3  
28            
29              
30             our $DEFAULT_SQS_VERSION = SQS_VERSION_2012_11_05;
31             our @EXPORT = qw(SQS_VERSION_2012_11_05);
32             our $URI_SAFE_CHARACTERS = '^A-Za-z0-9-_.~'; # defined by AWS, same as URI::Escape defaults
33              
34             sub new {
35 3     3 0 1651 my $class = shift;
36 3         7 my @args = @_;
37 3 100 66     16 if (scalar(@args) >= 2 && $args[0] ne 'UseIAMRole') {
38 2         3 my $access_key = shift @args;
39 2         3 my $secret_key = shift @args;
40 2         6 @args = (AWSAccessKeyId => $access_key,
41             SecretKey => $secret_key, @args);
42             }
43 3         13 my $self = {
44             Endpoint => +BASE_ENDPOINT,
45             SignatureVersion => 4,
46             Version => $DEFAULT_SQS_VERSION,
47             @args
48             };
49              
50 3 50       11 if (!defined($self->{UserAgent})) {
51 3         18 $self->{UserAgent} = LWP::UserAgent->new(keep_alive => 4);
52             }
53              
54 3 50       4161 if (defined($self->{Timeout})) {
55 0         0 $self->{UserAgent}->timeout($self->{Timeout});
56             }
57              
58 3 50       9 if (!defined($self->{Region})) {
59 3         7 $self->{Region} = 'us-east-1';
60             }
61              
62 3         10 $self->{UserAgent}->env_proxy;
63              
64 3 100 66     3630 if (!$self->{UseIAMRole} && (!$self->{AWSAccessKeyId} || !$self->{SecretKey})) {
      33        
65 1         225 croak "Missing AWSAccessKey or SecretKey";
66             }
67              
68 2         5 $self = bless($self, $class);
69 2         7 return $self;
70             }
71              
72             sub _api_version {
73 1     1   25 my $self = shift;
74 1         7 return $self->{Version};
75             }
76              
77             sub _dispatch {
78 0     0     my $self = shift;
79 0   0       my $params = shift || {};
80 0   0       my $force_array = shift || [];
81 0           my $url = $self->{Endpoint};
82 0           my $response;
83             my $post_body;
84 0           my $post_request = 0;
85              
86             $params = {
87             Version => $self->{Version},
88 0           %$params
89             };
90              
91 0 0 0       if (!$params->{Timestamp} && !$params->{Expires}) {
92 0           $params->{Timestamp} = _timestamp();
93             }
94              
95 0           foreach my $try (1..MAX_RETRIES) {
96            
97 0           my $req = HTTP::Request->new(POST => $url);
98 0           $req->header(host => URI->new($url)->host);
99 0           my $now = time;
100 0           my $http_date = strftime('%Y%m%dT%H%M%SZ', gmtime($now));
101 0           my $date = strftime('%Y%m%d', gmtime($now));
102            
103 0           $req->protocol('HTTP/1.1');
104 0           $req->header('Date' => $http_date);
105 0           $req->header('x-amz-target', 'AmazonSQSv20121105.' . $params->{Action});
106 0           $req->header('content-type' => 'application/x-www-form-urlencoded;charset=utf-8');
107              
108 0 0         if ($self->{UseIAMRole}) {
109 0           my $creds = VM::EC2::Security::CredentialCache->get();
110 0 0         defined($creds) || die("Unable to retrieve IAM role credentials");
111 0           $self->{AWSAccessKeyId} = $creds->accessKeyId;
112 0           $self->{SecretKey} = $creds->secretAccessKey;
113 0           $req->header('x-amz-security-token' => $creds->sessionToken);
114             }
115              
116 0           $params->{AWSAccessKeyId} = $self->{AWSAccessKeyId};
117              
118 0           my $escaped_params = $self->_escape_params($params);
119 0           my $payload = join('&', map { $_ . '=' . $escaped_params->{$_} } keys %$escaped_params);
  0            
120 0           $req->content($payload);
121 0           $req->header('Content-Length', length($payload));
122              
123             my $signer = AWS::Signature4->new(-access_key => $self->{AWSAccessKeyId},
124 0           -secret_key => $self->{SecretKey});
125 0           $signer->sign($req);
126              
127 0           $self->_debug_log($req->as_string());
128            
129 0           $response = $self->{UserAgent}->request($req);
130            
131 0 0         if ($response->is_success) { # note, 500 and 503 are NOT success :D
132 0           $self->_debug_log($response->content);
133 0           my $href = XMLin($response->content, ForceArray => $force_array, KeyAttr => {});
134 0           return $href;
135             } else {
136             # advice from internal AWS support - most client libraries try 3 times in the face
137             # of 500 errors, so ours should too
138             # use exponential backoff.
139            
140 0 0 0       if ($response->code == 500 || $response->code == 503) {
141 0           my $sleep_amount= 2 ** $try * 50 * 1000;
142 0           $self->_debug_log("Doing sleep for: $sleep_amount");
143 0           Time::HiRes::usleep($sleep_amount);
144 0           next;
145             }
146 0           die("Got an error: " . $response->as_string());
147             }
148             }
149              
150             # if we fall out of the loop, then we have either a non-500 error or a persistent 500.
151            
152 0           my $msg;
153 0           eval {
154 0           my $href = XMLin($response->content);
155 0           $msg = $href->{Error}{Message};
156             };
157            
158 0           my $error = "ERROR: On calling $params->{Action}: " . $response->status_line;
159 0 0         $error .= " ($msg)" if $msg;
160 0           croak $error;
161             }
162              
163             sub _debug_log {
164 0     0     my ($self, $msg) = @_;
165 0 0         return unless $self->{_Debug};
166 0           chomp($msg);
167 0           print {$self->{_Debug}} $msg . "\n\n";
  0            
168             }
169              
170             sub _escape_params {
171 0     0     my ($self, $params) = @_;
172              
173             # Need to escape + characters in signature
174             # see http://docs.amazonwebservices.com/AWSSimpleQueueService/2006-04-01/Query_QueryAuth.html
175              
176             # Likewise, need to escape + characters in ReceiptHandle
177             # Many characters are possible in MessageBody:
178             # #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
179             # probably should encode all keys and values for consistency and future-proofing
180 0           my $to_escape = qr{^(?:Signature|MessageBody|ReceiptHandle)|\.\d+\.(?:MessageBody|ReceiptHandle)$};
181 0           foreach my $key (keys %$params) {
182 0 0         next unless $key =~ m/$to_escape/;
183 0           my $octets = encode('utf-8-strict', $params->{$key});
184 0           $params->{$key} = uri_escape($octets, $URI_SAFE_CHARACTERS);
185             }
186 0           return $params;
187             }
188              
189             sub _escape_param {
190 0     0     my $params = shift;
191 0           my $single = shift;
192 0           my $multi_n = shift;
193            
194 0 0         if ($params->{$single}) {
195 0           $params->{$single} = uri_escape($params->{$single});
196             } else {
197 0           foreach my $i (1..10) {
198 0           my $multi = $multi_n;
199 0           $multi =~ s/\.n\./\.$i\./;
200 0 0         if ($params->{$multi}) {
201 0           $params->{$multi} = uri_escape($params->{$multi});
202             } else {
203 0           last;
204             }
205             }
206             }
207             }
208              
209             sub _max_get_msg_size {
210 0     0     my $self = shift;
211             # a user-defined cut-off
212 0 0         if (defined $self->{MAX_GET_MSG_SIZE}) {
213 0           return $self->{MAX_GET_MSG_SIZE};
214             }
215             # the default cut-off
216             else {
217 0           return DEF_MAX_GET_MSG_SIZE;
218             }
219             }
220              
221             sub _timestamp {
222 0     0     my $t = shift;
223 0 0         if (!defined($t)) {
224 0           $t = time;
225             }
226 0           my $formatted_time = HTTP::Date::time2isoz($t);
227 0           $formatted_time =~ s/ /T/;
228 0           return $formatted_time;
229             }
230              
231             1;
232              
233             __END__
234              
235             =head1 NAME
236              
237             Amazon::SQS::Simple::Base - No user-serviceable parts included
238              
239             =head1 AUTHOR
240              
241             Copyright 2007-2008 Simon Whitaker E<lt>swhitaker@cpan.orgE<gt>
242             Copyright 2013-2017 Mike (no relation) Whitaker E<lt>penfold@cpan.orgE<gt>
243              
244             This program is free software; you can redistribute it and/or modify it
245             under the same terms as Perl itself.
246              
247             =cut
248