larsburgess-rest-client 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,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 [:get, :head].include? method
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::escape(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, res, & block)
219
+ else
220
+ response.return!(self, res, & 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
@@ -0,0 +1,152 @@
1
+ module RestClient
2
+ # A class that can be instantiated for access to a RESTful resource,
3
+ # including authentication.
4
+ #
5
+ # Example:
6
+ #
7
+ # resource = RestClient::Resource.new('http://some/resource')
8
+ # jpg = resource.get(:accept => 'image/jpg')
9
+ #
10
+ # With HTTP basic authentication:
11
+ #
12
+ # resource = RestClient::Resource.new('http://protected/resource', :user => 'user', :password => 'password')
13
+ # resource.delete
14
+ #
15
+ # With a timeout (seconds):
16
+ #
17
+ # RestClient::Resource.new('http://slow', :timeout => 10)
18
+ #
19
+ # With an open timeout (seconds):
20
+ #
21
+ # RestClient::Resource.new('http://behindfirewall', :open_timeout => 10)
22
+ #
23
+ # You can also use resources to share common headers. For headers keys,
24
+ # symbols are converted to strings. Example:
25
+ #
26
+ # resource = RestClient::Resource.new('http://some/resource', :headers => { :client_version => 1 })
27
+ #
28
+ # This header will be transported as X-Client-Version (notice the X prefix,
29
+ # capitalization and hyphens)
30
+ #
31
+ # Use the [] syntax to allocate subresources:
32
+ #
33
+ # site = RestClient::Resource.new('http://example.com', :user => 'adam', :password => 'mypasswd')
34
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
35
+ #
36
+ class Resource
37
+ attr_reader :url, :options, :block
38
+
39
+ def initialize(url, options={}, backwards_compatibility=nil, &block)
40
+ @url = url
41
+ @block = block
42
+ if options.class == Hash
43
+ @options = options
44
+ else # compatibility with previous versions
45
+ @options = { :user => options, :password => backwards_compatibility }
46
+ end
47
+ end
48
+
49
+ def get(additional_headers={}, &block)
50
+ headers = (options[:headers] || {}).merge(additional_headers)
51
+ Request.execute(options.merge(
52
+ :method => :get,
53
+ :url => url,
54
+ :headers => headers), &(block || @block))
55
+ end
56
+
57
+ def post(payload, additional_headers={}, &block)
58
+ headers = (options[:headers] || {}).merge(additional_headers)
59
+ Request.execute(options.merge(
60
+ :method => :post,
61
+ :url => url,
62
+ :payload => payload,
63
+ :headers => headers), &(block || @block))
64
+ end
65
+
66
+ def put(payload, additional_headers={}, &block)
67
+ headers = (options[:headers] || {}).merge(additional_headers)
68
+ Request.execute(options.merge(
69
+ :method => :put,
70
+ :url => url,
71
+ :payload => payload,
72
+ :headers => headers), &(block || @block))
73
+ end
74
+
75
+ def delete(additional_headers={}, &block)
76
+ headers = (options[:headers] || {}).merge(additional_headers)
77
+ Request.execute(options.merge(
78
+ :method => :delete,
79
+ :url => url,
80
+ :headers => headers), &(block || @block))
81
+ end
82
+
83
+ def to_s
84
+ url
85
+ end
86
+
87
+ def user
88
+ options[:user]
89
+ end
90
+
91
+ def password
92
+ options[:password]
93
+ end
94
+
95
+ def headers
96
+ options[:headers] || {}
97
+ end
98
+
99
+ def timeout
100
+ options[:timeout]
101
+ end
102
+
103
+ def open_timeout
104
+ options[:open_timeout]
105
+ end
106
+
107
+ # Construct a subresource, preserving authentication.
108
+ #
109
+ # Example:
110
+ #
111
+ # site = RestClient::Resource.new('http://example.com', 'adam', 'mypasswd')
112
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
113
+ #
114
+ # This is especially useful if you wish to define your site in one place and
115
+ # call it in multiple locations:
116
+ #
117
+ # def orders
118
+ # RestClient::Resource.new('http://example.com/orders', 'admin', 'mypasswd')
119
+ # end
120
+ #
121
+ # orders.get # GET http://example.com/orders
122
+ # orders['1'].get # GET http://example.com/orders/1
123
+ # orders['1/items'].delete # DELETE http://example.com/orders/1/items
124
+ #
125
+ # Nest resources as far as you want:
126
+ #
127
+ # site = RestClient::Resource.new('http://example.com')
128
+ # posts = site['posts']
129
+ # first_post = posts['1']
130
+ # comments = first_post['comments']
131
+ # comments.post 'Hello', :content_type => 'text/plain'
132
+ #
133
+ def [](suburl, &new_block)
134
+ case
135
+ when block_given? then self.class.new(concat_urls(url, suburl), options, &new_block)
136
+ when block then self.class.new(concat_urls(url, suburl), options, &block)
137
+ else
138
+ self.class.new(concat_urls(url, suburl), options)
139
+ end
140
+ end
141
+
142
+ def concat_urls(url, suburl) # :nodoc:
143
+ url = url.to_s
144
+ suburl = suburl.to_s
145
+ if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/'
146
+ url + suburl
147
+ else
148
+ "#{url}/#{suburl}"
149
+ end
150
+ end
151
+ end
152
+ end