|
@@ -1,309 +1,408 @@
|
|
|
-#!/usr/bin/env python2.7
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
-import socket, threading, thread, select, signal, sys, time, getopt
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
# ==============================================================================
|
|
|
-# SCRIPT DE PROXY MULTIFILAMENTADO CON SOPORTE PARA IPV4 E IPV6
|
|
|
|
|
-# - Solucionado el error "Address already in use" con la opción IPV6_V6ONLY.
|
|
|
|
|
-# - Modificado por Gemini.
|
|
|
|
|
|
|
+# SCRIPT DE PROXY MULTIFILAMENTADO CON LÍMITE DE CONEXIONES Y RATE-LIMITING
|
|
|
|
|
+#
|
|
|
|
|
+# - Este script añade un límite al número máximo de conexiones activas que
|
|
|
|
|
+# el servidor puede manejar en un momento dado.
|
|
|
|
|
+# - Es la solución más efectiva para prevenir la saturación de la CPU en
|
|
|
|
|
+# entornos donde la arquitectura asíncrona no es viable.
|
|
|
|
|
+# - La limitación de velocidad se mantiene para proteger contra ataques de
|
|
|
|
|
+# denegación de servicio.
|
|
|
|
|
+#
|
|
|
|
|
+# Creado por Gemini
|
|
|
# ==============================================================================
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
-# Listen
|
|
|
|
|
-# Se usan direcciones específicas para evitar errores de getaddrinfo.
|
|
|
|
|
|
|
+import socket
|
|
|
|
|
+import threading
|
|
|
|
|
+import select
|
|
|
|
|
+import sys
|
|
|
|
|
+import time
|
|
|
|
|
+import os
|
|
|
|
|
+import logging
|
|
|
|
|
+import logging.handlers
|
|
|
|
|
+
|
|
|
|
|
+# ==============================================================================
|
|
|
|
|
+# CONFIGURACIÓN GLOBAL Y SETUP DE LOGGING
|
|
|
|
|
+# ==============================================================================
|
|
|
|
|
+# Direcciones de escucha para ambos protocolos.
|
|
|
IPV4_ADDR = '0.0.0.0'
|
|
IPV4_ADDR = '0.0.0.0'
|
|
|
IPV6_ADDR = '::'
|
|
IPV6_ADDR = '::'
|
|
|
|
|
|
|
|
|
|
+# Puerto de escucha. Se puede pasar como argumento.
|
|
|
if sys.argv[1:]:
|
|
if sys.argv[1:]:
|
|
|
- LISTENING_PORT = sys.argv[1]
|
|
|
|
|
|
|
+ LISTENING_PORT = int(sys.argv[1])
|
|
|
else:
|
|
else:
|
|
|
- LISTENING_PORT = 80
|
|
|
|
|
-#Pass
|
|
|
|
|
|
|
+ LISTENING_PORT = 80
|
|
|
|
|
+
|
|
|
|
|
+# Contraseña opcional para el proxy.
|
|
|
PASS = ''
|
|
PASS = ''
|
|
|
|
|
|
|
|
-# CONST
|
|
|
|
|
|
|
+# Controla la prioridad de conexión
|
|
|
|
|
+PRIORITIZE_IPV4 = True
|
|
|
|
|
+
|
|
|
|
|
+# 💡 CONFIGURACIÓN DE SEGURIDAD
|
|
|
|
|
+# Tiempo mínimo de espera (en segundos) entre dos conexiones de la misma IP.
|
|
|
|
|
+CONNECTION_COOLDOWN_TIME = 100
|
|
|
|
|
+
|
|
|
|
|
+# 💡 NUEVA CONFIGURACIÓN: Límite de conexiones activas
|
|
|
|
|
+MAX_CONNECTIONS = 200 # Máximo de conexiones simultáneas
|
|
|
|
|
+
|
|
|
|
|
+# Constantes
|
|
|
BUFLEN = 4096 * 4
|
|
BUFLEN = 4096 * 4
|
|
|
TIMEOUT = 60
|
|
TIMEOUT = 60
|
|
|
DEFAULT_HOST = '127.0.0.1:22'
|
|
DEFAULT_HOST = '127.0.0.1:22'
|
|
|
-RESPONSE = 'HTTP/1.1 101 Switching Protocols <strong>By: VPS-MX</strong>\r\n\r\n'
|
|
|
|
|
|
|
+RESPONSE = b'HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n'
|
|
|
|
|
+
|
|
|
|
|
+# Configuración del log
|
|
|
|
|
+LOG_FILE = '/root/proxy.log'
|
|
|
|
|
+MAX_LOG_SIZE = 5 * 1024 * 1024 # 5 MB
|
|
|
|
|
+BACKUP_COUNT = 5 # Cantidad de archivos de log a rotar
|
|
|
|
|
+
|
|
|
|
|
+def setup_logging():
|
|
|
|
|
+ """Configura el logger profesional con rotación de archivos."""
|
|
|
|
|
+ log_format = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
|
|
|
+
|
|
|
|
|
+ file_handler = logging.handlers.RotatingFileHandler(
|
|
|
|
|
+ LOG_FILE,
|
|
|
|
|
+ maxBytes=MAX_LOG_SIZE,
|
|
|
|
|
+ backupCount=BACKUP_COUNT
|
|
|
|
|
+ )
|
|
|
|
|
+ file_handler.setFormatter(log_format)
|
|
|
|
|
+
|
|
|
|
|
+ console_handler = logging.StreamHandler()
|
|
|
|
|
+ console_handler.setFormatter(log_format)
|
|
|
|
|
+
|
|
|
|
|
+ logger = logging.getLogger()
|
|
|
|
|
+ logger.setLevel(logging.INFO)
|
|
|
|
|
+ logger.addHandler(file_handler)
|
|
|
|
|
+ logger.addHandler(console_handler)
|
|
|
|
|
+
|
|
|
|
|
+ return logger
|
|
|
|
|
+
|
|
|
|
|
+# Inicializar el logger
|
|
|
|
|
+logger = setup_logging()
|
|
|
|
|
+
|
|
|
|
|
+# 💡 Variables compartidas para la limitación de velocidad y el semáforo
|
|
|
|
|
+last_connection_times = {}
|
|
|
|
|
+last_connection_lock = threading.Lock()
|
|
|
|
|
+connection_limit_semaphore = threading.Semaphore(MAX_CONNECTIONS)
|
|
|
|
|
|
|
|
|
|
+# ==============================================================================
|
|
|
|
|
+# CLASE DEL SERVIDOR
|
|
|
|
|
+# Gestiona la creación de sockets y la aceptación de conexiones
|
|
|
|
|
+# ==============================================================================
|
|
|
class Server(threading.Thread):
|
|
class Server(threading.Thread):
|
|
|
- def __init__(self, host, port):
|
|
|
|
|
- threading.Thread.__init__(self)
|
|
|
|
|
|
|
+ def __init__(self, port):
|
|
|
|
|
+ super().__init__()
|
|
|
self.running = False
|
|
self.running = False
|
|
|
- self.host = host
|
|
|
|
|
self.port = port
|
|
self.port = port
|
|
|
self.threads = []
|
|
self.threads = []
|
|
|
- self.threadsLock = threading.Lock()
|
|
|
|
|
- self.logLock = threading.Lock()
|
|
|
|
|
- self.soc = None
|
|
|
|
|
- self.soc_v6 = None
|
|
|
|
|
-
|
|
|
|
|
|
|
+ self.threads_lock = threading.Lock()
|
|
|
|
|
+ self.ipv4_socket = None
|
|
|
|
|
+ self.ipv6_socket = None
|
|
|
|
|
+
|
|
|
def run(self):
|
|
def run(self):
|
|
|
- self.printLog("\n:-------PythonProxy-------:\n")
|
|
|
|
|
- self.printLog("Listening addr: " + IPV4_ADDR + " and " + IPV6_ADDR)
|
|
|
|
|
- self.printLog("Listening port: " + str(self.port) + "\n")
|
|
|
|
|
- self.printLog(":-------------------------:\n")
|
|
|
|
|
|
|
+ logger.info("\n:-------PythonProxy-------:\n")
|
|
|
|
|
+ logger.info(f"Listening addr: {IPV4_ADDR} and {IPV6_ADDR}")
|
|
|
|
|
+ logger.info(f"Listening port: {self.port}\n")
|
|
|
|
|
+ logger.info(f"Límite de conexiones: {MAX_CONNECTIONS}\n")
|
|
|
|
|
+ logger.info(":-------------------------:\n")
|
|
|
|
|
|
|
|
# Intentar enlazar a IPv4
|
|
# Intentar enlazar a IPv4
|
|
|
try:
|
|
try:
|
|
|
- self.soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
|
- self.soc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
|
|
- self.soc.settimeout(2)
|
|
|
|
|
- self.soc.bind((IPV4_ADDR, int(self.port)))
|
|
|
|
|
- self.soc.listen(0)
|
|
|
|
|
- self.printLog("Esperando conexiones IPv4 en %s:%s" % (IPV4_ADDR, self.port))
|
|
|
|
|
|
|
+ self.ipv4_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
|
+ self.ipv4_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
|
|
+ self.ipv4_socket.bind((IPV4_ADDR, self.port))
|
|
|
|
|
+ self.ipv4_socket.listen(0)
|
|
|
|
|
+ logger.info(f"Esperando conexiones IPv4 en {IPV4_ADDR}:{self.port}")
|
|
|
except socket.error as e:
|
|
except socket.error as e:
|
|
|
- self.printLog("No se pudo enlazar a IPv4 (%s)" % e)
|
|
|
|
|
- if self.soc:
|
|
|
|
|
- self.soc.close()
|
|
|
|
|
- self.soc = None
|
|
|
|
|
|
|
+ logger.error(f"No se pudo enlazar a IPv4 ({e})")
|
|
|
|
|
+ if self.ipv4_socket:
|
|
|
|
|
+ self.ipv4_socket.close()
|
|
|
|
|
+ self.ipv4_socket = None
|
|
|
|
|
|
|
|
# Intentar enlazar a IPv6
|
|
# Intentar enlazar a IPv6
|
|
|
try:
|
|
try:
|
|
|
- self.soc_v6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
|
|
|
|
- self.soc_v6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
|
|
- # 💡 SOLUCIÓN: Establecer la opción IPV6_V6ONLY para evitar conflictos con IPv4.
|
|
|
|
|
- # Esto fuerza al socket IPv6 a solo aceptar conexiones IPv6.
|
|
|
|
|
- self.soc_v6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
|
|
|
|
- self.soc_v6.settimeout(2)
|
|
|
|
|
- self.soc_v6.bind((IPV6_ADDR, int(self.port), 0, 0))
|
|
|
|
|
- self.soc_v6.listen(0)
|
|
|
|
|
- self.printLog("Esperando conexiones IPv6 en %s:%s" % (IPV6_ADDR, self.port))
|
|
|
|
|
|
|
+ self.ipv6_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
|
|
|
|
+ self.ipv6_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
|
|
+ self.ipv6_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
|
|
|
|
+ self.ipv6_socket.bind((IPV6_ADDR, self.port, 0, 0))
|
|
|
|
|
+ self.ipv6_socket.listen(0)
|
|
|
|
|
+ logger.info(f"Esperando conexiones IPv6 en {IPV6_ADDR}:{self.port}")
|
|
|
except socket.error as e:
|
|
except socket.error as e:
|
|
|
- self.printLog("No se pudo enlazar a IPv6 (%s)" % e)
|
|
|
|
|
- if self.soc_v6:
|
|
|
|
|
- self.soc_v6.close()
|
|
|
|
|
- self.soc_v6 = None
|
|
|
|
|
|
|
+ logger.error(f"No se pudo enlazar a IPv6 ({e})")
|
|
|
|
|
+ if self.ipv6_socket:
|
|
|
|
|
+ self.ipv6_socket.close()
|
|
|
|
|
+ self.ipv6_socket = None
|
|
|
|
|
|
|
|
- if not self.soc and not self.soc_v6:
|
|
|
|
|
- self.printLog("No se pudo iniciar el servidor. Saliendo.")
|
|
|
|
|
|
|
+ if not self.ipv4_socket and not self.ipv6_socket:
|
|
|
|
|
+ logger.critical("No se pudo iniciar el servidor. Saliendo.")
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
self.running = True
|
|
self.running = True
|
|
|
-
|
|
|
|
|
active_sockets = []
|
|
active_sockets = []
|
|
|
- if self.soc:
|
|
|
|
|
- active_sockets.append(self.soc)
|
|
|
|
|
- if self.soc_v6:
|
|
|
|
|
- active_sockets.append(self.soc_v6)
|
|
|
|
|
|
|
+ if self.ipv4_socket:
|
|
|
|
|
+ active_sockets.append(self.ipv4_socket)
|
|
|
|
|
+ if self.ipv6_socket:
|
|
|
|
|
+ active_sockets.append(self.ipv6_socket)
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
while self.running:
|
|
while self.running:
|
|
|
- try:
|
|
|
|
|
- readable, _, _ = select.select(active_sockets, [], [], 2)
|
|
|
|
|
- for sock in readable:
|
|
|
|
|
- c, addr = sock.accept()
|
|
|
|
|
- c.setblocking(1)
|
|
|
|
|
- conn = ConnectionHandler(c, self, addr)
|
|
|
|
|
- conn.start()
|
|
|
|
|
- self.addConn(conn)
|
|
|
|
|
- except socket.timeout:
|
|
|
|
|
- continue
|
|
|
|
|
- except socket.error as e:
|
|
|
|
|
- if self.running:
|
|
|
|
|
- self.printLog("Error al aceptar conexión: %s" % e)
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
|
|
+ readable, _, _ = select.select(active_sockets, [], [], 2)
|
|
|
|
|
+ for sock in readable:
|
|
|
|
|
+ c, addr = sock.accept()
|
|
|
|
|
+ client_ip = addr[0]
|
|
|
|
|
+ current_time = time.time()
|
|
|
|
|
+
|
|
|
|
|
+ # 💡 Lógica del cooldown (rate-limiting)
|
|
|
|
|
+ with last_connection_lock:
|
|
|
|
|
+ last_time = last_connection_times.get(client_ip, 0)
|
|
|
|
|
+ if current_time - last_time < CONNECTION_COOLDOWN_TIME:
|
|
|
|
|
+ logger.warning(f"[{client_ip}] Conexión rechazada por rate-limiting.")
|
|
|
|
|
+ c.close()
|
|
|
|
|
+ continue
|
|
|
|
|
+ last_connection_times[client_ip] = current_time
|
|
|
|
|
+
|
|
|
|
|
+ # 💡 Intenta adquirir un "slot" del semáforo
|
|
|
|
|
+ if not connection_limit_semaphore.acquire(timeout=0):
|
|
|
|
|
+ logger.warning(f"[{client_ip}] Conexión rechazada. Límite de conexiones alcanzado.")
|
|
|
|
|
+ c.close()
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ c.setblocking(1)
|
|
|
|
|
+ conn = ConnectionHandler(c, self, addr)
|
|
|
|
|
+ conn.start()
|
|
|
|
|
+ self.add_conn(conn)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"Error en el bucle principal del servidor: {e}")
|
|
|
finally:
|
|
finally:
|
|
|
self.running = False
|
|
self.running = False
|
|
|
- if self.soc:
|
|
|
|
|
- self.soc.close()
|
|
|
|
|
- if self.soc_v6:
|
|
|
|
|
- self.soc_v6.close()
|
|
|
|
|
-
|
|
|
|
|
- def printLog(self, log):
|
|
|
|
|
- self.logLock.acquire()
|
|
|
|
|
- print log
|
|
|
|
|
- self.logLock.release()
|
|
|
|
|
-
|
|
|
|
|
- def addConn(self, conn):
|
|
|
|
|
- try:
|
|
|
|
|
- self.threadsLock.acquire()
|
|
|
|
|
|
|
+ if self.ipv4_socket:
|
|
|
|
|
+ self.ipv4_socket.close()
|
|
|
|
|
+ if self.ipv6_socket:
|
|
|
|
|
+ self.ipv6_socket.close()
|
|
|
|
|
+
|
|
|
|
|
+ def add_conn(self, conn):
|
|
|
|
|
+ """Añade un hilo de conexión a la lista de hilos activos de forma segura."""
|
|
|
|
|
+ with self.threads_lock:
|
|
|
if self.running:
|
|
if self.running:
|
|
|
self.threads.append(conn)
|
|
self.threads.append(conn)
|
|
|
- finally:
|
|
|
|
|
- self.threadsLock.release()
|
|
|
|
|
|
|
|
|
|
- def removeConn(self, conn):
|
|
|
|
|
- try:
|
|
|
|
|
- self.threadsLock.acquire()
|
|
|
|
|
- self.threads.remove(conn)
|
|
|
|
|
- finally:
|
|
|
|
|
- self.threadsLock.release()
|
|
|
|
|
|
|
+ def remove_conn(self, conn):
|
|
|
|
|
+ """Remueve un hilo de conexión de la lista de hilos activos de forma segura."""
|
|
|
|
|
+ with self.threads_lock:
|
|
|
|
|
+ if conn in self.threads:
|
|
|
|
|
+ self.threads.remove(conn)
|
|
|
|
|
|
|
|
def close(self):
|
|
def close(self):
|
|
|
- try:
|
|
|
|
|
- self.running = False
|
|
|
|
|
- self.threadsLock.acquire()
|
|
|
|
|
|
|
+ """Cierra el servidor y todos los hilos de conexión."""
|
|
|
|
|
+ self.running = False
|
|
|
|
|
+ with self.threads_lock:
|
|
|
threads = list(self.threads)
|
|
threads = list(self.threads)
|
|
|
for c in threads:
|
|
for c in threads:
|
|
|
c.close()
|
|
c.close()
|
|
|
- finally:
|
|
|
|
|
- self.threadsLock.release()
|
|
|
|
|
|
|
|
|
|
|
|
+# ==============================================================================
|
|
|
|
|
+# CLASE MANEJADORA DE CONEXIONES
|
|
|
|
|
+# Gestiona la lógica de cada conexión de cliente en un hilo separado
|
|
|
|
|
+# ==============================================================================
|
|
|
class ConnectionHandler(threading.Thread):
|
|
class ConnectionHandler(threading.Thread):
|
|
|
- def __init__(self, socClient, server, addr):
|
|
|
|
|
- threading.Thread.__init__(self)
|
|
|
|
|
- self.clientClosed = False
|
|
|
|
|
- self.targetClosed = True
|
|
|
|
|
- self.client = socClient
|
|
|
|
|
- self.client_buffer = ''
|
|
|
|
|
|
|
+ def __init__(self, client_socket, server, addr):
|
|
|
|
|
+ super().__init__()
|
|
|
|
|
+ self.client_closed = False
|
|
|
|
|
+ self.target_closed = True
|
|
|
|
|
+ self.client = client_socket
|
|
|
|
|
+ self.client_buffer = b''
|
|
|
self.server = server
|
|
self.server = server
|
|
|
- self.log = 'Connection: ' + str(addr)
|
|
|
|
|
|
|
+ self.addr = addr
|
|
|
|
|
+ self.log_prefix = f"{addr[0]}:{addr[1]}"
|
|
|
|
|
+ logger.info(f"Nueva conexión de {self.log_prefix}")
|
|
|
|
|
+ self.target = None
|
|
|
|
|
|
|
|
def close(self):
|
|
def close(self):
|
|
|
|
|
+ """Cierra los sockets del cliente y el destino."""
|
|
|
|
|
+ logger.info(f"Cerrando conexión {self.log_prefix}")
|
|
|
|
|
+
|
|
|
try:
|
|
try:
|
|
|
- if not self.clientClosed:
|
|
|
|
|
|
|
+ if not self.client_closed:
|
|
|
self.client.shutdown(socket.SHUT_RDWR)
|
|
self.client.shutdown(socket.SHUT_RDWR)
|
|
|
self.client.close()
|
|
self.client.close()
|
|
|
- except:
|
|
|
|
|
|
|
+ except Exception:
|
|
|
pass
|
|
pass
|
|
|
finally:
|
|
finally:
|
|
|
- self.clientClosed = True
|
|
|
|
|
|
|
+ self.client_closed = True
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
- if not self.targetClosed:
|
|
|
|
|
|
|
+ if not self.target_closed:
|
|
|
self.target.shutdown(socket.SHUT_RDWR)
|
|
self.target.shutdown(socket.SHUT_RDWR)
|
|
|
self.target.close()
|
|
self.target.close()
|
|
|
- except:
|
|
|
|
|
|
|
+ except Exception:
|
|
|
pass
|
|
pass
|
|
|
finally:
|
|
finally:
|
|
|
- self.targetClosed = True
|
|
|
|
|
|
|
+ self.target_closed = True
|
|
|
|
|
+
|
|
|
|
|
+ # 💡 Libera el semáforo al cerrar la conexión
|
|
|
|
|
+ connection_limit_semaphore.release()
|
|
|
|
|
|
|
|
def run(self):
|
|
def run(self):
|
|
|
try:
|
|
try:
|
|
|
self.client_buffer = self.client.recv(BUFLEN)
|
|
self.client_buffer = self.client.recv(BUFLEN)
|
|
|
- hostPort = self.findHeader(self.client_buffer, 'X-Real-Host')
|
|
|
|
|
- if hostPort == '':
|
|
|
|
|
- hostPort = DEFAULT_HOST
|
|
|
|
|
|
|
+ if not self.client_buffer:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ headers = self.client_buffer.decode('latin-1')
|
|
|
|
|
+ logger.debug(f"Headers received from {self.log_prefix}:\n{headers.strip()}")
|
|
|
|
|
|
|
|
- split = self.findHeader(self.client_buffer, 'X-Split')
|
|
|
|
|
- if split != '':
|
|
|
|
|
|
|
+ host_port = self.find_header(headers, 'X-Real-Host')
|
|
|
|
|
+ if not host_port:
|
|
|
|
|
+ host_port = DEFAULT_HOST
|
|
|
|
|
+
|
|
|
|
|
+ if self.find_header(headers, 'X-Split'):
|
|
|
self.client.recv(BUFLEN)
|
|
self.client.recv(BUFLEN)
|
|
|
|
|
|
|
|
- if hostPort != '':
|
|
|
|
|
- passwd = self.findHeader(self.client_buffer, 'X-Pass')
|
|
|
|
|
- if len(PASS) != 0 and passwd == PASS:
|
|
|
|
|
- self.method_CONNECT(hostPort)
|
|
|
|
|
- elif len(PASS) != 0 and passwd != PASS:
|
|
|
|
|
- self.client.send('HTTP/1.1 400 WrongPass!\r\n\r\n')
|
|
|
|
|
- elif hostPort.startswith('127.0.0.1') or hostPort.startswith('localhost'):
|
|
|
|
|
- self.method_CONNECT(hostPort)
|
|
|
|
|
|
|
+ if host_port:
|
|
|
|
|
+ passwd = self.find_header(headers, 'X-Pass')
|
|
|
|
|
+ if PASS and passwd == PASS:
|
|
|
|
|
+ logger.info(f"Autenticación exitosa para {self.log_prefix} -> {host_port}")
|
|
|
|
|
+ self.method_connect(host_port)
|
|
|
|
|
+ elif PASS and passwd != PASS:
|
|
|
|
|
+ logger.warning(f"Fallo de autenticación para {self.log_prefix} -> {host_port}")
|
|
|
|
|
+ self.client.send(b'HTTP/1.1 400 WrongPass!\r\n\r\n')
|
|
|
|
|
+ elif host_port.startswith('127.0.0.1') or host_port.startswith('localhost'):
|
|
|
|
|
+ logger.info(f"Conexión local permitida para {self.log_prefix} -> {host_port}")
|
|
|
|
|
+ self.method_connect(host_port)
|
|
|
else:
|
|
else:
|
|
|
- self.client.send('HTTP/1.1 403 Forbidden!\r\n\r\n')
|
|
|
|
|
|
|
+ logger.warning(f"Acceso denegado (sin contraseña) para {self.log_prefix} -> {host_port}")
|
|
|
|
|
+ self.client.send(b'HTTP/1.1 403 Forbidden!\r\n\r\n')
|
|
|
else:
|
|
else:
|
|
|
- self.server.printLog('- No X-Real-Host!')
|
|
|
|
|
- self.client.send('HTTP/1.1 400 NoXRealHost!\r\n\r\n')
|
|
|
|
|
|
|
+ logger.error(f"Encabezado 'X-Real-Host' no encontrado en la conexión de {self.log_prefix}")
|
|
|
|
|
+ self.client.send(b'HTTP/1.1 400 NoXRealHost!\r\n\r\n')
|
|
|
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- self.log += ' - error: ' + str(e)
|
|
|
|
|
- self.server.printLog(self.log)
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ logger.error(f"Error inesperado en el hilo de conexión {self.log_prefix}: {e}")
|
|
|
finally:
|
|
finally:
|
|
|
self.close()
|
|
self.close()
|
|
|
- self.server.removeConn(self)
|
|
|
|
|
|
|
+ self.server.remove_conn(self)
|
|
|
|
|
|
|
|
- def findHeader(self, head, header):
|
|
|
|
|
- aux = head.find(header + ': ')
|
|
|
|
|
- if aux == -1:
|
|
|
|
|
|
|
+ def find_header(self, head, header):
|
|
|
|
|
+ """Busca un encabezado en la solicitud HTTP."""
|
|
|
|
|
+ try:
|
|
|
|
|
+ aux = head.find(header + ': ')
|
|
|
|
|
+ if aux == -1: return ''
|
|
|
|
|
+ head = head[aux + len(header) + 2:]
|
|
|
|
|
+ aux = head.find('\r\n')
|
|
|
|
|
+ if aux == -1: return ''
|
|
|
|
|
+ return head[:aux]
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"Error al analizar el encabezado '{header}': {e}")
|
|
|
return ''
|
|
return ''
|
|
|
|
|
|
|
|
- aux = head.find(':', aux)
|
|
|
|
|
- head = head[aux+2:]
|
|
|
|
|
- aux = head.find('\r\n')
|
|
|
|
|
|
|
+ def connect_target(self, host_port):
|
|
|
|
|
+ """Intenta conectarse al host de destino con una lógica de reconexión."""
|
|
|
|
|
+ i = host_port.find(':')
|
|
|
|
|
+ if i != -1:
|
|
|
|
|
+ try:
|
|
|
|
|
+ port = int(host_port[i+1:])
|
|
|
|
|
+ host = host_port[:i]
|
|
|
|
|
+ except ValueError:
|
|
|
|
|
+ raise RuntimeError(f"Puerto inválido: {host_port[i+1:]}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ host = host_port
|
|
|
|
|
+ port = 22 # Puerto por defecto
|
|
|
|
|
|
|
|
- if aux == -1:
|
|
|
|
|
- return ''
|
|
|
|
|
- return head[:aux];
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ addr_info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
|
|
|
|
|
+ except socket.gaierror as e:
|
|
|
|
|
+ raise RuntimeError(f"Error de resolución de DNS para {host}: {e}")
|
|
|
|
|
|
|
|
- def connect_target(self, host):
|
|
|
|
|
- i = host.find(':')
|
|
|
|
|
- if i != -1:
|
|
|
|
|
- port = int(host[i+1:])
|
|
|
|
|
- host = host[:i]
|
|
|
|
|
|
|
+ if PRIORITIZE_IPV4:
|
|
|
|
|
+ prioritized_addrs = sorted(addr_info, key=lambda x: x[0] != socket.AF_INET)
|
|
|
else:
|
|
else:
|
|
|
- if self.method=='CONNECT':
|
|
|
|
|
- port = 22
|
|
|
|
|
- else:
|
|
|
|
|
- port = int(sys.argv[1])
|
|
|
|
|
-
|
|
|
|
|
- (soc_family, soc_type, proto, _, address) = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)[0]
|
|
|
|
|
- self.target = socket.socket(soc_family, soc_type, proto)
|
|
|
|
|
- self.targetClosed = False
|
|
|
|
|
- self.target.connect(address)
|
|
|
|
|
-
|
|
|
|
|
- def method_CONNECT(self, path):
|
|
|
|
|
- self.log += ' - CONNECT ' + path
|
|
|
|
|
- self.connect_target(path)
|
|
|
|
|
- self.client.sendall(RESPONSE)
|
|
|
|
|
- self.client_buffer = ''
|
|
|
|
|
- self.server.printLog(self.log)
|
|
|
|
|
- self.doCONNECT()
|
|
|
|
|
-
|
|
|
|
|
- def doCONNECT(self):
|
|
|
|
|
|
|
+ prioritized_addrs = addr_info
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(f"Intentando conectar a {host}:{port}. Direcciones disponibles: {[res[4] for res in prioritized_addrs]}")
|
|
|
|
|
+
|
|
|
|
|
+ for res in prioritized_addrs:
|
|
|
|
|
+ soc_family, soc_type, proto, _, address = res
|
|
|
|
|
+ address_str = address[0]
|
|
|
|
|
+ try:
|
|
|
|
|
+ self.target = socket.socket(soc_family, soc_type, proto)
|
|
|
|
|
+ self.target.connect(address)
|
|
|
|
|
+ self.target_closed = False
|
|
|
|
|
+ logger.info(f"Conexión exitosa a {address_str} (Familia: {soc_family})")
|
|
|
|
|
+ return
|
|
|
|
|
+ except socket.error as e:
|
|
|
|
|
+ logger.warning(f"Error al conectar a {address_str}: {e}. Intentando la siguiente dirección...")
|
|
|
|
|
+ if self.target:
|
|
|
|
|
+ self.target.close()
|
|
|
|
|
+ self.target = None
|
|
|
|
|
+
|
|
|
|
|
+ if self.target is None:
|
|
|
|
|
+ raise RuntimeError(f"No se pudo establecer una conexión con el host de destino: {host}")
|
|
|
|
|
+
|
|
|
|
|
+ def method_connect(self, path):
|
|
|
|
|
+ """Maneja el túnel de datos una vez que la conexión se ha establecido."""
|
|
|
|
|
+ try:
|
|
|
|
|
+ self.connect_target(path)
|
|
|
|
|
+ self.client.sendall(RESPONSE)
|
|
|
|
|
+ self.do_connect()
|
|
|
|
|
+ except RuntimeError as e:
|
|
|
|
|
+ logger.error(f"Error en la conexión del método CONNECT para {self.log_prefix}: {e}")
|
|
|
|
|
+ self.client.send(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"Error inesperado en method_connect para {self.log_prefix}: {e}")
|
|
|
|
|
+ self.client.send(b'HTTP/1.1 500 Internal Server Error\r\n\r\n')
|
|
|
|
|
+ finally:
|
|
|
|
|
+ self.client_buffer = b''
|
|
|
|
|
+
|
|
|
|
|
+ def do_connect(self):
|
|
|
|
|
+ """Bucle principal para el túnel de datos."""
|
|
|
socs = [self.client, self.target]
|
|
socs = [self.client, self.target]
|
|
|
count = 0
|
|
count = 0
|
|
|
- error = False
|
|
|
|
|
while True:
|
|
while True:
|
|
|
- count += 1
|
|
|
|
|
- (recv, _, err) = select.select(socs, [], socs, 3)
|
|
|
|
|
- if err:
|
|
|
|
|
- error = True
|
|
|
|
|
- if recv:
|
|
|
|
|
- for in_ in recv:
|
|
|
|
|
- try:
|
|
|
|
|
- data = in_.recv(BUFLEN)
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ readable, _, err = select.select(socs, [], socs, 3)
|
|
|
|
|
+ if err:
|
|
|
|
|
+ break
|
|
|
|
|
+ if readable:
|
|
|
|
|
+ for sock in readable:
|
|
|
|
|
+ data = sock.recv(BUFLEN)
|
|
|
if data:
|
|
if data:
|
|
|
- if in_ is self.target:
|
|
|
|
|
|
|
+ if sock is self.target:
|
|
|
self.client.send(data)
|
|
self.client.send(data)
|
|
|
else:
|
|
else:
|
|
|
- while data:
|
|
|
|
|
- byte = self.target.send(data)
|
|
|
|
|
- data = data[byte:]
|
|
|
|
|
|
|
+ self.target.sendall(data)
|
|
|
count = 0
|
|
count = 0
|
|
|
else:
|
|
else:
|
|
|
- break
|
|
|
|
|
- except:
|
|
|
|
|
- error = True
|
|
|
|
|
- break
|
|
|
|
|
- if count == TIMEOUT:
|
|
|
|
|
- error = True
|
|
|
|
|
- if error:
|
|
|
|
|
- break
|
|
|
|
|
-
|
|
|
|
|
-def print_usage():
|
|
|
|
|
- print 'Usage: proxy.py -p <port>'
|
|
|
|
|
- print ' proxy.py -b <bindAddr> -p <port>'
|
|
|
|
|
- print ' proxy.py -b 0.0.0.0 -p 80'
|
|
|
|
|
-
|
|
|
|
|
-def parse_args(argv):
|
|
|
|
|
- global IPV4_ADDR
|
|
|
|
|
- global IPV6_ADDR
|
|
|
|
|
- global LISTENING_PORT
|
|
|
|
|
- try:
|
|
|
|
|
- opts, args = getopt.getopt(argv,"hb:p:",["bind=","port="])
|
|
|
|
|
- except getopt.GetoptError:
|
|
|
|
|
- print_usage()
|
|
|
|
|
- sys.exit(2)
|
|
|
|
|
- for opt, arg in opts:
|
|
|
|
|
- if opt == '-h':
|
|
|
|
|
- print_usage()
|
|
|
|
|
- sys.exit()
|
|
|
|
|
- elif opt in ("-b", "--bind"):
|
|
|
|
|
- pass
|
|
|
|
|
- elif opt in ("-p", "--port"):
|
|
|
|
|
- LISTENING_PORT = int(arg)
|
|
|
|
|
|
|
+ logger.info(f"Conexión con {self.log_prefix} terminada.")
|
|
|
|
|
+ return
|
|
|
|
|
+ if count == TIMEOUT:
|
|
|
|
|
+ logger.info(f"Conexión con {self.log_prefix} excedió el tiempo de espera.")
|
|
|
|
|
+ return
|
|
|
|
|
+ count += 1
|
|
|
|
|
+ except (socket.error, socket.timeout) as e:
|
|
|
|
|
+ logger.error(f"Error en el túnel de datos para {self.log_prefix}: {e}")
|
|
|
|
|
+ return
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"Error inesperado en do_connect para {self.log_prefix}: {e}")
|
|
|
|
|
+ return
|
|
|
|
|
|
|
|
-def main(port=LISTENING_PORT):
|
|
|
|
|
- server = Server(None, port)
|
|
|
|
|
|
|
+# ==============================================================================
|
|
|
|
|
+# LÓGICA PRINCIPAL
|
|
|
|
|
+# ==============================================================================
|
|
|
|
|
+def main():
|
|
|
|
|
+ server = Server(LISTENING_PORT)
|
|
|
server.start()
|
|
server.start()
|
|
|
- while True:
|
|
|
|
|
- try:
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ while True:
|
|
|
time.sleep(2)
|
|
time.sleep(2)
|
|
|
- except KeyboardInterrupt:
|
|
|
|
|
- print 'Stopping...'
|
|
|
|
|
- server.close()
|
|
|
|
|
- break
|
|
|
|
|
|
|
+ except KeyboardInterrupt:
|
|
|
|
|
+ logger.info('Deteniendo el servidor...')
|
|
|
|
|
+ server.close()
|
|
|
|
|
+ server.join()
|
|
|
|
|
+ sys.exit(0)
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if __name__ == '__main__':
|
|
|
main()
|
|
main()
|
|
|
|
|
+
|