~ubuntu-branches/ubuntu/utopic/ruby-excon/utopic

« back to all changes in this revision

Viewing changes to lib/excon/connection.rb

  • Committer: Package Import Robot
  • Author(s): Laurent Bigonville
  • Date: 2012-03-07 16:36:21 UTC
  • Revision ID: package-import@ubuntu.com-20120307163621-bztj2b8860dxpzs7
Tags: upstream-0.10.0
ImportĀ upstreamĀ versionĀ 0.10.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
module Excon
 
2
  class Connection
 
3
    attr_reader :connection, :proxy
 
4
 
 
5
    # Initializes a new Connection instance
 
6
    #   @param [String] url The destination URL
 
7
    #   @param [Hash<Symbol, >] params One or more optional params
 
8
    #     @option params [String] :body Default text to be sent over a socket. Only used if :body absent in Connection#request params
 
9
    #     @option params [Hash<Symbol, String>] :headers The default headers to supply in a request. Only used if params[:headers] is not supplied to Connection#request
 
10
    #     @option params [String] :host The destination host's reachable DNS name or IP, in the form of a String
 
11
    #     @option params [String] :path Default path; appears after 'scheme://host:port/'. Only used if params[:path] is not supplied to Connection#request
 
12
    #     @option params [Fixnum] :port The port on which to connect, to the destination host
 
13
    #     @option params [Hash]   :query Default query; appended to the 'scheme://host:port/path/' in the form of '?key=value'. Will only be used if params[:query] is not supplied to Connection#request
 
14
    #     @option params [String] :scheme The protocol; 'https' causes OpenSSL to be used
 
15
    #     @option params [String] :proxy Proxy server; e.g. 'http://myproxy.com:8888'
 
16
    #     @option params [Fixnum] :retry_limit Set how many times we'll retry a failed request.  (Default 4)
 
17
    #     @option params [Class] :instrumentor Responds to #instrument as in ActiveSupport::Notifications
 
18
    #     @option params [String] :instrumentor_name Name prefix for #instrument events.  Defaults to 'excon'
 
19
    def initialize(url, params = {})
 
20
      uri = URI.parse(url)
 
21
      @connection = Excon.defaults.merge({
 
22
        :host              => uri.host,
 
23
        :path              => uri.path,
 
24
        :port              => uri.port.to_s,
 
25
        :query             => uri.query,
 
26
        :scheme            => uri.scheme,
 
27
      }).merge!(params)
 
28
      # merge does not deep-dup, so make sure headers is not the original
 
29
      @connection[:headers] = @connection[:headers].dup
 
30
 
 
31
      @proxy = nil
 
32
 
 
33
      # use proxy from the environment if present
 
34
      if ENV.has_key?('http_proxy')
 
35
        @proxy = setup_proxy(ENV['http_proxy'])
 
36
      elsif params.has_key?(:proxy)
 
37
        @proxy = setup_proxy(params[:proxy])
 
38
      end
 
39
 
 
40
      if @connection[:scheme] == HTTPS
 
41
        # use https_proxy if that has been specified
 
42
        if ENV.has_key?('https_proxy')
 
43
          @proxy = setup_proxy(ENV['https_proxy'])
 
44
        end
 
45
      end
 
46
 
 
47
      if @proxy
 
48
        @connection[:headers]['Proxy-Connection'] ||= 'Keep-Alive'
 
49
      end
 
50
 
 
51
      # Use Basic Auth if url contains a login
 
52
      if uri.user || uri.password
 
53
        auth = ["#{uri.user}:#{uri.password}"].pack('m').delete("\r\n")
 
54
        @connection[:headers]['Authorization'] ||= "Basic #{auth}"
 
55
      end
 
56
 
 
57
      @socket_key = '' << @connection[:host] << ':' << @connection[:port]
 
58
      reset
 
59
    end
 
60
 
 
61
    # Sends the supplied request to the destination host.
 
62
    #   @yield [chunk] @see Response#self.parse
 
63
    #   @param [Hash<Symbol, >] params One or more optional params, override defaults set in Connection.new
 
64
    #     @option params [String] :body text to be sent over a socket
 
65
    #     @option params [Hash<Symbol, String>] :headers The default headers to supply in a request
 
66
    #     @option params [String] :host The destination host's reachable DNS name or IP, in the form of a String
 
67
    #     @option params [String] :path appears after 'scheme://host:port/'
 
68
    #     @option params [Fixnum] :port The port on which to connect, to the destination host
 
69
    #     @option params [Hash]   :query appended to the 'scheme://host:port/path/' in the form of '?key=value'
 
70
    #     @option params [String] :scheme The protocol; 'https' causes OpenSSL to be used
 
71
    def request(params, &block)
 
72
      # connection has defaults, merge in new params to override
 
73
      params = @connection.merge(params)
 
74
      params[:headers] = @connection[:headers].merge(params[:headers] || {})
 
75
      params[:headers]['Host'] ||= '' << params[:host] << ':' << params[:port]
 
76
 
 
77
      # if path is empty or doesn't start with '/', insert one
 
78
      unless params[:path][0, 1] == '/'
 
79
        params[:path].insert(0, '/')
 
80
      end
 
81
 
 
82
      if params.has_key?(:instrumentor)
 
83
        if (retries_remaining ||= params[:retry_limit]) < params[:retry_limit]
 
84
          event_name = "#{params[:instrumentor_name]}.retry"
 
85
        else
 
86
          event_name = "#{params[:instrumentor_name]}.request"
 
87
        end
 
88
        params[:instrumentor].instrument(event_name, params) do
 
89
          request_kernel(params, &block)
 
90
        end
 
91
      else
 
92
        request_kernel(params, &block)
 
93
      end
 
94
    rescue => request_error
 
95
      if params[:idempotent] && [Excon::Errors::SocketError,
 
96
          Excon::Errors::HTTPStatusError].any? {|ex| request_error.kind_of? ex }
 
97
        retries_remaining ||= params[:retry_limit]
 
98
        retries_remaining -= 1
 
99
        if retries_remaining > 0
 
100
          if params[:body].respond_to?(:pos=)
 
101
            params[:body].pos = 0
 
102
          end
 
103
          retry
 
104
        else
 
105
          if params.has_key?(:instrumentor)
 
106
            params[:instrumentor].instrument("#{params[:instrumentor_name]}.error", :error => request_error)
 
107
          end
 
108
          raise(request_error)
 
109
        end
 
110
      else
 
111
        if params.has_key?(:instrumentor)
 
112
          params[:instrumentor].instrument("#{params[:instrumentor_name]}.error", :error => request_error)
 
113
        end
 
114
        raise(request_error)
 
115
      end
 
116
    end
 
117
 
 
118
    def reset
 
119
      (old_socket = sockets.delete(@socket_key)) && old_socket.close
 
120
    end
 
121
 
 
122
    # Generate HTTP request verb methods
 
123
    Excon::HTTP_VERBS.each do |method|
 
124
      eval <<-DEF
 
125
        def #{method}(params={}, &block)
 
126
          request(params.merge!(:method => :#{method}), &block)
 
127
        end
 
128
      DEF
 
129
    end
 
130
 
 
131
    def retry_limit=(new_retry_limit)
 
132
      puts("Excon::Connection#retry_limit= is deprecated, pass :retry_limit to the initializer (#{caller.first})")
 
133
      @connection[:retry_limit] = new_retry_limit
 
134
    end
 
135
 
 
136
    def retry_limit
 
137
      puts("Excon::Connection#retry_limit is deprecated, pass :retry_limit to the initializer (#{caller.first})")
 
138
      @connection[:retry_limit] ||= DEFAULT_RETRY_LIMIT
 
139
    end
 
140
 
 
141
  private
 
142
 
 
143
    def request_kernel(params, &block)
 
144
      begin
 
145
        response = if params[:mock]
 
146
          invoke_stub(params, &block)
 
147
        else
 
148
          socket.params = params
 
149
          # start with "METHOD /path"
 
150
          request = params[:method].to_s.upcase << ' '
 
151
          if @proxy
 
152
            request << params[:scheme] << '://' << params[:host] << ':' << params[:port]
 
153
          end
 
154
          request << params[:path]
 
155
 
 
156
          # add query to path, if there is one
 
157
          case params[:query]
 
158
          when String
 
159
            request << '?' << params[:query]
 
160
          when Hash
 
161
            request << '?'
 
162
            for key, values in params[:query]
 
163
              if values.nil?
 
164
                request << key.to_s << '&'
 
165
              else
 
166
                for value in [*values]
 
167
                  request << key.to_s << '=' << CGI.escape(value.to_s) << '&'
 
168
                end
 
169
              end
 
170
            end
 
171
            request.chop! # remove trailing '&'
 
172
          end
 
173
 
 
174
          # finish first line with "HTTP/1.1\r\n"
 
175
          request << HTTP_1_1
 
176
 
 
177
          # calculate content length and set to handle non-ascii
 
178
          unless params[:headers].has_key?('Content-Length')
 
179
            # The HTTP spec isn't clear on it, but specifically, GET requests don't usually send bodies;
 
180
            # if they don't, sending Content-Length:0 can cause issues.
 
181
            unless (params[:method].to_s.casecmp('GET') == 0 && params[:body].nil?)
 
182
              params[:headers]['Content-Length'] = case params[:body]
 
183
              when File
 
184
                params[:body].binmode
 
185
                File.size(params[:body])
 
186
              when String
 
187
                if FORCE_ENC
 
188
                  params[:body].force_encoding('BINARY')
 
189
                end
 
190
                params[:body].length
 
191
              else
 
192
                0
 
193
              end
 
194
            end
 
195
          end
 
196
 
 
197
          # add headers to request
 
198
          for key, values in params[:headers]
 
199
            for value in [*values]
 
200
              request << key.to_s << ': ' << value.to_s << CR_NL
 
201
            end
 
202
          end
 
203
 
 
204
          # add additional "\r\n" to indicate end of headers
 
205
          request << CR_NL
 
206
 
 
207
          # write out the request, sans body
 
208
          socket.write(request)
 
209
 
 
210
          # write out the body
 
211
          unless params[:body].nil? || params[:body].empty?
 
212
            if params[:body].is_a?(String)
 
213
              socket.write(params[:body])
 
214
            else
 
215
              while chunk = params[:body].read(CHUNK_SIZE)
 
216
                socket.write(chunk)
 
217
              end
 
218
            end
 
219
          end
 
220
 
 
221
          # read the response
 
222
          response = Excon::Response.parse(socket, params, &block)
 
223
 
 
224
          if response.headers['Connection'] == 'close'
 
225
            reset
 
226
          end
 
227
 
 
228
          response
 
229
        end
 
230
      rescue Excon::Errors::StubNotFound => stub_not_found
 
231
        raise(stub_not_found)
 
232
      rescue => socket_error
 
233
        reset
 
234
        raise(Excon::Errors::SocketError.new(socket_error))
 
235
      end
 
236
 
 
237
      if params.has_key?(:expects) && ![*params[:expects]].include?(response.status)
 
238
        reset
 
239
        raise(Excon::Errors.status_error(params, response))
 
240
      else
 
241
        response
 
242
      end
 
243
    end
 
244
 
 
245
    def invoke_stub(params)
 
246
      block_given = block_given?
 
247
      params[:captures] = {:headers => {}} # setup data to hold captures
 
248
      for stub, response in Excon.stubs
 
249
        headers_match = !stub.has_key?(:headers) || stub[:headers].keys.all? do |key|
 
250
          case value = stub[:headers][key]
 
251
          when Regexp
 
252
            if match = value.match(params[:headers][key])
 
253
              params[:captures][:headers][key] = match.captures
 
254
            end
 
255
            match
 
256
          else
 
257
            value == params[:headers][key]
 
258
          end
 
259
        end
 
260
        non_headers_match = (stub.keys - [:headers]).all? do |key|
 
261
          case value = stub[key]
 
262
          when Regexp
 
263
            if match = value.match(params[key])
 
264
              params[:captures][key] = match.captures
 
265
            end
 
266
            match
 
267
          else
 
268
            value == params[key]
 
269
          end
 
270
        end
 
271
        if headers_match && non_headers_match
 
272
          response_attributes = case response
 
273
          when Proc
 
274
            response.call(params)
 
275
          else
 
276
            response
 
277
          end
 
278
 
 
279
          # don't pass stuff into a block if there was an error
 
280
          if params[:expects] && ![*params[:expects]].include?(response_attributes[:status])
 
281
            block_given = false
 
282
          end
 
283
 
 
284
          if block_given && response_attributes.has_key?(:body)
 
285
            body = response_attributes.delete(:body)
 
286
            content_length = remaining = body.bytesize
 
287
            i = 0
 
288
            while i < body.length
 
289
              yield(body[i, CHUNK_SIZE], [remaining - CHUNK_SIZE, 0].max, content_length)
 
290
              remaining -= CHUNK_SIZE
 
291
              i += CHUNK_SIZE
 
292
            end
 
293
          end
 
294
          return Excon::Response.new(response_attributes)
 
295
        end
 
296
      end
 
297
      # if we reach here no stubs matched
 
298
      raise(Excon::Errors::StubNotFound.new('no stubs matched ' << params.inspect))
 
299
    end
 
300
 
 
301
    def socket
 
302
      sockets[@socket_key] ||= if @connection[:scheme] == HTTPS
 
303
        Excon::SSLSocket.new(@connection, @proxy)
 
304
      else
 
305
        Excon::Socket.new(@connection, @proxy)
 
306
      end
 
307
    end
 
308
 
 
309
    def sockets
 
310
      Thread.current[:_excon_sockets] ||= {}
 
311
    end
 
312
 
 
313
    def setup_proxy(proxy)
 
314
      uri = URI.parse(proxy)
 
315
      unless uri.host and uri.port and uri.scheme
 
316
        raise Excon::Errors::ProxyParseError, "Proxy is invalid"
 
317
      end
 
318
      {:host => uri.host, :port => uri.port, :scheme => uri.scheme}
 
319
    end
 
320
 
 
321
  end
 
322
end