mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-07-06 17:14:07 +00:00
Added access logs.
This commit is contained in:
@ -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/<path:link>', shadowsocks, name='shadowsocks'),
|
||||
path('dynamic/<path:link>', shadowsocks, name='shadowsocks'),
|
||||
path('headers/', print_headers, name='print_headers'),
|
||||
path('', RedirectView.as_view(url='/admin/', permanent=False)),
|
||||
]
|
96
vpn/admin.py
96
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"<pre>{formatted_data}</pre>")
|
||||
elif isinstance(data, str):
|
||||
return mark_safe(f"<pre>{data}</pre>")
|
||||
else:
|
||||
return mark_safe(f"<pre>{str(data)}</pre>")
|
||||
except Exception as e:
|
||||
return mark_safe(f"<span style='color: red;'>Error: {e}</span>")
|
||||
|
||||
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"<pre>{formatted_data}</pre>")
|
||||
elif isinstance(data, str):
|
||||
return mark_safe(f"<pre>{data}</pre>")
|
||||
else:
|
||||
return mark_safe(f"<pre>{str(data)}</pre>")
|
||||
return format_object(data)
|
||||
except Exception as e:
|
||||
return mark_safe(f"<span style='color: red;'>Error: {e}</span>")
|
||||
return mark_safe(f"<span style='color: red;'>Error: {e}</span>")
|
||||
|
||||
@admin.display(description='Links')
|
||||
def display_links(self, obj):
|
||||
links = obj.links.all()
|
||||
return mark_safe('<br>'.join([link.link for link in links]))
|
||||
|
||||
|
@ -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)
|
||||
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
|
@ -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):
|
||||
|
@ -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')
|
||||
|
@ -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}")
|
||||
|
34
vpn/views.py
34
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)
|
Reference in New Issue
Block a user