mirror of
https://github.com/Eugeny/tabby-web.git
synced 2026-08-16 22:36:02 +01:00
wip
This commit is contained in:
@@ -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' })
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
24
tabby/app/migrations/0003_auto_20210711_1855.py
Normal file
24
tabby/app/migrations/0003_auto_20210711_1855.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user