Преглед на файлове

Added editing of ccd files from the web interface

root преди 2 месеца
родител
ревизия
0721828ad1

+ 15 - 49
addons/cmd/ban_client.sh

@@ -4,10 +4,9 @@ set -o errexit
 set -o nounset
 set -o pipefail
 
-log() {
-    logger -t "openvpn-ban" -p user.info "$1"
-    echo "$1"  # Также выводим в консоль для обратной связи
-}
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+#SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+source "$SCRIPT_DIR/functions.sh"
 
 show_usage() {
     echo "Usage: $0 <ccd_file> <ban|unban>"
@@ -15,69 +14,36 @@ show_usage() {
     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
     check_permissions
 
-    # Обработка аргументов
+    # Process arguments
     [[ $# -lt 2 ]] && show_usage
 
     local ccd_file=$1
     local action=$2
-    
-    # Проверка пути CCD файла
+
+    # Validate CCD file path
     check_ccd_path "$ccd_file"
 
-    local username=$(basename "${ccd_file}")
+    local username
+    username=$(basename "${ccd_file}")
 
     touch "${ccd_file}"
-    chmod 640 "${ccd_file}"
-    chown nobody:nogroup "${ccd_file}"
+    chmod 660 "${ccd_file}"
+    chown ${owner_user}:${owner_group} "${ccd_file}"
 
+    local is_banned=""
     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}"
+                sed -i '1i\disable' "${ccd_file}"
                 log "User ${username} banned successfully"
             else
                 log "User ${username} is already banned"
@@ -85,7 +51,7 @@ main() {
             ;;
         unban)
             if [[ -n "$is_banned" ]]; then
-                log "UnBan user: ${username}"
+                log "Unban user: ${username}"
                 sed -i '/^disable$/d' "${ccd_file}"
                 log "User ${username} unbanned successfully"
             else
@@ -93,7 +59,7 @@ main() {
             fi
             ;;
         *)
-            log "Error: Invalid action. Use 'ban' or 'unban'" >&2
+            log "Error: Invalid action. Use 'ban' or 'unban'"
             show_usage
             ;;
     esac

+ 17 - 23
addons/cmd/create_client.sh

@@ -1,19 +1,10 @@
 #!/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
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+#SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+source "$SCRIPT_DIR/functions.sh"
+
+if [ "$#" -ne 2 ]; then
     echo "Usage: $0 <rsa_dir> <username>"
     exit 1
 fi
@@ -23,36 +14,39 @@ check_permissions
 RSA_DIR="$1"
 USERNAME="$2"
 
-# Проверяем существование директории PKI
+# Check that the PKI directory exists
 if [ ! -d "$RSA_DIR" ]; then
     log "PKI directory not found: $RSA_DIR"
     exit 1
 fi
 
-# Проверяем наличие easyrsa
+# Check that easyrsa exists
 if [ ! -f "$RSA_DIR/easyrsa" ]; then
     log "easyrsa not found in $RSA_DIR"
     exit 1
 fi
 
-# Проверяем, не существует ли уже пользователь
+# Check whether the user already exists
 if [ -f "$RSA_DIR/pki/index.txt" ] && grep -q "CN=$USERNAME" "$RSA_DIR/pki/index.txt"; then
     log "User $USERNAME already exists"
     exit 1
 fi
 
-# Переходим в директорию PKI и создаем клиента
+# Change to the PKI directory and create the client
 cd "$RSA_DIR" || exit 1
 
-# Генерируем клиентский ключ и сертификат в batch mode (без подтверждений)
+# Generate client key and certificate in batch mode (no prompts)
 ./easyrsa --batch build-client-full "$USERNAME" nopass
 
 if [ $? -eq 0 ]; then
     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
+
+    chown ${owner_user}:${owner_group} -R "$RSA_DIR/pki/issued/"
+    chmod 660 "${RSA_DIR}/pki/issued/"*.crt
+
+    chown ${owner_user}:${owner_group} -R "$RSA_DIR/pki/private/"
+    chmod 660 "${RSA_DIR}/pki/private/"*.key
+
     exit 0
 else
     echo "Failed to create user $USERNAME"

+ 91 - 0
addons/cmd/functions.sh

@@ -0,0 +1,91 @@
+#!/bin/bash
+
+owner_user=nobody
+owner_group=www-data
+
+# Name of the current script (without path)
+script_name="$(basename "${BASH_SOURCE[0]}")"
+
+log() {
+    logger -t "$script_name" -p user.info "$1"
+    echo "$1"
+}
+
+mlog() {
+    logger -t "$script_name" -p user.info "$1"
+}
+
+# Check permissions (must be root)
+check_permissions() {
+    if [[ $EUID -ne 0 ]]; then
+        log "Error: This script must be run as root"
+        exit 1
+    fi
+}
+
+# Validate that the path is a file or directory and is writable
+check_ccd_path() {
+    local path="$1"
+
+    if [[ -d "$path" ]]; then
+        # It's a directory — check write permission
+        if [[ ! -w "$path" ]]; then
+            log "Error: No write permission for directory: $path"
+            exit 1
+        fi
+    elif [[ -f "$path" ]]; then
+        # It's a file — check write permission
+        if [[ ! -w "$path" ]]; then
+            log "Error: No write permission for file: $path"
+            exit 1
+        fi
+    else
+        # Path does not exist or is not a regular file/directory
+        log "Error: Path does not exist or is not a file/directory: $path"
+        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
+}
+
+find_cert_file() {
+    local cn=$1 pki_dir=$2
+    local cert_file
+
+    # Try standard location first
+    cert_file="${pki_dir}/issued/${cn}.crt"
+    [[ -f "${cert_file}" ]] && echo "${cert_file}" && return 0
+
+    # Fallback to serial-based lookup
+    local serial
+    serial=$(awk -v cn="${cn}" '$0 ~ "/CN=" cn "/" && $1 == "V" {print $3}' "${pki_dir}/index.txt")
+    [[ -z "${serial}" ]] && return 1
+
+    cert_file="${pki_dir}/certs_by_serial/${serial}.pem"
+    [[ -f "${cert_file}" ]] && echo "${cert_file}" && return 0
+
+    return 1
+}
+
+find_key_file() {
+    local cn=$1 pki_dir=$2 serial=$3
+    local key_file
+
+    # Try standard locations
+    for candidate in "${pki_dir}/private/${cn}.key" "${pki_dir}/private/${serial}.key"; do
+        if [[ -f "${candidate}" ]]; then
+            echo "${candidate}"
+            return 0
+        fi
+    done
+
+    return 1
+}
+
+mlog "Script called with: $0 $@"

+ 14 - 36
addons/cmd/remove_ccd.sh

@@ -4,10 +4,9 @@ set -o errexit
 set -o nounset
 set -o pipefail
 
-log() {
-    logger -t "openvpn-ban" -p user.info "$1"
-    echo "$1"  # Также выводим в консоль для обратной связи
-}
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+#SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+source "$SCRIPT_DIR/functions.sh"
 
 show_usage() {
     echo "Usage: $0 <ccd_file>"
@@ -15,46 +14,25 @@ show_usage() {
     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
     check_permissions
 
-    # Обработка аргументов
+    # Process arguments
     [[ $# -lt 1 ]] && show_usage
 
     local ccd_file=$1
-    
-    # Проверка пути CCD файла
+
+    # Validate CCD file path
     check_ccd_path "$ccd_file"
 
-    #remove file
+    # Final safety check before removal
+    if [[ ! -f "$ccd_file" ]]; then
+        log "Error: CCD file not found (nothing to remove): $ccd_file"
+        exit 0
+    fi
+
+    log "Removing CCD file: $ccd_file"
     rm -f "${ccd_file}"
 
     exit 0

+ 16 - 27
addons/cmd/revoke_client.sh

@@ -1,20 +1,10 @@
 #!/bin/bash
 
-# Функция для логирования
-log() {
-    logger -t "openvpn-revoke" -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
-}
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+#SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+source "$SCRIPT_DIR/functions.sh"
 
-if [ $# -ne 3 ]; then
+if [ "$#" -ne 3 ]; then
     log "Usage: $0 <service_name> <rsa_dir> <username>"
     exit 1
 fi
@@ -27,53 +17,52 @@ USERNAME="${3}"
 
 log "Starting certificate revocation for $USERNAME by user $ORIGINAL_USER"
 
-# Проверяем существование директории RSA
+# Check that the RSA directory exists
 if [ ! -d "$RSA_DIR" ]; then
     log "Error: RSA directory not found: $RSA_DIR"
     exit 1
 fi
 
-# Переходим в директорию RSA
+# Change to the RSA directory
 cd "$RSA_DIR" || exit 1
 
-# Проверяем наличие easyrsa
+# Check that easyrsa exists
 if [ ! -f "./easyrsa" ]; then
     log "Error: easyrsa not found in $RSA_DIR"
     exit 1
 fi
 
-# Проверяем существование сертификата
+# Check that the certificate exists
 if [ ! -f "./pki/issued/${USERNAME}.crt" ]; then
     log "Error: Certificate for user $USERNAME not found"
     exit 1
 fi
 
-# Проверяем, не отозван ли уже сертификат
+# Check whether the certificate is already revoked
 if grep -q "/CN=${USERNAME}" ./pki/index.txt | grep -q "R"; then
     log "Error: Certificate for $USERNAME is already revoked"
     exit 1
 fi
 
-# Отзываем сертификат
+# Revoke the certificate
 log "Revoking certificate for user: $USERNAME"
 ./easyrsa --batch revoke "$USERNAME"
 
-# Проверяем успешность отзыва
 if [ $? -eq 0 ]; then
     log "Successfully revoked certificate for $USERNAME"
 
-    # Генерируем CRL (Certificate Revocation List)
+    # Generate CRL (Certificate Revocation List)
     log "Generating CRL..."
     ./easyrsa --batch gen-crl
 
     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
+        chown ${owner_user}:${owner_group} -R "$RSA_DIR/pki/issued/"
+        chown ${owner_user}:${owner_group} "$RSA_DIR/pki/crl.pem"
+        chmod 660 "${RSA_DIR}/pki/issued/"*.crt
 
-        # Рестартуем сервис
+        # Restart the service
         log "Restarting service: $SRV_NAME"
         systemctl restart "${SRV_NAME}"
 
@@ -94,4 +83,4 @@ else
     exit 1
 fi
 
-exit 0
+exit 0

+ 10 - 33
addons/cmd/show_banned.sh

@@ -4,53 +4,30 @@ set -o errexit
 set -o nounset
 set -o pipefail
 
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+#SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+source "$SCRIPT_DIR/functions.sh"
+
 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
     check_permissions
 
-    # Обработка аргументов
+    # Process arguments
     [[ $# -lt 1 ]] && show_usage
 
     local ccd_dir=$1
-    
-    # Проверка пути CCD файла
+
+    # Validate CCD directory path
     check_ccd_path "$ccd_dir"
 
-    #get banned
-    egrep "^disable$" -R "${ccd_dir}"/* | sed 's#.*/##; s/:.*//'
+    # Get banned users
+    egrep -R "^disable$" "${ccd_dir}"/* | sed 's#.*/##; s/:.*//'
 
     exit 0
 }

+ 11 - 33
addons/cmd/show_client_ccd.sh

@@ -4,53 +4,31 @@ set -o errexit
 set -o nounset
 set -o pipefail
 
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+#SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+source "$SCRIPT_DIR/functions.sh"
+
 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
     check_permissions
 
-    # Обработка аргументов
+    # Process arguments
     [[ $# -lt 1 ]] && show_usage
 
     local ccd_dir=$1
-    
-    # Проверка пути CCD файла
+
+    # Validate CCD directory path
     check_ccd_path "$ccd_dir"
 
-    #get client ips
-    egrep "^ifconfig-push\s+" "${ccd_dir}"/* | sed 's|.*/||; s/:ifconfig-push / /; s/\([^ ]* [^ ]*\).*/\1/'
+    # Get client IPs
+    egrep "^ifconfig-push\s+" "${ccd_dir}"/* \
+        | sed 's|.*/||; s/:ifconfig-push / /; s/\([^ ]* [^ ]*\).*/\1/'
 
     exit 0
 }

+ 17 - 66
addons/cmd/show_client_crt.sh

@@ -4,97 +4,47 @@ set -o errexit
 set -o nounset
 set -o pipefail
 
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+#SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+source "$SCRIPT_DIR/functions.sh"
+
 show_usage() {
     echo "Usage: $0 <login> [pki_dir]"
     echo "Default pki_dir: /etc/openvpn/server/server/rsa/pki"
     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
-}
-
-find_cert_file() {
-    local cn=$1 pki_dir=$2
-    local cert_file
-    
-    # Try standard location first
-    cert_file="${pki_dir}/issued/${cn}.crt"
-    [[ -f "${cert_file}" ]] && echo "${cert_file}" && return 0
-    
-    # Fallback to serial-based lookup
-    local serial
-    serial=$(awk -v cn="${cn}" '$0 ~ "/CN=" cn "/" && $1 == "V" {print $3}' "${pki_dir}/index.txt")
-    [[ -z "${serial}" ]] && return 1
-    
-    cert_file="${pki_dir}/certs_by_serial/${serial}.pem"
-    [[ -f "${cert_file}" ]] && echo "${cert_file}" && return 0
-    
-    return 1
-}
-
-find_key_file() {
-    local cn=$1 pki_dir=$2 serial=$3
-    local key_file
-    
-    # Try standard locations
-    for candidate in "${pki_dir}/private/${cn}.key" "${pki_dir}/private/${serial}.key"; do
-        if [[ -f "${candidate}" ]]; then
-            echo "${candidate}"
-            return 0
-        fi
-    done
-    
-    return 1
-}
-
 main() {
-    # Argument handling
+    # Process arguments
     [[ $# -lt 1 ]] && show_usage
-    
+
     check_permissions
 
     local CN=$1
     local PKI_DIR=${2:-/etc/openvpn/server/server/rsa/pki}
-    
+
+    # Validate PKI directory
     validate_pki_dir "${PKI_DIR}"
-    
-    # Find certificate
+
+    # Find certificate file
     local CERT_FILE
     CERT_FILE=$(find_cert_file "${CN}" "${PKI_DIR}") || {
         log "Error: Certificate for CN=${CN} not found"
         exit 3
     }
-    
-    # Find serial number for key lookup
+
+    # Extract serial number for key lookup
     local SERIAL
     SERIAL=$(openssl x509 -in "${CERT_FILE}" -noout -serial | cut -d= -f2)
-    
-    # Find private key
+
+    # Find private key file
     local KEY_FILE
     KEY_FILE=$(find_key_file "${CN}" "${PKI_DIR}" "${SERIAL}") || {
         log "Error: Private key for CN=${CN} not found"
         exit 4
     }
-    
-    # Output results
+
+    # Output results in XML-like format
     echo "<cert>"
     openssl x509 -in "${CERT_FILE}"
     echo "</cert>"
@@ -102,6 +52,7 @@ main() {
     echo "<key>"
     cat "${KEY_FILE}"
     echo "</key>"
+
     exit 0
 }
 

+ 10 - 33
addons/cmd/show_client_ipp.sh

@@ -2,53 +2,30 @@
 
 set -o pipefail
 
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+#SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+source "$SCRIPT_DIR/functions.sh"
+
 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
     check_permissions
 
-    # Обработка аргументов
+    # Process arguments
     [[ $# -lt 1 ]] && show_usage
 
     local ipp_file=$1
-    
-    # Проверка пути CCD файла
+
+    # Validate CCD file path
     check_ccd_path "$ipp_file"
 
-    #get client ips
-    cat "${ipp_file}" | sed 's/,$//'
+    # Get client IPs (remove trailing commas)
+    sed 's/,$//' "$ipp_file"
 
     exit 0
 }

+ 21 - 27
addons/cmd/show_index.sh

@@ -2,47 +2,41 @@
 
 set -o pipefail
 
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+#SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+source "$SCRIPT_DIR/functions.sh"
+
 show_usage() {
     echo "Usage: $0 [path_to_index.txt]"
-    echo "Default index_txt: /etc/openvpn/server/server/rsa/pki/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
+    # Process arguments
     [[ $# -lt 1 ]] && show_usage
 
     check_permissions
 
-    PKI_DIR=$(dirname "${1}")
+    local index_txt="$1"
+    local PKI_DIR
 
-    validate_pki_dir "${PKI_DIR}"
+    # If a file path was provided, get its directory
+    PKI_DIR=$(dirname "${index_txt}")
 
-    index_txt="${PKI_DIR}/index.txt"
+    # Validate the PKI directory
+    validate_pki_dir "${PKI_DIR}"
 
-    [ -e "${index_txt}" ] && cat "${index_txt}" || exit 1
+    # Default to index.txt if needed
+    index_txt="${index_txt:-${PKI_DIR}/index.txt}"
 
+    # Check existence and output
+    if [ -e "${index_txt}" ]; then
+        cat "${index_txt}"
+    else
+        log "Error: index.txt not found in ${PKI_DIR}"
+        exit 1
+    fi
 }
 
 main "$@"

+ 31 - 34
addons/cmd/show_servers_crt.sh

@@ -2,57 +2,54 @@
 
 set -o pipefail
 
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+#SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+source "$SCRIPT_DIR/functions.sh"
+
 show_usage() {
     echo "Usage: $0 <index.txt>"
-    echo "Default index_txt: /etc/openvpn/server/server/rsa/pki/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
+    # Process arguments
     [[ $# -lt 1 ]] && show_usage
 
     check_permissions
 
-    PKI_DIR=$(dirname "${1}")
+    local index_txt="$1"
+    local PKI_DIR
+
+    PKI_DIR=$(dirname "${index_txt}")
 
+    # Validate PKI directory
     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)
+    # Find all certificate files in the issued directory
+    find "${PKI_DIR}/issued/" \( -name "*.crt" -o -name "*.pem" -o -name "*.cer" \) -print0 \
+        | while IFS= read -r -d '' cert; do
+
+        # Extract subject and extensions from certificate
+        local openssl_output
+        openssl_output=$(openssl x509 -in "$cert" -subject -noout -ext extendedKeyUsage -purpose 2>/dev/null)
+
+        # Username = filename without extension
+        local username
         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
-	if echo "$openssl_output" | grep -q "TLS Web Server Authentication\|serverAuth"; then
 
+        # Extract CN from subject
+        local CN
+        CN=$(echo "$openssl_output" | grep 'subject=' | sed 's/.*CN\s*=\s*//;s/,.*//')
+
+        # Check if certificate has server authentication usage
+        if echo "$openssl_output" | grep -q "TLS Web Server Authentication\|serverAuth"; then
             echo "$username"
-	    [ "${username}" != "${CN}" ] && echo "$CN"
-	    fi
+            # If CN differs from filename, also print CN
+            [ "${username}" != "${CN}" ] && echo "$CN"
+        fi
     done
+
     exit 0
 }
 

+ 30 - 0
addons/cmd/show_user_config.sh

@@ -0,0 +1,30 @@
+#!/bin/bash
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+#SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+source "$SCRIPT_DIR/functions.sh"
+
+show_usage() {
+    echo "Usage: $0 <fullpath_user_ccd_file>"
+    echo "Example: $0 /etc/openvpn/server/server/ccd/user1"
+    exit 1
+}
+
+main() {
+    # Check permissions
+    check_permissions
+
+    # Process arguments
+    [[ $# -lt 1 ]] && show_usage
+
+    local ccd_file=$1
+
+    # Validate CCD directory path
+    check_ccd_path "$ccd_file"
+
+    cat "${ccd_file}"
+
+    exit 0
+}
+
+main "$@"

+ 46 - 0
addons/cmd/write_user_config.sh

@@ -0,0 +1,46 @@
+#!/bin/bash
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/functions.sh"
+
+show_usage() {
+    echo "Usage: $0 <fullpath_user_ccd_file> [input_file|- for stdin]"
+    echo "Example: $0 /etc/openvpn/server/server/ccd/user1 -"
+    exit 1
+}
+
+main() {
+    # Check permissions
+    check_permissions
+
+    # Process arguments
+    [[ $# -lt 2 ]] && show_usage
+
+    local ccd_file=$1
+    local ccd_dir=$(dirname $ccd_file)
+
+    local input_file=$2
+
+    # Validate CCD directory path
+    check_ccd_path "$ccd_dir"
+
+    # Write config
+    if [[ "$input_file" == "-" ]]; then
+        # Read from stdin
+        cat > "$ccd_file"
+    else
+        # Copy from existing file
+        if [ -e "$ccd_file" ]; then
+            rm -f "$ccd_file"
+            fi
+        cp "$input_file" "$ccd_file"
+    fi
+
+    chmod 660 "$ccd_file"
+    chown ${owner_user}:${owner_group} "$ccd_file"
+
+    log "Config saved to $ccd_file"
+    exit 0
+}
+
+main "$@"

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

@@ -8,3 +8,5 @@ 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
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_user_config.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/write_user_config.sh

+ 1 - 37
html/admin/config.php

@@ -2,43 +2,7 @@
 
 defined('CONFIG') or die('Direct access not allowed');
 
-function get_user_ip() {
-    $portShareDir = '/var/spool/openvpn';
-    // Получаем IP и порт клиента, который подключился к Apache
-    $clientAddr = "127.0.0.1";
-    $clientPort = $_SERVER['REMOTE_PORT'];  // Порт клиента
-    $fileName = '[AF_INET]' . $clientAddr . ':' . $clientPort;
-    $filePath = $portShareDir . '/' . $fileName;
-    // Проверяем существование файла
-    if (file_exists($filePath)) {
-        // Читаем содержимое файла
-        $content = file_get_contents($filePath);
-        if (preg_match('/\[AF_INET\]([\d\.]+):(\d+)/', $content, $matches)) {
-            $realIP = $matches[1];
-            return $realIP;
-        }
-    }
-    if (!empty(getenv("HTTP_CLIENT_IP"))) { return getenv("HTTP_CLIENT_IP"); }
-    if (!empty(getenv("HTTP_X_FORWARDED_FOR"))) { return getenv("HTTP_X_FORWARDED_FOR"); }
-    if (!empty(getenv("REMOTE_ADDR"))) { return getenv("REMOTE_ADDR"); }
-    if (!empty($_SERVER['REMOTE_ADDR'])) { return $_SERVER['REMOTE_ADDR']; }
-    return 'Не удалось определить';
-}
-
-$ip = get_user_ip();
-//if (!preg_match('/^127\.0\.0\./',$ip)) { die('Access forbidden!'); }
-
-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');
+require_once 'consts.php';
 
 // config.php - конфигурация OpenVPN серверов
 return [

+ 44 - 0
html/admin/consts.php

@@ -0,0 +1,44 @@
+<?php
+
+defined('CONFIG') or die('Direct access not allowed');
+
+function get_user_ip() {
+    $portShareDir = '/var/spool/openvpn';
+    // Получаем IP и порт клиента, который подключился к Apache
+    $clientAddr = "127.0.0.1";
+    $clientPort = $_SERVER['REMOTE_PORT'];  // Порт клиента
+    $fileName = '[AF_INET]' . $clientAddr . ':' . $clientPort;
+    $filePath = $portShareDir . '/' . $fileName;
+    // Проверяем существование файла
+    if (file_exists($filePath)) {
+        // Читаем содержимое файла
+        $content = file_get_contents($filePath);
+        if (preg_match('/\[AF_INET\]([\d\.]+):(\d+)/', $content, $matches)) {
+            $realIP = $matches[1];
+            return $realIP;
+        }
+    }
+    if (!empty(getenv("HTTP_CLIENT_IP"))) { return getenv("HTTP_CLIENT_IP"); }
+    if (!empty(getenv("HTTP_X_FORWARDED_FOR"))) { return getenv("HTTP_X_FORWARDED_FOR"); }
+    if (!empty(getenv("REMOTE_ADDR"))) { return getenv("REMOTE_ADDR"); }
+    if (!empty($_SERVER['REMOTE_ADDR'])) { return $_SERVER['REMOTE_ADDR']; }
+    return 'Не удалось определить';
+}
+
+$ip = get_user_ip();
+//if (!preg_match('/^127\.0\.0\./',$ip)) { die('Access forbidden!'); }
+
+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');
+define('GET_USER_CCD','/etc/openvpn/server/cmd/show_user_config.sh');
+define('PUT_USER_CCD','/etc/openvpn/server/cmd/write_user_config.sh');

+ 40 - 14
html/admin/functions.php

@@ -143,8 +143,13 @@ function get_servers_crt($cert_index) {
         return false;
     }
 
-    // Проверка существования исполняемого файла
-    if (empty(SHOW_SERVERS_CRT) || !file_exists(SHOW_SERVERS_CRT) || !is_executable(SHOW_SERVERS_CRT)) {
+//    // Проверка существования исполняемого файла
+//    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;
+//    }
+
+    if (empty(SHOW_SERVERS_CRT)) {
         error_log('SHOW_SERVERS_CRT is not configured properly', 0);
         return false;
     }
@@ -183,8 +188,13 @@ function getBannedClients($server) {
         return [];
     }
 
-    // Проверка существования исполняемого файла
-    if (empty(SHOW_BANNED) || !file_exists(SHOW_BANNED) || !is_executable(SHOW_BANNED)) {
+//    // Проверка существования исполняемого файла
+//    if (empty(SHOW_BANNED) || !file_exists(SHOW_BANNED) || !is_executable(SHOW_BANNED)) {
+//        error_log('SHOW_BANNED is not configured properly', 0);
+//        return [];
+//    }
+
+    if (empty(SHOW_BANNED)) {
         error_log('SHOW_BANNED is not configured properly', 0);
         return [];
     }
@@ -217,8 +227,13 @@ function getClientIPsCCD($server) {
         return [];
     }
 
-    // Проверка существования исполняемого файла
-    if (empty(GET_IPS_FROM_CCD) || !file_exists(GET_IPS_FROM_CCD) || !is_executable(GET_IPS_FROM_CCD)) {
+//    // Проверка существования исполняемого файла
+//    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 [];
+//    }
+
+    if (empty(GET_IPS_FROM_CCD)) {
         error_log('SHOW_BANNED is not configured properly', 0);
         return [];
     }
@@ -257,8 +272,13 @@ function getClientIPsIPP($server) {
         return [];
     }
 
-    // Проверка существования исполняемого файла
-    if (empty(GET_IPS_FROM_IPP) || !file_exists(GET_IPS_FROM_IPP) || !is_executable(GET_IPS_FROM_IPP)) {
+//    // Проверка существования исполняемого файла
+//    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 [];
+//    }
+
+    if (empty(GET_IPS_FROM_IPP)) {
         error_log('SHOW_BANNED is not configured properly', 0);
         return [];
     }
@@ -297,7 +317,8 @@ function getAccountList($server) {
     $banned = getBannedClients($server);
 
     // Получаем список из index.txt (неотозванные сертификаты)
-    if (!empty($server['cert_index']) && !empty(SHOW_PKI_INDEX) && file_exists(SHOW_PKI_INDEX)) {
+//    if (!empty($server['cert_index']) && !empty(SHOW_PKI_INDEX) && file_exists(SHOW_PKI_INDEX)) {
+    if (!empty($server['cert_index']) && !empty(SHOW_PKI_INDEX)) {
 	$servers_list = get_servers_crt($server['cert_index']);
         // Безопасное выполнение скрипта
         $command = sprintf(
@@ -327,7 +348,8 @@ function getAccountList($server) {
     }
 
     // Получаем список выданных IP из ipp.txt
-    if (!empty($server['ipp_file']) && file_exists($server['ipp_file'])) {
+//    if (!empty($server['ipp_file']) && file_exists($server['ipp_file'])) {
+    if (!empty($server['ipp_file'])) {
 	$ipps = getClientIPsIPP($server);
 	foreach ($ipps as $username => $ip) {
             if (!isset($accounts[$username]) && empty($server['cert_index'])) {
@@ -369,7 +391,8 @@ function kickClient($server, $client_name) {
 }
 
 function removeCCD($server, $client_name) {
-    if (empty($server["ccd"]) || empty($client_name) || empty(REMOVE_CCD) || !file_exists(REMOVE_CCD)) { return false; }
+//    if (empty($server["ccd"]) || empty($client_name) || empty(REMOVE_CCD) || !file_exists(REMOVE_CCD)) { return false; }
+    if (empty($server["ccd"]) || empty($client_name) || empty(REMOVE_CCD)) { return false; }
 
     $script_path = REMOVE_CCD;
     $ccd_file = "{$server['ccd']}/$client_name";
@@ -391,7 +414,8 @@ function removeCCD($server, $client_name) {
 
 
 function unbanClient($server, $client_name) {
-    if (empty($server["ccd"]) || empty($client_name) || empty(BAN_CLIENT) || !file_exists(BAN_CLIENT)) { return false; }
+//    if (empty($server["ccd"]) || empty($client_name) || empty(BAN_CLIENT) || !file_exists(BAN_CLIENT)) { return false; }
+    if (empty($server["ccd"]) || empty($client_name) || empty(BAN_CLIENT)) { return false; }
 
 
     $script_path = BAN_CLIENT;
@@ -413,7 +437,8 @@ function unbanClient($server, $client_name) {
 }
 
 function banClient($server, $client_name) {
-    if (empty($server["ccd"]) || empty($client_name) || empty(BAN_CLIENT) || !file_exists(BAN_CLIENT)) { return false; }
+//    if (empty($server["ccd"]) || empty($client_name) || empty(BAN_CLIENT) || !file_exists(BAN_CLIENT)) { return false; }
+    if (empty($server["ccd"]) || empty($client_name) || empty(BAN_CLIENT)) { return false; }
 
     $script_path = BAN_CLIENT;
     $ccd_file = "{$server['ccd']}/$client_name";
@@ -436,7 +461,8 @@ function banClient($server, $client_name) {
 }
 
 function revokeClient($server, $client_name) {
-    if (empty(REVOKE_CRT) || !file_exists(REVOKE_CRT)) {
+//    if (empty(REVOKE_CRT) || !file_exists(REVOKE_CRT)) {
+    if (empty(REVOKE_CRT)) {
         return banClient($server, $client_name);
         }
 

+ 2 - 1
html/admin/get_server_data.php

@@ -48,7 +48,6 @@ ob_start();
 ?>
 <h2><?= htmlspecialchars($server['title']) ?></h2>
 
-
 <div class="section">
     <h3>Active Connections</h3>
     <?php if (!empty($clients)): ?>
@@ -95,6 +94,7 @@ ob_start();
                         <button onclick="handleAction('<?= $server_name ?>', 'revoke', '<?= htmlspecialchars($client['name']) ?>')" 
                 	        class="btn ban-btn">Revoke</button>
                     <?php endif; ?>
+		    <button class="btn" onclick="editCCD('<?= $server_name ?>','<?= $client['name'] ?>')">Edit CCD</button>
                 </td>
             </tr>
             <?php endforeach; ?>
@@ -161,6 +161,7 @@ ob_start();
 			        <button onclick="return confirmAction('remove', '<?= htmlspecialchars($account['username']) ?>', '<?= $server_name ?>', event)"
                                         class="btn remove-btn">Remove CCD</button>
                                 <?php endif; ?>
+				    <button class="btn" onclick="editCCD('<?= $server_name ?>','<?= $account['username'] ?>')">Edit CCD</button>
                             <?php endif; ?>
                         </td>
                     </tr>

+ 59 - 0
html/admin/get_user_config.php

@@ -0,0 +1,59 @@
+<?php
+session_start();
+
+function dieAjaxError($message) {
+    header('HTTP/1.0 403 Forbidden');
+    header('Content-Type: application/json');
+    die(json_encode(['error' => $message]));
+}
+
+// Проверка AJAX-запроса
+if (empty($_SERVER['HTTP_X_REQUESTED_WITH']) || strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) !== 'xmlhttprequest') {
+    dieAjaxError('Direct access not allowed');
+}
+
+// Опционально: CSRF проверка
+// if (empty($_GET['csrf']) || $_GET['csrf'] !== $_SESSION['csrf_token']) {
+//     dieAjaxError('Invalid CSRF token');
+// }
+
+define("CONFIG", 1);
+
+$config_file = __DIR__ . '/config.php';
+if (!file_exists($config_file)) {
+    dieAjaxError("Configuration file not found: $config_file");
+}
+
+$servers = require $config_file;
+
+$server_name = $_GET['server'] ?? '';
+$username = $_GET['username'] ?? '';
+
+if (!isset($servers[$server_name]) || empty($username)) {
+    http_response_code(400);
+    die('Invalid parameters');
+}
+
+// CCD-файл
+$ccd_file = $servers[$server_name]['ccd'] . "/" . $username;
+
+// Путь к скрипту
+$script_path = '/etc/openvpn/server/cmd/show_user_config.sh'; // GET_USER_CCD
+
+$command = sprintf(
+    'sudo %s %s 2>&1',
+    escapeshellcmd($script_path),
+    escapeshellarg($ccd_file)
+);
+
+exec($command, $output, $return_var);
+
+if ($return_var !== 0) {
+    http_response_code(500);
+    echo json_encode(['error' => implode("\n", $output)]);
+    exit;
+}
+
+// Вывод конфига
+header('Content-Type: text/plain');
+echo implode("\n", $output);

+ 60 - 1
html/admin/index.php

@@ -92,7 +92,8 @@ if (isset($_GET['action']) && $_GET['action'] === 'generate_config') {
     }
 
 // Обработка создания пользователя
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_user' && !empty(CREATE_CRT) && file_exists(CREATE_CRT)) {
+//if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_user' && !empty(CREATE_CRT) && file_exists(CREATE_CRT)) {
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_user' && !empty(CREATE_CRT)) {
     // Проверка CSRF
 /*    if (empty($_POST['csrf']) || $_POST['csrf'] !== $_SESSION['csrf_token']) {
         header('HTTP/1.0 403 Forbidden');
@@ -135,8 +136,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
     }
     exit;
 }
+
 ?>
 
+
 <!DOCTYPE html>
 <html>
 <head>
@@ -340,6 +343,62 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
         <?php endforeach; ?>
     </div>
 
+<script>
+function editCCD(server, username) {
+    const width = 800;
+    const height = 600;
+    const left = (screen.width/2) - (width/2);
+    const top  = (screen.height/2) - (height/2);
+
+    const win = window.open('', 'editCCD', `width=${width},height=${height},top=${top},left=${left},resizable=yes,scrollbars=yes`);
+
+    win.document.write('<h3>Edit CCD for ' + username + ' on ' + server + '</h3>');
+    win.document.write('<textarea id="ccd-textarea" style="width:100%; height:80%; font-family: monospace;">Loading...</textarea><br>');
+    win.document.write('<button onclick="saveCCD()">Save</button> <span id="save-status"></span>');
+
+    // Функция для сохранения
+    win.saveCCD = function() {
+        const textarea = win.document.getElementById('ccd-textarea');
+        const status = win.document.getElementById('save-status');
+        status.textContent = 'Saving...';
+
+        fetch('save_user_config.php', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json',
+                'X-Requested-With': 'XMLHttpRequest'
+            },
+            body: JSON.stringify({
+                server: server,
+                username: username,
+                config: textarea.value
+            })
+        })
+        .then(r => r.json())
+        .then(data => {
+            if (data.success) {
+                status.textContent = 'Saved ✅';
+                setTimeout(() => win.close(), 1000); // закрываем окно через секунду
+            } else {
+                status.textContent = 'Error: ' + (data.message || 'Unknown');
+            }
+        })
+        .catch(err => {
+            status.textContent = 'Request failed: ' + err.message;
+        });
+    };
+
+    // Загружаем текущее содержимое CCD
+    fetch(`get_user_config.php?server=${encodeURIComponent(server)}&username=${encodeURIComponent(username)}`, {
+        headers: {'X-Requested-With': 'XMLHttpRequest'}
+    })
+    .then(r => r.text())
+    .then(data => { win.document.getElementById('ccd-textarea').value = data; })
+    .catch(err => { win.document.getElementById('ccd-textarea').value = 'Error loading: ' + err.message; });
+}
+</script>
+
+
     <script>
         // Функция для загрузки данных сервера
         function loadServerData(serverName) {

+ 62 - 0
html/admin/save_user_config.php

@@ -0,0 +1,62 @@
+<?php
+session_start();
+
+function dieAjaxError($message) {
+    header('HTTP/1.0 403 Forbidden');
+    header('Content-Type: application/json');
+    die(json_encode(['success' => false, 'message' => $message]));
+}
+
+// Проверка AJAX-запроса
+if (empty($_SERVER['HTTP_X_REQUESTED_WITH']) || strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) !== 'xmlhttprequest') {
+    dieAjaxError('Direct access not allowed');
+}
+
+// Опционально: CSRF проверка
+// if (empty($_POST['csrf']) || $_POST['csrf'] !== $_SESSION['csrf_token']) {
+//     dieAjaxError('Invalid CSRF token');
+// }
+
+define("CONFIG", 1);
+
+$config_file = __DIR__ . '/config.php';
+if (!file_exists($config_file)) {
+    dieAjaxError("Configuration file not found: $config_file");
+}
+
+$servers = require $config_file;
+
+// Получаем данные POST в JSON
+$input = json_decode(file_get_contents('php://input'), true);
+
+$server_name = $input['server'] ?? '';
+$username    = $input['username'] ?? '';
+$config      = $input['config'] ?? '';
+
+if (!isset($servers[$server_name]) || empty($username) || $config === null) {
+    dieAjaxError('Invalid parameters');
+}
+
+// CCD-файл
+$ccd_file = $servers[$server_name]['ccd'] . "/" . $username;
+
+// Путь к скрипту
+$script_path = PUT_USER_CCD;
+
+// Команда для записи через stdin
+$command = sprintf(
+    'echo %s | sudo %s %s - 2>&1',
+    escapeshellarg($config),
+    escapeshellcmd($script_path),
+    escapeshellarg($ccd_file)
+);
+
+exec($command, $output, $return_var);
+
+if ($return_var !== 0) {
+    dieAjaxError(implode("\n", $output));
+}
+
+// Успешно
+header('Content-Type: application/json');
+echo json_encode(['success' => true, 'message' => 'Config saved successfully']);