File Coverage

blib/lib/Furl/HTTP/OAuth.pm
Criterion Covered Total %
statement 88 166 53.0
branch 6 48 12.5
condition 5 33 15.1
subroutine 16 30 53.3
pod 13 15 86.6
total 128 292 43.8


line stmt bran cond sub pod time code
1             package Furl::HTTP::OAuth;
2             $Furl::HTTP::OAuth::VERSION = '0.002';
3 1     1   13237 use warnings;
  1         1  
  1         25  
4 1     1   3 use strict;
  1         1  
  1         13  
5 1     1   456 use URI;
  1         4783  
  1         22  
6 1     1   4 use URI::Escape;
  1         1  
  1         47  
7 1     1   536 use Furl::HTTP;
  1         13329  
  1         38  
8 1     1   374 use Digest::HMAC_SHA1;
  1         3876  
  1         30  
9 1     1   5 use Scalar::Util;
  1         1  
  1         36  
10              
11             # well-formed oauth_signature_method values
12 1     1   4 use constant HMAC_METHOD => 'HMAC-SHA1';
  1         1  
  1         44  
13 1     1   3 use constant PTEXT_METHOD => 'PLAINTEXT';
  1         2  
  1         1310  
14              
15             =encoding utf8
16              
17             =head1 NAME
18              
19             Furl::HTTP::OAuth - Make OAuth 1.0 signed requests with Furl
20              
21             =head1 SYNOPSIS
22              
23             my $client = Furl::HTTP::OAuth->new(
24             consumer_key => '',
25             consumer_secret => '',
26             token => '',
27             token_secret => '',
28             signature_method => 'HMAC-SHA1', # the default
29              
30             # accepts all Furl::HTTP->new options
31             agent => 'MyAgent/1.0',
32             timeout => 5
33             );
34              
35             my ($version, $code, $msg, $headers, $body) = $client->get('http://test.com');
36             ($version, $code, $msg, $headers, $body) = $client->put('http://test.com');
37             ($version, $code, $msg, $headers, $body) = $client->post('http://test.com');
38            
39             # OR...
40              
41             ($version, $code, $msg, $headers, $body) = $client->request(
42             # accepts all Furl::HTTP::request options
43             method => 'GET',
44             url => 'http://test.com',
45             );
46              
47             =head1 DESCRIPTION
48              
49             The goal of this module is to provide a simple interface for quickly signing and sending HTTP requests using OAuth 1.0 and Furl. You should be at least somewhat familiar with OAuth 1.0 and Furl before using this module.
50              
51             =head1 METHODS
52              
53             =head3 request
54              
55             See L's request method
56              
57             =head3 get
58              
59             See L's get method
60              
61             =head3 post
62              
63             See L's post method
64              
65             =head3 put
66              
67             See L's put method
68              
69             =head3 delete
70              
71             See L's delete method
72              
73             =head1 ATTRIBUTES
74              
75             =head3 consumer_key (String)
76              
77             Your OAuth consumer key
78              
79             =head3 consumer_secret (String)
80              
81             Your OAuth consumer secret
82              
83             =head3 token (String)
84              
85             Your OAuth token
86              
87             =head3 token_secret (String)
88              
89             Your OAuth token secret
90              
91             =head3 signature_method (String)
92              
93             Either 'HMAC-SHA1' (default) or 'PLAINTEXT'
94              
95             =head3 nonce (Coderef)
96              
97             The default is a coderef which returns an eight character string of random letters
98              
99             =head3 timestamp (Coderef)
100              
101             The default is a coderef which returns time()
102              
103             =head3 furl (Furl::HTTP)
104              
105             Underlying L object. Feel free to use your own.
106              
107             =head1 SEE ALSO
108              
109             L, L, L
110              
111             =head1 LICENSE
112              
113             (c) 2016 ascra
114              
115             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
116              
117             =cut
118              
119             sub new {
120 1     1 0 8 my $class = shift;
121 1         2 my %opts = ();
122              
123 1 50       2 if (@_) {
124 0 0 0     0 if (@_ == 1 && ref $_[0] eq 'HASH') {
125 0         0 %opts = %{$_[0]};
  0         0  
126             } else {
127 0         0 %opts = @_;
128             }
129             }
130              
131 1         2 my $consumer_key = delete $opts{consumer_key};
132 1         2 my $consumer_secret = delete $opts{consumer_secret};
133 1         1 my $signature_method = delete $opts{signature_method};
134 1         1 my $token = delete $opts{token};
135 1         2 my $token_secret = delete $opts{token_secret};
136              
137             # nonce generator
138             my $nonce = delete $opts{nonce} || sub {
139 1     1   8 my @chars = ("A".."Z", "a".."z");
140 1         2 my $str = "";
141            
142             $str .= $chars[int(rand(scalar(@chars)))]
143 1         35 for (1..8);
144            
145 1         9 return $str;
146 1   50     10 };
147              
148             # timestamp generator
149             my $timestamp = delete $opts{timestamp} || sub {
150 1     1   5 return time();
151 1   50     5 };
152              
153 1         7 bless {
154             consumer_key => $consumer_key,
155             consumer_secret => $consumer_secret,
156             signature_method => $signature_method,
157             token => $token,
158             token_secret => $token_secret,
159             nonce => $nonce,
160             timestamp => $timestamp,
161             furl => Furl::HTTP->new(%opts)
162             }, $class;
163             }
164            
165             sub request {
166 0     0 1 0 my $self = shift;
167 0         0 my %args = @_;
168              
169 0         0 my $url = $args{url};
170 0         0 my $scheme = $args{scheme};
171 0         0 my $host = $args{host};
172 0         0 my $port = $args{port};
173 0         0 my $path_query = $args{path_query};
174 0         0 my $content = $args{content};
175 0         0 my $method = $args{method};
176 0         0 my $headers = $args{headers};
177 0         0 my $write_file = $args{write_file};
178 0         0 my $write_code = $args{write_code};
179 0         0 my $signature = $args{signature};
180              
181 0         0 my $consumer_key = $self->consumer_key;
182 0         0 my $consumer_secret = $self->consumer_secret;
183 0         0 my $token = $self->token;
184 0         0 my $token_secret = $self->token_secret;
185 0   0     0 my $signature_method = $self->signature_method || '';
186 0         0 my $timestamp = &{$self->timestamp};
  0         0  
187 0         0 my $nonce = &{$self->nonce};
  0         0  
188 0         0 my $uri = undef;
189              
190 0 0       0 if ($url) {
191 0         0 $uri = URI->new($url);
192             } else {
193 0         0 $uri = URI->new;
194 0         0 $uri->scheme($scheme);
195 0         0 $uri->host($host);
196 0         0 $uri->port($port);
197 0         0 $uri->path_query($path_query);
198             }
199            
200             # build signature
201 0 0       0 if (! $signature) {
202 0 0       0 if (uc $signature_method eq PTEXT_METHOD) {
203 0         0 $signature_method = PTEXT_METHOD;
204 0         0 $signature = $self->gen_plain_sig(
205             consumer_secret => $consumer_secret,
206             token_secret => $token_secret
207             );
208             } else {
209 0         0 $signature_method = HMAC_METHOD;
210 0         0 $signature = $self->gen_sha1_sig(
211             method => $method,
212             uri => $uri,
213             content => $content,
214             consumer_key => $consumer_key,
215             consumer_secret => $consumer_secret,
216             token => $token,
217             token_secret => $token_secret,
218             timestamp => $timestamp,
219             nonce => $nonce,
220             );
221             }
222             }
223              
224             $uri->query_form([
225 0         0 $uri->query_form,
226             oauth_consumer_key => $consumer_key,
227             oauth_nonce => $nonce,
228             oauth_signature_method => $signature_method,
229             oauth_timestamp => $timestamp,
230             oauth_token => $token,
231             oauth_signature => $signature
232             ]);
233              
234 0         0 return $self->furl->request(
235             method => $method,
236             url => $uri->as_string,
237             content => $content,
238             headers => $headers,
239             write_file => $write_file,
240             write_code => $write_code
241             );
242             }
243              
244             sub get {
245 0     0 1 0 my ($self, $url, $headers) = @_;
246              
247 0         0 return $self->request(
248             method => 'GET',
249             url => $url,
250             headers => $headers
251             );
252             }
253              
254             sub head {
255 0     0 0 0 my ($self, $url, $headers) = @_;
256              
257 0         0 return $self->request(
258             method => 'HEAD',
259             url => $url,
260             headers => $headers
261             );
262             }
263              
264             sub post {
265 0     0 1 0 my ($self, $url, $headers, $content) = @_;
266              
267 0         0 return $self->request(
268             method => 'POST',
269             url => $url,
270             headers => $headers,
271             content => $content
272             );
273             }
274              
275             sub put {
276 0     0 1 0 my ($self, $url, $headers, $content) = @_;
277              
278 0         0 return $self->request(
279             method => 'PUT',
280             url => $url,
281             headers => $headers,
282             content => $content
283             );
284             }
285              
286             sub delete {
287 0     0 1 0 my ($self, $url, $headers) = @_;
288              
289 0         0 return $self->request(
290             method => 'DELETE',
291             url => $url,
292             headers => $headers
293             );
294             }
295              
296             sub _gen_sha1_sig {
297 5     5   273 my $self = shift;
298 5         18 my %args = @_;
299              
300 5         6 my $method = $args{method};
301 5         3 my $uri = $args{uri};
302 5         6 my $content = $args{content};
303 5         5 my $timestamp = $args{timestamp};
304 5         2 my $nonce = $args{nonce};
305 5         5 my $consumer_key = $args{consumer_key};
306 5         5 my $consumer_secret = $args{consumer_secret};
307 5         3 my $token = $args{token};
308 5         4 my $token_secret = $args{token_secret};
309            
310             # method part
311 5         7 my $base_string = uc($method) . '&';
312            
313             # url part
314             # exclude ports 80 and 443
315 5         12 my $port = $uri->port;
316 5 50 33     170 $port = $port && ($port == 443 || $port == 80) ? '' : (':' . $port);
317 5         15 $base_string .= _encode(
318             lc($uri->scheme . '://' . $uri->authority . $port . $uri->path)
319             ) . '&';
320            
321 5         294 my @query_form = $uri->query_form;
322 5         119 my @sorted_params = ();
323 5         6 my %params = ();
324              
325             # handle parameters in $content (hashref or arrayref supported)
326 5         5 my $c_reftype = ref $content;
327 5 0 33     12 if ($content && $c_reftype && ! _is_real_fh($content) &&
      33        
      0        
      0        
328             (($c_reftype eq 'HASH') || $c_reftype eq 'ARRAY')) {
329 0 0       0 @query_form = $c_reftype eq 'HASH' ? (@query_form, %$content) :
330             (@query_form, @$content);
331             }
332            
333             # for the sake of sorting, construct a param mapping
334 5         13 for (my $i = 0; $i <= (@query_form - 1); $i += 2) {
335 5         6 my $k = _encode($query_form[$i]);
336 5         114 my $v = _encode($query_form[$i + 1]);
337            
338 5 50       92 if (exists $params{$k}) {
339 0         0 push @{$params{$k}}, $v;
  0         0  
340             } else {
341 5         16 $params{$k} = [ $v ];
342             }
343             }
344            
345             # add oauth parameters
346 5         6 $params{oauth_consumer_key} = [ _encode($consumer_key) ];
347 5         91 $params{oauth_token} = [ _encode($token) ];
348 5         86 $params{oauth_signature_method} = [ _encode(HMAC_METHOD) ];
349 5         85 $params{oauth_timestamp} = [ _encode($timestamp) ];
350 5         86 $params{oauth_nonce} = [ _encode($nonce) ];
351            
352             # sort params and join each key/value with a '='
353 5         100 foreach my $key (sort keys %params) {
354 30         15 my @vals = @{$params{$key}};
  30         33  
355              
356             # if there's more than one value for the param, sort (see RFC)
357 30 50       41 @vals = sort @vals if (@vals > 1);
358              
359             push @sorted_params, $key . '=' . $_
360 30         58 for (@vals);
361             }
362            
363             # add sorted encoded params
364 5         14 $base_string .= _encode(join('&', @sorted_params));
365            
366             # compute digest
367 5         197 my $key = _encode($consumer_secret) . '&' . _encode($token_secret);
368 5         96 my $hmac = Digest::HMAC_SHA1->new($key);
369 5         133 $hmac->add($base_string);
370 5         28 my $signature = $hmac->b64digest;
371            
372             # pad signature
373 5         82 $signature .= '=' x (4 - (length($signature) % 4));
374              
375 5         49 return $signature;
376             }
377              
378             sub _gen_plain_sig {
379 0     0   0 my $self = shift;
380 0         0 my %args = @_;
381              
382 0   0     0 my $consumer_secret = $args{consumer_secret} || '';
383 0   0     0 my $token_secret = $args{token_secret} || '';
384              
385 0         0 return _encode($consumer_secret) . '&' . _encode($token_secret)
386             }
387              
388             sub _encode {
389 55     55   271 return URI::Escape::uri_escape($_[0], '^\w.~-');
390             }
391              
392             # stolen from Plack::Util::is_real_fh
393             sub _is_real_fh {
394 0     0   0 my $fh = shift;
395              
396 0 0       0 my $reftype = Scalar::Util::reftype($fh) or return;
397 0 0 0     0 if( $reftype eq 'IO'
      0        
398 0         0 or $reftype eq 'GLOB' && *{$fh}{IO} ){
399 0         0 my $m_fileno = $fh->fileno;
400 0 0       0 return unless defined $m_fileno;
401 0 0       0 return unless $m_fileno >= 0;
402 0         0 my $f_fileno = fileno($fh);
403 0 0       0 return unless defined $f_fileno;
404 0 0       0 return unless $f_fileno >= 0;
405 0         0 return 1;
406             }
407             else {
408 0         0 return;
409             }
410             }
411              
412             sub consumer_key {
413             return $_[0]->{consumer_key} =
414 0 0   0 1 0 (@_ == 2 ? $_[1] : $_[0]->{consumer_key});
415             }
416              
417             sub consumer_secret {
418             return $_->[0]->{consumer_secret} =
419 0 0   0 1 0 (@_ == 2 ? $_[1] : $_[0]->{consumer_secret});
420             }
421              
422             sub signature_method {
423             return $_->[0]->{signature_method} =
424 0 0   0 1 0 (@_ == 2 ? $_[1] : $_[0]->{signature_method});
425             }
426              
427             sub token {
428             return $_->[0]->{token} =
429 0 0   0 1 0 (@_ == 2 ? $_[1] : $_[0]->{token});
430             }
431              
432             sub token_secret {
433             return $_->[0]->{token_secret} =
434 0 0   0 1 0 (@_ == 2 ? $_[1] : $_[0]->{token_secret});
435             }
436              
437             sub nonce {
438             return $_->[0]->{nonce} =
439 1 50   1 1 7 (@_ == 2 ? $_[1] : $_[0]->{nonce});
440             }
441              
442             sub timestamp {
443             return $_->[0]->{timestamp} =
444 1 50   1 1 5560 (@_ == 2 ? $_[1] : $_[0]->{timestamp});
445             }
446              
447             sub furl {
448             return $_->[0]->{furl} =
449 0 0   0 1   (@_ == 2 ? $_[1] : $_[0]->{furl});
450             }
451              
452             1;