index.php 21 KB

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