ddf0ddf71e
Сквозная миграция HY2XS на современную Hysteria (2.12.2) и переход на v1. Build: - версия Hysteria резолвится на этапе сборки из HyNetworks/hysteria и замораживается в metadata пакета (version + immutable url + sha256); - compatibility gate: реальный бинарник должен принять канонический конфиг HY2XS для gecko и salamander до создания пакета; - сборка прогоняет тесты оркестратора и админки. Конфигурационный контракт: - HY2XS_CONFIG_SCHEMA_VERSION=2, чужая схема отклоняется fail-fast; - obfs стал настоящим union gecko|salamander, gecko — default; - obfs-блок рендерится оркестратором целиком, два подтипа одновременно структурно невозможны; - современный baseline: congestion bbr/standard, disableLossCompensation=false, disableStatelessReset=false, полный quic-блок. Исправления: - share URI для gecko: генератор был завязан на Obfs.Salamander.Password и выдавал нерабочую ссылку при любой другой обфускации; - SNI брался только из ACME-блока и уходил пустым при HY2XS_TLS_MODE=file; - экспорт конфига выносил trafficStats.secret, access_token и obfs-пароль; - экспорт терял неизвестные upstream-поля при round-trip через типизированную модель; - renderRuntimeEnv печатал тип обфускации литералом, расходясь с конфигом; - namedotcom удалён из ACME-реестра (нет в Hysteria с 2.11.0). Тесты: - 95 тестов оркестратора: env, рендер, семантика профиля, резолвер, rollover; - тесты URI и экспорта в Go; - tools/test/e2e-hysteria.sh с реальным клиентом Hysteria. UX: - подсказки и примеры в форме создания пира. Прочее: CHANGELOG.md, .gitattributes (LF для target-side файлов), документация на русском.
287 lines
6.7 KiB
Go
287 lines
6.7 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)
|
|
}
|
|
|
|
func UpdateHysteria2Config(c *gin.Context) {
|
|
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", 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 ImportHysteria2Config(c *gin.Context) {
|
|
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func Hysteria2AcmePath(c *gin.Context) {
|
|
hysteria2AcmePathVo, err := service.Hysteria2AcmePath()
|
|
if err != nil {
|
|
vo.Fail(err.Error(), c)
|
|
return
|
|
}
|
|
vo.Success(hysteria2AcmePathVo, c)
|
|
}
|
|
|
|
func RestartServer(c *gin.Context) {
|
|
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
|
|
}
|
|
|
|
func UploadCertFile(c *gin.Context) {
|
|
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
|
|
}
|