feature!: убрана поддержка Cloudlfare
К сожалению проксирование недоступно принципиально
This commit is contained in:
1 parent
96213e182e
commit
064bb70f32
7 files changed
+28
-56
No files matched your search
+3
-3
@@ -4,21 +4,21 @@ Django приложение связывающее все части сайта
|
||||
|
||||
-[x] Реализует кастомный менеджер модели и кастомную модель пользователя с отключенным email.
|
||||
-[x] Модель данных для поддержки интерфейса и функционала
|
||||
-[ ] Настройки домена, прокси и cloudflare у готовой точки
|
||||
-[ ] Настройки домена и прокси у готовой точки
|
||||
-[ ] Клиенты и статистика через API панели 3x-ui
|
||||
|
||||
Зависимости: `Telegram` (интерфейс бота, пользователи, уведомления) и `serverus` (настройка серверов).
|
||||
|
||||
## Модель данных
|
||||
|
||||
- `Point` - точка доступа: сервер с панелью 3x-ui и необязательный сервер маскировки. Владелец - `Telegram.User`. Хранит домен, cloudflare, прокси на выходе и доступ к панели.
|
||||
- `Point` - точка доступа: сервер с панелью 3x-ui и необязательный сервер маскировки. Владелец - `Telegram.User`. Хранит домен, прокси на выходе и доступ к панели.
|
||||
- `Server` - сервер точки с ролью `panel` или `mask`. Хранит текущие порт ssh, пароль root, ключ и отпечаток ssh, а также пароль root, присланный пользователем при добавлении (`default_root_password`).
|
||||
|
||||
С маскировкой клиенты подключаются к серверу маскировки, он перенаправляет трафик на панель и скрывает ее адрес. Маскировка требует домен.
|
||||
|
||||
Работа с записями идет через менеджеры и методы моделей, а не через `filter`/`create` напрямую:
|
||||
|
||||
- создание: `Point.objects.create_point(owner, panel, mask=None, *, domain, cloudflare, outbound_proxy)` создает точку вместе с серверами, имя и доступ к панели генерирует сама. `Server.objects.create_server(point, role, credentials)`. `Credentials` - адрес и пароль root, которые прислал пользователь
|
||||
- создание: `Point.objects.create_point(owner, panel, mask=None, *, domain, outbound_proxy)` создает точку вместе с серверами, имя и доступ к панели генерирует сама. `Server.objects.create_server(point, role, credentials)`. `Credentials` - адрес и пароль root, которые прислал пользователь
|
||||
- поиск: `Point.objects.owned_by(owner)`, дальше `find_by_name`, `find_by_pk`, `find_last`. У серверов `find_by_address` и `is_address_taken`, в том числе в пределах точки через `point.servers`
|
||||
- изменение: `point.set_status(status, error)`, `server.reset_fingerprint()`
|
||||
- удаление: `point.delete()`, серверы удаляются вместе с точкой
|
||||
|
||||
@@ -13,7 +13,6 @@ DRAFT = 'draft'
|
||||
DRAFT_SERVER = 'draft.server'
|
||||
DRAFT_MASK = 'draft.mask'
|
||||
DRAFT_DOMAIN = 'draft.domain'
|
||||
DRAFT_CLOUDFLARE = 'draft.cloudflare'
|
||||
DRAFT_PROXY = 'draft.proxy'
|
||||
SELECT = 'select'
|
||||
POINT = 'point'
|
||||
@@ -103,10 +102,6 @@ class MyPointInterface(Interface):
|
||||
def ask_domain(self, ctx):
|
||||
self.ask(ctx, DRAFT_DOMAIN, texts.DOMAIN_PROMPT, [[texts.CANCEL, texts.DELETE]])
|
||||
|
||||
@text(texts.CLOUDFLARE, state=DRAFT)
|
||||
def ask_cloudflare(self, ctx):
|
||||
self.ask(ctx, DRAFT_CLOUDFLARE, texts.CLOUDFLARE_PROMPT, [[texts.ENABLE, texts.DISABLE, texts.CANCEL]])
|
||||
|
||||
@text(texts.PROXY, state=DRAFT)
|
||||
def ask_proxy(self, ctx):
|
||||
self.ask(ctx, DRAFT_PROXY, texts.PROXY_PROMPT, [[texts.CF_WARP, texts.CANCEL, texts.DELETE]])
|
||||
@@ -118,7 +113,6 @@ class MyPointInterface(Interface):
|
||||
@text(texts.CANCEL, state=DRAFT_SERVER)
|
||||
@text(texts.CANCEL, state=DRAFT_MASK)
|
||||
@text(texts.CANCEL, state=DRAFT_DOMAIN)
|
||||
@text(texts.CANCEL, state=DRAFT_CLOUDFLARE)
|
||||
@text(texts.CANCEL, state=DRAFT_PROXY)
|
||||
def back_to_draft(self, ctx):
|
||||
self.show_draft(ctx)
|
||||
@@ -153,20 +147,6 @@ class MyPointInterface(Interface):
|
||||
ctx.data['draft']['domain'] = ''
|
||||
self.show_draft(ctx)
|
||||
|
||||
@text(texts.ENABLE, state=DRAFT_CLOUDFLARE)
|
||||
def enable_cloudflare(self, ctx):
|
||||
ctx.data['draft']['cloudflare'] = True
|
||||
self.show_draft(ctx)
|
||||
|
||||
@text(texts.DISABLE, state=DRAFT_CLOUDFLARE)
|
||||
def disable_cloudflare(self, ctx):
|
||||
ctx.data['draft']['cloudflare'] = False
|
||||
self.show_draft(ctx)
|
||||
|
||||
@state(DRAFT_CLOUDFLARE)
|
||||
def repeat_cloudflare(self, ctx):
|
||||
self.ask_cloudflare(ctx)
|
||||
|
||||
@state(DRAFT_PROXY)
|
||||
def set_proxy(self, ctx):
|
||||
proxy = ctx.text.strip()
|
||||
@@ -192,14 +172,11 @@ class MyPointInterface(Interface):
|
||||
return ctx.reply(texts.DRAFT_INCOMPLETE)
|
||||
if draft.get('mask') and not draft.get('domain'):
|
||||
return ctx.reply(texts.MASK_WITHOUT_DOMAIN)
|
||||
if draft.get('cloudflare') and not draft.get('domain'):
|
||||
return ctx.reply(texts.CLOUDFLARE_WITHOUT_DOMAIN)
|
||||
point = Point.objects.create_point(
|
||||
ctx.user,
|
||||
panel=Credentials(**draft['server']),
|
||||
mask=Credentials(**draft['mask']) if draft.get('mask') else None,
|
||||
domain=draft.get('domain', ''),
|
||||
cloudflare=draft.get('cloudflare', False),
|
||||
outbound_proxy=draft.get('proxy', ''),
|
||||
)
|
||||
tasks.install_point.send(point.pk, ctx.chat.pk)
|
||||
@@ -305,8 +282,7 @@ class MyPointInterface(Interface):
|
||||
|
||||
@text(texts.POINT_DOMAIN, state=POINT)
|
||||
@text(texts.POINT_PROXY, state=POINT)
|
||||
@text(texts.POINT_CLOUDFLARE, state=POINT)
|
||||
@text(texts.CLIENTS, state=POINT)
|
||||
def not_implemented(self, ctx):
|
||||
# TODO: настройки домена, прокси, cloudflare и клиенты через API панели 3x-ui
|
||||
# TODO: настройки домена, прокси и клиенты через API панели 3x-ui
|
||||
ctx.reply(texts.NOT_IMPLEMENTED)
|
||||
+3
-11
@@ -20,11 +20,8 @@ NO = 'Нет'
|
||||
SERVER = 'сервер'
|
||||
MASK = 'маскировка'
|
||||
DOMAIN = 'домен'
|
||||
CLOUDFLARE = 'cloudflare'
|
||||
PROXY = 'прокси'
|
||||
INSTALL = 'установить'
|
||||
ENABLE = 'включить'
|
||||
DISABLE = 'выключить'
|
||||
CF_WARP = 'cf_warp'
|
||||
|
||||
# меню сервера
|
||||
@@ -33,14 +30,13 @@ RELEASE = 'Вернуть'
|
||||
REINSTALL = 'Переустановить'
|
||||
POINT_DOMAIN = 'Домен'
|
||||
POINT_PROXY = 'Прокси'
|
||||
POINT_CLOUDFLARE = 'Cloudflare'
|
||||
CLIENTS = 'Клиенты'
|
||||
USE_OLD = 'Использовать старый'
|
||||
|
||||
MAIN_KEYBOARD = [[FAQ, ADD_SERVER, HELP]]
|
||||
MAIN_KEYBOARD_WITH_POINTS = [[FAQ, ADD_SERVER, HELP], [MANAGE]]
|
||||
DRAFT_KEYBOARD = [[DOMAIN, CLOUDFLARE, PROXY, MASK], [CANCEL, INSTALL, SERVER, HELP]]
|
||||
POINT_KEYBOARD = [[RELEASE, REINSTALL], [POINT_DOMAIN, POINT_PROXY, POINT_CLOUDFLARE], [CLIENTS], [MAIN_MENU]]
|
||||
DRAFT_KEYBOARD = [[DOMAIN, PROXY, MASK], [CANCEL, INSTALL, SERVER, HELP]]
|
||||
POINT_KEYBOARD = [[RELEASE, REINSTALL], [POINT_DOMAIN, POINT_PROXY], [CLIENTS], [MAIN_MENU]]
|
||||
CANCEL_KEYBOARD = [[CANCEL]]
|
||||
|
||||
EMPTY = '-'
|
||||
@@ -69,7 +65,7 @@ HELP_TEXT = '''<b>Добавить сервер</b>
|
||||
1. "добавить сервер"
|
||||
2. "сервер" - пришлите <code>ip:пароль</code> сервера для панели
|
||||
3. по желанию "маскировка" - пришлите <code>ip:пароль</code> сервера маскировки, для нее нужен "домен"
|
||||
4. по желанию "домен", "cloudflare", "прокси"
|
||||
4. по желанию "домен", "прокси"
|
||||
5. "установить" - установка идет в фоне, по окончании придет уведомление
|
||||
|
||||
<b>Управлять сервером</b>
|
||||
@@ -83,12 +79,10 @@ ADDRESS_TAKEN = 'Сервер {address} уже подключен.'
|
||||
ADDRESS_DUPLICATE = 'Панель и маскировка должны быть разными серверами.'
|
||||
DOMAIN_PROMPT = 'Пришли доменное имя. Доменное имя лучше подходит для маскировки вашей точки доступа.'
|
||||
DOMAIN_INVALID = 'Не похоже на доменное имя. Попробуй еще раз.'
|
||||
CLOUDFLARE_PROMPT = 'Если вы спрятали домен за клаудом, то нужно включить опцию тут.'
|
||||
PROXY_PROMPT = 'Можно указать свои в формате <code>protocol://username:password@host:port</code> или включить cf_warp.'
|
||||
PROXY_INVALID = 'Не похоже на <code>protocol://username:password@host:port</code>. Попробуй еще раз.'
|
||||
DRAFT_INCOMPLETE = 'Нужен сервер для панели: "сервер".'
|
||||
MASK_WITHOUT_DOMAIN = 'Сервер маскировки работает только с доменом. Укажи домен или убери маскировку.'
|
||||
CLOUDFLARE_WITHOUT_DOMAIN = 'Cloudflare работает только с доменом. Укажи домен или выключи cloudflare.'
|
||||
INSTALL_STARTED = 'Запускаю установку сервера. Отправлю уведомление когда все будет готово.'
|
||||
|
||||
SELECT_PROMPT = 'Пришлите server_name'
|
||||
@@ -119,7 +113,6 @@ def draft_card(draft: dict) -> str:
|
||||
f"Сервер: {value(draft.get('server', {}).get('address', ''))}",
|
||||
f"Сервер маскировки: {value(draft.get('mask', {}).get('address', ''))}",
|
||||
f"Домен: {value(draft.get('domain', ''))}",
|
||||
f"Cloudflare: {'Да' if draft.get('cloudflare') else EMPTY}",
|
||||
f"Прокси: {value(draft.get('proxy', ''))}",
|
||||
])
|
||||
|
||||
@@ -141,7 +134,6 @@ def point_card(point: Point) -> str:
|
||||
# TODO: онлайн/всего ключей из API панели
|
||||
f'Ключи: {EMPTY}',
|
||||
f'Домен: {value(point.domain)}',
|
||||
f"Cloudflare: {'Да' if point.cloudflare else EMPTY}",
|
||||
f'Прокси: {value(point.outbound_proxy)}',
|
||||
]
|
||||
if point.error:
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 6.1.1 on 2026-09-25 13:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('App', '0004_point_subscription_inbound'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='point',
|
||||
name='cloudflare',
|
||||
),
|
||||
]
|
||||
+1
-4
@@ -85,8 +85,7 @@ class PointQuerySet(models.QuerySet):
|
||||
|
||||
class PointManager(models.Manager.from_queryset(PointQuerySet)):
|
||||
def create_point(
|
||||
self, owner, panel: Credentials, mask: Credentials | None = None, *, domain: str = '',
|
||||
cloudflare: bool = False, outbound_proxy: str = '',
|
||||
self, owner, panel: Credentials, mask: Credentials | None = None, *, domain: str = '', outbound_proxy: str = '',
|
||||
) -> 'Point':
|
||||
"""Создает точку вместе с ее серверами. Вызывать через Point.objects.
|
||||
|
||||
@@ -98,7 +97,6 @@ class PointManager(models.Manager.from_queryset(PointQuerySet)):
|
||||
owner=owner,
|
||||
name=self._unique_name(),
|
||||
domain=domain,
|
||||
cloudflare=cloudflare,
|
||||
outbound_proxy=outbound_proxy,
|
||||
panel_username=generate_username(),
|
||||
panel_password=generate_password(),
|
||||
@@ -141,7 +139,6 @@ class Point(models.Model):
|
||||
error = models.TextField(blank=True)
|
||||
|
||||
domain = models.CharField(max_length=253, blank=True)
|
||||
cloudflare = models.BooleanField(default=False, help_text='Домен спрятан за Cloudflare')
|
||||
outbound_proxy = models.CharField(
|
||||
max_length=512, blank=True, help_text=f'protocol://user:password@host:port или {CF_WARP}',
|
||||
)
|
||||
|
||||
+2
-2
@@ -60,8 +60,8 @@ def set_domain(point: Point, domain: str):
|
||||
|
||||
Домен должен уже указывать на панель или на сервер маскировки.
|
||||
"""
|
||||
if not domain and (point.mask or point.cloudflare):
|
||||
raise ValueError('маскировка и cloudflare работают только с доменом')
|
||||
if not domain and point.mask:
|
||||
raise ValueError('маскировка работает только с доменом')
|
||||
with _installing(point):
|
||||
_check(point.panel, serverus.XuiPanel(point.panel.host()).issue_certificate(domain))
|
||||
point.domain = domain
|
||||
|
||||
+1
-11
@@ -72,19 +72,16 @@ class AddServerTests(BotTestCase):
|
||||
self.fill(texts.MASK, '10.0.0.2:mask-pw')
|
||||
self.say(texts.DOMAIN)
|
||||
self.say('Example.COM.')
|
||||
self.say(texts.CLOUDFLARE)
|
||||
self.say(texts.ENABLE)
|
||||
self.say(texts.PROXY)
|
||||
card = self.say(texts.CF_WARP)
|
||||
self.assertIn('Домен: example.com', card)
|
||||
self.assertIn('Cloudflare: Да', card)
|
||||
self.assertIn('Прокси: cf_warp', card)
|
||||
|
||||
self.assertEqual(self.say(texts.INSTALL), texts.INSTALL_STARTED)
|
||||
point = Point.objects.get()
|
||||
self.assertEqual((point.panel.address, point.panel.root_password), ('10.0.0.1', 'panel-pw'))
|
||||
self.assertEqual((point.mask.address, point.mask.root_password), ('10.0.0.2', 'mask-pw'))
|
||||
self.assertEqual((point.domain, point.cloudflare, point.outbound_proxy), ('example.com', True, 'cf_warp'))
|
||||
self.assertEqual((point.domain, point.outbound_proxy), ('example.com', 'cf_warp'))
|
||||
self.assertEqual(point.owner, TelegramUser.objects.get())
|
||||
send.assert_called_once_with(point.pk, Chat.objects.get().pk)
|
||||
# сообщения с паролями удалены из чата
|
||||
@@ -123,13 +120,6 @@ class AddServerTests(BotTestCase):
|
||||
self.assertEqual(self.say(texts.INSTALL), texts.INSTALL_STARTED)
|
||||
self.assertIsNone(Point.objects.get().mask)
|
||||
|
||||
def test_cloudflare_requires_domain(self, send):
|
||||
self.say(texts.ADD_SERVER)
|
||||
self.fill(texts.SERVER, '10.0.0.1:pw')
|
||||
self.fill(texts.CLOUDFLARE, texts.ENABLE)
|
||||
self.assertEqual(self.say(texts.INSTALL), texts.CLOUDFLARE_WITHOUT_DOMAIN)
|
||||
send.assert_not_called()
|
||||
|
||||
def test_cancel_in_prompt_returns_to_draft(self, send):
|
||||
self.say(texts.ADD_SERVER)
|
||||
self.fill(texts.SERVER, '10.0.0.1:pw')
|
||||
|
||||
Reference in new issue
Block a user