rest-client-maestro 1.7.2.maestro

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 (46) hide show
  1. data/.gitignore +8 -0
  2. data/.rspec +1 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +8 -0
  5. data/AUTHORS +69 -0
  6. data/Gemfile +7 -0
  7. data/README.rdoc +322 -0
  8. data/Rakefile +49 -0
  9. data/bin/restclient +93 -0
  10. data/history.md +134 -0
  11. data/lib/rest-client.rb +2 -0
  12. data/lib/rest_client.rb +2 -0
  13. data/lib/restclient/abstract_response.rb +106 -0
  14. data/lib/restclient/exceptions.rb +198 -0
  15. data/lib/restclient/net_http_ext.rb +55 -0
  16. data/lib/restclient/payload.rb +242 -0
  17. data/lib/restclient/raw_response.rb +34 -0
  18. data/lib/restclient/request.rb +346 -0
  19. data/lib/restclient/resource.rb +169 -0
  20. data/lib/restclient/response.rb +26 -0
  21. data/lib/restclient.rb +174 -0
  22. data/rest-client-maestro.gemspec +23 -0
  23. data/spec/integration/capath_equifax/578d5c04.0 +19 -0
  24. data/spec/integration/capath_equifax/594f1775.0 +19 -0
  25. data/spec/integration/capath_equifax/README +8 -0
  26. data/spec/integration/capath_equifax/equifax.crt +19 -0
  27. data/spec/integration/capath_verisign/415660c1.0 +14 -0
  28. data/spec/integration/capath_verisign/7651b327.0 +14 -0
  29. data/spec/integration/capath_verisign/README +8 -0
  30. data/spec/integration/capath_verisign/verisign.crt +14 -0
  31. data/spec/integration/certs/equifax.crt +19 -0
  32. data/spec/integration/certs/verisign.crt +14 -0
  33. data/spec/integration/integration_spec.rb +35 -0
  34. data/spec/integration/request_spec.rb +63 -0
  35. data/spec/spec_helper.rb +12 -0
  36. data/spec/unit/abstract_response_spec.rb +85 -0
  37. data/spec/unit/exceptions_spec.rb +95 -0
  38. data/spec/unit/master_shake.jpg +0 -0
  39. data/spec/unit/payload_spec.rb +245 -0
  40. data/spec/unit/raw_response_spec.rb +17 -0
  41. data/spec/unit/request2_spec.rb +32 -0
  42. data/spec/unit/request_spec.rb +621 -0
  43. data/spec/unit/resource_spec.rb +133 -0
  44. data/spec/unit/response_spec.rb +166 -0
  45. data/spec/unit/restclient_spec.rb +73 -0
  46. metadata +220 -0
@@ -0,0 +1,242 @@
1
+ require 'tempfile'
2
+ require 'stringio'
3
+ require 'mime/types'
4
+
5
+ module RestClient
6
+ module Payload
7
+ extend self
8
+
9
+ def generate(params)
10
+ if params.is_a?(String)
11
+ Base.new(params)
12
+ elsif params.is_a?(Hash)
13
+ if params.delete(:multipart) == true || has_file?(params)
14
+ Multipart.new(params)
15
+ else
16
+ UrlEncoded.new(params)
17
+ end
18
+ elsif params.respond_to?(:read)
19
+ Streamed.new(params)
20
+ else
21
+ nil
22
+ end
23
+ end
24
+
25
+ def has_file?(params)
26
+ params.any? do |_, v|
27
+ case v
28
+ when Hash
29
+ has_file?(v)
30
+ when Array
31
+ has_file_array?(v)
32
+ else
33
+ v.respond_to?(:path) && v.respond_to?(:read)
34
+ end
35
+ end
36
+ end
37
+
38
+ def has_file_array?(params)
39
+ params.any? do |v|
40
+ case v
41
+ when Hash
42
+ has_file?(v)
43
+ when Array
44
+ has_file_array?(v)
45
+ else
46
+ v.respond_to?(:path) && v.respond_to?(:read)
47
+ end
48
+ end
49
+ end
50
+
51
+ class Base
52
+ attr_reader :stream
53
+
54
+ def initialize(params)
55
+ build_stream(params)
56
+ end
57
+
58
+ def build_stream(params)
59
+ @stream = StringIO.new(params)
60
+ @stream.seek(0)
61
+ end
62
+
63
+ def read(bytes=nil)
64
+ @stream.read(bytes)
65
+ end
66
+
67
+ alias :to_s :read
68
+
69
+ # Flatten parameters by converting hashes of hashes to flat hashes
70
+ # {keys1 => {keys2 => value}} will be transformed into [keys1[key2], value]
71
+ def flatten_params(params, parent_key = nil)
72
+ result = []
73
+ params.each do |key, value|
74
+ calculated_key = parent_key ? "#{parent_key}[#{handle_key(key)}]" : handle_key(key)
75
+ if value.is_a? Hash
76
+ result += flatten_params(value, calculated_key)
77
+ elsif value.is_a? Array
78
+ result += flatten_params_array(value, calculated_key)
79
+ else
80
+ result << [calculated_key, value]
81
+ end
82
+ end
83
+ result
84
+ end
85
+
86
+ def flatten_params_array value, calculated_key
87
+ result = []
88
+ value.each do |elem|
89
+ if elem.is_a? Hash
90
+ result += flatten_params(elem, calculated_key)
91
+ elsif elem.is_a? Array
92
+ result += flatten_params_array(elem, calculated_key)
93
+ else
94
+ result << ["#{calculated_key}[]", elem]
95
+ end
96
+ end
97
+ result
98
+ end
99
+
100
+ def headers
101
+ {'Content-Length' => size.to_s}
102
+ end
103
+
104
+ def size
105
+ @stream.size
106
+ end
107
+
108
+ alias :length :size
109
+
110
+ def close
111
+ @stream.close unless @stream.closed?
112
+ end
113
+
114
+ def inspect
115
+ result = to_s.inspect
116
+ @stream.seek(0)
117
+ result
118
+ end
119
+
120
+ def short_inspect
121
+ (size > 500 ? "#{size} byte(s) length" : inspect)
122
+ end
123
+
124
+ end
125
+
126
+ class Streamed < Base
127
+ def build_stream(params = nil)
128
+ @stream = params
129
+ end
130
+
131
+ def size
132
+ if @stream.respond_to?(:size)
133
+ @stream.size
134
+ elsif @stream.is_a?(IO)
135
+ @stream.stat.size
136
+ end
137
+ end
138
+
139
+ alias :length :size
140
+ end
141
+
142
+ class UrlEncoded < Base
143
+ def build_stream(params = nil)
144
+ @stream = StringIO.new(flatten_params(params).collect do |entry|
145
+ "#{entry[0]}=#{handle_key(entry[1])}"
146
+ end.join("&"))
147
+ @stream.seek(0)
148
+ end
149
+
150
+ # for UrlEncoded escape the keys
151
+ def handle_key key
152
+ parser.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
153
+ end
154
+
155
+ def headers
156
+ super.merge({'Content-Type' => 'application/x-www-form-urlencoded'})
157
+ end
158
+
159
+ private
160
+ def parser
161
+ URI.const_defined?(:Parser) ? URI::Parser.new : URI
162
+ end
163
+ end
164
+
165
+ class Multipart < Base
166
+ EOL = "\r\n"
167
+
168
+ def build_stream(params)
169
+ b = "--#{boundary}"
170
+
171
+ @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
172
+ @stream.binmode
173
+ @stream.write(b + EOL)
174
+
175
+ if params.is_a? Hash
176
+ x = flatten_params(params)
177
+ else
178
+ x = params
179
+ end
180
+
181
+ last_index = x.length - 1
182
+ x.each_with_index do |a, index|
183
+ k, v = * a
184
+ if v.respond_to?(:read) && v.respond_to?(:path)
185
+ create_file_field(@stream, k, v)
186
+ else
187
+ create_regular_field(@stream, k, v)
188
+ end
189
+ @stream.write(EOL + b)
190
+ @stream.write(EOL) unless last_index == index
191
+ end
192
+ @stream.write('--')
193
+ @stream.write(EOL)
194
+ @stream.seek(0)
195
+ end
196
+
197
+ def create_regular_field(s, k, v)
198
+ s.write("Content-Disposition: form-data; name=\"#{k}\"")
199
+ s.write(EOL)
200
+ s.write(EOL)
201
+ s.write(v)
202
+ end
203
+
204
+ def create_file_field(s, k, v)
205
+ begin
206
+ s.write("Content-Disposition: form-data;")
207
+ s.write(" name=\"#{k}\";") unless (k.nil? || k=='')
208
+ s.write(" filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
209
+ s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : mime_for(v.path)}#{EOL}")
210
+ s.write(EOL)
211
+ while data = v.read(8124)
212
+ s.write(data)
213
+ end
214
+ ensure
215
+ v.close if v.respond_to?(:close)
216
+ end
217
+ end
218
+
219
+ def mime_for(path)
220
+ mime = MIME::Types.type_for path
221
+ mime.empty? ? 'text/plain' : mime[0].content_type
222
+ end
223
+
224
+ def boundary
225
+ @boundary ||= rand(1_000_000).to_s
226
+ end
227
+
228
+ # for Multipart do not escape the keys
229
+ def handle_key key
230
+ key
231
+ end
232
+
233
+ def headers
234
+ super.merge({'Content-Type' => %Q{multipart/form-data; boundary=#{boundary}}})
235
+ end
236
+
237
+ def close
238
+ @stream.close!
239
+ end
240
+ end
241
+ end
242
+ 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,346 @@
1
+ require 'tempfile'
2
+ require 'mime/types'
3
+ require 'cgi'
4
+ require 'netrc'
5
+
6
+ module RestClient
7
+ # This class is used internally by RestClient to send the request, but you can also
8
+ # call it directly if you'd like to use a method not supported by the
9
+ # main API. For example:
10
+ #
11
+ # RestClient::Request.execute(:method => :head, :url => 'http://example.com')
12
+ #
13
+ # Mandatory parameters:
14
+ # * :method
15
+ # * :url
16
+ # Optional parameters (have a look at ssl and/or uri for some explanations):
17
+ # * :headers a hash containing the request headers
18
+ # * :cookies will replace possible cookies in the :headers
19
+ # * :user and :password for basic auth, will be replaced by a user/password available in the :url
20
+ # * :block_response call the provided block with the HTTPResponse as parameter
21
+ # * :raw_response return a low-level RawResponse instead of a Response
22
+ # * :max_redirects maximum number of redirections (default to 10)
23
+ # * :verify_ssl enable ssl verification, possible values are constants from OpenSSL::SSL
24
+ # * :timeout and :open_timeout passing in -1 will disable the timeout by setting the corresponding net timeout values to nil
25
+ # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file, :ssl_ca_path
26
+ # * :ssl_version specifies the SSL version for the underlying Net::HTTP connection (defaults to 'SSLv3')
27
+ class Request
28
+
29
+ attr_reader :method, :url, :headers, :cookies,
30
+ :payload, :user, :password, :timeout, :max_redirects,
31
+ :open_timeout, :raw_response, :verify_ssl, :ssl_client_cert,
32
+ :ssl_client_key, :ssl_ca_file, :processed_headers, :args,
33
+ :ssl_version, :ssl_ca_path
34
+
35
+ def self.execute(args, & block)
36
+ new(args).execute(& block)
37
+ end
38
+
39
+ def initialize args
40
+ @method = args[:method] or raise ArgumentError, "must pass :method"
41
+ @headers = args[:headers] || {}
42
+ if args[:url]
43
+ @url = process_url_params(args[:url], headers)
44
+ else
45
+ raise ArgumentError, "must pass :url"
46
+ end
47
+ @cookies = @headers.delete(:cookies) || args[:cookies] || {}
48
+ @payload = Payload.generate(args[:payload])
49
+ @user = args[:user]
50
+ @password = args[:password]
51
+ @timeout = args[:timeout]
52
+ @open_timeout = args[:open_timeout]
53
+ @block_response = args[:block_response]
54
+ @raw_response = args[:raw_response] || false
55
+ @verify_ssl = args[:verify_ssl] || false
56
+ @ssl_client_cert = args[:ssl_client_cert] || nil
57
+ @ssl_client_key = args[:ssl_client_key] || nil
58
+ @ssl_ca_file = args[:ssl_ca_file] || nil
59
+ @ssl_ca_path = args[:ssl_ca_path] || nil
60
+ @ssl_version = args[:ssl_version] || 'SSLv3'
61
+ @tf = nil # If you are a raw request, this is your tempfile
62
+ @max_redirects = args[:max_redirects] || 10
63
+ @processed_headers = make_headers headers
64
+ @args = args
65
+ end
66
+
67
+ def execute & block
68
+ uri = parse_url_with_auth(url)
69
+ transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload, & block
70
+ ensure
71
+ payload.close if payload
72
+ end
73
+
74
+ # Extract the query parameters and append them to the url
75
+ def process_url_params url, headers
76
+ url_params = {}
77
+ headers.delete_if do |key, value|
78
+ if 'params' == key.to_s.downcase && value.is_a?(Hash)
79
+ url_params.merge! value
80
+ true
81
+ else
82
+ false
83
+ end
84
+ end
85
+ unless url_params.empty?
86
+ query_string = url_params.collect { |k, v| "#{k.to_s}=#{CGI::escape(v.to_s)}" }.join('&')
87
+ url + "?#{query_string}"
88
+ else
89
+ url
90
+ end
91
+ end
92
+
93
+ def make_headers user_headers
94
+ unless @cookies.empty?
95
+ user_headers[:cookie] = @cookies.map { |(key, val)| "#{key.to_s}=#{CGI::unescape(val.to_s)}" }.sort.join('; ')
96
+ end
97
+ headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
98
+ headers.merge!(@payload.headers) if @payload
99
+ headers
100
+ end
101
+
102
+ def net_http_class
103
+ if RestClient.proxy
104
+ proxy_uri = URI.parse(RestClient.proxy)
105
+ Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
106
+ else
107
+ Net::HTTP
108
+ end
109
+ end
110
+
111
+ def net_http_request_class(method)
112
+ Net::HTTP.const_get(method.to_s.capitalize)
113
+ end
114
+
115
+ def parse_url(url)
116
+ url = "http://#{url}" unless url.match(/^http/)
117
+ URI.parse(url)
118
+ end
119
+
120
+ def parse_url_with_auth(url)
121
+ uri = parse_url(url)
122
+ @user = CGI.unescape(uri.user) if uri.user
123
+ @password = CGI.unescape(uri.password) if uri.password
124
+ if !@user && !@password
125
+ @user, @password = Netrc.read[uri.host]
126
+ end
127
+ uri
128
+ end
129
+
130
+ def process_payload(p=nil, parent_key=nil)
131
+ unless p.is_a?(Hash)
132
+ p
133
+ else
134
+ @headers[:content_type] ||= 'application/x-www-form-urlencoded'
135
+ p.keys.map do |k|
136
+ key = parent_key ? "#{parent_key}[#{k}]" : k
137
+ if p[k].is_a? Hash
138
+ process_payload(p[k], key)
139
+ else
140
+ value = parser.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
141
+ "#{key}=#{value}"
142
+ end
143
+ end.join("&")
144
+ end
145
+ end
146
+
147
+ def transmit uri, req, payload, & block
148
+ setup_credentials req
149
+
150
+ net = net_http_class.new(uri.host, uri.port)
151
+ net.use_ssl = uri.is_a?(URI::HTTPS)
152
+ net.ssl_version = @ssl_version
153
+ err_msg = nil
154
+ if (@verify_ssl == false) || (@verify_ssl == OpenSSL::SSL::VERIFY_NONE)
155
+ net.verify_mode = OpenSSL::SSL::VERIFY_NONE
156
+ elsif @verify_ssl.is_a? Integer
157
+ net.verify_mode = @verify_ssl
158
+ net.verify_callback = lambda do |preverify_ok, ssl_context|
159
+ if (!preverify_ok) || ssl_context.error != 0
160
+ err_msg = "SSL Verification failed -- Preverify: #{preverify_ok}, Error: #{ssl_context.error_string} (#{ssl_context.error})"
161
+ return false
162
+ end
163
+ true
164
+ end
165
+ end
166
+ net.cert = @ssl_client_cert if @ssl_client_cert
167
+ net.key = @ssl_client_key if @ssl_client_key
168
+ net.ca_file = @ssl_ca_file if @ssl_ca_file
169
+ net.ca_path = @ssl_ca_path if @ssl_ca_path
170
+ net.read_timeout = @timeout if @timeout
171
+ net.open_timeout = @open_timeout if @open_timeout
172
+
173
+ # disable the timeout if the timeout value is -1
174
+ net.read_timeout = nil if @timeout == -1
175
+ net.open_timeout = nil if @open_timeout == -1
176
+
177
+ RestClient.before_execution_procs.each do |before_proc|
178
+ before_proc.call(req, args)
179
+ end
180
+
181
+ log_request
182
+
183
+ net.start do |http|
184
+ if payload
185
+ if payload.is_a?(RestClient::Payload::Streamed)
186
+ req.body_stream = payload
187
+ else
188
+ req.body = payload # or payload.to_s if we want to keep it equiv to current
189
+ end
190
+ end
191
+
192
+ if @block_response
193
+ http.request(req, & @block_response)
194
+ else
195
+ res = http.request(req) { |http_response| fetch_body(http_response) }
196
+ log_response res
197
+ process_result res, & block
198
+ end
199
+ end
200
+ rescue OpenSSL::SSL::SSLError => e
201
+ if err_msg
202
+ raise SSLCertificateNotVerified.new(err_msg)
203
+ else
204
+ raise e
205
+ end
206
+ rescue EOFError
207
+ raise RestClient::ServerBrokeConnection
208
+ rescue Timeout::Error
209
+ raise RestClient::RequestTimeout
210
+ end
211
+
212
+ def setup_credentials(req)
213
+ req.basic_auth(user, password) if user
214
+ end
215
+
216
+ def fetch_body(http_response)
217
+ if @raw_response
218
+ # Taken from Chef, which as in turn...
219
+ # Stolen from http://www.ruby-forum.com/topic/166423
220
+ # Kudos to _why!
221
+ @tf = Tempfile.new("rest-client")
222
+ size, total = 0, http_response.header['Content-Length'].to_i
223
+ http_response.read_body do |chunk|
224
+ @tf.write chunk
225
+ size += chunk.size
226
+ if RestClient.log
227
+ if size == 0
228
+ RestClient.log << "#{@method} #{@url} done (0 length file\n)"
229
+ elsif total == 0
230
+ RestClient.log << "#{@method} #{@url} (zero content length)\n"
231
+ else
232
+ RestClient.log << "#{@method} #{@url} %d%% done (%d of %d)\n" % [(size * 100) / total, size, total]
233
+ end
234
+ end
235
+ end
236
+ @tf.close
237
+ @tf
238
+ else
239
+ http_response.read_body
240
+ end
241
+ http_response
242
+ end
243
+
244
+ def process_result res, & block
245
+ if @raw_response
246
+ # We don't decode raw requests
247
+ response = RawResponse.new(@tf, res, args)
248
+ else
249
+ response = Response.create(Request.decode(res['content-encoding'], res.body), res, args)
250
+ end
251
+
252
+ if block_given?
253
+ block.call(response, self, res, & block)
254
+ else
255
+ response.return!(self, res, & block)
256
+ end
257
+
258
+ end
259
+
260
+ def self.decode content_encoding, body
261
+ if (!body) || body.empty?
262
+ body
263
+ elsif content_encoding == 'gzip'
264
+ Zlib::GzipReader.new(StringIO.new(body)).read
265
+ elsif content_encoding == 'deflate'
266
+ begin
267
+ Zlib::Inflate.new.inflate body
268
+ rescue Zlib::DataError
269
+ # No luck with Zlib decompression. Let's try with raw deflate,
270
+ # like some broken web servers do.
271
+ Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
272
+ end
273
+ else
274
+ body
275
+ end
276
+ end
277
+
278
+ def log_request
279
+ if RestClient.log
280
+ out = []
281
+ out << "RestClient.#{method} #{url.inspect}"
282
+ out << payload.short_inspect if payload
283
+ out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
284
+ RestClient.log << out.join(', ') + "\n"
285
+ end
286
+ end
287
+
288
+ def log_response res
289
+ if RestClient.log
290
+ size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
291
+ RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
292
+ end
293
+ end
294
+
295
+ # Return a hash of headers whose keys are capitalized strings
296
+ def stringify_headers headers
297
+ headers.inject({}) do |result, (key, value)|
298
+ if key.is_a? Symbol
299
+ key = key.to_s.split(/_/).map { |w| w.capitalize }.join('-')
300
+ end
301
+ if 'CONTENT-TYPE' == key.upcase
302
+ target_value = value.to_s
303
+ result[key] = MIME::Types.type_for_extension target_value
304
+ elsif 'ACCEPT' == key.upcase
305
+ # Accept can be composed of several comma-separated values
306
+ if value.is_a? Array
307
+ target_values = value
308
+ else
309
+ target_values = value.to_s.split ','
310
+ end
311
+ result[key] = target_values.map { |ext| MIME::Types.type_for_extension(ext.to_s.strip) }.join(', ')
312
+ else
313
+ result[key] = value.to_s
314
+ end
315
+ result
316
+ end
317
+ end
318
+
319
+ def default_headers
320
+ {:accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate'}
321
+ end
322
+
323
+ private
324
+ def parser
325
+ URI.const_defined?(:Parser) ? URI::Parser.new : URI
326
+ end
327
+
328
+ end
329
+ end
330
+
331
+ module MIME
332
+ class Types
333
+
334
+ # Return the first found content-type for a value considered as an extension or the value itself
335
+ def type_for_extension ext
336
+ candidates = @extension_index[ext]
337
+ candidates.empty? ? ext : candidates[0].content_type
338
+ end
339
+
340
+ class << self
341
+ def type_for_extension ext
342
+ @__types__.type_for_extension ext
343
+ end
344
+ end
345
+ end
346
+ end