rest-client 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 (62) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rspec +2 -0
  4. data/.rubocop-disables.yml +384 -0
  5. data/.rubocop.yml +3 -0
  6. data/.travis.yml +48 -0
  7. data/AUTHORS +98 -0
  8. data/Gemfile +11 -0
  9. data/LICENSE +21 -0
  10. data/README.md +784 -0
  11. data/Rakefile +132 -0
  12. data/bin/restclient +92 -0
  13. data/history.md +324 -0
  14. data/lib/rest-client.rb +2 -0
  15. data/lib/rest_client.rb +2 -0
  16. data/lib/restclient.rb +184 -0
  17. data/lib/restclient/abstract_response.rb +226 -0
  18. data/lib/restclient/exceptions.rb +244 -0
  19. data/lib/restclient/params_array.rb +72 -0
  20. data/lib/restclient/payload.rb +209 -0
  21. data/lib/restclient/platform.rb +49 -0
  22. data/lib/restclient/raw_response.rb +38 -0
  23. data/lib/restclient/request.rb +853 -0
  24. data/lib/restclient/resource.rb +168 -0
  25. data/lib/restclient/response.rb +80 -0
  26. data/lib/restclient/utils.rb +235 -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 +31 -0
  31. data/rest-client.windows.gemspec +19 -0
  32. data/spec/helpers.rb +22 -0
  33. data/spec/integration/_lib.rb +1 -0
  34. data/spec/integration/capath_digicert/244b5494.0 +19 -0
  35. data/spec/integration/capath_digicert/81b9768f.0 +19 -0
  36. data/spec/integration/capath_digicert/README +8 -0
  37. data/spec/integration/capath_digicert/digicert.crt +19 -0
  38. data/spec/integration/capath_verisign/415660c1.0 +14 -0
  39. data/spec/integration/capath_verisign/7651b327.0 +14 -0
  40. data/spec/integration/capath_verisign/README +8 -0
  41. data/spec/integration/capath_verisign/verisign.crt +14 -0
  42. data/spec/integration/certs/digicert.crt +19 -0
  43. data/spec/integration/certs/verisign.crt +14 -0
  44. data/spec/integration/httpbin_spec.rb +87 -0
  45. data/spec/integration/integration_spec.rb +125 -0
  46. data/spec/integration/request_spec.rb +127 -0
  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/master_shake.jpg +0 -0
  52. data/spec/unit/params_array_spec.rb +36 -0
  53. data/spec/unit/payload_spec.rb +263 -0
  54. data/spec/unit/raw_response_spec.rb +18 -0
  55. data/spec/unit/request2_spec.rb +54 -0
  56. data/spec/unit/request_spec.rb +1250 -0
  57. data/spec/unit/resource_spec.rb +134 -0
  58. data/spec/unit/response_spec.rb +241 -0
  59. data/spec/unit/restclient_spec.rb +79 -0
  60. data/spec/unit/utils_spec.rb +147 -0
  61. data/spec/unit/windows/root_certs_spec.rb +22 -0
  62. metadata +282 -0
@@ -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
@@ -0,0 +1,209 @@
1
+ require 'tempfile'
2
+ require 'securerandom'
3
+ require 'stringio'
4
+
5
+ require 'mime/types'
6
+
7
+ module RestClient
8
+ module Payload
9
+ extend self
10
+
11
+ def generate(params)
12
+ if params.is_a?(String)
13
+ Base.new(params)
14
+ elsif params.is_a?(Hash)
15
+ if params.delete(:multipart) == true || has_file?(params)
16
+ Multipart.new(params)
17
+ else
18
+ UrlEncoded.new(params)
19
+ end
20
+ elsif params.respond_to?(:read)
21
+ Streamed.new(params)
22
+ else
23
+ nil
24
+ end
25
+ end
26
+
27
+ def has_file?(params)
28
+ unless params.is_a?(Hash)
29
+ raise ArgumentError.new("Must pass Hash, not #{params.inspect}")
30
+ end
31
+ _has_file?(params)
32
+ end
33
+
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)
42
+ end
43
+ end
44
+
45
+ class Base
46
+ def initialize(params)
47
+ build_stream(params)
48
+ end
49
+
50
+ def build_stream(params)
51
+ @stream = StringIO.new(params)
52
+ @stream.seek(0)
53
+ end
54
+
55
+ def read(*args)
56
+ @stream.read(*args)
57
+ end
58
+
59
+ def to_s
60
+ result = read
61
+ @stream.seek(0)
62
+ result
63
+ end
64
+
65
+ def headers
66
+ {'Content-Length' => size.to_s}
67
+ end
68
+
69
+ def size
70
+ @stream.size
71
+ end
72
+
73
+ alias :length :size
74
+
75
+ def close
76
+ @stream.close unless @stream.closed?
77
+ end
78
+
79
+ def to_s_inspect
80
+ to_s.inspect
81
+ end
82
+
83
+ def short_inspect
84
+ (size > 500 ? "#{size} byte(s) length" : to_s_inspect)
85
+ end
86
+
87
+ end
88
+
89
+ class Streamed < Base
90
+ def build_stream(params = nil)
91
+ @stream = params
92
+ end
93
+
94
+ def size
95
+ if @stream.respond_to?(:size)
96
+ @stream.size
97
+ elsif @stream.is_a?(IO)
98
+ @stream.stat.size
99
+ end
100
+ end
101
+
102
+ alias :length :size
103
+ end
104
+
105
+ class UrlEncoded < Base
106
+ def build_stream(params = nil)
107
+ @stream = StringIO.new(Utils.encode_query_string(params))
108
+ @stream.seek(0)
109
+ end
110
+
111
+ def headers
112
+ super.merge({'Content-Type' => 'application/x-www-form-urlencoded'})
113
+ end
114
+ end
115
+
116
+ class Multipart < Base
117
+ EOL = "\r\n"
118
+
119
+ def build_stream(params)
120
+ b = '--' + boundary
121
+
122
+ @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
123
+ @stream.binmode
124
+ @stream.write(b + EOL)
125
+
126
+ case params
127
+ when Hash, ParamsArray
128
+ x = Utils.flatten_params(params)
129
+ else
130
+ x = params
131
+ end
132
+
133
+ last_index = x.length - 1
134
+ x.each_with_index do |a, index|
135
+ k, v = * a
136
+ if v.respond_to?(:read) && v.respond_to?(:path)
137
+ create_file_field(@stream, k, v)
138
+ else
139
+ create_regular_field(@stream, k, v)
140
+ end
141
+ @stream.write(EOL + b)
142
+ @stream.write(EOL) unless last_index == index
143
+ end
144
+ @stream.write('--')
145
+ @stream.write(EOL)
146
+ @stream.seek(0)
147
+ end
148
+
149
+ def create_regular_field(s, k, v)
150
+ s.write("Content-Disposition: form-data; name=\"#{k}\"")
151
+ s.write(EOL)
152
+ s.write(EOL)
153
+ s.write(v)
154
+ end
155
+
156
+ def create_file_field(s, k, v)
157
+ begin
158
+ s.write("Content-Disposition: form-data;")
159
+ s.write(" name=\"#{k}\";") unless (k.nil? || k=='')
160
+ s.write(" filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
161
+ s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : mime_for(v.path)}#{EOL}")
162
+ s.write(EOL)
163
+ while (data = v.read(8124))
164
+ s.write(data)
165
+ end
166
+ ensure
167
+ v.close if v.respond_to?(:close)
168
+ end
169
+ end
170
+
171
+ def mime_for(path)
172
+ mime = MIME::Types.type_for path
173
+ mime.empty? ? 'text/plain' : mime[0].content_type
174
+ end
175
+
176
+ def boundary
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
186
+ end
187
+
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
+ #
196
+ def handle_key key
197
+ key
198
+ end
199
+
200
+ def headers
201
+ super.merge({'Content-Type' => %Q{multipart/form-data; boundary=#{boundary}}})
202
+ end
203
+
204
+ def close
205
+ @stream.close!
206
+ end
207
+ end
208
+ end
209
+ 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
@@ -0,0 +1,38 @@
1
+ module RestClient
2
+ # The response from RestClient on a raw request looks like a string, but is
3
+ # actually one of these. 99% of the time you're making a rest call all you
4
+ # care about is the body, but on the occassion you want to fetch the
5
+ # headers you can:
6
+ #
7
+ # RestClient.get('http://example.com').headers[:content_type]
8
+ #
9
+ # In addition, if you do not use the response as a string, you can access
10
+ # a Tempfile object at res.file, which contains the path to the raw
11
+ # downloaded request body.
12
+ class RawResponse
13
+
14
+ include AbstractResponse
15
+
16
+ attr_reader :file, :request
17
+
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)
23
+ @net_http_res = net_http_res
24
+ @file = tempfile
25
+ @request = request
26
+ end
27
+
28
+ def to_s
29
+ @file.open
30
+ @file.read
31
+ end
32
+
33
+ def size
34
+ File.size file
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,853 @@
1
+ require 'tempfile'
2
+ require 'mime/types'
3
+ require 'cgi'
4
+ require 'netrc'
5
+ require 'set'
6
+
7
+ module RestClient
8
+ # This class is used internally by RestClient to send the request, but you can also
9
+ # call it directly if you'd like to use a method not supported by the
10
+ # main API. For example:
11
+ #
12
+ # RestClient::Request.execute(:method => :head, :url => 'http://example.com')
13
+ #
14
+ # Mandatory parameters:
15
+ # * :method
16
+ # * :url
17
+ # Optional parameters (have a look at ssl and/or uri for some explanations):
18
+ # * :headers a hash containing the request 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.
22
+ # * :user and :password for basic auth, will be replaced by a user/password available in the :url
23
+ # * :block_response call the provided block with the HTTPResponse as parameter
24
+ # * :raw_response return a low-level RawResponse instead of a Response
25
+ # * :max_redirects maximum number of redirections (default to 10)
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.
41
+ class Request
42
+
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
50
+
51
+ def self.execute(args, & block)
52
+ new(args).execute(& block)
53
+ end
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
+
62
+ def initialize args
63
+ @method = normalize_method(args[:method])
64
+ @headers = (args[:headers] || {}).dup
65
+ if args[:url]
66
+ @url = process_url_params(normalize_url(args[:url]), headers)
67
+ else
68
+ raise ArgumentError, "must pass :url"
69
+ end
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
+
77
+ @payload = Payload.generate(args[:payload])
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
92
+ @block_response = args[:block_response]
93
+ @raw_response = args[:raw_response] || false
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
+
134
+ @tf = nil # If you are a raw request, this is your tempfile
135
+ @max_redirects = args[:max_redirects] || 10
136
+ @processed_headers = make_headers headers
137
+ @args = args
138
+
139
+ @before_execution_proc = args[:before_execution_proc]
140
+ end
141
+
142
+ def execute & 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
146
+ ensure
147
+ payload.close if payload
148
+ end
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
+
168
+ # Extract the query parameters and append them to the url
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
186
+ headers.delete_if do |key, 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
193
+ true
194
+ else
195
+ false
196
+ end
197
+ end
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
208
+ else
209
+ url
210
+ end
211
+ end
212
+
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")
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)
364
+ headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
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
+
395
+ headers
396
+ end
397
+
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
418
+ else
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
+
436
+ end
437
+ end
438
+
439
+ def net_http_request_class(method)
440
+ Net::HTTP.const_get(method.capitalize, false)
441
+ end
442
+
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
450
+ end
451
+
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
494
+ end
495
+
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
519
+ else
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
563
+ else
564
+ target_values = value.to_s.split ','
565
+ end
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}")
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
610
+ end
611
+
612
+ def print_verify_callback_warnings
613
+ warned = false
614
+ if RestClient::Platform.mac_mri?
615
+ warn('warning: ssl_verify_callback return code is ignored on OS X')
616
+ warned = true
617
+ end
618
+ if RestClient::Platform.jruby?
619
+ warn('warning: SSL verify_callback may not work correctly in jruby')
620
+ warn('see https://github.com/jruby/jruby/issues/597')
621
+ warned = true
622
+ end
623
+ warned
624
+ end
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
+
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
+
649
+ setup_credentials req
650
+
651
+ net = net_http_object(uri.hostname, uri.port)
652
+ net.use_ssl = uri.is_a?(URI::HTTPS)
653
+ net.ssl_version = ssl_version if ssl_version
654
+ net.ciphers = ssl_ciphers if ssl_ciphers
655
+
656
+ net.verify_mode = verify_ssl
657
+
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
663
+
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.
667
+ if ssl_verify_callback
668
+ net.verify_callback = ssl_verify_callback
669
+
670
+ # Hilariously, jruby only calls the callback when cert_store is set to
671
+ # something, so make sure to set one.
672
+ # https://github.com/jruby/jruby/issues/597
673
+ if RestClient::Platform.jruby?
674
+ net.cert_store ||= OpenSSL::X509::Store.new
675
+ end
676
+
677
+ if ssl_verify_callback_warnings != false
678
+ if print_verify_callback_warnings
679
+ warn('pass :ssl_verify_callback_warnings => false to silence this')
680
+ end
681
+ end
682
+ end
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
+
705
+ RestClient.before_execution_procs.each do |before_proc|
706
+ before_proc.call(req, args)
707
+ end
708
+
709
+ if @before_execution_proc
710
+ @before_execution_proc.call(req, args)
711
+ end
712
+
713
+ log_request
714
+
715
+ net.start do |http|
716
+ established_connection = true
717
+
718
+ if @block_response
719
+ net_http_do_request(http, req, payload, &@block_response)
720
+ else
721
+ res = net_http_do_request(http, req, payload) { |http_response|
722
+ fetch_body(http_response)
723
+ }
724
+ log_response res
725
+ process_result res, & block
726
+ end
727
+ end
728
+ rescue EOFError
729
+ raise RestClient::ServerBrokeConnection
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
+
742
+ rescue OpenSSL::SSL::SSLError => 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
762
+ end
763
+
764
+ def setup_credentials(req)
765
+ req.basic_auth(user, password) if user && !headers.has_key?("Authorization")
766
+ end
767
+
768
+ def fetch_body(http_response)
769
+ if @raw_response
770
+ # Taken from Chef, which as in turn...
771
+ # Stolen from http://www.ruby-forum.com/topic/166423
772
+ # Kudos to _why!
773
+ @tf = Tempfile.new('rest-client.')
774
+ @tf.binmode
775
+ size, total = 0, http_response['Content-Length'].to_i
776
+ http_response.read_body do |chunk|
777
+ @tf.write chunk
778
+ size += chunk.size
779
+ if RestClient.log
780
+ if size == 0
781
+ RestClient.log << "%s %s done (0 length file)\n" % [@method, @url]
782
+ elsif total == 0
783
+ RestClient.log << "%s %s (zero content length)\n" % [@method, @url]
784
+ else
785
+ RestClient.log << "%s %s %d%% done (%d of %d)\n" % [@method, @url, (size * 100) / total, size, total]
786
+ end
787
+ end
788
+ end
789
+ @tf.close
790
+ @tf
791
+ else
792
+ http_response.read_body
793
+ end
794
+ http_response
795
+ end
796
+
797
+ def process_result res, & block
798
+ if @raw_response
799
+ # We don't decode raw requests
800
+ response = RawResponse.new(@tf, res, self)
801
+ else
802
+ decoded = Request.decode(res['content-encoding'], res.body)
803
+ response = Response.create(decoded, res, self)
804
+ end
805
+
806
+ if block_given?
807
+ block.call(response, self, res, & block)
808
+ else
809
+ response.return!(&block)
810
+ end
811
+
812
+ end
813
+
814
+ def parser
815
+ URI.const_defined?(:Parser) ? URI::Parser.new : URI
816
+ end
817
+
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
843
+ end
844
+
845
+ types = MIME::Types.type_for(ext)
846
+ if types.empty?
847
+ ext
848
+ else
849
+ types.first.content_type
850
+ end
851
+ end
852
+ end
853
+ end