From ecb669a5a103b765b504705e991c54e2edfb91a4 Mon Sep 17 00:00:00 2001 From: vladimir Date: Sat, 4 Jul 2026 18:03:02 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20connection.py=20=D0=B8=D0=B7-=D0=B7=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4=D0=B0=20=D0=BD=D0=B0?= =?UTF-8?q?=20outbound.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/xray_manager/core/__init__.py | 1 - src/xray_manager/core/connection.py | 625 ---------------------------- 2 files changed, 626 deletions(-) delete mode 100644 src/xray_manager/core/connection.py diff --git a/src/xray_manager/core/__init__.py b/src/xray_manager/core/__init__.py index e63c1a8..88cba9f 100644 --- a/src/xray_manager/core/__init__.py +++ b/src/xray_manager/core/__init__.py @@ -1,4 +1,3 @@ -from .connection import Connection, ConnectionFactory, ConnectionKey from .outbound import ( OUTBOUND_FIELDS, Outbound, diff --git a/src/xray_manager/core/connection.py b/src/xray_manager/core/connection.py deleted file mode 100644 index 1a5efe1..0000000 --- a/src/xray_manager/core/connection.py +++ /dev/null @@ -1,625 +0,0 @@ -from __future__ import annotations - -import secrets -import uuid -from abc import ABC, abstractmethod -from collections import defaultdict -from dataclasses import dataclass -from typing import TYPE_CHECKING, TypeVar -from urllib.parse import parse_qs, quote, unquote, urlencode, urlparse - -from ..utils import private_to_public - -if TYPE_CHECKING: - from .xray_config import XrayConfig - -C = TypeVar("C", bound="Connection") -T = TypeVar("T") - - -class ConnectionFactory: - _registry: dict[str, type["Connection"]] = {} - _scheme_registry: dict[str, type["Connection"]] = {} - - @classmethod - def register(cls, conn_cls: type[C]) -> type[C]: - protocol = conn_cls.PROTOCOL - scheme = conn_cls.LINK_SCHEME - - if protocol in cls._registry: - raise RuntimeError(f"Protocol already registered: {protocol}") - - if scheme in cls._scheme_registry: - raise RuntimeError(f"Scheme already registered: {scheme}") - - cls._registry[protocol] = conn_cls - cls._scheme_registry[scheme] = conn_cls - - return conn_cls - - @classmethod - def get_protocols(cls) -> list[str]: - return list(cls._registry) - - @classmethod - def from_link(cls, link: str) -> "Connection": - scheme = link.split("://", 1)[0] - - if scheme not in cls._scheme_registry: - raise ValueError(f"Unsupported link scheme: {scheme}") - - conn_cls = cls._scheme_registry[scheme] - return conn_cls.from_link(link) - - @classmethod - def from_inbound(cls, host: str, client: dict, inbound: dict): - protocol = inbound.get("protocol") - - if protocol not in cls._registry: - raise ValueError(f"Unsupported protocol: {protocol}") - - conn_cls = cls._registry[protocol] - username, connection = conn_cls.from_inbound(host, client, inbound) - return username, connection - - @classmethod - def from_spec( - cls, key: "ConnectionKey", xray_config: "XrayConfig" - ) -> "Connection": - if key.protocol not in cls._registry: - raise ValueError(f"Unsupported protocol: {key.protocol}") - - if key.exit_point not in xray_config.get_exit_points(): - raise ValueError(f"Exit point {key.exit_point} not found") - - inbound = xray_config.find_managed_inbound_by_protocol(key.protocol) - conn_cls = cls._registry[key.protocol] - return conn_cls.from_spec(xray_config.host, inbound, key.exit_point) - - -class Connection(ABC): - PROTOCOL: str - LINK_SCHEME: str - exit_point: str - - @classmethod - @abstractmethod - def from_link(cls, link: str) -> "Connection": ... - - @classmethod - @abstractmethod - def from_inbound( - cls, host: str, client: dict, inbound: dict - ) -> tuple[str, "Connection"]: ... - - @classmethod - @abstractmethod - def from_spec( - cls, host: str, inbound: dict, exit_point: str - ) -> "Connection": ... - - @abstractmethod - def to_link(self) -> str: ... - - @abstractmethod - def matches_inbound(self, inbound: dict) -> bool: ... - - @abstractmethod - def add_to_inbound(self, inbound: dict, username: str) -> None: ... - - @abstractmethod - def delete_from_inbound(self, inbound: dict, username: str) -> None: ... - - @property - @abstractmethod - def protocol(self) -> str: ... - - @staticmethod - def split_client_email(email: str) -> tuple[str, str]: - username, exit_point = email.rsplit("-", 1) - return username, exit_point - - @abstractmethod - def __hash__(self) -> int: ... - - @abstractmethod - def __eq__(self, other: object) -> bool: ... - - -@dataclass(frozen=True) -class ConnectionKey: - protocol: str - exit_point: str - - -@ConnectionFactory.register -@dataclass(eq=False) -class ShadowsocksConnection(Connection): - PROTOCOL = "shadowsocks" - LINK_SCHEME = "ss" - - host: str - port: int - method: str - server_password: str - client_password: str - exit_point: str - - @classmethod - def from_link(cls, link: str) -> "ShadowsocksConnection": - import base64 - - prefix_b64, rest = link[5:].split("@", 1) - host_port, exit_point = rest.split("#", 1) - host, port_str = host_port.split(":") - port = int(port_str) - - prefix_bytes = base64.urlsafe_b64decode(prefix_b64) - prefix_str = prefix_bytes.decode() - method, server_password, client_password = prefix_str.split(":") - - return cls( - host=host, - port=port, - method=method, - server_password=server_password, - client_password=client_password, - exit_point=exit_point, - ) - - @classmethod - def from_inbound( - cls, host: str, client: dict, inbound: dict - ) -> tuple[str, "ShadowsocksConnection"]: - port = inbound["port"] - method = inbound["settings"]["method"] - server_password = inbound["settings"]["password"] - client_password = client["password"] - email = client["email"] - username, exit_point = cls.split_client_email(email) - connection = cls( - host=host, - port=port, - method=method, - server_password=server_password, - client_password=client_password, - exit_point=exit_point, - ) - return username, connection - - @classmethod - def from_spec( - cls, host: str, inbound: dict, exit_point: str - ) -> "ShadowsocksConnection": - port = inbound["port"] - method = inbound["settings"]["method"] - server_password = inbound["settings"]["password"] - client_password = cls.generate_password(method) - - return cls( - host=host, - port=port, - method=method, - server_password=server_password, - client_password=client_password, - exit_point=exit_point, - ) - - def to_link(self) -> str: - import base64 - - prefix = f"{self.method}:{self.server_password}:{self.client_password}" - prefix_b64 = base64.urlsafe_b64encode(prefix.encode()).decode() - tag = self.exit_point - link = f"ss://{prefix_b64}@{self.host}:{self.port}#{tag}" - return link - - @staticmethod - def generate_password(method: str) -> str: - import base64 - import secrets - - key_lengths = { - "2022-blake3-aes-128-gcm": 16, - "2022-blake3-aes-256-gcm": 32, - "2022-blake3-chacha20-poly1305": 32, - } - - try: - length = key_lengths[method] - except KeyError: - raise ValueError(f"Unsupported shadowsocks method: {method}") - - key = secrets.token_bytes(length) - - return base64.b64encode(key).decode() - - def matches_inbound(self, inbound: dict): - if inbound.get("protocol") != self.PROTOCOL: - return False - - if inbound.get("port") != self.port: - return False - - settings = inbound.get("settings", {}) - - if settings.get("method") != self.method: - return False - - if settings.get("password") != self.server_password: - return False - - return True - - def add_to_inbound(self, inbound: dict, username: str) -> None: - client = { - "email": f"{username}-{self.exit_point}", - "password": self.client_password, - } - - clients = inbound["settings"].setdefault("clients", []) - clients.append(client) - - clients_by_user = defaultdict(list) - for client in clients: - username, _ = self.split_client_email(client["email"]) - clients_by_user[username].append(client) - - priority = {"default": 0, "managed": 1, "relay": 2} - - def sort_key(c): - _, exit_point = self.split_client_email(c["email"]) - return (priority.get(exit_point, 100), c["email"]) - - for user_clients in clients_by_user.values(): - user_clients.sort(key=sort_key) - - sorted_clients = [] - for username in sorted(clients_by_user): - sorted_clients.extend(clients_by_user[username]) - - inbound["settings"]["clients"] = sorted_clients - - def delete_from_inbound(self, inbound: dict, username: str) -> None: - target_email = f"{username}-{self.exit_point}" - settings = inbound["settings"] - clients = settings["clients"] - - filtered_clients = [] - for client in clients: - if client["email"] != target_email: - filtered_clients.append(client) - - inbound["settings"]["clients"] = filtered_clients - - @property - def protocol(self) -> str: - return self.PROTOCOL - - def __hash__(self) -> int: - return hash( - ( - self.host, - self.port, - self.method, - self.server_password, - self.client_password, - self.exit_point, - ) - ) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, ShadowsocksConnection): - return NotImplemented - - return ( - self.host == other.host - and self.port == other.port - and self.method == other.method - and self.server_password == other.server_password - and self.client_password == other.client_password - and self.exit_point == other.exit_point - ) - - -@ConnectionFactory.register -@dataclass(eq=False) -class VlessConnection(Connection): - PROTOCOL = "vless" - LINK_SCHEME = "vless" - - host: str - port: int - id: str - security: str - encryption: str - public_key: str - network_type: str - sni: str - short_id: str - exit_point: str - - flow: str = "xtls-rprx-vision" - header_type: str = "none" - finger_print: str = "chrome" - - @classmethod - def from_link(cls, link: str) -> "VlessConnection": - parsed = urlparse(link) - query = parse_qs(parsed.query) - - def require(value: T | None, name: str) -> T: - if value is None: - raise ValueError(f"Missing required field: {name}") - return value - - def require_str(value: str | None, name: str) -> str: - value = require(value, name) - if value == "": - raise ValueError(f"Empty value for required field: {name}") - return unquote(value) - - def get_required(key: str) -> str: - values = query.get(key) - if not values: - raise ValueError(f"Missing required query param: {key}") - raw = values[0] - if raw is None or raw == "": - raise ValueError(f"Empty value for required query param: {key}") - return unquote(raw) - - return cls( - host=require_str(parsed.hostname, "host"), - port=require(parsed.port, "port"), - id=require_str(parsed.username, "id"), - security=get_required("security"), - encryption=get_required("encryption"), - public_key=get_required("pbk"), - header_type=get_required("headerType"), - finger_print=get_required("fp"), - network_type=get_required("type"), - flow=get_required("flow"), - sni=get_required("sni"), - short_id=get_required("sid"), - exit_point=require_str(parsed.fragment, "exit_point"), - ) - - @classmethod - def from_inbound( - cls, host: str, client: dict, inbound: dict - ) -> tuple[str, "VlessConnection"]: - settings = inbound["settings"] - stream = inbound["streamSettings"] - reality = stream["realitySettings"] - - port = inbound["port"] - id = client["id"] - flow = client["flow"] - encryption = settings["decryption"] - network_type = stream["network"] - security = stream["security"] - sni = reality["serverNames"][0] - public_key = private_to_public(reality["privateKey"]) - - clients = settings["clients"] - short_ids = reality["shortIds"] - - try: - index = clients.index(client) - except ValueError: - raise RuntimeError(f"Client not found in inbound: {client}") - - try: - short_id = short_ids[index] - except IndexError: - raise RuntimeError(f"No shortId for client at index {index}") - - email = client["email"] - username, exit_point = cls.split_client_email(email) - connection = cls( - host=host, - port=port, - id=id, - security=security, - encryption=encryption, - public_key=public_key, - network_type=network_type, - flow=flow, - sni=sni, - short_id=short_id, - exit_point=exit_point, - ) - return username, connection - - @classmethod - def from_spec( - cls, host: str, inbound: dict, exit_point: str - ) -> "VlessConnection": - settings = inbound["settings"] - stream = inbound["streamSettings"] - reality = stream["realitySettings"] - - port = inbound["port"] - encryption = settings["decryption"] - network_type = stream["network"] - security = stream["security"] - sni = reality["serverNames"][0] - public_key = private_to_public(reality["privateKey"]) - - id = cls.generate_id() - short_id = cls.generate_short_id() - - return cls( - host=host, - port=port, - id=id, - security=security, - encryption=encryption, - public_key=public_key, - network_type=network_type, - sni=sni, - short_id=short_id, - exit_point=exit_point, - ) - - def to_link(self) -> str: - port = 443 - netloc = f"{self.id}@{self.host}:{port}" - params = { - "security": self.security, - "encryption": self.encryption, - "pbk": self.public_key, - "headerType": self.header_type, - "fp": self.finger_print, - "type": self.network_type, - "flow": self.flow, - "sni": self.sni, - "sid": self.short_id, - } - query = urlencode(params, quote_via=quote) - fragment = quote(self.exit_point) - return f"vless://{netloc}?{query}#{fragment}" - - @staticmethod - def generate_id() -> str: - return str(uuid.uuid4()) - - @staticmethod - def generate_short_id() -> str: - return secrets.token_hex(8) - - def matches_inbound(self, inbound: dict) -> bool: - if inbound.get("protocol") != self.PROTOCOL: - return False - - if inbound.get("port") != self.port: - return False - - settings = inbound.get("settings", {}) - if settings.get("decryption") != self.encryption: - return False - - stream_settings = inbound.get("streamSettings", {}) - if stream_settings.get("network") != self.network_type: - return False - if stream_settings.get("security") != self.security: - return False - - reality_settings = stream_settings.get("realitySettings", {}) - if self.sni not in reality_settings.get("serverNames", []): - return False - - inbound_private_key = reality_settings.get("privateKey") - if not inbound_private_key: - raise ValueError("Reality Settings do not have private key") - inbound_public_key = private_to_public(inbound_private_key) - if inbound_public_key != self.public_key: - return False - - return True - - def add_to_inbound(self, inbound: dict, username: str) -> None: - client = { - "email": f"{username}-{self.exit_point}", - "id": self.id, - "flow": self.flow, - } - - clients = inbound["settings"].setdefault("clients", []) - reality = inbound["streamSettings"].setdefault("realitySettings", {}) - short_ids = reality.setdefault("shortIds", []) - - clients.append(client) - short_ids.append(self.short_id) - - clients_and_sids_by_user = defaultdict(list) - for client, sid in zip(clients, short_ids): - username, _ = self.split_client_email(client["email"]) - clients_and_sids_by_user[username].append((client, sid)) - - priority = {"default": 0, "managed": 1, "relay": 2} - - def sort_key(pair): - client, _ = pair - _, exit_point = self.split_client_email(client["email"]) - return (priority.get(exit_point, 100), client["email"]) - - for pair in clients_and_sids_by_user.values(): - pair.sort(key=sort_key) - - sorted_clients = [] - sorted_short_ids = [] - for username in sorted(clients_and_sids_by_user): - for client, sid in clients_and_sids_by_user[username]: - sorted_clients.append(client) - sorted_short_ids.append(sid) - - inbound["settings"]["clients"] = sorted_clients - reality["shortIds"] = sorted_short_ids - - def delete_from_inbound(self, inbound: dict, username: str) -> None: - target_email = f"{username}-{self.exit_point}" - - clients = inbound["settings"].get("clients", []) - reality = inbound["streamSettings"].get("realitySettings", {}) - short_ids = reality.get("shortIds", []) - - if not clients or not short_ids: - return - - filtered_pairs = [ - (client, sid) - for client, sid in zip(clients, short_ids) - if client["email"] != target_email - ] - - inbound["settings"]["clients"] = [ - client for client, _ in filtered_pairs - ] - - inbound["streamSettings"]["realitySettings"]["shortIds"] = [ - sid for _, sid in filtered_pairs - ] - - @property - def protocol(self) -> str: - return self.PROTOCOL - - def __hash__(self) -> int: - return hash( - ( - self.host, - self.port, - self.id, - self.security, - self.encryption, - self.public_key, - self.header_type, - self.finger_print, - self.network_type, - self.flow, - self.sni, - self.short_id, - self.exit_point, - ) - ) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, VlessConnection): - return NotImplemented - - return ( - self.host == other.host - and self.port == other.port - and self.id == other.id - and self.security == other.security - and self.encryption == other.encryption - and self.public_key == other.public_key - and self.header_type == other.header_type - and self.finger_print == other.finger_print - and self.network_type == other.network_type - and self.flow == other.flow - and self.sni == other.sni - and self.short_id == other.short_id - and self.exit_point == other.exit_point - )