feature!: убрана поддержка Cloudlfare

К сожалению проксирование недоступно принципиально
This commit is contained in:
protokey committed 2026-09-25 17:40:00 +04:00
1 parent 96213e182e
commit 064bb70f32
7 files changed
+28 -56

No files matched your search

+3 -3
View File
@@ -4,21 +4,21 @@ Django приложение связывающее все части сайта
-[x] Реализует кастомный менеджер модели и кастомную модель пользователя с отключенным email. -[x] Реализует кастомный менеджер модели и кастомную модель пользователя с отключенным email.
-[x] Модель данных для поддержки интерфейса и функционала -[x] Модель данных для поддержки интерфейса и функционала
-[ ] Настройки домена, прокси и cloudflare у готовой точки -[ ] Настройки домена и прокси у готовой точки
-[ ] Клиенты и статистика через API панели 3x-ui -[ ] Клиенты и статистика через API панели 3x-ui
Зависимости: `Telegram` (интерфейс бота, пользователи, уведомления) и `serverus` (настройка серверов). Зависимости: `Telegram` (интерфейс бота, пользователи, уведомления) и `serverus` (настройка серверов).
## Модель данных ## Модель данных
- `Point` - точка доступа: сервер с панелью 3x-ui и необязательный сервер маскировки. Владелец - `Telegram.User`. Хранит домен, cloudflare, прокси на выходе и доступ к панели. - `Point` - точка доступа: сервер с панелью 3x-ui и необязательный сервер маскировки. Владелец - `Telegram.User`. Хранит домен, прокси на выходе и доступ к панели.
- `Server` - сервер точки с ролью `panel` или `mask`. Хранит текущие порт ssh, пароль root, ключ и отпечаток ssh, а также пароль root, присланный пользователем при добавлении (`default_root_password`). - `Server` - сервер точки с ролью `panel` или `mask`. Хранит текущие порт ssh, пароль root, ключ и отпечаток ssh, а также пароль root, присланный пользователем при добавлении (`default_root_password`).
С маскировкой клиенты подключаются к серверу маскировки, он перенаправляет трафик на панель и скрывает ее адрес. Маскировка требует домен. С маскировкой клиенты подключаются к серверу маскировки, он перенаправляет трафик на панель и скрывает ее адрес. Маскировка требует домен.
Работа с записями идет через менеджеры и методы моделей, а не через `filter`/`create` напрямую: Работа с записями идет через менеджеры и методы моделей, а не через `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.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.set_status(status, error)`, `server.reset_fingerprint()`
- удаление: `point.delete()`, серверы удаляются вместе с точкой - удаление: `point.delete()`, серверы удаляются вместе с точкой
+1 -25
View File
@@ -13,7 +13,6 @@ DRAFT = 'draft'
DRAFT_SERVER = 'draft.server' DRAFT_SERVER = 'draft.server'
DRAFT_MASK = 'draft.mask' DRAFT_MASK = 'draft.mask'
DRAFT_DOMAIN = 'draft.domain' DRAFT_DOMAIN = 'draft.domain'
DRAFT_CLOUDFLARE = 'draft.cloudflare'
DRAFT_PROXY = 'draft.proxy' DRAFT_PROXY = 'draft.proxy'
SELECT = 'select' SELECT = 'select'
POINT = 'point' POINT = 'point'
@@ -103,10 +102,6 @@ class MyPointInterface(Interface):
def ask_domain(self, ctx): def ask_domain(self, ctx):
self.ask(ctx, DRAFT_DOMAIN, texts.DOMAIN_PROMPT, [[texts.CANCEL, texts.DELETE]]) 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) @text(texts.PROXY, state=DRAFT)
def ask_proxy(self, ctx): def ask_proxy(self, ctx):
self.ask(ctx, DRAFT_PROXY, texts.PROXY_PROMPT, [[texts.CF_WARP, texts.CANCEL, texts.DELETE]]) 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_SERVER)
@text(texts.CANCEL, state=DRAFT_MASK) @text(texts.CANCEL, state=DRAFT_MASK)
@text(texts.CANCEL, state=DRAFT_DOMAIN) @text(texts.CANCEL, state=DRAFT_DOMAIN)
@text(texts.CANCEL, state=DRAFT_CLOUDFLARE)
@text(texts.CANCEL, state=DRAFT_PROXY) @text(texts.CANCEL, state=DRAFT_PROXY)
def back_to_draft(self, ctx): def back_to_draft(self, ctx):
self.show_draft(ctx) self.show_draft(ctx)
@@ -153,20 +147,6 @@ class MyPointInterface(Interface):
ctx.data['draft']['domain'] = '' ctx.data['draft']['domain'] = ''
self.show_draft(ctx) 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) @state(DRAFT_PROXY)
def set_proxy(self, ctx): def set_proxy(self, ctx):
proxy = ctx.text.strip() proxy = ctx.text.strip()
@@ -192,14 +172,11 @@ class MyPointInterface(Interface):
return ctx.reply(texts.DRAFT_INCOMPLETE) return ctx.reply(texts.DRAFT_INCOMPLETE)
if draft.get('mask') and not draft.get('domain'): if draft.get('mask') and not draft.get('domain'):
return ctx.reply(texts.MASK_WITHOUT_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( point = Point.objects.create_point(
ctx.user, ctx.user,
panel=Credentials(**draft['server']), panel=Credentials(**draft['server']),
mask=Credentials(**draft['mask']) if draft.get('mask') else None, mask=Credentials(**draft['mask']) if draft.get('mask') else None,
domain=draft.get('domain', ''), domain=draft.get('domain', ''),
cloudflare=draft.get('cloudflare', False),
outbound_proxy=draft.get('proxy', ''), outbound_proxy=draft.get('proxy', ''),
) )
tasks.install_point.send(point.pk, ctx.chat.pk) 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_DOMAIN, state=POINT)
@text(texts.POINT_PROXY, state=POINT) @text(texts.POINT_PROXY, state=POINT)
@text(texts.POINT_CLOUDFLARE, state=POINT)
@text(texts.CLIENTS, state=POINT) @text(texts.CLIENTS, state=POINT)
def not_implemented(self, ctx): def not_implemented(self, ctx):
# TODO: настройки домена, прокси, cloudflare и клиенты через API панели 3x-ui # TODO: настройки домена, прокси и клиенты через API панели 3x-ui
ctx.reply(texts.NOT_IMPLEMENTED) ctx.reply(texts.NOT_IMPLEMENTED)
+3 -11
View File
@@ -20,11 +20,8 @@ NO = 'Нет'
SERVER = 'сервер' SERVER = 'сервер'
MASK = 'маскировка' MASK = 'маскировка'
DOMAIN = 'домен' DOMAIN = 'домен'
CLOUDFLARE = 'cloudflare'
PROXY = 'прокси' PROXY = 'прокси'
INSTALL = 'установить' INSTALL = 'установить'
ENABLE = 'включить'
DISABLE = 'выключить'
CF_WARP = 'cf_warp' CF_WARP = 'cf_warp'
# меню сервера # меню сервера
@@ -33,14 +30,13 @@ RELEASE = 'Вернуть'
REINSTALL = 'Переустановить' REINSTALL = 'Переустановить'
POINT_DOMAIN = 'Домен' POINT_DOMAIN = 'Домен'
POINT_PROXY = 'Прокси' POINT_PROXY = 'Прокси'
POINT_CLOUDFLARE = 'Cloudflare'
CLIENTS = 'Клиенты' CLIENTS = 'Клиенты'
USE_OLD = 'Использовать старый' USE_OLD = 'Использовать старый'
MAIN_KEYBOARD = [[FAQ, ADD_SERVER, HELP]] MAIN_KEYBOARD = [[FAQ, ADD_SERVER, HELP]]
MAIN_KEYBOARD_WITH_POINTS = [[FAQ, ADD_SERVER, HELP], [MANAGE]] MAIN_KEYBOARD_WITH_POINTS = [[FAQ, ADD_SERVER, HELP], [MANAGE]]
DRAFT_KEYBOARD = [[DOMAIN, CLOUDFLARE, PROXY, MASK], [CANCEL, INSTALL, SERVER, HELP]] DRAFT_KEYBOARD = [[DOMAIN, PROXY, MASK], [CANCEL, INSTALL, SERVER, HELP]]
POINT_KEYBOARD = [[RELEASE, REINSTALL], [POINT_DOMAIN, POINT_PROXY, POINT_CLOUDFLARE], [CLIENTS], [MAIN_MENU]] POINT_KEYBOARD = [[RELEASE, REINSTALL], [POINT_DOMAIN, POINT_PROXY], [CLIENTS], [MAIN_MENU]]
CANCEL_KEYBOARD = [[CANCEL]] CANCEL_KEYBOARD = [[CANCEL]]
EMPTY = '-' EMPTY = '-'
@@ -69,7 +65,7 @@ HELP_TEXT = '''<b>Добавить сервер</b>
1. "добавить сервер" 1. "добавить сервер"
2. "сервер" - пришлите <code>ip:пароль</code> сервера для панели 2. "сервер" - пришлите <code>ip:пароль</code> сервера для панели
3. по желанию "маскировка" - пришлите <code>ip:пароль</code> сервера маскировки, для нее нужен "домен" 3. по желанию "маскировка" - пришлите <code>ip:пароль</code> сервера маскировки, для нее нужен "домен"
4. по желанию "домен", "cloudflare", "прокси" 4. по желанию "домен", "прокси"
5. "установить" - установка идет в фоне, по окончании придет уведомление 5. "установить" - установка идет в фоне, по окончании придет уведомление
<b>Управлять сервером</b> <b>Управлять сервером</b>
@@ -83,12 +79,10 @@ ADDRESS_TAKEN = 'Сервер {address} уже подключен.'
ADDRESS_DUPLICATE = 'Панель и маскировка должны быть разными серверами.' ADDRESS_DUPLICATE = 'Панель и маскировка должны быть разными серверами.'
DOMAIN_PROMPT = 'Пришли доменное имя. Доменное имя лучше подходит для маскировки вашей точки доступа.' DOMAIN_PROMPT = 'Пришли доменное имя. Доменное имя лучше подходит для маскировки вашей точки доступа.'
DOMAIN_INVALID = 'Не похоже на доменное имя. Попробуй еще раз.' DOMAIN_INVALID = 'Не похоже на доменное имя. Попробуй еще раз.'
CLOUDFLARE_PROMPT = 'Если вы спрятали домен за клаудом, то нужно включить опцию тут.'
PROXY_PROMPT = 'Можно указать свои в формате <code>protocol://username:password@host:port</code> или включить cf_warp.' PROXY_PROMPT = 'Можно указать свои в формате <code>protocol://username:password@host:port</code> или включить cf_warp.'
PROXY_INVALID = 'Не похоже на <code>protocol://username:password@host:port</code>. Попробуй еще раз.' PROXY_INVALID = 'Не похоже на <code>protocol://username:password@host:port</code>. Попробуй еще раз.'
DRAFT_INCOMPLETE = 'Нужен сервер для панели: "сервер".' DRAFT_INCOMPLETE = 'Нужен сервер для панели: "сервер".'
MASK_WITHOUT_DOMAIN = 'Сервер маскировки работает только с доменом. Укажи домен или убери маскировку.' MASK_WITHOUT_DOMAIN = 'Сервер маскировки работает только с доменом. Укажи домен или убери маскировку.'
CLOUDFLARE_WITHOUT_DOMAIN = 'Cloudflare работает только с доменом. Укажи домен или выключи cloudflare.'
INSTALL_STARTED = 'Запускаю установку сервера. Отправлю уведомление когда все будет готово.' INSTALL_STARTED = 'Запускаю установку сервера. Отправлю уведомление когда все будет готово.'
SELECT_PROMPT = 'Пришлите server_name' 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('server', {}).get('address', ''))}",
f"Сервер маскировки: {value(draft.get('mask', {}).get('address', ''))}", f"Сервер маскировки: {value(draft.get('mask', {}).get('address', ''))}",
f"Домен: {value(draft.get('domain', ''))}", f"Домен: {value(draft.get('domain', ''))}",
f"Cloudflare: {'Да' if draft.get('cloudflare') else EMPTY}",
f"Прокси: {value(draft.get('proxy', ''))}", f"Прокси: {value(draft.get('proxy', ''))}",
]) ])
@@ -141,7 +134,6 @@ def point_card(point: Point) -> str:
# TODO: онлайн/всего ключей из API панели # TODO: онлайн/всего ключей из API панели
f'Ключи: {EMPTY}', f'Ключи: {EMPTY}',
f'Домен: {value(point.domain)}', f'Домен: {value(point.domain)}',
f"Cloudflare: {'Да' if point.cloudflare else EMPTY}",
f'Прокси: {value(point.outbound_proxy)}', f'Прокси: {value(point.outbound_proxy)}',
] ]
if point.error: 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
View File
@@ -85,8 +85,7 @@ class PointQuerySet(models.QuerySet):
class PointManager(models.Manager.from_queryset(PointQuerySet)): class PointManager(models.Manager.from_queryset(PointQuerySet)):
def create_point( def create_point(
self, owner, panel: Credentials, mask: Credentials | None = None, *, domain: str = '', self, owner, panel: Credentials, mask: Credentials | None = None, *, domain: str = '', outbound_proxy: str = '',
cloudflare: bool = False, outbound_proxy: str = '',
) -> 'Point': ) -> 'Point':
"""Создает точку вместе с ее серверами. Вызывать через Point.objects. """Создает точку вместе с ее серверами. Вызывать через Point.objects.
@@ -98,7 +97,6 @@ class PointManager(models.Manager.from_queryset(PointQuerySet)):
owner=owner, owner=owner,
name=self._unique_name(), name=self._unique_name(),
domain=domain, domain=domain,
cloudflare=cloudflare,
outbound_proxy=outbound_proxy, outbound_proxy=outbound_proxy,
panel_username=generate_username(), panel_username=generate_username(),
panel_password=generate_password(), panel_password=generate_password(),
@@ -141,7 +139,6 @@ class Point(models.Model):
error = models.TextField(blank=True) error = models.TextField(blank=True)
domain = models.CharField(max_length=253, blank=True) domain = models.CharField(max_length=253, blank=True)
cloudflare = models.BooleanField(default=False, help_text='Домен спрятан за Cloudflare')
outbound_proxy = models.CharField( outbound_proxy = models.CharField(
max_length=512, blank=True, help_text=f'protocol://user:password@host:port или {CF_WARP}', max_length=512, blank=True, help_text=f'protocol://user:password@host:port или {CF_WARP}',
) )
+2 -2
View File
@@ -60,8 +60,8 @@ def set_domain(point: Point, domain: str):
Домен должен уже указывать на панель или на сервер маскировки. Домен должен уже указывать на панель или на сервер маскировки.
""" """
if not domain and (point.mask or point.cloudflare): if not domain and point.mask:
raise ValueError('маскировка и cloudflare работают только с доменом') raise ValueError('маскировка работает только с доменом')
with _installing(point): with _installing(point):
_check(point.panel, serverus.XuiPanel(point.panel.host()).issue_certificate(domain)) _check(point.panel, serverus.XuiPanel(point.panel.host()).issue_certificate(domain))
point.domain = domain point.domain = domain
+1 -11
View File
@@ -72,19 +72,16 @@ class AddServerTests(BotTestCase):
self.fill(texts.MASK, '10.0.0.2:mask-pw') self.fill(texts.MASK, '10.0.0.2:mask-pw')
self.say(texts.DOMAIN) self.say(texts.DOMAIN)
self.say('Example.COM.') self.say('Example.COM.')
self.say(texts.CLOUDFLARE)
self.say(texts.ENABLE)
self.say(texts.PROXY) self.say(texts.PROXY)
card = self.say(texts.CF_WARP) card = self.say(texts.CF_WARP)
self.assertIn('Домен: example.com', card) self.assertIn('Домен: example.com', card)
self.assertIn('Cloudflare: Да', card)
self.assertIn('Прокси: cf_warp', card) self.assertIn('Прокси: cf_warp', card)
self.assertEqual(self.say(texts.INSTALL), texts.INSTALL_STARTED) self.assertEqual(self.say(texts.INSTALL), texts.INSTALL_STARTED)
point = Point.objects.get() point = Point.objects.get()
self.assertEqual((point.panel.address, point.panel.root_password), ('10.0.0.1', 'panel-pw')) 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.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()) self.assertEqual(point.owner, TelegramUser.objects.get())
send.assert_called_once_with(point.pk, Chat.objects.get().pk) 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.assertEqual(self.say(texts.INSTALL), texts.INSTALL_STARTED)
self.assertIsNone(Point.objects.get().mask) 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): def test_cancel_in_prompt_returns_to_draft(self, send):
self.say(texts.ADD_SERVER) self.say(texts.ADD_SERVER)
self.fill(texts.SERVER, '10.0.0.1:pw') self.fill(texts.SERVER, '10.0.0.1:pw')