Procházet zdrojové kódy

Direct access from the browser to the configuration files of the openvpn server has been completely removed.
Added deletion of the client's ccd file

root před 7 měsíci
rodič
revize
65605af119

+ 27 - 13
README.md

@@ -7,8 +7,20 @@
 - Отображение подключенных пользователей в реальном времени
 - Управление доступом:
   - Блокировка/разблокировка пользователей (ban/unban)
-- Генерация конфигурационных файлов для клиентов
-- Автоматическое обновление данных (каждые 60 секунд)
+  - Создание сертификата клиента
+  - Удаление конфигурационного файла клиента
+  - Отзыв сертификата клиента
+- Генерация конфигурационных файлов для клиентов (открыть ссылку /ccd в браузере)
+- Автоматическое обновление данных (каждые 30 секунд)
+
+## !!!Важно!!!
+
+Каталог /ccd надо либо не устанавливать вообще, либо закрыть паролем через вэб-сервер. 
+Пользователи, которым выдаёте доступ должны совпадать с логином клиента в впн-сервере. 
+Тогда пользователь при обращении на данную ссылку сможет получить свой конфигурационный файл.
+Базу авторизации делать не стал специально - проще использовать авторизацию в вэб-сервере.
+
+Каталог /admin - закрывайте паролем обязательно. 
 
 ## Требования
 
@@ -22,7 +34,7 @@
 ###  Установите необходимые пакеты:
 
 ```bash
-apt install apache2 php
+apt install apache2 php php-mbstring
 a2enmod session
 ```
 
@@ -32,24 +44,22 @@ echo "management 127.0.0.1 3003 /etc/openvpn/server/password" >> /etc/openvpn/se
 echo "your_password" > /etc/openvpn/server/password
 ```
 
-### Настройте права доступа:
-```bash
-chmod 775 /etc/openvpn/server/server1/ccd
-chown nobody:www-data -R /etc/openvpn/server/server1/ccd
-chmod 644 /etc/openvpn/server/server1/ipp.txt
-chmod 644 /etc/openvpn/server/server1/rsa/pki/index.txt
-```
-
 ### Установите скрипты:
 ```bash
 cp addons/sudoers.d/www-data /etc/sudoers.d/
-cp addons/show_client_crt.sh /etc/openvpn/server/
-chmod 555 /etc/openvpn/server/show_client_crt.sh
+mkdir -p /etc/openvpn/server/cmd
+cp addons/cmd/*.sh /etc/openvpn/server/cmd/
+chmod 755 /etc/openvpn/server/cmd/
+chmod 555 /etc/openvpn/server/cmd/*.sh
 ```
 ### Создайте шаблон конфигурации клиента (без сертификатов) в каталоге сайта.
 
 ### Отредактируйте файл конфигурации config.php
 
+Здесь name - это имя сервиса в systemd!!! т.е. если сервис называется openvpn-server@server1, то name=server1
+
+Если какие-то опции не используются - значение долно быть пустым
+
 ```php
 'server1' => [
     'name' => 'server1',
@@ -74,4 +84,8 @@ Ban - заблокировать пользователя
 
 Unban - разблокировать пользователя
 
+Revoke - отозвать сертификат
+
+Remove - удалить ccd-файл конфигурации пользователя
+
 Для скачивания конфигурации клиента нажмите на имя пользователя

+ 102 - 0
addons/cmd/ban_client.sh

@@ -0,0 +1,102 @@
+#!/bin/bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+log() {
+    logger -t "openvpn-ban" -p user.info "$1"
+    echo "$1"  # Также выводим в консоль для обратной связи
+}
+
+show_usage() {
+    echo "Usage: $0 <ccd_file> <ban|unban>"
+    echo "Example: $0 /etc/openvpn/server/server/ccd/login ban"
+    exit 1
+}
+
+# Проверка прав
+check_permissions() {
+    if [[ $EUID -ne 0 ]]; then
+        log "Error: This script must be run as root" >&2
+        exit 1
+    fi
+}
+
+# Проверка что CCD файл находится в правильном пути
+check_ccd_path() {
+    local ccd_file=$1
+    local expected_path="/etc/openvpn/server"
+    
+    # Проверяем что путь начинается с /etc/openvpn/server/
+    if [[ ! "$ccd_file" =~ ^$expected_path/ ]]; then
+        log "Error: CCD file must be located under $expected_path/"
+        log "Provided path: $ccd_file"
+        exit 1
+    fi
+    
+    # Дополнительная проверка: файл должен существовать
+    if [[ ! -f "$ccd_file" ]]; then
+        log "Error: CCD file does not exist: $ccd_file"
+        exit 1
+    fi
+    
+    # Проверка прав на запись
+    if [[ ! -w "$ccd_file" ]]; then
+        log "Error: No write permission for CCD file: $ccd_file"
+        exit 1
+    fi
+}
+
+main() {
+    # Проверка прав
+    check_permissions
+
+    # Обработка аргументов
+    [[ $# -lt 2 ]] && show_usage
+
+    local ccd_file=$1
+    local action=$2
+    
+    # Проверка пути CCD файла
+    check_ccd_path "$ccd_file"
+
+    local username=$(basename "${ccd_file}")
+
+    touch "${ccd_file}"
+    chmod 640 "${ccd_file}"
+    chown nobody:nogroup "${ccd_file}"
+
+    if grep -q "^disable$" "$ccd_file"; then
+        is_banned="disable"
+    else
+        is_banned=""
+    fi
+
+    case "$action" in
+        ban)
+            if [[ -z "$is_banned" ]]; then
+                log "Ban user: ${username}"
+		sed -i '1i\disable' "${ccd_file}"
+                log "User ${username} banned successfully"
+            else
+                log "User ${username} is already banned"
+            fi
+            ;;
+        unban)
+            if [[ -n "$is_banned" ]]; then
+                log "UnBan user: ${username}"
+                sed -i '/^disable$/d' "${ccd_file}"
+                log "User ${username} unbanned successfully"
+            else
+                log "User ${username} is not banned"
+            fi
+            ;;
+        *)
+            log "Error: Invalid action. Use 'ban' or 'unban'" >&2
+            show_usage
+            ;;
+    esac
+}
+
+main "$@"

+ 22 - 10
addons/cmd/create_client.sh

@@ -1,33 +1,43 @@
 #!/bin/bash
 
+log() {
+    logger -t "openvpn-create" -p user.info "$1"
+    echo "$1"  # Также выводим в консоль для обратной связи
+}
+
+# Проверка прав
+check_permissions() {
+    if [[ $EUID -ne 0 ]]; then
+        log "Error: This script must be run as root" >&2
+        exit 1
+    fi
+}
+
 if [ $# -ne 2 ]; then
     echo "Usage: $0 <rsa_dir> <username>"
     exit 1
 fi
 
+check_permissions
+
 RSA_DIR="$1"
 USERNAME="$2"
 
-ORIGINAL_USER="$SUDO_USER"
-if [ -z "${ORIGINAL_USER}" ]; then
-    ORIGINAL_USER='www-data'
-    fi
-
 # Проверяем существование директории PKI
 if [ ! -d "$RSA_DIR" ]; then
-    echo "PKI directory not found: $RSA_DIR"
+    log "PKI directory not found: $RSA_DIR"
     exit 1
 fi
 
 # Проверяем наличие easyrsa
 if [ ! -f "$RSA_DIR/easyrsa" ]; then
-    echo "easyrsa not found in $RSA_DIR"
+    log "easyrsa not found in $RSA_DIR"
     exit 1
 fi
 
 # Проверяем, не существует ли уже пользователь
 if [ -f "$RSA_DIR/pki/index.txt" ] && grep -q "CN=$USERNAME" "$RSA_DIR/pki/index.txt"; then
-    echo "User $USERNAME already exists"
+    log "User $USERNAME already exists"
     exit 1
 fi
 
@@ -38,9 +48,11 @@ cd "$RSA_DIR" || exit 1
 ./easyrsa --batch build-client-full "$USERNAME" nopass
 
 if [ $? -eq 0 ]; then
-    echo "User $USERNAME created successfully"
-    chown nobody:${ORIGINAL_USER} -R "$RSA_DIR/pki/issued/"
+    log "User $USERNAME created successfully"
+    chown nobody:nogroup -R "$RSA_DIR/pki/issued/"
     chmod 640 "${RSA_DIR}"/pki/issued/*.crt
+    chown nobody:nogroup -R "$RSA_DIR/pki/private/"
+    chmod 640 "${RSA_DIR}"/pki/private/*.key
     exit 0
 else
     echo "Failed to create user $USERNAME"

+ 61 - 0
addons/cmd/remove_ccd.sh

@@ -0,0 +1,61 @@
+#!/bin/bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+log() {
+    logger -t "openvpn-ban" -p user.info "$1"
+    echo "$1"  # Также выводим в консоль для обратной связи
+}
+
+show_usage() {
+    echo "Usage: $0 <ccd_file>"
+    echo "Example: $0 /etc/openvpn/server/server/ccd/login"
+    exit 1
+}
+
+# Проверка прав
+check_permissions() {
+    if [[ $EUID -ne 0 ]]; then
+        log "Error: This script must be run as root" >&2
+        exit 1
+    fi
+}
+
+# Проверка что CCD файл находится в правильном пути
+check_ccd_path() {
+    local ccd_file=$1
+    local expected_path="/etc/openvpn/server"
+
+    # Проверяем что путь начинается с /etc/openvpn/server/
+    if [[ ! "$ccd_file" =~ ^$expected_path/ ]]; then
+        log "Error: CCD file must be located under $expected_path/"
+        log "Provided path: $ccd_file"
+        exit 1
+    fi
+
+    # Дополнительная проверка: файл должен существовать
+    if [[ ! -f "$ccd_file" ]]; then
+        log "Error: CCD file does not exist: $ccd_file"
+        exit 0
+    fi
+}
+
+main() {
+    # Проверка прав
+    check_permissions
+
+    # Обработка аргументов
+    [[ $# -lt 1 ]] && show_usage
+
+    local ccd_file=$1
+    
+    # Проверка пути CCD файла
+    check_ccd_path "$ccd_file"
+
+    #remove file
+    rm -f "${ccd_file}"
+}
+
+main "$@"

+ 14 - 9
addons/cmd/revoke_client.sh

@@ -6,20 +6,25 @@ log() {
     echo "$1"  # Также выводим в консоль для обратной связи
 }
 
+# Проверка прав
+check_permissions() {
+    if [[ $EUID -ne 0 ]]; then
+        log "Error: This script must be run as root" >&2
+        exit 1
+    fi
+}
+
 if [ $# -ne 3 ]; then
     log "Usage: $0 <service_name> <rsa_dir> <username>"
     exit 1
 fi
 
+check_permissions
+
 SRV_NAME="${1}"
 RSA_DIR="${2}"
 USERNAME="${3}"
 
-ORIGINAL_USER="$SUDO_USER"
-if [ -z "${ORIGINAL_USER}" ]; then
-    ORIGINAL_USER='www-data'
-fi
-
 log "Starting certificate revocation for $USERNAME by user $ORIGINAL_USER"
 
 # Проверяем существование директории RSA
@@ -61,13 +66,13 @@ if [ $? -eq 0 ]; then
     log "Generating CRL..."
     ./easyrsa --batch gen-crl
 
-    chown nobody:${ORIGINAL_USER} -R "$RSA_DIR/pki/issued/"
-    chown nobody:nogroup -R "$RSA_DIR/pki/crl.pem"
-    chmod 640 "${RSA_DIR}"/pki/issued/*.crt
-
     if [ $? -eq 0 ]; then
         log "CRL generated successfully"
 
+        chown nobody:nogroup -R "$RSA_DIR/pki/issued/"
+	chown nobody:nogroup -R "$RSA_DIR/pki/crl.pem"
+        chmod 640 "${RSA_DIR}"/pki/issued/*.crt
+
         # Рестартуем сервис
         log "Restarting service: $SRV_NAME"
         systemctl restart "${SRV_NAME}"

+ 56 - 0
addons/cmd/show_banned.sh

@@ -0,0 +1,56 @@
+#!/bin/bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+show_usage() {
+    echo "Usage: $0 <ccd_dir>"
+    echo "Example: $0 /etc/openvpn/server/server/ccd"
+    exit 1
+}
+
+# Проверка прав
+check_permissions() {
+    if [[ $EUID -ne 0 ]]; then
+        log "Error: This script must be run as root" >&2
+        exit 1
+    fi
+}
+
+# Проверка что CCD файл находится в правильном пути
+check_ccd_path() {
+    local ccd_file=$1
+    local expected_path="/etc/openvpn/server"
+
+    # Проверяем что путь начинается с /etc/openvpn/server/
+    if [[ ! "$ccd_file" =~ ^$expected_path/ ]]; then
+        log "Error: CCD must be located under $expected_path/"
+        log "Provided path: $ccd_file"
+        exit 1
+    fi
+
+    # Дополнительная проверка: каталог должен существовать
+    if [[ ! -d "$ccd_file" ]]; then
+        log "Error: file does not exist: $ccd_file"
+        exit 1
+    fi
+}
+
+main() {
+    # Проверка прав
+    check_permissions
+
+    # Обработка аргументов
+    [[ $# -lt 1 ]] && show_usage
+
+    local ccd_dir=$1
+    
+    # Проверка пути CCD файла
+    check_ccd_path "$ccd_dir"
+
+    #get banned
+    egrep "^disable$" -R "${ccd_dir}"/* | sed 's#.*/##; s/:.*//'
+}
+
+main "$@"

+ 57 - 0
addons/cmd/show_client_ccd.sh

@@ -0,0 +1,57 @@
+#!/bin/bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+show_usage() {
+    echo "Usage: $0 <ccd_dir>"
+    echo "Example: $0 /etc/openvpn/server/server/ccd"
+    exit 1
+}
+
+# Проверка прав
+check_permissions() {
+    if [[ $EUID -ne 0 ]]; then
+        log "Error: This script must be run as root" >&2
+        exit 1
+    fi
+}
+
+# Проверка что CCD файл находится в правильном пути
+check_ccd_path() {
+    local ccd_file=$1
+    local expected_path="/etc/openvpn/server"
+
+    # Проверяем что путь начинается с /etc/openvpn/server/
+    if [[ ! "$ccd_file" =~ ^$expected_path/ ]]; then
+        log "Error: CCD must be located under $expected_path/"
+        log "Provided path: $ccd_file"
+        exit 1
+    fi
+
+    # Дополнительная проверка: каталог должен существовать
+    if [[ ! -d "$ccd_file" ]]; then
+        log "Error: file does not exist: $ccd_file"
+        exit 1
+    fi
+}
+
+main() {
+    # Проверка прав
+    check_permissions
+
+    # Обработка аргументов
+    [[ $# -lt 1 ]] && show_usage
+
+    local ccd_dir=$1
+    
+    # Проверка пути CCD файла
+    check_ccd_path "$ccd_dir"
+
+    #get client ips
+    egrep "^ifconfig-push\s+" "${ccd_dir}"/* | sed 's|.*/||; s/:ifconfig-push / /; s/\([^ ]* [^ ]*\).*/\1/'
+
+}
+
+main "$@"

+ 18 - 4
addons/cmd/show_client_crt.sh

@@ -10,10 +10,23 @@ show_usage() {
     exit 1
 }
 
+log() {
+    logger -t "openvpn-www" -p user.info "$1"
+    echo "$1"  # Также выводим в консоль для обратной связи
+}
+
+# Проверка прав
+check_permissions() {
+    if [[ $EUID -ne 0 ]]; then
+        log "Error: This script must be run as root" >&2
+        exit 1
+    fi
+}
+
 validate_pki_dir() {
     local pki_dir=$1
     if [[ ! -d "${pki_dir}" || ! -f "${pki_dir}/index.txt" ]]; then
-        echo "Error: Invalid PKI directory - missing index.txt" >&2
+        log "Error: Invalid PKI directory - missing index.txt"
         exit 2
     fi
 }
@@ -56,6 +69,8 @@ main() {
     # Argument handling
     [[ $# -lt 1 ]] && show_usage
     
+    check_permissions
+
     local CN=$1
     local PKI_DIR=${2:-/etc/openvpn/server/server/rsa/pki}
     
@@ -64,7 +79,7 @@ main() {
     # Find certificate
     local CERT_FILE
     CERT_FILE=$(find_cert_file "${CN}" "${PKI_DIR}") || {
-        echo "Error: Certificate for CN=${CN} not found" >&2
+        log "Error: Certificate for CN=${CN} not found"
         exit 3
     }
     
@@ -75,13 +90,12 @@ main() {
     # Find private key
     local KEY_FILE
     KEY_FILE=$(find_key_file "${CN}" "${PKI_DIR}" "${SERIAL}") || {
-        echo "Error: Private key for CN=${CN} not found" >&2
+        log "Error: Private key for CN=${CN} not found"
         exit 4
     }
     
     # Output results
     echo "<cert>"
-#    openssl x509 -in "${CERT_FILE}" -notext
     openssl x509 -in "${CERT_FILE}"
     echo "</cert>"
     echo

+ 57 - 0
addons/cmd/show_client_ipp.sh

@@ -0,0 +1,57 @@
+#!/bin/bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+show_usage() {
+    echo "Usage: $0 <ipp_file>"
+    echo "Example: $0 /etc/openvpn/server/server/ipp.txt"
+    exit 1
+}
+
+# Проверка прав
+check_permissions() {
+    if [[ $EUID -ne 0 ]]; then
+        log "Error: This script must be run as root" >&2
+        exit 1
+    fi
+}
+
+# Проверка что CCD файл находится в правильном пути
+check_ccd_path() {
+    local ipp_file=$1
+    local expected_path="/etc/openvpn/server"
+
+    # Проверяем что путь начинается с /etc/openvpn/server/
+    if [[ ! "$ipp_file" =~ ^$expected_path/ ]]; then
+        log "Error: IPP must be located under $expected_path/"
+        log "Provided path: $ipp_file"
+        exit 1
+    fi
+
+    # Дополнительная проверка: каталог должен существовать
+    if [[ ! -e "$ipp_file" ]]; then
+        log "Error: file does not exist: $ipp_file"
+        exit 1
+    fi
+}
+
+main() {
+    # Проверка прав
+    check_permissions
+
+    # Обработка аргументов
+    [[ $# -lt 1 ]] && show_usage
+
+    local ipp_file=$1
+    
+    # Проверка пути CCD файла
+    check_ccd_path "$ipp_file"
+
+    #get client ips
+    cat "${ipp_file}" | sed 's/,$//'
+
+}
+
+main "$@"

+ 32 - 13
addons/cmd/show_index.sh

@@ -6,26 +6,45 @@ set -o pipefail
 
 show_usage() {
     echo "Usage: $0 [path_to_index.txt]"
+    echo "Default index_txt: /etc/openvpn/server/server/rsa/pki/index.txt"
     exit 1
 }
 
-# Argument handling
-[[ $# -lt 1 ]] && show_usage
+log() {
+    logger -t "openvpn-www" -p user.info "$1"
+    echo "$1"  # Также выводим в консоль для обратной связи
+}
 
-index_txt="${1}"
+# Проверка прав
+check_permissions() {
+    if [[ $EUID -ne 0 ]]; then
+        log "Error: This script must be run as root" >&2
+        exit 1
+    fi
+}
 
-ORIGINAL_USER="$SUDO_USER"
-if [ -z "${ORIGINAL_USER}" ]; then
-    ORIGINAL_USER='www-data'
+validate_pki_dir() {
+    local pki_dir=$1
+    if [[ ! -d "${pki_dir}" || ! -f "${pki_dir}/index.txt" ]]; then
+        log "Error: Invalid PKI directory - missing index.txt"
+        exit 2
     fi
+}
+
+main() {
+    # Argument handling
+    [[ $# -lt 1 ]] && show_usage
 
-[ -e "${index_txt}" ] && cat "${index_txt}" || exit 1
+    check_permissions
 
-PKI_DIR=$(dirname "${index_txt}")  # /etc/openvpn/server/server/rsa/pki
-RSA_DIR=$(dirname "${PKI_DIR}")    # /etc/openvpn/server/server/rsa
+    PKI_DIR=$(dirname "${1}")
 
-chown nobody:${ORIGINAL_USER} -R "$RSA_DIR/pki/issued/"
-chmod 750 "${RSA_DIR}/pki/issued/"
-chmod 640 "${RSA_DIR}"/pki/issued/*.crt
+    validate_pki_dir "${PKI_DIR}"
+
+    index_txt="${PKI_DIR}/index.txt"
+
+    [ -e "${index_txt}" ] && cat "${index_txt}" || exit 1
+
+}
 
-exit 0
+main "$@"

+ 58 - 0
addons/cmd/show_servers_crt.sh

@@ -0,0 +1,58 @@
+#!/bin/bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+show_usage() {
+    echo "Usage: $0 <index.txt>"
+    echo "Default index_txt: /etc/openvpn/server/server/rsa/pki/index.txt"
+    exit 1
+}
+
+log() {
+    logger -t "openvpn-www" -p user.info "$1"
+    echo "$1"  # Также выводим в консоль для обратной связи
+}
+
+# Проверка прав
+check_permissions() {
+    if [[ $EUID -ne 0 ]]; then
+        log "Error: This script must be run as root" >&2
+        exit 1
+    fi
+}
+
+validate_pki_dir() {
+    local pki_dir=$1
+    if [[ ! -d "${pki_dir}" || ! -f "${pki_dir}/index.txt" ]]; then
+        log "Error: Invalid PKI directory - missing index.txt"
+        exit 2
+    fi
+}
+
+main() {
+    # Argument handling
+    [[ $# -lt 1 ]] && show_usage
+
+    check_permissions
+
+    PKI_DIR=$(dirname "${1}")
+
+    validate_pki_dir "${PKI_DIR}"
+
+    find "${PKI_DIR}/issued/" \( -name "*.crt" -o -name "*.pem" -o -name "*.cer" \) -print0 | while IFS= read -r -d '' cert; do
+        # Одновременно получаем subject и проверяем расширения
+	openssl_output=$(openssl x509 -in "$cert" -subject -noout -ext extendedKeyUsage -purpose 2>/dev/null)
+        username=$(basename "${cert}" | sed 's/\.[^.]*$//')
+	CN=$(echo "$openssl_output" | grep 'subject=' | sed 's/.*CN=//;s/,.*//')
+        # Проверяем расширения из одного вывода openssl
+	if echo "$openssl_output" | grep -q "TLS Web Server Authentication\|serverAuth" || 
+    	    echo "$openssl_output" | grep -q "SSL server : Yes"; then
+            echo "$username"
+	    [ "${username}" != "${CN}" ] && echo "$CN"
+	    fi
+    done
+}
+
+main "$@"

+ 8 - 2
addons/sudoers.d/www-data

@@ -1,4 +1,10 @@
-www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_client_crt.sh
-www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_index.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/ban_client.sh
 www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/create_client.sh
 www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/revoke_client.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/remove_ccd.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_banned.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_client_crt.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_client_ccd.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_client_ipp.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_index.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_servers_crt.sh

+ 8 - 1
html/admin/config.php

@@ -2,15 +2,22 @@
 
 defined('CONFIG') or die('Direct access not allowed');
 
-define('REQUEST_INTERVAL', 60);
+define('REQUEST_INTERVAL', 30);
 define('SHOW_CERT_SCRIPT','/etc/openvpn/server/cmd/show_client_crt.sh');
 define('SHOW_PKI_INDEX','/etc/openvpn/server/cmd/show_index.sh');
 define('CREATE_CRT','/etc/openvpn/server/cmd/create_client.sh');
 define('REVOKE_CRT','/etc/openvpn/server/cmd/revoke_client.sh');
+define('SHOW_SERVERS_CRT','/etc/openvpn/server/cmd/show_servers_crt.sh');
+define('BAN_CLIENT','/etc/openvpn/server/cmd/ban_client.sh');
+define('SHOW_BANNED','/etc/openvpn/server/cmd/show_banned.sh');
+define('GET_IPS_FROM_CCD','/etc/openvpn/server/cmd/show_client_ccd.sh');
+define('GET_IPS_FROM_IPP','/etc/openvpn/server/cmd/show_client_ipp.sh');
+define('REMOVE_CCD','/etc/openvpn/server/cmd/remove_ccd.sh');
 
 // config.php - конфигурация OpenVPN серверов
 return [
     'server1' => [
+	//Service name, i.e. openvpn-server@server1 => server1
         'name' => 'server1',
         'title' => 'Server1',
         'config' => '/etc/openvpn/server/server.conf',

+ 255 - 146
html/admin/functions.php

@@ -19,14 +19,14 @@ function openvpnManagementCommand($server, $command) {
 
     $timeout = 5;
     $socket = @fsockopen($mgmt_host, $mgmt_port, $errno, $errstr, $timeout);
-    
+
     if (!$socket) {
         error_log("OpenVPN management connection failed to {$server['name']}: $errstr ($errno)");
         return false;
     }
 
     stream_set_timeout($socket, $timeout);
-    
+
     try {
         // Читаем приветственное сообщение
         $welcome = '';
@@ -90,6 +90,8 @@ function getOpenVPNStatus($server) {
     $response = openvpnManagementCommand($server, "status 2");
     if (!$response) return $_SESSION['cached_status'][$server['name']] ?? [];
 
+    $banned = getBannedClients($server);
+
     $clients = [];
     $lines = explode("\n", $response);
     $in_client_list = false;
@@ -120,7 +122,7 @@ function getOpenVPNStatus($server) {
                     'connected_since' => $parts[7],
                     'username' => $parts[8] ?? $parts[1],
                     'cipher' => end($parts),
-                    'banned' => isClientBanned($server, $parts[1]),
+                    'banned' => isset($banned[$parts[1]]),
                 ];
             }
         }
@@ -132,181 +134,315 @@ function getOpenVPNStatus($server) {
     return $clients;
 }
 
-function isServerCertificate($cert_index_path, $username) {
-    // Получаем путь к каталогу issued
-    $issued_dir = dirname(dirname($cert_index_path)) . '/pki/issued';
-    
-    // Проверяем существование каталога
-    if (!is_dir($issued_dir)) {
-        return 'fail: issued directory not found';
+function get_servers_crt($cert_index) {
+    // Проверка входных параметров
+    if (empty($cert_index) || !is_string($cert_index)) {
+        return false;
+    }
+
+    // Проверка существования исполняемого файла
+    if (empty(SHOW_SERVERS_CRT) || !file_exists(SHOW_SERVERS_CRT) || !is_executable(SHOW_SERVERS_CRT)) {
+        error_log('SHOW_SERVERS_CRT is not configured properly', 0);
+        return false;
+    }
+
+    $command = sprintf(
+        'sudo %s %s 2>&1',
+        escapeshellcmd(SHOW_SERVERS_CRT),
+        escapeshellarg($cert_index)
+    );
+
+    exec($command, $cert_content, $return_var);
+
+    if ($return_var !== 0) {
+        error_log(sprintf(
+            'Command failed: %s (return code: %d, output: %s)',
+            $command,
+            $return_var,
+            implode("\n", $cert_content)
+        ), 0);
+        return false;
+    }
+
+    if (empty($cert_content)) {
+        error_log('Empty certificate content for file: '.$cert_index, 0);
+        return false;
+    }
+
+    $result = array_fill_keys($cert_content, true);
+
+    return $result;
+}
+
+function getBannedClients($server) {
+    // Проверка входных параметров
+    if (empty($server["ccd"]) || !is_string($server["ccd"])) {
+        return [];
+    }
+
+    // Проверка существования исполняемого файла
+    if (empty(SHOW_BANNED) || !file_exists(SHOW_BANNED) || !is_executable(SHOW_BANNED)) {
+        error_log('SHOW_BANNED is not configured properly', 0);
+        return [];
+    }
+
+    $command = sprintf(
+        'sudo %s %s 2>&1',
+        escapeshellcmd(SHOW_BANNED),
+        escapeshellarg($server["ccd"])
+    );
+
+    exec($command, $banned_content, $return_var);
+    if ($return_var !== 0) {
+        error_log(sprintf(
+            'Command failed: %s (return code: %d)',
+            $command,
+            $return_var,
+        ), 0);
+        return [];
+    }
+
+    if (empty($banned_content)) { return []; }
+
+    $result = array_fill_keys($banned_content, true);
+    return $result;
+}
+
+function getClientIPsCCD($server) {
+    // Проверка входных параметров
+    if (empty($server["ccd"]) || !is_string($server["ccd"])) {
+        return [];
+    }
+
+    // Проверка существования исполняемого файла
+    if (empty(GET_IPS_FROM_CCD) || !file_exists(GET_IPS_FROM_CCD) || !is_executable(GET_IPS_FROM_CCD)) {
+        error_log('SHOW_BANNED is not configured properly', 0);
+        return [];
     }
-    
-    // Формируем путь к файлу сертификата
-    $cert_file = $issued_dir . '/' . $username . '.crt';
-    
-    // Проверяем существование файла
-    if (!file_exists($cert_file)) {
-        return 'success: certificate file not found';
+
+    $command = sprintf(
+        'sudo %s %s 2>&1',
+        escapeshellcmd(GET_IPS_FROM_CCD),
+        escapeshellarg($server["ccd"])
+    );
+
+    exec($command, $ccd_content, $return_var);
+    if ($return_var !== 0) {
+        error_log(sprintf(
+            'Command failed: %s (return code: %d)',
+            $command,
+            $return_var,
+        ), 0);
+        return [];
     }
-    
-    // Читаем содержимое сертификата
-    $cert_content = file_get_contents($cert_file);
-    if ($cert_content === false) {
-        return 'success: cannot read certificate file';
+
+    if (empty($ccd_content)) { return []; }
+
+    $result=[];
+    foreach ($ccd_content as $line) {
+	if (empty($line)) { continue; }
+        list($login, $ip) = explode(' ', trim($line), 2);
+	$result[$login] = $ip;
     }
-    
-    // Парсим сертификат
-    $cert_info = openssl_x509_parse($cert_content);
-    if ($cert_info === false) {
-        return 'success: invalid certificate format';
+
+    return $result;
+}
+
+function getClientIPsIPP($server) {
+    // Проверка входных параметров
+    if (empty($server["ipp_file"]) || !is_string($server["ipp_file"])) {
+        return [];
     }
-    
-    // Проверяем Subject CN (Common Name)
-    $common_name = $cert_info['subject']['CN'] ?? '';
-    if ( $common_name !==  $username) {
-        return 'success: common name '.$common_name.' differ from username '.$username;
+
+    // Проверка существования исполняемого файла
+    if (empty(GET_IPS_FROM_IPP) || !file_exists(GET_IPS_FROM_IPP) || !is_executable(GET_IPS_FROM_IPP)) {
+        error_log('SHOW_BANNED is not configured properly', 0);
+        return [];
     }
-    
-    // Проверяем Extended Key Usage (если есть)
-    $ext_key_usage = $cert_info['extensions']['extendedKeyUsage'] ?? '';
-    
-    // Проверяем, является ли это серверным сертификатом
-    // Серверные сертификаты обычно имеют:
-    // 1. CN, содержащее имя сервера (например, "server")
-    // 2. Extended Key Usage: TLS Web Server Authentication
-    $is_server_cert = (
-        stripos($ext_key_usage, 'TLS Web Server Authentication') !== false ||
-        stripos($ext_key_usage, 'serverAuth') !== false
+
+    $command = sprintf(
+        'sudo %s %s 2>&1',
+        escapeshellcmd(GET_IPS_FROM_IPP),
+        escapeshellarg($server["ipp_file"])
     );
 
-    return $is_server_cert ? 'fail: server certificate detected' : 'success';
+    exec($command, $ipp_content, $return_var);
+    if ($return_var !== 0) {
+        error_log(sprintf(
+            'Command failed: %s (return code: %d)',
+            $command,
+            $return_var,
+        ), 0);
+        return [];
+    }
+
+    if (empty($ipp_content)) { return []; }
+
+    $result=[];
+    foreach ($ipp_content as $line) {
+	if (empty($line)) { continue; }
+        list($login, $ip) = explode(',', trim($line), 2);
+	$result[$login] = $ip;
+    }
+
+    return $result;
 }
 
 function getAccountList($server) {
     $accounts = [];
 
+    $banned = getBannedClients($server);
+
     // Получаем список из index.txt (неотозванные сертификаты)
     if (!empty($server['cert_index']) && !empty(SHOW_PKI_INDEX) && file_exists(SHOW_PKI_INDEX)) {
+	$servers_list = get_servers_crt($server['cert_index']);
         // Безопасное выполнение скрипта
-	$command = sprintf(
-	    'sudo %s %s 2>&1',
+        $command = sprintf(
+            'sudo %s %s 2>&1',
             escapeshellcmd(SHOW_PKI_INDEX),
             escapeshellarg($server['cert_index']),
         );
-	exec($command,  $index_content, $return_var);
+        exec($command,  $index_content, $return_var);
         if ($return_var == 0) {
             foreach ($index_content as $line) {
-	        if (empty(trim($line))) { continue; }
-		if (preg_match('/\/CN=([^\/]+)/', $line, $matches)) {
-		        $username = trim($matches[1]);
-			}
-		if (empty($username)) { continue; }
-		$revoked = false;
-		if (preg_match('/^R\s+/',$line)) { $revoked = true; }
-		$result = isServerCertificate($server['cert_index'], $username);
-		if (strpos($result, 'fail:') === 0) { continue; }
+                if (empty(trim($line))) { continue; }
+                if (preg_match('/\/CN=([^\/]+)/', $line, $matches)) {
+                        $username = trim($matches[1]);
+                        }
+                if (empty($username)) { continue; }
+                $revoked = false;
+                if (preg_match('/^R\s+/',$line)) { $revoked = true; }
+                if (isset($servers_list[$username])) { continue; }
                 $accounts[$username] = [
-	        	"username" => $username,
-	        	"ip" => null,
-        		"banned" => isClientBanned($server, $username) || $revoked,
-			"revoked" => $revoked
-        		];
-		}
-	    }
+                        "username" => $username,
+                        "ip" => null,
+                        "banned" => isset($banned[$username]) || $revoked,
+                        "revoked" => $revoked
+                        ];
+                }
+            }
     }
 
     // Получаем список выданных IP из ipp.txt
     if (!empty($server['ipp_file']) && file_exists($server['ipp_file'])) {
-        $ipp_content = file_get_contents($server['ipp_file']);
-        $lines = explode("\n", $ipp_content);
-
-        foreach ($lines as $line) {
-            if (empty(trim($line))) continue;
-
-            $parts = explode(',', $line);
-            if (count($parts) >= 2) {
-                $username = $parts[0];
-                $ip = $parts[1];
-                if (!isset($accounts[$username]) && empty($server['cert_index'])) {
+	$ipps = getClientIPsIPP($server);
+	foreach ($ipps as $username => $ip) {
+            if (!isset($accounts[$username]) && empty($server['cert_index'])) {
                     $accounts[$username] = [
                         "username" => $username,
-                        "banned" => isClientBanned($server, $username),
-			"ip" => $ip,
-			"revoked" => false,
+                        "banned" => isset($banned[$username]),
+                        "ip" => $ip,
+                        "revoked" => false,
                     ];
                 }
-                if (isset($accounts[$username]) and !empty($server['cert_index'])) {
-		    $accounts[$username]["ip"] = $ip;
-		}
-            }
+            if (isset($accounts[$username]) and !empty($server['cert_index'])) {
+                    $accounts[$username]["ip"] = $ip;
+                }
         }
     }
 
     // Ищем IP-адреса в CCD файлах
     if (!empty($server['ccd']) && is_dir($server['ccd'])) {
-        $ccd_files = scandir($server['ccd']);
-        foreach ($ccd_files as $file) {
-            if ($file === '.' || $file === '..') continue;
-            
-            $username = $file;
-            $filepath = $server['ccd'] . '/' . $file;
-            $content = file_get_contents($filepath);
-            
-            // Ищем строку ifconfig-push с IP адресом
-            if (preg_match('/ifconfig-push\s+(\d+\.\d+\.\d+\.\d+)/', $content, $matches)) {
-                $ip = $matches[1];
-                if (!isset($accounts[$username]) && empty($server['cert_index'])) {
+	$ccds = getClientIPsCCD($server);
+	foreach ($ccds as $username => $ip) {
+            if (!isset($accounts[$username]) && empty($server['cert_index'])) {
                     $accounts[$username] = [
                         "username" => $username,
-                        "banned" => isClientBanned($server, $username),
-			"ip" => $ip,
-			"revoked" => false,
+                        "banned" => isset($banned[$username]),
+                        "ip" => $ip,
+                        "revoked" => false,
                     ];
                 }
-                if (isset($accounts[$username]) and !empty($server['cert_index'])) {
-		    $accounts[$username]["ip"] = $ip;
-		}
-            }
+            if (isset($accounts[$username]) and !empty($server['cert_index'])) {
+                    $accounts[$username]["ip"] = $ip;
+                }
         }
     }
     return $accounts;
 }
 
-function isClientBanned($server, $client_name) {
+function kickClient($server, $client_name) {
+    return openvpnManagementCommand($server, "kill $client_name");
+}
+
+function removeCCD($server, $client_name) {
+    if (empty($server["ccd"]) || empty($client_name) || empty(REMOVE_CCD) || !file_exists(REMOVE_CCD)) { return false; }
+
+    $script_path = REMOVE_CCD;
     $ccd_file = "{$server['ccd']}/$client_name";
-    return file_exists($ccd_file) && 
-           preg_match('/^disable$/m', file_get_contents($ccd_file));
+    $command = sprintf(
+        'sudo %s %s 2>&1',
+        escapeshellcmd($script_path),
+        escapeshellarg($ccd_file)
+    );
+    exec($command, $output, $return_var);
+
+    $_SESSION['last_request_time'] = [];
+
+    if ($return_var === 0) {
+        return true;
+    } else {
+        return false;
+    }
 }
 
-function kickClient($server, $client_name) {
-    return openvpnManagementCommand($server, "kill $client_name");
+
+function unbanClient($server, $client_name) {
+    if (empty($server["ccd"]) || empty($client_name) || empty(BAN_CLIENT) || !file_exists(BAN_CLIENT)) { return false; }
+
+
+    $script_path = BAN_CLIENT;
+    $ccd_file = "{$server['ccd']}/$client_name";
+    $command = sprintf(
+        'sudo %s %s unban 2>&1',
+        escapeshellcmd($script_path),
+        escapeshellarg($ccd_file)
+    );
+    exec($command, $output, $return_var);
+
+    $_SESSION['last_request_time'] = [];
+
+    if ($return_var === 0) {
+        return true;
+    } else {
+        return false;
+    }
 }
 
 function banClient($server, $client_name) {
+    if (empty($server["ccd"]) || empty($client_name) || empty(BAN_CLIENT) || !file_exists(BAN_CLIENT)) { return false; }
+
+    $script_path = BAN_CLIENT;
     $ccd_file = "{$server['ccd']}/$client_name";
-    
-    // Добавляем директиву disable
-    $content = file_exists($ccd_file) ? file_get_contents($ccd_file) : '';
-    if (!preg_match('/^disable$/m', $content)) {
-        file_put_contents($ccd_file, $content . "\ndisable\n");
-    }
+    $command = sprintf(
+        'sudo %s %s ban 2>&1',
+        escapeshellcmd($script_path),
+        escapeshellarg($ccd_file)
+    );
+    exec($command, $output, $return_var);
 
-    // Кикаем клиента
-    kickClient($server, $client_name);
-    return true;
+    $_SESSION['last_request_time'] = [];
+
+    if ($return_var === 0) {
+        // Кикаем клиента
+	kickClient($server, $client_name);
+        return true;
+    } else {
+        return false;
+    }
 }
 
 function revokeClient($server, $client_name) {
     if (empty(REVOKE_CRT) || !file_exists(REVOKE_CRT)) {
-	return banClient($server, $client_name);
-	}
+        return banClient($server, $client_name);
+        }
 
     $script_path = REVOKE_CRT;
     $rsa_dir = dirname(dirname($server['cert_index']));
 
     $command = sprintf(
         'sudo %s %s %s %s 2>&1',
-	escapeshellcmd($script_path),
+        escapeshellcmd($script_path),
         escapeshellarg('openvpn-server@'.$server['name']),
         escapeshellarg($rsa_dir),
         escapeshellarg($client_name)
@@ -315,22 +451,12 @@ function revokeClient($server, $client_name) {
     exec($command, $output, $return_var);
 
     if ($return_var === 0) {
-	return true;
+        return true;
     } else {
-	return false;
+        return false;
     }
 }
 
-function unbanClient($server, $client_name) {
-    $ccd_file = "{$server['ccd']}/$client_name";
-    if (file_exists($ccd_file)) {
-        $content = file_get_contents($ccd_file);
-        $new_content = preg_replace('/^disable$\n?/m', '', $content);
-        file_put_contents($ccd_file, $new_content);
-        return true;
-    }
-    return false;
-}
 
 function formatBytes($bytes) {
     $bytes = (int)$bytes;
@@ -340,23 +466,6 @@ function formatBytes($bytes) {
     return round($bytes/pow(1024,$pow),2).' '.$units[$pow];
 }
 
-function getBannedClients($server, $active_clients) {
-    $banned = [];
-    $active_names = array_column($active_clients, 'name');
-    
-    if (is_dir($server['ccd'])) {
-        foreach (scandir($server['ccd']) as $file) {
-            if ($file !== '.' && $file !== '..' && is_file("{$server['ccd']}/$file")) {
-                if (isClientBanned($server, $file) && !in_array($file, $active_names)) {
-                    $banned[] = $file;
-                }
-            }
-        }
-    }
-    
-    return $banned;
-}
-
 function isClientActive($active_clients,$username) {
     $active_names = array_column($active_clients, 'name');
     if (in_array($username,$active_names)) { return true; }

+ 7 - 2
html/admin/get_server_data.php

@@ -40,7 +40,7 @@ if (!isset($servers[$server_name])) {
 
 $server = $servers[$server_name];
 $clients = getOpenVPNStatus($server);
-$banned_clients = getBannedClients($server, $clients);
+$banned_clients = getBannedClients($server);
 $accounts = getAccountList($server);
 
 // Генерируем HTML для этого сервера
@@ -91,8 +91,10 @@ ob_start();
                         <button onclick="handleAction('<?= $server_name ?>', 'ban', '<?= htmlspecialchars($client['name']) ?>')" 
                                 class="btn ban-btn">Ban</button>
                     <?php endif; ?>
+		    <?php if (!empty($server['cert_index'])): ?>
                         <button onclick="handleAction('<?= $server_name ?>', 'revoke', '<?= htmlspecialchars($client['name']) ?>')" 
-                                class="btn ban-btn">Revoke</button>
+                	        class="btn ban-btn">Revoke</button>
+                    <?php endif; ?>
                 </td>
             </tr>
             <?php endforeach; ?>
@@ -155,6 +157,9 @@ ob_start();
 				<?php if (!empty($server['cert_index'])): ?>
 			        <button onclick="return confirmAction('revoke', '<?= htmlspecialchars($account['username']) ?>', '<?= $server_name ?>', event)"
                                         class="btn revoke-btn">Revoke</button>
+                                <?php else: ?>
+			        <button onclick="return confirmAction('remove', '<?= htmlspecialchars($account['username']) ?>', '<?= $server_name ?>', event)"
+                                        class="btn remove-btn">Remove CCD</button>
                                 <?php endif; ?>
                             <?php endif; ?>
                         </td>

+ 7 - 0
html/admin/handle_action.php

@@ -41,6 +41,9 @@ try {
         case 'unban':
             $result = unbanClient($server, $client_name);
             break;
+        case 'remove':
+            $result = removeCCD($server, $client_name);
+            break;
         default:
             throw new Exception('Invalid action');
     }
@@ -49,3 +52,7 @@ try {
 } catch (Exception $e) {
     echo json_encode(['success' => false, 'message' => $e->getMessage()]);
 }
+
+$clean_url = strtok($_SERVER['REQUEST_URI'], '?');
+header("Refresh:0; url=" . $clean_url);
+exit;

+ 18 - 21
html/admin/index.php

@@ -275,7 +275,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
             color: #999;
             font-style: italic;
         }
-        
         .revoke-btn {
             background-color: #ff9999;
         }
@@ -283,6 +282,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
             background-color: #e65c00;
         }
 
+        .remove-text {
+            color: #999;
+            font-style: italic;
+        }
+        .remove-btn {
+            background-color: #ff9999;
+        }
+        .remove-btn:hover {
+            background-color: #e65c00;
+        }
+
         .ban-btn:hover {
             background-color: #e65c00;
         }
@@ -434,29 +444,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
                 username: username,
                 csrf: csrf
             });
-            
-            // Вариант 1: Простое открытие (рекомендуется)
+
             window.open(`?${params.toString()}`, '_blank');
             document.body.removeChild(spinner);
             
-            /* 
-            // Вариант 2: Через fetch (если нужно строго AJAX)
-            fetch(`?${params.toString()}`, {
-                headers: {'X-Requested-With': 'XMLHttpRequest'}
-            })
-            .then(response => response.blob())
-            .then(blob => {
-                const url = URL.createObjectURL(blob);
-                const a = document.createElement('a');
-                a.href = url;
-                a.download = `${username}.ovpn`;
-                a.click();
-                URL.revokeObjectURL(url);
-            })
-            .catch(console.error)
-            .finally(() => document.body.removeChild(spinner));
-            */
-            
             return false;
         }
 
@@ -559,6 +550,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
                     message = `WARNING: Revoke certificate for ${username}?\n\nThis action is irreversible and will permanently disable the certificate!`;
                     isDangerous = true;
                     break;
+                case 'remove':
+                    message = `Remove user ${username} config file?`;
+                    break;
                 default:
                     message = `Perform ${action} on ${username}?`;
             }
@@ -578,5 +572,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
         }
     </script>
 
+&copy; 2024–<?= date('Y') ?> — OpenVPN Status Monitoring.  
+Based on <a href="https://github.com/rajven/openvpn-status-page" target="_blank">openvpn-status-page</a> by rajven. All rights reserved.
+
 </body>
 </html>