patron 0.14.0 → 1.0.0

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/README.md +83 -49
  4. data/ext/patron/extconf.rb +54 -23
  5. data/ext/patron/session_ext.c +1044 -923
  6. data/lib/patron/compat.rb +57 -0
  7. data/lib/patron/error.rb +21 -13
  8. data/lib/patron/header_parser.rb +27 -25
  9. data/lib/patron/request.rb +98 -144
  10. data/lib/patron/response.rb +28 -24
  11. data/lib/patron/response_decoding.rb +60 -67
  12. data/lib/patron/session.rb +114 -108
  13. data/lib/patron/util.rb +14 -12
  14. data/lib/patron/version.rb +3 -1
  15. data/lib/patron.rb +9 -15
  16. metadata +12 -58
  17. data/.autotest +0 -15
  18. data/.github/dependabot.yml +0 -6
  19. data/.github/workflows/ci.yml +0 -79
  20. data/.gitignore +0 -10
  21. data/.rspec +0 -0
  22. data/.travis.yml +0 -13
  23. data/.yardopts +0 -1
  24. data/Gemfile +0 -18
  25. data/Rakefile +0 -31
  26. data/ext/patron/.gitignore +0 -4
  27. data/ext/patron/membuffer.c +0 -85
  28. data/ext/patron/membuffer.h +0 -78
  29. data/ext/patron/sglib.h +0 -1952
  30. data/lib/patron/proxy_type.rb +0 -11
  31. data/patron.gemspec +0 -39
  32. data/pic.png +0 -0
  33. data/script/console +0 -11
  34. data/script/generate_client_certs +0 -65
  35. data/script/test_server +0 -7
  36. data/spec/header_parser_spec.rb +0 -101
  37. data/spec/patron_spec.rb +0 -28
  38. data/spec/request_spec.rb +0 -89
  39. data/spec/response_spec.rb +0 -145
  40. data/spec/sample_response_headers/headers_wetransfer.txt +0 -18
  41. data/spec/sample_response_headers/headers_wetransfer_with_proxy_status.txt +0 -20
  42. data/spec/sample_response_headers/headers_wetransfer_with_redirect.txt +0 -24
  43. data/spec/sample_response_headers/headers_with_set_cookie.txt +0 -9
  44. data/spec/sample_response_headers/sample_http2_header.txt +0 -16
  45. data/spec/sample_response_headers/webmock_headers_without_trailing_crlf.txt +0 -15
  46. data/spec/session_spec.rb +0 -865
  47. data/spec/session_ssl_spec.rb +0 -431
  48. data/spec/spec_helper.rb +0 -40
  49. data/spec/support/certs/cacert.pem +0 -36
  50. data/spec/support/certs/client.p12 +0 -0
  51. data/spec/support/certs/client_ca.pem +0 -20
  52. data/spec/support/certs/client_cert.pem +0 -20
  53. data/spec/support/certs/client_key.pem +0 -30
  54. data/spec/support/certs/privkey.pem +0 -51
  55. data/spec/support/certs/rogue_client.p12 +0 -0
  56. data/spec/support/config.ru +0 -153
  57. data/spec/support/fork.rb +0 -70
  58. data/spec/support/raw_request_capture.rb +0 -93
  59. data/spec/support/test_server.rb +0 -29
  60. data/spec/support/valgrind.supp +0 -2
  61. data/spec/util_spec.rb +0 -71
@@ -1,102 +1,95 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Patron
2
4
  # Contains methods used for decoding the HTTP response body. These are only ever used internally
3
5
  # by the Response class.
4
6
  module ResponseDecoding
5
-
7
+
6
8
  private
7
9
 
8
- CHARSET_CONTENT_TYPE_RE = /(?:charset|encoding)="?([a-z0-9-]+)"?/i.freeze
9
-
10
- MISREPORTED_ENCODING_ERROR = <<-EOF
11
- The server stated that the response has the charset matching %{declared}, but the actual
12
- response body failed to decode as such (not flagged as `valid_encoding?')
13
- Maybe the response body has a different encoding than suggested by the
14
- server, or a binary response has been tagged by the server as text by mistake.
15
- If you are performing requests against servers that are known to report wrong or invalid charsets, use
16
- `Response#body' instead and handle the character set coercion externally. For instance, you may elect to parse
17
- the resulting HTML/XML for charset declarations.
18
- EOF
19
-
20
- INVALID_CHARSET_NAME_ERROR = <<-EOF
21
- The server specified an invalid charset in the Content-Type header (%{content_type}), \
22
- or Ruby does not support this charset. If you are performing requests against servers \
23
- that are known to report wrong or invalid charsets, use 'Response#body` instead \
24
- and handle the character set coercion at call site.
25
- EOF
26
-
27
- INTERNAL_CHARSET_MISMATCH_ERROR = <<-EOF
28
- The response body is %{source_encoding}, but the current \
29
- `Encoding.default_internal' (or the encoding for a new empty string if you never \
30
- set `Encoding.default_internal') - %{target_encoding} - cannot be used to represent the response body in \
31
- a lossless way. Your options are:
32
- a) using `Response#body' instead
33
- b) switching your Ruby process to an encoding that supports the needed repertoire
34
- c) using `Response#inspectable_body' to convert the body in a lossy way
35
- EOF
10
+ CHARSET_CONTENT_TYPE_RE = /(?:charset|encoding)="?([a-z0-9-]+)"?/i
11
+
12
+ MISREPORTED_ENCODING_ERROR = <<~EOF
13
+ The server stated that the response has the charset matching %{declared}, but the actual
14
+ response body failed to decode as such (not flagged as `valid_encoding?')
15
+ Maybe the response body has a different encoding than suggested by the
16
+ server, or a binary response has been tagged by the server as text by mistake.
17
+ If you are performing requests against servers that are known to report wrong or invalid charsets, use
18
+ `Response#body' instead and handle the character set coercion externally. For instance, you may elect to parse
19
+ the resulting HTML/XML for charset declarations.
20
+ EOF
21
+
22
+ INVALID_CHARSET_NAME_ERROR = <<~EOF
23
+ The server specified an invalid charset in the Content-Type header (%{content_type}), \
24
+ or Ruby does not support this charset. If you are performing requests against servers \
25
+ that are known to report wrong or invalid charsets, use 'Response#body` instead \
26
+ and handle the character set coercion at call site.
27
+ EOF
28
+
29
+ INTERNAL_CHARSET_MISMATCH_ERROR = <<~EOF
30
+ The response body is %{source_encoding}, but the current \
31
+ `Encoding.default_internal' (or the encoding for a new empty string if you never \
32
+ set `Encoding.default_internal') - %{target_encoding} - cannot be used to represent the response body in \
33
+ a lossless way. Your options are:
34
+ a) using `Response#body' instead
35
+ b) switching your Ruby process to an encoding that supports the needed repertoire
36
+ c) using `Response#inspectable_body' to convert the body in a lossy way
37
+ EOF
36
38
 
37
39
  def decode_body(strict)
38
- # Try to detect the body encoding from headers
39
40
  body_encoding = encoding_from_headers_or_binary
40
-
41
- # See if the body actually _is_ in this encoding.
42
- encoding_matched = @body.force_encoding(body_encoding).valid_encoding?
43
- if !encoding_matched
44
- raise HeaderCharsetInvalid, MISREPORTED_ENCODING_ERROR % {declared: body_encoding}
41
+
42
+ # Re-tag a copy, so that #body stays binary no matter what the server claimed
43
+ tagged = @body.dup.force_encoding(body_encoding)
44
+ unless tagged.valid_encoding?
45
+ raise HeaderCharsetInvalid, MISREPORTED_ENCODING_ERROR % { declared: body_encoding }
45
46
  end
46
-
47
+
47
48
  if strict
48
- convert_encoding_and_raise(@body)
49
+ convert_encoding_and_raise(tagged)
49
50
  else
50
- @body.encode(internal_encoding, :undefined => :replace, :replace => '?')
51
+ tagged.encode(internal_encoding, undefined: :replace, replace: '?')
51
52
  end
52
53
  end
53
54
 
54
55
  def convert_encoding_and_raise(str)
55
56
  internal = internal_encoding
56
57
  str.encode(internal)
57
- rescue Encoding::UndefinedConversionError => e
58
+ rescue Encoding::UndefinedConversionError
58
59
  enc = str.encoding == Encoding::BINARY ? 'binary' : str.encoding.to_s
59
60
  raise NonRepresentableBody,
60
- INTERNAL_CHARSET_MISMATCH_ERROR % {source_encoding: enc, target_encoding: internal}
61
+ INTERNAL_CHARSET_MISMATCH_ERROR % { source_encoding: enc, target_encoding: internal }
61
62
  end
62
-
63
+
63
64
  def charset_from_content_type
64
- return $1 if @headers["Content-Type"].to_s =~ CHARSET_CONTENT_TYPE_RE
65
+ @headers["Content-Type"].to_s[CHARSET_CONTENT_TYPE_RE, 1]
65
66
  end
66
-
67
+
67
68
  def encoding_from_headers_or_binary
68
- return Encoding::BINARY unless charset_name = charset_from_content_type
69
+ return Encoding::BINARY unless (charset_name = charset_from_content_type)
69
70
  Encoding.find(charset_name)
70
- rescue ArgumentError => e # invalid charset name
71
+ rescue ArgumentError # invalid charset name
71
72
  raise HeaderCharsetInvalid,
72
- INVALID_CHARSET_NAME_ERROR % {content_type: @headers['Content-Type'].inspect}
73
+ INVALID_CHARSET_NAME_ERROR % { content_type: @headers['Content-Type'].inspect }
73
74
  end
74
-
75
+
75
76
  def internal_encoding
76
- # Use a trick here - instead of using `default_internal` we will create
77
- # an empty string, and then get it's encoding instead. For example, this holds
78
- # true on 2.1+ on OSX:
79
- #
80
- # Encoding.default_internal #=> nil
81
- # ''.encoding #=> #<Encoding:UTF-8>
77
+ # `Encoding.default_internal` is nil on a stock Ruby while a new empty String is UTF-8,
78
+ # so the String is the better oracle for "what does this process speak"
82
79
  Encoding.default_internal || ''.encoding
83
80
  end
84
-
81
+
85
82
  def decode_header_data(str)
86
- # Header data is tricky. Strictly speaking, it _must_ be ISO-encoded. However, Content-Disposition
87
- # sometimes gets sent as raw UTF8 - and most browsers (except for localized IE versions on Windows)
88
- # treat it as such. So a fallback chain of 8859-1->UTF8->binary seems the most sane.
89
- tries = [Encoding::ISO8859_1, Encoding::UTF_8, Encoding::BINARY]
90
- tries.each do |possible_enc|
91
- begin
92
- return str.encode(possible_enc)
93
- rescue ::Encoding::UndefinedConversionError
94
- next
95
- end
83
+ # Headers are ISO-8859-1 by the book, but Content-Disposition routinely arrives as raw UTF-8
84
+ # and every browser but localized IE treats it as such, so 8859-1 -> UTF-8 -> binary it is
85
+ [Encoding::ISO8859_1, Encoding::UTF_8, Encoding::BINARY].each do |possible_enc|
86
+ return str.encode(possible_enc)
87
+ rescue ::Encoding::UndefinedConversionError
88
+ next
96
89
  end
97
- str # if it doesn't encode, just give back what we got
90
+ str
98
91
  end
99
92
  end
100
-
101
- private_constant :ResponseDecoding if respond_to?(:private_constant)
93
+
94
+ private_constant :ResponseDecoding
102
95
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
  require 'patron/error'
3
5
  require 'patron/request'
@@ -6,17 +8,24 @@ require 'patron/response'
6
8
  require 'patron/session_ext'
7
9
  require 'patron/util'
8
10
  require 'patron/header_parser'
11
+ require 'patron/compat'
9
12
 
10
13
  module Patron
11
14
 
12
15
  # This class represents multiple request/response transactions with an HTTP
13
16
  # server. This is the primary API for Patron.
17
+ #
18
+ # A Session owns a libcurl multi handle with its connection cache, so requests to the
19
+ # same host reuse connections. A Session performs one request at a time: use one Session
20
+ # per thread (or per fiber). Performing a request while another thread is already
21
+ # performing one on the same Session raises Patron::Error. A Session belongs to the Ractor
22
+ # that created it and cannot be sent to or shared with another one; Requests and Responses can.
14
23
  class Session
15
24
 
16
- # @return [Integer] HTTP connection timeout in seconds. Defaults to 1 second.
25
+ # @return [Numeric] HTTP connection timeout in seconds. Defaults to 1 second.
17
26
  attr_accessor :connect_timeout
18
27
 
19
- # @return [Integer] HTTP transaction timeout in seconds. Defaults to 5 seconds.
28
+ # @return [Numeric] HTTP transaction timeout in seconds. Defaults to 5 seconds.
20
29
  attr_accessor :timeout
21
30
 
22
31
  # @return [Integer] DNS cache timeout in seconds. Defaults to 60 seconds. Set to 0 for no cache, or to -1 for indefinite caching.
@@ -33,7 +42,7 @@ module Patron
33
42
  # Username for http authentication
34
43
  # @return [String,nil] the HTTP basic auth username
35
44
  attr_accessor :username
36
-
45
+
37
46
  # Password for http authentication
38
47
  # @return [String,nil] the HTTP basic auth password
39
48
  attr_accessor :password
@@ -48,7 +57,7 @@ module Patron
48
57
  # @return [Hash] headers used in all requests.
49
58
  attr_accessor :headers
50
59
 
51
- # @return [Symbol] the authentication type for the request (`:basic`, `:digest` or `:token`).
60
+ # @return [Symbol] the authentication type for the request (`:basic`, `:digest` or `:any`).
52
61
  # @see Patron::Request#auth_type
53
62
  attr_accessor :auth_type
54
63
 
@@ -56,12 +65,14 @@ module Patron
56
65
  # Please consider twice before using this option..
57
66
  attr_accessor :insecure
58
67
 
59
- # @return [String] the SSL version for the requests, or nil if all versions are permitted
60
- # The supported values are nil, "SSLv2", "SSLv3", and "TLSv1".
68
+ # @return [String] the SSL version for the requests, or nil if all versions are permitted.
69
+ # The supported values are the keys of Patron::Request::SSL_VERSIONS:
70
+ # "TLSv1", "TLSv1_0", "TLSv1_1", "TLSv1_2" and "TLSv1_3".
61
71
  attr_accessor :ssl_version
62
72
 
63
- # @return [String] the HTTP version for the requests, or "None" if libcurl is to choose permitted versions
64
- # The supported values are "None", "HTTPv1_0", "HTTPv1_1", "HTTPv2_0", "HTTPv2_TLS", and "HTTPv2_PRIOR".
73
+ # @return [String] the HTTP version for the requests, or "None" if libcurl is to choose permitted versions.
74
+ # The supported values are the keys of Patron::Request::HTTP_VERSIONS: "None", "HTTPv1_0", "HTTPv1_1",
75
+ # "HTTPv2_0", "HTTPv2_TLS", "HTTPv2_PRIOR", "HTTPv3" and (with libcurl 7.88+) "HTTPv3_ONLY".
65
76
  attr_accessor :http_version
66
77
 
67
78
  # @return [String] path to the CA file used for certificate verification, or `nil` if CURL default is used
@@ -94,16 +105,15 @@ module Patron
94
105
  # response does not specify a charset in it's `Content-Type` header already, if it does that charset
95
106
  # will take precedence.
96
107
  attr_accessor :default_response_charset
97
-
108
+
98
109
  # @return [Boolean] Force curl to use IPv4
99
110
  attr_accessor :force_ipv4
100
111
 
101
112
  # @return [Boolean] Support automatic Content-Encoding decompression and set liberal Accept-Encoding headers
102
113
  attr_accessor :automatic_content_encoding
103
114
 
104
- # @return [Fixnum, nil] the amount of bytes downloaded. If it is set to nil
105
- # (default) no limit will be applied.
106
- # **Note that this only works on libCURL 7.34 and newer**
115
+ # @return [Integer, nil] the maximum number of bytes to download. If it is set to nil
116
+ # (default) no limit will be applied. Exceeding it raises Patron::DownloadLimitExceeded.
107
117
  attr_accessor :download_byte_limit
108
118
 
109
119
  # @return [Integer, nil] the time in number seconds that the transfer speed should be below the
@@ -118,24 +128,20 @@ module Patron
118
128
 
119
129
  # @return [#call, nil] callable object that will be called with 4 arguments
120
130
  # during request/response execution - `dltotal`, `dlnow`, `ultotal`, `ulnow`.
121
- # All these arguments are in bytes.
131
+ # All these arguments are in bytes. Returning `false` from the callback aborts the
132
+ # request with Patron::Aborted.
122
133
  attr_accessor :progress_callback
123
134
 
124
135
  # Create a new Session object for performing requests.
125
136
  #
126
- # @param args[Hash] options for the Session (same names as the writable attributes of the Session)
137
+ # @param options [Hash] the writable attributes of the Session, by name
127
138
  # @yield self
128
- def initialize(args = {}, &block)
129
-
130
- # Allows accessors to be set via constructor hash. Ex: {:base_url => 'www.home.com'}
131
- args.each do |attribute, value|
132
- send("#{attribute}=", value)
139
+ def initialize(**options, &block)
140
+ options.each do |attribute, value|
141
+ public_send("#{attribute}=", value)
133
142
  end
134
143
 
135
- # Allows accessors to be set via block.
136
- if block_given?
137
- yield self
138
- end
144
+ yield self if block_given?
139
145
 
140
146
  @headers ||= {}
141
147
  @headers['User-Agent'] ||= Patron.user_agent_string
@@ -156,7 +162,7 @@ module Patron
156
162
  def handle_cookies(file_path = nil)
157
163
  if file_path
158
164
  path = Pathname(file_path).expand_path
159
-
165
+
160
166
  if !File.exist?(file_path) && !File.writable?(path.dirname)
161
167
  raise ArgumentError, "Can't create file #{path} (permission error)"
162
168
  elsif File.exist?(file_path) && !File.writable?(file_path)
@@ -165,22 +171,24 @@ module Patron
165
171
  else
166
172
  path = nil
167
173
  end
168
-
169
- # Apparently calling this with an empty string sets the cookie file,
170
- # but calling it with a path to a writable file sets that file to be
171
- # the cookie jar (new cookies are written there)
172
- add_cookie_file(path.to_s)
173
-
174
+
175
+ # libcurl reads the cookie file and, when the path is not empty, also writes the jar back to it
176
+ @cookie_file_path = path.to_s
177
+ set_cookie_file(@cookie_file_path)
174
178
  self
175
179
  end
176
180
 
177
- # Enable debug output to stderr or to specified `file`.
181
+ # Enable libcurl's verbose output.
178
182
  #
179
- # @todo Change to an assignment of an IO object
180
- # @param file[String, nil] path to the file to write debug data to, or `nil` to print to `STDERR`
183
+ # @param destination [IO, String, nil] an IO to write to, a path to a file to write to, or `nil` for `$stderr`
181
184
  # @return self
182
- def enable_debug(file = nil)
183
- set_debug_file(file.to_s)
185
+ def enable_debug(destination = nil)
186
+ @debug_io = case destination
187
+ when nil then $stderr
188
+ when ->(d) { d.respond_to?(:write) } then destination
189
+ else File.open(destination.to_s, 'wb').tap { |f| f.sync = true }
190
+ end
191
+ set_debug_io(@debug_io)
184
192
  self
185
193
  end
186
194
 
@@ -210,7 +218,7 @@ module Patron
210
218
  # @param filename[String] path to the file to save the response body in
211
219
  # @return [Patron::Response]
212
220
  def get_file(url, filename, headers = {})
213
- request(:get, url, headers, :file => filename)
221
+ request(:get, url, headers, file: filename)
214
222
  end
215
223
 
216
224
  # Same as #get but performs a HEAD request.
@@ -238,7 +246,6 @@ module Patron
238
246
  # Uploads the passed `data` to the specified `url` using an HTTP PUT. Note that
239
247
  # unline ++post++, a Hash is not accepted as the ++data++ argument.
240
248
  #
241
- # @todo inconsistency with "post" - Hash not accepted
242
249
  # @param url[String] the URL to fetch
243
250
  # @param data[#to_s, #to_path] an object that can be converted to a String
244
251
  # to create the request body, or that responds to #to_path to upload the
@@ -246,13 +253,12 @@ module Patron
246
253
  # @param headers[Hash] the hash of header keys to values
247
254
  # @return [Patron::Response]
248
255
  def put(url, data, headers = {})
249
- request(:put, url, headers, :data => data)
256
+ request(:put, url, headers, data: data)
250
257
  end
251
258
 
252
259
  # Uploads the passed `data` to the specified `url` using an HTTP PATCH. Note that
253
260
  # unline ++post++, a Hash is not accepted as the ++data++ argument.
254
261
  #
255
- # @todo inconsistency with "post" - Hash not accepted
256
262
  # @param url[String] the URL to fetch
257
263
  # @param data[#to_s, #to_path] an object that can be converted to a String
258
264
  # to create the request body, or that responds to #to_path to upload the
@@ -260,7 +266,7 @@ module Patron
260
266
  # @param headers[Hash] the hash of header keys to values
261
267
  # @return [Patron::Response]
262
268
  def patch(url, data, headers = {})
263
- request(:patch, url, headers, :data => data)
269
+ request(:patch, url, headers, data: data)
264
270
  end
265
271
 
266
272
  # Uploads the contents of `file` to the specified `url` using an HTTP PUT. The file will be
@@ -271,7 +277,7 @@ module Patron
271
277
  # @param headers[Hash] the hash of header keys to values
272
278
  # @return [Patron::Response]
273
279
  def put_file(url, filename, headers = {})
274
- request(:put, url, headers, :file => filename)
280
+ request(:put, url, headers, file: filename)
275
281
  end
276
282
 
277
283
  # Uploads the passed `data` to the specified `url` using an HTTP POST.
@@ -285,10 +291,10 @@ module Patron
285
291
  # @return [Patron::Response]
286
292
  def post(url, data, headers = {})
287
293
  if data.is_a?(Hash)
288
- data = data.map {|k,v| urlencode(k.to_s) + '=' + urlencode(v.to_s) }.join('&')
289
- headers['Content-Type'] = 'application/x-www-form-urlencoded'
294
+ data = data.map { |k, v| urlencode(k.to_s) + '=' + urlencode(v.to_s) }.join('&')
295
+ headers = headers.merge('Content-Type' => 'application/x-www-form-urlencoded')
290
296
  end
291
- request(:post, url, headers, :data => data)
297
+ request(:post, url, headers, data: data)
292
298
  end
293
299
 
294
300
  # Uploads the contents of `file` to the specified `url` using an HTTP POST.
@@ -299,7 +305,7 @@ module Patron
299
305
  # @param headers[Hash] the hash of header keys to values
300
306
  # @return [Patron::Response]
301
307
  def post_file(url, filename, headers = {})
302
- request(:post, url, headers, :file => filename)
308
+ request(:post, url, headers, file: filename)
303
309
  end
304
310
 
305
311
  # Uploads the contents of `filename` to the specified `url` using an HTTP POST,
@@ -311,7 +317,7 @@ module Patron
311
317
  # @param headers[Hash] the hash of header keys to values
312
318
  # @return [Patron::Response]
313
319
  def post_multipart(url, data, filename, headers = {})
314
- request(:post, url, headers, {:data => data, :file => filename, :multipart => true})
320
+ request(:post, url, headers, data: data, file: filename, multipart: true)
315
321
  end
316
322
 
317
323
  # @!group WebDAV methods
@@ -322,97 +328,97 @@ module Patron
322
328
  # @param headers[Hash] the hash of header keys to values
323
329
  # @return [Patron::Response]
324
330
  def copy(url, dest, headers = {})
325
- headers['Destination'] = dest
326
- request(:copy, url, headers)
331
+ request(:copy, url, headers.merge('Destination' => dest))
327
332
  end
328
333
  # @!endgroup
329
-
334
+
330
335
  # @!group Basic API methods
331
336
  # Send an HTTP request to the specified `url`.
332
337
  #
333
338
  # @param action[#to_s] the HTTP verb
334
339
  # @param url[String] the URL for the request
335
340
  # @param headers[Hash] headers to send along with the request
336
- # @param options[Hash] any additonal setters to call on the Request
341
+ # @param options[Hash] any additional Request attributes, plus `:data`, `:file`, `:multipart` and `:query`
337
342
  # @see Patron::Request
338
343
  # @return [Patron::Response]
339
- def request(action, url, headers, options = {})
340
- req = build_request(action, url, headers, options)
341
- handle_request(req)
344
+ def request(action, url, headers = {}, *positional, **options)
345
+ # The 0.x positional options Hash is tolerated here rather than in a prepended module, since
346
+ # sniffer and friends alias_method-chain this method and a prepend would recurse into the alias
347
+ options = Compat.hash_to_keywords("Patron::Session#request with an options Hash", positional, options)
348
+ handle_request(build_request(action, url, headers, **options))
342
349
  end
343
-
350
+
344
351
  # Returns the class that will be used to build a Response
345
352
  # from a Curl call.
346
353
  #
347
354
  # Primarily useful if you need a very lightweight Response
348
355
  # object that does not have to perform all the parsing of
349
356
  # various headers/status codes. The method must return
350
- # a module that supports the same interface for +new+
351
- # as ++Patron::Response++
357
+ # a class that supports the same keyword arguments for `new`
358
+ # as Patron::Response.
352
359
  #
353
- # @return [#new] Returns any object that responds to `.new` with 6 arguments
360
+ # @return [#new]
354
361
  # @see Patron::Response#initialize
355
362
  def response_class
356
363
  ::Patron::Response
357
364
  end
358
-
365
+
359
366
  # Builds a request object that can be used by ++handle_request++
360
- # Note that internally, ++handle_request++ uses instance variables of
361
- # the Request object, and not it's public methods.
362
367
  #
363
368
  # @param action[String] the HTTP verb
364
369
  # @param url[#to_s] the addition to the base url component, or a complete URL
365
- # @param headers[Hash] a hash of headers, "Accept" will be automatically set to an empty string if not provided
370
+ # @param headers[Hash] a hash of headers, "Expect" will be automatically set to an empty string if not provided
366
371
  # @param options[Hash] any overriding options (will shadow the options from the Session object)
367
372
  # @return [Patron::Request] the request that will be passed to ++handle_request++
368
- def build_request(action, url, headers, options = {})
369
- # If the Expect header isn't set uploads are really slow
370
- headers['Expect'] ||= ''
371
-
372
- Request.new.tap do |req|
373
- req.action = action
374
- req.headers = self.headers.merge headers
375
- req.automatic_content_encoding = options.fetch :automatic_content_encoding, self.automatic_content_encoding
376
- req.timeout = options.fetch :timeout, self.timeout
377
- req.connect_timeout = options.fetch :connect_timeout, self.connect_timeout
378
- req.dns_cache_timeout = options.fetch :dns_cache_timeout, self.dns_cache_timeout
379
- req.low_speed_time = options.fetch :low_speed_time, self.low_speed_time
380
- req.low_speed_limit = options.fetch :low_speed_limit, self.low_speed_limit
381
- req.force_ipv4 = options.fetch :force_ipv4, self.force_ipv4
382
- req.max_redirects = options.fetch :max_redirects, self.max_redirects
383
- req.username = options.fetch :username, self.username
384
- req.password = options.fetch :password, self.password
385
- req.proxy = options.fetch :proxy, self.proxy
386
- req.proxy_type = options.fetch :proxy_type, self.proxy_type
387
- req.auth_type = options.fetch :auth_type, self.auth_type
388
- req.insecure = options.fetch :insecure, self.insecure
389
- req.ssl_version = options.fetch :ssl_version, self.ssl_version
390
- req.http_version = options.fetch :http_version, self.http_version
391
- req.cacert = options.fetch :cacert, self.cacert
392
- req.ssl_cert = options.fetch :ssl_cert, self.ssl_cert
393
- req.ssl_cert_type = options.fetch :ssl_cert_type, self.ssl_cert_type
394
- req.ssl_key = options.fetch :ssl_key, self.ssl_key
395
- req.ssl_key_password = options.fetch :ssl_key_password, self.ssl_key_password
396
- req.ignore_content_length = options.fetch :ignore_content_length, self.ignore_content_length
397
- req.buffer_size = options.fetch :buffer_size, self.buffer_size
398
- req.download_byte_limit = options.fetch :download_byte_limit, self.download_byte_limit
399
- req.progress_callback = options.fetch :progress_callback, self.progress_callback
400
- req.multipart = options[:multipart]
401
- req.upload_data = options[:data]
402
- req.file_name = options[:file]
403
-
404
- base_url = self.base_url.to_s
405
- url = url.to_s
406
- raise ArgumentError, "Empty URL" if base_url.empty? && url.empty?
407
- uri = URI.parse(base_url.empty? ? url : File.join(base_url, url))
408
- query = uri.query.to_s.split('&')
409
- query += options[:query].is_a?(Hash) ? Util.build_query_pairs_from_hash(options[:query]) : options[:query].to_s.split('&')
410
- uri.query = query.join('&')
411
- uri.query = nil if uri.query.empty?
412
- url = uri.to_s
413
- req.url = url
414
- end
373
+ def build_request(action, url, headers = {}, **options)
374
+ # Without an empty Expect header uploads wait for a "100 Continue" that may never come
375
+ headers = { 'Expect' => '' }.merge(self.headers).merge(headers)
376
+
377
+ attributes = REQUEST_ATTRIBUTES_FROM_SESSION.to_h { |name| [name, options.fetch(name) { public_send(name) }] }
378
+ Request.new(
379
+ **attributes,
380
+ action: action,
381
+ url: build_url(url, options[:query]),
382
+ headers: headers,
383
+ multipart: options[:multipart],
384
+ upload_data: options[:data],
385
+ file_name: options[:file]
386
+ )
415
387
  end
416
388
  # @!endgroup
389
+
390
+ private
391
+
392
+ REQUEST_ATTRIBUTES_FROM_SESSION = %i[
393
+ automatic_content_encoding timeout connect_timeout dns_cache_timeout low_speed_time low_speed_limit
394
+ force_ipv4 max_redirects username password proxy proxy_type auth_type insecure ssl_version http_version
395
+ cacert ssl_cert ssl_cert_type ssl_key ssl_key_password ignore_content_length buffer_size
396
+ download_byte_limit progress_callback
397
+ ].freeze
398
+ private_constant :REQUEST_ATTRIBUTES_FROM_SESSION
399
+
400
+ def build_url(url, query)
401
+ base_url = self.base_url.to_s
402
+ url = url.to_s
403
+ raise ArgumentError, "Empty URL" if base_url.empty? && url.empty?
404
+
405
+ full_url = if base_url.empty? || url.match?(%r{\A[a-z][a-z0-9+.-]*://}i)
406
+ url
407
+ else
408
+ "#{base_url.delete_suffix('/')}/#{url.delete_prefix('/')}"
409
+ end
410
+
411
+ uri = URI.parse(full_url)
412
+ pairs = uri.query.to_s.split('&')
413
+ pairs += query.is_a?(Hash) ? Util.build_query_pairs_from_hash(query) : query.to_s.split('&')
414
+ uri.query = pairs.empty? ? nil : pairs.join('&')
415
+ uri.to_s
416
+ end
417
+
418
+ # Called from the C extension after a fork, when the libcurl handles had to be recreated
419
+ def reapply_base_options
420
+ set_cookie_file(@cookie_file_path) if @cookie_file_path
421
+ set_debug_io(@debug_io) if @debug_io
422
+ end
417
423
  end
418
424
  end
data/lib/patron/util.rb CHANGED
@@ -1,23 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Patron
2
4
  module Util
3
5
  extend self
4
-
5
- def build_query_pairs_from_hash(hash, escape_values=false)
6
- pairs = []
7
- recursive = Proc.new do |h, prefix|
8
- h.each_pair do |k,v|
9
- key = prefix == '' ? k : "#{prefix}[#{k}]"
6
+
7
+ # Flattens a (possibly nested) Hash into "key[sub]=value" pairs, Rack style
8
+ def build_query_pairs_from_hash(hash, escape_values = false, prefix = nil, pairs = [])
9
+ hash.each_pair do |k, v|
10
+ key = prefix ? "#{prefix}[#{k}]" : k.to_s
11
+ if v.is_a?(Hash)
12
+ build_query_pairs_from_hash(v, escape_values, key, pairs)
13
+ else
10
14
  v = Patron::Session.escape(v.to_s) if escape_values
11
- v.is_a?(Hash) ? recursive.call(v, key) : pairs << "#{key}=#{v}"
15
+ pairs << "#{key}=#{v}"
12
16
  end
13
17
  end
14
- recursive.call(hash, '')
15
18
  pairs
16
19
  end
17
-
18
- def build_query_string_from_hash(hash, escape_values=false)
20
+
21
+ def build_query_string_from_hash(hash, escape_values = false)
19
22
  build_query_pairs_from_hash(hash, escape_values).join('&')
20
23
  end
21
-
22
24
  end
23
- end
25
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Patron
2
- VERSION = "0.14.0"
4
+ VERSION = "1.0.0"
3
5
  end
data/lib/patron.rb CHANGED
@@ -1,20 +1,14 @@
1
- require 'pathname'
1
+ # frozen_string_literal: true
2
2
 
3
- cwd = Pathname(__FILE__).dirname
4
- $:.unshift(cwd.to_s) unless $:.include?(cwd.to_s) || $:.include?(cwd.expand_path.to_s)
5
- require 'patron/session'
6
3
  require 'patron/version'
4
+ require 'patron/session'
7
5
 
8
6
  module Patron
9
- # Returns the version number of the gem
10
- # @return [String]
11
- def self.version
12
- VERSION
13
- end
14
-
15
- # Returns the default User-Agent string
16
- # @return [String]
17
- def self.user_agent_string
18
- @ua ||= "Patron/Ruby-%s-%s" % [version, libcurl_version]
19
- end
7
+ # @return [String] the version of the gem
8
+ def self.version = VERSION
9
+
10
+ # @return [String] the default User-Agent header value
11
+ def self.user_agent_string = USER_AGENT_STRING
12
+
13
+ USER_AGENT_STRING = "Patron/Ruby-#{VERSION}-#{libcurl_version}".freeze
20
14
  end