This commit is contained in:
Eugene Pankov
2021-07-11 22:28:50 +02:00
parent da8a631a6d
commit 3704dcc483
9 changed files with 158 additions and 15 deletions

View File

@@ -1,8 +1,10 @@
import os
import asyncio
import random
from dataclasses import dataclass
from tabby.app.consumers import GatewayAdminConnection
from django.conf import settings
from django.contrib.auth import logout
from dataclasses import dataclass
from pathlib import Path
from rest_framework import fields
from rest_framework.exceptions import PermissionDenied, NotFound
from rest_framework.permissions import IsAuthenticated
@@ -12,6 +14,7 @@ from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework_dataclasses.serializers import DataclassSerializer
from typing import List
from .models import Config, Gateway, User
@@ -19,6 +22,7 @@ from .models import Config, Gateway, User
@dataclass
class AppVersion:
version: str
plugins: List[str]
class AppVersionSerializer(DataclassSerializer):
@@ -28,6 +32,7 @@ class AppVersionSerializer(DataclassSerializer):
class GatewaySerializer(ModelSerializer):
url = fields.SerializerMethodField()
auth_token = fields.CharField()
class Meta:
fields = '__all__'
@@ -65,13 +70,29 @@ class AppVersionViewSet(ListModelMixin, GenericViewSet):
queryset = ''
def _get_versions(self):
return [AppVersion(version=x) for x in os.listdir(settings.APP_DIST_PATH)]
return [self._get_version(x) for x in settings.APP_DIST_PATH.iterdir()]
def _get_version(self, dir: Path):
plugins = [
x.name for x in dir.iterdir()
if x.is_dir() and x.name not in [
'tabby-web-container',
'tabby-web-demo',
]
]
return AppVersion(
version=dir.name,
plugins=plugins,
)
def list(self, request, *args, **kwargs):
return Response(self.serializer_class(
self._get_versions(),
many=True,
).data)
return Response(
self.serializer_class(
self._get_versions(),
many=True,
).data
)
class UserSerializer(ModelSerializer):
@@ -80,7 +101,14 @@ class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'active_config', 'custom_connection_gateway', 'custom_connection_gateway_token', 'is_pro')
fields = (
'id',
'username',
'active_config',
'custom_connection_gateway',
'custom_connection_gateway_token',
'is_pro',
)
read_only_fields = ('id', 'username')
def get_is_pro(self, obj):
@@ -121,8 +149,21 @@ class ChooseGatewayViewSet(RetrieveModelMixin, GenericViewSet):
queryset = Gateway.objects.filter(enabled=True)
serializer_class = GatewaySerializer
async def _authorize_client(self, gw):
c = GatewayAdminConnection(gw)
await c.connect()
token = await c.authorize_client()
await c.close()
return token
def get_object(self):
gateways = list(self.queryset)
if not len(gateways):
raise NotFound()
return random.choice(gateways)
gw = random.choice(gateways)
loop = asyncio.new_event_loop()
gw.auth_token = loop.run_until_complete(self._authorize_client(gw))
loop.close()
return gw

View File

@@ -1,11 +1,15 @@
import asyncio
import json
import os
import secrets
import ssl
import websockets
from channels.generic.websocket import AsyncWebsocketConsumer
from django.conf import settings
from urllib.parse import quote
from .models import Gateway
class GatewayConnection:
_ssl_context: ssl.SSLContext = None
@@ -42,6 +46,47 @@ class GatewayConnection:
await self.context.__aexit__(None, None, None)
class GatewayAdminConnection:
_ssl_context: ssl.SSLContext = None
def __init__(self, gateway: Gateway):
if not settings.CONNECTION_GATEWAY_AUTH_KEY:
raise RuntimeError('CONNECTION_GATEWAY_AUTH_KEY is required to manage connection gateways')
if not GatewayAdminConnection._ssl_context:
ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain(
os.path.realpath(settings.CONNECTION_GATEWAY_AUTH_CERTIFICATE),
os.path.realpath(settings.CONNECTION_GATEWAY_AUTH_KEY),
)
if settings.CONNECTION_GATEWAY_AUTH_CA:
ctx.load_verify_locations(
cafile=os.path.realpath(settings.CONNECTION_GATEWAY_AUTH_CA),
)
ctx.verify_mode = ssl.CERT_REQUIRED
GatewayAdminConnection._ssl_context = ctx
self.url = f'wss://{gateway.host}:{gateway.admin_port}'
async def connect(self):
self.context = websockets.connect(self.url, ssl=GatewayAdminConnection._ssl_context)
self.socket = await self.context.__aenter__()
async def authorize_client(self) -> str:
token = secrets.token_hex(32)
await self.send(json.dumps({
'_': 'authorize-client',
'token': token,
}))
return token
async def send(self, data):
await self.socket.send(data)
async def close(self):
await self.socket.close()
await self.context.__aexit__(None, None, None)
class TCPConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.closed = False

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.2.3 on 2021-07-11 18:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('app', '0002_gateway'),
]
operations = [
migrations.AddField(
model_name='gateway',
name='admin_port',
field=models.IntegerField(default=1235),
),
migrations.AlterField(
model_name='user',
name='active_config',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='app.config'),
),
]

View File

@@ -14,7 +14,7 @@ class Config(models.Model):
class User(AbstractUser):
active_config = models.ForeignKey(Config, null=True, on_delete=models.CASCADE, related_name='+')
active_config = models.ForeignKey(Config, null=True, on_delete=models.SET_NULL, related_name='+')
active_version = models.CharField(max_length=32, null=True)
custom_connection_gateway = models.CharField(max_length=255, null=True)
custom_connection_gateway_token = models.CharField(max_length=255, null=True)
@@ -25,6 +25,7 @@ class User(AbstractUser):
class Gateway(models.Model):
host = models.CharField(max_length=255)
port = models.IntegerField(default=1234)
admin_port = models.IntegerField(default=1235)
enabled = models.BooleanField(default=True)
secure = models.BooleanField(default=True)