rest-client 1.6.7 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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