Подготовить HY2XS к production-сборке
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
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"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Login(c *gin.Context) {
|
||||
loginDto, err := validateField(c, dto.LoginDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !service.ExistAccountUsername(*loginDto.Username, 0) {
|
||||
vo.Fail("account not exist", c)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := service.Login(*loginDto.Username, util.SHA224String(*loginDto.Pass))
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
jwtVo := vo.JwtVo{
|
||||
TokenType: constant.TokenType,
|
||||
AccessToken: token,
|
||||
}
|
||||
vo.Success(jwtVo, c)
|
||||
}
|
||||
|
||||
func PageAccount(c *gin.Context) {
|
||||
accountPageDto, err := validateField(c, dto.AccountPageDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
accounts, total, err := service.PageAccount(accountPageDto)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
onlineUsers, err := service.Hysteria2Online()
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
var accountVos []vo.AccountVo
|
||||
for _, item := range accounts {
|
||||
accountVo := vo.AccountVo{
|
||||
Username: *item.Username,
|
||||
Quota: *item.Quota,
|
||||
Download: *item.Download,
|
||||
Upload: *item.Upload,
|
||||
ExpireTime: *item.ExpireTime,
|
||||
KickUtilTime: *item.KickUtilTime,
|
||||
DeviceNo: *item.DeviceNo,
|
||||
Role: *item.Role,
|
||||
Deleted: *item.Deleted,
|
||||
BaseVo: vo.BaseVo{
|
||||
Id: *item.Id,
|
||||
CreateTime: *item.CreateTime,
|
||||
},
|
||||
LoginAt: *item.LoginAt,
|
||||
ConAt: *item.ConAt,
|
||||
Remark: *item.Remark,
|
||||
}
|
||||
if value, exists := onlineUsers[*item.Username]; exists {
|
||||
accountVo.Online = true
|
||||
accountVo.Device = value
|
||||
delete(onlineUsers, *item.Username)
|
||||
}
|
||||
accountVos = append(accountVos, accountVo)
|
||||
}
|
||||
accountPageVo := vo.AccountPageVo{
|
||||
AccountVos: accountVos,
|
||||
Total: total,
|
||||
}
|
||||
vo.Success(accountPageVo, c)
|
||||
}
|
||||
|
||||
func SaveAccount(c *gin.Context) {
|
||||
accountSaveDto, err := validateField(c, dto.AccountSaveDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if service.ExistAccountUsername(*accountSaveDto.Username, 0) {
|
||||
vo.Fail(fmt.Sprintf("username %s already exists", *accountSaveDto.Username), c)
|
||||
return
|
||||
}
|
||||
|
||||
passEncrypt := util.SHA224String(*accountSaveDto.Pass)
|
||||
conPass := fmt.Sprintf("%s.%s", *accountSaveDto.Username, *accountSaveDto.ConPass)
|
||||
account := entity.Account{
|
||||
Username: accountSaveDto.Username,
|
||||
Pass: &passEncrypt,
|
||||
ConPass: &conPass,
|
||||
Quota: accountSaveDto.Quota,
|
||||
ExpireTime: accountSaveDto.ExpireTime,
|
||||
DeviceNo: accountSaveDto.DeviceNo,
|
||||
Deleted: accountSaveDto.Deleted,
|
||||
Remark: accountSaveDto.Remark,
|
||||
}
|
||||
err = service.SaveAccount(account)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
vo.Success(nil, c)
|
||||
}
|
||||
|
||||
func DeleteAccount(c *gin.Context) {
|
||||
idDto, err := validateField(c, dto.IdDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
account, err := service.GetAccount(*idDto.Id)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if *account.Role == "admin" {
|
||||
vo.Fail("admin cannot be deleted", c)
|
||||
return
|
||||
}
|
||||
err = service.DeleteAccount([]int64{*idDto.Id})
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
vo.Success(nil, c)
|
||||
}
|
||||
|
||||
func UpdateAccount(c *gin.Context) {
|
||||
accountUpdateDto, err := validateField(c, dto.AccountUpdateDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if accountUpdateDto.Username != nil && *accountUpdateDto.Username != "" && service.ExistAccountUsername(*accountUpdateDto.Username, *accountUpdateDto.Id) {
|
||||
vo.Fail(fmt.Sprintf("username %s already exists", *accountUpdateDto.Username), c)
|
||||
return
|
||||
}
|
||||
|
||||
if accountUpdateDto.Deleted != nil && *accountUpdateDto.Deleted == 1 {
|
||||
account, err := service.GetAccount(*accountUpdateDto.Id)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if *account.Role == "admin" {
|
||||
vo.Fail("the admin account cannot be deleted", c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var passEncrypt *string
|
||||
if accountUpdateDto.Pass != nil && *accountUpdateDto.Pass != "" {
|
||||
passEncryptSha224 := util.SHA224String(*accountUpdateDto.Pass)
|
||||
passEncrypt = &passEncryptSha224
|
||||
}
|
||||
|
||||
account := entity.Account{
|
||||
Username: accountUpdateDto.Username,
|
||||
Pass: passEncrypt,
|
||||
ConPass: accountUpdateDto.ConPass,
|
||||
Quota: accountUpdateDto.Quota,
|
||||
ExpireTime: accountUpdateDto.ExpireTime,
|
||||
DeviceNo: accountUpdateDto.DeviceNo,
|
||||
Deleted: accountUpdateDto.Deleted,
|
||||
Remark: accountUpdateDto.Remark,
|
||||
BaseEntity: entity.BaseEntity{
|
||||
Id: accountUpdateDto.Id,
|
||||
},
|
||||
}
|
||||
if err = service.UpdateAccount(account); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
vo.Success(nil, c)
|
||||
}
|
||||
|
||||
func ResetTraffic(c *gin.Context) {
|
||||
idDto, err := validateField(c, dto.IdDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = service.ResetTraffic(*idDto.Id); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
vo.Success(nil, c)
|
||||
}
|
||||
|
||||
func GetAccountInfo(c *gin.Context) {
|
||||
accountInfoVo, err := service.GetAccountInfo(c)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
// 更新最近登录时间
|
||||
now := time.Now().UnixMilli()
|
||||
if err = service.UpdateAccount(entity.Account{
|
||||
BaseEntity: entity.BaseEntity{Id: &accountInfoVo.Id},
|
||||
LoginAt: &now,
|
||||
}); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
vo.Success(accountInfoVo, c)
|
||||
}
|
||||
|
||||
func GetAccount(c *gin.Context) {
|
||||
idDto, err := validateField(c, dto.IdDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
account, err := service.GetAccount(*idDto.Id)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
accountVo := vo.AccountVo{
|
||||
BaseVo: vo.BaseVo{
|
||||
Id: *account.Id,
|
||||
CreateTime: *account.CreateTime,
|
||||
},
|
||||
Username: *account.Username,
|
||||
Quota: *account.Quota,
|
||||
Download: *account.Download,
|
||||
Upload: *account.Upload,
|
||||
ExpireTime: *account.ExpireTime,
|
||||
DeviceNo: *account.DeviceNo,
|
||||
Role: *account.Role,
|
||||
Deleted: *account.Deleted,
|
||||
Remark: *account.Remark,
|
||||
}
|
||||
vo.Success(accountVo, c)
|
||||
}
|
||||
|
||||
func ImportAccount(c *gin.Context) {
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
vo.Fail(constant.SysError, c)
|
||||
return
|
||||
}
|
||||
// 文件大小 2MB
|
||||
if header.Size > 1024*1024*2 {
|
||||
vo.Fail("the file is too big", c)
|
||||
return
|
||||
}
|
||||
// 文件后缀.json
|
||||
if !strings.HasSuffix(header.Filename, ".json") {
|
||||
vo.Fail("file format not supported", c)
|
||||
return
|
||||
}
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
vo.Fail("json file read err", c)
|
||||
return
|
||||
}
|
||||
var accounts []entity.Account
|
||||
if err = json.Unmarshal(content, &accounts); err != nil {
|
||||
vo.Fail("content Unmarshal err", c)
|
||||
return
|
||||
}
|
||||
if err = service.UpsertAccount(accounts); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
vo.Success(nil, c)
|
||||
}
|
||||
|
||||
func ExportAccount(c *gin.Context) {
|
||||
accountExports, err := service.ListExportAccount()
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("AccountExport-%s.json", time.Now().Format("20060102150405"))
|
||||
filePath := constant.ExportPathDir + fileName
|
||||
|
||||
if err = util.ExportFile(filePath, accountExports, 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 ReleaseKickAccount(c *gin.Context) {
|
||||
idDto, err := validateField(c, dto.IdDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = service.ReleaseKickAccount(*idDto.Id); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
vo.Success(nil, c)
|
||||
}
|
||||
|
||||
func VerifyDefaultPass(c *gin.Context) {
|
||||
info, err := service.GetAccountInfo(c)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
account, err := service.GetAccount(info.Id)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
vo.Success(account.Pass != nil && *account.Pass == "02f382b76ca1ab7aa06ab03345c7712fd5b971fb0c0f2aef98bac9cd", c)
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
"hy2xs-admin/dao"
|
||||
"hy2xs-admin/model/bo"
|
||||
"hy2xs-admin/model/constant"
|
||||
"hy2xs-admin/model/dto"
|
||||
"hy2xs-admin/model/entity"
|
||||
"hy2xs-admin/model/vo"
|
||||
"hy2xs-admin/service"
|
||||
"hy2xs-admin/util"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
needResetPortHopping := false
|
||||
needRestart := false
|
||||
|
||||
for _, item := range configsUpdateDto.ConfigUpdateDtos {
|
||||
key := *item.Key
|
||||
value := *item.Value
|
||||
|
||||
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.Hysteria2ConfigPortHopping {
|
||||
re := regexp.MustCompile(`^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$`)
|
||||
if value != "" && !re.MatchString(value) {
|
||||
vo.Fail(fmt.Sprintf("port hopping: %s is invalid", value), c)
|
||||
return
|
||||
}
|
||||
hysteria2ConfigPortHopping, err := service.GetConfig(constant.Hysteria2ConfigPortHopping)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if *hysteria2ConfigPortHopping.Value != value {
|
||||
needResetPortHopping = 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 needResetPortHopping {
|
||||
if err := service.InitPortHopping(); 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.Value == "1") != running {
|
||||
enable := "0"
|
||||
if running {
|
||||
enable = "1"
|
||||
}
|
||||
if err := service.UpdateConfig(constant.Hysteria2Enable, enable); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
configVo.Value = enable
|
||||
}
|
||||
|
||||
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) {
|
||||
hysteria2ServerConfig, err := validateField(c, bo.Hysteria2ServerConfig{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hysteria2Config, err := service.GetHysteria2Config()
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
needResetPortHopping := false
|
||||
if hysteria2Config.Listen != nil &&
|
||||
*hysteria2Config.Listen != "" &&
|
||||
hysteria2ServerConfig.Listen != nil &&
|
||||
*hysteria2ServerConfig.Listen != "" &&
|
||||
*hysteria2ServerConfig.Listen != *hysteria2Config.Listen {
|
||||
needResetPortHopping = true
|
||||
}
|
||||
|
||||
if err = service.UpdateHysteria2Config(hysteria2ServerConfig); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if needResetPortHopping {
|
||||
if err := service.InitPortHopping(); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
running := service.Hysteria2IsRunning()
|
||||
if running {
|
||||
if err = service.RestartHysteria2(); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
}
|
||||
vo.Success(nil, c)
|
||||
}
|
||||
|
||||
func ExportHysteria2Config(c *gin.Context) {
|
||||
hysteria2ServerConfig, err := service.GetHysteria2Config()
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 默认值
|
||||
config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.JwtSecret})
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
var hUIWebPort string
|
||||
var jwtSecret string
|
||||
for _, item := range config {
|
||||
if *item.Key == constant.HUIWebPort {
|
||||
hUIWebPort = *item.Value
|
||||
} else if *item.Key == constant.JwtSecret {
|
||||
jwtSecret = *item.Value
|
||||
}
|
||||
}
|
||||
|
||||
if hUIWebPort == "" || jwtSecret == "" {
|
||||
logrus.Errorf("hUIWebPort or jwtSecret is nil")
|
||||
vo.Fail(constant.SysError, c)
|
||||
return
|
||||
}
|
||||
|
||||
authHttpUrl, err := service.GetAuthHttpUrl()
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
authType := "http"
|
||||
authHttpInsecure := true
|
||||
var auth bo.ServerConfigAuth
|
||||
auth.Type = &authType
|
||||
var http bo.ServerConfigAuthHTTP
|
||||
http.URL = &authHttpUrl
|
||||
http.Insecure = &authHttpInsecure
|
||||
auth.HTTP = &http
|
||||
hysteria2ServerConfig.Auth = &auth
|
||||
hysteria2ServerConfig.TrafficStats.Secret = &jwtSecret
|
||||
|
||||
fileName := fmt.Sprintf("Hysteria2Config-%s.yaml", time.Now().Format("20060102150405"))
|
||||
filePath := constant.ExportPathDir + fileName
|
||||
|
||||
if err = util.ExportFile(filePath, hysteria2ServerConfig, 1); 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 ImportHysteria2Config(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, ".yaml") {
|
||||
vo.Fail("file format not supported", c)
|
||||
return
|
||||
}
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
vo.Fail("yaml file read err", c)
|
||||
return
|
||||
}
|
||||
var hysteria2ServerConfig bo.Hysteria2ServerConfig
|
||||
if err = yaml.Unmarshal(content, &hysteria2ServerConfig); err != nil {
|
||||
vo.Fail("content Unmarshal err", c)
|
||||
return
|
||||
}
|
||||
|
||||
// 默认值
|
||||
config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.JwtSecret})
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
var hUIWebPort string
|
||||
var jwtSecret string
|
||||
for _, item := range config {
|
||||
if *item.Key == constant.HUIWebPort {
|
||||
hUIWebPort = *item.Value
|
||||
} else if *item.Key == constant.JwtSecret {
|
||||
jwtSecret = *item.Value
|
||||
}
|
||||
}
|
||||
|
||||
if hUIWebPort == "" || jwtSecret == "" {
|
||||
logrus.Errorf("hUIWebPort or jwtSecret is nil")
|
||||
vo.Fail(constant.SysError, c)
|
||||
return
|
||||
}
|
||||
|
||||
authHttpUrl, err := service.GetAuthHttpUrl()
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
authType := "http"
|
||||
authHttpInsecure := true
|
||||
var auth bo.ServerConfigAuth
|
||||
auth.Type = &authType
|
||||
var http bo.ServerConfigAuthHTTP
|
||||
http.URL = &authHttpUrl
|
||||
http.Insecure = &authHttpInsecure
|
||||
auth.HTTP = &http
|
||||
hysteria2ServerConfig.Auth = &auth
|
||||
hysteria2ServerConfig.TrafficStats.Secret = &jwtSecret
|
||||
|
||||
if err = service.SetHysteria2Config(hysteria2ServerConfig); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
running := service.Hysteria2IsRunning()
|
||||
if running {
|
||||
if err = service.RestartHysteria2(); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
vo.Success(nil, 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 := 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("file format not supported", 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
|
||||
}
|
||||
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) {
|
||||
go func() {
|
||||
_ = service.StopServer()
|
||||
}()
|
||||
vo.Success(nil, c)
|
||||
}
|
||||
|
||||
func UploadCertFile(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
vo.Fail(constant.SysError, c)
|
||||
return
|
||||
}
|
||||
ext := filepath.Ext(file.Filename)
|
||||
if ext != ".crt" && ext != ".key" {
|
||||
vo.Fail("file format not supported", c)
|
||||
return
|
||||
}
|
||||
if file.Size > 1024*1024 {
|
||||
vo.Fail("the file is too big", c)
|
||||
return
|
||||
}
|
||||
err = filepath.WalkDir(constant.BinDir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileExt := filepath.Ext(path)
|
||||
if !d.IsDir() && fileExt == ext {
|
||||
if err := os.Remove(path); err != nil {
|
||||
return fmt.Errorf("failed to delete file: %s, error: %v", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Errorf("error during file deletion: %v", err)
|
||||
vo.Fail("delete file failed", c)
|
||||
return
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
vo.Fail(constant.SysError, c)
|
||||
return
|
||||
}
|
||||
safeFilename := filepath.Base(file.Filename)
|
||||
certPath := filepath.Join(wd, constant.BinDir, safeFilename)
|
||||
|
||||
if err := c.SaveUploadedFile(file, certPath); err != nil {
|
||||
vo.Fail("file upload failed", c)
|
||||
return
|
||||
}
|
||||
vo.Success(certPath, c)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"hy2xs-admin/model/constant"
|
||||
"hy2xs-admin/model/dto"
|
||||
"hy2xs-admin/model/entity"
|
||||
"hy2xs-admin/model/vo"
|
||||
"hy2xs-admin/service"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Hysteria2Auth(c *gin.Context) {
|
||||
hysteria2AuthDto, err := validateField(c, dto.Hysteria2AuthDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id, username, err := service.Hysteria2Auth(*hysteria2AuthDto.Auth)
|
||||
if err != nil || username == "" {
|
||||
vo.Hysteria2AuthFail("", c)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新最近连接时间
|
||||
now := time.Now().UnixMilli()
|
||||
if err = service.UpdateAccount(entity.Account{
|
||||
BaseEntity: entity.BaseEntity{Id: &id},
|
||||
ConAt: &now,
|
||||
}); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
vo.Hysteria2AuthSuccess(username, c)
|
||||
}
|
||||
|
||||
func Hysteria2Kick(c *gin.Context) {
|
||||
hysteria2KickDto, err := validateField(c, dto.Hysteria2KickDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = service.Hysteria2Kick(hysteria2KickDto.Ids, *hysteria2KickDto.KickUtilTime)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
vo.Success(nil, c)
|
||||
}
|
||||
|
||||
func Hysteria2ChangeVersion(c *gin.Context) {
|
||||
vo.Fail("Смена версии Hysteria2 отключена: runtime управляется install-оркестратором HY2XS", c)
|
||||
}
|
||||
|
||||
func ListRelease(c *gin.Context) {
|
||||
vo.Success([]string{}, c)
|
||||
}
|
||||
|
||||
func Hysteria2Url(c *gin.Context) {
|
||||
hysteria2UrlDto, err := validateField(c, dto.Hysteria2UrlDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
url, err := service.Hysteria2Url(*hysteria2UrlDto.AccountId, *hysteria2UrlDto.Hostname)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
// 生成二维码
|
||||
qrCode, err := qrcode.Encode(url, qrcode.Medium, 300)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
hysteria2UrlVo := vo.Hysteria2UrlVo{
|
||||
Url: url,
|
||||
QrCode: qrCode,
|
||||
}
|
||||
vo.Success(hysteria2UrlVo, c)
|
||||
}
|
||||
|
||||
func Hysteria2SubscribeUrl(c *gin.Context) {
|
||||
hysteria2SubscribeUrlDto, err := validateField(c, dto.Hysteria2SubscribeUrlDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
subscribeUrl, err := service.Hysteria2SubscribeUrl(*hysteria2SubscribeUrlDto.AccountId,
|
||||
*hysteria2SubscribeUrlDto.Protocol,
|
||||
*hysteria2SubscribeUrlDto.Host)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
qrCode, err := qrcode.Encode(subscribeUrl, qrcode.Medium, 300)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
subscribeVo := vo.Hysteria2SubscribeVo{
|
||||
Url: subscribeUrl,
|
||||
QrCode: qrCode,
|
||||
}
|
||||
vo.Success(subscribeVo, c)
|
||||
}
|
||||
|
||||
func Hysteria2Subscribe(c *gin.Context) {
|
||||
conPass := c.Param("conPass")
|
||||
conPass, err := url.QueryUnescape(conPass)
|
||||
if err != nil {
|
||||
vo.Fail("url decode err", c)
|
||||
return
|
||||
}
|
||||
userAgent := strings.ToLower(c.Request.Header.Get("User-Agent"))
|
||||
host := c.Request.Host
|
||||
|
||||
if host == "" {
|
||||
vo.Fail("Host is empty", c)
|
||||
return
|
||||
}
|
||||
|
||||
var clientType string
|
||||
if strings.Contains(userAgent, constant.Shadowrocket) {
|
||||
clientType = constant.Shadowrocket
|
||||
} else if strings.Contains(userAgent, constant.Clash) {
|
||||
clientType = constant.Clash
|
||||
} else if strings.Contains(userAgent, constant.V2rayN) {
|
||||
clientType = constant.V2rayN
|
||||
} else if strings.Contains(userAgent, constant.NekoBox) {
|
||||
clientType = constant.NekoBox
|
||||
} else {
|
||||
clientType = constant.Clash
|
||||
}
|
||||
|
||||
userInfo, configStr, err := service.Hysteria2Subscribe(conPass, clientType, host)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if clientType == constant.Shadowrocket || clientType == constant.Clash {
|
||||
c.Header("content-disposition", "attachment; filename=hui.yaml")
|
||||
c.Header("profile-update-interval", "12")
|
||||
c.Header("subscription-userinfo", userInfo)
|
||||
} else if clientType == constant.V2rayN {
|
||||
configStr = base64.StdEncoding.EncodeToString([]byte(configStr))
|
||||
}
|
||||
|
||||
c.String(200, configStr)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"hy2xs-admin/model/constant"
|
||||
"hy2xs-admin/model/dto"
|
||||
"hy2xs-admin/model/vo"
|
||||
"hy2xs-admin/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
func LogSystem(c *gin.Context) {
|
||||
logSystemDto, err := validateField(c, dto.LogDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
exists := util.Exists(constant.SystemLogPath)
|
||||
logSystemVos := make([]vo.LogSystemVo, 0)
|
||||
if !exists {
|
||||
vo.Success(logSystemVos, c)
|
||||
return
|
||||
}
|
||||
numLine := 0
|
||||
if logSystemDto.NumLine != nil || *logSystemDto.NumLine > 0 {
|
||||
numLine = *logSystemDto.NumLine
|
||||
}
|
||||
logLines, total, err := util.ReadLinesFromBottom(constant.SystemLogPath, numLine)
|
||||
if err != nil {
|
||||
vo.Fail("Unable to read log file", c)
|
||||
return
|
||||
}
|
||||
|
||||
for _, line := range logLines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
logSystemVo := vo.LogSystemVo{}
|
||||
err := json.Unmarshal([]byte(line), &logSystemVo)
|
||||
if err != nil {
|
||||
vo.Fail("Unable to unmarshal log data", c)
|
||||
continue
|
||||
}
|
||||
logSystemVos = append(logSystemVos, logSystemVo)
|
||||
}
|
||||
vo.Success(vo.LogSystemPage[vo.LogSystemVo]{
|
||||
LogSystemVos: logSystemVos,
|
||||
Total: int64(total),
|
||||
}, c)
|
||||
}
|
||||
|
||||
func LogHysteria2(c *gin.Context) {
|
||||
logSystemDto, err := validateField(c, dto.LogDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
exists := util.Exists(constant.Hysteria2LogPath)
|
||||
logHysteria2Vos := make([]vo.LogHysteria2Vo, 0)
|
||||
if !exists {
|
||||
vo.Success(logHysteria2Vos, c)
|
||||
return
|
||||
}
|
||||
numLine := 0
|
||||
if logSystemDto.NumLine != nil || *logSystemDto.NumLine > 0 {
|
||||
numLine = *logSystemDto.NumLine
|
||||
}
|
||||
logLines, total, err := util.ReadLinesFromBottom(constant.Hysteria2LogPath, numLine)
|
||||
if err != nil {
|
||||
vo.Fail("Unable to read log file", c)
|
||||
return
|
||||
}
|
||||
|
||||
for _, line := range logLines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
logHysteria2Vo := vo.LogHysteria2Vo{}
|
||||
err := json.Unmarshal([]byte(line), &logHysteria2Vo)
|
||||
if err != nil {
|
||||
vo.Fail("Unable to unmarshal log data", c)
|
||||
continue
|
||||
}
|
||||
logHysteria2Vos = append(logHysteria2Vos, logHysteria2Vo)
|
||||
}
|
||||
vo.Success(vo.LogSystemPage[vo.LogHysteria2Vo]{
|
||||
LogSystemVos: logHysteria2Vos,
|
||||
Total: int64(total),
|
||||
}, c)
|
||||
}
|
||||
|
||||
func ExportLog(c *gin.Context) {
|
||||
logExportDto, err := validateField(c, dto.LogExportDto{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var fileName string
|
||||
var filePath string
|
||||
if *logExportDto.Option == 0 {
|
||||
fileName = fmt.Sprintf("hy2xs-admin-%s.log", time.Now().Format("20060102150405"))
|
||||
filePath = constant.SystemLogPath
|
||||
} else if *logExportDto.Option == 1 {
|
||||
fileName = fmt.Sprintf("hysteria2-%s.log", time.Now().Format("20060102150405"))
|
||||
filePath = constant.Hysteria2LogPath
|
||||
}
|
||||
|
||||
if !util.Exists(filePath) {
|
||||
vo.Fail("log 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)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"hy2xs-admin/model/vo"
|
||||
"hy2xs-admin/service"
|
||||
)
|
||||
|
||||
func MonitorSystem(c *gin.Context) {
|
||||
systemMonitorVo, err := service.MonitorSystem()
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
vo.Success(systemMonitorVo, c)
|
||||
}
|
||||
|
||||
func MonitorHysteria2(c *gin.Context) {
|
||||
hysteria2MonitorVo, err := service.MonitorHysteria2()
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
vo.Success(hysteria2MonitorVo, c)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"hy2xs-admin/model/constant"
|
||||
"hy2xs-admin/model/vo"
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var validate *validator.Validate
|
||||
|
||||
func init() {
|
||||
validate = validator.New()
|
||||
_ = validate.RegisterValidation("validateStr", validateStr)
|
||||
}
|
||||
|
||||
func validateStr(f validator.FieldLevel) bool {
|
||||
field := f.Field().String()
|
||||
// 字符串必须6-32位是字母或者数字或部分特殊字符的组合
|
||||
reg := "^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$"
|
||||
compile := regexp.MustCompile(reg)
|
||||
return field == "" || compile.MatchString(field)
|
||||
}
|
||||
|
||||
func validateField[T interface{}](c *gin.Context, field T) (T, error) {
|
||||
if c.Request.Method == http.MethodGet {
|
||||
_ = c.ShouldBindQuery(&field)
|
||||
} else if c.Request.Method == http.MethodPost ||
|
||||
c.Request.Method == http.MethodPut ||
|
||||
c.Request.Method == http.MethodDelete {
|
||||
_ = c.ShouldBindJSON(&field)
|
||||
}
|
||||
if err := validate.Struct(&field); err != nil {
|
||||
vo.Fail(constant.InvalidError, c)
|
||||
return field, fmt.Errorf(constant.InvalidError)
|
||||
}
|
||||
return field, nil
|
||||
}
|
||||
Reference in New Issue
Block a user