mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-08-21 14:37:16 +00:00
405 lines
18 KiB
Python
405 lines
18 KiB
Python
def userPortal(request, user_hash):
|
|
"""HTML portal for user to view their VPN access links and server information"""
|
|
from .models import User, ACLLink, UserStatistics, AccessLog
|
|
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 ACL links for the user with server information
|
|
acl_links = ACLLink.objects.filter(acl__user=user).select_related('acl__server', 'acl')
|
|
logger.info(f"Found {acl_links.count()} ACL links for user {user.username}")
|
|
|
|
# Calculate overall statistics from cached data (only where cache exists)
|
|
user_stats = UserStatistics.objects.filter(user=user)
|
|
if user_stats.exists():
|
|
total_connections = sum(stat.total_connections for stat in user_stats)
|
|
recent_connections = sum(stat.recent_connections for stat in user_stats)
|
|
logger.info(f"User {user.username} cached stats: total_connections={total_connections}, recent_connections={recent_connections}")
|
|
else:
|
|
# No cache available, set to zero and suggest cache update
|
|
total_connections = 0
|
|
recent_connections = 0
|
|
logger.warning(f"No cached statistics found for user {user.username}. Run statistics update task.")
|
|
|
|
# Determine protocol scheme
|
|
scheme = 'https' if request.is_secure() else 'http'
|
|
|
|
# Group links by server
|
|
servers_data = {}
|
|
total_links = 0
|
|
|
|
for link in acl_links:
|
|
server = link.acl.server
|
|
server_name = server.name
|
|
logger.debug(f"Processing link {link.link} for server {server_name}")
|
|
|
|
if server_name not in servers_data:
|
|
# Get server status and info
|
|
try:
|
|
server_status = server.get_server_status()
|
|
server_accessible = True
|
|
server_error = None
|
|
logger.debug(f"Server {server_name} status retrieved successfully")
|
|
except Exception as e:
|
|
logger.warning(f"Could not get status for server {server_name}: {e}")
|
|
server_status = {}
|
|
server_accessible = False
|
|
server_error = str(e)
|
|
|
|
# Calculate server-level totals from cached stats (only where cache exists)
|
|
server_stats = user_stats.filter(server_name=server_name)
|
|
if server_stats.exists():
|
|
server_total_connections = sum(stat.total_connections for stat in server_stats)
|
|
else:
|
|
server_total_connections = 0
|
|
|
|
servers_data[server_name] = {
|
|
'server': server,
|
|
'status': server_status,
|
|
'accessible': server_accessible,
|
|
'error': server_error,
|
|
'links': [],
|
|
'server_type': server.server_type,
|
|
'total_connections': server_total_connections,
|
|
}
|
|
logger.debug(f"Created server data for {server_name} with {server_total_connections} cached connections")
|
|
|
|
# Calculate time since last access
|
|
last_access_display = "Never used"
|
|
if link.last_access_time:
|
|
time_diff = timezone.now() - link.last_access_time
|
|
if time_diff.days > 0:
|
|
last_access_display = f"{time_diff.days} days ago"
|
|
elif time_diff.seconds > 3600:
|
|
hours = time_diff.seconds // 3600
|
|
last_access_display = f"{hours} hours ago"
|
|
elif time_diff.seconds > 60:
|
|
minutes = time_diff.seconds // 60
|
|
last_access_display = f"{minutes} minutes ago"
|
|
else:
|
|
last_access_display = "Just now"
|
|
|
|
# Get cached statistics for this specific link
|
|
try:
|
|
link_stats = UserStatistics.objects.get(
|
|
user=user,
|
|
server_name=server_name,
|
|
acl_link_id=link.link
|
|
)
|
|
logger.debug(f"Found cached stats for link {link.link}: {link_stats.total_connections} connections, max_daily={link_stats.max_daily}")
|
|
|
|
link_connections = link_stats.total_connections
|
|
link_recent_connections = link_stats.recent_connections
|
|
daily_usage = link_stats.daily_usage or []
|
|
max_daily = link_stats.max_daily
|
|
|
|
except UserStatistics.DoesNotExist:
|
|
logger.warning(f"No cached stats found for link {link.link} on server {server_name}, using fallback")
|
|
|
|
# Fallback: Since AccessLog doesn't track specific links, show zero for link-specific stats
|
|
# but keep server-level stats for context
|
|
link_connections = 0
|
|
link_recent_connections = 0
|
|
daily_usage = [0] * 30 # Empty 30-day chart
|
|
max_daily = 0
|
|
|
|
logger.warning(f"Using zero stats for uncached link {link.link} - AccessLog doesn't track individual links")
|
|
|
|
logger.debug(f"Link {link.link} stats: connections={link_connections}, recent={link_recent_connections}, max_daily={max_daily}")
|
|
|
|
# Add link information with statistics
|
|
link_url = f"{EXTERNAL_ADDRESS}/ss/{link.link}#{server_name}"
|
|
|
|
link_data = {
|
|
'link': link,
|
|
'url': link_url,
|
|
'comment': link.comment or 'Default',
|
|
'last_access': link.last_access_time,
|
|
'last_access_display': last_access_display,
|
|
'connections': link_connections,
|
|
'recent_connections': link_recent_connections,
|
|
'daily_usage': daily_usage,
|
|
'max_daily': max_daily,
|
|
}
|
|
|
|
servers_data[server_name]['links'].append(link_data)
|
|
total_links += 1
|
|
|
|
logger.debug(f"Added comprehensive link data for {link.link}")
|
|
|
|
logger.info(f"Prepared data for {len(servers_data)} servers and {total_links} total links")
|
|
logger.info(f"Portal statistics: total_connections={total_connections}, recent_connections={recent_connections}")
|
|
|
|
# Check if user has access to any Xray servers
|
|
from vpn.server_plugins import XrayCoreServer, XrayInboundServer
|
|
has_xray_servers = any(
|
|
isinstance(acl_link.acl.server.get_real_instance(), (XrayCoreServer, XrayInboundServer))
|
|
for acl_link in acl_links
|
|
)
|
|
|
|
context = {
|
|
'user': user,
|
|
'user_links': acl_links, # For accessing user's links in template
|
|
'servers_data': servers_data,
|
|
'total_servers': len(servers_data),
|
|
'total_links': total_links,
|
|
'total_connections': total_connections,
|
|
'recent_connections': recent_connections,
|
|
'external_address': EXTERNAL_ADDRESS,
|
|
'has_xray_servers': has_xray_servers,
|
|
'force_scheme': scheme, # Override request.scheme in template
|
|
}
|
|
|
|
logger.debug(f"Context prepared with keys: {list(context.keys())}")
|
|
logger.debug(f"Servers in context: {list(servers_data.keys())}")
|
|
|
|
# Log sample server data for debugging
|
|
for server_name, server_data in servers_data.items():
|
|
logger.debug(f"Server {server_name}: total_connections={server_data['total_connections']}, links_count={len(server_data['links'])}")
|
|
for i, link_data in enumerate(server_data['links']):
|
|
logger.debug(f" Link {i}: connections={link_data['connections']}, recent={link_data['recent_connections']}, last_access='{link_data['last_access_display']}'")
|
|
|
|
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)
|
|
|
|
if request.GET.get('mode') == 'json':
|
|
config = {
|
|
"info": "Managed by OutFleet_2 [github.com/house-of-vanity/OutFleet/]",
|
|
"password": server_user.password,
|
|
"method": server_user.method,
|
|
"prefix": "\u0005\u00dc_\u00e0\u0001",
|
|
"server": acl.server.client_hostname,
|
|
"server_port": server_user.port,
|
|
"access_url": server_user.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}:{server_user.port}",
|
|
"cipher": f"{server_user.method}",
|
|
"secret": f"{server_user.password}",
|
|
"prefix": "\u0005\u00dc_\u00e0\u0001"
|
|
},
|
|
"udp": {
|
|
"$type": "shadowsocks",
|
|
"endpoint": f"{acl.server.client_hostname}:{server_user.port}",
|
|
"cipher": f"{server_user.method}",
|
|
"secret": f"{server_user.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, link):
|
|
"""
|
|
Return Xray subscription with all available protocols for the user.
|
|
This generates a single subscription link that includes all inbounds the user has access to.
|
|
"""
|
|
from .models import ACLLink, AccessLog
|
|
from vpn.server_plugins import XrayCoreServer, XrayInboundServer
|
|
import logging
|
|
from django.utils import timezone
|
|
import base64
|
|
|
|
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 HttpResponse("Not found", status=404)
|
|
|
|
try:
|
|
# Get all servers this user has access to
|
|
user_acls = acl.user.acl_set.all()
|
|
subscription_configs = []
|
|
|
|
for user_acl in user_acls:
|
|
server = user_acl.server.get_real_instance()
|
|
|
|
# Handle XrayInboundServer (individual inbounds)
|
|
if isinstance(server, XrayInboundServer):
|
|
if server.xray_inbound:
|
|
config = server.get_user(acl.user, raw=True)
|
|
if config and 'connection_string' in config:
|
|
subscription_configs.append(config['connection_string'])
|
|
logger.info(f"Added XrayInboundServer config for {server.name}")
|
|
|
|
# Handle XrayCoreServer (parent server with multiple inbounds)
|
|
elif isinstance(server, XrayCoreServer):
|
|
try:
|
|
# Get all inbounds for this server that have this user
|
|
for inbound in server.inbounds.filter(enabled=True):
|
|
# Check if user has a client in this inbound
|
|
client = inbound.clients.filter(user=acl.user).first()
|
|
if client:
|
|
connection_string = server._generate_connection_string(client)
|
|
if connection_string:
|
|
subscription_configs.append(connection_string)
|
|
logger.info(f"Added inbound {inbound.tag} config for user {acl.user.username}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get configs from XrayCoreServer {server.name}: {e}")
|
|
|
|
if not subscription_configs:
|
|
logger.warning(f"No Xray configurations found for user {acl.user.username}")
|
|
AccessLog.objects.create(
|
|
user=acl.user.username,
|
|
server="Multiple",
|
|
acl_link_id=acl_link.link,
|
|
action="Failed",
|
|
data="No Xray configurations available"
|
|
)
|
|
return HttpResponse("No configurations available", 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 {acl.user.username}:\n{subscription_content}")
|
|
|
|
subscription_b64 = base64.b64encode(subscription_content.encode('utf-8')).decode('utf-8')
|
|
logger.info(f"Base64 subscription length: {len(subscription_b64)}")
|
|
|
|
# Update last access time
|
|
acl_link.last_access_time = timezone.now()
|
|
acl_link.save(update_fields=['last_access_time'])
|
|
|
|
# Create access log
|
|
AccessLog.objects.create(
|
|
user=acl.user.username,
|
|
server="Xray-Subscription",
|
|
acl_link_id=acl_link.link,
|
|
action="Success",
|
|
data=f"Generated subscription with {len(subscription_configs)} configs. Content: {subscription_content[:200]}..."
|
|
)
|
|
|
|
logger.info(f"Generated Xray subscription for {acl.user.username} with {len(subscription_configs)} configs")
|
|
|
|
# Return with proper headers for subscription
|
|
response = HttpResponse(subscription_b64, content_type="text/plain; charset=utf-8")
|
|
response['Content-Disposition'] = 'attachment; filename="xray_subscription.txt"'
|
|
response['Cache-Control'] = 'no-cache'
|
|
return response
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to generate Xray subscription for {acl.user.username}: {e}")
|
|
AccessLog.objects.create(
|
|
user=acl.user.username,
|
|
server="Xray-Subscription",
|
|
acl_link_id=acl_link.link,
|
|
action="Failed",
|
|
data=f"Failed to generate subscription: {e}"
|
|
)
|
|
return HttpResponse(f"Error generating subscription: {e}", status=500)
|
|
|