mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-07-07 01:24:06 +00:00
Added access logs.
This commit is contained in:
@ -17,12 +17,11 @@ Including another URLconf
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
from vpn.views import shadowsocks, print_headers
|
from vpn.views import shadowsocks
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('ss/<path:link>', shadowsocks, name='shadowsocks'),
|
path('ss/<path:link>', shadowsocks, name='shadowsocks'),
|
||||||
path('dynamic/<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)),
|
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.db.models import Count
|
||||||
|
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from .models import User
|
from .models import User, AccessLog
|
||||||
|
from django.utils.timezone import localtime
|
||||||
from vpn.models import User, ACL
|
from vpn.models import User, ACL, ACLLink
|
||||||
from vpn.forms import UserForm
|
from vpn.forms import UserForm
|
||||||
from .server_plugins import (
|
from .server_plugins import (
|
||||||
Server,
|
Server,
|
||||||
@ -23,6 +23,43 @@ admin.site.site_title = "VPN Manager"
|
|||||||
admin.site.site_header = "VPN Manager"
|
admin.site.site_header = "VPN Manager"
|
||||||
admin.site.index_title = "OutFleet"
|
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)
|
@admin.register(Server)
|
||||||
class ServerAdmin(PolymorphicParentModelAdmin):
|
class ServerAdmin(PolymorphicParentModelAdmin):
|
||||||
@ -56,6 +93,7 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
class UserAdmin(admin.ModelAdmin):
|
class UserAdmin(admin.ModelAdmin):
|
||||||
form = UserForm
|
form = UserForm
|
||||||
list_display = ('username', 'comment', 'registration_date', 'hash', 'server_count')
|
list_display = ('username', 'comment', 'registration_date', 'hash', 'server_count')
|
||||||
|
list_editable = ('hash', )
|
||||||
search_fields = ('username', 'hash')
|
search_fields = ('username', 'hash')
|
||||||
readonly_fields = ('hash',)
|
readonly_fields = ('hash',)
|
||||||
|
|
||||||
@ -78,13 +116,40 @@ class UserAdmin(admin.ModelAdmin):
|
|||||||
for server in selected_servers:
|
for server in selected_servers:
|
||||||
ACL.objects.get_or_create(user=obj, server=server)
|
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)
|
@admin.register(ACL)
|
||||||
class ACLAdmin(admin.ModelAdmin):
|
class ACLAdmin(admin.ModelAdmin):
|
||||||
list_display = ('user', 'server', 'server_type', 'link', 'created_at')
|
|
||||||
list_filter = ('user', 'server__server_type')
|
list_display = ('user', 'server', 'server_type', 'display_links', 'created_at')
|
||||||
search_fields = ('user__name', 'server__name', 'server__comment', 'user__comment', 'link')
|
list_editable = ('server', )
|
||||||
readonly_fields = ('user_info', )
|
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')
|
@admin.display(description='Server Type', ordering='server__server_type')
|
||||||
def server_type(self, obj):
|
def server_type(self, obj):
|
||||||
@ -96,13 +161,12 @@ class ACLAdmin(admin.ModelAdmin):
|
|||||||
user = obj.user
|
user = obj.user
|
||||||
try:
|
try:
|
||||||
data = server.get_user(user)
|
data = server.get_user(user)
|
||||||
|
return format_object(data)
|
||||||
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:
|
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
|
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):
|
class User(AbstractUser):
|
||||||
#is_active = False
|
#is_active = False
|
||||||
comment = models.TextField(default="", blank=True)
|
comment = models.TextField(default="", blank=True, help_text="Free form user comment")
|
||||||
registration_date = models.DateTimeField(auto_now_add=True)
|
registration_date = models.DateTimeField(auto_now_add=True, verbose_name="Created")
|
||||||
servers = models.ManyToManyField('Server', through='ACL', blank=True)
|
servers = models.ManyToManyField('Server', through='ACL', blank=True, help_text="Servers user has access to")
|
||||||
last_access = models.DateTimeField(null=True, blank=True)
|
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):
|
def get_servers(self):
|
||||||
return Server.objects.filter(acl__user=self)
|
return Server.objects.filter(acl__user=self)
|
||||||
@ -32,22 +41,15 @@ class User(AbstractUser):
|
|||||||
class ACL(models.Model):
|
class ACL(models.Model):
|
||||||
user = models.ForeignKey('User', on_delete=models.CASCADE)
|
user = models.ForeignKey('User', on_delete=models.CASCADE)
|
||||||
server = models.ForeignKey('Server', on_delete=models.CASCADE)
|
server = models.ForeignKey('Server', on_delete=models.CASCADE)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created")
|
||||||
link = models.CharField(max_length=64, unique=True, blank=True, null=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(fields=['user', 'server'], name='unique_user_server')
|
models.UniqueConstraint(fields=['user', 'server'], name='unique_user_server')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user.username} - {self.server.name}"
|
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)
|
@receiver(post_save, sender=ACL)
|
||||||
def acl_created_or_updated(sender, instance, created, **kwargs):
|
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)
|
@receiver(pre_delete, sender=ACL)
|
||||||
def acl_deleted(sender, instance, **kwargs):
|
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'),
|
('Wireguard', 'Wireguard'),
|
||||||
)
|
)
|
||||||
|
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100, help_text="Server name")
|
||||||
comment = models.TextField(default="", blank=True)
|
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)
|
server_type = models.CharField(max_length=50, choices=SERVER_TYPE_CHOICES, editable=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -37,11 +37,10 @@ class _FingerprintAdapter(requests.adapters.HTTPAdapter):
|
|||||||
|
|
||||||
class OutlineServer(Server):
|
class OutlineServer(Server):
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
admin_url = models.URLField()
|
admin_url = models.URLField(help_text="Management URL")
|
||||||
admin_access_cert = models.CharField(max_length=255)
|
admin_access_cert = models.CharField(max_length=255, help_text="Fingerprint")
|
||||||
client_server_name = models.CharField(max_length=255)
|
client_hostname = models.CharField(max_length=255, help_text="Server address for clients")
|
||||||
client_hostname = models.CharField(max_length=255)
|
client_port = models.CharField(max_length=5, help_text="Server port for clients")
|
||||||
client_port = models.CharField(max_length=5)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Outline'
|
verbose_name = 'Outline'
|
||||||
@ -146,7 +145,7 @@ class OutlineServer(Server):
|
|||||||
if server_user:
|
if server_user:
|
||||||
if server_user.method != "chacha20-ietf-poly1305" or \
|
if server_user.method != "chacha20-ietf-poly1305" or \
|
||||||
server_user.port != int(self.client_port) 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 \
|
server_user.password != user.hash or \
|
||||||
self.client.delete_key(user.hash):
|
self.client.delete_key(user.hash):
|
||||||
|
|
||||||
@ -213,19 +212,18 @@ class OutlineServer(Server):
|
|||||||
|
|
||||||
class OutlineServerAdmin(PolymorphicChildModelAdmin):
|
class OutlineServerAdmin(PolymorphicChildModelAdmin):
|
||||||
base_model = OutlineServer
|
base_model = OutlineServer
|
||||||
show_in_index = False # Не отображать в главном списке админки
|
show_in_index = False
|
||||||
list_display = (
|
list_display = (
|
||||||
'name',
|
'name',
|
||||||
'admin_url',
|
'admin_url',
|
||||||
'admin_access_cert',
|
'admin_access_cert',
|
||||||
'client_server_name',
|
|
||||||
'client_hostname',
|
'client_hostname',
|
||||||
'client_port',
|
'client_port',
|
||||||
'server_status_inline',
|
|
||||||
'user_count',
|
'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',)
|
exclude = ('server_type',)
|
||||||
|
|
||||||
@admin.display(description='Clients', ordering='user_count')
|
@admin.display(description='Clients', ordering='user_count')
|
||||||
|
@ -29,11 +29,13 @@ def sync_all_users():
|
|||||||
@shared_task(name="sync_all_users_on_server")
|
@shared_task(name="sync_all_users_on_server")
|
||||||
def sync_users(server_id):
|
def sync_users(server_id):
|
||||||
from .models import Server
|
from .models import Server
|
||||||
|
status = {}
|
||||||
try:
|
try:
|
||||||
server = Server.objects.get(id=server_id)
|
server = Server.objects.get(id=server_id)
|
||||||
server.sync_users()
|
sync = server.sync_users()
|
||||||
logger.info(f"Successfully synced users for server {server.name}")
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error syncing users for server {server.name}: {e}")
|
logger.error(f"Error syncing users for server {server.name}: {e}")
|
||||||
raise TaskFailedException(message=f"Error syncing users for server {server.name}")
|
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.shortcuts import get_object_or_404
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse, HttpResponse, Http404
|
||||||
from django.http import HttpResponse
|
|
||||||
|
|
||||||
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):
|
def shadowsocks(request, link):
|
||||||
from .models import ACL
|
from .models import ACLLink, AccessLog
|
||||||
acl = get_object_or_404(ACL, link=link)
|
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:
|
try:
|
||||||
server_user = acl.server.get_user(acl.user, raw=True)
|
server_user = acl.server.get_user(acl.user, raw=True)
|
||||||
except Exception as e:
|
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}"})
|
return JsonResponse({"error": f"Couldn't get credentials from server. {e}"})
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
@ -26,7 +26,13 @@ def shadowsocks(request, link):
|
|||||||
"server": acl.server.client_hostname,
|
"server": acl.server.client_hostname,
|
||||||
"server_port": server_user.port,
|
"server_port": server_user.port,
|
||||||
"access_url": server_user.access_url,
|
"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