|
@@ -182,6 +182,74 @@ function get_servers_crt($cert_index) {
|
|
|
return $result;
|
|
return $result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function get_crt_date_info($server, $client_name) {
|
|
|
|
|
+ $default = [
|
|
|
|
|
+ 'date' => '-',
|
|
|
|
|
+ 'status' => 'unknown',
|
|
|
|
|
+ 'days' => null,
|
|
|
|
|
+ 'valid' => false,
|
|
|
|
|
+ 'html' => '<span class="cert-date error">-</span>'
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ if (empty($server) || empty($client_name) || empty(SHOW_CRT_DATE)) {
|
|
|
|
|
+ return $default;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $pki_dir = dirname($server['cert_index']);
|
|
|
|
|
+
|
|
|
|
|
+ $command = sprintf(
|
|
|
|
|
+ 'sudo %s %s %s 2>&1',
|
|
|
|
|
+ escapeshellcmd(SHOW_CRT_DATE),
|
|
|
|
|
+ escapeshellarg($client_name),
|
|
|
|
|
+ escapeshellarg($pki_dir)
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ exec($command, $output, $return_var);
|
|
|
|
|
+
|
|
|
|
|
+ if ($return_var !== 0 || empty($output)) {
|
|
|
|
|
+ error_log("Cert check failed for $client_name");
|
|
|
|
|
+ return $default;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $parts = explode(';', trim($output[0]));
|
|
|
|
|
+
|
|
|
|
|
+ if (count($parts) < 5) {
|
|
|
|
|
+ return $default;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $until_str = $parts[2];
|
|
|
|
|
+ $status = $parts[3];
|
|
|
|
|
+ $days = (int)$parts[4];
|
|
|
|
|
+
|
|
|
|
|
+ $timestamp = strtotime($until_str);
|
|
|
|
|
+ if ($timestamp === false) {
|
|
|
|
|
+ return $default;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $formatted_date = date('Y-m-d', $timestamp);
|
|
|
|
|
+ $is_valid = ($status === 'VALID');
|
|
|
|
|
+
|
|
|
|
|
+ // Генерируем HTML
|
|
|
|
|
+ if (!$is_valid) {
|
|
|
|
|
+ $html = '<span class="cert-date expired">' . $formatted_date . ' (expired ' . $days . 'd ago)</span>';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if ($days < 7) {
|
|
|
|
|
+ $html = '<span class="cert-date expiring-soon">' . $formatted_date . ' (' . $days . 'd left)</span>';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $html = '<span class="cert-date valid">' . $formatted_date . ' (' . $days . 'd left)</span>';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'date' => $formatted_date,
|
|
|
|
|
+ 'status' => $status,
|
|
|
|
|
+ 'days' => $days,
|
|
|
|
|
+ 'valid' => $is_valid,
|
|
|
|
|
+ 'html' => $html
|
|
|
|
|
+ ];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
function getBannedClients($server) {
|
|
function getBannedClients($server) {
|
|
|
// Проверка входных параметров
|
|
// Проверка входных параметров
|
|
|
if (empty($server["ccd"]) || !is_string($server["ccd"])) {
|
|
if (empty($server["ccd"]) || !is_string($server["ccd"])) {
|
|
@@ -313,79 +381,186 @@ function getClientIPsIPP($server) {
|
|
|
|
|
|
|
|
function getAccountList($server) {
|
|
function getAccountList($server) {
|
|
|
$accounts = [];
|
|
$accounts = [];
|
|
|
-
|
|
|
|
|
$banned = getBannedClients($server);
|
|
$banned = getBannedClients($server);
|
|
|
|
|
|
|
|
- // Получаем список из index.txt (неотозванные сертификаты)
|
|
|
|
|
-// if (!empty($server['cert_index']) && !empty(SHOW_PKI_INDEX) && file_exists(SHOW_PKI_INDEX)) {
|
|
|
|
|
|
|
+ // Получаем список из index.txt
|
|
|
if (!empty($server['cert_index']) && !empty(SHOW_PKI_INDEX)) {
|
|
if (!empty($server['cert_index']) && !empty(SHOW_PKI_INDEX)) {
|
|
|
- $servers_list = get_servers_crt($server['cert_index']);
|
|
|
|
|
- // Безопасное выполнение скрипта
|
|
|
|
|
|
|
+ $servers_list = get_servers_crt($server['cert_index']);
|
|
|
|
|
+
|
|
|
$command = sprintf(
|
|
$command = sprintf(
|
|
|
'sudo %s %s 2>&1',
|
|
'sudo %s %s 2>&1',
|
|
|
escapeshellcmd(SHOW_PKI_INDEX),
|
|
escapeshellcmd(SHOW_PKI_INDEX),
|
|
|
- escapeshellarg($server['cert_index']),
|
|
|
|
|
|
|
+ escapeshellarg($server['cert_index'])
|
|
|
);
|
|
);
|
|
|
- exec($command, $index_content, $return_var);
|
|
|
|
|
|
|
+ exec($command, $index_content, $return_var);
|
|
|
|
|
+
|
|
|
if ($return_var == 0) {
|
|
if ($return_var == 0) {
|
|
|
foreach ($index_content as $line) {
|
|
foreach ($index_content as $line) {
|
|
|
- if (empty(trim($line))) { continue; }
|
|
|
|
|
- if (preg_match('/\/CN=([^\/]+)/', $line, $matches)) {
|
|
|
|
|
- $username = trim($matches[1]);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ $line = trim($line);
|
|
|
|
|
+ if (empty($line)) { continue; }
|
|
|
|
|
+
|
|
|
|
|
+ // Парсим строку index.txt
|
|
|
|
|
+ $cert_info = parse_index_line($line);
|
|
|
|
|
+
|
|
|
|
|
+ $username = $cert_info['username'];
|
|
|
|
|
+
|
|
|
if (empty($username)) { continue; }
|
|
if (empty($username)) { continue; }
|
|
|
- $revoked = false;
|
|
|
|
|
- if (preg_match('/^R\s+/',$line)) { $revoked = true; }
|
|
|
|
|
if (isset($servers_list[$username])) { continue; }
|
|
if (isset($servers_list[$username])) { continue; }
|
|
|
- $accounts[$username] = [
|
|
|
|
|
- "username" => $username,
|
|
|
|
|
- "ip" => null,
|
|
|
|
|
- "banned" => isset($banned[$username]) || $revoked,
|
|
|
|
|
- "revoked" => $revoked
|
|
|
|
|
- ];
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Парсим дату окончания
|
|
|
|
|
+ $cert_date = '-';
|
|
|
|
|
+ $days_left = null;
|
|
|
|
|
+ $valid = $cert_info['is_valid'];
|
|
|
|
|
+ $revoked = $cert_info['is_revoked'];
|
|
|
|
|
+ $expired = $cert_info['is_expired'];
|
|
|
|
|
+
|
|
|
|
|
+ if (!empty($cert_info['expires'])) {
|
|
|
|
|
+ $timestamp = parse_openvpn_date($cert_info['expires']);
|
|
|
|
|
+ if ($timestamp) {
|
|
|
|
|
+ $cert_date = date('Y-m-d', $timestamp);
|
|
|
|
|
+ $days_left = ceil(($timestamp - time()) / 86400);
|
|
|
|
|
+
|
|
|
|
|
+ // Корректируем статус если истек по дате, но не помечен как E
|
|
|
|
|
+ if ($valid && $days_left < 0) {
|
|
|
|
|
+ $expired = true;
|
|
|
|
|
+ $valid = false;
|
|
|
|
|
+ $days_left = abs($days_left);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ $accounts[$username] = [
|
|
|
|
|
+ "username" => $username,
|
|
|
|
|
+ "ip" => null,
|
|
|
|
|
+ "banned" => isset($banned[$username]) || $revoked || $expired,
|
|
|
|
|
+ "revoked" => $revoked,
|
|
|
|
|
+ "expired" => $expired,
|
|
|
|
|
+ "valid" => $valid && !$revoked && !$expired,
|
|
|
|
|
+ "cert_date" => $cert_date,
|
|
|
|
|
+ "days_left" => $days_left,
|
|
|
|
|
+ "serial" => $cert_info['serial'],
|
|
|
|
|
+ "status_code" => $cert_info['status'],
|
|
|
|
|
+ "revoke_date" => $cert_info['revoked_date']
|
|
|
|
|
+ ];
|
|
|
}
|
|
}
|
|
|
|
|
+ } else {
|
|
|
|
|
+ error_log("Failed to execute SHOW_PKI_INDEX: " . implode("\n", $index_content));
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Получаем список выданных IP из ipp.txt
|
|
// Получаем список выданных IP из ipp.txt
|
|
|
-// if (!empty($server['ipp_file']) && file_exists($server['ipp_file'])) {
|
|
|
|
|
if (!empty($server['ipp_file'])) {
|
|
if (!empty($server['ipp_file'])) {
|
|
|
- $ipps = getClientIPsIPP($server);
|
|
|
|
|
- foreach ($ipps as $username => $ip) {
|
|
|
|
|
|
|
+ $ipps = getClientIPsIPP($server);
|
|
|
|
|
+ foreach ($ipps as $username => $ip) {
|
|
|
if (!isset($accounts[$username]) && empty($server['cert_index'])) {
|
|
if (!isset($accounts[$username]) && empty($server['cert_index'])) {
|
|
|
- $accounts[$username] = [
|
|
|
|
|
- "username" => $username,
|
|
|
|
|
- "banned" => isset($banned[$username]),
|
|
|
|
|
- "ip" => $ip,
|
|
|
|
|
- "revoked" => false,
|
|
|
|
|
- ];
|
|
|
|
|
- }
|
|
|
|
|
- if (isset($accounts[$username]) and !empty($server['cert_index'])) {
|
|
|
|
|
- $accounts[$username]["ip"] = $ip;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ $accounts[$username] = getDefaultAccount($username, isset($banned[$username]));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isset($accounts[$username]) && !empty($server['cert_index'])) {
|
|
|
|
|
+ $accounts[$username]["ip"] = $ip;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Ищем IP-адреса в CCD файлах
|
|
// Ищем IP-адреса в CCD файлах
|
|
|
if (!empty($server['ccd']) && is_dir($server['ccd'])) {
|
|
if (!empty($server['ccd']) && is_dir($server['ccd'])) {
|
|
|
- $ccds = getClientIPsCCD($server);
|
|
|
|
|
- foreach ($ccds as $username => $ip) {
|
|
|
|
|
|
|
+ $ccds = getClientIPsCCD($server);
|
|
|
|
|
+ foreach ($ccds as $username => $ip) {
|
|
|
if (!isset($accounts[$username]) && empty($server['cert_index'])) {
|
|
if (!isset($accounts[$username]) && empty($server['cert_index'])) {
|
|
|
- $accounts[$username] = [
|
|
|
|
|
- "username" => $username,
|
|
|
|
|
- "banned" => isset($banned[$username]),
|
|
|
|
|
- "ip" => $ip,
|
|
|
|
|
- "revoked" => false,
|
|
|
|
|
- ];
|
|
|
|
|
- }
|
|
|
|
|
- if (isset($accounts[$username]) and !empty($server['cert_index'])) {
|
|
|
|
|
- $accounts[$username]["ip"] = $ip;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ $accounts[$username] = getDefaultAccount($username, isset($banned[$username]));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isset($accounts[$username]) && !empty($server['cert_index'])) {
|
|
|
|
|
+ $accounts[$username]["ip"] = $ip;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
return $accounts;
|
|
return $accounts;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function parse_index_line($line) {
|
|
|
|
|
+ // Разбиваем по табуляции (стандартный разделитель index.txt)
|
|
|
|
|
+ $parts = explode("\t", trim($line));
|
|
|
|
|
+
|
|
|
|
|
+ if (count($parts) < 6) {
|
|
|
|
|
+ // Если табуляция не сработала, пробуем разбить по пробелам с учетом пустых полей
|
|
|
|
|
+ $parts = preg_split('/\s+/', $line);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Структура index.txt:
|
|
|
|
|
+ // [0] - Status (V/R/E)
|
|
|
|
|
+ // [1] - Expiration date (YYMMDDHHMMSSZ)
|
|
|
|
|
+ // [2] - Revocation date (пусто или дата)
|
|
|
|
|
+ // [3] - Serial number (hex)
|
|
|
|
|
+ // [4] - Distinguished Name (например: "unknown /CN=username")
|
|
|
|
|
+ // [5] - может быть еще одно поле если DN содержит пробелы
|
|
|
|
|
+
|
|
|
|
|
+ $status = $parts[0] ?? '';
|
|
|
|
|
+ $expires = $parts[1] ?? '';
|
|
|
|
|
+ $revoked = $parts[2] ?? '';
|
|
|
|
|
+ $serial = $parts[3] ?? '';
|
|
|
|
|
+
|
|
|
|
|
+ // Последнее поле - это DN, может содержать пробелы
|
|
|
|
|
+ // Объединяем все оставшиеся части
|
|
|
|
|
+ $dn = implode(' ', array_slice($parts, 4));
|
|
|
|
|
+
|
|
|
|
|
+ // Извлекаем username из DN
|
|
|
|
|
+ $username = '';
|
|
|
|
|
+ if (preg_match('/\/CN=([^\/\s]+)/', $dn, $matches)) {
|
|
|
|
|
+ $username = trim($matches[1]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'status' => $status,
|
|
|
|
|
+ 'expires' => $expires,
|
|
|
|
|
+ 'revoked_date' => $revoked ?: null,
|
|
|
|
|
+ 'serial' => $serial,
|
|
|
|
|
+ 'dn' => $dn,
|
|
|
|
|
+ 'username' => $username,
|
|
|
|
|
+ 'is_valid' => ($status === 'V'),
|
|
|
|
|
+ 'is_revoked' => ($status === 'R'),
|
|
|
|
|
+ 'is_expired' => ($status === 'E')
|
|
|
|
|
+ ];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Вспомогательная функция для создания записи по умолчанию
|
|
|
|
|
+function getDefaultAccount($username, $is_banned = false) {
|
|
|
|
|
+ return [
|
|
|
|
|
+ "username" => $username,
|
|
|
|
|
+ "ip" => null,
|
|
|
|
|
+ "banned" => $is_banned,
|
|
|
|
|
+ "revoked" => false,
|
|
|
|
|
+ "expired" => false,
|
|
|
|
|
+ "valid" => true,
|
|
|
|
|
+ "cert_date" => '-',
|
|
|
|
|
+ "days_left" => null,
|
|
|
|
|
+ "serial" => null,
|
|
|
|
|
+ "status_code" => 'V',
|
|
|
|
|
+ "revoke_date" => null
|
|
|
|
|
+ ];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Функция парсинга дат OpenVPN (YYMMDDHHMMSSZ)
|
|
|
|
|
+function parse_openvpn_date($date_str) {
|
|
|
|
|
+ if (empty($date_str) || $date_str === 'unknown') {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ // Формат: YYMMDDHHMMSSZ (например: 351119103201Z)
|
|
|
|
|
+ // 35 = 2035 год, 11 = ноябрь, 19 = день, 10:32:01
|
|
|
|
|
+ if (preg_match('/^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/', $date_str, $matches)) {
|
|
|
|
|
+ $year = 2000 + intval($matches[1]);
|
|
|
|
|
+ $month = intval($matches[2]);
|
|
|
|
|
+ $day = intval($matches[3]);
|
|
|
|
|
+ $hour = intval($matches[4]);
|
|
|
|
|
+ $minute = intval($matches[5]);
|
|
|
|
|
+ $second = intval($matches[6]);
|
|
|
|
|
+ // Проверка валидности даты
|
|
|
|
|
+ if (checkdate($month, $day, $year)) {
|
|
|
|
|
+ return mktime($hour, $minute, $second, $month, $day, $year);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function kickClient($server, $client_name) {
|
|
function kickClient($server, $client_name) {
|
|
|
return openvpnManagementCommand($server, "kill $client_name");
|
|
return openvpnManagementCommand($server, "kill $client_name");
|
|
|
}
|
|
}
|
|
@@ -500,3 +675,92 @@ function isClientActive($active_clients,$username) {
|
|
|
if (in_array($username,$active_names)) { return true; }
|
|
if (in_array($username,$active_names)) { return true; }
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+function process_create_user($servers, $server_name = null, $username = null, $force = false) {
|
|
|
|
|
+ // Если параметры не переданы (явно null), берем из $_POST
|
|
|
|
|
+ if ($server_name === null) {
|
|
|
|
|
+ $server_name = $_POST['server'] ?? '';
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($username === null) {
|
|
|
|
|
+ $username = trim($_POST['username'] ?? '');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Проверка наличия скрипта создания
|
|
|
|
|
+ if (empty(CREATE_CRT)) {
|
|
|
|
|
+ send_json_response(false, 'Create certificate script not configured');
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (empty($username) || !isset($servers[$server_name]) || empty($servers[$server_name]['cert_index'])) {
|
|
|
|
|
+ send_json_response(false, 'Invalid parameters');
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Нормализация имени пользователя
|
|
|
|
|
+// mb_internal_encoding('UTF-8');
|
|
|
|
|
+// $username = mb_strtolower($username);
|
|
|
|
|
+
|
|
|
|
|
+ // Проверка на пробельные символы
|
|
|
|
|
+ if (preg_match('/\s/', $username)) {
|
|
|
|
|
+ send_json_response(false, 'Username cannot contain spaces');
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Проверка на специальные символы
|
|
|
|
|
+ if (!preg_match('/^[a-zA-Z0-9_-]+$/', $username)) {
|
|
|
|
|
+ send_json_response(false, 'Username can only contain letters, numbers, underscores and hyphens');
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Проверка длины имени
|
|
|
|
|
+ if (strlen($username) < 3 || strlen($username) > 32) {
|
|
|
|
|
+ send_json_response(false, 'Username must be between 3 and 32 characters');
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $server = $servers[$server_name];
|
|
|
|
|
+ $rsa_dir = dirname(dirname($server['cert_index']));
|
|
|
|
|
+
|
|
|
|
|
+ // Выполнение команды создания пользователя
|
|
|
|
|
+ if (!$force) {
|
|
|
|
|
+ $command = sprintf(
|
|
|
|
|
+ 'sudo %s %s %s 2>&1',
|
|
|
|
|
+ escapeshellcmd(CREATE_CRT),
|
|
|
|
|
+ escapeshellarg($rsa_dir),
|
|
|
|
|
+ escapeshellarg($username)
|
|
|
|
|
+ );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $command = sprintf(
|
|
|
|
|
+ 'sudo %s %s %s --force 2>&1',
|
|
|
|
|
+ escapeshellcmd(CREATE_CRT),
|
|
|
|
|
+ escapeshellarg($rsa_dir),
|
|
|
|
|
+ escapeshellarg($username)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ error_log("Creating user: $username on server: $server_name");
|
|
|
|
|
+ error_log("Command: $command");
|
|
|
|
|
+
|
|
|
|
|
+ exec($command, $output, $return_var);
|
|
|
|
|
+
|
|
|
|
|
+ if ($return_var === 0) {
|
|
|
|
|
+ // Логируем успешное создание
|
|
|
|
|
+ error_log("User $username created successfully on server $server_name");
|
|
|
|
|
+ send_json_response(true, 'User created successfully');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $error_message = implode("\n", $output);
|
|
|
|
|
+ error_log("Failed to create user $username: $error_message");
|
|
|
|
|
+ send_json_response(false, 'Failed to create user: ' . $error_message);
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Вспомогательная функция для отправки JSON ответов
|
|
|
|
|
+function send_json_response($success, $message, $data = []) {
|
|
|
|
|
+ header('Content-Type: application/json');
|
|
|
|
|
+ echo json_encode(array_merge([
|
|
|
|
|
+ 'success' => $success,
|
|
|
|
|
+ 'message' => $message
|
|
|
|
|
+ ], $data));
|
|
|
|
|
+ exit;
|
|
|
|
|
+}
|