From d675dbc2798ba922adb3afdffc6d8a2d9d6e5139 Mon Sep 17 00:00:00 2001 From: matz Date: Sat, 19 Jul 2003 10:05:54 +0000 Subject: [PATCH] * lib/xmlrpc: import. * eval.c (thgroup_add): should return group for terminated thread case. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@4102 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- ChangeLog | 5 + MANIFEST | 11 + config.guess | 2 +- eval.c | 2 +- lib/gserver.rb | 175 ++++++++ lib/xmlrpc/base64.rb | 81 ++++ lib/xmlrpc/client.rb | 570 ++++++++++++++++++++++++++ lib/xmlrpc/config.rb | 40 ++ lib/xmlrpc/create.rb | 280 +++++++++++++ lib/xmlrpc/datetime.rb | 138 +++++++ lib/xmlrpc/httpserver.rb | 178 +++++++++ lib/xmlrpc/marshal.rb | 76 ++++ lib/xmlrpc/parser.rb | 803 +++++++++++++++++++++++++++++++++++++ lib/xmlrpc/server.rb | 839 +++++++++++++++++++++++++++++++++++++++ lib/xmlrpc/utils.rb | 172 ++++++++ 15 files changed, 3370 insertions(+), 2 deletions(-) create mode 100644 lib/gserver.rb create mode 100644 lib/xmlrpc/base64.rb create mode 100644 lib/xmlrpc/client.rb create mode 100644 lib/xmlrpc/config.rb create mode 100644 lib/xmlrpc/create.rb create mode 100644 lib/xmlrpc/datetime.rb create mode 100644 lib/xmlrpc/httpserver.rb create mode 100644 lib/xmlrpc/marshal.rb create mode 100644 lib/xmlrpc/parser.rb create mode 100644 lib/xmlrpc/server.rb create mode 100644 lib/xmlrpc/utils.rb diff --git a/ChangeLog b/ChangeLog index cca0f235bb..b9242de41a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,6 +4,11 @@ Sat Jul 19 19:03:24 2003 Takaaki Uematsu Sat Jul 19 11:27:25 2003 Yukihiro Matsumoto + * lib/xmlrpc: import. + + * eval.c (thgroup_add): should return group for terminated thread + case. + * eval.c (thgroup_add): do not raise ThreadError on terminated thread addition for compatibility. just warning. diff --git a/MANIFEST b/MANIFEST index 170951393b..e63b323a27 100644 --- a/MANIFEST +++ b/MANIFEST @@ -141,6 +141,7 @@ lib/forwardable.rb lib/ftools.rb lib/getoptlong.rb lib/getopts.rb +lib/gserver.rb lib/importenv.rb lib/ipaddr.rb lib/irb.rb @@ -317,6 +318,16 @@ lib/rexml/parsers/sax2parser.rb lib/rexml/parsers/streamparser.rb lib/rexml/parsers/ultralightparser.rb lib/rexml/parsers/xpathparser.rb +lib/xmlrpc/base64.rb +lib/xmlrpc/client.rb +lib/xmlrpc/config.rb +lib/xmlrpc/create.rb +lib/xmlrpc/datetime.rb +lib/xmlrpc/httpserver.rb +lib/xmlrpc/marshal.rb +lib/xmlrpc/parser.rb +lib/xmlrpc/server.rb +lib/xmlrpc/utils.rb lib/yaml.rb lib/yaml/basenode.rb lib/yaml/constants.rb diff --git a/config.guess b/config.guess index 73ea71ed8b..844739cdaf 100644 --- a/config.guess +++ b/config.guess @@ -1119,7 +1119,7 @@ EOF *86) UNAME_PROCESSOR=i686 ;; powerpc) UNAME_PROCESSOR=powerpc ;; esac - echo ${UNAME_PROCESSOR}-apple-darwin${UNAME_RELEASE} + echo ${UNAME_PROCESSOR}-apple-darwin exit 0 ;; *:procnto*:*:* | *:QNX:[0123456789]*:*) UNAME_PROCESSOR=`uname -p` diff --git a/eval.c b/eval.c index 1a774323ee..57260f9c44 100644 --- a/eval.c +++ b/eval.c @@ -9944,7 +9944,7 @@ thgroup_add(group, thread) if (!th->thgroup) { rb_warn("terminated thread"); - return; + return group; } if (OBJ_FROZEN(th->thgroup)) { rb_raise(rb_eThreadError, "can't move from the frozen thread group"); diff --git a/lib/gserver.rb b/lib/gserver.rb new file mode 100644 index 0000000000..14f765a90c --- /dev/null +++ b/lib/gserver.rb @@ -0,0 +1,175 @@ +# Copyright (C) 2001 John W. Small All Rights Reserved +# mailto:jsmall@laser.net subject:ruby-generic-server +# Freeware + +require "socket" +require "thread" + +class GServer + + DEFAULT_HOST = "127.0.0.1" + + def serve(io) + end + + @@services = {} # Hash of opened ports, i.e. services + @@servicesMutex = Mutex.new + + def GServer.stop(port, host = DEFAULT_HOST) + @@servicesMutex.synchronize { + @@services[host][port].stop + } + end + + def GServer.in_service?(port, host = DEFAULT_HOST) + @@services.has_key?(host) and + @@services[host].has_key?(port) + end + + def stop + @connectionsMutex.synchronize { + if @tcpServerThread + @tcpServerThread.raise "stop" + end + } + end + + def stopped? + @tcpServerThread == nil + end + + def shutdown + @shutdown = true + end + + def connections + @connections.size + end + + def join + @tcpServerThread.join if @tcpServerThread + end + + attr_reader :port, :host, :maxConnections + attr_accessor :stdlog, :audit, :debug + + def connecting(client) + addr = client.peeraddr + log("#{self.class.to_s} #{@host}:#{@port} client:#{addr[1]} " + + "#{addr[2]}<#{addr[3]}> connect") + true + end + + def disconnecting(clientPort) + log("#{self.class.to_s} #{@host}:#{@port} " + + "client:#{clientPort} disconnect") + end + + protected :connecting, :disconnecting + + def starting() + log("#{self.class.to_s} #{@host}:#{@port} start") + end + + def stopping() + log("#{self.class.to_s} #{@host}:#{@port} stop") + end + + protected :starting, :stopping + + def error(detail) + log(detail.backtrace.join("\n")) + end + + def log(msg) + if @stdlog + @stdlog.puts("[#{Time.new.ctime}] %s" % msg) + @stdlog.flush + end + end + + protected :error, :log + + def initialize(port, host = DEFAULT_HOST, maxConnections = 4, + stdlog = $stderr, audit = false, debug = false) + @tcpServerThread = nil + @port = port + @host = host + @maxConnections = maxConnections + @connections = [] + @connectionsMutex = Mutex.new + @connectionsCV = ConditionVariable.new + @stdlog = stdlog + @audit = audit + @debug = debug + end + + def start(maxConnections = -1) + raise "running" if !stopped? + @shutdown = false + @maxConnections = maxConnections if maxConnections > 0 + @@servicesMutex.synchronize { + if GServer.in_service?(@port,@host) + raise "Port already in use: #{host}:#{@port}!" + end + @tcpServer = TCPServer.new(@host,@port) + @port = @tcpServer.addr[1] + @@services[@host] = {} unless @@services.has_key?(@host) + @@services[@host][@port] = self; + } + @tcpServerThread = Thread.new { + begin + starting if @audit + while !@shutdown + @connectionsMutex.synchronize { + while @connections.size >= @maxConnections + @connectionsCV.wait(@connectionsMutex) + end + } + client = @tcpServer.accept + @connections << Thread.new(client) { |myClient| + begin + myPort = myClient.peeraddr[1] + serve(myClient) if !@audit or connecting(myClient) + rescue => detail + error(detail) if @debug + ensure + begin + myClient.close + rescue + end + @connectionsMutex.synchronize { + @connections.delete(Thread.current) + @connectionsCV.signal + } + disconnecting(myPort) if @audit + end + } + end + rescue => detail + error(detail) if @debug + ensure + begin + @tcpServer.close + rescue + end + if @shutdown + @connectionsMutex.synchronize { + while @connections.size > 0 + @connectionsCV.wait(@connectionsMutex) + end + } + else + @connections.each { |c| c.raise "stop" } + end + @tcpServerThread = nil + @@servicesMutex.synchronize { + @@services[@host].delete(@port) + } + stopping if @audit + end + } + self + end + +end diff --git a/lib/xmlrpc/base64.rb b/lib/xmlrpc/base64.rb new file mode 100644 index 0000000000..f9a21c703a --- /dev/null +++ b/lib/xmlrpc/base64.rb @@ -0,0 +1,81 @@ +=begin += xmlrpc/base64.rb +Copyright (C) 2001, 2002, 2003 by Michael Neumann (mneumann@ntecs.de) + +Released under the same term of license as Ruby. + += Classes +* (()) + += XMLRPC::Base64 +== Description +This class is necessary for (('xmlrpc4r')) to determine that a string should +be transmitted base64-encoded and not as a raw-string. +You can use (({XMLRPC::Base64})) on the client and server-side as a +parameter and/or return-value. + +== Class Methods +--- XMLRPC::Base64.new( str, state = :dec ) + Creates a new (({XMLRPC::Base64})) instance with string ((|str|)) as the + internal string. When ((|state|)) is (({:dec})) it assumes that the + string ((|str|)) is not in base64 format (perhaps already decoded), + otherwise if ((|state|)) is (({:enc})) it decodes ((|str|)) + and stores it as the internal string. + +--- XMLRPC::Base64.decode( str ) + Decodes string ((|str|)) with base64 and returns that value. + +--- XMLRPC::Base64.encode( str ) + Encodes string ((|str|)) with base64 and returns that value. + +== Instance Methods +--- XMLRPC::Base64#decoded + Returns the internal string decoded. + +--- XMLRPC::Base64#encoded + Returns the internal string encoded with base64. + +=end + +module XMLRPC + +class Base64 + + def initialize(str, state = :dec) + case state + when :enc + @str = Base64.decode(str) + when :dec + @str = str + else + raise ArgumentError, "wrong argument; either :enc or :dec" + end + end + + def decoded + @str + end + + def encoded + Base64.encode(@str) + end + + + def Base64.decode(str) + str.gsub(/\s+/, "").unpack("m")[0] + end + + def Base64.encode(str) + [str].pack("m") + end + +end + + +end # module XMLRPC + + +=begin += History + $Id$ +=end diff --git a/lib/xmlrpc/client.rb b/lib/xmlrpc/client.rb new file mode 100644 index 0000000000..198819ba66 --- /dev/null +++ b/lib/xmlrpc/client.rb @@ -0,0 +1,570 @@ +=begin += xmlrpc/client.rb +Copyright (C) 2001, 2002, 2003 by Michael Neumann (mneumann@ntecs.de) + +Released under the same term of license as Ruby. + += Classes +* (()) +* (()) + + += XMLRPC::Client +== Synopsis + require "xmlrpc/client" + + server = XMLRPC::Client.new("www.ruby-lang.org", "/RPC2", 80) + begin + param = server.call("michael.add", 4, 5) + puts "4 + 5 = #{param}" + rescue XMLRPC::FaultException => e + puts "Error:" + puts e.faultCode + puts e.faultString + end + +or + + require "xmlrpc/client" + + server = XMLRPC::Client.new("www.ruby-lang.org", "/RPC2", 80) + ok, param = server.call2("michael.add", 4, 5) + if ok then + puts "4 + 5 = #{param}" + else + puts "Error:" + puts param.faultCode + puts param.faultString + end + +== Description +Class (({XMLRPC::Client})) provides remote procedure calls to a XML-RPC server. +After setting the connection-parameters with (()) which +creates a new (({XMLRPC::Client})) instance, you can execute a remote procedure +by sending the (()) or (()) +message to this new instance. The given parameters indicate which method to +call on the remote-side and of course the parameters for the remote procedure. + +== Class Methods +--- XMLRPC::Client.new( host=nil, path=nil, port=nil, proxy_host=nil, proxy_port=nil, user=nil, password=nil, use_ssl=false, timeout =nil) + Creates an object which represents the remote XML-RPC server on the + given host ((|host|)). If the server is CGI-based, ((|path|)) is the + path to the CGI-script, which will be called, otherwise (in the + case of a standalone server) ((|path|)) should be (({"/RPC2"})). + ((|port|)) is the port on which the XML-RPC server listens. + If ((|proxy_host|)) is given, then a proxy server listening at + ((|proxy_host|)) is used. ((|proxy_port|)) is the port of the + proxy server. + + Default values for ((|host|)), ((|path|)) and ((|port|)) are 'localhost', '/RPC2' and + '80' respectively using SSL '443'. + + If ((|user|)) and ((|password|)) are given, each time a request is send, + a Authorization header is send. Currently only Basic Authentification is + implemented no Digest. + + If ((|use_ssl|)) is set to (({true})), comunication over SSL is enabled. + Note, that you need the SSL package from RAA installed. + + Parameter ((|timeout|)) is the time to wait for a XML-RPC response, defaults to 30. + +--- XMLRPC::Client.new2( uri, proxy=nil, timeout=nil) +: uri + URI specifying protocol (http or https), host, port, path, user and password. + Example: https://user:password@host:port/path + +: proxy + Is of the form "host:port". + +: timeout + Defaults to 30. + +--- XMLRPC::Client.new3( hash={} ) + Parameter ((|hash|)) has following case-insensitive keys: + * host + * path + * port + * proxy_host + * proxy_port + * user + * password + * use_ssl + * timeout + + Calls (()) with the corresponding values. + +== Instance Methods +--- XMLRPC::Client#call( method, *args ) + Invokes the method named ((|method|)) with the parameters given by + ((|args|)) on the XML-RPC server. + The parameter ((|method|)) is converted into a (({String})) and should + be a valid XML-RPC method-name. + Each parameter of ((|args|)) must be of one of the following types, + where (({Hash})), (({Struct})) and (({Array})) can contain any of these listed ((:types:)): + * (({Fixnum})), (({Bignum})) + * (({TrueClass})), (({FalseClass})) ((({true})), (({false}))) + * (({String})), (({Symbol})) + * (({Float})) + * (({Hash})), (({Struct})) + * (({Array})) + * (({Date})), (({Time})), (({XMLRPC::DateTime})) + * (({XMLRPC::Base64})) + * A Ruby object which class includes XMLRPC::Marshallable (only if Config::ENABLE_MARSHALLABLE is (({true}))). + That object is converted into a hash, with one additional key/value pair "___class___" which contains the class name + for restoring later that object. + + The method returns the return-value from the RPC + ((-stands for Remote Procedure Call-)). + The type of the return-value is one of the above shown, + only that a (({Bignum})) is only allowed when it fits in 32-bit and + that a XML-RPC (('dateTime.iso8601')) type is always returned as + a ((<(({XMLRPC::DateTime}))|URL:datetime.html>)) object and + a (({Struct})) is never returned, only a (({Hash})), the same for a (({Symbol})), where + always a (({String})) is returned. + A (({XMLRPC::Base64})) is returned as a (({String})) from xmlrpc4r version 1.6.1 on. + + If the remote procedure returned a fault-structure, then a + (({XMLRPC::FaultException})) exception is raised, which has two accessor-methods + (({faultCode})) and (({faultString})) of type (({Integer})) and (({String})). + +--- XMLRPC::Client#call2( method, *args ) + The difference between this method and (()) is, that + this method do ((*not*)) raise a (({XMLRPC::FaultException})) exception. + The method returns an array of two values. The first value indicates if + the second value is a return-value ((({true}))) or an object of type + (({XMLRPC::FaultException})). + Both are explained in (()). + +--- XMLRPC::Client#multicall( *methods ) + You can use this method to execute several methods on a XMLRPC server which supports + the multi-call extension. + Example: + + s.multicall( + ['michael.add', 3, 4], + ['michael.sub', 4, 5] + ) + # => [7, -1] + +--- XMLRPC::Client#multicall2( *methods ) + Same as (()), but returns like (()) two parameters + instead of raising an (({XMLRPC::FaultException})). + +--- XMLRPC::Client#proxy( prefix, *args ) + Returns an object of class (({XMLRPC::Client::Proxy})), initialized with + ((|prefix|)) and ((|args|)). A proxy object returned by this method behaves + like (()), i.e. a call on that object will raise a + (({XMLRPC::FaultException})) when a fault-structure is returned by that call. + +--- XMLRPC::Client#proxy2( prefix, *args ) + Almost the same like (()) only that a call on the returned + (({XMLRPC::Client::Proxy})) object behaves like (()), i.e. + a call on that object will return two parameters. + + + + +--- XMLRPC::Client#call_async(...) +--- XMLRPC::Client#call2_async(...) +--- XMLRPC::Client#multicall_async(...) +--- XMLRPC::Client#multicall2_async(...) +--- XMLRPC::Client#proxy_async(...) +--- XMLRPC::Client#proxy2_async(...) + In contrast to corresponding methods without "_async", these can be + called concurrently and use for each request a new connection, where the + non-asynchronous counterparts use connection-alive (one connection for all requests) + if possible. + + Note, that you have to use Threads to call these methods concurrently. + The following example calls two methods concurrently: + + Thread.new { + p client.call_async("michael.add", 4, 5) + } + + Thread.new { + p client.call_async("michael.div", 7, 9) + } + + +--- XMLRPC::Client#timeout +--- XMLRPC::Client#user +--- XMLRPC::Client#password + Return the corresponding attributes. + +--- XMLRPC::Client#timeout= (new_timeout) +--- XMLRPC::Client#user= (new_user) +--- XMLRPC::Client#password= (new_password) + Set the corresponding attributes. + + +--- XMLRPC::Client#set_writer( writer ) + Sets the XML writer to use for generating XML output. + Should be an instance of a class from module (({XMLRPC::XMLWriter})). + If this method is not called, then (({XMLRPC::Config::DEFAULT_WRITER})) is used. + +--- XMLRPC::Client#set_parser( parser ) + Sets the XML parser to use for parsing XML documents. + Should be an instance of a class from module (({XMLRPC::XMLParser})). + If this method is not called, then (({XMLRPC::Config::DEFAULT_PARSER})) is used. + + += XMLRPC::Client::Proxy +== Synopsis + require "xmlrpc/client" + + server = XMLRPC::Client.new("www.ruby-lang.org", "/RPC2", 80) + + michael = server.proxy("michael") + michael2 = server.proxy("michael", 4) + + # both calls should return the same value '9'. + p michael.add(4,5) + p michael2.add(5) + +== Description +Class (({XMLRPC::Client::Proxy})) makes XML-RPC calls look nicer! +You can call any method onto objects of that class - the object handles +(({method_missing})) and will forward the method call to a XML-RPC server. +Don't use this class directly, but use instead method (()) or +(()). + +== Class Methods +--- XMLRPC::Client::Proxy.new( server, prefix, args=[], meth=:call, delim="." ) + Creates an object which provides (({method_missing})). + + ((|server|)) must be of type (({XMLRPC::Client})), which is the XML-RPC server to be used + for a XML-RPC call. ((|prefix|)) and ((|delim|)) will be prepended to the methodname + called onto this object. + + Parameter ((|meth|)) is the method (call, call2, call_async, call2_async) to use for + a RPC. + + ((|args|)) are arguments which are automatically given + to every XML-RPC call before the arguments provides through (({method_missing})). + +== Instance Methods +Every method call is forwarded to the XML-RPC server defined in (()). + +Note: Inherited methods from class (({Object})) cannot be used as XML-RPC names, because they get around +(({method_missing})). + + + += History + $Id$ + +=end + + + +require "xmlrpc/parser" +require "xmlrpc/create" +require "xmlrpc/config" +require "xmlrpc/utils" # ParserWriterChooseMixin +require "net/http" + +module XMLRPC + + class Client + + USER_AGENT = "XMLRPC::Client (Ruby #{RUBY_VERSION})" + + include ParserWriterChooseMixin + include ParseContentType + + + # Constructors ------------------------------------------------------------------- + + def initialize(host=nil, path=nil, port=nil, proxy_host=nil, proxy_port=nil, + user=nil, password=nil, use_ssl=nil, timeout=nil) + + @host = host || "localhost" + @path = path || "/RPC2" + @proxy_host = proxy_host + @proxy_port = proxy_port + @proxy_host ||= 'localhost' if @proxy_port != nil + @proxy_port ||= 8080 if @proxy_host != nil + @use_ssl = use_ssl || false + @timeout = timeout || 30 + + if use_ssl + require "net/https" + @port = port || 443 + else + @port = port || 80 + end + + @user, @password = user, password + + set_auth + + # convert ports to integers + @port = @port.to_i if @port != nil + @proxy_port = @proxy_port.to_i if @proxy_port != nil + + # HTTP object for synchronous calls + Net::HTTP.version_1_2 + @http = Net::HTTP.new(@host, @port, @proxy_host, @proxy_port) + @http.use_ssl = @use_ssl if @use_ssl + @http.read_timeout = @timeout + @http.open_timeout = @timeout + + @parser = nil + @create = nil + end + + + def self.new2(uri, proxy=nil, timeout=nil) + if match = /^([^:]+):\/\/(([^@]+)@)?([^\/]+)(\/.*)?$/.match(uri) + proto = match[1] + user, passwd = (match[3] || "").split(":") + host, port = match[4].split(":") + path = match[5] + + if proto != "http" and proto != "https" + raise "Wrong protocol specified. Only http or https allowed!" + end + + else + raise "Wrong URI as parameter!" + end + + proxy_host, proxy_port = (proxy || "").split(":") + + self.new(host, path, port, proxy_host, proxy_port, user, passwd, (proto == "https"), timeout) + end + + + def self.new3(hash={}) + + # convert all keys into lowercase strings + h = {} + hash.each { |k,v| h[k.to_s.downcase] = v } + + self.new(h['host'], h['path'], h['port'], h['proxy_host'], h['proxy_port'], h['user'], h['password'], + h['use_ssl'], h['timeout']) + end + + + # Attribute Accessors ------------------------------------------------------------------- + + attr_reader :timeout, :user, :password + + def timeout=(new_timeout) + @timeout = new_timeout + @http.read_timeout = @timeout + @http.open_timeout = @timeout + end + + def user=(new_user) + @user = new_user + set_auth + end + + def password=(new_password) + @password = new_password + set_auth + end + + # Call methods -------------------------------------------------------------- + + def call(method, *args) + ok, param = call2(method, *args) + if ok + param + else + raise param + end + end + + def call2(method, *args) + request = create().methodCall(method, *args) + data = do_rpc(request, false) + parser().parseMethodResponse(data) + end + + def call_async(method, *args) + ok, param = call2_async(method, *args) + if ok + param + else + raise param + end + end + + def call2_async(method, *args) + request = create().methodCall(method, *args) + data = do_rpc(request, true) + parser().parseMethodResponse(data) + end + + + # Multicall methods -------------------------------------------------------------- + + def multicall(*methods) + ok, params = multicall2(*methods) + if ok + params + else + raise params + end + end + + def multicall2(*methods) + gen_multicall(methods, false) + end + + def multicall_async(*methods) + ok, params = multicall2_async(*methods) + if ok + params + else + raise params + end + end + + def multicall2_async(*methods) + gen_multicall(methods, true) + end + + + # Proxy generating methods ------------------------------------------ + + def proxy(prefix, *args) + Proxy.new(self, prefix, args, :call) + end + + def proxy2(prefix, *args) + Proxy.new(self, prefix, args, :call2) + end + + def proxy_async(prefix, *args) + Proxy.new(self, prefix, args, :call_async) + end + + def proxy2_async(prefix, *args) + Proxy.new(self, prefix, args, :call2_async) + end + + + private # ---------------------------------------------------------- + + def set_auth + if @user.nil? + @auth = nil + else + a = "#@user" + a << ":#@password" if @password != nil + @auth = ("Basic " + [a].pack("m")).chomp + end + end + + def do_rpc(request, async=false) + header = { + "User-Agent" => USER_AGENT, + "Content-Type" => "text/xml", + "Content-Length" => request.size.to_s, + "Connection" => (async ? "close" : "keep-alive") + } + + if @auth != nil + # add authorization header + header["Authorization"] = @auth + end + + resp = nil + + if async + # use a new HTTP object for each call + Net::HTTP.version_1_2 + http = Net::HTTP.new(@host, @port, @proxy_host, @proxy_port) + http.use_ssl = @use_ssl if @use_ssl + http.read_timeout = @timeout + http.open_timeout = @timeout + + # post request + http.start { + resp = http.post2(@path, request, header) + } + else + # reuse the HTTP object for each call => connection alive is possible + + # post request + resp = @http.post2(@path, request, header) + end + + data = resp.body + + if resp.code == "401" + # Authorization Required + raise "Authorization failed.\nHTTP-Error: #{resp.code} #{resp.message}" + elsif resp.code[0,1] != "2" + raise "HTTP-Error: #{resp.code} #{resp.message}" + end + + ct = parse_content_type(resp["Content-Type"]).first + if ct != "text/xml" + if ct == "text/html" + raise "Wrong content-type: \n#{data}" + else + raise "Wrong content-type" + end + end + + expected = resp["Content-Length"] || "" + if data.nil? or data.size == 0 + raise "Wrong size. Was #{data.size}, should be #{expected}" + elsif expected.to_i != data.size and resp["Transfer-Encoding"].nil? + raise "Wrong size. Was #{data.size}, should be #{expected}" + end + + return data + end + + def gen_multicall(methods=[], async=false) + meth = :call2 + meth = :call2_async if async + + ok, params = self.send(meth, "system.multicall", + methods.collect {|m| {'methodName' => m[0], 'params' => m[1..-1]} } + ) + + if ok + params = params.collect do |param| + if param.is_a? Array + param[0] + elsif param.is_a? Hash + XMLRPC::FaultException.new(param["faultCode"], param["faultString"]) + else + raise "Wrong multicall return value" + end + end + end + + return ok, params + end + + + + class Proxy + + def initialize(server, prefix, args=[], meth=:call, delim=".") + @server = server + @prefix = prefix + delim + @args = args + @meth = meth + end + + def method_missing(mid, *args) + pre = @prefix + mid.to_s + arg = @args + args + @server.send(@meth, pre, *arg) + end + + end # class Proxy + + end # class Client + +end # module XMLRPC + diff --git a/lib/xmlrpc/config.rb b/lib/xmlrpc/config.rb new file mode 100644 index 0000000000..c4d2c41aac --- /dev/null +++ b/lib/xmlrpc/config.rb @@ -0,0 +1,40 @@ +# +# $Id$ +# Configuration file for XML-RPC for Ruby +# + +module XMLRPC + + module Config + + DEFAULT_WRITER = XMLWriter::Simple # or XMLWriter::XMLParser + + # available parser: + # * XMLParser::NQXMLTreeParser + # * XMLParser::NQXMLStreamParser + # * XMLParser::XMLTreeParser + # * XMLParser::XMLStreamParser (fastest) + # * XMLParser::REXMLStreamParser + # * XMLParser::XMLScanStreamParser + DEFAULT_PARSER = XMLParser::REXMLStreamParser + + # enable tag + ENABLE_NIL_CREATE = false + ENABLE_NIL_PARSER = false + + # allows integers greater than 32-bit if true + ENABLE_BIGINT = false + + # enable marshalling ruby objects which include XMLRPC::Marshallable + ENABLE_MARSHALLING = true + + # enable multiCall extension by default + ENABLE_MULTICALL = false + + # enable Introspection extension by default + ENABLE_INTROSPECTION = false + + end + +end + diff --git a/lib/xmlrpc/create.rb b/lib/xmlrpc/create.rb new file mode 100644 index 0000000000..072e72ab46 --- /dev/null +++ b/lib/xmlrpc/create.rb @@ -0,0 +1,280 @@ +# +# Creates XML-RPC call/response documents +# +# Copyright (C) 2001, 2002, 2003 by Michael Neumann (mneumann@ntecs.de) +# +# $Id$ +# + +require "date" +require "xmlrpc/base64" + +module XMLRPC + + module XMLWriter + + class Abstract + def ele(name, *children) + element(name, nil, *children) + end + + def tag(name, txt) + element(name, nil, text(txt)) + end + end + + + class Simple < Abstract + + def document_to_str(doc) + doc + end + + def document(*params) + params.join("") + end + + def pi(name, *params) + "" + end + + def element(name, attrs, *children) + raise "attributes not yet implemented" unless attrs.nil? + if children.empty? + "<#{name}/>" + else + "<#{name}>" + children.join("") + "" + end + end + + def text(txt) + cleaned = txt.dup + cleaned.gsub!(/&/, '&') + cleaned.gsub!(//, '>') + cleaned + end + + end # class Simple + + + class XMLParser < Abstract + + def initialize + require "xmltreebuilder" + end + + def document_to_str(doc) + doc.to_s + end + + def document(*params) + XML::SimpleTree::Document.new(*params) + end + + def pi(name, *params) + XML::SimpleTree::ProcessingInstruction.new(name, *params) + end + + def element(name, attrs, *children) + XML::SimpleTree::Element.new(name, attrs, *children) + end + + def text(txt) + XML::SimpleTree::Text.new(txt) + end + + end # class XMLParser + + end # module XMLWriter + + class Create + + def initialize(xml_writer = nil) + @writer = xml_writer || Config::DEFAULT_WRITER.new + end + + + def methodCall(name, *params) + name = name.to_s + + if name !~ /[a-zA-Z0-9_.:\/]+/ + raise ArgumentError, "Wrong XML-RPC method-name" + end + + parameter = params.collect do |param| + @writer.ele("param", conv2value(param)) + end + + tree = @writer.document( + @writer.pi("xml", 'version="1.0"'), + @writer.ele("methodCall", + @writer.tag("methodName", name), + @writer.ele("params", *parameter) + ) + ) + + @writer.document_to_str(tree) + "\n" + end + + + + # + # generates a XML-RPC methodResponse document + # + # if is_ret == false then the params array must + # contain only one element, which is a structure + # of a fault return-value. + # + # if is_ret == true then a normal + # return-value of all the given params is created. + # + def methodResponse(is_ret, *params) + + if is_ret + resp = params.collect do |param| + @writer.ele("param", conv2value(param)) + end + + resp = [@writer.ele("params", *resp)] + else + if params.size != 1 or params[0] === XMLRPC::FaultException + raise ArgumentError, "no valid fault-structure given" + end + resp = @writer.ele("fault", conv2value(params[0].to_h)) + end + + + tree = @writer.document( + @writer.pi("xml", 'version="1.0"'), + @writer.ele("methodResponse", resp) + ) + + @writer.document_to_str(tree) + "\n" + end + + + + ##################################### + private + ##################################### + + # + # converts a Ruby object into + # a XML-RPC tag + # + def conv2value(param) + + val = case param + when Fixnum + @writer.tag("i4", param.to_s) + + when Bignum + if Config::ENABLE_BIGINT + @writer.tag("i4", param.to_s) + else + if param >= -(2**31) and param <= (2**31-1) + @writer.tag("i4", param.to_s) + else + raise "Bignum is too big! Must be signed 32-bit integer!" + end + end + when TrueClass, FalseClass + @writer.tag("boolean", param ? "1" : "0") + + when String + @writer.tag("string", param) + + when Symbol + @writer.tag("string", param.to_s) + + when NilClass + if Config::ENABLE_NIL_CREATE + @writer.ele("nil") + else + raise "Wrong type NilClass. Not allowed!" + end + + when Float + @writer.tag("double", param.to_s) + + when Struct + h = param.members.collect do |key| + value = param[key] + @writer.ele("member", + @writer.tag("name", key.to_s), + conv2value(value) + ) + end + + @writer.ele("struct", *h) + + when Hash + # TODO: can a Hash be empty? + + h = param.collect do |key, value| + @writer.ele("member", + @writer.tag("name", key.to_s), + conv2value(value) + ) + end + + @writer.ele("struct", *h) + + when Array + # TODO: can an Array be empty? + a = param.collect {|v| conv2value(v) } + + @writer.ele("array", + @writer.ele("data", *a) + ) + + when Date + t = param + @writer.tag("dateTime.iso8601", + format("%.4d%02d%02dT00:00:00", t.year, t.month, t.day)) + + when Time + @writer.tag("dateTime.iso8601", param.strftime("%Y%m%dT%H:%M:%S")) + + when XMLRPC::DateTime + @writer.tag("dateTime.iso8601", + format("%.4d%02d%02dT%02d:%02d:%02d", *param.to_a)) + + when XMLRPC::Base64 + @writer.tag("base64", param.encoded) + + else + if Config::ENABLE_MARSHALLING and param.class.included_modules.include? XMLRPC::Marshallable + # convert Ruby object into Hash + ret = {"___class___" => param.class.name} + param.__get_instance_variables.each {|name, val| + if val.nil? + ret[name] = val if Config::ENABLE_NIL_CREATE + else + ret[name] = val + end + } + return conv2value(ret) + else + ok, pa = wrong_type(param) + if ok + return conv2value(pa) + else + raise "Wrong type!" + end + end + end + + @writer.ele("value", val) + end + + def wrong_type(value) + false + end + + + end # class Create + +end # module XMLRPC + diff --git a/lib/xmlrpc/datetime.rb b/lib/xmlrpc/datetime.rb new file mode 100644 index 0000000000..e3bc6943f0 --- /dev/null +++ b/lib/xmlrpc/datetime.rb @@ -0,0 +1,138 @@ +=begin += xmlrpc/datetime.rb +Copyright (C) 2001, 2002, 2003 by Michael Neumann (mneumann@ntecs.de) + +Released under the same term of license as Ruby. + += Classes +* (()) + += XMLRPC::DateTime +== Description +This class is important to handle XMLRPC (('dateTime.iso8601')) values, +correcly, because normal UNIX-dates (class (({Date}))) only handle dates +from year 1970 on, and class (({Time})) handles dates without the time +component. (({XMLRPC::DateTime})) is able to store a XMLRPC +(('dateTime.iso8601')) value correctly. + +== Class Methods +--- XMLRPC::DateTime.new( year, month, day, hour, min, sec ) + Creates a new (({XMLRPC::DateTime})) instance with the + parameters ((|year|)), ((|month|)), ((|day|)) as date and + ((|hour|)), ((|min|)), ((|sec|)) as time. + Raises (({ArgumentError})) if a parameter is out of range, or ((|year|)) is not + of type (({Integer})). + +== Instance Methods +--- XMLRPC::DateTime#year +--- XMLRPC::DateTime#month +--- XMLRPC::DateTime#day +--- XMLRPC::DateTime#hour +--- XMLRPC::DateTime#min +--- XMLRPC::DateTime#sec + Return the value of the specified date/time component. + +--- XMLRPC::DateTime#mon + Alias for (()). + +--- XMLRPC::DateTime#year=( value ) +--- XMLRPC::DateTime#month=( value ) +--- XMLRPC::DateTime#day=( value ) +--- XMLRPC::DateTime#hour=( value ) +--- XMLRPC::DateTime#min=( value ) +--- XMLRPC::DateTime#sec=( value ) + Set ((|value|)) as the new date/time component. + Raises (({ArgumentError})) if ((|value|)) is out of range, or in the case + of (({XMLRPC::DateTime#year=})) if ((|value|)) is not of type (({Integer})). + +--- XMLRPC::DateTime#mon=( value ) + Alias for (()). + +--- XMLRPC::DateTime#to_time + Return a (({Time})) object of the date/time which (({self})) represents. + If the (('year')) is below 1970, this method returns (({nil})), + because (({Time})) cannot handle years below 1970. + The used timezone is GMT. + +--- XMLRPC::DateTime#to_date + Return a (({Date})) object of the date which (({self})) represents. + The (({Date})) object do ((*not*)) contain the time component (only date). + +--- XMLRPC::DateTime#to_a + Returns all date/time components in an array. + Returns (({[year, month, day, hour, min, sec]})). +=end + +require "date" + +module XMLRPC + +class DateTime + + attr_reader :year, :month, :day, :hour, :min, :sec + + def year= (value) + raise ArgumentError, "date/time out of range" unless value.is_a? Integer + @year = value + end + + def month= (value) + raise ArgumentError, "date/time out of range" unless (1..12).include? value + @month = value + end + + def day= (value) + raise ArgumentError, "date/time out of range" unless (1..31).include? value + @day = value + end + + def hour= (value) + raise ArgumentError, "date/time out of range" unless (0..24).include? value + @hour = value + end + + def min= (value) + raise ArgumentError, "date/time out of range" unless (0..59).include? value + @min = value + end + + def sec= (value) + raise ArgumentError, "date/time out of range" unless (0..59).include? value + @sec = value + end + + alias mon month + alias mon= month= + + + def initialize(year, month, day, hour, min, sec) + self.year, self.month, self.day = year, month, day + self.hour, self.min, self.sec = hour, min, sec + end + + def to_time + if @year >= 1970 + Time.gm(*to_a) + else + nil + end + end + + def to_date + Date.new(*to_a[0,3]) + end + + def to_a + [@year, @month, @day, @hour, @min, @sec] + end + +end + + +end # module XMLRPC + + +=begin += History + $Id$ +=end diff --git a/lib/xmlrpc/httpserver.rb b/lib/xmlrpc/httpserver.rb new file mode 100644 index 0000000000..9afb5fd5ec --- /dev/null +++ b/lib/xmlrpc/httpserver.rb @@ -0,0 +1,178 @@ +# +# Implements a simple HTTP-server by using John W. Small's (jsmall@laser.net) +# ruby-generic-server. +# +# Copyright (C) 2001, 2002, 2003 by Michael Neumann (mneumann@ntecs.de) +# +# $Id$ +# + + +require "gserver" + +class HttpServer < GServer + + ## + # handle_obj specifies the object, that receives calls to request_handler + # and ip_auth_handler + def initialize(handle_obj, port = 8080, host = DEFAULT_HOST, maxConnections = 4, + stdlog = $stdout, audit = true, debug = true) + @handler = handle_obj + super(port, host, maxConnections, stdlog, audit, debug) + end + +private + + # Constants ----------------------------------------------- + + CRLF = "\r\n" + HTTP_PROTO = "HTTP/1.0" + SERVER_NAME = "HttpServer (Ruby #{RUBY_VERSION})" + + DEFAULT_HEADER = { + "Server" => SERVER_NAME + } + + ## + # Mapping of status code and error message + # + StatusCodeMapping = { + 200 => "OK", + 400 => "Bad Request", + 403 => "Forbidden", + 405 => "Method Not Allowed", + 411 => "Length Required", + 500 => "Internal Server Error" + } + + # Classes ------------------------------------------------- + + class Request + attr_reader :data, :header, :method, :path, :proto + + def initialize(data, method=nil, path=nil, proto=nil) + @header, @data = Table.new, data + @method, @path, @proto = method, path, proto + end + + def content_length + len = @header['Content-Length'] + return nil if len.nil? + return len.to_i + end + + end + + class Response + attr_reader :header + attr_accessor :body, :status, :status_message + + def initialize(status=200) + @status = status + @status_message = nil + @header = Table.new + end + end + + + ## + # a case-insensitive Hash class for HTTP header + # + class Table + include Enumerable + + def initialize(hash={}) + @hash = hash + update(hash) + end + + def [](key) + @hash[key.to_s.capitalize] + end + + def []=(key, value) + @hash[key.to_s.capitalize] = value + end + + def update(hash) + hash.each {|k,v| self[k] = v} + self + end + + def each + @hash.each {|k,v| yield k.capitalize, v } + end + + def writeTo(port) + each { |k,v| port << "#{k}: #{v}" << CRLF } + end + end # class Table + + + # Helper Methods ------------------------------------------ + + def http_header(header=nil) + new_header = Table.new(DEFAULT_HEADER) + new_header.update(header) unless header.nil? + + new_header["Connection"] = "close" + new_header["Date"] = http_date(Time.now) + + new_header + end + + def http_date( aTime ) + aTime.gmtime.strftime( "%a, %d %b %Y %H:%M:%S GMT" ) + end + + def http_resp(status_code, status_message=nil, header=nil, body=nil) + status_message ||= StatusCodeMapping[status_code] + + str = "" + str << "#{HTTP_PROTO} #{status_code} #{status_message}" << CRLF + http_header(header).writeTo(str) + str << CRLF + str << body unless body.nil? + str + end + + # Main Serve Loop ----------------------------------------- + + def serve(io) + # perform IP authentification + unless @handler.ip_auth_handler(io) + io << http_resp(403, "Forbidden") + return + end + + # parse first line + if io.gets =~ /^(\S+)\s+(\S+)\s+(\S+)/ + request = Request.new(io, $1, $2, $3) + else + io << http_resp(400, "Bad Request") + return + end + + # parse HTTP headers + while (line=io.gets) !~ /^(\n|\r)/ + if line =~ /^([\w-]+):\s*(.*)$/ + request.header[$1] = $2.strip + end + end + + io.binmode + response = Response.new + + # execute request handler + @handler.request_handler(request, response) + + # write response back to the client + io << http_resp(response.status, response.status_message, + response.header, response.body) + + rescue Exception => e + io << http_resp(500, "Internal Server Error") + end + +end # class HttpServer + diff --git a/lib/xmlrpc/marshal.rb b/lib/xmlrpc/marshal.rb new file mode 100644 index 0000000000..26510124c2 --- /dev/null +++ b/lib/xmlrpc/marshal.rb @@ -0,0 +1,76 @@ +# +# Marshalling of XML-RPC methodCall and methodResponse +# +# Copyright (C) 2001, 2002, 2003 by Michael Neumann (mneumann@ntecs.de) +# +# $Id$ +# + +require "xmlrpc/parser" +require "xmlrpc/create" +require "xmlrpc/config" +require "xmlrpc/utils" + +module XMLRPC + + class Marshal + include ParserWriterChooseMixin + + # class methods ------------------------------- + + class << self + + def dump_call( methodName, *params ) + new.dump_call( methodName, *params ) + end + + def dump_response( param ) + new.dump_response( param ) + end + + def load_call( stringOrReadable ) + new.load_call( stringOrReadable ) + end + + def load_response( stringOrReadable ) + new.load_response( stringOrReadable ) + end + + alias dump dump_response + alias load load_response + + end # class self + + # instance methods ---------------------------- + + def initialize( parser = nil, writer = nil ) + set_parser( parser ) + set_writer( writer ) + end + + def dump_call( methodName, *params ) + create.methodCall( methodName, *params ) + end + + def dump_response( param ) + create.methodResponse( ! param.kind_of?( XMLRPC::FaultException ) , param ) + end + + ## + # returns [ methodname, params ] + # + def load_call( stringOrReadable ) + parser.parseMethodCall( stringOrReadable ) + end + + ## + # returns paramOrFault + # + def load_response( stringOrReadable ) + parser.parseMethodResponse( stringOrReadable )[1] + end + + end # class Marshal + +end + diff --git a/lib/xmlrpc/parser.rb b/lib/xmlrpc/parser.rb new file mode 100644 index 0000000000..233dd596fd --- /dev/null +++ b/lib/xmlrpc/parser.rb @@ -0,0 +1,803 @@ +# +# Parser for XML-RPC call and response +# +# Copyright (C) 2001, 2002, 2003 by Michael Neumann (mneumann@ntecs.de) +# +# $Id$ +# + + +require "date" +require "xmlrpc/base64" +require "xmlrpc/datetime" + + +# add some methods to NQXML::Node +module NQXML + class Node + + def removeChild(node) + @children.delete(node) + end + def childNodes + @children + end + def hasChildNodes + not @children.empty? + end + def [] (index) + @children[index] + end + + def nodeType + if @entity.instance_of? NQXML::Text then :TEXT + elsif @entity.instance_of? NQXML::Comment then :COMMENT + #elsif @entity.instance_of? NQXML::Element then :ELEMENT + elsif @entity.instance_of? NQXML::Tag then :ELEMENT + else :ELSE + end + end + + def nodeValue + #TODO: error when wrong Entity-type + @entity.text + end + def nodeName + #TODO: error when wrong Entity-type + @entity.name + end + end # class Node +end # module NQXML + +module XMLRPC + + class FaultException < Exception + attr_reader :faultCode, :faultString + + def initialize(faultCode, faultString) + @faultCode = faultCode + @faultString = faultString + end + + # returns a hash + def to_h + {"faultCode" => @faultCode, "faultString" => @faultString} + end + end + + module Convert + def self.int(str) + str.to_i + end + + def self.boolean(str) + case str + when "0" then false + when "1" then true + else + raise "RPC-value of type boolean is wrong" + end + end + + def self.double(str) + str.to_f + end + + def self.dateTime(str) + if str =~ /^(-?\d\d\d\d)(\d\d)(\d\d)T(\d\d):(\d\d):(\d\d)$/ then + # TODO: Time.gm ??? .local ??? + a = [$1, $2, $3, $4, $5, $6].collect{|i| i.to_i} + + XMLRPC::DateTime.new(*a) + #if a[0] >= 1970 then + # Time.gm(*a) + #else + # Date.new(*a[0,3]) + #end + else + raise "wrong dateTime.iso8601 format" + end + end + + def self.base64(str) + XMLRPC::Base64.decode(str) + end + + def self.struct(hash) + # convert to marhalled object + klass = hash["___class___"] + if klass.nil? or Config::ENABLE_MARSHALLING == false + hash + else + begin + mod = Module + klass.split("::").each {|const| mod = mod.const_get const.strip } + + Thread.critical = true + # let initialize take 0 parameters + mod.module_eval %{ + begin + alias __initialize initialize + rescue NameError + end + def initialize; end + } + + obj = mod.new + + # restore old initialize + mod.module_eval %{ + undef initialize + begin + alias initialize __initialize + rescue NameError + end + } + Thread.critical = false + + hash.delete "___class___" + hash.each {|k,v| obj.__set_instance_variable(k, v) } + obj + rescue + hash + end + end + end + + def self.fault(hash) + if hash.kind_of? Hash and hash.size == 2 and + hash.has_key? "faultCode" and hash.has_key? "faultString" and + hash["faultCode"].kind_of? Integer and hash["faultString"].kind_of? String + + XMLRPC::FaultException.new(hash["faultCode"], hash["faultString"]) + else + raise "wrong fault-structure: #{hash.inspect}" + end + end + + end # module Convert + + module XMLParser + + class AbstractTreeParser + + def parseMethodResponse(str) + methodResponse_document(createCleanedTree(str)) + end + + def parseMethodCall(str) + methodCall_document(createCleanedTree(str)) + end + + private + + # + # remove all whitespaces but in the tags i4, int, boolean.... + # and all comments + # + def removeWhitespacesAndComments(node) + remove = [] + childs = node.childNodes.to_a + childs.each do |nd| + case _nodeType(nd) + when :TEXT + # TODO: add nil? + unless %w(i4 int boolean string double dateTime.iso8601 base64).include? node.nodeName + + if node.nodeName == "value" + if not node.childNodes.to_a.detect {|n| _nodeType(n) == :ELEMENT}.nil? + remove << nd if nd.nodeValue.strip == "" + end + else + remove << nd if nd.nodeValue.strip == "" + end + end + when :COMMENT + remove << nd + else + removeWhitespacesAndComments(nd) + end + end + + remove.each { |i| node.removeChild(i) } + end + + + def nodeMustBe(node, name) + cmp = case name + when Array + name.include?(node.nodeName) + when String + name == node.nodeName + else + raise "error" + end + + if not cmp then + raise "wrong xml-rpc (name)" + end + + node + end + + # + # returns, when successfully the only child-node + # + def hasOnlyOneChild(node, name=nil) + if node.childNodes.to_a.size != 1 + raise "wrong xml-rpc (size)" + end + if name != nil then + nodeMustBe(node.firstChild, name) + end + end + + + def assert(b) + if not b then + raise "assert-fail" + end + end + + # the node `node` has empty string or string + def text_zero_one(node) + nodes = node.childNodes.to_a.size + + if nodes == 1 + text(node.firstChild) + elsif nodes == 0 + "" + else + raise "wrong xml-rpc (size)" + end + end + + + def integer(node) + #TODO: check string for float because to_i returnsa + # 0 when wrong string + nodeMustBe(node, %w(i4 int)) + hasOnlyOneChild(node) + + Convert.int(text(node.firstChild)) + end + + def boolean(node) + nodeMustBe(node, "boolean") + hasOnlyOneChild(node) + + Convert.boolean(text(node.firstChild)) + end + + def v_nil(node) + nodeMustBe(node, "nil") + assert( node.childNodes.to_a.size == 0 ) + nil + end + + def string(node) + nodeMustBe(node, "string") + text_zero_one(node) + end + + def double(node) + #TODO: check string for float because to_f returnsa + # 0.0 when wrong string + nodeMustBe(node, "double") + hasOnlyOneChild(node) + + Convert.double(text(node.firstChild)) + end + + def dateTime(node) + nodeMustBe(node, "dateTime.iso8601") + hasOnlyOneChild(node) + + Convert.dateTime( text(node.firstChild) ) + end + + def base64(node) + nodeMustBe(node, "base64") + #hasOnlyOneChild(node) + + Convert.base64(text_zero_one(node)) + end + + def member(node) + nodeMustBe(node, "member") + assert( node.childNodes.to_a.size == 2 ) + + [ name(node[0]), value(node[1]) ] + end + + def name(node) + nodeMustBe(node, "name") + #hasOnlyOneChild(node) + text_zero_one(node) + end + + def array(node) + nodeMustBe(node, "array") + hasOnlyOneChild(node, "data") + data(node.firstChild) + end + + def data(node) + nodeMustBe(node, "data") + + node.childNodes.to_a.collect do |val| + value(val) + end + end + + def param(node) + nodeMustBe(node, "param") + hasOnlyOneChild(node, "value") + value(node.firstChild) + end + + def methodResponse(node) + nodeMustBe(node, "methodResponse") + hasOnlyOneChild(node, %w(params fault)) + child = node.firstChild + + case child.nodeName + when "params" + [ true, params(child,false) ] + when "fault" + [ false, fault(child) ] + else + raise "unexpected error" + end + + end + + def methodName(node) + nodeMustBe(node, "methodName") + hasOnlyOneChild(node) + text(node.firstChild) + end + + def params(node, call=true) + nodeMustBe(node, "params") + + if call + node.childNodes.to_a.collect do |n| + param(n) + end + else # response (only one param) + hasOnlyOneChild(node) + param(node.firstChild) + end + end + + def fault(node) + nodeMustBe(node, "fault") + hasOnlyOneChild(node, "value") + f = value(node.firstChild) + Convert.fault(f) + end + + + + # _nodeType is defined in the subclass + def text(node) + assert( _nodeType(node) == :TEXT ) + assert( node.hasChildNodes == false ) + assert( node.nodeValue != nil ) + + node.nodeValue.to_s + end + + def struct(node) + nodeMustBe(node, "struct") + + hash = {} + node.childNodes.to_a.each do |me| + n, v = member(me) + hash[n] = v + end + + Convert.struct(hash) + end + + + def value(node) + nodeMustBe(node, "value") + nodes = node.childNodes.to_a.size + if nodes == 0 + return "" + elsif nodes > 1 + raise "wrong xml-rpc (size)" + end + + child = node.firstChild + + case _nodeType(child) + when :TEXT + text_zero_one(node) + when :ELEMENT + case child.nodeName + when "i4", "int" then integer(child) + when "boolean" then boolean(child) + when "string" then string(child) + when "double" then double(child) + when "dateTime.iso8601" then dateTime(child) + when "base64" then base64(child) + when "struct" then struct(child) + when "array" then array(child) + when "nil" + if Config::ENABLE_NIL_PARSER + v_nil(child) + else + raise "wrong/unknown XML-RPC type 'nil'" + end + else + raise "wrong/unknown XML-RPC type" + end + else + raise "wrong type of node" + end + + end + + def methodCall(node) + nodeMustBe(node, "methodCall") + assert( (1..2).include?( node.childNodes.to_a.size ) ) + name = methodName(node[0]) + + if node.childNodes.to_a.size == 2 then + pa = params(node[1]) + else # no parameters given + pa = [] + end + [name, pa] + end + + end # module TreeParserMixin + + class AbstractStreamParser + def parseMethodResponse(str) + parser = @parser_class.new + parser.parse(str) + raise "No valid method response!" if parser.method_name != nil + if parser.fault != nil + # is a fault structure + [false, parser.fault] + else + # is a normal return value + raise "Missing return value!" if parser.params.size == 0 + raise "To many return values. Only one allowed!" if parser.params.size > 1 + [true, parser.params[0]] + end + end + + def parseMethodCall(str) + parser = @parser_class.new + parser.parse(str) + raise "No valid method call - missing method name!" if parser.method_name.nil? + [parser.method_name, parser.params] + end + end + + module StreamParserMixin + attr_reader :params + attr_reader :method_name + attr_reader :fault + + def initialize(*a) + super(*a) + @params = [] + @values = [] + @val_stack = [] + + @names = [] + @name = [] + + @structs = [] + @struct = {} + + @method_name = nil + @fault = nil + + @data = nil + end + + def startElement(name, attrs=[]) + @data = nil + case name + when "value" + @value = nil + when "nil" + raise "wrong/unknown XML-RPC type 'nil'" unless Config::ENABLE_NIL_PARSER + @value = :nil + when "array" + @val_stack << @values + @values = [] + when "struct" + @names << @name + @name = [] + + @structs << @struct + @struct = {} + end + end + + def endElement(name) + @data ||= "" + case name + when "string" + @value = @data + when "i4", "int" + @value = Convert.int(@data) + when "boolean" + @value = Convert.boolean(@data) + when "double" + @value = Convert.double(@data) + when "dateTime.iso8601" + @value = Convert.dateTime(@data) + when "base64" + @value = Convert.base64(@data) + when "value" + @value = @data if @value.nil? + @values << (@value == :nil ? nil : @value) + when "array" + @value = @values + @values = @val_stack.pop + when "struct" + @value = Convert.struct(@struct) + + @name = @names.pop + @struct = @structs.pop + when "name" + @name[0] = @data + when "member" + @struct[@name[0]] = @values.pop + + when "param" + @params << @values[0] + @values = [] + + when "fault" + @fault = Convert.fault(@values[0]) + + when "methodName" + @method_name = @data + end + + @data = nil + end + + def character(data) + if @data + @data << data + else + @data = data + end + end + + end # module StreamParserMixin + + # --------------------------------------------------------------------------- + class XMLStreamParser < AbstractStreamParser + def initialize + require "xmlparser" + eval %{ + class XMLRPCParser < ::XMLParser + include StreamParserMixin + end + } + @parser_class = XMLRPCParser + end + end # class XMLStreamParser + # --------------------------------------------------------------------------- + class NQXMLStreamParser < AbstractStreamParser + def initialize + require "nqxml/streamingparser" + @parser_class = XMLRPCParser + end + + class XMLRPCParser + include StreamParserMixin + + def parse(str) + parser = NQXML::StreamingParser.new(str) + parser.each do |ele| + case ele + when NQXML::Text + @data = ele.text + #character(ele.text) + when NQXML::Tag + if ele.isTagEnd + endElement(ele.name) + else + startElement(ele.name, ele.attrs) + end + end + end # do + end # method parse + end # class XMLRPCParser + + end # class NQXMLStreamParser + # --------------------------------------------------------------------------- + class XMLTreeParser < AbstractTreeParser + + def initialize + require "xmltreebuilder" + + # The new XMLParser library (0.6.2+) uses a slightly different DOM implementation. + # The following code removes the differences between both versions. + if defined? XML::DOM::Builder + return if defined? XML::DOM::Node::DOCUMENT # code below has been already executed + klass = XML::DOM::Node + klass.const_set("DOCUMENT", klass::DOCUMENT_NODE) + klass.const_set("TEXT", klass::TEXT_NODE) + klass.const_set("COMMENT", klass::COMMENT_NODE) + klass.const_set("ELEMENT", klass::ELEMENT_NODE) + end + end + + private + + def _nodeType(node) + tp = node.nodeType + if tp == XML::SimpleTree::Node::TEXT then :TEXT + elsif tp == XML::SimpleTree::Node::COMMENT then :COMMENT + elsif tp == XML::SimpleTree::Node::ELEMENT then :ELEMENT + else :ELSE + end + end + + + def methodResponse_document(node) + assert( node.nodeType == XML::SimpleTree::Node::DOCUMENT ) + hasOnlyOneChild(node, "methodResponse") + + methodResponse(node.firstChild) + end + + def methodCall_document(node) + assert( node.nodeType == XML::SimpleTree::Node::DOCUMENT ) + hasOnlyOneChild(node, "methodCall") + + methodCall(node.firstChild) + end + + def createCleanedTree(str) + doc = XML::SimpleTreeBuilder.new.parse(str) + doc.documentElement.normalize + removeWhitespacesAndComments(doc) + doc + end + + end # class XMLParser + # --------------------------------------------------------------------------- + class NQXMLTreeParser < AbstractTreeParser + + def initialize + require "nqxml/treeparser" + end + + private + + def _nodeType(node) + node.nodeType + end + + def methodResponse_document(node) + methodResponse(node) + end + + def methodCall_document(node) + methodCall(node) + end + + def createCleanedTree(str) + doc = ::NQXML::TreeParser.new(str).document.rootNode + removeWhitespacesAndComments(doc) + doc + end + + end # class NQXMLTreeParser + # --------------------------------------------------------------------------- + class REXMLStreamParser < AbstractStreamParser + def initialize + require "rexml/document" + @parser_class = StreamListener + end + + class StreamListener + include StreamParserMixin + + alias :tag_start :startElement + alias :tag_end :endElement + alias :text :character + + def method_missing(*a) + # ignore + end + + def parse(str) + parser = REXML::Document.parse_stream(str, self) + end + end + + end + # --------------------------------------------------------------------------- + class XMLScanStreamParser < AbstractStreamParser + def initialize + require "xmlscan/parser" + @parser_class = XMLScanParser + end + + class XMLScanParser + include StreamParserMixin + + Entities = { + "lt" => "<", + "gt" => ">", + "amp" => "&", + "quot" => '"', + "apos" => "'" + } + + def parse(str) + parser = XMLScan::XMLParser.new(self) + parser.parse(str) + end + + alias :on_stag :startElement + alias :on_etag :endElement + + def on_stag_end(name); end + + def on_stag_end_empty(name) + startElement(name) + endElement(name) + end + + def on_chardata(str) + character(str) + end + + def on_entityref(ent) + str = Entities[ent] + if str + character(str) + else + raise "unknown entity" + end + end + + def on_charref(code) + character(code.chr) + end + + def on_charref_hex(code) + character(code.chr) + end + + def method_missing(*a) + end + + # TODO: call/implement? + # valid_name? + # valid_chardata? + # valid_char? + # parse_error + + end + end + # --------------------------------------------------------------------------- + XMLParser = XMLTreeParser + NQXMLParser = NQXMLTreeParser + + Classes = [XMLStreamParser, XMLTreeParser, + NQXMLStreamParser, NQXMLTreeParser, + REXMLStreamParser, XMLScanStreamParser] + + end # module XMLParser + + +end # module XMLRPC + diff --git a/lib/xmlrpc/server.rb b/lib/xmlrpc/server.rb new file mode 100644 index 0000000000..f5d8059912 --- /dev/null +++ b/lib/xmlrpc/server.rb @@ -0,0 +1,839 @@ +=begin += xmlrpc/server.rb +Copyright (C) 2001, 2002, 2003 by Michael Neumann (mneumann@ntecs.de) + +Released under the same term of license as Ruby. + += Classes +* (()) +* (()) +* (()) +* (()) +* (()) + += XMLRPC::BasicServer +== Description +Is the base class for all XML-RPC server-types (CGI, standalone). +You can add handler and set a default handler. +Do not use this server, as this is/should be an abstract class. + +=== How the method to call is found +The arity (number of accepted arguments) of a handler (method or (({Proc})) object) is +compared to the given arguments submitted by the client for a RPC ((-Remote Procedure Call-)). +A handler is only called if it accepts the number of arguments, otherwise the search +for another handler will go on. When at the end no handler was found, +the (()) will be called. +With this technique it is possible to do overloading by number of parameters, but +only for (({Proc})) handler, because you cannot define two methods of the same name in +the same class. + + +== Class Methods +--- XMLRPC::BasicServer.new( class_delim="." ) + Creates a new (({XMLRPC::BasicServer})) instance, which should not be + done, because (({XMLRPC::BasicServer})) is an abstract class. This + method should be called from a subclass indirectly by a (({super})) call + in the method (({initialize})). The paramter ((|class_delim|)) is used + in (()) when an object is + added as handler, to delimit the object-prefix and the method-name. + +== Instance Methods +--- XMLRPC::BasicServer#add_handler( name, signature=nil, help=nil ) { aBlock } + Adds ((|aBlock|)) to the list of handlers, with ((|name|)) as the name of the method. + Parameters ((|signature|)) and ((|help|)) are used by the Introspection method if specified, + where ((|signature|)) is either an Array containing strings each representing a type of it's + signature (the first is the return value) or an Array of Arrays if the method has multiple + signatures. Value type-names are "int, boolean, double, string, dateTime.iso8601, base64, array, struct". + + Parameter ((|help|)) is a String with informations about how to call this method etc. + + A handler method or code-block can return the types listed at + (()). + When a method fails, it can tell it the client by throwing an + (({XMLRPC::FaultException})) like in this example: + s.add_handler("michael.div") do |a,b| + if b == 0 + raise XMLRPC::FaultException.new(1, "division by zero") + else + a / b + end + end + The client gets in the case of (({b==0})) an object back of type + (({XMLRPC::FaultException})) that has a ((|faultCode|)) and ((|faultString|)) + field. + +--- XMLRPC::BasicServer#add_handler( prefix, obj ) + This is the second form of (()). + To add an object write: + server.add_handler("michael", MyHandlerClass.new) + All public methods of (({MyHandlerClass})) are accessible to + the XML-RPC clients by (('michael."name of method"')). This is + where the ((|class_delim|)) in (()) + has it's role, a XML-RPC method-name is defined by + ((|prefix|)) + ((|class_delim|)) + (('"name of method"')). + +--- XMLRPC::BasicServer#add_handler( interface, obj ) + This is the third form of (()). + + Use (({XMLRPC::interface})) to generate an ServiceInterface object, which + represents an interface (with signature and help text) for a handler class. + + Parameter ((|interface|)) must be of type (({XMLRPC::ServiceInterface})). + Adds all methods of ((|obj|)) which are defined in ((|interface|)) to the + server. + + This is the recommended way of adding services to a server! + + +--- XMLRPC::BasicServer#get_default_handler + Returns the default-handler, which is called when no handler for + a method-name is found. + It is a (({Proc})) object or (({nil})). + +--- XMLRPC::BasicServer#set_default_handler ( &handler ) + Sets ((|handler|)) as the default-handler, which is called when + no handler for a method-name is found. ((|handler|)) is a code-block. + The default-handler is called with the (XML-RPC) method-name as first argument, and + the other arguments are the parameters given by the client-call. + + If no block is specified the default of (({XMLRPC::BasicServer})) is used, which raises a + XMLRPC::FaultException saying "method missing". + + +--- XMLRPC::BasicServer#set_writer( writer ) + Sets the XML writer to use for generating XML output. + Should be an instance of a class from module (({XMLRPC::XMLWriter})). + If this method is not called, then (({XMLRPC::Config::DEFAULT_WRITER})) is used. + +--- XMLRPC::BasicServer#set_parser( parser ) + Sets the XML parser to use for parsing XML documents. + Should be an instance of a class from module (({XMLRPC::XMLParser})). + If this method is not called, then (({XMLRPC::Config::DEFAULT_PARSER})) is used. + +--- XMLRPC::BasicServer#add_introspection + Adds the introspection handlers "system.listMethods", "system.methodSignature" and "system.methodHelp", + where only the first one works. + +--- XMLRPC::BasicServer#add_multicall + Adds the multi-call handler "system.multicall". + +--- XMLRPC::BasicServer#get_service_hook + Returns the service-hook, which is called on each service request (RPC) unless it's (({nil})). + +--- XMLRPC::BasicServer#set_service_hook ( &handler ) + A service-hook is called for each service request (RPC). + You can use a service-hook for example to wrap existing methods and catch exceptions of them or + convert values to values recognized by XMLRPC. You can disable it by passing (({nil})) as parameter + ((|handler|)) . + + The service-hook is called with a (({Proc})) object and with the parameters for this (({Proc})). + An example: + + server.set_service_hook {|obj, *args| + begin + ret = obj.call(*args) # call the original service-method + # could convert the return value + resuce + # rescue exceptions + end + } + +=end + + + +require "xmlrpc/parser" +require "xmlrpc/create" +require "xmlrpc/config" +require "xmlrpc/httpserver" +require "xmlrpc/utils" # ParserWriterChooseMixin + + + +module XMLRPC + + +class BasicServer + + include ParserWriterChooseMixin + include ParseContentType + + ERR_METHOD_MISSING = 1 + ERR_UNCAUGHT_EXCEPTION = 2 + ERR_MC_WRONG_PARAM = 3 + ERR_MC_MISSING_PARAMS = 4 + ERR_MC_MISSING_METHNAME = 5 + ERR_MC_RECURSIVE_CALL = 6 + ERR_MC_WRONG_PARAM_PARAMS = 7 + ERR_MC_EXPECTED_STRUCT = 8 + + + def initialize(class_delim=".") + @handler = [] + @default_handler = nil + @service_hook = nil + + @class_delim = class_delim + @create = nil + @parser = nil + + add_multicall if Config::ENABLE_MULTICALL + add_introspection if Config::ENABLE_INTROSPECTION + end + + def add_handler(prefix, obj_or_signature=nil, help=nil, &block) + if block_given? + # proc-handler + @handler << [prefix, block, obj_or_signature, help] + else + if prefix.kind_of? String + # class-handler + raise ArgumentError, "Expected non-nil value" if obj_or_signature.nil? + @handler << [prefix + @class_delim, obj_or_signature] + elsif prefix.kind_of? XMLRPC::Service::BasicInterface + # class-handler with interface + # add all methods + @handler += prefix.get_methods(obj_or_signature, @class_delim) + else + raise ArgumentError, "Wrong type for parameter 'prefix'" + end + end + self + end + + def get_service_hook + @service_hook + end + + def set_service_hook(&handler) + @service_hook = handler + self + end + + def get_default_handler + @default_handler + end + + def set_default_handler (&handler) + @default_handler = handler + self + end + + def add_multicall + add_handler("system.multicall", %w(array array), "Multicall Extension") do |arrStructs| + unless arrStructs.is_a? Array + raise XMLRPC::FaultException.new(ERR_MC_WRONG_PARAM, "system.multicall expects an array") + end + + arrStructs.collect {|call| + if call.is_a? Hash + methodName = call["methodName"] + params = call["params"] + + if params.nil? + multicall_fault(ERR_MC_MISSING_PARAMS, "Missing params") + elsif methodName.nil? + multicall_fault(ERR_MC_MISSING_METHNAME, "Missing methodName") + else + if methodName == "system.multicall" + multicall_fault(ERR_MC_RECURSIVE_CALL, "Recursive system.multicall forbidden") + else + unless params.is_a? Array + multicall_fault(ERR_MC_WRONG_PARAM_PARAMS, "Parameter params have to be an Array") + else + ok, val = call_method(methodName, *params) + if ok + # correct return value + [val] + else + # exception + multicall_fault(val.faultCode, val.faultString) + end + end + end + end + + else + multicall_fault(ERR_MC_EXPECTED_STRUCT, "system.multicall expected struct") + end + } + end # end add_handler + self + end + + def add_introspection + add_handler("system.listMethods",%w(array), "List methods available on this XML-RPC server") do + methods = [] + @handler.each do |name, obj| + if obj.kind_of? Proc + methods << name + else + obj.methods.each {|meth| methods << name + meth} + end + end + methods + end + + add_handler("system.methodSignature", %w(array string), "Returns method signature") do |meth| + sigs = [] + @handler.each do |name, obj, sig| + if obj.kind_of? Proc and sig != nil and name == meth + if sig[0].kind_of? Array + # sig contains multiple signatures, e.g. [["array"], ["array", "string"]] + sig.each {|s| sigs << s} + else + # sig is a single signature, e.g. ["array"] + sigs << sig + end + end + end + sigs.uniq! || sigs # remove eventually duplicated signatures + end + + add_handler("system.methodHelp", %w(string string), "Returns help on using this method") do |meth| + help = nil + @handler.each do |name, obj, sig, hlp| + if obj.kind_of? Proc and name == meth + help = hlp + break + end + end + help || "" + end + + self + end + + + + def process(data) + method, params = parser().parseMethodCall(data) + handle(method, *params) + end + + private # -------------------------------------------------------------- + + def multicall_fault(nr, str) + {"faultCode" => nr, "faultString" => str} + end + + # + # method dispatch + # + def dispatch(methodname, *args) + for name, obj in @handler + if obj.kind_of? Proc + next unless methodname == name + else + next unless methodname =~ /^#{name}(.+)$/ + next unless obj.respond_to? $1 + obj = obj.method($1) + end + + if check_arity(obj, args.size) + if @service_hook.nil? + return obj.call(*args) + else + return @service_hook.call(obj, *args) + end + end + end + + if @default_handler.nil? + raise XMLRPC::FaultException.new(ERR_METHOD_MISSING, "Method #{methodname} missing or wrong number of parameters!") + else + @default_handler.call(methodname, *args) + end + end + + + # + # returns true, if the arity of "obj" matches + # + def check_arity(obj, n_args) + ary = obj.arity + + if ary >= 0 + n_args == ary + else + n_args >= (ary+1).abs + end + end + + + + def call_method(methodname, *args) + begin + [true, dispatch(methodname, *args)] + rescue XMLRPC::FaultException => e + [false, e] + rescue Exception => e + [false, XMLRPC::FaultException.new(ERR_UNCAUGHT_EXCEPTION, "Uncaught exception #{e.message} in method #{methodname}")] + end + end + + # + # + # + def handle(methodname, *args) + create().methodResponse(*call_method(methodname, *args)) + end + + +end + + +=begin += XMLRPC::CGIServer +== Synopsis + require "xmlrpc/server" + + s = XMLRPC::CGIServer.new + + s.add_handler("michael.add") do |a,b| + a + b + end + + s.add_handler("michael.div") do |a,b| + if b == 0 + raise XMLRPC::FaultException.new(1, "division by zero") + else + a / b + end + end + + s.set_default_handler do |name, *args| + raise XMLRPC::FaultException.new(-99, "Method #{name} missing" + + " or wrong number of parameters!") + end + + s.serve + +== Description +Implements a CGI-based XML-RPC server. + +== Superclass +(()) + +== Class Methods +--- XMLRPC::CGIServer.new( *a ) + Creates a new (({XMLRPC::CGIServer})) instance. All parameters given + are by-passed to (()). You can only create + ((*one*)) (({XMLRPC::CGIServer})) instance, because more than one makes + no sense. + +== Instance Methods +--- XMLRPC::CGIServer#serve + Call this after you have added all you handlers to the server. + This method processes a XML-RPC methodCall and sends the answer + back to the client. + Make sure that you don't write to standard-output in a handler, or in + any other part of your program, this would case a CGI-based server to fail! +=end + +class CGIServer < BasicServer + @@obj = nil + + def CGIServer.new(*a) + @@obj = super(*a) if @@obj.nil? + @@obj + end + + def initialize(*a) + super(*a) + end + + def serve + catch(:exit_serve) { + length = ENV['CONTENT_LENGTH'].to_i + + http_error(405, "Method Not Allowed") unless ENV['REQUEST_METHOD'] == "POST" + http_error(400, "Bad Request") unless ENV['CONTENT_TYPE'] == "text/xml" + http_error(411, "Length Required") unless length > 0 + + # TODO: do we need a call to binmode? + $stdin.binmode if $stdin.respond_to? :binmode + data = $stdin.read(length) + + http_error(400, "Bad Request") if data.nil? or data.size != length + + http_write(process(data), "Content-type" => "text/xml") + } + end + + + private + + def http_error(status, message) + err = "#{status} #{message}" + msg = <<-"MSGEND" + + + #{err} + + +

#{err}

+

Unexpected error occured while processing XML-RPC request!

+ + + MSGEND + + http_write(msg, "Status" => err, "Content-type" => "text/html") + throw :exit_serve # exit from the #serve method + end + + def http_write(body, header) + h = {} + header.each {|key, value| h[key.to_s.capitalize] = value} + h['Status'] ||= "200 OK" + h['Content-length'] ||= body.size.to_s + + str = "" + h.each {|key, value| str << "#{key}: #{value}\r\n"} + str << "\r\n#{body}" + + print str + end + +end + +=begin += XMLRPC::ModRubyServer +== Description +Implements a XML-RPC server, which works with Apache mod_ruby. + +Use it in the same way as CGIServer! + +== Superclass +(()) +=end + +class ModRubyServer < BasicServer + @@obj = nil + + def ModRubyServer.new(*a) + @@obj = super(*a) if @@obj.nil? + @@obj + end + + def initialize(*a) + @ap = Apache::request + super(*a) + end + + def serve + catch(:exit_serve) { + header = {} + @ap.each_header {|key, value| header[key.capitalize] = value} + + length = header['Content-length'].to_i + + http_error(405, "Method Not Allowed") unless @ap.request_method == "POST" + http_error(400, "Bad Request") unless parse_content_type(header['Content-type']).first == "text/xml" + http_error(411, "Length Required") unless length > 0 + + # TODO: do we need a call to binmode? + @ap.binmode + data = @ap.read(length) + + http_error(400, "Bad Request") if data.nil? or data.size != length + + http_write(process(data), 200, "Content-type" => "text/xml") + } + end + + + private + + def http_error(status, message) + err = "#{status} #{message}" + msg = <<-"MSGEND" + + + #{err} + + +

#{err}

+

Unexpected error occured while processing XML-RPC request!

+ + + MSGEND + + http_write(msg, status, "Status" => err, "Content-type" => "text/html") + throw :exit_serve # exit from the #serve method + end + + def http_write(body, status, header) + h = {} + header.each {|key, value| h[key.to_s.capitalize] = value} + h['Status'] ||= "200 OK" + h['Content-length'] ||= body.size.to_s + + h.each {|key, value| @ap[key] = value } + @ap.content_type = h["Content-type"] + @ap.status = status.to_i + @ap.send_http_header + + @ap.print body + end + +end + + + + +=begin += XMLRPC::Server +== Synopsis + require "xmlrpc/server" + + s = XMLRPC::Server.new(8080) + + s.add_handler("michael.add") do |a,b| + a + b + end + + s.add_handler("michael.div") do |a,b| + if b == 0 + raise XMLRPC::FaultException.new(1, "division by zero") + else + a / b + end + end + + s.set_default_handler do |name, *args| + raise XMLRPC::FaultException.new(-99, "Method #{name} missing" + + " or wrong number of parameters!") + end + + s.serve + +== Description +Implements a standalone XML-RPC server. The method (({serve}))) is left if a SIGHUP is sent to the +program. + +== Superclass +(()) + +== Class Methods +--- XMLRPC::Server.new( port=8080, host="127.0.0.1", maxConnections=4, stdlog=$stdout, audit=true, debug=true, *a ) + Creates a new (({XMLRPC::Server})) instance, which is a XML-RPC server listening on + port ((|port|)) and accepts requests for the host ((|host|)), which is by default only the localhost. + The server is not started, to start it you have to call (()). + + The parameters ((|maxConnections|)), ((|stdlog|)), ((|audit|)) and ((|debug|)) are passed to the HTTP server and + specify it's behaviour more precise. + + All additionally given parameters in ((|*a|)) are by-passed to (()). + + +== Instance Methods +--- XMLRPC::Server#serve + Call this after you have added all you handlers to the server. + This method starts the server to listen for XML-RPC requests and answer them. + +--- XMLRPC::Server#shutdown + Stops and shuts the server down. + +--- XMLRPC::Server#set_valid_ip( *ip_addr ) + Specifies the valid IP addresses that are allowed to connect to the server. + Each IP is either a (({String})) or a (({Regexp})). + +--- XMLRPC::Server#get_valid_ip + Return the via method (()) specified + valid IP addresses. + +=end + +class Server < BasicServer + + def initialize(port=8080, host="127.0.0.1", maxConnections=4, stdlog=$stdout, audit=true, debug=true, *a) + super(*a) + @server = ::HttpServer.new(self, port, host, maxConnections, stdlog, audit, debug) + @valid_ip = nil + end + + def serve + if RUBY_PLATFORM =~ /mingw|mswin32/ + signal = 1 + else + signal = "HUP" + end + trap(signal) { @server.shutdown } + + @server.start.join + end + + def shutdown + @server.shutdown + end + + def set_valid_ip(*ip_addr) + if ip_addr.size == 1 and ip_addr[0].nil? + @valid_ip = nil + else + @valid_ip = ip_addr + end + end + + def get_valid_ip + @valid_ip + end + + # methods that get called by HttpServer ------------------------------------------ + + def request_handler(request, response) + $stderr.puts "in request_handler" if $DEBUG + + if request.method != "POST" + # Method not allowed + response.status = 405 + return + end + + if parse_content_type(request.header['Content-type']).first != "text/xml" + # Bad request + response.status = 400 + return + end + + length = request.content_length || 0 + + unless length > 0 + # Length required + response.status = 411 + return + end + + data = request.data.read(length) + + if data.nil? or data.size != length + # Bad request + response.status = 400 + return + end + + resp = process(data) + raise if resp.nil? or resp.size <= 0 # => Internal Server Error + + response.status = 200 + response.header['Content-Length'] = resp.size + response.header['Content-Type'] = "text/xml" + response.body = resp + + end + + ## + # Is called before request_handler and should return true if + # the client is allowed to connect to the server. + # `io' is a Socket object. + def ip_auth_handler(io) + if @valid_ip + client_ip = io.peeraddr[3] + @valid_ip.each { |ip| + return true if client_ip =~ ip + } + false + else + true + end + end + +end + + +=begin += XMLRPC::WEBrickServlet +== Synopsis + + require "webrick" + require "xmlrpc/server" + + s = XMLRPC::WEBrickServlet.new + s.add_handler("michael.add") do |a,b| + a + b + end + + s.add_handler("michael.div") do |a,b| + if b == 0 + raise XMLRPC::FaultException.new(1, "division by zero") + else + a / b + end + end + + s.set_default_handler do |name, *args| + raise XMLRPC::FaultException.new(-99, "Method #{name} missing" + + " or wrong number of parameters!") + end + + httpserver = WEBrick::HTTPServer.new(:Port => 8080) + httpserver.mount("RPC2", s) + trap("HUP") { httpserver.shutdown } # use 1 instead of "HUP" on Windows + httpserver.start +== Description +Implements a servlet for use with WEBrick, a pure Ruby (HTTP-) server framework. + +== Superclass +(()) + +=end + +class WEBrickServlet < BasicServer + def initialize(*a) + super + require "webrick/httpstatus" + end + + # deprecated from WEBrick/1.2.2. + # but does not break anything. + def require_path_info? + false + end + + def get_instance(config, *options) + # TODO: set config & options + self + end + + def service(request, response) + if request.request_method != "POST" + raise HTTPStatus::MethodNotAllowed, + "unsupported method `#{request.request_method}'." + end + + if parse_content_type(request['Content-type']).first != "text/xml" + raise HTTPStatus::BadRequest + end + + length = (request['Content-length'] || 0).to_i + + raise HTTPStatus::LengthRequired unless length > 0 + + data = request.body + + if data.nil? or data.size != length + raise HTTPStatus::BadRequest + end + + resp = process(data) + if resp.nil? or resp.size <= 0 + raise HTTPStatus::InternalServerError + end + + response.status = 200 + response['Content-Length'] = resp.size + response['Content-Type'] = "text/xml" + response.body = resp + end +end + + +end # module XMLRPC + + +=begin += History + $Id$ +=end + diff --git a/lib/xmlrpc/utils.rb b/lib/xmlrpc/utils.rb new file mode 100644 index 0000000000..675f23275b --- /dev/null +++ b/lib/xmlrpc/utils.rb @@ -0,0 +1,172 @@ +# +# Defines ParserWriterChooseMixin, which makes it possible to choose a +# different XML writer and/or XML parser then the default one. +# The Mixin is used in client.rb (class Client) and server.rb (class +# BasicServer) +# +# Copyright (C) 2001, 2002, 2003 by Michael Neumann (mneumann@ntecs.de) +# +# $Id$ +# + +module XMLRPC + + # + # This module enables a user-class to be marshalled + # by XML-RPC for Ruby into a Hash, with one additional + # key/value pair "___class___" => ClassName + # + module Marshallable + def __get_instance_variables + instance_variables.collect {|var| [var[1..-1], eval(var)] } + end + + def __set_instance_variable(key, value) + eval("@#$1 = value") if key =~ /^([\w_][\w_0-9]*)$/ + end + end + + + module ParserWriterChooseMixin + + def set_writer(writer) + @create = Create.new(writer) + self + end + + def set_parser(parser) + @parser = parser + self + end + + private + + def create + # if set_writer was not already called then call it now + if @create.nil? then + set_writer(Config::DEFAULT_WRITER.new) + end + @create + end + + def parser + # if set_parser was not already called then call it now + if @parser.nil? then + set_parser(Config::DEFAULT_PARSER.new) + end + @parser + end + + end # module ParserWriterChooseMixin + + + module Service + + # + # base class for Service Interface definitions, used + # by BasicServer#add_handler + # + + class BasicInterface + attr_reader :prefix, :methods + + def initialize(prefix) + @prefix = prefix + @methods = [] + end + + def add_method(sig, help=nil, meth_name=nil) + mname = nil + sig = [sig] if sig.kind_of? String + + sig = sig.collect do |s| + name, si = parse_sig(s) + raise "Wrong signatures!" if mname != nil and name != mname + mname = name + si + end + + @methods << [mname, meth_name || mname, sig, help] + end + + private # --------------------------------- + + def parse_sig(sig) + # sig is a String + if sig =~ /^\s*(\w+)\s+([^(]+)(\(([^)]*)\))?\s*$/ + params = [$1] + name = $2.strip + $4.split(",").each {|i| params << i.strip} if $4 != nil + return name, params + else + raise "Syntax error in signature" + end + end + + end # class BasicInterface + + # + # class which wraps a Service Interface definition, used + # by BasicServer#add_handler + # + class Interface < BasicInterface + def initialize(prefix, &p) + raise "No interface specified" if p.nil? + super(prefix) + instance_eval &p + end + + def get_methods(obj, delim=".") + prefix = @prefix + delim + @methods.collect { |name, meth, sig, help| + [prefix + name, obj.method(meth).to_proc, sig, help] + } + end + + private # --------------------------------- + + def meth(*a) + add_method(*a) + end + + end # class Interface + + class PublicInstanceMethodsInterface < BasicInterface + def initialize(prefix) + super(prefix) + end + + def get_methods(obj, delim=".") + prefix = @prefix + delim + obj.class.public_instance_methods.collect { |name| + [prefix + name, obj.method(name).to_proc, nil, nil] + } + end + end + + + end # module Service + + + # + # short-form to create a Service::Interface + # + def self.interface(prefix, &p) + Service::Interface.new(prefix, &p) + end + + # short-cut for creating a PublicInstanceMethodsInterface + def self.iPIMethods(prefix) + Service::PublicInstanceMethodsInterface.new(prefix) + end + + + module ParseContentType + def parse_content_type(str) + a, *b = str.split(";") + return a.strip, *b + end + end + +end # module XMLRPC +