rest-client 1.1.0 → 1.6.3

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.

Potentially problematic release.


This version of rest-client might be problematic. Click here for more details.

@@ -1,252 +1,314 @@
1
1
  require 'tempfile'
2
+ require 'mime/types'
3
+ require 'cgi'
2
4
 
3
5
  module RestClient
4
- # This class is used internally by RestClient to send the request, but you can also
5
- # call it directly if you'd like to use a method not supported by the
6
- # main API. For example:
7
- #
8
- # RestClient::Request.execute(:method => :head, :url => 'http://example.com')
9
- #
10
- # Mandatory parameters:
11
- # * :method
12
- # * :url
13
- # Optional parameters (have a look at ssl and/or uri for some explanations):
14
- # * :headers a hash containing the request headers
15
- # * :cookies will replace possible cookies in the :headers
16
- # * :user and :password for basic auth, will be replaced by a user/password available in the :url
17
- # * :raw_response return a low-level RawResponse instead of a Response
18
- # * :verify_ssl enable ssl verification, possible values are constants from OpenSSL::SSL
19
- # * :timeout and :open_timeout
20
- # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file
21
- class Request
22
- attr_reader :method, :url, :payload, :headers,
23
- :cookies, :user, :password, :timeout, :open_timeout,
24
- :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file,
25
- :raw_response
26
-
27
- def self.execute(args)
28
- new(args).execute
29
- end
30
-
31
- def initialize(args)
32
- @method = args[:method] or raise ArgumentError, "must pass :method"
33
- @url = args[:url] or raise ArgumentError, "must pass :url"
34
- @headers = args[:headers] || {}
35
- @cookies = @headers.delete(:cookies) || args[:cookies] || {}
36
- @payload = Payload.generate(args[:payload])
37
- @user = args[:user]
38
- @password = args[:password]
39
- @timeout = args[:timeout]
40
- @open_timeout = args[:open_timeout]
41
- @raw_response = args[:raw_response] || false
42
- @verify_ssl = args[:verify_ssl] || false
43
- @ssl_client_cert = args[:ssl_client_cert] || nil
44
- @ssl_client_key = args[:ssl_client_key] || nil
45
- @ssl_ca_file = args[:ssl_ca_file] || nil
46
- @tf = nil # If you are a raw request, this is your tempfile
47
- end
48
-
49
- def execute
50
- execute_inner
51
- rescue Redirect => e
52
- @url = e.url
53
- @method = :get
54
- @payload = nil
55
- execute
56
- end
57
-
58
- def execute_inner
59
- uri = parse_url_with_auth(url)
60
- transmit uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload
61
- end
62
-
63
- def make_headers(user_headers)
64
- unless @cookies.empty?
65
- user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ')
66
- end
67
-
68
- headers = default_headers.merge(user_headers).inject({}) do |final, (key, value)|
69
- final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s
70
- final
71
- end
72
-
73
- headers.merge!(@payload.headers) if @payload
74
- headers
75
- end
76
-
77
- def net_http_class
78
- if RestClient.proxy
79
- proxy_uri = URI.parse(RestClient.proxy)
80
- Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
81
- else
82
- Net::HTTP
83
- end
84
- end
85
-
86
- def net_http_request_class(method)
87
- Net::HTTP.const_get(method.to_s.capitalize)
88
- end
89
-
90
- def parse_url(url)
91
- url = "http://#{url}" unless url.match(/^http/)
92
- URI.parse(url)
93
- end
94
-
95
- def parse_url_with_auth(url)
96
- uri = parse_url(url)
97
- @user = uri.user if uri.user
98
- @password = uri.password if uri.password
99
- uri
100
- end
101
-
102
- def process_payload(p=nil, parent_key=nil)
103
- unless p.is_a?(Hash)
104
- p
105
- else
106
- @headers[:content_type] ||= 'application/x-www-form-urlencoded'
107
- p.keys.map do |k|
108
- key = parent_key ? "#{parent_key}[#{k}]" : k
109
- if p[k].is_a? Hash
110
- process_payload(p[k], key)
111
- else
112
- value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
113
- "#{key}=#{value}"
114
- end
115
- end.join("&")
116
- end
117
- end
118
-
119
- def transmit(uri, req, payload)
120
- setup_credentials(req)
121
-
122
- net = net_http_class.new(uri.host, uri.port)
123
- net.use_ssl = uri.is_a?(URI::HTTPS)
124
- if @verify_ssl == false
125
- net.verify_mode = OpenSSL::SSL::VERIFY_NONE
126
- elsif @verify_ssl.is_a? Integer
127
- net.verify_mode = @verify_ssl
128
- end
129
- net.cert = @ssl_client_cert if @ssl_client_cert
130
- net.key = @ssl_client_key if @ssl_client_key
131
- net.ca_file = @ssl_ca_file if @ssl_ca_file
132
- net.read_timeout = @timeout if @timeout
133
- net.open_timeout = @open_timeout if @open_timeout
134
-
135
- display_log request_log
136
-
137
- net.start do |http|
138
- res = http.request(req, payload) { |http_response| fetch_body(http_response) }
139
- result = process_result(res)
140
- display_log response_log(res)
141
-
142
- if result.kind_of?(String) or @method == :head
143
- Response.new(result, res)
144
- elsif @raw_response
145
- RawResponse.new(@tf, res)
146
- else
147
- Response.new(nil, res)
148
- end
149
- end
150
- rescue EOFError
151
- raise RestClient::ServerBrokeConnection
152
- rescue Timeout::Error
153
- raise RestClient::RequestTimeout
154
- end
155
-
156
- def setup_credentials(req)
157
- req.basic_auth(user, password) if user
158
- end
159
-
160
- def fetch_body(http_response)
161
- if @raw_response
162
- # Taken from Chef, which as in turn...
163
- # Stolen from http://www.ruby-forum.com/topic/166423
164
- # Kudos to _why!
165
- @tf = Tempfile.new("rest-client")
166
- size, total = 0, http_response.header['Content-Length'].to_i
167
- http_response.read_body do |chunk|
168
- @tf.write(chunk)
169
- size += chunk.size
170
- if size == 0
171
- display_log("#{@method} #{@url} done (0 length file)")
172
- elsif total == 0
173
- display_log("#{@method} #{@url} (zero content length)")
174
- else
175
- display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total])
176
- end
177
- end
178
- @tf.close
179
- @tf
180
- else
181
- http_response.read_body
182
- end
183
- http_response
184
- end
185
-
186
- def process_result(res)
187
- if res.code =~ /\A2\d{2}\z/
188
- # We don't decode raw requests
189
- unless @raw_response
190
- self.class.decode res['content-encoding'], res.body if res.body
191
- end
192
- elsif %w(301 302 303).include? res.code
193
- url = res.header['Location']
194
-
195
- if url !~ /^http/
196
- uri = URI.parse(@url)
197
- uri.path = "/#{url}".squeeze('/')
198
- url = uri.to_s
199
- end
200
-
201
- raise Redirect, url
202
- elsif res.code == "304"
203
- raise NotModified, res
204
- elsif res.code == "401"
205
- raise Unauthorized, res
206
- elsif res.code == "404"
207
- raise ResourceNotFound, res
208
- else
209
- raise RequestFailed, res
210
- end
211
- end
212
-
213
- def self.decode(content_encoding, body)
214
- if content_encoding == 'gzip' and not body.empty?
215
- Zlib::GzipReader.new(StringIO.new(body)).read
216
- elsif content_encoding == 'deflate'
217
- Zlib::Inflate.new.inflate(body)
218
- else
219
- body
220
- end
221
- end
222
-
223
- def request_log
224
- out = []
225
- out << "RestClient.#{method} #{url.inspect}"
226
- out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
227
- out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
228
- out.join(', ')
229
- end
230
-
231
- def response_log(res)
232
- size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
233
- "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes"
234
- end
235
-
236
- def display_log(msg)
237
- return unless log_to = RestClient.log
238
-
239
- if log_to == 'stdout'
240
- STDOUT.puts msg
241
- elsif log_to == 'stderr'
242
- STDERR.puts msg
243
- else
244
- File.open(log_to, 'a') { |f| f.puts msg }
245
- end
246
- end
247
-
248
- def default_headers
249
- { :accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate' }
250
- end
251
- end
6
+ # This class is used internally by RestClient to send the request, but you can also
7
+ # call it directly if you'd like to use a method not supported by the
8
+ # main API. For example:
9
+ #
10
+ # RestClient::Request.execute(:method => :head, :url => 'http://example.com')
11
+ #
12
+ # Mandatory parameters:
13
+ # * :method
14
+ # * :url
15
+ # Optional parameters (have a look at ssl and/or uri for some explanations):
16
+ # * :headers a hash containing the request headers
17
+ # * :cookies will replace possible cookies in the :headers
18
+ # * :user and :password for basic auth, will be replaced by a user/password available in the :url
19
+ # * :block_response call the provided block with the HTTPResponse as parameter
20
+ # * :raw_response return a low-level RawResponse instead of a Response
21
+ # * :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
24
+ # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file
25
+ class Request
26
+
27
+ attr_reader :method, :url, :headers, :cookies,
28
+ :payload, :user, :password, :timeout, :max_redirects,
29
+ :open_timeout, :raw_response, :verify_ssl, :ssl_client_cert,
30
+ :ssl_client_key, :ssl_ca_file, :processed_headers, :args
31
+
32
+ def self.execute(args, & block)
33
+ new(args).execute(& block)
34
+ end
35
+
36
+ def initialize args
37
+ @method = args[:method] or raise ArgumentError, "must pass :method"
38
+ @headers = args[:headers] || {}
39
+ if args[:url]
40
+ @url = process_get_params(args[:url], headers)
41
+ else
42
+ raise ArgumentError, "must pass :url"
43
+ end
44
+ @cookies = @headers.delete(:cookies) || args[:cookies] || {}
45
+ @payload = Payload.generate(args[:payload])
46
+ @user = args[:user]
47
+ @password = args[:password]
48
+ @timeout = args[:timeout]
49
+ @open_timeout = args[:open_timeout]
50
+ @block_response = args[:block_response]
51
+ @raw_response = args[:raw_response] || false
52
+ @verify_ssl = args[:verify_ssl] || false
53
+ @ssl_client_cert = args[:ssl_client_cert] || nil
54
+ @ssl_client_key = args[:ssl_client_key] || nil
55
+ @ssl_ca_file = args[:ssl_ca_file] || nil
56
+ @tf = nil # If you are a raw request, this is your tempfile
57
+ @max_redirects = args[:max_redirects] || 10
58
+ @processed_headers = make_headers headers
59
+ @args = args
60
+ end
61
+
62
+ def execute & block
63
+ uri = parse_url_with_auth(url)
64
+ transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload, & block
65
+ end
66
+
67
+ # Extract the query parameters for get request and append them to the url
68
+ def process_get_params url, headers
69
+ if [:get, :head, :delete].include? method
70
+ get_params = {}
71
+ headers.delete_if do |key, value|
72
+ if 'params' == key.to_s.downcase && value.is_a?(Hash)
73
+ get_params.merge! value
74
+ true
75
+ else
76
+ false
77
+ end
78
+ end
79
+ unless get_params.empty?
80
+ query_string = get_params.collect { |k, v| "#{k.to_s}=#{CGI::escape(v.to_s)}" }.join('&')
81
+ url + "?#{query_string}"
82
+ else
83
+ url
84
+ end
85
+ else
86
+ url
87
+ end
88
+ end
89
+
90
+ def make_headers user_headers
91
+ unless @cookies.empty?
92
+ user_headers[:cookie] = @cookies.map { |(key, val)| "#{key.to_s}=#{CGI::unescape(val)}" }.sort.join(';')
93
+ end
94
+ headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
95
+ headers.merge!(@payload.headers) if @payload
96
+ headers
97
+ end
98
+
99
+ def net_http_class
100
+ if RestClient.proxy
101
+ proxy_uri = URI.parse(RestClient.proxy)
102
+ Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
103
+ else
104
+ Net::HTTP
105
+ end
106
+ end
107
+
108
+ def net_http_request_class(method)
109
+ Net::HTTP.const_get(method.to_s.capitalize)
110
+ end
111
+
112
+ def parse_url(url)
113
+ url = "http://#{url}" unless url.match(/^http/)
114
+ URI.parse(url)
115
+ end
116
+
117
+ def parse_url_with_auth(url)
118
+ uri = parse_url(url)
119
+ @user = CGI.unescape(uri.user) if uri.user
120
+ @password = CGI.unescape(uri.password) if uri.password
121
+ uri
122
+ end
123
+
124
+ def process_payload(p=nil, parent_key=nil)
125
+ unless p.is_a?(Hash)
126
+ p
127
+ else
128
+ @headers[:content_type] ||= 'application/x-www-form-urlencoded'
129
+ p.keys.map do |k|
130
+ key = parent_key ? "#{parent_key}[#{k}]" : k
131
+ if p[k].is_a? Hash
132
+ process_payload(p[k], key)
133
+ else
134
+ value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
135
+ "#{key}=#{value}"
136
+ end
137
+ end.join("&")
138
+ end
139
+ end
140
+
141
+ def transmit uri, req, payload, & block
142
+ setup_credentials req
143
+
144
+ net = net_http_class.new(uri.host, uri.port)
145
+ net.use_ssl = uri.is_a?(URI::HTTPS)
146
+ if (@verify_ssl == false) || (@verify_ssl == OpenSSL::SSL::VERIFY_NONE)
147
+ net.verify_mode = OpenSSL::SSL::VERIFY_NONE
148
+ elsif @verify_ssl.is_a? Integer
149
+ net.verify_mode = @verify_ssl
150
+ net.verify_callback = lambda do |preverify_ok, ssl_context|
151
+ if (!preverify_ok) || ssl_context.error != 0
152
+ err_msg = "SSL Verification failed -- Preverify: #{preverify_ok}, Error: #{ssl_context.error_string} (#{ssl_context.error})"
153
+ raise SSLCertificateNotVerified.new(err_msg)
154
+ end
155
+ true
156
+ end
157
+ end
158
+ net.cert = @ssl_client_cert if @ssl_client_cert
159
+ net.key = @ssl_client_key if @ssl_client_key
160
+ net.ca_file = @ssl_ca_file if @ssl_ca_file
161
+ net.read_timeout = @timeout if @timeout
162
+ net.open_timeout = @open_timeout if @open_timeout
163
+
164
+ RestClient.before_execution_procs.each do |before_proc|
165
+ before_proc.call(req, args)
166
+ end
167
+
168
+ log_request
169
+
170
+ net.start do |http|
171
+ if @block_response
172
+ http.request(req, payload ? payload.to_s : nil, & @block_response)
173
+ else
174
+ res = http.request(req, payload ? payload.to_s : nil) { |http_response| fetch_body(http_response) }
175
+ log_response res
176
+ process_result res, & block
177
+ end
178
+ end
179
+ rescue EOFError
180
+ raise RestClient::ServerBrokeConnection
181
+ rescue Timeout::Error
182
+ raise RestClient::RequestTimeout
183
+ end
184
+
185
+ def setup_credentials(req)
186
+ req.basic_auth(user, password) if user
187
+ end
188
+
189
+ def fetch_body(http_response)
190
+ if @raw_response
191
+ # Taken from Chef, which as in turn...
192
+ # Stolen from http://www.ruby-forum.com/topic/166423
193
+ # Kudos to _why!
194
+ @tf = Tempfile.new("rest-client")
195
+ size, total = 0, http_response.header['Content-Length'].to_i
196
+ http_response.read_body do |chunk|
197
+ @tf.write chunk
198
+ size += chunk.size
199
+ if RestClient.log
200
+ if size == 0
201
+ RestClient.log << "#{@method} #{@url} done (0 length file\n)"
202
+ elsif total == 0
203
+ RestClient.log << "#{@method} #{@url} (zero content length)\n"
204
+ else
205
+ RestClient.log << "#{@method} #{@url} %d%% done (%d of %d)\n" % [(size * 100) / total, size, total]
206
+ end
207
+ end
208
+ end
209
+ @tf.close
210
+ @tf
211
+ else
212
+ http_response.read_body
213
+ end
214
+ http_response
215
+ end
216
+
217
+ def process_result res, & block
218
+ if @raw_response
219
+ # We don't decode raw requests
220
+ response = RawResponse.new(@tf, res, args)
221
+ else
222
+ response = Response.create(Request.decode(res['content-encoding'], res.body), res, args)
223
+ end
224
+
225
+ if block_given?
226
+ block.call(response, self, res, & block)
227
+ else
228
+ response.return!(self, res, & block)
229
+ end
230
+
231
+ end
232
+
233
+ def self.decode content_encoding, body
234
+ if (!body) || body.empty?
235
+ body
236
+ elsif content_encoding == 'gzip'
237
+ Zlib::GzipReader.new(StringIO.new(body)).read
238
+ elsif content_encoding == 'deflate'
239
+ begin
240
+ Zlib::Inflate.new.inflate body
241
+ rescue Zlib::DataError
242
+ # No luck with Zlib decompression. Let's try with raw deflate,
243
+ # like some broken web servers do.
244
+ Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
245
+ end
246
+ else
247
+ body
248
+ end
249
+ end
250
+
251
+ def log_request
252
+ if RestClient.log
253
+ out = []
254
+ out << "RestClient.#{method} #{url.inspect}"
255
+ out << payload.short_inspect if payload
256
+ out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
257
+ RestClient.log << out.join(', ') + "\n"
258
+ end
259
+ end
260
+
261
+ def log_response res
262
+ if RestClient.log
263
+ size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
264
+ RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
265
+ end
266
+ end
267
+
268
+ # Return a hash of headers whose keys are capitalized strings
269
+ def stringify_headers headers
270
+ headers.inject({}) do |result, (key, value)|
271
+ if key.is_a? Symbol
272
+ key = key.to_s.split(/_/).map { |w| w.capitalize }.join('-')
273
+ end
274
+ if 'CONTENT-TYPE' == key.upcase
275
+ target_value = value.to_s
276
+ result[key] = MIME::Types.type_for_extension target_value
277
+ elsif 'ACCEPT' == key.upcase
278
+ # Accept can be composed of several comma-separated values
279
+ if value.is_a? Array
280
+ target_values = value
281
+ else
282
+ target_values = value.to_s.split ','
283
+ end
284
+ result[key] = target_values.map { |ext| MIME::Types.type_for_extension(ext.to_s.strip) }.join(', ')
285
+ else
286
+ result[key] = value.to_s
287
+ end
288
+ result
289
+ end
290
+ end
291
+
292
+ def default_headers
293
+ {:accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate'}
294
+ end
295
+
296
+ end
297
+ end
298
+
299
+ module MIME
300
+ class Types
301
+
302
+ # Return the first found content-type for a value considered as an extension or the value itself
303
+ def type_for_extension ext
304
+ candidates = @extension_index[ext]
305
+ candidates.empty? ? ext : candidates[0].content_type
306
+ end
307
+
308
+ class << self
309
+ def type_for_extension ext
310
+ @__types__.type_for_extension ext
311
+ end
312
+ end
313
+ end
252
314
  end