Xray works

This commit is contained in:
AB from home.homenet
2025-08-08 05:46:36 +03:00
parent 56b0b160e3
commit 787432cbcf
46 changed files with 5625 additions and 3551 deletions

View File

@@ -0,0 +1,62 @@
"""Xray API Python Library"""
from .client import XrayClient
from .stats import StatsManager, StatItem, SystemStats
from .subscription import SubscriptionLinkGenerator
from .exceptions import (
XrayAPIError, XrayConnectionError, XrayCommandError,
XrayConfigError, XrayNotFoundError, XrayValidationError
)
from .models import (
# Base
XrayProtocol, TransportProtocol, SecurityType,
# Protocols
VLESSClient, VMeSSUser, TrojanUser, TrojanFallback,
VLESSInboundConfig, VMeSSInboundConfig, TrojanServerConfig,
generate_uuid, validate_uuid,
# Transports
StreamSettings, create_tcp_stream, create_ws_stream,
create_grpc_stream, create_http_stream, create_xhttp_stream,
# Security
TLSConfig, REALITYConfig,
create_tls_config, create_reality_config,
generate_self_signed_certificate, create_tls_config_with_self_signed,
# Inbound
InboundConfig, InboundBuilder, SniffingConfig
)
__version__ = "0.1.0"
__all__ = [
# Client
'XrayClient',
# Stats
'StatsManager', 'StatItem', 'SystemStats',
# Subscription
'SubscriptionLinkGenerator',
# Exceptions
'XrayAPIError', 'XrayConnectionError', 'XrayCommandError',
'XrayConfigError', 'XrayNotFoundError', 'XrayValidationError',
# Enums
'XrayProtocol', 'TransportProtocol', 'SecurityType',
# Models
'VLESSClient', 'VMeSSUser', 'TrojanUser', 'TrojanFallback',
'VLESSInboundConfig', 'VMeSSInboundConfig', 'TrojanServerConfig',
'StreamSettings', 'TLSConfig', 'REALITYConfig',
'InboundConfig', 'InboundBuilder', 'SniffingConfig',
# Utils
'generate_uuid', 'validate_uuid',
'create_tcp_stream', 'create_ws_stream',
'create_grpc_stream', 'create_http_stream', 'create_xhttp_stream',
'create_tls_config', 'create_reality_config',
'generate_self_signed_certificate', 'create_tls_config_with_self_signed',
]

235
vpn/xray_api_v2/client.py Normal file
View File

@@ -0,0 +1,235 @@
"""Xray API client implementation"""
import json
import subprocess
from typing import Dict, Any, List, Optional, Union
from pathlib import Path
import tempfile
import os
from .exceptions import (
XrayConnectionError,
XrayCommandError,
XrayConfigError
)
from .models.base import BaseXrayModel
class XrayClient:
"""Client for interacting with Xray API via CLI commands"""
def __init__(self, server: str = "127.0.0.1:8080", timeout: int = 3):
"""
Initialize Xray client
Args:
server: API server address (host:port)
timeout: Command timeout in seconds
"""
self.server = server
self.timeout = timeout
self._xray_binary = "xray"
def execute_command(self,
command: str,
args: List[str] = None,
json_files: List[Union[str, Dict, BaseXrayModel]] = None) -> Dict[str, Any]:
"""
Execute xray API command
Args:
command: API command (e.g., 'adi', 'adu', 'lsi')
args: Additional command arguments
json_files: JSON configurations (paths, dicts, or models)
Returns:
Command output as dictionary
"""
cmd = [self._xray_binary, "api", command]
cmd.extend([f"--server={self.server}", f"--timeout={self.timeout}"])
if args:
cmd.extend(args)
temp_files = []
try:
# Handle JSON configurations
if json_files:
for config in json_files:
if isinstance(config, str):
# File path provided
cmd.append(config)
else:
# Create temporary file for dict or model
temp_file = self._create_temp_json(config)
temp_files.append(temp_file)
cmd.append(temp_file.name)
# Execute command
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=self.timeout + 5 # Add buffer to subprocess timeout
)
if result.returncode != 0:
raise XrayCommandError(f"Command failed: {result.stderr}")
# Parse output
output = result.stdout.strip()
if not output:
return {}
try:
return json.loads(output)
except json.JSONDecodeError:
# Some commands return plain text
return {"output": output}
except subprocess.TimeoutExpired:
raise XrayConnectionError(f"Command timed out after {self.timeout} seconds")
except Exception as e:
raise XrayCommandError(f"Command execution failed: {str(e)}")
finally:
# Cleanup temporary files
for temp_file in temp_files:
try:
os.unlink(temp_file.name)
except:
pass
def _create_temp_json(self, config: Union[Dict, BaseXrayModel]) -> tempfile.NamedTemporaryFile:
"""Create temporary JSON file from config"""
if isinstance(config, BaseXrayModel):
data = config.to_xray_json()
else:
data = config
temp_file = tempfile.NamedTemporaryFile(
mode='w',
suffix='.json',
delete=False
)
json.dump(data, temp_file, indent=2)
temp_file.close()
return temp_file
# Inbound management
def add_inbound(self, *configs: Union[Dict, BaseXrayModel]) -> Dict[str, Any]:
"""Add one or more inbounds"""
# Wrap inbound configs in the required format
wrapped_configs = []
for config in configs:
if isinstance(config, BaseXrayModel):
config_dict = config.to_xray_json()
else:
config_dict = config
# Wrap in inbounds array if not already wrapped
if "inbounds" not in config_dict:
wrapped_config = {"inbounds": [config_dict]}
else:
wrapped_config = config_dict
wrapped_configs.append(wrapped_config)
return self.execute_command("adi", json_files=wrapped_configs)
def remove_inbound(self, tag: str) -> Dict[str, Any]:
"""Remove inbound by tag"""
return self.execute_command("rmi", args=[tag])
def list_inbounds(self) -> List[Dict[str, Any]]:
"""List all inbounds"""
result = self.execute_command("lsi")
return result.get("inbounds", [])
# Outbound management
def add_outbound(self, *configs: Union[Dict, BaseXrayModel]) -> Dict[str, Any]:
"""Add one or more outbounds"""
return self.execute_command("ado", json_files=list(configs))
def remove_outbound(self, tag: str) -> Dict[str, Any]:
"""Remove outbound by tag"""
return self.execute_command("rmo", args=[tag])
def list_outbounds(self) -> List[Dict[str, Any]]:
"""List all outbounds"""
result = self.execute_command("lso")
return result.get("outbounds", [])
# User management
def add_users(self, *configs: Union[Dict, BaseXrayModel]) -> Dict[str, Any]:
"""Add users to inbounds using JSON files"""
return self.execute_command("adu", json_files=list(configs))
def remove_users(self, tag: str, *emails: str) -> Dict[str, Any]:
"""Remove users from inbounds using tag and email list"""
args = [f"-tag={tag}"] + list(emails)
return self.execute_command("rmu", args=args)
def get_inbound_user(self, tag: str, email: Optional[str] = None) -> Dict[str, Any]:
"""Get inbound user(s) information using -tag flag"""
args = [f"-tag={tag}"]
if email:
args.append(f"-email={email}")
return self.execute_command("inbounduser", args=args)
def get_inbound_user_count(self, tag: str) -> int:
"""Get inbound user count using -tag flag"""
args = [f"-tag={tag}"]
result = self.execute_command("inboundusercount", args=args)
# Parse the result - might be in output field or direct number
if isinstance(result, dict):
return result.get("count", 0)
return 0
# Statistics
def get_stats(self, pattern: str = "", reset: bool = False) -> List[Dict[str, Any]]:
"""Get statistics"""
args = [pattern]
if reset:
args.append("-reset")
if pattern:
args.extend(["-json"])
result = self.execute_command("statsquery", args=args)
return result.get("stat", [])
def get_system_stats(self) -> Dict[str, Any]:
"""Get system statistics"""
return self.execute_command("statssys")
def get_online_stats(self, email: str) -> Dict[str, Any]:
"""Get online session count for user"""
return self.execute_command("statsonline", args=[email])
def get_online_ips(self, email: str) -> List[Dict[str, Any]]:
"""Get user's online IP addresses"""
result = self.execute_command("statsonlineiplist", args=[email])
return result.get("ips", [])
# Routing rules
def add_routing_rules(self, *configs: Union[Dict, BaseXrayModel]) -> Dict[str, Any]:
"""Add routing rules"""
return self.execute_command("adrules", json_files=list(configs))
def remove_routing_rules(self, *tags: str) -> Dict[str, Any]:
"""Remove routing rules by tags"""
return self.execute_command("rmrules", args=list(tags))
# Other operations
def restart_logger(self) -> Dict[str, Any]:
"""Restart the logger"""
return self.execute_command("restartlogger")
def get_balancer_info(self, tag: str) -> Dict[str, Any]:
"""Get balancer information"""
return self.execute_command("bi", args=[tag])
def override_balancer(self, tag: str, selectors: List[str]) -> Dict[str, Any]:
"""Override balancer selection"""
return self.execute_command("bo", args=[tag] + selectors)
def block_connection(self, source_ip: str, seconds: int) -> Dict[str, Any]:
"""Block connections from source IP"""
return self.execute_command("sib", args=[source_ip, str(seconds)])

View File

View File

@@ -0,0 +1,33 @@
"""User management command wrappers"""
from dataclasses import dataclass
from typing import List, Optional, Dict, Any
from ..models.base import BaseXrayModel
@dataclass
class AddUserRequest(BaseXrayModel):
"""Request to add user to inbound"""
inboundTag: str
user: Dict[str, Any] # Protocol-specific user config
@dataclass
class RemoveUserRequest(BaseXrayModel):
"""Request to remove user from inbound"""
inboundTag: str
email: str
@dataclass
class UserStats(BaseXrayModel):
"""User statistics data"""
email: str
uplink: int = 0
downlink: int = 0
online: bool = False
ips: List[str] = None
def __post_init__(self):
if self.ips is None:
self.ips = []

View File

@@ -0,0 +1,31 @@
"""Exceptions for xray-api library"""
class XrayAPIError(Exception):
"""Base exception for all xray-api errors"""
pass
class XrayConnectionError(XrayAPIError):
"""Connection to Xray API server failed"""
pass
class XrayCommandError(XrayAPIError):
"""Xray command execution failed"""
pass
class XrayConfigError(XrayAPIError):
"""Invalid configuration"""
pass
class XrayNotFoundError(XrayAPIError):
"""Resource not found"""
pass
class XrayValidationError(XrayAPIError):
"""Validation error"""
pass

View File

@@ -0,0 +1,55 @@
"""Xray API models"""
from .base import (
BaseXrayModel, XrayConfig, XrayProtocol,
TransportProtocol, SecurityType
)
from .protocols import (
# VLESS
VLESSAccount, VLESSClient, VLESSInboundConfig,
# VMess
VMeSSAccount, VMeSSUser, VMeSSInboundConfig, VMeSSSecurityConfig,
# Trojan
TrojanAccount, TrojanUser, TrojanServerConfig, TrojanFallback,
# Shadowsocks
ShadowsocksAccount, ShadowsocksUser, ShadowsocksServerConfig,
# Utils
generate_uuid, validate_uuid, create_protocol_config
)
from .transports import (
StreamSettings, TCPSettings, WebSocketSettings, GRPCSettings,
HTTPSettings, XHTTPSettings, KCPSettings, QUICSettings, DomainSocketSettings,
create_tcp_stream, create_ws_stream, create_grpc_stream, create_http_stream, create_xhttp_stream
)
from .security import (
TLSConfig, REALITYConfig, XTLSConfig, Certificate,
create_tls_config, create_reality_config, create_reality_client_config,
generate_self_signed_certificate, create_tls_config_with_self_signed
)
from .inbound import (
InboundConfig, ReceiverConfig, SniffingConfig, InboundBuilder
)
__all__ = [
# Base
'BaseXrayModel', 'XrayConfig', 'XrayProtocol', 'TransportProtocol', 'SecurityType',
# Protocols
'VLESSAccount', 'VLESSClient', 'VLESSInboundConfig',
'VMeSSAccount', 'VMeSSUser', 'VMeSSInboundConfig', 'VMeSSSecurityConfig',
'TrojanAccount', 'TrojanUser', 'TrojanServerConfig', 'TrojanFallback',
'ShadowsocksAccount', 'ShadowsocksUser', 'ShadowsocksServerConfig',
'generate_uuid', 'validate_uuid', 'create_protocol_config',
# Transports
'StreamSettings', 'TCPSettings', 'WebSocketSettings', 'GRPCSettings',
'HTTPSettings', 'XHTTPSettings', 'KCPSettings', 'QUICSettings', 'DomainSocketSettings',
'create_tcp_stream', 'create_ws_stream', 'create_grpc_stream', 'create_http_stream', 'create_xhttp_stream',
# Security
'TLSConfig', 'REALITYConfig', 'XTLSConfig', 'Certificate',
'create_tls_config', 'create_reality_config', 'create_reality_client_config',
'generate_self_signed_certificate', 'create_tls_config_with_self_signed',
# Inbound
'InboundConfig', 'ReceiverConfig', 'SniffingConfig', 'InboundBuilder',
]

View File

@@ -0,0 +1,97 @@
"""Base models for xray-api library"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, asdict, field
from typing import Dict, Any, Optional, Type, TypeVar
import json
from enum import Enum
T = TypeVar('T', bound='BaseXrayModel')
class BaseXrayModel(ABC):
"""Base class for all Xray configuration models"""
def to_dict(self) -> Dict[str, Any]:
"""Convert model to dictionary for storage"""
if hasattr(self, '__dataclass_fields__'):
return self._clean_dict(asdict(self))
return self._clean_dict(self.__dict__.copy())
def to_xray_json(self) -> Dict[str, Any]:
"""Convert model to Xray API format"""
return self.to_dict()
@classmethod
def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
"""Create model instance from dictionary"""
return cls(**data)
def _clean_dict(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Remove None values and empty collections"""
cleaned = {}
for key, value in data.items():
if value is None:
continue
if isinstance(value, (list, dict)) and not value:
continue
if isinstance(value, BaseXrayModel):
cleaned[key] = value.to_dict()
elif isinstance(value, list):
cleaned[key] = [
item.to_dict() if isinstance(item, BaseXrayModel) else item
for item in value
]
elif isinstance(value, Enum):
cleaned[key] = value.value
else:
cleaned[key] = value
return cleaned
def to_json(self) -> str:
"""Convert to JSON string"""
return json.dumps(self.to_dict(), indent=2)
@dataclass
class XrayConfig(BaseXrayModel):
"""Base configuration class"""
_TypedMessage_: Optional[str] = field(default=None, init=False)
def __post_init__(self):
"""Set TypedMessage after initialization"""
if self._TypedMessage_ is None and hasattr(self, '__xray_type__'):
self._TypedMessage_ = self.__xray_type__
class XrayProtocol(str, Enum):
"""Supported Xray protocols"""
VLESS = "vless"
VMESS = "vmess"
TROJAN = "trojan"
SHADOWSOCKS = "shadowsocks"
DOKODEMO = "dokodemo-door"
FREEDOM = "freedom"
BLACKHOLE = "blackhole"
DNS = "dns"
HTTP = "http"
SOCKS = "socks"
class TransportProtocol(str, Enum):
"""Transport protocols"""
TCP = "tcp"
KCP = "kcp"
WS = "ws"
HTTP = "http"
XHTTP = "xhttp"
DOMAINSOCKET = "domainsocket"
QUIC = "quic"
GRPC = "grpc"
class SecurityType(str, Enum):
"""Security types"""
NONE = "none"
TLS = "tls"
REALITY = "reality"
XTLS = "xtls"

View File

@@ -0,0 +1,176 @@
"""Inbound configuration models"""
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List, Union
from .base import BaseXrayModel, XrayConfig, XrayProtocol
from .protocols import (
VLESSInboundConfig, VMeSSInboundConfig,
TrojanServerConfig, ShadowsocksServerConfig
)
from .transports import StreamSettings
@dataclass
class SniffingConfig(BaseXrayModel):
"""Traffic sniffing configuration"""
enabled: bool = True
destOverride: Optional[List[str]] = None
metadataOnly: bool = False
def __post_init__(self):
if self.destOverride is None:
self.destOverride = ["http", "tls"]
@dataclass
class ReceiverConfig(XrayConfig):
"""Receiver configuration for inbound"""
__xray_type__ = "xray.app.proxyman.ReceiverConfig"
listen: str = "0.0.0.0"
port: Optional[int] = None
portList: Optional[Union[int, str]] = None # Can be int or range like "10000-20000"
streamSettings: Optional[StreamSettings] = None
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray format"""
config = {"listen": self.listen}
# Either port or portList must be set
if self.port is not None:
config["port"] = self.port
elif self.portList is not None:
config["portList"] = self.portList
else:
raise ValueError("Either port or portList must be specified")
if self.streamSettings:
config["streamSettings"] = self.streamSettings.to_xray_json()
return config
@dataclass
class InboundConfig(BaseXrayModel):
"""Complete inbound configuration"""
tag: str
protocol: XrayProtocol
settings: Union[VLESSInboundConfig, VMeSSInboundConfig, TrojanServerConfig, ShadowsocksServerConfig]
listen: str = "0.0.0.0"
port: Optional[int] = None
portList: Optional[Union[int, str]] = None
streamSettings: Optional[StreamSettings] = None
sniffing: Optional[SniffingConfig] = None
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray API format with proper field order and structure"""
config = {
"listen": self.listen,
"tag": self.tag,
"protocol": self.protocol.value if hasattr(self.protocol, 'value') else str(self.protocol),
}
# Add port or portList (port comes before protocol in working format)
if self.port is not None:
config["port"] = self.port
elif self.portList is not None:
config["portList"] = self.portList
else:
raise ValueError("Either port or portList must be specified")
# Add protocol settings with _TypedMessage_
settings = self.settings.to_xray_json()
if "_TypedMessage_" not in settings:
# Add _TypedMessage_ based on protocol
protocol_type_map = {
"vless": "xray.proxy.vless.inbound.Config",
"vmess": "xray.proxy.vmess.inbound.Config",
"trojan": "xray.proxy.trojan.inbound.Config",
"shadowsocks": "xray.proxy.shadowsocks.inbound.Config"
}
protocol_name = self.protocol.value if hasattr(self.protocol, 'value') else str(self.protocol)
if protocol_name in protocol_type_map:
settings["_TypedMessage_"] = protocol_type_map[protocol_name]
config["settings"] = settings
# Add stream settings
if self.streamSettings:
config["streamSettings"] = self.streamSettings.to_xray_json()
else:
config["streamSettings"] = {"network": "tcp"} # Default TCP
# Add sniffing
if self.sniffing:
config["sniffing"] = self.sniffing.to_dict()
return config
# Builder for easier configuration
class InboundBuilder:
"""Builder for creating inbound configurations"""
def __init__(self, tag: str, protocol: XrayProtocol):
self.tag = tag
self.protocol = protocol
self._settings = None
self._listen = "0.0.0.0"
self._port = None
self._port_list = None
self._stream_settings = None
self._sniffing = None
def listen(self, address: str) -> 'InboundBuilder':
"""Set listen address"""
self._listen = address
return self
def port(self, port: int) -> 'InboundBuilder':
"""Set single port"""
self._port = port
self._port_list = None
return self
def port_range(self, start: int, end: int) -> 'InboundBuilder':
"""Set port range"""
self._port_list = f"{start}-{end}"
self._port = None
return self
def stream_settings(self, settings: StreamSettings) -> 'InboundBuilder':
"""Set stream settings"""
self._stream_settings = settings
return self
def sniffing(self, enabled: bool = True, dest_override: Optional[List[str]] = None) -> 'InboundBuilder':
"""Configure sniffing"""
self._sniffing = SniffingConfig(
enabled=enabled,
destOverride=dest_override
)
return self
def protocol_settings(self, settings: Union[VLESSInboundConfig, VMeSSInboundConfig, TrojanServerConfig, ShadowsocksServerConfig]) -> 'InboundBuilder':
"""Set protocol-specific settings"""
self._settings = settings
return self
def build(self) -> InboundConfig:
"""Build the inbound configuration"""
if not self._settings:
raise ValueError("Protocol settings must be specified")
if not self._port and not self._port_list:
raise ValueError("Either port or port range must be specified")
return InboundConfig(
tag=self.tag,
protocol=self.protocol,
settings=self._settings,
listen=self._listen,
port=self._port,
portList=self._port_list,
streamSettings=self._stream_settings,
sniffing=self._sniffing
)

View File

@@ -0,0 +1,266 @@
"""Protocol models for Xray"""
from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any
from uuid import uuid4
import re
from .base import BaseXrayModel, XrayConfig, XrayProtocol
def validate_uuid(uuid_str: str) -> bool:
"""Validate UUID format"""
pattern = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', re.I)
return bool(pattern.match(uuid_str))
def generate_uuid() -> str:
"""Generate new UUID"""
return str(uuid4())
# VLESS Protocol
@dataclass
class VLESSAccount(XrayConfig):
"""VLESS account configuration"""
__xray_type__ = "xray.proxy.vless.Account"
id: str
flow: Optional[str] = None
encryption: str = "none"
def __post_init__(self):
super().__post_init__()
if not validate_uuid(self.id):
raise ValueError(f"Invalid UUID: {self.id}")
@dataclass
class VLESSClient(BaseXrayModel):
"""VLESS client configuration"""
email: str
account: VLESSAccount
level: int = 0
@classmethod
def create(cls, email: str, uuid: Optional[str] = None, flow: Optional[str] = None) -> 'VLESSClient':
"""Create VLESS client with optional UUID generation"""
if uuid is None:
uuid = generate_uuid()
account = VLESSAccount(id=uuid, flow=flow)
return cls(email=email, account=account)
@dataclass
class VLESSInboundConfig(XrayConfig):
"""VLESS inbound configuration"""
__xray_type__ = "xray.proxy.vless.inbound.Config"
clients: List[VLESSClient] = field(default_factory=list)
decryption: str = "none"
fallbacks: Optional[List[Dict[str, Any]]] = None
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray API format with proper structure"""
config = {
"_TypedMessage_": self.__xray_type__,
"clients": [],
"decryption": self.decryption
}
# Convert clients to proper format
for client in self.clients:
client_data = {
"id": client.account.id,
"level": client.level,
"email": client.email
}
if client.account.flow:
client_data["flow"] = client.account.flow
config["clients"].append(client_data)
if self.fallbacks:
config["fallbacks"] = self.fallbacks
return config
# VMess Protocol
@dataclass
class VMeSSSecurityConfig(BaseXrayModel):
"""VMess security configuration"""
type: str = "AUTO" # AUTO, AES-128-GCM, CHACHA20-POLY1305, NONE
@dataclass
class VMeSSAccount(XrayConfig):
"""VMess account configuration"""
__xray_type__ = "xray.proxy.vmess.Account"
id: str
securitySettings: Optional[VMeSSSecurityConfig] = None
def __post_init__(self):
super().__post_init__()
if not validate_uuid(self.id):
raise ValueError(f"Invalid UUID: {self.id}")
if self.securitySettings is None:
self.securitySettings = VMeSSSecurityConfig()
@dataclass
class VMeSSUser(BaseXrayModel):
"""VMess user configuration"""
email: str
account: VMeSSAccount
level: int = 0
@classmethod
def create(cls, email: str, uuid: Optional[str] = None, security: str = "AUTO") -> 'VMeSSUser':
"""Create VMess user with optional UUID generation"""
if uuid is None:
uuid = generate_uuid()
account = VMeSSAccount(
id=uuid,
securitySettings=VMeSSSecurityConfig(type=security)
)
return cls(email=email, account=account)
@dataclass
class VMeSSInboundConfig(XrayConfig):
"""VMess inbound configuration"""
__xray_type__ = "xray.proxy.vmess.inbound.Config"
user: List[VMeSSUser] = field(default_factory=list)
disableInsecureEncryption: bool = False
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray API format with proper structure"""
config = {
"_TypedMessage_": self.__xray_type__,
"clients": []
}
# Convert users to proper format
for user in self.user:
client_data = {
"id": user.account.id,
"level": user.level,
"email": user.email,
"alterId": 0 # VMess specific
}
config["clients"].append(client_data)
return config
# Trojan Protocol
@dataclass
class TrojanAccount(XrayConfig):
"""Trojan account configuration"""
__xray_type__ = "xray.proxy.trojan.Account"
password: str
@classmethod
def generate_password(cls) -> str:
"""Generate secure password"""
return generate_uuid()
@dataclass
class TrojanUser(BaseXrayModel):
"""Trojan user configuration"""
email: str
account: TrojanAccount
level: int = 0
@classmethod
def create(cls, email: str, password: Optional[str] = None) -> 'TrojanUser':
"""Create Trojan user with optional password generation"""
if password is None:
password = TrojanAccount.generate_password()
account = TrojanAccount(password=password)
return cls(email=email, account=account)
@dataclass
class TrojanFallback(BaseXrayModel):
"""Trojan fallback configuration"""
dest: str
type: str = "tcp"
xver: int = 0
@dataclass
class TrojanServerConfig(XrayConfig):
"""Trojan server configuration"""
__xray_type__ = "xray.proxy.trojan.ServerConfig"
users: List[TrojanUser] = field(default_factory=list)
fallbacks: Optional[List[TrojanFallback]] = None
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray API format with proper structure"""
config = {
"_TypedMessage_": self.__xray_type__,
"clients": []
}
# Convert users to proper format
for user in self.users:
client_data = {
"password": user.account.password,
"level": user.level,
"email": user.email
}
config["clients"].append(client_data)
if self.fallbacks:
config["fallbacks"] = [fb.to_dict() for fb in self.fallbacks]
return config
# Shadowsocks Protocol
@dataclass
class ShadowsocksAccount(XrayConfig):
"""Shadowsocks account configuration"""
__xray_type__ = "xray.proxy.shadowsocks.Account"
method: str # aes-256-gcm, aes-128-gcm, chacha20-poly1305, etc.
password: str
@dataclass
class ShadowsocksUser(BaseXrayModel):
"""Shadowsocks user configuration"""
email: str
account: ShadowsocksAccount
level: int = 0
@dataclass
class ShadowsocksServerConfig(XrayConfig):
"""Shadowsocks server configuration"""
__xray_type__ = "xray.proxy.shadowsocks.ServerConfig"
users: List[ShadowsocksUser] = field(default_factory=list)
network: str = "tcp,udp"
# Protocol config factory
def create_protocol_config(protocol: XrayProtocol, **kwargs) -> XrayConfig:
"""Factory to create protocol configurations"""
protocol_map = {
XrayProtocol.VLESS: VLESSInboundConfig,
XrayProtocol.VMESS: VMeSSInboundConfig,
XrayProtocol.TROJAN: TrojanServerConfig,
XrayProtocol.SHADOWSOCKS: ShadowsocksServerConfig,
}
config_class = protocol_map.get(protocol)
if not config_class:
raise ValueError(f"Unsupported protocol: {protocol}")
return config_class(**kwargs)

View File

@@ -0,0 +1,389 @@
"""Security configuration models for Xray"""
from dataclasses import dataclass, field
from typing import Optional, List, Dict, Any, Tuple
from pathlib import Path
import secrets
from datetime import datetime, timedelta
from .base import BaseXrayModel, XrayConfig, SecurityType
# TLS Configuration
@dataclass
class Certificate(BaseXrayModel):
"""TLS certificate configuration"""
certificateFile: Optional[str] = None
keyFile: Optional[str] = None
certificate: Optional[List[str]] = None # PEM format lines
key: Optional[List[str]] = None # PEM format lines
usage: str = "encipherment"
ocspStapling: int = 3600
oneTimeLoading: bool = False
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray format"""
config = {}
if self.certificateFile and self.keyFile:
config["certificateFile"] = self.certificateFile
config["keyFile"] = self.keyFile
elif self.certificate and self.key:
config["certificate"] = self.certificate
config["key"] = self.key
config["usage"] = self.usage
if self.ocspStapling:
config["ocspStapling"] = self.ocspStapling
if self.oneTimeLoading:
config["OneTimeLoading"] = self.oneTimeLoading
return config
@dataclass
class TLSConfig(XrayConfig):
"""TLS configuration"""
__xray_type__ = "xray.transport.internet.tls.Config"
serverName: Optional[str] = None
allowInsecure: bool = False
alpn: Optional[List[str]] = None
minVersion: str = "1.2"
maxVersion: str = "1.3"
preferServerCipherSuites: bool = True
cipherSuites: Optional[str] = None
certificates: Optional[List[Certificate]] = None
disableSystemRoot: bool = False
enableSessionResumption: bool = False
fingerprint: Optional[str] = None # Client-side
pinnedPeerCertificateChainSha256: Optional[List[str]] = None
rejectUnknownSni: bool = False # Server-side
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray format with proper field handling"""
config = super().to_xray_json()
# Handle certificates properly
if self.certificates:
config["certificates"] = [cert.to_xray_json() for cert in self.certificates]
return config
# REALITY Configuration
@dataclass
class REALITYConfig(XrayConfig):
"""REALITY configuration"""
__xray_type__ = "xray.transport.internet.reality.Config"
# Server-side
show: bool = False
dest: Optional[str] = None # e.g., "example.com:443"
xver: int = 0
serverNames: Optional[List[str]] = None
privateKey: Optional[str] = None
shortIds: Optional[List[str]] = None
# Client-side
serverName: Optional[str] = None
fingerprint: str = "chrome"
publicKey: Optional[str] = None
shortId: Optional[str] = None
spiderX: Optional[str] = None
@classmethod
def generate_keys(cls) -> Dict[str, str]:
"""Generate REALITY key pair using xray x25519"""
import subprocess
try:
# Use xray x25519 to generate proper keys
result = subprocess.run(['xray', 'x25519'], capture_output=True, text=True, check=True)
lines = result.stdout.strip().split('\n')
private_key = ""
public_key = ""
for line in lines:
if line.startswith('Private key:'):
private_key = line.split(': ')[1].strip()
elif line.startswith('Public key:'):
public_key = line.split(': ')[1].strip()
return {
"privateKey": private_key,
"publicKey": public_key
}
except (subprocess.CalledProcessError, FileNotFoundError, IndexError):
# Fallback to base64 encoded random bytes (32 bytes for X25519)
import base64
private_bytes = secrets.token_bytes(32)
public_bytes = secrets.token_bytes(32)
return {
"privateKey": base64.b64encode(private_bytes).decode().rstrip('='),
"publicKey": base64.b64encode(public_bytes).decode().rstrip('=')
}
@classmethod
def generate_short_id(cls) -> str:
"""Generate random short ID (1-16 hex chars)"""
# Generate 1-8 bytes (2-16 hex chars)
length = secrets.randbelow(8) + 1
return secrets.token_hex(length)
# XTLS Configuration (deprecated but still supported)
@dataclass
class XTLSConfig(XrayConfig):
"""XTLS configuration (legacy)"""
__xray_type__ = "xray.transport.internet.xtls.Config"
serverName: Optional[str] = None
allowInsecure: bool = False
alpn: Optional[List[str]] = None
minVersion: str = "1.2"
maxVersion: str = "1.3"
preferServerCipherSuites: bool = True
cipherSuites: Optional[str] = None
certificates: Optional[List[Certificate]] = None
disableSystemRoot: bool = False
fingerprint: Optional[str] = None
rejectUnknownSni: bool = False
# Security factory
def create_tls_config(
server_name: Optional[str] = None,
cert_file: Optional[str] = None,
key_file: Optional[str] = None,
alpn: Optional[List[str]] = None,
fingerprint: Optional[str] = None,
**kwargs
) -> TLSConfig:
"""Create TLS configuration"""
config = TLSConfig(
serverName=server_name,
alpn=alpn or ["h2", "http/1.1"],
fingerprint=fingerprint,
**kwargs
)
if cert_file and key_file:
config.certificates = [Certificate(
certificateFile=cert_file,
keyFile=key_file
)]
return config
def create_reality_config(
dest: str,
server_names: Optional[List[str]] = None,
private_key: Optional[str] = None,
short_ids: Optional[List[str]] = None,
**kwargs
) -> REALITYConfig:
"""Create REALITY configuration for server"""
if not private_key:
keys = REALITYConfig.generate_keys()
private_key = keys["privateKey"]
if not short_ids:
short_ids = [REALITYConfig.generate_short_id()]
return REALITYConfig(
show=False,
dest=dest,
serverNames=server_names or [dest.split(":")[0]],
privateKey=private_key,
shortIds=short_ids,
**kwargs
)
def create_reality_client_config(
server_name: str,
public_key: str,
short_id: str,
fingerprint: str = "chrome",
**kwargs
) -> REALITYConfig:
"""Create REALITY configuration for client"""
return REALITYConfig(
serverName=server_name,
publicKey=public_key,
shortId=short_id,
fingerprint=fingerprint,
**kwargs
)
# Certificate generation utilities
def generate_self_signed_certificate(
common_name: str = "localhost",
san_list: Optional[List[str]] = None,
days: int = 365,
key_size: int = 2048,
save_to_files: bool = False,
cert_path: Optional[str] = None,
key_path: Optional[str] = None
) -> Tuple[str, str]:
"""
Generate self-signed certificate
Args:
common_name: Certificate common name
san_list: Subject Alternative Names (domains/IPs)
days: Certificate validity in days
key_size: RSA key size
save_to_files: Whether to save to files
cert_path: Path to save certificate
key_path: Path to save private key
Returns:
Tuple of (certificate_pem, private_key_pem)
"""
try:
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtensionOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
except ImportError:
raise ImportError("cryptography package is required for certificate generation. Install with: pip install cryptography")
# Generate private key
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=key_size,
backend=default_backend()
)
# Create certificate subject
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "State"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "City"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Xray Self-Signed"),
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
])
# Create certificate builder
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(issuer)
builder = builder.public_key(private_key.public_key())
builder = builder.serial_number(x509.random_serial_number())
builder = builder.not_valid_before(datetime.utcnow())
builder = builder.not_valid_after(datetime.utcnow() + timedelta(days=days))
# Add Subject Alternative Names
san_list = san_list or [common_name]
alt_names = []
for san in san_list:
if san.replace('.', '').isdigit(): # IP address
alt_names.append(x509.IPAddress(ipaddress.ip_address(san)))
else: # Domain name
alt_names.append(x509.DNSName(san))
builder = builder.add_extension(
x509.SubjectAlternativeName(alt_names),
critical=False,
)
# Add basic constraints
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=0),
critical=True,
)
# Add key usage
builder = builder.add_extension(
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=True,
data_encipherment=False,
key_agreement=False,
key_cert_sign=True,
crl_sign=False,
encipher_only=False,
decipher_only=False,
),
critical=True,
)
# Sign certificate
certificate = builder.sign(private_key, hashes.SHA256(), default_backend())
# Serialize to PEM
cert_pem = certificate.public_bytes(serialization.Encoding.PEM).decode('utf-8')
key_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
).decode('utf-8')
# Save to files if requested
if save_to_files:
cert_file = cert_path or f"{common_name}_cert.pem"
key_file = key_path or f"{common_name}_key.pem"
with open(cert_file, 'w') as f:
f.write(cert_pem)
with open(key_file, 'w') as f:
f.write(key_pem)
# Set appropriate permissions
Path(key_file).chmod(0o600)
return cert_pem, key_pem
def create_tls_config_with_self_signed(
common_name: str = "localhost",
san_list: Optional[List[str]] = None,
alpn: Optional[List[str]] = None,
**tls_kwargs
) -> Tuple[TLSConfig, str, str]:
"""
Create TLS configuration with self-signed certificate
Returns:
Tuple of (TLSConfig, certificate_pem, private_key_pem)
"""
cert_pem, key_pem = generate_self_signed_certificate(
common_name=common_name,
san_list=san_list
)
# Convert PEM to lines for Xray format
cert_lines = cert_pem.strip().split('\n')
key_lines = key_pem.strip().split('\n')
# Create certificate config
certificate = Certificate(
certificate=cert_lines,
key=key_lines,
oneTimeLoading=True
)
# Create TLS config
tls_config = TLSConfig(
serverName=common_name,
alpn=alpn or ["h2", "http/1.1"],
certificates=[certificate],
**tls_kwargs
)
return tls_config, cert_pem, key_pem
# Add missing import
try:
import ipaddress
except ImportError:
ipaddress = None

View File

@@ -0,0 +1,241 @@
"""Transport configuration models for Xray"""
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List
from .base import BaseXrayModel, XrayConfig, TransportProtocol
# TCP Transport
@dataclass
class TCPSettings(XrayConfig):
"""TCP transport settings"""
__xray_type__ = "xray.transport.internet.tcp.Config"
acceptProxyProtocol: bool = False
header: Optional[Dict[str, Any]] = None
# KCP Transport
@dataclass
class KCPSettings(XrayConfig):
"""KCP transport settings"""
__xray_type__ = "xray.transport.internet.kcp.Config"
mtu: int = 1350
tti: int = 50
uplinkCapacity: int = 5
downlinkCapacity: int = 20
congestion: bool = False
readBufferSize: int = 2
writeBufferSize: int = 2
header: Optional[Dict[str, Any]] = None
# WebSocket Transport
@dataclass
class WebSocketSettings(XrayConfig):
"""WebSocket transport settings"""
__xray_type__ = "xray.transport.internet.websocket.Config"
path: str = "/"
headers: Optional[Dict[str, str]] = None
acceptProxyProtocol: bool = False
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray format"""
config = super().to_xray_json()
# Ensure headers is a dict even if empty
if self.headers:
config["headers"] = self.headers
return config
# HTTP/2 Transport
@dataclass
class HTTPSettings(XrayConfig):
"""HTTP/2 transport settings"""
__xray_type__ = "xray.transport.internet.http.Config"
path: str = "/"
host: Optional[List[str]] = None
method: str = "PUT"
headers: Optional[Dict[str, List[str]]] = None
# XHTTP Transport (New)
@dataclass
class XHTTPSettings(XrayConfig):
"""XHTTP transport settings"""
__xray_type__ = "xray.transport.internet.xhttp.Config"
path: str = "/"
host: Optional[str] = None
method: str = "GET"
headers: Optional[Dict[str, Any]] = None
mode: str = "auto"
# Domain Socket Transport
@dataclass
class DomainSocketSettings(XrayConfig):
"""Domain socket transport settings"""
__xray_type__ = "xray.transport.internet.domainsocket.Config"
path: str
abstract: bool = False
padding: bool = False
# QUIC Transport
@dataclass
class QUICSettings(XrayConfig):
"""QUIC transport settings"""
__xray_type__ = "xray.transport.internet.quic.Config"
security: str = "none"
key: str = ""
header: Optional[Dict[str, Any]] = None
# gRPC Transport
@dataclass
class GRPCSettings(XrayConfig):
"""gRPC transport settings"""
__xray_type__ = "xray.transport.internet.grpc.encoding.Config"
serviceName: str = ""
multiMode: bool = False
idle_timeout: int = 60
health_check_timeout: int = 20
permit_without_stream: bool = False
initial_windows_size: int = 0
# Stream Settings
@dataclass
class StreamSettings(BaseXrayModel):
"""Stream settings for inbound/outbound"""
network: TransportProtocol = TransportProtocol.TCP
security: Optional[str] = None
tlsSettings: Optional[Any] = None
xtlsSettings: Optional[Any] = None
realitySettings: Optional[Any] = None
tcpSettings: Optional[TCPSettings] = None
kcpSettings: Optional[KCPSettings] = None
wsSettings: Optional[WebSocketSettings] = None
httpSettings: Optional[HTTPSettings] = None
xhttpSettings: Optional[XHTTPSettings] = None
dsSettings: Optional[DomainSocketSettings] = None
quicSettings: Optional[QUICSettings] = None
grpcSettings: Optional[GRPCSettings] = None
sockopt: Optional[Dict[str, Any]] = None
def to_xray_json(self) -> Dict[str, Any]:
"""Convert to Xray format with correct field names"""
config = {
"network": self.network.value if isinstance(self.network, TransportProtocol) else self.network
}
if self.security:
config["security"] = self.security
# Map transport settings
transport_map = {
TransportProtocol.TCP: ("tcpSettings", self.tcpSettings),
TransportProtocol.KCP: ("kcpSettings", self.kcpSettings),
TransportProtocol.WS: ("wsSettings", self.wsSettings),
TransportProtocol.HTTP: ("httpSettings", self.httpSettings),
TransportProtocol.XHTTP: ("xhttpSettings", self.xhttpSettings),
TransportProtocol.DOMAINSOCKET: ("dsSettings", self.dsSettings),
TransportProtocol.QUIC: ("quicSettings", self.quicSettings),
TransportProtocol.GRPC: ("grpcSettings", self.grpcSettings),
}
network = self.network if isinstance(self.network, TransportProtocol) else TransportProtocol(self.network)
field_name, settings = transport_map.get(network, (None, None))
if field_name and settings:
config[field_name] = settings.to_xray_json() if hasattr(settings, 'to_xray_json') else settings
# Add security settings
if self.tlsSettings:
config["tlsSettings"] = self.tlsSettings if isinstance(self.tlsSettings, dict) else self.tlsSettings.to_xray_json()
if self.xtlsSettings:
config["xtlsSettings"] = self.xtlsSettings if isinstance(self.xtlsSettings, dict) else self.xtlsSettings.to_xray_json()
if self.realitySettings:
config["realitySettings"] = self.realitySettings if isinstance(self.realitySettings, dict) else self.realitySettings.to_xray_json()
if self.sockopt:
config["sockopt"] = self.sockopt
return config
# Factory functions
def create_tcp_stream(
security: Optional[str] = None,
**kwargs
) -> StreamSettings:
"""Create TCP stream settings"""
return StreamSettings(
network=TransportProtocol.TCP,
security=security,
tcpSettings=TCPSettings(**kwargs) if kwargs else None
)
def create_ws_stream(
path: str = "/",
headers: Optional[Dict[str, str]] = None,
security: Optional[str] = None,
**kwargs
) -> StreamSettings:
"""Create WebSocket stream settings"""
return StreamSettings(
network=TransportProtocol.WS,
security=security,
wsSettings=WebSocketSettings(path=path, headers=headers, **kwargs)
)
def create_grpc_stream(
service_name: str = "",
security: Optional[str] = None,
**kwargs
) -> StreamSettings:
"""Create gRPC stream settings"""
return StreamSettings(
network=TransportProtocol.GRPC,
security=security,
grpcSettings=GRPCSettings(serviceName=service_name, **kwargs)
)
def create_http_stream(
path: str = "/",
host: Optional[List[str]] = None,
security: Optional[str] = None,
**kwargs
) -> StreamSettings:
"""Create HTTP/2 stream settings"""
return StreamSettings(
network=TransportProtocol.HTTP,
security=security,
httpSettings=HTTPSettings(path=path, host=host, **kwargs)
)
def create_xhttp_stream(
path: str = "/",
host: Optional[str] = None,
security: Optional[str] = None,
mode: str = "auto",
**kwargs
) -> StreamSettings:
"""Create XHTTP stream settings"""
return StreamSettings(
network=TransportProtocol.XHTTP,
security=security,
xhttpSettings=XHTTPSettings(path=path, host=host, mode=mode, **kwargs)
)

View File

184
vpn/xray_api_v2/stats.py Normal file
View File

@@ -0,0 +1,184 @@
"""Statistics functionality for Xray API"""
from dataclasses import dataclass
from typing import List, Dict, Optional, Tuple, Any
from datetime import datetime
from .client import XrayClient
from .models.base import BaseXrayModel
@dataclass
class StatItem(BaseXrayModel):
"""Single statistics item"""
name: str
value: int
@property
def parts(self) -> List[str]:
"""Split stat name into parts"""
return self.name.split(">>>")
@property
def stat_type(self) -> str:
"""Get stat type (inbound/outbound/user)"""
return self.parts[0] if self.parts else ""
@property
def tag(self) -> str:
"""Get inbound/outbound tag"""
return self.parts[1] if len(self.parts) > 1 else ""
@property
def metric(self) -> str:
"""Get metric name (traffic/uplink/downlink)"""
return self.parts[-1] if self.parts else ""
@dataclass
class SystemStats(BaseXrayModel):
"""System statistics"""
numGoroutine: int = 0
numGC: int = 0
alloc: int = 0
totalAlloc: int = 0
sys: int = 0
mallocs: int = 0
frees: int = 0
liveObjects: int = 0
pauseTotalNs: int = 0
uptime: int = 0
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'SystemStats':
"""Create from API response with proper field mapping"""
# Map API response fields to model fields
field_mapping = {
'NumGoroutine': 'numGoroutine',
'NumGC': 'numGC',
'Alloc': 'alloc',
'TotalAlloc': 'totalAlloc',
'Sys': 'sys',
'Mallocs': 'mallocs',
'Frees': 'frees',
'LiveObjects': 'liveObjects',
'PauseTotalNs': 'pauseTotalNs',
'Uptime': 'uptime'
}
normalized = {}
for api_key, model_key in field_mapping.items():
if api_key in data:
normalized[model_key] = data[api_key]
return cls(**normalized)
@property
def uptime_seconds(self) -> int:
"""Get uptime in seconds"""
return self.uptime
@property
def memory_mb(self) -> float:
"""Get allocated memory in MB"""
return self.alloc / 1024 / 1024
class StatsManager:
"""Manager for Xray statistics"""
def __init__(self, client: XrayClient):
self.client = client
def get_all_stats(self, reset: bool = False) -> List[StatItem]:
"""Get all statistics"""
stats = self.client.get_stats("", reset=reset)
result = []
for stat in stats:
if isinstance(stat, dict) and 'name' in stat and 'value' in stat:
result.append(StatItem(name=stat['name'], value=stat['value']))
return result
def get_inbound_stats(self, tag: str, reset: bool = False) -> Dict[str, int]:
"""Get statistics for specific inbound"""
pattern = f"inbound>>>{tag}>>>traffic>>>"
stats = self.client.get_stats(pattern, reset=reset)
result = {"uplink": 0, "downlink": 0}
for stat in stats:
item = StatItem(**stat)
if item.metric in result:
result[item.metric] = item.value
return result
def get_outbound_stats(self, tag: str, reset: bool = False) -> Dict[str, int]:
"""Get statistics for specific outbound"""
pattern = f"outbound>>>{tag}>>>traffic>>>"
stats = self.client.get_stats(pattern, reset=reset)
result = {"uplink": 0, "downlink": 0}
for stat in stats:
item = StatItem(**stat)
if item.metric in result:
result[item.metric] = item.value
return result
def get_user_stats(self, email: str, reset: bool = False) -> Dict[str, int]:
"""Get statistics for specific user"""
pattern = f"user>>>{email}>>>traffic>>>"
stats = self.client.get_stats(pattern, reset=reset)
result = {"uplink": 0, "downlink": 0}
for stat in stats:
item = StatItem(**stat)
if item.metric in result:
result[item.metric] = item.value
return result
def get_user_online_info(self, email: str) -> Dict[str, Any]:
"""Get user online information"""
online = self.client.get_online_stats(email)
ips_data = self.client.get_online_ips(email)
return {
"email": email,
"online": online.get("count", 0) > 0,
"sessions": online.get("count", 0),
"ips": ips_data
}
def get_system_stats(self) -> SystemStats:
"""Get system statistics"""
stats = self.client.get_system_stats()
return SystemStats.from_dict(stats)
def get_traffic_summary(self) -> Dict[str, Dict[str, int]]:
"""Get traffic summary for all inbounds/outbounds"""
all_stats = self.get_all_stats()
summary = {
"inbounds": {},
"outbounds": {},
"users": {}
}
for stat in all_stats:
if stat.stat_type == "inbound":
if stat.tag not in summary["inbounds"]:
summary["inbounds"][stat.tag] = {"uplink": 0, "downlink": 0}
summary["inbounds"][stat.tag][stat.metric] = stat.value
elif stat.stat_type == "outbound":
if stat.tag not in summary["outbounds"]:
summary["outbounds"][stat.tag] = {"uplink": 0, "downlink": 0}
summary["outbounds"][stat.tag][stat.metric] = stat.value
elif stat.stat_type == "user":
user_email = stat.parts[1] if len(stat.parts) > 1 else ""
if user_email and user_email not in summary["users"]:
summary["users"][user_email] = {"uplink": 0, "downlink": 0}
if user_email:
summary["users"][user_email][stat.metric] = stat.value
return summary

View File

@@ -0,0 +1,392 @@
"""Subscription link generation for Xray protocols"""
import json
import base64
import urllib.parse
from typing import Dict, Any, Optional, List, Union
from .models.base import BaseXrayModel
from .models.inbound import InboundConfig
from .models.protocols import VLESSInboundConfig, VMeSSInboundConfig, TrojanServerConfig
class SubscriptionLinkGenerator:
"""Generate subscription links for various Xray protocols"""
@staticmethod
def _url_encode(value: str) -> str:
"""URL encode string with safe characters"""
return urllib.parse.quote(value, safe='')
@staticmethod
def _format_ipv6(address: str) -> str:
"""Format IPv6 address with brackets if needed"""
if ':' in address and not address.startswith('['):
return f'[{address}]'
return address
@classmethod
def generate_vless_link(cls,
uuid: str,
address: str,
port: int,
remark: str = "VLESS",
encryption: str = "none",
security: str = "none",
sni: Optional[str] = None,
transport_type: str = "tcp",
path: Optional[str] = None,
host: Optional[str] = None,
alpn: Optional[str] = None,
fp: str = "chrome",
flow: Optional[str] = None,
allow_insecure: bool = False,
# REALITY parameters
pbk: Optional[str] = None,
sid: Optional[str] = None,
spx: Optional[str] = None,
**kwargs) -> str:
"""
Generate VLESS subscription link
Args:
uuid: Client UUID
address: Server address
port: Server port
remark: Link description/name
encryption: Encryption method (default: none)
security: Security layer (none, tls, reality)
sni: SNI for TLS
transport_type: Transport type (tcp, ws, grpc, xhttp, h2, kcp)
path: Path for WebSocket/HTTP2/gRPC
host: Host header for WebSocket
alpn: ALPN negotiation
fp: Fingerprint
flow: Flow control (xtls-rprx-vision, etc.)
pbk: REALITY public key
sid: REALITY short ID
spx: REALITY spider X
**kwargs: Additional parameters
Returns:
VLESS subscription link
"""
# Build query parameters
params = {
'encryption': encryption,
'security': security,
'type': transport_type,
'fp': fp
}
# Add optional parameters
if sni:
params['sni'] = sni
if path:
params['path'] = path # Don't double encode - urlencode will handle it
if host:
params['host'] = host
if alpn:
params['alpn'] = alpn
if flow:
params['flow'] = flow
if allow_insecure:
params['allowInsecure'] = '1'
# Add REALITY parameters
if pbk:
params['pbk'] = pbk
if sid:
params['sid'] = sid
if spx:
params['spx'] = spx
# Add any additional parameters
params.update(kwargs)
# Remove None values
params = {k: v for k, v in params.items() if v is not None}
# Build query string (keep - and _ safe for REALITY keys)
query_string = urllib.parse.urlencode(params, safe='-_')
# Format address (handle IPv6)
formatted_address = cls._format_ipv6(address)
# Build VLESS URL
url = f"vless://{uuid}@{formatted_address}:{port}?{query_string}#{cls._url_encode(remark)}"
return url
@classmethod
def generate_vmess_link(cls,
uuid: str,
address: str,
port: int,
remark: str = "VMess",
alterId: int = 0,
security: str = "auto",
network: str = "tcp",
transport_type: Optional[str] = None,
path: Optional[str] = None,
host: Optional[str] = None,
tls: str = "",
sni: Optional[str] = None,
alpn: Optional[str] = None,
fp: Optional[str] = None,
allow_insecure: bool = False,
**kwargs) -> str:
"""
Generate VMess subscription link
Args:
uuid: Client UUID
address: Server address
port: Server port
remark: Link description/name
alterId: Alter ID (default: 0)
security: Security method (auto, aes-128-gcm, chacha20-poly1305, none)
network: Network type (tcp, kcp, ws, h2, quic, grpc)
transport_type: Transport type (same as network for compatibility)
path: Path for WebSocket/HTTP2/gRPC
host: Host header
tls: TLS settings ("" for none, "tls" for TLS)
sni: SNI for TLS
alpn: ALPN negotiation
fp: Fingerprint
**kwargs: Additional parameters
Returns:
VMess subscription link
"""
# Use transport_type if provided, otherwise use network
net = transport_type or network
# Build VMess configuration object
vmess_config = {
'v': '2',
'ps': remark,
'add': address,
'port': str(port),
'id': uuid,
'aid': str(alterId),
'scy': security,
'net': net,
'type': kwargs.get('type', 'none'),
'host': host or '',
'path': path or '',
'tls': tls,
'sni': sni or '',
'alpn': alpn or '',
'fp': fp or ''
}
# Add allowInsecure only if True (VMess format - try multiple approaches)
if allow_insecure:
vmess_config['allowInsecure'] = 1
vmess_config['skip-cert-verify'] = True # For compatibility with some clients
# Add any additional parameters
for key, value in kwargs.items():
if key not in vmess_config and value is not None:
vmess_config[key] = str(value)
# Convert to JSON and base64 encode
json_str = json.dumps(vmess_config, separators=(',', ':'))
encoded = base64.b64encode(json_str.encode('utf-8')).decode('ascii')
return f"vmess://{encoded}"
@classmethod
def generate_trojan_link(cls,
password: str,
address: str,
port: int,
remark: str = "Trojan",
security: str = "tls",
sni: Optional[str] = None,
transport_type: str = "tcp",
path: Optional[str] = None,
host: Optional[str] = None,
alpn: Optional[str] = None,
fp: str = "chrome",
allow_insecure: bool = False,
**kwargs) -> str:
"""
Generate Trojan subscription link
Args:
password: Trojan password
address: Server address
port: Server port
remark: Link description/name
security: Security layer (tls, none)
sni: SNI for TLS
transport_type: Transport type (tcp, ws, grpc)
path: Path for WebSocket/gRPC
host: Host header
alpn: ALPN negotiation
fp: Fingerprint
**kwargs: Additional parameters
Returns:
Trojan subscription link
"""
# Build query parameters
params = {
'security': security,
'type': transport_type,
'fp': fp
}
# Add optional parameters
if sni:
params['sni'] = sni
if path:
params['path'] = path # Don't double encode - urlencode will handle it
if host:
params['host'] = host
if alpn:
params['alpn'] = alpn
if allow_insecure:
params['allowInsecure'] = '1'
# Add any additional parameters
params.update(kwargs)
# Remove None values
params = {k: v for k, v in params.items() if v is not None}
# Build query string
query_string = urllib.parse.urlencode(params)
# Format address (handle IPv6)
formatted_address = cls._format_ipv6(address)
# URL encode password
encoded_password = cls._url_encode(password)
# Build Trojan URL
url = f"trojan://{encoded_password}@{formatted_address}:{port}?{query_string}#{cls._url_encode(remark)}"
return url
@classmethod
def generate_shadowsocks_link(cls,
method: str,
password: str,
address: str,
port: int,
remark: str = "Shadowsocks") -> str:
"""
Generate Shadowsocks subscription link
Args:
method: Encryption method
password: SS password
address: Server address
port: Server port
remark: Link description/name
Returns:
Shadowsocks subscription link
"""
# Format: ss://base64(method:password)@server:port#remark
user_info = f"{method}:{password}"
encoded_user_info = base64.b64encode(user_info.encode('utf-8')).decode('ascii')
# Format address (handle IPv6)
formatted_address = cls._format_ipv6(address)
return f"ss://{encoded_user_info}@{formatted_address}:{port}#{cls._url_encode(remark)}"
@classmethod
def generate_subscription_from_inbound(cls,
inbound: InboundConfig,
server_address: str,
remark_prefix: str = "",
**transport_params) -> List[str]:
"""
Generate subscription links from inbound configuration
Args:
inbound: Inbound configuration
server_address: Public server address
remark_prefix: Prefix for link remarks
**transport_params: Additional transport parameters
Returns:
List of subscription links for all users
"""
links = []
protocol = inbound.protocol.lower()
port = inbound.port
tag = inbound.tag
# Extract protocol-specific settings
if isinstance(inbound.settings, VLESSInboundConfig):
for client in inbound.settings.clients:
remark = f"{remark_prefix}{tag}_{client.email}" if remark_prefix else f"{tag}_{client.email}"
link = cls.generate_vless_link(
uuid=client.account.id,
address=server_address,
port=port,
remark=remark,
**transport_params
)
links.append(link)
elif isinstance(inbound.settings, VMeSSInboundConfig):
for client in inbound.settings.user:
remark = f"{remark_prefix}{tag}_{client.email}" if remark_prefix else f"{tag}_{client.email}"
link = cls.generate_vmess_link(
uuid=client.account.id,
address=server_address,
port=port,
remark=remark,
**transport_params
)
links.append(link)
elif isinstance(inbound.settings, TrojanServerConfig):
for client in inbound.settings.users:
remark = f"{remark_prefix}{tag}_{client.email}" if remark_prefix else f"{tag}_{client.email}"
link = cls.generate_trojan_link(
password=client.password,
address=server_address,
port=port,
remark=remark,
**transport_params
)
links.append(link)
return links
@classmethod
def create_subscription_content(cls, links: List[str]) -> str:
"""
Create base64 encoded subscription content
Args:
links: List of subscription links
Returns:
Base64 encoded subscription content
"""
content = '\n'.join(links)
return base64.b64encode(content.encode('utf-8')).decode('ascii')
@classmethod
def parse_subscription_content(cls, content: str) -> List[str]:
"""
Parse base64 encoded subscription content
Args:
content: Base64 encoded subscription content
Returns:
List of subscription links
"""
try:
decoded = base64.b64decode(content).decode('utf-8')
return [link.strip() for link in decoded.split('\n') if link.strip()]
except Exception as e:
raise ValueError(f"Invalid subscription content: {e}")