source: extensions/gsdl-video/trunk/installed/cmdline/lib/ruby/1.8/webrick/httpauth/digestauth.rb@ 18425

Last change on this file since 18425 was 18425, checked in by davidb, 15 years ago

Video extension to Greenstone

File size: 11.2 KB
Line 
1#
2# httpauth/digestauth.rb -- HTTP digest access authentication
3#
4# Author: IPR -- Internet Programming with Ruby -- writers
5# Copyright (c) 2003 Internet Programming with Ruby writers.
6# Copyright (c) 2003 H.M.
7#
8# The original implementation is provided by H.M.
9# URL: http://rwiki.jin.gr.jp/cgi-bin/rw-cgi.rb?cmd=view;name=
10# %C7%A7%BE%DA%B5%A1%C7%BD%A4%F2%B2%FE%C2%A4%A4%B7%A4%C6%A4%DF%A4%EB
11#
12# $IPR: digestauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $
13
14require 'webrick/config'
15require 'webrick/httpstatus'
16require 'webrick/httpauth/authenticator'
17require 'digest/md5'
18require 'digest/sha1'
19
20module WEBrick
21 module HTTPAuth
22 class DigestAuth
23 include Authenticator
24
25 AuthScheme = "Digest"
26 OpaqueInfo = Struct.new(:time, :nonce, :nc)
27 attr_reader :algorithm, :qop
28
29 def self.make_passwd(realm, user, pass)
30 pass ||= ""
31 Digest::MD5::hexdigest([user, realm, pass].join(":"))
32 end
33
34 def initialize(config, default=Config::DigestAuth)
35 check_init(config)
36 @config = default.dup.update(config)
37 @algorithm = @config[:Algorithm]
38 @domain = @config[:Domain]
39 @qop = @config[:Qop]
40 @use_opaque = @config[:UseOpaque]
41 @use_next_nonce = @config[:UseNextNonce]
42 @check_nc = @config[:CheckNc]
43 @use_auth_info_header = @config[:UseAuthenticationInfoHeader]
44 @nonce_expire_period = @config[:NonceExpirePeriod]
45 @nonce_expire_delta = @config[:NonceExpireDelta]
46 @internet_explorer_hack = @config[:InternetExplorerHack]
47 @opera_hack = @config[:OperaHack]
48
49 case @algorithm
50 when 'MD5','MD5-sess'
51 @h = Digest::MD5
52 when 'SHA1','SHA1-sess' # it is a bonus feature :-)
53 @h = Digest::SHA1
54 else
55 msg = format('Alogrithm "%s" is not supported.', @algorithm)
56 raise ArgumentError.new(msg)
57 end
58
59 @instance_key = hexdigest(self.__id__, Time.now.to_i, Process.pid)
60 @opaques = {}
61 @last_nonce_expire = Time.now
62 @mutex = Mutex.new
63 end
64
65 def authenticate(req, res)
66 unless result = @mutex.synchronize{ _authenticate(req, res) }
67 challenge(req, res)
68 end
69 if result == :nonce_is_stale
70 challenge(req, res, true)
71 end
72 return true
73 end
74
75 def challenge(req, res, stale=false)
76 nonce = generate_next_nonce(req)
77 if @use_opaque
78 opaque = generate_opaque(req)
79 @opaques[opaque].nonce = nonce
80 end
81
82 param = Hash.new
83 param["realm"] = HTTPUtils::quote(@realm)
84 param["domain"] = HTTPUtils::quote(@domain.to_a.join(" ")) if @domain
85 param["nonce"] = HTTPUtils::quote(nonce)
86 param["opaque"] = HTTPUtils::quote(opaque) if opaque
87 param["stale"] = stale.to_s
88 param["algorithm"] = @algorithm
89 param["qop"] = HTTPUtils::quote(@qop.to_a.join(",")) if @qop
90
91 res[@response_field] =
92 "#{@auth_scheme} " + param.map{|k,v| "#{k}=#{v}" }.join(", ")
93 info("%s: %s", @response_field, res[@response_field]) if $DEBUG
94 raise @auth_exception
95 end
96
97 private
98
99 MustParams = ['username','realm','nonce','uri','response']
100 MustParamsAuth = ['cnonce','nc']
101
102 def _authenticate(req, res)
103 unless digest_credentials = check_scheme(req)
104 return false
105 end
106
107 auth_req = split_param_value(digest_credentials)
108 if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
109 req_params = MustParams + MustParamsAuth
110 else
111 req_params = MustParams
112 end
113 req_params.each{|key|
114 unless auth_req.has_key?(key)
115 error('%s: parameter missing. "%s"', auth_req['username'], key)
116 raise HTTPStatus::BadRequest
117 end
118 }
119
120 if !check_uri(req, auth_req)
121 raise HTTPStatus::BadRequest
122 end
123
124 if auth_req['realm'] != @realm
125 error('%s: realm unmatch. "%s" for "%s"',
126 auth_req['username'], auth_req['realm'], @realm)
127 return false
128 end
129
130 auth_req['algorithm'] ||= 'MD5'
131 if auth_req['algorithm'] != @algorithm &&
132 (@opera_hack && auth_req['algorithm'] != @algorithm.upcase)
133 error('%s: algorithm unmatch. "%s" for "%s"',
134 auth_req['username'], auth_req['algorithm'], @algorithm)
135 return false
136 end
137
138 if (@qop.nil? && auth_req.has_key?('qop')) ||
139 (@qop && (! @qop.member?(auth_req['qop'])))
140 error('%s: the qop is not allowed. "%s"',
141 auth_req['username'], auth_req['qop'])
142 return false
143 end
144
145 password = @userdb.get_passwd(@realm, auth_req['username'], @reload_db)
146 unless password
147 error('%s: the user is not allowd.', auth_req['username'])
148 return false
149 end
150
151 nonce_is_invalid = false
152 if @use_opaque
153 info("@opaque = %s", @opaque.inspect) if $DEBUG
154 if !(opaque = auth_req['opaque'])
155 error('%s: opaque is not given.', auth_req['username'])
156 nonce_is_invalid = true
157 elsif !(opaque_struct = @opaques[opaque])
158 error('%s: invalid opaque is given.', auth_req['username'])
159 nonce_is_invalid = true
160 elsif !check_opaque(opaque_struct, req, auth_req)
161 @opaques.delete(auth_req['opaque'])
162 nonce_is_invalid = true
163 end
164 elsif !check_nonce(req, auth_req)
165 nonce_is_invalid = true
166 end
167
168 if /-sess$/ =~ auth_req['algorithm'] ||
169 (@opera_hack && /-SESS$/ =~ auth_req['algorithm'])
170 ha1 = hexdigest(password, auth_req['nonce'], auth_req['cnonce'])
171 else
172 ha1 = password
173 end
174
175 if auth_req['qop'] == "auth" || auth_req['qop'] == nil
176 ha2 = hexdigest(req.request_method, auth_req['uri'])
177 ha2_res = hexdigest("", auth_req['uri'])
178 elsif auth_req['qop'] == "auth-int"
179 ha2 = hexdigest(req.request_method, auth_req['uri'],
180 hexdigest(req.body))
181 ha2_res = hexdigest("", auth_req['uri'], hexdigest(res.body))
182 end
183
184 if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
185 param2 = ['nonce', 'nc', 'cnonce', 'qop'].map{|key|
186 auth_req[key]
187 }.join(':')
188 digest = hexdigest(ha1, param2, ha2)
189 digest_res = hexdigest(ha1, param2, ha2_res)
190 else
191 digest = hexdigest(ha1, auth_req['nonce'], ha2)
192 digest_res = hexdigest(ha1, auth_req['nonce'], ha2_res)
193 end
194
195 if digest != auth_req['response']
196 error("%s: digest unmatch.", auth_req['username'])
197 return false
198 elsif nonce_is_invalid
199 error('%s: digest is valid, but nonce is not valid.',
200 auth_req['username'])
201 return :nonce_is_stale
202 elsif @use_auth_info_header
203 auth_info = {
204 'nextnonce' => generate_next_nonce(req),
205 'rspauth' => digest_res
206 }
207 if @use_opaque
208 opaque_struct.time = req.request_time
209 opaque_struct.nonce = auth_info['nextnonce']
210 opaque_struct.nc = "%08x" % (auth_req['nc'].hex + 1)
211 end
212 if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
213 ['qop','cnonce','nc'].each{|key|
214 auth_info[key] = auth_req[key]
215 }
216 end
217 res[@resp_info_field] = auth_info.keys.map{|key|
218 if key == 'nc'
219 key + '=' + auth_info[key]
220 else
221 key + "=" + HTTPUtils::quote(auth_info[key])
222 end
223 }.join(', ')
224 end
225 info('%s: authentication scceeded.', auth_req['username'])
226 req.user = auth_req['username']
227 return true
228 end
229
230 def split_param_value(string)
231 ret = {}
232 while string.size != 0
233 case string
234 when /^\s*([\w\-\.\*\%\!]+)=\s*\"((\\.|[^\"])*)\"\s*,?/
235 key = $1
236 matched = $2
237 string = $'
238 ret[key] = matched.gsub(/\\(.)/, "\\1")
239 when /^\s*([\w\-\.\*\%\!]+)=\s*([^,\"]*),?/
240 key = $1
241 matched = $2
242 string = $'
243 ret[key] = matched.clone
244 when /^s*^,/
245 string = $'
246 else
247 break
248 end
249 end
250 ret
251 end
252
253 def generate_next_nonce(req)
254 now = "%012d" % req.request_time.to_i
255 pk = hexdigest(now, @instance_key)[0,32]
256 nonce = [now + ":" + pk].pack("m*").chop # it has 60 length of chars.
257 nonce
258 end
259
260 def check_nonce(req, auth_req)
261 username = auth_req['username']
262 nonce = auth_req['nonce']
263
264 pub_time, pk = nonce.unpack("m*")[0].split(":", 2)
265 if (!pub_time || !pk)
266 error("%s: empty nonce is given", username)
267 return false
268 elsif (hexdigest(pub_time, @instance_key)[0,32] != pk)
269 error("%s: invalid private-key: %s for %s",
270 username, hexdigest(pub_time, @instance_key)[0,32], pk)
271 return false
272 end
273
274 diff_time = req.request_time.to_i - pub_time.to_i
275 if (diff_time < 0)
276 error("%s: difference of time-stamp is negative.", username)
277 return false
278 elsif diff_time > @nonce_expire_period
279 error("%s: nonce is expired.", username)
280 return false
281 end
282
283 return true
284 end
285
286 def generate_opaque(req)
287 @mutex.synchronize{
288 now = req.request_time
289 if now - @last_nonce_expire > @nonce_expire_delta
290 @opaques.delete_if{|key,val|
291 (now - val.time) > @nonce_expire_period
292 }
293 @last_nonce_expire = now
294 end
295 begin
296 opaque = Utils::random_string(16)
297 end while @opaques[opaque]
298 @opaques[opaque] = OpaqueInfo.new(now, nil, '00000001')
299 opaque
300 }
301 end
302
303 def check_opaque(opaque_struct, req, auth_req)
304 if (@use_next_nonce && auth_req['nonce'] != opaque_struct.nonce)
305 error('%s: nonce unmatched. "%s" for "%s"',
306 auth_req['username'], auth_req['nonce'], opaque_struct.nonce)
307 return false
308 elsif !check_nonce(req, auth_req)
309 return false
310 end
311 if (@check_nc && auth_req['nc'] != opaque_struct.nc)
312 error('%s: nc unmatched."%s" for "%s"',
313 auth_req['username'], auth_req['nc'], opaque_struct.nc)
314 return false
315 end
316 true
317 end
318
319 def check_uri(req, auth_req)
320 uri = auth_req['uri']
321 if uri != req.request_uri.to_s && uri != req.unparsed_uri &&
322 (@internet_explorer_hack && uri != req.path)
323 error('%s: uri unmatch. "%s" for "%s"', auth_req['username'],
324 auth_req['uri'], req.request_uri.to_s)
325 return false
326 end
327 true
328 end
329
330 def hexdigest(*args)
331 @h.hexdigest(args.join(":"))
332 end
333 end
334
335 class ProxyDigestAuth < DigestAuth
336 include ProxyAuthenticator
337
338 def check_uri(req, auth_req)
339 return true
340 end
341 end
342 end
343end
Note: See TracBrowser for help on using the repository browser.