Files
HY2XS_flamy/apps/router/router.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

74 lines
2.5 KiB
Go

package router
import (
"github.com/gin-gonic/gin"
"hy2xs-admin/dao"
"hy2xs-admin/frontend"
"hy2xs-admin/middleware"
"hy2xs-admin/model/constant"
"os"
)
// Router собирает HTTP-контур админки.
//
// Панель всегда живёт в корне: настраиваемый web-контекст был возможностью H UI
// разворачивать панель по произвольному пути, а HY2XS слушает только loopback
// и отдаёт панель через SSH-туннель или reverse proxy. Настройка, которую UI
// уже не позволял менять, а backend продолжал читать из SQLite, — это не
// точка расширения, а лишний источник истины.
func Router(router *gin.Engine) {
router.GET("/healthz", func(c *gin.Context) {
sqliteReady := dao.IsSqliteReady()
configReadable := false
if stat, err := os.Stat(constant.Hysteria2ConfigPath); err == nil {
configReadable = !stat.IsDir()
}
if !sqliteReady || !configReadable {
c.JSON(503, gin.H{
"ok": false,
"service": "hy2xs-admin",
"sqlite_ready": sqliteReady,
"config_readable": configReadable,
})
return
}
c.JSON(200, gin.H{
"ok": true,
"service": "hy2xs-admin",
"sqlite_ready": true,
"config_readable": true,
})
})
frontend.InitFrontend(router, "/")
// Machine-контур. Не часть операторского API: это внутренний канал между
// Hysteria и админкой, поэтому у него отдельное пространство имён,
// собственный middleware и никакого JWT.
machineAPI := router.Group(constant.HysteriaMachineAuthPath)
machineAPI.Use(middleware.LocalOnlyHandler(), middleware.MachineAuthHandler(), middleware.LogHandler())
initHysteria2MachineAuthRouter(machineAPI)
api := router.Group(constant.AdminAPIBase)
authAPI := api.Group("")
authAPI.Use(middleware.FilterHandler(), middleware.LogHandler(), middleware.RateLimiterHandler())
initAuthRouter(authAPI)
adminAPI := api.Group("")
adminAPI.Use(
middleware.FilterHandler(),
middleware.LogHandler(),
middleware.RateLimiterHandler(),
middleware.JWTHandler(),
middleware.AdminHandler(),
)
initAdminRouter(adminAPI)
initDashboardRouter(adminAPI)
initPeerRouter(adminAPI)
initConfigRouter(adminAPI)
initLogRouter(adminAPI)
}