This commit is contained in:
A B
2024-10-20 21:37:04 +00:00
commit 9ffab39f4f
26 changed files with 1014 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
db.sqlite3
debug.log
*.swp
*.swo
*.pyc
staticfiles/
*.__pycache__.*
vpn/migrations/

10
Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM python:3
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD [ "python", "./manage.py", "runserver", "0.0.0.0:8000" ]

22
manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

3
mysite/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ('celery_app',)

16
mysite/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for mysite project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
application = get_asgi_application()

21
mysite/celery.py Normal file
View File

@ -0,0 +1,21 @@
import logging
import os
from celery import Celery
from celery import shared_task
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
logger = logging.getLogger(__name__)
app = Celery('mysite')
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django apps.
app.autodiscover_tasks()

14
mysite/middleware.py Normal file
View File

@ -0,0 +1,14 @@
from django.urls import resolve
from django.http import Http404, HttpResponseNotFound
class RequestLogger:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
print(f"Original: {request.build_absolute_uri()}")
print(f"Path : {request.path}")
response = self.get_response(request)
return response

208
mysite/settings.py Normal file
View File

@ -0,0 +1,208 @@
from pathlib import Path
import os
import environ
from django.core.management.utils import get_random_secret_key
ENV = environ.Env(
DEBUG=(bool, False)
)
environ.Env.read_env()
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY=ENV('SECRET_KEY', default=get_random_secret_key())
TIME_ZONE = ENV('TIMEZONE', default='Asia/Nicosia')
CELERY_BROKER_URL = ENV('CELERY_BROKER_URL', default='redis://localhost:6379/0')
CELERY_RESULT_BACKEND = 'django-db'
CELERY_TIMEZONE = ENV('TIMEZONE', default='Asia/Nicosia')
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_RESULT_EXTENDED = True
# CACHES = {
# 'default': {
# 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
# 'LOCATION': 'cache_table',
# }
# }
DEBUG = ENV('DEBUG')
ALLOWED_HOSTS = ENV.list('ALLOWED_HOSTS', default=["*"])
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True
CSRF_TRUSTED_ORIGINS = ENV.list('CSRF_TRUSTED_ORIGINS', default=[])
STATIC_ROOT = BASE_DIR / "staticfiles"
LOGIN_REDIRECT_URL = '/'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '[{asctime}] {levelname} {name} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': os.path.join(BASE_DIR, 'debug.log'),
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['console'],
'level': 'INFO',
'propagate': True,
},
'vpn': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': False,
},
'requests': {
'handlers': ['console'],
'level': 'INFO',
'propagate': False,
},
'urllib3': {
'handlers': ['console'],
'level': 'INFO',
'propagate': False,
},
},
}
INSTALLED_APPS = [
'jazzmin',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'polymorphic',
'corsheaders',
'django_celery_results',
'vpn',
]
MIDDLEWARE = [
#'mysite.middleware.RequestLogger',
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware',
]
ROOT_URLCONF = 'mysite.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'vpn', 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'mysite.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
# CREATE USER outfleet WITH PASSWORD 'password';
# GRANT ALL PRIVILEGES ON DATABASE outfleet TO outfleet;
# ALTER DATABASE outfleet OWNER TO outfleet;
DATABASES = {
'sqlite': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
},
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': ENV('POSTGRES_DB', default="outfleet"),
'USER': ENV('POSTGRES_USER', default="outfleet"),
'PASSWORD': ENV('POSTGRES_PASSWORD', default="password"),
'HOST': ENV('POSTGRES_HOST', default='localhost'),
'PORT': ENV('POSTGRES_PORT', default='5432'),
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

27
mysite/urls.py Normal file
View File

@ -0,0 +1,27 @@
"""
URL configuration for mysite project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.views.generic import RedirectView
from vpn.views import shadowsocks
urlpatterns = [
path('admin/', admin.site.urls),
path('ss/<str:link>', shadowsocks, name='shadowsocks'),
path('dynamic/<str:link>', shadowsocks, name='shadowsocks'),
path('', RedirectView.as_view(url='/admin/', permanent=False)),
]

16
mysite/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for mysite project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
application = get_wsgi_application()

14
requirements.txt Normal file
View File

@ -0,0 +1,14 @@
django-environ==0.11.2
Django==5.1.2
celery==5.4.0
django-jazzmin==3.0.1
django-polymorphic==3.1.0
django-cors-headers==4.5.0
requests==2.32.3
outline-vpn-api==6.3.0
Redis==5.1.1
whitenoise==6.7.0
psycopg2-binary==2.9.10
setuptools==75.2.0
shortuuid==1.0.13
django-celery-results==2.5.1

View File

@ -0,0 +1,21 @@
// static/admin/js/generate_uuid.js
function generateUUID() {
let uuid = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const length = 10;
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
uuid += characters[randomIndex];
}
const hashField = document.getElementById('id_hash');
if (hashField) {
hashField.value = uuid;
}
}
document.addEventListener('DOMContentLoaded', function () {
generateUUID();
});

0
vpn/__init__.py Normal file
View File

100
vpn/admin.py Normal file
View File

@ -0,0 +1,100 @@
import json
from polymorphic.admin import (
PolymorphicParentModelAdmin,
)
from django.contrib import admin
from django.utils.safestring import mark_safe
from django.db.models import Count
from vpn.models import User, ACL
from vpn.forms import UserForm
from .server_plugins import (
Server,
WireguardServer,
WireguardServerAdmin,
OutlineServer,
OutlineServerAdmin)
@admin.register(Server)
class ServerAdmin(PolymorphicParentModelAdmin):
base_model = Server
child_models = (OutlineServer, WireguardServer)
list_display = ('name', 'server_type', 'comment', 'registration_date', 'user_count', 'server_status_inline')
search_fields = ('name', 'comment')
list_filter = ('server_type', )
@admin.display(description='User Count', ordering='user_count')
def user_count(self, obj):
return obj.user_count
@admin.display(description='Status')
def server_status_inline(self, obj):
status = obj.get_server_status()
if 'error' in status:
return mark_safe(f"<span style='color: red;'>Error: {status['error']}</span>")
# Преобразуем JSON в красивый формат
import json
pretty_status = ", ".join(f"{key}: {value}" for key, value in status.items())
return mark_safe(f"<pre>{pretty_status}</pre>")
server_status_inline.short_description = "Status"
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.annotate(user_count=Count('acl'))
return qs
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
form = UserForm
list_display = ('name', 'comment', 'registration_date', 'hash', 'server_count')
search_fields = ('name', 'hash')
readonly_fields = ('hash',)
@admin.display(description='Allowed servers', ordering='server_count')
def server_count(self, obj):
return obj.server_count
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.annotate(server_count=Count('acl'))
return qs
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
selected_servers = form.cleaned_data.get('servers', [])
ACL.objects.filter(user=obj).exclude(server__in=selected_servers).delete()
for server in selected_servers:
ACL.objects.get_or_create(user=obj, server=server)
@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', )
@admin.display(description='Server Type', ordering='server__server_type')
def server_type(self, obj):
return obj.server.get_server_type_display()
@admin.display(description='Client info')
def user_info(self, obj):
server = obj.server
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>")
except Exception as e:
return mark_safe(f"<span style='color: red;'>Error: {e}</span>")

6
vpn/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class VPN(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'vpn'

14
vpn/forms.py Normal file
View File

@ -0,0 +1,14 @@
from django import forms
from .models import User
from .server_plugins import Server
class UserForm(forms.ModelForm):
servers = forms.ModelMultipleChoiceField(
queryset=Server.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False
)
class Meta:
model = User
fields = ['name', 'comment', 'servers']

59
vpn/models.py Normal file
View File

@ -0,0 +1,59 @@
import uuid
from django.db import models
from vpn.tasks import sync_user
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from .server_plugins import Server
import shortuuid
class User(models.Model):
name = models.CharField(max_length=100)
comment = models.TextField(default="", blank=True)
registration_date = models.DateTimeField(auto_now_add=True)
servers = models.ManyToManyField('Server', through='ACL', blank=True)
last_access = models.DateTimeField(null=True, blank=True)
hash = models.CharField(max_length=64, unique=True)
def get_servers(self):
return Server.objects.filter(acl__user=self)
def save(self, *args, **kwargs):
if not self.hash:
self.hash = shortuuid.ShortUUID().random(length=16)
sync_user.delay_on_commit(self.id)
super().save(*args, **kwargs)
def __str__(self):
return self.name
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)
class Meta:
constraints = [
models.UniqueConstraint(fields=['user', 'server'], name='unique_user_server')
]
def __str__(self):
return f"{self.user.name} - {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):
if created:
sync_user.delay(instance.user.id)
else:
pass
@receiver(pre_delete, sender=ACL)
def acl_deleted(sender, instance, **kwargs):
sync_user.delay(instance.user.id)

View File

@ -0,0 +1,4 @@
from .generic import Server
from .outline import OutlineServer, OutlineServerAdmin
from .wireguard import WireguardServer, WireguardServerAdmin
from .urls import urlpatterns

View File

@ -0,0 +1,44 @@
from polymorphic.models import PolymorphicModel
from django.db import models
from vpn.tasks import sync_server
class Server(PolymorphicModel):
SERVER_TYPE_CHOICES = (
('Outline', 'Outline'),
('Wireguard', 'Wireguard'),
)
name = models.CharField(max_length=100)
comment = models.TextField(default="", blank=True)
registration_date = models.DateTimeField(auto_now_add=True)
server_type = models.CharField(max_length=50, choices=SERVER_TYPE_CHOICES, editable=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def save(self, *args, **kwargs):
sync_server.delay(self.id)
super().save(*args, **kwargs)
def get_server_status(self, *args, **kwargs):
return {"name": self.name}
def sync(self, *args, **kwargs):
pass
def add_user(self, *args, **kwargs):
pass
def get_user(self, *args, **kwargs):
pass
def delete_user(self, *args, **kwargs):
pass
class Meta:
verbose_name = "Server"
verbose_name_plural = "Servers"
def __str__(self):
return self.name

View File

@ -0,0 +1,225 @@
import logging
from venv import logger
import requests
from django.db import models
from .generic import Server
from urllib3 import PoolManager
from outline_vpn.outline_vpn import OutlineVPN, OutlineLibraryException
from polymorphic.admin import PolymorphicChildModelAdmin
from django.contrib import admin
from django.utils.safestring import mark_safe
from django.db.models import Count
class OutlineConnectionError(Exception):
def __init__(self, message, original_exception=None):
super().__init__(message)
self.original_exception = original_exception
class _FingerprintAdapter(requests.adapters.HTTPAdapter):
"""
This adapter injected into the requests session will check that the
fingerprint for the certificate matches for every request
"""
def __init__(self, fingerprint=None, **kwargs):
self.fingerprint = str(fingerprint)
super(_FingerprintAdapter, self).__init__(**kwargs)
def init_poolmanager(self, connections, maxsize, block=False):
self.poolmanager = PoolManager(
num_pools=connections,
maxsize=maxsize,
block=block,
assert_fingerprint=self.fingerprint,
)
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)
class Meta:
verbose_name = 'Outline'
verbose_name_plural = 'Outline'
def save(self, *args, **kwargs):
self.server_type = 'Outline'
super().save(*args, **kwargs)
@property
def status(self):
return self.get_server_status(raw=True)
@property
def client(self):
return OutlineVPN(api_url=self.admin_url, cert_sha256=self.admin_access_cert)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __str__(self):
return f"{self.name} ({self.client_hostname}:{self.client_port})"
def get_server_status(self, raw=False):
status = {}
try:
info = self.client.get_server_information()
if raw:
status = info
else:
status.update(info)
except Exception as e:
status.update({f"error": e})
return status
def sync(self):
status = {}
try:
state = self.client.get_server_information()
if state["name"] != self.name:
self.client.set_server_name(self.name)
status["name"] = f"{state['name']} -> {self.name}"
elif state["hostnameForAccessKeys"] != self.client_hostname:
self.client.set_hostname(self.client_hostname)
status["hostnameForAccessKeys"] = f"{state['hostnameForAccessKeys']} -> {self.client_hostname}"
elif int(state["portForNewAccessKeys"]) != int(self.client_port):
self.client.set_port_new_for_access_keys(int(self.client_port))
status["portForNewAccessKeys"] = f"{state['portForNewAccessKeys']} -> {self.client_port}"
if len(status) == 0:
status = {"status": "Nothing to do"}
return status
except AttributeError as e:
raise OutlineConnectionError("Client error. Can't connect.", original_exception=e)
def _get_key(self, user):
try:
return self.client.get_key(user.hash)
except Exception as e:
logger.warning(f"sync error: {e}")
return None
def get_user(self, user, raw=False):
user_info = self._get_key(user)
if raw:
return user_info
else:
outline_key_dict = user_info.__dict__
outline_key_dict = {
key: value
for key, value in user_info.__dict__.items()
if not key.startswith('_') and key not in [] # fields to mask
}
return outline_key_dict
def add_user(self, user):
server_user = self._get_key(user)
logger.warning(server_user)
result = {}
key = None
if server_user:
self.client.delete_key(user.hash)
key = self.client.create_key(
name=user.name,
method=server_user.method,
password=user.hash,
data_limit=None,
port=server_user.port
)
else:
key = self.client.create_key(
key_id=user.hash,
name=user.name,
method=server_user.method,
password=user.hash,
data_limit=None,
port=server_user.port
)
try:
result['key_id'] = key.key_id
result['name'] = key.name
result['method'] = key.method
result['password'] = key.password
result['data_limit'] = key.data_limit
result['port'] = key.port
except Exception as e:
result = {"error": str(e)}
return result
def delete_user(self, user):
server_user = self._get_key(user)
result = None
if server_user:
self.logger.info(f"[{self.name}] TEST")
self.client.delete_key(server_user.key_id)
result = {"status": "User was deleted"}
self.logger.info(f"[{self.name}] User deleted: {user.name} on server {self.name}")
else:
result = {"status": "User absent, nothing to do."}
return result
class OutlineServerAdmin(PolymorphicChildModelAdmin):
base_model = OutlineServer
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'
)
readonly_fields = ('server_status_full', )
exclude = ('server_type',)
@admin.display(description='Clients', ordering='user_count')
def user_count(self, obj):
return obj.user_count
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.annotate(user_count=Count('acl__user'))
return qs
def server_status_inline(self, obj):
status = obj.get_server_status()
if 'error' in status:
return mark_safe(f"<span style='color: red;'>Error: {status['error']}</span>")
# Преобразуем JSON в красивый формат
import json
pretty_status = json.dumps(status, indent=4)
return mark_safe(f"<pre>{pretty_status}</pre>")
server_status_inline.short_description = "Status"
def server_status_full(self, obj):
if obj and obj.pk:
status = obj.get_server_status()
if 'error' in status:
return mark_safe(f"<span style='color: red;'>Error: {status['error']}</span>")
import json
pretty_status = json.dumps(status, indent=4)
return mark_safe(f"<pre>{pretty_status}</pre>")
return "N/A"
server_status_full.short_description = "Server Status"
def get_model_perms(self, request):
"""It disables display for sub-model"""
return {}
admin.site.register(OutlineServer, OutlineServerAdmin)

View File

@ -0,0 +1,6 @@
from django.urls import path
from vpn.views import shadowsocks
urlpatterns = [
path('ss/<str:hash_value>/', shadowsocks, name='shadowsocks'),
]

View File

@ -0,0 +1,83 @@
from .generic import Server
from django.db import models
from polymorphic.admin import (
PolymorphicChildModelAdmin,
)
from django.contrib import admin
from django.db.models import Count
from django.utils.safestring import mark_safe
class WireguardServer(Server):
address = models.CharField(max_length=100)
port = models.IntegerField()
client_private_key = models.CharField(max_length=255)
server_publick_key = models.CharField(max_length=255)
class Meta:
verbose_name = 'Wireguard'
verbose_name_plural = 'Wireguard'
def save(self, *args, **kwargs):
self.server_type = 'Wireguard'
super().save(*args, **kwargs)
def __str__(self):
return f"{self.name} ({self.address})"
def get_server_status(self):
status = super().get_server_status()
status.update({
"address": self.address,
"port": self.port,
"client_private_key": self.client_private_key,
"server_publick_key": self.server_publick_key,
})
return status
class WireguardServerAdmin(PolymorphicChildModelAdmin):
base_model = WireguardServer
show_in_index = False # Не отображать в главном списке админки
list_display = (
'name',
'address',
'port',
'server_publick_key',
'client_private_key',
'server_status_inline',
'user_count',
'registration_date'
)
readonly_fields = ('server_status_full', )
exclude = ('server_type',)
@admin.display(description='Clients', ordering='user_count')
def user_count(self, obj):
return obj.user_count
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.annotate(user_count=Count('acl__user'))
return qs
def server_status_inline(self, obj):
status = obj.get_server_status()
if 'error' in status:
return mark_safe(f"<span style='color: red;'>Error: {status['error']}</span>")
return mark_safe(f"<pre>{status}</pre>")
server_status_inline.short_description = "Server Status"
def server_status_full(self, obj):
if obj and obj.pk:
status = obj.get_server_status()
if 'error' in status:
return mark_safe(f"<span style='color: red;'>Error: {status['error']}</span>")
return mark_safe(f"<pre>{status}</pre>")
return "N/A"
server_status_full.short_description = "Server Status"
def get_model_perms(self, request):
"""It disables display for sub-model"""
return {}
admin.site.register(WireguardServer, WireguardServerAdmin)

54
vpn/tasks.py Normal file
View File

@ -0,0 +1,54 @@
import logging
from celery import shared_task
#from django_celery_results.models import TaskResult
from outline_vpn.outline_vpn import OutlineServerErrorException
logger = logging.getLogger(__name__)
class TaskFailedException(Exception):
def __init__(self, message=""):
self.message = message
super().__init__(f"{self.message}")
@shared_task(name="sync.server")
def sync_server(id):
from vpn.server_plugins import Server
# task_result = TaskResult.objects.get_task(self.request.id)
# task_result.status='RUNNING'
# task_result.save()
return {"status": Server.objects.get(id=id).sync()}
@shared_task(name="sync.user")
def sync_user(id):
from .models import User, ACL
from vpn.server_plugins import Server
errors = {}
result = {}
user = User.objects.get(id=id)
acls = ACL.objects.filter(user=user)
servers = Server.objects.all()
for server in servers:
try:
if acls.filter(server=server).exists():
result[server.name] = server.add_user(user)
else:
result[server.name] = server.delete_user(user)
except User.DoesNotExist as e:
result = {"error": e}
logger.error("User not found.")
except Exception as e:
errors[server.name] = {"error": e}
finally:
if errors:
logger.error("ERROR ERROR")
raise TaskFailedException(message=f"Errors during taks: {errors}")
else:
logger.error(f"PUK PUEK. {errors}")
return result

View File

@ -0,0 +1,10 @@
{% extends "admin/change_form.html" %}
{% load static %}
{% block after_field_sets %}
<div>
<h2>Create ACLs</h2>
{{ adminform.form.servers }}
</div>
{% endblock %}

3
vpn/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

26
vpn/views.py Normal file
View File

@ -0,0 +1,26 @@
from django.shortcuts import render
# views.py
from django.shortcuts import get_object_or_404
from django.http import JsonResponse
from django.utils import timezone
def shadowsocks(request, link):
from .models import ACL
acl = get_object_or_404(ACL, link=link)
server_user = acl.server.get_user(acl.user, raw=True)
config = {
"info": "Managed by OutFleet_v2 [github.com/house-of-vanity/OutFleet/]",
"password": server_user.password,
"method": server_user.method,
"prefix": "\u0005\u00dc_\u00e0\u0001",
"server": acl.server.client_server_name,
"server_port": server_user.port,
"access_url": server_user.access_url,
}
return JsonResponse(config)