mirror of
https://github.com/Eugeny/tabby-web.git
synced 2026-08-18 15:26:04 +01:00
wip
This commit is contained in:
@@ -21,6 +21,7 @@ export interface Config {
|
|||||||
|
|
||||||
export interface Version {
|
export interface Version {
|
||||||
version: string
|
version: string
|
||||||
|
plugins: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InstanceInfo {
|
export interface InstanceInfo {
|
||||||
@@ -31,6 +32,7 @@ export interface Gateway {
|
|||||||
host: string
|
host: string
|
||||||
port: number
|
port: number
|
||||||
url: string
|
url: string
|
||||||
|
auth_token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
.list-group.list-group-light
|
.list-group.list-group-light
|
||||||
ng-container(*ngFor='let config of configService.configs')
|
ng-container(*ngFor='let config of configService.configs')
|
||||||
button.list-group-item.list-group-item-action(
|
button.list-group-item.list-group-item-action(
|
||||||
*ngIf='config !== configService.activeConfig',
|
*ngIf='config.id !== configService.activeConfig?.id',
|
||||||
(click)='selectConfig(config)'
|
(click)='selectConfig(config)'
|
||||||
)
|
)
|
||||||
fa-icon([icon]='_configIcon')
|
fa-icon([icon]='_configIcon')
|
||||||
|
|||||||
@@ -46,4 +46,11 @@ export class ConfigModalComponent {
|
|||||||
this.modalInstance.dismiss()
|
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>()
|
close$ = new Subject<Buffer>()
|
||||||
|
|
||||||
url: string
|
url: string
|
||||||
|
authToken: string
|
||||||
webSocket: WebSocket
|
webSocket: WebSocket
|
||||||
initialBuffer: Buffer
|
initialBuffer: Buffer
|
||||||
options: {
|
options: {
|
||||||
@@ -27,15 +28,27 @@ export class SocketProxy {
|
|||||||
async connect (options) {
|
async connect (options) {
|
||||||
this.options = options
|
this.options = options
|
||||||
this.url = this.appConnector.loginService.user.custom_connection_gateway
|
this.url = this.appConnector.loginService.user.custom_connection_gateway
|
||||||
|
this.authToken = this.appConnector.loginService.user.custom_connection_gateway_token
|
||||||
if (!this.url) {
|
if (!this.url) {
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
this.error$.next(err)
|
this.error$.next(err)
|
||||||
return
|
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 => {
|
this.webSocket.onmessage = async event => {
|
||||||
if (typeof(event.data) === 'string') {
|
if (typeof(event.data) === 'string') {
|
||||||
this.handleServiceMessage(JSON.parse(event.data))
|
this.handleServiceMessage(JSON.parse(event.data))
|
||||||
@@ -53,7 +66,7 @@ export class SocketProxy {
|
|||||||
this.sendServiceMessage({
|
this.sendServiceMessage({
|
||||||
_: 'hello',
|
_: 'hello',
|
||||||
version: 1,
|
version: 1,
|
||||||
auth_token: this.appConnector.loginService.user.custom_connection_gateway_token,
|
auth_token: this.authToken,
|
||||||
})
|
})
|
||||||
} else if (msg._ === 'ready') {
|
} else if (msg._ === 'ready') {
|
||||||
this.sendServiceMessage({
|
this.sendServiceMessage({
|
||||||
@@ -139,7 +152,7 @@ export class AppConnectorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPluginsToLoad (): string[] {
|
getPluginsToLoad (): string[] {
|
||||||
return [
|
const loadOrder = [
|
||||||
'tabby-core',
|
'tabby-core',
|
||||||
'tabby-settings',
|
'tabby-settings',
|
||||||
'tabby-terminal',
|
'tabby-terminal',
|
||||||
@@ -147,6 +160,11 @@ export class AppConnectorService {
|
|||||||
'tabby-community-color-schemes',
|
'tabby-community-color-schemes',
|
||||||
'tabby-web',
|
'tabby-web',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
return [
|
||||||
|
...loadOrder.filter(x => this.version.plugins.includes(x)),
|
||||||
|
...this.version.plugins.filter(x => !loadOrder.includes(x)),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
createSocket () {
|
createSocket () {
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ export class ConfigService {
|
|||||||
this.selectConfig(this.configs.find(c => c.id === this.loginService.user.active_config) ?? this.configs[0])
|
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 () {
|
private async init () {
|
||||||
this.configs = await this.http.get('/api/1/configs').toPromise()
|
this.configs = await this.http.get('/api/1/configs').toPromise()
|
||||||
this.versions = await this.http.get('/api/1/versions').toPromise()
|
this.versions = await this.http.get('/api/1/versions').toPromise()
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import os
|
import asyncio
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from tabby.app.consumers import GatewayAdminConnection
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import logout
|
from django.contrib.auth import logout
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
from rest_framework import fields
|
from rest_framework import fields
|
||||||
from rest_framework.exceptions import PermissionDenied, NotFound
|
from rest_framework.exceptions import PermissionDenied, NotFound
|
||||||
from rest_framework.permissions import IsAuthenticated
|
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.viewsets import GenericViewSet, ModelViewSet
|
||||||
from rest_framework.serializers import ModelSerializer, Serializer
|
from rest_framework.serializers import ModelSerializer, Serializer
|
||||||
from rest_framework_dataclasses.serializers import DataclassSerializer
|
from rest_framework_dataclasses.serializers import DataclassSerializer
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from .models import Config, Gateway, User
|
from .models import Config, Gateway, User
|
||||||
|
|
||||||
@@ -19,6 +22,7 @@ from .models import Config, Gateway, User
|
|||||||
@dataclass
|
@dataclass
|
||||||
class AppVersion:
|
class AppVersion:
|
||||||
version: str
|
version: str
|
||||||
|
plugins: List[str]
|
||||||
|
|
||||||
|
|
||||||
class AppVersionSerializer(DataclassSerializer):
|
class AppVersionSerializer(DataclassSerializer):
|
||||||
@@ -28,6 +32,7 @@ class AppVersionSerializer(DataclassSerializer):
|
|||||||
|
|
||||||
class GatewaySerializer(ModelSerializer):
|
class GatewaySerializer(ModelSerializer):
|
||||||
url = fields.SerializerMethodField()
|
url = fields.SerializerMethodField()
|
||||||
|
auth_token = fields.CharField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
@@ -65,13 +70,29 @@ class AppVersionViewSet(ListModelMixin, GenericViewSet):
|
|||||||
queryset = ''
|
queryset = ''
|
||||||
|
|
||||||
def _get_versions(self):
|
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):
|
def list(self, request, *args, **kwargs):
|
||||||
return Response(self.serializer_class(
|
return Response(
|
||||||
self._get_versions(),
|
self.serializer_class(
|
||||||
many=True,
|
self._get_versions(),
|
||||||
).data)
|
many=True,
|
||||||
|
).data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(ModelSerializer):
|
class UserSerializer(ModelSerializer):
|
||||||
@@ -80,7 +101,14 @@ class UserSerializer(ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
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')
|
read_only_fields = ('id', 'username')
|
||||||
|
|
||||||
def get_is_pro(self, obj):
|
def get_is_pro(self, obj):
|
||||||
@@ -121,8 +149,21 @@ class ChooseGatewayViewSet(RetrieveModelMixin, GenericViewSet):
|
|||||||
queryset = Gateway.objects.filter(enabled=True)
|
queryset = Gateway.objects.filter(enabled=True)
|
||||||
serializer_class = GatewaySerializer
|
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):
|
def get_object(self):
|
||||||
gateways = list(self.queryset)
|
gateways = list(self.queryset)
|
||||||
if not len(gateways):
|
if not len(gateways):
|
||||||
raise NotFound()
|
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 asyncio
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import secrets
|
||||||
import ssl
|
import ssl
|
||||||
import websockets
|
import websockets
|
||||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from .models import Gateway
|
||||||
|
|
||||||
|
|
||||||
class GatewayConnection:
|
class GatewayConnection:
|
||||||
_ssl_context: ssl.SSLContext = None
|
_ssl_context: ssl.SSLContext = None
|
||||||
@@ -42,6 +46,47 @@ class GatewayConnection:
|
|||||||
await self.context.__aexit__(None, None, None)
|
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):
|
class TCPConsumer(AsyncWebsocketConsumer):
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
self.closed = False
|
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):
|
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)
|
active_version = models.CharField(max_length=32, null=True)
|
||||||
custom_connection_gateway = models.CharField(max_length=255, null=True)
|
custom_connection_gateway = models.CharField(max_length=255, null=True)
|
||||||
custom_connection_gateway_token = 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):
|
class Gateway(models.Model):
|
||||||
host = models.CharField(max_length=255)
|
host = models.CharField(max_length=255)
|
||||||
port = models.IntegerField(default=1234)
|
port = models.IntegerField(default=1234)
|
||||||
|
admin_port = models.IntegerField(default=1235)
|
||||||
enabled = models.BooleanField(default=True)
|
enabled = models.BooleanField(default=True)
|
||||||
secure = models.BooleanField(default=True)
|
secure = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user