Files
Marzban/app/models/proxy.py
Mohammad ea6a3d2eb6 Migrate to Pydantic V2 (#1495)
* chore: update pydantic to version 2.10.2 and refactor model validators

* refactor: simplify field validators by removing unnecessary pre and always flags

* remove typing_extensions==4.9.0 from requirements.txt

* refactor: remove allow_reuse flag from status field validator

* refactor: simplify field validators by removing pre and always flags

* refactor: update user model imports and enhance account class with abstract method

* refactor: update model_config to use dictionary format in Admin and SubscriptionUserResponse classes

* fix typo in UserDataResetByNext

* change pre=True to mode="before"

* refactor: update validation methods and model configuration in User and Proxy classes

* change pre=False with mode="after"

* Migrated to Pydantic V2

* fix: custom subscriptions not workong

* some small changes

* add missing properties to example schema

* replace from_orm with model_validate

---------

Co-authored-by: MahdiButcher <madibutchercoding@gmail.com>
Co-authored-by: Mahdi Butcher <MahdiButcherCoding@gmail.com>
2024-12-09 21:56:24 +03:30

207 lines
5.5 KiB
Python

import json
import re
from enum import Enum
from typing import Optional, Union
from uuid import UUID, uuid4
from pydantic import field_validator, ConfigDict, BaseModel, Field
from app.utils.system import random_password
from xray_api.types.account import (
ShadowsocksAccount,
ShadowsocksMethods,
TrojanAccount,
VLESSAccount,
VMessAccount,
XTLSFlows
)
FRAGMENT_PATTERN = re.compile(r'^((\d{1,4}-\d{1,4})|(\d{1,4})),((\d{1,3}-\d{1,3})|(\d{1,3})),(tlshello|\d|\d\-\d)$')
NOISE_PATTERN = re.compile(
r'^(rand:(\d{1,4}-\d{1,4}|\d{1,4})|str:.+|base64:.+)(,(\d{1,4}-\d{1,4}|\d{1,4}))?(&(rand:(\d{1,4}-\d{1,4}|\d{1,4})|str:.+|base64:.+)(,(\d{1,4}-\d{1,4}|\d{1,4}))?)*$')
class ProxyTypes(str, Enum):
# proxy_type = protocol
VMess = "vmess"
VLESS = "vless"
Trojan = "trojan"
Shadowsocks = "shadowsocks"
@property
def account_model(self):
if self == self.VMess:
return VMessAccount
if self == self.VLESS:
return VLESSAccount
if self == self.Trojan:
return TrojanAccount
if self == self.Shadowsocks:
return ShadowsocksAccount
@property
def settings_model(self):
if self == self.VMess:
return VMessSettings
if self == self.VLESS:
return VLESSSettings
if self == self.Trojan:
return TrojanSettings
if self == self.Shadowsocks:
return ShadowsocksSettings
class ProxySettings(BaseModel, use_enum_values=True):
@classmethod
def from_dict(cls, proxy_type: ProxyTypes, _dict: dict):
return ProxyTypes(proxy_type).settings_model.model_validate(_dict)
def dict(self, *, no_obj=False, **kwargs):
if no_obj:
return json.loads(self.json())
return super().dict(**kwargs)
class VMessSettings(ProxySettings):
id: UUID = Field(default_factory=uuid4)
def revoke(self):
self.id = uuid4()
class VLESSSettings(ProxySettings):
id: UUID = Field(default_factory=uuid4)
flow: XTLSFlows = XTLSFlows.NONE
def revoke(self):
self.id = uuid4()
class TrojanSettings(ProxySettings):
password: str = Field(default_factory=random_password)
flow: XTLSFlows = XTLSFlows.NONE
def revoke(self):
self.password = random_password()
class ShadowsocksSettings(ProxySettings):
password: str = Field(default_factory=random_password)
method: ShadowsocksMethods = ShadowsocksMethods.CHACHA20_POLY1305
def revoke(self):
self.password = random_password()
class ProxyHostSecurity(str, Enum):
inbound_default = "inbound_default"
none = "none"
tls = "tls"
ProxyHostALPN = Enum(
"ProxyHostALPN",
{
"none": "",
"h3": "h3",
"h2": "h2",
"http/1.1": "http/1.1",
"h3,h2,http/1.1": "h3,h2,http/1.1",
"h3,h2": "h3,h2",
"h2,http/1.1": "h2,http/1.1",
},
)
ProxyHostFingerprint = Enum(
"ProxyHostFingerprint",
{
"none": "",
"chrome": "chrome",
"firefox": "firefox",
"safari": "safari",
"ios": "ios",
"android": "android",
"edge": "edge",
"360": "360",
"qq": "qq",
"random": "random",
"randomized": "randomized",
},
)
class FormatVariables(dict):
def __missing__(self, key):
return key.join("{}")
class ProxyHost(BaseModel):
remark: str
address: str
port: Optional[int] = Field(None, nullable=True)
sni: Optional[str] = Field(None, nullable=True)
host: Optional[str] = Field(None, nullable=True)
path: Optional[str] = Field(None, nullable=True)
security: ProxyHostSecurity = ProxyHostSecurity.inbound_default
alpn: ProxyHostALPN = ProxyHostALPN.none
fingerprint: ProxyHostFingerprint = ProxyHostFingerprint.none
allowinsecure: Union[bool, None] = None
is_disabled: Union[bool, None] = None
mux_enable: Union[bool, None] = None
fragment_setting: Optional[str] = Field(None, nullable=True)
noise_setting: Optional[str] = Field(None, nullable=True)
random_user_agent: Union[bool, None] = None
model_config = ConfigDict(from_attributes=True)
@field_validator("remark", mode="after")
def validate_remark(cls, v):
try:
v.format_map(FormatVariables())
except ValueError as exc:
raise ValueError("Invalid formatting variables")
return v
@field_validator("address", mode="after")
def validate_address(cls, v):
try:
v.format_map(FormatVariables())
except ValueError as exc:
raise ValueError("Invalid formatting variables")
return v
@field_validator("fragment_setting", check_fields=False)
@classmethod
def validate_fragment(cls, v):
if v and not FRAGMENT_PATTERN.match(v):
raise ValueError(
"Fragment setting must be like this: length,interval,packet (10-100,100-200,tlshello)."
)
return v
@field_validator("noise_setting", check_fields=False)
@classmethod
def validate_noise(cls, v):
if v:
if not NOISE_PATTERN.match(v):
raise ValueError(
"Noise setting must be like this: packet,delay (rand:10-20,100-200)."
)
if len(v) > 2000:
raise ValueError(
"Noise can't be longer that 2000 character"
)
return v
class ProxyInbound(BaseModel):
tag: str
protocol: ProxyTypes
network: str
tls: str
port: Union[int, str]