2025-07-20 23:04:58 +03:00
|
|
|
def userPortal(request, user_hash):
|
2025-08-08 05:46:36 +03:00
|
|
|
"""HTML portal for user to view their VPN access links and subscription groups"""
|
|
|
|
from .models import User
|
|
|
|
from .models_xray import UserSubscription, SubscriptionGroup, Inbound
|
2025-07-21 12:47:47 +03:00
|
|
|
from django.utils import timezone
|
2025-07-21 13:23:10 +03:00
|
|
|
from datetime import timedelta
|
2025-07-20 23:04:58 +03:00
|
|
|
import logging
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
try:
|
|
|
|
user = get_object_or_404(User, hash=user_hash)
|
|
|
|
logger.info(f"User portal accessed for user {user.username}")
|
|
|
|
except Http404:
|
|
|
|
logger.warning(f"User portal access attempt with invalid hash: {user_hash}")
|
|
|
|
return render(request, 'vpn/user_portal_error.html', {
|
|
|
|
'error_title': 'Access Denied',
|
|
|
|
'error_message': 'Invalid access link. Please contact your administrator.'
|
|
|
|
}, status=403)
|
|
|
|
|
|
|
|
try:
|
2025-08-08 05:46:36 +03:00
|
|
|
# Get all active subscription groups for the user
|
|
|
|
user_subscriptions = UserSubscription.objects.filter(
|
|
|
|
user=user,
|
|
|
|
active=True,
|
|
|
|
subscription_group__is_active=True
|
|
|
|
).select_related('subscription_group').prefetch_related('subscription_group__inbounds')
|
2025-07-21 12:47:47 +03:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
logger.info(f"Found {user_subscriptions.count()} active subscription groups for user {user.username}")
|
|
|
|
|
2025-08-08 06:50:04 +03:00
|
|
|
# Calculate overall Xray subscription statistics
|
|
|
|
from .models import AccessLog
|
|
|
|
total_connections = AccessLog.objects.filter(
|
|
|
|
user=user.username,
|
|
|
|
action='Success',
|
|
|
|
server='Xray-Subscription'
|
|
|
|
).count()
|
|
|
|
|
|
|
|
recent_connections = AccessLog.objects.filter(
|
|
|
|
user=user.username,
|
|
|
|
action='Success',
|
|
|
|
server='Xray-Subscription',
|
|
|
|
timestamp__gte=timezone.now() - timedelta(days=30)
|
|
|
|
).count()
|
|
|
|
|
|
|
|
logger.info(f"Xray statistics for user {user.username}: total={total_connections}, recent={recent_connections}")
|
2025-07-20 23:04:58 +03:00
|
|
|
|
2025-08-05 01:50:11 +03:00
|
|
|
# Determine protocol scheme
|
2025-08-08 10:32:14 +03:00
|
|
|
scheme = 'https' # Always use HTTPS as SSL is handled by ingress
|
2025-08-05 01:50:11 +03:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
# Group inbounds by subscription group
|
|
|
|
groups_data = {}
|
|
|
|
total_inbounds = 0
|
2025-07-20 23:04:58 +03:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
for subscription in user_subscriptions:
|
|
|
|
group = subscription.subscription_group
|
|
|
|
group_name = group.name
|
|
|
|
logger.debug(f"Processing subscription group {group_name}")
|
2025-07-20 23:04:58 +03:00
|
|
|
|
2025-08-08 07:39:01 +03:00
|
|
|
# Get all deployed inbounds for this group (count actual server deployments)
|
|
|
|
from .models_xray import ServerInbound
|
|
|
|
deployed_inbounds = ServerInbound.objects.filter(
|
|
|
|
inbound__in=group.inbounds.all(),
|
|
|
|
active=True
|
|
|
|
).select_related('inbound', 'server')
|
2025-07-21 12:47:47 +03:00
|
|
|
|
2025-08-08 06:50:04 +03:00
|
|
|
# Calculate connections for this specific group
|
|
|
|
group_connections = AccessLog.objects.filter(
|
|
|
|
user=user.username,
|
|
|
|
action='Success',
|
|
|
|
server='Xray-Subscription',
|
|
|
|
data__icontains=f'"group": "{group_name}"'
|
|
|
|
).count()
|
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
groups_data[group_name] = {
|
|
|
|
'group': group,
|
|
|
|
'subscription': subscription,
|
|
|
|
'inbounds': [],
|
2025-08-08 06:50:04 +03:00
|
|
|
'total_connections': group_connections,
|
2025-08-08 07:39:01 +03:00
|
|
|
'deployed_count': deployed_inbounds.count(), # Actual deployed inbounds count
|
2025-08-08 05:46:36 +03:00
|
|
|
}
|
2025-07-21 12:47:47 +03:00
|
|
|
|
2025-08-08 07:39:01 +03:00
|
|
|
# Process each deployed inbound (each server-inbound combination)
|
|
|
|
for server_inbound in deployed_inbounds:
|
|
|
|
inbound = server_inbound.inbound
|
2025-08-08 05:46:36 +03:00
|
|
|
logger.debug(f"Processing inbound {inbound.name} in group {group_name}")
|
2025-07-21 13:23:10 +03:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
# Generate connection URLs based on protocol
|
|
|
|
connection_urls = []
|
2025-07-21 13:23:10 +03:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
if inbound.protocol == 'vless':
|
|
|
|
# Generate VLESS URL - this is a placeholder implementation
|
|
|
|
# In the real implementation, you'd generate proper VLESS URLs with user UUID
|
2025-08-08 10:32:14 +03:00
|
|
|
connection_url = f"vless://user-uuid@{get_external_host()}:{inbound.port}#{inbound.name}"
|
2025-08-08 05:46:36 +03:00
|
|
|
connection_urls.append({
|
|
|
|
'url': connection_url,
|
|
|
|
'protocol': 'VLESS',
|
|
|
|
'name': inbound.name
|
|
|
|
})
|
|
|
|
elif inbound.protocol == 'vmess':
|
|
|
|
# Generate VMess URL - placeholder
|
2025-08-08 10:32:14 +03:00
|
|
|
connection_url = f"vmess://user-config@{get_external_host()}:{inbound.port}#{inbound.name}"
|
2025-08-08 05:46:36 +03:00
|
|
|
connection_urls.append({
|
|
|
|
'url': connection_url,
|
|
|
|
'protocol': 'VMess',
|
|
|
|
'name': inbound.name
|
|
|
|
})
|
|
|
|
elif inbound.protocol == 'trojan':
|
|
|
|
# Generate Trojan URL - placeholder
|
2025-08-08 10:32:14 +03:00
|
|
|
connection_url = f"trojan://user-password@{get_external_host()}:{inbound.port}#{inbound.name}"
|
2025-08-08 05:46:36 +03:00
|
|
|
connection_urls.append({
|
|
|
|
'url': connection_url,
|
|
|
|
'protocol': 'Trojan',
|
|
|
|
'name': inbound.name
|
|
|
|
})
|
|
|
|
elif inbound.protocol == 'shadowsocks':
|
|
|
|
# Generate Shadowsocks URL - placeholder
|
2025-08-08 10:32:14 +03:00
|
|
|
connection_url = f"ss://user-config@{get_external_host()}:{inbound.port}#{inbound.name}"
|
2025-08-08 05:46:36 +03:00
|
|
|
connection_urls.append({
|
|
|
|
'url': connection_url,
|
|
|
|
'protocol': 'Shadowsocks',
|
|
|
|
'name': inbound.name
|
|
|
|
})
|
2025-07-21 12:47:47 +03:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
inbound_data = {
|
|
|
|
'inbound': inbound,
|
|
|
|
'connection_urls': connection_urls,
|
|
|
|
'protocol': inbound.protocol.upper(),
|
|
|
|
'port': inbound.port,
|
2025-08-08 10:32:14 +03:00
|
|
|
'domain': get_external_host(),
|
2025-08-08 05:46:36 +03:00
|
|
|
'network': inbound.network,
|
|
|
|
'security': inbound.security,
|
|
|
|
'connections': 0, # Placeholder during transition
|
|
|
|
'last_access_display': "Never used", # Placeholder
|
2025-08-08 07:39:01 +03:00
|
|
|
'server': server_inbound.server, # Server that deployed this inbound
|
|
|
|
'server_name': server_inbound.server.name, # Server name for display
|
2025-08-08 05:46:36 +03:00
|
|
|
}
|
2025-07-21 12:47:47 +03:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
groups_data[group_name]['inbounds'].append(inbound_data)
|
|
|
|
total_inbounds += 1
|
|
|
|
|
|
|
|
logger.debug(f"Added inbound data for {inbound.name}")
|
2025-07-21 12:47:47 +03:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
logger.info(f"Prepared data for {len(groups_data)} subscription groups and {total_inbounds} total inbounds")
|
2025-07-21 12:47:47 +03:00
|
|
|
logger.info(f"Portal statistics: total_connections={total_connections}, recent_connections={recent_connections}")
|
2025-07-20 23:04:58 +03:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
# Check if user has any Xray subscription groups
|
|
|
|
has_xray_access = user_subscriptions.exists()
|
2025-08-05 01:23:07 +03:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
# Also get old-style ACL links for backwards compatibility
|
|
|
|
acl_links = []
|
|
|
|
try:
|
|
|
|
from .models import ACLLink
|
|
|
|
acl_links = ACLLink.objects.filter(acl__user=user).select_related('acl__server', 'acl')
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2025-07-20 23:04:58 +03:00
|
|
|
context = {
|
|
|
|
'user': user,
|
2025-08-08 05:46:36 +03:00
|
|
|
'user_subscriptions': user_subscriptions, # For accessing user's subscriptions in template
|
|
|
|
'groups_data': groups_data,
|
|
|
|
'total_groups': len(groups_data),
|
|
|
|
'total_inbounds': total_inbounds,
|
2025-07-21 12:47:47 +03:00
|
|
|
'total_connections': total_connections,
|
|
|
|
'recent_connections': recent_connections,
|
2025-08-08 10:32:14 +03:00
|
|
|
'external_address': get_external_host(),
|
2025-08-08 05:46:36 +03:00
|
|
|
'has_xray_access': has_xray_access,
|
2025-08-05 01:50:11 +03:00
|
|
|
'force_scheme': scheme, # Override request.scheme in template
|
2025-08-08 05:46:36 +03:00
|
|
|
'acl_links': acl_links, # For backwards compatibility
|
|
|
|
'has_old_links': len(acl_links) > 0,
|
2025-08-08 10:32:14 +03:00
|
|
|
'xray_subscription_url': f"https://{request.get_host()}/xray/{user.hash}",
|
2025-07-20 23:04:58 +03:00
|
|
|
}
|
|
|
|
|
2025-07-21 12:47:47 +03:00
|
|
|
logger.debug(f"Context prepared with keys: {list(context.keys())}")
|
2025-08-08 05:46:36 +03:00
|
|
|
logger.debug(f"Groups in context: {list(groups_data.keys())}")
|
2025-07-21 12:47:47 +03:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
# Log sample group data for debugging
|
|
|
|
for group_name, group_data in groups_data.items():
|
|
|
|
logger.debug(f"Group {group_name}: total_connections={group_data['total_connections']}, inbounds_count={len(group_data['inbounds'])}")
|
|
|
|
for i, inbound_data in enumerate(group_data['inbounds']):
|
|
|
|
logger.debug(f" Inbound {i}: protocol={inbound_data['protocol']}, port={inbound_data['port']}, connections={inbound_data['connections']}")
|
2025-07-21 12:47:47 +03:00
|
|
|
|
2025-07-20 23:04:58 +03:00
|
|
|
return render(request, 'vpn/user_portal.html', context)
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-07-21 12:47:47 +03:00
|
|
|
logger.error(f"Error loading user portal for {user.username}: {e}", exc_info=True)
|
2025-07-20 23:04:58 +03:00
|
|
|
return render(request, 'vpn/user_portal_error.html', {
|
|
|
|
'error_title': 'Server Error',
|
|
|
|
'error_message': 'Unable to load your VPN information. Please try again later or contact support.'
|
|
|
|
}, status=500)
|
2025-02-23 19:18:23 +00:00
|
|
|
import yaml
|
2025-02-23 19:33:54 +00:00
|
|
|
import json
|
2025-07-20 23:04:58 +03:00
|
|
|
import logging
|
|
|
|
from django.shortcuts import get_object_or_404, render
|
2024-10-28 17:15:49 +00:00
|
|
|
from django.http import JsonResponse, HttpResponse, Http404
|
2025-02-25 12:39:08 +02:00
|
|
|
from mysite.settings import EXTERNAL_ADDRESS
|
2025-08-08 10:32:14 +03:00
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
|
|
def get_external_host():
|
|
|
|
"""Extract hostname from EXTERNAL_ADDRESS, removing scheme"""
|
|
|
|
parsed = urlparse(EXTERNAL_ADDRESS)
|
|
|
|
if parsed.hostname:
|
|
|
|
return parsed.hostname
|
|
|
|
# If no scheme, assume it's just a hostname
|
|
|
|
return EXTERNAL_ADDRESS
|
2025-02-25 12:39:08 +02:00
|
|
|
|
|
|
|
def userFrontend(request, user_hash):
|
|
|
|
from .models import User, ACLLink
|
|
|
|
try:
|
|
|
|
user = get_object_or_404(User, hash=user_hash)
|
|
|
|
except Http404:
|
|
|
|
return JsonResponse({"error": "Not allowed"}, status=403)
|
|
|
|
|
|
|
|
acl_links = {}
|
|
|
|
for link in ACLLink.objects.filter(acl__user=user).select_related('acl__server'):
|
|
|
|
server_name = link.acl.server.name
|
|
|
|
if server_name not in acl_links:
|
|
|
|
acl_links[server_name] = []
|
2025-08-08 10:32:14 +03:00
|
|
|
acl_links[server_name].append({"link": f"https://{request.get_host()}/ss/{link.link}#{link.acl.server.name}", "comment": link.comment})
|
2025-02-25 12:39:08 +02:00
|
|
|
|
|
|
|
return JsonResponse(acl_links)
|
2024-10-20 21:57:12 +00:00
|
|
|
|
|
|
|
def shadowsocks(request, link):
|
2024-10-28 17:15:49 +00:00
|
|
|
from .models import ACLLink, AccessLog
|
2025-07-20 22:30:04 +03:00
|
|
|
import logging
|
2025-07-21 12:12:31 +03:00
|
|
|
from django.utils import timezone
|
2025-07-20 22:30:04 +03:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2024-10-28 17:15:49 +00:00
|
|
|
try:
|
|
|
|
acl_link = get_object_or_404(ACLLink, link=link)
|
|
|
|
acl = acl_link.acl
|
2025-07-20 22:30:04 +03:00
|
|
|
logger.info(f"Found ACL link for user {acl.user.username} on server {acl.server.name}")
|
2024-10-28 17:15:49 +00:00
|
|
|
except Http404:
|
2025-07-20 22:30:04 +03:00
|
|
|
logger.warning(f"ACL link not found: {link}")
|
2025-07-21 13:49:43 +03:00
|
|
|
AccessLog.objects.create(
|
|
|
|
user=None,
|
|
|
|
server="Unknown",
|
|
|
|
acl_link_id=link,
|
|
|
|
action="Failed",
|
|
|
|
data=f"ACL not found for link: {link}"
|
|
|
|
)
|
2024-10-28 17:15:49 +00:00
|
|
|
return JsonResponse({"error": "Not allowed"}, status=403)
|
2025-02-23 21:22:12 +00:00
|
|
|
|
2024-10-21 13:22:03 +00:00
|
|
|
try:
|
|
|
|
server_user = acl.server.get_user(acl.user, raw=True)
|
2025-07-20 22:30:04 +03:00
|
|
|
logger.info(f"Successfully retrieved user credentials for {acl.user.username} from {acl.server.name}")
|
2024-10-26 12:22:19 +00:00
|
|
|
except Exception as e:
|
2025-07-20 22:30:04 +03:00
|
|
|
logger.error(f"Failed to get user credentials for {acl.user.username} from {acl.server.name}: {e}")
|
2025-07-21 13:49:43 +03:00
|
|
|
AccessLog.objects.create(
|
|
|
|
user=acl.user.username,
|
|
|
|
server=acl.server.name,
|
|
|
|
acl_link_id=acl_link.link,
|
|
|
|
action="Failed",
|
|
|
|
data=f"Failed to get credentials: {e}"
|
|
|
|
)
|
2025-07-20 22:30:04 +03:00
|
|
|
return JsonResponse({"error": f"Couldn't get credentials from server. {e}"}, status=500)
|
2024-10-21 13:22:03 +00:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
# Handle both dict and object formats for server_user
|
|
|
|
if isinstance(server_user, dict):
|
|
|
|
password = server_user.get('password', '')
|
|
|
|
method = server_user.get('method', 'aes-128-gcm')
|
|
|
|
port = server_user.get('port', 8080)
|
|
|
|
access_url = server_user.get('access_url', '')
|
|
|
|
else:
|
|
|
|
password = getattr(server_user, 'password', '')
|
|
|
|
method = getattr(server_user, 'method', 'aes-128-gcm')
|
|
|
|
port = getattr(server_user, 'port', 8080)
|
|
|
|
access_url = getattr(server_user, 'access_url', '')
|
|
|
|
|
2025-02-23 21:22:12 +00:00
|
|
|
if request.GET.get('mode') == 'json':
|
|
|
|
config = {
|
2025-02-25 12:39:08 +02:00
|
|
|
"info": "Managed by OutFleet_2 [github.com/house-of-vanity/OutFleet/]",
|
2025-08-08 05:46:36 +03:00
|
|
|
"password": password,
|
|
|
|
"method": method,
|
2025-02-23 21:22:12 +00:00
|
|
|
"prefix": "\u0005\u00dc_\u00e0\u0001",
|
|
|
|
"server": acl.server.client_hostname,
|
2025-08-08 05:46:36 +03:00
|
|
|
"server_port": port,
|
|
|
|
"access_url": access_url,
|
2025-02-23 21:22:12 +00:00
|
|
|
"outfleet": {
|
|
|
|
"acl_link": link,
|
|
|
|
"server_name": acl.server.name,
|
|
|
|
"server_type": acl.server.server_type,
|
|
|
|
}
|
|
|
|
}
|
2025-02-23 21:29:03 +00:00
|
|
|
response = json.dumps(config, indent=2)
|
2025-02-23 21:22:12 +00:00
|
|
|
else:
|
|
|
|
config = {
|
|
|
|
"transport": {
|
|
|
|
"$type": "tcpudp",
|
|
|
|
"tcp": {
|
|
|
|
"$type": "shadowsocks",
|
2025-08-08 05:46:36 +03:00
|
|
|
"endpoint": f"{acl.server.client_hostname}:{port}",
|
|
|
|
"cipher": f"{method}",
|
|
|
|
"secret": f"{password}",
|
2025-02-23 21:22:12 +00:00
|
|
|
"prefix": "\u0005\u00dc_\u00e0\u0001"
|
|
|
|
},
|
|
|
|
"udp": {
|
|
|
|
"$type": "shadowsocks",
|
2025-08-08 05:46:36 +03:00
|
|
|
"endpoint": f"{acl.server.client_hostname}:{port}",
|
|
|
|
"cipher": f"{method}",
|
|
|
|
"secret": f"{password}",
|
2025-02-23 21:22:12 +00:00
|
|
|
"prefix": "\u0005\u00dc_\u00e0\u0001"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-02-23 21:29:03 +00:00
|
|
|
response = yaml.dump(config, allow_unicode=True)
|
2025-02-23 21:22:12 +00:00
|
|
|
|
2025-07-21 12:12:31 +03:00
|
|
|
# Update last access time for this specific link
|
|
|
|
acl_link.last_access_time = timezone.now()
|
|
|
|
acl_link.save(update_fields=['last_access_time'])
|
|
|
|
|
2025-07-21 13:49:43 +03:00
|
|
|
# Create AccessLog with specific link tracking
|
|
|
|
AccessLog.objects.create(
|
|
|
|
user=acl.user.username,
|
|
|
|
server=acl.server.name,
|
|
|
|
acl_link_id=acl_link.link,
|
|
|
|
action="Success",
|
|
|
|
data=response
|
|
|
|
)
|
2025-02-23 19:18:23 +00:00
|
|
|
|
2025-02-23 21:22:12 +00:00
|
|
|
return HttpResponse(response, content_type=f"{ 'application/json; charset=utf-8' if request.GET.get('mode') == 'json' else 'text/html' }")
|
2025-02-23 19:18:23 +00:00
|
|
|
|
2025-08-05 01:23:07 +03:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
def xray_subscription(request, user_hash):
|
2025-08-05 01:23:07 +03:00
|
|
|
"""
|
|
|
|
Return Xray subscription with all available protocols for the user.
|
2025-08-08 05:46:36 +03:00
|
|
|
This generates configs based on user's subscription groups.
|
2025-08-05 01:23:07 +03:00
|
|
|
"""
|
2025-08-08 05:46:36 +03:00
|
|
|
from .models import User, AccessLog
|
|
|
|
from .models_xray import UserSubscription
|
2025-08-05 01:23:07 +03:00
|
|
|
import logging
|
|
|
|
from django.utils import timezone
|
|
|
|
import base64
|
2025-08-08 05:46:36 +03:00
|
|
|
import uuid
|
|
|
|
import json
|
2025-08-05 01:23:07 +03:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
# Clean user_hash from any trailing slashes
|
|
|
|
user_hash = user_hash.rstrip('/')
|
|
|
|
|
2025-08-05 01:23:07 +03:00
|
|
|
try:
|
2025-08-08 05:46:36 +03:00
|
|
|
user = get_object_or_404(User, hash=user_hash)
|
|
|
|
logger.info(f"Found user {user.username} for Xray subscription generation")
|
2025-08-05 01:23:07 +03:00
|
|
|
except Http404:
|
2025-08-08 05:46:36 +03:00
|
|
|
logger.warning(f"User not found for hash: {user_hash}")
|
2025-08-05 01:23:07 +03:00
|
|
|
AccessLog.objects.create(
|
|
|
|
user=None,
|
|
|
|
server="Unknown",
|
2025-08-08 05:46:36 +03:00
|
|
|
acl_link_id=user_hash,
|
2025-08-05 01:23:07 +03:00
|
|
|
action="Failed",
|
2025-08-08 05:46:36 +03:00
|
|
|
data=f"User not found for hash: {user_hash}"
|
2025-08-05 01:23:07 +03:00
|
|
|
)
|
|
|
|
return HttpResponse("Not found", status=404)
|
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
# Check if this is a JSON request for web display
|
|
|
|
if request.GET.get('format') == 'json':
|
|
|
|
return xray_subscription_json(request, user, user_hash)
|
|
|
|
|
2025-08-05 01:23:07 +03:00
|
|
|
try:
|
2025-08-08 05:46:36 +03:00
|
|
|
# Check if specific group is requested
|
|
|
|
group_filter = request.GET.get('group')
|
|
|
|
|
|
|
|
# Get subscription groups for the user
|
|
|
|
user_subscriptions = UserSubscription.objects.filter(
|
|
|
|
user=user,
|
|
|
|
active=True,
|
|
|
|
subscription_group__is_active=True
|
|
|
|
).select_related('subscription_group').prefetch_related('subscription_group__inbounds')
|
|
|
|
|
|
|
|
# Filter by specific group if requested
|
|
|
|
if group_filter:
|
|
|
|
user_subscriptions = user_subscriptions.filter(subscription_group__name=group_filter)
|
|
|
|
logger.info(f"Filtering subscription for group: {group_filter}")
|
|
|
|
|
2025-08-05 01:23:07 +03:00
|
|
|
subscription_configs = []
|
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
for subscription in user_subscriptions:
|
|
|
|
group = subscription.subscription_group
|
|
|
|
logger.info(f"Processing subscription group {group.name} for user {user.username}")
|
2025-08-05 01:23:07 +03:00
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
# Get all inbounds from this group
|
|
|
|
for inbound in group.inbounds.all():
|
2025-08-05 01:23:07 +03:00
|
|
|
try:
|
2025-08-08 07:39:01 +03:00
|
|
|
# Find all servers where this inbound is deployed
|
|
|
|
from .models_xray import ServerInbound
|
|
|
|
deployed_servers = ServerInbound.objects.filter(
|
|
|
|
inbound=inbound,
|
|
|
|
active=True
|
|
|
|
).select_related('server')
|
2025-08-08 05:46:36 +03:00
|
|
|
|
2025-08-08 07:39:01 +03:00
|
|
|
# Generate connection string for each server where inbound is deployed
|
|
|
|
for server_inbound in deployed_servers:
|
|
|
|
server = server_inbound.server
|
|
|
|
# Get server's client_hostname for XrayServerV2
|
|
|
|
server_hostname = getattr(server.get_real_instance(), 'client_hostname', None)
|
|
|
|
connection_string = generate_xray_connection_string(user, inbound, server.name, server_hostname)
|
|
|
|
|
|
|
|
if connection_string:
|
|
|
|
subscription_configs.append(connection_string)
|
|
|
|
logger.info(f"Added {inbound.protocol} config for inbound {inbound.name} on server {server.name}")
|
2025-08-08 05:46:36 +03:00
|
|
|
|
2025-08-05 01:23:07 +03:00
|
|
|
except Exception as e:
|
2025-08-08 05:46:36 +03:00
|
|
|
logger.warning(f"Failed to generate config for inbound {inbound.name}: {e}")
|
|
|
|
continue
|
2025-08-05 01:23:07 +03:00
|
|
|
|
|
|
|
if not subscription_configs:
|
2025-08-08 05:46:36 +03:00
|
|
|
group_msg = f" for group '{group_filter}'" if group_filter else ""
|
|
|
|
logger.warning(f"No Xray configurations found for user {user.username}{group_msg}")
|
2025-08-05 01:23:07 +03:00
|
|
|
AccessLog.objects.create(
|
2025-08-08 05:46:36 +03:00
|
|
|
user=user.username,
|
|
|
|
server="Xray-Subscription",
|
|
|
|
acl_link_id=user_hash,
|
2025-08-05 01:23:07 +03:00
|
|
|
action="Failed",
|
2025-08-08 05:46:36 +03:00
|
|
|
data=f"No Xray configurations available{group_msg}"
|
2025-08-05 01:23:07 +03:00
|
|
|
)
|
2025-08-08 05:46:36 +03:00
|
|
|
return HttpResponse(f"No configurations available{group_msg}", status=404)
|
2025-08-05 01:23:07 +03:00
|
|
|
|
|
|
|
# Join all configs with newlines and encode in base64 for subscription format
|
|
|
|
subscription_content = '\n'.join(subscription_configs)
|
2025-08-08 05:46:36 +03:00
|
|
|
logger.info(f"Raw subscription content for {user.username}: {len(subscription_configs)} configs")
|
2025-08-05 01:23:07 +03:00
|
|
|
|
|
|
|
subscription_b64 = base64.b64encode(subscription_content.encode('utf-8')).decode('utf-8')
|
|
|
|
logger.info(f"Base64 subscription length: {len(subscription_b64)}")
|
|
|
|
|
|
|
|
# Create access log
|
2025-08-08 05:46:36 +03:00
|
|
|
group_msg = f" for group '{group_filter}'" if group_filter else ""
|
2025-08-05 01:23:07 +03:00
|
|
|
AccessLog.objects.create(
|
2025-08-08 05:46:36 +03:00
|
|
|
user=user.username,
|
2025-08-05 01:23:07 +03:00
|
|
|
server="Xray-Subscription",
|
2025-08-08 05:46:36 +03:00
|
|
|
acl_link_id=user_hash,
|
2025-08-05 01:23:07 +03:00
|
|
|
action="Success",
|
2025-08-08 05:46:36 +03:00
|
|
|
data=f"Generated subscription with {len(subscription_configs)} configs from {user_subscriptions.count()} groups{group_msg}"
|
2025-08-05 01:23:07 +03:00
|
|
|
)
|
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
logger.info(f"Generated Xray subscription for {user.username} with {len(subscription_configs)} configs{group_msg}")
|
2025-08-05 01:23:07 +03:00
|
|
|
|
|
|
|
# Return with proper headers for subscription
|
|
|
|
response = HttpResponse(subscription_b64, content_type="text/plain; charset=utf-8")
|
2025-08-08 12:41:33 +03:00
|
|
|
response['Content-Disposition'] = f'attachment; filename="{user.username}"'
|
2025-08-05 01:23:07 +03:00
|
|
|
response['Cache-Control'] = 'no-cache'
|
2025-08-08 12:41:33 +03:00
|
|
|
|
|
|
|
# Add subscription-specific headers like other providers
|
|
|
|
import base64 as b64
|
|
|
|
profile_title_b64 = b64.b64encode("OutFleet VPN".encode('utf-8')).decode('utf-8')
|
|
|
|
response['profile-title'] = f'base64:{profile_title_b64}'
|
|
|
|
response['profile-update-interval'] = '24' # Update every 24 hours
|
|
|
|
response['profile-web-page-url'] = f'https://{request.get_host()}/u/{user_hash}'
|
|
|
|
response['support-url'] = f'https://{request.get_host()}/admin/'
|
|
|
|
|
|
|
|
# Add user info without limits (unlimited service)
|
|
|
|
# Set very high limits to indicate "unlimited"
|
|
|
|
import time
|
|
|
|
expire_timestamp = int(time.time()) + (365 * 24 * 60 * 60) # 1 year from now
|
|
|
|
response['subscription-userinfo'] = f'upload=0; download=0; total=1099511627776; expire={expire_timestamp}'
|
|
|
|
|
2025-08-05 01:23:07 +03:00
|
|
|
return response
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-08-08 05:46:36 +03:00
|
|
|
logger.error(f"Failed to generate Xray subscription for {user.username}: {e}")
|
2025-08-05 01:23:07 +03:00
|
|
|
AccessLog.objects.create(
|
2025-08-08 05:46:36 +03:00
|
|
|
user=user.username,
|
2025-08-05 01:23:07 +03:00
|
|
|
server="Xray-Subscription",
|
2025-08-08 05:46:36 +03:00
|
|
|
acl_link_id=user_hash,
|
2025-08-05 01:23:07 +03:00
|
|
|
action="Failed",
|
|
|
|
data=f"Failed to generate subscription: {e}"
|
|
|
|
)
|
|
|
|
return HttpResponse(f"Error generating subscription: {e}", status=500)
|
|
|
|
|
2025-08-08 05:46:36 +03:00
|
|
|
|
|
|
|
def xray_subscription_json(request, user, user_hash):
|
|
|
|
"""Return Xray subscription in JSON format for web display"""
|
|
|
|
from .models_xray import UserSubscription
|
|
|
|
import logging
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Get all active subscription groups for the user
|
|
|
|
user_subscriptions = UserSubscription.objects.filter(
|
|
|
|
user=user,
|
|
|
|
active=True,
|
|
|
|
subscription_group__is_active=True
|
|
|
|
).select_related('subscription_group').prefetch_related('subscription_group__inbounds')
|
|
|
|
|
|
|
|
groups_data = {}
|
|
|
|
|
|
|
|
for subscription in user_subscriptions:
|
|
|
|
group = subscription.subscription_group
|
|
|
|
group_configs = []
|
|
|
|
|
|
|
|
# Get all inbounds from this group
|
|
|
|
for inbound in group.inbounds.all():
|
|
|
|
try:
|
2025-08-08 07:39:01 +03:00
|
|
|
# Find all servers where this inbound is deployed
|
|
|
|
from .models_xray import ServerInbound
|
|
|
|
deployed_servers = ServerInbound.objects.filter(
|
|
|
|
inbound=inbound,
|
|
|
|
active=True
|
|
|
|
).select_related('server')
|
2025-08-08 05:46:36 +03:00
|
|
|
|
2025-08-08 07:39:01 +03:00
|
|
|
# Generate connection string for each server where inbound is deployed
|
|
|
|
for server_inbound in deployed_servers:
|
|
|
|
server = server_inbound.server
|
|
|
|
# Get server's client_hostname for XrayServerV2
|
|
|
|
server_hostname = getattr(server.get_real_instance(), 'client_hostname', None)
|
|
|
|
connection_string = generate_xray_connection_string(user, inbound, server.name, server_hostname)
|
|
|
|
|
|
|
|
if connection_string:
|
|
|
|
config_name = f"{server.name} {inbound.name}"
|
|
|
|
group_configs.append({
|
|
|
|
'name': config_name,
|
2025-08-08 05:46:36 +03:00
|
|
|
'protocol': inbound.protocol.upper(),
|
|
|
|
'port': inbound.port,
|
|
|
|
'network': inbound.network,
|
|
|
|
'security': inbound.security,
|
2025-08-08 07:39:01 +03:00
|
|
|
'domain': host,
|
2025-08-08 05:46:36 +03:00
|
|
|
'connection_string': connection_string
|
|
|
|
})
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
logger.warning(f"Failed to generate config for inbound {inbound.name}: {e}")
|
|
|
|
continue
|
|
|
|
|
|
|
|
if group_configs:
|
|
|
|
groups_data[group.name] = {
|
|
|
|
'group_name': group.name,
|
|
|
|
'description': group.description,
|
|
|
|
'configs': group_configs
|
|
|
|
}
|
|
|
|
|
|
|
|
return JsonResponse(groups_data)
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
logger.error(f"Failed to generate Xray JSON for {user.username}: {e}")
|
|
|
|
return JsonResponse({'error': str(e)}, status=500)
|
|
|
|
|
|
|
|
|
2025-08-08 07:39:01 +03:00
|
|
|
def generate_xray_connection_string(user, inbound, server_name=None, server_hostname=None):
|
2025-08-08 05:46:36 +03:00
|
|
|
"""Generate Xray connection string for user and inbound"""
|
|
|
|
import uuid
|
|
|
|
import base64
|
|
|
|
import json
|
|
|
|
from urllib.parse import quote
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Generate user UUID based on user ID and inbound
|
|
|
|
user_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"{user.username}-{inbound.name}"))
|
|
|
|
|
2025-08-08 10:32:14 +03:00
|
|
|
# Get host (use server's client_hostname if available, fallback to external host)
|
|
|
|
host = server_hostname if server_hostname else get_external_host()
|
2025-08-08 05:46:36 +03:00
|
|
|
|
|
|
|
if inbound.protocol == 'vless':
|
|
|
|
# VLESS URL format: vless://uuid@host:port?params#name
|
|
|
|
params = []
|
|
|
|
|
2025-08-08 12:41:33 +03:00
|
|
|
# Always add transport type for VLESS
|
|
|
|
params.append(f"type={inbound.network}")
|
2025-08-08 05:46:36 +03:00
|
|
|
|
|
|
|
if inbound.security != 'none':
|
|
|
|
params.append(f"security={inbound.security}")
|
2025-08-08 07:39:01 +03:00
|
|
|
if inbound.security == 'tls' and host:
|
|
|
|
params.append(f"sni={host}")
|
2025-08-08 05:46:36 +03:00
|
|
|
|
|
|
|
if inbound.network == 'ws':
|
|
|
|
params.append(f"path=/{inbound.name}")
|
|
|
|
elif inbound.network == 'grpc':
|
|
|
|
params.append(f"serviceName={inbound.name}")
|
|
|
|
|
|
|
|
param_string = '&'.join(params)
|
|
|
|
query_part = f"?{param_string}" if param_string else ""
|
|
|
|
|
2025-08-08 07:39:01 +03:00
|
|
|
# Generate config name: ServerName InboundName (e.g., "Israel VLESS-Premium")
|
|
|
|
config_name = f"{server_name} {inbound.name}" if server_name else inbound.name
|
|
|
|
connection_string = f"vless://{user_uuid}@{host}:{inbound.port}{query_part}#{quote(config_name)}"
|
2025-08-08 05:46:36 +03:00
|
|
|
|
|
|
|
elif inbound.protocol == 'vmess':
|
|
|
|
# VMess JSON format encoded in base64
|
2025-08-08 07:39:01 +03:00
|
|
|
# Generate config name: ServerName InboundName (e.g., "Israel VMESS-Premium")
|
|
|
|
config_name = f"{server_name} {inbound.name}" if server_name else inbound.name
|
2025-08-08 05:46:36 +03:00
|
|
|
vmess_config = {
|
|
|
|
"v": "2",
|
2025-08-08 07:39:01 +03:00
|
|
|
"ps": config_name,
|
2025-08-08 05:46:36 +03:00
|
|
|
"add": host,
|
|
|
|
"port": str(inbound.port),
|
|
|
|
"id": user_uuid,
|
|
|
|
"aid": "0",
|
|
|
|
"scy": "auto",
|
|
|
|
"net": inbound.network,
|
|
|
|
"type": "none",
|
2025-08-08 07:39:01 +03:00
|
|
|
"host": host if host else "",
|
2025-08-08 05:46:36 +03:00
|
|
|
"path": f"/{inbound.name}" if inbound.network == 'ws' else "",
|
|
|
|
"tls": inbound.security if inbound.security != 'none' else ""
|
|
|
|
}
|
|
|
|
|
|
|
|
vmess_json = json.dumps(vmess_config)
|
|
|
|
vmess_b64 = base64.b64encode(vmess_json.encode()).decode()
|
|
|
|
connection_string = f"vmess://{vmess_b64}"
|
|
|
|
|
|
|
|
elif inbound.protocol == 'trojan':
|
|
|
|
# Trojan URL format: trojan://password@host:port?params#name
|
|
|
|
# Use user UUID as password
|
|
|
|
params = []
|
|
|
|
|
2025-08-08 07:39:01 +03:00
|
|
|
if inbound.security != 'none' and host:
|
|
|
|
params.append(f"sni={host}")
|
2025-08-08 05:46:36 +03:00
|
|
|
|
|
|
|
if inbound.network != 'tcp':
|
|
|
|
params.append(f"type={inbound.network}")
|
|
|
|
if inbound.network == 'ws':
|
|
|
|
params.append(f"path=/{inbound.name}")
|
|
|
|
elif inbound.network == 'grpc':
|
|
|
|
params.append(f"serviceName={inbound.name}")
|
|
|
|
|
|
|
|
param_string = '&'.join(params)
|
|
|
|
query_part = f"?{param_string}" if param_string else ""
|
|
|
|
|
2025-08-08 07:39:01 +03:00
|
|
|
# Generate config name: ServerName InboundName (e.g., "Israel TROJAN-Premium")
|
|
|
|
config_name = f"{server_name} {inbound.name}" if server_name else inbound.name
|
|
|
|
connection_string = f"trojan://{user_uuid}@{host}:{inbound.port}{query_part}#{quote(config_name)}"
|
2025-08-08 05:46:36 +03:00
|
|
|
|
|
|
|
else:
|
|
|
|
# Fallback for unknown protocols
|
2025-08-08 07:39:01 +03:00
|
|
|
config_name = f"{server_name} {inbound.name}" if server_name else inbound.name
|
|
|
|
connection_string = f"{inbound.protocol}://{user_uuid}@{host}:{inbound.port}#{quote(config_name)}"
|
2025-08-08 05:46:36 +03:00
|
|
|
|
|
|
|
return connection_string
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
logger.error(f"Failed to generate connection string for {inbound.name}: {e}")
|
|
|
|
return None
|
|
|
|
|