patron 0.13.4 → 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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -0
  3. data/README.md +113 -50
  4. data/ext/patron/extconf.rb +55 -20
  5. data/ext/patron/session_ext.c +1092 -761
  6. data/lib/patron/compat.rb +57 -0
  7. data/lib/patron/error.rb +25 -13
  8. data/lib/patron/header_parser.rb +27 -25
  9. data/lib/patron/request.rb +98 -142
  10. data/lib/patron/response.rb +33 -24
  11. data/lib/patron/response_decoding.rb +60 -67
  12. data/lib/patron/session.rb +127 -104
  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 -49
  17. data/.autotest +0 -15
  18. data/.github/workflows/ci.yml +0 -26
  19. data/.gitignore +0 -10
  20. data/.rspec +0 -0
  21. data/.travis.yml +0 -13
  22. data/.yardopts +0 -1
  23. data/Gemfile +0 -17
  24. data/Rakefile +0 -31
  25. data/ext/patron/.gitignore +0 -4
  26. data/ext/patron/membuffer.c +0 -85
  27. data/ext/patron/membuffer.h +0 -78
  28. data/ext/patron/sglib.h +0 -1952
  29. data/lib/patron/proxy_type.rb +0 -11
  30. data/patron.gemspec +0 -38
  31. data/pic.png +0 -0
  32. data/script/console +0 -11
  33. data/script/test_server +0 -7
  34. data/spec/header_parser_spec.rb +0 -101
  35. data/spec/patron_spec.rb +0 -28
  36. data/spec/request_spec.rb +0 -77
  37. data/spec/response_spec.rb +0 -145
  38. data/spec/sample_response_headers/headers_wetransfer.txt +0 -18
  39. data/spec/sample_response_headers/headers_wetransfer_with_proxy_status.txt +0 -20
  40. data/spec/sample_response_headers/headers_wetransfer_with_redirect.txt +0 -24
  41. data/spec/sample_response_headers/headers_with_set_cookie.txt +0 -9
  42. data/spec/sample_response_headers/sample_http2_header.txt +0 -16
  43. data/spec/sample_response_headers/webmock_headers_without_trailing_crlf.txt +0 -15
  44. data/spec/session_spec.rb +0 -698
  45. data/spec/session_ssl_spec.rb +0 -313
  46. data/spec/spec_helper.rb +0 -34
  47. data/spec/support/certs/cacert.pem +0 -36
  48. data/spec/support/certs/privkey.pem +0 -51
  49. data/spec/support/config.ru +0 -146
  50. data/spec/support/fork.rb +0 -70
  51. data/spec/support/test_server.rb +0 -26
  52. 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,17 +65,32 @@ 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
68
79
  attr_accessor :cacert
69
80
 
81
+ # @return [String] path to the client SSL certificate file, or `nil` if no client certificate is used
82
+ attr_accessor :ssl_cert
83
+
84
+ # @return [String] the client SSL certificate file type: "PEM" (the libcurl default), "DER" or "P12" (PKCS#12)
85
+ attr_accessor :ssl_cert_type
86
+
87
+ # @return [String] path to the PEM private key for the client SSL certificate, if it is not bundled with
88
+ # the certificate itself (as it is with PKCS#12)
89
+ attr_accessor :ssl_key
90
+
91
+ # @return [String] the password for the client SSL certificate private key
92
+ attr_accessor :ssl_key_password
93
+
70
94
  # @return [Boolean] whether Content-Range and Content-Length headers should be ignored
71
95
  attr_accessor :ignore_content_length
72
96
 
@@ -81,16 +105,15 @@ module Patron
81
105
  # response does not specify a charset in it's `Content-Type` header already, if it does that charset
82
106
  # will take precedence.
83
107
  attr_accessor :default_response_charset
84
-
108
+
85
109
  # @return [Boolean] Force curl to use IPv4
86
110
  attr_accessor :force_ipv4
87
111
 
88
112
  # @return [Boolean] Support automatic Content-Encoding decompression and set liberal Accept-Encoding headers
89
113
  attr_accessor :automatic_content_encoding
90
114
 
91
- # @return [Fixnum, nil] the amount of bytes downloaded. If it is set to nil
92
- # (default) no limit will be applied.
93
- # **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.
94
117
  attr_accessor :download_byte_limit
95
118
 
96
119
  # @return [Integer, nil] the time in number seconds that the transfer speed should be below the
@@ -105,24 +128,20 @@ module Patron
105
128
 
106
129
  # @return [#call, nil] callable object that will be called with 4 arguments
107
130
  # during request/response execution - `dltotal`, `dlnow`, `ultotal`, `ulnow`.
108
- # 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.
109
133
  attr_accessor :progress_callback
110
134
 
111
135
  # Create a new Session object for performing requests.
112
136
  #
113
- # @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
114
138
  # @yield self
115
- def initialize(args = {}, &block)
116
-
117
- # Allows accessors to be set via constructor hash. Ex: {:base_url => 'www.home.com'}
118
- args.each do |attribute, value|
119
- send("#{attribute}=", value)
139
+ def initialize(**options, &block)
140
+ options.each do |attribute, value|
141
+ public_send("#{attribute}=", value)
120
142
  end
121
143
 
122
- # Allows accessors to be set via block.
123
- if block_given?
124
- yield self
125
- end
144
+ yield self if block_given?
126
145
 
127
146
  @headers ||= {}
128
147
  @headers['User-Agent'] ||= Patron.user_agent_string
@@ -143,7 +162,7 @@ module Patron
143
162
  def handle_cookies(file_path = nil)
144
163
  if file_path
145
164
  path = Pathname(file_path).expand_path
146
-
165
+
147
166
  if !File.exist?(file_path) && !File.writable?(path.dirname)
148
167
  raise ArgumentError, "Can't create file #{path} (permission error)"
149
168
  elsif File.exist?(file_path) && !File.writable?(file_path)
@@ -152,22 +171,24 @@ module Patron
152
171
  else
153
172
  path = nil
154
173
  end
155
-
156
- # Apparently calling this with an empty string sets the cookie file,
157
- # but calling it with a path to a writable file sets that file to be
158
- # the cookie jar (new cookies are written there)
159
- add_cookie_file(path.to_s)
160
-
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)
161
178
  self
162
179
  end
163
180
 
164
- # Enable debug output to stderr or to specified `file`.
181
+ # Enable libcurl's verbose output.
165
182
  #
166
- # @todo Change to an assignment of an IO object
167
- # @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`
168
184
  # @return self
169
- def enable_debug(file = nil)
170
- 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)
171
192
  self
172
193
  end
173
194
 
@@ -197,7 +218,7 @@ module Patron
197
218
  # @param filename[String] path to the file to save the response body in
198
219
  # @return [Patron::Response]
199
220
  def get_file(url, filename, headers = {})
200
- request(:get, url, headers, :file => filename)
221
+ request(:get, url, headers, file: filename)
201
222
  end
202
223
 
203
224
  # Same as #get but performs a HEAD request.
@@ -225,7 +246,6 @@ module Patron
225
246
  # Uploads the passed `data` to the specified `url` using an HTTP PUT. Note that
226
247
  # unline ++post++, a Hash is not accepted as the ++data++ argument.
227
248
  #
228
- # @todo inconsistency with "post" - Hash not accepted
229
249
  # @param url[String] the URL to fetch
230
250
  # @param data[#to_s, #to_path] an object that can be converted to a String
231
251
  # to create the request body, or that responds to #to_path to upload the
@@ -233,13 +253,12 @@ module Patron
233
253
  # @param headers[Hash] the hash of header keys to values
234
254
  # @return [Patron::Response]
235
255
  def put(url, data, headers = {})
236
- request(:put, url, headers, :data => data)
256
+ request(:put, url, headers, data: data)
237
257
  end
238
258
 
239
259
  # Uploads the passed `data` to the specified `url` using an HTTP PATCH. Note that
240
260
  # unline ++post++, a Hash is not accepted as the ++data++ argument.
241
261
  #
242
- # @todo inconsistency with "post" - Hash not accepted
243
262
  # @param url[String] the URL to fetch
244
263
  # @param data[#to_s, #to_path] an object that can be converted to a String
245
264
  # to create the request body, or that responds to #to_path to upload the
@@ -247,7 +266,7 @@ module Patron
247
266
  # @param headers[Hash] the hash of header keys to values
248
267
  # @return [Patron::Response]
249
268
  def patch(url, data, headers = {})
250
- request(:patch, url, headers, :data => data)
269
+ request(:patch, url, headers, data: data)
251
270
  end
252
271
 
253
272
  # Uploads the contents of `file` to the specified `url` using an HTTP PUT. The file will be
@@ -258,7 +277,7 @@ module Patron
258
277
  # @param headers[Hash] the hash of header keys to values
259
278
  # @return [Patron::Response]
260
279
  def put_file(url, filename, headers = {})
261
- request(:put, url, headers, :file => filename)
280
+ request(:put, url, headers, file: filename)
262
281
  end
263
282
 
264
283
  # Uploads the passed `data` to the specified `url` using an HTTP POST.
@@ -272,10 +291,10 @@ module Patron
272
291
  # @return [Patron::Response]
273
292
  def post(url, data, headers = {})
274
293
  if data.is_a?(Hash)
275
- data = data.map {|k,v| urlencode(k.to_s) + '=' + urlencode(v.to_s) }.join('&')
276
- 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')
277
296
  end
278
- request(:post, url, headers, :data => data)
297
+ request(:post, url, headers, data: data)
279
298
  end
280
299
 
281
300
  # Uploads the contents of `file` to the specified `url` using an HTTP POST.
@@ -286,7 +305,7 @@ module Patron
286
305
  # @param headers[Hash] the hash of header keys to values
287
306
  # @return [Patron::Response]
288
307
  def post_file(url, filename, headers = {})
289
- request(:post, url, headers, :file => filename)
308
+ request(:post, url, headers, file: filename)
290
309
  end
291
310
 
292
311
  # Uploads the contents of `filename` to the specified `url` using an HTTP POST,
@@ -298,7 +317,7 @@ module Patron
298
317
  # @param headers[Hash] the hash of header keys to values
299
318
  # @return [Patron::Response]
300
319
  def post_multipart(url, data, filename, headers = {})
301
- request(:post, url, headers, {:data => data, :file => filename, :multipart => true})
320
+ request(:post, url, headers, data: data, file: filename, multipart: true)
302
321
  end
303
322
 
304
323
  # @!group WebDAV methods
@@ -309,93 +328,97 @@ module Patron
309
328
  # @param headers[Hash] the hash of header keys to values
310
329
  # @return [Patron::Response]
311
330
  def copy(url, dest, headers = {})
312
- headers['Destination'] = dest
313
- request(:copy, url, headers)
331
+ request(:copy, url, headers.merge('Destination' => dest))
314
332
  end
315
333
  # @!endgroup
316
-
334
+
317
335
  # @!group Basic API methods
318
336
  # Send an HTTP request to the specified `url`.
319
337
  #
320
338
  # @param action[#to_s] the HTTP verb
321
339
  # @param url[String] the URL for the request
322
340
  # @param headers[Hash] headers to send along with the request
323
- # @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`
324
342
  # @see Patron::Request
325
343
  # @return [Patron::Response]
326
- def request(action, url, headers, options = {})
327
- req = build_request(action, url, headers, options)
328
- 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))
329
349
  end
330
-
350
+
331
351
  # Returns the class that will be used to build a Response
332
352
  # from a Curl call.
333
353
  #
334
354
  # Primarily useful if you need a very lightweight Response
335
355
  # object that does not have to perform all the parsing of
336
356
  # various headers/status codes. The method must return
337
- # a module that supports the same interface for +new+
338
- # as ++Patron::Response++
357
+ # a class that supports the same keyword arguments for `new`
358
+ # as Patron::Response.
339
359
  #
340
- # @return [#new] Returns any object that responds to `.new` with 6 arguments
360
+ # @return [#new]
341
361
  # @see Patron::Response#initialize
342
362
  def response_class
343
363
  ::Patron::Response
344
364
  end
345
-
365
+
346
366
  # Builds a request object that can be used by ++handle_request++
347
- # Note that internally, ++handle_request++ uses instance variables of
348
- # the Request object, and not it's public methods.
349
367
  #
350
368
  # @param action[String] the HTTP verb
351
369
  # @param url[#to_s] the addition to the base url component, or a complete URL
352
- # @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
353
371
  # @param options[Hash] any overriding options (will shadow the options from the Session object)
354
372
  # @return [Patron::Request] the request that will be passed to ++handle_request++
355
- def build_request(action, url, headers, options = {})
356
- # If the Expect header isn't set uploads are really slow
357
- headers['Expect'] ||= ''
358
-
359
- Request.new.tap do |req|
360
- req.action = action
361
- req.headers = self.headers.merge headers
362
- req.automatic_content_encoding = options.fetch :automatic_content_encoding, self.automatic_content_encoding
363
- req.timeout = options.fetch :timeout, self.timeout
364
- req.connect_timeout = options.fetch :connect_timeout, self.connect_timeout
365
- req.dns_cache_timeout = options.fetch :dns_cache_timeout, self.dns_cache_timeout
366
- req.low_speed_time = options.fetch :low_speed_time, self.low_speed_time
367
- req.low_speed_limit = options.fetch :low_speed_limit, self.low_speed_limit
368
- req.force_ipv4 = options.fetch :force_ipv4, self.force_ipv4
369
- req.max_redirects = options.fetch :max_redirects, self.max_redirects
370
- req.username = options.fetch :username, self.username
371
- req.password = options.fetch :password, self.password
372
- req.proxy = options.fetch :proxy, self.proxy
373
- req.proxy_type = options.fetch :proxy_type, self.proxy_type
374
- req.auth_type = options.fetch :auth_type, self.auth_type
375
- req.insecure = options.fetch :insecure, self.insecure
376
- req.ssl_version = options.fetch :ssl_version, self.ssl_version
377
- req.http_version = options.fetch :http_version, self.http_version
378
- req.cacert = options.fetch :cacert, self.cacert
379
- req.ignore_content_length = options.fetch :ignore_content_length, self.ignore_content_length
380
- req.buffer_size = options.fetch :buffer_size, self.buffer_size
381
- req.download_byte_limit = options.fetch :download_byte_limit, self.download_byte_limit
382
- req.progress_callback = options.fetch :progress_callback, self.progress_callback
383
- req.multipart = options[:multipart]
384
- req.upload_data = options[:data]
385
- req.file_name = options[:file]
386
-
387
- base_url = self.base_url.to_s
388
- url = url.to_s
389
- raise ArgumentError, "Empty URL" if base_url.empty? && url.empty?
390
- uri = URI.parse(base_url.empty? ? url : File.join(base_url, url))
391
- query = uri.query.to_s.split('&')
392
- query += options[:query].is_a?(Hash) ? Util.build_query_pairs_from_hash(options[:query]) : options[:query].to_s.split('&')
393
- uri.query = query.join('&')
394
- uri.query = nil if uri.query.empty?
395
- url = uri.to_s
396
- req.url = url
397
- 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
+ )
398
387
  end
399
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
400
423
  end
401
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.13.4"
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