httparty-responsibly 0.17.1

Sign up to get free protection for your applications and to get access to all the features.
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