""" New Xray models for flexible inbound and subscription management. """ import json import uuid from datetime import datetime, timedelta from django.db import models from django.core.exceptions import ValidationError from django.utils import timezone class XrayConfiguration(models.Model): """Global Xray configuration - Admin menu settings""" grpc_address = models.CharField( max_length=255, default="127.0.0.1:10085", help_text="Xray gRPC API address (host:port)" ) default_client_hostname = models.CharField( max_length=255, help_text="Default hostname for client connections" ) stats_enabled = models.BooleanField( default=True, help_text="Enable traffic statistics" ) cert_renewal_days = models.IntegerField( default=60, help_text="Renew certificates X days before expiration" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Xray Configuration" verbose_name_plural = "Xray Configuration" def __str__(self): return f"Xray Config - {self.grpc_address}" def save(self, *args, **kwargs): # Ensure only one configuration exists if not self.pk and XrayConfiguration.objects.exists(): raise ValidationError("Only one Xray configuration allowed") super().save(*args, **kwargs) class Credentials(models.Model): """Universal credentials storage for various services""" CRED_TYPES = [ ('cloudflare', 'Cloudflare API'), ('dns_provider', 'DNS Provider'), ('email', 'Email SMTP'), ('other', 'Other') ] name = models.CharField( max_length=100, unique=True, help_text="Descriptive name for these credentials" ) cred_type = models.CharField( max_length=20, choices=CRED_TYPES, help_text="Type of credentials" ) credentials = models.JSONField( help_text="Credentials data (e.g., {'api_token': '...', 'email': '...'})" ) description = models.TextField( blank=True, help_text="Description of what these credentials are used for" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Credentials" verbose_name_plural = "Credentials" ordering = ['cred_type', 'name'] def __str__(self): return f"{self.name} ({self.get_cred_type_display()})" def get_credential(self, key: str, default=None): """Safely get credential value""" return self.credentials.get(key, default) class Certificate(models.Model): """SSL/TLS Certificate management""" CERT_TYPES = [ ('self_signed', 'Self-Signed'), ('letsencrypt', "Let's Encrypt"), ('custom', 'Custom') ] domain = models.CharField( max_length=255, unique=True, help_text="Domain name for this certificate" ) certificate_pem = models.TextField( blank=True, help_text="Certificate in PEM format (auto-generated for Let's Encrypt)" ) private_key_pem = models.TextField( blank=True, help_text="Private key in PEM format (auto-generated for Let's Encrypt)" ) cert_type = models.CharField( max_length=20, choices=CERT_TYPES, help_text="Type of certificate" ) expires_at = models.DateTimeField( null=True, blank=True, help_text="Certificate expiration date (auto-filled after generation)" ) credentials = models.ForeignKey( Credentials, null=True, blank=True, on_delete=models.SET_NULL, help_text="Credentials for Let's Encrypt (Cloudflare API)" ) acme_email = models.EmailField( blank=True, help_text="Email address for ACME account registration (required for Let's Encrypt)" ) auto_renew = models.BooleanField( default=True, help_text="Automatically renew certificate before expiration" ) last_renewed = models.DateTimeField( null=True, blank=True, help_text="Last renewal timestamp" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Certificate" verbose_name_plural = "Certificates" ordering = ['domain'] def __str__(self): return f"{self.domain} ({self.get_cert_type_display()})" @property def is_expired(self): """Check if certificate is expired""" if not self.expires_at: return False return timezone.now() > self.expires_at @property def days_until_expiration(self): """Days until certificate expires""" if not self.expires_at: return None delta = self.expires_at - timezone.now() return delta.days @property def needs_renewal(self): """Check if certificate needs renewal""" if not self.auto_renew or not self.expires_at: return False try: config = XrayConfiguration.objects.first() renewal_days = config.cert_renewal_days if config else 60 except: renewal_days = 60 days_left = self.days_until_expiration if days_left is None: return False return days_left <= renewal_days class Inbound(models.Model): """Independent inbound configuration""" PROTOCOLS = [ ('vless', 'VLESS'), ('vmess', 'VMess'), ('trojan', 'Trojan'), ('shadowsocks', 'Shadowsocks') ] NETWORKS = [ ('tcp', 'TCP'), ('ws', 'WebSocket'), ('grpc', 'gRPC'), ('http', 'HTTP/2'), ('quic', 'QUIC') ] SECURITIES = [ ('none', 'None'), ('tls', 'TLS'), ('reality', 'REALITY') ] name = models.CharField( max_length=100, unique=True, help_text="Unique identifier for this inbound" ) protocol = models.CharField( max_length=20, choices=PROTOCOLS, help_text="Protocol type" ) port = models.IntegerField( help_text="Port to listen on" ) network = models.CharField( max_length=20, choices=NETWORKS, default='tcp', help_text="Transport protocol" ) security = models.CharField( max_length=20, choices=SECURITIES, default='none', help_text="Security type" ) certificate = models.ForeignKey( Certificate, null=True, blank=True, on_delete=models.SET_NULL, help_text="Certificate for TLS" ) domain = models.CharField( max_length=255, blank=True, help_text="Client connection domain" ) # Full configuration for Xray full_config = models.JSONField( default=dict, blank=True, help_text="Complete configuration for creating inbound on server (auto-generated if empty)" ) # Additional settings listen_address = models.CharField( max_length=45, default="0.0.0.0", help_text="IP address to listen on" ) enable_sniffing = models.BooleanField( default=True, help_text="Enable protocol sniffing" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Inbound" verbose_name_plural = "Inbounds" ordering = ['protocol', 'port'] unique_together = [['port', 'listen_address']] def __str__(self): return f"{self.name} ({self.protocol.upper()}:{self.port})" def generate_tag(self): """Generate unique tag for inbound""" return f"{self.protocol}-{self.port}-{uuid.uuid4().hex[:8]}" def build_config(self): """Build full configuration for Xray""" try: # Build basic Xray inbound configuration config = { "tag": self.name, "port": self.port, "listen": self.listen_address, "protocol": self.protocol, "settings": self._build_protocol_settings(), "streamSettings": self._build_stream_settings(), "sniffing": { "enabled": self.enable_sniffing, "destOverride": ["http", "tls"] } if self.enable_sniffing else {} } # Store the built config self.full_config = config return self.full_config except Exception as e: # Fallback to basic config if detailed build fails import logging logger = logging.getLogger(__name__) logger.warning(f"Failed to build detailed config for {self.name}: {e}") self.full_config = { "tag": self.name, "port": self.port, "listen": self.listen_address, "protocol": self.protocol, "settings": {}, "streamSettings": {} } return self.full_config def _build_protocol_settings(self): """Build protocol-specific settings""" settings = {} if self.protocol == 'vless': settings = { "clients": [], # Will be populated when users are added "decryption": "none" } elif self.protocol == 'vmess': settings = { "clients": [] # Will be populated when users are added } elif self.protocol == 'trojan': settings = { "clients": [] # Will be populated when users are added } elif self.protocol == 'shadowsocks': settings = { "method": "aes-128-gcm", # Default method "password": "", # Will be set when configured "network": "tcp,udp" } return settings def _build_stream_settings(self): """Build stream transport settings""" stream_settings = { "network": self.network } # Add network-specific settings if self.network == "ws": stream_settings["wsSettings"] = { "path": f"/{self.name}", "headers": {} } elif self.network == "grpc": stream_settings["grpcSettings"] = { "serviceName": self.name } elif self.network == "http": stream_settings["httpSettings"] = { "path": f"/{self.name}", "host": [self.domain] if self.domain else [] } # Add security settings if self.security == "tls": stream_settings["security"] = "tls" tls_settings = { "serverName": self.domain or "localhost", "alpn": ["h2", "http/1.1"] } if self.certificate: tls_settings.update({ "certificates": [{ "certificateFile": f"/etc/xray/certs/{self.certificate.domain}.crt", "keyFile": f"/etc/xray/certs/{self.certificate.domain}.key" }] }) stream_settings["tlsSettings"] = tls_settings elif self.security == "reality": stream_settings["security"] = "reality" # Reality settings would be configured here stream_settings["realitySettings"] = { "dest": self.domain or "example.com:443", "serverNames": [self.domain] if self.domain else ["example.com"], "privateKey": "", # Would be generated "shortIds": [""] # Would be generated } return stream_settings class SubscriptionGroup(models.Model): """Groups of inbounds for subscription management""" name = models.CharField( max_length=100, unique=True, help_text="Group name (e.g., 'VLESS Premium', 'VMess Basic')" ) description = models.TextField( blank=True, help_text="Description of this subscription group" ) inbounds = models.ManyToManyField( Inbound, blank=True, help_text="Inbounds included in this group" ) is_active = models.BooleanField( default=True, help_text="Whether this group is active" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Subscription Group" verbose_name_plural = "Subscription Groups" ordering = ['name'] def __str__(self): return self.name @property def inbound_count(self): """Number of inbounds in this group""" return self.inbounds.count() @property def user_count(self): """Number of users subscribed to this group""" return self.usersubscription_set.filter(active=True).count() class UserSubscription(models.Model): """User subscriptions to groups""" user = models.ForeignKey( 'User', on_delete=models.CASCADE, related_name='xray_subscriptions' ) subscription_group = models.ForeignKey( SubscriptionGroup, on_delete=models.CASCADE ) active = models.BooleanField( default=True, help_text="Whether this subscription is active" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "User Subscription" verbose_name_plural = "User Subscriptions" unique_together = ['user', 'subscription_group'] ordering = ['user__username', 'subscription_group__name'] def __str__(self): return f"{self.user.username} - {self.subscription_group.name}" class ServerInbound(models.Model): """Many-to-many relationship between servers and inbounds to track deployment""" server = models.ForeignKey('Server', on_delete=models.CASCADE, related_name='deployed_inbounds') inbound = models.ForeignKey(Inbound, on_delete=models.CASCADE, related_name='deployed_servers') active = models.BooleanField(default=True, help_text="Whether this inbound is active on the server") deployed_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) # Store deployment-specific configuration if needed deployment_config = models.JSONField(default=dict, blank=True, help_text="Server-specific deployment configuration") class Meta: verbose_name = "Server Inbound Deployment" verbose_name_plural = "Server Inbound Deployments" ordering = ['server__name', 'inbound__name'] unique_together = [('server', 'inbound')] def __str__(self): status = "Active" if self.active else "Inactive" return f"{self.server.name} -> {self.inbound.name} ({status})"