rest-client 2.0.0.rc2-x64-mingw32 → 2.0.0.rc3-x64-mingw32
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/.gitignore +1 -0
- data/.rubocop-disables.yml +17 -8
- data/.travis.yml +32 -2
- data/AUTHORS +6 -0
- data/README.md +578 -0
- data/Rakefile +1 -1
- data/history.md +45 -13
- data/lib/restclient.rb +4 -2
- data/lib/restclient/abstract_response.rb +51 -25
- data/lib/restclient/exceptions.rb +45 -6
- data/lib/restclient/params_array.rb +72 -0
- data/lib/restclient/payload.rb +40 -69
- data/lib/restclient/raw_response.rb +1 -2
- data/lib/restclient/request.rb +372 -199
- data/lib/restclient/response.rb +11 -8
- data/lib/restclient/utils.rb +144 -2
- data/lib/restclient/version.rb +1 -1
- data/rest-client.gemspec +5 -5
- data/spec/helpers.rb +8 -0
- data/spec/integration/httpbin_spec.rb +7 -7
- data/spec/integration/integration_spec.rb +34 -24
- data/spec/integration/request_spec.rb +1 -1
- data/spec/spec_helper.rb +8 -1
- data/spec/unit/abstract_response_spec.rb +76 -33
- data/spec/unit/exceptions_spec.rb +27 -21
- data/spec/unit/params_array_spec.rb +36 -0
- data/spec/unit/payload_spec.rb +71 -53
- data/spec/unit/raw_response_spec.rb +3 -3
- data/spec/unit/request2_spec.rb +29 -7
- data/spec/unit/request_spec.rb +552 -415
- data/spec/unit/resource_spec.rb +25 -25
- data/spec/unit/response_spec.rb +86 -64
- data/spec/unit/restclient_spec.rb +13 -13
- data/spec/unit/utils_spec.rb +117 -41
- data/spec/unit/windows/root_certs_spec.rb +2 -2
- metadata +15 -12
- data/README.rdoc +0 -410
data/lib/restclient/payload.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'tempfile'
|
2
|
+
require 'securerandom'
|
2
3
|
require 'stringio'
|
4
|
+
|
3
5
|
require 'mime/types'
|
4
6
|
|
5
7
|
module RestClient
|
@@ -23,28 +25,20 @@ module RestClient
|
|
23
25
|
end
|
24
26
|
|
25
27
|
def has_file?(params)
|
26
|
-
params.
|
27
|
-
|
28
|
-
when Hash
|
29
|
-
has_file?(v)
|
30
|
-
when Array
|
31
|
-
has_file_array?(v)
|
32
|
-
else
|
33
|
-
v.respond_to?(:path) && v.respond_to?(:read)
|
34
|
-
end
|
28
|
+
unless params.is_a?(Hash)
|
29
|
+
raise ArgumentError.new("Must pass Hash, not #{params.inspect}")
|
35
30
|
end
|
31
|
+
_has_file?(params)
|
36
32
|
end
|
37
33
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
v.respond_to?(:path) && v.respond_to?(:read)
|
47
|
-
end
|
34
|
+
def _has_file?(obj)
|
35
|
+
case obj
|
36
|
+
when Hash, ParamsArray
|
37
|
+
obj.any? {|_, v| _has_file?(v) }
|
38
|
+
when Array
|
39
|
+
obj.any? {|v| _has_file?(v) }
|
40
|
+
else
|
41
|
+
obj.respond_to?(:path) && obj.respond_to?(:read)
|
48
42
|
end
|
49
43
|
end
|
50
44
|
|
@@ -62,36 +56,9 @@ module RestClient
|
|
62
56
|
@stream.read(*args)
|
63
57
|
end
|
64
58
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
# {keys1 => {keys2 => value}} will be transformed into [keys1[key2], value]
|
69
|
-
def flatten_params(params, parent_key = nil)
|
70
|
-
result = []
|
71
|
-
params.each do |key, value|
|
72
|
-
calculated_key = parent_key ? "#{parent_key}[#{handle_key(key)}]" : handle_key(key)
|
73
|
-
if value.is_a? Hash
|
74
|
-
result += flatten_params(value, calculated_key)
|
75
|
-
elsif value.is_a? Array
|
76
|
-
result += flatten_params_array(value, calculated_key)
|
77
|
-
else
|
78
|
-
result << [calculated_key, value]
|
79
|
-
end
|
80
|
-
end
|
81
|
-
result
|
82
|
-
end
|
83
|
-
|
84
|
-
def flatten_params_array value, calculated_key
|
85
|
-
result = []
|
86
|
-
value.each do |elem|
|
87
|
-
if elem.is_a? Hash
|
88
|
-
result += flatten_params(elem, calculated_key)
|
89
|
-
elsif elem.is_a? Array
|
90
|
-
result += flatten_params_array(elem, calculated_key)
|
91
|
-
else
|
92
|
-
result << ["#{calculated_key}[]", elem]
|
93
|
-
end
|
94
|
-
end
|
59
|
+
def to_s
|
60
|
+
result = read
|
61
|
+
@stream.seek(0)
|
95
62
|
result
|
96
63
|
end
|
97
64
|
|
@@ -109,14 +76,12 @@ module RestClient
|
|
109
76
|
@stream.close unless @stream.closed?
|
110
77
|
end
|
111
78
|
|
112
|
-
def
|
113
|
-
|
114
|
-
@stream.seek(0)
|
115
|
-
result
|
79
|
+
def to_s_inspect
|
80
|
+
to_s.inspect
|
116
81
|
end
|
117
82
|
|
118
83
|
def short_inspect
|
119
|
-
(size > 500 ? "#{size} byte(s) length" :
|
84
|
+
(size > 500 ? "#{size} byte(s) length" : to_s_inspect)
|
120
85
|
end
|
121
86
|
|
122
87
|
end
|
@@ -139,37 +104,28 @@ module RestClient
|
|
139
104
|
|
140
105
|
class UrlEncoded < Base
|
141
106
|
def build_stream(params = nil)
|
142
|
-
@stream = StringIO.new(
|
143
|
-
"#{entry[0]}=#{handle_key(entry[1])}"
|
144
|
-
end.join("&"))
|
107
|
+
@stream = StringIO.new(Utils.encode_query_string(params))
|
145
108
|
@stream.seek(0)
|
146
109
|
end
|
147
110
|
|
148
|
-
# for UrlEncoded escape the keys
|
149
|
-
def handle_key key
|
150
|
-
Parser.escape(key.to_s, Escape)
|
151
|
-
end
|
152
|
-
|
153
111
|
def headers
|
154
112
|
super.merge({'Content-Type' => 'application/x-www-form-urlencoded'})
|
155
113
|
end
|
156
|
-
|
157
|
-
Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
158
|
-
Escape = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
|
159
114
|
end
|
160
115
|
|
161
116
|
class Multipart < Base
|
162
117
|
EOL = "\r\n"
|
163
118
|
|
164
119
|
def build_stream(params)
|
165
|
-
b =
|
120
|
+
b = '--' + boundary
|
166
121
|
|
167
122
|
@stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
|
168
123
|
@stream.binmode
|
169
124
|
@stream.write(b + EOL)
|
170
125
|
|
171
|
-
|
172
|
-
|
126
|
+
case params
|
127
|
+
when Hash, ParamsArray
|
128
|
+
x = Utils.flatten_params(params)
|
173
129
|
else
|
174
130
|
x = params
|
175
131
|
end
|
@@ -218,10 +174,25 @@ module RestClient
|
|
218
174
|
end
|
219
175
|
|
220
176
|
def boundary
|
221
|
-
@boundary
|
177
|
+
return @boundary if defined?(@boundary) && @boundary
|
178
|
+
|
179
|
+
# Use the same algorithm used by WebKit: generate 16 random
|
180
|
+
# alphanumeric characters, replacing `+` `/` with `A` `B` (included in
|
181
|
+
# the list twice) to round out the set of 64.
|
182
|
+
s = SecureRandom.base64(12)
|
183
|
+
s.tr!('+/', 'AB')
|
184
|
+
|
185
|
+
@boundary = '----RubyFormBoundary' + s
|
222
186
|
end
|
223
187
|
|
224
188
|
# for Multipart do not escape the keys
|
189
|
+
#
|
190
|
+
# Ostensibly multipart keys MAY be percent encoded per RFC 7578, but in
|
191
|
+
# practice no major browser that I'm aware of uses percent encoding.
|
192
|
+
#
|
193
|
+
# Further discussion of multipart encoding:
|
194
|
+
# https://github.com/rest-client/rest-client/pull/403#issuecomment-156976930
|
195
|
+
#
|
225
196
|
def handle_key key
|
226
197
|
key
|
227
198
|
end
|
@@ -19,9 +19,8 @@ module RestClient
|
|
19
19
|
"<RestClient::RawResponse @code=#{code.inspect}, @file=#{file.inspect}, @request=#{request.inspect}>"
|
20
20
|
end
|
21
21
|
|
22
|
-
def initialize(tempfile, net_http_res,
|
22
|
+
def initialize(tempfile, net_http_res, request)
|
23
23
|
@net_http_res = net_http_res
|
24
|
-
@args = args
|
25
24
|
@file = tempfile
|
26
25
|
@request = request
|
27
26
|
end
|
data/lib/restclient/request.rb
CHANGED
@@ -16,7 +16,9 @@ module RestClient
|
|
16
16
|
# * :url
|
17
17
|
# Optional parameters (have a look at ssl and/or uri for some explanations):
|
18
18
|
# * :headers a hash containing the request headers
|
19
|
-
# * :cookies
|
19
|
+
# * :cookies may be a Hash{String/Symbol => String} of cookie values, an
|
20
|
+
# Array<HTTP::Cookie>, or an HTTP::CookieJar containing cookies. These
|
21
|
+
# will be added to a cookie jar before the request is sent.
|
20
22
|
# * :user and :password for basic auth, will be replaced by a user/password available in the :url
|
21
23
|
# * :block_response call the provided block with the HTTPResponse as parameter
|
22
24
|
# * :raw_response return a low-level RawResponse instead of a Response
|
@@ -38,9 +40,7 @@ module RestClient
|
|
38
40
|
# called with the HTTP request and request params.
|
39
41
|
class Request
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
-
attr_reader :method, :url, :headers, :cookies, :payload, :proxy,
|
43
|
+
attr_reader :method, :uri, :url, :headers, :payload, :proxy,
|
44
44
|
:user, :password, :read_timeout, :max_redirects,
|
45
45
|
:open_timeout, :raw_response, :processed_headers, :args,
|
46
46
|
:ssl_opts
|
@@ -117,14 +117,20 @@ module RestClient
|
|
117
117
|
end
|
118
118
|
|
119
119
|
def initialize args
|
120
|
-
@method = args[:method]
|
120
|
+
@method = normalize_method(args[:method])
|
121
121
|
@headers = (args[:headers] || {}).dup
|
122
122
|
if args[:url]
|
123
|
-
@url = process_url_params(args[:url], headers)
|
123
|
+
@url = process_url_params(normalize_url(args[:url]), headers)
|
124
124
|
else
|
125
125
|
raise ArgumentError, "must pass :url"
|
126
126
|
end
|
127
|
-
|
127
|
+
|
128
|
+
@user = @password = nil
|
129
|
+
parse_url_with_auth!(url)
|
130
|
+
|
131
|
+
# process cookie arguments found in headers or args
|
132
|
+
@cookie_jar = process_cookie_args!(@uri, @headers, args)
|
133
|
+
|
128
134
|
@payload = Payload.generate(args[:payload])
|
129
135
|
@user = args[:user]
|
130
136
|
@password = args[:password]
|
@@ -171,17 +177,21 @@ module RestClient
|
|
171
177
|
end
|
172
178
|
end
|
173
179
|
|
174
|
-
#
|
175
|
-
if
|
176
|
-
@ssl_opts[:cert_store] = self.class.default_ssl_cert_store
|
177
|
-
end
|
180
|
+
# Set some other default SSL options, but only if we have an HTTPS URI.
|
181
|
+
if use_ssl?
|
178
182
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
183
|
+
# If there's no CA file, CA path, or cert store provided, use default
|
184
|
+
if !ssl_ca_file && !ssl_ca_path && !@ssl_opts.include?(:cert_store)
|
185
|
+
@ssl_opts[:cert_store] = self.class.default_ssl_cert_store
|
186
|
+
end
|
187
|
+
|
188
|
+
unless @ssl_opts.include?(:ciphers)
|
189
|
+
# If we're on a Ruby version that has insecure default ciphers,
|
190
|
+
# override it with our default list.
|
191
|
+
if WeakDefaultCiphers.include?(
|
192
|
+
OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.fetch(:ciphers))
|
193
|
+
@ssl_opts[:ciphers] = DefaultCiphers
|
194
|
+
end
|
185
195
|
end
|
186
196
|
end
|
187
197
|
|
@@ -194,12 +204,9 @@ module RestClient
|
|
194
204
|
end
|
195
205
|
|
196
206
|
def execute & block
|
197
|
-
uri = parse_url_with_auth(url)
|
198
|
-
|
199
207
|
# With 2.0.0+, net/http accepts URI objects in requests and handles wrapping
|
200
208
|
# IPv6 addresses in [] for use in the Host request header.
|
201
|
-
|
202
|
-
transmit uri, net_http_request_class(method).new(request_uri, processed_headers), payload, & block
|
209
|
+
transmit uri, net_http_request_class(method).new(uri, processed_headers), payload, & block
|
203
210
|
ensure
|
204
211
|
payload.close if payload
|
205
212
|
end
|
@@ -214,66 +221,223 @@ module RestClient
|
|
214
221
|
end
|
215
222
|
end
|
216
223
|
|
224
|
+
# Return true if the request URI will use HTTPS.
|
225
|
+
#
|
226
|
+
# @return [Boolean]
|
227
|
+
#
|
228
|
+
def use_ssl?
|
229
|
+
uri.is_a?(URI::HTTPS)
|
230
|
+
end
|
231
|
+
|
217
232
|
# Extract the query parameters and append them to the url
|
218
|
-
|
219
|
-
|
233
|
+
#
|
234
|
+
# Look through the headers hash for a :params option (case-insensitive,
|
235
|
+
# may be string or symbol). If present and the value is a Hash or
|
236
|
+
# RestClient::ParamsArray, *delete* the key/value pair from the headers
|
237
|
+
# hash and encode the value into a query string. Append this query string
|
238
|
+
# to the URL and return the resulting URL.
|
239
|
+
#
|
240
|
+
# @param [String] url
|
241
|
+
# @param [Hash] headers An options/headers hash to process. Mutation
|
242
|
+
# warning: the params key may be removed if present!
|
243
|
+
#
|
244
|
+
# @return [String] resulting url with query string
|
245
|
+
#
|
246
|
+
def process_url_params(url, headers)
|
247
|
+
url_params = nil
|
248
|
+
|
249
|
+
# find and extract/remove "params" key if the value is a Hash/ParamsArray
|
220
250
|
headers.delete_if do |key, value|
|
221
|
-
if
|
222
|
-
|
251
|
+
if key.to_s.downcase == 'params' &&
|
252
|
+
(value.is_a?(Hash) || value.is_a?(RestClient::ParamsArray))
|
253
|
+
if url_params
|
254
|
+
raise ArgumentError.new("Multiple 'params' options passed")
|
255
|
+
end
|
256
|
+
url_params = value
|
223
257
|
true
|
224
258
|
else
|
225
259
|
false
|
226
260
|
end
|
227
261
|
end
|
228
|
-
|
229
|
-
|
230
|
-
|
262
|
+
|
263
|
+
# build resulting URL with query string
|
264
|
+
if url_params && !url_params.empty?
|
265
|
+
query_string = RestClient::Utils.encode_query_string(url_params)
|
266
|
+
|
267
|
+
if url.include?('?')
|
268
|
+
url + '&' + query_string
|
269
|
+
else
|
270
|
+
url + '?' + query_string
|
271
|
+
end
|
231
272
|
else
|
232
273
|
url
|
233
274
|
end
|
234
275
|
end
|
235
276
|
|
236
|
-
|
237
|
-
|
277
|
+
# Render a hash of key => value pairs for cookies in the Request#cookie_jar
|
278
|
+
# that are valid for the Request#uri. This will not necessarily include all
|
279
|
+
# cookies if there are duplicate keys. It's safer to use the cookie_jar
|
280
|
+
# directly if that's a concern.
|
281
|
+
#
|
282
|
+
# @see Request#cookie_jar
|
283
|
+
#
|
284
|
+
# @return [Hash]
|
285
|
+
#
|
286
|
+
def cookies
|
287
|
+
hash = {}
|
238
288
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
289
|
+
@cookie_jar.cookies(uri).each do |c|
|
290
|
+
hash[c.name] = c.value
|
291
|
+
end
|
292
|
+
|
293
|
+
hash
|
294
|
+
end
|
295
|
+
|
296
|
+
# @return [HTTP::CookieJar]
|
297
|
+
def cookie_jar
|
298
|
+
@cookie_jar
|
299
|
+
end
|
300
|
+
|
301
|
+
# Render a Cookie HTTP request header from the contents of the @cookie_jar,
|
302
|
+
# or nil if the jar is empty.
|
303
|
+
#
|
304
|
+
# @see Request#cookie_jar
|
305
|
+
#
|
306
|
+
# @return [String, nil]
|
307
|
+
#
|
308
|
+
def make_cookie_header
|
309
|
+
return nil if cookie_jar.nil?
|
310
|
+
|
311
|
+
arr = cookie_jar.cookies(url)
|
312
|
+
return nil if arr.empty?
|
313
|
+
|
314
|
+
return HTTP::Cookie.cookie_value(arr)
|
315
|
+
end
|
316
|
+
|
317
|
+
# Process cookies passed as hash or as HTTP::CookieJar. For backwards
|
318
|
+
# compatibility, these may be passed as a :cookies option masquerading
|
319
|
+
# inside the headers hash. To avoid confusion, if :cookies is passed in
|
320
|
+
# both headers and Request#initialize, raise an error.
|
321
|
+
#
|
322
|
+
# :cookies may be a:
|
323
|
+
# - Hash{String/Symbol => String}
|
324
|
+
# - Array<HTTP::Cookie>
|
325
|
+
# - HTTP::CookieJar
|
326
|
+
#
|
327
|
+
# Passing as a hash:
|
328
|
+
# Keys may be symbols or strings. Values must be strings.
|
329
|
+
# Infer the domain name from the request URI and allow subdomains (as
|
330
|
+
# though '.example.com' had been set in a Set-Cookie header). Assume a
|
331
|
+
# path of '/'.
|
332
|
+
#
|
333
|
+
# RestClient::Request.new(url: 'http://example.com', method: :get,
|
334
|
+
# :cookies => {:foo => 'Value', 'bar' => '123'}
|
335
|
+
# )
|
336
|
+
#
|
337
|
+
# results in cookies as though set from the server by:
|
338
|
+
# Set-Cookie: foo=Value; Domain=.example.com; Path=/
|
339
|
+
# Set-Cookie: bar=123; Domain=.example.com; Path=/
|
340
|
+
#
|
341
|
+
# which yields a client cookie header of:
|
342
|
+
# Cookie: foo=Value; bar=123
|
343
|
+
#
|
344
|
+
# Passing as HTTP::CookieJar, which will be passed through directly:
|
345
|
+
#
|
346
|
+
# jar = HTTP::CookieJar.new
|
347
|
+
# jar.add(HTTP::Cookie.new('foo', 'Value', domain: 'example.com',
|
348
|
+
# path: '/', for_domain: false))
|
349
|
+
#
|
350
|
+
# RestClient::Request.new(..., :cookies => jar)
|
351
|
+
#
|
352
|
+
# @param [URI::HTTP] uri The URI for the request. This will be used to
|
353
|
+
# infer the domain name for cookies passed as strings in a hash. To avoid
|
354
|
+
# this implicit behavior, pass a full cookie jar or use HTTP::Cookie hash
|
355
|
+
# values.
|
356
|
+
# @param [Hash] headers The headers hash from which to pull the :cookies
|
357
|
+
# option. MUTATION NOTE: This key will be deleted from the hash if
|
358
|
+
# present.
|
359
|
+
# @param [Hash] args The options passed to Request#initialize. This hash
|
360
|
+
# will be used as another potential source for the :cookies key.
|
361
|
+
# These args will not be mutated.
|
362
|
+
#
|
363
|
+
# @return [HTTP::CookieJar] A cookie jar containing the parsed cookies.
|
364
|
+
#
|
365
|
+
def process_cookie_args!(uri, headers, args)
|
366
|
+
|
367
|
+
# Avoid ambiguity in whether options from headers or options from
|
368
|
+
# Request#initialize should take precedence by raising ArgumentError when
|
369
|
+
# both are present. Prior versions of rest-client claimed to give
|
370
|
+
# precedence to init options, but actually gave precedence to headers.
|
371
|
+
# Avoid that mess by erroring out instead.
|
372
|
+
if headers[:cookies] && args[:cookies]
|
373
|
+
raise ArgumentError.new(
|
374
|
+
"Cannot pass :cookies in Request.new() and in headers hash")
|
375
|
+
end
|
376
|
+
|
377
|
+
cookies_data = headers.delete(:cookies) || args[:cookies]
|
378
|
+
|
379
|
+
# return copy of cookie jar as is
|
380
|
+
if cookies_data.is_a?(HTTP::CookieJar)
|
381
|
+
return cookies_data.dup
|
382
|
+
end
|
383
|
+
|
384
|
+
# convert cookies hash into a CookieJar
|
385
|
+
jar = HTTP::CookieJar.new
|
386
|
+
|
387
|
+
(cookies_data || []).each do |key, val|
|
388
|
+
|
389
|
+
# Support for Array<HTTP::Cookie> mode:
|
390
|
+
# If key is a cookie object, add it to the jar directly and assert that
|
391
|
+
# there is no separate val.
|
392
|
+
if key.is_a?(HTTP::Cookie)
|
393
|
+
if val
|
394
|
+
raise ArgumentError.new("extra cookie val: #{val.inspect}")
|
248
395
|
end
|
396
|
+
|
397
|
+
jar.add(key)
|
398
|
+
next
|
249
399
|
end
|
250
400
|
|
251
|
-
|
252
|
-
|
401
|
+
if key.is_a?(Symbol)
|
402
|
+
key = key.to_s
|
403
|
+
end
|
404
|
+
|
405
|
+
# assume implicit domain from the request URI, and set for_domain to
|
406
|
+
# permit subdomains
|
407
|
+
jar.add(HTTP::Cookie.new(key, val, domain: uri.hostname.downcase,
|
408
|
+
path: '/', for_domain: true))
|
253
409
|
end
|
254
|
-
|
255
|
-
|
256
|
-
headers
|
410
|
+
|
411
|
+
jar
|
257
412
|
end
|
258
413
|
|
259
|
-
#
|
414
|
+
# Generate headers for use by a request. Header keys will be stringified
|
415
|
+
# using `#stringify_headers` to normalize them as capitalized strings.
|
416
|
+
#
|
417
|
+
# The final headers consist of:
|
418
|
+
# - default headers from #default_headers
|
419
|
+
# - user_headers provided here
|
420
|
+
# - headers from the payload object (e.g. Content-Type, Content-Lenth)
|
421
|
+
# - cookie headers from #make_cookie_header
|
260
422
|
#
|
261
|
-
#
|
262
|
-
# more liberal.
|
423
|
+
# @param [Hash] user_headers User-provided headers to include
|
263
424
|
#
|
264
|
-
#
|
265
|
-
# equals sign, semicolon, comma, or space.
|
425
|
+
# @return [Hash<String, String>] A hash of HTTP headers => values
|
266
426
|
#
|
267
|
-
def
|
268
|
-
|
427
|
+
def make_headers(user_headers)
|
428
|
+
headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
|
429
|
+
headers.merge!(@payload.headers) if @payload
|
269
430
|
|
270
|
-
|
271
|
-
|
431
|
+
# merge in cookies
|
432
|
+
cookies = make_cookie_header
|
433
|
+
if cookies && !cookies.empty?
|
434
|
+
if headers['Cookie']
|
435
|
+
warn('warning: overriding "Cookie" header with :cookies option')
|
436
|
+
end
|
437
|
+
headers['Cookie'] = cookies
|
438
|
+
end
|
272
439
|
|
273
|
-
|
274
|
-
# but control characters, comma, and semicolon.
|
275
|
-
def valid_cookie_value?(value)
|
276
|
-
! Regexp.new('[\x0-\x1f\x7f,;]').match(value)
|
440
|
+
headers
|
277
441
|
end
|
278
442
|
|
279
443
|
# The proxy URI for this request. If `:proxy` was provided on this request,
|
@@ -318,7 +482,7 @@ module RestClient
|
|
318
482
|
end
|
319
483
|
|
320
484
|
def net_http_request_class(method)
|
321
|
-
Net::HTTP.const_get(method.
|
485
|
+
Net::HTTP.const_get(method.capitalize, false)
|
322
486
|
end
|
323
487
|
|
324
488
|
def net_http_do_request(http, req, body=nil, &block)
|
@@ -330,50 +494,19 @@ module RestClient
|
|
330
494
|
end
|
331
495
|
end
|
332
496
|
|
333
|
-
#
|
334
|
-
# (i.e. scheme followed by '//'), a scheme of 'http' will be added. This
|
335
|
-
# mimics the behavior of browsers and user agents like cURL.
|
497
|
+
# Normalize a URL by adding a protocol if none is present.
|
336
498
|
#
|
337
|
-
#
|
499
|
+
# If the string has no HTTP-like scheme (i.e. scheme followed by '//'), a
|
500
|
+
# scheme of 'http' will be added. This mimics the behavior of browsers and
|
501
|
+
# user agents like cURL.
|
338
502
|
#
|
339
|
-
# @
|
503
|
+
# @param [String] url A URL string.
|
340
504
|
#
|
341
|
-
# @
|
505
|
+
# @return [String]
|
342
506
|
#
|
343
|
-
def
|
344
|
-
# Prepend http:// unless the string already contains an RFC 3986 scheme
|
345
|
-
# followed by two forward slashes. (The slashes are not part of the URI
|
346
|
-
# RFC, but specified by the URL RFC 1738.)
|
347
|
-
# https://tools.ietf.org/html/rfc3986#section-3.1
|
507
|
+
def normalize_url(url)
|
348
508
|
url = 'http://' + url unless url.match(%r{\A[a-z][a-z0-9+.-]*://}i)
|
349
|
-
|
350
|
-
end
|
351
|
-
|
352
|
-
def parse_url_with_auth(url)
|
353
|
-
uri = parse_url(url)
|
354
|
-
@user = CGI.unescape(uri.user) if uri.user
|
355
|
-
@password = CGI.unescape(uri.password) if uri.password
|
356
|
-
if !@user && !@password
|
357
|
-
@user, @password = Netrc.read[uri.hostname]
|
358
|
-
end
|
359
|
-
uri
|
360
|
-
end
|
361
|
-
|
362
|
-
def process_payload(p=nil, parent_key=nil)
|
363
|
-
unless p.is_a?(Hash)
|
364
|
-
p
|
365
|
-
else
|
366
|
-
@headers[:content_type] ||= 'application/x-www-form-urlencoded'
|
367
|
-
p.keys.map do |k|
|
368
|
-
key = parent_key ? "#{parent_key}[#{k}]" : k
|
369
|
-
if p[k].is_a? Hash
|
370
|
-
process_payload(p[k], key)
|
371
|
-
else
|
372
|
-
value = parser.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
373
|
-
"#{key}=#{value}"
|
374
|
-
end
|
375
|
-
end.join("&")
|
376
|
-
end
|
509
|
+
url
|
377
510
|
end
|
378
511
|
|
379
512
|
# Return a certificate store that can be used to validate certificates with
|
@@ -405,6 +538,122 @@ module RestClient
|
|
405
538
|
cert_store
|
406
539
|
end
|
407
540
|
|
541
|
+
def self.decode content_encoding, body
|
542
|
+
if (!body) || body.empty?
|
543
|
+
body
|
544
|
+
elsif content_encoding == 'gzip'
|
545
|
+
Zlib::GzipReader.new(StringIO.new(body)).read
|
546
|
+
elsif content_encoding == 'deflate'
|
547
|
+
begin
|
548
|
+
Zlib::Inflate.new.inflate body
|
549
|
+
rescue Zlib::DataError
|
550
|
+
# No luck with Zlib decompression. Let's try with raw deflate,
|
551
|
+
# like some broken web servers do.
|
552
|
+
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
|
553
|
+
end
|
554
|
+
else
|
555
|
+
body
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
def redacted_uri
|
560
|
+
if uri.password
|
561
|
+
sanitized_uri = uri.dup
|
562
|
+
sanitized_uri.password = 'REDACTED'
|
563
|
+
sanitized_uri
|
564
|
+
else
|
565
|
+
uri
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
def redacted_url
|
570
|
+
redacted_uri.to_s
|
571
|
+
end
|
572
|
+
|
573
|
+
def log_request
|
574
|
+
return unless RestClient.log
|
575
|
+
|
576
|
+
out = []
|
577
|
+
|
578
|
+
out << "RestClient.#{method} #{redacted_url.inspect}"
|
579
|
+
out << payload.short_inspect if payload
|
580
|
+
out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
|
581
|
+
RestClient.log << out.join(', ') + "\n"
|
582
|
+
end
|
583
|
+
|
584
|
+
def log_response res
|
585
|
+
return unless RestClient.log
|
586
|
+
|
587
|
+
size = if @raw_response
|
588
|
+
File.size(@tf.path)
|
589
|
+
else
|
590
|
+
res.body.nil? ? 0 : res.body.size
|
591
|
+
end
|
592
|
+
|
593
|
+
RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
|
594
|
+
end
|
595
|
+
|
596
|
+
# Return a hash of headers whose keys are capitalized strings
|
597
|
+
def stringify_headers headers
|
598
|
+
headers.inject({}) do |result, (key, value)|
|
599
|
+
if key.is_a? Symbol
|
600
|
+
key = key.to_s.split(/_/).map(&:capitalize).join('-')
|
601
|
+
end
|
602
|
+
if 'CONTENT-TYPE' == key.upcase
|
603
|
+
result[key] = maybe_convert_extension(value.to_s)
|
604
|
+
elsif 'ACCEPT' == key.upcase
|
605
|
+
# Accept can be composed of several comma-separated values
|
606
|
+
if value.is_a? Array
|
607
|
+
target_values = value
|
608
|
+
else
|
609
|
+
target_values = value.to_s.split ','
|
610
|
+
end
|
611
|
+
result[key] = target_values.map { |ext|
|
612
|
+
maybe_convert_extension(ext.to_s.strip)
|
613
|
+
}.join(', ')
|
614
|
+
else
|
615
|
+
result[key] = value.to_s
|
616
|
+
end
|
617
|
+
result
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
def default_headers
|
622
|
+
{
|
623
|
+
:accept => '*/*',
|
624
|
+
:accept_encoding => 'gzip, deflate',
|
625
|
+
:user_agent => RestClient::Platform.default_user_agent,
|
626
|
+
}
|
627
|
+
end
|
628
|
+
|
629
|
+
private
|
630
|
+
|
631
|
+
# Parse the `@url` string into a URI object and save it as
|
632
|
+
# `@uri`. Also save any basic auth user or password as @user and @password.
|
633
|
+
# If no auth info was passed, check for credentials in a Netrc file.
|
634
|
+
#
|
635
|
+
# @param [String] url A URL string.
|
636
|
+
#
|
637
|
+
# @return [URI]
|
638
|
+
#
|
639
|
+
# @raise URI::InvalidURIError on invalid URIs
|
640
|
+
#
|
641
|
+
def parse_url_with_auth!(url)
|
642
|
+
uri = URI.parse(url)
|
643
|
+
|
644
|
+
if uri.hostname.nil?
|
645
|
+
raise URI::InvalidURIError.new("bad URI(no host provided): #{url}")
|
646
|
+
end
|
647
|
+
|
648
|
+
@user = CGI.unescape(uri.user) if uri.user
|
649
|
+
@password = CGI.unescape(uri.password) if uri.password
|
650
|
+
if !@user && !@password
|
651
|
+
@user, @password = Netrc.read[uri.hostname]
|
652
|
+
end
|
653
|
+
|
654
|
+
@uri = uri
|
655
|
+
end
|
656
|
+
|
408
657
|
def print_verify_callback_warnings
|
409
658
|
warned = false
|
410
659
|
if RestClient::Platform.mac_mri?
|
@@ -419,10 +668,27 @@ module RestClient
|
|
419
668
|
warned
|
420
669
|
end
|
421
670
|
|
671
|
+
# Parse a method and return a normalized string version.
|
672
|
+
#
|
673
|
+
# Raise ArgumentError if the method is falsy, but otherwise do no
|
674
|
+
# validation.
|
675
|
+
#
|
676
|
+
# @param method [String, Symbol]
|
677
|
+
#
|
678
|
+
# @return [String]
|
679
|
+
#
|
680
|
+
# @see net_http_request_class
|
681
|
+
#
|
682
|
+
def normalize_method(method)
|
683
|
+
raise ArgumentError.new('must pass :method') unless method
|
684
|
+
method.to_s.downcase
|
685
|
+
end
|
686
|
+
|
422
687
|
def transmit uri, req, payload, & block
|
423
688
|
|
424
689
|
# We set this to true in the net/http block so that we can distinguish
|
425
|
-
# read_timeout from open_timeout.
|
690
|
+
# read_timeout from open_timeout. Now that we only support Ruby 2.0+,
|
691
|
+
# this is only needed for Timeout exceptions thrown outside of Net::HTTP.
|
426
692
|
established_connection = false
|
427
693
|
|
428
694
|
setup_credentials req
|
@@ -491,7 +757,6 @@ module RestClient
|
|
491
757
|
|
492
758
|
log_request
|
493
759
|
|
494
|
-
|
495
760
|
net.start do |http|
|
496
761
|
established_connection = true
|
497
762
|
|
@@ -507,18 +772,12 @@ module RestClient
|
|
507
772
|
end
|
508
773
|
rescue EOFError
|
509
774
|
raise RestClient::ServerBrokeConnection
|
775
|
+
rescue Net::OpenTimeout => err
|
776
|
+
raise RestClient::Exceptions::OpenTimeout.new(nil, err)
|
777
|
+
rescue Net::ReadTimeout => err
|
778
|
+
raise RestClient::Exceptions::ReadTimeout.new(nil, err)
|
510
779
|
rescue Timeout::Error, Errno::ETIMEDOUT => err
|
511
|
-
# Net::HTTP
|
512
|
-
if defined?(Net::OpenTimeout)
|
513
|
-
case err
|
514
|
-
when Net::OpenTimeout
|
515
|
-
raise RestClient::Exceptions::OpenTimeout.new(nil, err)
|
516
|
-
when Net::ReadTimeout
|
517
|
-
raise RestClient::Exceptions::ReadTimeout.new(nil, err)
|
518
|
-
end
|
519
|
-
end
|
520
|
-
|
521
|
-
# compatibility for Ruby 1.9.3, handling for non-Net::HTTP timeouts
|
780
|
+
# handling for non-Net::HTTP timeouts
|
522
781
|
if established_connection
|
523
782
|
raise RestClient::Exceptions::ReadTimeout.new(nil, err)
|
524
783
|
else
|
@@ -558,7 +817,7 @@ module RestClient
|
|
558
817
|
# Kudos to _why!
|
559
818
|
@tf = Tempfile.new('rest-client.')
|
560
819
|
@tf.binmode
|
561
|
-
size, total = 0, http_response
|
820
|
+
size, total = 0, http_response['Content-Length'].to_i
|
562
821
|
http_response.read_body do |chunk|
|
563
822
|
@tf.write chunk
|
564
823
|
size += chunk.size
|
@@ -583,10 +842,10 @@ module RestClient
|
|
583
842
|
def process_result res, & block
|
584
843
|
if @raw_response
|
585
844
|
# We don't decode raw requests
|
586
|
-
response = RawResponse.new(@tf, res,
|
845
|
+
response = RawResponse.new(@tf, res, self)
|
587
846
|
else
|
588
847
|
decoded = Request.decode(res['content-encoding'], res.body)
|
589
|
-
response = Response.create(decoded, res,
|
848
|
+
response = Response.create(decoded, res, self)
|
590
849
|
end
|
591
850
|
|
592
851
|
if block_given?
|
@@ -597,92 +856,6 @@ module RestClient
|
|
597
856
|
|
598
857
|
end
|
599
858
|
|
600
|
-
def self.decode content_encoding, body
|
601
|
-
if (!body) || body.empty?
|
602
|
-
body
|
603
|
-
elsif content_encoding == 'gzip'
|
604
|
-
Zlib::GzipReader.new(StringIO.new(body)).read
|
605
|
-
elsif content_encoding == 'deflate'
|
606
|
-
begin
|
607
|
-
Zlib::Inflate.new.inflate body
|
608
|
-
rescue Zlib::DataError
|
609
|
-
# No luck with Zlib decompression. Let's try with raw deflate,
|
610
|
-
# like some broken web servers do.
|
611
|
-
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
|
612
|
-
end
|
613
|
-
else
|
614
|
-
body
|
615
|
-
end
|
616
|
-
end
|
617
|
-
|
618
|
-
def log_request
|
619
|
-
return unless RestClient.log
|
620
|
-
|
621
|
-
out = []
|
622
|
-
sanitized_url = begin
|
623
|
-
uri = URI.parse(url)
|
624
|
-
uri.password = "REDACTED" if uri.password
|
625
|
-
uri.to_s
|
626
|
-
rescue URI::InvalidURIError
|
627
|
-
# An attacker may be able to manipulate the URL to be
|
628
|
-
# invalid, which could force discloure of a password if
|
629
|
-
# we show any of the un-parsed URL here.
|
630
|
-
"[invalid uri]"
|
631
|
-
end
|
632
|
-
|
633
|
-
out << "RestClient.#{method} #{sanitized_url.inspect}"
|
634
|
-
out << payload.short_inspect if payload
|
635
|
-
out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
|
636
|
-
RestClient.log << out.join(', ') + "\n"
|
637
|
-
end
|
638
|
-
|
639
|
-
def log_response res
|
640
|
-
return unless RestClient.log
|
641
|
-
|
642
|
-
size = if @raw_response
|
643
|
-
File.size(@tf.path)
|
644
|
-
else
|
645
|
-
res.body.nil? ? 0 : res.body.size
|
646
|
-
end
|
647
|
-
|
648
|
-
RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
|
649
|
-
end
|
650
|
-
|
651
|
-
# Return a hash of headers whose keys are capitalized strings
|
652
|
-
def stringify_headers headers
|
653
|
-
headers.inject({}) do |result, (key, value)|
|
654
|
-
if key.is_a? Symbol
|
655
|
-
key = key.to_s.split(/_/).map(&:capitalize).join('-')
|
656
|
-
end
|
657
|
-
if 'CONTENT-TYPE' == key.upcase
|
658
|
-
result[key] = maybe_convert_extension(value.to_s)
|
659
|
-
elsif 'ACCEPT' == key.upcase
|
660
|
-
# Accept can be composed of several comma-separated values
|
661
|
-
if value.is_a? Array
|
662
|
-
target_values = value
|
663
|
-
else
|
664
|
-
target_values = value.to_s.split ','
|
665
|
-
end
|
666
|
-
result[key] = target_values.map { |ext|
|
667
|
-
maybe_convert_extension(ext.to_s.strip)
|
668
|
-
}.join(', ')
|
669
|
-
else
|
670
|
-
result[key] = value.to_s
|
671
|
-
end
|
672
|
-
result
|
673
|
-
end
|
674
|
-
end
|
675
|
-
|
676
|
-
def default_headers
|
677
|
-
{
|
678
|
-
:accept => '*/*',
|
679
|
-
:accept_encoding => 'gzip, deflate',
|
680
|
-
:user_agent => RestClient::Platform.default_user_agent,
|
681
|
-
}
|
682
|
-
end
|
683
|
-
|
684
|
-
private
|
685
|
-
|
686
859
|
def parser
|
687
860
|
URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
688
861
|
end
|