connection.rb 5.6 KB
Newer Older
1
require 'active_support/core_ext/benchmark'
2 3 4 5 6 7
require 'net/https'
require 'date'
require 'time'
require 'uri'

module ActiveResource
8 9 10
  # Class to handle connections to remote web services.
  # This class is used by ActiveResource::Base to interface with REST
  # services.
11
  class Connection
12 13 14 15 16 17 18

    HTTP_FORMAT_HEADER_NAMES = {  :get => 'Accept',
      :put => 'Content-Type',
      :post => 'Content-Type',
      :delete => 'Accept'
    }

M
Marshall Huss 已提交
19
    attr_reader :site, :user, :password, :timeout, :proxy
20
    attr_accessor :format
21

22 23 24 25 26 27
    class << self
      def requests
        @@requests ||= []
      end
    end

28 29
    # The +site+ parameter is required and will set the +site+
    # attribute to the URI for the remote resource service.
30
    def initialize(site, format = ActiveResource::Formats::XmlFormat)
31
      raise ArgumentError, 'Missing site URI' unless site
32
      @user = @password = nil
33
      self.site = site
34
      self.format = format
35
    end
36

37
    # Set URI for remote service.
38 39
    def site=(site)
      @site = site.is_a?(URI) ? site : URI.parse(site)
40 41
      @user = URI.decode(@site.user) if @site.user
      @password = URI.decode(@site.password) if @site.password
42 43
    end

M
Marshall Huss 已提交
44 45 46 47 48
    # Set the proxy for remote service.
    def proxy=(proxy)
      @proxy = proxy.is_a?(URI) ? proxy : URI.parse(proxy)
    end

P
Pratik Naik 已提交
49
    # Sets the user for remote service.
50 51 52 53
    def user=(user)
      @user = user
    end

P
Pratik Naik 已提交
54
    # Sets the password for remote service.
55 56
    def password=(password)
      @password = password
57
    end
58

P
Pratik Naik 已提交
59
    # Sets the number of seconds after which HTTP requests to the remote service should time out.
60 61 62 63
    def timeout=(timeout)
      @timeout = timeout
    end

P
Pratik Naik 已提交
64
    # Executes a GET request.
65
    # Used to get (find) resources.
66
    def get(path, headers = {})
67
      format.decode(request(:get, path, build_request_headers(headers, :get)).body)
68
    end
69

P
Pratik Naik 已提交
70
    # Executes a DELETE request (see HTTP protocol documentation if unfamiliar).
71
    # Used to delete resources.
72
    def delete(path, headers = {})
73
      request(:delete, path, build_request_headers(headers, :delete))
74
    end
75

P
Pratik Naik 已提交
76
    # Executes a PUT request (see HTTP protocol documentation if unfamiliar).
77
    # Used to update resources.
78
    def put(path, body = '', headers = {})
79
      request(:put, path, body.to_s, build_request_headers(headers, :put))
80 81
    end

P
Pratik Naik 已提交
82
    # Executes a POST request.
83
    # Used to create new resources.
84
    def post(path, body = '', headers = {})
85
      request(:post, path, body.to_s, build_request_headers(headers, :post))
86
    end
87

P
Pratik Naik 已提交
88
    # Executes a HEAD request.
89 90
    # Used to obtain meta-information about resources, such as whether they exist and their size (via response headers).
    def head(path, headers = {})
91 92 93
      request(:head, path, build_request_headers(headers))
    end

94

95
    private
P
Pratik Naik 已提交
96
      # Makes a request to the remote service.
97
      def request(method, path, *arguments)
98
        logger.info "#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}" if logger
99
        result = nil
J
Jeremy Kemper 已提交
100 101
        ms = Benchmark.ms { result = http.send(method, path, *arguments) }
        logger.info "--> %d %s (%d %.0fms)" % [result.code, result.message, result.body ? result.body.length : 0, ms] if logger
102
        handle_response(result)
103 104
      rescue Timeout::Error => e
        raise TimeoutError.new(e.message)
105
      end
106

P
Pratik Naik 已提交
107
      # Handles response and error codes from the remote service.
108
      def handle_response(response)
109
        case response.code.to_i
110 111
          when 301,302
            raise(Redirection.new(response))
112
          when 200...400
113
            response
114 115 116 117 118 119
          when 400
            raise(BadRequest.new(response))
          when 401
            raise(UnauthorizedAccess.new(response))
          when 403
            raise(ForbiddenAccess.new(response))
120 121
          when 404
            raise(ResourceNotFound.new(response))
122 123
          when 405
            raise(MethodNotAllowed.new(response))
124 125
          when 409
            raise(ResourceConflict.new(response))
126 127
          when 422
            raise(ResourceInvalid.new(response))
128
          when 401...500
129 130 131 132 133 134 135 136
            raise(ClientError.new(response))
          when 500...600
            raise(ServerError.new(response))
          else
            raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
        end
      end

P
Pratik Naik 已提交
137
      # Creates new Net::HTTP instance for communication with the
138
      # remote service and resources.
139
      def http
140 141 142 143 144 145 146 147 148 149 150 151
        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)
M
Marshall Huss 已提交
152
        http.use_ssl = @site.is_a?(URI::HTTPS)
153
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl?
154 155 156 157 158 159 160

        # Net::HTTP timeouts default to 60 seconds.
        if @timeout
          http.open_timeout = @timeout
          http.read_timeout = @timeout
        end

161
        http
162
      end
163 164

      def default_header
165
        @default_header ||= {}
166
      end
167

168
      # Builds headers for request to remote service.
169
      def build_request_headers(headers, http_method=nil)
E
Edgar J. Suarez 已提交
170
        authorization_header.update(default_header).update(http_format_header(http_method)).update(headers)
171
      end
172

173
      # Sets authorization header
174
      def authorization_header
175
        (@user || @password ? { 'Authorization' => 'Basic ' + ["#{@user}:#{ @password}"].pack('m').delete("\r\n") } : {})
176
      end
177

178 179 180 181
      def http_format_header(http_method)
        {HTTP_FORMAT_HEADER_NAMES[http_method] => format.mime_type}
      end

182
      def logger #:nodoc:
183
        Base.logger
184
      end
185
  end
186
end