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.
- data/README.txt +248 -0
- data/lib/rufus/verbs.rb +74 -0
- data/lib/rufus/verbs/conditional.rb +142 -0
- data/lib/rufus/verbs/endpoint.rb +532 -0
- data/test/auth_test.rb +46 -0
- data/test/block_test.rb +49 -0
- data/test/conditional_test.rb +49 -0
- data/test/dryrun_test.rb +42 -0
- data/test/https_test.rb +42 -0
- data/test/iconditional_test.rb +68 -0
- data/test/items.rb +262 -0
- data/test/proxy_test.rb +82 -0
- data/test/redir_test.rb +26 -0
- data/test/simple_test.rb +74 -0
- data/test/test.rb +12 -0
- data/test/testbase.rb +47 -0
- metadata +70 -0
@@ -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
|
+
|