require 'uri' require 'net/http' module REST # Request holds a HTTP request class Request attr_accessor :verb, :url, :body, :headers, :options, :request # * verb: The verb to use in the request, either :get, :head, :patch, :put, or :post # * url: The URL to send the request to, must be a URI instance # * body: The body to use in the request # * headers: A hash of headers to add to the request # * options: A hash of additional options # * username: Username to use for basic authentication # * password: Password to use for basic authentication # * tls_verify/verify_ssl: Verify the server certificate against known CA's # * tls_ca_file: Use a specific file for CA certificates instead of the built-in one # this only works when :tls_verify is also set. # * tls_key_and_certificate_file: The client key and certificate file to use for this # request # * tls_certificate: The client certficate to use for this request # * tls_key: The client private key to use for this request # * configure_block: An optional block that yields the underlying Net::HTTP # request object allowing for more fine-grained configuration # # == Examples # # request = REST::Request.new(:get, URI.parse('http://example.com/pigeons/1')) # # request = REST::Request.new(:head, URI.parse('http://example.com/pigeons/1')) # # request = REST::Request.new(:post, # URI.parse('http://example.com/pigeons'), # {'name' => 'Homr'}.to_json, # {'Accept' => 'application/json, */*', 'Content-Type' => 'application/json; charset=utf-8'} # ) # # # Pass a block to configure the underlying +Net::HTTP+ request. # request = REST::Request.new(:get, URI.parse('http://example.com/pigeons/largest')) do |http_request| # http_request.open_timeout = 15 # seconds # end # # == Authentication example # # request = REST::Request.new(:put, # URI.parse('http://example.com/pigeons/1'), # {'name' => 'Homer'}.to_json, # {'Accept' => 'application/json, */*', 'Content-Type' => 'application/json; charset=utf-8'}, # {:username => 'Admin', :password => 'secret'} # ) # # == TLS / SSL examples # # # Use a client key and certificate # request = REST::Request.new(:get, URI.parse('https://example.com/pigeons/1'), nil, {}, { # :tls_key_and_certificate_file => '/home/alice/keys/example.pem' # }) # # # Use a client certificate and key from a specific location # key_and_certificate = File.read('/home/alice/keys/example.pem') # request = REST::Request.new(:get, URI.parse('https://example.com/pigeons/1'), nil, {}, { # :tls_key => OpenSSL::PKey::RSA.new(key_and_certificate), # :tls_certificate => OpenSSL::X509::Certificate.new(key_and_certificate) # }) # # # Verify the server certificate against a specific certificate # request = REST::Request.new(:get, URI.parse('https://example.com/pigeons/1'), nil, {}, { # :tls_verify => true, # :tls_ca_file => '/home/alice/keys/example.pem' # }) def initialize(verb, url, body=nil, headers={}, options={}, &configure_block) @verb = verb @url = url @body = body @headers = headers @options = options @configure_block = configure_block end # Returns the path (including the query) for the request def path [url.path.empty? ? '/' : url.path, url.query].compact.join('?') end def proxy_env { 'http' => ENV['HTTP_PROXY'] || ENV['http_proxy'], 'https' => ENV['HTTPS_PROXY'] || ENV['https_proxy'] } end def proxy_settings proxy_env[url.scheme] ? URI.parse(proxy_env[url.scheme]) : nil end def http_proxy if settings = proxy_settings Net::HTTP.Proxy(settings.host, settings.port, settings.user, settings.password) end end # Configures and returns a new Net::HTTP request object def http_request if http_proxy http_request = http_proxy.new(url.host, url.port) else http_request = Net::HTTP.new(url.host, url.port) end # enable SSL/TLS if url.scheme == 'https' require 'net/https' require 'openssl' http_request.use_ssl = true if options[:tls_verify] or options[:verify_ssl] if http_request.respond_to?(:enable_post_connection_check=) http_request.enable_post_connection_check = true end # from http://curl.haxx.se/ca/cacert.pem http_request.ca_file = options[:tls_ca_file] || File.expand_path('../../../support/cacert.pem', __FILE__) http_request.verify_mode = OpenSSL::SSL::VERIFY_PEER else http_request.verify_mode = OpenSSL::SSL::VERIFY_NONE end if options[:tls_key_and_certificate_file] key_and_certificate = File.read(options[:tls_key_and_certificate_file]) options[:tls_key] = OpenSSL::PKey::RSA.new(key_and_certificate) options[:tls_certificate] = OpenSSL::X509::Certificate.new(key_and_certificate) end if options[:tls_key] and options[:tls_certificate] http_request.key = options[:tls_key] http_request.cert = options[:tls_certificate] elsif options[:tls_key] || options[:tls_certificate] raise ArgumentError, "Please specify both the certificate and private key (:tls_key and :tls_certificate)" end end if @configure_block @configure_block.call(http_request) end http_request end def request_for_verb case verb when :get Net::HTTP::Get.new(path, headers) when :head Net::HTTP::Head.new(path, headers) when :delete Net::HTTP::Delete.new(path, headers) when :patch if defined?(Net::HTTP::Patch) Net::HTTP::Patch.new(path, headers) else raise ArgumentError, "This version of the Ruby standard library doesn't support PATCH" end when :put Net::HTTP::Put.new(path, headers) when :post Net::HTTP::Post.new(path, headers) else raise ArgumentError, "Unknown HTTP verb `#{verb}'" end end # Performs the actual request and returns a REST::Response object with the response def perform self.request = request_for_verb if [:patch, :put, :post].include?(verb) request.body = body end if options[:username] and options[:password] request.basic_auth(options[:username], options[:password]) end http_request = http_request() begin response = http_request.start { |http| http.request(request) } rescue EOFError => error raise REST::DisconnectedError, error.message end REST::Response.new(response.code, response.instance_variable_get('@header'), response.body) end # Shortcut for REST::Request.new(*args).perform. # # See new for options. def self.perform(*args, &configure_block) request = new(*args, &configure_block) request.perform end end end