ruby--ruby/test/webrick/test_httpauth.rb

367 lines
12 KiB
Ruby

# frozen_string_literal: false
require "test/unit"
require "net/http"
require "tempfile"
require "webrick"
require "webrick/httpauth/basicauth"
require "stringio"
require_relative "utils"
class TestWEBrickHTTPAuth < Test::Unit::TestCase
def teardown
WEBrick::Utils::TimeoutHandler.terminate
super
end
def test_basic_auth
log_tester = lambda {|log, access_log|
assert_equal(1, log.length)
assert_match(/ERROR WEBrick::HTTPStatus::Unauthorized/, log[0])
}
TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
realm = "WEBrick's realm"
path = "/basic_auth"
server.mount_proc(path){|req, res|
WEBrick::HTTPAuth.basic_auth(req, res, realm){|user, pass|
user == "webrick" && pass == "supersecretpassword"
}
res.body = "hoge"
}
http = Net::HTTP.new(addr, port)
g = Net::HTTP::Get.new(path)
g.basic_auth("webrick", "supersecretpassword")
http.request(g){|res| assert_equal("hoge", res.body, log.call)}
g.basic_auth("webrick", "not super")
http.request(g){|res| assert_not_equal("hoge", res.body, log.call)}
}
end
def test_basic_auth_sha
Tempfile.create("test_webrick_auth") {|tmpfile|
tmpfile.puts("webrick:{SHA}GJYFRpBbdchp595jlh3Bhfmgp8k=")
tmpfile.flush
assert_raise(NotImplementedError){
WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path)
}
}
end
def test_basic_auth_md5
Tempfile.create("test_webrick_auth") {|tmpfile|
tmpfile.puts("webrick:$apr1$IOVMD/..$rmnOSPXr0.wwrLPZHBQZy0")
tmpfile.flush
assert_raise(NotImplementedError){
WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path)
}
}
end
[nil, :crypt, :bcrypt].each do |hash_algo|
# OpenBSD does not support insecure DES-crypt
next if /openbsd/ =~ RUBY_PLATFORM && hash_algo != :bcrypt
begin
case hash_algo
when :crypt
# require 'string/crypt'
when :bcrypt
require 'bcrypt'
end
rescue LoadError
next
end
define_method(:"test_basic_auth_htpasswd_#{hash_algo}") do
log_tester = lambda {|log, access_log|
log.reject! {|line| /\A\s*\z/ =~ line }
pats = [
/ERROR Basic WEBrick's realm: webrick: password unmatch\./,
/ERROR WEBrick::HTTPStatus::Unauthorized/
]
pats.each {|pat|
assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}")
log.reject! {|line| pat =~ line }
}
assert_equal([], log)
}
TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
realm = "WEBrick's realm"
path = "/basic_auth2"
Tempfile.create("test_webrick_auth") {|tmpfile|
tmpfile.close
tmp_pass = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo)
tmp_pass.set_passwd(realm, "webrick", "supersecretpassword")
tmp_pass.set_passwd(realm, "foo", "supersecretpassword")
tmp_pass.flush
htpasswd = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo)
users = []
htpasswd.each{|user, pass| users << user }
assert_equal(2, users.size, log.call)
assert(users.member?("webrick"), log.call)
assert(users.member?("foo"), log.call)
server.mount_proc(path){|req, res|
auth = WEBrick::HTTPAuth::BasicAuth.new(
:Realm => realm, :UserDB => htpasswd,
:Logger => server.logger
)
auth.authenticate(req, res)
res.body = "hoge"
}
http = Net::HTTP.new(addr, port)
g = Net::HTTP::Get.new(path)
g.basic_auth("webrick", "supersecretpassword")
http.request(g){|res| assert_equal("hoge", res.body, log.call)}
g.basic_auth("webrick", "not super")
http.request(g){|res| assert_not_equal("hoge", res.body, log.call)}
}
}
end
define_method(:"test_basic_auth_bad_username_htpasswd_#{hash_algo}") do
log_tester = lambda {|log, access_log|
assert_equal(2, log.length)
assert_match(/ERROR Basic WEBrick's realm: foo\\ebar: the user is not allowed\./, log[0])
assert_match(/ERROR WEBrick::HTTPStatus::Unauthorized/, log[1])
}
TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
realm = "WEBrick's realm"
path = "/basic_auth"
Tempfile.create("test_webrick_auth") {|tmpfile|
tmpfile.close
tmp_pass = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo)
tmp_pass.set_passwd(realm, "webrick", "supersecretpassword")
tmp_pass.set_passwd(realm, "foo", "supersecretpassword")
tmp_pass.flush
htpasswd = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo)
users = []
htpasswd.each{|user, pass| users << user }
server.mount_proc(path){|req, res|
auth = WEBrick::HTTPAuth::BasicAuth.new(
:Realm => realm, :UserDB => htpasswd,
:Logger => server.logger
)
auth.authenticate(req, res)
res.body = "hoge"
}
http = Net::HTTP.new(addr, port)
g = Net::HTTP::Get.new(path)
g.basic_auth("foo\ebar", "passwd")
http.request(g){|res| assert_not_equal("hoge", res.body, log.call) }
}
}
end
end
DIGESTRES_ = /
([a-zA-Z\-]+)
[ \t]*(?:\r\n[ \t]*)*
=
[ \t]*(?:\r\n[ \t]*)*
(?:
"((?:[^"]+|\\[\x00-\x7F])*)" |
([!\#$%&'*+\-.0-9A-Z^_`a-z|~]+)
)/x
def test_digest_auth
log_tester = lambda {|log, access_log|
log.reject! {|line| /\A\s*\z/ =~ line }
pats = [
/ERROR Digest WEBrick's realm: no credentials in the request\./,
/ERROR WEBrick::HTTPStatus::Unauthorized/,
/ERROR Digest WEBrick's realm: webrick: digest unmatch\./
]
pats.each {|pat|
assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}")
log.reject! {|line| pat =~ line }
}
assert_equal([], log)
}
TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
realm = "WEBrick's realm"
path = "/digest_auth"
Tempfile.create("test_webrick_auth") {|tmpfile|
tmpfile.close
tmp_pass = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path)
tmp_pass.set_passwd(realm, "webrick", "supersecretpassword")
tmp_pass.set_passwd(realm, "foo", "supersecretpassword")
tmp_pass.flush
htdigest = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path)
users = []
htdigest.each{|user, pass| users << user }
assert_equal(2, users.size, log.call)
assert(users.member?("webrick"), log.call)
assert(users.member?("foo"), log.call)
auth = WEBrick::HTTPAuth::DigestAuth.new(
:Realm => realm, :UserDB => htdigest,
:Algorithm => 'MD5',
:Logger => server.logger
)
server.mount_proc(path){|req, res|
auth.authenticate(req, res)
res.body = "hoge"
}
Net::HTTP.start(addr, port) do |http|
g = Net::HTTP::Get.new(path)
params = {}
http.request(g) do |res|
assert_equal('401', res.code, log.call)
res["www-authenticate"].scan(DIGESTRES_) do |key, quoted, token|
params[key.downcase] = token || quoted.delete('\\')
end
params['uri'] = "http://#{addr}:#{port}#{path}"
end
g['Authorization'] = credentials_for_request('webrick', "supersecretpassword", params)
http.request(g){|res| assert_equal("hoge", res.body, log.call)}
params['algorithm'].downcase! #4936
g['Authorization'] = credentials_for_request('webrick', "supersecretpassword", params)
http.request(g){|res| assert_equal("hoge", res.body, log.call)}
g['Authorization'] = credentials_for_request('webrick', "not super", params)
http.request(g){|res| assert_not_equal("hoge", res.body, log.call)}
end
}
}
end
def test_digest_auth_int
log_tester = lambda {|log, access_log|
log.reject! {|line| /\A\s*\z/ =~ line }
pats = [
/ERROR Digest wb auth-int realm: no credentials in the request\./,
/ERROR WEBrick::HTTPStatus::Unauthorized/,
/ERROR Digest wb auth-int realm: foo: digest unmatch\./
]
pats.each {|pat|
assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}")
log.reject! {|line| pat =~ line }
}
assert_equal([], log)
}
TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
realm = "wb auth-int realm"
path = "/digest_auth_int"
Tempfile.create("test_webrick_auth_int") {|tmpfile|
tmpfile.close
tmp_pass = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path)
tmp_pass.set_passwd(realm, "foo", "Hunter2")
tmp_pass.flush
htdigest = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path)
users = []
htdigest.each{|user, pass| users << user }
assert_equal %w(foo), users
auth = WEBrick::HTTPAuth::DigestAuth.new(
:Realm => realm, :UserDB => htdigest,
:Algorithm => 'MD5',
:Logger => server.logger,
:Qop => %w(auth-int),
)
server.mount_proc(path){|req, res|
auth.authenticate(req, res)
res.body = "bbb"
}
Net::HTTP.start(addr, port) do |http|
post = Net::HTTP::Post.new(path)
params = {}
data = 'hello=world'
body = StringIO.new(data)
post.content_length = data.bytesize
post['Content-Type'] = 'application/x-www-form-urlencoded'
post.body_stream = body
http.request(post) do |res|
assert_equal('401', res.code, log.call)
res["www-authenticate"].scan(DIGESTRES_) do |key, quoted, token|
params[key.downcase] = token || quoted.delete('\\')
end
params['uri'] = "http://#{addr}:#{port}#{path}"
end
body.rewind
cred = credentials_for_request('foo', 'Hunter3', params, body)
post['Authorization'] = cred
post.body_stream = body
http.request(post){|res|
assert_equal('401', res.code, log.call)
assert_not_equal("bbb", res.body, log.call)
}
body.rewind
cred = credentials_for_request('foo', 'Hunter2', params, body)
post['Authorization'] = cred
post.body_stream = body
http.request(post){|res| assert_equal("bbb", res.body, log.call)}
end
}
}
end
def test_digest_auth_invalid
digest_auth = WEBrick::HTTPAuth::DigestAuth.new(Realm: 'realm', UserDB: '')
def digest_auth.error(fmt, *)
end
def digest_auth.try_bad_request(len)
request = {"Authorization" => %[Digest a="#{'\b'*len}]}
authenticate request, nil
end
bad_request = WEBrick::HTTPStatus::BadRequest
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
assert_raise(bad_request) {digest_auth.try_bad_request(10)}
limit = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0)
[20, 50, 100, 200].each do |len|
assert_raise(bad_request) do
Timeout.timeout(len*limit) {digest_auth.try_bad_request(len)}
end
end
end
private
def credentials_for_request(user, password, params, body = nil)
cnonce = "hoge"
nonce_count = 1
ha1 = "#{user}:#{params['realm']}:#{password}"
if body
dig = Digest::MD5.new
while buf = body.read(16384)
dig.update(buf)
end
body.rewind
ha2 = "POST:#{params['uri']}:#{dig.hexdigest}"
else
ha2 = "GET:#{params['uri']}"
end
request_digest =
"#{Digest::MD5.hexdigest(ha1)}:" \
"#{params['nonce']}:#{'%08x' % nonce_count}:#{cnonce}:#{params['qop']}:" \
"#{Digest::MD5.hexdigest(ha2)}"
"Digest username=\"#{user}\"" \
", realm=\"#{params['realm']}\"" \
", nonce=\"#{params['nonce']}\"" \
", uri=\"#{params['uri']}\"" \
", qop=#{params['qop']}" \
", nc=#{'%08x' % nonce_count}" \
", cnonce=\"#{cnonce}\"" \
", response=\"#{Digest::MD5.hexdigest(request_digest)}\"" \
", opaque=\"#{params['opaque']}\"" \
", algorithm=#{params['algorithm']}"
end
end