PageRenderTime 115ms CodeModel.GetById 65ms app.highlight 46ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/httparty/request.rb

http://github.com/jnunemaker/httparty
Ruby | 379 lines | 315 code | 60 blank | 4 comment | 68 complexity | a4d4b21a2cf4c0d78aade3332bbc420f MD5 | raw file
  1require 'erb'
  2
  3module HTTParty
  4  class Request #:nodoc:
  5    SupportedHTTPMethods = [
  6      Net::HTTP::Get,
  7      Net::HTTP::Post,
  8      Net::HTTP::Patch,
  9      Net::HTTP::Put,
 10      Net::HTTP::Delete,
 11      Net::HTTP::Head,
 12      Net::HTTP::Options,
 13      Net::HTTP::Move,
 14      Net::HTTP::Copy,
 15      Net::HTTP::Mkcol,
 16      Net::HTTP::Lock,
 17      Net::HTTP::Unlock,
 18    ]
 19
 20    SupportedURISchemes  = ['http', 'https', 'webcal', nil]
 21
 22    NON_RAILS_QUERY_STRING_NORMALIZER = proc do |query|
 23      Array(query).sort_by { |a| a[0].to_s }.map do |key, value|
 24        if value.nil?
 25          key.to_s
 26        elsif value.respond_to?(:to_ary)
 27          value.to_ary.map {|v| "#{key}=#{ERB::Util.url_encode(v.to_s)}"}
 28        else
 29          HashConversions.to_params(key => value)
 30        end
 31      end.flatten.join('&')
 32    end
 33
 34    JSON_API_QUERY_STRING_NORMALIZER = proc do |query|
 35      Array(query).sort_by { |a| a[0].to_s }.map do |key, value|
 36        if value.nil?
 37          key.to_s
 38        elsif value.respond_to?(:to_ary)
 39          values = value.to_ary.map{|v| ERB::Util.url_encode(v.to_s)}
 40          "#{key}=#{values.join(',')}"
 41        else
 42          HashConversions.to_params(key => value)
 43        end
 44      end.flatten.join('&')
 45    end
 46
 47    attr_accessor :http_method, :options, :last_response, :redirect, :last_uri
 48    attr_reader :path
 49
 50    def initialize(http_method, path, o = {})
 51      @changed_hosts = false
 52      @credentials_sent = false
 53
 54      self.http_method = http_method
 55      self.options = {
 56        limit: o.delete(:no_follow) ? 1 : 5,
 57        assume_utf16_is_big_endian: true,
 58        default_params: {},
 59        follow_redirects: true,
 60        parser: Parser,
 61        uri_adapter: URI,
 62        connection_adapter: ConnectionAdapter
 63      }.merge(o)
 64      self.path = path
 65      set_basic_auth_from_uri
 66    end
 67
 68    def path=(uri)
 69      uri_adapter = options[:uri_adapter]
 70
 71      @path = if uri.is_a?(uri_adapter)
 72        uri
 73      elsif String.try_convert(uri)
 74        uri_adapter.parse(uri).normalize
 75      else
 76        raise ArgumentError,
 77          "bad argument (expected #{uri_adapter} object or URI string)"
 78      end
 79    end
 80
 81    def request_uri(uri)
 82      if uri.respond_to? :request_uri
 83        uri.request_uri
 84      else
 85        uri.path
 86      end
 87    end
 88
 89    def uri
 90      if redirect && path.relative? && path.path[0] != "/"
 91        last_uri_host = @last_uri.path.gsub(/[^\/]+$/, "")
 92
 93        path.path = "/#{path.path}" if last_uri_host[-1] != "/"
 94        path.path = last_uri_host + path.path
 95      end
 96
 97      if path.relative? && path.host
 98        new_uri = options[:uri_adapter].parse("#{@last_uri.scheme}:#{path}").normalize
 99      elsif path.relative?
100        new_uri = options[:uri_adapter].parse("#{base_uri}#{path}").normalize
101      else
102        new_uri = path.clone
103      end
104
105      # avoid double query string on redirects [#12]
106      unless redirect
107        new_uri.query = query_string(new_uri)
108      end
109
110      unless SupportedURISchemes.include? new_uri.scheme
111        raise UnsupportedURIScheme, "'#{new_uri}' Must be HTTP, HTTPS or Generic"
112      end
113
114      @last_uri = new_uri
115    end
116
117    def base_uri
118      if redirect
119        base_uri = "#{@last_uri.scheme}://#{@last_uri.host}"
120        base_uri += ":#{@last_uri.port}" if @last_uri.port != 80
121        base_uri
122      else
123        options[:base_uri] && HTTParty.normalize_base_uri(options[:base_uri])
124      end
125    end
126
127    def format
128      options[:format] || (format_from_mimetype(last_response['content-type']) if last_response)
129    end
130
131    def parser
132      options[:parser]
133    end
134
135    def connection_adapter
136      options[:connection_adapter]
137    end
138
139    def perform(&block)
140      validate
141      setup_raw_request
142      chunked_body = nil
143      current_http = http
144
145      self.last_response = current_http.request(@raw_request) do |http_response|
146        if block
147          chunks = []
148
149          http_response.read_body do |fragment|
150            encoded_fragment = encode_text(fragment, http_response['content-type'])
151            chunks << encoded_fragment if !options[:stream_body]
152            block.call ResponseFragment.new(encoded_fragment, http_response, current_http)
153          end
154
155          chunked_body = chunks.join
156        end
157      end
158
159      handle_host_redirection if response_redirects?
160      result = handle_unauthorized
161      result ||= handle_response(chunked_body, &block)
162      result
163    end
164
165    def handle_unauthorized(&block)
166      return unless digest_auth? && response_unauthorized? && response_has_digest_auth_challenge?
167      return if @credentials_sent
168      @credentials_sent = true
169      perform(&block)
170    end
171
172    def raw_body
173      @raw_request.body
174    end
175
176    private
177
178    def http
179      connection_adapter.call(uri, options)
180    end
181
182    def credentials
183      (options[:basic_auth] || options[:digest_auth]).to_hash
184    end
185
186    def username
187      credentials[:username]
188    end
189
190    def password
191      credentials[:password]
192    end
193
194    def normalize_query(query)
195      if query_string_normalizer
196        query_string_normalizer.call(query)
197      else
198        HashConversions.to_params(query)
199      end
200    end
201
202    def query_string_normalizer
203      options[:query_string_normalizer]
204    end
205
206    def setup_raw_request
207      if options[:headers].respond_to?(:to_hash)
208        headers_hash = options[:headers].to_hash
209      else
210        headers_hash = nil
211      end
212
213      @raw_request = http_method.new(request_uri(uri), headers_hash)
214      @raw_request.body_stream = options[:body_stream] if options[:body_stream]
215
216      if options[:body]
217        body = Body.new(
218          options[:body],
219          query_string_normalizer: query_string_normalizer,
220          force_multipart: options[:multipart]
221        )
222
223        if body.multipart?
224          content_type = "multipart/form-data; boundary=#{body.boundary}"
225          @raw_request['Content-Type'] = content_type
226        end
227        @raw_request.body = body.call
228      end
229
230      if options[:basic_auth] && send_authorization_header?
231        @raw_request.basic_auth(username, password)
232        @credentials_sent = true
233      end
234      setup_digest_auth if digest_auth? && response_unauthorized? && response_has_digest_auth_challenge?
235    end
236
237    def digest_auth?
238      !!options[:digest_auth]
239    end
240
241    def response_unauthorized?
242      !!last_response && last_response.code == '401'
243    end
244
245    def response_has_digest_auth_challenge?
246      !last_response['www-authenticate'].nil? && last_response['www-authenticate'].length > 0
247    end
248
249    def setup_digest_auth
250      @raw_request.digest_auth(username, password, last_response)
251    end
252
253    def query_string(uri)
254      query_string_parts = []
255      query_string_parts << uri.query unless uri.query.nil?
256
257      if options[:query].respond_to?(:to_hash)
258        query_string_parts << normalize_query(options[:default_params].merge(options[:query].to_hash))
259      else
260        query_string_parts << normalize_query(options[:default_params]) unless options[:default_params].empty?
261        query_string_parts << options[:query] unless options[:query].nil?
262      end
263
264      query_string_parts.reject!(&:empty?) unless query_string_parts == [""]
265      query_string_parts.size > 0 ? query_string_parts.join('&') : nil
266    end
267
268    def assume_utf16_is_big_endian
269      options[:assume_utf16_is_big_endian]
270    end
271
272    def handle_response(body, &block)
273      if response_redirects?
274        options[:limit] -= 1
275        if options[:logger]
276          logger = HTTParty::Logger.build(options[:logger], options[:log_level], options[:log_format])
277          logger.format(self, last_response)
278        end
279        self.path = last_response['location']
280        self.redirect = true
281        if last_response.class == Net::HTTPSeeOther
282          unless options[:maintain_method_across_redirects] && options[:resend_on_redirect]
283            self.http_method = Net::HTTP::Get
284          end
285        elsif last_response.code != '307' && last_response.code != '308'
286          unless options[:maintain_method_across_redirects]
287            self.http_method = Net::HTTP::Get
288          end
289        end
290        capture_cookies(last_response)
291        perform(&block)
292      else
293        body ||= last_response.body
294        body = body.nil? ? body : encode_text(body, last_response['content-type'])
295        Response.new(self, last_response, lambda { parse_response(body) }, body: body)
296      end
297    end
298
299    def handle_host_redirection
300      check_duplicate_location_header
301      redirect_path = options[:uri_adapter].parse(last_response['location']).normalize
302      return if redirect_path.relative? || path.host == redirect_path.host
303      @changed_hosts = true
304    end
305
306    def check_duplicate_location_header
307      location = last_response.get_fields('location')
308      if location.is_a?(Array) && location.count > 1
309        raise DuplicateLocationHeader.new(last_response)
310      end
311    end
312
313    def send_authorization_header?
314      !@changed_hosts
315    end
316
317    def response_redirects?
318      case last_response
319      when Net::HTTPNotModified # 304
320        false
321      when Net::HTTPRedirection
322        options[:follow_redirects] && last_response.key?('location')
323      end
324    end
325
326    def parse_response(body)
327      parser.call(body, format)
328    end
329
330    def capture_cookies(response)
331      return unless response['Set-Cookie']
332      cookies_hash = HTTParty::CookieHash.new
333      cookies_hash.add_cookies(options[:headers].to_hash['Cookie']) if options[:headers] && options[:headers].to_hash['Cookie']
334      response.get_fields('Set-Cookie').each { |cookie| cookies_hash.add_cookies(cookie) }
335
336      options[:headers] ||= {}
337      options[:headers]['Cookie'] = cookies_hash.to_cookie_string
338    end
339
340    # Uses the HTTP Content-Type header to determine the format of the
341    # response It compares the MIME type returned to the types stored in the
342    # SupportedFormats hash
343    def format_from_mimetype(mimetype)
344      if mimetype && parser.respond_to?(:format_from_mimetype)
345        parser.format_from_mimetype(mimetype)
346      end
347    end
348
349    def validate
350      raise HTTParty::RedirectionTooDeep.new(last_response), 'HTTP redirects too deep' if options[:limit].to_i <= 0
351      raise ArgumentError, 'only get, post, patch, put, delete, head, and options methods are supported' unless SupportedHTTPMethods.include?(http_method)
352      raise ArgumentError, ':headers must be a hash' if options[:headers] && !options[:headers].respond_to?(:to_hash)
353      raise ArgumentError, 'only one authentication method, :basic_auth or :digest_auth may be used at a time' if options[:basic_auth] && options[:digest_auth]
354      raise ArgumentError, ':basic_auth must be a hash' if options[:basic_auth] && !options[:basic_auth].respond_to?(:to_hash)
355      raise ArgumentError, ':digest_auth must be a hash' if options[:digest_auth] && !options[:digest_auth].respond_to?(:to_hash)
356      raise ArgumentError, ':query must be hash if using HTTP Post' if post? && !options[:query].nil? && !options[:query].respond_to?(:to_hash)
357    end
358
359    def post?
360      Net::HTTP::Post == http_method
361    end
362
363    def set_basic_auth_from_uri
364      if path.userinfo
365        username, password = path.userinfo.split(':')
366        options[:basic_auth] = {username: username, password: password}
367        @credentials_sent = true
368      end
369    end
370
371    def encode_text(text, content_type)
372      TextEncoder.new(
373        text,
374        content_type: content_type,
375        assume_utf16_is_big_endian: assume_utf16_is_big_endian
376      ).call
377    end
378  end
379end