rest-client-next-dshelf 1.6.1

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.
@@ -0,0 +1,21 @@
1
+ #
2
+ # Replace the request method in Net::HTTP to sniff the body type
3
+ # and set the stream if appropriate
4
+ #
5
+ # Taken from:
6
+ # http://www.missiondata.com/blog/ruby/29/streaming-data-to-s3-with-ruby/
7
+
8
+ module Net
9
+ class HTTP
10
+ alias __request__ request
11
+
12
+ def request(req, body=nil, &block)
13
+ if body != nil && body.respond_to?(:read)
14
+ req.body_stream = body
15
+ return __request__(req, nil, &block)
16
+ else
17
+ return __request__(req, body, &block)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,220 @@
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.respond_to?(:read)
13
+ Streamed.new(params)
14
+ elsif params
15
+ if params.delete(:multipart) == true || has_file?(params)
16
+ Multipart.new(params)
17
+ else
18
+ UrlEncoded.new(params)
19
+ end
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
+ else
31
+ v.respond_to?(:path) && v.respond_to?(:read)
32
+ end
33
+ end
34
+ end
35
+
36
+ class Base
37
+ def initialize(params)
38
+ build_stream(params)
39
+ end
40
+
41
+ def build_stream(params)
42
+ @stream = StringIO.new(params)
43
+ @stream.seek(0)
44
+ end
45
+
46
+ def read(bytes=nil)
47
+ @stream.read(bytes)
48
+ end
49
+
50
+ alias :to_s :read
51
+
52
+ # Flatten parameters by converting hashes of hashes to flat hashes
53
+ # {keys1 => {keys2 => value}} will be transformed into [keys1[key2], value]
54
+ def flatten_params(params, parent_key = nil)
55
+ result = []
56
+ params.each do |key, value|
57
+ calculated_key = parent_key ? "#{parent_key}[#{handle_key(key)}]" : handle_key(key)
58
+ if value.is_a? Hash
59
+ result += flatten_params(value, calculated_key)
60
+ elsif value.is_a? Array
61
+ result += flatten_params_array(value, calculated_key)
62
+ else
63
+ result << [calculated_key, value]
64
+ end
65
+ end
66
+ result
67
+ end
68
+
69
+ def flatten_params_array value, calculated_key
70
+ result = []
71
+ value.each do |elem|
72
+ if elem.is_a? Hash
73
+ result += flatten_params(elem, calculated_key)
74
+ elsif elem.is_a? Array
75
+ result += flatten_params_array(elem, calculated_key)
76
+ else
77
+ result << ["#{calculated_key}[]", elem]
78
+ end
79
+ end
80
+ result
81
+ end
82
+
83
+ def headers
84
+ {'Content-Length' => size.to_s}
85
+ end
86
+
87
+ def size
88
+ @stream.size
89
+ end
90
+
91
+ alias :length :size
92
+
93
+ def close
94
+ @stream.close
95
+ end
96
+
97
+ def inspect
98
+ result = to_s.inspect
99
+ @stream.seek(0)
100
+ result
101
+ end
102
+
103
+ def short_inspect
104
+ (size > 500 ? "#{size} byte(s) length" : inspect)
105
+ end
106
+
107
+ end
108
+
109
+ class Streamed < Base
110
+ def build_stream(params = nil)
111
+ @stream = params
112
+ end
113
+
114
+ def size
115
+ if @stream.respond_to?(:size)
116
+ @stream.size
117
+ elsif @stream.is_a?(IO)
118
+ @stream.stat.size
119
+ end
120
+ end
121
+
122
+ alias :length :size
123
+ end
124
+
125
+ class UrlEncoded < Base
126
+ def build_stream(params = nil)
127
+ @stream = StringIO.new(flatten_params(params).collect do |entry|
128
+ "#{entry[0]}=#{handle_key(entry[1])}"
129
+ end.join("&"))
130
+ @stream.seek(0)
131
+ end
132
+
133
+ # for UrlEncoded escape the keys
134
+ def handle_key key
135
+ URI.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
136
+ end
137
+
138
+ def headers
139
+ super.merge({'Content-Type' => 'application/x-www-form-urlencoded'})
140
+ end
141
+ end
142
+
143
+ class Multipart < Base
144
+ EOL = "\r\n"
145
+
146
+ def build_stream(params)
147
+ b = "--#{boundary}"
148
+
149
+ @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
150
+ @stream.binmode
151
+ @stream.write(b + EOL)
152
+
153
+ if params.is_a? Hash
154
+ x = flatten_params(params)
155
+ else
156
+ x = params
157
+ end
158
+
159
+ last_index = x.length - 1
160
+ x.each_with_index do |a, index|
161
+ k, v = * a
162
+ if v.respond_to?(:read) && v.respond_to?(:path)
163
+ create_file_field(@stream, k, v)
164
+ else
165
+ create_regular_field(@stream, k, v)
166
+ end
167
+ @stream.write(EOL + b)
168
+ @stream.write(EOL) unless last_index == index
169
+ end
170
+ @stream.write('--')
171
+ @stream.write(EOL)
172
+ @stream.seek(0)
173
+ end
174
+
175
+ def create_regular_field(s, k, v)
176
+ s.write("Content-Disposition: form-data; name=\"#{k}\"")
177
+ s.write(EOL)
178
+ s.write(EOL)
179
+ s.write(v)
180
+ end
181
+
182
+ def create_file_field(s, k, v)
183
+ begin
184
+ s.write("Content-Disposition: form-data;")
185
+ s.write(" name=\"#{k}\";") unless (k.nil? || k=='')
186
+ s.write(" filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
187
+ s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : mime_for(v.path)}#{EOL}")
188
+ s.write(EOL)
189
+ while data = v.read(8124)
190
+ s.write(data)
191
+ end
192
+ ensure
193
+ v.close
194
+ end
195
+ end
196
+
197
+ def mime_for(path)
198
+ mime = MIME::Types.type_for path
199
+ mime.empty? ? 'text/plain' : mime[0].content_type
200
+ end
201
+
202
+ def boundary
203
+ @boundary ||= rand(1_000_000).to_s
204
+ end
205
+
206
+ # for Multipart do not escape the keys
207
+ def handle_key key
208
+ key
209
+ end
210
+
211
+ def headers
212
+ super.merge({'Content-Type' => %Q{multipart/form-data; boundary=#{boundary}}})
213
+ end
214
+
215
+ def close
216
+ @stream.close
217
+ end
218
+ end
219
+ end
220
+ 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,308 @@
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
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
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
+ Zlib::Inflate.new.inflate body
240
+ else
241
+ body
242
+ end
243
+ end
244
+
245
+ def log_request
246
+ if RestClient.log
247
+ out = []
248
+ out << "RestClient.#{method} #{url.inspect}"
249
+ out << payload.short_inspect if payload
250
+ out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
251
+ RestClient.log << out.join(', ') + "\n"
252
+ end
253
+ end
254
+
255
+ def log_response res
256
+ if RestClient.log
257
+ size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
258
+ RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
259
+ end
260
+ end
261
+
262
+ # Return a hash of headers whose keys are capitalized strings
263
+ def stringify_headers headers
264
+ headers.inject({}) do |result, (key, value)|
265
+ if key.is_a? Symbol
266
+ key = key.to_s.split(/_/).map { |w| w.capitalize }.join('-')
267
+ end
268
+ if 'CONTENT-TYPE' == key.upcase
269
+ target_value = value.to_s
270
+ result[key] = MIME::Types.type_for_extension target_value
271
+ elsif 'ACCEPT' == key.upcase
272
+ # Accept can be composed of several comma-separated values
273
+ if value.is_a? Array
274
+ target_values = value
275
+ else
276
+ target_values = value.to_s.split ','
277
+ end
278
+ result[key] = target_values.map { |ext| MIME::Types.type_for_extension(ext.to_s.strip) }.join(', ')
279
+ else
280
+ result[key] = value.to_s
281
+ end
282
+ result
283
+ end
284
+ end
285
+
286
+ def default_headers
287
+ {:accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate'}
288
+ end
289
+
290
+ end
291
+ end
292
+
293
+ module MIME
294
+ class Types
295
+
296
+ # Return the first found content-type for a value considered as an extension or the value itself
297
+ def type_for_extension ext
298
+ candidates = @extension_index[ext]
299
+ candidates.empty? ? ext : candidates[0].content_type
300
+ end
301
+
302
+ class << self
303
+ def type_for_extension ext
304
+ @__types__.type_for_extension ext
305
+ end
306
+ end
307
+ end
308
+ end