lib/rufus/verbs/endpoint.rb in rufus-verbs-0.10 vs lib/rufus/verbs/endpoint.rb in rufus-verbs-1.0.0

- old
+ new

@@ -1,8 +1,7 @@ -# #-- -# Copyright (c) 2008, John Mettraux, jmettraux@gmail.com +# Copyright (c) 2008-2010, John Mettraux, jmettraux@gmail.com # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell @@ -18,22 +17,15 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # -# (MIT license) +# Made in Japan. #++ -# -# -# John Mettraux -# -# Made in Japan -# -# 2008/01/11 -# +require 'cgi' require 'uri' require 'yaml' # for StringIO (at least for now) require 'net/http' require 'zlib' @@ -44,604 +36,606 @@ module Rufus module Verbs - USER_AGENT = "Ruby rufus-verbs #{VERSION}" + USER_AGENT = "Ruby rufus-verbs #{VERSION}" + # + # An EndPoint can be used to share common options among a set of + # requests. + # + # ep = EndPoint.new( + # :host => "restful.server", + # :port => 7080, + # :resource => "inventory/tools") + # + # res = ep.get :id => 1 + # # still a silver bullet ? + # + # res = ep.get :id => 0 + # # where did the hammer go ? + # + # When a request gets prepared, the option values will be looked up + # in (1) its local (request) options, then (2) in the EndPoint options. + # + class EndPoint + + include CookieMixin + include DigestAuthMixin + include VerboseMixin + # - # An EndPoint can be used to share common options among a set of - # requests. + # The endpoint initialization opts (Hash instance) # - # ep = EndPoint.new( - # :host => "restful.server", - # :port => 7080, - # :resource => "inventory/tools") - # - # res = ep.get :id => 1 - # # still a silver bullet ? - # - # res = ep.get :id => 0 - # # where did the hammer go ? - # - # When a request gets prepared, the option values will be looked up - # in (1) its local (request) options, then (2) in the EndPoint options. - # - class EndPoint + attr_reader :opts - include CookieMixin - include DigestAuthMixin - include VerboseMixin + def initialize (opts) - # - # The endpoint initialization opts (Hash instance) - # - attr_reader :opts + @opts = opts - def initialize (opts) + compute_target @opts - @opts = opts + @opts[:http_basic_authentication] = + opts[:http_basic_authentication] || opts[:hba] - compute_target @opts + @opts[:user_agent] ||= USER_AGENT - @opts[:http_basic_authentication] = - opts[:http_basic_authentication] || opts[:hba] + @opts[:proxy] ||= ENV['HTTP_PROXY'] - @opts[:user_agent] ||= USER_AGENT + prepare_cookie_jar + end - @opts[:proxy] ||= ENV['HTTP_PROXY'] + def get (*args) - prepare_cookie_jar - end + request :get, args + end - def get (*args) + def post (*args, &block) - request :get, args - end + request :post, args, &block + end - def post (*args, &block) + def put (*args, &block) - request :post, args, &block - end + request :put, args, &block + end - def put (*args, &block) + def delete (*args) - request :put, args, &block - end + request :delete, args + end - def delete (*args) + def head (*args) - request :delete, args - end + request :head, args + end - def head (*args) + def options (*args) - request :head, args - end + request :options, args + end - def options (*args) + # + # This is the method called by the module methods verbs. + # + # For example, + # + # RufusVerbs.get(args) + # + # calls + # + # RufusVerbs::EndPoint.request(:get, args) + # + def self.request (method, args, &block) - request :options, args - end + opts = extract_opts args - # - # This is the method called by the module methods verbs. - # - # For example, - # - # RufusVerbs.get(args) - # - # calls - # - # RufusVerbs::EndPoint.request(:get, args) - # - def self.request (method, args, &block) + EndPoint.new(opts).request(method, opts, &block) + end - opts = extract_opts args + # + # The instance methods get, post, put and delete ultimately calls + # this request() method. All the work is done here. + # + def request (method, args, &block) - EndPoint.new(opts).request(method, opts, &block) - end + # prepare request - # - # The instance methods get, post, put and delete ultimately calls - # this request() method. All the work is done here. - # - def request (method, args, &block) + opts = EndPoint.extract_opts args - # prepare request + compute_target opts - opts = EndPoint.extract_opts args + req = create_request method, opts - compute_target opts + add_payload(req, opts, &block) if method == :post or method == :put - req = create_request method, opts + add_authentication(req, opts) - add_payload(req, opts, &block) if method == :post or method == :put + add_conditional_headers(req, opts) if method == :get - add_authentication(req, opts) + mention_cookies(req, opts) + # if the :cookies option is disabled (the default) + # will have no effect - add_conditional_headers(req, opts) if method == :get + vlog_request opts, req - mention_cookies(req, opts) - # if the :cookies option is disabled (the default) - # will have no effect + return req if o(opts, :dry_run) == true - vlog_request opts, req + # trigger request - return req if o(opts, :dry_run) == true + http = prepare_http opts - # trigger request + vlog_http opts, http - http = prepare_http opts + res = nil - vlog_http opts, http + http.start do + res = http.request req + end - res = nil + # handle response - http.start do - res = http.request req - end + class << res + attr_accessor :request + end + res.request = req - # handle response + vlog_response opts, res - class << res - attr_accessor :request - end - res.request = req + register_cookies res, opts + # if the :cookies option is disabled (the default) + # will have no effect - vlog_response opts, res + return res if o(opts, :raw_response) - register_cookies res, opts - # if the :cookies option is disabled (the default) - # will have no effect + check_authentication_info res, opts + # used in case of :digest_authentication + # will have no effect else - return res if o(opts, :raw_response) + res = handle_response method, res, opts - check_authentication_info res, opts - # used in case of :digest_authentication - # will have no effect else + return parse_options(res) if method == :options - res = handle_response method, res, opts + return res.body if o(opts, :body) - return parse_options(res) if method == :options + res + end - return res.body if o(opts, :body) + private - res - end + # + # Manages various args formats : + # + # uri + # [ uri ] + # [ uri, opts ] + # opts + # + def self.extract_opts (args) - private + opts = {} - # - # Manages various args formats : - # - # uri - # [ uri ] - # [ uri, opts ] - # opts - # - def self.extract_opts (args) + args = [ args ] unless args.is_a?(Array) - opts = {} + opts = args.last \ + if args.last.is_a?(Hash) - args = [ args ] unless args.is_a?(Array) + opts[:uri] = args.first \ + if args.first.is_a?(String) or args.first.is_a?(URI) - opts = args.last \ - if args.last.is_a?(Hash) + opts + end - opts[:uri] = args.first \ - if args.first.is_a?(String) or args.first.is_a?(URI) + # + # Returns the value from the [request] opts or from the + # [endpoint] @opts. + # + def o (opts, key) - opts - end + keys = Array key + keys.each { |k| (v = opts[k]; return v if v != nil) } + keys.each { |k| (v = @opts[k]; return v if v != nil) } + nil + end - # - # Returns the value from the [request] opts or from the - # [endpoint] @opts. - # - def o (opts, key) + # + # Returns scheme, host, port, path, query + # + def compute_target (opts) - keys = Array key - keys.each { |k| (v = opts[k]; return v if v != nil) } - keys.each { |k| (v = @opts[k]; return v if v != nil) } - nil - end + u = opts[:uri] || opts[:u] - # - # Returns scheme, host, port, path, query - # - def compute_target (opts) + r = if opts[:host] - u = opts[:uri] || opts[:u] + [ opts[:scheme] || 'http', + opts[:host], + opts[:port] || 80, + opts[:path] || '/', + opts[:query] || opts[:params] ] - r = if opts[:host] + elsif u - [ opts[:scheme] || 'http', - opts[:host], - opts[:port] || 80, - opts[:path] || '/', - opts[:query] || opts[:params] ] + u = URI.parse u.to_s unless u.is_a?(URI) + [ u.scheme, + u.host, + u.port, + u.path, + query_to_h(u.query) ] + else - elsif u + [] + end - u = URI.parse u.to_s unless u.is_a?(URI) - [ u.scheme, - u.host, - u.port, - u.path, - query_to_h(u.query) ] - else + opts[:scheme] = r[0] || @opts[:scheme] + opts[:host] = r[1] || @opts[:host] + opts[:port] = r[2] || @opts[:port] + opts[:path] = r[3] || @opts[:path] - [] - end + opts[:query] = + r[4] || + opts[:params] || opts[:query] || + @opts[:query] || @opts[:params] || + {} - opts[:scheme] = r[0] || @opts[:scheme] - opts[:host] = r[1] || @opts[:host] - opts[:port] = r[2] || @opts[:port] - opts[:path] = r[3] || @opts[:path] + opts.delete :path if opts[:path] == '' - opts[:query] = - r[4] || - opts[:params] || opts[:query] || - @opts[:query] || @opts[:params] || - {} + opts[:c_uri] = [ + opts[:scheme], + opts[:host], + opts[:port], + opts[:path], + opts[:query] ].inspect + # + # can be used for conditional gets - opts.delete :path if opts[:path] == "" + r + end - opts[:c_uri] = [ - opts[:scheme], - opts[:host], - opts[:port], - opts[:path], - opts[:query] ].inspect - # - # can be used for conditional gets + # + # Creates the Net::HTTP request instance. + # + # If :fake_put is set, will use Net::HTTP::Post + # and make sure the query string contains '_method=put' (or + # '_method=delete'). + # + # This call will also advertise this rufus-verbs as + # 'accepting the gzip encoding' (in case of GET). + # + def create_request (method, opts) - r - end + if (o(opts, :fake_put) and + (method == :put or method == :delete)) - # - # Creates the Net::HTTP request instance. - # - # If :fake_put is set, will use Net::HTTP::Post - # and make sure the query string contains '_method=put' (or - # '_method=delete'). - # - # This call will also advertise this rufus-verbs as - # 'accepting the gzip encoding' (in case of GET). - # - def create_request (method, opts) + opts[:query][:_method] = method.to_s + method = :post + end - if (o(opts, :fake_put) and - (method == :put or method == :delete)) + path = compute_path opts - opts[:query][:_method] = method.to_s - method = :post - end + r = eval("Net::HTTP::#{method.to_s.capitalize}").new path - path = compute_path opts + r['User-Agent'] = o(opts, :user_agent) + # potentially overriden by opts[:headers] - r = eval("Net::HTTP::#{method.to_s.capitalize}").new path + h = opts[:headers] || opts[:h] + h.each { |k, v| r[k] = v } if h - r['User-Agent'] = o(opts, :user_agent) - # potentially overriden by opts[:headers] + r['Accept-Encoding'] = 'gzip' \ + if method == :get and not o(opts, :nozip) - h = opts[:headers] || opts[:h] - h.each { |k, v| r[k] = v } if h + r + end - r['Accept-Encoding'] = 'gzip' \ - if method == :get and not o(opts, :nozip) + # + # If @user and @pass are set, will activate basic authentication. + # Else if the @auth option is set, will assume it contains a Proc + # and will call it (with the request as a parameter). + # + # This comment is too much... Just read the code... + # + def add_authentication (req, opts) - r - end + b = o(opts, :http_basic_authentication) + d = o(opts, :digest_authentication) + o = o(opts, :auth) - # - # If @user and @pass are set, will activate basic authentication. - # Else if the @auth option is set, will assume it contains a Proc - # and will call it (with the request as a parameter). - # - # This comment is too much... Just read the code... - # - def add_authentication (req, opts) + if b and b != false - b = o(opts, :http_basic_authentication) - d = o(opts, :digest_authentication) - o = o(opts, :auth) + req.basic_auth b[0], b[1] - if b and b != false + elsif d and d != false - req.basic_auth b[0], b[1] + digest_auth req, opts - elsif d and d != false + elsif o and o != false - digest_auth req, opts + o.call req + end + end - elsif o and o != false + # + # In that base class, it's empty. + # It's implemented in ConditionalEndPoint. + # + # Only called for a GET. + # + def add_conditional_headers (req, opts) - o.call req - end - end + # nada + end - # - # In that base class, it's empty. - # It's implemented in ConditionalEndPoint. - # - # Only called for a GET. - # - def add_conditional_headers (req, opts) + # + # Prepares a Net::HTTP instance, with potentially some + # https settings. + # + def prepare_http (opts) - # nada - end + compute_proxy opts - # - # Prepares a Net::HTTP instance, with potentially some - # https settings. - # - def prepare_http (opts) + http = Net::HTTP.new( + opts[:host], opts[:port], + opts[:proxy_host], opts[:proxy_port], + opts[:proxy_user], opts[:proxy_pass]) - compute_proxy opts + set_timeout http, opts - http = Net::HTTP.new( - opts[:host], opts[:port], - opts[:proxy_host], opts[:proxy_port], - opts[:proxy_user], opts[:proxy_pass]) + return http unless opts[:scheme].to_s == 'https' - set_timeout http, opts + require 'net/https' - return http unless opts[:scheme] == 'https' + http.use_ssl = true + http.enable_post_connection_check = true \ + if http.respond_to?(:enable_post_connection_check=) - require 'net/https' + http.verify_mode = if o(opts, :ssl_verify_peer) + OpenSSL::SSL::VERIFY_PEER + else + OpenSSL::SSL::VERIFY_NONE + end - http.use_ssl = true - http.enable_post_connection_check = true + store = OpenSSL::X509::Store.new + store.set_default_paths + http.cert_store = store - http.verify_mode = if o(opts, :ssl_verify_peer) - OpenSSL::SSL::VERIFY_PEER - else - OpenSSL::SSL::VERIFY_NONE - end + http + end - store = OpenSSL::X509::Store.new - store.set_default_paths - http.cert_store = store + # + # Sets both the open_timeout and the read_timeout for the http + # instance + # + def set_timeout (http, opts) - http - end + to = o(opts, :timeout) || o(opts, :to) + to = to.to_i - # - # Sets both the open_timeout and the read_timeout for the http - # instance - # - def set_timeout (http, opts) + return if to == 0 - to = o(opts, :timeout) || o(opts, :to) - to = to.to_i + http.open_timeout = to + http.read_timeout = to + end - return if to == 0 + # + # Makes sure the request opts hold the proxy information. + # + # If the option :proxy is set to false, no proxy will be used. + # + def compute_proxy (opts) - http.open_timeout = to - http.read_timeout = to - end + p = o(opts, :proxy) - # - # Makes sure the request opts hold the proxy information. - # - # If the option :proxy is set to false, no proxy will be used. - # - def compute_proxy (opts) + return unless p - p = o(opts, :proxy) + u = URI.parse p.to_s - return unless p + raise "not an HTTP[S] proxy '#{u.host}'" \ + unless u.scheme.match(/^http/) - u = URI.parse p.to_s + opts[:proxy_host] = u.host + opts[:proxy_port] = u.port + opts[:proxy_user] = u.user + opts[:proxy_pass] = u.password + end - raise "not an HTTP[S] proxy '#{u.host}'" \ - unless u.scheme.match(/^http/) + # + # Determines the full path of the request (path_info and + # query_string). + # + # For example : + # + # /items/4?style=whatever&maxcount=12 + # + def compute_path (opts) - opts[:proxy_host] = u.host - opts[:proxy_port] = u.port - opts[:proxy_user] = u.user - opts[:proxy_pass] = u.password - end + b = o(opts, :base) + r = o(opts, [ :res, :resource ]) + i = o(opts, :id) - # - # Determines the full path of the request (path_info and - # query_string). - # - # For example : - # - # /items/4?style=whatever&maxcount=12 - # - def compute_path (opts) + path = o(opts, :path) - b = o(opts, :base) - r = o(opts, [ :res, :resource ]) - i = o(opts, :id) + if b or r or i + path = b ? "/#{b}" : '' + path += "/#{r}" if r + path += "/#{i}" if i + end - path = o(opts, :path) + path = path[1..-1] if path[0..1] == '//' - if b or r or i - path = "" - path = "/#{b}" if b - path += "/#{r}" if r - path += "/#{i}" if i - end + query = opts[:query] || opts[:params] - path = path[1..-1] if path[0..1] == '//' + return path if not query or query.size < 1 - query = opts[:query] || opts[:params] + path + '?' + h_to_query(query, opts) + end - return path if not query or query.size < 1 + # + # "a=A&b=B" -> { "a" => "A", "b" => "B" } + # + def query_to_h (q) - path + '?' + h_to_query(query, opts) - end + return nil unless q - # - # "a=A&b=B" -> { "a" => "A", "b" => "B" } - # - def query_to_h (q) + q.split('&').inject({}) do |r, e| + s = e.split('=') + r[s[0]] = s[1] + r + end + end - return nil unless q + # + # { "a" => "A", "b" => "B" } -> "a=A&b=B" + # + def h_to_query (h, opts) - q.split("&").inject({}) do |r, e| - s = e.split("=") - r[s[0]] = s[1] - r - end - end + h.entries.collect { |k, v| + unless o(opts, :no_escape) + #k = URI.escape k.to_s + #v = URI.escape v.to_s + k = CGI.escape(k.to_s) + v = CGI.escape(v.to_s) + end + "#{k}=#{v}" + }.join('&') + end - # - # { "a" => "A", "b" => "B" } -> "a=A&b=B" - # - def h_to_query (h, opts) + # + # Fills the request body (with the content of :d or :fd). + # + def add_payload (req, opts, &block) - h.entries.collect { |k, v| - unless o(opts, :no_escape) - k = URI.escape k.to_s - v = URI.escape v.to_s - end - "#{k}=#{v}" - }.join("&") - end + d = opts[:d] || opts[:data] + fd = opts[:fd] || opts[:form_data] - # - # Fills the request body (with the content of :d or :fd). - # - def add_payload (req, opts, &block) + if d + req.body = d + elsif fd + sep = opts[:fd_sep] #|| nil + req.set_form_data fd, sep + elsif block + req.body = block.call req + else + req.body = '' + end + end - d = opts[:d] || opts[:data] - fd = opts[:fd] || opts[:form_data] + # + # Handles the server response. + # Eventually follows redirections. + # + # Once the final response has been hit, will make sure + # it's decompressed. + # + def handle_response (method, res, opts) - if d - req.body = d - elsif fd - sep = opts[:fd_sep] #|| nil - req.set_form_data fd, sep - elsif block - req.body = block.call req - else - req.body = "" - end - end + nored = o(opts, [ :no_redirections, :noredir ]) - # - # Handles the server response. - # Eventually follows redirections. - # - # Once the final response has been hit, will make sure - # it's decompressed. - # - def handle_response (method, res, opts) + #if res.is_a?(Net::HTTPRedirection) + if [ 301, 302, 303, 307 ].include?(res.code.to_i) and (nored != true) - nored = o(opts, [ :no_redirections, :noredir ]) + maxr = o(opts, :max_redirections) - #if res.is_a?(Net::HTTPRedirection) - if [ 301, 303, 307 ].include?(res.code.to_i) and (nored != true) + if maxr + maxr = maxr - 1 + raise 'too many redirections' if maxr == -1 + opts[:max_redirections] = maxr + end - maxr = o(opts, :max_redirections) + location = res['Location'] - if maxr - maxr = maxr - 1 - raise "too many redirections" if maxr == -1 - opts[:max_redirections] = maxr - end + prev_host = [ opts[:scheme], opts[:host] ] - location = res['Location'] + if location.match /^http/ + u = URI::parse location + opts[:scheme] = u.scheme + opts[:host] = u.host + opts[:port] = u.port + opts[:path] = u.path + opts[:query] = u.query + else + opts[:path], opts[:query] = location.split "?" + end - prev_host = [ opts[:scheme], opts[:host] ] + if (authentication_is_on?(opts) and + [ opts[:scheme], opts[:host] ] != prev_host) - if location.match /^http/ - u = URI::parse location - opts[:scheme] = u.scheme - opts[:host] = u.host - opts[:port] = u.port - opts[:path] = u.path - opts[:query] = u.query - else - opts[:path], opts[:query] = location.split "?" - end + raise( + "getting redirected to #{location} while " + + "authentication is on. Stopping.") + end - if (authentication_is_on?(opts) and - [ opts[:scheme], opts[:host] ] != prev_host) + opts[:query] = query_to_h opts[:query] - raise( - "getting redirected to #{location} while " + - "authentication is on. Stopping.") - end + return request(method, opts) + # + # following the redirection + end - opts[:query] = query_to_h opts[:query] + decompress res - return request(method, opts) - # - # following the redirection - end + res + end - decompress res + # + # Returns an array of symbols, like for example + # + # [ :get, :post ] + # + # obtained by parsing the 'Allow' response header. + # + # This method is used to provide the result of an OPTIONS + # HTTP method. + # + def parse_options (res) - res - end + s = res['Allow'] - # - # Returns an array of symbols, like for example - # - # [ :get, :post ] - # - # obtained by parsing the 'Allow' response header. - # - # This method is used to provide the result of an OPTIONS - # HTTP method. - # - def parse_options (res) + return [] unless s - s = res['Allow'] + s.split(',').collect do |m| + m.strip.downcase.to_sym + end + end - return [] unless s + # + # Returns true if the current request has authentication + # going on. + # + def authentication_is_on? (opts) - s.split(",").collect do |m| - m.strip.downcase.to_sym - end - end + (o(opts, [ :http_basic_authentication, :hba, :auth ]) != nil) + end - # - # Returns true if the current request has authentication - # going on. - # - def authentication_is_on? (opts) + # + # Inflates the response body if necessary. + # + def decompress (res) - (o(opts, [ :http_basic_authentication, :hba, :auth ]) != nil) - end + if res['content-encoding'] == 'gzip' - # - # Inflates the response body if necessary. - # - def decompress (res) + class << res - if res['content-encoding'] == 'gzip' + attr_accessor :deflated_body - class << res + alias :old_body :body - attr_accessor :deflated_body + def body + @deflated_body || old_body + end + end + # + # reopened the response to add + # a 'deflated_body' attr and let the the body + # method point to it - alias :old_body :body + # now deflate... - def body - @deflated_body || old_body - end - end - # - # reopened the response to add - # a 'deflated_body' attr and let the the body - # method point to it - - # now deflate... - - io = StringIO.new res.body - gz = Zlib::GzipReader.new io - res.deflated_body = gz.read - gz.close - end - end + io = StringIO.new res.body + gz = Zlib::GzipReader.new io + res.deflated_body = gz.read + gz.close + end end + end end end