浏览代码

Added configuration generation for the openvpn client

root 8 月之前
父节点
当前提交
7f0e280f86
共有 8 个文件被更改,包括 351 次插入48 次删除
  1. 69 39
      README.md
  2. 2 2
      addons/set_perm.sh
  3. 92 0
      addons/show_client_crt.sh
  4. 1 0
      addons/sudoers.d/www-data
  5. 2 0
      config.php
  6. 18 7
      get_server_data.php
  7. 133 0
      index.php
  8. 34 0
      server1.ovpn.template

+ 69 - 39
README.md

@@ -1,47 +1,77 @@
-Проект рисует страницу состояния Openvpn сервера
+# OpenVPN Status Monitor
 
-Возможности:
-- Отображаются подключенные пользователи
-- Можно забанить/разбанить пользователя
+Проект предоставляет веб-интерфейс для мониторинга состояния OpenVPN сервера.
 
-Для работы скрипта нужен апач и php на сервере с openvpn:
+## Возможности
 
-apt install apache2 php 
+- Отображение подключенных пользователей в реальном времени
+- Управление доступом:
+  - Блокировка/разблокировка пользователей (ban/unban)
+- Генерация конфигурационных файлов для клиентов
+- Автоматическое обновление данных (каждые 60 секунд)
 
-a2enmod session
-
-В конфиге сервера openvpn надо включить интерфейс управления:
-
-management 127.0.0.1 3003 /etc/openvpn/server/password
-
-В файл /etc/openvpn/server/password надо на первой строчке написать пароль подключения
-
-У апача должны быть права записи в каталог конфигурации пользователя:
-
-chmod 775 /etc/openvpn/server/server/ccd
+## Требования
 
-chown nobody:www-data -R /etc/openvpn/server/server/ccd
+- Сервер с OpenVPN
+- Веб-сервер Apache2
+- PHP 7.4+
+- Доступ к управляющему интерфейсу OpenVPN
 
-И права на чтение списка сертфикатов и списка выданных адресов:
+## Установка
 
-chmod 644 /etc/openvpn/server/server/ipp.txt
+###  Установите необходимые пакеты:
 
-chmod 644 /etc/openvpn/server/server/rsa/pki/index.txt
-
-Конфигурация opnepvn-сервера в скрипте - в массив servers вписать нужные сервера:
-
-    'server1' => [
-        'name' => 'server1',
-        'title' => 'Server1',
-        'config' => '/etc/openvpn/server/server.conf',
-        'ccd' => '/etc/openvpn/server/server/ccd',
-        'port' => '3003',
-        'host' => '127.0.0.1',
-        'password' => 'password',
-        'cert_index' => '/etc/openvpn/server/server/rsa/pki/index.txt',
-        'ipp_file' => '/etc/openvpn/server/server/ipp.txt'
-    ],
-
-Ну и всё.
-
-Upd: Добавлено чтение пользователей VPN-сервера из списка выданных сертификатов и списка выданных ip-адресов. 
+```bash
+apt install apache2 php
+a2enmod session
+```
+
+### Настройте OpenVPN:
+```bash
+echo "management 127.0.0.1 3003 /etc/openvpn/server/password" >> /etc/openvpn/server.conf
+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
+```
+### Создайте шаблон конфигурации клиента (без сертификатов) в каталоге сайта.
+
+### Отредактируйте файл конфигурации config.php
+
+```php
+'server1' => [
+    'name' => 'server1',
+    'title' => 'Server1',
+    'config' => '/etc/openvpn/server/server.conf',
+    'ccd' => '/etc/openvpn/server/server/ccd',
+    'port' => '3003',
+    'host' => '127.0.0.1',
+    'password' => 'password',
+    'cfg_template' => 'server1.ovpn.template',
+    'cert_index' => '/etc/openvpn/server/server/rsa/pki/index.txt',
+    'ipp_file' => '/etc/openvpn/server/server/ipp.txt'
+],
+```
+## Использование
+
+Откройте веб-интерфейс в браузере
+
+Для управления пользователями используйте кнопки:
+
+Ban - заблокировать пользователя
+
+Unban - разблокировать пользователя
+
+Для скачивания конфигурации клиента нажмите на имя пользователя

+ 2 - 2
addons/set_perm.sh

@@ -1,6 +1,6 @@
 #!/bin/bash
 
-chmod 644 /etc/openvpn/server/server2/ipp.txt
-chmod 644 /etc/openvpn/server/server2/rsa/pki/index.txt
+chmod 644 /etc/openvpn/server/server1/ipp.txt
+chmod 644 /etc/openvpn/server/server1/rsa/pki/index.txt
 
 exit

+ 92 - 0
addons/show_client_crt.sh

@@ -0,0 +1,92 @@
+#!/bin/bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+show_usage() {
+    echo "Usage: $0 <login> [pki_dir]"
+    echo "Default pki_dir: /etc/openvpn/server/server1/rsa/pki"
+    exit 1
+}
+
+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
+        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
+    [[ $# -lt 1 ]] && show_usage
+    
+    local CN=$1
+    local PKI_DIR=${2:-/etc/openvpn/server/server/rsa/pki}
+    
+    validate_pki_dir "${PKI_DIR}"
+    
+    # Find certificate
+    local CERT_FILE
+    CERT_FILE=$(find_cert_file "${CN}" "${PKI_DIR}") || {
+        echo "Error: Certificate for CN=${CN} not found" >&2
+        exit 3
+    }
+    
+    # Find serial number for key lookup
+    local SERIAL
+    SERIAL=$(openssl x509 -in "${CERT_FILE}" -noout -serial | cut -d= -f2)
+    
+    # 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
+        exit 4
+    }
+    
+    # Output results
+    echo "<cert>"
+    openssl x509 -in "${CERT_FILE}" -notext
+    echo "</cert>"
+    echo
+    echo "<key>"
+    cat "${KEY_FILE}"
+    echo "</key>"
+}
+
+main "$@"

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

@@ -0,0 +1 @@
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/show_client_crt.sh

+ 2 - 0
config.php

@@ -3,6 +3,7 @@
 defined('CONFIG') or die('Direct access not allowed');
 
 define('REQUEST_INTERVAL', 60);
+define('SHOW_CERT_SCRIPT','/etc/openvpn/server/show_client_crt.sh');
 
 // config.php - конфигурация OpenVPN серверов
 return [
@@ -14,6 +15,7 @@ return [
         'port' => '3003',
         'host' => '127.0.0.1',
         'password' => 'password',
+        'cfg_template' => 'server1.ovpn.template',
         'cert_index' => '/etc/openvpn/server/server/rsa/pki/index.txt',
         'ipp_file' => '/etc/openvpn/server/server/ipp.txt'
     ],

+ 18 - 7
get_server_data.php

@@ -8,9 +8,9 @@ if (empty($_SERVER['HTTP_X_REQUESTED_WITH']) || strtolower($_SERVER['HTTP_X_REQU
 }
 
 // 2. Проверяем CSRF-токен
-if (empty($_GET['csrf']) || $_GET['csrf'] !== $_SESSION['csrf_token']) {
-    dieAjaxError('Invalid CSRF token');
-}
+//if (empty($_GET['csrf']) || $_GET['csrf'] !== $_SESSION['csrf_token']) {
+//    dieAjaxError('Invalid CSRF token');
+//}
 
 // Если все проверки пройдены, выполняем основной код
 function dieAjaxError($message) {
@@ -31,6 +31,8 @@ if (!file_exists($config_file)) {
 $servers = require_once $config_file;
 
 $server_name = $_GET['server'] ?? '';
+$action = $_GET['action'] ?? '';
+$username = $_GET['username'] ?? '';
 
 if (!isset($servers[$server_name])) {
     die("Invalid server name");
@@ -46,6 +48,7 @@ ob_start();
 ?>
 <h2><?= htmlspecialchars($server['title']) ?></h2>
 
+
 <div class="section">
     <h3>Active Connections</h3>
     <?php if (!empty($clients)): ?>
@@ -64,7 +67,11 @@ ob_start();
         <tbody>
             <?php foreach ($clients as $client): ?>
             <tr class="<?= $client['banned'] ? 'banned' : '' ?>">
-                <td><?= htmlspecialchars($client['name']) ?></td>
+                <td>
+		    <a href="#" onclick="return generateConfig('<?= $server_name ?>', '<?= htmlspecialchars($client['name']) ?>', event)">
+			<?= htmlspecialchars($client['name']) ?>
+		    </a>
+                </td>
                 <td><?= htmlspecialchars($client['real_ip']) ?></td>
                 <td><?= htmlspecialchars($client['virtual_ip']) ?></td>
                 <td>↓<?= $client['bytes_received'] ?> ↑<?= $client['bytes_sent'] ?></td>
@@ -109,10 +116,14 @@ ob_start();
                 </thead>
                 <tbody>
                     <?php foreach ($accounts as $account):
-		    if (isClientActive($clients,$account["username"])) { continue; }
-		    ?>
+                    if (isClientActive($clients,$account["username"])) { continue; }
+                    ?>
                     <tr>
-                        <td><?= htmlspecialchars($account["username"]) ?></td>
+                        <td>
+			    <a href="#" onclick="return generateConfig('<?= $server_name ?>', '<?= htmlspecialchars($account['username']) ?>', event)">
+			        <?= htmlspecialchars($account['username']) ?>
+			    </a>
+                        </td>
                         <td><?= htmlspecialchars($account['ip'] ?? 'N/A') ?></td>
                         <td>
                             <span class="status-badge <?= $account['banned'] ? 'status-banned' : 'status-active' ?>">

+ 133 - 0
index.php

@@ -21,12 +21,82 @@ if (!isset($_SESSION['last_request_time']) || !is_array($_SESSION['last_request_
     $_SESSION['last_request_time'] = []; // Создаем пустой массив
 }
 
+if (isset($_GET['action']) && $_GET['action'] === 'generate_config') {
+    session_start();
+    
+    // Проверка CSRF
+//    if (empty($_GET['csrf']) || $_GET['csrf'] !== $_SESSION['csrf_token']) {
+//        header('HTTP/1.0 403 Forbidden');
+//        die('Invalid CSRF token');
+//    }
+
+    $server_name = $_GET['server'] ?? '';
+    $username = $_GET['username'] ?? '';
+    
+    if (empty($username) || !isset($servers[$server_name])) {
+        die('Invalid parameters');
+    }
+    
+    $server = $servers[$server_name];
+    $script_path = SHOW_CERT_SCRIPT;
+    $pki_dir = dirname($server['cert_index']);
+    $template_path = $server['cfg_template'] ?? '';
+    
+    $output =[];
+    if (!empty($pki_dir)) {
+        // Безопасное выполнение скрипта
+	$command = sprintf(
+    	    'sudo %s %s %s 2>&1',
+            escapeshellcmd($script_path),
+	    escapeshellarg($username),
+    	    escapeshellarg($pki_dir)
+        );
+	exec($command, $output, $return_var);
+        if ($return_var !== 0) {
+	    die('Failed to generate config: ' . implode("\n", $output));
+	    }
+	}
+
+    // Формируем контент
+    $template_content = file_exists($template_path) && is_readable($template_path)
+        ? file_get_contents($template_path)
+        : null;
+    
+    // Получаем вывод скрипта
+    $script_output = !empty($output) ? implode("\n", $output) : null;
+    
+    // Формируем итоговый контент по приоритетам
+    if ($template_content !== null && $script_output !== null) {
+        // Оба источника доступны - объединяем
+        $config_content = $template_content . "\n" . $script_output;
+    } elseif ($template_content !== null) {
+        // Только шаблон доступен
+        $config_content = $template_content;
+    } elseif ($script_output !== null) {
+        // Только вывод скрипта доступен
+        $config_content = $script_output;
+    } else {
+        // Ничего не доступно - ошибка
+        die('Error: Neither template nor script output available');
+    }
+    
+    // Прямая отдача контента
+    header('Content-Type: application/octet-stream');
+    header('Content-Disposition: attachment; filename="' . $server['name'].'-'.$username . '.ovpn"');
+    header('Content-Length: ' . strlen($config_content));
+    echo $config_content;
+    $clean_url = strtok($_SERVER['REQUEST_URI'], '?');
+    header("Refresh:0; url=" . $clean_url);
+    exit;
+    }
+
 ?>
 
 <!DOCTYPE html>
 <html>
 <head>
     <title><?= htmlspecialchars($page_title) ?></title>
+    <meta name="csrf_token" content="<?= $_SESSION['csrf_token'] ?>">
     <style>
         body { font-family: Arial, sans-serif; margin: 20px; }
         table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
@@ -66,6 +136,23 @@ if (!isset($_SESSION['last_request_time']) || !is_array($_SESSION['last_request_
         }
         .loading { color: #666; font-style: italic; }
         .last-update { font-size: 0.8em; color: #666; margin-top: 5px; }
+        .spinner {
+            position: fixed;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            width: 40px;
+            height: 40px;
+            border: 4px solid rgba(0, 0, 0, 0.1);
+            border-radius: 50%;
+            border-left-color: #09f;
+            animation: spin 1s linear infinite;
+            z-index: 1000;
+        }
+        @keyframes spin {
+            0% { transform: translate(-50%, -50%) rotate(0deg); }
+            100% { transform: translate(-50%, -50%) rotate(360deg); }
+        }
     </style>
 </head>
 <body>
@@ -164,6 +251,52 @@ if (!isset($_SESSION['last_request_time']) || !is_array($_SESSION['last_request_
                 button.classList.remove('collapsed');
             }
         }
+
+        function generateConfig(server, username, event) {
+            event.preventDefault();
+            
+            if (!confirm('Сгенерировать конфигурацию для ' + username + '?')) {
+                return false;
+            }
+            
+            // Индикатор загрузки
+            const spinner = document.createElement('div');
+            spinner.className = 'spinner';
+            document.body.appendChild(spinner);
+            
+            const csrf = document.querySelector('meta[name="csrf_token"]').content;
+            const params = new URLSearchParams({
+                server: server,
+                action: 'generate_config',
+                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;
+        }
+
     </script>
 
 </body>

+ 34 - 0
server1.ovpn.template

@@ -0,0 +1,34 @@
+client
+dev tun
+
+<connection>
+remote vpn.example.com 1194 udp
+nobind
+connect-timeout 15
+</connection>
+
+resolv-retry infinite
+
+persist-key
+persist-tun
+
+remote-cert-tls server
+
+verb 3
+mute 20
+
+#cipher AES-256-CBC
+#cipher AES-128-CBC
+#cipher AES-256-GCM
+#cipher AES-128-GCM
+
+#data-ciphers "AES-256-CBC:AES-128-CBC:AES-256-GCM:AES-128-GCM"
+
+#auth sha256
+#auth-nocache
+
+<ca>
+-----BEGIN CERTIFICATE-----
+...
+-----END CERTIFICATE-----
+</ca>