rest-client 1.8.0 → 2.1.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 +5 -5
- data/.gitignore +2 -0
- data/.mailmap +10 -0
- data/.rspec +2 -1
- data/.rubocop +2 -0
- data/.rubocop-disables.yml +386 -0
- data/.rubocop.yml +8 -0
- data/.travis.yml +56 -8
- data/AUTHORS +26 -1
- data/README.md +901 -0
- data/Rakefile +27 -3
- data/bin/restclient +3 -5
- data/history.md +181 -0
- data/lib/restclient/abstract_response.rb +172 -55
- data/lib/restclient/exceptions.rb +96 -55
- data/lib/restclient/params_array.rb +72 -0
- data/lib/restclient/payload.rb +70 -74
- data/lib/restclient/platform.rb +19 -0
- data/lib/restclient/raw_response.rb +21 -7
- data/lib/restclient/request.rb +540 -281
- data/lib/restclient/resource.rb +19 -9
- data/lib/restclient/response.rb +75 -6
- data/lib/restclient/utils.rb +274 -0
- data/lib/restclient/version.rb +2 -1
- data/lib/restclient.rb +21 -3
- data/rest-client.gemspec +12 -10
- data/spec/ISS.jpg +0 -0
- data/spec/helpers.rb +54 -0
- data/spec/integration/_lib.rb +1 -0
- data/spec/integration/capath_digicert/3513523f.0 +22 -0
- data/spec/integration/capath_digicert/399e7759.0 +22 -0
- data/spec/integration/capath_digicert/digicert.crt +20 -17
- data/spec/integration/certs/digicert.crt +20 -17
- data/spec/integration/httpbin_spec.rb +128 -0
- data/spec/integration/integration_spec.rb +97 -14
- data/spec/integration/request_spec.rb +25 -2
- data/spec/spec_helper.rb +28 -1
- data/spec/unit/_lib.rb +1 -0
- data/spec/unit/abstract_response_spec.rb +95 -38
- data/spec/unit/exceptions_spec.rb +41 -28
- data/spec/unit/params_array_spec.rb +36 -0
- data/spec/unit/payload_spec.rb +118 -68
- data/spec/unit/raw_response_spec.rb +10 -6
- data/spec/unit/request2_spec.rb +34 -12
- data/spec/unit/request_spec.rb +745 -424
- data/spec/unit/resource_spec.rb +31 -27
- data/spec/unit/response_spec.rb +134 -57
- data/spec/unit/restclient_spec.rb +16 -15
- data/spec/unit/utils_spec.rb +147 -0
- data/spec/unit/windows/root_certs_spec.rb +3 -3
- metadata +79 -29
- data/README.rdoc +0 -324
- data/spec/integration/capath_digicert/244b5494.0 +0 -19
- data/spec/integration/capath_digicert/81b9768f.0 +0 -19
- data/spec/unit/master_shake.jpg +0 -0
data/lib/restclient/request.rb
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
require 'tempfile'
|
2
|
-
require 'mime/types'
|
3
2
|
require 'cgi'
|
4
3
|
require 'netrc'
|
5
4
|
require 'set'
|
6
5
|
|
6
|
+
begin
|
7
|
+
# Use mime/types/columnar if available, for reduced memory usage
|
8
|
+
require 'mime/types/columnar'
|
9
|
+
rescue LoadError
|
10
|
+
require 'mime/types'
|
11
|
+
end
|
12
|
+
|
7
13
|
module RestClient
|
8
14
|
# This class is used internally by RestClient to send the request, but you can also
|
9
15
|
# call it directly if you'd like to use a method not supported by the
|
@@ -16,106 +22,80 @@ module RestClient
|
|
16
22
|
# * :url
|
17
23
|
# Optional parameters (have a look at ssl and/or uri for some explanations):
|
18
24
|
# * :headers a hash containing the request headers
|
19
|
-
# * :cookies
|
25
|
+
# * :cookies may be a Hash{String/Symbol => String} of cookie values, an
|
26
|
+
# Array<HTTP::Cookie>, or an HTTP::CookieJar containing cookies. These
|
27
|
+
# will be added to a cookie jar before the request is sent.
|
20
28
|
# * :user and :password for basic auth, will be replaced by a user/password available in the :url
|
21
29
|
# * :block_response call the provided block with the HTTPResponse as parameter
|
22
30
|
# * :raw_response return a low-level RawResponse instead of a Response
|
31
|
+
# * :log Set the log for this request only, overriding RestClient.log, if
|
32
|
+
# any.
|
33
|
+
# * :stream_log_percent (Only relevant with :raw_response => true) Customize
|
34
|
+
# the interval at which download progress is logged. Defaults to every
|
35
|
+
# 10% complete.
|
23
36
|
# * :max_redirects maximum number of redirections (default to 10)
|
37
|
+
# * :proxy An HTTP proxy URI to use for this request. Any value here
|
38
|
+
# (including nil) will override RestClient.proxy.
|
24
39
|
# * :verify_ssl enable ssl verification, possible values are constants from
|
25
40
|
# OpenSSL::SSL::VERIFY_*, defaults to OpenSSL::SSL::VERIFY_PEER
|
26
|
-
# * :
|
27
|
-
# open a connection, in seconds. Pass nil to disable the timeout.
|
41
|
+
# * :read_timeout and :open_timeout are how long to wait for a response and
|
42
|
+
# to open a connection, in seconds. Pass nil to disable the timeout.
|
43
|
+
# * :timeout can be used to set both timeouts
|
28
44
|
# * :ssl_client_cert, :ssl_client_key, :ssl_ca_file, :ssl_ca_path,
|
29
45
|
# :ssl_cert_store, :ssl_verify_callback, :ssl_verify_callback_warnings
|
30
46
|
# * :ssl_version specifies the SSL version for the underlying Net::HTTP connection
|
31
47
|
# * :ssl_ciphers sets SSL ciphers for the connection. See
|
32
48
|
# OpenSSL::SSL::SSLContext#ciphers=
|
49
|
+
# * :before_execution_proc a Proc to call before executing the request. This
|
50
|
+
# proc, like procs from RestClient.before_execution_procs, will be
|
51
|
+
# called with the HTTP request and request params.
|
33
52
|
class Request
|
34
53
|
|
35
|
-
attr_reader :method, :url, :headers, :
|
36
|
-
:
|
54
|
+
attr_reader :method, :uri, :url, :headers, :payload, :proxy,
|
55
|
+
:user, :password, :read_timeout, :max_redirects,
|
37
56
|
:open_timeout, :raw_response, :processed_headers, :args,
|
38
57
|
:ssl_opts
|
39
58
|
|
59
|
+
# An array of previous redirection responses
|
60
|
+
attr_accessor :redirection_history
|
61
|
+
|
40
62
|
def self.execute(args, & block)
|
41
63
|
new(args).execute(& block)
|
42
64
|
end
|
43
65
|
|
44
|
-
# This is similar to the list now in ruby core, but adds HIGH and RC4-MD5
|
45
|
-
# for better compatibility (similar to Firefox) and moves AES-GCM cipher
|
46
|
-
# suites above DHE/ECDHE CBC suites (similar to Chromium).
|
47
|
-
# https://github.com/ruby/ruby/commit/699b209cf8cf11809620e12985ad33ae33b119ee
|
48
|
-
#
|
49
|
-
# This list will be used by default if the Ruby global OpenSSL default
|
50
|
-
# ciphers appear to be a weak list.
|
51
|
-
DefaultCiphers = %w{
|
52
|
-
!aNULL
|
53
|
-
!eNULL
|
54
|
-
!EXPORT
|
55
|
-
!SSLV2
|
56
|
-
!LOW
|
57
|
-
|
58
|
-
ECDHE-ECDSA-AES128-GCM-SHA256
|
59
|
-
ECDHE-RSA-AES128-GCM-SHA256
|
60
|
-
ECDHE-ECDSA-AES256-GCM-SHA384
|
61
|
-
ECDHE-RSA-AES256-GCM-SHA384
|
62
|
-
DHE-RSA-AES128-GCM-SHA256
|
63
|
-
DHE-DSS-AES128-GCM-SHA256
|
64
|
-
DHE-RSA-AES256-GCM-SHA384
|
65
|
-
DHE-DSS-AES256-GCM-SHA384
|
66
|
-
AES128-GCM-SHA256
|
67
|
-
AES256-GCM-SHA384
|
68
|
-
ECDHE-ECDSA-AES128-SHA256
|
69
|
-
ECDHE-RSA-AES128-SHA256
|
70
|
-
ECDHE-ECDSA-AES128-SHA
|
71
|
-
ECDHE-RSA-AES128-SHA
|
72
|
-
ECDHE-ECDSA-AES256-SHA384
|
73
|
-
ECDHE-RSA-AES256-SHA384
|
74
|
-
ECDHE-ECDSA-AES256-SHA
|
75
|
-
ECDHE-RSA-AES256-SHA
|
76
|
-
DHE-RSA-AES128-SHA256
|
77
|
-
DHE-RSA-AES256-SHA256
|
78
|
-
DHE-RSA-AES128-SHA
|
79
|
-
DHE-RSA-AES256-SHA
|
80
|
-
DHE-DSS-AES128-SHA256
|
81
|
-
DHE-DSS-AES256-SHA256
|
82
|
-
DHE-DSS-AES128-SHA
|
83
|
-
DHE-DSS-AES256-SHA
|
84
|
-
AES128-SHA256
|
85
|
-
AES256-SHA256
|
86
|
-
AES128-SHA
|
87
|
-
AES256-SHA
|
88
|
-
ECDHE-ECDSA-RC4-SHA
|
89
|
-
ECDHE-RSA-RC4-SHA
|
90
|
-
RC4-SHA
|
91
|
-
|
92
|
-
HIGH
|
93
|
-
+RC4
|
94
|
-
RC4-MD5
|
95
|
-
}.join(":")
|
96
|
-
|
97
|
-
# A set of weak default ciphers that we will override by default.
|
98
|
-
WeakDefaultCiphers = Set.new([
|
99
|
-
"ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM:+LOW",
|
100
|
-
])
|
101
|
-
|
102
66
|
SSLOptionList = %w{client_cert client_key ca_file ca_path cert_store
|
103
67
|
version ciphers verify_callback verify_callback_warnings}
|
104
68
|
|
69
|
+
def inspect
|
70
|
+
"<RestClient::Request @method=#{@method.inspect}, @url=#{@url.inspect}>"
|
71
|
+
end
|
72
|
+
|
105
73
|
def initialize args
|
106
|
-
@method = args[:method]
|
107
|
-
@headers = args[:headers] || {}
|
74
|
+
@method = normalize_method(args[:method])
|
75
|
+
@headers = (args[:headers] || {}).dup
|
108
76
|
if args[:url]
|
109
|
-
@url = process_url_params(args[:url], headers)
|
77
|
+
@url = process_url_params(normalize_url(args[:url]), headers)
|
110
78
|
else
|
111
79
|
raise ArgumentError, "must pass :url"
|
112
80
|
end
|
113
|
-
|
81
|
+
|
82
|
+
@user = @password = nil
|
83
|
+
parse_url_with_auth!(url)
|
84
|
+
|
85
|
+
# process cookie arguments found in headers or args
|
86
|
+
@cookie_jar = process_cookie_args!(@uri, @headers, args)
|
87
|
+
|
114
88
|
@payload = Payload.generate(args[:payload])
|
115
|
-
|
116
|
-
@
|
89
|
+
|
90
|
+
@user = args[:user] if args.include?(:user)
|
91
|
+
@password = args[:password] if args.include?(:password)
|
92
|
+
|
117
93
|
if args.include?(:timeout)
|
118
|
-
@
|
94
|
+
@read_timeout = args[:timeout]
|
95
|
+
@open_timeout = args[:timeout]
|
96
|
+
end
|
97
|
+
if args.include?(:read_timeout)
|
98
|
+
@read_timeout = args[:read_timeout]
|
119
99
|
end
|
120
100
|
if args.include?(:open_timeout)
|
121
101
|
@open_timeout = args[:open_timeout]
|
@@ -123,6 +103,14 @@ module RestClient
|
|
123
103
|
@block_response = args[:block_response]
|
124
104
|
@raw_response = args[:raw_response] || false
|
125
105
|
|
106
|
+
@stream_log_percent = args[:stream_log_percent] || 10
|
107
|
+
if @stream_log_percent <= 0 || @stream_log_percent > 100
|
108
|
+
raise ArgumentError.new(
|
109
|
+
"Invalid :stream_log_percent #{@stream_log_percent.inspect}")
|
110
|
+
end
|
111
|
+
|
112
|
+
@proxy = args.fetch(:proxy) if args.include?(:proxy)
|
113
|
+
|
126
114
|
@ssl_opts = {}
|
127
115
|
|
128
116
|
if args.include?(:verify_ssl)
|
@@ -151,29 +139,28 @@ module RestClient
|
|
151
139
|
end
|
152
140
|
end
|
153
141
|
|
154
|
-
#
|
155
|
-
if
|
156
|
-
@ssl_opts[:cert_store] = self.class.default_ssl_cert_store
|
157
|
-
end
|
142
|
+
# Set some other default SSL options, but only if we have an HTTPS URI.
|
143
|
+
if use_ssl?
|
158
144
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
if WeakDefaultCiphers.include?(
|
163
|
-
OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.fetch(:ciphers))
|
164
|
-
@ssl_opts[:ciphers] = DefaultCiphers
|
145
|
+
# If there's no CA file, CA path, or cert store provided, use default
|
146
|
+
if !ssl_ca_file && !ssl_ca_path && !@ssl_opts.include?(:cert_store)
|
147
|
+
@ssl_opts[:cert_store] = self.class.default_ssl_cert_store
|
165
148
|
end
|
166
149
|
end
|
167
150
|
|
168
|
-
@
|
151
|
+
@log = args[:log]
|
169
152
|
@max_redirects = args[:max_redirects] || 10
|
170
153
|
@processed_headers = make_headers headers
|
154
|
+
@processed_headers_lowercase = Hash[@processed_headers.map {|k, v| [k.downcase, v]}]
|
171
155
|
@args = args
|
156
|
+
|
157
|
+
@before_execution_proc = args[:before_execution_proc]
|
172
158
|
end
|
173
159
|
|
174
160
|
def execute & block
|
175
|
-
|
176
|
-
|
161
|
+
# With 2.0.0+, net/http accepts URI objects in requests and handles wrapping
|
162
|
+
# IPv6 addresses in [] for use in the Host request header.
|
163
|
+
transmit uri, net_http_request_class(method).new(uri, processed_headers), payload, & block
|
177
164
|
ensure
|
178
165
|
payload.close if payload
|
179
166
|
end
|
@@ -188,82 +175,298 @@ module RestClient
|
|
188
175
|
end
|
189
176
|
end
|
190
177
|
|
178
|
+
# Return true if the request URI will use HTTPS.
|
179
|
+
#
|
180
|
+
# @return [Boolean]
|
181
|
+
#
|
182
|
+
def use_ssl?
|
183
|
+
uri.is_a?(URI::HTTPS)
|
184
|
+
end
|
185
|
+
|
191
186
|
# Extract the query parameters and append them to the url
|
192
|
-
|
193
|
-
|
187
|
+
#
|
188
|
+
# Look through the headers hash for a :params option (case-insensitive,
|
189
|
+
# may be string or symbol). If present and the value is a Hash or
|
190
|
+
# RestClient::ParamsArray, *delete* the key/value pair from the headers
|
191
|
+
# hash and encode the value into a query string. Append this query string
|
192
|
+
# to the URL and return the resulting URL.
|
193
|
+
#
|
194
|
+
# @param [String] url
|
195
|
+
# @param [Hash] headers An options/headers hash to process. Mutation
|
196
|
+
# warning: the params key may be removed if present!
|
197
|
+
#
|
198
|
+
# @return [String] resulting url with query string
|
199
|
+
#
|
200
|
+
def process_url_params(url, headers)
|
201
|
+
url_params = nil
|
202
|
+
|
203
|
+
# find and extract/remove "params" key if the value is a Hash/ParamsArray
|
194
204
|
headers.delete_if do |key, value|
|
195
|
-
if
|
196
|
-
|
205
|
+
if key.to_s.downcase == 'params' &&
|
206
|
+
(value.is_a?(Hash) || value.is_a?(RestClient::ParamsArray))
|
207
|
+
if url_params
|
208
|
+
raise ArgumentError.new("Multiple 'params' options passed")
|
209
|
+
end
|
210
|
+
url_params = value
|
197
211
|
true
|
198
212
|
else
|
199
213
|
false
|
200
214
|
end
|
201
215
|
end
|
202
|
-
|
203
|
-
|
204
|
-
|
216
|
+
|
217
|
+
# build resulting URL with query string
|
218
|
+
if url_params && !url_params.empty?
|
219
|
+
query_string = RestClient::Utils.encode_query_string(url_params)
|
220
|
+
|
221
|
+
if url.include?('?')
|
222
|
+
url + '&' + query_string
|
223
|
+
else
|
224
|
+
url + '?' + query_string
|
225
|
+
end
|
205
226
|
else
|
206
227
|
url
|
207
228
|
end
|
208
229
|
end
|
209
230
|
|
210
|
-
|
211
|
-
|
231
|
+
# Render a hash of key => value pairs for cookies in the Request#cookie_jar
|
232
|
+
# that are valid for the Request#uri. This will not necessarily include all
|
233
|
+
# cookies if there are duplicate keys. It's safer to use the cookie_jar
|
234
|
+
# directly if that's a concern.
|
235
|
+
#
|
236
|
+
# @see Request#cookie_jar
|
237
|
+
#
|
238
|
+
# @return [Hash]
|
239
|
+
#
|
240
|
+
def cookies
|
241
|
+
hash = {}
|
212
242
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
243
|
+
@cookie_jar.cookies(uri).each do |c|
|
244
|
+
hash[c.name] = c.value
|
245
|
+
end
|
246
|
+
|
247
|
+
hash
|
248
|
+
end
|
249
|
+
|
250
|
+
# @return [HTTP::CookieJar]
|
251
|
+
def cookie_jar
|
252
|
+
@cookie_jar
|
253
|
+
end
|
254
|
+
|
255
|
+
# Render a Cookie HTTP request header from the contents of the @cookie_jar,
|
256
|
+
# or nil if the jar is empty.
|
257
|
+
#
|
258
|
+
# @see Request#cookie_jar
|
259
|
+
#
|
260
|
+
# @return [String, nil]
|
261
|
+
#
|
262
|
+
def make_cookie_header
|
263
|
+
return nil if cookie_jar.nil?
|
264
|
+
|
265
|
+
arr = cookie_jar.cookies(url)
|
266
|
+
return nil if arr.empty?
|
267
|
+
|
268
|
+
return HTTP::Cookie.cookie_value(arr)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Process cookies passed as hash or as HTTP::CookieJar. For backwards
|
272
|
+
# compatibility, these may be passed as a :cookies option masquerading
|
273
|
+
# inside the headers hash. To avoid confusion, if :cookies is passed in
|
274
|
+
# both headers and Request#initialize, raise an error.
|
275
|
+
#
|
276
|
+
# :cookies may be a:
|
277
|
+
# - Hash{String/Symbol => String}
|
278
|
+
# - Array<HTTP::Cookie>
|
279
|
+
# - HTTP::CookieJar
|
280
|
+
#
|
281
|
+
# Passing as a hash:
|
282
|
+
# Keys may be symbols or strings. Values must be strings.
|
283
|
+
# Infer the domain name from the request URI and allow subdomains (as
|
284
|
+
# though '.example.com' had been set in a Set-Cookie header). Assume a
|
285
|
+
# path of '/'.
|
286
|
+
#
|
287
|
+
# RestClient::Request.new(url: 'http://example.com', method: :get,
|
288
|
+
# :cookies => {:foo => 'Value', 'bar' => '123'}
|
289
|
+
# )
|
290
|
+
#
|
291
|
+
# results in cookies as though set from the server by:
|
292
|
+
# Set-Cookie: foo=Value; Domain=.example.com; Path=/
|
293
|
+
# Set-Cookie: bar=123; Domain=.example.com; Path=/
|
294
|
+
#
|
295
|
+
# which yields a client cookie header of:
|
296
|
+
# Cookie: foo=Value; bar=123
|
297
|
+
#
|
298
|
+
# Passing as HTTP::CookieJar, which will be passed through directly:
|
299
|
+
#
|
300
|
+
# jar = HTTP::CookieJar.new
|
301
|
+
# jar.add(HTTP::Cookie.new('foo', 'Value', domain: 'example.com',
|
302
|
+
# path: '/', for_domain: false))
|
303
|
+
#
|
304
|
+
# RestClient::Request.new(..., :cookies => jar)
|
305
|
+
#
|
306
|
+
# @param [URI::HTTP] uri The URI for the request. This will be used to
|
307
|
+
# infer the domain name for cookies passed as strings in a hash. To avoid
|
308
|
+
# this implicit behavior, pass a full cookie jar or use HTTP::Cookie hash
|
309
|
+
# values.
|
310
|
+
# @param [Hash] headers The headers hash from which to pull the :cookies
|
311
|
+
# option. MUTATION NOTE: This key will be deleted from the hash if
|
312
|
+
# present.
|
313
|
+
# @param [Hash] args The options passed to Request#initialize. This hash
|
314
|
+
# will be used as another potential source for the :cookies key.
|
315
|
+
# These args will not be mutated.
|
316
|
+
#
|
317
|
+
# @return [HTTP::CookieJar] A cookie jar containing the parsed cookies.
|
318
|
+
#
|
319
|
+
def process_cookie_args!(uri, headers, args)
|
320
|
+
|
321
|
+
# Avoid ambiguity in whether options from headers or options from
|
322
|
+
# Request#initialize should take precedence by raising ArgumentError when
|
323
|
+
# both are present. Prior versions of rest-client claimed to give
|
324
|
+
# precedence to init options, but actually gave precedence to headers.
|
325
|
+
# Avoid that mess by erroring out instead.
|
326
|
+
if headers[:cookies] && args[:cookies]
|
327
|
+
raise ArgumentError.new(
|
328
|
+
"Cannot pass :cookies in Request.new() and in headers hash")
|
329
|
+
end
|
330
|
+
|
331
|
+
cookies_data = headers.delete(:cookies) || args[:cookies]
|
332
|
+
|
333
|
+
# return copy of cookie jar as is
|
334
|
+
if cookies_data.is_a?(HTTP::CookieJar)
|
335
|
+
return cookies_data.dup
|
336
|
+
end
|
337
|
+
|
338
|
+
# convert cookies hash into a CookieJar
|
339
|
+
jar = HTTP::CookieJar.new
|
340
|
+
|
341
|
+
(cookies_data || []).each do |key, val|
|
342
|
+
|
343
|
+
# Support for Array<HTTP::Cookie> mode:
|
344
|
+
# If key is a cookie object, add it to the jar directly and assert that
|
345
|
+
# there is no separate val.
|
346
|
+
if key.is_a?(HTTP::Cookie)
|
347
|
+
if val
|
348
|
+
raise ArgumentError.new("extra cookie val: #{val.inspect}")
|
222
349
|
end
|
350
|
+
|
351
|
+
jar.add(key)
|
352
|
+
next
|
353
|
+
end
|
354
|
+
|
355
|
+
if key.is_a?(Symbol)
|
356
|
+
key = key.to_s
|
223
357
|
end
|
224
358
|
|
225
|
-
|
359
|
+
# assume implicit domain from the request URI, and set for_domain to
|
360
|
+
# permit subdomains
|
361
|
+
jar.add(HTTP::Cookie.new(key, val, domain: uri.hostname.downcase,
|
362
|
+
path: '/', for_domain: true))
|
226
363
|
end
|
227
|
-
|
228
|
-
|
229
|
-
headers
|
364
|
+
|
365
|
+
jar
|
230
366
|
end
|
231
367
|
|
232
|
-
#
|
368
|
+
# Generate headers for use by a request. Header keys will be stringified
|
369
|
+
# using `#stringify_headers` to normalize them as capitalized strings.
|
370
|
+
#
|
371
|
+
# The final headers consist of:
|
372
|
+
# - default headers from #default_headers
|
373
|
+
# - user_headers provided here
|
374
|
+
# - headers from the payload object (e.g. Content-Type, Content-Lenth)
|
375
|
+
# - cookie headers from #make_cookie_header
|
233
376
|
#
|
234
|
-
#
|
235
|
-
#
|
377
|
+
# BUG: stringify_headers does not alter the capitalization of headers that
|
378
|
+
# are passed as strings, it only normalizes those passed as symbols. This
|
379
|
+
# behavior will probably remain for a while for compatibility, but it means
|
380
|
+
# that the warnings that attempt to detect accidental header overrides may
|
381
|
+
# not always work.
|
382
|
+
# https://github.com/rest-client/rest-client/issues/599
|
236
383
|
#
|
237
|
-
#
|
238
|
-
# equals sign, semicolon, comma, or space.
|
384
|
+
# @param [Hash] user_headers User-provided headers to include
|
239
385
|
#
|
240
|
-
|
241
|
-
|
386
|
+
# @return [Hash<String, String>] A hash of HTTP headers => values
|
387
|
+
#
|
388
|
+
def make_headers(user_headers)
|
389
|
+
headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
|
242
390
|
|
243
|
-
|
391
|
+
# override headers from the payload (e.g. Content-Type, Content-Length)
|
392
|
+
if @payload
|
393
|
+
payload_headers = @payload.headers
|
394
|
+
|
395
|
+
# Warn the user if we override any headers that were previously
|
396
|
+
# present. This usually indicates that rest-client was passed
|
397
|
+
# conflicting information, e.g. if it was asked to render a payload as
|
398
|
+
# x-www-form-urlencoded but a Content-Type application/json was
|
399
|
+
# also supplied by the user.
|
400
|
+
payload_headers.each_pair do |key, val|
|
401
|
+
if headers.include?(key) && headers[key] != val
|
402
|
+
warn("warning: Overriding #{key.inspect} header " +
|
403
|
+
"#{headers.fetch(key).inspect} with #{val.inspect} " +
|
404
|
+
"due to payload")
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
headers.merge!(payload_headers)
|
409
|
+
end
|
410
|
+
|
411
|
+
# merge in cookies
|
412
|
+
cookies = make_cookie_header
|
413
|
+
if cookies && !cookies.empty?
|
414
|
+
if headers['Cookie']
|
415
|
+
warn('warning: overriding "Cookie" header with :cookies option')
|
416
|
+
end
|
417
|
+
headers['Cookie'] = cookies
|
418
|
+
end
|
419
|
+
|
420
|
+
headers
|
244
421
|
end
|
245
422
|
|
246
|
-
#
|
247
|
-
#
|
248
|
-
|
249
|
-
|
423
|
+
# The proxy URI for this request. If `:proxy` was provided on this request,
|
424
|
+
# use it over `RestClient.proxy`.
|
425
|
+
#
|
426
|
+
# Return false if a proxy was explicitly set and is falsy.
|
427
|
+
#
|
428
|
+
# @return [URI, false, nil]
|
429
|
+
#
|
430
|
+
def proxy_uri
|
431
|
+
if defined?(@proxy)
|
432
|
+
if @proxy
|
433
|
+
URI.parse(@proxy)
|
434
|
+
else
|
435
|
+
false
|
436
|
+
end
|
437
|
+
elsif RestClient.proxy_set?
|
438
|
+
if RestClient.proxy
|
439
|
+
URI.parse(RestClient.proxy)
|
440
|
+
else
|
441
|
+
false
|
442
|
+
end
|
443
|
+
else
|
444
|
+
nil
|
445
|
+
end
|
250
446
|
end
|
251
447
|
|
252
|
-
def
|
253
|
-
|
254
|
-
|
255
|
-
|
448
|
+
def net_http_object(hostname, port)
|
449
|
+
p_uri = proxy_uri
|
450
|
+
|
451
|
+
if p_uri.nil?
|
452
|
+
# no proxy set
|
453
|
+
Net::HTTP.new(hostname, port)
|
454
|
+
elsif !p_uri
|
455
|
+
# proxy explicitly set to none
|
456
|
+
Net::HTTP.new(hostname, port, nil, nil, nil, nil)
|
256
457
|
else
|
257
|
-
Net::HTTP
|
458
|
+
Net::HTTP.new(hostname, port,
|
459
|
+
p_uri.hostname, p_uri.port, p_uri.user, p_uri.password)
|
460
|
+
|
258
461
|
end
|
259
462
|
end
|
260
463
|
|
261
464
|
def net_http_request_class(method)
|
262
|
-
Net::HTTP.const_get(method.
|
465
|
+
Net::HTTP.const_get(method.capitalize, false)
|
263
466
|
end
|
264
467
|
|
265
468
|
def net_http_do_request(http, req, body=nil, &block)
|
266
|
-
if body
|
469
|
+
if body && body.respond_to?(:read)
|
267
470
|
req.body_stream = body
|
268
471
|
return http.request(req, nil, &block)
|
269
472
|
else
|
@@ -271,36 +474,19 @@ module RestClient
|
|
271
474
|
end
|
272
475
|
end
|
273
476
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
end
|
288
|
-
|
289
|
-
def process_payload(p=nil, parent_key=nil)
|
290
|
-
unless p.is_a?(Hash)
|
291
|
-
p
|
292
|
-
else
|
293
|
-
@headers[:content_type] ||= 'application/x-www-form-urlencoded'
|
294
|
-
p.keys.map do |k|
|
295
|
-
key = parent_key ? "#{parent_key}[#{k}]" : k
|
296
|
-
if p[k].is_a? Hash
|
297
|
-
process_payload(p[k], key)
|
298
|
-
else
|
299
|
-
value = parser.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
300
|
-
"#{key}=#{value}"
|
301
|
-
end
|
302
|
-
end.join("&")
|
303
|
-
end
|
477
|
+
# Normalize a URL by adding a protocol if none is present.
|
478
|
+
#
|
479
|
+
# If the string has no HTTP-like scheme (i.e. scheme followed by '//'), a
|
480
|
+
# scheme of 'http' will be added. This mimics the behavior of browsers and
|
481
|
+
# user agents like cURL.
|
482
|
+
#
|
483
|
+
# @param [String] url A URL string.
|
484
|
+
#
|
485
|
+
# @return [String]
|
486
|
+
#
|
487
|
+
def normalize_url(url)
|
488
|
+
url = 'http://' + url unless url.match(%r{\A[a-z][a-z0-9+.-]*://}i)
|
489
|
+
url
|
304
490
|
end
|
305
491
|
|
306
492
|
# Return a certificate store that can be used to validate certificates with
|
@@ -332,6 +518,106 @@ module RestClient
|
|
332
518
|
cert_store
|
333
519
|
end
|
334
520
|
|
521
|
+
def redacted_uri
|
522
|
+
if uri.password
|
523
|
+
sanitized_uri = uri.dup
|
524
|
+
sanitized_uri.password = 'REDACTED'
|
525
|
+
sanitized_uri
|
526
|
+
else
|
527
|
+
uri
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
def redacted_url
|
532
|
+
redacted_uri.to_s
|
533
|
+
end
|
534
|
+
|
535
|
+
# Default to the global logger if there's not a request-specific one
|
536
|
+
def log
|
537
|
+
@log || RestClient.log
|
538
|
+
end
|
539
|
+
|
540
|
+
def log_request
|
541
|
+
return unless log
|
542
|
+
|
543
|
+
out = []
|
544
|
+
|
545
|
+
out << "RestClient.#{method} #{redacted_url.inspect}"
|
546
|
+
out << payload.short_inspect if payload
|
547
|
+
out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
|
548
|
+
log << out.join(', ') + "\n"
|
549
|
+
end
|
550
|
+
|
551
|
+
# Return a hash of headers whose keys are capitalized strings
|
552
|
+
#
|
553
|
+
# BUG: stringify_headers does not fix the capitalization of headers that
|
554
|
+
# are already Strings. Leaving this behavior as is for now for
|
555
|
+
# backwards compatibility.
|
556
|
+
# https://github.com/rest-client/rest-client/issues/599
|
557
|
+
#
|
558
|
+
def stringify_headers headers
|
559
|
+
headers.inject({}) do |result, (key, value)|
|
560
|
+
if key.is_a? Symbol
|
561
|
+
key = key.to_s.split(/_/).map(&:capitalize).join('-')
|
562
|
+
end
|
563
|
+
if 'CONTENT-TYPE' == key.upcase
|
564
|
+
result[key] = maybe_convert_extension(value.to_s)
|
565
|
+
elsif 'ACCEPT' == key.upcase
|
566
|
+
# Accept can be composed of several comma-separated values
|
567
|
+
if value.is_a? Array
|
568
|
+
target_values = value
|
569
|
+
else
|
570
|
+
target_values = value.to_s.split ','
|
571
|
+
end
|
572
|
+
result[key] = target_values.map { |ext|
|
573
|
+
maybe_convert_extension(ext.to_s.strip)
|
574
|
+
}.join(', ')
|
575
|
+
else
|
576
|
+
result[key] = value.to_s
|
577
|
+
end
|
578
|
+
result
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
# Default headers set by RestClient. In addition to these headers, servers
|
583
|
+
# will receive headers set by Net::HTTP, such as Accept-Encoding and Host.
|
584
|
+
#
|
585
|
+
# @return [Hash<Symbol, String>]
|
586
|
+
def default_headers
|
587
|
+
{
|
588
|
+
:accept => '*/*',
|
589
|
+
:user_agent => RestClient::Platform.default_user_agent,
|
590
|
+
}
|
591
|
+
end
|
592
|
+
|
593
|
+
private
|
594
|
+
|
595
|
+
# Parse the `@url` string into a URI object and save it as
|
596
|
+
# `@uri`. Also save any basic auth user or password as @user and @password.
|
597
|
+
# If no auth info was passed, check for credentials in a Netrc file.
|
598
|
+
#
|
599
|
+
# @param [String] url A URL string.
|
600
|
+
#
|
601
|
+
# @return [URI]
|
602
|
+
#
|
603
|
+
# @raise URI::InvalidURIError on invalid URIs
|
604
|
+
#
|
605
|
+
def parse_url_with_auth!(url)
|
606
|
+
uri = URI.parse(url)
|
607
|
+
|
608
|
+
if uri.hostname.nil?
|
609
|
+
raise URI::InvalidURIError.new("bad URI(no host provided): #{url}")
|
610
|
+
end
|
611
|
+
|
612
|
+
@user = CGI.unescape(uri.user) if uri.user
|
613
|
+
@password = CGI.unescape(uri.password) if uri.password
|
614
|
+
if !@user && !@password
|
615
|
+
@user, @password = Netrc.read[uri.hostname]
|
616
|
+
end
|
617
|
+
|
618
|
+
@uri = uri
|
619
|
+
end
|
620
|
+
|
335
621
|
def print_verify_callback_warnings
|
336
622
|
warned = false
|
337
623
|
if RestClient::Platform.mac_mri?
|
@@ -346,10 +632,32 @@ module RestClient
|
|
346
632
|
warned
|
347
633
|
end
|
348
634
|
|
635
|
+
# Parse a method and return a normalized string version.
|
636
|
+
#
|
637
|
+
# Raise ArgumentError if the method is falsy, but otherwise do no
|
638
|
+
# validation.
|
639
|
+
#
|
640
|
+
# @param method [String, Symbol]
|
641
|
+
#
|
642
|
+
# @return [String]
|
643
|
+
#
|
644
|
+
# @see net_http_request_class
|
645
|
+
#
|
646
|
+
def normalize_method(method)
|
647
|
+
raise ArgumentError.new('must pass :method') unless method
|
648
|
+
method.to_s.downcase
|
649
|
+
end
|
650
|
+
|
349
651
|
def transmit uri, req, payload, & block
|
652
|
+
|
653
|
+
# We set this to true in the net/http block so that we can distinguish
|
654
|
+
# read_timeout from open_timeout. Now that we only support Ruby 2.0+,
|
655
|
+
# this is only needed for Timeout exceptions thrown outside of Net::HTTP.
|
656
|
+
established_connection = false
|
657
|
+
|
350
658
|
setup_credentials req
|
351
659
|
|
352
|
-
net =
|
660
|
+
net = net_http_object(uri.hostname, uri.port)
|
353
661
|
net.use_ssl = uri.is_a?(URI::HTTPS)
|
354
662
|
net.ssl_version = ssl_version if ssl_version
|
355
663
|
net.ciphers = ssl_ciphers if ssl_ciphers
|
@@ -388,16 +696,16 @@ module RestClient
|
|
388
696
|
warn('Try passing :verify_ssl => false instead.')
|
389
697
|
end
|
390
698
|
|
391
|
-
if defined? @
|
392
|
-
if @
|
393
|
-
warn '
|
394
|
-
@
|
699
|
+
if defined? @read_timeout
|
700
|
+
if @read_timeout == -1
|
701
|
+
warn 'Deprecated: to disable timeouts, please use nil instead of -1'
|
702
|
+
@read_timeout = nil
|
395
703
|
end
|
396
|
-
net.read_timeout = @
|
704
|
+
net.read_timeout = @read_timeout
|
397
705
|
end
|
398
706
|
if defined? @open_timeout
|
399
707
|
if @open_timeout == -1
|
400
|
-
warn '
|
708
|
+
warn 'Deprecated: to disable timeouts, please use nil instead of -1'
|
401
709
|
@open_timeout = nil
|
402
710
|
end
|
403
711
|
net.open_timeout = @open_timeout
|
@@ -407,24 +715,47 @@ module RestClient
|
|
407
715
|
before_proc.call(req, args)
|
408
716
|
end
|
409
717
|
|
718
|
+
if @before_execution_proc
|
719
|
+
@before_execution_proc.call(req, args)
|
720
|
+
end
|
721
|
+
|
410
722
|
log_request
|
411
723
|
|
724
|
+
start_time = Time.now
|
725
|
+
tempfile = nil
|
412
726
|
|
413
727
|
net.start do |http|
|
728
|
+
established_connection = true
|
729
|
+
|
414
730
|
if @block_response
|
415
|
-
net_http_do_request(http, req, payload
|
416
|
-
&@block_response)
|
731
|
+
net_http_do_request(http, req, payload, &@block_response)
|
417
732
|
else
|
418
|
-
res = net_http_do_request(http, req, payload
|
419
|
-
|
420
|
-
|
421
|
-
|
733
|
+
res = net_http_do_request(http, req, payload) { |http_response|
|
734
|
+
if @raw_response
|
735
|
+
# fetch body into tempfile
|
736
|
+
tempfile = fetch_body_to_tempfile(http_response)
|
737
|
+
else
|
738
|
+
# fetch body
|
739
|
+
http_response.read_body
|
740
|
+
end
|
741
|
+
http_response
|
742
|
+
}
|
743
|
+
process_result(res, start_time, tempfile, &block)
|
422
744
|
end
|
423
745
|
end
|
424
746
|
rescue EOFError
|
425
747
|
raise RestClient::ServerBrokeConnection
|
426
|
-
rescue
|
427
|
-
raise RestClient::
|
748
|
+
rescue Net::OpenTimeout => err
|
749
|
+
raise RestClient::Exceptions::OpenTimeout.new(nil, err)
|
750
|
+
rescue Net::ReadTimeout => err
|
751
|
+
raise RestClient::Exceptions::ReadTimeout.new(nil, err)
|
752
|
+
rescue Timeout::Error, Errno::ETIMEDOUT => err
|
753
|
+
# handling for non-Net::HTTP timeouts
|
754
|
+
if established_connection
|
755
|
+
raise RestClient::Exceptions::ReadTimeout.new(nil, err)
|
756
|
+
else
|
757
|
+
raise RestClient::Exceptions::OpenTimeout.new(nil, err)
|
758
|
+
end
|
428
759
|
|
429
760
|
rescue OpenSSL::SSL::SSLError => error
|
430
761
|
# TODO: deprecate and remove RestClient::SSLCertificateNotVerified and just
|
@@ -449,136 +780,64 @@ module RestClient
|
|
449
780
|
end
|
450
781
|
|
451
782
|
def setup_credentials(req)
|
452
|
-
|
783
|
+
if user && !@processed_headers_lowercase.include?('authorization')
|
784
|
+
req.basic_auth(user, password)
|
785
|
+
end
|
453
786
|
end
|
454
787
|
|
455
|
-
def
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
788
|
+
def fetch_body_to_tempfile(http_response)
|
789
|
+
# Taken from Chef, which as in turn...
|
790
|
+
# Stolen from http://www.ruby-forum.com/topic/166423
|
791
|
+
# Kudos to _why!
|
792
|
+
tf = Tempfile.new('rest-client.')
|
793
|
+
tf.binmode
|
794
|
+
|
795
|
+
size = 0
|
796
|
+
total = http_response['Content-Length'].to_i
|
797
|
+
stream_log_bucket = nil
|
798
|
+
|
799
|
+
http_response.read_body do |chunk|
|
800
|
+
tf.write chunk
|
801
|
+
size += chunk.size
|
802
|
+
if log
|
803
|
+
if total == 0
|
804
|
+
log << "streaming %s %s (%d of unknown) [0 Content-Length]\n" % [@method.upcase, @url, size]
|
805
|
+
else
|
806
|
+
percent = (size * 100) / total
|
807
|
+
current_log_bucket, _ = percent.divmod(@stream_log_percent)
|
808
|
+
if current_log_bucket != stream_log_bucket
|
809
|
+
stream_log_bucket = current_log_bucket
|
810
|
+
log << "streaming %s %s %d%% done (%d of %d)\n" % [@method.upcase, @url, (size * 100) / total, size, total]
|
473
811
|
end
|
474
812
|
end
|
475
813
|
end
|
476
|
-
@tf.close
|
477
|
-
@tf
|
478
|
-
else
|
479
|
-
http_response.read_body
|
480
814
|
end
|
481
|
-
|
815
|
+
tf.close
|
816
|
+
tf
|
482
817
|
end
|
483
818
|
|
484
|
-
|
819
|
+
# @param res The Net::HTTP response object
|
820
|
+
# @param start_time [Time] Time of request start
|
821
|
+
def process_result(res, start_time, tempfile=nil, &block)
|
485
822
|
if @raw_response
|
486
|
-
|
487
|
-
|
823
|
+
unless tempfile
|
824
|
+
raise ArgumentError.new('tempfile is required')
|
825
|
+
end
|
826
|
+
response = RawResponse.new(tempfile, res, self, start_time)
|
488
827
|
else
|
489
|
-
response = Response.create(
|
828
|
+
response = Response.create(res.body, res, self, start_time)
|
490
829
|
end
|
491
830
|
|
831
|
+
response.log_response
|
832
|
+
|
492
833
|
if block_given?
|
493
834
|
block.call(response, self, res, & block)
|
494
835
|
else
|
495
|
-
response.return!(
|
836
|
+
response.return!(&block)
|
496
837
|
end
|
497
838
|
|
498
839
|
end
|
499
840
|
|
500
|
-
def self.decode content_encoding, body
|
501
|
-
if (!body) || body.empty?
|
502
|
-
body
|
503
|
-
elsif content_encoding == 'gzip'
|
504
|
-
Zlib::GzipReader.new(StringIO.new(body)).read
|
505
|
-
elsif content_encoding == 'deflate'
|
506
|
-
begin
|
507
|
-
Zlib::Inflate.new.inflate body
|
508
|
-
rescue Zlib::DataError
|
509
|
-
# No luck with Zlib decompression. Let's try with raw deflate,
|
510
|
-
# like some broken web servers do.
|
511
|
-
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
|
512
|
-
end
|
513
|
-
else
|
514
|
-
body
|
515
|
-
end
|
516
|
-
end
|
517
|
-
|
518
|
-
def log_request
|
519
|
-
return unless RestClient.log
|
520
|
-
|
521
|
-
out = []
|
522
|
-
sanitized_url = begin
|
523
|
-
uri = URI.parse(url)
|
524
|
-
uri.password = "REDACTED" if uri.password
|
525
|
-
uri.to_s
|
526
|
-
rescue URI::InvalidURIError
|
527
|
-
# An attacker may be able to manipulate the URL to be
|
528
|
-
# invalid, which could force discloure of a password if
|
529
|
-
# we show any of the un-parsed URL here.
|
530
|
-
"[invalid uri]"
|
531
|
-
end
|
532
|
-
|
533
|
-
out << "RestClient.#{method} #{sanitized_url.inspect}"
|
534
|
-
out << payload.short_inspect if payload
|
535
|
-
out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
|
536
|
-
RestClient.log << out.join(', ') + "\n"
|
537
|
-
end
|
538
|
-
|
539
|
-
def log_response res
|
540
|
-
return unless RestClient.log
|
541
|
-
|
542
|
-
size = if @raw_response
|
543
|
-
File.size(@tf.path)
|
544
|
-
else
|
545
|
-
res.body.nil? ? 0 : res.body.size
|
546
|
-
end
|
547
|
-
|
548
|
-
RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
|
549
|
-
end
|
550
|
-
|
551
|
-
# Return a hash of headers whose keys are capitalized strings
|
552
|
-
def stringify_headers headers
|
553
|
-
headers.inject({}) do |result, (key, value)|
|
554
|
-
if key.is_a? Symbol
|
555
|
-
key = key.to_s.split(/_/).map { |w| w.capitalize }.join('-')
|
556
|
-
end
|
557
|
-
if 'CONTENT-TYPE' == key.upcase
|
558
|
-
result[key] = maybe_convert_extension(value.to_s)
|
559
|
-
elsif 'ACCEPT' == key.upcase
|
560
|
-
# Accept can be composed of several comma-separated values
|
561
|
-
if value.is_a? Array
|
562
|
-
target_values = value
|
563
|
-
else
|
564
|
-
target_values = value.to_s.split ','
|
565
|
-
end
|
566
|
-
result[key] = target_values.map { |ext|
|
567
|
-
maybe_convert_extension(ext.to_s.strip)
|
568
|
-
}.join(', ')
|
569
|
-
else
|
570
|
-
result[key] = value.to_s
|
571
|
-
end
|
572
|
-
result
|
573
|
-
end
|
574
|
-
end
|
575
|
-
|
576
|
-
def default_headers
|
577
|
-
{:accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate'}
|
578
|
-
end
|
579
|
-
|
580
|
-
private
|
581
|
-
|
582
841
|
def parser
|
583
842
|
URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
584
843
|
end
|