rufus-verbs 0.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,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
+