markdillon-restclient 1.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,289 @@
1
+ require 'tempfile'
2
+
3
+ module RestClient
4
+ # This class is used internally by RestClient to send the request, but you can also
5
+ # access it internally if you'd like to use a method not directly supported by the
6
+ # main API. For example:
7
+ #
8
+ # RestClient::Request.execute(:method => :head, :url => 'http://example.com')
9
+ #
10
+ class Request
11
+ EOL = "\r\n"
12
+ attr_reader :method, :url, :payload, :headers,
13
+ :cookies, :user, :password, :timeout, :open_timeout,
14
+ :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file,
15
+ :raw_response
16
+
17
+ def self.execute(args)
18
+ new(args).execute
19
+ end
20
+
21
+ def initialize(args)
22
+ @method = args[:method] or raise ArgumentError, "must pass :method"
23
+ @url = args[:url] or raise ArgumentError, "must pass :url"
24
+ @headers = args[:headers] || {}
25
+ @cookies = @headers.delete(:cookies) || args[:cookies] || {}
26
+ @payload = process_payload(args[:payload])
27
+ @user = args[:user]
28
+ @password = args[:password]
29
+ @timeout = args[:timeout]
30
+ @open_timeout = args[:open_timeout]
31
+ @raw_response = args[:raw_response] || false
32
+ @verify_ssl = args[:verify_ssl] || false
33
+ @ssl_client_cert = args[:ssl_client_cert] || nil
34
+ @ssl_client_key = args[:ssl_client_key] || nil
35
+ @ssl_ca_file = args[:ssl_ca_file] || nil
36
+ @tf = nil # If you are a raw request, this is your tempfile
37
+ end
38
+
39
+ def execute
40
+ execute_inner
41
+ rescue Redirect => e
42
+ @url = e.url
43
+ execute
44
+ end
45
+
46
+ def execute_inner
47
+ uri = parse_url_with_auth(url)
48
+ transmit uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload
49
+ end
50
+
51
+ def make_headers(user_headers)
52
+ unless @cookies.empty?
53
+ user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ')
54
+ end
55
+
56
+ default_headers.merge(user_headers).inject({}) do |final, (key, value)|
57
+ final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s
58
+ final
59
+ end
60
+ end
61
+
62
+ def net_http_class
63
+ if RestClient.proxy
64
+ proxy_uri = URI.parse(RestClient.proxy)
65
+ Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
66
+ else
67
+ Net::HTTP
68
+ end
69
+ end
70
+
71
+ def net_http_request_class(method)
72
+ Net::HTTP.const_get(method.to_s.capitalize)
73
+ end
74
+
75
+ def parse_url(url)
76
+ url = "http://#{url}" unless url.match(/^http/)
77
+ URI.parse(url)
78
+ end
79
+
80
+ def parse_url_with_auth(url)
81
+ uri = parse_url(url)
82
+ @user = uri.user if uri.user
83
+ @password = uri.password if uri.password
84
+ uri
85
+ end
86
+
87
+ def process_payload(p=nil, parent_key=nil)
88
+ unless p.is_a?(Hash)
89
+ p
90
+ else
91
+ if p.delete(:multipart) == true
92
+ multipart = Multipart.new(p)
93
+ @headers[:content_type] = %Q{multipart/form-data; boundary="#{multipart.boundary}"}
94
+ return multipart
95
+ else
96
+ @headers[:content_type] ||= 'application/x-www-form-urlencoded'
97
+ p.keys.map do |k|
98
+ key = parent_key ? "#{parent_key}[#{k}]" : k
99
+ if p[k].is_a? Hash
100
+ process_payload(p[k], key)
101
+ else
102
+ value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
103
+ "#{key}=#{value}"
104
+ end
105
+ end.join("&")
106
+ end
107
+ end
108
+ end
109
+
110
+ def build_multipart(params)
111
+ b = "--#{boundary}"
112
+
113
+ @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
114
+ @stream.write(b)
115
+ params.each do |k,v|
116
+ @stream.write(EOL)
117
+ if v.respond_to?(:read) && v.respond_to?(:path)
118
+ create_file_field(@stream, k,v)
119
+ else
120
+ create_regular_field(@stream, k,v)
121
+ end
122
+ @stream.write(EOL + b)
123
+ end
124
+ @stream.write('--')
125
+ @stream.write(EOL)
126
+ @stream.seek(0)
127
+ end
128
+
129
+ def create_regular_field(s, k, v)
130
+ s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"")
131
+ s.write(EOL)
132
+ s.write(EOL)
133
+ s.write(v)
134
+ end
135
+
136
+ def create_file_field(s, k, v, content_type = nil)
137
+ content_type ||= `file -b --mime #{v.path}`.gsub(/\n/, '')
138
+ begin
139
+ s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{v.path}\"#{EOL}")
140
+ s.write("Content-Type: #{content_type}#{EOL}")
141
+ s.write(EOL)
142
+ while data = v.read(8124)
143
+ s.write(data)
144
+ end
145
+ ensure
146
+ v.close
147
+ end
148
+ end
149
+
150
+ def boundary
151
+ @boundary ||= rand(1_000_000).to_s
152
+ end
153
+
154
+ def transmit(uri, req, payload)
155
+ setup_credentials(req)
156
+
157
+ net = net_http_class.new(uri.host, uri.port)
158
+ net.use_ssl = uri.is_a?(URI::HTTPS)
159
+ if @verify_ssl == false
160
+ net.verify_mode = OpenSSL::SSL::VERIFY_NONE
161
+ elsif @verify_ssl.is_a? Integer
162
+ net.verify_mode = @verify_ssl
163
+ end
164
+ net.cert = @ssl_client_cert if @ssl_client_cert
165
+ net.key = @ssl_client_key if @ssl_client_key
166
+ net.ca_file = @ssl_ca_file if @ssl_ca_file
167
+ net.read_timeout = @timeout if @timeout
168
+ net.open_timeout = @open_timeout if @open_timeout
169
+
170
+ display_log request_log
171
+
172
+ net.start do |http|
173
+ res = http.request(req, payload) { |http_response| fetch_body(http_response) }
174
+ result = process_result(res)
175
+ display_log response_log(res)
176
+
177
+ if result.kind_of?(String) or @method == :head
178
+ Response.new(result, res)
179
+ elsif @raw_response
180
+ RawResponse.new(@tf, res)
181
+ else
182
+ nil
183
+ end
184
+ end
185
+ rescue EOFError
186
+ raise RestClient::ServerBrokeConnection
187
+ rescue Timeout::Error
188
+ raise RestClient::RequestTimeout
189
+ ensure
190
+ @payload.close
191
+ end
192
+
193
+ def setup_credentials(req)
194
+ req.basic_auth(user, password) if user
195
+ end
196
+
197
+ def fetch_body(http_response)
198
+ if @raw_response
199
+ # Taken from Chef, which as in turn...
200
+ # Stolen from http://www.ruby-forum.com/topic/166423
201
+ # Kudos to _why!
202
+ @tf = Tempfile.new("restclient")
203
+ size, total = 0, http_response.header['Content-Length'].to_i
204
+ http_response.read_body do |chunk|
205
+ @tf.write(chunk)
206
+ size += chunk.size
207
+ if size == 0
208
+ display_log("#{@method} #{@url} done (0 length file)")
209
+ elsif total == 0
210
+ display_log("#{@method} #{@url} (zero content length)")
211
+ else
212
+ display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total])
213
+ end
214
+ end
215
+ @tf.close
216
+ @tf
217
+ else
218
+ http_response.read_body
219
+ end
220
+ http_response
221
+ end
222
+
223
+ def process_result(res)
224
+ if res.code =~ /\A2\d{2}\z/
225
+ # We don't decode raw requests
226
+ unless @raw_response
227
+ decode res['content-encoding'], res.body if res.body
228
+ end
229
+ elsif %w(301 302 303).include? res.code
230
+ url = res.header['Location']
231
+
232
+ if url !~ /^http/
233
+ uri = URI.parse(@url)
234
+ uri.path = "/#{url}".squeeze('/')
235
+ url = uri.to_s
236
+ end
237
+
238
+ raise Redirect, url
239
+ elsif res.code == "304"
240
+ raise NotModified, res
241
+ elsif res.code == "401"
242
+ raise Unauthorized, res
243
+ elsif res.code == "404"
244
+ raise ResourceNotFound, res
245
+ else
246
+ raise RequestFailed, res
247
+ end
248
+ end
249
+
250
+ def decode(content_encoding, body)
251
+ if content_encoding == 'gzip' and not body.empty?
252
+ Zlib::GzipReader.new(StringIO.new(body)).read
253
+ elsif content_encoding == 'deflate'
254
+ Zlib::Inflate.new.inflate(body)
255
+ else
256
+ body
257
+ end
258
+ end
259
+
260
+ def request_log
261
+ out = []
262
+ out << "RestClient.#{method} #{url.inspect}"
263
+ out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
264
+ out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
265
+ out.join(', ')
266
+ end
267
+
268
+ def response_log(res)
269
+ size = @raw_response ? File.size(@tf.path) : res.body.size
270
+ "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes"
271
+ end
272
+
273
+ def display_log(msg)
274
+ return unless log_to = RestClient.log
275
+
276
+ if log_to == 'stdout'
277
+ STDOUT.puts msg
278
+ elsif log_to == 'stderr'
279
+ STDERR.puts msg
280
+ else
281
+ File.open(log_to, 'a') { |f| f.puts msg }
282
+ end
283
+ end
284
+
285
+ def default_headers
286
+ { :accept => 'application/xml', :accept_encoding => 'gzip, deflate' }
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,146 @@
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
38
+
39
+ def initialize(url, options={}, backwards_compatibility=nil)
40
+ @url = url
41
+ if options.class == Hash
42
+ @options = options
43
+ else # compatibility with previous versions
44
+ @options = { :user => options, :password => backwards_compatibility }
45
+ end
46
+ end
47
+
48
+ def get(additional_headers={})
49
+ Request.execute(options.merge(
50
+ :method => :get,
51
+ :url => url,
52
+ :headers => headers.merge(additional_headers)
53
+ ))
54
+ end
55
+
56
+ def post(payload, additional_headers={})
57
+ Request.execute(options.merge(
58
+ :method => :post,
59
+ :url => url,
60
+ :payload => payload,
61
+ :headers => headers.merge(additional_headers)
62
+ ))
63
+ end
64
+
65
+ def put(payload, additional_headers={})
66
+ Request.execute(options.merge(
67
+ :method => :put,
68
+ :url => url,
69
+ :payload => payload,
70
+ :headers => headers.merge(additional_headers)
71
+ ))
72
+ end
73
+
74
+ def delete(additional_headers={})
75
+ Request.execute(options.merge(
76
+ :method => :delete,
77
+ :url => url,
78
+ :headers => headers.merge(additional_headers)
79
+ ))
80
+ end
81
+
82
+ def to_s
83
+ url
84
+ end
85
+
86
+ def user
87
+ options[:user]
88
+ end
89
+
90
+ def password
91
+ options[:password]
92
+ end
93
+
94
+ def headers
95
+ options[:headers] || {}
96
+ end
97
+
98
+ def timeout
99
+ options[:timeout]
100
+ end
101
+
102
+ def open_timeout
103
+ options[:open_timeout]
104
+ end
105
+
106
+ # Construct a subresource, preserving authentication.
107
+ #
108
+ # Example:
109
+ #
110
+ # site = RestClient::Resource.new('http://example.com', 'adam', 'mypasswd')
111
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
112
+ #
113
+ # This is especially useful if you wish to define your site in one place and
114
+ # call it in multiple locations:
115
+ #
116
+ # def orders
117
+ # RestClient::Resource.new('http://example.com/orders', 'admin', 'mypasswd')
118
+ # end
119
+ #
120
+ # orders.get # GET http://example.com/orders
121
+ # orders['1'].get # GET http://example.com/orders/1
122
+ # orders['1/items'].delete # DELETE http://example.com/orders/1/items
123
+ #
124
+ # Nest resources as far as you want:
125
+ #
126
+ # site = RestClient::Resource.new('http://example.com')
127
+ # posts = site['posts']
128
+ # first_post = posts['1']
129
+ # comments = first_post['comments']
130
+ # comments.post 'Hello', :content_type => 'text/plain'
131
+ #
132
+ def [](suburl)
133
+ self.class.new(concat_urls(url, suburl), options)
134
+ end
135
+
136
+ def concat_urls(url, suburl) # :nodoc:
137
+ url = url.to_s
138
+ suburl = suburl.to_s
139
+ if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/'
140
+ url + suburl
141
+ else
142
+ "#{url}/#{suburl}"
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,20 @@
1
+ require File.dirname(__FILE__) + '/mixin/response'
2
+
3
+ module RestClient
4
+ # The response from RestClient looks like a string, but is actually one of
5
+ # these. 99% of the time you're making a rest call all you care about is
6
+ # the body, but on the occassion you want to fetch the headers you can:
7
+ #
8
+ # RestClient.get('http://example.com').headers[:content_type]
9
+ #
10
+ class Response < String
11
+
12
+ include RestClient::Mixin::Response
13
+
14
+ def initialize(string, net_http_res)
15
+ @net_http_res = net_http_res
16
+ super(string || "")
17
+ end
18
+
19
+ end
20
+ end