rufus-verbs 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,532 @@
1
+ #
2
+ #--
3
+ # Copyright (c) 2008, John Mettraux, jmettraux@gmail.com
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+ #
23
+ # (MIT license)
24
+ #++
25
+ #
26
+
27
+ #
28
+ # John Mettraux
29
+ #
30
+ # Made in Japan
31
+ #
32
+ # 2008/01/11
33
+ #
34
+
35
+ require 'uri'
36
+ require 'yaml' # for StringIO (at least for now)
37
+ require 'net/http'
38
+ require 'zlib'
39
+
40
+
41
+ module Rufus
42
+ module Verbs
43
+
44
+ VERSION = "0.1"
45
+ USER_AGENT = "Ruby rufus-verbs #{VERSION}"
46
+
47
+ #
48
+ # An EndPoint can be used to share common options among a set of
49
+ # requests.
50
+ #
51
+ # ep = EndPoint.new(
52
+ # :host => "restful.server",
53
+ # :port => 7080,
54
+ # :resource => "inventory/tools")
55
+ #
56
+ # res = ep.get :id => 1
57
+ # # still a silver bullet ?
58
+ #
59
+ # res = ep.get :id => 0
60
+ # # where did the hammer go ?
61
+ #
62
+ # When a request gets prepared, the option values will be looked up
63
+ # in (1) its local (request) options, then (2) in the EndPoint options.
64
+ #
65
+ class EndPoint
66
+
67
+ #
68
+ # The endpoint initialization opts (Hash instance)
69
+ #
70
+ attr_reader :opts
71
+
72
+ def initialize (opts)
73
+
74
+ @opts = opts
75
+
76
+ compute_target @opts
77
+
78
+ @opts[:http_basic_authentication] =
79
+ opts[:http_basic_authentication] || opts[:hba]
80
+
81
+ @opts[:user_agent] ||= USER_AGENT
82
+
83
+ @opts[:proxy] ||= ENV['HTTP_PROXY']
84
+ end
85
+
86
+ def get (*args)
87
+
88
+ request :get, args
89
+ end
90
+
91
+ def post (*args, &block)
92
+
93
+ request :post, args, &block
94
+ end
95
+
96
+ def put (*args, &block)
97
+
98
+ request :put, args, &block
99
+ end
100
+
101
+ def delete (*args)
102
+
103
+ request :delete, args
104
+ end
105
+
106
+ #
107
+ # This is the method called by the module methods verbs.
108
+ #
109
+ # For example,
110
+ #
111
+ # RufusVerbs.get(args)
112
+ #
113
+ # calls
114
+ #
115
+ # RufusVerbs::EndPoint.request(:get, args)
116
+ #
117
+ def self.request (method, args, &block)
118
+
119
+ opts = extract_opts args
120
+
121
+ EndPoint.new(opts).request(method, opts, &block)
122
+ end
123
+
124
+ #
125
+ # The instance methods get, post, put and delete ultimately calls
126
+ # this request() method. All the work is done here.
127
+ #
128
+ def request (method, args, &block)
129
+
130
+ opts = EndPoint.extract_opts args
131
+
132
+ compute_target opts
133
+
134
+ req = create_request method, opts
135
+
136
+ add_payload(req, opts, &block) if method == :post or method == :put
137
+
138
+ add_authentication(req, opts)
139
+
140
+ add_conditional_headers(req, opts) if method == :get
141
+
142
+ return req if o(opts, :dry_run) == true
143
+
144
+ http = prepare_http opts
145
+
146
+ res = nil
147
+
148
+ http.start do
149
+ res = http.request req
150
+ end
151
+
152
+ return res if o(opts, :raw_response)
153
+
154
+ res = handle_response method, res, opts
155
+
156
+ return res.body if o(opts, :body)
157
+
158
+ res
159
+ end
160
+
161
+ private
162
+
163
+ #
164
+ # Manages various args formats :
165
+ #
166
+ # uri
167
+ # [ uri ]
168
+ # [ uri, opts ]
169
+ # opts
170
+ #
171
+ def self.extract_opts (args)
172
+
173
+ opts = {}
174
+
175
+ args = [ args ] unless args.is_a?(Array)
176
+
177
+ opts = args.last if args.last.is_a?(Hash)
178
+ opts[:uri] = args.first if args.first.is_a?(String)
179
+
180
+ opts
181
+ end
182
+
183
+ #
184
+ # Returns the value from the [request] opts or from the
185
+ # [endpoint] @opts.
186
+ #
187
+ def o (opts, key)
188
+
189
+ keys = Array key
190
+ keys.each { |k| (v = opts[k] and return v) }
191
+ keys.each { |k| (v = @opts[k] and return v) }
192
+ nil
193
+ end
194
+
195
+ #
196
+ # Returns scheme, host, port, path, query
197
+ #
198
+ def compute_target (opts)
199
+
200
+ u = opts[:uri] || opts[:u]
201
+
202
+ r = if opts[:host]
203
+
204
+ [ opts[:scheme] || 'http',
205
+ opts[:host],
206
+ opts[:port] || 80,
207
+ opts[:path] || '/',
208
+ opts[:query] || {} ]
209
+
210
+ elsif u
211
+
212
+ u = URI.parse u.to_s unless u.is_a?(URI)
213
+ [ u.scheme,
214
+ u.host,
215
+ u.port,
216
+ u.path,
217
+ query_to_h(u.query) ]
218
+ else
219
+
220
+ []
221
+ end
222
+
223
+ opts[:scheme] = r[0] || @opts[:scheme]
224
+ opts[:host] = r[1] || @opts[:host]
225
+ opts[:port] = r[2] || @opts[:port]
226
+ opts[:path] = r[3] || @opts[:path]
227
+ opts[:query] = r[4] || @opts[:query]
228
+
229
+ opts[:c_uri] = [
230
+ opts[:scheme],
231
+ opts[:host],
232
+ opts[:port],
233
+ opts[:path],
234
+ opts[:query] ].inspect
235
+ #
236
+ # can be used for conditional gets
237
+
238
+ r
239
+ end
240
+
241
+ #
242
+ # Creates the Net::HTTP request instance.
243
+ #
244
+ # If :fake_put is set, will use Net::HTTP::Post
245
+ # and make sure the query string contains '_method=put' (or
246
+ # '_method=delete').
247
+ #
248
+ # This call will also advertise this rufus-verbs as
249
+ # 'accepting the gzip encoding' (in case of GET).
250
+ #
251
+ def create_request (method, opts)
252
+
253
+ if (o(opts, :fake_put) and
254
+ (method == :put or method == :delete))
255
+
256
+ opts[:query][:_method] = method.to_s
257
+ method = :post
258
+ end
259
+
260
+ p = compute_path opts
261
+
262
+ r = eval("Net::HTTP::#{method.to_s.capitalize}").new p
263
+
264
+ r['User-Agent'] = o(opts, :user_agent)
265
+ # potentially overriden by opts[:headers]
266
+
267
+ h = opts[:headers] || opts[:h]
268
+ h.each { |k, v| r[k] = v } if h
269
+
270
+ r['Accept-Encoding'] = 'gzip' \
271
+ if method == :get and not o(opts, :nozip)
272
+
273
+ r
274
+ end
275
+
276
+ #
277
+ # If @user and @pass are set, will activate basic authentication.
278
+ # Else if the @auth option is set, will assume it contains a Proc
279
+ # and will call it (with the request as a parameter).
280
+ #
281
+ # This comment is too much... Just read the code...
282
+ #
283
+ def add_authentication (req, opts)
284
+
285
+ a = opts[:http_basic_authentication]
286
+
287
+ if a
288
+
289
+ req.basic_auth a[0], a[1]
290
+
291
+ elsif opts[:auth]
292
+
293
+ opts[:auth].call req
294
+ end
295
+ end
296
+
297
+ #
298
+ # In that base class, it's empty.
299
+ # It's implemented in ConditionalEndPoint.
300
+ #
301
+ # Only called for a GET.
302
+ #
303
+ def add_conditional_headers (req, opts)
304
+
305
+ # nada
306
+ end
307
+
308
+ #
309
+ # Prepares a Net::HTTP instance, with potentially some
310
+ # https settings.
311
+ #
312
+ def prepare_http (opts)
313
+
314
+ compute_proxy opts
315
+
316
+ http = Net::HTTP.new(
317
+ opts[:host], opts[:port],
318
+ opts[:proxy_host], opts[:proxy_port],
319
+ opts[:proxy_user], opts[:proxy_pass])
320
+
321
+ return http unless opts[:scheme] == 'https'
322
+
323
+ require 'net/https'
324
+
325
+ http.use_ssl = true
326
+ http.enable_post_connection_check = true
327
+
328
+ http.verify_mode = if o(opts, :ssl_verify_peer)
329
+ OpenSSL::SSL::VERIFY_NONE
330
+ else
331
+ OpenSSL::SSL::VERIFY_PEER
332
+ end
333
+
334
+ store = OpenSSL::X509::Store.new
335
+ store.set_default_paths
336
+ http.cert_store = store
337
+
338
+ http
339
+ end
340
+
341
+ #
342
+ # Makes sure the request opts hold the proxy information.
343
+ #
344
+ # If the option :proxy is set to false, no proxy will be used.
345
+ #
346
+ def compute_proxy (opts)
347
+
348
+ p = o(opts, :proxy)
349
+
350
+ return unless p
351
+
352
+ u = URI.parse p.to_s
353
+
354
+ raise "not an HTTP[S] proxy '#{u.host}'" \
355
+ unless u.scheme.match(/^http/)
356
+
357
+ opts[:proxy_host] = u.host
358
+ opts[:proxy_port] = u.port
359
+ opts[:proxy_user] = u.user
360
+ opts[:proxy_pass] = u.password
361
+ end
362
+
363
+ #
364
+ # Determines the full path of the request (path_info and
365
+ # query_string).
366
+ #
367
+ # For example :
368
+ #
369
+ # /items/4?style=whatever&maxcount=12
370
+ #
371
+ def compute_path (opts)
372
+
373
+ b = o(opts, :base)
374
+ r = o(opts, [ :res, :resource ])
375
+ i = o(opts, :id)
376
+
377
+ p = o(opts, :path)
378
+
379
+ path = p
380
+
381
+ if b or r or i
382
+ path = ""
383
+ path = "/#{b}" if b
384
+ path += "/#{r}" if r
385
+ path += "/#{i}" if i
386
+ end
387
+
388
+ query = opts[:query]
389
+
390
+ return path if not query or query.size < 1
391
+
392
+ path + '?' + h_to_query(query)
393
+ end
394
+
395
+ #
396
+ # "a=A&b=B" -> { "a" => "A", "b" => "B" }
397
+ #
398
+ def query_to_h (q)
399
+
400
+ return nil unless q
401
+
402
+ q.split("&").inject({}) do |r, e|
403
+ s = e.split("=")
404
+ r[s[0]] = s[1]
405
+ r
406
+ end
407
+ end
408
+
409
+ #
410
+ # { "a" => "A", "b" => "B" } -> "a=A&b=B"
411
+ #
412
+ def h_to_query (h)
413
+
414
+ h.entries.collect { |e| e.join("=") }.join("&")
415
+ end
416
+
417
+ #
418
+ # Fills the request body (with the content of :d or :fd).
419
+ #
420
+ def add_payload (req, opts, &block)
421
+
422
+ d = opts[:d] || opts[:data]
423
+ fd = opts[:fd] || opts[:form_data]
424
+
425
+ if d
426
+ req.body = d
427
+ elsif fd
428
+ sep = opts[:fd_sep] #|| nil
429
+ req.set_form_data fd, sep
430
+ elsif block
431
+ req.body = block.call req
432
+ else
433
+ req.body = ""
434
+ end
435
+ end
436
+
437
+ #
438
+ # Handles the server response.
439
+ # Eventually follows redirections.
440
+ #
441
+ # Once the final response has been hit, will make sure
442
+ # it's decompressed.
443
+ #
444
+ def handle_response (method, res, opts)
445
+
446
+ #if res.is_a?(Net::HTTPRedirection)
447
+ if [ 301, 303, 307 ].include?(res.code.to_i)
448
+
449
+ maxr = o(opts, :max_redirections)
450
+
451
+ if maxr
452
+ maxr = maxr - 1
453
+ raise "too many redirections" if maxr == -1
454
+ opts[:max_redirections] = maxr
455
+ end
456
+
457
+ location = res['Location']
458
+
459
+ prev_host = [ opts[:scheme], opts[:host] ]
460
+
461
+ if location.match /^http/
462
+ u = URI::parse location
463
+ opts[:scheme] = u.scheme
464
+ opts[:host] = u.host
465
+ opts[:port] = u.port
466
+ opts[:path] = u.path
467
+ opts[:query] = u.query
468
+ else
469
+ opts[:path], opts[:query] = location.split "?"
470
+ end
471
+
472
+ if (authentication_is_on?(opts) and
473
+ [ opts[:scheme], opts[:host] ] != prev_host)
474
+
475
+ raise(
476
+ "getting redirected to #{location} while " +
477
+ "authentication is on. Stopping.")
478
+ end
479
+
480
+ opts[:query] = query_to_h opts[:query]
481
+
482
+ return request(method, opts)
483
+ end
484
+
485
+ decompress res
486
+
487
+ res
488
+ end
489
+
490
+ #
491
+ # Returns true if the current request has authentication
492
+ # going on.
493
+ #
494
+ def authentication_is_on? (opts)
495
+
496
+ (o(opts, [ :http_basic_authentication, :hba, :auth ]) != nil)
497
+ end
498
+
499
+ #
500
+ # Inflates the response body if necessary.
501
+ #
502
+ def decompress (res)
503
+
504
+ if res['content-encoding'] == 'gzip'
505
+
506
+ class << res
507
+
508
+ attr_accessor :deflated_body
509
+
510
+ alias :old_body :body
511
+
512
+ def body
513
+ @deflated_body || old_body
514
+ end
515
+ end
516
+ #
517
+ # reopened the response to add
518
+ # a 'deflated_body' attr and let the the body
519
+ # method point to it
520
+
521
+ # now deflate...
522
+
523
+ io = StringIO.new res.body
524
+ gz = Zlib::GzipReader.new io
525
+ res.deflated_body = gz.read
526
+ gz.close
527
+ end
528
+ end
529
+ end
530
+ end
531
+ end
532
+