# 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
