httparty 0.21.0 → 0.24.3

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.
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTParty
4
+ class Request
5
+ class StreamingMultipartBody
6
+ NEWLINE = "\r\n"
7
+ CHUNK_SIZE = 64 * 1024 # 64 KB chunks
8
+
9
+ def initialize(parts, boundary)
10
+ @parts = parts
11
+ @boundary = boundary
12
+ @part_index = 0
13
+ @state = :header
14
+ @current_file = nil
15
+ @header_buffer = nil
16
+ @header_offset = 0
17
+ @footer_sent = false
18
+ end
19
+
20
+ def size
21
+ @size ||= calculate_size
22
+ end
23
+
24
+ def read(length = nil, outbuf = nil)
25
+ outbuf = outbuf ? outbuf.replace(''.b) : ''.b
26
+
27
+ return read_all(outbuf) if length.nil?
28
+
29
+ while outbuf.bytesize < length
30
+ chunk = read_chunk(length - outbuf.bytesize)
31
+ break if chunk.nil?
32
+ outbuf << chunk
33
+ end
34
+
35
+ outbuf.empty? ? nil : outbuf
36
+ end
37
+
38
+ def rewind
39
+ @part_index = 0
40
+ @state = :header
41
+ @current_file = nil
42
+ @header_buffer = nil
43
+ @header_offset = 0
44
+ @footer_sent = false
45
+ @parts.each do |_key, value, _is_file|
46
+ value.rewind if value.respond_to?(:rewind)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def read_all(outbuf)
53
+ while (chunk = read_chunk(CHUNK_SIZE))
54
+ outbuf << chunk
55
+ end
56
+ outbuf.empty? ? nil : outbuf
57
+ end
58
+
59
+ def read_chunk(max_length)
60
+ loop do
61
+ return nil if @part_index >= @parts.size && @footer_sent
62
+
63
+ if @part_index >= @parts.size
64
+ @footer_sent = true
65
+ return "--#{@boundary}--#{NEWLINE}".b
66
+ end
67
+
68
+ key, value, is_file = @parts[@part_index]
69
+
70
+ case @state
71
+ when :header
72
+ chunk = read_header_chunk(key, value, is_file, max_length)
73
+ return chunk if chunk
74
+
75
+ when :body
76
+ chunk = read_body_chunk(value, is_file, max_length)
77
+ return chunk if chunk
78
+
79
+ when :newline
80
+ @state = :header
81
+ @part_index += 1
82
+ return NEWLINE.b
83
+ end
84
+ end
85
+ end
86
+
87
+ def read_header_chunk(key, value, is_file, max_length)
88
+ if @header_buffer.nil?
89
+ @header_buffer = build_part_header(key, value, is_file)
90
+ @header_offset = 0
91
+ end
92
+
93
+ remaining = @header_buffer.bytesize - @header_offset
94
+ if remaining > 0
95
+ chunk_size = [remaining, max_length].min
96
+ chunk = @header_buffer.byteslice(@header_offset, chunk_size)
97
+ @header_offset += chunk_size
98
+ return chunk
99
+ end
100
+
101
+ @header_buffer = nil
102
+ @header_offset = 0
103
+ @state = :body
104
+ nil
105
+ end
106
+
107
+ def read_body_chunk(value, is_file, max_length)
108
+ if is_file
109
+ chunk = read_file_chunk(value, max_length)
110
+ if chunk
111
+ return chunk
112
+ else
113
+ @current_file = nil
114
+ @state = :newline
115
+ return nil
116
+ end
117
+ else
118
+ @state = :newline
119
+ return value.to_s.b
120
+ end
121
+ end
122
+
123
+ def read_file_chunk(file, max_length)
124
+ chunk_size = [max_length, CHUNK_SIZE].min
125
+ chunk = file.read(chunk_size)
126
+ return nil if chunk.nil?
127
+ chunk.force_encoding(Encoding::BINARY) if chunk.respond_to?(:force_encoding)
128
+ chunk
129
+ end
130
+
131
+ def build_part_header(key, value, is_file)
132
+ header = "--#{@boundary}#{NEWLINE}".b
133
+ header << %(Content-Disposition: form-data; name="#{key}").b
134
+ if is_file
135
+ header << %(; filename="#{file_name(value).gsub(/["\r\n]/, replacement_table)}").b
136
+ header << NEWLINE.b
137
+ header << "Content-Type: #{content_type(value)}#{NEWLINE}".b
138
+ else
139
+ header << NEWLINE.b
140
+ end
141
+ header << NEWLINE.b
142
+ header
143
+ end
144
+
145
+ def calculate_size
146
+ total = 0
147
+ @parts.each do |key, value, is_file|
148
+ total += build_part_header(key, value, is_file).bytesize
149
+ total += content_size(value, is_file)
150
+ total += NEWLINE.bytesize
151
+ end
152
+ total += "--#{@boundary}--#{NEWLINE}".bytesize
153
+ total
154
+ end
155
+
156
+ def content_size(value, is_file)
157
+ if is_file
158
+ if value.respond_to?(:size)
159
+ value.size
160
+ elsif value.respond_to?(:stat)
161
+ value.stat.size
162
+ else
163
+ value.read.bytesize.tap { value.rewind }
164
+ end
165
+ else
166
+ value.to_s.b.bytesize
167
+ end
168
+ end
169
+
170
+ def content_type(object)
171
+ return object.content_type if object.respond_to?(:content_type)
172
+ require 'mini_mime'
173
+ mime = MiniMime.lookup_by_filename(object.path)
174
+ mime ? mime.content_type : 'application/octet-stream'
175
+ end
176
+
177
+ def file_name(object)
178
+ object.respond_to?(:original_filename) ? object.original_filename : File.basename(object.path)
179
+ end
180
+
181
+ def replacement_table
182
+ @replacement_table ||= {
183
+ '"' => '%22',
184
+ "\r" => '%0D',
185
+ "\n" => '%0A'
186
+ }.freeze
187
+ end
188
+ end
189
+ end
190
+ end
@@ -113,6 +113,8 @@ module HTTParty
113
113
  new_uri = path.clone
114
114
  end
115
115
 
116
+ validate_uri_safety!(new_uri) unless redirect
117
+
116
118
  # avoid double query string on redirects [#12]
117
119
  unless redirect
118
120
  new_uri.query = query_string(new_uri)
@@ -153,24 +155,28 @@ module HTTParty
153
155
  chunked_body = nil
154
156
  current_http = http
155
157
 
156
- self.last_response = current_http.request(@raw_request) do |http_response|
157
- if block
158
- chunks = []
158
+ begin
159
+ self.last_response = current_http.request(@raw_request) do |http_response|
160
+ if block
161
+ chunks = []
159
162
 
160
- http_response.read_body do |fragment|
161
- encoded_fragment = encode_text(fragment, http_response['content-type'])
162
- chunks << encoded_fragment if !options[:stream_body]
163
- block.call ResponseFragment.new(encoded_fragment, http_response, current_http)
164
- end
163
+ http_response.read_body do |fragment|
164
+ encoded_fragment = encode_text(fragment, http_response['content-type'])
165
+ chunks << encoded_fragment if !options[:stream_body]
166
+ block.call ResponseFragment.new(encoded_fragment, http_response, current_http)
167
+ end
165
168
 
166
- chunked_body = chunks.join
169
+ chunked_body = chunks.join
170
+ end
167
171
  end
168
- end
169
172
 
170
- handle_host_redirection if response_redirects?
171
- result = handle_unauthorized
172
- result ||= handle_response(chunked_body, &block)
173
- result
173
+ handle_host_redirection if response_redirects?
174
+ result = handle_unauthorized
175
+ result ||= handle_response(chunked_body, &block)
176
+ result
177
+ rescue *COMMON_NETWORK_ERRORS => e
178
+ raise options[:foul] ? HTTParty::NetworkError.new("#{e.class}: #{e.message}") : e
179
+ end
174
180
  end
175
181
 
176
182
  def handle_unauthorized(&block)
@@ -241,8 +247,17 @@ module HTTParty
241
247
  if body.multipart?
242
248
  content_type = "multipart/form-data; boundary=#{body.boundary}"
243
249
  @raw_request['Content-Type'] = content_type
250
+ elsif options[:body].respond_to?(:to_hash) && !@raw_request['Content-Type']
251
+ @raw_request['Content-Type'] = 'application/x-www-form-urlencoded'
252
+ end
253
+
254
+ if body.streaming? && options[:stream_body] == true
255
+ stream = body.to_stream
256
+ @raw_request.body_stream = stream
257
+ @raw_request['Content-Length'] = stream.size.to_s
258
+ else
259
+ @raw_request.body = body.call
244
260
  end
245
- @raw_request.body = body.call
246
261
  end
247
262
 
248
263
  @raw_request.instance_variable_set(:@decode_content, decompress_content?)
@@ -295,24 +310,7 @@ module HTTParty
295
310
 
296
311
  def handle_response(raw_body, &block)
297
312
  if response_redirects?
298
- options[:limit] -= 1
299
- if options[:logger]
300
- logger = HTTParty::Logger.build(options[:logger], options[:log_level], options[:log_format])
301
- logger.format(self, last_response)
302
- end
303
- self.path = last_response['location']
304
- self.redirect = true
305
- if last_response.class == Net::HTTPSeeOther
306
- unless options[:maintain_method_across_redirects] && options[:resend_on_redirect]
307
- self.http_method = Net::HTTP::Get
308
- end
309
- elsif last_response.code != '307' && last_response.code != '308'
310
- unless options[:maintain_method_across_redirects]
311
- self.http_method = Net::HTTP::Get
312
- end
313
- end
314
- capture_cookies(last_response)
315
- perform(&block)
313
+ handle_redirection(&block)
316
314
  else
317
315
  raw_body ||= last_response.body
318
316
 
@@ -331,10 +329,34 @@ module HTTParty
331
329
  end
332
330
  end
333
331
 
332
+ def handle_redirection(&block)
333
+ options[:limit] -= 1
334
+ if options[:logger]
335
+ logger = HTTParty::Logger.build(options[:logger], options[:log_level], options[:log_format])
336
+ logger.format(self, last_response)
337
+ end
338
+ self.path = last_response['location']
339
+ self.redirect = true
340
+ if last_response.class == Net::HTTPSeeOther
341
+ unless options[:maintain_method_across_redirects] && options[:resend_on_redirect]
342
+ self.http_method = Net::HTTP::Get
343
+ end
344
+ elsif last_response.code != '307' && last_response.code != '308'
345
+ unless options[:maintain_method_across_redirects]
346
+ self.http_method = Net::HTTP::Get
347
+ end
348
+ end
349
+ if http_method == Net::HTTP::Get
350
+ clear_body
351
+ end
352
+ capture_cookies(last_response)
353
+ perform(&block)
354
+ end
355
+
334
356
  def handle_host_redirection
335
357
  check_duplicate_location_header
336
358
  redirect_path = options[:uri_adapter].parse(last_response['location']).normalize
337
- return if redirect_path.relative? || path.host == redirect_path.host
359
+ return if redirect_path.relative? || path.host == redirect_path.host || uri.host == redirect_path.host
338
360
  @changed_hosts = true
339
361
  end
340
362
 
@@ -362,6 +384,14 @@ module HTTParty
362
384
  parser.call(body, format)
363
385
  end
364
386
 
387
+ # Some Web Application Firewalls reject incoming GET requests that have a body
388
+ # if we redirect, and the resulting verb is GET then we will clear the body that
389
+ # may be left behind from the initiating request
390
+ def clear_body
391
+ options[:body] = nil
392
+ @raw_request.body = nil
393
+ end
394
+
365
395
  def capture_cookies(response)
366
396
  return unless response['Set-Cookie']
367
397
  cookies_hash = HTTParty::CookieHash.new
@@ -414,5 +444,23 @@ module HTTParty
414
444
  assume_utf16_is_big_endian: assume_utf16_is_big_endian
415
445
  ).call
416
446
  end
447
+
448
+ def validate_uri_safety!(new_uri)
449
+ return if options[:skip_uri_validation]
450
+
451
+ configured_base_uri = options[:base_uri]
452
+ return unless configured_base_uri
453
+
454
+ normalized_base = options[:uri_adapter].parse(
455
+ HTTParty.normalize_base_uri(configured_base_uri)
456
+ )
457
+
458
+ return if new_uri.host == normalized_base.host
459
+
460
+ raise UnsafeURIError,
461
+ "Requested URI '#{new_uri}' has host '#{new_uri.host}' but the " \
462
+ "configured base_uri '#{normalized_base}' has host '#{normalized_base.host}'. " \
463
+ "This request could send credentials to an unintended server."
464
+ end
417
465
  end
418
466
  end
@@ -67,12 +67,12 @@ module HTTParty
67
67
  end
68
68
 
69
69
  # Support old multiple_choice? method from pre 2.0.0 era.
70
- if ::RUBY_VERSION >= '2.0.0' && ::RUBY_PLATFORM != 'java'
70
+ if ::RUBY_PLATFORM != 'java'
71
71
  alias_method :multiple_choice?, :multiple_choices?
72
72
  end
73
73
 
74
74
  # Support old status codes method from pre 2.6.0 era.
75
- if ::RUBY_VERSION >= '2.6.0' && ::RUBY_PLATFORM != 'java'
75
+ if ::RUBY_PLATFORM != 'java'
76
76
  alias_method :gateway_time_out?, :gateway_timeout?
77
77
  alias_method :request_entity_too_large?, :payload_too_large?
78
78
  alias_method :request_time_out?, :request_timeout?
@@ -133,13 +133,27 @@ module HTTParty
133
133
  end
134
134
 
135
135
  def throw_exception
136
- if @request.options[:raise_on] && @request.options[:raise_on].include?(code)
136
+ raise_on = @request.options[:raise_on]
137
+ return if raise_on.nil?
138
+
139
+ matchers = raise_on.is_a?(Range) ? [raise_on] : [*raise_on]
140
+
141
+ if matchers.any? { |matcher| code_matches?(matcher) }
137
142
  ::Kernel.raise ::HTTParty::ResponseError.new(@response), "Code #{code} - #{body}"
138
143
  end
139
144
  end
140
145
 
141
146
  private
142
147
 
148
+ def code_matches?(matcher)
149
+ case matcher
150
+ when Range
151
+ matcher.cover?(code) || matcher.cover?(code.to_s)
152
+ else
153
+ /#{matcher}/.match?(code.to_s)
154
+ end
155
+ end
156
+
143
157
  def warn_about_nil_deprecation
144
158
  trace_line = caller.reject { |line| line.include?('httparty') }.first
145
159
  warning = "[DEPRECATION] HTTParty will no longer override `response#nil?`. " \
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTParty
4
- VERSION = '0.21.0'
4
+ VERSION = '0.24.3'
5
5
  end
data/lib/httparty.rb CHANGED
@@ -3,11 +3,6 @@
3
3
  require 'pathname'
4
4
  require 'net/http'
5
5
  require 'uri'
6
- require 'zlib'
7
- require 'multi_xml'
8
- require 'mini_mime'
9
- require 'json'
10
- require 'csv'
11
6
 
12
7
  require 'httparty/module_inheritable_attributes'
13
8
  require 'httparty/cookie_hash'
@@ -46,7 +41,7 @@ module HTTParty
46
41
  # [:+local_host+:] Local address to bind to before connecting.
47
42
  # [:+local_port+:] Local port to bind to before connecting.
48
43
  # [:+body_stream+:] Allow streaming to a REST server to specify a body_stream.
49
- # [:+stream_body+:] Allow for streaming large files without loading them into memory.
44
+ # [:+stream_body+:] When downloading with a block, avoids accumulating the response in memory. When uploading files, streams the request body to reduce memory usage (opt-in).
50
45
  # [:+multipart+:] Force content-type to be multipart
51
46
  #
52
47
  # There are also another set of options with names corresponding to various class methods. The methods in question are those that let you set a class-wide default, and the options override the defaults on a request-by-request basis. Those options are:
@@ -67,6 +62,16 @@ module HTTParty
67
62
  # * :+ssl_ca_path+: see HTTParty::ClassMethods.ssl_ca_path.
68
63
 
69
64
  module ClassMethods
65
+ # Turns on or off the foul option.
66
+ #
67
+ # class Foo
68
+ # include HTTParty
69
+ # foul true
70
+ # end
71
+ def foul(bool)
72
+ default_options[:foul] = bool
73
+ end
74
+
70
75
  # Turns on logging
71
76
  #
72
77
  # class Foo
@@ -79,14 +84,15 @@ module HTTParty
79
84
  default_options[:log_format] = format
80
85
  end
81
86
 
82
- # Raises HTTParty::ResponseError if response's code matches this statuses
87
+ # Raises HTTParty::ResponseError if the response status matches a code,
88
+ # range, or regular expression string.
83
89
  #
84
90
  # class Foo
85
91
  # include HTTParty
86
- # raise_on [404, 500]
92
+ # raise_on [404, 500..599, '3[0-9]*']
87
93
  # end
88
94
  def raise_on(codes = [])
89
- default_options[:raise_on] = *codes
95
+ default_options[:raise_on] = codes.is_a?(Range) ? [codes] : [*codes]
90
96
  end
91
97
 
92
98
  # Allows setting http proxy information to be used
@@ -591,6 +597,13 @@ module HTTParty
591
597
  perform_request Net::HTTP::Unlock, path, options, &block
592
598
  end
593
599
 
600
+ def build_request(http_method, path, options = {})
601
+ options = ModuleInheritableAttributes.hash_deep_dup(default_options).merge(options)
602
+ HeadersProcessor.new(headers, options).call
603
+ process_cookies(options)
604
+ Request.new(http_method, path, options)
605
+ end
606
+
594
607
  attr_reader :default_options
595
608
 
596
609
  private
@@ -606,10 +619,7 @@ module HTTParty
606
619
  end
607
620
 
608
621
  def perform_request(http_method, path, options, &block) #:nodoc:
609
- options = ModuleInheritableAttributes.hash_deep_dup(default_options).merge(options)
610
- HeadersProcessor.new(headers, options).call
611
- process_cookies(options)
612
- Request.new(http_method, path, options).perform(&block)
622
+ build_request(http_method, path, options).perform(&block)
613
623
  end
614
624
 
615
625
  def process_cookies(options) #:nodoc:
@@ -676,6 +686,10 @@ module HTTParty
676
686
  def self.options(*args, &block)
677
687
  Basement.options(*args, &block)
678
688
  end
689
+
690
+ def self.build_request(*args, &block)
691
+ Basement.build_request(*args, &block)
692
+ end
679
693
  end
680
694
 
681
695
  require 'httparty/hash_conversions'
data/script/release CHANGED
@@ -18,9 +18,9 @@ gem_name=httparty
18
18
  rm -rf $gem_name-*.gem
19
19
  gem build -q $gem_name.gemspec
20
20
 
21
- # Make sure we're on the master branch.
22
- (git branch | grep -q '* master') || {
23
- echo "Only release from the master branch."
21
+ # Make sure we're on the main branch.
22
+ (git branch | grep -q '* main') || {
23
+ echo "Only release from the main branch."
24
24
  exit 1
25
25
  }
26
26
 
@@ -39,4 +39,4 @@ git fetch -t origin
39
39
 
40
40
  # Tag it and bag it.
41
41
  gem push $gem_name-*.gem && git tag "$tag" &&
42
- git push origin master && git push origin "$tag"
42
+ git push origin main && git push origin "$tag"
metadata CHANGED
@@ -1,16 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httparty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.0
4
+ version: 0.24.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  - Sandro Turriate
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2022-12-30 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: csv
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
14
27
  - !ruby/object:Gem::Dependency
15
28
  name: multi_xml
16
29
  requirement: !ruby/object:Gem::Requirement
@@ -48,6 +61,7 @@ extensions: []
48
61
  extra_rdoc_files: []
49
62
  files:
50
63
  - ".editorconfig"
64
+ - ".github/dependabot.yml"
51
65
  - ".github/workflows/ci.yml"
52
66
  - ".gitignore"
53
67
  - ".rubocop.yml"
@@ -76,6 +90,7 @@ files:
76
90
  - examples/microsoft_graph.rb
77
91
  - examples/multipart.rb
78
92
  - examples/nokogiri_html_parser.rb
93
+ - examples/party_foul_mode.rb
79
94
  - examples/peer_cert.rb
80
95
  - examples/rescue_json.rb
81
96
  - examples/rubyurl.rb
@@ -102,6 +117,7 @@ files:
102
117
  - lib/httparty/request.rb
103
118
  - lib/httparty/request/body.rb
104
119
  - lib/httparty/request/multipart_boundary.rb
120
+ - lib/httparty/request/streaming_multipart_body.rb
105
121
  - lib/httparty/response.rb
106
122
  - lib/httparty/response/headers.rb
107
123
  - lib/httparty/response_fragment.rb
@@ -114,7 +130,8 @@ files:
114
130
  homepage: https://github.com/jnunemaker/httparty
115
131
  licenses:
116
132
  - MIT
117
- metadata: {}
133
+ metadata:
134
+ changelog_uri: https://github.com/jnunemaker/httparty/releases
118
135
  post_install_message: When you HTTParty, you must party hard!
119
136
  rdoc_options: []
120
137
  require_paths:
@@ -123,15 +140,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
123
140
  requirements:
124
141
  - - ">="
125
142
  - !ruby/object:Gem::Version
126
- version: 2.3.0
143
+ version: 2.7.0
127
144
  required_rubygems_version: !ruby/object:Gem::Requirement
128
145
  requirements:
129
146
  - - ">="
130
147
  - !ruby/object:Gem::Version
131
148
  version: '0'
132
149
  requirements: []
133
- rubygems_version: 3.3.7
134
- signing_key:
150
+ rubygems_version: 4.0.18
135
151
  specification_version: 4
136
152
  summary: Makes http fun! Also, makes consuming restful web services dead easy.
137
153
  test_files: []