rest 2.7.2 → 3.0.0

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,54 @@
1
+ module Rest
2
+ module InternalClient
3
+
4
+ module Mimes
5
+
6
+ def self.mime_for(path)
7
+ ret = Mimes.mime_for_ext(path)
8
+ if ret
9
+ return ret
10
+ end
11
+ return 'text/plain'
12
+ end
13
+
14
+ # takes something like json and returns application/json.
15
+ # If no conversion, returns ext passed in
16
+ def self.mime_for_ext(path)
17
+ ret = nil
18
+ dot = path.rindex('.')
19
+ if dot
20
+ ext = path[dot+1..path.length]
21
+ else
22
+ ext = path
23
+ end
24
+ # feel free to add more mime-types anyone.
25
+ case ext
26
+ when 'xml'
27
+ ret = 'application/xml'
28
+ when 'gif'
29
+ ret = 'image/gif'
30
+ when 'jpg', 'jpeg'
31
+ ret = 'image/jpeg'
32
+ when 'json'
33
+ ret = 'application/json'
34
+ when 'html'
35
+ ret = 'text/html'
36
+ when 'mp3'
37
+ ret = 'audio/mpeg'
38
+ when 'pdf'
39
+ ret = 'application/pdf'
40
+ end
41
+ return ret
42
+ end
43
+
44
+ def self.mime_for_or_not(ext)
45
+ ret = Mimes.mime_for_ext(ext)
46
+ if ret
47
+ return ret
48
+ end
49
+ return ext
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,55 @@
1
+ # module Net
2
+ # class HTTP
3
+ #
4
+ # # Adding the patch method if it doesn't exist (rest-client issue: https://github.com/archiloque/rest-client/issues/79)
5
+ # if !defined?(Net::HTTP::Patch)
6
+ # # Code taken from this commit: https://github.com/ruby/ruby/commit/ab70e53ac3b5102d4ecbe8f38d4f76afad29d37d#lib/net/http.rb
7
+ # class Protocol
8
+ # # Sends a PATCH request to the +path+ and gets a response,
9
+ # # as an HTTPResponse object.
10
+ # def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
11
+ # send_entity(path, data, initheader, dest, Patch, &block)
12
+ # end
13
+ #
14
+ # # Executes a request which uses a representation
15
+ # # and returns its body.
16
+ # def send_entity(path, data, initheader, dest, type, &block)
17
+ # res = nil
18
+ # request(type.new(path, initheader), data) { |r|
19
+ # r.read_body dest, &block
20
+ # res = r
21
+ # }
22
+ # unless @newimpl
23
+ # res.value
24
+ # return res, res.body
25
+ # end
26
+ # res
27
+ # end
28
+ # end
29
+ #
30
+ # class Patch < HTTPRequest
31
+ # METHOD = 'PATCH'
32
+ # REQUEST_HAS_BODY = true
33
+ # RESPONSE_HAS_BODY = true
34
+ # end
35
+ # end
36
+ #
37
+ # #
38
+ # # Replace the request method in Net::HTTP to sniff the body type
39
+ # # and set the stream if appropriate
40
+ # #
41
+ # # Taken from:
42
+ # # http://www.missiondata.com/blog/ruby/29/streaming-data-to-s3-with-ruby/
43
+ #
44
+ # alias __request__ request
45
+ #
46
+ # def request(req, body=nil, &block)
47
+ # if body != nil && body.respond_to?(:read)
48
+ # req.body_stream = body
49
+ # return __request__(req, nil, &block)
50
+ # else
51
+ # return __request__(req, body, &block)
52
+ # end
53
+ # end
54
+ # end
55
+ # end
@@ -0,0 +1,237 @@
1
+ require 'tempfile'
2
+ require 'stringio'
3
+
4
+ module Rest
5
+ module InternalClient
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
+ def initialize(params)
53
+ build_stream(params)
54
+ end
55
+
56
+ def build_stream(params)
57
+ @stream = StringIO.new(params)
58
+ @stream.seek(0)
59
+ end
60
+
61
+ def read(bytes=nil)
62
+ @stream.read(bytes)
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
82
+ end
83
+
84
+ def flatten_params_array value, calculated_key
85
+ result = []
86
+ value.each do |elem|
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
95
+ result
96
+ end
97
+
98
+ def headers
99
+ {'Content-Length' => size.to_s}
100
+ end
101
+
102
+ def size
103
+ @stream.size
104
+ end
105
+
106
+ alias :length :size
107
+
108
+ def close
109
+ @stream.close unless @stream.closed?
110
+ end
111
+
112
+ def inspect
113
+ result = to_s.inspect
114
+ @stream.seek(0)
115
+ result
116
+ end
117
+
118
+ def short_inspect
119
+ (size > 500 ? "#{size} byte(s) length" : inspect)
120
+ end
121
+
122
+ end
123
+
124
+ class Streamed < Base
125
+ def build_stream(params = nil)
126
+ @stream = params
127
+ end
128
+
129
+ def size
130
+ if @stream.respond_to?(:size)
131
+ @stream.size
132
+ elsif @stream.is_a?(IO)
133
+ @stream.stat.size
134
+ end
135
+ end
136
+
137
+ alias :length :size
138
+ end
139
+
140
+ class UrlEncoded < Base
141
+ def build_stream(params = nil)
142
+ @stream = StringIO.new(flatten_params(params).collect do |entry|
143
+ "#{entry[0]}=#{handle_key(entry[1])}"
144
+ end.join("&"))
145
+ @stream.seek(0)
146
+ end
147
+
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
+ def headers
154
+ super.merge({'Content-Type' => 'application/x-www-form-urlencoded'})
155
+ end
156
+
157
+ private
158
+ def parser
159
+ URI.const_defined?(:Parser) ? URI::Parser.new : URI
160
+ end
161
+ end
162
+
163
+ class Multipart < Base
164
+ EOL = "\r\n"
165
+
166
+ def build_stream(params)
167
+ b = "--#{boundary}"
168
+
169
+ @stream = Tempfile.new("InternalClient.Stream.#{rand(1000)}")
170
+ @stream.binmode
171
+ @stream.write(b + EOL)
172
+
173
+ if params.is_a? Hash
174
+ x = flatten_params(params)
175
+ else
176
+ x = params
177
+ end
178
+
179
+ last_index = x.length - 1
180
+ x.each_with_index do |a, index|
181
+ k, v = * a
182
+ if v.respond_to?(:read) && v.respond_to?(:path)
183
+ create_file_field(@stream, k, v)
184
+ else
185
+ create_regular_field(@stream, k, v)
186
+ end
187
+ @stream.write(EOL + b)
188
+ @stream.write(EOL) unless last_index == index
189
+ end
190
+ @stream.write('--')
191
+ @stream.write(EOL)
192
+ @stream.seek(0)
193
+ end
194
+
195
+ def create_regular_field(s, k, v)
196
+ s.write("Content-Disposition: form-data; name=\"#{k}\"")
197
+ s.write(EOL)
198
+ s.write(EOL)
199
+ s.write(v)
200
+ end
201
+
202
+ def create_file_field(s, k, v)
203
+ begin
204
+ s.write("Content-Disposition: form-data;")
205
+ s.write(" name=\"#{k}\";") unless (k.nil? || k=='')
206
+ s.write(" filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
207
+ s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : Mimes.mime_for(v.path)}#{EOL}")
208
+ s.write(EOL)
209
+ while data = v.read(8124)
210
+ s.write(data)
211
+ end
212
+ ensure
213
+ v.close if v.respond_to?(:close)
214
+ end
215
+ end
216
+
217
+
218
+ def boundary
219
+ @boundary ||= rand(1_000_000).to_s
220
+ end
221
+
222
+ # for Multipart do not escape the keys
223
+ def handle_key key
224
+ key
225
+ end
226
+
227
+ def headers
228
+ super.merge({'Content-Type' => %Q{multipart/form-data; boundary=#{boundary}}})
229
+ end
230
+
231
+ def close
232
+ @stream.close!
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,36 @@
1
+ module Rest
2
+ module InternalClient
3
+ # The response from InternalClient on a raw request looks like a string, but is
4
+ # actually one of these. 99% of the time you're making a rest call all you
5
+ # care about is the body, but on the occassion you want to fetch the
6
+ # headers you can:
7
+ #
8
+ # InternalClient.get('http://example.com').headers[:content_type]
9
+ #
10
+ # In addition, if you do not use the response as a string, you can access
11
+ # a Tempfile object at res.file, which contains the path to the raw
12
+ # downloaded request body.
13
+ class RawResponse
14
+
15
+ include AbstractResponse
16
+
17
+ attr_reader :file
18
+
19
+ def initialize tempfile, net_http_res, args
20
+ @net_http_res = net_http_res
21
+ @args = args
22
+ @file = tempfile
23
+ end
24
+
25
+ def to_s
26
+ @file.open
27
+ @file.read
28
+ end
29
+
30
+ def size
31
+ File.size file
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,322 @@
1
+ require 'tempfile'
2
+ require 'cgi'
3
+ require 'netrc'
4
+
5
+ module Rest
6
+ module InternalClient
7
+ # This class is used internally by InternalClient 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
+ # InternalClient::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 'SSLv23')
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] || 'SSLv23'
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 InternalClient.proxy
104
+ proxy_uri = URI.parse(InternalClient.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
+ InternalClient.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 @block_response
185
+ http.request(req, payload ? payload.to_s : nil, & @block_response)
186
+ else
187
+ res = http.request(req, payload ? payload.to_s : nil) { |http_response| fetch_body(http_response) }
188
+ log_response res
189
+ process_result res, & block
190
+ end
191
+ end
192
+ rescue OpenSSL::SSL::SSLError => e
193
+ if err_msg
194
+ raise SSLCertificateNotVerified.new(err_msg)
195
+ else
196
+ raise e
197
+ end
198
+ rescue EOFError
199
+ raise InternalClient::ServerBrokeConnection
200
+ rescue Timeout::Error
201
+ raise InternalClient::RequestTimeout
202
+ end
203
+
204
+ def setup_credentials(req)
205
+ req.basic_auth(user, password) if user
206
+ end
207
+
208
+ def fetch_body(http_response)
209
+ if @raw_response
210
+ # Taken from Chef, which as in turn...
211
+ # Stolen from http://www.ruby-forum.com/topic/166423
212
+ # Kudos to _why!
213
+ @tf = Tempfile.new("rest-client")
214
+ size, total = 0, http_response.header['Content-Length'].to_i
215
+ http_response.read_body do |chunk|
216
+ @tf.write chunk
217
+ size += chunk.size
218
+ if InternalClient.log
219
+ if size == 0
220
+ InternalClient.log << "#{@method} #{@url} done (0 length file\n)"
221
+ elsif total == 0
222
+ InternalClient.log << "#{@method} #{@url} (zero content length)\n"
223
+ else
224
+ InternalClient.log << "#{@method} #{@url} %d%% done (%d of %d)\n" % [(size * 100) / total, size, total]
225
+ end
226
+ end
227
+ end
228
+ @tf.close
229
+ @tf
230
+ else
231
+ http_response.read_body
232
+ end
233
+ http_response
234
+ end
235
+
236
+ def process_result res, & block
237
+ if @raw_response
238
+ # We don't decode raw requests
239
+ response = RawResponse.new(@tf, res, args)
240
+ else
241
+ response = Response.create(Request.decode(res['content-encoding'], res.body), res, args)
242
+ end
243
+
244
+ if block_given?
245
+ block.call(response, self, res, & block)
246
+ else
247
+ response.return!(self, res, & block)
248
+ end
249
+
250
+ end
251
+
252
+ def self.decode content_encoding, body
253
+ if (!body) || body.empty?
254
+ body
255
+ elsif content_encoding == 'gzip'
256
+ Zlib::GzipReader.new(StringIO.new(body)).read
257
+ elsif content_encoding == 'deflate'
258
+ begin
259
+ Zlib::Inflate.new.inflate body
260
+ rescue Zlib::DataError
261
+ # No luck with Zlib decompression. Let's try with raw deflate,
262
+ # like some broken web servers do.
263
+ Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
264
+ end
265
+ else
266
+ body
267
+ end
268
+ end
269
+
270
+ def log_request
271
+ if InternalClient.log
272
+ out = []
273
+ out << "InternalClient.#{method} #{url.inspect}"
274
+ out << payload.short_inspect if payload
275
+ out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
276
+ InternalClient.log << out.join(', ') + "\n"
277
+ end
278
+ end
279
+
280
+ def log_response res
281
+ if InternalClient.log
282
+ size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
283
+ InternalClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
284
+ end
285
+ end
286
+
287
+ # Return a hash of headers whose keys are capitalized strings
288
+ def stringify_headers headers
289
+ headers.inject({}) do |result, (key, value)|
290
+ if key.is_a? Symbol
291
+ key = key.to_s.split(/_/).map { |w| w.capitalize }.join('-')
292
+ end
293
+ if 'CONTENT-TYPE' == key.upcase
294
+ target_value = value.to_s
295
+ result[key] = Mimes.mime_for_or_not target_value
296
+ elsif 'ACCEPT' == key.upcase
297
+ # Accept can be composed of several comma-separated values
298
+ if value.is_a? Array
299
+ target_values = value
300
+ else
301
+ target_values = value.to_s.split ','
302
+ end
303
+ result[key] = target_values.map { |ext| Mimes.mime_for_or_not(ext.to_s.strip) }.join(', ')
304
+ else
305
+ result[key] = value.to_s
306
+ end
307
+ result
308
+ end
309
+ end
310
+
311
+ def default_headers
312
+ {:accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate'}
313
+ end
314
+
315
+ private
316
+ def parser
317
+ URI.const_defined?(:Parser) ? URI::Parser.new : URI
318
+ end
319
+
320
+ end
321
+ end
322
+ end