#!/bin/bash

SCRIPT_VER="2.3"

APP="OpenVPN"
APP_CATEGORY="System Services"
APP_SCOPE="system"
APP_ENABLED="yes"
SYS_PKG_NAME="openvpn"
STORED_PASSWORD_SERVICE="user"
SUPPORTED_ON="CentOS7,CentOS8,CentOSStream8,Debian10,Ubuntu20.04,Ubuntu22.04,Ubuntu24.04"
LIBRARY="functions4.sh"

function ovpn_runtime_dir () {
    echo "/run/openvpn"
}

function ovpn_service_name () {
    echo "rsb-openvpn"
}

function ovpn_legacy_service_name () {
    echo "openvpn@rsb-server"
}

function ovpn_service_unit_path () {
    echo "/etc/systemd/system/$( ovpn_service_name ).service"
}

function ovpn_legacy_service_unit_path () {
    echo "/etc/systemd/system/openvpn@.service"
}

function ovpn_monit_path () {
    echo "/etc/monit/conf.d/$( ovpn_service_name )"
}

function ovpn_legacy_monit_path () {
    echo "/etc/monit/conf.d/openvpn@rsb-server"
}

function ovpn_pid_file () {
    echo "$( ovpn_runtime_dir )/rsb-server.pid"
}

function ovpn_openvpn_version_family () {
    openvpn --version 2>/dev/null \
        | awk 'NR == 1 {print $2}' \
        | awk -F. '{print $1 "." $2}'
}

function ovpn_remove_legacy_repos () {
    local _f
    local _updated="no"

    case "${LSB_VENDOR}" in
      "Debian"|"Ubuntu")
          for _f in \
              "openvpn24.list" \
              "openvpn24.list.BAK" \
              "openvpn25.list" \
              "openvpn25.list.BAK" \
              "openvpn26.list" \
              "openvpn26.list.BAK" 
          do
              if [ -e "/etc/apt/sources.list.d/${_f}" ] ; then
                  RM_FILES "/etc/apt/sources.list.d/${_f}" || return 1
                  _updated="yes"
              fi
          done
          if [ "${_updated}" = "yes" ] ; then
              APT_UPDATE || return 1
          fi
          ;;
    esac

    return 0
}

function ovpn_is_legacy_rsb_conf () {
    local _f="$1"

    [ -e "${_f}" ] || return 1
    grep -q -E '^server 10\.10\.10\.0 255\.255\.255\.0$' "${_f}" || return 1
    grep -q -E '^ca /etc/openvpn/rsb-ca\.crt$' "${_f}" || return 1
    grep -q -E '^cert /etc/openvpn/rsb-server\.crt$' "${_f}" || return 1
    grep -q -E '^key /etc/openvpn/rsb-server\.key$' "${_f}" || return 1
    grep -q -E '^up /etc/openvpn/rsb-server-up\.sh$' "${_f}" || return 1
    grep -q -E '^down /etc/openvpn/rsb-server-down\.sh$' "${_f}" || return 1
}

function ovpn_has_known_rsb_ca () {
    local _ca_file="${1:-/etc/openvpn/rsb-ca.crt}"
    local _fp

    [ -e "${_ca_file}" ] || return 1
    _fp="$({ openssl x509 -noout -fingerprint -sha256 -in "${_ca_file}" 2>/dev/null || true; } | cut -d'=' -f2)"
    [ -n "${_fp}" ] || return 1

    case "${_fp}" in
      "7C:88:FD:42:04:10:51:25:66:3E:BF:9A:50:3F:AC:F0:38:98:51:74:A1:9B:55:5A:BC:03:1D:D9:65:7C:B8:71"|\
      "9B:5A:A1:A6:EC:D7:5A:02:9A:CD:D7:A2:EA:B5:07:B7:37:84:C9:99:D7:E1:DA:CE:B2:3E:81:72:05:44:B0:A2"|\
      "B9:F5:14:5C:00:4B:3E:3F:91:09:2C:D3:51:40:F6:55:0D:08:8F:F7:6A:E4:BE:2D:6B:93:DE:11:09:4A:61:34")
          return 0
          ;;
    esac

    return 1
}

function ovpn_is_rsb_conf () {
    local _f="$1"

    [ -e "${_f}" ] || return 1
    grep -q -E '^# RSB_OPENVPN_MANAGED=1$' "${_f}" && return 0
    ovpn_is_legacy_rsb_conf "${_f}" || return 1
    ovpn_has_known_rsb_ca "/etc/openvpn/rsb-ca.crt"
}

function ovpn_backup_existing_bundle () {
    local _backup_dir
    local _f

    _backup_dir="/etc/openvpn/rsb-backup-$( date -u +%Y%m%d%H%M%S )"

    MKDIR "${_backup_dir}" || return 1
    for _f in \
        "/etc/openvpn/dh4096.pem" \
        "/etc/openvpn/rsb-ca.crt" \
        "/etc/openvpn/rsb-server-acct.sh" \
        "/etc/openvpn/rsb-server-client-connect.sh" \
        "/etc/openvpn/rsb-server-client-disconnect.sh" \
        "/etc/openvpn/rsb-server.conf" \
        "/etc/openvpn/rsb-server.crt" \
        "/etc/openvpn/rsb-server-down.sh" \
        "/etc/openvpn/rsb-server.key" \
        "/etc/openvpn/rsb-server-up.sh" \
        "/etc/pam.d/openvpn" \
        "$( ovpn_service_unit_path )" \
        "$( ovpn_legacy_service_unit_path )" \
        "$( ovpn_monit_path )" \
        "$( ovpn_legacy_monit_path )"
    do
        [ -e "${_f}" ] || continue
        CP_FILE "${_f}" "${_backup_dir}/$( basename "${_f}" )" || return 1
    done

    return 0
}

function ovpn_guard_existing_bundle () {
    local _conf="/etc/openvpn/rsb-server.conf"

    [ -e "${_conf}" ] || return 0
    if ! ovpn_is_rsb_conf "${_conf}" ; then
        ERROR "OpenVPN: refusing to overwrite non-RSB config: ${_conf}"
        JSON_MSG="Existing non-RSB OpenVPN config detected at ${_conf}; refusing to overwrite"
        return 1
    fi

    ovpn_backup_existing_bundle || return 1
    return 0
}

function ovpn_get_plugin_path () {
    local _src

    for _src in \
        "/usr/lib/openvpn/openvpn-plugin-auth-pam.so" \
        "/usr/lib64/openvpn/plugins/openvpn-plugin-auth-pam.so" \
        "/usr/lib/x86_64-linux-gnu/openvpn/plugins/openvpn-plugin-auth-pam.so" \
        "/usr/lib/openvpn/plugins/openvpn-plugin-auth-pam.so" 
    do
        [ -e "${_src}" ] && echo "${_src}" && return 0
    done

    return 1
}

function ovpn_ensure_plugin_path () {
    local _src
    local _compat="/usr/lib/openvpn/openvpn-plugin-auth-pam.so"

    _src="$( ovpn_get_plugin_path )" || return 1
    if [ "${_src}" != "${_compat}" ] ; then
        [ -d "/usr/lib/openvpn" ] || MKDIR "/usr/lib/openvpn" || return 1
        [ -L "${_compat}" ] && RM_FILES "${_compat}"
        [ -e "${_compat}" ] || LN "${_src}" "${_compat}" || return 1
    fi

    echo "${_compat}"
    return 0
}

function ovpn_get_iptables () {
    local _iptables

    _iptables="$( command -v iptables 2>/dev/null )"
    if [ -n "${_iptables}" ] ; then
        echo "${_iptables}"
        return 0
    fi

    for _iptables in \
        "/usr/sbin/iptables" \
        "/sbin/iptables" \
        "/usr/bin/iptables" 
    do
        [ -x "${_iptables}" ] && echo "${_iptables}" && return 0
    done

    return 1
}

function ovpn_write_pam_config () {
    case "${LSB_VENDOR}" in
      "CentOS"|"CentOSStream")
          cat <<-EOF_PAM > "/etc/pam.d/openvpn"
		#%PAM-1.0
		auth       include      system-auth
		account    include      system-auth
		password   include      system-auth
		session    optional     pam_keyinit.so revoke
		session    include      system-auth
		EOF_PAM
          ;;
    esac

    return 0
}

function ovpn_install_systemd_unit () {
    local _runtime_dir
    local _unit_file

    _runtime_dir="$( ovpn_runtime_dir )"
    _unit_file="$( ovpn_service_unit_path )"

    cat <<-EOF_UNIT > "${_unit_file}"
	[Unit]
	Description=RapidSeedbox OpenVPN
	After=network-online.target
	Wants=network-online.target
	Documentation=man:openvpn(8)

	[Service]
	PrivateTmp=true
	KillMode=mixed
	Type=forking
	RuntimeDirectory=openvpn
	RuntimeDirectoryMode=0755
	ExecStart=/usr/sbin/openvpn --daemon ovpn-rsb-server --status ${_runtime_dir}/rsb-server.status 10 --cd /etc/openvpn --script-security 2 --config /etc/openvpn/rsb-server.conf --writepid ${_runtime_dir}/rsb-server.pid
	PIDFile=${_runtime_dir}/rsb-server.pid
	ExecReload=/bin/kill -HUP \$MAINPID
	WorkingDirectory=/etc/openvpn
	ProtectSystem=yes
	CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_READ_SEARCH CAP_AUDIT_WRITE
	DeviceAllow=/dev/null rw
	DeviceAllow=/dev/net/tun rw

	[Install]
	WantedBy=multi-user.target
	EOF_UNIT

    SYSTEMD_RELOAD
    return 0
}

function ovpn_apply_cipher_block () {
    local _mm
    local _tmp_conf="${TMP_DIR}/rsb-server.conf"

    _mm="$( ovpn_openvpn_version_family )"
    [ -z "${_mm}" ] && _mm="2.4"

    awk -v mm="${_mm}" '
        /^# RSB_CIPHER_BLOCK_BEGIN$/ {
            print
            print "cipher AES-256-GCM"
            if (mm == "2.4") {
                print "ncp-ciphers AES-256-GCM:AES-128-GCM"
            } else {
                print "data-ciphers AES-256-GCM:AES-128-GCM"
            }
            skip=1
            next
        }
        /^# RSB_CIPHER_BLOCK_END$/ {
            skip=0
            print
            next
        }
        !skip {print}
    ' /etc/openvpn/rsb-server.conf > "${_tmp_conf}" || return 1
    MV "${_tmp_conf}" "/etc/openvpn/rsb-server.conf" || return 1

    return 0
}

function ovpn_configure_server () {
    local _f
    local _iptables
    local _pid_file
    local _plugin

    ovpn_guard_existing_bundle || return 1

    _plugin="$( ovpn_ensure_plugin_path )" || {
        ERROR "Could not find OpenVPN PAM auth plugin"
        return 1
    }
    _iptables="$( ovpn_get_iptables )" || {
        ERROR "Could not find iptables binary"
        return 1
    }
    _pid_file="$( ovpn_pid_file )"

    WGET "${SUPPORT}/openvpn/openvpn-rsb-config.tar.gz" || return 1
    TAR_ZXF "${TMP_DIR}/openvpn-rsb-config.tar.gz" "/etc/" || return 1
    ovpn_write_pam_config || return 1
    ovpn_apply_cipher_block || return 1

    RUN_LOG "sed -i -E 's|^plugin[[:space:]].*openvpn-plugin-auth-pam\\.so.*$|plugin ${_plugin} openvpn|' /etc/openvpn/rsb-server.conf" \
        || return 1
    RUN_LOG "sed -i -e 's|iptables|${_iptables}|g' /etc/openvpn/rsb-server-up.sh" \
        || return 1
    RUN_LOG "sed -i -e 's|iptables|${_iptables}|g' /etc/openvpn/rsb-server-down.sh" \
        || return 1
    RUN_LOG "sed -i -e 's|venet0|${DEV}|g' /etc/openvpn/rsb-server-up.sh" \
        || return 1
    RUN_LOG "sed -i -e 's|venet0|${DEV}|g' /etc/openvpn/rsb-server-down.sh" \
        || return 1
    RUN_LOG "sed -i -e 's|^writepid\\s*.*$|writepid ${_pid_file}|' /etc/openvpn/rsb-server.conf" \
        || return 1
    RUN_LOG "sed -i -re '/^status[[:space:]]+/d' /etc/openvpn/rsb-server.conf" \
        || return 1

    for _f in \
        "/etc/openvpn/dh4096.pem" \
        "/etc/openvpn/rsb-ca.crt" \
        "/etc/openvpn/rsb-server.conf" \
        "/etc/openvpn/rsb-server.crt" \
        "/etc/openvpn/rsb-server-down.sh" \
        "/etc/openvpn/rsb-server.key" \
        "/etc/openvpn/rsb-server-up.sh" \
    ; do
        [ -e "${_f}" ] || continue
        CHOWN_FILE root:root "${_f}" || return 1
    done
    [ -e "/etc/pam.d/openvpn" ] && CHOWN_FILE root:root "/etc/pam.d/openvpn"
    [ -e "/etc/openvpn/rsb-server.key" ] && CHMOD_FILE 0600 "/etc/openvpn/rsb-server.key"
    [ -e "/etc/openvpn/rsb-server-up.sh" ] && CHMOD_FILE 0700 "/etc/openvpn/rsb-server-up.sh"
    [ -e "/etc/openvpn/rsb-server-down.sh" ] && CHMOD_FILE 0700 "/etc/openvpn/rsb-server-down.sh"

    ovpn_install_systemd_unit || return 1
    return 0
}

function ovpn_install_monit () {
    local _pid_file
    local _monit_file
    local _service

    _pid_file="$( ovpn_pid_file )"
    _monit_file="$( ovpn_monit_path )"
    _service="$( ovpn_service_name )"
    if [ -d "/etc/monit/conf.d" ] ; then
        cat <<-EOF_MONIT > "${_monit_file}"
		check process ${_service} with pidfile ${_pid_file}
		  start program = "/usr/sbin/service ${_service} restart"
		  stop  program = "/usr/sbin/service ${_service} stop"
		  if 5 restarts with 5 cycles then timeout
		EOF_MONIT
        MONIT_RELOAD
    fi

    return 0
}

function ovpn_stop_service () {
    local _service
    local _legacy_service
    local _monit_file
    local _legacy_monit_file

    _service="$( ovpn_service_name )"
    _legacy_service="$( ovpn_legacy_service_name )"
    _monit_file="$( ovpn_monit_path )"
    _legacy_monit_file="$( ovpn_legacy_monit_path )"

    [ -e "${_monit_file}" ] && RM_FILES "${_monit_file}"
    [ -e "${_legacy_monit_file}" ] && RM_FILES "${_legacy_monit_file}"
    [ -d "/etc/monit/conf.d" ] && MONIT_RELOAD

    SVC_STOP    "${_service}"
    SVC_DISABLE "${_service}"
    SVC_STOP    "${_legacy_service}"
    SVC_DISABLE "${_legacy_service}"

    return 0
}

function ovpn_remove_systemd_unit () {
    local _removed="no"
    local _unit_file
    local _legacy_unit_file

    _unit_file="$( ovpn_service_unit_path )"
    _legacy_unit_file="$( ovpn_legacy_service_unit_path )"

    if [ -e "${_unit_file}" ] ; then
        RM_FILES "${_unit_file}"
        _removed="yes"
    fi
    if [ -e "${_legacy_unit_file}" ] ; then
        RM_FILES "${_legacy_unit_file}"
        _removed="yes"
    fi

    [ "${_removed}" = "yes" ] && SYSTEMD_RELOAD
    return 0
}

function ovpn_cleanup_files () {
    local _f

    for _f in \
        "/etc/openvpn/dh4096.pem" \
        "/etc/openvpn/rsb-ca.crt" \
        "/etc/openvpn/rsb-server-acct.sh" \
        "/etc/openvpn/rsb-server-client-connect.sh" \
        "/etc/openvpn/rsb-server-client-disconnect.sh" \
        "/etc/openvpn/rsb-server.conf" \
        "/etc/openvpn/rsb-server.crt" \
        "/etc/openvpn/rsb-server-down.sh" \
        "/etc/openvpn/rsb-server.key" \
        "/etc/openvpn/rsb-server-up.sh" \
        "/etc/pam.d/openvpn"
    do
        [ -e "${_f}" ] && RM_FILES "${_f}"
    done

    rmdir "/etc/openvpn" > /dev/null 2>&1 || true
    return 0
}

if [ "${SUPPORT-}" = '' ]; then
    SUPPORT="https://installers.rapidseedbox.com/scripts/whmcs_client_installers/support"
fi

LOGFILE="/var/log/whmcs_client_installers/${0}.log"
[ ! -d "/var/log/whmcs_client_installers" ] && mkdir -p "/var/log/whmcs_client_installers"

wget -4 -nc -q --no-check-certificate --timeout=20 --tries=5 "${SUPPORT}/${LIBRARY}" >> "${LOGFILE}" 2>&1
if [ ! -f "./${LIBRARY}" ] ; then
    echo "55"
    echo "Error downloading \"${SUPPORT}/${LIBRARY}\"" >> "${LOGFILE}"
    exit 55
fi

# shellcheck source=support/functions4.sh
. "./${LIBRARY}"
SCRIPTENTRY

while : ; do

    main_rc=1

    CHECK_APP ; _rc=$?
    if [ ${_rc} -ne 0 ] ; then
        main_rc=${_rc}
        break
    fi

    ovpn_remove_legacy_repos || break

    case "${LSB_VENDOR}" in
      "CentOS"|"CentOSStream")
          YUM_INSTALL "epel-release" || break
          ;;
    esac

    ${PKG_INSTALL} "openvpn" "easy-rsa" || break
    ovpn_configure_server || break

    SVC_ENABLE "$( ovpn_service_name )"
    SVC_START  "$( ovpn_service_name )"
    ovpn_install_monit || break

    main_rc=0
    break

done


if [ "${main_rc}" -ne 0 ] && [ "${main_rc}" -ne 111 ] && [ "${main_rc}" -ne 117 ] ; then
    ovpn_stop_service
    ${PKG_REMOVE} "openvpn"
    ${PKG_AUTOREMOVE}
    ovpn_cleanup_files
    ovpn_remove_systemd_unit
    ovpn_remove_legacy_repos
fi

SCRIPTEXIT
