feat: удаление connection.py из-за перехода на outbound.py

This commit is contained in:
2026-07-04 18:03:02 +03:00
parent 904f9cd973
commit ecb669a5a1
2 changed files with 0 additions and 626 deletions
-1
View File
@@ -1,4 +1,3 @@
from .connection import Connection, ConnectionFactory, ConnectionKey
from .outbound import (
OUTBOUND_FIELDS,
Outbound,
-625
View File
@@ -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
)