Compare commits
4
Commits
9da0596f6d
...
0a3659c852
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a3659c852 | ||
|
|
d00081e632 | ||
|
|
8063160e38 | ||
|
|
89e53a4a48 |
@@ -10,9 +10,128 @@ from urllib.parse import parse_qs, quote, unquote, urlencode, urlparse
|
|||||||
|
|
||||||
from .outbound_fields import OUTBOUND_FIELDS
|
from .outbound_fields import OUTBOUND_FIELDS
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .xray_config import XrayConfig
|
||||||
|
from .xray_manager_config import XrayManagerConfig
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class OutboundSpec:
|
||||||
|
protocol: str
|
||||||
|
target: str
|
||||||
|
|
||||||
|
|
||||||
|
O = TypeVar("O", bound=Outbound)
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class OutboundFactory:
|
||||||
|
_registry: dict[str, type[Outbound]] = {}
|
||||||
|
_scheme_registry: dict[str, type[Outbound]] = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(cls, outbound_cls: type[O]) -> type[O]:
|
||||||
|
protocol = outbound_cls.PROTOCOL
|
||||||
|
scheme = outbound_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] = outbound_cls
|
||||||
|
cls._scheme_registry[scheme] = outbound_cls
|
||||||
|
|
||||||
|
return outbound_cls
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_protocols(cls) -> list[str]:
|
||||||
|
return list(cls._registry)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_link(cls, link: str) -> Outbound:
|
||||||
|
scheme = link.split("://", 1)[0]
|
||||||
|
|
||||||
|
if scheme not in cls._scheme_registry:
|
||||||
|
raise ValueError(f"Unsupported link scheme: {scheme}")
|
||||||
|
|
||||||
|
outbound_cls = cls._scheme_registry[scheme]
|
||||||
|
return outbound_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}")
|
||||||
|
|
||||||
|
outbound_cls = cls._registry[protocol]
|
||||||
|
username, outbound = outbound_cls.from_inbound(host, client, inbound)
|
||||||
|
return username, outbound
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_spec(
|
||||||
|
cls,
|
||||||
|
xray_config: XrayConfig,
|
||||||
|
xray_manager_config: XrayManagerConfig,
|
||||||
|
spec: OutboundSpec,
|
||||||
|
) -> Outbound:
|
||||||
|
if spec.protocol not in cls._registry:
|
||||||
|
raise ValueError(f"Unsupported protocol: {spec.protocol}")
|
||||||
|
|
||||||
|
if spec.target not in xray_manager_config.get_targets_list():
|
||||||
|
raise ValueError(f"Target {spec.target} not found")
|
||||||
|
|
||||||
|
inbound = xray_config.find_managed_inbound_by_protocol(spec.protocol)
|
||||||
|
outbound_cls = cls._registry[spec.protocol]
|
||||||
|
return outbound_cls.from_scrath(xray_config.host, inbound, spec.target)
|
||||||
|
|
||||||
|
|
||||||
class Outbound(ABC):
|
class Outbound(ABC):
|
||||||
pass
|
PROTOCOL: str
|
||||||
|
LINK_SCHEME: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
def from_link(cls, link: str) -> Outbound: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
def from_inbound(
|
||||||
|
cls, host: str, client: dict, inbound: dict
|
||||||
|
) -> tuple[str, Outbound]: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
def from_scrath(cls, host: str, inbound: dict, target: str) -> Outbound: ...
|
||||||
|
|
||||||
|
@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, target = email.rsplit("-", 1)
|
||||||
|
return username, target
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __hash__(self) -> int: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __eq__(self, other: object) -> bool: ...
|
||||||
|
|
||||||
|
|
||||||
_ShadowsocksFields = make_dataclass(
|
_ShadowsocksFields = make_dataclass(
|
||||||
@@ -20,14 +139,39 @@ _ShadowsocksFields = make_dataclass(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@OutboundFactory.register
|
||||||
|
@dataclass
|
||||||
class ShadowsocksOutbound(_ShadowsocksFields, Outbound):
|
class ShadowsocksOutbound(_ShadowsocksFields, Outbound):
|
||||||
pass
|
PROTOCOL = "shadowsocks"
|
||||||
|
LINK_SCHEME = "ss"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_link(cls, link: str) -> ShadowsocksOutbound:
|
||||||
|
import base64
|
||||||
|
|
||||||
|
prefix_b64, rest = link[5:].split("@", 1)
|
||||||
|
host_port, target_pretty_name = 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_VlessFields = make_dataclass(
|
_VlessFields = make_dataclass("_VlessFieldss", fields=OUTBOUND_FIELDS["vless"])
|
||||||
"_VlessFieldss", fields=OUTBOUND_FIELDS["vless"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@OutboundFactory.register
|
||||||
|
@dataclass
|
||||||
class VlessOutbound(_VlessFields, Outbound):
|
class VlessOutbound(_VlessFields, Outbound):
|
||||||
pass
|
pass
|
||||||
@@ -62,6 +62,7 @@ class XrayConfig:
|
|||||||
f"Managed inbound not found for protocol: {protocol}"
|
f"Managed inbound not found for protocol: {protocol}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Устаревший метод. Перейти на использование xray_config.get_targets_list()
|
||||||
def get_exit_points(self) -> set[str]:
|
def get_exit_points(self) -> set[str]:
|
||||||
exit_points = set()
|
exit_points = set()
|
||||||
for data in self.outbounds_data.values():
|
for data in self.outbounds_data.values():
|
||||||
|
|||||||
@@ -73,6 +73,13 @@ class XrayManagerConfig:
|
|||||||
for username, folder_name in self.profile_folder_mapping.items()
|
for username, folder_name in self.profile_folder_mapping.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def targets(self) -> dict[str, dict[str, str]]:
|
||||||
|
return self.config.get("target", {})
|
||||||
|
|
||||||
|
def get_targets_list(self) -> list[str]:
|
||||||
|
return list(self.targets.keys())
|
||||||
|
|
||||||
def set_profile_folder(self, username: str, folder_name: str):
|
def set_profile_folder(self, username: str, folder_name: str):
|
||||||
self.config["profiles"]["folder_mapping"][username] = folder_name
|
self.config["profiles"]["folder_mapping"][username] = folder_name
|
||||||
self._save_json()
|
self._save_json()
|
||||||
|
|||||||
@@ -5,6 +5,27 @@
|
|||||||
"host": "localhost.internal",
|
"host": "localhost.internal",
|
||||||
"base_path": "tests/mock_data/var/lib/xray-manager/profiles",
|
"base_path": "tests/mock_data/var/lib/xray-manager/profiles",
|
||||||
"file_name": "list.txt",
|
"file_name": "list.txt",
|
||||||
"folder_mapping": {}
|
"folder_mapping": {
|
||||||
|
"test": "OiVqwjq0V9kI",
|
||||||
|
"test2": "P8TC4FUicrvC"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"default": {
|
||||||
|
"route": "out-fr1",
|
||||||
|
"pretty_name": "🚩 Default"
|
||||||
|
},
|
||||||
|
"fr1": {
|
||||||
|
"route": "out-fr1",
|
||||||
|
"pretty_name": "🇫🇷 France #1"
|
||||||
|
},
|
||||||
|
"us1": {
|
||||||
|
"route": "out-us1",
|
||||||
|
"pretty_name": "🇺🇸 USA #1"
|
||||||
|
},
|
||||||
|
"us2": {
|
||||||
|
"route": "out-us2",
|
||||||
|
"pretty_name": "🇺🇸 USA #2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user