httparty 0.14.0 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of httparty might be problematic. Click here for more details.

Files changed (101) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +18 -0
  3. data/.github/workflows/ci.yml +23 -0
  4. data/.gitignore +2 -0
  5. data/.rubocop_todo.yml +1 -1
  6. data/{History → Changelog.md} +216 -63
  7. data/Gemfile +6 -1
  8. data/README.md +8 -8
  9. data/bin/httparty +3 -1
  10. data/docs/README.md +108 -37
  11. data/examples/README.md +34 -12
  12. data/examples/aaws.rb +7 -3
  13. data/examples/body_stream.rb +14 -0
  14. data/examples/crack.rb +1 -1
  15. data/examples/custom_parsers.rb +5 -1
  16. data/examples/delicious.rb +4 -4
  17. data/examples/headers_and_user_agents.rb +7 -3
  18. data/examples/idn.rb +10 -0
  19. data/examples/logging.rb +4 -4
  20. data/examples/microsoft_graph.rb +52 -0
  21. data/examples/multipart.rb +22 -0
  22. data/examples/peer_cert.rb +9 -0
  23. data/examples/stackexchange.rb +1 -1
  24. data/examples/stream_download.rb +26 -0
  25. data/examples/tripit_sign_in.rb +1 -1
  26. data/examples/twitter.rb +2 -2
  27. data/examples/whoismyrep.rb +1 -1
  28. data/httparty.gemspec +7 -4
  29. data/lib/httparty/connection_adapter.rb +73 -16
  30. data/lib/httparty/cookie_hash.rb +10 -8
  31. data/lib/httparty/decompressor.rb +92 -0
  32. data/lib/httparty/exceptions.rb +4 -1
  33. data/lib/httparty/hash_conversions.rb +30 -12
  34. data/lib/httparty/headers_processor.rb +32 -0
  35. data/lib/httparty/logger/apache_formatter.rb +31 -6
  36. data/lib/httparty/logger/curl_formatter.rb +9 -7
  37. data/lib/httparty/logger/logger.rb +5 -1
  38. data/lib/httparty/logger/logstash_formatter.rb +61 -0
  39. data/lib/httparty/module_inheritable_attributes.rb +6 -4
  40. data/lib/httparty/net_digest_auth.rb +19 -19
  41. data/lib/httparty/parser.rb +25 -14
  42. data/lib/httparty/request/body.rb +98 -0
  43. data/lib/httparty/request/multipart_boundary.rb +13 -0
  44. data/lib/httparty/request.rb +137 -110
  45. data/lib/httparty/response/headers.rb +23 -19
  46. data/lib/httparty/response.rb +81 -22
  47. data/lib/httparty/response_fragment.rb +21 -0
  48. data/lib/httparty/text_encoder.rb +72 -0
  49. data/lib/httparty/utils.rb +13 -0
  50. data/lib/httparty/version.rb +3 -1
  51. data/lib/httparty.rb +79 -30
  52. data/website/css/common.css +1 -1
  53. metadata +37 -103
  54. data/.travis.yml +0 -9
  55. data/features/basic_authentication.feature +0 -20
  56. data/features/command_line.feature +0 -95
  57. data/features/deals_with_http_error_codes.feature +0 -26
  58. data/features/digest_authentication.feature +0 -30
  59. data/features/handles_compressed_responses.feature +0 -27
  60. data/features/handles_multiple_formats.feature +0 -57
  61. data/features/steps/env.rb +0 -27
  62. data/features/steps/httparty_response_steps.rb +0 -56
  63. data/features/steps/httparty_steps.rb +0 -43
  64. data/features/steps/mongrel_helper.rb +0 -127
  65. data/features/steps/remote_service_steps.rb +0 -92
  66. data/features/supports_read_timeout_option.feature +0 -13
  67. data/features/supports_redirection.feature +0 -22
  68. data/features/supports_timeout_option.feature +0 -13
  69. data/spec/fixtures/delicious.xml +0 -23
  70. data/spec/fixtures/empty.xml +0 -0
  71. data/spec/fixtures/google.html +0 -3
  72. data/spec/fixtures/ssl/generate.sh +0 -29
  73. data/spec/fixtures/ssl/generated/1fe462c2.0 +0 -16
  74. data/spec/fixtures/ssl/generated/bogushost.crt +0 -13
  75. data/spec/fixtures/ssl/generated/ca.crt +0 -16
  76. data/spec/fixtures/ssl/generated/ca.key +0 -15
  77. data/spec/fixtures/ssl/generated/selfsigned.crt +0 -14
  78. data/spec/fixtures/ssl/generated/server.crt +0 -13
  79. data/spec/fixtures/ssl/generated/server.key +0 -15
  80. data/spec/fixtures/ssl/openssl-exts.cnf +0 -9
  81. data/spec/fixtures/twitter.csv +0 -2
  82. data/spec/fixtures/twitter.json +0 -1
  83. data/spec/fixtures/twitter.xml +0 -403
  84. data/spec/fixtures/undefined_method_add_node_for_nil.xml +0 -2
  85. data/spec/httparty/connection_adapter_spec.rb +0 -495
  86. data/spec/httparty/cookie_hash_spec.rb +0 -100
  87. data/spec/httparty/exception_spec.rb +0 -45
  88. data/spec/httparty/hash_conversions_spec.rb +0 -49
  89. data/spec/httparty/logger/apache_formatter_spec.rb +0 -41
  90. data/spec/httparty/logger/curl_formatter_spec.rb +0 -119
  91. data/spec/httparty/logger/logger_spec.rb +0 -38
  92. data/spec/httparty/net_digest_auth_spec.rb +0 -240
  93. data/spec/httparty/parser_spec.rb +0 -173
  94. data/spec/httparty/request_spec.rb +0 -1183
  95. data/spec/httparty/response_spec.rb +0 -291
  96. data/spec/httparty/ssl_spec.rb +0 -74
  97. data/spec/httparty_spec.rb +0 -872
  98. data/spec/spec_helper.rb +0 -59
  99. data/spec/support/ssl_test_helper.rb +0 -47
  100. data/spec/support/ssl_test_server.rb +0 -80
  101. data/spec/support/stub_response.rb +0 -49
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'digest/md5'
2
4
  require 'net/http'
3
5
 
@@ -12,13 +14,13 @@ module Net
12
14
  response
13
15
  )
14
16
 
15
- @header['Authorization'] = authenticator.authorization_header
16
- @header['cookie'] = append_cookies(authenticator) if response['Set-Cookie']
17
- end
17
+ authenticator.authorization_header.each do |v|
18
+ add_field('Authorization', v)
19
+ end
18
20
 
19
- def append_cookies(authenticator)
20
- cookies = @header['cookie'] ? @header['cookie'] : []
21
- cookies.concat(authenticator.cookie_header)
21
+ authenticator.cookie_header.each do |v|
22
+ add_field('Cookie', v)
23
+ end
22
24
  end
23
25
 
24
26
  class DigestAuthenticator
@@ -44,12 +46,9 @@ module Net
44
46
  header << %(algorithm="#{@response['algorithm']}") if algorithm_present?
45
47
 
46
48
  if qop_present?
47
- fields = [
48
- %(cnonce="#{@cnonce}"),
49
- %(qop="#{@response['qop']}"),
50
- "nc=00000001"
51
- ]
52
- fields.each { |field| header << field }
49
+ header << %(cnonce="#{@cnonce}")
50
+ header << %(qop="#{@response['qop']}")
51
+ header << 'nc=00000001'
53
52
  end
54
53
 
55
54
  header << %(opaque="#{@response['opaque']}") if opaque_present?
@@ -64,7 +63,8 @@ module Net
64
63
 
65
64
  def parse(response_header)
66
65
  header = response_header['www-authenticate']
67
- .gsub(/qop=(auth(?:-int)?)/, 'qop="\\1"')
66
+
67
+ header = header.gsub(/qop=(auth(?:-int)?)/, 'qop="\\1"')
68
68
 
69
69
  header =~ /Digest (.*)/
70
70
  params = {}
@@ -97,13 +97,13 @@ module Net
97
97
  end
98
98
 
99
99
  def random
100
- format "%x", (Time.now.to_i + rand(65535))
100
+ format '%x', (Time.now.to_i + rand(65535))
101
101
  end
102
102
 
103
103
  def request_digest
104
104
  a = [md5(a1), @response['nonce'], md5(a2)]
105
- a.insert(2, "00000001", @cnonce, @response['qop']) if qop_present?
106
- md5(a.join(":"))
105
+ a.insert(2, '00000001', @cnonce, @response['qop']) if qop_present?
106
+ md5(a.join(':'))
107
107
  end
108
108
 
109
109
  def md5(str)
@@ -113,11 +113,11 @@ module Net
113
113
  def algorithm_present?
114
114
  @response.key?('algorithm') && !@response['algorithm'].empty?
115
115
  end
116
-
116
+
117
117
  def use_md5_sess?
118
118
  algorithm_present? && @response['algorithm'] == 'MD5-sess'
119
119
  end
120
-
120
+
121
121
  def a1
122
122
  a1_user_realm_pwd = [@username, @response['realm'], @password].join(':')
123
123
  if use_md5_sess?
@@ -128,7 +128,7 @@ module Net
128
128
  end
129
129
 
130
130
  def a2
131
- [@method, @path].join(":")
131
+ [@method, @path].join(':')
132
132
  end
133
133
  end
134
134
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HTTParty
2
4
  # The default parser used by HTTParty, supports xml, json, html, csv and
3
5
  # plain text.
@@ -38,16 +40,18 @@ module HTTParty
38
40
  # @abstract Read the Custom Parsers section for more information.
39
41
  class Parser
40
42
  SupportedFormats = {
41
- 'text/xml' => :xml,
42
- 'application/xml' => :xml,
43
- 'application/json' => :json,
44
- 'text/json' => :json,
45
- 'application/javascript' => :plain,
46
- 'text/javascript' => :plain,
47
- 'text/html' => :html,
48
- 'text/plain' => :plain,
49
- 'text/csv' => :csv,
50
- 'application/csv' => :csv,
43
+ 'text/xml' => :xml,
44
+ 'application/xml' => :xml,
45
+ 'application/json' => :json,
46
+ 'application/vnd.api+json' => :json,
47
+ 'application/hal+json' => :json,
48
+ 'text/json' => :json,
49
+ 'application/javascript' => :plain,
50
+ 'text/javascript' => :plain,
51
+ 'text/html' => :html,
52
+ 'text/plain' => :plain,
53
+ 'text/csv' => :csv,
54
+ 'application/csv' => :csv,
51
55
  'text/comma-separated-values' => :csv
52
56
  }
53
57
 
@@ -99,8 +103,11 @@ module HTTParty
99
103
  # @return [nil] when the response body is nil, an empty string, spaces only or "null"
100
104
  def parse
101
105
  return nil if body.nil?
102
- return nil if body == "null"
106
+ return nil if body == 'null'
103
107
  return nil if body.valid_encoding? && body.strip.empty?
108
+ if body.valid_encoding? && body.encoding == Encoding::UTF_8
109
+ @body = body.gsub(/\A#{UTF8_BOM}/, '')
110
+ end
104
111
  if supports_format?
105
112
  parse_supported_format
106
113
  else
@@ -114,6 +121,8 @@ module HTTParty
114
121
  MultiXml.parse(body)
115
122
  end
116
123
 
124
+ UTF8_BOM = "\xEF\xBB\xBF"
125
+
117
126
  def json
118
127
  JSON.parse(body, :quirks_mode => true, :allow_nan => true)
119
128
  end
@@ -135,9 +144,11 @@ module HTTParty
135
144
  end
136
145
 
137
146
  def parse_supported_format
138
- send(format)
139
- rescue NoMethodError => e
140
- raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format.", e.backtrace
147
+ if respond_to?(format, true)
148
+ send(format)
149
+ else
150
+ raise NotImplementedError, "#{self.class.name} has not implemented a parsing method for the #{format.inspect} format."
151
+ end
141
152
  end
142
153
  end
143
154
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'multipart_boundary'
4
+
5
+ module HTTParty
6
+ class Request
7
+ class Body
8
+ NEWLINE = "\r\n"
9
+ private_constant :NEWLINE
10
+
11
+ def initialize(params, query_string_normalizer: nil, force_multipart: false)
12
+ @params = params
13
+ @query_string_normalizer = query_string_normalizer
14
+ @force_multipart = force_multipart
15
+ end
16
+
17
+ def call
18
+ if params.respond_to?(:to_hash)
19
+ multipart? ? generate_multipart : normalize_query(params)
20
+ else
21
+ params
22
+ end
23
+ end
24
+
25
+ def boundary
26
+ @boundary ||= MultipartBoundary.generate
27
+ end
28
+
29
+ def multipart?
30
+ params.respond_to?(:to_hash) && (force_multipart || has_file?(params))
31
+ end
32
+
33
+ private
34
+
35
+ def generate_multipart
36
+ normalized_params = params.flat_map { |key, value| HashConversions.normalize_keys(key, value) }
37
+
38
+ multipart = normalized_params.inject(''.dup) do |memo, (key, value)|
39
+ memo << "--#{boundary}#{NEWLINE}"
40
+ memo << %(Content-Disposition: form-data; name="#{key}")
41
+ # value.path is used to support ActionDispatch::Http::UploadedFile
42
+ # https://github.com/jnunemaker/httparty/pull/585
43
+ memo << %(; filename="#{file_name(value)}") if file?(value)
44
+ memo << NEWLINE
45
+ memo << "Content-Type: #{content_type(value)}#{NEWLINE}" if file?(value)
46
+ memo << NEWLINE
47
+ memo << content_body(value)
48
+ memo << NEWLINE
49
+ end
50
+
51
+ multipart << "--#{boundary}--#{NEWLINE}"
52
+ end
53
+
54
+ def has_file?(value)
55
+ if value.respond_to?(:to_hash)
56
+ value.to_hash.any? { |_, v| has_file?(v) }
57
+ elsif value.respond_to?(:to_ary)
58
+ value.to_ary.any? { |v| has_file?(v) }
59
+ else
60
+ file?(value)
61
+ end
62
+ end
63
+
64
+ def file?(object)
65
+ object.respond_to?(:path) && object.respond_to?(:read)
66
+ end
67
+
68
+ def normalize_query(query)
69
+ if query_string_normalizer
70
+ query_string_normalizer.call(query)
71
+ else
72
+ HashConversions.to_params(query)
73
+ end
74
+ end
75
+
76
+ def content_body(object)
77
+ if file?(object)
78
+ object = (file = object).read
79
+ file.rewind if file.respond_to?(:rewind)
80
+ end
81
+
82
+ object.to_s
83
+ end
84
+
85
+ def content_type(object)
86
+ return object.content_type if object.respond_to?(:content_type)
87
+ mime = MIME::Types.type_for(object.path)
88
+ mime.empty? ? 'application/octet-stream' : mime[0].content_type
89
+ end
90
+
91
+ def file_name(object)
92
+ object.respond_to?(:original_filename) ? object.original_filename : File.basename(object.path)
93
+ end
94
+
95
+ attr_reader :params, :query_string_normalizer, :force_multipart
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module HTTParty
6
+ class Request
7
+ class MultipartBoundary
8
+ def self.generate
9
+ "------------------------#{SecureRandom.urlsafe_base64(12)}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
1
5
  module HTTParty
2
6
  class Request #:nodoc:
3
7
  SupportedHTTPMethods = [
@@ -11,6 +15,8 @@ module HTTParty
11
15
  Net::HTTP::Move,
12
16
  Net::HTTP::Copy,
13
17
  Net::HTTP::Mkcol,
18
+ Net::HTTP::Lock,
19
+ Net::HTTP::Unlock,
14
20
  ]
15
21
 
16
22
  SupportedURISchemes = ['http', 'https', 'webcal', nil]
@@ -27,10 +33,31 @@ module HTTParty
27
33
  end.flatten.join('&')
28
34
  end
29
35
 
36
+ JSON_API_QUERY_STRING_NORMALIZER = proc do |query|
37
+ Array(query).sort_by { |a| a[0].to_s }.map do |key, value|
38
+ if value.nil?
39
+ key.to_s
40
+ elsif value.respond_to?(:to_ary)
41
+ values = value.to_ary.map{|v| ERB::Util.url_encode(v.to_s)}
42
+ "#{key}=#{values.join(',')}"
43
+ else
44
+ HashConversions.to_params(key => value)
45
+ end
46
+ end.flatten.join('&')
47
+ end
48
+
49
+ def self._load(data)
50
+ http_method, path, options = Marshal.load(data)
51
+ new(http_method, path, options)
52
+ end
53
+
30
54
  attr_accessor :http_method, :options, :last_response, :redirect, :last_uri
31
55
  attr_reader :path
32
56
 
33
57
  def initialize(http_method, path, o = {})
58
+ @changed_hosts = false
59
+ @credentials_sent = false
60
+
34
61
  self.http_method = http_method
35
62
  self.options = {
36
63
  limit: o.delete(:no_follow) ? 1 : 5,
@@ -51,7 +78,7 @@ module HTTParty
51
78
  @path = if uri.is_a?(uri_adapter)
52
79
  uri
53
80
  elsif String.try_convert(uri)
54
- uri_adapter.parse uri
81
+ uri_adapter.parse(uri).normalize
55
82
  else
56
83
  raise ArgumentError,
57
84
  "bad argument (expected #{uri_adapter} object or URI string)"
@@ -67,14 +94,20 @@ module HTTParty
67
94
  end
68
95
 
69
96
  def uri
70
- if redirect && path.relative? && path.path[0] != "/"
71
- last_uri_host = @last_uri.path.gsub(/[^\/]+$/, "")
97
+ if redirect && path.relative? && path.path[0] != '/'
98
+ last_uri_host = @last_uri.path.gsub(/[^\/]+$/, '')
72
99
 
73
- path.path = "/#{path.path}" if last_uri_host[-1] != "/"
74
- path.path = last_uri_host + path.path
100
+ path.path = "/#{path.path}" if last_uri_host[-1] != '/'
101
+ path.path = "#{last_uri_host}#{path.path}"
75
102
  end
76
103
 
77
- new_uri = path.relative? ? options[:uri_adapter].parse("#{base_uri}#{path}") : path.clone
104
+ if path.relative? && path.host
105
+ new_uri = options[:uri_adapter].parse("#{@last_uri.scheme}:#{path}").normalize
106
+ elsif path.relative?
107
+ new_uri = options[:uri_adapter].parse("#{base_uri}#{path}").normalize
108
+ else
109
+ new_uri = path.clone
110
+ end
78
111
 
79
112
  # avoid double query string on redirects [#12]
80
113
  unless redirect
@@ -91,7 +124,7 @@ module HTTParty
91
124
  def base_uri
92
125
  if redirect
93
126
  base_uri = "#{@last_uri.scheme}://#{@last_uri.host}"
94
- base_uri += ":#{@last_uri.port}" if @last_uri.port != 80
127
+ base_uri = "#{base_uri}:#{@last_uri.port}" if @last_uri.port != 80
95
128
  base_uri
96
129
  else
97
130
  options[:base_uri] && HTTParty.normalize_base_uri(options[:base_uri])
@@ -114,39 +147,52 @@ module HTTParty
114
147
  validate
115
148
  setup_raw_request
116
149
  chunked_body = nil
150
+ current_http = http
117
151
 
118
- self.last_response = http.request(@raw_request) do |http_response|
152
+ self.last_response = current_http.request(@raw_request) do |http_response|
119
153
  if block
120
154
  chunks = []
121
155
 
122
156
  http_response.read_body do |fragment|
123
- chunks << fragment unless options[:stream_body]
124
- block.call(fragment)
157
+ encoded_fragment = encode_text(fragment, http_response['content-type'])
158
+ chunks << encoded_fragment if !options[:stream_body]
159
+ block.call ResponseFragment.new(encoded_fragment, http_response, current_http)
125
160
  end
126
161
 
127
162
  chunked_body = chunks.join
128
163
  end
129
164
  end
130
165
 
131
- handle_deflation unless http_method == Net::HTTP::Head
132
166
  handle_host_redirection if response_redirects?
133
- handle_response(chunked_body, &block)
167
+ result = handle_unauthorized
168
+ result ||= handle_response(chunked_body, &block)
169
+ result
170
+ end
171
+
172
+ def handle_unauthorized(&block)
173
+ return unless digest_auth? && response_unauthorized? && response_has_digest_auth_challenge?
174
+ return if @credentials_sent
175
+ @credentials_sent = true
176
+ perform(&block)
134
177
  end
135
178
 
136
179
  def raw_body
137
180
  @raw_request.body
138
181
  end
139
182
 
183
+ def _dump(_level)
184
+ opts = options.dup
185
+ opts.delete(:logger)
186
+ opts.delete(:parser) if opts[:parser] && opts[:parser].is_a?(Proc)
187
+ Marshal.dump([http_method, path, opts])
188
+ end
189
+
140
190
  private
141
191
 
142
192
  def http
143
193
  connection_adapter.call(uri, options)
144
194
  end
145
195
 
146
- def body
147
- options[:body].respond_to?(:to_hash) ? normalize_query(options[:body]) : options[:body]
148
- end
149
-
150
196
  def credentials
151
197
  (options[:basic_auth] || options[:digest_auth]).to_hash
152
198
  end
@@ -172,106 +218,78 @@ module HTTParty
172
218
  end
173
219
 
174
220
  def setup_raw_request
175
- @raw_request = http_method.new(request_uri(uri))
176
- @raw_request.body = body if body
177
- @raw_request.body_stream = options[:body_stream] if options[:body_stream]
178
- @raw_request.initialize_http_header(options[:headers].to_hash) if options[:headers].respond_to?(:to_hash)
179
- @raw_request.basic_auth(username, password) if options[:basic_auth] && send_authorization_header?
180
- setup_digest_auth if options[:digest_auth]
181
- end
182
-
183
- def setup_digest_auth
184
- auth_request = http_method.new(uri.request_uri)
185
- auth_request.initialize_http_header(options[:headers].to_hash) if options[:headers].respond_to?(:to_hash)
186
- res = http.request(auth_request)
187
-
188
- if !res['www-authenticate'].nil? && res['www-authenticate'].length > 0
189
- @raw_request.digest_auth(username, password, res)
190
- end
191
- end
192
-
193
- def query_string(uri)
194
- query_string_parts = []
195
- query_string_parts << uri.query unless uri.query.nil?
196
-
197
- if options[:query].respond_to?(:to_hash)
198
- query_string_parts << normalize_query(options[:default_params].merge(options[:query].to_hash))
221
+ if options[:headers].respond_to?(:to_hash)
222
+ headers_hash = options[:headers].to_hash
199
223
  else
200
- query_string_parts << normalize_query(options[:default_params]) unless options[:default_params].empty?
201
- query_string_parts << options[:query] unless options[:query].nil?
224
+ headers_hash = nil
202
225
  end
203
226
 
204
- query_string_parts.reject!(&:empty?) unless query_string_parts == [""]
205
- query_string_parts.size > 0 ? query_string_parts.join('&') : nil
206
- end
227
+ @raw_request = http_method.new(request_uri(uri), headers_hash)
228
+ @raw_request.body_stream = options[:body_stream] if options[:body_stream]
207
229
 
208
- def get_charset
209
- content_type = last_response["content-type"]
210
- if content_type.nil?
211
- return nil
212
- end
230
+ if options[:body]
231
+ body = Body.new(
232
+ options[:body],
233
+ query_string_normalizer: query_string_normalizer,
234
+ force_multipart: options[:multipart]
235
+ )
213
236
 
214
- if content_type =~ /;\s*charset\s*=\s*([^=,;"\s]+)/i
215
- return $1
237
+ if body.multipart?
238
+ content_type = "multipart/form-data; boundary=#{body.boundary}"
239
+ @raw_request['Content-Type'] = content_type
240
+ end
241
+ @raw_request.body = body.call
216
242
  end
217
243
 
218
- if content_type =~ /;\s*charset\s*=\s*"((\\.|[^\\"])+)"/i
219
- return $1.gsub(/\\(.)/, '\1')
220
- end
244
+ @raw_request.instance_variable_set(:@decode_content, decompress_content?)
221
245
 
222
- nil
246
+ if options[:basic_auth] && send_authorization_header?
247
+ @raw_request.basic_auth(username, password)
248
+ @credentials_sent = true
249
+ end
250
+ setup_digest_auth if digest_auth? && response_unauthorized? && response_has_digest_auth_challenge?
223
251
  end
224
252
 
225
- def encode_with_ruby_encoding(body, charset)
226
- encoding = Encoding.find(charset)
227
- body.force_encoding(encoding)
228
- rescue
229
- body
253
+ def digest_auth?
254
+ !!options[:digest_auth]
230
255
  end
231
256
 
232
- def assume_utf16_is_big_endian
233
- options[:assume_utf16_is_big_endian]
257
+ def decompress_content?
258
+ !options[:skip_decompression]
234
259
  end
235
260
 
236
- def encode_utf_16(body)
237
- if body.bytesize >= 2
238
- if body.getbyte(0) == 0xFF && body.getbyte(1) == 0xFE
239
- return body.force_encoding("UTF-16LE")
240
- elsif body.getbyte(0) == 0xFE && body.getbyte(1) == 0xFF
241
- return body.force_encoding("UTF-16BE")
242
- end
243
- end
261
+ def response_unauthorized?
262
+ !!last_response && last_response.code == '401'
263
+ end
244
264
 
245
- if assume_utf16_is_big_endian
246
- body.force_encoding("UTF-16BE")
247
- else
248
- body.force_encoding("UTF-16LE")
249
- end
265
+ def response_has_digest_auth_challenge?
266
+ !last_response['www-authenticate'].nil? && last_response['www-authenticate'].length > 0
250
267
  end
251
268
 
252
- def _encode_body(body)
253
- charset = get_charset
269
+ def setup_digest_auth
270
+ @raw_request.digest_auth(username, password, last_response)
271
+ end
254
272
 
255
- if charset.nil?
256
- return body
257
- end
273
+ def query_string(uri)
274
+ query_string_parts = []
275
+ query_string_parts << uri.query unless uri.query.nil?
258
276
 
259
- if "utf-16".casecmp(charset) == 0
260
- encode_utf_16(body)
277
+ if options[:query].respond_to?(:to_hash)
278
+ query_string_parts << normalize_query(options[:default_params].merge(options[:query].to_hash))
261
279
  else
262
- encode_with_ruby_encoding(body, charset)
280
+ query_string_parts << normalize_query(options[:default_params]) unless options[:default_params].empty?
281
+ query_string_parts << options[:query] unless options[:query].nil?
263
282
  end
283
+
284
+ query_string_parts.reject!(&:empty?) unless query_string_parts == ['']
285
+ query_string_parts.size > 0 ? query_string_parts.join('&') : nil
264
286
  end
265
287
 
266
- def encode_body(body)
267
- if "".respond_to?(:encoding)
268
- _encode_body(body)
269
- else
270
- body
271
- end
288
+ def assume_utf16_is_big_endian
289
+ options[:assume_utf16_is_big_endian]
272
290
  end
273
291
 
274
- def handle_response(body, &block)
292
+ def handle_response(raw_body, &block)
275
293
  if response_redirects?
276
294
  options[:limit] -= 1
277
295
  if options[:logger]
@@ -292,31 +310,26 @@ module HTTParty
292
310
  capture_cookies(last_response)
293
311
  perform(&block)
294
312
  else
295
- body ||= last_response.body
296
- body = encode_body(body)
297
- Response.new(self, last_response, lambda { parse_response(body) }, body: body)
298
- end
299
- end
313
+ raw_body ||= last_response.body
314
+
315
+ body = decompress(raw_body, last_response['content-encoding']) unless raw_body.nil?
316
+
317
+ unless body.nil?
318
+ body = encode_text(body, last_response['content-type'])
300
319
 
301
- # Inspired by Ruby 1.9
302
- def handle_deflation
303
- return if response_redirects?
304
- return if last_response.body.nil?
320
+ if decompress_content?
321
+ last_response.delete('content-encoding')
322
+ raw_body = body
323
+ end
324
+ end
305
325
 
306
- case last_response["content-encoding"]
307
- when "gzip", "x-gzip"
308
- body_io = StringIO.new(last_response.body)
309
- last_response.body.replace Zlib::GzipReader.new(body_io).read
310
- last_response.delete('content-encoding')
311
- when "deflate"
312
- last_response.body.replace Zlib::Inflate.inflate(last_response.body)
313
- last_response.delete('content-encoding')
326
+ Response.new(self, last_response, lambda { parse_response(body) }, body: raw_body)
314
327
  end
315
328
  end
316
329
 
317
330
  def handle_host_redirection
318
331
  check_duplicate_location_header
319
- redirect_path = options[:uri_adapter].parse last_response['location']
332
+ redirect_path = options[:uri_adapter].parse(last_response['location']).normalize
320
333
  return if redirect_path.relative? || path.host == redirect_path.host
321
334
  @changed_hosts = true
322
335
  end
@@ -329,7 +342,7 @@ module HTTParty
329
342
  end
330
343
 
331
344
  def send_authorization_header?
332
- !defined?(@changed_hosts)
345
+ !@changed_hosts
333
346
  end
334
347
 
335
348
  def response_redirects?
@@ -350,6 +363,7 @@ module HTTParty
350
363
  cookies_hash = HTTParty::CookieHash.new
351
364
  cookies_hash.add_cookies(options[:headers].to_hash['Cookie']) if options[:headers] && options[:headers].to_hash['Cookie']
352
365
  response.get_fields('Set-Cookie').each { |cookie| cookies_hash.add_cookies(cookie) }
366
+
353
367
  options[:headers] ||= {}
354
368
  options[:headers]['Cookie'] = cookies_hash.to_cookie_string
355
369
  end
@@ -381,7 +395,20 @@ module HTTParty
381
395
  if path.userinfo
382
396
  username, password = path.userinfo.split(':')
383
397
  options[:basic_auth] = {username: username, password: password}
398
+ @credentials_sent = true
384
399
  end
385
400
  end
401
+
402
+ def decompress(body, encoding)
403
+ Decompressor.new(body, encoding).decompress
404
+ end
405
+
406
+ def encode_text(text, content_type)
407
+ TextEncoder.new(
408
+ text,
409
+ content_type: content_type,
410
+ assume_utf16_is_big_endian: assume_utf16_is_big_endian
411
+ ).call
412
+ end
386
413
  end
387
414
  end