From 01eba908adcd150a7b816af0dbe167c4c4912a90 Mon Sep 17 00:00:00 2001 From: gotoyuzo Date: Wed, 23 Jul 2003 16:51:36 +0000 Subject: * lib/webrick: imported. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@4130 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- lib/webrick/httpauth/authenticator.rb | 79 ++++++++ lib/webrick/httpauth/basicauth.rb | 66 +++++++ lib/webrick/httpauth/digestauth.rb | 348 ++++++++++++++++++++++++++++++++++ lib/webrick/httpauth/htdigest.rb | 91 +++++++++ lib/webrick/httpauth/htgroup.rb | 61 ++++++ lib/webrick/httpauth/htpasswd.rb | 75 ++++++++ lib/webrick/httpauth/userdb.rb | 29 +++ 7 files changed, 749 insertions(+) create mode 100644 lib/webrick/httpauth/authenticator.rb create mode 100644 lib/webrick/httpauth/basicauth.rb create mode 100644 lib/webrick/httpauth/digestauth.rb create mode 100644 lib/webrick/httpauth/htdigest.rb create mode 100644 lib/webrick/httpauth/htgroup.rb create mode 100644 lib/webrick/httpauth/htpasswd.rb create mode 100644 lib/webrick/httpauth/userdb.rb (limited to 'lib/webrick/httpauth') diff --git a/lib/webrick/httpauth/authenticator.rb b/lib/webrick/httpauth/authenticator.rb new file mode 100644 index 0000000000..fe2dbf4e0c --- /dev/null +++ b/lib/webrick/httpauth/authenticator.rb @@ -0,0 +1,79 @@ +# +# httpauth/authenticator.rb -- Authenticator mix-in module. +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: authenticator.rb,v 1.3 2003/02/20 07:15:47 gotoyuzo Exp $ + +module WEBrick + module HTTPAuth + module Authenticator + RequestField = "Authorization" + ResponseField = "WWW-Authenticate" + ResponseInfoField = "Authentication-Info" + AuthException = HTTPStatus::Unauthorized + AuthScheme = nil # must override by the derived class + + attr_reader :realm, :userdb, :logger + + private + + def check_init(config) + [:UserDB, :Realm].each{|sym| + unless config[sym] + raise ArgumentError, "Argument #{sym.inspect} missing." + end + } + @realm = config[:Realm] + @userdb = config[:UserDB] + @logger = config[:Logger] || Log::new($stderr) + @reload_db = config[:AutoReloadUserDB] + @request_field = self::class::RequestField + @response_field = self::class::ResponseField + @resp_info_field = self::class::ResponseInfoField + @auth_exception = self::class::AuthException + @auth_scheme = self::class::AuthScheme + end + + def check_scheme(req) + unless credentials = req[@request_field] + error("no credentials in the request.") + return nil + end + unless match = /^#{@auth_scheme}\s+/.match(credentials) + error("invalid scheme in %s.", credentials) + info("%s: %s", @request_field, credentials) if $DEBUG + return nil + end + return match.post_match + end + + def log(meth, fmt, *args) + msg = format("%s %s: ", @auth_scheme, @realm) + msg << fmt % args + @logger.send(meth, msg) + end + + def error(fmt, *args) + if @logger.error? + log(:error, fmt, *args) + end + end + + def info(fmt, *args) + if @logger.info? + log(:info, fmt, *args) + end + end + end + + module ProxyAuthenticator + RequestField = "Proxy-Authorization" + ResponseField = "Proxy-Authenticate" + InfoField = "Proxy-Authentication-Info" + AuthException = HTTPStatus::ProxyAuthenticationRequired + end + end +end diff --git a/lib/webrick/httpauth/basicauth.rb b/lib/webrick/httpauth/basicauth.rb new file mode 100644 index 0000000000..926a6b8289 --- /dev/null +++ b/lib/webrick/httpauth/basicauth.rb @@ -0,0 +1,66 @@ +# +# httpauth/basicauth.rb -- HTTP basic access authentication +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: basicauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $ + +require 'webrick/config' +require 'webrick/httpstatus' +require 'webrick/httpauth/authenticator' +require 'base64' + +module WEBrick + module HTTPAuth + class BasicAuth + include Authenticator + + AuthScheme = "Basic" + + def self.make_passwd(realm, user, pass) + pass ||= "" + pass.crypt(Utils::random_string(2)) + end + + attr_reader :realm, :userdb, :logger + + def initialize(config, default=Config::BasicAuth) + check_init(config) + @config = default.dup.update(config) + end + + def authenticate(req, res) + unless basic_credentials = check_scheme(req) + challenge(req, res) + end + userid, password = decode64(basic_credentials).split(":", 2) + password ||= "" + if userid.empty? + error("user id was not given.") + challenge(req, res) + end + unless encpass = @userdb.get_passwd(@realm, userid, @reload_db) + error("%s: the user is not allowed.", userid) + challenge(req, res) + end + if password.crypt(encpass) != encpass + error("%s: password unmatch.", userid) + challenge(req, res) + end + info("%s: authentication succeeded.", userid) + req.user = userid + end + + def challenge(req, res) + res[@response_field] = "#{@auth_scheme} realm=\"#{@realm}\"" + raise @auth_exception + end + end + + class ProxyBasicAuth < BasicAuth + include ProxyAuthenticator + end + end +end diff --git a/lib/webrick/httpauth/digestauth.rb b/lib/webrick/httpauth/digestauth.rb new file mode 100644 index 0000000000..2b0660ed29 --- /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 diff --git a/lib/webrick/httpauth/htdigest.rb b/lib/webrick/httpauth/htdigest.rb new file mode 100644 index 0000000000..3949756f2b --- /dev/null +++ b/lib/webrick/httpauth/htdigest.rb @@ -0,0 +1,91 @@ +# +# httpauth/htdigest.rb -- Apache compatible htdigest file +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: htdigest.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $ + +require 'webrick/httpauth/userdb' +require 'webrick/httpauth/digestauth' +require 'tempfile' + +module WEBrick + module HTTPAuth + class Htdigest + include UserDB + + def initialize(path) + @path = path + @mtime = Time.at(0) + @digest = Hash.new + @mutex = Mutex::new + @auth_type = DigestAuth + open(@path,"a").close unless File::exist?(@path) + reload + end + + def reload + mtime = File::mtime(@path) + if mtime > @mtime + @digest.clear + open(@path){|io| + while line = io.gets + line.chomp! + user, realm, pass = line.split(/:/, 3) + unless @digest[realm] + @digest[realm] = Hash.new + end + @digest[realm][user] = pass + end + } + @mtime = mtime + end + end + + def flush(output=nil) + output ||= @path + tmp = Tempfile.new("htpasswd", File::dirname(output)) + begin + each{|item| tmp.puts(item.join(":")) } + tmp.close + File::rename(tmp.path, output) + rescue + tmp.close(true) + end + end + + def get_passwd(realm, user, reload_db) + reload() if reload_db + if hash = @digest[realm] + hash[user] + end + end + + def set_passwd(realm, user, pass) + @mutex.synchronize{ + unless @digest[realm] + @digest[realm] = Hash.new + end + @digest[realm][user] = make_passwd(realm, user, pass) + } + end + + def delete_passwd(realm, user) + if hash = @digest[realm] + hash.delete(user) + end + end + + def each + @digest.keys.sort.each{|realm| + hash = @digest[realm] + hash.keys.sort.each{|user| + yield([user, realm, hash[user]]) + } + } + end + end + end +end diff --git a/lib/webrick/httpauth/htgroup.rb b/lib/webrick/httpauth/htgroup.rb new file mode 100644 index 0000000000..c9270c61cc --- /dev/null +++ b/lib/webrick/httpauth/htgroup.rb @@ -0,0 +1,61 @@ +# +# httpauth/htgroup.rb -- Apache compatible htgroup file +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: htgroup.rb,v 1.1 2003/02/16 22:22:56 gotoyuzo Exp $ + +require 'tempfile' + +module WEBrick + module HTTPAuth + class Htgroup + def initialize(path) + @path = path + @mtime = Time.at(0) + @group = Hash.new + open(@path,"a").close unless File::exist?(@path) + reload + end + + def reload + if (mtime = File::mtime(@path)) > @mtime + @group.clear + open(@path){|io| + while line = io.gets + line.chomp! + group, members = line.split(/:\s*/) + @group[group] = members.split(/\s+/) + end + } + @mtime = mtime + end + end + + def flush(output=nil) + output ||= @path + tmp = Tempfile.new("htgroup", File::dirname(output)) + begin + @group.keys.sort.each{|group| + tmp.puts(format("%s: %s", group, self.members(group).join(" "))) + } + tmp.close + File::rename(tmp.path, output) + rescue + tmp.close(true) + end + end + + def members(group) + reload + @group[group] || [] + end + + def add(group, members) + @group[group] = members(group) | members + end + end + end +end diff --git a/lib/webrick/httpauth/htpasswd.rb b/lib/webrick/httpauth/htpasswd.rb new file mode 100644 index 0000000000..a4a80647d8 --- /dev/null +++ b/lib/webrick/httpauth/htpasswd.rb @@ -0,0 +1,75 @@ +# +# httpauth/htpasswd -- Apache compatible htpasswd file +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: htpasswd.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $ + +require 'webrick/httpauth/userdb' +require 'webrick/httpauth/basicauth' +require 'tempfile' + +module WEBrick + module HTTPAuth + class Htpasswd + include UserDB + + def initialize(path) + @path = path + @mtime = Time.at(0) + @passwd = Hash.new + @auth_type = BasicAuth + open(@path,"a").close unless File::exist?(@path) + reload + end + + def reload + mtime = File::mtime(@path) + if mtime > @mtime + @passwd.clear + open(@path){|io| + while line = io.gets + line.chomp! + user, pass = line.split(":") + @passwd[user] = pass + end + } + @mtime = mtime + end + end + + def flush(output=nil) + output ||= @path + tmp = Tempfile.new("htpasswd", File::dirname(output)) + begin + each{|item| tmp.puts(item.join(":")) } + tmp.close + File::rename(tmp.path, output) + rescue + tmp.close(true) + end + end + + def get_passwd(realm, user, reload_db) + reload() if reload_db + @passwd[user] + end + + def set_passwd(realm, user, pass) + @passwd[user] = make_passwd(realm, user, pass) + end + + def delete_passwd(realm, user) + @passwd.delete(user) + end + + def each + @passwd.keys.sort.each{|user| + yield([user, @passwd[user]]) + } + end + end + end +end diff --git a/lib/webrick/httpauth/userdb.rb b/lib/webrick/httpauth/userdb.rb new file mode 100644 index 0000000000..33e01405f4 --- /dev/null +++ b/lib/webrick/httpauth/userdb.rb @@ -0,0 +1,29 @@ +# +# httpauth/userdb.rb -- UserDB mix-in module. +# +# Author: IPR -- Internet Programming with Ruby -- writers +# Copyright (c) 2003 Internet Programming with Ruby writers. All rights +# reserved. +# +# $IPR: userdb.rb,v 1.2 2003/02/20 07:15:48 gotoyuzo Exp $ + +module WEBrick + module HTTPAuth + module UserDB + attr_accessor :auth_type # BasicAuth or DigestAuth + + def make_passwd(realm, user, pass) + @auth_type::make_passwd(realm, user, pass) + end + + def set_passwd(realm, user, pass) + self[user] = pass + end + + def get_passwd(realm, user, reload_db=false) + # reload_db is dummy + make_passwd(realm, user, self[user]) + end + end + end +end -- cgit v1.2.3