1
0

index.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. <?php
  2. define("CONFIG", 1);
  3. // Настройки
  4. $page_title = 'OpenVPN Status';
  5. // Подключаем конфигурационный файл
  6. $config_file = __DIR__ . '/config.php';
  7. if (!file_exists($config_file)) {
  8. die("Configuration file not found: $config_file");
  9. }
  10. $servers = require $config_file;
  11. require_once 'functions.php';
  12. session_start();
  13. $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
  14. // Проверяем и инициализируем массив, если его нет
  15. if (!isset($_SESSION['last_request_time']) || !is_array($_SESSION['last_request_time'])) {
  16. $_SESSION['last_request_time'] = []; // Создаем пустой массив
  17. }
  18. if (isset($_GET['action']) && $_GET['action'] === 'generate_config') {
  19. session_start();
  20. // Проверка CSRF
  21. // if (empty($_GET['csrf']) || $_GET['csrf'] !== $_SESSION['csrf_token']) {
  22. // header('HTTP/1.0 403 Forbidden');
  23. // die('Invalid CSRF token');
  24. // }
  25. $server_name = $_GET['server'] ?? '';
  26. $username = $_GET['username'] ?? '';
  27. if (empty($username) || !isset($servers[$server_name])) {
  28. die('Invalid parameters');
  29. }
  30. $server = $servers[$server_name];
  31. $script_path = SHOW_CERT_SCRIPT;
  32. $pki_dir = dirname($server['cert_index']);
  33. $template_path = $server['cfg_template'] ?? '';
  34. $output =[];
  35. if (!empty($pki_dir)) {
  36. // Безопасное выполнение скрипта
  37. $command = sprintf(
  38. 'sudo %s %s %s 2>&1',
  39. escapeshellcmd($script_path),
  40. escapeshellarg($username),
  41. escapeshellarg($pki_dir)
  42. );
  43. exec($command, $output, $return_var);
  44. if ($return_var !== 0) {
  45. die('Failed to generate config: ' . implode("\n", $output));
  46. }
  47. }
  48. // Формируем контент
  49. $template_content = file_exists($template_path) && is_readable($template_path)
  50. ? file_get_contents($template_path)
  51. : null;
  52. // Получаем вывод скрипта
  53. $script_output = !empty($output) ? implode("\n", $output) : null;
  54. // Формируем итоговый контент по приоритетам
  55. if ($template_content !== null && $script_output !== null) {
  56. // Оба источника доступны - объединяем
  57. $config_content = $template_content . "\n" . $script_output;
  58. } elseif ($template_content !== null) {
  59. // Только шаблон доступен
  60. $config_content = $template_content;
  61. } elseif ($script_output !== null) {
  62. // Только вывод скрипта доступен
  63. $config_content = $script_output;
  64. } else {
  65. // Ничего не доступно - ошибка
  66. die('Error: Neither template nor script output available');
  67. }
  68. // Прямая отдача контента
  69. header('Content-Type: application/octet-stream');
  70. header('Content-Disposition: attachment; filename="' . $server['name'].'-'.$username . '.ovpn"');
  71. header('Content-Length: ' . strlen($config_content));
  72. echo $config_content;
  73. $clean_url = strtok($_SERVER['REQUEST_URI'], '?');
  74. header("Refresh:0; url=" . $clean_url);
  75. exit;
  76. }
  77. // Обработка создания пользователя
  78. if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_user') {
  79. process_create_user($servers);
  80. exit;
  81. }
  82. ?>
  83. <!DOCTYPE html>
  84. <html>
  85. <head>
  86. <title><?= htmlspecialchars($page_title) ?></title>
  87. <meta name="csrf_token" content="<?= $_SESSION['csrf_token'] ?>">
  88. <style>
  89. body { font-family: Arial, sans-serif; margin: 20px; }
  90. table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
  91. th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
  92. th { background-color: #f2f2f2; }
  93. .banned { background-color: #ffeeee; }
  94. .actions { white-space: nowrap; }
  95. .btn { padding: 3px 8px; margin: 2px; cursor: pointer; border: 1px solid #ccc; border-radius: 3px; }
  96. .kick-btn { background-color: #ffcccc; }
  97. .ban-btn { background-color: #ff9999; }
  98. .unban-btn { background-color: #ccffcc; }
  99. .section { margin-bottom: 30px; }
  100. .status-badge { padding: 2px 5px; border-radius: 3px; font-size: 0.8em; }
  101. .status-active { background-color: #ccffcc; }
  102. .status-banned { background-color: #ff9999; }
  103. .cert-date {
  104. font-size: 0.85em;
  105. font-family: monospace;
  106. padding: 2px 6px;
  107. border-radius: 4px;
  108. }
  109. .cert-date.valid {
  110. color: #28a745;
  111. background-color: #e8f5e9;
  112. }
  113. .cert-date.expiring-soon {
  114. color: #ff9800;
  115. background-color: #fff3e0;
  116. }
  117. .cert-date.expires-today {
  118. color: #ff5722;
  119. background-color: #fbe9e7;
  120. font-weight: bold;
  121. }
  122. .cert-date.expired {
  123. color: #f44336;
  124. background-color: #ffebee;
  125. text-decoration: line-through;
  126. }
  127. .cert-date.error {
  128. color: #9e9e9e;
  129. background-color: #f5f5f5;
  130. }
  131. .server-section { border: 1px solid #ddd; padding: 15px; margin-bottom: 20px; border-radius: 5px; }
  132. .spoiler { margin-top: 10px; }
  133. .spoiler-title {
  134. cursor: pointer;
  135. color: #0066cc;
  136. padding: 5px;
  137. background-color: #f0f0f0;
  138. border: 1px solid #ddd;
  139. border-radius: 3px;
  140. display: inline-block;
  141. margin-bottom: 5px;
  142. }
  143. .spoiler-title:after { content: " ▼"; }
  144. .spoiler-title.collapsed:after { content: " ►"; }
  145. .spoiler-content {
  146. display: none;
  147. padding: 10px;
  148. border: 1px solid #ddd;
  149. margin-top: 5px;
  150. background-color: #f9f9f9;
  151. border-radius: 3px;
  152. }
  153. .loading { color: #666; font-style: italic; }
  154. .last-update { font-size: 0.8em; color: #666; margin-top: 5px; }
  155. .spinner {
  156. position: fixed;
  157. top: 50%;
  158. left: 50%;
  159. transform: translate(-50%, -50%);
  160. width: 40px;
  161. height: 40px;
  162. border: 4px solid rgba(0, 0, 0, 0.1);
  163. border-radius: 50%;
  164. border-left-color: #09f;
  165. animation: spin 1s linear infinite;
  166. z-index: 1000;
  167. }
  168. @keyframes spin {
  169. 0% { transform: translate(-50%, -50%) rotate(0deg); }
  170. 100% { transform: translate(-50%, -50%) rotate(360deg); }
  171. }
  172. .create-user-form {
  173. margin: 15px 0;
  174. padding: 10px;
  175. background-color: #f9f9f9;
  176. border: 1px solid #ddd;
  177. border-radius: 5px;
  178. }
  179. .create-user-form input[type="text"],
  180. .create-user-form select {
  181. padding: 5px;
  182. border: 1px solid #ccc;
  183. border-radius: 3px;
  184. margin-right: 5px;
  185. }
  186. .create-user-form button {
  187. padding: 5px 10px;
  188. background-color: #4CAF50;
  189. color: white;
  190. border: none;
  191. border-radius: 3px;
  192. cursor: pointer;
  193. }
  194. .create-user-form button:hover {
  195. background-color: #45a049;
  196. }
  197. .create-user-form .error {
  198. color: #d9534f;
  199. margin-top: 5px;
  200. }
  201. .create-user-form .success {
  202. color: #5cb85c;
  203. margin-top: 5px;
  204. }
  205. .user-creation-section {
  206. margin-bottom: 30px;
  207. border: 1px solid #ddd;
  208. padding: 15px;
  209. border-radius: 5px;
  210. }
  211. /* Стили для временных сообщений */
  212. .temp-message {
  213. position: fixed;
  214. top: 20px;
  215. right: 20px;
  216. padding: 10px 20px;
  217. border-radius: 5px;
  218. color: white;
  219. z-index: 1000;
  220. opacity: 0;
  221. transition: opacity 0.3s;
  222. }
  223. .temp-message.success {
  224. background: green;
  225. }
  226. .temp-message.error {
  227. background: red;
  228. }
  229. /* Стили для disabled кнопок */
  230. .btn:disabled {
  231. opacity: 0.6;
  232. cursor: not-allowed;
  233. }
  234. .revoked-text {
  235. color: #999;
  236. font-style: italic;
  237. }
  238. .revoke-btn {
  239. background-color: #ff9999;
  240. }
  241. .revoke-btn:hover {
  242. background-color: #e65c00;
  243. }
  244. .remove-text {
  245. color: #999;
  246. font-style: italic;
  247. }
  248. .remove-btn {
  249. background-color: #ff9999;
  250. }
  251. .remove-btn:hover {
  252. background-color: #e65c00;
  253. }
  254. .ban-btn:hover {
  255. background-color: #e65c00;
  256. }
  257. .unban-btn:hover {
  258. background-color: #499E24;
  259. }
  260. </style>
  261. </head>
  262. <body>
  263. <h1><?= htmlspecialchars($page_title) ?></h1>
  264. <!-- Секция создания пользователей -->
  265. <div class="user-creation-section">
  266. <h2>Create User</h2>
  267. <div class="create-user-form">
  268. <form onsubmit="return createUser(event)">
  269. <select name="server" required>
  270. <option value="">Select Server</option>
  271. <?php foreach ($servers as $server_name => $server): ?>
  272. <?php if (!empty($server['cert_index'])): ?>
  273. <option value="<?= htmlspecialchars($server_name) ?>">
  274. <?= htmlspecialchars($server['title']) ?>
  275. </option>
  276. <?php endif; ?>
  277. <?php endforeach; ?>
  278. </select>
  279. <input type="text" name="username" placeholder="Username" required
  280. pattern="[^\s]+" title="Username cannot contain spaces">
  281. <button type="submit">Create User</button>
  282. <div class="message"></div>
  283. </form>
  284. </div>
  285. </div>
  286. <div id="server-container">
  287. <?php foreach ($servers as $server_name => $server): ?>
  288. <div class="server-section" id="server-<?= htmlspecialchars($server_name) ?>">
  289. <h2><?= htmlspecialchars($server['title']) ?></h2>
  290. <div class="loading">Loading data...</div>
  291. </div>
  292. <?php endforeach; ?>
  293. </div>
  294. <script>
  295. function editCCD(server, username) {
  296. const width = 800;
  297. const height = 600;
  298. const left = (screen.width/2) - (width/2);
  299. const top = (screen.height/2) - (height/2);
  300. const win = window.open('', 'editCCD', `width=${width},height=${height},top=${top},left=${left},resizable=yes,scrollbars=yes`);
  301. win.document.write('<h3>Edit CCD for ' + username + ' on ' + server + '</h3>');
  302. win.document.write('<textarea id="ccd-textarea" style="width:100%; height:80%; font-family: monospace;">Loading...</textarea><br>');
  303. win.document.write('<button onclick="saveCCD()">Save</button> <span id="save-status"></span>');
  304. // Функция для сохранения
  305. win.saveCCD = function() {
  306. const textarea = win.document.getElementById('ccd-textarea');
  307. const status = win.document.getElementById('save-status');
  308. status.textContent = 'Saving...';
  309. fetch('save_user_config.php', {
  310. method: 'POST',
  311. headers: {
  312. 'Content-Type': 'application/json',
  313. 'X-Requested-With': 'XMLHttpRequest'
  314. },
  315. body: JSON.stringify({
  316. server: server,
  317. username: username,
  318. config: textarea.value
  319. })
  320. })
  321. .then(r => r.json())
  322. .then(data => {
  323. if (data.success) {
  324. status.textContent = 'Saved ✅';
  325. setTimeout(() => win.close(), 1000); // закрываем окно через секунду
  326. } else {
  327. status.textContent = 'Error: ' + (data.message || 'Unknown');
  328. }
  329. })
  330. .catch(err => {
  331. status.textContent = 'Request failed: ' + err.message;
  332. });
  333. };
  334. // Загружаем текущее содержимое CCD
  335. fetch(`get_user_config.php?server=${encodeURIComponent(server)}&username=${encodeURIComponent(username)}`, {
  336. headers: {'X-Requested-With': 'XMLHttpRequest'}
  337. })
  338. .then(r => r.text())
  339. .then(data => { win.document.getElementById('ccd-textarea').value = data; })
  340. .catch(err => { win.document.getElementById('ccd-textarea').value = 'Error loading: ' + err.message; });
  341. }
  342. </script>
  343. <script>
  344. // Функция для загрузки данных сервера
  345. function loadServerData(serverName) {
  346. const serverElement = document.getElementById(`server-${serverName}`);
  347. fetch(`get_server_data.php?server=${serverName}&csrf=<?= $_SESSION['csrf_token'] ?>`,{
  348. headers: {
  349. 'X-Requested-With': 'XMLHttpRequest'
  350. }
  351. })
  352. .then(response => response.text())
  353. .then(html => {
  354. serverElement.innerHTML = html;
  355. // Обновляем данные каждые 60 секунд
  356. setTimeout(() => loadServerData(serverName), 60000);
  357. })
  358. .catch(error => {
  359. serverElement.querySelector('.loading').textContent = 'Error loading data';
  360. console.error('Error:', error);
  361. // Повторяем попытку через 10 секунд при ошибке
  362. setTimeout(() => loadServerData(serverName), 10000);
  363. });
  364. }
  365. // Загружаем данные для всех серверов
  366. document.addEventListener('DOMContentLoaded', function() {
  367. <?php foreach ($servers as $server_name => $server): ?>
  368. loadServerData('<?= $server_name ?>');
  369. <?php endforeach; ?>
  370. });
  371. // Функция для обработки действий (ban/unban)
  372. function handleAction(serverName, action, clientName) {
  373. const params = new URLSearchParams();
  374. params.append('server', serverName);
  375. params.append('action', action);
  376. params.append('client', clientName);
  377. fetch('handle_action.php', {
  378. method: 'POST',
  379. headers: {
  380. 'Content-Type': 'application/x-www-form-urlencoded',
  381. 'X-Requested-With': 'XMLHttpRequest'
  382. },
  383. body: params
  384. })
  385. .then(response => {
  386. // 2. Проверяем статус ответа
  387. if (!response.ok) {
  388. throw new Error(`Server returned ${response.status} status`);
  389. }
  390. return response.json();
  391. })
  392. .then(data => {
  393. // 3. Проверяем структуру ответа
  394. if (!data || typeof data.success === 'undefined') {
  395. throw new Error('Invalid server response');
  396. }
  397. if (data.success) {
  398. loadServerData(serverName);
  399. } else {
  400. console.error('Server error:', data.message);
  401. alert(`Error: ${data.message || 'Operation failed'}`);
  402. }
  403. })
  404. .catch(error => {
  405. // 4. Правильное отображение ошибки
  406. console.error('Request failed:', error);
  407. alert(`Request failed: ${error.message}`);
  408. });
  409. }
  410. // Функция для переключения спойлера
  411. function toggleSpoiler(button) {
  412. const content = button.nextElementSibling;
  413. if (content.style.display === "block") {
  414. content.style.display = "none";
  415. button.classList.add('collapsed');
  416. } else {
  417. content.style.display = "block";
  418. button.classList.remove('collapsed');
  419. }
  420. }
  421. function generateConfig(server, username, event) {
  422. event.preventDefault();
  423. if (!confirm('Сгенерировать конфигурацию для ' + username + '?')) {
  424. return false;
  425. }
  426. // Индикатор загрузки
  427. const spinner = document.createElement('div');
  428. spinner.className = 'spinner';
  429. document.body.appendChild(spinner);
  430. const csrf = document.querySelector('meta[name="csrf_token"]').content;
  431. const params = new URLSearchParams({
  432. server: server,
  433. action: 'generate_config',
  434. username: username,
  435. csrf: csrf
  436. });
  437. window.open(`?${params.toString()}`, '_blank');
  438. document.body.removeChild(spinner);
  439. return false;
  440. }
  441. // Функция для создания пользователя
  442. function createUser(event) {
  443. event.preventDefault();
  444. const form = event.target;
  445. const serverSelect = form.querySelector('select[name="server"]');
  446. const usernameInput = form.querySelector('input[name="username"]');
  447. const messageDiv = form.querySelector('.message');
  448. const button = form.querySelector('button');
  449. const serverName = serverSelect.value;
  450. const username = usernameInput.value.trim();
  451. // Валидация
  452. if (!serverName) {
  453. messageDiv.textContent = 'Please select a server';
  454. messageDiv.className = 'message error';
  455. return false;
  456. }
  457. if (!username) {
  458. messageDiv.textContent = 'Please enter username';
  459. messageDiv.className = 'message error';
  460. return false;
  461. }
  462. if (/\s/.test(username)) {
  463. messageDiv.textContent = 'Username cannot contain spaces';
  464. messageDiv.className = 'message error';
  465. return false;
  466. }
  467. // Блокируем кнопку на время выполнения
  468. button.disabled = true;
  469. messageDiv.textContent = 'Creating user...';
  470. messageDiv.className = 'message';
  471. const csrf = document.querySelector('meta[name="csrf_token"]').content;
  472. const formData = new FormData();
  473. formData.append('server', serverName);
  474. formData.append('action', 'create_user');
  475. formData.append('username', username);
  476. formData.append('csrf', csrf);
  477. fetch('', {
  478. method: 'POST',
  479. body: formData,
  480. headers: {
  481. 'X-Requested-With': 'XMLHttpRequest'
  482. }
  483. })
  484. .then(response => response.json())
  485. .then(data => {
  486. if (data.success) {
  487. messageDiv.textContent = data.message || 'User created successfully';
  488. messageDiv.className = 'message success';
  489. usernameInput.value = '';
  490. // Перезагружаем данные выбранного сервера
  491. loadServerData(serverName);
  492. } else {
  493. messageDiv.textContent = data.message || 'Error creating user';
  494. messageDiv.className = 'message error';
  495. }
  496. })
  497. .catch(error => {
  498. messageDiv.textContent = 'Request failed: ' + error.message;
  499. messageDiv.className = 'message error';
  500. })
  501. .finally(() => {
  502. button.disabled = false;
  503. // Очищаем сообщение через 5 секунд
  504. setTimeout(() => {
  505. messageDiv.textContent = '';
  506. messageDiv.className = 'message';
  507. }, 5000);
  508. });
  509. return false;
  510. }
  511. // Простая версия с разными confirm сообщениями
  512. function confirmAction(action, username, serverName, event) {
  513. event.preventDefault();
  514. let message;
  515. let isDangerous = false;
  516. switch(action) {
  517. case 'ban':
  518. message = `Ban user ${username}?`;
  519. break;
  520. case 'unban':
  521. message = `Unban user ${username}?`;
  522. break;
  523. case 'revoke':
  524. message = `WARNING: Revoke certificate for ${username}?\n\nThis action is irreversible and will permanently disable the certificate!`;
  525. isDangerous = true;
  526. break;
  527. case 'remove':
  528. message = `Remove user ${username} config file?`;
  529. break;
  530. default:
  531. message = `Perform ${action} on ${username}?`;
  532. }
  533. if (isDangerous) {
  534. // Двойное подтверждение для опасных действий
  535. if (confirm('⚠ ️ DANGEROUS ACTION - Please confirm')) {
  536. if (confirm(message)) {
  537. handleAction(serverName, action, username);
  538. }
  539. }
  540. } else {
  541. if (confirm(message)) {
  542. handleAction(serverName, action, username);
  543. }
  544. }
  545. return false;
  546. }
  547. </script>
  548. &copy; 2024–<?= date('Y') ?> — OpenVPN Status Monitoring.
  549. Based on <a href="https://github.com/rajven/openvpn-status-page" target="_blank">openvpn-status-page</a> by rajven. All rights reserved.
  550. </body>
  551. </html>