2010-09-02 15:10:29 -04:00
|
|
|
require 'active_support/core_ext/benchmark'
|
2010-09-27 19:57:26 -04:00
|
|
|
require 'active_support/core_ext/uri'
|
2006-06-25 10:44:22 -04:00
|
|
|
require 'net/https'
|
|
|
|
require 'date'
|
|
|
|
require 'time'
|
|
|
|
require 'uri'
|
|
|
|
|
|
|
|
module ActiveResource
|
2007-06-23 13:29:54 -04:00
|
|
|
# Class to handle connections to remote web services.
|
|
|
|
# This class is used by ActiveResource::Base to interface with REST
|
|
|
|
# services.
|
2006-06-25 10:44:22 -04:00
|
|
|
class Connection
|
2008-08-29 21:19:18 -04:00
|
|
|
|
|
|
|
HTTP_FORMAT_HEADER_NAMES = { :get => 'Accept',
|
|
|
|
:put => 'Content-Type',
|
|
|
|
:post => 'Content-Type',
|
2009-08-09 17:24:50 -04:00
|
|
|
:delete => 'Accept',
|
|
|
|
:head => 'Accept'
|
2008-08-29 21:19:18 -04:00
|
|
|
}
|
|
|
|
|
2009-10-02 16:57:31 -04:00
|
|
|
attr_reader :site, :user, :password, :auth_type, :timeout, :proxy, :ssl_options
|
2007-09-20 19:18:05 -04:00
|
|
|
attr_accessor :format
|
2006-08-31 05:31:11 -04:00
|
|
|
|
2006-06-25 10:44:22 -04:00
|
|
|
class << self
|
|
|
|
def requests
|
|
|
|
@@requests ||= []
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2007-06-23 13:29:54 -04:00
|
|
|
# The +site+ parameter is required and will set the +site+
|
|
|
|
# attribute to the URI for the remote resource service.
|
2009-05-06 03:14:55 -04:00
|
|
|
def initialize(site, format = ActiveResource::Formats::XmlFormat)
|
2006-12-05 14:12:51 -05:00
|
|
|
raise ArgumentError, 'Missing site URI' unless site
|
2008-02-17 19:21:18 -05:00
|
|
|
@user = @password = nil
|
2006-12-05 14:12:51 -05:00
|
|
|
self.site = site
|
2007-09-20 19:18:05 -04:00
|
|
|
self.format = format
|
2006-09-29 12:25:49 -04:00
|
|
|
end
|
2006-12-05 14:12:51 -05:00
|
|
|
|
2007-01-16 01:34:10 -05:00
|
|
|
# Set URI for remote service.
|
2006-09-29 12:25:49 -04:00
|
|
|
def site=(site)
|
2010-09-27 19:57:26 -04:00
|
|
|
@site = site.is_a?(URI) ? site : URI.parser.parse(site)
|
|
|
|
@user = URI.parser.unescape(@site.user) if @site.user
|
|
|
|
@password = URI.parser.unescape(@site.password) if @site.password
|
2008-02-17 19:21:18 -05:00
|
|
|
end
|
|
|
|
|
2009-05-18 18:34:44 -04:00
|
|
|
# Set the proxy for remote service.
|
|
|
|
def proxy=(proxy)
|
2010-09-27 19:57:26 -04:00
|
|
|
@proxy = proxy.is_a?(URI) ? proxy : URI.parser.parse(proxy)
|
2009-05-18 18:34:44 -04:00
|
|
|
end
|
|
|
|
|
2009-03-24 08:15:43 -04:00
|
|
|
# Sets the user for remote service.
|
2008-02-17 19:21:18 -05:00
|
|
|
def user=(user)
|
|
|
|
@user = user
|
|
|
|
end
|
|
|
|
|
2009-03-24 08:15:43 -04:00
|
|
|
# Sets the password for remote service.
|
2008-02-17 19:21:18 -05:00
|
|
|
def password=(password)
|
|
|
|
@password = password
|
2006-06-25 10:44:22 -04:00
|
|
|
end
|
2006-08-31 05:31:11 -04:00
|
|
|
|
2009-10-02 16:57:31 -04:00
|
|
|
# Sets the auth type for remote service.
|
|
|
|
def auth_type=(auth_type)
|
|
|
|
@auth_type = legitimize_auth_type(auth_type)
|
|
|
|
end
|
|
|
|
|
2009-03-24 08:15:43 -04:00
|
|
|
# Sets the number of seconds after which HTTP requests to the remote service should time out.
|
2008-04-13 02:40:30 -04:00
|
|
|
def timeout=(timeout)
|
|
|
|
@timeout = timeout
|
|
|
|
end
|
|
|
|
|
2009-08-09 13:57:20 -04:00
|
|
|
# Hash of options applied to Net::HTTP instance when +site+ protocol is 'https'.
|
|
|
|
def ssl_options=(opts={})
|
|
|
|
@ssl_options = opts
|
|
|
|
end
|
|
|
|
|
2009-03-24 08:15:43 -04:00
|
|
|
# Executes a GET request.
|
2007-01-16 01:34:10 -05:00
|
|
|
# Used to get (find) resources.
|
2007-04-28 23:14:36 -04:00
|
|
|
def get(path, headers = {})
|
2010-09-18 06:09:37 -04:00
|
|
|
with_auth { request(:get, path, build_request_headers(headers, :get, self.site.merge(path))) }
|
2006-06-25 10:44:22 -04:00
|
|
|
end
|
2006-08-31 05:31:11 -04:00
|
|
|
|
2009-03-24 08:15:43 -04:00
|
|
|
# Executes a DELETE request (see HTTP protocol documentation if unfamiliar).
|
2007-01-16 01:34:10 -05:00
|
|
|
# Used to delete resources.
|
2007-04-28 23:14:36 -04:00
|
|
|
def delete(path, headers = {})
|
2009-10-02 16:57:31 -04:00
|
|
|
with_auth { request(:delete, path, build_request_headers(headers, :delete, self.site.merge(path))) }
|
2006-06-25 10:44:22 -04:00
|
|
|
end
|
2006-08-31 05:31:11 -04:00
|
|
|
|
2009-03-24 08:15:43 -04:00
|
|
|
# Executes a PUT request (see HTTP protocol documentation if unfamiliar).
|
2007-01-16 01:34:10 -05:00
|
|
|
# Used to update resources.
|
2007-04-28 23:14:36 -04:00
|
|
|
def put(path, body = '', headers = {})
|
2009-10-02 16:57:31 -04:00
|
|
|
with_auth { request(:put, path, body.to_s, build_request_headers(headers, :put, self.site.merge(path))) }
|
2006-06-25 10:44:22 -04:00
|
|
|
end
|
|
|
|
|
2009-03-24 08:15:43 -04:00
|
|
|
# Executes a POST request.
|
2007-01-16 01:34:10 -05:00
|
|
|
# Used to create new resources.
|
2007-04-28 23:14:36 -04:00
|
|
|
def post(path, body = '', headers = {})
|
2009-10-02 16:57:31 -04:00
|
|
|
with_auth { request(:post, path, body.to_s, build_request_headers(headers, :post, self.site.merge(path))) }
|
2006-06-25 10:44:22 -04:00
|
|
|
end
|
2006-08-31 05:31:11 -04:00
|
|
|
|
2009-03-24 08:15:43 -04:00
|
|
|
# Executes a HEAD request.
|
2008-05-02 09:45:23 -04:00
|
|
|
# Used to obtain meta-information about resources, such as whether they exist and their size (via response headers).
|
|
|
|
def head(path, headers = {})
|
2009-10-02 16:57:31 -04:00
|
|
|
with_auth { request(:head, path, build_request_headers(headers, :head, self.site.merge(path))) }
|
2008-02-09 17:21:55 -05:00
|
|
|
end
|
|
|
|
|
2006-06-25 10:44:22 -04:00
|
|
|
private
|
2009-03-24 08:15:43 -04:00
|
|
|
# Makes a request to the remote service.
|
2006-10-23 18:21:09 -04:00
|
|
|
def request(method, path, *arguments)
|
2010-04-28 00:16:06 -04:00
|
|
|
result = ActiveSupport::Notifications.instrument("request.active_resource") do |payload|
|
2010-01-15 08:45:18 -05:00
|
|
|
payload[:method] = method
|
|
|
|
payload[:request_uri] = "#{site.scheme}://#{site.host}:#{site.port}#{path}"
|
|
|
|
payload[:result] = http.send(method, path, *arguments)
|
2010-01-13 16:59:27 -05:00
|
|
|
end
|
2006-10-23 18:21:09 -04:00
|
|
|
handle_response(result)
|
2008-04-16 06:39:19 -04:00
|
|
|
rescue Timeout::Error => e
|
|
|
|
raise TimeoutError.new(e.message)
|
2009-08-09 13:57:20 -04:00
|
|
|
rescue OpenSSL::SSL::SSLError => e
|
|
|
|
raise SSLError.new(e.message)
|
2006-08-31 05:31:11 -04:00
|
|
|
end
|
2006-06-25 10:44:22 -04:00
|
|
|
|
2009-03-24 08:15:43 -04:00
|
|
|
# Handles response and error codes from the remote service.
|
2006-08-31 05:31:11 -04:00
|
|
|
def handle_response(response)
|
2006-06-25 10:44:22 -04:00
|
|
|
case response.code.to_i
|
2007-08-09 15:22:04 -04:00
|
|
|
when 301,302
|
|
|
|
raise(Redirection.new(response))
|
2006-08-31 05:31:11 -04:00
|
|
|
when 200...400
|
2006-06-25 10:44:22 -04:00
|
|
|
response
|
2007-12-14 18:09:46 -05:00
|
|
|
when 400
|
|
|
|
raise(BadRequest.new(response))
|
|
|
|
when 401
|
|
|
|
raise(UnauthorizedAccess.new(response))
|
|
|
|
when 403
|
|
|
|
raise(ForbiddenAccess.new(response))
|
2006-06-25 10:44:22 -04:00
|
|
|
when 404
|
|
|
|
raise(ResourceNotFound.new(response))
|
2007-05-26 16:57:08 -04:00
|
|
|
when 405
|
|
|
|
raise(MethodNotAllowed.new(response))
|
2006-09-08 20:11:12 -04:00
|
|
|
when 409
|
|
|
|
raise(ResourceConflict.new(response))
|
2009-08-09 17:53:04 -04:00
|
|
|
when 410
|
|
|
|
raise(ResourceGone.new(response))
|
2007-01-16 19:46:32 -05:00
|
|
|
when 422
|
|
|
|
raise(ResourceInvalid.new(response))
|
2006-09-07 20:07:30 -04:00
|
|
|
when 401...500
|
2006-06-25 10:44:22 -04:00
|
|
|
raise(ClientError.new(response))
|
|
|
|
when 500...600
|
|
|
|
raise(ServerError.new(response))
|
|
|
|
else
|
|
|
|
raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2009-03-24 08:15:43 -04:00
|
|
|
# Creates new Net::HTTP instance for communication with the
|
2007-01-16 01:34:10 -05:00
|
|
|
# remote service and resources.
|
2006-06-25 10:44:22 -04:00
|
|
|
def http
|
2009-08-09 05:24:35 -04:00
|
|
|
configure_http(new_http)
|
|
|
|
end
|
|
|
|
|
|
|
|
def new_http
|
|
|
|
if @proxy
|
|
|
|
Net::HTTP.new(@site.host, @site.port, @proxy.host, @proxy.port, @proxy.user, @proxy.password)
|
|
|
|
else
|
|
|
|
Net::HTTP.new(@site.host, @site.port)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def configure_http(http)
|
2009-08-09 13:57:20 -04:00
|
|
|
http = apply_ssl_options(http)
|
2009-08-09 05:24:35 -04:00
|
|
|
|
|
|
|
# Net::HTTP timeouts default to 60 seconds.
|
|
|
|
if @timeout
|
|
|
|
http.open_timeout = @timeout
|
|
|
|
http.read_timeout = @timeout
|
|
|
|
end
|
|
|
|
|
2007-11-18 19:06:23 -05:00
|
|
|
http
|
2006-06-25 10:44:22 -04:00
|
|
|
end
|
2007-09-20 19:18:05 -04:00
|
|
|
|
2009-08-09 13:57:20 -04:00
|
|
|
def apply_ssl_options(http)
|
|
|
|
return http unless @site.is_a?(URI::HTTPS)
|
|
|
|
|
|
|
|
http.use_ssl = true
|
|
|
|
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
|
|
return http unless defined?(@ssl_options)
|
|
|
|
|
|
|
|
http.ca_path = @ssl_options[:ca_path] if @ssl_options[:ca_path]
|
|
|
|
http.ca_file = @ssl_options[:ca_file] if @ssl_options[:ca_file]
|
|
|
|
|
|
|
|
http.cert = @ssl_options[:cert] if @ssl_options[:cert]
|
|
|
|
http.key = @ssl_options[:key] if @ssl_options[:key]
|
|
|
|
|
|
|
|
http.cert_store = @ssl_options[:cert_store] if @ssl_options[:cert_store]
|
|
|
|
http.ssl_timeout = @ssl_options[:ssl_timeout] if @ssl_options[:ssl_timeout]
|
|
|
|
|
|
|
|
http.verify_mode = @ssl_options[:verify_mode] if @ssl_options[:verify_mode]
|
|
|
|
http.verify_callback = @ssl_options[:verify_callback] if @ssl_options[:verify_callback]
|
|
|
|
http.verify_depth = @ssl_options[:verify_depth] if @ssl_options[:verify_depth]
|
|
|
|
|
|
|
|
http
|
|
|
|
end
|
|
|
|
|
2007-09-20 19:18:05 -04:00
|
|
|
def default_header
|
2008-08-29 21:19:18 -04:00
|
|
|
@default_header ||= {}
|
2007-09-20 19:18:05 -04:00
|
|
|
end
|
2008-04-13 02:40:30 -04:00
|
|
|
|
2007-01-16 01:34:10 -05:00
|
|
|
# Builds headers for request to remote service.
|
2009-10-02 16:57:31 -04:00
|
|
|
def build_request_headers(headers, http_method, uri)
|
|
|
|
authorization_header(http_method, uri).update(default_header).update(http_format_header(http_method)).update(headers)
|
|
|
|
end
|
|
|
|
|
|
|
|
def response_auth_header
|
|
|
|
@response_auth_header ||= ""
|
|
|
|
end
|
|
|
|
|
|
|
|
def with_auth
|
|
|
|
retried ||= false
|
|
|
|
yield
|
|
|
|
rescue UnauthorizedAccess => e
|
|
|
|
raise if retried || auth_type != :digest
|
|
|
|
@response_auth_header = e.response['WWW-Authenticate']
|
|
|
|
retried = true
|
|
|
|
retry
|
|
|
|
end
|
|
|
|
|
|
|
|
def authorization_header(http_method, uri)
|
|
|
|
if @user || @password
|
|
|
|
if auth_type == :digest
|
|
|
|
{ 'Authorization' => digest_auth_header(http_method, uri) }
|
|
|
|
else
|
|
|
|
{ 'Authorization' => 'Basic ' + ["#{@user}:#{@password}"].pack('m').delete("\r\n") }
|
|
|
|
end
|
|
|
|
else
|
|
|
|
{}
|
|
|
|
end
|
2006-09-29 12:25:49 -04:00
|
|
|
end
|
2008-04-13 02:40:30 -04:00
|
|
|
|
2009-10-02 16:57:31 -04:00
|
|
|
def digest_auth_header(http_method, uri)
|
|
|
|
params = extract_params_from_response
|
|
|
|
|
|
|
|
ha1 = Digest::MD5.hexdigest("#{@user}:#{params['realm']}:#{@password}")
|
|
|
|
ha2 = Digest::MD5.hexdigest("#{http_method.to_s.upcase}:#{uri.path}")
|
|
|
|
|
|
|
|
params.merge!('cnonce' => client_nonce)
|
|
|
|
request_digest = Digest::MD5.hexdigest([ha1, params['nonce'], "0", params['cnonce'], params['qop'], ha2].join(":"))
|
|
|
|
"Digest #{auth_attributes_for(uri, request_digest, params)}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def client_nonce
|
|
|
|
Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535)))
|
|
|
|
end
|
|
|
|
|
|
|
|
def extract_params_from_response
|
|
|
|
params = {}
|
|
|
|
if response_auth_header =~ /^(\w+) (.*)/
|
|
|
|
$2.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
|
|
|
|
end
|
|
|
|
params
|
|
|
|
end
|
|
|
|
|
|
|
|
def auth_attributes_for(uri, request_digest, params)
|
|
|
|
[
|
|
|
|
%Q(username="#{@user}"),
|
|
|
|
%Q(realm="#{params['realm']}"),
|
|
|
|
%Q(qop="#{params['qop']}"),
|
|
|
|
%Q(uri="#{uri.path}"),
|
|
|
|
%Q(nonce="#{params['nonce']}"),
|
|
|
|
%Q(nc="0"),
|
|
|
|
%Q(cnonce="#{params['cnonce']}"),
|
|
|
|
%Q(opaque="#{params['opaque']}"),
|
|
|
|
%Q(response="#{request_digest}")].join(", ")
|
2006-09-29 12:25:49 -04:00
|
|
|
end
|
2006-10-23 18:21:09 -04:00
|
|
|
|
2008-08-29 21:19:18 -04:00
|
|
|
def http_format_header(http_method)
|
|
|
|
{HTTP_FORMAT_HEADER_NAMES[http_method] => format.mime_type}
|
|
|
|
end
|
|
|
|
|
2009-10-02 16:57:31 -04:00
|
|
|
def legitimize_auth_type(auth_type)
|
|
|
|
return :basic if auth_type.nil?
|
|
|
|
auth_type = auth_type.to_sym
|
|
|
|
[:basic, :digest].include?(auth_type) ? auth_type : :basic
|
|
|
|
end
|
2006-06-25 10:44:22 -04:00
|
|
|
end
|
2007-11-18 19:06:23 -05:00
|
|
|
end
|