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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +77 -0
- data/README.md +83 -49
- data/ext/patron/extconf.rb +54 -23
- data/ext/patron/session_ext.c +1044 -923
- data/lib/patron/compat.rb +57 -0
- data/lib/patron/error.rb +21 -13
- data/lib/patron/header_parser.rb +27 -25
- data/lib/patron/request.rb +98 -144
- data/lib/patron/response.rb +28 -24
- data/lib/patron/response_decoding.rb +60 -67
- data/lib/patron/session.rb +114 -108
- data/lib/patron/util.rb +14 -12
- data/lib/patron/version.rb +3 -1
- data/lib/patron.rb +9 -15
- metadata +12 -58
- data/.autotest +0 -15
- data/.github/dependabot.yml +0 -6
- data/.github/workflows/ci.yml +0 -79
- data/.gitignore +0 -10
- data/.rspec +0 -0
- data/.travis.yml +0 -13
- data/.yardopts +0 -1
- data/Gemfile +0 -18
- data/Rakefile +0 -31
- data/ext/patron/.gitignore +0 -4
- data/ext/patron/membuffer.c +0 -85
- data/ext/patron/membuffer.h +0 -78
- data/ext/patron/sglib.h +0 -1952
- data/lib/patron/proxy_type.rb +0 -11
- data/patron.gemspec +0 -39
- data/pic.png +0 -0
- data/script/console +0 -11
- data/script/generate_client_certs +0 -65
- data/script/test_server +0 -7
- data/spec/header_parser_spec.rb +0 -101
- data/spec/patron_spec.rb +0 -28
- data/spec/request_spec.rb +0 -89
- data/spec/response_spec.rb +0 -145
- data/spec/sample_response_headers/headers_wetransfer.txt +0 -18
- data/spec/sample_response_headers/headers_wetransfer_with_proxy_status.txt +0 -20
- data/spec/sample_response_headers/headers_wetransfer_with_redirect.txt +0 -24
- data/spec/sample_response_headers/headers_with_set_cookie.txt +0 -9
- data/spec/sample_response_headers/sample_http2_header.txt +0 -16
- data/spec/sample_response_headers/webmock_headers_without_trailing_crlf.txt +0 -15
- data/spec/session_spec.rb +0 -865
- data/spec/session_ssl_spec.rb +0 -431
- data/spec/spec_helper.rb +0 -40
- data/spec/support/certs/cacert.pem +0 -36
- data/spec/support/certs/client.p12 +0 -0
- data/spec/support/certs/client_ca.pem +0 -20
- data/spec/support/certs/client_cert.pem +0 -20
- data/spec/support/certs/client_key.pem +0 -30
- data/spec/support/certs/privkey.pem +0 -51
- data/spec/support/certs/rogue_client.p12 +0 -0
- data/spec/support/config.ru +0 -153
- data/spec/support/fork.rb +0 -70
- data/spec/support/raw_request_capture.rb +0 -93
- data/spec/support/test_server.rb +0 -29
- data/spec/support/valgrind.supp +0 -2
- 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
|
|
9
|
-
|
|
10
|
-
MISREPORTED_ENCODING_ERROR =
|
|
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 =
|
|
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 =
|
|
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
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
raise HeaderCharsetInvalid,
|
|
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(
|
|
49
|
+
convert_encoding_and_raise(tagged)
|
|
49
50
|
else
|
|
50
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
77
|
-
#
|
|
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
|
-
#
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
90
|
+
str
|
|
98
91
|
end
|
|
99
92
|
end
|
|
100
|
-
|
|
101
|
-
private_constant :ResponseDecoding
|
|
93
|
+
|
|
94
|
+
private_constant :ResponseDecoding
|
|
102
95
|
end
|
data/lib/patron/session.rb
CHANGED
|
@@ -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 [
|
|
25
|
+
# @return [Numeric] HTTP connection timeout in seconds. Defaults to 1 second.
|
|
17
26
|
attr_accessor :connect_timeout
|
|
18
27
|
|
|
19
|
-
# @return [
|
|
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 `:
|
|
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
|
|
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",
|
|
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 [
|
|
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
|
|
137
|
+
# @param options [Hash] the writable attributes of the Session, by name
|
|
127
138
|
# @yield self
|
|
128
|
-
def initialize(
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
181
|
+
# Enable libcurl's verbose output.
|
|
178
182
|
#
|
|
179
|
-
# @
|
|
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(
|
|
183
|
-
|
|
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, :
|
|
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, :
|
|
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, :
|
|
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, :
|
|
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
|
|
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, :
|
|
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, :
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
351
|
-
# as
|
|
357
|
+
# a class that supports the same keyword arguments for `new`
|
|
358
|
+
# as Patron::Response.
|
|
352
359
|
#
|
|
353
|
-
# @return [#new]
|
|
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, "
|
|
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
|
|
369
|
-
#
|
|
370
|
-
headers
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
data/lib/patron/version.rb
CHANGED
data/lib/patron.rb
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
|
-
|
|
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
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|