1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/lib/webrick/httpauth/digestauth.rb
nagachika 16b426e966 merge revision(s) 62960,62961,62962,62963,62964,62965:
webrick/httprequest: limit request headers size

	We use the same 112 KB limit started (AFAIK) by Mongrel, Thin,
	and Puma to prevent malicious users from using up all the memory
	with a single request.  This also limits the damage done by
	excessive ranges in multipart Range: requests.

	Due to the way we rely on IO#gets and the desire to keep
	the code simple, the actual maximum header may be 4093 bytes
	larger than 112 KB, but we're splitting hairs at that point.

	* lib/webrick/httprequest.rb: define MAX_HEADER_LENGTH
	  (read_header): raise when headers exceed max length

	webrick/httpservlet/cgihandler: reduce memory use

	WEBrick::HTTPRequest#body can be passed a block to process the
	body in chunks.  Use this feature to avoid building a giant
	string in memory.

	* lib/webrick/httpservlet/cgihandler.rb (do_GET):
	  avoid reading entire request body into memory
	  (do_POST is aliased to do_GET, so it handles bodies)

	webrick/httprequest: raise correct exception

	"BadRequest" alone does not resolve correctly, it is in the
	HTTPStatus namespace.

	* lib/webrick/httprequest.rb (read_chunked): use correct exception
	* test/webrick/test_httpserver.rb (test_eof_in_chunk): new test

	webrick/httprequest: use InputBufferSize for chunked requests

	While WEBrick::HTTPRequest#body provides a Proc interface
	for streaming large request bodies, clients must not force
	the server to use an excessively large chunk size.

	* lib/webrick/httprequest.rb (read_chunk_size): limit each
	  read and block.call to :InputBufferSize in config.
	* test/webrick/test_httpserver.rb (test_big_chunks): new test

	webrick: add test for Digest auth-int

	No changes to the actual code, this is a new test for
	a feature for which no tests existed.  I don't understand
	the Digest authentication code well at all, but this is
	necessary for the subsequent change.

	* test/webrick/test_httpauth.rb (test_digest_auth_int): new test
	  (credentials_for_request): support bodies with POST

	webrick/httpauth/digestauth: stream req.body

	WARNING! WARNING! WARNING!  LIKELY BROKEN CHANGE

	Pass a proc to WEBrick::HTTPRequest#body to avoid reading a
	potentially large request body into memory during
	authentication.

	WARNING! this will break apps completely which want to do
	something with the body besides calculating the MD5 digest
	of it.

	Also, keep in mind that probably nobody uses "auth-int".
	Servers such as Apache, lighttpd, nginx don't seem to
	support it; nor does curl when using POST/PUT bodies;
	and we didn't have tests for it until now...

	* lib/webrick/httpauth/digestauth.rb (_authenticate): stream req.body

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/branches/ruby_2_4@63004 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2018-03-28 12:23:29 +00:00

410 lines
13 KiB
Ruby

# frozen_string_literal: false
#
# 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'
module WEBrick
module HTTPAuth
##
# RFC 2617 Digest Access Authentication for WEBrick
#
# Use this class to add digest authentication to a WEBrick servlet.
#
# Here is an example of how to set up DigestAuth:
#
# config = { :Realm => 'DigestAuth example realm' }
#
# htdigest = WEBrick::HTTPAuth::Htdigest.new 'my_password_file'
# htdigest.set_passwd config[:Realm], 'username', 'password'
# htdigest.flush
#
# config[:UserDB] = htdigest
#
# digest_auth = WEBrick::HTTPAuth::DigestAuth.new config
#
# When using this as with a servlet be sure not to create a new DigestAuth
# object in the servlet's #initialize. By default WEBrick creates a new
# servlet instance for every request and the DigestAuth object must be
# used across requests.
class DigestAuth
include Authenticator
AuthScheme = "Digest" # :nodoc:
##
# Struct containing the opaque portion of the digest authentication
OpaqueInfo = Struct.new(:time, :nonce, :nc) # :nodoc:
##
# Digest authentication algorithm
attr_reader :algorithm
##
# Quality of protection. RFC 2617 defines "auth" and "auth-int"
attr_reader :qop
##
# Used by UserDB to create a digest password entry
def self.make_passwd(realm, user, pass)
pass ||= ""
Digest::MD5::hexdigest([user, realm, pass].join(":"))
end
##
# Creates a new DigestAuth instance. Be sure to use the same DigestAuth
# instance for multiple requests as it saves state between requests in
# order to perform authentication.
#
# See WEBrick::Config::DigestAuth for default configuration entries
#
# You must supply the following configuration entries:
#
# :Realm:: The name of the realm being protected.
# :UserDB:: A database of usernames and passwords.
# A WEBrick::HTTPAuth::Htdigest instance should be used.
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]
case @algorithm
when 'MD5','MD5-sess'
@h = Digest::MD5
when 'SHA1','SHA1-sess' # it is a bonus feature :-)
@h = Digest::SHA1
else
msg = format('Algorithm "%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 = Thread::Mutex.new
end
##
# Authenticates a +req+ and returns a 401 Unauthorized using +res+ if
# the authentication was not correct.
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
##
# Returns a challenge response which asks for authentication information
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
# :stopdoc:
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'].upcase != @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 allowed.', 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$/i =~ 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 = hexdigest("", auth_req['uri'])
elsif auth_req['qop'] == "auth-int"
body_digest = @h.new
req.body { |chunk| body_digest.update(chunk) }
body_digest = body_digest.hexdigest
ha2 = hexdigest(req.request_method, auth_req['uri'], body_digest)
ha2_res = hexdigest("", auth_req['uri'], body_digest)
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 succeeded.', auth_req['username'])
req.user = auth_req['username']
return true
end
def split_param_value(string)
ret = {}
while string.bytesize != 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 = [now + ":" + pk].pack("m0") # 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 = nonce.unpack("m*")[0].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
# :startdoc:
end
##
# Digest authentication for proxy servers. See DigestAuth for details.
class ProxyDigestAuth < DigestAuth
include ProxyAuthenticator
private
def check_uri(req, auth_req) # :nodoc:
return true
end
end
end
end