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.
Files changed (55) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -0
  3. data/.mailmap +10 -0
  4. data/.rspec +2 -1
  5. data/.rubocop +2 -0
  6. data/.rubocop-disables.yml +386 -0
  7. data/.rubocop.yml +8 -0
  8. data/.travis.yml +56 -8
  9. data/AUTHORS +26 -1
  10. data/README.md +901 -0
  11. data/Rakefile +27 -3
  12. data/bin/restclient +3 -5
  13. data/history.md +181 -0
  14. data/lib/restclient/abstract_response.rb +172 -55
  15. data/lib/restclient/exceptions.rb +96 -55
  16. data/lib/restclient/params_array.rb +72 -0
  17. data/lib/restclient/payload.rb +70 -74
  18. data/lib/restclient/platform.rb +19 -0
  19. data/lib/restclient/raw_response.rb +21 -7
  20. data/lib/restclient/request.rb +540 -281
  21. data/lib/restclient/resource.rb +19 -9
  22. data/lib/restclient/response.rb +75 -6
  23. data/lib/restclient/utils.rb +274 -0
  24. data/lib/restclient/version.rb +2 -1
  25. data/lib/restclient.rb +21 -3
  26. data/rest-client.gemspec +12 -10
  27. data/spec/ISS.jpg +0 -0
  28. data/spec/helpers.rb +54 -0
  29. data/spec/integration/_lib.rb +1 -0
  30. data/spec/integration/capath_digicert/3513523f.0 +22 -0
  31. data/spec/integration/capath_digicert/399e7759.0 +22 -0
  32. data/spec/integration/capath_digicert/digicert.crt +20 -17
  33. data/spec/integration/certs/digicert.crt +20 -17
  34. data/spec/integration/httpbin_spec.rb +128 -0
  35. data/spec/integration/integration_spec.rb +97 -14
  36. data/spec/integration/request_spec.rb +25 -2
  37. data/spec/spec_helper.rb +28 -1
  38. data/spec/unit/_lib.rb +1 -0
  39. data/spec/unit/abstract_response_spec.rb +95 -38
  40. data/spec/unit/exceptions_spec.rb +41 -28
  41. data/spec/unit/params_array_spec.rb +36 -0
  42. data/spec/unit/payload_spec.rb +118 -68
  43. data/spec/unit/raw_response_spec.rb +10 -6
  44. data/spec/unit/request2_spec.rb +34 -12
  45. data/spec/unit/request_spec.rb +745 -424
  46. data/spec/unit/resource_spec.rb +31 -27
  47. data/spec/unit/response_spec.rb +134 -57
  48. data/spec/unit/restclient_spec.rb +16 -15
  49. data/spec/unit/utils_spec.rb +147 -0
  50. data/spec/unit/windows/root_certs_spec.rb +3 -3
  51. metadata +79 -29
  52. data/README.rdoc +0 -324
  53. data/spec/integration/capath_digicert/244b5494.0 +0 -19
  54. data/spec/integration/capath_digicert/81b9768f.0 +0 -19
  55. data/spec/unit/master_shake.jpg +0 -0
@@ -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 will replace possible cookies in the :headers
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
- # * :timeout and :open_timeout are how long to wait for a response and to
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, :cookies,
36
- :payload, :user, :password, :timeout, :max_redirects,
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] or raise ArgumentError, "must pass :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
- @cookies = @headers.delete(:cookies) || args[:cookies] || {}
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
- @user = args[:user]
116
- @password = args[:password]
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
- @timeout = args[:timeout]
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
- # If there's no CA file, CA path, or cert store provided, use default
155
- if !ssl_ca_file && !ssl_ca_path && !@ssl_opts.include?(:cert_store)
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
- unless @ssl_opts.include?(:ciphers)
160
- # If we're on a Ruby version that has insecure default ciphers,
161
- # override it with our default list.
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
- @tf = nil # If you are a raw request, this is your tempfile
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
- uri = parse_url_with_auth(url)
176
- transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload, & block
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
- def process_url_params url, headers
193
- url_params = {}
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 'params' == key.to_s.downcase && value.is_a?(Hash)
196
- url_params.merge! value
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
- unless url_params.empty?
203
- query_string = url_params.collect { |k, v| "#{k.to_s}=#{CGI::escape(v.to_s)}" }.join('&')
204
- url + "?#{query_string}"
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
- def make_headers user_headers
211
- unless @cookies.empty?
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
- # Validate that the cookie names and values look sane. If you really
214
- # want to pass scary characters, just set the Cookie header directly.
215
- # RFC6265 is actually much more restrictive than we are.
216
- @cookies.each do |key, val|
217
- unless valid_cookie_key?(key)
218
- raise ArgumentError.new("Invalid cookie name: #{key.inspect}")
219
- end
220
- unless valid_cookie_value?(val)
221
- raise ArgumentError.new("Invalid cookie value: #{val.inspect}")
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
- user_headers[:cookie] = @cookies.map { |key, val| "#{key}=#{val}" }.sort.join('; ')
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
- headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
228
- headers.merge!(@payload.headers) if @payload
229
- headers
364
+
365
+ jar
230
366
  end
231
367
 
232
- # Do some sanity checks on cookie keys.
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
- # Properly it should be a valid TOKEN per RFC 2616, but lots of servers are
235
- # more liberal.
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
- # Disallow the empty string as well as keys containing control characters,
238
- # equals sign, semicolon, comma, or space.
384
+ # @param [Hash] user_headers User-provided headers to include
239
385
  #
240
- def valid_cookie_key?(string)
241
- return false if string.empty?
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
- ! Regexp.new('[\x0-\x1f\x7f=;, ]').match(string)
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
- # Validate cookie values. Rather than following RFC 6265, allow anything
247
- # but control characters, comma, and semicolon.
248
- def valid_cookie_value?(value)
249
- ! Regexp.new('[\x0-\x1f\x7f,;]').match(value)
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 net_http_class
253
- if RestClient.proxy
254
- proxy_uri = URI.parse(RestClient.proxy)
255
- Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
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.to_s.capitalize)
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 != nil && body.respond_to?(:read)
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
- def parse_url(url)
275
- url = "http://#{url}" unless url.match(/^http/)
276
- URI.parse(url)
277
- end
278
-
279
- def parse_url_with_auth(url)
280
- uri = parse_url(url)
281
- @user = CGI.unescape(uri.user) if uri.user
282
- @password = CGI.unescape(uri.password) if uri.password
283
- if !@user && !@password
284
- @user, @password = Netrc.read[uri.host]
285
- end
286
- uri
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 = net_http_class.new(uri.host, uri.port)
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? @timeout
392
- if @timeout == -1
393
- warn 'To disable read timeouts, please set timeout to nil instead of -1'
394
- @timeout = nil
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 = @timeout
704
+ net.read_timeout = @read_timeout
397
705
  end
398
706
  if defined? @open_timeout
399
707
  if @open_timeout == -1
400
- warn 'To disable open timeouts, please set open_timeout to nil instead of -1'
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 ? payload.to_s : nil,
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 ? payload.to_s : nil) \
419
- { |http_response| fetch_body(http_response) }
420
- log_response res
421
- process_result res, & block
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 Timeout::Error, Errno::ETIMEDOUT
427
- raise RestClient::RequestTimeout
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
- req.basic_auth(user, password) if user
783
+ if user && !@processed_headers_lowercase.include?('authorization')
784
+ req.basic_auth(user, password)
785
+ end
453
786
  end
454
787
 
455
- def fetch_body(http_response)
456
- if @raw_response
457
- # Taken from Chef, which as in turn...
458
- # Stolen from http://www.ruby-forum.com/topic/166423
459
- # Kudos to _why!
460
- @tf = Tempfile.new("rest-client")
461
- @tf.binmode
462
- size, total = 0, http_response.header['Content-Length'].to_i
463
- http_response.read_body do |chunk|
464
- @tf.write chunk
465
- size += chunk.size
466
- if RestClient.log
467
- if size == 0
468
- RestClient.log << "%s %s done (0 length file)\n" % [@method, @url]
469
- elsif total == 0
470
- RestClient.log << "%s %s (zero content length)\n" % [@method, @url]
471
- else
472
- RestClient.log << "%s %s %d%% done (%d of %d)\n" % [@method, @url, (size * 100) / total, size, total]
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
- http_response
815
+ tf.close
816
+ tf
482
817
  end
483
818
 
484
- def process_result res, & block
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
- # We don't decode raw requests
487
- response = RawResponse.new(@tf, res, args, self)
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(Request.decode(res['content-encoding'], res.body), res, args, self)
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!(self, res, & block)
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