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

@@ -21,6 +21,7 @@ export interface Config {
export interface Version {
version: string
plugins: string[]
}
export interface InstanceInfo {
@@ -31,6 +32,7 @@ export interface Gateway {
host: string
port: number
url: string
auth_token: string
}
@Injectable({ providedIn: 'root' })

View File

@@ -31,7 +31,7 @@
.list-group.list-group-light
ng-container(*ngFor='let config of configService.configs')
button.list-group-item.list-group-item-action(
*ngIf='config !== configService.activeConfig',
*ngIf='config.id !== configService.activeConfig?.id',
(click)='selectConfig(config)'
)
fa-icon([icon]='_configIcon')

View File

@@ -46,4 +46,11 @@ export class ConfigModalComponent {
this.modalInstance.dismiss()
}
async deleteConfig () {
if (confirm('Delete this config? This cannot be undone.')) {
await this.configService.deleteConfig(this.configService.activeConfig)
}
this.configService.selectDefaultConfig()
this.modalInstance.dismiss()
}
}

View File

@@ -13,6 +13,7 @@ export class SocketProxy {
close$ = new Subject<Buffer>()
url: string
authToken: string
webSocket: WebSocket
initialBuffer: Buffer
options: {
@@ -27,15 +28,27 @@ export class SocketProxy {
async connect (options) {
this.options = options
this.url = this.appConnector.loginService.user.custom_connection_gateway
this.authToken = this.appConnector.loginService.user.custom_connection_gateway_token
if (!this.url) {
try {
this.url = (await this.appConnector.chooseConnectionGateway()).url
const gateway = await this.appConnector.chooseConnectionGateway()
this.url = gateway.url
this.authToken = gateway.auth_token
} catch (err) {
this.error$.next(err)
return
}
}
this.webSocket = new WebSocket(this.url)
try {
this.webSocket = new WebSocket(this.url)
} catch (err) {
this.error$.next(err)
return
}
this.webSocket.onerror = err => {
this.error$.next(new Error(`Failed to connect to the connection gateway at ${this.url}`))
return
}
this.webSocket.onmessage = async event => {
if (typeof(event.data) === 'string') {
this.handleServiceMessage(JSON.parse(event.data))
@@ -53,7 +66,7 @@ export class SocketProxy {
this.sendServiceMessage({
_: 'hello',
version: 1,
auth_token: this.appConnector.loginService.user.custom_connection_gateway_token,
auth_token: this.authToken,
})
} else if (msg._ === 'ready') {
this.sendServiceMessage({
@@ -139,7 +152,7 @@ export class AppConnectorService {
}
getPluginsToLoad (): string[] {
return [
const loadOrder = [
'tabby-core',
'tabby-settings',
'tabby-terminal',
@@ -147,6 +160,11 @@ export class AppConnectorService {
'tabby-community-color-schemes',
'tabby-web',
]
return [
...loadOrder.filter(x => this.version.plugins.includes(x)),
...this.version.plugins.filter(x => !loadOrder.includes(x)),
]
}
createSocket () {

View File

@@ -76,6 +76,11 @@ export class ConfigService {
this.selectConfig(this.configs.find(c => c.id === this.loginService.user.active_config) ?? this.configs[0])
}
async deleteConfig (config: Config) {
await this.http.delete(`/api/1/configs/${config.id}`).toPromise()
this.configs = this.configs.filter(x => x.id !== config.id)
}
private async init () {
this.configs = await this.http.get('/api/1/configs').toPromise()
this.versions = await this.http.get('/api/1/versions').toPromise()

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)