index.php 23 KB

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