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