rest-client 1.1.0 → 1.2.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.

@@ -1,30 +1,30 @@
1
1
  require File.dirname(__FILE__) + '/mixin/response'
2
2
 
3
3
  module RestClient
4
- # The response from RestClient on a raw request looks like a string, but is
5
- # actually one of these. 99% of the time you're making a rest call all you
6
- # care about is the body, but on the occassion you want to fetch the
7
- # headers you can:
8
- #
9
- # RestClient.get('http://example.com').headers[:content_type]
10
- #
11
- # In addition, if you do not use the response as a string, you can access
12
- # a Tempfile object at res.file, which contains the path to the raw
13
- # downloaded request body.
14
- class RawResponse
15
- include RestClient::Mixin::Response
4
+ # The response from RestClient on a raw request looks like a string, but is
5
+ # actually one of these. 99% of the time you're making a rest call all you
6
+ # care about is the body, but on the occassion you want to fetch the
7
+ # headers you can:
8
+ #
9
+ # RestClient.get('http://example.com').headers[:content_type]
10
+ #
11
+ # In addition, if you do not use the response as a string, you can access
12
+ # a Tempfile object at res.file, which contains the path to the raw
13
+ # downloaded request body.
14
+ class RawResponse
15
+ include RestClient::Mixin::Response
16
16
 
17
- attr_reader :file
17
+ attr_reader :file
18
18
 
19
- def initialize(tempfile, net_http_res)
20
- @net_http_res = net_http_res
21
- @file = tempfile
22
- end
19
+ def initialize(tempfile, net_http_res)
20
+ @net_http_res = net_http_res
21
+ @file = tempfile
22
+ end
23
23
 
24
- def to_s
25
- @file.open
26
- @file.read
27
- end
24
+ def to_s
25
+ @file.open
26
+ @file.read
27
+ end
28
28
 
29
- end
29
+ end
30
30
  end
@@ -1,252 +1,287 @@
1
1
  require 'tempfile'
2
+ require 'mime/types'
2
3
 
3
4
  module RestClient
4
- # This class is used internally by RestClient to send the request, but you can also
5
- # call it directly if you'd like to use a method not supported by the
6
- # main API. For example:
7
- #
8
- # RestClient::Request.execute(:method => :head, :url => 'http://example.com')
9
- #
10
- # Mandatory parameters:
11
- # * :method
12
- # * :url
13
- # Optional parameters (have a look at ssl and/or uri for some explanations):
14
- # * :headers a hash containing the request headers
15
- # * :cookies will replace possible cookies in the :headers
16
- # * :user and :password for basic auth, will be replaced by a user/password available in the :url
17
- # * :raw_response return a low-level RawResponse instead of a Response
18
- # * :verify_ssl enable ssl verification, possible values are constants from OpenSSL::SSL
19
- # * :timeout and :open_timeout
20
- # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file
21
- class Request
22
- attr_reader :method, :url, :payload, :headers,
23
- :cookies, :user, :password, :timeout, :open_timeout,
24
- :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file,
25
- :raw_response
26
-
27
- def self.execute(args)
28
- new(args).execute
29
- end
30
-
31
- def initialize(args)
32
- @method = args[:method] or raise ArgumentError, "must pass :method"
33
- @url = args[:url] or raise ArgumentError, "must pass :url"
34
- @headers = args[:headers] || {}
35
- @cookies = @headers.delete(:cookies) || args[:cookies] || {}
36
- @payload = Payload.generate(args[:payload])
37
- @user = args[:user]
38
- @password = args[:password]
39
- @timeout = args[:timeout]
40
- @open_timeout = args[:open_timeout]
41
- @raw_response = args[:raw_response] || false
42
- @verify_ssl = args[:verify_ssl] || false
43
- @ssl_client_cert = args[:ssl_client_cert] || nil
44
- @ssl_client_key = args[:ssl_client_key] || nil
45
- @ssl_ca_file = args[:ssl_ca_file] || nil
46
- @tf = nil # If you are a raw request, this is your tempfile
47
- end
48
-
49
- def execute
50
- execute_inner
51
- rescue Redirect => e
52
- @url = e.url
53
- @method = :get
54
- @payload = nil
55
- execute
56
- end
57
-
58
- def execute_inner
59
- uri = parse_url_with_auth(url)
60
- transmit uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload
61
- end
62
-
63
- def make_headers(user_headers)
64
- unless @cookies.empty?
65
- user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ')
66
- end
67
-
68
- headers = default_headers.merge(user_headers).inject({}) do |final, (key, value)|
69
- final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s
70
- final
71
- end
72
-
73
- headers.merge!(@payload.headers) if @payload
74
- headers
75
- end
76
-
77
- def net_http_class
78
- if RestClient.proxy
79
- proxy_uri = URI.parse(RestClient.proxy)
80
- Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
81
- else
82
- Net::HTTP
83
- end
84
- end
85
-
86
- def net_http_request_class(method)
87
- Net::HTTP.const_get(method.to_s.capitalize)
88
- end
89
-
90
- def parse_url(url)
91
- url = "http://#{url}" unless url.match(/^http/)
92
- URI.parse(url)
93
- end
94
-
95
- def parse_url_with_auth(url)
96
- uri = parse_url(url)
97
- @user = uri.user if uri.user
98
- @password = uri.password if uri.password
99
- uri
100
- end
101
-
102
- def process_payload(p=nil, parent_key=nil)
103
- unless p.is_a?(Hash)
104
- p
105
- else
106
- @headers[:content_type] ||= 'application/x-www-form-urlencoded'
107
- p.keys.map do |k|
108
- key = parent_key ? "#{parent_key}[#{k}]" : k
109
- if p[k].is_a? Hash
110
- process_payload(p[k], key)
111
- else
112
- value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
113
- "#{key}=#{value}"
114
- end
115
- end.join("&")
116
- end
117
- end
118
-
119
- def transmit(uri, req, payload)
120
- setup_credentials(req)
121
-
122
- net = net_http_class.new(uri.host, uri.port)
123
- net.use_ssl = uri.is_a?(URI::HTTPS)
124
- if @verify_ssl == false
125
- net.verify_mode = OpenSSL::SSL::VERIFY_NONE
126
- elsif @verify_ssl.is_a? Integer
127
- net.verify_mode = @verify_ssl
128
- end
129
- net.cert = @ssl_client_cert if @ssl_client_cert
130
- net.key = @ssl_client_key if @ssl_client_key
131
- net.ca_file = @ssl_ca_file if @ssl_ca_file
132
- net.read_timeout = @timeout if @timeout
133
- net.open_timeout = @open_timeout if @open_timeout
134
-
135
- display_log request_log
136
-
137
- net.start do |http|
138
- res = http.request(req, payload) { |http_response| fetch_body(http_response) }
139
- result = process_result(res)
140
- display_log response_log(res)
141
-
142
- if result.kind_of?(String) or @method == :head
143
- Response.new(result, res)
144
- elsif @raw_response
145
- RawResponse.new(@tf, res)
146
- else
147
- Response.new(nil, res)
148
- end
149
- end
150
- rescue EOFError
151
- raise RestClient::ServerBrokeConnection
152
- rescue Timeout::Error
153
- raise RestClient::RequestTimeout
154
- end
155
-
156
- def setup_credentials(req)
157
- req.basic_auth(user, password) if user
158
- end
159
-
160
- def fetch_body(http_response)
161
- if @raw_response
162
- # Taken from Chef, which as in turn...
163
- # Stolen from http://www.ruby-forum.com/topic/166423
164
- # Kudos to _why!
165
- @tf = Tempfile.new("rest-client")
166
- size, total = 0, http_response.header['Content-Length'].to_i
167
- http_response.read_body do |chunk|
168
- @tf.write(chunk)
169
- size += chunk.size
170
- if size == 0
171
- display_log("#{@method} #{@url} done (0 length file)")
172
- elsif total == 0
173
- display_log("#{@method} #{@url} (zero content length)")
174
- else
175
- display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total])
176
- end
177
- end
178
- @tf.close
179
- @tf
180
- else
181
- http_response.read_body
182
- end
183
- http_response
184
- end
185
-
186
- def process_result(res)
187
- if res.code =~ /\A2\d{2}\z/
188
- # We don't decode raw requests
189
- unless @raw_response
190
- self.class.decode res['content-encoding'], res.body if res.body
191
- end
192
- elsif %w(301 302 303).include? res.code
193
- url = res.header['Location']
194
-
195
- if url !~ /^http/
196
- uri = URI.parse(@url)
197
- uri.path = "/#{url}".squeeze('/')
198
- url = uri.to_s
199
- end
200
-
201
- raise Redirect, url
202
- elsif res.code == "304"
203
- raise NotModified, res
204
- elsif res.code == "401"
205
- raise Unauthorized, res
206
- elsif res.code == "404"
207
- raise ResourceNotFound, res
208
- else
209
- raise RequestFailed, res
210
- end
211
- end
212
-
213
- def self.decode(content_encoding, body)
214
- if content_encoding == 'gzip' and not body.empty?
215
- Zlib::GzipReader.new(StringIO.new(body)).read
216
- elsif content_encoding == 'deflate'
217
- Zlib::Inflate.new.inflate(body)
218
- else
219
- body
220
- end
221
- end
222
-
223
- def request_log
224
- out = []
225
- out << "RestClient.#{method} #{url.inspect}"
226
- out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
227
- out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
228
- out.join(', ')
229
- end
230
-
231
- def response_log(res)
232
- size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
233
- "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes"
234
- end
235
-
236
- def display_log(msg)
237
- return unless log_to = RestClient.log
238
-
239
- if log_to == 'stdout'
240
- STDOUT.puts msg
241
- elsif log_to == 'stderr'
242
- STDERR.puts msg
243
- else
244
- File.open(log_to, 'a') { |f| f.puts msg }
245
- end
246
- end
247
-
248
- def default_headers
249
- { :accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate' }
250
- end
251
- end
5
+ # This class is used internally by RestClient to send the request, but you can also
6
+ # call it directly if you'd like to use a method not supported by the
7
+ # main API. For example:
8
+ #
9
+ # RestClient::Request.execute(:method => :head, :url => 'http://example.com')
10
+ #
11
+ # Mandatory parameters:
12
+ # * :method
13
+ # * :url
14
+ # Optional parameters (have a look at ssl and/or uri for some explanations):
15
+ # * :headers a hash containing the request headers
16
+ # * :cookies will replace possible cookies in the :headers
17
+ # * :user and :password for basic auth, will be replaced by a user/password available in the :url
18
+ # * :raw_response return a low-level RawResponse instead of a Response
19
+ # * :verify_ssl enable ssl verification, possible values are constants from OpenSSL::SSL
20
+ # * :timeout and :open_timeout
21
+ # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file
22
+ class Request
23
+ attr_reader :method, :url, :payload, :headers, :processed_headers,
24
+ :cookies, :user, :password, :timeout, :open_timeout,
25
+ :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file,
26
+ :raw_response
27
+
28
+ def self.execute(args)
29
+ new(args).execute
30
+ end
31
+
32
+ def initialize(args)
33
+ @method = args[:method] or raise ArgumentError, "must pass :method"
34
+ @url = args[:url] or raise ArgumentError, "must pass :url"
35
+ @headers = args[:headers] || {}
36
+ @cookies = @headers.delete(:cookies) || args[:cookies] || {}
37
+ @payload = Payload.generate(args[:payload])
38
+ @user = args[:user]
39
+ @password = args[:password]
40
+ @timeout = args[:timeout]
41
+ @open_timeout = args[:open_timeout]
42
+ @raw_response = args[:raw_response] || false
43
+ @verify_ssl = args[:verify_ssl] || false
44
+ @ssl_client_cert = args[:ssl_client_cert] || nil
45
+ @ssl_client_key = args[:ssl_client_key] || nil
46
+ @ssl_ca_file = args[:ssl_ca_file] || nil
47
+ @tf = nil # If you are a raw request, this is your tempfile
48
+ @processed_headers = make_headers headers
49
+ end
50
+
51
+ def execute
52
+ execute_inner
53
+ rescue Redirect => e
54
+ @url = e.url
55
+ @method = :get
56
+ @payload = nil
57
+ execute
58
+ end
59
+
60
+ def execute_inner
61
+ uri = parse_url_with_auth(url)
62
+ transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload
63
+ end
64
+
65
+ def make_headers user_headers
66
+ unless @cookies.empty?
67
+ user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ')
68
+ end
69
+
70
+ headers = default_headers.merge(user_headers).inject({}) do |final, (key, value)|
71
+ target_key = key.to_s.gsub(/_/, '-').capitalize
72
+ if 'CONTENT-TYPE' == target_key.upcase
73
+ target_value = value.to_s
74
+ final[target_key] = MIME::Types.type_for_extension target_value
75
+ elsif 'ACCEPT' == target_key.upcase
76
+ # Accept can be composed of several comma-separated values
77
+ if value.is_a? Array
78
+ target_values = value
79
+ else
80
+ target_values = value.to_s.split ','
81
+ end
82
+ final[target_key] = target_values.map{ |ext| MIME::Types.type_for_extension(ext.to_s.strip)}.join(', ')
83
+ else
84
+ final[target_key] = value.to_s
85
+ end
86
+ final
87
+ end
88
+
89
+ headers.merge!(@payload.headers) if @payload
90
+ headers
91
+ end
92
+
93
+ def net_http_class
94
+ if RestClient.proxy
95
+ proxy_uri = URI.parse(RestClient.proxy)
96
+ Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
97
+ else
98
+ Net::HTTP
99
+ end
100
+ end
101
+
102
+ def net_http_request_class(method)
103
+ Net::HTTP.const_get(method.to_s.capitalize)
104
+ end
105
+
106
+ def parse_url(url)
107
+ url = "http://#{url}" unless url.match(/^http/)
108
+ URI.parse(url)
109
+ end
110
+
111
+ def parse_url_with_auth(url)
112
+ uri = parse_url(url)
113
+ @user = uri.user if uri.user
114
+ @password = uri.password if uri.password
115
+ uri
116
+ end
117
+
118
+ def process_payload(p=nil, parent_key=nil)
119
+ unless p.is_a?(Hash)
120
+ p
121
+ else
122
+ @headers[:content_type] ||= 'application/x-www-form-urlencoded'
123
+ p.keys.map do |k|
124
+ key = parent_key ? "#{parent_key}[#{k}]" : k
125
+ if p[k].is_a? Hash
126
+ process_payload(p[k], key)
127
+ else
128
+ value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
129
+ "#{key}=#{value}"
130
+ end
131
+ end.join("&")
132
+ end
133
+ end
134
+
135
+ def transmit(uri, req, payload)
136
+ setup_credentials(req)
137
+
138
+ net = net_http_class.new(uri.host, uri.port)
139
+ net.use_ssl = uri.is_a?(URI::HTTPS)
140
+ if @verify_ssl == false
141
+ net.verify_mode = OpenSSL::SSL::VERIFY_NONE
142
+ elsif @verify_ssl.is_a? Integer
143
+ net.verify_mode = @verify_ssl
144
+ end
145
+ net.cert = @ssl_client_cert if @ssl_client_cert
146
+ net.key = @ssl_client_key if @ssl_client_key
147
+ net.ca_file = @ssl_ca_file if @ssl_ca_file
148
+ net.read_timeout = @timeout if @timeout
149
+ net.open_timeout = @open_timeout if @open_timeout
150
+
151
+ display_log request_log
152
+
153
+ net.start do |http|
154
+ res = http.request(req, payload) { |http_response| fetch_body(http_response) }
155
+ result = process_result(res)
156
+ display_log response_log(res)
157
+
158
+ if result.kind_of?(String) or @method == :head
159
+ Response.new(result, res)
160
+ elsif @raw_response
161
+ RawResponse.new(@tf, res)
162
+ else
163
+ Response.new(nil, res)
164
+ end
165
+ end
166
+ rescue EOFError
167
+ raise RestClient::ServerBrokeConnection
168
+ rescue Timeout::Error
169
+ raise RestClient::RequestTimeout
170
+ end
171
+
172
+ def setup_credentials(req)
173
+ req.basic_auth(user, password) if user
174
+ end
175
+
176
+ def fetch_body(http_response)
177
+ if @raw_response
178
+ # Taken from Chef, which as in turn...
179
+ # Stolen from http://www.ruby-forum.com/topic/166423
180
+ # Kudos to _why!
181
+ @tf = Tempfile.new("rest-client")
182
+ size, total = 0, http_response.header['Content-Length'].to_i
183
+ http_response.read_body do |chunk|
184
+ @tf.write(chunk)
185
+ size += chunk.size
186
+ if size == 0
187
+ display_log("#{@method} #{@url} done (0 length file)")
188
+ elsif total == 0
189
+ display_log("#{@method} #{@url} (zero content length)")
190
+ else
191
+ display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total])
192
+ end
193
+ end
194
+ @tf.close
195
+ @tf
196
+ else
197
+ http_response.read_body
198
+ end
199
+ http_response
200
+ end
201
+
202
+ def process_result(res)
203
+ if res.code =~ /\A2\d{2}\z/
204
+ # We don't decode raw requests
205
+ unless @raw_response
206
+ self.class.decode res['content-encoding'], res.body if res.body
207
+ end
208
+ elsif %w(301 302 303).include? res.code
209
+ url = res.header['Location']
210
+
211
+ if url !~ /^http/
212
+ uri = URI.parse(@url)
213
+ uri.path = "/#{url}".squeeze('/')
214
+ url = uri.to_s
215
+ end
216
+
217
+ raise Redirect, url
218
+ elsif res.code == "304"
219
+ raise NotModified, res
220
+ elsif res.code == "401"
221
+ raise Unauthorized, res
222
+ elsif res.code == "404"
223
+ raise ResourceNotFound, res
224
+ else
225
+ raise RequestFailed, res
226
+ end
227
+ end
228
+
229
+ def self.decode(content_encoding, body)
230
+ if content_encoding == 'gzip' and not body.empty?
231
+ Zlib::GzipReader.new(StringIO.new(body)).read
232
+ elsif content_encoding == 'deflate'
233
+ Zlib::Inflate.new.inflate(body)
234
+ else
235
+ body
236
+ end
237
+ end
238
+
239
+ def request_log
240
+ if RestClient.log
241
+ out = []
242
+ out << "RestClient.#{method} #{url.inspect}"
243
+ out << "headers: #{processed_headers.inspect}"
244
+ out << "paylod: #{payload.short_inspect}" if payload
245
+ out.join(', ')
246
+ end
247
+ end
248
+
249
+ def response_log(res)
250
+ size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
251
+ "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes"
252
+ end
253
+
254
+ def display_log(msg)
255
+ return unless log_to = RestClient.log
256
+
257
+ if log_to == 'stdout'
258
+ STDOUT.puts msg
259
+ elsif log_to == 'stderr'
260
+ STDERR.puts msg
261
+ else
262
+ File.open(log_to, 'a') { |f| f.puts msg }
263
+ end
264
+ end
265
+
266
+ def default_headers
267
+ { :accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate' }
268
+ end
269
+ end
270
+ end
271
+
272
+ module MIME
273
+ class Types
274
+
275
+ # Return the first found content-type for a value considered as an extension or the value itself
276
+ def type_for_extension ext
277
+ candidates = @extension_index[ext]
278
+ candidates.empty? ? ext : candidates[0].content_type
279
+ end
280
+
281
+ class << self
282
+ def type_for_extension ext
283
+ @__types__.type_for_extension ext
284
+ end
285
+ end
286
+ end
252
287
  end