rest-client 1.7.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  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 +199 -0
  14. data/lib/restclient/abstract_response.rb +196 -50
  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 +20 -1
  19. data/lib/restclient/raw_response.rb +21 -6
  20. data/lib/restclient/request.rb +572 -284
  21. data/lib/restclient/resource.rb +19 -9
  22. data/lib/restclient/response.rb +75 -9
  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 +13 -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 +29 -6
  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 -35
  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 -5
  44. data/spec/unit/request2_spec.rb +34 -12
  45. data/spec/unit/request_spec.rb +751 -418
  46. data/spec/unit/resource_spec.rb +31 -27
  47. data/spec/unit/response_spec.rb +144 -58
  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 +121 -70
  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
376
+ #
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
233
383
  #
234
- # Properly it should be a valid TOKEN per RFC 2616, but lots of servers are
235
- # more liberal.
384
+ # @param [Hash] user_headers User-provided headers to include
236
385
  #
237
- # Disallow the empty string as well as keys containing control characters,
238
- # equals sign, semicolon, comma, or space.
386
+ # @return [Hash<String, String>] A hash of HTTP headers => values
239
387
  #
240
- def valid_cookie_key?(string)
241
- return false if string.empty?
388
+ def make_headers(user_headers)
389
+ headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
390
+
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
242
407
 
243
- ! Regexp.new('[\x0-\x1f\x7f=;, ]').match(string)
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,9 +518,109 @@ 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
- if RestClient::Platform.mac?
623
+ if RestClient::Platform.mac_mri?
338
624
  warn('warning: ssl_verify_callback return code is ignored on OS X')
339
625
  warned = true
340
626
  end
@@ -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,143 +780,100 @@ 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)
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)
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)
496
- end
497
-
498
- end
499
-
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
- out << "RestClient.#{method} #{url.inspect}"
523
- out << payload.short_inspect if payload
524
- out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
525
- RestClient.log << out.join(', ') + "\n"
526
- end
527
-
528
- def log_response res
529
- return unless RestClient.log
530
-
531
- size = if @raw_response
532
- File.size(@tf.path)
533
- else
534
- res.body.nil? ? 0 : res.body.size
535
- end
536
-
537
- RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
538
- end
539
-
540
- # Return a hash of headers whose keys are capitalized strings
541
- def stringify_headers headers
542
- headers.inject({}) do |result, (key, value)|
543
- if key.is_a? Symbol
544
- key = key.to_s.split(/_/).map { |w| w.capitalize }.join('-')
545
- end
546
- if 'CONTENT-TYPE' == key.upcase
547
- target_value = value.to_s
548
- result[key] = MIME::Types.type_for_extension target_value
549
- elsif 'ACCEPT' == key.upcase
550
- # Accept can be composed of several comma-separated values
551
- if value.is_a? Array
552
- target_values = value
553
- else
554
- target_values = value.to_s.split ','
555
- end
556
- result[key] = target_values.map { |ext| MIME::Types.type_for_extension(ext.to_s.strip) }.join(', ')
557
- else
558
- result[key] = value.to_s
559
- end
560
- result
836
+ response.return!(&block)
561
837
  end
562
- end
563
838
 
564
- def default_headers
565
- {:accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate'}
566
839
  end
567
840
 
568
- private
569
-
570
841
  def parser
571
842
  URI.const_defined?(:Parser) ? URI::Parser.new : URI
572
843
  end
573
844
 
574
- end
575
- end
576
-
577
- module MIME
578
- class Types
579
-
580
- # Return the first found content-type for a value considered as an extension or the value itself
581
- def type_for_extension ext
582
- candidates = @extension_index[ext]
583
- candidates.empty? ? ext : candidates[0].content_type
584
- end
845
+ # Given a MIME type or file extension, return either a MIME type or, if
846
+ # none is found, the input unchanged.
847
+ #
848
+ # >> maybe_convert_extension('json')
849
+ # => 'application/json'
850
+ #
851
+ # >> maybe_convert_extension('unknown')
852
+ # => 'unknown'
853
+ #
854
+ # >> maybe_convert_extension('application/xml')
855
+ # => 'application/xml'
856
+ #
857
+ # @param ext [String]
858
+ #
859
+ # @return [String]
860
+ #
861
+ def maybe_convert_extension(ext)
862
+ unless ext =~ /\A[a-zA-Z0-9_@-]+\z/
863
+ # Don't look up strings unless they look like they could be a file
864
+ # extension known to mime-types.
865
+ #
866
+ # There currently isn't any API public way to look up extensions
867
+ # directly out of MIME::Types, but the type_for() method only strips
868
+ # off after a period anyway.
869
+ return ext
870
+ end
585
871
 
586
- class << self
587
- def type_for_extension ext
588
- @__types__.type_for_extension ext
872
+ types = MIME::Types.type_for(ext)
873
+ if types.empty?
874
+ ext
875
+ else
876
+ types.first.content_type
589
877
  end
590
878
  end
591
879
  end