httparty-responsibly 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +18 -0
  3. data/.gitignore +13 -0
  4. data/.rubocop.yml +92 -0
  5. data/.rubocop_todo.yml +124 -0
  6. data/.simplecov +1 -0
  7. data/.travis.yml +11 -0
  8. data/CONTRIBUTING.md +23 -0
  9. data/Changelog.md +509 -0
  10. data/Gemfile +24 -0
  11. data/Guardfile +16 -0
  12. data/MIT-LICENSE +20 -0
  13. data/README.md +78 -0
  14. data/Rakefile +10 -0
  15. data/bin/httparty +123 -0
  16. data/cucumber.yml +1 -0
  17. data/docs/README.md +106 -0
  18. data/examples/README.md +86 -0
  19. data/examples/aaws.rb +32 -0
  20. data/examples/basic.rb +28 -0
  21. data/examples/body_stream.rb +14 -0
  22. data/examples/crack.rb +19 -0
  23. data/examples/custom_parsers.rb +68 -0
  24. data/examples/delicious.rb +37 -0
  25. data/examples/google.rb +16 -0
  26. data/examples/headers_and_user_agents.rb +10 -0
  27. data/examples/logging.rb +36 -0
  28. data/examples/microsoft_graph.rb +52 -0
  29. data/examples/multipart.rb +22 -0
  30. data/examples/nokogiri_html_parser.rb +19 -0
  31. data/examples/peer_cert.rb +9 -0
  32. data/examples/rescue_json.rb +17 -0
  33. data/examples/rubyurl.rb +14 -0
  34. data/examples/stackexchange.rb +24 -0
  35. data/examples/stream_download.rb +26 -0
  36. data/examples/tripit_sign_in.rb +44 -0
  37. data/examples/twitter.rb +31 -0
  38. data/examples/whoismyrep.rb +10 -0
  39. data/httparty-responsibly.gemspec +27 -0
  40. data/lib/httparty.rb +668 -0
  41. data/lib/httparty/connection_adapter.rb +254 -0
  42. data/lib/httparty/cookie_hash.rb +21 -0
  43. data/lib/httparty/exceptions.rb +33 -0
  44. data/lib/httparty/hash_conversions.rb +69 -0
  45. data/lib/httparty/headers_processor.rb +30 -0
  46. data/lib/httparty/logger/apache_formatter.rb +45 -0
  47. data/lib/httparty/logger/curl_formatter.rb +91 -0
  48. data/lib/httparty/logger/logger.rb +28 -0
  49. data/lib/httparty/logger/logstash_formatter.rb +59 -0
  50. data/lib/httparty/module_inheritable_attributes.rb +56 -0
  51. data/lib/httparty/net_digest_auth.rb +136 -0
  52. data/lib/httparty/parser.rb +150 -0
  53. data/lib/httparty/request.rb +386 -0
  54. data/lib/httparty/request/body.rb +84 -0
  55. data/lib/httparty/request/multipart_boundary.rb +11 -0
  56. data/lib/httparty/response.rb +140 -0
  57. data/lib/httparty/response/headers.rb +33 -0
  58. data/lib/httparty/response_fragment.rb +19 -0
  59. data/lib/httparty/text_encoder.rb +70 -0
  60. data/lib/httparty/utils.rb +11 -0
  61. data/lib/httparty/version.rb +3 -0
  62. data/script/release +42 -0
  63. data/website/css/common.css +47 -0
  64. data/website/index.html +73 -0
  65. metadata +138 -0
@@ -0,0 +1,386 @@
1
+ require 'erb'
2
+
3
+ module 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
+ @raw_request = http_method.new(request_uri(uri))
208
+ @raw_request.body_stream = options[:body_stream] if options[:body_stream]
209
+
210
+ if options[:headers].respond_to?(:to_hash)
211
+ headers_hash = options[:headers].to_hash
212
+
213
+ @raw_request.initialize_http_header(headers_hash)
214
+ # If the caller specified a header of 'Accept-Encoding', assume they want to
215
+ # deal with encoding of content. Disable the internal logic in Net:HTTP
216
+ # that handles encoding, if the platform supports it.
217
+ if @raw_request.respond_to?(:decode_content) && (headers_hash.key?('Accept-Encoding') || headers_hash.key?('accept-encoding'))
218
+ # Using the '[]=' sets decode_content to false
219
+ @raw_request['accept-encoding'] = @raw_request['accept-encoding']
220
+ end
221
+ end
222
+
223
+ if options[:body]
224
+ body = Body.new(
225
+ options[:body],
226
+ query_string_normalizer: query_string_normalizer,
227
+ force_multipart: options[:multipart]
228
+ )
229
+
230
+ if body.multipart?
231
+ content_type = "multipart/form-data; boundary=#{body.boundary}"
232
+ @raw_request['Content-Type'] = content_type
233
+ end
234
+ @raw_request.body = body.call
235
+ end
236
+
237
+ if options[:basic_auth] && send_authorization_header?
238
+ @raw_request.basic_auth(username, password)
239
+ @credentials_sent = true
240
+ end
241
+ setup_digest_auth if digest_auth? && response_unauthorized? && response_has_digest_auth_challenge?
242
+ end
243
+
244
+ def digest_auth?
245
+ !!options[:digest_auth]
246
+ end
247
+
248
+ def response_unauthorized?
249
+ !!last_response && last_response.code == '401'
250
+ end
251
+
252
+ def response_has_digest_auth_challenge?
253
+ !last_response['www-authenticate'].nil? && last_response['www-authenticate'].length > 0
254
+ end
255
+
256
+ def setup_digest_auth
257
+ @raw_request.digest_auth(username, password, last_response)
258
+ end
259
+
260
+ def query_string(uri)
261
+ query_string_parts = []
262
+ query_string_parts << uri.query unless uri.query.nil?
263
+
264
+ if options[:query].respond_to?(:to_hash)
265
+ query_string_parts << normalize_query(options[:default_params].merge(options[:query].to_hash))
266
+ else
267
+ query_string_parts << normalize_query(options[:default_params]) unless options[:default_params].empty?
268
+ query_string_parts << options[:query] unless options[:query].nil?
269
+ end
270
+
271
+ query_string_parts.reject!(&:empty?) unless query_string_parts == [""]
272
+ query_string_parts.size > 0 ? query_string_parts.join('&') : nil
273
+ end
274
+
275
+ def assume_utf16_is_big_endian
276
+ options[:assume_utf16_is_big_endian]
277
+ end
278
+
279
+ def handle_response(body, &block)
280
+ if response_redirects?
281
+ options[:limit] -= 1
282
+ if options[:logger]
283
+ logger = HTTParty::Logger.build(options[:logger], options[:log_level], options[:log_format])
284
+ logger.format(self, last_response)
285
+ end
286
+ self.path = last_response['location']
287
+ self.redirect = true
288
+ if last_response.class == Net::HTTPSeeOther
289
+ unless options[:maintain_method_across_redirects] && options[:resend_on_redirect]
290
+ self.http_method = Net::HTTP::Get
291
+ end
292
+ elsif last_response.code != '307' && last_response.code != '308'
293
+ unless options[:maintain_method_across_redirects]
294
+ self.http_method = Net::HTTP::Get
295
+ end
296
+ end
297
+ capture_cookies(last_response)
298
+ perform(&block)
299
+ else
300
+ body ||= last_response.body
301
+ body = body.nil? ? body : encode_text(body, last_response['content-type'])
302
+ Response.new(self, last_response, lambda { parse_response(body) }, body: body)
303
+ end
304
+ end
305
+
306
+ def handle_host_redirection
307
+ check_duplicate_location_header
308
+ redirect_path = options[:uri_adapter].parse(last_response['location']).normalize
309
+ return if redirect_path.relative? || path.host == redirect_path.host
310
+ @changed_hosts = true
311
+ end
312
+
313
+ def check_duplicate_location_header
314
+ location = last_response.get_fields('location')
315
+ if location.is_a?(Array) && location.count > 1
316
+ raise DuplicateLocationHeader.new(last_response)
317
+ end
318
+ end
319
+
320
+ def send_authorization_header?
321
+ !@changed_hosts
322
+ end
323
+
324
+ def response_redirects?
325
+ case last_response
326
+ when Net::HTTPNotModified # 304
327
+ false
328
+ when Net::HTTPRedirection
329
+ options[:follow_redirects] && last_response.key?('location')
330
+ end
331
+ end
332
+
333
+ def parse_response(body)
334
+ parser.call(body, format)
335
+ end
336
+
337
+ def capture_cookies(response)
338
+ return unless response['Set-Cookie']
339
+ cookies_hash = HTTParty::CookieHash.new
340
+ cookies_hash.add_cookies(options[:headers].to_hash['Cookie']) if options[:headers] && options[:headers].to_hash['Cookie']
341
+ response.get_fields('Set-Cookie').each { |cookie| cookies_hash.add_cookies(cookie) }
342
+
343
+ options[:headers] ||= {}
344
+ options[:headers]['Cookie'] = cookies_hash.to_cookie_string
345
+ end
346
+
347
+ # Uses the HTTP Content-Type header to determine the format of the
348
+ # response It compares the MIME type returned to the types stored in the
349
+ # SupportedFormats hash
350
+ def format_from_mimetype(mimetype)
351
+ if mimetype && parser.respond_to?(:format_from_mimetype)
352
+ parser.format_from_mimetype(mimetype)
353
+ end
354
+ end
355
+
356
+ def validate
357
+ raise HTTParty::RedirectionTooDeep.new(last_response), 'HTTP redirects too deep' if options[:limit].to_i <= 0
358
+ raise ArgumentError, 'only get, post, patch, put, delete, head, and options methods are supported' unless SupportedHTTPMethods.include?(http_method)
359
+ raise ArgumentError, ':headers must be a hash' if options[:headers] && !options[:headers].respond_to?(:to_hash)
360
+ raise ArgumentError, 'only one authentication method, :basic_auth or :digest_auth may be used at a time' if options[:basic_auth] && options[:digest_auth]
361
+ raise ArgumentError, ':basic_auth must be a hash' if options[:basic_auth] && !options[:basic_auth].respond_to?(:to_hash)
362
+ raise ArgumentError, ':digest_auth must be a hash' if options[:digest_auth] && !options[:digest_auth].respond_to?(:to_hash)
363
+ raise ArgumentError, ':query must be hash if using HTTP Post' if post? && !options[:query].nil? && !options[:query].respond_to?(:to_hash)
364
+ end
365
+
366
+ def post?
367
+ Net::HTTP::Post == http_method
368
+ end
369
+
370
+ def set_basic_auth_from_uri
371
+ if path.userinfo
372
+ username, password = path.userinfo.split(':')
373
+ options[:basic_auth] = {username: username, password: password}
374
+ @credentials_sent = true
375
+ end
376
+ end
377
+
378
+ def encode_text(text, content_type)
379
+ TextEncoder.new(
380
+ text,
381
+ content_type: content_type,
382
+ assume_utf16_is_big_endian: assume_utf16_is_big_endian
383
+ ).call
384
+ end
385
+ end
386
+ end
@@ -0,0 +1,84 @@
1
+ require_relative 'multipart_boundary'
2
+
3
+ module HTTParty
4
+ class Request
5
+ class Body
6
+ def initialize(params, query_string_normalizer: nil, force_multipart: false)
7
+ @params = params
8
+ @query_string_normalizer = query_string_normalizer
9
+ @force_multipart = force_multipart
10
+ end
11
+
12
+ def call
13
+ if params.respond_to?(:to_hash)
14
+ multipart? ? generate_multipart : normalize_query(params)
15
+ else
16
+ params
17
+ end
18
+ end
19
+
20
+ def boundary
21
+ @boundary ||= MultipartBoundary.generate
22
+ end
23
+
24
+ def multipart?
25
+ params.respond_to?(:to_hash) && (force_multipart || has_file?(params))
26
+ end
27
+
28
+ private
29
+
30
+ def generate_multipart
31
+ normalized_params = params.flat_map { |key, value| HashConversions.normalize_keys(key, value) }
32
+
33
+ multipart = normalized_params.inject('') do |memo, (key, value)|
34
+ memo += "--#{boundary}\r\n"
35
+ memo += %(Content-Disposition: form-data; name="#{key}")
36
+ # value.path is used to support ActionDispatch::Http::UploadedFile
37
+ # https://github.com/jnunemaker/httparty/pull/585
38
+ memo += %(; filename="#{file_name(value)}") if file?(value)
39
+ memo += "\r\n"
40
+ memo += "Content-Type: #{content_type(value)}\r\n" if file?(value)
41
+ memo += "\r\n"
42
+ memo += file?(value) ? value.read : value.to_s
43
+ memo += "\r\n"
44
+ end
45
+
46
+ multipart += "--#{boundary}--\r\n"
47
+ end
48
+
49
+ def has_file?(value)
50
+ if value.respond_to?(:to_hash)
51
+ value.to_hash.any? { |_, v| has_file?(v) }
52
+ elsif value.respond_to?(:to_ary)
53
+ value.to_ary.any? { |v| has_file?(v) }
54
+ else
55
+ file?(value)
56
+ end
57
+ end
58
+
59
+ def file?(object)
60
+ object.respond_to?(:path) && object.respond_to?(:read)
61
+ end
62
+
63
+ def normalize_query(query)
64
+ if query_string_normalizer
65
+ query_string_normalizer.call(query)
66
+ else
67
+ HashConversions.to_params(query)
68
+ end
69
+ end
70
+
71
+ def content_type(object)
72
+ return object.content_type if object.respond_to?(:content_type)
73
+ mime = MIME::Types.type_for(object.path)
74
+ mime.empty? ? 'application/octet-stream' : mime[0].content_type
75
+ end
76
+
77
+ def file_name(object)
78
+ object.respond_to?(:original_filename) ? object.original_filename : File.basename(object.path)
79
+ end
80
+
81
+ attr_reader :params, :query_string_normalizer, :force_multipart
82
+ end
83
+ end
84
+ end