File Coverage

blib/lib/RPC/XML/Client.pm
Criterion Covered Total %
statement 78 237 32.9
branch 10 104 9.6
condition 4 46 8.7
subroutine 18 33 54.5
pod 5 5 100.0
total 115 425 27.0


line stmt bran cond sub pod time code
1             ###############################################################################
2             #
3             # This file copyright (c) 2001-2014 Randy J. Ray, all rights reserved
4             #
5             # Copying and distribution are permitted under the terms of the Artistic
6             # License 2.0 (http://www.opensource.org/licenses/artistic-license-2.0.php) or
7             # the GNU LGPL (http://www.opensource.org/licenses/lgpl-2.1.php).
8             #
9             ###############################################################################
10             #
11             # Description: This class implements an RPC::XML client, using LWP to
12             # manage the underlying communication protocols. It relies
13             # on the RPC::XML transaction core for data management.
14             #
15             # Functions: new
16             # send_request
17             # simple_request
18             # uri
19             # useragent
20             # request
21             #
22             # Libraries: LWP::UserAgent
23             # HTTP::Request
24             # URI
25             # RPC::XML
26             # RPC::XML::ParserFactory
27             # Compress::Raw::Zlib is used if available
28             #
29             # Global Consts: $VERSION
30             #
31             ###############################################################################
32              
33             package RPC::XML::Client;
34              
35 4     4   2519 use 5.008008;
  4         9  
  4         109  
36 4     4   13 use strict;
  4         5  
  4         81  
37 4     4   12 use warnings;
  4         4  
  4         89  
38 4     4   11 use vars qw($VERSION $COMPRESSION_AVAILABLE);
  4         4  
  4         184  
39 4         20 use subs qw(new simple_request send_request uri useragent request
40 4     4   15 fault_handler error_handler combined_handler timeout);
  4         5  
41              
42 4     4   242 use Scalar::Util 'blessed';
  4         4  
  4         145  
43 4     4   689 use File::Temp;
  4         11582  
  4         233  
44 4     4   20 use IO::Handle;
  4         7  
  4         129  
45 4     4   25 use Module::Load;
  4         5  
  4         39  
46              
47 4     4   128375 use LWP::UserAgent;
  4         168186  
  4         165  
48 4     4   28 use HTTP::Request;
  4         6  
  4         74  
49 4     4   14 use URI;
  4         4  
  4         56  
50              
51 4     4   14 use RPC::XML;
  4         3  
  4         148  
52 4     4   542 use RPC::XML::ParserFactory;
  4         4  
  4         25  
53              
54             BEGIN
55             {
56             # Check for compression support
57             $COMPRESSION_AVAILABLE =
58 4 50   4   7 (eval { load Compress::Zlib; 1; }) ? 'deflate' : q{};
  4         15  
  4         59400  
59             }
60              
61             $VERSION = '1.42';
62             $VERSION = eval $VERSION; ## no critic (ProhibitStringyEval)
63              
64             ###############################################################################
65             #
66             # Sub Name: new
67             #
68             # Description: Create a LWP::UA instance and add some extra material
69             # specific to our purposes.
70             #
71             # Arguments: NAME IN/OUT TYPE DESCRIPTION
72             # $class in scalar Class to bless into
73             # $location in scalar URI path for requests to go to
74             # %attrs in hash Extra info
75             #
76             # Globals: $VERSION
77             #
78             # Returns: Success: object reference
79             # Failure: error string
80             #
81             ###############################################################################
82             sub new
83             {
84 2     2   11761 my ($class, $location, %attrs) = @_;
85              
86 2   33     13 $class = ref($class) || $class;
87 2 100       4 if (! $location)
88             {
89 1         3 return "${class}::new: Missing location argument";
90             }
91              
92 1         1 my ($self, $UA, $REQ);
93              
94             # Start by getting the LWP::UA object
95 0         0 $UA = LWP::UserAgent->new(
96 1 50       9 (exists $attrs{useragent}) ? @{$attrs{useragent}} : ()
97             );
98 1         2609 $UA->agent(sprintf '%s/%s %s', $class, $VERSION, $UA->agent);
99 1         77 $self->{__useragent} = $UA;
100 1         1 delete $attrs{useragent};
101              
102             # Next get the request object for later use
103 1         6 $REQ = HTTP::Request->new(POST => $location);
104 1         4567 $self->{__request} = $REQ;
105 1         12 $REQ->header(Content_Type => 'text/xml');
106 1         66 $REQ->protocol('HTTP/1.0');
107              
108             # Note compression support
109 1         9 $self->{__compress} = $COMPRESSION_AVAILABLE;
110             # It looks wasteful to keep using the hash key, but it makes it easier
111             # to change the string in just one place (BEGIN block, above) if I have to.
112             # Also (for now) I prefer to manipulate the private keys directly, before
113             # blessing $self, rather than using accessors. This is just for performance
114             # and I might change my mind later.
115 1 50       3 if ($self->{__compress})
116             {
117 1         3 $REQ->header(Accept_Encoding => $self->{__compress});
118             }
119 1   50     31 $self->{__compress_thresh} = $attrs{compress_thresh} || 4096;
120 1         9 $self->{__compress_re} = qr/$self->{__compress}/;
121             # They can change this value with a method
122 1         1 $self->{__compress_requests} = 0;
123 1         2 delete $attrs{compress_thresh};
124              
125             # Parameters to control the point at which messages are shunted to temp
126             # files due to size, and where to home the temp files. Start with a size
127             # threshhold of 1Meg and no specific dir (which will fall-through to the
128             # tmpdir() method of File::Spec).
129 1   50     4 $self->{__message_file_thresh} = $attrs{message_file_thresh} || 1_048_576;
130 1   50     5 $self->{__message_temp_dir} = $attrs{message_temp_dir} || q{};
131 1         2 delete @attrs{qw(message_file_thresh message_temp_dir)};
132              
133             # Note and preserve any error or fault handlers. Check the combo-handler
134             # first, as it is superceded by anything more specific.
135 1 50       2 if (ref $attrs{combined_handler})
136             {
137 0         0 $self->{__error_cb} = $attrs{combined_handler};
138 0         0 $self->{__fault_cb} = $attrs{combined_handler};
139 0         0 delete $attrs{combined_handler};
140             }
141 1 50       3 if (ref $attrs{fault_handler})
142             {
143 0         0 $self->{__fault_cb} = $attrs{fault_handler};
144 0         0 delete $attrs{fault_handler};
145             }
146 1 50       2 if (ref $attrs{error_handler})
147             {
148 0         0 $self->{__error_cb} = $attrs{error_handler};
149 0         0 delete $attrs{error_handler};
150             }
151              
152             # Get the RPC::XML::Parser instance from the ParserFactory
153 0           $self->{__parser} =
154 1 50       11 RPC::XML::ParserFactory->new($attrs{parser} ? @{$attrs{parser}} : ())
    50          
155             or return "${class}::new: Unable to get RPC::XML::Parser object";
156 0           delete $attrs{parser};
157              
158             # Now preserve any remaining attributes passed in
159 0           for (keys %attrs)
160             {
161 0           $self->{$_} = $attrs{$_};
162             }
163              
164 0           return bless $self, $class;
165             }
166              
167             ###############################################################################
168             #
169             # Sub Name: simple_request
170             #
171             # Description: Simplify the request process by both allowing for direct
172             # data on the incoming side, and for returning a native
173             # value rather than an object reference.
174             #
175             # Arguments: NAME IN/OUT TYPE DESCRIPTION
176             # $self in ref Class instance
177             # @args in list Various args -- see comments
178             #
179             # Globals: $RPC::XML::ERROR
180             #
181             # Returns: Success: value
182             # Failure: undef, error in $RPC::XML::ERROR
183             #
184             ###############################################################################
185             sub simple_request
186             {
187 0     0     my ($self, @args) = @_;
188              
189 0           my $return;
190              
191 0           $RPC::XML::ERROR = q{};
192              
193 0           $return = $self->send_request(@args);
194 0 0         if (! ref $return)
195             {
196 0           $RPC::XML::ERROR = ref($self) . "::simple_request: $return";
197 0           return;
198             }
199              
200 0           return $return->value;
201             }
202              
203             ###############################################################################
204             #
205             # Sub Name: send_request
206             #
207             # Description: Take a RPC::XML::request object, dispatch a request, and
208             # parse the response. The return value should be a
209             # RPC::XML::response object, or an error string.
210             #
211             # Arguments: NAME IN/OUT TYPE DESCRIPTION
212             # $self in ref Class instance
213             # $req in ref RPC::XML::request object or
214             # remote method name
215             # @args in list If $req is a method name, these
216             # are potential arguments for
217             # the remote call
218             #
219             # Returns: Success: RPC::XML::response object instance
220             # Failure: error string
221             #
222             ###############################################################################
223             sub send_request ## no critic (ProhibitExcessComplexity)
224             {
225 0     0     my ($self, $req, @args) = @_;
226              
227 0           my ($me, $message, $response, $reqclone, $content, $can_compress, $value,
228             $do_compress, $req_fh, $tmpdir, $com_engine);
229              
230 0           $me = ref($self) . '::send_request';
231              
232 0 0 0       if (! $req)
    0          
233             {
234 0           return "$me: No request object or remote method name given";
235             }
236             elsif (! (blessed $req and $req->isa('RPC::XML::request')))
237             {
238             # Assume that $req is the name of the routine to be called
239 0 0         if (! ($req = RPC::XML::request->new($req, @args)))
240             {
241 0           return "$me: Error creating RPC::XML::request object: " .
242             $RPC::XML::ERROR;
243             }
244             }
245              
246             # Start by setting up the request-clone for using in this instance
247 0           $reqclone = $self->request->clone;
248 0 0         if (! $reqclone->header('Host')) {
249 0           $reqclone->header(Host => URI->new($reqclone->uri)->host_port);
250             }
251 0           $can_compress = $self->compress; # Avoid making 4+ calls to the method
252 0 0 0       if ($self->compress_requests and $can_compress and
      0        
253             $req->length >= $self->compress_thresh)
254             {
255             # If this is a candidate for compression, set a flag and note it
256             # in the Content-encoding header.
257 0           $do_compress = 1;
258 0           $reqclone->content_encoding($can_compress);
259             }
260              
261             # Next step, determine our content's disposition. If it is above the
262             # threshhold for a requested file cut-off, send it to a temp file and use
263             # a closure on the request object to manage content.
264 0 0 0       if ($self->message_file_thresh and
265             $self->message_file_thresh <= $req->length)
266             {
267 0           require File::Spec;
268             # Start by creating a temp-file
269 0   0       $tmpdir = $self->message_temp_dir || File::Spec->tmpdir;
270             # File::Temp->new() croaks on error, rather than just returning undef
271 0           $req_fh = eval { File::Temp->new(UNLINK => 1, DIR => $tmpdir) };
  0            
272 0 0         if (! $req_fh)
273             {
274 0           return "$me: Error opening tmpfile: $@";
275             }
276 0           binmode $req_fh;
277             # Make it auto-flush
278 0           $req_fh->autoflush();
279              
280             # Now that we have it, spool the request to it. This is a little
281             # hairy, since we still have to allow for compression. And though the
282             # request could theoretically be HUGE, in order to compress we have to
283             # write it to a second temp-file first, so that we can compress it
284             # into the primary handle.
285 0 0 0       if ($do_compress && ($req->length >= $self->compress_thresh))
286             {
287 0           my $fh_compress = eval {
288 0           File::Temp->new(UNLINK => 1, DIR => $tmpdir);
289             };
290 0 0         if (! $fh_compress)
291             {
292 0           return "$me: Error opening compression tmpfile: $@";
293             }
294             # Make it auto-flush
295 0           $fh_compress->autoflush();
296              
297             # Write the request to the second FH
298 0           $req->serialize($fh_compress);
299 0           seek $fh_compress, 0, 0;
300              
301             # Spin up the compression engine
302 0           $com_engine = Compress::Zlib::deflateInit();
303 0 0         if (! $com_engine)
304             {
305 0           return "$me: Unable to initialize the Compress::Zlib engine";
306             }
307              
308             # Spool from the second FH through the compression engine, into
309             # the intended FH.
310 0           my $buf = q{};
311 0           my $out;
312 0           while (read $fh_compress, $buf, 4096)
313             {
314 0           $out = $com_engine->deflate(\$buf);
315 0 0         if (! defined $out)
316             {
317 0           return "$me: Compression failure in deflate()";
318             }
319 0           print {$req_fh} $out;
  0            
320             }
321             # Make sure we have all that's left
322 0           $out = $com_engine->flush;
323 0 0         if (! defined $out)
324             {
325 0           return "$me: Compression flush failure in deflate()";
326             }
327 0           print {$req_fh} $out;
  0            
328              
329             # Close the secondary FH. Rewinding the primary is done later.
330 0 0         if (! close $fh_compress)
331             {
332 0           return "$me: Error closing spool-file: $!";
333             }
334             }
335             else
336             {
337 0           $req->serialize($req_fh);
338             }
339 0           seek $req_fh, 0, 0;
340              
341 0           $reqclone->content_length(-s $req_fh);
342             $reqclone->content(sub {
343 0     0     my $b = q{};
344 0 0         if (! defined read $req_fh, $b, 4096)
345             {
346 0           return;
347             }
348              
349 0           return $b;
350 0           });
351             }
352             else
353             {
354             # Treat the content strictly in-memory
355 0           utf8::encode($content = $req->as_string);
356 0 0         if ($do_compress)
357             {
358 0           $content = Compress::Zlib::compress($content);
359             }
360 0           $reqclone->content($content);
361             # Because $content has been force-downgraded, length() should work
362 0           $reqclone->content_length(length $content);
363             }
364              
365             # Content used to be handled as an in-memory string. Now, to avoid eating
366             # up huge chunks due to potentially-massive messages (thanks Tellme), we
367             # parse incrementally with the XML::Parser::ExpatNB class. What's more,
368             # to use the callback-form of request(), we can't get just the headers
369             # first. We have to check things like compression and such on the fly.
370 0           my $compression;
371 0           my $parser = $self->parser->parse(); # Gets the ExpatNB object
372             my $cb = sub {
373 0     0     my ($data_in, $resp) = @_;
374              
375 0 0         if (! defined $compression)
376             {
377 0 0 0       $compression =
378             (($resp->content_encoding || q{}) =~
379             $self->compress_re) ? 1 : 0;
380 0 0 0       if ($compression and (! $can_compress))
381             {
382 0           die "$me: Compressed content encoding not supported\n";
383             }
384 0 0         if ($compression)
385             {
386 0 0         if (! ($com_engine = Compress::Zlib::inflateInit()))
387             {
388 0           die "$me: Unable to initialize de-compression engine\n";
389             }
390             }
391             }
392              
393 0 0         if ($compression)
394             {
395 0           my $error;
396 0 0         if (! (($data_in, $error) = $com_engine->inflate($data_in)))
397             {
398 0           die "$me: Error in inflate() expanding data: $error\n";
399             }
400             }
401              
402 0           $parser->parse_more($data_in);
403 0           1;
404 0           };
405              
406 0           $response = $self->useragent->request($reqclone, $cb);
407 0 0         if ($message = $response->headers->header('X-Died'))
408             {
409             # One of the die's was triggered
410 0 0         return ('CODE' eq ref $self->error_handler) ?
411             $self->error_handler->($message) : $message;
412             }
413 0 0         if (! $response->is_success)
414             {
415 0           $message = "$me: HTTP server error: " . $response->message;
416 0 0         return ('CODE' eq ref $self->error_handler) ?
417             $self->error_handler->($message) : $message;
418             }
419              
420             # Whee. No errors from the callback or the server. Finalize the parsing
421             # process.
422 0 0         if (! eval { $value = $parser->parse_done(); 1; })
  0            
  0            
423             {
424 0 0         if ($@)
425             {
426             # One of the die's was triggered
427 0 0         return ('CODE' eq ref $self->error_handler) ?
428             $self->error_handler->($@) : $@;
429             }
430             }
431              
432             # Check if there is a callback to be invoked in the case of
433             # errors or faults
434 0 0         if (! ref $value)
    0          
435             {
436 0           $message = "$me: parse-level error: $value";
437 0 0         return ('CODE' eq ref $self->error_handler) ?
438             $self->error_handler->($message) : $message;
439             }
440             elsif ($value->is_fault)
441             {
442 0 0         return ('CODE' eq ref $self->fault_handler) ?
443             $self->fault_handler->($value->value) : $value->value;
444             }
445              
446 0           return $value->value;
447             }
448              
449             ###############################################################################
450             #
451             # Sub Name: timeout
452             #
453             # Description: Get or set the timeout() setting on the underlying
454             # LWP::UserAgent object.
455             #
456             # Arguments: NAME IN/OUT TYPE DESCRIPTION
457             # $self in ref Object of this class
458             # $time in scalar New timeout value, if passed
459             #
460             # Returns: Return value from LWP::UserAgent->timeout()
461             #
462             ###############################################################################
463             sub timeout ## no critic (RequireArgUnpacking)
464             {
465 0     0     my $self = shift;
466              
467 0           return $self->useragent->timeout(@_);
468             }
469              
470             ###############################################################################
471             #
472             # Sub Name: uri
473             #
474             # Description: Get or set the URI portion of the request
475             #
476             # Arguments: NAME IN/OUT TYPE DESCRIPTION
477             # $self in ref Object of this class
478             # $uri in scalar New URI, if passed
479             #
480             # Returns: Current URI, undef if trying to set an invalid URI
481             #
482             ###############################################################################
483             sub uri ## no critic (RequireArgUnpacking)
484             {
485 0     0     my $self = shift;
486              
487 0           return $self->request->uri(@_);
488             }
489              
490             ###############################################################################
491             #
492             # Sub Name: credentials
493             #
494             # Description: Set basic-auth credentials on the underlying user-agent
495             # object
496             #
497             # Arguments: NAME IN/OUT TYPE DESCRIPTION
498             # $self in ref Object of this class
499             # $realm in scalar Realm to authenticate for
500             # $user in scalar User name to authenticate
501             # $pass in scalar Password for $user
502             #
503             # Returns: $self
504             #
505             ###############################################################################
506             sub credentials
507             {
508 0     0 1   my ($self, $realm, $user, $pass) = @_;
509              
510 0           my $uri = URI->new($self->uri);
511 0           $self->useragent->credentials($uri->host_port, $realm, $user, $pass);
512              
513 0           return $self;
514             }
515              
516             # Immutable accessor methods
517             BEGIN
518             {
519 4     4   22 no strict 'refs'; ## no critic (ProhibitNoStrict)
  4         5  
  4         211  
520              
521 4     4   9 for my $method (qw(useragent request compress_re compress parser))
522             {
523 20     0   1039 *{$method} = sub { shift->{"__$method"} }
  0         0  
524 20         43 }
525             }
526              
527             # Fetch/set the compression threshhold
528             sub compress_thresh
529             {
530 0     0 1   my $self = shift;
531 0   0       my $value = shift || 0;
532              
533 0           my $old = $self->{__compress_thresh};
534 0 0         if ($value)
535             {
536 0           $self->{__compress_thresh} = $value;
537             }
538              
539 0           return $old;
540             }
541              
542             # This doesn't actually *get* the original value, it only sets the value
543             sub compress_requests
544             {
545 0     0 1   my ($self, $value) = @_;
546              
547 0 0         if (! $value)
548             {
549 0           return $self->{__compress_requests};
550             }
551              
552 0 0         return $self->{__compress_requests} = $value ? 1 : 0;
553             }
554              
555             # These are get/set accessors for the fault-handler, error-handler and the
556             # combined fault/error handler.
557             sub fault_handler
558             {
559 0     0     my ($self, $newval) = @_;
560              
561 0           my $val = $self->{__fault_cb};
562 0 0 0       if ($newval and ref $newval)
563             {
564 0           $self->{__fault_cb} = $newval;
565             }
566             # Special: an explicit undef is used to clear the callback
567 0 0 0       if (@_ == 2 and (! defined $newval))
568             {
569 0           $self->{__fault_cb} = undef;
570             }
571              
572 0           return $val;
573             }
574              
575             sub error_handler
576             {
577 0     0     my ($self, $newval) = @_;
578              
579 0           my $val = $self->{__error_cb};
580 0 0 0       if ($newval and ref $newval)
581             {
582 0           $self->{__error_cb} = $newval;
583             }
584             # Special: an explicit undef is used to clear the callback
585 0 0 0       if (@_ == 2 and (! defined $newval))
586             {
587 0           $self->{__error_cb} = undef;
588             }
589              
590 0           return $val;
591             }
592              
593             sub combined_handler
594             {
595 0     0     my ($self, $newval) = @_;
596              
597 0           return ($self->fault_handler($newval), $self->error_handler($newval));
598             }
599              
600             # Control whether, and at what point, messages are considered too large to
601             # handle in-memory.
602             sub message_file_thresh
603             {
604 0     0 1   my ($self, $thresh) = @_;
605              
606 0 0         if (! $thresh)
607             {
608 0           return $self->{__message_file_thresh};
609             }
610              
611 0           return $self->{__message_file_thresh} = $thresh;
612             }
613              
614             sub message_temp_dir
615             {
616 0     0 1   my ($self, $dir) = @_;
617              
618 0 0         if (! $dir)
619             {
620 0           return $self->{__message_temp_dir};
621             }
622              
623 0           return $self->{__message_temp_dir} = $dir;
624             }
625              
626             1;
627              
628             __END__