1# xmlrpc/client.rb 2# Copyright (C) 2001, 2002, 2003 by Michael Neumann (mneumann@ntecs.de) 3# 4# Released under the same term of license as Ruby. 5# 6# History 7# $Id: client.rb 44981 2014-02-15 15:48:17Z nagachika $ 8# 9require "xmlrpc/parser" 10require "xmlrpc/create" 11require "xmlrpc/config" 12require "xmlrpc/utils" # ParserWriterChooseMixin 13require "net/http" 14require "uri" 15 16module XMLRPC # :nodoc: 17 18 # Provides remote procedure calls to a XML-RPC server. 19 # 20 # After setting the connection-parameters with XMLRPC::Client.new which 21 # creates a new XMLRPC::Client instance, you can execute a remote procedure 22 # by sending the XMLRPC::Client#call or XMLRPC::Client#call2 23 # message to this new instance. 24 # 25 # The given parameters indicate which method to call on the remote-side and 26 # of course the parameters for the remote procedure. 27 # 28 # require "xmlrpc/client" 29 # 30 # server = XMLRPC::Client.new("www.ruby-lang.org", "/RPC2", 80) 31 # begin 32 # param = server.call("michael.add", 4, 5) 33 # puts "4 + 5 = #{param}" 34 # rescue XMLRPC::FaultException => e 35 # puts "Error:" 36 # puts e.faultCode 37 # puts e.faultString 38 # end 39 # 40 # or 41 # 42 # require "xmlrpc/client" 43 # 44 # server = XMLRPC::Client.new("www.ruby-lang.org", "/RPC2", 80) 45 # ok, param = server.call2("michael.add", 4, 5) 46 # if ok then 47 # puts "4 + 5 = #{param}" 48 # else 49 # puts "Error:" 50 # puts param.faultCode 51 # puts param.faultString 52 # end 53 class Client 54 55 USER_AGENT = "XMLRPC::Client (Ruby #{RUBY_VERSION})" 56 57 include ParserWriterChooseMixin 58 include ParseContentType 59 60 61 # Creates an object which represents the remote XML-RPC server on the 62 # given +host+. If the server is CGI-based, +path+ is the 63 # path to the CGI-script, which will be called, otherwise (in the 64 # case of a standalone server) +path+ should be <tt>"/RPC2"</tt>. 65 # +port+ is the port on which the XML-RPC server listens. 66 # 67 # If +proxy_host+ is given, then a proxy server listening at 68 # +proxy_host+ is used. +proxy_port+ is the port of the 69 # proxy server. 70 # 71 # Default values for +host+, +path+ and +port+ are 'localhost', '/RPC2' and 72 # '80' respectively using SSL '443'. 73 # 74 # If +user+ and +password+ are given, each time a request is sent, 75 # an Authorization header is sent. Currently only Basic Authentication is 76 # implemented, no Digest. 77 # 78 # If +use_ssl+ is set to +true+, communication over SSL is enabled. 79 # 80 # Parameter +timeout+ is the time to wait for a XML-RPC response, defaults to 30. 81 def initialize(host=nil, path=nil, port=nil, proxy_host=nil, proxy_port=nil, 82 user=nil, password=nil, use_ssl=nil, timeout=nil) 83 84 @http_header_extra = nil 85 @http_last_response = nil 86 @cookie = nil 87 88 @host = host || "localhost" 89 @path = path || "/RPC2" 90 @proxy_host = proxy_host 91 @proxy_port = proxy_port 92 @proxy_host ||= 'localhost' if @proxy_port != nil 93 @proxy_port ||= 8080 if @proxy_host != nil 94 @use_ssl = use_ssl || false 95 @timeout = timeout || 30 96 97 if use_ssl 98 require "net/https" 99 @port = port || 443 100 else 101 @port = port || 80 102 end 103 104 @user, @password = user, password 105 106 set_auth 107 108 # convert ports to integers 109 @port = @port.to_i if @port != nil 110 @proxy_port = @proxy_port.to_i if @proxy_port != nil 111 112 # HTTP object for synchronous calls 113 @http = net_http(@host, @port, @proxy_host, @proxy_port) 114 @http.use_ssl = @use_ssl if @use_ssl 115 @http.read_timeout = @timeout 116 @http.open_timeout = @timeout 117 118 @parser = nil 119 @create = nil 120 end 121 122 123 class << self 124 125 # Creates an object which represents the remote XML-RPC server at the 126 # given +uri+. The URI should have a host, port, path, user and password. 127 # Example: https://user:password@host:port/path 128 # 129 # Raises an ArgumentError if the +uri+ is invalid, 130 # or if the protocol isn't http or https. 131 # 132 # If a +proxy+ is given it should be in the form of "host:port". 133 # 134 # The optional +timeout+ defaults to 30 seconds. 135 def new2(uri, proxy=nil, timeout=nil) 136 begin 137 url = URI(uri) 138 rescue URI::InvalidURIError => e 139 raise ArgumentError, e.message, e.backtrace 140 end 141 142 unless URI::HTTP === url 143 raise ArgumentError, "Wrong protocol specified. Only http or https allowed!" 144 end 145 146 proto = url.scheme 147 user = url.user 148 passwd = url.password 149 host = url.host 150 port = url.port 151 path = url.path.empty? ? nil : url.request_uri 152 153 proxy_host, proxy_port = (proxy || "").split(":") 154 proxy_port = proxy_port.to_i if proxy_port 155 156 self.new(host, path, port, proxy_host, proxy_port, user, passwd, (proto == "https"), timeout) 157 end 158 159 alias new_from_uri new2 160 161 # Receives a Hash and calls XMLRPC::Client.new 162 # with the corresponding values. 163 # 164 # The +hash+ parameter has following case-insensitive keys: 165 # * host 166 # * path 167 # * port 168 # * proxy_host 169 # * proxy_port 170 # * user 171 # * password 172 # * use_ssl 173 # * timeout 174 def new3(hash={}) 175 176 # convert all keys into lowercase strings 177 h = {} 178 hash.each { |k,v| h[k.to_s.downcase] = v } 179 180 self.new(h['host'], h['path'], h['port'], h['proxy_host'], h['proxy_port'], h['user'], h['password'], 181 h['use_ssl'], h['timeout']) 182 end 183 184 alias new_from_hash new3 185 186 end 187 188 189 # Add additional HTTP headers to the request 190 attr_accessor :http_header_extra 191 192 # Returns the Net::HTTPResponse object of the last RPC. 193 attr_reader :http_last_response 194 195 # Get and set the HTTP Cookie header. 196 attr_accessor :cookie 197 198 199 # Return the corresponding attributes. 200 attr_reader :timeout, :user, :password 201 202 # Sets the Net::HTTP#read_timeout and Net::HTTP#open_timeout to 203 # +new_timeout+ 204 def timeout=(new_timeout) 205 @timeout = new_timeout 206 @http.read_timeout = @timeout 207 @http.open_timeout = @timeout 208 end 209 210 # Changes the user for the Basic Authentication header to +new_user+ 211 def user=(new_user) 212 @user = new_user 213 set_auth 214 end 215 216 # Changes the password for the Basic Authentication header to 217 # +new_password+ 218 def password=(new_password) 219 @password = new_password 220 set_auth 221 end 222 223 # Invokes the method named +method+ with the parameters given by 224 # +args+ on the XML-RPC server. 225 # 226 # The +method+ parameter is converted into a String and should 227 # be a valid XML-RPC method-name. 228 # 229 # Each parameter of +args+ must be of one of the following types, 230 # where Hash, Struct and Array can contain any of these listed _types_: 231 # 232 # * Fixnum, Bignum 233 # * TrueClass, FalseClass, +true+, +false+ 234 # * String, Symbol 235 # * Float 236 # * Hash, Struct 237 # * Array 238 # * Date, Time, XMLRPC::DateTime 239 # * XMLRPC::Base64 240 # * A Ruby object which class includes XMLRPC::Marshallable 241 # (only if Config::ENABLE_MARSHALLABLE is +true+). 242 # That object is converted into a hash, with one additional key/value 243 # pair <code>___class___</code> which contains the class name 244 # for restoring that object later. 245 # 246 # The method returns the return-value from the Remote Procedure Call. 247 # 248 # The type of the return-value is one of the types shown above. 249 # 250 # A Bignum is only allowed when it fits in 32-bit. A XML-RPC 251 # +dateTime.iso8601+ type is always returned as a XMLRPC::DateTime object. 252 # Struct is never returned, only a Hash, the same for a Symbol, where as a 253 # String is always returned. XMLRPC::Base64 is returned as a String from 254 # xmlrpc4r version 1.6.1 on. 255 # 256 # If the remote procedure returned a fault-structure, then a 257 # XMLRPC::FaultException exception is raised, which has two accessor-methods 258 # +faultCode+ an Integer, and +faultString+ a String. 259 def call(method, *args) 260 ok, param = call2(method, *args) 261 if ok 262 param 263 else 264 raise param 265 end 266 end 267 268 # The difference between this method and XMLRPC::Client#call is, that 269 # this method will <b>NOT</b> raise a XMLRPC::FaultException exception. 270 # 271 # The method returns an array of two values. The first value indicates if 272 # the second value is +true+ or an XMLRPC::FaultException. 273 # 274 # Both are explained in XMLRPC::Client#call. 275 # 276 # Simple to remember: The "2" in "call2" denotes the number of values it returns. 277 def call2(method, *args) 278 request = create().methodCall(method, *args) 279 data = do_rpc(request, false) 280 parser().parseMethodResponse(data) 281 end 282 283 # Similar to XMLRPC::Client#call, however can be called concurrently and 284 # use a new connection for each request. In contrast to the corresponding 285 # method without the +_async+ suffix, which use connect-alive (one 286 # connection for all requests). 287 # 288 # Note, that you have to use Thread to call these methods concurrently. 289 # The following example calls two methods concurrently: 290 # 291 # Thread.new { 292 # p client.call_async("michael.add", 4, 5) 293 # } 294 # 295 # Thread.new { 296 # p client.call_async("michael.div", 7, 9) 297 # } 298 # 299 def call_async(method, *args) 300 ok, param = call2_async(method, *args) 301 if ok 302 param 303 else 304 raise param 305 end 306 end 307 308 # Same as XMLRPC::Client#call2, but can be called concurrently. 309 # 310 # See also XMLRPC::Client#call_async 311 def call2_async(method, *args) 312 request = create().methodCall(method, *args) 313 data = do_rpc(request, true) 314 parser().parseMethodResponse(data) 315 end 316 317 318 # You can use this method to execute several methods on a XMLRPC server 319 # which support the multi-call extension. 320 # 321 # s.multicall( 322 # ['michael.add', 3, 4], 323 # ['michael.sub', 4, 5] 324 # ) 325 # # => [7, -1] 326 def multicall(*methods) 327 ok, params = multicall2(*methods) 328 if ok 329 params 330 else 331 raise params 332 end 333 end 334 335 # Same as XMLRPC::Client#multicall, but returns two parameters instead of 336 # raising an XMLRPC::FaultException. 337 # 338 # See XMLRPC::Client#call2 339 def multicall2(*methods) 340 gen_multicall(methods, false) 341 end 342 343 # Similar to XMLRPC::Client#multicall, however can be called concurrently and 344 # use a new connection for each request. In contrast to the corresponding 345 # method without the +_async+ suffix, which use connect-alive (one 346 # connection for all requests). 347 # 348 # Note, that you have to use Thread to call these methods concurrently. 349 # The following example calls two methods concurrently: 350 # 351 # Thread.new { 352 # p client.multicall_async("michael.add", 4, 5) 353 # } 354 # 355 # Thread.new { 356 # p client.multicall_async("michael.div", 7, 9) 357 # } 358 # 359 def multicall_async(*methods) 360 ok, params = multicall2_async(*methods) 361 if ok 362 params 363 else 364 raise params 365 end 366 end 367 368 # Same as XMLRPC::Client#multicall2, but can be called concurrently. 369 # 370 # See also XMLRPC::Client#multicall_async 371 def multicall2_async(*methods) 372 gen_multicall(methods, true) 373 end 374 375 376 # Returns an object of class XMLRPC::Client::Proxy, initialized with 377 # +prefix+ and +args+. 378 # 379 # A proxy object returned by this method behaves like XMLRPC::Client#call, 380 # i.e. a call on that object will raise a XMLRPC::FaultException when a 381 # fault-structure is returned by that call. 382 def proxy(prefix=nil, *args) 383 Proxy.new(self, prefix, args, :call) 384 end 385 386 # Almost the same like XMLRPC::Client#proxy only that a call on the returned 387 # XMLRPC::Client::Proxy object will return two parameters. 388 # 389 # See XMLRPC::Client#call2 390 def proxy2(prefix=nil, *args) 391 Proxy.new(self, prefix, args, :call2) 392 end 393 394 # Similar to XMLRPC::Client#proxy, however can be called concurrently and 395 # use a new connection for each request. In contrast to the corresponding 396 # method without the +_async+ suffix, which use connect-alive (one 397 # connection for all requests). 398 # 399 # Note, that you have to use Thread to call these methods concurrently. 400 # The following example calls two methods concurrently: 401 # 402 # Thread.new { 403 # p client.proxy_async("michael.add", 4, 5) 404 # } 405 # 406 # Thread.new { 407 # p client.proxy_async("michael.div", 7, 9) 408 # } 409 # 410 def proxy_async(prefix=nil, *args) 411 Proxy.new(self, prefix, args, :call_async) 412 end 413 414 # Same as XMLRPC::Client#proxy2, but can be called concurrently. 415 # 416 # See also XMLRPC::Client#proxy_async 417 def proxy2_async(prefix=nil, *args) 418 Proxy.new(self, prefix, args, :call2_async) 419 end 420 421 422 private 423 424 def net_http(host, port, proxy_host, proxy_port) 425 Net::HTTP.new host, port, proxy_host, proxy_port 426 end 427 428 def set_auth 429 if @user.nil? 430 @auth = nil 431 else 432 a = "#@user" 433 a << ":#@password" if @password != nil 434 @auth = "Basic " + [a].pack("m0") 435 end 436 end 437 438 def do_rpc(request, async=false) 439 header = { 440 "User-Agent" => USER_AGENT, 441 "Content-Type" => "text/xml; charset=utf-8", 442 "Content-Length" => request.bytesize.to_s, 443 "Connection" => (async ? "close" : "keep-alive") 444 } 445 446 header["Cookie"] = @cookie if @cookie 447 header.update(@http_header_extra) if @http_header_extra 448 449 if @auth != nil 450 # add authorization header 451 header["Authorization"] = @auth 452 end 453 454 resp = nil 455 @http_last_response = nil 456 457 if async 458 # use a new HTTP object for each call 459 http = net_http(@host, @port, @proxy_host, @proxy_port) 460 http.use_ssl = @use_ssl if @use_ssl 461 http.read_timeout = @timeout 462 http.open_timeout = @timeout 463 464 # post request 465 http.start { 466 resp = http.request_post(@path, request, header) 467 } 468 else 469 # reuse the HTTP object for each call => connection alive is possible 470 # we must start connection explicitely first time so that http.request 471 # does not assume that we don't want keepalive 472 @http.start if not @http.started? 473 474 # post request 475 resp = @http.request_post(@path, request, header) 476 end 477 478 @http_last_response = resp 479 480 data = resp.body 481 482 if resp.code == "401" 483 # Authorization Required 484 raise "Authorization failed.\nHTTP-Error: #{resp.code} #{resp.message}" 485 elsif resp.code[0,1] != "2" 486 raise "HTTP-Error: #{resp.code} #{resp.message}" 487 end 488 489 # assume text/xml on instances where Content-Type header is not set 490 ct_expected = resp["Content-Type"] || 'text/xml' 491 ct = parse_content_type(ct_expected).first 492 if ct != "text/xml" 493 if ct == "text/html" 494 raise "Wrong content-type (received '#{ct}' but expected 'text/xml'): \n#{data}" 495 else 496 raise "Wrong content-type (received '#{ct}' but expected 'text/xml')" 497 end 498 end 499 500 expected = resp["Content-Length"] || "<unknown>" 501 if data.nil? or data.bytesize == 0 502 raise "Wrong size. Was #{data.bytesize}, should be #{expected}" 503 elsif expected != "<unknown>" and expected.to_i != data.bytesize and resp["Transfer-Encoding"].nil? 504 raise "Wrong size. Was #{data.bytesize}, should be #{expected}" 505 end 506 507 set_cookies = resp.get_fields("Set-Cookie") 508 if set_cookies and !set_cookies.empty? 509 require 'webrick/cookie' 510 @cookie = set_cookies.collect do |set_cookie| 511 cookie = WEBrick::Cookie.parse_set_cookie(set_cookie) 512 WEBrick::Cookie.new(cookie.name, cookie.value).to_s 513 end.join("; ") 514 end 515 516 return data 517 end 518 519 def gen_multicall(methods=[], async=false) 520 meth = :call2 521 meth = :call2_async if async 522 523 ok, params = self.send(meth, "system.multicall", 524 methods.collect {|m| {'methodName' => m[0], 'params' => m[1..-1]} } 525 ) 526 527 if ok 528 params = params.collect do |param| 529 if param.is_a? Array 530 param[0] 531 elsif param.is_a? Hash 532 XMLRPC::FaultException.new(param["faultCode"], param["faultString"]) 533 else 534 raise "Wrong multicall return value" 535 end 536 end 537 end 538 539 return ok, params 540 end 541 542 543 544 # XML-RPC calls look nicer! 545 # 546 # You can call any method onto objects of that class - the object handles 547 # XMLRPC::Client::Proxy#method_missing and will forward the method call to 548 # a XML-RPC server. 549 # 550 # Don't use this class directly, instead use the public instance method 551 # XMLRPC::Client#proxy or XMLRPC::Client#proxy2. 552 # 553 # require "xmlrpc/client" 554 # 555 # server = XMLRPC::Client.new("www.ruby-lang.org", "/RPC2", 80) 556 # 557 # michael = server.proxy("michael") 558 # michael2 = server.proxy("michael", 4) 559 # 560 # # both calls should return the same value '9'. 561 # p michael.add(4,5) 562 # p michael2.add(5) 563 class Proxy 564 565 # Creates an object which provides XMLRPC::Client::Proxy#method_missing. 566 # 567 # The given +server+ must be an instance of XMLRPC::Client, which is the 568 # XML-RPC server to be used for a XML-RPC call. 569 # 570 # +prefix+ and +delim+ will be prepended to the method name called onto this object. 571 # 572 # An optional parameter +meth+ is the method to use for a RPC. 573 # It can be either, call, call2, call_async, call2_async 574 # 575 # +args+ are arguments which are automatically given to every XML-RPC 576 # call before being provided through +method_missing+. 577 def initialize(server, prefix, args=[], meth=:call, delim=".") 578 @server = server 579 @prefix = prefix ? prefix + delim : "" 580 @args = args 581 @meth = meth 582 end 583 584 # Every method call is forwarded to the XML-RPC server defined in 585 # XMLRPC::Client::Proxy#new. 586 # 587 # Note: Inherited methods from class Object cannot be used as XML-RPC 588 # names, because they get around +method_missing+. 589 def method_missing(mid, *args) 590 pre = @prefix + mid.to_s 591 arg = @args + args 592 @server.send(@meth, pre, *arg) 593 end 594 595 end # class Proxy 596 597 end # class Client 598 599end # module XMLRPC 600 601