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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +97 -0
- data/README.md +113 -50
- data/ext/patron/extconf.rb +55 -20
- data/ext/patron/session_ext.c +1092 -761
- data/lib/patron/compat.rb +57 -0
- data/lib/patron/error.rb +25 -13
- data/lib/patron/header_parser.rb +27 -25
- data/lib/patron/request.rb +98 -142
- data/lib/patron/response.rb +33 -24
- data/lib/patron/response_decoding.rb +60 -67
- data/lib/patron/session.rb +127 -104
- data/lib/patron/util.rb +14 -12
- data/lib/patron/version.rb +3 -1
- data/lib/patron.rb +9 -15
- metadata +12 -49
- data/.autotest +0 -15
- data/.github/workflows/ci.yml +0 -26
- data/.gitignore +0 -10
- data/.rspec +0 -0
- data/.travis.yml +0 -13
- data/.yardopts +0 -1
- data/Gemfile +0 -17
- 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 -38
- data/pic.png +0 -0
- data/script/console +0 -11
- 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 -77
- 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 -698
- data/spec/session_ssl_spec.rb +0 -313
- data/spec/spec_helper.rb +0 -34
- data/spec/support/certs/cacert.pem +0 -36
- data/spec/support/certs/privkey.pem +0 -51
- data/spec/support/config.ru +0 -146
- data/spec/support/fork.rb +0 -70
- data/spec/support/test_server.rb +0 -26
- 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,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
|
|
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
|
|
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 [
|
|
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
|
|
137
|
+
# @param options [Hash] the writable attributes of the Session, by name
|
|
114
138
|
# @yield self
|
|
115
|
-
def initialize(
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
181
|
+
# Enable libcurl's verbose output.
|
|
165
182
|
#
|
|
166
|
-
# @
|
|
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(
|
|
170
|
-
|
|
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, :
|
|
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, :
|
|
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, :
|
|
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, :
|
|
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
|
|
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, :
|
|
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, :
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
338
|
-
# as
|
|
357
|
+
# a class that supports the same keyword arguments for `new`
|
|
358
|
+
# as Patron::Response.
|
|
339
359
|
#
|
|
340
|
-
# @return [#new]
|
|
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, "
|
|
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
|
|
356
|
-
#
|
|
357
|
-
headers
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
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
|