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