rufus-verbs 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.txt CHANGED
@@ -21,14 +21,16 @@ currently :
21
21
  * proxy-aware (HTTP_PROXY env var or :proxy option)
22
22
  * conditional GET (via ConditionalEndPoint class)
23
23
  * request body built via a block (post and put)
24
+ * cookie-aware (if :cookies option is explicitely set to true)
25
+ * http digest authentication (rfc 2617) (auth ok, auth-int not tested)
24
26
 
25
27
  maybe later :
26
28
 
27
29
  * retry on failure
28
- * greediness (automatic parsing for content like JSON or YAML)
29
- * http digest authentication
30
30
  * cache awareness
31
+ * greediness (automatic parsing for content like JSON or YAML)
31
32
  * head, options
33
+ * persistent cookie jar
32
34
 
33
35
 
34
36
  == getting it
@@ -105,12 +107,29 @@ A ConditionalEndPoint is an EndPoint that will use conditional GETs whenever pos
105
107
  # first call will retrieve the representation completely
106
108
 
107
109
  res = ep.get :id => 1
108
- # the server (provided that it supports conditional GET) only
110
+ # the server (provided that it supports conditional GETs) only
109
111
  # returned a 304 answer, the response is returned from the
110
112
  # ConditionalEndPoint cache
111
113
 
112
114
  More about conditional GETs at http://ruturajv.wordpress.com/2005/12/27/conditional-get-request/
113
115
 
116
+ Cookies may be activated for an endpoint in this way :
117
+
118
+ ep = EndPoint.new :cookies => true
119
+
120
+ res = ep.get "http://resta.farian.zion/tools/3"
121
+ res = ep.post("http://resta.farian.zion/tools") { "hammer" }
122
+
123
+ Digest authentication and basic authentication are available at request or endpoint level :
124
+
125
+ ep = EndPoint.new :http_basic_authentication => [ "toto", "secretpass" ]
126
+
127
+ ep = EndPoint.new :digest_authentication => [ "toto", "secretpass" ]
128
+
129
+ res = get "http://server/doc0", :hba => [ "toto", "secretpass" ]
130
+
131
+ res = get "http://server/doc1", :digest_authentication => [ "toto", "secretpass" ]
132
+
114
133
  The tests may provide good intel as on 'rufus-verbs' usage as well : http://rufus.rubyforge.org/svn/trunk/verbs/test/
115
134
 
116
135
 
@@ -145,12 +164,19 @@ by default, a request returns a Net::HTTPResponse instance, with :body => true,
145
164
  * <b>:cache_size</b> (integer, ConditionalEndPoint only)
146
165
  by default, a ConditionalEndPoint will cache 147 results (and it'll start discarded the least recently used cached responses). This option is used to set this cache's size.
147
166
 
167
+ * <b>:cookies</b> (boolean/integer, E)
168
+ when not set or set to false, rufus-verbs won't care about cookies. If set to true or an integer value, set-cookie requests by the servers will be honoured. The integer value will be interpreted as the size of the 'cookie jar', the default size being 77. The least recently used cookies will be discarded.
169
+ The cookie jar implementation is not persistent.
170
+
148
171
  * <b>:d</b> (string, R)
149
172
  the short variant of :data
150
173
 
151
174
  * <b>:data</b> (string, R)
152
175
  the data (request body) for a put or a post.
153
176
 
177
+ * <b>:digest_authentication</b> (pair of strings, RE)
178
+ the pair username/password to be used for digest authentication.
179
+
154
180
  * <b>:dry_run</b> (boolean, RE)
155
181
  when <tt>:dry_run => true</tt>, the request will be prepared but not executed and will be returned instead of the HTTP response (used for testing)
156
182
 
@@ -163,13 +189,13 @@ the short version of :form_data
163
189
  * <b>:form_data</b>
164
190
  this option expects a hash. The (post or put) request body will then be built with this hash.
165
191
 
166
- * <b>:hba</b> (pair, RE)
192
+ * <b>:hba</b> (pair of strings, RE)
167
193
  short for :http_basic_authentication
168
194
 
169
195
  * <b>:host</b> (string, RE)
170
196
  the host or IP address for the request
171
197
 
172
- * <b>:http_basic_authentication</b> (pair, RE)
198
+ * <b>:http_basic_authentication</b> (pair of strings, RE)
173
199
  will activate HTTP basic authentication, takes a pair (array) argument [ user, pass ]
174
200
 
175
201
  * <b>:id</b> (string, R)
@@ -205,6 +231,9 @@ the resource (or the middle) of a full resource path (see :base)
205
231
  * <b>:scheme</b> (string, R)
206
232
  'http' or 'https'
207
233
 
234
+ * <b>:ssl_verify_peer</b> (boolean, RE)
235
+ by default, rufus-verbs doesn't verify ssl certificates. With this option set to true, it will.
236
+
208
237
  * <b>:u</b> (uri, string, RE)
209
238
  the short version of :uri
210
239
 
@@ -222,9 +251,10 @@ the gem rufus-lru[http://rufus.rubyforge.org/rufus-lru]
222
251
 
223
252
  == mailing list
224
253
 
225
- On the OpenWFEru-user list[http://groups.google.com/group/openwferu-users] for now :
254
+ On the Rufus-Ruby list[http://groups.google.com/group/rufus-ruby] :
255
+
256
+ http://groups.google.com/group/rufus-ruby
226
257
 
227
- http://groups.google.com/group/openwferu-users
228
258
 
229
259
  == issue tracker
230
260
 
@@ -32,6 +32,10 @@
32
32
  # 2008/01/16
33
33
  #
34
34
 
35
+ require 'rubygems'
36
+ require 'rufus/lru'
37
+
38
+
35
39
  module Rufus::Verbs
36
40
 
37
41
  #
@@ -61,13 +65,6 @@ module Rufus::Verbs
61
65
 
62
66
  cs = opts[:cache_size] || 147
63
67
 
64
- require 'rubygems'
65
- begin
66
- require 'rufus/lru'
67
- rescue LoadError
68
- raise "gem 'rufus-lru' is missing, please install it."
69
- end
70
-
71
68
  @cache = LruHash.new cs
72
69
  end
73
70
 
@@ -0,0 +1,320 @@
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/19
33
+ #
34
+
35
+ require 'webrick/cookie'
36
+
37
+ require 'rubygems'
38
+ require 'rufus/lru'
39
+
40
+
41
+ module Rufus
42
+ module Verbs
43
+
44
+ #
45
+ # Cookies related methods
46
+ #
47
+ # http://www.ietf.org/rfc/rfc2109.txt
48
+ #
49
+ module CookieMixin
50
+
51
+ protected
52
+
53
+ #
54
+ # Prepares the instance variable @cookies for storing
55
+ # cooking for this endpoint.
56
+ #
57
+ # Reads the :cookies endpoint option for determining the
58
+ # size of the cookie jar (77 by default).
59
+ #
60
+ def prepare_cookie_jar
61
+
62
+ o = @opts[:cookies]
63
+
64
+ return unless o and o != false
65
+
66
+ s = o.to_s.to_i
67
+ s = 77 if s < 1
68
+
69
+ @cookies = CookieJar.new s
70
+ end
71
+
72
+ #
73
+ # Parses the HTTP response for a potential 'Set-Cookie' header,
74
+ # parses and returns it as a hash.
75
+ #
76
+ def parse_cookies (response)
77
+
78
+ c = response['Set-Cookie']
79
+ return nil unless c
80
+ Cookie.parse_set_cookies c
81
+ end
82
+
83
+ #
84
+ # (This method will have no effect if the EndPoint is not
85
+ # tracking cookies)
86
+ #
87
+ # Registers a potential cookie set by the server.
88
+ #
89
+ def register_cookies (response, opts)
90
+
91
+ return unless @cookies
92
+
93
+ cs = parse_cookies response
94
+
95
+ return unless cs
96
+
97
+ # "The origin server effectively ends a session by
98
+ # sending the client a Set-Cookie header with Max-Age=0"
99
+
100
+ cs.each do |c|
101
+
102
+ host = opts[:host]
103
+ path = opts[:path]
104
+ cpath = c.path || "/"
105
+
106
+ next unless cookie_acceptable?(opts, c)
107
+
108
+ domain = c.domain || host
109
+
110
+ if c.max_age == 0
111
+ @cookies.remove_cookie domain, path, c
112
+ else
113
+ @cookies.add_cookie domain, path, c
114
+ end
115
+ end
116
+ end
117
+
118
+ #
119
+ # Checks if the cookie is acceptable in the context of
120
+ # the request that sent it.
121
+ #
122
+ def cookie_acceptable? (opts, cookie)
123
+
124
+ # reject if :
125
+ #
126
+ # * The value for the Path attribute is not a prefix of the
127
+ # request-URI.
128
+ # * The value for the Domain attribute contains no embedded dots
129
+ # or does not start with a dot.
130
+ # * The value for the request-host does not domain-match the
131
+ # Domain attribute.
132
+ # * The request-host is a FQDN (not IP address) and has the form
133
+ # HD, where D is the value of the Domain attribute, and H is a
134
+ # string that contains one or more dots.
135
+
136
+ cdomain = cookie.domain
137
+
138
+ if cdomain
139
+
140
+ return false unless cdomain.index '.'
141
+ return false if cdomain[0, 1] != '.'
142
+
143
+ h, d = split_host(opts[:host])
144
+ return false if d != cdomain
145
+ end
146
+
147
+ path = opts[:path]
148
+ cpath = cookie.path || "/"
149
+
150
+ return false if path[0..cpath.length-1] != cpath
151
+
152
+ true
153
+ end
154
+
155
+ #
156
+ # Places the 'Cookie' header in the request if appropriate.
157
+ #
158
+ # (This method will have no effect if the EndPoint is not
159
+ # tracking cookies)
160
+ #
161
+ def mention_cookies (request, opts)
162
+
163
+ return unless @cookies
164
+
165
+ cs = @cookies.fetch_cookies opts[:host], opts[:path]
166
+
167
+ request['Cookie'] = cs.collect { |c| c.to_header_s }.join(",")
168
+ end
169
+ end
170
+
171
+ #
172
+ # An extension of the cookie implementation found in WEBrick.
173
+ #
174
+ # Unmodified for now.
175
+ #
176
+ class Cookie < WEBrick::Cookie
177
+
178
+ def to_header_s
179
+
180
+ ret = ""
181
+ ret << @name << "=" << @value
182
+ ret << "; " << "$Version=" << @version.to_s if @version > 0
183
+ ret << "; " << "$Domain=" << @domain if @domain
184
+ ret << "; " << "$Port=" << @port if @port
185
+ ret << "; " << "$Path=" << @path if @path
186
+ ret
187
+ end
188
+ end
189
+
190
+ #
191
+ # Cookies are stored by domain, they via this CookieKey which gathers
192
+ # path and name of the cookie.
193
+ #
194
+ class CookieKey
195
+
196
+ attr_reader :name, :path
197
+
198
+ def initialize (path, cookie)
199
+
200
+ @name = cookie.name
201
+ @path = path || cookie.path
202
+ end
203
+
204
+ #
205
+ # longer paths first
206
+ #
207
+ def <=> (other)
208
+
209
+ -1 * (@path <=> other.path)
210
+ end
211
+
212
+ def hash
213
+ "#{@name}|#{@path}".hash
214
+ end
215
+
216
+ def == (other)
217
+ (@path == other.path and @name == other.name)
218
+ end
219
+
220
+ alias eql? ==
221
+ end
222
+
223
+ #
224
+ # A few methods about hostnames.
225
+ #
226
+ # (in a mixin... could be helpful somewhere else later)
227
+ #
228
+ module HostMixin
229
+
230
+ #
231
+ # Matching a classical IP address (not a v6 though).
232
+ # Should be sufficient for now.
233
+ #
234
+ IP_REGEX = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
235
+
236
+ #
237
+ # Returns a pair host/domain, note that the domain starts with a dot.
238
+ #
239
+ # split_host('localhost') --> [ 'localhost', nil ]
240
+ # split_host('benz.car.co.nz') --> [ 'benz', '.car.co.nz' ]
241
+ # split_host('127.0.0.1') --> [ '127.0.0.1', nil ]
242
+ # split_host('::1') --> [ '::1', nil ]
243
+ #
244
+ def split_host (host)
245
+
246
+ return [ host, nil ] if IP_REGEX.match host
247
+ i = host.index('.')
248
+ return [ host, nil ] unless i
249
+ [ host[0..i-1], host[i..-1] ]
250
+ end
251
+ end
252
+
253
+ #
254
+ # The container for cookies. Features methods for storing and retrieving
255
+ # cookies easily.
256
+ #
257
+ class CookieJar
258
+ include HostMixin
259
+
260
+ def initialize (jar_size)
261
+
262
+ @per_domain = LruHash.new jar_size
263
+ end
264
+
265
+ #
266
+ # Returns the count of cookies currently stored in this jar.
267
+ #
268
+ def size
269
+
270
+ @per_domain.keys.inject(0) { |i, d| i + @per_domain[d].size }
271
+ end
272
+
273
+ def add_cookie (domain, path, cookie)
274
+
275
+ (@per_domain[domain] ||= {})[CookieKey.new(path, cookie)] = cookie
276
+ end
277
+
278
+ def remove_cookie (domain, path, cookie)
279
+
280
+ (d = @per_domain[domain])
281
+ return unless d
282
+ d.delete CookieKey.new(path, cookie)
283
+ end
284
+
285
+ #
286
+ # Retrieves the cookies that matches the combination host/path.
287
+ # If the retrieved cookie is expired, will remove it from the jar
288
+ # and return nil.
289
+ #
290
+ def fetch_cookies (host, path)
291
+
292
+ c = do_fetch(@per_domain[host], path)
293
+
294
+ h, d = split_host host
295
+ c += do_fetch(@per_domain[d], path) if d
296
+
297
+ c
298
+ end
299
+
300
+ private
301
+
302
+ #
303
+ # Returns all the cookies that match a domain (host) and a path.
304
+ #
305
+ def do_fetch (dh, path)
306
+
307
+ return [] unless dh
308
+
309
+ keys = dh.keys.sort.find_all do |k|
310
+ path[0..k.path.length-1] == k.path
311
+ end
312
+ keys.inject([]) do |r, k|
313
+ r << dh[k]
314
+ r
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
320
+
@@ -0,0 +1,261 @@
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/21
33
+ #
34
+
35
+ require 'digest/md5'
36
+
37
+
38
+ module Rufus
39
+ module Verbs
40
+
41
+ #
42
+ # Specified by http://www.ietf.org/rfc/rfc2617.txt
43
+ # Inspired by http://segment7.net/projects/ruby/snippets/digest_auth.rb
44
+ #
45
+ # The EndPoint classes mixes this in to support digest authentication.
46
+ #
47
+ module DigestAuthMixin
48
+
49
+ #
50
+ # Makes sure digest_auth is on
51
+ #
52
+ def digest_auth (req, opts)
53
+
54
+ #return if no_digest_auth
55
+ # already done in add_authentication()
56
+
57
+ @cnonce ||= generate_cnonce
58
+ @nonce_count ||= 0
59
+
60
+ mention_digest_auth(req, opts) \
61
+ and return
62
+
63
+ mention_digest_auth(req, opts) \
64
+ if request_challenge(req, opts)
65
+ end
66
+
67
+ #
68
+ # Sets the 'Authorization' header with the appropriate info.
69
+ #
70
+ def mention_digest_auth (req, opts)
71
+
72
+ return false unless @challenge
73
+
74
+ req['Authorization'] = generate_header req, opts
75
+
76
+ true
77
+ end
78
+
79
+ #
80
+ # Interprets the information in the response's 'Authorization-Info'
81
+ # header.
82
+ #
83
+ def check_authentication_info (res, opts)
84
+
85
+ return if no_digest_auth
86
+ # not using digest authentication
87
+
88
+ return unless @challenge
89
+ # not yet authenticated
90
+
91
+ authinfo = AuthInfo.new res
92
+ @challenge.nonce = authinfo.nextnonce
93
+ end
94
+
95
+ protected
96
+
97
+ #
98
+ # Returns true if :digest_authentication is set at endpoint
99
+ # or request level.
100
+ #
101
+ def no_digest_auth
102
+
103
+ (not o(opts, :digest_authentication))
104
+ end
105
+
106
+ #
107
+ # To be enhanced.
108
+ #
109
+ # (For example http://www.intertwingly.net/blog/1585.html)
110
+ #
111
+ def generate_cnonce
112
+
113
+ Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535)))
114
+ end
115
+
116
+ def request_challenge (req, opts)
117
+
118
+ op = opts.dup
119
+
120
+ op[:digest_authentication] = false
121
+ # preventing an infinite loop
122
+
123
+ method = req.class.const_get(:METHOD).downcase.to_sym
124
+ #method = :get
125
+
126
+ res = request(method, op)
127
+
128
+ return false if res.code.to_i != 401
129
+
130
+ @challenge = Challenge.new res
131
+
132
+ true
133
+ end
134
+
135
+ #
136
+ # Generates an MD5 digest of the arguments (joined by ":").
137
+ #
138
+ def h (*args)
139
+
140
+ Digest::MD5.hexdigest(args.join(":"))
141
+ end
142
+
143
+ #
144
+ # Generates the Authentication header that will be returned
145
+ # to the server.
146
+ #
147
+ def generate_header (req, opts)
148
+
149
+ @nonce_count += 1
150
+
151
+ user, pass = o(opts, :digest_authentication)
152
+ realm = @challenge.realm || ""
153
+ method = req.class.const_get(:METHOD)
154
+ path = opts[:path]
155
+
156
+ a1 = if @challenge.algorithm == 'MD5-sess'
157
+ h(h(user, realm, pass), @challenge.nonce, @cnonce)
158
+ else
159
+ h(user, realm, pass)
160
+ end
161
+
162
+ a2, qop = if @challenge.qop.include?("auth-int")
163
+ [ h(method, path, h(req.body)), "auth-int" ]
164
+ else
165
+ [ h(method, path), "auth" ]
166
+ end
167
+
168
+ nc = ('%08x' % @nonce_count)
169
+
170
+ digest = h(
171
+ #a1, @challenge.nonce, nc, @cnonce, @challenge.qop, a2)
172
+ a1, @challenge.nonce, nc, @cnonce, "auth", a2)
173
+
174
+ header = ""
175
+ header << "Digest username=\"#{user}\", "
176
+ header << "realm=\"#{realm}\", "
177
+ header << "qop=\"#{qop}\", "
178
+ header << "uri=\"#{path}\", "
179
+ header << "nonce=\"#{@challenge.nonce}\", "
180
+ #header << "nc=##{nc}, "
181
+ header << "nc=#{nc}, "
182
+ header << "cnonce=\"#{@cnonce}\", "
183
+ header << "algorithm=\"#{@challenge.algorithm}\", "
184
+ #header << "algorithm=\"MD5-sess\", "
185
+ header << "response=\"#{digest}\", "
186
+ header << "opaque=\"#{@challenge.opaque}\""
187
+
188
+ header
189
+ end
190
+
191
+ #
192
+ # A common parent class for Challenge and AuthInfo.
193
+ # Their header parsing code is here.
194
+ #
195
+ class ServerReply
196
+
197
+ def initialize (res)
198
+
199
+ s = res[header_name]
200
+ return nil unless s
201
+
202
+ s = s[7..-1] if s[0, 6] == "Digest"
203
+
204
+ s = s.split ","
205
+
206
+ s.each do |e|
207
+
208
+ k, v = parse_entry e
209
+
210
+ if k == 'stale'
211
+ @stale = (v.downcase == 'true')
212
+ elsif k == 'nc'
213
+ @nc = v.to_i
214
+ elsif k == 'qop'
215
+ @qop = v.split ","
216
+ else
217
+ instance_variable_set "@#{k}".to_sym, v
218
+ end
219
+ end
220
+ end
221
+
222
+ protected
223
+
224
+ def parse_entry (e)
225
+
226
+ k, v = e.split "=", 2
227
+ v = v[1..-2] if v[0, 1] == '"'
228
+ [ k.strip, v.strip ]
229
+ end
230
+ end
231
+
232
+ #
233
+ # Used when parsing a 'www-authenticate' header challenge.
234
+ #
235
+ class Challenge < ServerReply
236
+
237
+ attr_accessor \
238
+ :opaque, :algorithm, :qop, :stale, :nonce, :realm, :charset
239
+
240
+ def header_name
241
+ 'www-authenticate'
242
+ end
243
+ end
244
+
245
+ #
246
+ # Used when parsing a 'authentication-info' header info.
247
+ #
248
+ class AuthInfo < ServerReply
249
+
250
+ attr_accessor \
251
+ :cnonce, :rspauth, :nextnonce, :qop, :nc
252
+
253
+ def header_name
254
+ 'authentication-info'
255
+ end
256
+ end
257
+ end
258
+
259
+ end
260
+ end
261
+
@@ -37,11 +37,14 @@ require 'yaml' # for StringIO (at least for now)
37
37
  require 'net/http'
38
38
  require 'zlib'
39
39
 
40
+ require 'rufus/verbs/version'
41
+ require 'rufus/verbs/cookies'
42
+ require 'rufus/verbs/digest'
43
+
40
44
 
41
45
  module Rufus
42
46
  module Verbs
43
47
 
44
- VERSION = "0.2"
45
48
  USER_AGENT = "Ruby rufus-verbs #{VERSION}"
46
49
 
47
50
  #
@@ -64,6 +67,9 @@ module Verbs
64
67
  #
65
68
  class EndPoint
66
69
 
70
+ include CookieMixin
71
+ include DigestAuthMixin
72
+
67
73
  #
68
74
  # The endpoint initialization opts (Hash instance)
69
75
  #
@@ -81,6 +87,8 @@ module Verbs
81
87
  @opts[:user_agent] ||= USER_AGENT
82
88
 
83
89
  @opts[:proxy] ||= ENV['HTTP_PROXY']
90
+
91
+ prepare_cookie_jar
84
92
  end
85
93
 
86
94
  def get (*args)
@@ -127,6 +135,8 @@ module Verbs
127
135
  #
128
136
  def request (method, args, &block)
129
137
 
138
+ # prepare request
139
+
130
140
  opts = EndPoint.extract_opts args
131
141
 
132
142
  compute_target opts
@@ -139,8 +149,14 @@ module Verbs
139
149
 
140
150
  add_conditional_headers(req, opts) if method == :get
141
151
 
152
+ mention_cookies(req, opts)
153
+ # if the :cookies option is disabled (the default)
154
+ # will have no effect
155
+
142
156
  return req if o(opts, :dry_run) == true
143
157
 
158
+ # trigger request
159
+
144
160
  http = prepare_http opts
145
161
 
146
162
  res = nil
@@ -149,8 +165,18 @@ module Verbs
149
165
  res = http.request req
150
166
  end
151
167
 
168
+ # handle response
169
+
170
+ register_cookies res, opts
171
+ # if the :cookies option is disabled (the default)
172
+ # will have no effect
173
+
152
174
  return res if o(opts, :raw_response)
153
175
 
176
+ check_authentication_info res, opts
177
+ # used in case of :digest_authentication
178
+ # will have no effect else
179
+
154
180
  res = handle_response method, res, opts
155
181
 
156
182
  return res.body if o(opts, :body)
@@ -187,8 +213,8 @@ module Verbs
187
213
  def o (opts, key)
188
214
 
189
215
  keys = Array key
190
- keys.each { |k| (v = opts[k] and return v) }
191
- keys.each { |k| (v = @opts[k] and return v) }
216
+ keys.each { |k| (v = opts[k]; return v if v != nil) }
217
+ keys.each { |k| (v = @opts[k]; return v if v != nil) }
192
218
  nil
193
219
  end
194
220
 
@@ -284,15 +310,21 @@ module Verbs
284
310
  #
285
311
  def add_authentication (req, opts)
286
312
 
287
- a = opts[:http_basic_authentication]
313
+ b = o(opts, :http_basic_authentication)
314
+ d = o(opts, :digest_authentication)
315
+ o = o(opts, :auth)
316
+
317
+ if b and b != false
318
+
319
+ req.basic_auth b[0], b[1]
288
320
 
289
- if a
321
+ elsif d and d != false
290
322
 
291
- req.basic_auth a[0], a[1]
323
+ digest_auth req, opts
292
324
 
293
- elsif opts[:auth]
325
+ elsif o and o != false
294
326
 
295
- opts[:auth].call req
327
+ o.call req
296
328
  end
297
329
  end
298
330
 
@@ -0,0 +1,45 @@
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/21
33
+ #
34
+
35
+
36
+ module Rufus
37
+ module Verbs
38
+
39
+ #
40
+ # The version of this rufus-verbs [gem]
41
+ #
42
+ VERSION = '0.3'
43
+ end
44
+ end
45
+
@@ -13,7 +13,7 @@ require 'testbase'
13
13
  require 'rufus/verbs'
14
14
 
15
15
 
16
- class AuthTest < Test::Unit::TestCase
16
+ class Auth0Test < Test::Unit::TestCase
17
17
  include TestBaseMixin
18
18
 
19
19
  include Rufus::Verbs
@@ -23,7 +23,7 @@ class AuthTest < Test::Unit::TestCase
23
23
  #
24
24
  def setup
25
25
 
26
- @server = ItemServer.new :auth => true
26
+ @server = ItemServer.new :auth => :basic
27
27
  @server.start
28
28
  end
29
29
 
@@ -0,0 +1,68 @@
1
+
2
+ #
3
+ # Testing rufus-verbs
4
+ #
5
+ # jmettraux@gmail.com
6
+ #
7
+ # Sun Jan 13 12:33:03 JST 2008
8
+ #
9
+
10
+ require 'test/unit'
11
+ require 'testbase'
12
+
13
+ require 'rufus/verbs'
14
+
15
+
16
+ class Auth1Test < Test::Unit::TestCase
17
+ include TestBaseMixin
18
+
19
+ include Rufus::Verbs
20
+
21
+ #
22
+ # Using an items server with the authentication on.
23
+ #
24
+ def setup
25
+
26
+ @server = ItemServer.new :auth => :digest
27
+ @server.start
28
+ end
29
+
30
+
31
+ def test_0
32
+
33
+ #res = get :uri => "http://localhost:7777/items"
34
+ #assert_equal 200, res.code.to_i
35
+ #assert_equal "{}", res.body.strip
36
+ #res = expect 401, nil, get(:uri => "http://localhost:7777/items")
37
+ #p res['www-authenticate']
38
+
39
+ #$DEBUG = true
40
+
41
+ ep = EndPoint.new :digest_authentication => [ "test", "pass" ]
42
+
43
+ expect 200, {}, ep.get("http://localhost:7777/items")
44
+ assert_equal 2, $dcount
45
+
46
+ expect 200, {}, ep.get("http://localhost:7777/items")
47
+ assert_equal 3, $dcount
48
+
49
+ expect 201, nil, ep.post("http://localhost:7777/items/1") { "hammer" }
50
+ assert_equal 4, $dcount
51
+
52
+ expect 200, { 1 => "hammer" }, ep.get("http://localhost:7777/items")
53
+ assert_equal 5, $dcount
54
+
55
+ expect 401, nil, get(:uri => "http://localhost:7777/items")
56
+ assert_equal 6, $dcount
57
+
58
+ expect 401, nil, get(
59
+ :uri => "http://localhost:7777/items",
60
+ :http_basic_authentication => [ "toto", "toto" ])
61
+ assert_equal 7, $dcount
62
+
63
+ expect 200, { 1 => "hammer" }, get(
64
+ :uri => "http://localhost:7777/items",
65
+ :digest_authentication => [ "test", "pass" ])
66
+ assert_equal 9, $dcount
67
+ end
68
+ end
@@ -0,0 +1,122 @@
1
+
2
+ #
3
+ # Testing rufus-verbs
4
+ #
5
+ # jmettraux@gmail.com
6
+ #
7
+ # Sat Jan 19 18:22:48 JST 2008
8
+ #
9
+
10
+ require 'test/unit'
11
+ #require 'testbase'
12
+
13
+ require 'rufus/verbs'
14
+ require 'rufus/verbs/cookies'
15
+
16
+
17
+ class Cookie0Test < Test::Unit::TestCase
18
+ #include TestBaseMixin
19
+
20
+ include Rufus::Verbs::CookieMixin
21
+ include Rufus::Verbs::HostMixin
22
+
23
+ #
24
+ # testing split_host(s)
25
+ #
26
+ def test_0
27
+
28
+ assert_equal [ 'localhost', nil ], split_host('localhost')
29
+ assert_equal [ 'benz', '.car.co.nz' ], split_host('benz.car.co.nz')
30
+ assert_equal [ '127.0.0.1', nil ], split_host('127.0.0.1')
31
+ assert_equal [ '::1', nil ], split_host('::1')
32
+ end
33
+
34
+ #
35
+ # testing the CookieJar
36
+ #
37
+ def test_1
38
+
39
+ cookie0 = TestCookie.new
40
+ cookie1 = TestCookie.new
41
+
42
+ jar = Rufus::Verbs::CookieJar.new 77
43
+ assert_equal 0, jar.size
44
+
45
+ jar.add_cookie(".rubyforge.org", "/", cookie0)
46
+ assert_equal 1, jar.size
47
+ assert_equal [ cookie0 ], jar.fetch_cookies("rufus.rubyforge.org", "/main")
48
+
49
+ jar.add_cookie("rufus.rubyforge.org", "/sub", cookie1)
50
+ assert_equal 2, jar.size
51
+ assert_equal [ cookie1, cookie0 ], jar.fetch_cookies("rufus.rubyforge.org", "/sub/0")
52
+ assert_equal [ cookie0 ], jar.fetch_cookies("rufus.rubyforge.org", "/main")
53
+ assert_equal [ cookie0 ], jar.fetch_cookies("rufus.rubyforge.org", "/")
54
+
55
+ jar.remove_cookie("rufus.rubyforge.org", "/sub", cookie1)
56
+ assert_equal 1, jar.size
57
+ end
58
+
59
+ #
60
+ # testing cookie_acceptable?(opts, cookie)
61
+ #
62
+ def test_2
63
+
64
+ jar = Rufus::Verbs::CookieJar.new 77
65
+
66
+ opts = { :host => 'rufus.rubyforge.org', :path => '/' }
67
+ c = TestCookie.new '.rubyforge.org', '/'
68
+ assert cookie_acceptable?(opts, c)
69
+
70
+ # * The value for the Domain attribute contains no embedded dots
71
+ # or does not start with a dot.
72
+
73
+ opts = { :host => 'rufus.rubyforge.org', :path => '/' }
74
+ c = TestCookie.new 'rufus.rubyforge.org', '/'
75
+ assert ! cookie_acceptable?(opts, c)
76
+
77
+ opts = { :host => 'rufus.rubyforge.org', :path => '/' }
78
+ c = TestCookie.new 'org', '/'
79
+ assert ! cookie_acceptable?(opts, c)
80
+
81
+ # * The value for the Path attribute is not a prefix of the
82
+ # request-URI.
83
+
84
+ opts = { :host => 'rufus.rubyforge.org', :path => '/this' }
85
+ c = TestCookie.new '.rubyforge.org', '/that'
86
+ assert ! cookie_acceptable?(opts, c)
87
+
88
+ # * The value for the request-host does not domain-match the
89
+ # Domain attribute.
90
+
91
+ opts = { :host => 'rufus.rubyforg.org', :path => '/' }
92
+ c = TestCookie.new '.rubyforge.org', '/'
93
+ assert ! cookie_acceptable?(opts, c)
94
+
95
+ # * The request-host is a FQDN (not IP address) and has the form
96
+ # HD, where D is the value of the Domain attribute, and H is a
97
+ # string that contains one or more dots.
98
+
99
+ # implicit...
100
+ end
101
+
102
+ #def test_webrick_cookie
103
+ # require 'webrick/cookie'
104
+ # cookie = "PREF=ID=18da97219de4985:TM=12007507:LM=12007507:S=Guc1JcA15ySZYl2n; expires=Mon, 18-Jan-2010 09:30:37 GMT; path=/; domain=.google.com"
105
+ # p WEBrick::Cookie.parse_set_cookie(cookie)
106
+ # p Rufus::Verbs::Cookie.parse_set_cookie(cookie)
107
+ #end
108
+
109
+ protected
110
+
111
+ class TestCookie
112
+
113
+ attr_reader :domain, :path, :name
114
+
115
+ def initialize (domain=nil, path=nil, name='whatever')
116
+
117
+ @domain = domain
118
+ @path = path
119
+ @name = name
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,60 @@
1
+
2
+ #
3
+ # Testing rufus-verbs
4
+ #
5
+ # jmettraux@gmail.com
6
+ #
7
+ # Sun Jan 13 12:33:03 JST 2008
8
+ #
9
+
10
+ require 'test/unit'
11
+ require 'testbase'
12
+
13
+ require 'rufus/verbs'
14
+
15
+
16
+ class Cookie1Test < Test::Unit::TestCase
17
+ include TestBaseMixin
18
+
19
+ include Rufus::Verbs
20
+
21
+
22
+ def test_0
23
+
24
+ ep = EndPoint.new :cookies => true
25
+ class << ep
26
+ attr_reader :cookies
27
+ end
28
+
29
+ assert_equal 0, ep.cookies.size
30
+
31
+ ep.get :uri => "http://localhost:7777/cookie"
32
+ assert_equal 1, ep.cookies.size
33
+
34
+ req = ep.get :uri => "http://localhost:7777/cookie", :dry_run => true
35
+ assert_match /^tcookie=\d*$/, req['Cookie']
36
+ end
37
+
38
+ def test_1
39
+
40
+ ep0 = EndPoint.new :cookies => true
41
+ ep1 = EndPoint.new :cookies => true
42
+
43
+ ep0.post("http://localhost:7777/cookie") { "smurf0" }
44
+ ep1.post("http://localhost:7777/cookie") { "smurf1" }
45
+
46
+ expect 200, [ 'smurf0' ], ep0.get("http://localhost:7777/cookie")
47
+ expect 200, [ 'smurf1' ], ep1.get("http://localhost:7777/cookie")
48
+ end
49
+
50
+ def test_2
51
+
52
+ ep = EndPoint.new :cookies => false # explicitely
53
+
54
+ res0 = ep.post("http://localhost:7777/cookie") { "smurf0" }
55
+ res1 = expect 200, [], ep.get("http://localhost:7777/cookie")
56
+
57
+ assert_not_equal res0['Set-Cookie'], res1['Set-Cookie']
58
+ end
59
+ end
60
+
@@ -11,6 +11,12 @@ require 'date'
11
11
  require 'webrick'
12
12
 
13
13
 
14
+ $dcount = 0 # tracking the number of hits when doing digest auth
15
+
16
+
17
+ #
18
+ # the hash for the /items resource (collection)
19
+ #
14
20
  class LastModifiedHash
15
21
 
16
22
  def initialize
@@ -52,8 +58,12 @@ end
52
58
  class ItemServlet < WEBrick::HTTPServlet::AbstractServlet
53
59
 
54
60
  @@items = {}
61
+
55
62
  @@lastmod = LastModifiedHash.new
56
63
 
64
+ @@authenticator = WEBrick::HTTPAuth::DigestAuth.new(
65
+ :UserDB => WEBrick::HTTPAuth::Htdigest.new('test/test.htdigest'),
66
+ :Realm => 'test_realm')
57
67
 
58
68
  def initialize (server, *options)
59
69
 
@@ -66,9 +76,17 @@ class ItemServlet < WEBrick::HTTPServlet::AbstractServlet
66
76
  #
67
77
  def service (req, res)
68
78
 
69
- WEBrick::HTTPAuth.basic_auth(req, res, "items") do |u, p|
70
- (u != nil and u == p)
71
- end if @auth
79
+ if @auth == :basic
80
+
81
+ WEBrick::HTTPAuth.basic_auth(req, res, "items") do |u, p|
82
+ (u != nil and u == p)
83
+ end
84
+
85
+ elsif @auth == :digest
86
+
87
+ $dcount += 1
88
+ @@authenticator.authenticate(req, res)
89
+ end
72
90
 
73
91
  super
74
92
  end
@@ -166,9 +184,9 @@ class ItemServlet < WEBrick::HTTPServlet::AbstractServlet
166
184
 
167
185
  return true unless since or match
168
186
 
169
- puts
170
- p [ since, match ]
171
- puts
187
+ #puts
188
+ #p [ since, match ]
189
+ #puts
172
190
 
173
191
  (since or match)
174
192
  end
@@ -217,6 +235,42 @@ class ThingServlet < WEBrick::HTTPServlet::AbstractServlet
217
235
  end
218
236
  end
219
237
 
238
+ #
239
+ # testing Rufus::Verbs cookies...
240
+ #
241
+ class CookieServlet < WEBrick::HTTPServlet::AbstractServlet
242
+
243
+ @@sessions = {}
244
+
245
+ def do_GET (req, res)
246
+
247
+ res.body = get_session(req, res).inspect
248
+ end
249
+
250
+ def do_POST (req, res)
251
+
252
+ get_session(req, res) << req.body.strip
253
+ res.body = "ok."
254
+ end
255
+
256
+ protected
257
+
258
+ def get_session (req, res)
259
+
260
+ c = req.cookies.find { |c| c.name == 'tcookie' }
261
+
262
+ if c
263
+ @@sessions[c.value]
264
+ else
265
+ s = []
266
+ key = (Time.now.to_f * 100000).to_i.to_s
267
+ @@sessions[key] = s
268
+ res.cookies << WEBrick::Cookie.new('tcookie', key)
269
+ s
270
+ end
271
+ end
272
+ end
273
+
220
274
  #
221
275
  # Serving items, a dummy resource...
222
276
  # Also serving things, which just redirect to items...
@@ -241,6 +295,7 @@ class ItemServer
241
295
 
242
296
  @server.mount "/items", ItemServlet
243
297
  @server.mount "/things", ThingServlet
298
+ @server.mount "/cookie", CookieServlet
244
299
 
245
300
  [ 'INT', 'TERM' ].each do |signal|
246
301
  trap(signal) { shutdown }
@@ -0,0 +1 @@
1
+ test:test_realm:fbab5e064c6fa79c414735bcdcf2ce4a
@@ -1,12 +1,18 @@
1
1
 
2
2
  require 'dryrun_test'
3
3
  require 'iconditional_test'
4
+
4
5
  require 'simple_test'
5
- require 'auth_test'
6
+
7
+ require 'auth0_test'
8
+ require 'auth1_test'
9
+
6
10
  require 'redir_test'
7
11
  require 'block_test'
8
12
  require 'https_test'
9
13
  require 'proxy_test'
10
14
 
11
15
  require 'conditional_test'
16
+ require 'cookie0_test'
17
+ require 'cookie1_test'
12
18
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rufus-verbs
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.2"
4
+ version: "0.3"
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Mettraux
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-01-18 00:00:00 +09:00
12
+ date: 2008-01-23 00:00:00 +09:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -25,11 +25,17 @@ files:
25
25
  - lib/rufus
26
26
  - lib/rufus/verbs
27
27
  - lib/rufus/verbs/conditional.rb
28
+ - lib/rufus/verbs/cookies.rb
29
+ - lib/rufus/verbs/digest.rb
28
30
  - lib/rufus/verbs/endpoint.rb
31
+ - lib/rufus/verbs/version.rb
29
32
  - lib/rufus/verbs.rb
30
- - test/auth_test.rb
33
+ - test/auth0_test.rb
34
+ - test/auth1_test.rb
31
35
  - test/block_test.rb
32
36
  - test/conditional_test.rb
37
+ - test/cookie0_test.rb
38
+ - test/cookie1_test.rb
33
39
  - test/dryrun_test.rb
34
40
  - test/https_test.rb
35
41
  - test/iconditional_test.rb
@@ -37,6 +43,7 @@ files:
37
43
  - test/proxy_test.rb
38
44
  - test/redir_test.rb
39
45
  - test/simple_test.rb
46
+ - test/test.htdigest
40
47
  - test/test.rb
41
48
  - test/testbase.rb
42
49
  - README.txt