Files
OutFleet/vpn/views.py
AB from home.homenet 787432cbcf Xray works
2025-08-08 05:46:36 +03:00

553 lines
23 KiB
Python

def userPortal(request, user_hash):
"""HTML portal for user to view their VPN access links and subscription groups"""
from .models import User
from .models_xray import UserSubscription, SubscriptionGroup, Inbound
from django.utils import timezone
from datetime import timedelta
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:
# 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')
logger.info(f"Found {user_subscriptions.count()} active subscription groups for user {user.username}")
# For now, set statistics to zero as we're transitioning systems
total_connections = 0
recent_connections = 0
logger.info(f"Using zero stats during transition for user {user.username}")
# Determine protocol scheme
scheme = 'https' if request.is_secure() else 'http'
# Group inbounds by subscription group
groups_data = {}
total_inbounds = 0
for subscription in user_subscriptions:
group = subscription.subscription_group
group_name = group.name
logger.debug(f"Processing subscription group {group_name}")
# Get all inbounds for this group
group_inbounds = group.inbounds.all()
groups_data[group_name] = {
'group': group,
'subscription': subscription,
'inbounds': [],
'total_connections': 0, # Placeholder during transition
}
for inbound in group_inbounds:
logger.debug(f"Processing inbound {inbound.name} in group {group_name}")
# Generate connection URLs based on protocol
connection_urls = []
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
connection_url = f"vless://user-uuid@{inbound.domain or EXTERNAL_ADDRESS}:{inbound.port}#{inbound.name}"
connection_urls.append({
'url': connection_url,
'protocol': 'VLESS',
'name': inbound.name
})
elif inbound.protocol == 'vmess':
# Generate VMess URL - placeholder
connection_url = f"vmess://user-config@{inbound.domain or EXTERNAL_ADDRESS}:{inbound.port}#{inbound.name}"
connection_urls.append({
'url': connection_url,
'protocol': 'VMess',
'name': inbound.name
})
elif inbound.protocol == 'trojan':
# Generate Trojan URL - placeholder
connection_url = f"trojan://user-password@{inbound.domain or EXTERNAL_ADDRESS}:{inbound.port}#{inbound.name}"
connection_urls.append({
'url': connection_url,
'protocol': 'Trojan',
'name': inbound.name
})
elif inbound.protocol == 'shadowsocks':
# Generate Shadowsocks URL - placeholder
connection_url = f"ss://user-config@{inbound.domain or EXTERNAL_ADDRESS}:{inbound.port}#{inbound.name}"
connection_urls.append({
'url': connection_url,
'protocol': 'Shadowsocks',
'name': inbound.name
})
inbound_data = {
'inbound': inbound,
'connection_urls': connection_urls,
'protocol': inbound.protocol.upper(),
'port': inbound.port,
'domain': inbound.domain or EXTERNAL_ADDRESS,
'network': inbound.network,
'security': inbound.security,
'connections': 0, # Placeholder during transition
'last_access_display': "Never used", # Placeholder
}
groups_data[group_name]['inbounds'].append(inbound_data)
total_inbounds += 1
logger.debug(f"Added inbound data for {inbound.name}")
logger.info(f"Prepared data for {len(groups_data)} subscription groups and {total_inbounds} total inbounds")
logger.info(f"Portal statistics: total_connections={total_connections}, recent_connections={recent_connections}")
# Check if user has any Xray subscription groups
has_xray_access = user_subscriptions.exists()
# 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
context = {
'user': user,
'user_subscriptions': user_subscriptions, # For accessing user's subscriptions in template
'groups_data': groups_data,
'total_groups': len(groups_data),
'total_inbounds': total_inbounds,
'total_connections': total_connections,
'recent_connections': recent_connections,
'external_address': EXTERNAL_ADDRESS,
'has_xray_access': has_xray_access,
'force_scheme': scheme, # Override request.scheme in template
'acl_links': acl_links, # For backwards compatibility
'has_old_links': len(acl_links) > 0,
'xray_subscription_url': f"{EXTERNAL_ADDRESS}/xray/{user.hash}",
}
logger.debug(f"Context prepared with keys: {list(context.keys())}")
logger.debug(f"Groups in context: {list(groups_data.keys())}")
# 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']}")
return render(request, 'vpn/user_portal.html', context)
except Exception as e:
logger.error(f"Error loading user portal for {user.username}: {e}", exc_info=True)
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)
import yaml
import json
import logging
from django.shortcuts import get_object_or_404, render
from django.http import JsonResponse, HttpResponse, Http404
from mysite.settings import EXTERNAL_ADDRESS
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] = []
acl_links[server_name].append({"link": f"{EXTERNAL_ADDRESS}/ss/{link.link}#{link.acl.server.name}", "comment": link.comment})
return JsonResponse(acl_links)
def shadowsocks(request, link):
from .models import ACLLink, AccessLog
import logging
from django.utils import timezone
logger = logging.getLogger(__name__)
try:
acl_link = get_object_or_404(ACLLink, link=link)
acl = acl_link.acl
logger.info(f"Found ACL link for user {acl.user.username} on server {acl.server.name}")
except Http404:
logger.warning(f"ACL link not found: {link}")
AccessLog.objects.create(
user=None,
server="Unknown",
acl_link_id=link,
action="Failed",
data=f"ACL not found for link: {link}"
)
return JsonResponse({"error": "Not allowed"}, status=403)
try:
server_user = acl.server.get_user(acl.user, raw=True)
logger.info(f"Successfully retrieved user credentials for {acl.user.username} from {acl.server.name}")
except Exception as e:
logger.error(f"Failed to get user credentials for {acl.user.username} from {acl.server.name}: {e}")
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}"
)
return JsonResponse({"error": f"Couldn't get credentials from server. {e}"}, status=500)
# 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', '')
if request.GET.get('mode') == 'json':
config = {
"info": "Managed by OutFleet_2 [github.com/house-of-vanity/OutFleet/]",
"password": password,
"method": method,
"prefix": "\u0005\u00dc_\u00e0\u0001",
"server": acl.server.client_hostname,
"server_port": port,
"access_url": access_url,
"outfleet": {
"acl_link": link,
"server_name": acl.server.name,
"server_type": acl.server.server_type,
}
}
response = json.dumps(config, indent=2)
else:
config = {
"transport": {
"$type": "tcpudp",
"tcp": {
"$type": "shadowsocks",
"endpoint": f"{acl.server.client_hostname}:{port}",
"cipher": f"{method}",
"secret": f"{password}",
"prefix": "\u0005\u00dc_\u00e0\u0001"
},
"udp": {
"$type": "shadowsocks",
"endpoint": f"{acl.server.client_hostname}:{port}",
"cipher": f"{method}",
"secret": f"{password}",
"prefix": "\u0005\u00dc_\u00e0\u0001"
}
}
}
response = yaml.dump(config, allow_unicode=True)
# Update last access time for this specific link
acl_link.last_access_time = timezone.now()
acl_link.save(update_fields=['last_access_time'])
# 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
)
return HttpResponse(response, content_type=f"{ 'application/json; charset=utf-8' if request.GET.get('mode') == 'json' else 'text/html' }")
def xray_subscription(request, user_hash):
"""
Return Xray subscription with all available protocols for the user.
This generates configs based on user's subscription groups.
"""
from .models import User, AccessLog
from .models_xray import UserSubscription
import logging
from django.utils import timezone
import base64
import uuid
import json
logger = logging.getLogger(__name__)
# Clean user_hash from any trailing slashes
user_hash = user_hash.rstrip('/')
try:
user = get_object_or_404(User, hash=user_hash)
logger.info(f"Found user {user.username} for Xray subscription generation")
except Http404:
logger.warning(f"User not found for hash: {user_hash}")
AccessLog.objects.create(
user=None,
server="Unknown",
acl_link_id=user_hash,
action="Failed",
data=f"User not found for hash: {user_hash}"
)
return HttpResponse("Not found", status=404)
# Check if this is a JSON request for web display
if request.GET.get('format') == 'json':
return xray_subscription_json(request, user, user_hash)
try:
# 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}")
subscription_configs = []
for subscription in user_subscriptions:
group = subscription.subscription_group
logger.info(f"Processing subscription group {group.name} for user {user.username}")
# Get all inbounds from this group
for inbound in group.inbounds.all():
try:
# Generate connection string based on protocol
connection_string = generate_xray_connection_string(user, inbound)
if connection_string:
subscription_configs.append(connection_string)
logger.info(f"Added {inbound.protocol} config for inbound {inbound.name}")
except Exception as e:
logger.warning(f"Failed to generate config for inbound {inbound.name}: {e}")
continue
if not subscription_configs:
group_msg = f" for group '{group_filter}'" if group_filter else ""
logger.warning(f"No Xray configurations found for user {user.username}{group_msg}")
AccessLog.objects.create(
user=user.username,
server="Xray-Subscription",
acl_link_id=user_hash,
action="Failed",
data=f"No Xray configurations available{group_msg}"
)
return HttpResponse(f"No configurations available{group_msg}", status=404)
# Join all configs with newlines and encode in base64 for subscription format
subscription_content = '\n'.join(subscription_configs)
logger.info(f"Raw subscription content for {user.username}: {len(subscription_configs)} configs")
subscription_b64 = base64.b64encode(subscription_content.encode('utf-8')).decode('utf-8')
logger.info(f"Base64 subscription length: {len(subscription_b64)}")
# Create access log
group_msg = f" for group '{group_filter}'" if group_filter else ""
AccessLog.objects.create(
user=user.username,
server="Xray-Subscription",
acl_link_id=user_hash,
action="Success",
data=f"Generated subscription with {len(subscription_configs)} configs from {user_subscriptions.count()} groups{group_msg}"
)
logger.info(f"Generated Xray subscription for {user.username} with {len(subscription_configs)} configs{group_msg}")
# Return with proper headers for subscription
response = HttpResponse(subscription_b64, content_type="text/plain; charset=utf-8")
response['Content-Disposition'] = f'attachment; filename="{user.username}_xray_subscription.txt"'
response['Cache-Control'] = 'no-cache'
return response
except Exception as e:
logger.error(f"Failed to generate Xray subscription for {user.username}: {e}")
AccessLog.objects.create(
user=user.username,
server="Xray-Subscription",
acl_link_id=user_hash,
action="Failed",
data=f"Failed to generate subscription: {e}"
)
return HttpResponse(f"Error generating subscription: {e}", status=500)
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:
# Generate connection string
connection_string = generate_xray_connection_string(user, inbound)
if connection_string:
group_configs.append({
'name': inbound.name,
'protocol': inbound.protocol.upper(),
'port': inbound.port,
'network': inbound.network,
'security': inbound.security,
'domain': inbound.domain,
'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)
def generate_xray_connection_string(user, inbound):
"""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}"))
# Get host (domain or EXTERNAL_ADDRESS)
host = inbound.domain if inbound.domain else EXTERNAL_ADDRESS
if inbound.protocol == 'vless':
# VLESS URL format: vless://uuid@host:port?params#name
params = []
if inbound.network != 'tcp':
params.append(f"type={inbound.network}")
if inbound.security != 'none':
params.append(f"security={inbound.security}")
if inbound.security == 'tls' and inbound.domain:
params.append(f"sni={inbound.domain}")
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 ""
connection_string = f"vless://{user_uuid}@{host}:{inbound.port}{query_part}#{quote(inbound.name)}"
elif inbound.protocol == 'vmess':
# VMess JSON format encoded in base64
vmess_config = {
"v": "2",
"ps": inbound.name,
"add": host,
"port": str(inbound.port),
"id": user_uuid,
"aid": "0",
"scy": "auto",
"net": inbound.network,
"type": "none",
"host": inbound.domain if inbound.domain else "",
"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 = []
if inbound.security != 'none' and inbound.domain:
params.append(f"sni={inbound.domain}")
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 ""
connection_string = f"trojan://{user_uuid}@{host}:{inbound.port}{query_part}#{quote(inbound.name)}"
else:
# Fallback for unknown protocols
connection_string = f"{inbound.protocol}://{user_uuid}@{host}:{inbound.port}#{quote(inbound.name)}"
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