diff options
Diffstat (limited to 'lib/webrick/httpauth/digestauth.rb')
-rw-r--r-- | lib/webrick/httpauth/digestauth.rb | 348 |
1 files changed, 348 insertions, 0 deletions
diff --git a/lib/webrick/httpauth/digestauth.rb b/lib/webrick/httpauth/digestauth.rb new file mode 100644 index 000000000..2b0660ed2 --- /dev/null +++ b/lib/webrick/httpauth/digestauth.rb @@ -0,0 +1,348 @@ +# +# httpauth/digestauth.rb -- HTTP digest access authentication +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. +# Copyright (c) 2003 H.M. +# +# The original implementation is provided by H.M. +# URL: http://rwiki.jin.gr.jp/cgi-bin/rw-cgi.rb?cmd=view;name= +# %C7%A7%BE%DA%B5%A1%C7%BD%A4%F2%B2%FE%C2%A4%A4%B7%A4%C6%A4%DF%A4%EB +# +# $IPR: digestauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $ + +require 'webrick/config' +require 'webrick/httpstatus' +require 'webrick/httpauth/authenticator' +require 'digest/md5' +require 'digest/sha1' +require 'base64' + +module WEBrick + module HTTPAuth + class DigestAuth + include Authenticator + + AuthScheme = "Digest" + OpaqueInfo = Struct.new(:time, :nonce, :nc) + attr_reader :algorithm, :qop + + def self.make_passwd(realm, user, pass) + pass ||= "" + Digest::MD5::hexdigest([user, realm, pass].join(":")) + end + + def initialize(config, default=Config::DigestAuth) + check_init(config) + @config = default.dup.update(config) + @algorithm = @config[:Algorithm] + @domain = @config[:Domain] + @qop = @config[:Qop] + @use_opaque = @config[:UseOpaque] + @use_next_nonce = @config[:UseNextNonce] + @check_nc = @config[:CheckNc] + @use_auth_info_header = @config[:UseAuthenticationInfoHeader] + @nonce_expire_period = @config[:NonceExpirePeriod] + @nonce_expire_delta = @config[:NonceExpireDelta] + @internet_explorer_hack = @config[:InternetExplorerHack] + @opera_hack = @config[:OperaHack] + + case @algorithm + when 'MD5','MD5-sess' + @h = Digest::MD5 + when 'SHA1','SHA1-sess' # it is a bonus feature :-) + @h = Digest::SHA1 + else + msg = format('Alogrithm "%s" is not supported.', @algorithm) + raise ArgumentError.new(msg) + end + + @instance_key = hexdigest(self.__id__, Time.now.to_i, Process.pid) + @opaques = {} + @last_nonce_expire = Time.now + @mutex = Mutex.new + end + + def authenticate(req, res) + unless result = @mutex.synchronize{ _authenticate(req, res) } + challenge(req, res) + end + if result == :nonce_is_stale + challenge(req, res, true) + end + return true + end + + def challenge(req, res, stale=false) + nonce = generate_next_nonce(req) + if @use_opaque + opaque = generate_opaque(req) + @opaques[opaque].nonce = nonce + end + + param = Hash.new + param["realm"] = HTTPUtils::quote(@realm) + param["domain"] = HTTPUtils::quote(@domain.to_a.join(" ")) if @domain + param["nonce"] = HTTPUtils::quote(nonce) + param["opaque"] = HTTPUtils::quote(opaque) if opaque + param["stale"] = stale.to_s + param["algorithm"] = @algorithm + param["qop"] = HTTPUtils::quote(@qop.to_a.join(",")) if @qop + + res[@response_field] = + "#{@auth_scheme} " + param.map{|k,v| "#{k}=#{v}" }.join(", ") + info("%s: %s", @response_field, res[@response_field]) if $DEBUG + raise @auth_exception + end + + private + + MustParams = ['username','realm','nonce','uri','response'] + MustParamsAuth = ['cnonce','nc'] + + def _authenticate(req, res) + unless digest_credentials = check_scheme(req) + return false + end + + auth_req = split_param_value(digest_credentials) + if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int" + req_params = MustParams + MustParamsAuth + else + req_params = MustParams + end + req_params.each{|key| + unless auth_req.has_key?(key) + error('%s: parameter missing. "%s"', auth_req['username'], key) + raise HTTPStatus::BadRequest + end + } + + if !check_uri(req, auth_req) + raise HTTPStatus::BadRequest + end + + if auth_req['realm'] != @realm + error('%s: realm unmatch. "%s" for "%s"', + auth_req['username'], auth_req['realm'], @realm) + return false + end + + auth_req['algorithm'] ||= 'MD5' + if auth_req['algorithm'] != @algorithm && + (@opera_hack && auth_req['algorithm'] != @algorithm.upcase) + error('%s: algorithm unmatch. "%s" for "%s"', + auth_req['username'], auth_req['algorithm'], @algorithm) + return false + end + + if (@qop.nil? && auth_req.has_key?('qop')) || + (@qop && (! @qop.member?(auth_req['qop']))) + error('%s: the qop is not allowed. "%s"', + auth_req['username'], auth_req['qop']) + return false + end + + password = @userdb.get_passwd(@realm, auth_req['username'], @reload_db) + unless password + error('%s: the user is not allowd.', auth_req['username']) + return false + end + + nonce_is_invalid = false + if @use_opaque + info("@opaque = %s", @opaque.inspect) if $DEBUG + if !(opaque = auth_req['opaque']) + error('%s: opaque is not given.', auth_req['username']) + nonce_is_invalid = true + elsif !(opaque_struct = @opaques[opaque]) + error('%s: invalid opaque is given.', auth_req['username']) + nonce_is_invalid = true + elsif !check_opaque(opaque_struct, req, auth_req) + @opaques.delete(auth_req['opaque']) + nonce_is_invalid = true + end + elsif !check_nonce(req, auth_req) + nonce_is_invalid = true + end + + if /-sess$/ =~ auth_req['algorithm'] || + (@opera_hack && /-SESS$/ =~ auth_req['algorithm']) + ha1 = hexdigest(password, auth_req['nonce'], auth_req['cnonce']) + else + ha1 = password + end + + if auth_req['qop'] == "auth" || auth_req['qop'] == nil + ha2 = hexdigest(req.request_method, auth_req['uri']) + ha2_res = digest("", auth_req['uri']) + elsif auth_req['qop'] == "auth-int" + ha2 = hexdigest(req.request_method, auth_req['uri'], + hexdigest(req.body)) + ha2_res = digest("", auth_req['uri'], hexdigest(req.body)) + end + + if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int" + param2 = ['nonce', 'nc', 'cnonce', 'qop'].map{|key| + auth_req[key] + }.join(':') + digest = hexdigest(ha1, param2, ha2) + digest_res = hexdigest(ha1, param2, ha2_res) + else + digest = hexdigest(ha1, auth_req['nonce'], ha2) + digest_res = hexdigest(ha1, auth_req['nonce'], ha2_res) + end + + if digest != auth_req['response'] + error("%s: digest unmatch.", auth_req['username']) + return false + elsif nonce_is_invalid + error('%s: digest is valid, but nonce is not valid.', + auth_req['username']) + return :nonce_is_stale + elsif @use_auth_info_header + auth_info = { + 'nextnonce' => generate_next_nonce(req), + 'rspauth' => digest_res + } + if @use_opaque + opaque_struct.time = req.request_time + opaque_struct.nonce = auth_info['nextnonce'] + opaque_struct.nc = "%08x" % (auth_req['nc'].hex + 1) + end + if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int" + ['qop','cnonce','nc'].each{|key| + auth_info[key] = auth_req[key] + } + end + res[@resp_info_field] = auth_info.keys.map{|key| + if key == 'nc' + key + '=' + auth_info[key] + else + key + "=" + HTTPUtils::quote(auth_info[key]) + end + }.join(', ') + end + info('%s: authentication scceeded.', auth_req['username']) + req.user = auth_req['username'] + return true + end + + def split_param_value(string) + ret = {} + while string.size != 0 + case string + when /^\s*([\w\-\.\*\%\!]+)=\s*\"((\\.|[^\"])*)\"\s*,?/ + key = $1 + matched = $2 + string = $' + ret[key] = matched.gsub(/\\(.)/, "\\1") + when /^\s*([\w\-\.\*\%\!]+)=\s*([^,\"]*),?/ + key = $1 + matched = $2 + string = $' + ret[key] = matched.clone + when /^s*^,/ + string = $' + else + break + end + end + ret + end + + def generate_next_nonce(req) + now = "%012d" % req.request_time.to_i + pk = hexdigest(now, @instance_key)[0,32] + nonce = encode64(now + ":" + pk).chop # it has 60 length of chars. + nonce + end + + def check_nonce(req, auth_req) + username = auth_req['username'] + nonce = auth_req['nonce'] + + pub_time, pk = decode64(nonce).split(":", 2) + if (!pub_time || !pk) + error("%s: empty nonce is given", username) + return false + elsif (hexdigest(pub_time, @instance_key)[0,32] != pk) + error("%s: invalid private-key: %s for %s", + username, hexdigest(pub_time, @instance_key)[0,32], pk) + return false + end + + diff_time = req.request_time.to_i - pub_time.to_i + if (diff_time < 0) + error("%s: difference of time-stamp is negative.", username) + return false + elsif diff_time > @nonce_expire_period + error("%s: nonce is expired.", username) + return false + end + + return true + end + + def generate_opaque(req) + @mutex.synchronize{ + now = req.request_time + if now - @last_nonce_expire > @nonce_expire_delta + @opaques.delete_if{|key,val| + (now - val.time) > @nonce_expire_period + } + @last_nonce_expire = now + end + begin + opaque = Utils::random_string(16) + end while @opaques[opaque] + @opaques[opaque] = OpaqueInfo.new(now, nil, '00000001') + opaque + } + end + + def check_opaque(opaque_struct, req, auth_req) + if (@use_next_nonce && auth_req['nonce'] != opaque_struct.nonce) + error('%s: nonce unmatched. "%s" for "%s"', + auth_req['username'], auth_req['nonce'], opaque_struct.nonce) + return false + elsif !check_nonce(req, auth_req) + return false + end + if (@check_nc && auth_req['nc'] != opaque_struct.nc) + error('%s: nc unmatched."%s" for "%s"', + auth_req['username'], auth_req['nc'], opaque_struct.nc) + return false + end + true + end + + def check_uri(req, auth_req) + uri = auth_req['uri'] + if uri != req.request_uri.to_s && uri != req.unparsed_uri && + (@internet_explorer_hack && uri != req.path) + error('%s: uri unmatch. "%s" for "%s"', auth_req['username'], + auth_req['uri'], req.request_uri.to_s) + return false + end + true + end + + def hexdigest(*args) + @h.hexdigest(args.join(":")) + end + + def digest(*args) + @h.digest(args.join(":")) + end + end + + class ProxyDigestAuth < DigestAuth + include ProxyAuthenticator + + def check_uri(req, auth_req) + return true + end + end + end +end |