File Coverage

blib/lib/Furl/HTTP/OAuth.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Furl::HTTP::OAuth;
2             $Furl::HTTP::OAuth::VERSION = '0.001';
3 1     1   16302 use warnings;
  1         2  
  1         40  
4 1     1   5 use strict;
  1         1  
  1         25  
5 1     1   759 use URI;
  1         6747  
  1         25  
6 1     1   5 use URI::Escape;
  1         1  
  1         52  
7 1     1   252 use Furl::HTTP;
  0            
  0            
8             use Digest::HMAC_SHA1;
9             use Scalar::Util;
10              
11             # well-formed oauth_signature_method values
12             use constant HMAC_METHOD => 'HMAC-SHA1';
13             use constant PTEXT_METHOD => 'PLAINTEXT';
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             my $class = shift;
121             my %opts = ();
122              
123             if (@_) {
124             if (@_ == 1 && ref $_[0] eq 'HASH') {
125             %opts = %{$_[0]};
126             } else {
127             %opts = @_;
128             }
129             }
130              
131             my $consumer_key = delete $opts{consumer_key};
132             my $consumer_secret = delete $opts{consumer_secret};
133             my $signature_method = delete $opts{signature_method};
134             my $token = delete $opts{token};
135             my $token_secret = delete $opts{token_secret};
136              
137             # nonce generator
138             my $nonce = delete $opts{nonce} || sub {
139             my @chars = ("A".."Z", "a".."z");
140             my $str = "";
141            
142             $str .= $chars[int(rand(scalar(@chars)))]
143             for (1..8);
144            
145             return $str;
146             };
147              
148             # timestamp generator
149             my $timestamp = delete $opts{timestamp} || sub {
150             return time();
151             };
152              
153             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             my $self = shift;
167             my %args = @_;
168              
169             my $url = $args{url};
170             my $scheme = $args{scheme};
171             my $host = $args{host};
172             my $port = $args{port};
173             my $path_query = $args{path_query};
174             my $content = $args{content};
175             my $method = $args{method};
176             my $headers = $args{headers};
177             my $write_file = $args{write_file};
178             my $write_code = $args{write_code};
179             my $signature = $args{signature};
180              
181             my $consumer_key = $self->consumer_key;
182             my $consumer_secret = $self->consumer_secret;
183             my $token = $self->token;
184             my $token_secret = $self->token_secret;
185             my $signature_method = $self->signature_method || '';
186             my $timestamp = &{$self->timestamp};
187             my $nonce = &{$self->nonce};
188             my $uri = undef;
189              
190             if ($url) {
191             $uri = URI->new($url);
192             } else {
193             $uri = URI->new;
194             $uri->scheme($scheme);
195             $uri->host($host);
196             $uri->port($port);
197             $uri->path_query($path_query);
198             }
199            
200             # build signature
201             if (! $signature) {
202             if (uc $signature_method eq PTEXT_METHOD) {
203             $signature_method = PTEXT_METHOD;
204             $signature = $self->gen_plain_sig(
205             consumer_secret => $consumer_secret,
206             token_secret => $token_secret
207             );
208             } else {
209             $signature_method = HMAC_METHOD;
210             $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             $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             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             my ($self, $url, $headers) = @_;
246              
247             return $self->request(
248             method => 'GET',
249             url => $url,
250             headers => $headers
251             );
252             }
253              
254             sub head {
255             my ($self, $url, $headers) = @_;
256              
257             return $self->request(
258             method => 'HEAD',
259             url => $url,
260             headers => $headers
261             );
262             }
263              
264             sub post {
265             my ($self, $url, $headers, $content) = @_;
266              
267             return $self->request(
268             method => 'POST',
269             url => $url,
270             headers => $headers,
271             content => $content
272             );
273             }
274              
275             sub put {
276             my ($self, $url, $headers, $content) = @_;
277              
278             return $self->request(
279             method => 'PUT',
280             url => $url,
281             headers => $headers,
282             content => $content
283             );
284             }
285              
286             sub delete {
287             my ($self, $url, $headers) = @_;
288              
289             return $self->request(
290             method => 'DELETE',
291             url => $url,
292             headers => $headers
293             );
294             }
295              
296             sub _gen_sha1_sig {
297             my $self = shift;
298             my %args = @_;
299              
300             my $method = $args{method};
301             my $uri = $args{uri};
302             my $content = $args{content};
303             my $timestamp = $args{timestamp};
304             my $nonce = $args{nonce};
305             my $consumer_key = $args{consumer_key};
306             my $consumer_secret = $args{consumer_secret};
307             my $token = $args{token};
308             my $token_secret = $args{token_secret};
309            
310             # method part
311             my $base_string = uc($method) . '&';
312            
313             # url part
314             # exclude ports 80 and 443
315             my $port = $uri->port;
316             $port = $port && ($port == 443 || $port == 80) ? '' : (':' . $port);
317             $base_string .= _encode(
318             lc($uri->scheme . '://' . $uri->authority . $port . $uri->path)
319             ) . '&';
320            
321             my @query_form = $uri->query_form;
322             my @sorted_params = ();
323             my %params = ();
324              
325             # handle parameters in $content (hashref or arrayref supported)
326             my $c_reftype = ref $content;
327             if ($content && $c_reftype && ! _is_real_fh($content) &&
328             (($c_reftype eq 'HASH') || $c_reftype eq 'ARRAY')) {
329             @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             for (my $i = 0; $i <= (@query_form - 1); $i += 2) {
335             my $k = _encode($query_form[$i]);
336             my $v = _encode($query_form[$i + 1]);
337            
338             if (exists $params{$k}) {
339             push @{$params{$k}}, $v;
340             } else {
341             $params{$k} = [ $v ];
342             }
343             }
344            
345             # add oauth parameters
346             $params{oauth_consumer_key} = [ _encode($consumer_key) ];
347             $params{oauth_token} = [ _encode($token) ];
348             $params{oauth_signature_method} = [ _encode(HMAC_METHOD) ];
349             $params{oauth_timestamp} = [ _encode($timestamp) ];
350             $params{oauth_nonce} = [ _encode($nonce) ];
351            
352             # sort params and join each key/value with a '='
353             foreach my $key (sort keys %params) {
354             my @vals = @{$params{$key}};
355              
356             # if there's more than one value for the param, sort (see RFC)
357             @vals = sort @vals if (@vals > 1);
358              
359             push @sorted_params, $key . '=' . $_
360             for (@vals);
361             }
362            
363             # add sorted encoded params
364             $base_string .= _encode(join('&', @sorted_params));
365            
366             # compute digest
367             my $key = _encode($consumer_secret) . '&' . _encode($token_secret);
368             my $hmac = Digest::HMAC_SHA1->new($key);
369             $hmac->add($base_string);
370             my $signature = $hmac->b64digest;
371            
372             # pad signature
373             $signature .= '=' x (4 - (length($signature) % 4));
374              
375             return $signature;
376             }
377              
378             sub _gen_plain_sig {
379             my $self = shift;
380             my %args = @_;
381              
382             my $consumer_secret = $args{consumer_secret} || '';
383             my $token_secret = $args{token_secret} || '';
384              
385             return _encode($consumer_secret) . '&' . _encode($token_secret)
386             }
387              
388             sub _encode {
389             return URI::Escape::uri_escape($_[0], '^\w.~-');
390             }
391              
392             # stolen from Plack::Util::is_real_fh
393             sub _is_real_fh {
394             my $fh = shift;
395              
396             my $reftype = Scalar::Util::reftype($fh) or return;
397             if( $reftype eq 'IO'
398             or $reftype eq 'GLOB' && *{$fh}{IO} ){
399             my $m_fileno = $fh->fileno;
400             return unless defined $m_fileno;
401             return unless $m_fileno >= 0;
402             my $f_fileno = fileno($fh);
403             return unless defined $f_fileno;
404             return unless $f_fileno >= 0;
405             return 1;
406             }
407             else {
408             return;
409             }
410             }
411              
412             sub consumer_key {
413             return $_[0]->{consumer_key} =
414             (@_ == 2 ? $_[1] : $_[0]->{consumer_key});
415             }
416              
417             sub consumer_secret {
418             return $_->[0]->{consumer_secret} =
419             (@_ == 2 ? $_[1] : $_[0]->{consumer_secret});
420             }
421              
422             sub signature_method {
423             return $_->[0]->{signature_method} =
424             (@_ == 2 ? $_[1] : $_[0]->{signature_method});
425             }
426              
427             sub token {
428             return $_->[0]->{token} =
429             (@_ == 2 ? $_[1] : $_[0]->{token});
430             }
431              
432             sub token_secret {
433             return $_->[0]->{token_secret} =
434             (@_ == 2 ? $_[1] : $_[0]->{token_secret});
435             }
436              
437             sub nonce {
438             return $_->[0]->{nonce} =
439             (@_ == 2 ? $_[1] : $_[0]->{nonce});
440             }
441              
442             sub timestamp {
443             return $_->[0]->{timestamp} =
444             (@_ == 2 ? $_[1] : $_[0]->{timestamp});
445             }
446              
447             sub furl {
448             return $_->[0]->{furl} =
449             (@_ == 2 ? $_[1] : $_[0]->{furl});
450             }
451              
452             1;