/lib/httparty/request.rb
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