import asyncio import logging import socket import sys import binascii import re from typing import Tuple # ==================== КОНФИГУРАЦИЯ ==================== CONVERT_MODE = 1 # 1 - HEX, 0 - прозрачный REPLACE_ON = 0 # 0 - без замены текста # Внутренний интерфейс (для браузера) LISTEN_IP = "10.10.0.1" LISTEN_PORT = 8443 # Внешний интерфейс (в сторону Proxy2 на VPS) # SOURCE_IP можно не привязывать - оставляем None, система выберет сама SOURCE_IP = None # Если нужна привязка - укажите "10.10.0.1" # Адрес VPS (Proxy2) TARGET_HOST = "91.221.99.183" # Публичный IP VPS TARGET_PORT = 8080 MAX_CLIENTS = 500 IDLE_TIMEOUT = 60 CONNECT_TIMEOUT = 10 TOTAL_TIMEOUT = 300 BUFFER_LIMIT = 65536 LOG_LEVEL = "INFO" # === НАСТРОЙКИ ДЛЯ ДОБАВЛЕНИЯ ТЕКСТОВОГО ЗАГОЛОВКА === # Администратор вручную задаёт текст заголовка HTTP_PREFIX = b"GET /wiki/ HTTP/1.1\r\nHost: ru.ruwiki.ru\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\r\nAccept: text/html\r\nAccept-Language: ru-RU,ru;q=0.9\r\nAccept-Encoding: gzip, deflate, br\r\nConnection: keep-alive\r\n\r\n" # 0 = автоматическое определение при старте HTTP_PREFIX_SIZE = 0 # ===================================================== logging.basicConfig( level=getattr(logging, LOG_LEVEL), format='%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logger = logging.getLogger(__name__) active_connections = 0 connections_lock = asyncio.Lock() # Разделитель между префиксом и HEX-данными SEPARATOR = b"\n---HEX---\n" def CONVERTER(data: bytes) -> bytes: """Преобразует двоичные данные в HEX-строку с разделителем \\n""" if not data: return b"\n" hex_data = binascii.hexlify(data) return hex_data + b"\n" def DECONVERTER(text_line: bytes) -> bytes: """Преобразует HEX-строку обратно в двоичные данные""" if not text_line: return b'' try: hex_str = text_line.strip() if not hex_str: return b'' if len(hex_str) % 2 != 0: logger.error(f"HEX-строка нечётной длины: {hex_str[:100]}") return b'' if not all(c in b'0123456789abcdefABCDEF' for c in hex_str): logger.error(f"HEX-строка содержит недопустимые символы: {hex_str[:100]}") return b'' return binascii.unhexlify(hex_str) except binascii.Error as e: logger.error(f"Ошибка DECONVERTER (неверный HEX): {e}") return b'' except Exception as e: logger.error(f"Ошибка DECONVERTER: {e}") return b'' def PASS_THROUGH(data: bytes) -> bytes: return data def ADDTEXT1(hex_data: bytes) -> bytes: """Добавляет текстовый префикс и разделитель перед HEX-данными""" return HTTP_PREFIX + SEPARATOR + hex_data def extract_host_from_request(data: bytes) -> str: try: text = data.decode('ascii', errors='ignore') match = re.search(r'CONNECT\s+([^\s:]+)', text, re.IGNORECASE) if match: return match.group(1) match = re.search(r'^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)\s+https?://([^/:]+)', text, re.IGNORECASE) if match: return match.group(2) match = re.search(r'Host:\s*([^\r\n]+)', text, re.IGNORECASE) if match: return match.group(1).strip() return "unknown" except: return "unknown" if CONVERT_MODE == 1: logger.info("Режим: HEX-конвертация") TO_SERVER = CONVERTER TO_CLIENT = DECONVERTER else: logger.info("Режим: прозрачный туннель") TO_SERVER = PASS_THROUGH TO_CLIENT = PASS_THROUGH if REPLACE_ON == 1: logger.info("Замена текста: ВКЛЮЧЕНА") else: logger.info("Замена текста: ВЫКЛЮЧЕНА") # Проверка и настройка HTTP_PREFIX_SIZE if HTTP_PREFIX_SIZE == 0: HTTP_PREFIX_SIZE = len(HTTP_PREFIX) logger.warning(f"HTTP_PREFIX_SIZE не задан, установлен автоматически: {HTTP_PREFIX_SIZE} байт") else: if len(HTTP_PREFIX) != HTTP_PREFIX_SIZE: logger.error(f"Ошибка: длина HTTP_PREFIX ({len(HTTP_PREFIX)}) не совпадает с HTTP_PREFIX_SIZE ({HTTP_PREFIX_SIZE})") sys.exit(1) logger.info(f"HTTP_PREFIX длина: {HTTP_PREFIX_SIZE} байт") logger.info(f"HTTP_PREFIX (первые 100 байт): {HTTP_PREFIX[:100]!r}") async def connect_to_target() -> Tuple[asyncio.StreamReader, asyncio.StreamWriter]: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) if SOURCE_IP: sock.bind((SOURCE_IP, 0)) logger.info(f"Привязка к исходящему IP: {SOURCE_IP}") loop = asyncio.get_event_loop() try: await asyncio.wait_for( loop.sock_connect(sock, (TARGET_HOST, TARGET_PORT)), timeout=CONNECT_TIMEOUT ) reader, writer = await asyncio.open_connection(sock=sock) logger.info(f"Подключение к Proxy2 {TARGET_HOST}:{TARGET_PORT} установлено") return reader, writer except Exception as e: sock.close() raise async def handle_client(client_reader, client_writer): global active_connections client_addr = client_writer.get_extra_info('peername') client_id = f"{client_addr[0]}:{client_addr[1]}" if client_addr else "unknown" client_socket = client_writer.get_extra_info('socket') if client_socket: client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) server_reader = None server_writer = None try: server_reader, server_writer = await connect_to_target() buffer = b"" async def client_to_server(): nonlocal server_writer try: while True: try: data = await asyncio.wait_for( client_reader.read(8192), timeout=IDLE_TIMEOUT ) except asyncio.TimeoutError: continue if not data: logger.info(f"[{client_id}] Нет данных от клиента") break target_site = extract_host_from_request(data) if target_site != "unknown": logger.info(f"[{client_id}] ЗАПРОС → {target_site}") hex_data = TO_SERVER(data) packet = ADDTEXT1(hex_data) server_writer.write(packet) await server_writer.drain() except Exception as e: logger.error(f"[{client_id}] Ошибка client_to_server: {e}") async def server_to_client(): nonlocal server_reader, client_writer, buffer try: while True: try: chunk = await asyncio.wait_for( server_reader.read(8192), timeout=IDLE_TIMEOUT ) except asyncio.TimeoutError: continue if not chunk: logger.info(f"[{client_id}] Нет данных от Proxy2") break buffer += chunk while b'\n' in buffer: line, buffer = buffer.split(b'\n', 1) line = line.strip() if line: binary_data = TO_CLIENT(line) if binary_data: client_writer.write(binary_data) await client_writer.drain() logger.debug(f"[{client_id}] Отправлено {len(binary_data)} байт клиенту") if len(buffer) > BUFFER_LIMIT: logger.warning(f"[{client_id}] Буфер превысил лимит, сбрасываем") buffer = b"" except Exception as e: logger.error(f"[{client_id}] Ошибка server_to_client: {e}") tasks = [ asyncio.create_task(client_to_server()), asyncio.create_task(server_to_client()) ] async with connections_lock: active_connections += 1 logger.info(f"[{client_id}] Новое соединение. Активных: {active_connections}") done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) for task in pending: task.cancel() try: await task except asyncio.CancelledError: pass except Exception as e: logger.error(f"[{client_id}] Ошибка: {e}") finally: if client_writer and not client_writer.is_closing(): client_writer.close() if server_writer and not server_writer.is_closing(): server_writer.close() async with connections_lock: if active_connections > 0: active_connections -= 1 logger.info(f"[{client_id}] Соединение закрыто. Активных: {active_connections}") async def connection_handler(client_reader, client_writer): try: await handle_client(client_reader, client_writer) except Exception as e: logger.error(f"Критическая ошибка: {e}") async def monitor_connections(): while True: await asyncio.sleep(30) async with connections_lock: logger.info(f"Статус: активных {active_connections}") async def main(): logger.info(f"Целевой адрес (Proxy2 на VPS): {TARGET_HOST}:{TARGET_PORT}") if SOURCE_IP: logger.info(f"Исходящий интерфейс: {SOURCE_IP}") else: logger.info("Исходящий интерфейс: автоматический") server = await asyncio.start_server( connection_handler, host=LISTEN_IP, port=LISTEN_PORT, backlog=100 ) logger.info(f"Сервер запущен на {LISTEN_IP}:{LISTEN_PORT} (для браузера)") logger.info(f"Режим: {'HEX-конвертация' if CONVERT_MODE == 1 else 'Прозрачный'}") logger.info("Нажмите Ctrl+C для остановки") asyncio.create_task(monitor_connections()) try: async with server: await server.serve_forever() except KeyboardInterrupt: logger.info("Остановка...") server.close() await server.wait_closed() logger.info("Сервер остановлен") if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: pass except Exception as e: logger.critical(f"Ошибка: {e}") sys.exit(1)