rest-client 2.0.0.rc2 → 2.0.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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