rest-client 1.6.14

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.

Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +3 -0
  5. data/AUTHORS +75 -0
  6. data/Gemfile +7 -0
  7. data/README.rdoc +300 -0
  8. data/Rakefile +49 -0
  9. data/bin/restclient +93 -0
  10. data/history.md +160 -0
  11. data/lib/rest-client.rb +2 -0
  12. data/lib/rest_client.rb +2 -0
  13. data/lib/restclient.rb +170 -0
  14. data/lib/restclient/abstract_response.rb +106 -0
  15. data/lib/restclient/exceptions.rb +198 -0
  16. data/lib/restclient/net_http_ext.rb +55 -0
  17. data/lib/restclient/payload.rb +240 -0
  18. data/lib/restclient/platform.rb +29 -0
  19. data/lib/restclient/raw_response.rb +34 -0
  20. data/lib/restclient/request.rb +360 -0
  21. data/lib/restclient/resource.rb +169 -0
  22. data/lib/restclient/response.rb +26 -0
  23. data/lib/restclient/version.rb +7 -0
  24. data/rest-client.gemspec +26 -0
  25. data/spec/abstract_response_spec.rb +85 -0
  26. data/spec/base.rb +13 -0
  27. data/spec/exceptions_spec.rb +98 -0
  28. data/spec/integration/capath_digicert/244b5494.0 +19 -0
  29. data/spec/integration/capath_digicert/81b9768f.0 +19 -0
  30. data/spec/integration/capath_digicert/README +8 -0
  31. data/spec/integration/capath_digicert/digicert.crt +19 -0
  32. data/spec/integration/certs/digicert.crt +19 -0
  33. data/spec/integration/certs/verisign.crt +14 -0
  34. data/spec/integration/request_spec.rb +75 -0
  35. data/spec/integration_spec.rb +38 -0
  36. data/spec/master_shake.jpg +0 -0
  37. data/spec/payload_spec.rb +244 -0
  38. data/spec/raw_response_spec.rb +17 -0
  39. data/spec/request2_spec.rb +35 -0
  40. data/spec/request_spec.rb +528 -0
  41. data/spec/resource_spec.rb +136 -0
  42. data/spec/response_spec.rb +169 -0
  43. data/spec/restclient_spec.rb +73 -0
  44. metadata +192 -0
@@ -0,0 +1,29 @@
1
+ module RestClient
2
+ module Platform
3
+ # Return true if we are running on a darwin-based Ruby platform. This will
4
+ # be false for jruby even on OS X.
5
+ #
6
+ # @return [Boolean]
7
+ def self.mac?
8
+ RUBY_PLATFORM.include?('darwin')
9
+ end
10
+
11
+ # Return true if we are running on Windows.
12
+ #
13
+ # @return [Boolean]
14
+ #
15
+ def self.windows?
16
+ # Ruby only sets File::ALT_SEPARATOR on Windows, and the Ruby standard
17
+ # library uses that to test what platform it's on.
18
+ !!File::ALT_SEPARATOR
19
+ end
20
+
21
+ # Return true if we are running on jruby.
22
+ #
23
+ # @return [Boolean]
24
+ #
25
+ def self.jruby?
26
+ RUBY_PLATFORM == 'java'
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
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
17
+
18
+ def initialize tempfile, net_http_res, args
19
+ @net_http_res = net_http_res
20
+ @args = args
21
+ @file = tempfile
22
+ end
23
+
24
+ def to_s
25
+ @file.open
26
+ @file.read
27
+ end
28
+
29
+ def size
30
+ File.size file
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,360 @@
1
+ require 'tempfile'
2
+ require 'mime/types'
3
+ require 'cgi'
4
+
5
+ module RestClient
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 passing in -1 will disable the timeout by setting the corresponding net timeout values to nil
24
+ # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file
25
+ # * :ssl_verify_callback, :ssl_verify_callback_warnings
26
+ class Request
27
+
28
+ attr_reader :method, :url, :headers, :cookies,
29
+ :payload, :user, :password, :timeout, :max_redirects,
30
+ :open_timeout, :raw_response, :verify_ssl, :ssl_client_cert,
31
+ :ssl_client_key, :ssl_ca_file, :processed_headers, :args,
32
+ :ssl_verify_callback, :ssl_verify_callback_warnings
33
+
34
+ def self.execute(args, & block)
35
+ new(args).execute(& block)
36
+ end
37
+
38
+ def initialize args
39
+ @method = args[:method] or raise ArgumentError, "must pass :method"
40
+ @headers = args[:headers] || {}
41
+ if args[:url]
42
+ @url = process_url_params(args[:url], headers)
43
+ else
44
+ raise ArgumentError, "must pass :url"
45
+ end
46
+ @cookies = @headers.delete(:cookies) || args[:cookies] || {}
47
+ @payload = Payload.generate(args[:payload])
48
+ @user = args[:user]
49
+ @password = args[:password]
50
+ @timeout = args[:timeout]
51
+ @open_timeout = args[:open_timeout]
52
+ @block_response = args[:block_response]
53
+ @raw_response = args[:raw_response] || false
54
+ @verify_ssl = args[:verify_ssl] || false
55
+ @ssl_client_cert = args[:ssl_client_cert] || nil
56
+ @ssl_client_key = args[:ssl_client_key] || nil
57
+ @ssl_ca_file = args[:ssl_ca_file] || nil
58
+ @ssl_verify_callback = args[:ssl_verify_callback] || nil
59
+ @ssl_verify_callback_warnings = args.fetch(:ssl_verify_callback, true)
60
+ @tf = nil # If you are a raw request, this is your tempfile
61
+ @max_redirects = args[:max_redirects] || 10
62
+ @processed_headers = make_headers headers
63
+ @args = args
64
+ end
65
+
66
+ def execute & block
67
+ uri = parse_url_with_auth(url)
68
+ transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload, & block
69
+ ensure
70
+ payload.close if payload
71
+ end
72
+
73
+ # Extract the query parameters and append them to the url
74
+ def process_url_params url, headers
75
+ url_params = {}
76
+ headers.delete_if do |key, value|
77
+ if 'params' == key.to_s.downcase && value.is_a?(Hash)
78
+ url_params.merge! value
79
+ true
80
+ else
81
+ false
82
+ end
83
+ end
84
+ unless url_params.empty?
85
+ query_string = url_params.collect { |k, v| "#{k.to_s}=#{CGI::escape(v.to_s)}" }.join('&')
86
+ url + "?#{query_string}"
87
+ else
88
+ url
89
+ end
90
+ end
91
+
92
+ def make_headers user_headers
93
+ unless @cookies.empty?
94
+ user_headers[:cookie] = @cookies.map { |(key, val)| "#{key.to_s}=#{CGI::unescape(val)}" }.sort.join('; ')
95
+ end
96
+ headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
97
+ headers.merge!(@payload.headers) if @payload
98
+ headers
99
+ end
100
+
101
+ def net_http_class
102
+ if RestClient.proxy
103
+ proxy_uri = URI.parse(RestClient.proxy)
104
+ Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
105
+ else
106
+ Net::HTTP
107
+ end
108
+ end
109
+
110
+ def net_http_request_class(method)
111
+ Net::HTTP.const_get(method.to_s.capitalize)
112
+ end
113
+
114
+ def parse_url(url)
115
+ url = "http://#{url}" unless url.match(/^http/)
116
+ URI.parse(url)
117
+ end
118
+
119
+ def parse_url_with_auth(url)
120
+ uri = parse_url(url)
121
+ @user = CGI.unescape(uri.user) if uri.user
122
+ @password = CGI.unescape(uri.password) if uri.password
123
+ uri
124
+ end
125
+
126
+ def process_payload(p=nil, parent_key=nil)
127
+ unless p.is_a?(Hash)
128
+ p
129
+ else
130
+ @headers[:content_type] ||= 'application/x-www-form-urlencoded'
131
+ p.keys.map do |k|
132
+ key = parent_key ? "#{parent_key}[#{k}]" : k
133
+ if p[k].is_a? Hash
134
+ process_payload(p[k], key)
135
+ else
136
+ value = parser.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
137
+ "#{key}=#{value}"
138
+ end
139
+ end.join("&")
140
+ end
141
+ end
142
+
143
+ def print_verify_callback_warnings
144
+ warned = false
145
+ if RestClient::Platform.mac?
146
+ warn('warning: ssl_verify_callback return code is ignored on OS X')
147
+ warned = true
148
+ end
149
+ if RestClient::Platform.jruby?
150
+ warn('warning: SSL verify_callback may not work correctly in jruby')
151
+ warn('see https://github.com/jruby/jruby/issues/597')
152
+ warned = true
153
+ end
154
+ warned
155
+ end
156
+
157
+ def transmit uri, req, payload, & block
158
+ setup_credentials req
159
+
160
+ net = net_http_class.new(uri.host, uri.port)
161
+ net.use_ssl = uri.is_a?(URI::HTTPS)
162
+ if @verify_ssl
163
+ if @verify_ssl.is_a? Integer
164
+ net.verify_mode = @verify_ssl
165
+ else
166
+ net.verify_mode = OpenSSL::SSL::VERIFY_PEER
167
+ end
168
+ else
169
+ net.verify_mode = OpenSSL::SSL::VERIFY_NONE
170
+ end
171
+ net.cert = @ssl_client_cert if @ssl_client_cert
172
+ net.key = @ssl_client_key if @ssl_client_key
173
+ net.ca_file = @ssl_ca_file if @ssl_ca_file
174
+ net.read_timeout = @timeout if @timeout
175
+ net.open_timeout = @open_timeout if @open_timeout
176
+
177
+ # disable the timeout if the timeout value is -1
178
+ net.read_timeout = nil if @timeout == -1
179
+ net.open_timeout = nil if @open_timeout == -1
180
+
181
+ # verify_callback isn't well supported on all platforms, but do allow
182
+ # users to set one if they want.
183
+ if ssl_verify_callback
184
+ net.verify_callback = ssl_verify_callback
185
+
186
+ # Hilariously, jruby only calls the callback when cert_store is set to
187
+ # something, so make sure to set one.
188
+ # https://github.com/jruby/jruby/issues/597
189
+ if RestClient::Platform.jruby?
190
+ net.cert_store ||= OpenSSL::X509::Store.new
191
+ end
192
+
193
+ if ssl_verify_callback_warnings != false
194
+ if print_verify_callback_warnings
195
+ warn('pass :ssl_verify_callback_warnings => false to silence this')
196
+ end
197
+ end
198
+ end
199
+
200
+ RestClient.before_execution_procs.each do |before_proc|
201
+ before_proc.call(req, args)
202
+ end
203
+
204
+ log_request
205
+
206
+ net.start do |http|
207
+ if @block_response
208
+ http.request(req, payload ? payload.to_s : nil, & @block_response)
209
+ else
210
+ res = http.request(req, payload ? payload.to_s : nil) { |http_response| fetch_body(http_response) }
211
+ log_response res
212
+ process_result res, & block
213
+ end
214
+ end
215
+ rescue EOFError
216
+ raise RestClient::ServerBrokeConnection
217
+ rescue Timeout::Error
218
+ raise RestClient::RequestTimeout
219
+ rescue OpenSSL::SSL::SSLError => error
220
+ # UGH. Not sure if this is needed at all. SSLCertificateNotVerified is not being used internally.
221
+ # I think it would be better to leave SSLError processing to the client (they'd have to do that anyway...)
222
+ raise SSLCertificateNotVerified.new(error.message) if error.message.include?("certificate verify failed")
223
+ raise error
224
+ end
225
+
226
+ def setup_credentials(req)
227
+ req.basic_auth(user, password) if user
228
+ end
229
+
230
+ def fetch_body(http_response)
231
+ if @raw_response
232
+ # Taken from Chef, which as in turn...
233
+ # Stolen from http://www.ruby-forum.com/topic/166423
234
+ # Kudos to _why!
235
+ @tf = Tempfile.new("rest-client")
236
+ size, total = 0, http_response.header['Content-Length'].to_i
237
+ http_response.read_body do |chunk|
238
+ @tf.write chunk
239
+ size += chunk.size
240
+ if RestClient.log
241
+ if size == 0
242
+ RestClient.log << "#{@method} #{@url} done (0 length file\n)"
243
+ elsif total == 0
244
+ RestClient.log << "#{@method} #{@url} (zero content length)\n"
245
+ else
246
+ RestClient.log << "#{@method} #{@url} %d%% done (%d of %d)\n" % [(size * 100) / total, size, total]
247
+ end
248
+ end
249
+ end
250
+ @tf.close
251
+ @tf
252
+ else
253
+ http_response.read_body
254
+ end
255
+ http_response
256
+ end
257
+
258
+ def process_result res, & block
259
+ if @raw_response
260
+ # We don't decode raw requests
261
+ response = RawResponse.new(@tf, res, args)
262
+ else
263
+ response = Response.create(Request.decode(res['content-encoding'], res.body), res, args)
264
+ end
265
+
266
+ if block_given?
267
+ block.call(response, self, res, & block)
268
+ else
269
+ response.return!(self, res, & block)
270
+ end
271
+
272
+ end
273
+
274
+ def self.decode content_encoding, body
275
+ if (!body) || body.empty?
276
+ body
277
+ elsif content_encoding == 'gzip'
278
+ Zlib::GzipReader.new(StringIO.new(body)).read
279
+ elsif content_encoding == 'deflate'
280
+ begin
281
+ Zlib::Inflate.new.inflate body
282
+ rescue Zlib::DataError
283
+ # No luck with Zlib decompression. Let's try with raw deflate,
284
+ # like some broken web servers do.
285
+ Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
286
+ end
287
+ else
288
+ body
289
+ end
290
+ end
291
+
292
+ def log_request
293
+ if RestClient.log
294
+ out = []
295
+ out << "RestClient.#{method} #{url.inspect}"
296
+ out << payload.short_inspect if payload
297
+ out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
298
+ RestClient.log << out.join(', ') + "\n"
299
+ end
300
+ end
301
+
302
+ def log_response res
303
+ if RestClient.log
304
+ size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
305
+ RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
306
+ end
307
+ end
308
+
309
+ # Return a hash of headers whose keys are capitalized strings
310
+ def stringify_headers headers
311
+ headers.inject({}) do |result, (key, value)|
312
+ if key.is_a? Symbol
313
+ key = key.to_s.split(/_/).map { |w| w.capitalize }.join('-')
314
+ end
315
+ if 'CONTENT-TYPE' == key.upcase
316
+ target_value = value.to_s
317
+ result[key] = MIME::Types.type_for_extension target_value
318
+ elsif 'ACCEPT' == key.upcase
319
+ # Accept can be composed of several comma-separated values
320
+ if value.is_a? Array
321
+ target_values = value
322
+ else
323
+ target_values = value.to_s.split ','
324
+ end
325
+ result[key] = target_values.map { |ext| MIME::Types.type_for_extension(ext.to_s.strip) }.join(', ')
326
+ else
327
+ result[key] = value.to_s
328
+ end
329
+ result
330
+ end
331
+ end
332
+
333
+ def default_headers
334
+ {:accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate'}
335
+ end
336
+
337
+ private
338
+ def parser
339
+ URI.const_defined?(:Parser) ? URI::Parser.new : URI
340
+ end
341
+
342
+ end
343
+ end
344
+
345
+ module MIME
346
+ class Types
347
+
348
+ # Return the first found content-type for a value considered as an extension or the value itself
349
+ def type_for_extension ext
350
+ candidates = @extension_index[ext]
351
+ candidates.empty? ? ext : candidates[0].content_type
352
+ end
353
+
354
+ class << self
355
+ def type_for_extension ext
356
+ @__types__.type_for_extension ext
357
+ end
358
+ end
359
+ end
360
+ end
@@ -0,0 +1,169 @@
1
+ module RestClient
2
+ # A class that can be instantiated for access to a RESTful resource,
3
+ # including authentication.
4
+ #
5
+ # Example:
6
+ #
7
+ # resource = RestClient::Resource.new('http://some/resource')
8
+ # jpg = resource.get(:accept => 'image/jpg')
9
+ #
10
+ # With HTTP basic authentication:
11
+ #
12
+ # resource = RestClient::Resource.new('http://protected/resource', :user => 'user', :password => 'password')
13
+ # resource.delete
14
+ #
15
+ # With a timeout (seconds):
16
+ #
17
+ # RestClient::Resource.new('http://slow', :timeout => 10)
18
+ #
19
+ # With an open timeout (seconds):
20
+ #
21
+ # RestClient::Resource.new('http://behindfirewall', :open_timeout => 10)
22
+ #
23
+ # You can also use resources to share common headers. For headers keys,
24
+ # symbols are converted to strings. Example:
25
+ #
26
+ # resource = RestClient::Resource.new('http://some/resource', :headers => { :client_version => 1 })
27
+ #
28
+ # This header will be transported as X-Client-Version (notice the X prefix,
29
+ # capitalization and hyphens)
30
+ #
31
+ # Use the [] syntax to allocate subresources:
32
+ #
33
+ # site = RestClient::Resource.new('http://example.com', :user => 'adam', :password => 'mypasswd')
34
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
35
+ #
36
+ class Resource
37
+ attr_reader :url, :options, :block
38
+
39
+ def initialize(url, options={}, backwards_compatibility=nil, &block)
40
+ @url = url
41
+ @block = block
42
+ if options.class == Hash
43
+ @options = options
44
+ else # compatibility with previous versions
45
+ @options = { :user => options, :password => backwards_compatibility }
46
+ end
47
+ end
48
+
49
+ def get(additional_headers={}, &block)
50
+ headers = (options[:headers] || {}).merge(additional_headers)
51
+ Request.execute(options.merge(
52
+ :method => :get,
53
+ :url => url,
54
+ :headers => headers), &(block || @block))
55
+ end
56
+
57
+ def head(additional_headers={}, &block)
58
+ headers = (options[:headers] || {}).merge(additional_headers)
59
+ Request.execute(options.merge(
60
+ :method => :head,
61
+ :url => url,
62
+ :headers => headers), &(block || @block))
63
+ end
64
+
65
+ def post(payload, additional_headers={}, &block)
66
+ headers = (options[:headers] || {}).merge(additional_headers)
67
+ Request.execute(options.merge(
68
+ :method => :post,
69
+ :url => url,
70
+ :payload => payload,
71
+ :headers => headers), &(block || @block))
72
+ end
73
+
74
+ def put(payload, additional_headers={}, &block)
75
+ headers = (options[:headers] || {}).merge(additional_headers)
76
+ Request.execute(options.merge(
77
+ :method => :put,
78
+ :url => url,
79
+ :payload => payload,
80
+ :headers => headers), &(block || @block))
81
+ end
82
+
83
+ def patch(payload, additional_headers={}, &block)
84
+ headers = (options[:headers] || {}).merge(additional_headers)
85
+ Request.execute(options.merge(
86
+ :method => :patch,
87
+ :url => url,
88
+ :payload => payload,
89
+ :headers => headers), &(block || @block))
90
+ end
91
+
92
+ def delete(additional_headers={}, &block)
93
+ headers = (options[:headers] || {}).merge(additional_headers)
94
+ Request.execute(options.merge(
95
+ :method => :delete,
96
+ :url => url,
97
+ :headers => headers), &(block || @block))
98
+ end
99
+
100
+ def to_s
101
+ url
102
+ end
103
+
104
+ def user
105
+ options[:user]
106
+ end
107
+
108
+ def password
109
+ options[:password]
110
+ end
111
+
112
+ def headers
113
+ options[:headers] || {}
114
+ end
115
+
116
+ def timeout
117
+ options[:timeout]
118
+ end
119
+
120
+ def open_timeout
121
+ options[:open_timeout]
122
+ end
123
+
124
+ # Construct a subresource, preserving authentication.
125
+ #
126
+ # Example:
127
+ #
128
+ # site = RestClient::Resource.new('http://example.com', 'adam', 'mypasswd')
129
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
130
+ #
131
+ # This is especially useful if you wish to define your site in one place and
132
+ # call it in multiple locations:
133
+ #
134
+ # def orders
135
+ # RestClient::Resource.new('http://example.com/orders', 'admin', 'mypasswd')
136
+ # end
137
+ #
138
+ # orders.get # GET http://example.com/orders
139
+ # orders['1'].get # GET http://example.com/orders/1
140
+ # orders['1/items'].delete # DELETE http://example.com/orders/1/items
141
+ #
142
+ # Nest resources as far as you want:
143
+ #
144
+ # site = RestClient::Resource.new('http://example.com')
145
+ # posts = site['posts']
146
+ # first_post = posts['1']
147
+ # comments = first_post['comments']
148
+ # comments.post 'Hello', :content_type => 'text/plain'
149
+ #
150
+ def [](suburl, &new_block)
151
+ case
152
+ when block_given? then self.class.new(concat_urls(url, suburl), options, &new_block)
153
+ when block then self.class.new(concat_urls(url, suburl), options, &block)
154
+ else
155
+ self.class.new(concat_urls(url, suburl), options)
156
+ end
157
+ end
158
+
159
+ def concat_urls(url, suburl) # :nodoc:
160
+ url = url.to_s
161
+ suburl = suburl.to_s
162
+ if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/'
163
+ url + suburl
164
+ else
165
+ "#{url}/#{suburl}"
166
+ end
167
+ end
168
+ end
169
+ end