File Coverage

lib/OAuthomatic/Caller.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package OAuthomatic::Caller;
2             # ABSTRACT: actually make OAuth-signed calls
3              
4              
5 1     1   972 use Moose;
  0            
  0            
6              
7             use version;
8             use Const::Fast;
9             use LWP::UserAgent;
10             use URI;
11             use URI::QueryParam;
12             use HTTP::Request;
13             use HTTP::Headers;
14             use Net::OAuth;
15             use OAuthomatic::Error;
16             use OAuthomatic::Internal::Util;
17             use namespace::sweep;
18              
19             const my $OAUTH_REALM => "OAuthomatic";
20              
21             const my $FORM_CONTENT_TYPE => "application/x-www-form-urlencoded";
22             const my $DEFAULT_FORM_CONTENT_TYPE => "application/x-www-form-urlencoded; charset=utf-8";
23             const my $DEFAULT_CONTENT_TYPE => "text/plain; charset=utf-8";
24              
25             const my $_HAS_BUGGY_PROXY_IN_LWP => (
26             version->parse($LWP::UserAgent::VERSION) < version->parse("6.06") );
27              
28              
29             has 'config' => (
30             is => 'ro', isa => 'OAuthomatic::Config', required => 1,
31             handles => ['debug']);
32              
33              
34             has 'server' => (
35             is => 'ro', isa => 'OAuthomatic::Server', required => 1,
36             handles => [
37             'oauth_authorize_page', 'oauth_temporary_url', 'oauth_token_url',
38             'protocol_version', 'signature_method'
39             ]);
40              
41             # Object responsible for executing calls.
42             has 'user_agent' => (
43             is => 'ro', isa => 'LWP::UserAgent', default => sub {
44             # Workaround for SSL-over-proxy problems. Disables buggy proxy behaviour in
45             # LWP (proxy should be handled by Crypt::SSLeay below thanks to env variable).
46             # https://rt.cpan.org/Public/Bug/Display.html?id=1894
47             # http://www.perlmonks.org/?node_id=994683
48             # FIXME: when LWP 6.06 turns popular, drop it and just depend on 6.06
49             my $ua;
50             if($_HAS_BUGGY_PROXY_IN_LWP && $ENV{https_proxy}) {
51             require Net::SSL;
52             local $Net::HTTPS::SSL_SOCKET_CLASS = "Net::SSL"; # Force use of Net::SSL
53             $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 });
54             $ua->proxy(https => undef);
55             } else {
56             $ua = LWP::UserAgent->new;
57             }
58             return $ua;
59             });
60              
61             # Application identity
62             has 'client_cred' => (
63             is => 'rw', isa => 'Maybe[OAuthomatic::Types::ClientCred]', required => 1,
64             trigger => sub {
65             my ($self, $cred, $old_cred) = @_;
66             return if @_ == 2; # Don't act during object build
67             return if OAuthomatic::Types::ClientCred->equal($cred, $old_cred);
68             # Changed client_cred means access creds are no longer valid.
69             $self->token_cred(undef);
70             });
71              
72             # Current access credentials, for use in normal calls
73             has 'token_cred' => (
74             is => 'rw', isa => 'Maybe[OAuthomatic::Types::TokenCred]', required => 1);
75              
76              
77             sub create_authorization_url {
78             my ($self, $callback_url) = @_;
79              
80             # Make temporary credentials
81             my $temporary_cred = $self->_create_temporary_cred($callback_url);
82              
83             # Calculate authorization url
84             my $url = URI->new($self->oauth_authorize_page);
85             $url->query_param(oauth_token => $temporary_cred->token);
86              
87             $temporary_cred->authorize_page($url);
88              
89             return $temporary_cred;
90             }
91              
92             # Create request token
93             sub _create_temporary_cred {
94             my ($self, $callback_url) = @_;
95              
96             unless ($self->_using_legacy_oauth10) {
97             unless($callback_url) {
98             OAuthomatic::Error::Generic->throw(
99             ident => "missing required argument",
100             extra => "callback_url is required");
101             }
102             }
103              
104             my $response = $self->_execute_oauth_request_ext(
105             class => 'RequestToken', # No token
106             callback => $callback_url,
107             method => 'POST', url => $self->oauth_temporary_url,
108             auth_in_post => 1);
109              
110             my $response_params = _parse_form_reply(
111             $response, ['oauth_token', 'oauth_token_secret']);
112              
113             unless($response_params->{oauth_callback_confirmed}
114             || $self->_using_legacy_oauth10) {
115             OAuthomatic::Error::Protocol->throw(
116             ident => "Bad OAuth reply",
117             extra => "Missing oauth_callback_confirmed (is site using OAuth 1.0?)");
118             }
119              
120             return OAuthomatic::Types::TemporaryCred->new(
121             data => $response_params,
122             remap => {'oauth_token' => 'token', 'oauth_token_secret' => 'secret'});
123             }
124              
125              
126             sub create_token_cred {
127             my ($self, $temporary_cred, $verifier_cred) = @_;
128              
129             unless ($verifier_cred || $self->_using_legacy_oauth10) { # FIXME maybe drop support for it?
130             OAuthomatic::Error::Generic->throw(
131             ident => "Missing parameter",
132             extra => "For OAuth 1.0a verifier is required");
133             }
134              
135             my $response = $self->_execute_oauth_request_ext(
136             class => 'AccessToken',
137             token => $temporary_cred, verifier => $verifier_cred,
138             method => 'POST', url => $self->oauth_token_url,
139             auth_in_post => 1);
140              
141             my $response_params = _parse_form_reply(
142             $response, ['oauth_token', 'oauth_token_secret']);
143              
144             my $cred = OAuthomatic::Types::TokenCred->new(
145             data => $response_params,
146             remap => {'oauth_token' => 'token', 'oauth_token_secret' => 'secret'});
147              
148             $self->token_cred($cred);
149              
150             return $cred;
151             }
152              
153             ###########################################################################
154             # Call-making
155             ###########################################################################
156              
157              
158             sub build_oauth_request {
159             my $self = shift;
160              
161             unless($self->token_cred) {
162             OAuthomatic::Error::Generic->throw(
163             ident => "Unauthorized",
164             extra => "Missing token.");
165             }
166              
167             return $self->_build_oauth_request_ext(
168             class => 'ProtectedResource',
169             token => $self->token_cred,
170             @_);
171             }
172              
173              
174             sub execute_oauth_request {
175             my $self = shift;
176              
177             unless($self->token_cred) {
178             OAuthomatic::Error::Generic->throw(
179             ident => "Unauthorized",
180             extra => "Missing token.");
181             }
182              
183             return $self->_execute_oauth_request_ext(
184             class => 'ProtectedResource',
185             token => $self->token_cred,
186             @_);
187             }
188              
189              
190             sub _build_oauth_request_ext {
191             my ($self, %args) = @_;
192              
193             foreach my $req_arg (qw(class method url)) {
194             unless($args{$req_arg}) {
195             OAuthomatic::Error::Generic->throw(
196             ident => "Bad parameter",
197             extra => "Missing required parameter '$req_arg'");
198             }
199             }
200              
201             my $class = $args{class};
202             my $token = $args{token};
203             my $verifier = $args{verifier};
204             my $callback = $args{callback};
205             my $method = $args{method};
206             my $url = $args{url};
207             my $url_args = $args{url_args};
208             my $body_form = $args{body_form};
209             my $body = $args{body};
210             my $content_type = $args{content_type};
211             my $auth_in_post = $args{auth_in_post};
212              
213             # Sanity checks
214              
215             if($method =~ /^(?:POST|PUT)$/x) {
216             $body_form = {} unless ($body || $body_form);
217             OAuthomatic::Error::Generic->throw(
218             ident => "Bad parameter",
219             extra => "Can not specify both body and body_form")
220             if $body && $body_form;
221             OAuthomatic::Error::Generic->throw(
222             ident => "Bad parameter",
223             extra => "Can't use plain body with auth_in_post")
224             if $auth_in_post && $body;
225             }
226             elsif($method =~ /^(?:GET|DELETE)$/x) {
227             OAuthomatic::Error::Generic->throw(
228             ident => "Bad parameter",
229             extra => "Can not specify body* for $method request")
230             if ($body || $body_form);
231             OAuthomatic::Error::Generic->throw(
232             ident => "Bad parameter",
233             extra => "Can't use auth_in_post for $method request")
234             if $auth_in_post;
235             }
236             else {
237             OAuthomatic::Error::Generic->throw(
238             ident => "Bad parameter",
239             extra => "Unknown method: $method (expected GET, POST, PUT or DELETE)");
240             }
241              
242             if($url_args) {
243             unless(ref($url_args) eq 'HASH') {
244             OAuthomatic::Error::Generic->throw(
245             ident => "Bad parameter",
246             extra => "url_args should be specified as hash reference, but are given as " . (ref($url_args) || "scalar value $url_args"));
247             }
248             }
249              
250             if($body_form) {
251             $content_type ||= $DEFAULT_FORM_CONTENT_TYPE;
252             OAuthomatic::Error::Generic->throw(
253             ident => "Bad parameter",
254             extra => "With body_form, the only allowed content_type is $FORM_CONTENT_TYPE (plus coding), but $content_type was given")
255             unless $content_type =~ /^$FORM_CONTENT_TYPE(;.*)?$/;
256             } else {
257             $content_type ||= $DEFAULT_CONTENT_TYPE;
258             OAuthomatic::Error::Generic->throw(
259             ident => "Bad parameter",
260             extra => "Can not specify content_type=$content_type and plain body, use body_form!")
261             if $content_type =~ /^$FORM_CONTENT_TYPE(;.*)?$/;
262             }
263              
264             if($verifier) {
265             OAuthomatic::Error::Generic->throw(
266             ident => "Bad parameter",
267             extra => "Attempt to specify verifier without token")
268             unless $token;
269             OAuthomatic::Error::Generic->throw(
270             ident => "Token mismatch",
271             extra => "Obtained verifier is for token " . $verifier->token
272             . " while (temporary) token is " . $token->token)
273             unless $verifier->token eq $token->token;
274             }
275              
276             # Calculate final URL
277              
278             # FIXME: URI encodes as utf-8 true strings or as Latin-1 binary strings. Consider
279             # using default encoding if non-utf8
280             my $uri = URI->new($url);
281             # Append additional params, if given
282             if($url_args) {
283             foreach my $key (keys %$url_args) {
284             $uri->query_param($key, $url_args->{$key});
285             }
286             }
287              
288             # Net::OAuth is used to
289             # (a) calculate signature
290             # and
291             # (b) render authorization header
292             my $oauth_request = Net::OAuth->request($class)->new(
293             request_method => $method,
294             request_url => $uri,
295             $self->client_cred->as_hash("consumer"), # consumer_key, consumer_secret
296             ($token
297             ? ($token->as_hash()) # token, token_secret
298             : ()),
299             signature_method => $self->signature_method,
300             protocol_version => $self->_net_oauth_version_constant(), # FIXME: isn't it version?
301             # version => $self->protocol_version, # changes oauth_version but better not, bb fails
302             timestamp => time,
303             nonce => $self->_nonce,
304             ($verifier
305             ? (verifier => $verifier->verifier)
306             : ()),
307             ($callback
308             ? (callback => $callback)
309             : ()),
310             ($body_form
311             ? (extra_params => $body_form) # body parts used in signature
312             : ()),
313             );
314             $oauth_request->sign;
315             OAuthomatic::Error::Generic->throw(
316             ident => "OAuth signature verification failed.")
317             unless $oauth_request->verify;
318              
319             my $headers = HTTP::Headers->new();
320             # FIXME: handle custom headers (Accept?)
321              
322             if($auth_in_post) {
323             my $oauth_par = $oauth_request->to_hash();
324             if($body_form && %$body_form) {
325             # Merge OAuth with other post oauth_par
326             @{$body_form}{keys %$oauth_par} = values %$oauth_par;
327             } else {
328             $body_form = $oauth_par;
329             }
330             } else {
331             # This builds "OAuth realm=... oauth_consumer_key=.. ..." text
332             my $oauth_header_text = $oauth_request->to_authorization_header($OAUTH_REALM);
333             $headers->header('Authorization' => $oauth_header_text);
334             }
335              
336             my $http_request = HTTP::Request->new($method, $uri, $headers);
337             if($body) {
338             fill_httpmsg_text($http_request, $body, $content_type);
339             } elsif($body_form) {
340             fill_httpmsg_form($http_request, $body_form);
341             }
342              
343             return $http_request;
344             }
345              
346              
347             sub _execute_oauth_request_ext {
348             my $self = shift;
349             my $request = $self->_build_oauth_request_ext(@_);
350              
351             print "[OAuthomatic] Executing request: ", $request->as_string, "\n" if $self->debug;
352              
353             my $response = $self->user_agent->request($request);
354              
355             print "[OAuthomatic] Obtained response: ", $response->as_string, "\n" if $self->debug;
356              
357             OAuthomatic::Error::HTTPFailure->throw(
358             ident => 'OAuth-signed HTTP call failed',
359             request => $request, response => $response)
360             unless $response->is_success;
361              
362             return $response;
363             }
364              
365             ###########################################################################
366             # Helpers
367             ###########################################################################
368              
369             sub _net_oauth_version_constant {
370             my $self = shift;
371             my $ver = $self->protocol_version;
372             return Net::OAuth::PROTOCOL_VERSION_1_0A() if $ver eq '1.0a';
373             return Net::OAuth::PROTOCOL_VERSION_1_0() if $ver eq '1.0';
374             OAuthomatic::Error::Generic->throw(
375             ident => "Invalid parameter",
376             extra => "Invalid protocol version: $ver");
377             }
378              
379             sub _using_legacy_oauth10 {
380             my $self = shift;
381             return 1 if $self->protocol_version eq '1.0';
382             return 0;
383             }
384              
385             # Pair (timestamp-rounded-to-second, nonce) should be unique. Usual
386             # random should do in practical cases, but there is tiny risk of
387             # collisions. Marsenne-Twister, with its uniform distribution,
388             # reduces this risk even further.
389             BEGIN {
390             eval {
391             require Math::Random::MT;
392             my $rnd_gen = Math::Random::MT->new();
393             *_nonce = sub {
394             return $rnd_gen->rand(2**32);
395             };
396             1;
397             } or do {
398             srand();
399             *_nonce = sub {
400             return int( rand( 2**32 ) );
401             };
402             };
403             # Random setup
404             srand();
405             };
406              
407             # Checks response: throws exception on error, parses and returns param hash
408             # Second param, if present, is a list of required parameters
409             sub _parse_form_reply {
410             my ($http_response, $required_params) = @_;
411              
412             my $params = parse_httpmsg_form($http_response, 1);
413              
414             if($required_params) {
415             foreach my $par_name (@$required_params) {
416             # FIXME: bind http_response
417             OAuthomatic::Error::Generic->throw(
418             ident => "Invalid reply obtained",
419             extra => "Missing required item in parsed reply: $par_name\n"
420             . "Reply items: " . join(", ", keys %$params). "\n")
421             unless $params->{$par_name};
422             }
423             }
424              
425             return $params;
426             }
427              
428             1;
429              
430             __END__
431              
432             =pod
433              
434             =encoding UTF-8
435              
436             =head1 NAME
437              
438             OAuthomatic::Caller - actually make OAuth-signed calls
439              
440             =head1 VERSION
441              
442             version 0.01
443              
444             =head1 DESCRIPTION
445              
446             Sign OAuth calls and execute them.
447              
448             This object is mostly used internally by L<OAuthomatic>, but may be useful
449             separately if you want to implement initialization scheme by yourself but
450             prefer it's API and structural exceptions to raw L<Net::OAuth>.
451              
452             =head1 METHODS
453              
454             =head2 create_authorization_url($callback_url) => TemporaryCred
455              
456             Calculates URL which user should visit to authorize app (and
457             associated temporary token).
458              
459             =head2 create_token_cred
460              
461             Acquires access token, preserves them in the object (so future calls
462             will be authenticated), and return (so it can be saved etc).
463              
464             =head2 build_oauth_request(method => ..., ...)
465              
466             Prepare properly signed L<HTTP::Request> but do not execute it, just
467             return ready-to-be-sent object.
468              
469             Parameters: identical as in L</execute_oauth_request>
470              
471             =head2 execute_oauth_request(method => $method, url => $url, url_args => $args,
472             body_form => $body_form, body => $body,
473             content_type => $content_type)
474              
475             Make a request to C<url> using the given HTTP method and signing
476             request with OAuth credentials.
477              
478             =over 4
479              
480             =item method
481              
482             One of C<GET>, C<POST>, C<PUT>, C<DELETE>.
483              
484             =item url
485              
486             Actual URL to call (C<http://some.site.com/api/...>)
487              
488             =item url_args (optional)
489              
490             Additional arguments to escape and add to the URL. This is simply shortcut,
491             three calls below are equivalent:
492              
493             $c->execute_oauth_request(method => "GET",
494             url => "http://some.where/api?x=1&y=2&z=a+b");
495              
496             $c->execute_oauth_request(method => "GET",
497             url => "http://some.where/api",
498             url_args => {x => 1, y => 2, z => 'a b'});
499              
500             $c->execute_oauth_request(method => "GET",
501             url => "http://some.where/api?x=1",
502             url_args => {y => 2, z => 'a b'});
503              
504             =item body_form OR body
505              
506             Exactly one of those must be specified for POST and PUT (none for GET or DELETE).
507              
508             Specifying C<body_form> means, that we are creating www-urlencoded
509             form. Parameters will be included in OAuth signature. Example:
510              
511             $c->execute_oauth_request(method => "POST",
512             url => "http://some.where/api",
513             body_form => {par1 => 'abc', par2 => 'd f'});
514              
515             Note that this is not just a shortcut for setting body to already
516             serialized form. Case of urlencoded form is treated in a special way
517             by OAuth (those values impact OAuth signature). To avoid signature
518             verification errors, OAuthomatic will reject such attempts:
519              
520             # WRONG AND WILL FAIL. Use body_form if you post form.
521             $c->execute_oauth_request(method => "POST",
522             url => "http://some.where/api",
523             body => 'par1=abc&par2=d+f',
524             content_type => 'application/x-www-form-urlencoded');
525              
526             Specifying C<body> means, that we post non-form body (for example
527             JSON, XML or even binary data). Example:
528              
529             $c->execute_oauth_request(method => "POST",
530             url => "http://some.where/api",
531             body => "<product><item-no>3434</item-no><price>334.22</price></product>",
532             content_type => "application/xml; charset=utf-8");
533              
534             Value of body can be either binary string (which will be posted as-is), or
535             perl unicode string (which will be encoded according to the content type, what by
536             default means utf-8).
537              
538             Such content is not covered by OAuth signature, so less secure (at
539             least if it is posted over non-SSL connection).
540              
541             For longer bodies, references are supported:
542              
543             $c->execute_oauth_request(method => "POST",
544             url => "http://some.where/api",
545             body => \$body_string,
546             content_type => "application/xml; charset=utf-8");
547              
548             =item content_type
549              
550             Used to set content type of the request. If missing, it is set to
551             C<text/plain; charset=utf-8> if C<body> param is specified and to
552             C<application/x-www-form-urlencoded; charset=utf-8> if C<body_form>
553             param is specified.
554              
555             Note that module author does not test behaviour on encodings different
556             than utf-8 (although they may work).
557              
558             =back
559              
560             =head2 _execute_oauth_request_ext
561              
562             Common code for API and OAuth-protocol calls. Uses all parameters
563             described in L</execute_oauth_request> and two additional:
564              
565             =over 4
566              
567             =item class
568              
569             ProtectedResource, UserAuth, RequestToken etc (XXX from
570             Net::Oauth::XXXXRequest)
571              
572             =item token
573              
574             Actual token to use while signing (skip to use only client token) -
575             either $self->token_cred, or some temporary_cred, depending on task at
576             hand.
577              
578             =back
579              
580             =head1 ATTRIBUTES
581              
582             =head2 config
583              
584             L<OAuthomatic::Config> object used to bundle various configuration params.
585              
586             =head2 server
587              
588             L<OAuthomatic::Server> object used to bundle server-related configuration params.
589              
590             =head1 INTERNAL METHODS
591              
592             =head2 _build_oauth_request_ext
593              
594             Common code for API and OAuth-protocol calls. Uses all parameters
595             described in L</execute_oauth_request> and some additional:
596              
597             =over 4
598              
599             =item class
600              
601             ProtectedResource, UserAuth, RequestToken etc (XXX from Net::Oauth::XXXXRequest)
602              
603             =item token
604              
605             Actual token to use while signing (skip to use only client token) - either $self->token_cred,
606             or some temporary_cred, depending on task at hand.
607              
608             =item verifier
609              
610             Verifier to be added to access token creation.
611              
612             =item callback
613              
614             Callback url for temporary token creation.
615              
616             =item auth_in_post
617              
618             True if authorization tokens are to be merged into POST body, false if
619             they are to be preserved in Authorize header.
620              
621             =back
622              
623             =head1 AUTHOR
624              
625             Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
626              
627             =head1 COPYRIGHT AND LICENSE
628              
629             This software is copyright (c) 2015 by Marcin Kasperski.
630              
631             This is free software; you can redistribute it and/or modify it under
632             the same terms as the Perl 5 programming language system itself.
633              
634             =cut