vpn/app/models.py
2026-09-23 01:19:36 +00:00

199 lines
5.4 KiB
Python

from __future__ import annotations
import calendar
import secrets
import uuid
from datetime import UTC, date, datetime
from enum import StrEnum
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
def utc_now() -> datetime:
return datetime.now(UTC)
class EngineState(StrEnum):
STOPPED = "stopped"
STARTING = "starting"
RUNNING = "running"
STOPPING = "stopping"
ERROR = "error"
UNSUPPORTED = "unsupported"
class EngineKind(StrEnum):
SIGNBOX = "signbox"
FREETURN = "freeturn"
class RunMode(StrEnum):
VPN = "vpn"
PROXY = "proxy"
BOTH = "both"
class ProxyAccess(StrEnum):
LOCAL = "local"
HOTSPOT = "hotspot"
class ProxyProtocol(StrEnum):
SOCKS5 = "socks5"
HTTP = "http"
class AppPolicy(StrEnum):
PROXY = "proxy"
DIRECT = "direct"
VOID = "void"
class Strategy(StrEnum):
BASIC = "basic"
ENHANCED = "enhanced"
class CapabilitySet(BaseModel):
supports_vpn: bool = False
supports_proxy: bool = False
supports_socks: bool = False
supports_http: bool = False
supports_udp: bool = False
supports_mobile: bool = False
supports_desktop: bool = False
supports_stats: bool = False
supports_hot_reload: bool = False
supports_both: bool = False
class ProfileMetadata(BaseModel):
model_config = ConfigDict(extra="forbid")
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str = "Новый профиль"
created_at: datetime = Field(default_factory=utc_now)
updated_at: datetime = Field(default_factory=utc_now)
class SignBoxProfile(ProfileMetadata):
kind: EngineKind = EngineKind.SIGNBOX
strategy: Strategy = Strategy.BASIC
config: dict[str, Any] = Field(default_factory=dict)
mtu: int = Field(default=1280, ge=576, le=9000)
split_tunneling: bool = False
raw_config: str | None = None
class FreeTurnProfile(ProfileMetadata):
kind: EngineKind = EngineKind.FREETURN
key: str = ""
options: dict[str, Any] = Field(default_factory=dict)
preset: int = Field(default=1, ge=1, le=4)
stability_over_load: bool = False
captcha_always_browser: bool = False
@property
def stream_settings(self) -> tuple[int, int]:
values = {1: (4, 4), 2: (8, 4), 3: (12, 6), 4: (24, 6)}
threads, per_credential = values[self.preset]
if self.stability_over_load and self.preset >= 3:
per_credential = 4
return threads, per_credential
class ProxySettings(BaseModel):
enabled: bool = True
access: ProxyAccess = ProxyAccess.LOCAL
socks_enabled: bool = True
socks_port: int = Field(default=1080, ge=1, le=65535)
socks_random_port: bool = False
http_enabled: bool = True
http_port: int = Field(default=8080, ge=1, le=65535)
http_random_port: bool = False
authentication: bool = False
username: str = ""
password: str = ""
bind_address: str = "127.0.0.1"
@field_validator("username", "password")
@classmethod
def no_control_chars(cls, value: str) -> str:
if any(ord(char) < 32 for char in value):
raise ValueError("credentials must not contain control characters")
return value
def validate_for_access(self) -> None:
if self.access is ProxyAccess.HOTSPOT and not self.authentication:
return
if self.authentication and (not self.username or not self.password):
raise ValueError("proxy authentication requires username and password")
class VpnSettings(BaseModel):
mode: RunMode = RunMode.VPN
tun_address: str = "10.0.0.1"
tun_prefix: int = Field(default=24, ge=8, le=30)
mtu: int = Field(default=1280, ge=576, le=9000)
split_tunneling: bool = False
app_rules: dict[str, AppPolicy] = Field(default_factory=dict)
@field_validator("app_rules")
@classmethod
def validate_rules(cls, value: dict[str, AppPolicy]) -> dict[str, AppPolicy]:
# A VOID rule must never be represented as a VpnService exclusion.
return dict(value)
class RuntimeRequest(BaseModel):
engine: EngineKind
mode: RunMode
profile_id: str
proxy: ProxySettings = Field(default_factory=ProxySettings)
vpn: VpnSettings = Field(default_factory=VpnSettings)
engine_config: dict[str, Any] = Field(default_factory=dict)
config_revision: int = Field(default=0, ge=0)
class RuntimeStats(BaseModel):
upload_bytes: int = 0
download_bytes: int = 0
upload_bps: int = 0
download_bps: int = 0
uptime_seconds: int = 0
class RuntimeSnapshot(BaseModel):
state: EngineState = EngineState.STOPPED
engine: EngineKind | None = None
mode: RunMode | None = None
profile_id: str | None = None
message: str | None = None
stats: RuntimeStats = Field(default_factory=RuntimeStats)
proxy_addresses: list[str] = Field(default_factory=list)
captcha_url: str | None = None
revision: int = 0
class PaymentInfo(BaseModel):
monthly_price_rub: int = Field(ge=0)
payment_day: int = Field(ge=1, le=31)
def due_date(self, year: int, month: int) -> date:
last_day = calendar.monthrange(year, month)[1]
return date(year, month, min(self.payment_day, last_day))
class DeviceSubscription(BaseModel):
device_id: str
device_token: str
subscription_url: str
def generate_device_token() -> str:
return secrets.token_urlsafe(32)
def generate_device_id() -> str:
return str(uuid.uuid4())