File Coverage

blib/lib/Ceph/RadosGW/Admin/HTTPRequest.pm
Criterion Covered Total %
statement 88 124 70.9
branch 11 38 28.9
condition 8 16 50.0
subroutine 16 19 84.2
pod 2 2 100.0
total 125 199 62.8


line stmt bran cond sub pod time code
1             package Ceph::RadosGW::Admin::HTTPRequest;
2             $Ceph::RadosGW::Admin::HTTPRequest::VERSION = '0.2';
3 2     2   1194 use Moose 0.85;
  2         755627  
  2         15  
4 2     2   13741 use MooseX::StrictConstructor 0.16;
  2         42378  
  2         16  
5 2     2   14752 use HTTP::Date;
  2         5  
  2         160  
6 2     2   1398 use MIME::Base64 qw( encode_base64 );
  2         1290  
  2         121  
7 2     2   13 use Moose::Util::TypeConstraints;
  2         4  
  2         20  
8 2     2   3306 use URI::Escape qw( uri_escape_utf8 );
  2         3  
  2         119  
9 2     2   1047 use URI::QueryParam;
  2         1198  
  2         51  
10 2     2   11 use URI;
  2         4  
  2         33  
11 2     2   827 use Digest::HMAC_SHA1;
  2         9083  
  2         85  
12 2     2   14 use Digest::MD5 ();
  2         3  
  2         2454  
13              
14             # this is almost a direct copy of
15             # https://metacpan.org/pod/Net::Amazon::S3::HTTPRequest
16              
17             # ABSTRACT: Create a signed HTTP::Request
18              
19             my $METADATA_PREFIX = 'x-amz-meta-';
20             my $AMAZON_HEADER_PREFIX = 'x-amz-';
21              
22             enum 'HTTPMethod' => [ qw(DELETE GET HEAD PUT POST) ];
23              
24             has 'url' => ( is => 'ro', isa => 'Str', required => 1 );
25             has 'method' => ( is => 'ro', isa => 'HTTPMethod', required => 1 );
26             has 'path' => ( is => 'ro', isa => 'Str', required => 1 );
27             has 'access_key' => ( is => 'ro', isa => 'Str', required => 1 );
28             has 'secret_key' => ( is => 'ro', isa => 'Str', required => 1 );
29              
30             has 'headers' =>
31             ( is => 'ro', isa => 'HashRef', required => 0, default => sub { {} } );
32             has 'content' =>
33             ( is => 'ro', isa => 'Str|CodeRef', required => 0, default => '' );
34             has 'metadata' =>
35             ( is => 'ro', isa => 'HashRef', required => 0, default => sub { {} } );
36              
37             __PACKAGE__->meta->make_immutable;
38              
39             # make the HTTP::Request object
40             sub http_request {
41 47     47 1 69 my $self = shift;
42 47         1507 my $method = $self->method;
43 47         1340 my $path = $self->path;
44 47         1329 my $headers = $self->headers;
45 47         1324 my $content = $self->content;
46 47         1380 my $metadata = $self->metadata;
47 47         1336 my $uri = $self->url . $path;
48            
49 47         133 my $http_headers = $self->_merge_meta( $headers, $metadata );
50              
51 47 50       179 $self->_add_auth_header( $http_headers, $method, $path )
52             unless exists $headers->{Authorization};
53            
54              
55            
56 47         1523 my $request
57             = HTTP::Request->new( $method, $uri, $http_headers, $content );
58              
59             #my $req_as = $request->as_string;
60             #$req_as =~ s/[^\n\r\x20-\x7f]/?/g;
61             #$req_as = substr( $req_as, 0, 1024 ) . "\n\n";
62             #warn $req_as;
63              
64 47         7088 return $request;
65             }
66              
67             sub query_string_authentication_uri {
68 0     0 1 0 my ( $self, $expires ) = @_;
69 0         0 my $method = $self->method;
70 0         0 my $path = $self->path;
71 0         0 my $headers = $self->headers;
72              
73 0         0 my $aws_access_key_id = $self->access_key;
74 0         0 my $aws_secret_access_key = $self->secret_key;
75 0         0 my $canonical_string
76             = $self->_canonical_string( $method, $path, $headers, $expires );
77 0         0 my $encoded_canonical
78             = $self->_encode( $aws_secret_access_key, $canonical_string );
79              
80 0         0 my $uri = $self->url . $path;
81 0         0 $uri = URI->new($uri);
82              
83 0         0 $uri->query_param( AWSAccessKeyId => $aws_access_key_id );
84 0         0 $uri->query_param( Expires => $expires );
85 0         0 $uri->query_param( Signature => $encoded_canonical );
86              
87 0         0 return $uri;
88             }
89              
90              
91             sub _add_auth_header {
92 47     47   70 my ( $self, $headers, $method, $path ) = @_;
93 47         1451 my $aws_access_key_id = $self->access_key;
94 47         1338 my $aws_secret_access_key = $self->secret_key;
95              
96 47 50       120 if ( not $headers->header('Date') ) {
97 47         1418 $headers->header( Date => time2str(time) );
98             }
99            
100 47 50       1849 if ( not $headers->header('Content-Type') ) {
101 47         870 $headers->header( 'Content-Type' => 'text/plain' );
102             }
103            
104 47 50       1089 if ( not $headers->header('Content-MD5') ) {
105 47         2320 $headers->header( 'Content-MD5' => Digest::MD5::md5_base64($self->content));
106             }
107              
108 47         1226 my $canonical_string
109             = $self->_canonical_string( $method, $path, $headers );
110 47         111 my $encoded_canonical
111             = $self->_encode( $aws_secret_access_key, $canonical_string );
112 47         489 $headers->header(
113             Authorization => "AWS $aws_access_key_id:$encoded_canonical" );
114             }
115              
116             # generate a canonical string for the given parameters. expires is optional and is
117             # only used by query string authentication.
118             sub _canonical_string {
119 47     47   81 my ( $self, $method, $path, $headers, $expires ) = @_;
120 47         74 my %interesting_headers = ();
121 47         205 while ( my ( $key, $value ) = each %$headers ) {
122 141         144 my $lk = lc $key;
123 141 50 100     585 if ( $lk eq 'content-md5'
      66        
      33        
124             or $lk eq 'content-type'
125             or $lk eq 'date'
126             or $lk =~ /^$AMAZON_HEADER_PREFIX/ )
127             {
128 141         193 $interesting_headers{$lk} = $self->_trim($value);
129             }
130             }
131              
132            
133              
134             # just in case someone used this. it's not necessary in this lib.
135 47 50       85 $interesting_headers{'date'} = ''
136             if $interesting_headers{'x-amz-date'};
137              
138             # if you're using expires for query string auth, then it trumps date
139             # (and x-amz-date)
140 47 50       62 $interesting_headers{'date'} = $expires if $expires;
141              
142 47         71 my $buf = "$method\n";
143 47         161 foreach my $key ( sort keys %interesting_headers ) {
144 141 50       309 if ( $key =~ /^$AMAZON_HEADER_PREFIX/ ) {
145 0         0 $buf .= "$key:$interesting_headers{$key}\n";
146             } else {
147 141         263 $buf .= "$interesting_headers{$key}\n";
148             }
149             }
150              
151             # don't include anything after the first ? in the resource...
152 47         145 $path =~ /^([^?]*)/;
153 47         89 $path = "/$1";
154 47         189 $path =~ s:/+:/:g;
155 47         62 $buf .= $path;
156            
157            
158             # ...unless there any parameters we're interested in...
159 47 50       195 if ( $path =~ /[&?](acl|torrent|location|uploads|delete)($|=|&)/ ) {
    50          
160 0         0 $buf .= "?$1";
161             } elsif ( my %query_params = URI->new($path)->query_form ){
162             #see if the remaining parsed query string provides us with any query string or upload id
163 0 0 0     0 if($query_params{partNumber} && $query_params{uploadId}){
    0          
164             #re-evaluate query string, the order of the params is important for request signing, so we can't depend on URI to do the right thing
165 0         0 $buf .= sprintf("?partNumber=%s&uploadId=%s", $query_params{partNumber}, $query_params{uploadId});
166             }
167             elsif($query_params{uploadId}){
168 0         0 $buf .= sprintf("?uploadId=%s",$query_params{uploadId});
169             }
170             }
171              
172             #warn "Buf:\n$buf\n";
173            
174 47         2311 return $buf;
175             }
176              
177             # finds the hmac-sha1 hash of the canonical string and the aws secret access key and then
178             # base64 encodes the result (optionally urlencoding after that).
179             sub _encode {
180 47     47   69 my ( $self, $aws_secret_access_key, $str, $urlencode ) = @_;
181 47         193 my $hmac = Digest::HMAC_SHA1->new($aws_secret_access_key);
182 47         1705 $hmac->add($str);
183 47         266 my $b64 = encode_base64( $hmac->digest, '' );
184 47 50       962 if ($urlencode) {
185 0         0 return $self->_urlencode($b64);
186             } else {
187 47         202 return $b64;
188             }
189             }
190              
191             # EU buckets must be accessed via their DNS name. This routine figures out if
192             # a given bucket name can be safely used as a DNS name.
193             sub _is_dns_bucket {
194 0     0   0 my $bucketname = $_[0];
195              
196 0 0       0 if ( length $bucketname > 63 ) {
197 0         0 return 0;
198             }
199 0 0       0 if ( length $bucketname < 3 ) {
200 0         0 return;
201             }
202 0 0       0 return 0 unless $bucketname =~ m{^[a-z0-9][a-z0-9.-]+$};
203 0         0 my @components = split /\./, $bucketname;
204 0         0 for my $c (@components) {
205 0 0       0 return 0 if $c =~ m{^-};
206 0 0       0 return 0 if $c =~ m{-$};
207 0 0       0 return 0 if $c eq '';
208             }
209 0         0 return 1;
210             }
211              
212             # generates an HTTP::Headers objects given one hash that represents http
213             # headers to set and another hash that represents an object's metadata.
214             sub _merge_meta {
215 47     47   57 my ( $self, $headers, $metadata ) = @_;
216 47   50     88 $headers ||= {};
217 47   50     77 $metadata ||= {};
218              
219 47         174 my $http_header = HTTP::Headers->new;
220 47         398 while ( my ( $k, $v ) = each %$headers ) {
221 0         0 $http_header->header( $k => $v );
222             }
223 47         127 while ( my ( $k, $v ) = each %$metadata ) {
224 0         0 $http_header->header( "$METADATA_PREFIX$k" => $v );
225             }
226              
227 47         65 return $http_header;
228             }
229              
230             sub _trim {
231 141     141   138 my ( $self, $value ) = @_;
232 141         218 $value =~ s/^\s+//;
233 141         210 $value =~ s/\s+$//;
234 141         436 return $value;
235             }
236              
237             sub _urlencode {
238 0     0     my ( $self, $unencoded ) = @_;
239 0           return uri_escape_utf8( $unencoded, '^A-Za-z0-9_-' );
240             }
241              
242             1;
243              
244             __END__
245              
246             =pod
247              
248             =head1 NAME
249              
250             Ceph::RadosGW::Admin::HTTPRequest::HTTPRequest - Create a signed HTTP::Request
251              
252             =head1 VERSION
253              
254             version 0.60
255              
256             =head1 SYNOPSIS
257              
258             my $http_request = Ceph::RadosGW::Admin::HTTPRequest::HTTPRequest->new(
259             method => 'PUT',
260             path => $self->bucket . '/',
261             headers => $headers,
262             content => $content,
263             )->http_request;
264              
265             =head1 DESCRIPTION
266              
267             This module creates an HTTP::Request object that is signed
268             appropriately for Amazon S3.
269              
270             =for test_synopsis no strict 'vars'
271              
272             =head1 METHODS
273              
274             =head2 http_request
275              
276             This method creates, signs and returns a HTTP::Request object.
277              
278             =head2 query_string_authentication_uri
279              
280             This method creates, signs and returns a query string authentication
281             URI.
282              
283             =head1 AUTHOR
284              
285             Pedro Figueiredo <me@pedrofigueiredo.org>
286              
287             =head1 COPYRIGHT AND LICENSE
288              
289             This software is copyright (c) 2014 by Amazon Digital Services, Leon Brocard, Brad Fitzpatrick, Pedro Figueiredo.
290              
291             This is free software; you can redistribute it and/or modify it under
292             the same terms as the Perl 5 programming language system itself.
293              
294             =cut