#!/bin/sh

# Copyright (C) 2014-2025 Internet Systems Consortium, Inc. ("ISC")
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# Exit with error if commands exit with non-zero and if undefined variables are
# used.
set -eu

# Path to the temporary configuration file.
CFG_FILE="@abs_top_builddir@/src/bin/dhcp6/tests/test_config.json"
# Path to the Kea log file.
LOG_FILE="@abs_top_builddir@/src/bin/dhcp6/tests/test.log"
# Path to the Kea lease file.
export KEA_DHCP_DATA_DIR="@abs_top_builddir@/src/bin/dhcp6/tests"
LEASE_FILE="@abs_top_builddir@/src/bin/dhcp6/tests/test_leases.csv"
# Path to the Kea LFC application
export KEA_LFC_EXECUTABLE="@abs_top_builddir@/src/bin/lfc/kea-lfc"
# Path to test hooks library
HOOK_FAIL_LOAD_PATH="@abs_top_builddir@/src/bin/dhcp6/tests/libco3.so"
# Path to test hooks library
HOOK_FAIL_POLL_PATH="@abs_top_builddir@/src/bin/dhcp6/tests/libco4.so"

# Set env KEA_HOOKS_PATH to override DEFAULT_HOOKS_PATH
export KEA_HOOKS_PATH="@abs_top_builddir@/src/bin/dhcp6/tests"

# Set env KEA_LOG_FILE_DIR to override default log path
export KEA_LOG_FILE_DIR="@abs_top_builddir@/src/bin/dhcp6/tests"

# Kea configuration to be stored in the configuration file.
CONFIG="{
    \"Dhcp6\":
    {
        \"interfaces-config\": {
          \"interfaces\": [ ]
        },
        \"server-id\": {
          \"type\": \"LLT\",
          \"persist\": false
        },
        \"preferred-lifetime\": 3000,
        \"valid-lifetime\": 4000,
        \"renew-timer\": 1000,
        \"rebind-timer\": 2000,
        \"lease-database\":
        {
            \"type\": \"memfile\",
            \"name\": \"$LEASE_FILE\",
            \"persist\": false,
            \"lfc-interval\": 0
        },
        \"subnet6\": [
        {
            \"subnet\": \"2001:db8:1::/64\",
            \"id\": 1,
            \"pools\": [ { \"pool\": \"2001:db8:1::10-2001:db8:1::100\" } ]
        } ],
        \"ddns-qualifying-suffix\": \"\",
        \"dhcp-ddns\": {
            \"enable-updates\": true
        },
        \"loggers\": [
        {
            \"name\": \"kea-dhcp6\",
            \"output-options\": [
                {
                    \"output\": \"$LOG_FILE\"
                }
            ],
            \"severity\": \"INFO\"
        }
        ]
    }
}"

# Invalid configuration (syntax error) to check that Kea can check syntax.
# This config has following errors:
# - it should be interfaces-config/interfaces, not interfaces
# - it should be subnet6/pools, no subnet6/pool
CONFIG_BAD_SYNTAX="{
    \"Dhcp6\":
    {
        \"interfaces\": [ ],
        \"preferred-lifetime\": 3000,
        \"valid-lifetime\": 4000,
        \"renew-timer\": 1000,
        \"rebind-timer\": 2000,
        \"lease-database\":
        {
            \"type\": \"memfile\",
            \"persist\": false
        },
        \"subnet6\": [
        {
            \"subnet\": \"2001:db8:1::/64\",
            \"id\": 1,
            \"pool\": [ { \"pool\": \"2001:db8:1::10-2001:db8:1::100\" } ]
        } ],
        \"loggers\": [
        {
            \"name\": \"kea-dhcp6\",
            \"output-options\": [
                {
                    \"output\": \"$LOG_FILE\"
                }
            ],
            \"severity\": \"INFO\"
        }
        ]
    }
}"

# Invalid configuration (negative preferred-lifetime) to check that Kea
# gracefully handles reconfiguration errors.
CONFIG_INVALID="{
    \"Dhcp6\":
    {
        \"interfaces-config\": {
          \"interfaces\": [ ]
        },
        \"preferred-lifetime\": -3,
        \"valid-lifetime\": 4000,
        \"renew-timer\": 1000,
        \"rebind-timer\": 2000,
        \"lease-database\":
        {
            \"type\": \"memfile\",
            \"persist\": false
        },
        \"subnet6\": [
        {
            \"subnet\": \"2001:db8:1::/64\",
            \"id\": 1,
            \"pool\": [ { \"pool\": \"2001:db8:1::10-2001:db8:1::100\" } ]
        } ],
        \"loggers\": [
        {
            \"name\": \"kea-dhcp6\",
            \"output-options\": [
                {
                    \"output\": \"$LOG_FILE\"
                }
            ],
            \"severity\": \"INFO\"
        }
        ]
    }
}"

# This config has bad pool values. The pool it out of scope for the subnet
# it is defined in. Syntactically the config is correct, though.
CONFIG_BAD_VALUES="{
    \"Dhcp6\":
    {
        \"interfaces-config\": {
          \"interfaces\": [ ]
        },
        \"server-id\": {
          \"type\": \"LLT\",
          \"persist\": false
        },
        \"preferred-lifetime\": 3000,
        \"valid-lifetime\": 4000,
        \"renew-timer\": 1000,
        \"rebind-timer\": 2000,
        \"lease-database\":
        {
            \"type\": \"memfile\",
            \"name\": \"$LEASE_FILE\",
            \"persist\": false,
            \"lfc-interval\": 0
        },
        \"subnet6\": [
        {
            \"subnet\": \"2001:db8::/64\",
            \"id\": 1,
            \"pools\": [ { \"pool\": \"3000::-3000::ffff\" } ]
        } ],
        \"ddns-qualifying-suffix\": \"\",
        \"dhcp-ddns\": {
            \"enable-updates\": true
        }
    }
}"

# Invalid configuration (hook explicitly fails to load) to check that performing
# extra configuration checks detects the error.
INVALID_CONFIG_HOOKS_LOAD="{
    \"Dhcp6\":
    {
        \"interfaces-config\": {
          \"interfaces\": [ ]
        },
        \"multi-threading\": {
          \"enable-multi-threading\": false
        },
        \"server-id\": {
          \"type\": \"LLT\",
          \"persist\": false
        },
        \"preferred-lifetime\": 3000,
        \"valid-lifetime\": 4000,
        \"renew-timer\": 1000,
        \"rebind-timer\": 2000,
        \"lease-database\":
        {
            \"type\": \"memfile\",
            \"name\": \"$LEASE_FILE\",
            \"persist\": false,
            \"lfc-interval\": 0
        },
        \"subnet6\": [
        {
            \"subnet\": \"2001:db8:1::/64\",
            \"id\": 1,
            \"pools\": [ { \"pool\": \"2001:db8:1::10-2001:db8:1::100\" } ]
        } ],
        \"ddns-qualifying-suffix\": \"\",
        \"dhcp-ddns\": {
            \"enable-updates\": true
        },
        \"hooks-libraries\": [
        {
            \"library\": \"$HOOK_FAIL_LOAD_PATH\",
                \"parameters\": {
                    \"mode\": \"fail-on-load\"
            }
        } ],
        \"loggers\": [
        {
            \"name\": \"kea-dhcp6\",
            \"output-options\": [
                {
                    \"output\": \"$LOG_FILE\"
                }
            ],
            \"severity\": \"INFO\"
        }
        ]
    }
}"

# Invalid configuration (hook point returns error) to check that performing
# extra configuration checks detects the error.
INVALID_CONFIG_HOOKS_CALLOUT_FAIL_ON_LOAD="{
    \"Dhcp6\":
    {
        \"interfaces-config\": {
            \"interfaces\": [ ]
        },
        \"multi-threading\": {
            \"enable-multi-threading\": false
        },
        \"server-id\": {
            \"type\": \"LLT\",
            \"persist\": false
        },
        \"preferred-lifetime\": 3000,
        \"valid-lifetime\": 4000,
        \"renew-timer\": 1000,
        \"rebind-timer\": 2000,
        \"lease-database\":
        {
            \"type\": \"memfile\",
            \"name\": \"$LEASE_FILE\",
            \"persist\": false,
            \"lfc-interval\": 0
        },
        \"subnet6\": [
        {
            \"subnet\": \"2001:db8:1::/64\",
            \"id\": 1,
            \"pools\": [ { \"pool\": \"2001:db8:1::10-2001:db8:1::100\" } ]
        } ],
        \"ddns-qualifying-suffix\": \"\",
        \"dhcp-ddns\": {
            \"enable-updates\": true
        },
        \"hooks-libraries\": [
        {
            \"library\": \"$HOOK_FAIL_LOAD_PATH\",
                \"parameters\": {
                    \"mode\": \"fail-without-error\"
            }
        } ],
        \"loggers\": [
        {
            \"name\": \"kea-dhcp6\",
            \"output-options\": [
                {
                    \"output\": \"$LOG_FILE\"
                }
            ],
            \"severity\": \"INFO\"
        }
        ]
    }
}"

# Invalid configuration (poll after load throws exception) to check that performing
# extra configuration checks detects the error.
INVALID_CONFIG_HOOKS_CALLOUT_FAIL_ON_POLL="{
    \"Dhcp6\":
    {
        \"interfaces-config\": {
            \"interfaces\": [ ]
        },
        \"multi-threading\": {
            \"enable-multi-threading\": false
        },
        \"server-id\": {
            \"type\": \"LLT\",
            \"persist\": false
        },
        \"preferred-lifetime\": 3000,
        \"valid-lifetime\": 4000,
        \"renew-timer\": 1000,
        \"rebind-timer\": 2000,
        \"lease-database\":
        {
            \"type\": \"memfile\",
            \"name\": \"$LEASE_FILE\",
            \"persist\": false,
            \"lfc-interval\": 0
        },
        \"subnet6\": [
        {
            \"subnet\": \"2001:db8:1::/64\",
            \"id\": 1,
            \"pools\": [ { \"pool\": \"2001:db8:1::10-2001:db8:1::100\" } ]
        } ],
        \"ddns-qualifying-suffix\": \"\",
        \"dhcp-ddns\": {
            \"enable-updates\": true
        },
        \"hooks-libraries\": [
        {
            \"library\": \"$HOOK_FAIL_POLL_PATH\",
                \"parameters\": {
                    \"mode\": \"fail-without-error\"
            }
        } ],
        \"loggers\": [
        {
            \"name\": \"kea-dhcp6\",
            \"output-options\": [
                {
                    \"output\": \"$LOG_FILE\"
                }
            ],
            \"severity\": \"INFO\"
        }
        ]
    }
}"

# Set the location of the executable.
bin="kea-dhcp6"
bin_path="@abs_top_builddir@/src/bin/dhcp6"

# Import common test library.
# shellcheck source=src/lib/testutils/dhcp_test_lib.sh.in
. "@abs_top_builddir@/src/lib/testutils/dhcp_test_lib.sh"

# This test verifies that syntax checking works properly. This function
# requires 3 parameters:
# test_name
# config - string with a content of the config (will be written to a file)
# expected_code - expected exit code returned by kea (0 - success, 1 - failure)
syntax_check_test() {
    local test_name="${1}"
    local config="${2}"
    local expected_code="${3}"
    local check_type="${4}"

    # Log the start of the test and print test name.
    test_start "${test_name}"
    # Create correct configuration file.
    create_config "${config}"
    # Check it
    printf "Running command %s.\n" "\"${bin_path}/${bin} ${check_type} ${CFG_FILE}\""
    run_command \
        "${bin_path}/${bin}" "${check_type}" "${CFG_FILE}"
    if [ "${EXIT_CODE}" -ne "${expected_code}" ]; then
        printf 'ERROR: expected exit code %s, got %s\n' "${expected_code}" "${EXIT_CODE}"
        clean_exit 1
    fi

    test_finish 0
}

# This test verifies that DHCPv6 can be reconfigured with a SIGHUP signal.
dynamic_reconfiguration_test() {
    # Log the start of the test and print test name.
    test_start "dhcpv6_srv.dynamic_reconfiguration"
    # Create new configuration file.
    create_config "${CONFIG}"
    # Instruct Kea to log to the specific file.
    set_logger
    # Start Kea.
    start_kea "${bin_path}/${bin}"
    # Wait up to 20s for Kea to start.
    wait_for_kea 20
    if [ "${_WAIT_FOR_KEA}" -eq 0 ]; then
        printf "ERROR: timeout waiting for Kea to start.\n"
        clean_exit 1
    fi

    # Check if it is still running. It could have terminated (e.g. as a result
    # of configuration failure).
    get_pid "${bin}"
    if [ "${_GET_PIDS_NUM}" -ne 1 ]; then
        printf "ERROR: expected one Kea process to be started. Found %d processes\
 started.\n" "${_GET_PIDS_NUM}"
        clean_exit 1
    fi

    # Check in the log file, how many times server has been configured. It should
    # be just once on startup.
    get_reconfigs
    if [ "${_GET_RECONFIGS}" -ne 1 ]; then
        printf "ERROR: server hasn't been configured.\n"
        clean_exit 1
    else
        printf "Server successfully configured.\n"
    fi

    # Now use invalid configuration.
    create_config "${CONFIG_INVALID}"

    # Try to reconfigure by sending SIGHUP
    send_signal 1 "${bin}"

    # The configuration should fail and the error message should be there.
    wait_for_message 10 "DHCP6_CONFIG_LOAD_FAIL" 1

    # After receiving SIGHUP the server should try to reconfigure itself.
    # The configuration provided is invalid so it should result in
    # reconfiguration failure but the server should still be running.
    get_reconfigs
    if [ "${_GET_RECONFIGS}" -ne 1 ]; then
        printf "ERROR: server has been reconfigured despite bogus configuration.\n"
        clean_exit 1
    elif [ "${_GET_RECONFIG_ERRORS}" -ne 1 ]; then
        printf "ERROR: server did not report reconfiguration error despite attempt\
 to configure it with invalid configuration.\n"
        clean_exit 1
    fi

    # Make sure the server is still operational.
    get_pid "${bin}"
    if [ "${_GET_PIDS_NUM}" -ne 1 ]; then
        printf "ERROR: Kea process was killed when attempting reconfiguration.\n"
        clean_exit 1
    fi

    # Restore the good configuration.
    create_config "${CONFIG}"

    # Reconfigure the server with SIGHUP.
    send_signal 1 "${bin}"

    # There should be two occurrences of the DHCP6_CONFIG_COMPLETE messages.
    # Wait for it up to 10s.
    wait_for_message 10 "DHCP6_CONFIG_COMPLETE" 2

    # After receiving SIGHUP the server should get reconfigured and the
    # reconfiguration should be noted in the log file. We should now
    # have two configurations logged in the log file.
    if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then
        printf "ERROR: server hasn't been reconfigured.\n"
        clean_exit 1
    else
        printf "Server successfully reconfigured.\n"
    fi

    # Make sure the server is still operational.
    get_pid "${bin}"
    if [ "${_GET_PIDS_NUM}" -ne 1 ]; then
        printf "ERROR: Kea process was killed when attempting reconfiguration.\n"
        clean_exit 1
    fi

    # When the server receives a signal the call to select() function is
    # interrupted. This should not be logged as an error.
    get_log_messages "DHCP6_PACKET_RECEIVE_FAIL"
    assert_eq 0 "${_GET_LOG_MESSAGES}" \
        "Expected get_log_messages DHCP6_PACKET_RECEIVE_FAIL return %d, \
returned %d."

    # All ok. Shut down Kea and exit.
    test_finish 0
}

# This test verifies that DHCPv6 server is shut down gracefully when it
# receives a SIGINT or SIGTERM signal.
shutdown_test() {
    local test_name="${1}"  # Test name
    local signum="${2}"     # Signal number

    # Log the start of the test and print test name.
    test_start "${test_name}"
    # Create new configuration file.
    create_config "${CONFIG}"
    # Instruct Kea to log to the specific file.
    set_logger
    # Start Kea.
    start_kea "${bin_path}/${bin}"
    # Wait up to 20s for Kea to start.
    wait_for_kea 20
    if [ "${_WAIT_FOR_KEA}" -eq 0 ]; then
        printf "ERROR: timeout waiting for Kea to start.\n"
        clean_exit 1
    fi

    # Check if it is still running. It could have terminated (e.g. as a result
    # of configuration failure).
    get_pid "${bin}"
    if [ "${_GET_PIDS_NUM}" -ne 1 ]; then
        printf "ERROR: expected one Kea process to be started. Found %d processes\
 started.\n" "${_GET_PIDS_NUM}"
        clean_exit 1
    fi

    # Check in the log file, how many times server has been configured. It should
    # be just once on startup.
    get_reconfigs
    if [ "${_GET_RECONFIGS}" -ne 1 ]; then
        printf "ERROR: server hasn't been configured.\n"
        clean_exit 1
    else
        printf "Server successfully configured.\n"
    fi

    # Send signal to Kea (SIGTERM, SIGINT etc.)
    send_signal "${signum}" "${bin}"

    # Wait up to 10s for the server's graceful shutdown. The graceful shut down
    # should be recorded in the log file with the appropriate message.
    wait_for_message 10 "DHCP6_SHUTDOWN" 1
    if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then
        printf "ERROR: Server did not record shutdown in the log.\n"
        clean_exit 1
    fi

    # Make sure the server is down.
    wait_for_server_down 5 "${bin}"
    assert_eq 1 "${_WAIT_FOR_SERVER_DOWN}" \
        "Expected wait_for_server_down return %d, returned %d"

    # When the server receives a signal the call to select() function is
    # interrupted. This should not be logged as an error.
    get_log_messages "DHCP6_PACKET_RECEIVE_FAIL"
    assert_eq 0 "${_GET_LOG_MESSAGES}" \
        "Expected get_log_messages DHCP6_PACKET_RECEIVE_FAIL return %d, \
returned %d."

    test_finish 0
}

# This test verifies that DHCPv6 can be configured to run lease file cleanup
# periodically.
lfc_timer_test() {
    # Log the start of the test and print test name.
    test_start "dhcpv6_srv.lfc_timer_test"
    # Create a configuration with the LFC enabled, by replacing the section
    # with the lfc-interval and persist parameters.
    LFC_CONFIG=$(printf '%s' "${CONFIG}" | sed -e 's/\"lfc-interval\": 0/\"lfc-interval\": 3/g' \
                        | sed -e 's/\"persist\": false,/\"persist\": true,/g')
    # Create new configuration file.
    create_config "${LFC_CONFIG}"
    # Instruct Kea to log to the specific file.
    set_logger
    # Start Kea.
    start_kea "${bin_path}/${bin}"
    # Wait up to 20s for Kea to start.
    wait_for_kea 20
    if [ "${_WAIT_FOR_KEA}" -eq 0 ]; then
        printf "ERROR: timeout waiting for Kea to start.\n"
        clean_exit 1
    fi

    # Check if it is still running. It could have terminated (e.g. as a result
    # of configuration failure).
    get_pid "${bin}"
    if [ "${_GET_PIDS_NUM}" -ne 1 ]; then
        printf "ERROR: expected one Kea process to be started. Found %d processes\
 started.\n" "${_GET_PIDS_NUM}"
        clean_exit 1
    fi

    # Check if Kea emits the log message indicating that LFC is started.
    wait_for_message 10 "DHCPSRV_MEMFILE_LFC_EXECUTE" 1
    if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then
        printf "ERROR: Server did not execute LFC.\n"
        clean_exit 1
    fi

    # Give it a short time to run.
    sleep 1

    # Modify the interval.
    LFC_CONFIG=$(printf '%s' "${LFC_CONFIG}" | sed -e 's/\"lfc-interval\": 3/\"lfc-interval\": 4/g')
    # Create new configuration file.
    create_config "${LFC_CONFIG}"

    # Reconfigure the server with SIGHUP.
    send_signal 1 "${bin}"

    # There should be two occurrences of the DHCP4_CONFIG_COMPLETE messages.
    # Wait for it up to 10s.
    wait_for_message 10 "DHCP6_CONFIG_COMPLETE" 2

    # After receiving SIGHUP the server should get reconfigured and the
    # reconfiguration should be noted in the log file. We should now
    # have two configurations logged in the log file.
    if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then
        printf "ERROR: server hasn't been reconfigured.\n"
        clean_exit 1
    else
        printf "Server successfully reconfigured.\n"
    fi

    # Make sure the server is still operational.
    get_pid "${bin}"
    if [ "${_GET_PIDS_NUM}" -ne 1 ]; then
        printf "ERROR: Kea process was killed when attempting reconfiguration.\n"
        clean_exit 1
    fi

    # Wait for the LFC to run the second time.
    wait_for_message 10 "DHCPSRV_MEMFILE_LFC_EXECUTE" 2
    if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then
        printf "ERROR: Server did not execute LFC.\n"
        clean_exit 1
    fi

    # Send signal to Kea SIGTERM
    send_signal 15 "${bin}"

    # Wait up to 10s for the server's graceful shutdown. The graceful shut down
    # should be recorded in the log file with the appropriate message.
    wait_for_message 10 "DHCP6_SHUTDOWN" 1
    if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then
        printf "ERROR: Server did not record shutdown in the log.\n"
        clean_exit 1
    fi

    # Make sure the server is down.
    wait_for_server_down 5 "${bin}"
    assert_eq 1 "${_WAIT_FOR_SERVER_DOWN}" \
        "Expected wait_for_server_down return %d, returned %d"

    # All ok. Shut down Kea and exit.
    test_finish 0
}

server_pid_file_test "${CONFIG}" DHCP6_ALREADY_RUNNING
dynamic_reconfiguration_test
shutdown_test "dhcpv6.sigterm_test" 15
shutdown_test "dhcpv6.sigint_test" 2
version_test "dhcpv6.version"
logger_vars_test "dhcpv6.variables"
lfc_timer_test
syntax_check_test "dhcpv6.syntax_check_success" "${CONFIG}" 0 -t
syntax_check_test "dhcpv6.syntax_check_bad_syntax" "${CONFIG_BAD_SYNTAX}" 1 -t
syntax_check_test "dhcpv6.syntax_check_bad_values" "${CONFIG_BAD_VALUES}" 1 -t
syntax_check_test "dhcpv6.syntax_check_hooks_load_fail" "${INVALID_CONFIG_HOOKS_LOAD}" 1 -T
syntax_check_test "dhcpv6.syntax_check_hooks_callout_fail_on_load" "${INVALID_CONFIG_HOOKS_CALLOUT_FAIL_ON_LOAD}" 1 -T
syntax_check_test "dhcpv6.syntax_check_hooks_callout_fail_on_poll" "${INVALID_CONFIG_HOOKS_CALLOUT_FAIL_ON_POLL}" 1 -T
password_redact_test "dhcpv6.password_redact_test" "$(kea_dhcp_config 6)" 0
