Files
HY2XS_flamy/apps/controller/config.go
T
founder 19ffc80130 refactor(admin): удалить мёртвый updater/config-write API и его UI
Маршруты, операциями которых продукт не владеет, отвечали заглушкой
"managed by orchestrator" или пустым списком:

  POST /hysteria2ChangeVersion
  GET  /listRelease
  POST /config/updateHysteria2Config
  POST /config/importHysteria2Config
  POST /config/restartServer
  POST /config/uploadCertFile
  GET  /config/hysteria2AcmePath   (не имел потребителя вовсе)

Они удалены, а не оставлены заглушками. Причины две. API-контракт не
должен обещать updater, которого у продукта принципиально нет:
маршрут, всегда возвращающий отказ, вводит в заблуждение. И это лишняя
attack surface плюс технический мусор от прежней архитектуры.

Вместе с маршрутами убраны мёртвые сервисы (StartHysteria2,
StopHysteria2, RestartHysteria2, SetHysteria2Config,
UpdateHysteria2Config, GetAuthHttpUrl, Hysteria2AcmePath), неиспользуемые
типы и клиентские функции фронтенда.

Отдельно - кнопки. "Перезапустить панель" и загрузка сертификатов
обращались к заглушкам, то есть гарантированно возвращали ошибку.
Кнопка, которая всегда падает, - не точка расширения на будущее, а
дефект UX. Удалены вместе со строками i18n.

Конфигурация Hysteria остаётся доступной панели на чтение и на
выгрузку: getHysteria2Config и exportHysteria2Config.
2026-08-27 12:15:53 +05:00

262 lines
6.1 KiB
Go

package controller
import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/dto"
"hy2xs-admin/model/entity"
"hy2xs-admin/model/vo"
"hy2xs-admin/service"
"hy2xs-admin/util"
"io"
"path/filepath"
"strconv"
"strings"
"time"
)
func isOrchestratorManagedConfigKey(key string) bool {
switch key {
case constant.HUIWebPort,
constant.HUIWebContext,
constant.HUICrtPath,
constant.HUIKeyPath,
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
}
port, crtPath, keyPath, err := service.GetPortAndCert()
if err != nil {
vo.Fail(err.Error(), c)
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.HUIWebPort && strconv.FormatInt(port, 10) != value {
port, err := strconv.Atoi(value)
if err != nil {
vo.Fail(fmt.Sprintf("port: %s is invalid", value), c)
return
}
if !util.IsPortAvailable(uint(port), "tcp") {
vo.Fail(fmt.Sprintf("port: %s is used", value), c)
return
}
needRestart = true
}
if key == constant.HUICrtPath && crtPath != value {
if value != "" && !util.Exists(value) {
vo.Fail(fmt.Sprintf("crt path: %s is not exist", value), c)
return
}
needRestart = true
}
if key == constant.HUIKeyPath && keyPath != value {
if value != "" && !util.Exists(value) {
vo.Fail(fmt.Sprintf("key path: %s is not exist", value), c)
return
}
needRestart = true
}
if key == constant.HUIWebContext {
huiWebContext, err := service.GetConfig(constant.HUIWebContext)
if err != nil {
vo.Fail(err.Error(), c)
return
}
if *huiWebContext.Value != value {
needRestart = true
}
}
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)
}
func ExportConfig(c *gin.Context) {
configs, err := service.ListConfigNotIn([]string{constant.Hysteria2Config})
if err != nil {
vo.Fail(err.Error(), c)
return
}
fileName := fmt.Sprintf("SystemConfig-%s.json", time.Now().Format("20060102150405"))
filePath := filepath.Join(constant.ExportPathDir, fileName)
if err = util.ExportFile(filePath, configs, 0); err != nil {
vo.Fail(err.Error(), c)
return
}
if !util.Exists(filePath) {
vo.Fail("file not exist", c)
return
}
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Transfer-Encoding", "binary")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName))
c.File(filePath)
}
func ImportConfig(c *gin.Context) {
file, header, err := c.Request.FormFile("file")
if err != nil {
vo.Fail(constant.SysError, c)
return
}
if header.Size > 1024*1024*2 {
vo.Fail("the file is too big", c)
return
}
if !strings.HasSuffix(header.Filename, ".json") {
vo.Fail(constant.InvalidError, c)
return
}
content, err := io.ReadAll(file)
if err != nil {
vo.Fail("json file read err", c)
return
}
var configs []entity.Config
if err = json.Unmarshal(content, &configs); err != nil {
vo.Fail("content Unmarshal err", c)
return
}
for _, cfg := range configs {
if cfg.Key != nil && isOrchestratorManagedConfigKey(*cfg.Key) {
vo.Fail(fmt.Sprintf("%s managed by orchestrator: use hy2xs-orchestrator reconfigure", *cfg.Key), c)
return
}
}
if err = service.UpsertConfig(configs); err != nil {
vo.Fail(err.Error(), c)
return
}
go func() {
_ = service.StopServer()
}()
vo.Success(nil, c)
}