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.
- checksums.yaml +5 -5
- data/.gitignore +6 -6
- data/.rspec +2 -1
- data/.rubocop-disables.yml +384 -0
- data/.rubocop.yml +3 -0
- data/.travis.yml +46 -1
- data/AUTHORS +28 -5
- data/Gemfile +5 -1
- data/LICENSE +21 -0
- data/README.md +784 -0
- data/Rakefile +95 -12
- data/bin/restclient +11 -12
- data/history.md +180 -16
- data/lib/restclient.rb +25 -11
- data/lib/restclient/abstract_response.rb +171 -51
- data/lib/restclient/exceptions.rb +102 -56
- data/lib/restclient/params_array.rb +72 -0
- data/lib/restclient/payload.rb +43 -74
- data/lib/restclient/platform.rb +22 -2
- data/lib/restclient/raw_response.rb +7 -3
- data/lib/restclient/request.rb +672 -179
- data/lib/restclient/resource.rb +6 -7
- data/lib/restclient/response.rb +64 -10
- data/lib/restclient/utils.rb +235 -0
- data/lib/restclient/version.rb +2 -1
- data/lib/restclient/windows.rb +8 -0
- data/lib/restclient/windows/root_certs.rb +105 -0
- data/rest-client.gemspec +16 -11
- data/rest-client.windows.gemspec +19 -0
- data/spec/helpers.rb +22 -0
- data/spec/integration/_lib.rb +1 -0
- data/spec/integration/capath_verisign/415660c1.0 +14 -0
- data/spec/integration/capath_verisign/7651b327.0 +14 -0
- data/spec/integration/capath_verisign/README +8 -0
- data/spec/integration/capath_verisign/verisign.crt +14 -0
- data/spec/integration/httpbin_spec.rb +87 -0
- data/spec/integration/integration_spec.rb +125 -0
- data/spec/integration/request_spec.rb +72 -20
- data/spec/spec_helper.rb +29 -0
- data/spec/unit/_lib.rb +1 -0
- data/spec/unit/abstract_response_spec.rb +145 -0
- data/spec/unit/exceptions_spec.rb +108 -0
- data/spec/{master_shake.jpg → unit/master_shake.jpg} +0 -0
- data/spec/unit/params_array_spec.rb +36 -0
- data/spec/{payload_spec.rb → unit/payload_spec.rb} +73 -54
- data/spec/{raw_response_spec.rb → unit/raw_response_spec.rb} +5 -4
- data/spec/unit/request2_spec.rb +54 -0
- data/spec/unit/request_spec.rb +1250 -0
- data/spec/unit/resource_spec.rb +134 -0
- data/spec/unit/response_spec.rb +241 -0
- data/spec/unit/restclient_spec.rb +79 -0
- data/spec/unit/utils_spec.rb +147 -0
- data/spec/unit/windows/root_certs_spec.rb +22 -0
- metadata +143 -53
- data/README.rdoc +0 -300
- data/lib/restclient/net_http_ext.rb +0 -55
- data/spec/abstract_response_spec.rb +0 -85
- data/spec/base.rb +0 -13
- data/spec/exceptions_spec.rb +0 -98
- data/spec/integration_spec.rb +0 -38
- data/spec/request2_spec.rb +0 -35
- data/spec/request_spec.rb +0 -528
- data/spec/resource_spec.rb +0 -136
- data/spec/response_spec.rb +0 -169
- 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
|
data/lib/restclient/payload.rb
CHANGED
@@ -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.
|
27
|
-
|
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
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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(
|
62
|
-
@stream.read(
|
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
|
85
|
-
result =
|
86
|
-
|
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
|
113
|
-
|
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" :
|
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(
|
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 =
|
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
|
-
|
174
|
-
|
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
|
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
|
data/lib/restclient/platform.rb
CHANGED
@@ -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.
|
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
|
-
|
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
|
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
|
data/lib/restclient/request.rb
CHANGED
@@ -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
|
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
|
-
# * :
|
23
|
-
#
|
24
|
-
# * :
|
25
|
-
#
|
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, :
|
29
|
-
:
|
30
|
-
:open_timeout, :raw_response, :
|
31
|
-
:
|
32
|
-
|
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]
|
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
|
-
|
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
|
-
|
49
|
-
@
|
50
|
-
@
|
51
|
-
|
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
|
-
|
55
|
-
@
|
56
|
-
|
57
|
-
@
|
58
|
-
|
59
|
-
|
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
|
-
|
68
|
-
|
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
|
-
|
75
|
-
|
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
|
78
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
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.
|
440
|
+
Net::HTTP.const_get(method.capitalize, false)
|
112
441
|
end
|
113
442
|
|
114
|
-
def
|
115
|
-
|
116
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
127
|
-
|
128
|
-
|
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
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
-
|
137
|
-
"#{key}=#{value}"
|
564
|
+
target_values = value.to_s.split ','
|
138
565
|
end
|
139
|
-
|
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.
|
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 =
|
651
|
+
net = net_http_object(uri.hostname, uri.port)
|
161
652
|
net.use_ssl = uri.is_a?(URI::HTTPS)
|
162
|
-
if
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
178
|
-
net.
|
179
|
-
net.
|
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
|
-
#
|
182
|
-
#
|
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
|
719
|
+
net_http_do_request(http, req, payload, &@block_response)
|
209
720
|
else
|
210
|
-
res = http
|
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
|
218
|
-
raise RestClient::
|
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
|
-
#
|
221
|
-
#
|
222
|
-
|
223
|
-
|
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(
|
236
|
-
|
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 << "
|
781
|
+
RestClient.log << "%s %s done (0 length file)\n" % [@method, @url]
|
243
782
|
elsif total == 0
|
244
|
-
RestClient.log << "
|
783
|
+
RestClient.log << "%s %s (zero content length)\n" % [@method, @url]
|
245
784
|
else
|
246
|
-
RestClient.log << "
|
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,
|
800
|
+
response = RawResponse.new(@tf, res, self)
|
262
801
|
else
|
263
|
-
|
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!(
|
809
|
+
response.return!(&block)
|
270
810
|
end
|
271
811
|
|
272
812
|
end
|
273
813
|
|
274
|
-
def
|
275
|
-
|
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
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
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
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
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
|