rest-client 1.6.7 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.mailmap +10 -0
  4. data/.rspec +2 -0
  5. data/.rubocop +2 -0
  6. data/.rubocop-disables.yml +386 -0
  7. data/.rubocop.yml +8 -0
  8. data/.travis.yml +62 -0
  9. data/AUTHORS +106 -0
  10. data/Gemfile +11 -0
  11. data/LICENSE +21 -0
  12. data/README.md +901 -0
  13. data/Rakefile +109 -35
  14. data/bin/restclient +11 -12
  15. data/history.md +244 -1
  16. data/lib/restclient.rb +27 -18
  17. data/lib/restclient/abstract_response.rb +197 -51
  18. data/lib/restclient/exceptions.rb +110 -59
  19. data/lib/restclient/params_array.rb +72 -0
  20. data/lib/restclient/payload.rb +74 -75
  21. data/lib/restclient/platform.rb +49 -0
  22. data/lib/restclient/raw_response.rb +21 -6
  23. data/lib/restclient/request.rb +747 -183
  24. data/lib/restclient/resource.rb +22 -13
  25. data/lib/restclient/response.rb +75 -9
  26. data/lib/restclient/utils.rb +274 -0
  27. data/lib/restclient/version.rb +8 -0
  28. data/lib/restclient/windows.rb +8 -0
  29. data/lib/restclient/windows/root_certs.rb +105 -0
  30. data/rest-client.gemspec +32 -0
  31. data/rest-client.windows.gemspec +19 -0
  32. data/spec/ISS.jpg +0 -0
  33. data/spec/helpers.rb +54 -0
  34. data/spec/integration/_lib.rb +1 -0
  35. data/spec/integration/capath_digicert/3513523f.0 +22 -0
  36. data/spec/integration/capath_digicert/399e7759.0 +22 -0
  37. data/spec/integration/capath_digicert/README +8 -0
  38. data/spec/integration/capath_digicert/digicert.crt +22 -0
  39. data/spec/integration/capath_verisign/415660c1.0 +14 -0
  40. data/spec/integration/capath_verisign/7651b327.0 +14 -0
  41. data/spec/integration/capath_verisign/README +8 -0
  42. data/spec/integration/capath_verisign/verisign.crt +14 -0
  43. data/spec/integration/certs/digicert.crt +22 -0
  44. data/spec/integration/httpbin_spec.rb +128 -0
  45. data/spec/integration/integration_spec.rb +118 -0
  46. data/spec/integration/request_spec.rb +109 -7
  47. data/spec/spec_helper.rb +29 -0
  48. data/spec/unit/_lib.rb +1 -0
  49. data/spec/unit/abstract_response_spec.rb +145 -0
  50. data/spec/unit/exceptions_spec.rb +108 -0
  51. data/spec/unit/params_array_spec.rb +36 -0
  52. data/spec/unit/payload_spec.rb +295 -0
  53. data/spec/unit/raw_response_spec.rb +22 -0
  54. data/spec/unit/request2_spec.rb +54 -0
  55. data/spec/unit/request_spec.rb +1238 -0
  56. data/spec/unit/resource_spec.rb +134 -0
  57. data/spec/unit/response_spec.rb +252 -0
  58. data/spec/unit/restclient_spec.rb +80 -0
  59. data/spec/unit/utils_spec.rb +147 -0
  60. data/spec/unit/windows/root_certs_spec.rb +22 -0
  61. metadata +265 -117
  62. data/README.rdoc +0 -285
  63. data/VERSION +0 -1
  64. data/lib/restclient/net_http_ext.rb +0 -55
  65. data/spec/abstract_response_spec.rb +0 -85
  66. data/spec/base.rb +0 -16
  67. data/spec/exceptions_spec.rb +0 -98
  68. data/spec/integration/certs/equifax.crt +0 -19
  69. data/spec/integration_spec.rb +0 -38
  70. data/spec/master_shake.jpg +0 -0
  71. data/spec/payload_spec.rb +0 -234
  72. data/spec/raw_response_spec.rb +0 -17
  73. data/spec/request2_spec.rb +0 -40
  74. data/spec/request_spec.rb +0 -529
  75. data/spec/resource_spec.rb +0 -134
  76. data/spec/response_spec.rb +0 -169
  77. data/spec/restclient_spec.rb +0 -73
@@ -0,0 +1,72 @@
1
+ module RestClient
2
+
3
+ # The ParamsArray class is used to represent an ordered list of [key, value]
4
+ # pairs. Use this when you need to include a key multiple times or want
5
+ # explicit control over parameter ordering.
6
+ #
7
+ # Most of the request payload & parameter functions normally accept a Hash of
8
+ # keys => values, which does not allow for duplicated keys.
9
+ #
10
+ # @see RestClient::Utils.encode_query_string
11
+ # @see RestClient::Utils.flatten_params
12
+ #
13
+ class ParamsArray
14
+ include Enumerable
15
+
16
+ # @param array [Array<Array>] An array of parameter key,value pairs. These
17
+ # pairs may be 2 element arrays [key, value] or single element hashes
18
+ # {key => value}. They may also be single element arrays to represent a
19
+ # key with no value.
20
+ #
21
+ # @example
22
+ # >> ParamsArray.new([[:foo, 123], [:foo, 456], [:bar, 789]])
23
+ # This will be encoded as "foo=123&foo=456&bar=789"
24
+ #
25
+ # @example
26
+ # >> ParamsArray.new({foo: 123, bar: 456})
27
+ # This is valid, but there's no reason not to just use the Hash directly
28
+ # instead of a ParamsArray.
29
+ #
30
+ #
31
+ def initialize(array)
32
+ @array = process_input(array)
33
+ end
34
+
35
+ def each(*args, &blk)
36
+ @array.each(*args, &blk)
37
+ end
38
+
39
+ def empty?
40
+ @array.empty?
41
+ end
42
+
43
+ private
44
+
45
+ def process_input(array)
46
+ array.map {|v| process_pair(v) }
47
+ end
48
+
49
+ # A pair may be:
50
+ # - A single element hash, e.g. {foo: 'bar'}
51
+ # - A two element array, e.g. ['foo', 'bar']
52
+ # - A one element array, e.g. ['foo']
53
+ #
54
+ def process_pair(pair)
55
+ case pair
56
+ when Hash
57
+ if pair.length != 1
58
+ raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}")
59
+ end
60
+ pair.to_a.fetch(0)
61
+ when Array
62
+ if pair.length > 2
63
+ raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}")
64
+ end
65
+ [pair.fetch(0), pair[1]]
66
+ else
67
+ # recurse, converting any non-array to an array
68
+ process_pair(pair.to_a)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,50 +1,58 @@
1
1
  require 'tempfile'
2
+ require 'securerandom'
2
3
  require 'stringio'
3
- require 'mime/types'
4
+
5
+ begin
6
+ # Use mime/types/columnar if available, for reduced memory usage
7
+ require 'mime/types/columnar'
8
+ rescue LoadError
9
+ require 'mime/types'
10
+ end
4
11
 
5
12
  module RestClient
6
13
  module Payload
7
14
  extend self
8
15
 
9
16
  def generate(params)
10
- if params.is_a?(String)
17
+ if params.is_a?(RestClient::Payload::Base)
18
+ # pass through Payload objects unchanged
19
+ params
20
+ elsif params.is_a?(String)
11
21
  Base.new(params)
12
- elsif params.respond_to?(:read)
13
- Streamed.new(params)
14
- elsif params
22
+ elsif params.is_a?(Hash)
15
23
  if params.delete(:multipart) == true || has_file?(params)
16
24
  Multipart.new(params)
17
25
  else
18
26
  UrlEncoded.new(params)
19
27
  end
28
+ elsif params.is_a?(ParamsArray)
29
+ if _has_file?(params)
30
+ Multipart.new(params)
31
+ else
32
+ UrlEncoded.new(params)
33
+ end
34
+ elsif params.respond_to?(:read)
35
+ Streamed.new(params)
20
36
  else
21
37
  nil
22
38
  end
23
39
  end
24
40
 
25
41
  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
42
+ unless params.is_a?(Hash)
43
+ raise ArgumentError.new("Must pass Hash, not #{params.inspect}")
35
44
  end
45
+ _has_file?(params)
36
46
  end
37
47
 
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
48
+ def _has_file?(obj)
49
+ case obj
50
+ when Hash, ParamsArray
51
+ obj.any? {|_, v| _has_file?(v) }
52
+ when Array
53
+ obj.any? {|v| _has_file?(v) }
54
+ else
55
+ obj.respond_to?(:path) && obj.respond_to?(:read)
48
56
  end
49
57
  end
50
58
 
@@ -58,40 +66,13 @@ module RestClient
58
66
  @stream.seek(0)
59
67
  end
60
68
 
61
- def read(bytes=nil)
62
- @stream.read(bytes)
63
- end
64
-
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
69
+ def read(*args)
70
+ @stream.read(*args)
82
71
  end
83
72
 
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
73
+ def to_s
74
+ result = read
75
+ @stream.seek(0)
95
76
  result
96
77
  end
97
78
 
@@ -109,14 +90,20 @@ module RestClient
109
90
  @stream.close unless @stream.closed?
110
91
  end
111
92
 
112
- def inspect
113
- result = to_s.inspect
114
- @stream.seek(0)
115
- result
93
+ def closed?
94
+ @stream.closed?
95
+ end
96
+
97
+ def to_s_inspect
98
+ to_s.inspect
116
99
  end
117
100
 
118
101
  def short_inspect
119
- (size > 500 ? "#{size} byte(s) length" : inspect)
102
+ if size && size > 500
103
+ "#{size} byte(s) length"
104
+ else
105
+ to_s_inspect
106
+ end
120
107
  end
121
108
 
122
109
  end
@@ -134,22 +121,18 @@ module RestClient
134
121
  end
135
122
  end
136
123
 
124
+ # TODO (breaks compatibility): ought to use mime_for() to autodetect the
125
+ # Content-Type for stream objects that have a filename.
126
+
137
127
  alias :length :size
138
128
  end
139
129
 
140
130
  class UrlEncoded < Base
141
131
  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("&"))
132
+ @stream = StringIO.new(Utils.encode_query_string(params))
145
133
  @stream.seek(0)
146
134
  end
147
135
 
148
- # for UrlEncoded escape the keys
149
- def handle_key key
150
- URI.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
151
- end
152
-
153
136
  def headers
154
137
  super.merge({'Content-Type' => 'application/x-www-form-urlencoded'})
155
138
  end
@@ -159,14 +142,15 @@ module RestClient
159
142
  EOL = "\r\n"
160
143
 
161
144
  def build_stream(params)
162
- b = "--#{boundary}"
145
+ b = '--' + boundary
163
146
 
164
- @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
147
+ @stream = Tempfile.new('rest-client.multipart.')
165
148
  @stream.binmode
166
149
  @stream.write(b + EOL)
167
150
 
168
- if params.is_a? Hash
169
- x = flatten_params(params)
151
+ case params
152
+ when Hash, ParamsArray
153
+ x = Utils.flatten_params(params)
170
154
  else
171
155
  x = params
172
156
  end
@@ -201,7 +185,7 @@ module RestClient
201
185
  s.write(" filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
202
186
  s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : mime_for(v.path)}#{EOL}")
203
187
  s.write(EOL)
204
- while data = v.read(8124)
188
+ while (data = v.read(8124))
205
189
  s.write(data)
206
190
  end
207
191
  ensure
@@ -215,10 +199,25 @@ module RestClient
215
199
  end
216
200
 
217
201
  def boundary
218
- @boundary ||= rand(1_000_000).to_s
202
+ return @boundary if defined?(@boundary) && @boundary
203
+
204
+ # Use the same algorithm used by WebKit: generate 16 random
205
+ # alphanumeric characters, replacing `+` `/` with `A` `B` (included in
206
+ # the list twice) to round out the set of 64.
207
+ s = SecureRandom.base64(12)
208
+ s.tr!('+/', 'AB')
209
+
210
+ @boundary = '----RubyFormBoundary' + s
219
211
  end
220
212
 
221
213
  # for Multipart do not escape the keys
214
+ #
215
+ # Ostensibly multipart keys MAY be percent encoded per RFC 7578, but in
216
+ # practice no major browser that I'm aware of uses percent encoding.
217
+ #
218
+ # Further discussion of multipart encoding:
219
+ # https://github.com/rest-client/rest-client/pull/403#issuecomment-156976930
220
+ #
222
221
  def handle_key key
223
222
  key
224
223
  end
@@ -0,0 +1,49 @@
1
+ require 'rbconfig'
2
+
3
+ module RestClient
4
+ module Platform
5
+ # Return true if we are running on a darwin-based Ruby platform. This will
6
+ # be false for jruby even on OS X.
7
+ #
8
+ # @return [Boolean]
9
+ def self.mac_mri?
10
+ RUBY_PLATFORM.include?('darwin')
11
+ end
12
+
13
+ # Return true if we are running on Windows.
14
+ #
15
+ # @return [Boolean]
16
+ #
17
+ def self.windows?
18
+ # Ruby only sets File::ALT_SEPARATOR on Windows, and the Ruby standard
19
+ # library uses that to test what platform it's on.
20
+ !!File::ALT_SEPARATOR
21
+ end
22
+
23
+ # Return true if we are running on jruby.
24
+ #
25
+ # @return [Boolean]
26
+ #
27
+ def self.jruby?
28
+ # defined on mri >= 1.9
29
+ RUBY_ENGINE == 'jruby'
30
+ end
31
+
32
+ def self.architecture
33
+ "#{RbConfig::CONFIG['host_os']} #{RbConfig::CONFIG['host_cpu']}"
34
+ end
35
+
36
+ def self.ruby_agent_version
37
+ case RUBY_ENGINE
38
+ when 'jruby'
39
+ "jruby/#{JRUBY_VERSION} (#{RUBY_VERSION}p#{RUBY_PATCHLEVEL})"
40
+ else
41
+ "#{RUBY_ENGINE}/#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}"
42
+ end
43
+ end
44
+
45
+ def self.default_user_agent
46
+ "rest-client/#{VERSION} (#{architecture}) #{ruby_agent_version}"
47
+ end
48
+ end
49
+ end
@@ -13,21 +13,36 @@ module RestClient
13
13
 
14
14
  include AbstractResponse
15
15
 
16
- attr_reader :file
16
+ attr_reader :file, :request, :start_time, :end_time
17
17
 
18
- def initialize tempfile, net_http_res, args
19
- @net_http_res = net_http_res
20
- @args = args
18
+ def inspect
19
+ "<RestClient::RawResponse @code=#{code.inspect}, @file=#{file.inspect}, @request=#{request.inspect}>"
20
+ end
21
+
22
+ # @param [Tempfile] tempfile The temporary file containing the body
23
+ # @param [Net::HTTPResponse] net_http_res
24
+ # @param [RestClient::Request] request
25
+ # @param [Time] start_time
26
+ def initialize(tempfile, net_http_res, request, start_time=nil)
21
27
  @file = tempfile
28
+
29
+ # reopen the tempfile so we can read it
30
+ @file.open
31
+
32
+ response_set_vars(net_http_res, request, start_time)
22
33
  end
23
34
 
24
35
  def to_s
25
- @file.open
36
+ body
37
+ end
38
+
39
+ def body
40
+ @file.rewind
26
41
  @file.read
27
42
  end
28
43
 
29
44
  def size
30
- File.size file
45
+ file.size
31
46
  end
32
47
 
33
48
  end
@@ -1,6 +1,14 @@
1
1
  require 'tempfile'
2
- require 'mime/types'
3
2
  require 'cgi'
3
+ require 'netrc'
4
+ require 'set'
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
4
12
 
5
13
  module RestClient
6
14
  # This class is used internally by RestClient to send the request, but you can also
@@ -14,268 +22,546 @@ module RestClient
14
22
  # * :url
15
23
  # Optional parameters (have a look at ssl and/or uri for some explanations):
16
24
  # * :headers a hash containing the request headers
17
- # * :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.
18
28
  # * :user and :password for basic auth, will be replaced by a user/password available in the :url
19
29
  # * :block_response call the provided block with the HTTPResponse as parameter
20
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.
21
36
  # * :max_redirects maximum number of redirections (default to 10)
22
- # * :verify_ssl enable ssl verification, possible values are constants from OpenSSL::SSL
23
- # * :timeout and :open_timeout passing in -1 will disable the timeout by setting the corresponding net timeout values to nil
24
- # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file
37
+ # * :proxy An HTTP proxy URI to use for this request. Any value here
38
+ # (including nil) will override RestClient.proxy.
39
+ # * :verify_ssl enable ssl verification, possible values are constants from
40
+ # OpenSSL::SSL::VERIFY_*, defaults to OpenSSL::SSL::VERIFY_PEER
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
44
+ # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file, :ssl_ca_path,
45
+ # :ssl_cert_store, :ssl_verify_callback, :ssl_verify_callback_warnings
46
+ # * :ssl_version specifies the SSL version for the underlying Net::HTTP connection
47
+ # * :ssl_ciphers sets SSL ciphers for the connection. See
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.
25
52
  class Request
26
53
 
27
- attr_reader :method, :url, :headers, :cookies,
28
- :payload, :user, :password, :timeout, :max_redirects,
29
- :open_timeout, :raw_response, :verify_ssl, :ssl_client_cert,
30
- :ssl_client_key, :ssl_ca_file, :processed_headers, :args
54
+ attr_reader :method, :uri, :url, :headers, :payload, :proxy,
55
+ :user, :password, :read_timeout, :max_redirects,
56
+ :open_timeout, :raw_response, :processed_headers, :args,
57
+ :ssl_opts
58
+
59
+ # An array of previous redirection responses
60
+ attr_accessor :redirection_history
31
61
 
32
62
  def self.execute(args, & block)
33
63
  new(args).execute(& block)
34
64
  end
35
65
 
66
+ SSLOptionList = %w{client_cert client_key ca_file ca_path cert_store
67
+ version ciphers verify_callback verify_callback_warnings}
68
+
69
+ def inspect
70
+ "<RestClient::Request @method=#{@method.inspect}, @url=#{@url.inspect}>"
71
+ end
72
+
36
73
  def initialize args
37
- @method = args[:method] or raise ArgumentError, "must pass :method"
38
- @headers = args[:headers] || {}
74
+ @method = normalize_method(args[:method])
75
+ @headers = (args[:headers] || {}).dup
39
76
  if args[:url]
40
- @url = process_url_params(args[:url], headers)
77
+ @url = process_url_params(normalize_url(args[:url]), headers)
41
78
  else
42
79
  raise ArgumentError, "must pass :url"
43
80
  end
44
- @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
+
45
88
  @payload = Payload.generate(args[:payload])
46
- @user = args[:user]
47
- @password = args[:password]
48
- @timeout = args[:timeout]
49
- @open_timeout = args[:open_timeout]
89
+
90
+ @user = args[:user] if args.include?(:user)
91
+ @password = args[:password] if args.include?(:password)
92
+
93
+ if args.include?(: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]
99
+ end
100
+ if args.include?(:open_timeout)
101
+ @open_timeout = args[:open_timeout]
102
+ end
50
103
  @block_response = args[:block_response]
51
104
  @raw_response = args[:raw_response] || false
52
- @verify_ssl = args[:verify_ssl] || false
53
- @ssl_client_cert = args[:ssl_client_cert] || nil
54
- @ssl_client_key = args[:ssl_client_key] || nil
55
- @ssl_ca_file = args[:ssl_ca_file] || nil
56
- @tf = nil # If you are a raw request, this is your tempfile
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
+
114
+ @ssl_opts = {}
115
+
116
+ if args.include?(:verify_ssl)
117
+ v_ssl = args.fetch(:verify_ssl)
118
+ if v_ssl
119
+ if v_ssl == true
120
+ # interpret :verify_ssl => true as VERIFY_PEER
121
+ @ssl_opts[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
122
+ else
123
+ # otherwise pass through any truthy values
124
+ @ssl_opts[:verify_ssl] = v_ssl
125
+ end
126
+ else
127
+ # interpret all falsy :verify_ssl values as VERIFY_NONE
128
+ @ssl_opts[:verify_ssl] = OpenSSL::SSL::VERIFY_NONE
129
+ end
130
+ else
131
+ # if :verify_ssl was not passed, default to VERIFY_PEER
132
+ @ssl_opts[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
133
+ end
134
+
135
+ SSLOptionList.each do |key|
136
+ source_key = ('ssl_' + key).to_sym
137
+ if args.has_key?(source_key)
138
+ @ssl_opts[key.to_sym] = args.fetch(source_key)
139
+ end
140
+ end
141
+
142
+ # Set some other default SSL options, but only if we have an HTTPS URI.
143
+ if use_ssl?
144
+
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
148
+ end
149
+ end
150
+
151
+ @log = args[:log]
57
152
  @max_redirects = args[:max_redirects] || 10
58
153
  @processed_headers = make_headers headers
154
+ @processed_headers_lowercase = Hash[@processed_headers.map {|k, v| [k.downcase, v]}]
59
155
  @args = args
156
+
157
+ @before_execution_proc = args[:before_execution_proc]
60
158
  end
61
159
 
62
160
  def execute & block
63
- uri = parse_url_with_auth(url)
64
- 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
65
164
  ensure
66
165
  payload.close if payload
67
166
  end
68
167
 
168
+ # SSL-related options
169
+ def verify_ssl
170
+ @ssl_opts.fetch(:verify_ssl)
171
+ end
172
+ SSLOptionList.each do |key|
173
+ define_method('ssl_' + key) do
174
+ @ssl_opts[key.to_sym]
175
+ end
176
+ end
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
+
69
186
  # Extract the query parameters and append them to the url
70
- def process_url_params url, headers
71
- 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
72
204
  headers.delete_if do |key, value|
73
- if 'params' == key.to_s.downcase && value.is_a?(Hash)
74
- 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
75
211
  true
76
212
  else
77
213
  false
78
214
  end
79
215
  end
80
- unless url_params.empty?
81
- query_string = url_params.collect { |k, v| "#{k.to_s}=#{CGI::escape(v.to_s)}" }.join('&')
82
- 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
83
226
  else
84
227
  url
85
228
  end
86
229
  end
87
230
 
88
- def make_headers user_headers
89
- unless @cookies.empty?
90
- user_headers[:cookie] = @cookies.map { |(key, val)| "#{key.to_s}=#{CGI::unescape(val)}" }.sort.join('; ')
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 = {}
242
+
243
+ @cookie_jar.cookies(uri).each do |c|
244
+ hash[c.name] = c.value
91
245
  end
92
- headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
93
- headers.merge!(@payload.headers) if @payload
94
- headers
95
- end
96
246
 
97
- def net_http_class
98
- if RestClient.proxy
99
- proxy_uri = URI.parse(RestClient.proxy)
100
- Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
101
- else
102
- Net::HTTP
103
- end
247
+ hash
104
248
  end
105
249
 
106
- def net_http_request_class(method)
107
- Net::HTTP.const_get(method.to_s.capitalize)
250
+ # @return [HTTP::CookieJar]
251
+ def cookie_jar
252
+ @cookie_jar
108
253
  end
109
254
 
110
- def parse_url(url)
111
- url = "http://#{url}" unless url.match(/^http/)
112
- URI.parse(url)
113
- end
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?
114
264
 
115
- def parse_url_with_auth(url)
116
- uri = parse_url(url)
117
- @user = CGI.unescape(uri.user) if uri.user
118
- @password = CGI.unescape(uri.password) if uri.password
119
- uri
265
+ arr = cookie_jar.cookies(url)
266
+ return nil if arr.empty?
267
+
268
+ return HTTP::Cookie.cookie_value(arr)
120
269
  end
121
270
 
122
- def process_payload(p=nil, parent_key=nil)
123
- unless p.is_a?(Hash)
124
- p
125
- else
126
- @headers[:content_type] ||= 'application/x-www-form-urlencoded'
127
- p.keys.map do |k|
128
- key = parent_key ? "#{parent_key}[#{k}]" : k
129
- if p[k].is_a? Hash
130
- process_payload(p[k], key)
131
- else
132
- value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
133
- "#{key}=#{value}"
134
- end
135
- end.join("&")
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")
136
329
  end
137
- end
138
330
 
139
- def transmit uri, req, payload, & block
140
- setup_credentials req
331
+ cookies_data = headers.delete(:cookies) || args[:cookies]
141
332
 
142
- net = net_http_class.new(uri.host, uri.port)
143
- net.use_ssl = uri.is_a?(URI::HTTPS)
144
- if (@verify_ssl == false) || (@verify_ssl == OpenSSL::SSL::VERIFY_NONE)
145
- net.verify_mode = OpenSSL::SSL::VERIFY_NONE
146
- elsif @verify_ssl.is_a? Integer
147
- net.verify_mode = @verify_ssl
148
- net.verify_callback = lambda do |preverify_ok, ssl_context|
149
- if (!preverify_ok) || ssl_context.error != 0
150
- err_msg = "SSL Verification failed -- Preverify: #{preverify_ok}, Error: #{ssl_context.error_string} (#{ssl_context.error})"
151
- raise SSLCertificateNotVerified.new(err_msg)
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}")
152
349
  end
153
- true
350
+
351
+ jar.add(key)
352
+ next
154
353
  end
155
- end
156
- net.cert = @ssl_client_cert if @ssl_client_cert
157
- net.key = @ssl_client_key if @ssl_client_key
158
- net.ca_file = @ssl_ca_file if @ssl_ca_file
159
- net.read_timeout = @timeout if @timeout
160
- net.open_timeout = @open_timeout if @open_timeout
161
354
 
162
- # disable the timeout if the timeout value is -1
163
- net.read_timeout = nil if @timeout == -1
164
- net.out_timeout = nil if @open_timeout == -1
355
+ if key.is_a?(Symbol)
356
+ key = key.to_s
357
+ end
165
358
 
166
- RestClient.before_execution_procs.each do |before_proc|
167
- before_proc.call(req, args)
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))
168
363
  end
169
364
 
170
- log_request
365
+ jar
366
+ end
171
367
 
172
- net.start do |http|
173
- if @block_response
174
- http.request(req, payload ? payload.to_s : nil, & @block_response)
175
- else
176
- res = http.request(req, payload ? payload.to_s : nil) { |http_response| fetch_body(http_response) }
177
- log_response res
178
- process_result res, & block
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
383
+ #
384
+ # @param [Hash] user_headers User-provided headers to include
385
+ #
386
+ # @return [Hash<String, String>] A hash of HTTP headers => values
387
+ #
388
+ def make_headers(user_headers)
389
+ headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
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
179
406
  end
407
+
408
+ headers.merge!(payload_headers)
180
409
  end
181
- rescue EOFError
182
- raise RestClient::ServerBrokeConnection
183
- rescue Timeout::Error
184
- raise RestClient::RequestTimeout
185
- end
186
410
 
187
- def setup_credentials(req)
188
- req.basic_auth(user, password) if user
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
189
421
  end
190
422
 
191
- def fetch_body(http_response)
192
- if @raw_response
193
- # Taken from Chef, which as in turn...
194
- # Stolen from http://www.ruby-forum.com/topic/166423
195
- # Kudos to _why!
196
- @tf = Tempfile.new("rest-client")
197
- size, total = 0, http_response.header['Content-Length'].to_i
198
- http_response.read_body do |chunk|
199
- @tf.write chunk
200
- size += chunk.size
201
- if RestClient.log
202
- if size == 0
203
- RestClient.log << "#{@method} #{@url} done (0 length file\n)"
204
- elsif total == 0
205
- RestClient.log << "#{@method} #{@url} (zero content length)\n"
206
- else
207
- RestClient.log << "#{@method} #{@url} %d%% done (%d of %d)\n" % [(size * 100) / total, size, total]
208
- end
209
- end
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
210
442
  end
211
- @tf.close
212
- @tf
213
443
  else
214
- http_response.read_body
444
+ nil
215
445
  end
216
- http_response
217
446
  end
218
447
 
219
- def process_result res, & block
220
- if @raw_response
221
- # We don't decode raw requests
222
- response = RawResponse.new(@tf, res, args)
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)
223
457
  else
224
- response = Response.create(Request.decode(res['content-encoding'], res.body), res, args)
458
+ Net::HTTP.new(hostname, port,
459
+ p_uri.hostname, p_uri.port, p_uri.user, p_uri.password)
460
+
225
461
  end
462
+ end
226
463
 
227
- if block_given?
228
- block.call(response, self, res, & block)
464
+ def net_http_request_class(method)
465
+ Net::HTTP.const_get(method.capitalize, false)
466
+ end
467
+
468
+ def net_http_do_request(http, req, body=nil, &block)
469
+ if body && body.respond_to?(:read)
470
+ req.body_stream = body
471
+ return http.request(req, nil, &block)
229
472
  else
230
- response.return!(self, res, & block)
473
+ return http.request(req, body, &block)
231
474
  end
475
+ end
232
476
 
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
233
490
  end
234
491
 
235
- def self.decode content_encoding, body
236
- if (!body) || body.empty?
237
- body
238
- elsif content_encoding == 'gzip'
239
- Zlib::GzipReader.new(StringIO.new(body)).read
240
- elsif content_encoding == 'deflate'
241
- begin
242
- Zlib::Inflate.new.inflate body
243
- rescue Zlib::DataError
244
- # No luck with Zlib decompression. Let's try with raw deflate,
245
- # like some broken web servers do.
246
- Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
492
+ # Return a certificate store that can be used to validate certificates with
493
+ # the system certificate authorities. This will probably not do anything on
494
+ # OS X, which monkey patches OpenSSL in terrible ways to insert its own
495
+ # validation. On most *nix platforms, this will add the system certifcates
496
+ # using OpenSSL::X509::Store#set_default_paths. On Windows, this will use
497
+ # RestClient::Windows::RootCerts to look up the CAs trusted by the system.
498
+ #
499
+ # @return [OpenSSL::X509::Store]
500
+ #
501
+ def self.default_ssl_cert_store
502
+ cert_store = OpenSSL::X509::Store.new
503
+ cert_store.set_default_paths
504
+
505
+ # set_default_paths() doesn't do anything on Windows, so look up
506
+ # certificates using the win32 API.
507
+ if RestClient::Platform.windows?
508
+ RestClient::Windows::RootCerts.instance.to_a.uniq.each do |cert|
509
+ begin
510
+ cert_store.add_cert(cert)
511
+ rescue OpenSSL::X509::StoreError => err
512
+ # ignore duplicate certs
513
+ raise unless err.message == 'cert already in hash table'
514
+ end
247
515
  end
248
- else
249
- body
250
516
  end
517
+
518
+ cert_store
251
519
  end
252
520
 
253
- def log_request
254
- if RestClient.log
255
- out = []
256
- out << "RestClient.#{method} #{url.inspect}"
257
- out << payload.short_inspect if payload
258
- out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
259
- RestClient.log << out.join(', ') + "\n"
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
260
528
  end
261
529
  end
262
530
 
263
- def log_response res
264
- if RestClient.log
265
- size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
266
- RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
267
- end
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"
268
549
  end
269
550
 
270
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
+ #
271
558
  def stringify_headers headers
272
559
  headers.inject({}) do |result, (key, value)|
273
560
  if key.is_a? Symbol
274
- key = key.to_s.split(/_/).map { |w| w.capitalize }.join('-')
561
+ key = key.to_s.split(/_/).map(&:capitalize).join('-')
275
562
  end
276
563
  if 'CONTENT-TYPE' == key.upcase
277
- target_value = value.to_s
278
- result[key] = MIME::Types.type_for_extension target_value
564
+ result[key] = maybe_convert_extension(value.to_s)
279
565
  elsif 'ACCEPT' == key.upcase
280
566
  # Accept can be composed of several comma-separated values
281
567
  if value.is_a? Array
@@ -283,7 +569,9 @@ module RestClient
283
569
  else
284
570
  target_values = value.to_s.split ','
285
571
  end
286
- result[key] = target_values.map { |ext| MIME::Types.type_for_extension(ext.to_s.strip) }.join(', ')
572
+ result[key] = target_values.map { |ext|
573
+ maybe_convert_extension(ext.to_s.strip)
574
+ }.join(', ')
287
575
  else
288
576
  result[key] = value.to_s
289
577
  end
@@ -291,25 +579,301 @@ module RestClient
291
579
  end
292
580
  end
293
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>]
294
586
  def default_headers
295
- {:accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate'}
587
+ {
588
+ :accept => '*/*',
589
+ :user_agent => RestClient::Platform.default_user_agent,
590
+ }
296
591
  end
297
592
 
298
- end
299
- end
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
300
620
 
301
- module MIME
302
- class Types
621
+ def print_verify_callback_warnings
622
+ warned = false
623
+ if RestClient::Platform.mac_mri?
624
+ warn('warning: ssl_verify_callback return code is ignored on OS X')
625
+ warned = true
626
+ end
627
+ if RestClient::Platform.jruby?
628
+ warn('warning: SSL verify_callback may not work correctly in jruby')
629
+ warn('see https://github.com/jruby/jruby/issues/597')
630
+ warned = true
631
+ end
632
+ warned
633
+ end
303
634
 
304
- # Return the first found content-type for a value considered as an extension or the value itself
305
- def type_for_extension ext
306
- candidates = @extension_index[ext]
307
- candidates.empty? ? ext : candidates[0].content_type
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
308
649
  end
309
650
 
310
- class << self
311
- def type_for_extension ext
312
- @__types__.type_for_extension ext
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
+
658
+ setup_credentials req
659
+
660
+ net = net_http_object(uri.hostname, uri.port)
661
+ net.use_ssl = uri.is_a?(URI::HTTPS)
662
+ net.ssl_version = ssl_version if ssl_version
663
+ net.ciphers = ssl_ciphers if ssl_ciphers
664
+
665
+ net.verify_mode = verify_ssl
666
+
667
+ net.cert = ssl_client_cert if ssl_client_cert
668
+ net.key = ssl_client_key if ssl_client_key
669
+ net.ca_file = ssl_ca_file if ssl_ca_file
670
+ net.ca_path = ssl_ca_path if ssl_ca_path
671
+ net.cert_store = ssl_cert_store if ssl_cert_store
672
+
673
+ # We no longer rely on net.verify_callback for the main SSL verification
674
+ # because it's not well supported on all platforms (see comments below).
675
+ # But do allow users to set one if they want.
676
+ if ssl_verify_callback
677
+ net.verify_callback = ssl_verify_callback
678
+
679
+ # Hilariously, jruby only calls the callback when cert_store is set to
680
+ # something, so make sure to set one.
681
+ # https://github.com/jruby/jruby/issues/597
682
+ if RestClient::Platform.jruby?
683
+ net.cert_store ||= OpenSSL::X509::Store.new
684
+ end
685
+
686
+ if ssl_verify_callback_warnings != false
687
+ if print_verify_callback_warnings
688
+ warn('pass :ssl_verify_callback_warnings => false to silence this')
689
+ end
690
+ end
691
+ end
692
+
693
+ if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE
694
+ warn('WARNING: OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE')
695
+ warn('This dangerous monkey patch leaves you open to MITM attacks!')
696
+ warn('Try passing :verify_ssl => false instead.')
697
+ end
698
+
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
703
+ end
704
+ net.read_timeout = @read_timeout
705
+ end
706
+ if defined? @open_timeout
707
+ if @open_timeout == -1
708
+ warn 'Deprecated: to disable timeouts, please use nil instead of -1'
709
+ @open_timeout = nil
710
+ end
711
+ net.open_timeout = @open_timeout
712
+ end
713
+
714
+ RestClient.before_execution_procs.each do |before_proc|
715
+ before_proc.call(req, args)
716
+ end
717
+
718
+ if @before_execution_proc
719
+ @before_execution_proc.call(req, args)
720
+ end
721
+
722
+ log_request
723
+
724
+ start_time = Time.now
725
+ tempfile = nil
726
+
727
+ net.start do |http|
728
+ established_connection = true
729
+
730
+ if @block_response
731
+ net_http_do_request(http, req, payload, &@block_response)
732
+ else
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)
744
+ end
745
+ end
746
+ rescue EOFError
747
+ raise RestClient::ServerBrokeConnection
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
759
+
760
+ rescue OpenSSL::SSL::SSLError => error
761
+ # TODO: deprecate and remove RestClient::SSLCertificateNotVerified and just
762
+ # pass through OpenSSL::SSL::SSLError directly.
763
+ #
764
+ # Exceptions in verify_callback are ignored [1], and jruby doesn't support
765
+ # it at all [2]. RestClient has to catch OpenSSL::SSL::SSLError and either
766
+ # re-throw it as is, or throw SSLCertificateNotVerified based on the
767
+ # contents of the message field of the original exception.
768
+ #
769
+ # The client has to handle OpenSSL::SSL::SSLError exceptions anyway, so
770
+ # we shouldn't make them handle both OpenSSL and RestClient exceptions.
771
+ #
772
+ # [1] https://github.com/ruby/ruby/blob/89e70fe8e7/ext/openssl/ossl.c#L238
773
+ # [2] https://github.com/jruby/jruby/issues/597
774
+
775
+ if error.message.include?("certificate verify failed")
776
+ raise SSLCertificateNotVerified.new(error.message)
777
+ else
778
+ raise error
779
+ end
780
+ end
781
+
782
+ def setup_credentials(req)
783
+ if user && !@processed_headers_lowercase.include?('authorization')
784
+ req.basic_auth(user, password)
785
+ end
786
+ end
787
+
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]
811
+ end
812
+ end
813
+ end
814
+ end
815
+ tf.close
816
+ tf
817
+ end
818
+
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)
822
+ if @raw_response
823
+ unless tempfile
824
+ raise ArgumentError.new('tempfile is required')
825
+ end
826
+ response = RawResponse.new(tempfile, res, self, start_time)
827
+ else
828
+ response = Response.create(res.body, res, self, start_time)
829
+ end
830
+
831
+ response.log_response
832
+
833
+ if block_given?
834
+ block.call(response, self, res, & block)
835
+ else
836
+ response.return!(&block)
837
+ end
838
+
839
+ end
840
+
841
+ def parser
842
+ URI.const_defined?(:Parser) ? URI::Parser.new : URI
843
+ end
844
+
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
871
+
872
+ types = MIME::Types.type_for(ext)
873
+ if types.empty?
874
+ ext
875
+ else
876
+ types.first.content_type
313
877
  end
314
878
  end
315
879
  end