#!/bin/sh

# PROVIDE: rustproxy
# REQUIRE: LOGIN NETWORKING
# KEYWORD: shutdown

. /etc/rc.subr

name="rustproxy"
rcvar="${name}_enable"

load_rc_config "$name"

: ${rustproxy_enable:="NO"}
# RustProxy on OPNsense/FreeBSD runs as root.  In practice the application
# needs access to its complete data/certificate state and service startup was
# unreliable after daemon(8) dropped privileges to the dedicated account.
: ${rustproxy_user:="root"}
: ${rustproxy_group:="wheel"}
: ${rustproxy_data_dir:="/var/db/rustproxy"}
: ${rustproxy_log_dir:="/var/log/rustproxy"}
: ${rustproxy_log_file:="${rustproxy_log_dir}/rustproxy.log"}
: ${rustproxy_run_dir:="/var/run/rustproxy"}
: ${rustproxy_command:="/usr/local/sbin/rust-proxy"}
: ${rustproxy_restart_delay:="5"}
: ${rustproxy_allow_remote_admin:="1"}
# FreeBSD uses jemalloc rather than glibc.  Keep the same low-arena intent as
# Linux MALLOC_ARENA_MAX=2 with the native jemalloc knob.  rc.conf can override
# rustproxy_malloc_conf or set it to an empty value to disable the tuning.
if [ "${rustproxy_malloc_conf+x}" != "x" ]; then
    rustproxy_malloc_conf="${MALLOC_CONF:-narenas:2}"
fi

# Split listener settings written by the current OPNsense model.
: ${rustproxy_proxy_address:=""}
: ${rustproxy_proxy_port:=""}
: ${rustproxy_tls_enabled:="1"}
: ${rustproxy_tls_address:=""}
: ${rustproxy_tls_port:=""}
: ${rustproxy_admin_address:=""}
: ${rustproxy_admin_port:=""}

# Combined listener settings used by plugin model 1.2.x and earlier.
: ${rustproxy_proxy_listen_legacy:=""}
: ${rustproxy_tls_listen_legacy:=""}
: ${rustproxy_admin_listen_legacy:=""}

# Old rc.conf-style variable names are also accepted for hand-written configs.
: ${rustproxy_proxy_listen:=""}
: ${rustproxy_tls_listen:=""}
: ${rustproxy_admin_listen:=""}

enabled="0"
config_file="/usr/local/etc/rustproxy/opnsense.conf"
if [ -r "$config_file" ]; then
    . "$config_file"
fi

if [ "$enabled" = "1" ]; then
    rustproxy_enable="YES"
else
    rustproxy_enable="NO"
fi

rustproxy_valid_port()
{
    case "$1" in
        ''|*[!0-9]*) return 1 ;;
    esac
    [ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null
}

rustproxy_valid_listener()
{
    _rp_listener="$1"
    case "$_rp_listener" in
        \[*\]:*) _rp_port="${_rp_listener##*:}" ;;
        *:*) _rp_port="${_rp_listener##*:}" ;;
        *) return 1 ;;
    esac
    rustproxy_valid_port "$_rp_port"
}

rustproxy_format_listener()
{
    _rp_address="$1"
    _rp_port="$2"
    case "$_rp_address" in
        \[*\]) printf '%s:%s\n' "$_rp_address" "$_rp_port" ;;
        *:*) printf '[%s]:%s\n' "$_rp_address" "$_rp_port" ;;
        *) printf '%s:%s\n' "$_rp_address" "$_rp_port" ;;
    esac
}

rustproxy_resolve_listener()
{
    _rp_address="$1"
    _rp_port="$2"
    _rp_legacy="$3"
    _rp_old_rc="$4"
    _rp_default_address="$5"
    _rp_default_port="$6"
    _rp_name="$7"

    # New split settings are authoritative as soon as either half is set.
    if [ -n "$_rp_address" ] || [ -n "$_rp_port" ]; then
        [ -n "$_rp_address" ] || _rp_address="$_rp_default_address"
        [ -n "$_rp_port" ] || _rp_port="$_rp_default_port"
        if ! rustproxy_valid_port "$_rp_port"; then
            echo "Invalid RustProxy ${_rp_name} port: ${_rp_port}" >&2
            return 1
        fi
        rustproxy_format_listener "$_rp_address" "$_rp_port"
        return 0
    fi

    # Preserve a valid listener from plugin 1.2.x during upgrade.
    if [ -n "$_rp_legacy" ] && rustproxy_valid_listener "$_rp_legacy"; then
        printf '%s\n' "$_rp_legacy"
        return 0
    fi

    # Preserve hand-written rc.conf settings as another compatibility path.
    if [ -n "$_rp_old_rc" ] && rustproxy_valid_listener "$_rp_old_rc"; then
        printf '%s\n' "$_rp_old_rc"
        return 0
    fi

    # Malformed legacy values such as "0.0.0.0:" must never reach RustProxy.
    # Fall back to a known-good listener and let the user set the separate port
    # field in Services -> RustProxy.
    rustproxy_format_listener "$_rp_default_address" "$_rp_default_port"
}

rustproxy_proxy_listen_resolved="$(rustproxy_resolve_listener \
    "$rustproxy_proxy_address" "$rustproxy_proxy_port" \
    "$rustproxy_proxy_listen_legacy" "$rustproxy_proxy_listen" \
    "0.0.0.0" "8080" "HTTP")" || exit 1

rustproxy_tls_listen_resolved="$(rustproxy_resolve_listener \
    "$rustproxy_tls_address" "$rustproxy_tls_port" \
    "$rustproxy_tls_listen_legacy" "$rustproxy_tls_listen" \
    "0.0.0.0" "8181" "HTTPS/TLS")" || exit 1

rustproxy_admin_listen_resolved="$(rustproxy_resolve_listener \
    "$rustproxy_admin_address" "$rustproxy_admin_port" \
    "$rustproxy_admin_listen_legacy" "$rustproxy_admin_listen" \
    "0.0.0.0" "9090" "admin")" || exit 1

rustproxy_runtime_args="--data-dir ${rustproxy_data_dir} --admin-listen ${rustproxy_admin_listen_resolved} --update-check-interval-secs 0 serve --proxy-listen ${rustproxy_proxy_listen_resolved}"
if [ "$rustproxy_tls_enabled" = "1" ]; then
    rustproxy_runtime_args="${rustproxy_runtime_args} --tls-listen ${rustproxy_tls_listen_resolved}"
fi
if [ "$rustproxy_allow_remote_admin" = "1" ]; then
    rustproxy_runtime_args="${rustproxy_runtime_args} --allow-remote-admin"
fi

pidfile="${rustproxy_run_dir}/daemon.pid"
child_pidfile="${rustproxy_run_dir}/rustproxy.pid"
command="/usr/sbin/daemon"

rustproxy_daemon_user_arg=""
if [ "$rustproxy_user" != "root" ]; then
    rustproxy_daemon_user_arg="-u ${rustproxy_user}"
fi
command_args="-r -R ${rustproxy_restart_delay} -P ${pidfile} -p ${child_pidfile} ${rustproxy_daemon_user_arg} -o ${rustproxy_log_file} ${rustproxy_command} ${rustproxy_runtime_args}"
required_files="${rustproxy_command}"
start_precmd="rustproxy_prestart"
start_postcmd="rustproxy_poststart"
stop_postcmd="rustproxy_poststop"
status_cmd="rustproxy_status"

rustproxy_prestart()
{
    # RustProxy on OPNsense/FreeBSD uses jemalloc.  MALLOC_ARENA_MAX is a
    # glibc/Linux setting and has no useful effect here, so export MALLOC_CONF
    # instead.  daemon(8) inherits this environment and passes it to RustProxy.
    if [ -n "${rustproxy_malloc_conf}" ]; then
        MALLOC_CONF="${rustproxy_malloc_conf}"
        export MALLOC_CONF
    else
        unset MALLOC_CONF
    fi

    # /var/run is ephemeral and recreated on boot.
    /usr/bin/install -d -o "${rustproxy_user}" -g "${rustproxy_group}" -m 0750 "${rustproxy_run_dir}"
    /usr/sbin/chown "${rustproxy_user}:${rustproxy_group}" "${rustproxy_run_dir}"
    /bin/chmod 0750 "${rustproxy_run_dir}"

    /usr/bin/install -d -o "${rustproxy_user}" -g "${rustproxy_group}" -m 0700 "${rustproxy_data_dir}"
    /usr/sbin/chown "${rustproxy_user}:${rustproxy_group}" "${rustproxy_data_dir}"
    /bin/chmod 0700 "${rustproxy_data_dir}"

    /usr/bin/install -d -o "${rustproxy_user}" -g "${rustproxy_group}" -m 0750 "${rustproxy_log_dir}"
    /usr/bin/touch "${rustproxy_log_file}"
    /usr/sbin/chown "${rustproxy_user}:${rustproxy_group}" "${rustproxy_log_file}"
    /bin/chmod 0640 "${rustproxy_log_file}"

    echo "RustProxy listeners: HTTP=${rustproxy_proxy_listen_resolved} TLS=${rustproxy_tls_listen_resolved} ADMIN=${rustproxy_admin_listen_resolved}"
    if [ -n "${rustproxy_malloc_conf}" ]; then
        echo "RustProxy allocator: MALLOC_CONF=${rustproxy_malloc_conf}"
    fi
    echo "RustProxy exec: ${rustproxy_command} ${rustproxy_runtime_args}"
    printf '%s\n' "${rustproxy_command} ${rustproxy_runtime_args}" > "${rustproxy_run_dir}/command.txt"
    /bin/chmod 0640 "${rustproxy_run_dir}/command.txt"
}

rustproxy_child_pid()
{
    [ -r "${child_pidfile}" ] || return 1
    _rp_pid="$(/bin/cat "${child_pidfile}" 2>/dev/null)"
    case "${_rp_pid}" in
        ''|*[!0-9]*) return 1 ;;
    esac
    /bin/kill -0 "${_rp_pid}" 2>/dev/null || return 1
    printf '%s\n' "${_rp_pid}"
}

rustproxy_poststart()
{
    # daemon(8) may stay alive with -r even when rust-proxy exits immediately.
    # Verify the supervised child so service/configd cannot report a false OK.
    /bin/sleep 1
    if _rp_pid="$(rustproxy_child_pid)"; then
        echo "RustProxy child started as PID ${_rp_pid}"
        return 0
    fi

    echo "RustProxy failed to stay running; stopping the restart supervisor." >&2
    if [ -s "${rustproxy_log_file}" ]; then
        echo "Last RustProxy log lines:" >&2
        /usr/bin/tail -n 40 "${rustproxy_log_file}" >&2
    fi
    if [ -r "${pidfile}" ]; then
        _rp_supervisor_pid="$(/bin/cat "${pidfile}" 2>/dev/null)"
        case "${_rp_supervisor_pid}" in
            ''|*[!0-9]*) ;;
            *) /bin/kill -TERM "${_rp_supervisor_pid}" 2>/dev/null || true ;;
        esac
    fi
    /bin/sleep 1
    /bin/rm -f "${child_pidfile}" "${pidfile}"
    return 1
}

rustproxy_status()
{
    if _rp_pid="$(rustproxy_child_pid)"; then
        echo "rustproxy is running as pid ${_rp_pid}"
        return 0
    fi

    _rp_supervisor_pid=""
    if [ -r "${pidfile}" ]; then
        _rp_supervisor_pid="$(/bin/cat "${pidfile}" 2>/dev/null)"
    fi
    case "${_rp_supervisor_pid}" in
        ''|*[!0-9]*) ;;
        *)
            if /bin/kill -0 "${_rp_supervisor_pid}" 2>/dev/null; then
                echo "rustproxy is not running (daemon supervisor ${_rp_supervisor_pid} is waiting/restarting)"
                return 1
            fi
            ;;
    esac

    echo "rustproxy is not running"
    return 1
}

rustproxy_poststop()
{
    /bin/rm -f "${child_pidfile}" "${pidfile}"
}

run_rc_command "$1"
