Files
HY2XS_flamy/apps/controller/config.go
T
founder b52fac1394 fix(admin): убрать каналы утечки секретов и остатки H UI из runtime
Экспорт в панели формировался через os.Create в /var/lib/hy2xs-admin/export,
и файл там оставался навсегда. При includeSecrets=true это означало
расшифрованные секреты пиров — фактические учётные данные доступа — в
открытом виде на диске, накапливающиеся с каждым нажатием кнопки. Выгрузки
формируются в памяти, каталога export/ больше нет.

Generic export/import таблицы config удалён целиком. Он исключал только сырой
Hysteria YAML, а в той же таблице лежат JWT_SECRET, PEER_SECRET_KEY,
PEER_SECRET_ENCRYPTION_KEY и HYSTERIA2_TRAFFIC_STATS_SECRET: кнопка Export
выгружала их открытым текстом, импорт позволял подменить. Для
PEER_SECRET_ENCRYPTION_KEY подмена ломает расшифровку секретов уже
существующих пиров. Production-сценария у этой пары не было.

Импорт пиров шёл мимо всей валидации, которую проходит обычное создание пира:
в базу попадало имя любой длины и с любыми символами, disabled с произвольным
числом, отрицательные счётчики. Файл применялся построчно, поэтому ошибка в
середине оставляла список наполовину изменённым, а импорт мог перезаписать
bootstrap-admin-peer, чей секрет продублирован в bootstrap-admin.secret.
Партия проверяется целиком до первой записи, неизвестные поля отклоняются.

Убран слой сетевых настроек панели: H_UI_WEB_PORT, H_UI_WEB_CONTEXT,
H_UI_CRT_PATH, H_UI_KEY_PATH и собственный TLS. Оркестратор передавал порт
аргументом, админка писала его в SQLite и тут же читала обратно, а UI
показывал поля disabled — второй источник истины, из которого ничего нельзя
было изменить. HUI_DATA/HUI_LOG заменены на HY2XS_DATA_DIR/HY2XS_LOG_DIR,
база переименована в hy2xs-admin.db, reference-схема — в schema.sql.

API namespace разделён по природе маршрутов: операторский API на /api,
machine-auth Hysteria на /internal/hysteria/auth. Путь machine-auth —
runtime-контракт, он уезжает в config.yaml и post-install.env, поэтому
объявлен одной константой на компонент.

Go-санитайзер экспорта вырезал секреты из URL только у ключей url/addr:
будущее upstream-поле с другим именем уносило учётные данные и access_token
целиком, а URL внутри списков не обрабатывались вовсе. Граница определяется
значением, а не именем ключа — как в TS-санитайзере оркестратора.

Заодно индикатор загрузки и цвета 401/404 переведены на брендовый токен:
NProgress приходил со своим #29d и был единственным элементом вне палитры.
2026-08-27 20:49:07 +05:00

164 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package controller
import (
"fmt"
"github.com/gin-gonic/gin"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/dto"
"hy2xs-admin/model/vo"
"hy2xs-admin/service"
"time"
)
// Ключи, которыми владеет install-оркестратор: панель обязана отказать в
// записи, а не молча создать второй источник истины.
//
// Сетевых настроек самой панели в этом списке больше нет, потому что их нет
// и в базе: порт и bind приходят из контракта запуска, TLS терминируется
// снаружи. См. model/constant/config.go.
func isOrchestratorManagedConfigKey(key string) bool {
switch key {
case constant.Hysteria2Enable,
constant.Hysteria2Config,
constant.Hysteria2TrafficStatsSecret:
return true
default:
return false
}
}
func UpdateConfigs(c *gin.Context) {
configsUpdateDto, err := validateField(c, dto.ConfigsUpdateDto{})
if err != nil {
return
}
needRestart := false
for _, item := range configsUpdateDto.ConfigUpdateDtos {
key := *item.Key
value := *item.Value
if isOrchestratorManagedConfigKey(key) {
vo.Fail(fmt.Sprintf("%s managed by orchestrator: use hy2xs-orchestrator reconfigure", key), c)
return
}
if key == constant.ResetTrafficCron {
resetTrafficCron, err := service.GetConfig(constant.ResetTrafficCron)
if err != nil {
vo.Fail(err.Error(), c)
return
}
if *resetTrafficCron.Value != value {
needRestart = true
}
}
if err = service.UpdateConfig(key, value); err != nil {
vo.Fail(err.Error(), c)
return
}
}
if needRestart {
go func() {
_ = service.StopServer()
}()
}
vo.Success(nil, c)
}
func GetConfig(c *gin.Context) {
configDto, err := validateField(c, dto.ConfigDto{})
if err != nil {
return
}
config, err := service.GetConfig(*configDto.Key)
if err != nil {
vo.Fail(err.Error(), c)
return
}
configVo := vo.ConfigVo{
Key: *config.Key,
Value: *config.Value,
}
running := service.Hysteria2IsRunning()
if *config.Key == constant.Hysteria2Enable {
if running {
configVo.Value = "1"
} else {
configVo.Value = "0"
}
}
vo.Success(configVo, c)
}
func ListConfig(c *gin.Context) {
configsDto, err := validateField(c, dto.ConfigsDto{})
if err != nil {
return
}
configs, err := service.ListConfig(configsDto.Keys)
if err != nil {
vo.Fail(err.Error(), c)
return
}
var configVos []vo.ConfigVo
for _, item := range configs {
configVo := vo.ConfigVo{
Key: *item.Key,
Value: *item.Value,
}
configVos = append(configVos, configVo)
}
vo.Success(configVos, c)
}
func GetHysteria2Config(c *gin.Context) {
config, err := service.GetHysteria2Config()
if err != nil {
vo.Fail(err.Error(), c)
return
}
vo.Success(config, c)
}
// ExportHysteria2Config отдаёт оператору фактический серверный конфиг.
//
// Экспорт работает от исходного YAML, а не от типизированной модели: поля,
// о которых HY2XS ещё не знает, обязаны пережить выгрузку. Секреты при этом
// вырезаются — файл покидает сервер.
func ExportHysteria2Config(c *gin.Context) {
sanitized, err := service.ExportHysteria2ConfigYaml()
if err != nil {
vo.Fail(err.Error(), c)
return
}
fileName := fmt.Sprintf("Hysteria2Config-%s.yaml", time.Now().Format("20060102150405"))
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Transfer-Encoding", "binary")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName))
c.Data(200, "application/octet-stream", sanitized)
}
// Generic-выгрузки и загрузки таблицы `config` здесь нет намеренно.
//
// Она отдавала таблицу целиком, исключая только сырой Hysteria YAML, а в той
// же таблице лежат JWT_SECRET, PEER_SECRET_KEY, PEER_SECRET_ENCRYPTION_KEY и
// HYSTERIA2_TRAFFIC_STATS_SECRET. Кнопка «Export» в панели выгружала их в
// открытом виде, а зеркальный импорт позволял их подменить — включая ключ
// шифрования, без которого перестают расшифровываться секреты уже
// существующих пиров.
//
// Осмысленного production-сценария у этой пары не было: конфигурацией сервера
// владеет install-оркестратор, перенос пиров делают ImportPeer/ExportPeer, а
// серверный конфиг Hysteria выгружается отдельным санитизирующим маршрутом.
// Поэтому маршруты удалены, а не оставлены с фильтром: список ключей,
// требующий ручного сопровождения, снова разошёлся бы со схемой базы.