index.php 22 KB

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