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.
@@ -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.any? do |_, v|
27
- case v
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 has_file_array?(params)
39
- params.any? do |v|
40
- case v
41
- when Hash
42
- has_file?(v)
43
- when Array
44
- has_file_array?(v)
45
- else
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
- alias :to_s :read
66
-
67
- # Flatten parameters by converting hashes of hashes to flat hashes
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 inspect
113
- result = to_s.inspect
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" : inspect)
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(flatten_params(params).collect do |entry|
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 = "--#{boundary}"
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
- if params.is_a? Hash
172
- x = flatten_params(params)
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 ||= rand(1_000_000).to_s
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, args, request)
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
@@ -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 will replace possible cookies in the :headers
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
- # TODO: rename timeout to read_timeout
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] or raise ArgumentError, "must pass :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
- @cookies = @headers.delete(:cookies) || args[:cookies] || {}
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
- # If there's no CA file, CA path, or cert store provided, use default
175
- if !ssl_ca_file && !ssl_ca_path && !@ssl_opts.include?(:cert_store)
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
- unless @ssl_opts.include?(:ciphers)
180
- # If we're on a Ruby version that has insecure default ciphers,
181
- # override it with our default list.
182
- if WeakDefaultCiphers.include?(
183
- OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.fetch(:ciphers))
184
- @ssl_opts[:ciphers] = DefaultCiphers
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
- request_uri = RUBY_VERSION >= "2.0.0" ? uri : uri.request_uri
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
- def process_url_params url, headers
219
- url_params = {}
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 'params' == key.to_s.downcase && value.is_a?(Hash)
222
- url_params.merge! value
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
- unless url_params.empty?
229
- query_string = url_params.collect { |k, v| "#{k.to_s}=#{CGI::escape(v.to_s)}" }.join('&')
230
- url + "?#{query_string}"
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
- def make_headers user_headers
237
- unless @cookies.empty?
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
- # Validate that the cookie names and values look sane. If you really
240
- # want to pass scary characters, just set the Cookie header directly.
241
- # RFC6265 is actually much more restrictive than we are.
242
- @cookies.each do |key, val|
243
- unless valid_cookie_key?(key)
244
- raise ArgumentError.new("Invalid cookie name: #{key.inspect}")
245
- end
246
- unless valid_cookie_value?(val)
247
- raise ArgumentError.new("Invalid cookie value: #{val.inspect}")
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
- user_headers = user_headers.dup
252
- user_headers[:cookie] = @cookies.map { |key, val| "#{key}=#{val}" }.sort.join('; ')
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
- headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
255
- headers.merge!(@payload.headers) if @payload
256
- headers
410
+
411
+ jar
257
412
  end
258
413
 
259
- # Do some sanity checks on cookie keys.
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
- # Properly it should be a valid TOKEN per RFC 2616, but lots of servers are
262
- # more liberal.
423
+ # @param [Hash] user_headers User-provided headers to include
263
424
  #
264
- # Disallow the empty string as well as keys containing control characters,
265
- # equals sign, semicolon, comma, or space.
425
+ # @return [Hash<String, String>] A hash of HTTP headers => values
266
426
  #
267
- def valid_cookie_key?(string)
268
- return false if string.empty?
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
- ! Regexp.new('[\x0-\x1f\x7f=;, ]').match(string)
271
- end
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
- # Validate cookie values. Rather than following RFC 6265, allow anything
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.to_s.capitalize)
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
- # Parse a string into a URI object. If the string has no HTTP-like scheme
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
- # @param url [String] A URL string.
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
- # @return [URI]
503
+ # @param [String] url A URL string.
340
504
  #
341
- # @raise URI::InvalidURIError on invalid URIs
505
+ # @return [String]
342
506
  #
343
- def parse_url(url)
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
- URI.parse(url)
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. This isn't needed in Ruby >= 2.0.
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 has OpenTimeout, ReadTimeout in Ruby >= 2.0
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.header['Content-Length'].to_i
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, args, self)
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, args, self)
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