rest-client 1.6.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.

Potentially problematic release.


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

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