############################################################################### # # This file copyright (c) 2001-2009 Randy J. Ray, all rights reserved # # Copying and distribution are permitted under the terms of the Artistic # License 2.0 (http://www.opensource.org/licenses/artistic-license-2.0.php) or # the GNU LGPL (http://www.opensource.org/licenses/lgpl-2.1.php). # ############################################################################### # # Description: This class implements an RPC::XML server, using the core # XML::RPC transaction code. The server may be created with # or without an HTTP::Daemon object instance to answer the # requests. # # Functions: new # version # url # product_tokens # started # path # host # port # requests # response # compress # compress_thresh # compress_re # message_file_thresh # message_temp_dir # xpl_path # add_method # method_from_file # get_method # server_loop # post_configure_hook # pre_loop_hook # process_request # dispatch # call # add_default_methods # add_methods_in_dir # delete_method # list_methods # share_methods # copy_methods # timeout # server_fault # # Libraries: AutoLoader # HTTP::Daemon (conditionally) # HTTP::Response # HTTP::Status # URI # RPC::XML # RPC::XML::ParserFactory # RPC::XML::Procedure # # Global Consts: $VERSION # $INSTALL_DIR # %FAULT_TABLE # ############################################################################### package RPC::XML::Server; use 5.006001; use strict; use warnings; use vars qw($VERSION @ISA $INSTANCE $INSTALL_DIR %FAULT_TABLE @XPL_PATH $IO_SOCKET_SSL_HACK_NEEDED $COMPRESSION_AVAILABLE); use Carp 'carp'; use AutoLoader 'AUTOLOAD'; use File::Spec; use HTTP::Status; use HTTP::Response; use URI; use Scalar::Util 'blessed'; use RPC::XML; use RPC::XML::ParserFactory; use RPC::XML::Procedure; BEGIN { $INSTALL_DIR = (File::Spec->splitpath(__FILE__))[1]; @XPL_PATH = ($INSTALL_DIR, File::Spec->curdir); # For now, I have an ugly hack in place to make the functionality that # runs under HTTP::Daemon/Net::Server work better with SSL. This flag # starts out true, then gets set to false the first time the hack is # applied, so that it doesn't get repeated over and over... $IO_SOCKET_SSL_HACK_NEEDED = 1; # Check for compression support eval { require Compress::Zlib; }; $COMPRESSION_AVAILABLE = ($@) ? '' : 'deflate'; # Set up the initial table of fault-types and their codes/messages %FAULT_TABLE = ( badxml => [100 => 'XML parse error: %s'], badmethod => [200 => 'Method lookup error: %s'], badsignature => [201 => 'Method signature error: %s'], execerror => [300 => 'Code execution error: %s'], ); } $VERSION = '1.54'; $VERSION = eval $VERSION; ## no critic ############################################################################### # # Sub Name: new # # Description: Create a new RPC::XML::Server object. This entails getting # a HTTP::Daemon object, saving several internal values, and # other operations. # # Arguments: NAME IN/OUT TYPE DESCRIPTION # $class in scalar Ref or string for the class # %args in hash Additional arguments # # Returns: Success: object reference # Failure: error string # ############################################################################### sub new { my $class = shift; my %args = @_; my ( $self, $http, $resp, $host, $port, $queue, $path, $URI, $srv_name, $srv_version, $timeout ); $class = ref($class) || $class; $self = bless {}, $class; $srv_version = delete $args{server_version} || $self->version; $srv_name = delete $args{server_name} || $class; $self->{__version} = "$srv_name/$srv_version"; if (delete $args{no_http}) { $self->{__host} = delete $args{host} || ''; $self->{__port} = delete $args{port} || ''; } else { require HTTP::Daemon; $host = delete $args{host} || ''; $port = delete $args{port} || ''; $queue = delete $args{queue} || 5; $http = HTTP::Daemon->new( Reuse => 1, ($host ? (LocalHost => $host) : ()), ($port ? (LocalPort => $port) : ()), ($queue ? (Listen => $queue) : ()) ); return "${class}::new: Unable to create HTTP::Daemon object" unless $http; $URI = URI->new($http->url); $self->{__host} = $URI->host; $self->{__port} = $URI->port; $self->{__daemon} = $http; } # Create and store the cached response object for later cloning and use $resp = HTTP::Response->new(); return "${class}::new: Unable to create HTTP::Response object" unless $resp; $resp->header( # This is essentially the same string returned by the # default "identity" method that may be loaded from a # XPL file. But it hasn't been loaded yet, and may not # be, hence we set it here (possibly from option values) RPC_Server => $self->{__version}, RPC_Encoding => 'XML-RPC', # Set any other headers as well Accept => 'text/xml' ); $resp->content_type('text/xml'); $resp->code(RC_OK); $resp->message('OK'); $self->{__response} = $resp; # Basic (scalar) properties $self->{__path} = delete $args{path} || ''; $self->{__started} = 0; $self->{__method_table} = {}; $self->{__requests} = 0; $self->{__auto_methods} = delete $args{auto_methods} || 0; $self->{__auto_updates} = delete $args{auto_updates} || 0; $self->{__debug} = delete $args{debug} || 0; $self->{__xpl_path} = delete $args{xpl_path} || []; $self->{__timeout} = delete $args{timeout} || 10; $self->{__parser} = RPC::XML::ParserFactory->new( $args{parser} ? @{delete $args{parser}} : ()); # Set up the default methods unless requested not to $self->add_default_methods unless (delete $args{no_default}); # Compression support $self->{__compress} = ''; if (delete $args{no_compress}) { $self->{__compress} = ''; } else { $self->{__compress} = $COMPRESSION_AVAILABLE; # Add some more headers to the default response object for compression. # It looks wasteful to keep using the hash key, but it makes it easier # to change the string in just one place (above) if I have to. $resp->header(Accept_Encoding => $self->{__compress}) if $self->{__compress}; $self->{__compress_thresh} = delete $args{compress_thresh} || 4096; # Yes, I know this is redundant. It's for future expansion/flexibility. $self->{__compress_re} = $self->{__compress} ? qr/$self->{__compress}/ : qr/deflate/; } # Parameters to control the point at which messages are shunted to temp # files due to size, and where to home the temp files. Start with a size # threshhold of 1Meg and no specific dir (which will fall-through to the # tmpdir() method of File::Spec). $self->{__message_file_thresh} = delete $args{message_file_thresh} || 1048576; $self->{__message_temp_dir} = delete $args{message_temp_dir} || ''; # Set up the table of response codes/messages that will be used when the # server is sending a controlled error message to a client (as opposed to # something HTTP-level that is less within our control). $self->{__fault_table} = {%FAULT_TABLE}; if ($args{fault_code_base}) { my $base = delete $args{fault_code_base}; # Apply the numerical offset to all (current) error codes for my $key (keys %{$self->{__fault_table}}) { if (ref($self->{__fault_table}->{$key})) { # A ref is a listref where the first element is the code $self->{__fault_table}->{$key}->[0] += $base; } else { $self->{__fault_table}->{$key} += $base; } } } if ($args{fault_table}) { my $local_table = delete $args{fault_table}; # Merge any data from this table into the object's fault-table for my $key (keys %$local_table) { $self->{__fault_table}->{$key} = (ref $local_table->{$key}) ? [@{$local_table->{$key}}] : $local_table->{$key}; } } # Copy the remaining args over untouched $self->{$_} = $args{$_} for (keys %args); $self; } # Most of these tiny subs are accessors to the internal hash keys. They not # only control access to the internals, they ease sub-classing. sub version { $RPC::XML::Server::VERSION } sub INSTALL_DIR { $INSTALL_DIR } sub url { my $self = shift; return $self->{__daemon}->url if $self->{__daemon}; return unless (my $host = $self->host); my $path = $self->path; my $port = $self->port; if ($port == 443) { return "https://$host$path"; } elsif ($port == 80) { return "http://$host$path"; } else { return "http://$host:$port$path"; } } sub product_tokens { sprintf "%s/%s", (ref $_[0] || $_[0]), $_[0]->version; } # This fetches/sets the internal "started" timestamp. Unlike the other # plain-but-mutable attributes, this isn't set to the passed-value but # rather a non-null argument sets it from the current time. sub started { my ($self, $set) = @_; my $old = $self->{__started} || 0; $self->{__started} = time if $set; $old; } BEGIN { no strict 'refs'; ## no critic # These are mutable member values for which the logic only differs in # the name of the field to modify: for my $method (qw(compress_thresh message_file_thresh message_temp_dir)) { *$method = sub { my ($self, $set) = @_; my $old = $self->{"__$method"}; $self->{"__$method"} = $set if (defined $set); $old; } } # These are immutable member values, so this simple block applies to all for my $method ( qw(path host port requests response compress compress_re parser) ) { *$method = sub { shift->{"__$method"} } } } # Get/set the search path for XPL files sub xpl_path { my $self = shift; my $ret = $self->{__xpl_path}; $self->{__xpl_path} = $_[0] if ($_[0] and ref($_[0]) eq 'ARRAY'); $ret; } ############################################################################### # # Sub Name: add_method # # Description: Add a funtion-to-method mapping to the server object. # # Arguments: NAME IN/OUT TYPE DESCRIPTION # $self in ref Object to add to # $meth in scalar Hash ref of data or file name # # Returns: Success: $self # Failure: error string # ############################################################################### sub add_method { my $self = shift; my $meth = shift; my ($name, $val); my $me = ref($self) . '::add_method'; if (!ref($meth)) { $val = $self->method_from_file($meth); if (!ref($val)) { return "$me: Error loading from file $meth: $val"; } else { $meth = $val; } } elsif (ref($meth) eq 'HASH') { my $class = 'RPC::XML::' . ucfirst($meth->{type} || 'method'); $meth = $class->new($meth); } elsif (!(blessed $meth and $meth->isa('RPC::XML::Procedure'))) { return "$me: Method argument must be a file name, a hash " . 'reference or an object derived from RPC::XML::Procedure'; } # Do some sanity-checks return "$me: Method missing required data; check name, code and/or " . 'signature' unless $meth->is_valid; $name = $meth->name; $self->{__method_table}->{$name} = $meth; $self; } 1; =pod =head1 NAME RPC::XML::Server - A sample server implementation based on RPC::XML =head1 SYNOPSIS use RPC::XML::Server; ... $srv = RPC::XML::Server->new(port => 9000); # Several of these, most likely: $srv->add_method(...); ... $srv->server_loop; # Never returns =head1 DESCRIPTION This is a sample XML-RPC server built upon the B data classes, and using B and B for the communication layer. =head1 USAGE Use of the B is based on an object model. A server is instantiated from the class, methods (subroutines) are made public by adding them through the object interface, and then the server object is responsible for dispatching requests (and possibly for the HTTP listening, as well). =head2 Static Methods These methods are static to the package, and are used to provide external access to internal settings: =over 4 =item INSTALL_DIR Returns the directory that this module is installed into. This is used by methods such as C to locate the XPL files that are shipped with the distribution. =item version Returns the version string associated with this package. =item product_tokens This returns the identifying string for the server, in the format C consistent with other applications such as Apache and B. It is provided here as part of the compatibility with B that is required for effective integration with B. =back =head2 Methods The following are object (non-static) methods. Unless otherwise explicitly noted, all methods return the invoking object reference upon success, and a non-reference error string upon failure. See L below for details of how the server class manages gzip-based compression and expansion of messages. =over 4 =item new(OPTIONS) Creates a new object of the class and returns the blessed reference. Depending on the options, the object will contain some combination of an HTTP listener, a pre-populated B object, a B-generated object, and a dispatch table with the set of default methods pre-loaded. The options that B accepts are passed as a hash of key/value pairs (not a hash reference). The accepted options are: =over 4 =item B If passed with a C value, prevents the creation and storage of the B object. This allows for deployment of a server object in other environments. Note that if this is set, the B method described below will silently attempt to use the B module. =item B If passed with a C value, prevents the loading of the default methods provided with the B distribution. These may be later loaded using the B interface described later. The methods themselves are described below (see L<"The Default Methods Provided">). =item B =item B =item B =item B These four are specific to the HTTP-based nature of the server. The B argument sets the additional URI path information that clients would use to contact the server. Internally, it is not used except in outgoing status and introspection reports. The B, B and B arguments are passed to the B constructor if they are passed. They set the hostname, TCP/IP port, and socket listening queue, respectively. They may also be used if the server object tries to use B as an alternative server core. =item B If you plan to add methods to the server object by passing filenames to the C call, this argument may be used to specify one or more additional directories to be searched when the passed-in filename is a relative path. The value for this must be an array reference. See also B and B, below. =item B Specify a value (in seconds) for the B server to use as a timeout value when reading request data from an inbound connection. The default value is 10 seconds. This value is not used except by B. =item B If specified and set to a true value, enables the automatic searching for a requested remote method that is unknown to the server object handling the request. If set to "no" (or not set at all), then a request for an unknown function causes the object instance to report an error. If the routine is still not found, the error is reported. Enabling this is a security risk, and should only be permitted by a server administrator with fully informed acknowledgement and consent. =item B If specified and set to a "true" value, enables the checking of the modification time of the file from which a method was originally loaded. If the file has changed, the method is re-loaded before execution is handed off. As with the auto-loading of methods, this represents a security risk, and should only be permitted by a server administrator with fully informed acknowledgement and consent. =item B If this parameter is passed, its value is expected to be an array reference. The contents of that array are passed to the B method of the B class, which creates the parser object that the server object caches for its use. See the B manual page for a list of recognized parameters to the constructor. =item B If this key is passed, the value associated with it is assumed to be a numerical limit to the size of in-memory messages. Any out-bound request that would be larger than this when stringified is instead written to an anonynous temporary file, and spooled from there instead. This is useful for cases in which the request includes B objects that are themselves spooled from file-handles. This test is independent of compression, so even if compression of a request would drop it below this threshhold, it will be spooled anyway. The file itself is created via File::Temp with UNLINK=>1, so once it is freed the disk space is immediately freed. =item B If a message is to be spooled to a temporary file, this key can define a specific directory in which to open those files. If this is not given, then the C method from the B package is used, instead. =item B Specify a base integer value that is added to the numerical codes for all faults the server can return. See L for the list of faults that are built-in to the server class. This allows an application to "move" the B pre-defined fault codes out of the way of codes that the application itself may generate. Note that this value is B applied to any faults specified via the next option, C. It is assumed that the developer has already applied any offset to those codes. =item B Specify one or more fault types to either add to or override the built-in set of faults for the server object. The value of this parameter is a hash reference whose keys are the fault type and whose values are either a scalar (which is taken to be the numerical code) or a list reference with two elements (the code followed by the string). See L for the list of faults that are built-in to the server class, and for more information on defining your own. =back Any other keys in the options hash not explicitly used by the constructor are copied over verbatim onto the object, for the benefit of sub-classing this class. All internal keys are prefixed with C<__> to avoid confusion. Feel free to use this prefix only if you wish to re-introduce confusion. =item url This returns the HTTP URL that the server will be responding to, when it is in the connection-accept loop. If the server object was created without a built-in HTTP listener, then this method returns C. =item requests Returns the number of requests this server object has marshalled. Note that in multi-process environments (such as Apache or Net::Server::PreFork) the value returned will only reflect the messages dispatched by the specific process itself. =item response Each instance of this class (and any subclasses that do not completely override the C method) creates and stores an instance of B, which is then used by the B or B processing loops in constructing the response to clients. The response object has all common headers pre-set for efficiency. This method returns a reference to that object. =item started([BOOL]) Gets and possibly sets the clock-time when the server starts accepting connections. If a value is passed that evaluates to true, then the current clock time is marked as the starting time. In either case, the current value is returned. The clock-time is based on the internal B