functions.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. <?php
  2. defined('CONFIG') or die('Direct access not allowed');
  3. function canRequestStatus($server) {
  4. if (!isset($_SESSION['last_request_time'][$server['name']])) { return true; }
  5. if (time() - $_SESSION['last_request_time'][$server['name']] >= REQUEST_INTERVAL) { return true; }
  6. return false;
  7. }
  8. function updateLastRequestTime($server) {
  9. $_SESSION['last_request_time'][$server['name']] = time();
  10. }
  11. function openvpnManagementCommand($server, $command) {
  12. $mgmt_host = $server['host'];
  13. $mgmt_port = $server['port'];
  14. $mgmt_pass = $server['password'];
  15. $timeout = 5;
  16. $socket = @fsockopen($mgmt_host, $mgmt_port, $errno, $errstr, $timeout);
  17. if (!$socket) {
  18. error_log("OpenVPN management connection failed to {$server['name']}: $errstr ($errno)");
  19. return false;
  20. }
  21. stream_set_timeout($socket, $timeout);
  22. try {
  23. // Читаем приветственное сообщение
  24. $welcome = '';
  25. while (!feof($socket)) {
  26. $line = fgets($socket);
  27. if ($line === false) break;
  28. $welcome .= $line;
  29. if (strpos($welcome, 'ENTER PASSWORD:') !== false) break;
  30. }
  31. // Отправляем пароль
  32. if (@fwrite($socket, "$mgmt_pass\n") === false) {
  33. throw new Exception("Failed to send password");
  34. }
  35. // Ждем подтверждения аутентификации
  36. $authResponse = '';
  37. while (!feof($socket)) {
  38. $line = fgets($socket);
  39. if ($line === false) break;
  40. $authResponse .= $line;
  41. if (strpos($authResponse, 'SUCCESS:') !== false || strpos($authResponse, '>INFO:') !== false) break;
  42. }
  43. // Отправляем команду
  44. if (@fwrite($socket, "$command\n") === false) {
  45. throw new Exception("Failed to send command");
  46. }
  47. // Читаем ответ
  48. $response = '';
  49. $expectedEnd = strpos($command, 'status') !== false ? "END\r\n" : ">";
  50. while (!feof($socket)) {
  51. $line = fgets($socket);
  52. if ($line === false) break;
  53. $response .= $line;
  54. if (strpos($response, $expectedEnd) !== false) break;
  55. }
  56. return $response;
  57. } catch (Exception $e) {
  58. error_log("OpenVPN management error ({$server['name']}): " . $e->getMessage());
  59. return false;
  60. } finally {
  61. @fwrite($socket, "quit\n");
  62. @fclose($socket);
  63. }
  64. }
  65. function getOpenVPNStatus($server) {
  66. // Проверяем, можно ли делать запрос
  67. if (!canRequestStatus($server)) {
  68. // Возвращаем кэшированные данные или пустой массив
  69. return $_SESSION['cached_status'][$server['name']] ?? [];
  70. }
  71. // Обновляем время последнего запроса
  72. updateLastRequestTime($server);
  73. $response = openvpnManagementCommand($server, "status 2");
  74. if (!$response) return $_SESSION['cached_status'][$server['name']] ?? [];
  75. $clients = [];
  76. $lines = explode("\n", $response);
  77. $in_client_list = false;
  78. foreach ($lines as $line) {
  79. $line = trim($line);
  80. if (strpos($line, 'HEADER,CLIENT_LIST') === 0) {
  81. $in_client_list = true;
  82. continue;
  83. }
  84. if (strpos($line, 'HEADER,ROUTING_TABLE') === 0) {
  85. $in_client_list = false;
  86. continue;
  87. }
  88. //CLIENT_LIST,Common Name,Real Address,Virtual Address,Virtual IPv6 Address,Bytes Received,Bytes Sent,Connected Since,Connected Since (time_t),Username,Client ID,Peer ID,Data Channel Cipher
  89. if ($in_client_list && strpos($line, 'CLIENT_LIST') === 0) {
  90. $parts = explode(',', $line);
  91. if (count($parts) >= 9) {
  92. $clients[] = [
  93. 'name' => $parts[1],
  94. 'real_ip' => $parts[2],
  95. 'virtual_ip' => $parts[3],
  96. 'bytes_received' => formatBytes($parts[5]),
  97. 'bytes_sent' => formatBytes($parts[6]),
  98. 'connected_since' => $parts[7],
  99. 'username' => $parts[8] ?? $parts[1],
  100. 'cipher' => end($parts),
  101. 'banned' => isClientBanned($server, $parts[1]),
  102. ];
  103. }
  104. }
  105. }
  106. // Кэшируем результат
  107. $_SESSION['cached_status'][$server['name']] = $clients;
  108. return $clients;
  109. }
  110. function isServerCertificate($cert_index_path, $username) {
  111. // Получаем путь к каталогу issued
  112. $issued_dir = dirname(dirname($cert_index_path)) . '/pki/issued';
  113. // Проверяем существование каталога
  114. if (!is_dir($issued_dir)) {
  115. return 'fail: issued directory not found';
  116. }
  117. // Формируем путь к файлу сертификата
  118. $cert_file = $issued_dir . '/' . $username . '.crt';
  119. // Проверяем существование файла
  120. if (!file_exists($cert_file)) {
  121. return 'success: certificate file not found';
  122. }
  123. // Читаем содержимое сертификата
  124. $cert_content = file_get_contents($cert_file);
  125. if ($cert_content === false) {
  126. return 'success: cannot read certificate file';
  127. }
  128. // Парсим сертификат
  129. $cert_info = openssl_x509_parse($cert_content);
  130. if ($cert_info === false) {
  131. return 'success: invalid certificate format';
  132. }
  133. // Проверяем Subject CN (Common Name)
  134. $common_name = $cert_info['subject']['CN'] ?? '';
  135. if ( $common_name !== $username) {
  136. return 'success: common name '.$common_name.' differ from username '.$username;
  137. }
  138. // Проверяем Extended Key Usage (если есть)
  139. $ext_key_usage = $cert_info['extensions']['extendedKeyUsage'] ?? '';
  140. // Проверяем, является ли это серверным сертификатом
  141. // Серверные сертификаты обычно имеют:
  142. // 1. CN, содержащее имя сервера (например, "server")
  143. // 2. Extended Key Usage: TLS Web Server Authentication
  144. $is_server_cert = (
  145. stripos($ext_key_usage, 'TLS Web Server Authentication') !== false ||
  146. stripos($ext_key_usage, 'serverAuth') !== false
  147. );
  148. return $is_server_cert ? 'fail: server certificate detected' : 'success';
  149. }
  150. function getAccountList($server) {
  151. $accounts = [];
  152. // Получаем список из index.txt (неотозванные сертификаты)
  153. if (!empty($server['cert_index']) && !empty(SHOW_PKI_INDEX) && file_exists(SHOW_PKI_INDEX)) {
  154. // Безопасное выполнение скрипта
  155. $command = sprintf(
  156. 'sudo %s %s 2>&1',
  157. escapeshellcmd(SHOW_PKI_INDEX),
  158. escapeshellarg($server['cert_index']),
  159. );
  160. exec($command, $index_content, $return_var);
  161. if ($return_var == 0) {
  162. foreach ($index_content as $line) {
  163. if (empty(trim($line))) { continue; }
  164. if (preg_match('/\/CN=([^\/]+)/', $line, $matches)) {
  165. $username = trim($matches[1]);
  166. }
  167. if (empty($username)) { continue; }
  168. $revoked = false;
  169. if (preg_match('/^R\s+/',$line)) { $revoked = true; }
  170. $result = isServerCertificate($server['cert_index'], $username);
  171. if (strpos($result, 'fail:') === 0) { continue; }
  172. $accounts[$username] = [
  173. "username" => $username,
  174. "ip" => null,
  175. "banned" => isClientBanned($server, $username) || $revoked,
  176. "revoked" => $revoked
  177. ];
  178. }
  179. }
  180. }
  181. // Получаем список выданных IP из ipp.txt
  182. if (!empty($server['ipp_file']) && file_exists($server['ipp_file'])) {
  183. $ipp_content = file_get_contents($server['ipp_file']);
  184. $lines = explode("\n", $ipp_content);
  185. foreach ($lines as $line) {
  186. if (empty(trim($line))) continue;
  187. $parts = explode(',', $line);
  188. if (count($parts) >= 2) {
  189. $username = $parts[0];
  190. $ip = $parts[1];
  191. if (!isset($accounts[$username]) && empty($server['cert_index'])) {
  192. $accounts[$username] = [
  193. "username" => $username,
  194. "banned" => isClientBanned($server, $username),
  195. "ip" => $ip,
  196. "revoked" => false,
  197. ];
  198. }
  199. if (isset($accounts[$username]) and !empty($server['cert_index'])) {
  200. $accounts[$username]["ip"] = $ip;
  201. }
  202. }
  203. }
  204. }
  205. // Ищем IP-адреса в CCD файлах
  206. if (!empty($server['ccd']) && is_dir($server['ccd'])) {
  207. $ccd_files = scandir($server['ccd']);
  208. foreach ($ccd_files as $file) {
  209. if ($file === '.' || $file === '..') continue;
  210. $username = $file;
  211. $filepath = $server['ccd'] . '/' . $file;
  212. $content = file_get_contents($filepath);
  213. // Ищем строку ifconfig-push с IP адресом
  214. if (preg_match('/ifconfig-push\s+(\d+\.\d+\.\d+\.\d+)/', $content, $matches)) {
  215. $ip = $matches[1];
  216. if (!isset($accounts[$username]) && empty($server['cert_index'])) {
  217. $accounts[$username] = [
  218. "username" => $username,
  219. "banned" => isClientBanned($server, $username),
  220. "ip" => $ip,
  221. "revoked" => false,
  222. ];
  223. }
  224. if (isset($accounts[$username]) and !empty($server['cert_index'])) {
  225. $accounts[$username]["ip"] = $ip;
  226. }
  227. }
  228. }
  229. }
  230. return $accounts;
  231. }
  232. function isClientBanned($server, $client_name) {
  233. $ccd_file = "{$server['ccd']}/$client_name";
  234. return file_exists($ccd_file) &&
  235. preg_match('/^disable$/m', file_get_contents($ccd_file));
  236. }
  237. function kickClient($server, $client_name) {
  238. return openvpnManagementCommand($server, "kill $client_name");
  239. }
  240. function banClient($server, $client_name) {
  241. $ccd_file = "{$server['ccd']}/$client_name";
  242. // Добавляем директиву disable
  243. $content = file_exists($ccd_file) ? file_get_contents($ccd_file) : '';
  244. if (!preg_match('/^disable$/m', $content)) {
  245. file_put_contents($ccd_file, $content . "\ndisable\n");
  246. }
  247. // Кикаем клиента
  248. kickClient($server, $client_name);
  249. return true;
  250. }
  251. function revokeClient($server, $client_name) {
  252. if (empty(REVOKE_CRT) || !file_exists(REVOKE_CRT)) {
  253. return banClient($server, $client_name);
  254. }
  255. $script_path = REVOKE_CRT;
  256. $rsa_dir = dirname(dirname($server['cert_index']));
  257. $command = sprintf(
  258. 'sudo %s %s %s %s 2>&1',
  259. escapeshellcmd($script_path),
  260. escapeshellarg('openvpn-server@'.$server['name']),
  261. escapeshellarg($rsa_dir),
  262. escapeshellarg($client_name)
  263. );
  264. exec($command, $output, $return_var);
  265. if ($return_var === 0) {
  266. return true;
  267. } else {
  268. return false;
  269. }
  270. }
  271. function unbanClient($server, $client_name) {
  272. $ccd_file = "{$server['ccd']}/$client_name";
  273. if (file_exists($ccd_file)) {
  274. $content = file_get_contents($ccd_file);
  275. $new_content = preg_replace('/^disable$\n?/m', '', $content);
  276. file_put_contents($ccd_file, $new_content);
  277. return true;
  278. }
  279. return false;
  280. }
  281. function formatBytes($bytes) {
  282. $bytes = (int)$bytes;
  283. if ($bytes <= 0) return '0 B';
  284. $units = ['B', 'KB', 'MB', 'GB', 'TB'];
  285. $pow = floor(log($bytes)/log(1024));
  286. return round($bytes/pow(1024,$pow),2).' '.$units[$pow];
  287. }
  288. function getBannedClients($server, $active_clients) {
  289. $banned = [];
  290. $active_names = array_column($active_clients, 'name');
  291. if (is_dir($server['ccd'])) {
  292. foreach (scandir($server['ccd']) as $file) {
  293. if ($file !== '.' && $file !== '..' && is_file("{$server['ccd']}/$file")) {
  294. if (isClientBanned($server, $file) && !in_array($file, $active_names)) {
  295. $banned[] = $file;
  296. }
  297. }
  298. }
  299. }
  300. return $banned;
  301. }
  302. function isClientActive($active_clients,$username) {
  303. $active_names = array_column($active_clients, 'name');
  304. if (in_array($username,$active_names)) { return true; }
  305. return false;
  306. }