diff options
| author | vlapa <vlapa@ya.ru> | 2026-06-13 20:33:26 +0300 |
|---|---|---|
| committer | vlapa <vlapa@ya.ru> | 2026-06-13 20:33:26 +0300 |
| commit | e5abc1fe99340b979b18864d978179f71fe8f5c5 (patch) | |
| tree | 98fd0ad7c6d0744894d6e74e446daacd9e6dd1b5 /files/TG_bot_VPN-main/utils | |
First
Diffstat (limited to 'files/TG_bot_VPN-main/utils')
| -rw-r--r-- | files/TG_bot_VPN-main/utils/__init__.py | 1 | ||||
| -rw-r--r-- | files/TG_bot_VPN-main/utils/functions.py | 65 | ||||
| -rw-r--r-- | files/TG_bot_VPN-main/utils/generate_vpn_keys.py | 458 | ||||
| -rw-r--r-- | files/TG_bot_VPN-main/utils/misc/__init__.py | 1 | ||||
| -rw-r--r-- | files/TG_bot_VPN-main/utils/set_bot_commands.py | 8 | ||||
| -rw-r--r-- | files/TG_bot_VPN-main/utils/tasks.py | 112 | ||||
| -rw-r--r-- | files/TG_bot_VPN-main/utils/work_vpn_keys.py | 183 |
7 files changed, 828 insertions, 0 deletions
diff --git a/files/TG_bot_VPN-main/utils/__init__.py b/files/TG_bot_VPN-main/utils/__init__.py new file mode 100644 index 0000000..ab665e1 --- /dev/null +++ b/files/TG_bot_VPN-main/utils/__init__.py @@ -0,0 +1 @@ +from . import misc diff --git a/files/TG_bot_VPN-main/utils/functions.py b/files/TG_bot_VPN-main/utils/functions.py new file mode 100644 index 0000000..44e9433 --- /dev/null +++ b/files/TG_bot_VPN-main/utils/functions.py @@ -0,0 +1,65 @@ +from config_data.config import DEFAULT_COMMANDS, ADMIN_COMMANDS +from loader import bot, app_logger +import json + + + +def is_subscribed(chat_id, user_id): + """ + Функция для проверки, подписан ли пользователь на канал. + :param chat_id: id канала + :param user_id: id пользователя + :return: bool + """ + result = bot.get_chat_member(chat_id, user_id) + if result.status in ("creator", "administrator", "member", "restricted"): + return True + return False + + +def valid_ip(address): + try: + host_bytes = address.split('.') + valid = [int(b) for b in host_bytes] + valid = [b for b in valid if 0 <= b <= 255] + return len(host_bytes) == 4 and len(valid) == 4 + except (TypeError, ValueError, IndexError): + return False + +def convert_amnezia_xray_json_to_vless_str(amnezia_str: str) -> str | None: + """ + Функция для конвертации AMnezia Xray JSON в VLESS строчку + :param amnezia_str: JSON-строка с настройками Amnezia Xray + :return: VLESS-строка либо None объект + """ + try: + json_object = json.loads(amnezia_str) + except Exception: + app_logger.error("Не удалось преобразовать JSON в объект!") + return None + try: + outbounds = json_object["outbounds"][0]["settings"]["vnext"][0] + stream_settings = json_object["outbounds"][0]["streamSettings"] + + address_and_port = f"{outbounds['address']}:{outbounds['port']}" + flow = outbounds["users"][0]["flow"] + user_id = outbounds["users"][0]["id"] + type_of_net = stream_settings["network"] + security = stream_settings["security"] + fp = stream_settings["realitySettings"]["fingerprint"] + pbk = stream_settings["realitySettings"]["publicKey"] + sni = stream_settings["realitySettings"]["serverName"] + sid = stream_settings["realitySettings"]["shortId"] + + url = (f"vless://{user_id}@{address_and_port}?flow={flow}&type={type_of_net}&" + f"security={security}&fp={fp}&sni={sni}&pbk={pbk}&sid={sid}") + except Exception as ex: + app_logger.error(f"Не получилось конвертировать конфиг Amnezia!\n{ex}") + return None + return url + +def get_all_commands_bot(): + total_commands = [f"/{elem[0]}" for elem in DEFAULT_COMMANDS] + total_commands.extend([f"/{elem[0]}" for elem in ADMIN_COMMANDS]) + total_commands.extend(["🌍 Серверы", "❓ Справка", "📖 Инструкция"]) + return total_commands diff --git a/files/TG_bot_VPN-main/utils/generate_vpn_keys.py b/files/TG_bot_VPN-main/utils/generate_vpn_keys.py new file mode 100644 index 0000000..b9981c7 --- /dev/null +++ b/files/TG_bot_VPN-main/utils/generate_vpn_keys.py @@ -0,0 +1,458 @@ +import base64 +import os +import json +import uuid +import tempfile +import paramiko +import qrcode +import secrets +import random +from cryptography.hazmat.primitives.asymmetric import x25519 +from cryptography.hazmat.primitives import serialization +from loader import app_logger +from database.models import VPNKey, Server +import copy +from config_data.config import ( + DEFAULT_SERVER_USER, + DEFAULT_SERVER_PASSWORD, + XRAY_CONFIG_PATH, + QR_CODE_DIR, + XRAY_REALITY_FINGERPRINT, + DOMAINS_LIST, +) + +def generate_x25519_keys_base64() -> dict: + """ + Генерирует пару ключей X25519 и возвращает их в base64-формате, + что соответствует требованиям Xray Reality. + """ + private_key = x25519.X25519PrivateKey.generate() + public_key = private_key.public_key() + private_bytes = private_key.private_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption() + ) + public_bytes = public_key.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw + ) + private_b64 = base64.b64encode(private_bytes).decode('utf-8') + public_b64 = base64.b64encode(public_bytes).decode('utf-8') + return {"private": private_b64, "public": public_b64} + + +def remote_generate_xray_keys(server_obj: Server) -> dict: + cmd = "xray x25519" + output = execute_ssh_command( + ip=server_obj.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=cmd, + timeout=30 + ) + keys = {} + for line in output.splitlines(): + if line.lower().startswith("private"): + keys["private"] = line.split(":", 1)[1].strip() + elif line.lower().startswith("public"): + keys["public"] = line.split(":", 1)[1].strip() + if "private" in keys and "public" in keys: + return keys + else: + app_logger.error("Не удалось сгенерировать ключи через 'xray x25519' на сервере.") + return None + + +def ensure_reality_params(config_template: dict, server_obj: Server) -> tuple: + # Выбираем случайный домен из DOMAINS_LIST + domain = random.choice(DOMAINS_LIST) + # Генерируем ключи (приватный и публичный) на сервере + keys = remote_generate_xray_keys(server_obj) + if not keys: + raise Exception("Удаленная генерация ключей не удалась") + # Генерируем список shortIds (например, 6 значений) + short_ids = [secrets.token_hex(4) for _ in range(6)] + + # Создаем глубокую копию шаблона, чтобы не изменять оригинал + config = copy.deepcopy(config_template) + + # Обновляем параметры в секции realitySettings + reality = config["inbounds"][0]["streamSettings"]["realitySettings"] + reality["dest"] = f"{domain}:443" + reality["serverNames"] = [domain] + reality["privateKey"] = keys["private"] + reality["publicKey"] = keys["public"] # <-- Добавляем поле publicKey + reality["shortIds"] = short_ids + + # Обновляем домены в routing.rules (если указаны) + for rule in config.get("routing", {}).get("rules", []): + if "domain" in rule: + rule["domain"] = [domain] + + # Возвращаем обновленный конфиг и публичный ключ для формирования VLESS-ссылки + return config, keys["public"] + + +SECURE_XRAY_CONFIG = { + "log": { + "loglevel": "debug" + }, + "inbounds": [ + { + "listen": "0.0.0.0", + "port": 443, + "protocol": "vless", + "settings": { + "clients": [], + "decryption": "none" + }, + "streamSettings": { + "network": "tcp", + "security": "reality", + "realitySettings": { + "dest": "", + "serverNames": [], + "privateKey": "", + "shortIds": [], + "publicKey": "" + } + }, + "sniffing": { + "enabled": True, + "destOverride": ["http", "tls", "quic"] + } + } + ], + "outbounds": [ + { + "protocol": "freedom", + "tag": "direct" + }, + { + "protocol": "blackhole", + "tag": "block" + } + ] +} + + +def execute_ssh_command(ip: str, username: str, password: str, command: str, timeout: int = 60) -> str: + client = paramiko.SSHClient() + try: + client.load_system_host_keys() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect( + hostname=ip, + username=username, + password=password, + timeout=10, + allow_agent=False, + look_for_keys=False + ) + stdin, stdout, stderr = client.exec_command(command, timeout=timeout, get_pty=True) + output = stdout.read().decode('utf-8').strip() + err = stderr.read().decode('utf-8').strip() + if err: + app_logger.error(f"Ошибка выполнения команды на {ip}: {err}") + return output + except Exception as ex: + app_logger.error(f"Ошибка при подключении к {ip}: {ex}") + return "" + finally: + client.close() + + + + +def setup_server(server_obj: Server) -> bool: + """ + Настраивает сервер для работы VPN. + Если возникают какие-то ошибки при установке пакетов - + https://timeweb.cloud/docs/unix-guides/troubleshooting-unix/ustranenie-oshibki-could-not-get-lock-var-lib-dpkg-lock + + Алгоритм: + 1. Подключение по SSH с правами администратора. + 2. Проверка наличия DEFAULT_SERVER_USER. Если отсутствует, создаём пользователя и устанавливаем пароль. + Для установки пароля используется команда chpasswd, что исключает необходимость интерактивного ввода. + 3. С использованием SFTP загружается обновлённый и безопасный конфиг Xray. + 4. Перезапуск службы Xray. + + :param server_obj: объект Server с данными для подключения. + :return: True, если настройка прошла успешно, иначе False. + """ + try: + # Подключаемся к серверу под администратором + with paramiko.SSHClient() as ssh_admin: + ssh_admin.load_system_host_keys() + ssh_admin.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh_admin.connect( + hostname=server_obj.ip_address, + username=server_obj.username, + password=server_obj.password, + timeout=10, + allow_agent=False, + look_for_keys=False + ) + # Проверяем наличие пользователя DEFAULT_SERVER_USER + check_cmd = f"id {DEFAULT_SERVER_USER}" + stdin, stdout, stderr = ssh_admin.exec_command(check_cmd, timeout=30) + error_output = stderr.read().decode('utf-8').lower() + if "no such user" in error_output: + app_logger.info( + f"Пользователь {DEFAULT_SERVER_USER} не найден на сервере {server_obj.location}. Создаем его.") + # Создаем пользователя + ssh_admin.exec_command(f"useradd -m -s /bin/bash -G sudo {DEFAULT_SERVER_USER}", timeout=20) + # Устанавливаем пароль с помощью chpasswd (без интерактивного ввода) + passwd_cmd = f'echo "{DEFAULT_SERVER_USER}:{DEFAULT_SERVER_PASSWORD}" | chpasswd' + ssh_admin.exec_command(passwd_cmd, timeout=20) + app_logger.info(f"Пользователь {DEFAULT_SERVER_USER} создан и пароль установлен.") + else: + app_logger.info(f"Пользователь {DEFAULT_SERVER_USER} уже существует") + update_jq_cmd = ( + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S apt update && " + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S apt upgrade -y && " + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S apt install -y jq" + ) + execute_ssh_command( + ip=server_obj.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=update_jq_cmd, + timeout=300 + ) + app_logger.info(f"Системные компоненты обновлены, зависимости установлены.") + xray_check_cmd = "which xray" + xray_installed = execute_ssh_command( + ip=server_obj.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=xray_check_cmd, + timeout=30 + ) + if not xray_installed: + app_logger.info("Xray не найден, начинаю установку с использованием официального скрипта.") + install_xray_cmd = ( + "curl -O https://raw.githubusercontent.com/XTLS/Xray-install/main/install-release.sh && " + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S bash install-release.sh" + ) + output_install_xray = execute_ssh_command( + ip=server_obj.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=install_xray_cmd, + timeout=300 + ) + app_logger.info(f"Установка Xray выполнена успешно. Вывод: {output_install_xray}") + else: + app_logger.info("Xray уже установлен. Сервер настроен") + return True + + # Разрешаем 443 порт + allow_443_port_command = ( + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S ufw allow 443/tcp && " + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S netstat -tulpn | grep ':443'" + ) + port_output = execute_ssh_command( + ip=server_obj.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=allow_443_port_command, + timeout=300 + ) + app_logger.info(f"Порт 443 разрешен. Проверка: {port_output}") + # Подключаемся для загрузки конфигурации Xray + ssh = paramiko.SSHClient() + ssh.load_system_host_keys() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect( + hostname=server_obj.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + timeout=10, + allow_agent=False, + look_for_keys=False + ) + sftp = ssh.open_sftp() + + # заполняем конфигурацию данными + final_config, public_key = ensure_reality_params(SECURE_XRAY_CONFIG, server_obj) + + # Сохраняем public key в модель сервера + server_obj.public_key = public_key + server_obj.save() + + # Сохраняем локально безопасный конфиг Xray + local_config_path = os.path.join(tempfile.gettempdir(), f"secure_xray_config_{server_obj.id}.json") + with open(local_config_path, "w") as f: + json.dump(final_config, f, indent=2) + # Загружаем конфиг на сервер + + temp_remote_path = f"/tmp/secure_xray_config_{server_obj.id}.json" + sftp.put(local_config_path, temp_remote_path) + app_logger.info(f"Конфигурационный файл загружен во временную директорию: {temp_remote_path}") + + # Перемещаем файл с помощью sudo, чтобы обойти ограничение прав доступа. + move_cmd = f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S mv {temp_remote_path} {XRAY_CONFIG_PATH}' + move_output = execute_ssh_command( + ip=server_obj.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=move_cmd, + timeout=30 + ) + app_logger.info(f"Файл конфигурации перемещён в {XRAY_CONFIG_PATH}. Вывод: {move_output}") + sftp.close() + ssh.close() + + # Проверяем конфиг файл на валидность + test_config = f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S xray run -test -confdir /usr/local/etc/xray/' + test_output = execute_ssh_command( + ip=server_obj.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=test_config, + timeout=30 + ) + app_logger.info(f"Конфиг проверен. Вывод: {test_output}") + + # Синхронизируем время на сервере + time_syn_cmd = f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S timedatectl set-ntp true' + execute_ssh_command( + ip=server_obj.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=time_syn_cmd, + timeout=30 + ) + app_logger.info(f"Время на сервере синхронизировано.") + + # Перезапускаем службу Xray для применения конфигурации + restart_cmd = f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S systemctl restart xray' + restart_output = execute_ssh_command( + ip=server_obj.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=restart_cmd, + timeout=30 + ) + app_logger.info(f"Служба Xray перезапущена. Вывод: {restart_output}") + return True + except Exception as ex: + app_logger.error(f"Ошибка при настройке сервера {server_obj.location}: {ex}") + return False + + +def generate_key(server_obj: Server) -> VPNKey | None: + """ + Генерирует новый VPN ключ для сервера. + + Алгоритм: + 1. Генерация уникального UUID для клиента. + 2. Обновление конфигурации Xray на сервере с помощью утилиты jq (добавление нового клиента в список). + 3. Перезапуск службы Xray. + 4. Формирование VLESS-ссылки с параметрами Xray Reality. + 5. Генерация QR-кода для VLESS-ссылки. + 6. Создание записи VPNKey в базе данных. + + :param server_obj: объект Server. + :return: объект VPNKey с рабочей VLESS‑ссылкой и путем к QR‑коду, либо None при ошибке. + """ + try: + # Шаг 1. Генерация UUID для нового клиента + client_uuid = str(uuid.uuid4()) + app_logger.info(f"Сгенерирован новый UUID для клиента: {client_uuid}") + + # Шаг 2. Обновление конфигурации Xray через jq + update_cmd = ( + f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S sh -c ' + f'"jq \'.inbounds[0].settings.clients += [{{\\"id\\": \\"{client_uuid}\\", ' + f'\\"flow\\": \\"xtls-rprx-vision\\"}}]\' {XRAY_CONFIG_PATH} > {XRAY_CONFIG_PATH}.tmp && ' + f'mv {XRAY_CONFIG_PATH}.tmp {XRAY_CONFIG_PATH}"' + ) + update_output = execute_ssh_command( + ip=server_obj.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=update_cmd, + timeout=30 + ) + app_logger.info(f"Конфигурация Xray обновлена. Вывод: {update_output}") + + # Шаг 3. Перезапуск службы Xray + restart_cmd = f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S systemctl restart xray' + restart_output = execute_ssh_command( + ip=server_obj.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=restart_cmd, + timeout=30 + ) + app_logger.info(f"Служба Xray перезапущена. Вывод: {restart_output}") + + # Шаг 4. Формирование VLESS-ссылки + # Получаем данные из конфиг файла + config_content = execute_ssh_command( + ip=server_obj.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=f"cat {XRAY_CONFIG_PATH}", + timeout=30 + ) + try: + config_json = json.loads(config_content) + # Извлекаем данные из секции realitySettings во втором inbound (индекс 1) + reality_settings = config_json["inbounds"][0]["streamSettings"]["realitySettings"] + # Предполагаем, что в конфиге теперь есть оба параметра: publicKey и serverNames + server_name = reality_settings.get("serverNames", [None])[0] + public_key = server_obj.public_key + short_id = random.choice(reality_settings["shortIds"]) + if not server_name or not public_key or not short_id: + raise ValueError("Недостаточно параметров Xray Reality в конфиге.") + except Exception as e: + app_logger.error(f"Ошибка при парсинге конфигурационного файла: {e}") + return None + + vless_link = ( + f"vless://{client_uuid}@{server_obj.ip_address}:443?" + f"security=reality&" + f"encryption=none&" + f"flow=xtls-rprx-vision&" + f"type=tcp&" + f"fp={XRAY_REALITY_FINGERPRINT}&" + f"sni={server_name}&" + f"pbk={public_key}&" + f"sid={short_id}#GuardVPN" + ) + app_logger.info(f"Сформирована VLESS ссылка: {vless_link}") + + + # Шаг 5. Генерация QR-кода + key_number = len(server_obj.keys) + 1 if hasattr(server_obj, "keys") else 1 + qr_code_filename = f"vpn_key_{server_obj.id}_{key_number}.png" + qr_code_path = os.path.join(QR_CODE_DIR, qr_code_filename) + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_H, + box_size=10, + border=4 + ) + qr.add_data(vless_link) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + img.save(qr_code_path) + app_logger.info(f"QR-код сгенерирован и сохранён по пути: {qr_code_path}") + + # Шаг 6. Создание записи VPNKey в БД с использованием peewee. + vpn_key = VPNKey.create( + server=server_obj, + name=f"VPN Key {server_obj.location} #{key_number}", + key=vless_link, + qr_code=qr_code_path, + is_valid=True + ) + app_logger.info(f"VPN ключ успешно создан: {vpn_key.key}") + return vpn_key + except Exception as ex: + app_logger.error(f"Ошибка при генерации VPN ключа для сервера {server_obj.location}: {ex}") + return None diff --git a/files/TG_bot_VPN-main/utils/misc/__init__.py b/files/TG_bot_VPN-main/utils/misc/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/files/TG_bot_VPN-main/utils/misc/__init__.py @@ -0,0 +1 @@ + diff --git a/files/TG_bot_VPN-main/utils/set_bot_commands.py b/files/TG_bot_VPN-main/utils/set_bot_commands.py new file mode 100644 index 0000000..6494889 --- /dev/null +++ b/files/TG_bot_VPN-main/utils/set_bot_commands.py @@ -0,0 +1,8 @@ +from telebot.types import BotCommand +from config_data.config import DEFAULT_COMMANDS + + +def set_default_commands(bot): + bot.set_my_commands( + [BotCommand(*i) for i in DEFAULT_COMMANDS] + ) diff --git a/files/TG_bot_VPN-main/utils/tasks.py b/files/TG_bot_VPN-main/utils/tasks.py new file mode 100644 index 0000000..be6e7c8 --- /dev/null +++ b/files/TG_bot_VPN-main/utils/tasks.py @@ -0,0 +1,112 @@ +import datetime + +from apscheduler.schedulers.background import BackgroundScheduler + +from keyboards.inline.subscribed import get_renew_markup +from loader import bot, app_logger +from database.models import VPNKey, User +from config_data.config import CHANNEL_ID, ALLOWED_USERS +from utils.functions import is_subscribed +from utils.work_vpn_keys import revoke_key + +# Глобальный словарь для хранения job_id запланированных задач по отзыву ключей +pending_revocation_jobs = {} + + +def check_and_revoke_keys(): + """ + Проверяет, подписаны ли пользователи, которым выданы VPN ключи. + Если обнаруживается, что хотя бы один пользователь, привязанный к ключу, отписался, + отправляет ему уведомление и отзывает ключ. + """ + # Получаем активные ключи + app_logger.info("Проверка пользователей...") + + active_keys = VPNKey.select().where(VPNKey.is_valid == False) + for vpn_key in active_keys: + revoke_this = False + for user in vpn_key.users: + # Если пользователь не подписан, отправляем уведомление + if not is_subscribed(CHANNEL_ID, user.user_id): + user.is_subscribed = False + user.vpn_key = None + user.save() + try: + bot.send_message(user.user_id, + "Ваш VPN ключ отозван, так как вы отписались от канала.") + app_logger.info(f"Пользователь {user.full_name} отписался от канала, " + f"поэтому его ключ {vpn_key.name} был отозван!") + # for admin_id in ALLOWED_USERS: + # bot.send_message(admin_id, + # f"Пользователь {user.full_name} отписался от канала, " + # f"поэтому его ключ {vpn_key.name} был отозван!") + except Exception as e: + app_logger.error(f"Не удалось отправить уведомление пользователю {user.user_id}: {e}") + revoke_this = True + if revoke_this: + if revoke_key(vpn_key): + app_logger.info(f"VPN ключ {vpn_key.id} отозван из-за отписки пользователя(ей).") + else: + app_logger.error(f"Ошибка при отзыве ключа {vpn_key.id}.") + + +def schedule_key_revocation(vpn_key: VPNKey, user_obj: User, scheduler: BackgroundScheduler): + """ + Планирует задачу отзыва ключа через 1 час. + Сохраняет идентификатор задачи в глобальном словаре по vpn_key.id. + """ + def revoke_job(): + # Если задача не была отменена (то есть пользователь не продлил ключ) + if vpn_key.id in pending_revocation_jobs: + try: + bot.send_message( + user_obj.user_id, + f"⌛ Время продления VPN ключа «{vpn_key.name}» истекло! Ключ был аннулирован.\n" + f"Вы можете создать новый ключ, нажав на кнопку Серверы 👇" + ) + except Exception as e: + app_logger.error(f"Ошибка отправки уведомления пользователю {user_obj.full_name}: {e}") + revoke_key(vpn_key) + user_obj.vpn_key = None + user_obj.save() + app_logger.info(f"VPN ключ {vpn_key.name} аннулирован по истечении времени продления!") + # Убираем задачу из pending_revocation_jobs + pending_revocation_jobs.pop(vpn_key.id, None) + + # Планируем выполнение через 1 час + job = scheduler.add_job(revoke_job, 'date', run_date=datetime.datetime.now() + datetime.timedelta(hours=1)) + pending_revocation_jobs[vpn_key.id] = job.id + app_logger.info(f"Запланирована задача отзыва для ключа {vpn_key.name} через 1 час.") + +def cancel_key_revocation(vpn_key: VPNKey, scheduler: BackgroundScheduler): + """ + Отменяет запланированную задачу отзыва ключа, если она существует. + """ + job_id = pending_revocation_jobs.pop(vpn_key.id, None) + if job_id: + scheduler.remove_job(job_id) + app_logger.info(f"Запланированная задача отзыва для ключа {vpn_key.name} отменена.") + + +def send_renewal_notifications(scheduler: BackgroundScheduler): + """ + Каждые 8 часов рассылает пользователям уведомления о продлении VPN ключей. + Для каждого активного (зарезервированного) ключа отправляется сообщение с inline-кнопкой. + При отправке планируется задача, которая через 1 час отзовет ключ, если пользователь не нажмёт кнопку. + """ + # Получаем список пользователей, у которых назначен VPN ключ (предполагается, что у пользователя есть vpn_key) + app_logger.info("Отправка уведомлений о продлении VPN ключей...") + + for user in User.select().where(User.vpn_key.is_null(False)): + vpn_key = user.vpn_key + try: + bot.send_message( + user.user_id, + f"⏰ Напоминаем, что срок действия вашего VPN ключа «{vpn_key.name}» скоро истечёт.\n" + "Нажмите кнопку ниже, чтобы продлить его действие.", + reply_markup=get_renew_markup(vpn_key.id) + ) + # Планируем задачу отзыва ключа через 1 час + schedule_key_revocation(vpn_key, user, scheduler) + except Exception as e: + app_logger.error(f"Ошибка отправки уведомления пользователю {user.user_id}: {e}") diff --git a/files/TG_bot_VPN-main/utils/work_vpn_keys.py b/files/TG_bot_VPN-main/utils/work_vpn_keys.py new file mode 100644 index 0000000..f030ec3 --- /dev/null +++ b/files/TG_bot_VPN-main/utils/work_vpn_keys.py @@ -0,0 +1,183 @@ +import os + +from config_data.config import DEFAULT_SERVER_PASSWORD, XRAY_CONFIG_PATH, DEFAULT_SERVER_USER +from database.models import VPNKey, Server +from loader import app_logger +from utils.generate_vpn_keys import execute_ssh_command + + +def suspend_key(vpn_key: VPNKey) -> bool: + """ + Приостанавливает действие ключа, удаляя его из конфигурации Xray на сервере. + Обновляет статус is_valid в базе данных. + """ + client_uuid = vpn_key.extract_uuid() + if not client_uuid: + app_logger.error(f"Не удалось извлечь UUID из ключа {vpn_key.id}") + return False + + server = vpn_key.server + try: + # Удаление клиента из конфигурации с помощью jq + remove_cmd = ( + f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S sh -c ' + f'"jq \'del(.inbounds[0].settings.clients[] | select(.id == \\"{client_uuid}\\"))\' ' + f'{XRAY_CONFIG_PATH} > {XRAY_CONFIG_PATH}.tmp && ' + f'mv {XRAY_CONFIG_PATH}.tmp {XRAY_CONFIG_PATH}"' + ) + output = execute_ssh_command( + ip=server.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=remove_cmd, + timeout=30 + ) + app_logger.info(f"Клиент {client_uuid} удалён из конфига. Вывод: {output}") + + # Перезапуск Xray + restart_output = execute_ssh_command( + ip=server.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S systemctl restart xray', + timeout=30 + ) + app_logger.info(f"Xray перезапущен: {restart_output}") + + # Обновление статуса ключа + vpn_key.is_valid = False + vpn_key.save() + return True + except Exception as ex: + app_logger.error(f"Ошибка при приостановке ключа {vpn_key.id}: {ex}") + return False + +def resume_key(vpn_key: VPNKey) -> bool: + """ + Возобновляет действие ключа, добавляя его обратно в конфигурацию Xray. + Обновляет статус is_valid в базе данных. + """ + client_uuid = vpn_key.extract_uuid() + if not client_uuid: + app_logger.error(f"Не удалось извлечь UUID из ключа {vpn_key.id}") + return False + + server = vpn_key.server + try: + # Добавление клиента в конфигурацию + add_cmd = ( + f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S sh -c ' + f'"jq \'.inbounds[0].settings.clients += [{{"id": "{client_uuid}", "flow": "xtls-rprx-vision"}}]\' ' + f'{XRAY_CONFIG_PATH} > {XRAY_CONFIG_PATH}.tmp && ' + f'mv {XRAY_CONFIG_PATH}.tmp {XRAY_CONFIG_PATH}"' + ) + output = execute_ssh_command( + ip=server.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=add_cmd, + timeout=30 + ) + app_logger.info(f"Клиент {client_uuid} добавлен в конфиг. Вывод: {output}") + + # Перезапуск Xray + restart_output = execute_ssh_command( + ip=server.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=f'echo {DEFAULT_SERVER_PASSWORD} | sudo -S systemctl restart xray', + timeout=30 + ) + app_logger.info(f"Xray перезапущен: {restart_output}") + + # Обновление статуса ключа + vpn_key.is_valid = True + vpn_key.save() + return True + except Exception as ex: + app_logger.error(f"Ошибка при возобновлении ключа {vpn_key.id}: {ex}") + return False + +def revoke_key(vpn_key: VPNKey) -> bool: + """ + Полностью отзывает ключ: удаляет из конфигурации Xray и из базы данных. + """ + try: + # Удаление из конфигурации + if not suspend_key(vpn_key): + return False + + # Удаление QR-кода, если он существует + if os.path.exists(vpn_key.qr_code): + os.remove(vpn_key.qr_code) + + # Удаление из базы данных + vpn_key.delete_instance() + + app_logger.info(f"Ключ {vpn_key.id} полностью отозван.") + return True + except Exception as ex: + app_logger.error(f"Ошибка при отзыве ключа {vpn_key.id}: {ex}") + return False + +def get_active_keys(server: Server) -> list[VPNKey]: + """ + Возвращает список активных ключей для указанного сервера. + """ + return list(server.keys.where(VPNKey.is_valid == True)) + +def get_inactive_keys(server: Server) -> list[VPNKey]: + """ + Возвращает список неактивных ключей для указанного сервера. + """ + return list(server.keys.where(VPNKey.is_valid == False)) + + +def cleanup_server(server: Server) -> bool: + """Полная очистка сервера от Xray и конфигов, с подробным логированием.""" + app_logger.info(f"Администратор запросил полное удаление севрера {server.location}. Начинаю очистку...") + try: + cmds = [ + # Остановка и отключение службы Xray + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S systemctl stop xray", + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S systemctl disable xray", + # Удаление Xray с помощью официального скрипта + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S bash -c 'curl -sL https://raw.githubusercontent.com/XTLS/Xray-install/main/install-release.sh | bash -s -- remove'", + # Очистка пакетов и конфигов + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S apt-get purge xray -y", + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S rm -rf /usr/local/etc/xray/", + # Восстановление фаервола: удаление правила для 443/tcp + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S ufw delete allow 443/tcp", + # Удаление созданного пользователя + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S pkill -9 -u {DEFAULT_SERVER_USER} || true", + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S userdel -rf {DEFAULT_SERVER_USER} || true", + # Очистка cron задач + f"echo {DEFAULT_SERVER_PASSWORD} | sudo -S crontab -u {DEFAULT_SERVER_USER} -r || true" + ] + full_output = "" + for cmd in cmds: + output = execute_ssh_command( + ip=server.ip_address, + username=DEFAULT_SERVER_USER, + password=DEFAULT_SERVER_PASSWORD, + command=cmd, + timeout=60 + ) + full_output += f"\nКоманда: {cmd}\nВывод: {output}" + + # Удаляем запись о сервере из БД + server.delete_instance() + + # Удаление связанных VPN ключей + for vpn_key in VPNKey.select().where(VPNKey.server == server): + for user in vpn_key.users: + user.vpn_key = None + user.save() + app_logger.info(f"VPN ключ {vpn_key.name} удален!") + vpn_key.delete_instance() + + app_logger.info(f"Cleanup завершён. Общий вывод: {full_output}") + return True + except Exception as ex: + app_logger.error(f"Server cleanup failed: {ex}") + return False
\ No newline at end of file |
