| Class | WEBrick::HTTPAuth::DigestAuth |
| In: |
lib/webrick/httpauth/digestauth.rb
|
| Parent: | Object |
| AuthScheme | = | "Digest" |
| OpaqueInfo | = | Struct.new(:time, :nonce, :nc) |
| MustParams | = | ['username','realm','nonce','uri','response'] |
| MustParamsAuth | = | ['cnonce','nc'] |
| algorithm | [R] | |
| qop | [R] |
# File lib/webrick/httpauth/digestauth.rb, line 29
29: def self.make_passwd(realm, user, pass)
30: pass ||= ""
31: Digest::MD5::hexdigest([user, realm, pass].join(":"))
32: end
# File lib/webrick/httpauth/digestauth.rb, line 34
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
# File lib/webrick/httpauth/digestauth.rb, line 65
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
# File lib/webrick/httpauth/digestauth.rb, line 75
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
# File lib/webrick/httpauth/digestauth.rb, line 102
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
# File lib/webrick/httpauth/digestauth.rb, line 260
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
# File lib/webrick/httpauth/digestauth.rb, line 303
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
# File lib/webrick/httpauth/digestauth.rb, line 319
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
# File lib/webrick/httpauth/digestauth.rb, line 253
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
# File lib/webrick/httpauth/digestauth.rb, line 286
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
# File lib/webrick/httpauth/digestauth.rb, line 330
330: def hexdigest(*args)
331: @h.hexdigest(args.join(":"))
332: end
# File lib/webrick/httpauth/digestauth.rb, line 230
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