rest-client 1.6.14 → 2.0.2

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 (65) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +6 -6
  3. data/.rspec +2 -1
  4. data/.rubocop-disables.yml +384 -0
  5. data/.rubocop.yml +3 -0
  6. data/.travis.yml +46 -1
  7. data/AUTHORS +28 -5
  8. data/Gemfile +5 -1
  9. data/LICENSE +21 -0
  10. data/README.md +784 -0
  11. data/Rakefile +95 -12
  12. data/bin/restclient +11 -12
  13. data/history.md +180 -16
  14. data/lib/restclient.rb +25 -11
  15. data/lib/restclient/abstract_response.rb +171 -51
  16. data/lib/restclient/exceptions.rb +102 -56
  17. data/lib/restclient/params_array.rb +72 -0
  18. data/lib/restclient/payload.rb +43 -74
  19. data/lib/restclient/platform.rb +22 -2
  20. data/lib/restclient/raw_response.rb +7 -3
  21. data/lib/restclient/request.rb +672 -179
  22. data/lib/restclient/resource.rb +6 -7
  23. data/lib/restclient/response.rb +64 -10
  24. data/lib/restclient/utils.rb +235 -0
  25. data/lib/restclient/version.rb +2 -1
  26. data/lib/restclient/windows.rb +8 -0
  27. data/lib/restclient/windows/root_certs.rb +105 -0
  28. data/rest-client.gemspec +16 -11
  29. data/rest-client.windows.gemspec +19 -0
  30. data/spec/helpers.rb +22 -0
  31. data/spec/integration/_lib.rb +1 -0
  32. data/spec/integration/capath_verisign/415660c1.0 +14 -0
  33. data/spec/integration/capath_verisign/7651b327.0 +14 -0
  34. data/spec/integration/capath_verisign/README +8 -0
  35. data/spec/integration/capath_verisign/verisign.crt +14 -0
  36. data/spec/integration/httpbin_spec.rb +87 -0
  37. data/spec/integration/integration_spec.rb +125 -0
  38. data/spec/integration/request_spec.rb +72 -20
  39. data/spec/spec_helper.rb +29 -0
  40. data/spec/unit/_lib.rb +1 -0
  41. data/spec/unit/abstract_response_spec.rb +145 -0
  42. data/spec/unit/exceptions_spec.rb +108 -0
  43. data/spec/{master_shake.jpg → unit/master_shake.jpg} +0 -0
  44. data/spec/unit/params_array_spec.rb +36 -0
  45. data/spec/{payload_spec.rb → unit/payload_spec.rb} +73 -54
  46. data/spec/{raw_response_spec.rb → unit/raw_response_spec.rb} +5 -4
  47. data/spec/unit/request2_spec.rb +54 -0
  48. data/spec/unit/request_spec.rb +1250 -0
  49. data/spec/unit/resource_spec.rb +134 -0
  50. data/spec/unit/response_spec.rb +241 -0
  51. data/spec/unit/restclient_spec.rb +79 -0
  52. data/spec/unit/utils_spec.rb +147 -0
  53. data/spec/unit/windows/root_certs_spec.rb +22 -0
  54. metadata +143 -53
  55. data/README.rdoc +0 -300
  56. data/lib/restclient/net_http_ext.rb +0 -55
  57. data/spec/abstract_response_spec.rb +0 -85
  58. data/spec/base.rb +0 -13
  59. data/spec/exceptions_spec.rb +0 -98
  60. data/spec/integration_spec.rb +0 -38
  61. data/spec/request2_spec.rb +0 -35
  62. data/spec/request_spec.rb +0 -528
  63. data/spec/resource_spec.rb +0 -136
  64. data/spec/response_spec.rb +0 -169
  65. 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,5 +1,7 @@
1
1
  require 'tempfile'
2
+ require 'securerandom'
2
3
  require 'stringio'
4
+
3
5
  require 'mime/types'
4
6
 
5
7
  module RestClient
@@ -23,28 +25,20 @@ module RestClient
23
25
  end
24
26
 
25
27
  def has_file?(params)
26
- params.any? do |_, v|
27
- case v
28
- when Hash
29
- has_file?(v)
30
- when Array
31
- has_file_array?(v)
32
- else
33
- v.respond_to?(:path) && v.respond_to?(:read)
34
- end
28
+ unless params.is_a?(Hash)
29
+ raise ArgumentError.new("Must pass Hash, not #{params.inspect}")
35
30
  end
31
+ _has_file?(params)
36
32
  end
37
33
 
38
- def has_file_array?(params)
39
- params.any? do |v|
40
- case v
41
- when Hash
42
- has_file?(v)
43
- when Array
44
- has_file_array?(v)
45
- else
46
- v.respond_to?(:path) && v.respond_to?(:read)
47
- end
34
+ def _has_file?(obj)
35
+ case obj
36
+ when Hash, ParamsArray
37
+ obj.any? {|_, v| _has_file?(v) }
38
+ when Array
39
+ obj.any? {|v| _has_file?(v) }
40
+ else
41
+ obj.respond_to?(:path) && obj.respond_to?(:read)
48
42
  end
49
43
  end
50
44
 
@@ -58,40 +52,13 @@ module RestClient
58
52
  @stream.seek(0)
59
53
  end
60
54
 
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
55
+ def read(*args)
56
+ @stream.read(*args)
82
57
  end
83
58
 
84
- def flatten_params_array value, calculated_key
85
- result = []
86
- value.each do |elem|
87
- if elem.is_a? Hash
88
- result += flatten_params(elem, calculated_key)
89
- elsif elem.is_a? Array
90
- result += flatten_params_array(elem, calculated_key)
91
- else
92
- result << ["#{calculated_key}[]", elem]
93
- end
94
- end
59
+ def to_s
60
+ result = read
61
+ @stream.seek(0)
95
62
  result
96
63
  end
97
64
 
@@ -109,14 +76,12 @@ module RestClient
109
76
  @stream.close unless @stream.closed?
110
77
  end
111
78
 
112
- def inspect
113
- result = to_s.inspect
114
- @stream.seek(0)
115
- result
79
+ def to_s_inspect
80
+ to_s.inspect
116
81
  end
117
82
 
118
83
  def short_inspect
119
- (size > 500 ? "#{size} byte(s) length" : inspect)
84
+ (size > 500 ? "#{size} byte(s) length" : to_s_inspect)
120
85
  end
121
86
 
122
87
  end
@@ -139,39 +104,28 @@ module RestClient
139
104
 
140
105
  class UrlEncoded < Base
141
106
  def build_stream(params = nil)
142
- @stream = StringIO.new(flatten_params(params).collect do |entry|
143
- "#{entry[0]}=#{handle_key(entry[1])}"
144
- end.join("&"))
107
+ @stream = StringIO.new(Utils.encode_query_string(params))
145
108
  @stream.seek(0)
146
109
  end
147
110
 
148
- # for UrlEncoded escape the keys
149
- def handle_key key
150
- parser.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
151
- end
152
-
153
111
  def headers
154
112
  super.merge({'Content-Type' => 'application/x-www-form-urlencoded'})
155
113
  end
156
-
157
- private
158
- def parser
159
- URI.const_defined?(:Parser) ? URI::Parser.new : URI
160
- end
161
114
  end
162
115
 
163
116
  class Multipart < Base
164
117
  EOL = "\r\n"
165
118
 
166
119
  def build_stream(params)
167
- b = "--#{boundary}"
120
+ b = '--' + boundary
168
121
 
169
122
  @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
170
123
  @stream.binmode
171
124
  @stream.write(b + EOL)
172
125
 
173
- if params.is_a? Hash
174
- x = flatten_params(params)
126
+ case params
127
+ when Hash, ParamsArray
128
+ x = Utils.flatten_params(params)
175
129
  else
176
130
  x = params
177
131
  end
@@ -206,7 +160,7 @@ module RestClient
206
160
  s.write(" filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
207
161
  s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : mime_for(v.path)}#{EOL}")
208
162
  s.write(EOL)
209
- while data = v.read(8124)
163
+ while (data = v.read(8124))
210
164
  s.write(data)
211
165
  end
212
166
  ensure
@@ -220,10 +174,25 @@ module RestClient
220
174
  end
221
175
 
222
176
  def boundary
223
- @boundary ||= rand(1_000_000).to_s
177
+ return @boundary if defined?(@boundary) && @boundary
178
+
179
+ # Use the same algorithm used by WebKit: generate 16 random
180
+ # alphanumeric characters, replacing `+` `/` with `A` `B` (included in
181
+ # the list twice) to round out the set of 64.
182
+ s = SecureRandom.base64(12)
183
+ s.tr!('+/', 'AB')
184
+
185
+ @boundary = '----RubyFormBoundary' + s
224
186
  end
225
187
 
226
188
  # for Multipart do not escape the keys
189
+ #
190
+ # Ostensibly multipart keys MAY be percent encoded per RFC 7578, but in
191
+ # practice no major browser that I'm aware of uses percent encoding.
192
+ #
193
+ # Further discussion of multipart encoding:
194
+ # https://github.com/rest-client/rest-client/pull/403#issuecomment-156976930
195
+ #
227
196
  def handle_key key
228
197
  key
229
198
  end
@@ -1,10 +1,12 @@
1
+ require 'rbconfig'
2
+
1
3
  module RestClient
2
4
  module Platform
3
5
  # Return true if we are running on a darwin-based Ruby platform. This will
4
6
  # be false for jruby even on OS X.
5
7
  #
6
8
  # @return [Boolean]
7
- def self.mac?
9
+ def self.mac_mri?
8
10
  RUBY_PLATFORM.include?('darwin')
9
11
  end
10
12
 
@@ -23,7 +25,25 @@ module RestClient
23
25
  # @return [Boolean]
24
26
  #
25
27
  def self.jruby?
26
- RUBY_PLATFORM == 'java'
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}"
27
47
  end
28
48
  end
29
49
  end
@@ -13,12 +13,16 @@ module RestClient
13
13
 
14
14
  include AbstractResponse
15
15
 
16
- attr_reader :file
16
+ attr_reader :file, :request
17
17
 
18
- def initialize tempfile, net_http_res, args
18
+ def inspect
19
+ "<RestClient::RawResponse @code=#{code.inspect}, @file=#{file.inspect}, @request=#{request.inspect}>"
20
+ end
21
+
22
+ def initialize(tempfile, net_http_res, request)
19
23
  @net_http_res = net_http_res
20
- @args = args
21
24
  @file = tempfile
25
+ @request = request
22
26
  end
23
27
 
24
28
  def to_s
@@ -1,6 +1,8 @@
1
1
  require 'tempfile'
2
2
  require 'mime/types'
3
3
  require 'cgi'
4
+ require 'netrc'
5
+ require 'set'
4
6
 
5
7
  module RestClient
6
8
  # This class is used internally by RestClient to send the request, but you can also
@@ -14,135 +16,602 @@ module RestClient
14
16
  # * :url
15
17
  # Optional parameters (have a look at ssl and/or uri for some explanations):
16
18
  # * :headers a hash containing the request headers
17
- # * :cookies will replace possible cookies in the :headers
19
+ # * :cookies may be a Hash{String/Symbol => String} of cookie values, an
20
+ # Array<HTTP::Cookie>, or an HTTP::CookieJar containing cookies. These
21
+ # will be added to a cookie jar before the request is sent.
18
22
  # * :user and :password for basic auth, will be replaced by a user/password available in the :url
19
23
  # * :block_response call the provided block with the HTTPResponse as parameter
20
24
  # * :raw_response return a low-level RawResponse instead of a Response
21
25
  # * :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
25
- # * :ssl_verify_callback, :ssl_verify_callback_warnings
26
+ # * :proxy An HTTP proxy URI to use for this request. Any value here
27
+ # (including nil) will override RestClient.proxy.
28
+ # * :verify_ssl enable ssl verification, possible values are constants from
29
+ # OpenSSL::SSL::VERIFY_*, defaults to OpenSSL::SSL::VERIFY_PEER
30
+ # * :read_timeout and :open_timeout are how long to wait for a response and
31
+ # to open a connection, in seconds. Pass nil to disable the timeout.
32
+ # * :timeout can be used to set both timeouts
33
+ # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file, :ssl_ca_path,
34
+ # :ssl_cert_store, :ssl_verify_callback, :ssl_verify_callback_warnings
35
+ # * :ssl_version specifies the SSL version for the underlying Net::HTTP connection
36
+ # * :ssl_ciphers sets SSL ciphers for the connection. See
37
+ # OpenSSL::SSL::SSLContext#ciphers=
38
+ # * :before_execution_proc a Proc to call before executing the request. This
39
+ # proc, like procs from RestClient.before_execution_procs, will be
40
+ # called with the HTTP request and request params.
26
41
  class Request
27
42
 
28
- attr_reader :method, :url, :headers, :cookies,
29
- :payload, :user, :password, :timeout, :max_redirects,
30
- :open_timeout, :raw_response, :verify_ssl, :ssl_client_cert,
31
- :ssl_client_key, :ssl_ca_file, :processed_headers, :args,
32
- :ssl_verify_callback, :ssl_verify_callback_warnings
43
+ attr_reader :method, :uri, :url, :headers, :payload, :proxy,
44
+ :user, :password, :read_timeout, :max_redirects,
45
+ :open_timeout, :raw_response, :processed_headers, :args,
46
+ :ssl_opts
47
+
48
+ # An array of previous redirection responses
49
+ attr_accessor :redirection_history
33
50
 
34
51
  def self.execute(args, & block)
35
52
  new(args).execute(& block)
36
53
  end
37
54
 
55
+ SSLOptionList = %w{client_cert client_key ca_file ca_path cert_store
56
+ version ciphers verify_callback verify_callback_warnings}
57
+
58
+ def inspect
59
+ "<RestClient::Request @method=#{@method.inspect}, @url=#{@url.inspect}>"
60
+ end
61
+
38
62
  def initialize args
39
- @method = args[:method] or raise ArgumentError, "must pass :method"
40
- @headers = args[:headers] || {}
63
+ @method = normalize_method(args[:method])
64
+ @headers = (args[:headers] || {}).dup
41
65
  if args[:url]
42
- @url = process_url_params(args[:url], headers)
66
+ @url = process_url_params(normalize_url(args[:url]), headers)
43
67
  else
44
68
  raise ArgumentError, "must pass :url"
45
69
  end
46
- @cookies = @headers.delete(:cookies) || args[:cookies] || {}
70
+
71
+ @user = @password = nil
72
+ parse_url_with_auth!(url)
73
+
74
+ # process cookie arguments found in headers or args
75
+ @cookie_jar = process_cookie_args!(@uri, @headers, args)
76
+
47
77
  @payload = Payload.generate(args[:payload])
48
- @user = args[:user]
49
- @password = args[:password]
50
- @timeout = args[:timeout]
51
- @open_timeout = args[:open_timeout]
78
+
79
+ @user = args[:user] if args.include?(:user)
80
+ @password = args[:password] if args.include?(:password)
81
+
82
+ if args.include?(:timeout)
83
+ @read_timeout = args[:timeout]
84
+ @open_timeout = args[:timeout]
85
+ end
86
+ if args.include?(:read_timeout)
87
+ @read_timeout = args[:read_timeout]
88
+ end
89
+ if args.include?(:open_timeout)
90
+ @open_timeout = args[:open_timeout]
91
+ end
52
92
  @block_response = args[:block_response]
53
93
  @raw_response = args[:raw_response] || false
54
- @verify_ssl = args[:verify_ssl] || false
55
- @ssl_client_cert = args[:ssl_client_cert] || nil
56
- @ssl_client_key = args[:ssl_client_key] || nil
57
- @ssl_ca_file = args[:ssl_ca_file] || nil
58
- @ssl_verify_callback = args[:ssl_verify_callback] || nil
59
- @ssl_verify_callback_warnings = args.fetch(:ssl_verify_callback, true)
94
+
95
+ @proxy = args.fetch(:proxy) if args.include?(:proxy)
96
+
97
+ @ssl_opts = {}
98
+
99
+ if args.include?(:verify_ssl)
100
+ v_ssl = args.fetch(:verify_ssl)
101
+ if v_ssl
102
+ if v_ssl == true
103
+ # interpret :verify_ssl => true as VERIFY_PEER
104
+ @ssl_opts[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
105
+ else
106
+ # otherwise pass through any truthy values
107
+ @ssl_opts[:verify_ssl] = v_ssl
108
+ end
109
+ else
110
+ # interpret all falsy :verify_ssl values as VERIFY_NONE
111
+ @ssl_opts[:verify_ssl] = OpenSSL::SSL::VERIFY_NONE
112
+ end
113
+ else
114
+ # if :verify_ssl was not passed, default to VERIFY_PEER
115
+ @ssl_opts[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
116
+ end
117
+
118
+ SSLOptionList.each do |key|
119
+ source_key = ('ssl_' + key).to_sym
120
+ if args.has_key?(source_key)
121
+ @ssl_opts[key.to_sym] = args.fetch(source_key)
122
+ end
123
+ end
124
+
125
+ # Set some other default SSL options, but only if we have an HTTPS URI.
126
+ if use_ssl?
127
+
128
+ # If there's no CA file, CA path, or cert store provided, use default
129
+ if !ssl_ca_file && !ssl_ca_path && !@ssl_opts.include?(:cert_store)
130
+ @ssl_opts[:cert_store] = self.class.default_ssl_cert_store
131
+ end
132
+ end
133
+
60
134
  @tf = nil # If you are a raw request, this is your tempfile
61
135
  @max_redirects = args[:max_redirects] || 10
62
136
  @processed_headers = make_headers headers
63
137
  @args = args
138
+
139
+ @before_execution_proc = args[:before_execution_proc]
64
140
  end
65
141
 
66
142
  def execute & block
67
- uri = parse_url_with_auth(url)
68
- transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload, & block
143
+ # With 2.0.0+, net/http accepts URI objects in requests and handles wrapping
144
+ # IPv6 addresses in [] for use in the Host request header.
145
+ transmit uri, net_http_request_class(method).new(uri, processed_headers), payload, & block
69
146
  ensure
70
147
  payload.close if payload
71
148
  end
72
149
 
150
+ # SSL-related options
151
+ def verify_ssl
152
+ @ssl_opts.fetch(:verify_ssl)
153
+ end
154
+ SSLOptionList.each do |key|
155
+ define_method('ssl_' + key) do
156
+ @ssl_opts[key.to_sym]
157
+ end
158
+ end
159
+
160
+ # Return true if the request URI will use HTTPS.
161
+ #
162
+ # @return [Boolean]
163
+ #
164
+ def use_ssl?
165
+ uri.is_a?(URI::HTTPS)
166
+ end
167
+
73
168
  # Extract the query parameters and append them to the url
74
- def process_url_params url, headers
75
- url_params = {}
169
+ #
170
+ # Look through the headers hash for a :params option (case-insensitive,
171
+ # may be string or symbol). If present and the value is a Hash or
172
+ # RestClient::ParamsArray, *delete* the key/value pair from the headers
173
+ # hash and encode the value into a query string. Append this query string
174
+ # to the URL and return the resulting URL.
175
+ #
176
+ # @param [String] url
177
+ # @param [Hash] headers An options/headers hash to process. Mutation
178
+ # warning: the params key may be removed if present!
179
+ #
180
+ # @return [String] resulting url with query string
181
+ #
182
+ def process_url_params(url, headers)
183
+ url_params = nil
184
+
185
+ # find and extract/remove "params" key if the value is a Hash/ParamsArray
76
186
  headers.delete_if do |key, value|
77
- if 'params' == key.to_s.downcase && value.is_a?(Hash)
78
- url_params.merge! value
187
+ if key.to_s.downcase == 'params' &&
188
+ (value.is_a?(Hash) || value.is_a?(RestClient::ParamsArray))
189
+ if url_params
190
+ raise ArgumentError.new("Multiple 'params' options passed")
191
+ end
192
+ url_params = value
79
193
  true
80
194
  else
81
195
  false
82
196
  end
83
197
  end
84
- unless url_params.empty?
85
- query_string = url_params.collect { |k, v| "#{k.to_s}=#{CGI::escape(v.to_s)}" }.join('&')
86
- url + "?#{query_string}"
198
+
199
+ # build resulting URL with query string
200
+ if url_params && !url_params.empty?
201
+ query_string = RestClient::Utils.encode_query_string(url_params)
202
+
203
+ if url.include?('?')
204
+ url + '&' + query_string
205
+ else
206
+ url + '?' + query_string
207
+ end
87
208
  else
88
209
  url
89
210
  end
90
211
  end
91
212
 
92
- def make_headers user_headers
93
- unless @cookies.empty?
94
- user_headers[:cookie] = @cookies.map { |(key, val)| "#{key.to_s}=#{CGI::unescape(val)}" }.sort.join('; ')
213
+ # Render a hash of key => value pairs for cookies in the Request#cookie_jar
214
+ # that are valid for the Request#uri. This will not necessarily include all
215
+ # cookies if there are duplicate keys. It's safer to use the cookie_jar
216
+ # directly if that's a concern.
217
+ #
218
+ # @see Request#cookie_jar
219
+ #
220
+ # @return [Hash]
221
+ #
222
+ def cookies
223
+ hash = {}
224
+
225
+ @cookie_jar.cookies(uri).each do |c|
226
+ hash[c.name] = c.value
227
+ end
228
+
229
+ hash
230
+ end
231
+
232
+ # @return [HTTP::CookieJar]
233
+ def cookie_jar
234
+ @cookie_jar
235
+ end
236
+
237
+ # Render a Cookie HTTP request header from the contents of the @cookie_jar,
238
+ # or nil if the jar is empty.
239
+ #
240
+ # @see Request#cookie_jar
241
+ #
242
+ # @return [String, nil]
243
+ #
244
+ def make_cookie_header
245
+ return nil if cookie_jar.nil?
246
+
247
+ arr = cookie_jar.cookies(url)
248
+ return nil if arr.empty?
249
+
250
+ return HTTP::Cookie.cookie_value(arr)
251
+ end
252
+
253
+ # Process cookies passed as hash or as HTTP::CookieJar. For backwards
254
+ # compatibility, these may be passed as a :cookies option masquerading
255
+ # inside the headers hash. To avoid confusion, if :cookies is passed in
256
+ # both headers and Request#initialize, raise an error.
257
+ #
258
+ # :cookies may be a:
259
+ # - Hash{String/Symbol => String}
260
+ # - Array<HTTP::Cookie>
261
+ # - HTTP::CookieJar
262
+ #
263
+ # Passing as a hash:
264
+ # Keys may be symbols or strings. Values must be strings.
265
+ # Infer the domain name from the request URI and allow subdomains (as
266
+ # though '.example.com' had been set in a Set-Cookie header). Assume a
267
+ # path of '/'.
268
+ #
269
+ # RestClient::Request.new(url: 'http://example.com', method: :get,
270
+ # :cookies => {:foo => 'Value', 'bar' => '123'}
271
+ # )
272
+ #
273
+ # results in cookies as though set from the server by:
274
+ # Set-Cookie: foo=Value; Domain=.example.com; Path=/
275
+ # Set-Cookie: bar=123; Domain=.example.com; Path=/
276
+ #
277
+ # which yields a client cookie header of:
278
+ # Cookie: foo=Value; bar=123
279
+ #
280
+ # Passing as HTTP::CookieJar, which will be passed through directly:
281
+ #
282
+ # jar = HTTP::CookieJar.new
283
+ # jar.add(HTTP::Cookie.new('foo', 'Value', domain: 'example.com',
284
+ # path: '/', for_domain: false))
285
+ #
286
+ # RestClient::Request.new(..., :cookies => jar)
287
+ #
288
+ # @param [URI::HTTP] uri The URI for the request. This will be used to
289
+ # infer the domain name for cookies passed as strings in a hash. To avoid
290
+ # this implicit behavior, pass a full cookie jar or use HTTP::Cookie hash
291
+ # values.
292
+ # @param [Hash] headers The headers hash from which to pull the :cookies
293
+ # option. MUTATION NOTE: This key will be deleted from the hash if
294
+ # present.
295
+ # @param [Hash] args The options passed to Request#initialize. This hash
296
+ # will be used as another potential source for the :cookies key.
297
+ # These args will not be mutated.
298
+ #
299
+ # @return [HTTP::CookieJar] A cookie jar containing the parsed cookies.
300
+ #
301
+ def process_cookie_args!(uri, headers, args)
302
+
303
+ # Avoid ambiguity in whether options from headers or options from
304
+ # Request#initialize should take precedence by raising ArgumentError when
305
+ # both are present. Prior versions of rest-client claimed to give
306
+ # precedence to init options, but actually gave precedence to headers.
307
+ # Avoid that mess by erroring out instead.
308
+ if headers[:cookies] && args[:cookies]
309
+ raise ArgumentError.new(
310
+ "Cannot pass :cookies in Request.new() and in headers hash")
95
311
  end
312
+
313
+ cookies_data = headers.delete(:cookies) || args[:cookies]
314
+
315
+ # return copy of cookie jar as is
316
+ if cookies_data.is_a?(HTTP::CookieJar)
317
+ return cookies_data.dup
318
+ end
319
+
320
+ # convert cookies hash into a CookieJar
321
+ jar = HTTP::CookieJar.new
322
+
323
+ (cookies_data || []).each do |key, val|
324
+
325
+ # Support for Array<HTTP::Cookie> mode:
326
+ # If key is a cookie object, add it to the jar directly and assert that
327
+ # there is no separate val.
328
+ if key.is_a?(HTTP::Cookie)
329
+ if val
330
+ raise ArgumentError.new("extra cookie val: #{val.inspect}")
331
+ end
332
+
333
+ jar.add(key)
334
+ next
335
+ end
336
+
337
+ if key.is_a?(Symbol)
338
+ key = key.to_s
339
+ end
340
+
341
+ # assume implicit domain from the request URI, and set for_domain to
342
+ # permit subdomains
343
+ jar.add(HTTP::Cookie.new(key, val, domain: uri.hostname.downcase,
344
+ path: '/', for_domain: true))
345
+ end
346
+
347
+ jar
348
+ end
349
+
350
+ # Generate headers for use by a request. Header keys will be stringified
351
+ # using `#stringify_headers` to normalize them as capitalized strings.
352
+ #
353
+ # The final headers consist of:
354
+ # - default headers from #default_headers
355
+ # - user_headers provided here
356
+ # - headers from the payload object (e.g. Content-Type, Content-Lenth)
357
+ # - cookie headers from #make_cookie_header
358
+ #
359
+ # @param [Hash] user_headers User-provided headers to include
360
+ #
361
+ # @return [Hash<String, String>] A hash of HTTP headers => values
362
+ #
363
+ def make_headers(user_headers)
96
364
  headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
97
- headers.merge!(@payload.headers) if @payload
365
+
366
+ # override headers from the payload (e.g. Content-Type, Content-Length)
367
+ if @payload
368
+ payload_headers = @payload.headers
369
+
370
+ # Warn the user if we override any headers that were previously
371
+ # present. This usually indicates that rest-client was passed
372
+ # conflicting information, e.g. if it was asked to render a payload as
373
+ # x-www-form-urlencoded but a Content-Type application/json was
374
+ # also supplied by the user.
375
+ payload_headers.each_pair do |key, val|
376
+ if headers.include?(key) && headers[key] != val
377
+ warn("warning: Overriding #{key.inspect} header " +
378
+ "#{headers.fetch(key).inspect} with #{val.inspect} " +
379
+ "due to payload")
380
+ end
381
+ end
382
+
383
+ headers.merge!(payload_headers)
384
+ end
385
+
386
+ # merge in cookies
387
+ cookies = make_cookie_header
388
+ if cookies && !cookies.empty?
389
+ if headers['Cookie']
390
+ warn('warning: overriding "Cookie" header with :cookies option')
391
+ end
392
+ headers['Cookie'] = cookies
393
+ end
394
+
98
395
  headers
99
396
  end
100
397
 
101
- def net_http_class
102
- if RestClient.proxy
103
- proxy_uri = URI.parse(RestClient.proxy)
104
- Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
398
+ # The proxy URI for this request. If `:proxy` was provided on this request,
399
+ # use it over `RestClient.proxy`.
400
+ #
401
+ # Return false if a proxy was explicitly set and is falsy.
402
+ #
403
+ # @return [URI, false, nil]
404
+ #
405
+ def proxy_uri
406
+ if defined?(@proxy)
407
+ if @proxy
408
+ URI.parse(@proxy)
409
+ else
410
+ false
411
+ end
412
+ elsif RestClient.proxy_set?
413
+ if RestClient.proxy
414
+ URI.parse(RestClient.proxy)
415
+ else
416
+ false
417
+ end
105
418
  else
106
- Net::HTTP
419
+ nil
420
+ end
421
+ end
422
+
423
+ def net_http_object(hostname, port)
424
+ p_uri = proxy_uri
425
+
426
+ if p_uri.nil?
427
+ # no proxy set
428
+ Net::HTTP.new(hostname, port)
429
+ elsif !p_uri
430
+ # proxy explicitly set to none
431
+ Net::HTTP.new(hostname, port, nil, nil, nil, nil)
432
+ else
433
+ Net::HTTP.new(hostname, port,
434
+ p_uri.hostname, p_uri.port, p_uri.user, p_uri.password)
435
+
107
436
  end
108
437
  end
109
438
 
110
439
  def net_http_request_class(method)
111
- Net::HTTP.const_get(method.to_s.capitalize)
440
+ Net::HTTP.const_get(method.capitalize, false)
112
441
  end
113
442
 
114
- def parse_url(url)
115
- url = "http://#{url}" unless url.match(/^http/)
116
- URI.parse(url)
443
+ def net_http_do_request(http, req, body=nil, &block)
444
+ if body && body.respond_to?(:read)
445
+ req.body_stream = body
446
+ return http.request(req, nil, &block)
447
+ else
448
+ return http.request(req, body, &block)
449
+ end
117
450
  end
118
451
 
119
- def parse_url_with_auth(url)
120
- uri = parse_url(url)
121
- @user = CGI.unescape(uri.user) if uri.user
122
- @password = CGI.unescape(uri.password) if uri.password
123
- uri
452
+ # Normalize a URL by adding a protocol if none is present.
453
+ #
454
+ # If the string has no HTTP-like scheme (i.e. scheme followed by '//'), a
455
+ # scheme of 'http' will be added. This mimics the behavior of browsers and
456
+ # user agents like cURL.
457
+ #
458
+ # @param [String] url A URL string.
459
+ #
460
+ # @return [String]
461
+ #
462
+ def normalize_url(url)
463
+ url = 'http://' + url unless url.match(%r{\A[a-z][a-z0-9+.-]*://}i)
464
+ url
465
+ end
466
+
467
+ # Return a certificate store that can be used to validate certificates with
468
+ # the system certificate authorities. This will probably not do anything on
469
+ # OS X, which monkey patches OpenSSL in terrible ways to insert its own
470
+ # validation. On most *nix platforms, this will add the system certifcates
471
+ # using OpenSSL::X509::Store#set_default_paths. On Windows, this will use
472
+ # RestClient::Windows::RootCerts to look up the CAs trusted by the system.
473
+ #
474
+ # @return [OpenSSL::X509::Store]
475
+ #
476
+ def self.default_ssl_cert_store
477
+ cert_store = OpenSSL::X509::Store.new
478
+ cert_store.set_default_paths
479
+
480
+ # set_default_paths() doesn't do anything on Windows, so look up
481
+ # certificates using the win32 API.
482
+ if RestClient::Platform.windows?
483
+ RestClient::Windows::RootCerts.instance.to_a.uniq.each do |cert|
484
+ begin
485
+ cert_store.add_cert(cert)
486
+ rescue OpenSSL::X509::StoreError => err
487
+ # ignore duplicate certs
488
+ raise unless err.message == 'cert already in hash table'
489
+ end
490
+ end
491
+ end
492
+
493
+ cert_store
124
494
  end
125
495
 
126
- def process_payload(p=nil, parent_key=nil)
127
- unless p.is_a?(Hash)
128
- p
496
+ def self.decode content_encoding, body
497
+ if (!body) || body.empty?
498
+ body
499
+ elsif content_encoding == 'gzip'
500
+ Zlib::GzipReader.new(StringIO.new(body)).read
501
+ elsif content_encoding == 'deflate'
502
+ begin
503
+ Zlib::Inflate.new.inflate body
504
+ rescue Zlib::DataError
505
+ # No luck with Zlib decompression. Let's try with raw deflate,
506
+ # like some broken web servers do.
507
+ Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
508
+ end
509
+ else
510
+ body
511
+ end
512
+ end
513
+
514
+ def redacted_uri
515
+ if uri.password
516
+ sanitized_uri = uri.dup
517
+ sanitized_uri.password = 'REDACTED'
518
+ sanitized_uri
129
519
  else
130
- @headers[:content_type] ||= 'application/x-www-form-urlencoded'
131
- p.keys.map do |k|
132
- key = parent_key ? "#{parent_key}[#{k}]" : k
133
- if p[k].is_a? Hash
134
- process_payload(p[k], key)
520
+ uri
521
+ end
522
+ end
523
+
524
+ def redacted_url
525
+ redacted_uri.to_s
526
+ end
527
+
528
+ def log_request
529
+ return unless RestClient.log
530
+
531
+ out = []
532
+
533
+ out << "RestClient.#{method} #{redacted_url.inspect}"
534
+ out << payload.short_inspect if payload
535
+ out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
536
+ RestClient.log << out.join(', ') + "\n"
537
+ end
538
+
539
+ def log_response res
540
+ return unless RestClient.log
541
+
542
+ size = if @raw_response
543
+ File.size(@tf.path)
544
+ else
545
+ res.body.nil? ? 0 : res.body.size
546
+ end
547
+
548
+ RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
549
+ end
550
+
551
+ # Return a hash of headers whose keys are capitalized strings
552
+ def stringify_headers headers
553
+ headers.inject({}) do |result, (key, value)|
554
+ if key.is_a? Symbol
555
+ key = key.to_s.split(/_/).map(&:capitalize).join('-')
556
+ end
557
+ if 'CONTENT-TYPE' == key.upcase
558
+ result[key] = maybe_convert_extension(value.to_s)
559
+ elsif 'ACCEPT' == key.upcase
560
+ # Accept can be composed of several comma-separated values
561
+ if value.is_a? Array
562
+ target_values = value
135
563
  else
136
- value = parser.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
137
- "#{key}=#{value}"
564
+ target_values = value.to_s.split ','
138
565
  end
139
- end.join("&")
566
+ result[key] = target_values.map { |ext|
567
+ maybe_convert_extension(ext.to_s.strip)
568
+ }.join(', ')
569
+ else
570
+ result[key] = value.to_s
571
+ end
572
+ result
573
+ end
574
+ end
575
+
576
+ def default_headers
577
+ {
578
+ :accept => '*/*',
579
+ :accept_encoding => 'gzip, deflate',
580
+ :user_agent => RestClient::Platform.default_user_agent,
581
+ }
582
+ end
583
+
584
+ private
585
+
586
+ # Parse the `@url` string into a URI object and save it as
587
+ # `@uri`. Also save any basic auth user or password as @user and @password.
588
+ # If no auth info was passed, check for credentials in a Netrc file.
589
+ #
590
+ # @param [String] url A URL string.
591
+ #
592
+ # @return [URI]
593
+ #
594
+ # @raise URI::InvalidURIError on invalid URIs
595
+ #
596
+ def parse_url_with_auth!(url)
597
+ uri = URI.parse(url)
598
+
599
+ if uri.hostname.nil?
600
+ raise URI::InvalidURIError.new("bad URI(no host provided): #{url}")
140
601
  end
602
+
603
+ @user = CGI.unescape(uri.user) if uri.user
604
+ @password = CGI.unescape(uri.password) if uri.password
605
+ if !@user && !@password
606
+ @user, @password = Netrc.read[uri.hostname]
607
+ end
608
+
609
+ @uri = uri
141
610
  end
142
611
 
143
612
  def print_verify_callback_warnings
144
613
  warned = false
145
- if RestClient::Platform.mac?
614
+ if RestClient::Platform.mac_mri?
146
615
  warn('warning: ssl_verify_callback return code is ignored on OS X')
147
616
  warned = true
148
617
  end
@@ -154,32 +623,47 @@ module RestClient
154
623
  warned
155
624
  end
156
625
 
626
+ # Parse a method and return a normalized string version.
627
+ #
628
+ # Raise ArgumentError if the method is falsy, but otherwise do no
629
+ # validation.
630
+ #
631
+ # @param method [String, Symbol]
632
+ #
633
+ # @return [String]
634
+ #
635
+ # @see net_http_request_class
636
+ #
637
+ def normalize_method(method)
638
+ raise ArgumentError.new('must pass :method') unless method
639
+ method.to_s.downcase
640
+ end
641
+
157
642
  def transmit uri, req, payload, & block
643
+
644
+ # We set this to true in the net/http block so that we can distinguish
645
+ # read_timeout from open_timeout. Now that we only support Ruby 2.0+,
646
+ # this is only needed for Timeout exceptions thrown outside of Net::HTTP.
647
+ established_connection = false
648
+
158
649
  setup_credentials req
159
650
 
160
- net = net_http_class.new(uri.host, uri.port)
651
+ net = net_http_object(uri.hostname, uri.port)
161
652
  net.use_ssl = uri.is_a?(URI::HTTPS)
162
- if @verify_ssl
163
- if @verify_ssl.is_a? Integer
164
- net.verify_mode = @verify_ssl
165
- else
166
- net.verify_mode = OpenSSL::SSL::VERIFY_PEER
167
- end
168
- else
169
- net.verify_mode = OpenSSL::SSL::VERIFY_NONE
170
- end
171
- net.cert = @ssl_client_cert if @ssl_client_cert
172
- net.key = @ssl_client_key if @ssl_client_key
173
- net.ca_file = @ssl_ca_file if @ssl_ca_file
174
- net.read_timeout = @timeout if @timeout
175
- net.open_timeout = @open_timeout if @open_timeout
653
+ net.ssl_version = ssl_version if ssl_version
654
+ net.ciphers = ssl_ciphers if ssl_ciphers
655
+
656
+ net.verify_mode = verify_ssl
176
657
 
177
- # disable the timeout if the timeout value is -1
178
- net.read_timeout = nil if @timeout == -1
179
- net.open_timeout = nil if @open_timeout == -1
658
+ net.cert = ssl_client_cert if ssl_client_cert
659
+ net.key = ssl_client_key if ssl_client_key
660
+ net.ca_file = ssl_ca_file if ssl_ca_file
661
+ net.ca_path = ssl_ca_path if ssl_ca_path
662
+ net.cert_store = ssl_cert_store if ssl_cert_store
180
663
 
181
- # verify_callback isn't well supported on all platforms, but do allow
182
- # users to set one if they want.
664
+ # We no longer rely on net.verify_callback for the main SSL verification
665
+ # because it's not well supported on all platforms (see comments below).
666
+ # But do allow users to set one if they want.
183
667
  if ssl_verify_callback
184
668
  net.verify_callback = ssl_verify_callback
185
669
 
@@ -197,34 +681,88 @@ module RestClient
197
681
  end
198
682
  end
199
683
 
684
+ if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE
685
+ warn('WARNING: OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE')
686
+ warn('This dangerous monkey patch leaves you open to MITM attacks!')
687
+ warn('Try passing :verify_ssl => false instead.')
688
+ end
689
+
690
+ if defined? @read_timeout
691
+ if @read_timeout == -1
692
+ warn 'Deprecated: to disable timeouts, please use nil instead of -1'
693
+ @read_timeout = nil
694
+ end
695
+ net.read_timeout = @read_timeout
696
+ end
697
+ if defined? @open_timeout
698
+ if @open_timeout == -1
699
+ warn 'Deprecated: to disable timeouts, please use nil instead of -1'
700
+ @open_timeout = nil
701
+ end
702
+ net.open_timeout = @open_timeout
703
+ end
704
+
200
705
  RestClient.before_execution_procs.each do |before_proc|
201
706
  before_proc.call(req, args)
202
707
  end
203
708
 
709
+ if @before_execution_proc
710
+ @before_execution_proc.call(req, args)
711
+ end
712
+
204
713
  log_request
205
714
 
206
715
  net.start do |http|
716
+ established_connection = true
717
+
207
718
  if @block_response
208
- http.request(req, payload ? payload.to_s : nil, & @block_response)
719
+ net_http_do_request(http, req, payload, &@block_response)
209
720
  else
210
- res = http.request(req, payload ? payload.to_s : nil) { |http_response| fetch_body(http_response) }
721
+ res = net_http_do_request(http, req, payload) { |http_response|
722
+ fetch_body(http_response)
723
+ }
211
724
  log_response res
212
725
  process_result res, & block
213
726
  end
214
727
  end
215
728
  rescue EOFError
216
729
  raise RestClient::ServerBrokeConnection
217
- rescue Timeout::Error
218
- raise RestClient::RequestTimeout
730
+ rescue Net::OpenTimeout => err
731
+ raise RestClient::Exceptions::OpenTimeout.new(nil, err)
732
+ rescue Net::ReadTimeout => err
733
+ raise RestClient::Exceptions::ReadTimeout.new(nil, err)
734
+ rescue Timeout::Error, Errno::ETIMEDOUT => err
735
+ # handling for non-Net::HTTP timeouts
736
+ if established_connection
737
+ raise RestClient::Exceptions::ReadTimeout.new(nil, err)
738
+ else
739
+ raise RestClient::Exceptions::OpenTimeout.new(nil, err)
740
+ end
741
+
219
742
  rescue OpenSSL::SSL::SSLError => error
220
- # UGH. Not sure if this is needed at all. SSLCertificateNotVerified is not being used internally.
221
- # I think it would be better to leave SSLError processing to the client (they'd have to do that anyway...)
222
- raise SSLCertificateNotVerified.new(error.message) if error.message.include?("certificate verify failed")
223
- raise error
743
+ # TODO: deprecate and remove RestClient::SSLCertificateNotVerified and just
744
+ # pass through OpenSSL::SSL::SSLError directly.
745
+ #
746
+ # Exceptions in verify_callback are ignored [1], and jruby doesn't support
747
+ # it at all [2]. RestClient has to catch OpenSSL::SSL::SSLError and either
748
+ # re-throw it as is, or throw SSLCertificateNotVerified based on the
749
+ # contents of the message field of the original exception.
750
+ #
751
+ # The client has to handle OpenSSL::SSL::SSLError exceptions anyway, so
752
+ # we shouldn't make them handle both OpenSSL and RestClient exceptions.
753
+ #
754
+ # [1] https://github.com/ruby/ruby/blob/89e70fe8e7/ext/openssl/ossl.c#L238
755
+ # [2] https://github.com/jruby/jruby/issues/597
756
+
757
+ if error.message.include?("certificate verify failed")
758
+ raise SSLCertificateNotVerified.new(error.message)
759
+ else
760
+ raise error
761
+ end
224
762
  end
225
763
 
226
764
  def setup_credentials(req)
227
- req.basic_auth(user, password) if user
765
+ req.basic_auth(user, password) if user && !headers.has_key?("Authorization")
228
766
  end
229
767
 
230
768
  def fetch_body(http_response)
@@ -232,18 +770,19 @@ module RestClient
232
770
  # Taken from Chef, which as in turn...
233
771
  # Stolen from http://www.ruby-forum.com/topic/166423
234
772
  # Kudos to _why!
235
- @tf = Tempfile.new("rest-client")
236
- size, total = 0, http_response.header['Content-Length'].to_i
773
+ @tf = Tempfile.new('rest-client.')
774
+ @tf.binmode
775
+ size, total = 0, http_response['Content-Length'].to_i
237
776
  http_response.read_body do |chunk|
238
777
  @tf.write chunk
239
778
  size += chunk.size
240
779
  if RestClient.log
241
780
  if size == 0
242
- RestClient.log << "#{@method} #{@url} done (0 length file\n)"
781
+ RestClient.log << "%s %s done (0 length file)\n" % [@method, @url]
243
782
  elsif total == 0
244
- RestClient.log << "#{@method} #{@url} (zero content length)\n"
783
+ RestClient.log << "%s %s (zero content length)\n" % [@method, @url]
245
784
  else
246
- RestClient.log << "#{@method} #{@url} %d%% done (%d of %d)\n" % [(size * 100) / total, size, total]
785
+ RestClient.log << "%s %s %d%% done (%d of %d)\n" % [@method, @url, (size * 100) / total, size, total]
247
786
  end
248
787
  end
249
788
  end
@@ -258,102 +797,56 @@ module RestClient
258
797
  def process_result res, & block
259
798
  if @raw_response
260
799
  # We don't decode raw requests
261
- response = RawResponse.new(@tf, res, args)
800
+ response = RawResponse.new(@tf, res, self)
262
801
  else
263
- response = Response.create(Request.decode(res['content-encoding'], res.body), res, args)
802
+ decoded = Request.decode(res['content-encoding'], res.body)
803
+ response = Response.create(decoded, res, self)
264
804
  end
265
805
 
266
806
  if block_given?
267
807
  block.call(response, self, res, & block)
268
808
  else
269
- response.return!(self, res, & block)
809
+ response.return!(&block)
270
810
  end
271
811
 
272
812
  end
273
813
 
274
- def self.decode content_encoding, body
275
- if (!body) || body.empty?
276
- body
277
- elsif content_encoding == 'gzip'
278
- Zlib::GzipReader.new(StringIO.new(body)).read
279
- elsif content_encoding == 'deflate'
280
- begin
281
- Zlib::Inflate.new.inflate body
282
- rescue Zlib::DataError
283
- # No luck with Zlib decompression. Let's try with raw deflate,
284
- # like some broken web servers do.
285
- Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
286
- end
287
- else
288
- body
289
- end
290
- end
291
-
292
- def log_request
293
- if RestClient.log
294
- out = []
295
- out << "RestClient.#{method} #{url.inspect}"
296
- out << payload.short_inspect if payload
297
- out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
298
- RestClient.log << out.join(', ') + "\n"
299
- end
814
+ def parser
815
+ URI.const_defined?(:Parser) ? URI::Parser.new : URI
300
816
  end
301
817
 
302
- def log_response res
303
- if RestClient.log
304
- size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
305
- RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
818
+ # Given a MIME type or file extension, return either a MIME type or, if
819
+ # none is found, the input unchanged.
820
+ #
821
+ # >> maybe_convert_extension('json')
822
+ # => 'application/json'
823
+ #
824
+ # >> maybe_convert_extension('unknown')
825
+ # => 'unknown'
826
+ #
827
+ # >> maybe_convert_extension('application/xml')
828
+ # => 'application/xml'
829
+ #
830
+ # @param ext [String]
831
+ #
832
+ # @return [String]
833
+ #
834
+ def maybe_convert_extension(ext)
835
+ unless ext =~ /\A[a-zA-Z0-9_@-]+\z/
836
+ # Don't look up strings unless they look like they could be a file
837
+ # extension known to mime-types.
838
+ #
839
+ # There currently isn't any API public way to look up extensions
840
+ # directly out of MIME::Types, but the type_for() method only strips
841
+ # off after a period anyway.
842
+ return ext
306
843
  end
307
- end
308
844
 
309
- # Return a hash of headers whose keys are capitalized strings
310
- def stringify_headers headers
311
- headers.inject({}) do |result, (key, value)|
312
- if key.is_a? Symbol
313
- key = key.to_s.split(/_/).map { |w| w.capitalize }.join('-')
314
- end
315
- if 'CONTENT-TYPE' == key.upcase
316
- target_value = value.to_s
317
- result[key] = MIME::Types.type_for_extension target_value
318
- elsif 'ACCEPT' == key.upcase
319
- # Accept can be composed of several comma-separated values
320
- if value.is_a? Array
321
- target_values = value
322
- else
323
- target_values = value.to_s.split ','
324
- end
325
- result[key] = target_values.map { |ext| MIME::Types.type_for_extension(ext.to_s.strip) }.join(', ')
326
- else
327
- result[key] = value.to_s
328
- end
329
- result
330
- end
331
- end
332
-
333
- def default_headers
334
- {:accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate'}
335
- end
336
-
337
- private
338
- def parser
339
- URI.const_defined?(:Parser) ? URI::Parser.new : URI
340
- end
341
-
342
- end
343
- end
344
-
345
- module MIME
346
- class Types
347
-
348
- # Return the first found content-type for a value considered as an extension or the value itself
349
- def type_for_extension ext
350
- candidates = @extension_index[ext]
351
- candidates.empty? ? ext : candidates[0].content_type
352
- end
353
-
354
- class << self
355
- def type_for_extension ext
356
- @__types__.type_for_extension ext
845
+ types = MIME::Types.type_for(ext)
846
+ if types.empty?
847
+ ext
848
+ else
849
+ types.first.content_type
357
850
  end
358
851
  end
359
852
  end