diff --git a/mysite/urls.py b/mysite/urls.py index 2a93aab..32a1951 100644 --- a/mysite/urls.py +++ b/mysite/urls.py @@ -17,12 +17,11 @@ Including another URLconf from django.contrib import admin from django.urls import path, include from django.views.generic import RedirectView -from vpn.views import shadowsocks, print_headers +from vpn.views import shadowsocks urlpatterns = [ path('admin/', admin.site.urls), path('ss/', shadowsocks, name='shadowsocks'), path('dynamic/', shadowsocks, name='shadowsocks'), - path('headers/', print_headers, name='print_headers'), path('', RedirectView.as_view(url='/admin/', permanent=False)), ] \ No newline at end of file diff --git a/vpn/admin.py b/vpn/admin.py index e6672aa..2713fbe 100644 --- a/vpn/admin.py +++ b/vpn/admin.py @@ -7,9 +7,9 @@ from django.utils.safestring import mark_safe from django.db.models import Count from django.contrib.auth.admin import UserAdmin -from .models import User - -from vpn.models import User, ACL +from .models import User, AccessLog +from django.utils.timezone import localtime +from vpn.models import User, ACL, ACLLink from vpn.forms import UserForm from .server_plugins import ( Server, @@ -23,6 +23,43 @@ admin.site.site_title = "VPN Manager" admin.site.site_header = "VPN Manager" admin.site.index_title = "OutFleet" +def format_object(data): + try: + if isinstance(data, dict): + formatted_data = json.dumps(data, indent=2) + return mark_safe(f"
{formatted_data}
") + elif isinstance(data, str): + return mark_safe(f"
{data}
") + else: + return mark_safe(f"
{str(data)}
") + except Exception as e: + return mark_safe(f"Error: {e}") + +class UserNameFilter(admin.SimpleListFilter): + title = 'User' + parameter_name = 'user' + + def lookups(self, request, model_admin): + users = set(User.objects.values_list('username', flat=True)) + return [(user, user) for user in users] + + def queryset(self, request, queryset): + if self.value(): + return queryset.filter(user__username=self.value()) + return queryset + +class ServerNameFilter(admin.SimpleListFilter): + title = 'Server Name' + parameter_name = 'acl__server__name' + + def lookups(self, request, model_admin): + servers = set(ACL.objects.values_list('server__name', flat=True)) + return [(server, server) for server in servers] + + def queryset(self, request, queryset): + if self.value(): + return queryset.filter(acl__server__name=self.value()) + return queryset @admin.register(Server) class ServerAdmin(PolymorphicParentModelAdmin): @@ -56,6 +93,7 @@ class ServerAdmin(PolymorphicParentModelAdmin): class UserAdmin(admin.ModelAdmin): form = UserForm list_display = ('username', 'comment', 'registration_date', 'hash', 'server_count') + list_editable = ('hash', ) search_fields = ('username', 'hash') readonly_fields = ('hash',) @@ -78,13 +116,40 @@ class UserAdmin(admin.ModelAdmin): for server in selected_servers: ACL.objects.get_or_create(user=obj, server=server) +@admin.register(AccessLog) +class AccessLogAdmin(admin.ModelAdmin): + list_display = ('user', 'server', 'action', 'formatted_timestamp') + list_filter = (UserNameFilter, ServerNameFilter, 'timestamp') + search_fields = ('accesslog__user', 'server', 'timestamp') + readonly_fields = ('server', 'user', 'formatted_timestamp', 'action', 'formated_data') + + @admin.display(description='Timestamp') + def formatted_timestamp(self, obj): + local_time = localtime(obj.timestamp) + return local_time.strftime('%Y-%m-%d %H:%M:%S %Z') + + @admin.display(description='Details') + def formated_data(self, obj): + return format_object(obj.data) + + +class ACLLinkInline(admin.TabularInline): + model = ACLLink + extra = 1 + help_text = 'Add or change ACL links' + verbose_name = 'Dynamic link' + verbose_name_plural = 'Dynamic links' + fields = ('link',) @admin.register(ACL) class ACLAdmin(admin.ModelAdmin): - list_display = ('user', 'server', 'server_type', 'link', 'created_at') - list_filter = ('user', 'server__server_type') - search_fields = ('user__name', 'server__name', 'server__comment', 'user__comment', 'link') - readonly_fields = ('user_info', ) + + list_display = ('user', 'server', 'server_type', 'display_links', 'created_at') + list_editable = ('server', ) + list_filter = (UserNameFilter, 'server__server_type', ServerNameFilter) + search_fields = ('user__name', 'server__name', 'server__comment', 'user__comment', 'links__link') + readonly_fields = ('user_info',) + inlines = [ACLLinkInline] @admin.display(description='Server Type', ordering='server__server_type') def server_type(self, obj): @@ -96,13 +161,12 @@ class ACLAdmin(admin.ModelAdmin): user = obj.user try: data = server.get_user(user) - - if isinstance(data, dict): - formatted_data = json.dumps(data, indent=2) - return mark_safe(f"
{formatted_data}
") - elif isinstance(data, str): - return mark_safe(f"
{data}
") - else: - return mark_safe(f"
{str(data)}
") + return format_object(data) except Exception as e: - return mark_safe(f"Error: {e}") \ No newline at end of file + return mark_safe(f"Error: {e}") + + @admin.display(description='Links') + def display_links(self, obj): + links = obj.links.all() + return mark_safe('
'.join([link.link for link in links])) + diff --git a/vpn/models.py b/vpn/models.py index 114636b..d36fbcc 100644 --- a/vpn/models.py +++ b/vpn/models.py @@ -8,14 +8,23 @@ import shortuuid from django.contrib.auth.models import AbstractUser +class AccessLog(models.Model): + user = models.CharField(max_length=256, blank=True, null=True, editable=False) + server = models.CharField(max_length=256, blank=True, null=True, editable=False) + action = models.CharField(max_length=100, editable=False) + data = models.TextField(default="", blank=True, editable=False) + timestamp = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.action} {self.user} request for {self.server} at {self.timestamp}" class User(AbstractUser): #is_active = False - comment = models.TextField(default="", blank=True) - registration_date = models.DateTimeField(auto_now_add=True) - servers = models.ManyToManyField('Server', through='ACL', blank=True) + comment = models.TextField(default="", blank=True, help_text="Free form user comment") + registration_date = models.DateTimeField(auto_now_add=True, verbose_name="Created") + servers = models.ManyToManyField('Server', through='ACL', blank=True, help_text="Servers user has access to") last_access = models.DateTimeField(null=True, blank=True) - hash = models.CharField(max_length=64, unique=True) + hash = models.CharField(max_length=64, unique=True, help_text="Random user hash. It's using for client config generation.") def get_servers(self): return Server.objects.filter(acl__user=self) @@ -32,22 +41,15 @@ class User(AbstractUser): class ACL(models.Model): user = models.ForeignKey('User', on_delete=models.CASCADE) server = models.ForeignKey('Server', on_delete=models.CASCADE) - created_at = models.DateTimeField(auto_now_add=True) - link = models.CharField(max_length=64, unique=True, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created") class Meta: constraints = [ models.UniqueConstraint(fields=['user', 'server'], name='unique_user_server') ] - def __str__(self): return f"{self.user.username} - {self.server.name}" - - def save(self, *args, **kwargs): - if not self.link: - self.link = shortuuid.ShortUUID().random(length=16) - super().save(*args, **kwargs) @receiver(post_save, sender=ACL) def acl_created_or_updated(sender, instance, created, **kwargs): @@ -55,4 +57,17 @@ def acl_created_or_updated(sender, instance, created, **kwargs): @receiver(pre_delete, sender=ACL) def acl_deleted(sender, instance, **kwargs): - sync_user.delay_on_commit(instance.user.id, instance.server.id) \ No newline at end of file + sync_user.delay_on_commit(instance.user.id, instance.server.id) + + +class ACLLink(models.Model): + acl = models.ForeignKey(ACL, related_name='links', on_delete=models.CASCADE) + link = models.CharField(max_length=64, unique=True, blank=True, null=True, verbose_name="Access link", help_text="Access link to get dynamic configuration") + + def save(self, *args, **kwargs): + if not self.link: + self.link = shortuuid.ShortUUID().random(length=16) + super().save(*args, **kwargs) + + def __str__(self): + return self.link \ No newline at end of file diff --git a/vpn/server_plugins/generic.py b/vpn/server_plugins/generic.py index 4b9a740..57fbd11 100644 --- a/vpn/server_plugins/generic.py +++ b/vpn/server_plugins/generic.py @@ -9,9 +9,9 @@ class Server(PolymorphicModel): ('Wireguard', 'Wireguard'), ) - name = models.CharField(max_length=100) + name = models.CharField(max_length=100, help_text="Server name") comment = models.TextField(default="", blank=True) - registration_date = models.DateTimeField(auto_now_add=True) + registration_date = models.DateTimeField(auto_now_add=True, verbose_name="Created") server_type = models.CharField(max_length=50, choices=SERVER_TYPE_CHOICES, editable=False) def __init__(self, *args, **kwargs): diff --git a/vpn/server_plugins/outline.py b/vpn/server_plugins/outline.py index 70e9fae..2c6f55a 100644 --- a/vpn/server_plugins/outline.py +++ b/vpn/server_plugins/outline.py @@ -37,11 +37,10 @@ class _FingerprintAdapter(requests.adapters.HTTPAdapter): class OutlineServer(Server): logger = logging.getLogger(__name__) - admin_url = models.URLField() - admin_access_cert = models.CharField(max_length=255) - client_server_name = models.CharField(max_length=255) - client_hostname = models.CharField(max_length=255) - client_port = models.CharField(max_length=5) + admin_url = models.URLField(help_text="Management URL") + admin_access_cert = models.CharField(max_length=255, help_text="Fingerprint") + client_hostname = models.CharField(max_length=255, help_text="Server address for clients") + client_port = models.CharField(max_length=5, help_text="Server port for clients") class Meta: verbose_name = 'Outline' @@ -146,7 +145,7 @@ class OutlineServer(Server): if server_user: if server_user.method != "chacha20-ietf-poly1305" or \ server_user.port != int(self.client_port) or \ - server_user.username != user.username or \ + server_user.name != user.username or \ server_user.password != user.hash or \ self.client.delete_key(user.hash): @@ -213,19 +212,18 @@ class OutlineServer(Server): class OutlineServerAdmin(PolymorphicChildModelAdmin): base_model = OutlineServer - show_in_index = False # Не отображать в главном списке админки + show_in_index = False list_display = ( 'name', 'admin_url', 'admin_access_cert', - 'client_server_name', 'client_hostname', 'client_port', - 'server_status_inline', 'user_count', - 'registration_date' + 'server_status_inline', ) - readonly_fields = ('server_status_full', ) + readonly_fields = ('server_status_full', 'registration_date',) + list_editable = ('admin_url', 'admin_access_cert', 'client_hostname', 'client_port',) exclude = ('server_type',) @admin.display(description='Clients', ordering='user_count') diff --git a/vpn/tasks.py b/vpn/tasks.py index efb37f4..9f9d523 100644 --- a/vpn/tasks.py +++ b/vpn/tasks.py @@ -29,11 +29,13 @@ def sync_all_users(): @shared_task(name="sync_all_users_on_server") def sync_users(server_id): from .models import Server - + status = {} try: server = Server.objects.get(id=server_id) - server.sync_users() - logger.info(f"Successfully synced users for server {server.name}") + sync = server.sync_users() + if sync: + logger.info(f"Successfully synced users for server {server.name}") + return f"Successfully synced users for server {server.name}" except Exception as e: logger.error(f"Error syncing users for server {server.name}: {e}") raise TaskFailedException(message=f"Error syncing users for server {server.name}") diff --git a/vpn/views.py b/vpn/views.py index 15c6651..b0ca342 100644 --- a/vpn/views.py +++ b/vpn/views.py @@ -1,21 +1,21 @@ +import json from django.shortcuts import get_object_or_404 -from django.http import JsonResponse -from django.http import HttpResponse +from django.http import JsonResponse, HttpResponse, Http404 -def print_headers(request): - headers = {key: value for key, value in request.META.items() if key.startswith('HTTP_')} - - for key, value in headers.items(): - print(f'{key}: {value}') - - return HttpResponse(f"Headers: {headers}") def shadowsocks(request, link): - from .models import ACL - acl = get_object_or_404(ACL, link=link) + from .models import ACLLink, AccessLog + try: + acl_link = get_object_or_404(ACLLink, link=link) + acl = acl_link.acl + except Http404: + AccessLog.objects.create(user=None, server="Unknown", 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) except Exception as e: + AccessLog.objects.create(user=acl.user, server=acl.server.name, action="Failed", data=f"{e}") return JsonResponse({"error": f"Couldn't get credentials from server. {e}"}) config = { @@ -26,7 +26,13 @@ def shadowsocks(request, link): "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, + } } - return JsonResponse(config) - - + + AccessLog.objects.create(user=acl.user, server=acl.server.name, action="Success", data=json.dumps(config, indent=2)) + + return JsonResponse(config) \ No newline at end of file