index.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  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. .ban-btn:hover {
  247. background-color: #e65c00;
  248. }
  249. .unban-btn:hover {
  250. background-color: #499E24;
  251. }
  252. </style>
  253. </head>
  254. <body>
  255. <h1><?= htmlspecialchars($page_title) ?></h1>
  256. <!-- Секция создания пользователей -->
  257. <div class="user-creation-section">
  258. <h2>Create User</h2>
  259. <div class="create-user-form">
  260. <form onsubmit="return createUser(event)">
  261. <select name="server" required>
  262. <option value="">Select Server</option>
  263. <?php foreach ($servers as $server_name => $server): ?>
  264. <?php if (!empty($server['cert_index'])): ?>
  265. <option value="<?= htmlspecialchars($server_name) ?>">
  266. <?= htmlspecialchars($server['title']) ?>
  267. </option>
  268. <?php endif; ?>
  269. <?php endforeach; ?>
  270. </select>
  271. <input type="text" name="username" placeholder="Username" required
  272. pattern="[^\s]+" title="Username cannot contain spaces">
  273. <button type="submit">Create User</button>
  274. <div class="message"></div>
  275. </form>
  276. </div>
  277. </div>
  278. <div id="server-container">
  279. <?php foreach ($servers as $server_name => $server): ?>
  280. <div class="server-section" id="server-<?= htmlspecialchars($server_name) ?>">
  281. <h2><?= htmlspecialchars($server['title']) ?></h2>
  282. <div class="loading">Loading data...</div>
  283. </div>
  284. <?php endforeach; ?>
  285. </div>
  286. <script>
  287. // Функция для загрузки данных сервера
  288. function loadServerData(serverName) {
  289. const serverElement = document.getElementById(`server-${serverName}`);
  290. fetch(`get_server_data.php?server=${serverName}&csrf=<?= $_SESSION['csrf_token'] ?>`,{
  291. headers: {
  292. 'X-Requested-With': 'XMLHttpRequest'
  293. }
  294. })
  295. .then(response => response.text())
  296. .then(html => {
  297. serverElement.innerHTML = html;
  298. // Обновляем данные каждые 60 секунд
  299. setTimeout(() => loadServerData(serverName), 60000);
  300. })
  301. .catch(error => {
  302. serverElement.querySelector('.loading').textContent = 'Error loading data';
  303. console.error('Error:', error);
  304. // Повторяем попытку через 10 секунд при ошибке
  305. setTimeout(() => loadServerData(serverName), 10000);
  306. });
  307. }
  308. // Загружаем данные для всех серверов
  309. document.addEventListener('DOMContentLoaded', function() {
  310. <?php foreach ($servers as $server_name => $server): ?>
  311. loadServerData('<?= $server_name ?>');
  312. <?php endforeach; ?>
  313. });
  314. // Функция для обработки действий (ban/unban)
  315. function handleAction(serverName, action, clientName) {
  316. const params = new URLSearchParams();
  317. params.append('server', serverName);
  318. params.append('action', action);
  319. params.append('client', clientName);
  320. fetch('handle_action.php', {
  321. method: 'POST',
  322. headers: {
  323. 'Content-Type': 'application/x-www-form-urlencoded',
  324. 'X-Requested-With': 'XMLHttpRequest'
  325. },
  326. body: params
  327. })
  328. .then(response => {
  329. // 2. Проверяем статус ответа
  330. if (!response.ok) {
  331. throw new Error(`Server returned ${response.status} status`);
  332. }
  333. return response.json();
  334. })
  335. .then(data => {
  336. // 3. Проверяем структуру ответа
  337. if (!data || typeof data.success === 'undefined') {
  338. throw new Error('Invalid server response');
  339. }
  340. if (data.success) {
  341. loadServerData(serverName);
  342. } else {
  343. console.error('Server error:', data.message);
  344. alert(`Error: ${data.message || 'Operation failed'}`);
  345. }
  346. })
  347. .catch(error => {
  348. // 4. Правильное отображение ошибки
  349. console.error('Request failed:', error);
  350. alert(`Request failed: ${error.message}`);
  351. });
  352. }
  353. // Функция для переключения спойлера
  354. function toggleSpoiler(button) {
  355. const content = button.nextElementSibling;
  356. if (content.style.display === "block") {
  357. content.style.display = "none";
  358. button.classList.add('collapsed');
  359. } else {
  360. content.style.display = "block";
  361. button.classList.remove('collapsed');
  362. }
  363. }
  364. function generateConfig(server, username, event) {
  365. event.preventDefault();
  366. if (!confirm('Сгенерировать конфигурацию для ' + username + '?')) {
  367. return false;
  368. }
  369. // Индикатор загрузки
  370. const spinner = document.createElement('div');
  371. spinner.className = 'spinner';
  372. document.body.appendChild(spinner);
  373. const csrf = document.querySelector('meta[name="csrf_token"]').content;
  374. const params = new URLSearchParams({
  375. server: server,
  376. action: 'generate_config',
  377. username: username,
  378. csrf: csrf
  379. });
  380. // Вариант 1: Простое открытие (рекомендуется)
  381. window.open(`?${params.toString()}`, '_blank');
  382. document.body.removeChild(spinner);
  383. /*
  384. // Вариант 2: Через fetch (если нужно строго AJAX)
  385. fetch(`?${params.toString()}`, {
  386. headers: {'X-Requested-With': 'XMLHttpRequest'}
  387. })
  388. .then(response => response.blob())
  389. .then(blob => {
  390. const url = URL.createObjectURL(blob);
  391. const a = document.createElement('a');
  392. a.href = url;
  393. a.download = `${username}.ovpn`;
  394. a.click();
  395. URL.revokeObjectURL(url);
  396. })
  397. .catch(console.error)
  398. .finally(() => document.body.removeChild(spinner));
  399. */
  400. return false;
  401. }
  402. // Функция для создания пользователя
  403. function createUser(event) {
  404. event.preventDefault();
  405. const form = event.target;
  406. const serverSelect = form.querySelector('select[name="server"]');
  407. const usernameInput = form.querySelector('input[name="username"]');
  408. const messageDiv = form.querySelector('.message');
  409. const button = form.querySelector('button');
  410. const serverName = serverSelect.value;
  411. const username = usernameInput.value.trim();
  412. // Валидация
  413. if (!serverName) {
  414. messageDiv.textContent = 'Please select a server';
  415. messageDiv.className = 'message error';
  416. return false;
  417. }
  418. if (!username) {
  419. messageDiv.textContent = 'Please enter username';
  420. messageDiv.className = 'message error';
  421. return false;
  422. }
  423. if (/\s/.test(username)) {
  424. messageDiv.textContent = 'Username cannot contain spaces';
  425. messageDiv.className = 'message error';
  426. return false;
  427. }
  428. // Блокируем кнопку на время выполнения
  429. button.disabled = true;
  430. messageDiv.textContent = 'Creating user...';
  431. messageDiv.className = 'message';
  432. const csrf = document.querySelector('meta[name="csrf_token"]').content;
  433. const formData = new FormData();
  434. formData.append('server', serverName);
  435. formData.append('action', 'create_user');
  436. formData.append('username', username);
  437. formData.append('csrf', csrf);
  438. fetch('', {
  439. method: 'POST',
  440. body: formData,
  441. headers: {
  442. 'X-Requested-With': 'XMLHttpRequest'
  443. }
  444. })
  445. .then(response => response.json())
  446. .then(data => {
  447. if (data.success) {
  448. messageDiv.textContent = data.message || 'User created successfully';
  449. messageDiv.className = 'message success';
  450. usernameInput.value = '';
  451. // Перезагружаем данные выбранного сервера
  452. loadServerData(serverName);
  453. } else {
  454. messageDiv.textContent = data.message || 'Error creating user';
  455. messageDiv.className = 'message error';
  456. }
  457. })
  458. .catch(error => {
  459. messageDiv.textContent = 'Request failed: ' + error.message;
  460. messageDiv.className = 'message error';
  461. })
  462. .finally(() => {
  463. button.disabled = false;
  464. // Очищаем сообщение через 5 секунд
  465. setTimeout(() => {
  466. messageDiv.textContent = '';
  467. messageDiv.className = 'message';
  468. }, 5000);
  469. });
  470. return false;
  471. }
  472. // Простая версия с разными confirm сообщениями
  473. function confirmAction(action, username, serverName, event) {
  474. event.preventDefault();
  475. let message;
  476. let isDangerous = false;
  477. switch(action) {
  478. case 'ban':
  479. message = `Ban user ${username}?`;
  480. break;
  481. case 'unban':
  482. message = `Unban user ${username}?`;
  483. break;
  484. case 'revoke':
  485. message = `WARNING: Revoke certificate for ${username}?\n\nThis action is irreversible and will permanently disable the certificate!`;
  486. isDangerous = true;
  487. break;
  488. default:
  489. message = `Perform ${action} on ${username}?`;
  490. }
  491. if (isDangerous) {
  492. // Двойное подтверждение для опасных действий
  493. if (confirm('⚠ ️ DANGEROUS ACTION - Please confirm')) {
  494. if (confirm(message)) {
  495. handleAction(serverName, action, username);
  496. }
  497. }
  498. } else {
  499. if (confirm(message)) {
  500. handleAction(serverName, action, username);
  501. }
  502. }
  503. return false;
  504. }
  505. </script>
  506. </body>
  507. </html>