Подготовить HY2XS к production-сборке

This commit is contained in:
2026-04-25 23:13:12 +05:00
commit 84a4e94567
277 changed files with 26513 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
package service
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"hy2xs-admin/dao"
"hy2xs-admin/model/bo"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/dto"
"hy2xs-admin/model/entity"
"hy2xs-admin/model/vo"
)
func Login(username string, pass string) (string, error) {
account, err := dao.GetAccount("username = ? and pass = ? and role = 'admin' and deleted = 0", username, pass)
if err != nil {
return "", err
}
accountBo := bo.AccountBo{
Id: *account.Id,
Username: *account.Username,
Roles: []string{*account.Role},
Deleted: *account.Deleted,
}
return GenToken(accountBo)
}
func PageAccount(accountPageDto dto.AccountPageDto) ([]entity.Account, int64, error) {
return dao.PageAccount(accountPageDto)
}
func SaveAccount(account entity.Account) error {
_, err := dao.SaveAccount(account)
return err
}
func DeleteAccount(ids []int64) error {
return dao.DeleteAccount(ids)
}
func UpdateAccount(account entity.Account) error {
updates := map[string]interface{}{}
if account.Username != nil && *account.Username != "" {
updates["username"] = *account.Username
}
if account.Pass != nil && *account.Pass != "" {
updates["pass"] = *account.Pass
}
if account.ConPass != nil && *account.ConPass != "" {
updates["con_pass"] = fmt.Sprintf("%s.%s", *account.Username, *account.ConPass)
}
if account.Quota != nil {
updates["quota"] = *account.Quota
}
if account.ExpireTime != nil {
updates["expire_time"] = *account.ExpireTime
}
if account.Download != nil {
updates["download"] = *account.Download
}
if account.Upload != nil {
updates["upload"] = *account.Upload
}
if account.DeviceNo != nil {
updates["device_no"] = *account.DeviceNo
}
if account.Deleted != nil {
updates["deleted"] = *account.Deleted
}
if account.LoginAt != nil && *account.LoginAt > 0 {
updates["login_at"] = *account.LoginAt
}
if account.ConAt != nil && *account.ConAt > 0 {
updates["con_at"] = *account.ConAt
}
if account.Remark != nil {
updates["remark"] = *account.Remark
}
return dao.UpdateAccount([]int64{*account.Id}, updates)
}
func ResetTraffic(id int64) error {
return dao.UpdateAccount([]int64{id}, map[string]interface{}{"download": 0, "upload": 0})
}
func ExistAccountUsername(username string, id int64) bool {
var err error
if id != 0 {
_, err = dao.GetAccount("username = ? and id != ?", username, id)
} else {
_, err = dao.GetAccount("username = ?", username)
}
if err != nil {
if err.Error() == constant.WrongPassword {
return false
}
}
return true
}
func GetAccount(id int64) (entity.Account, error) {
return dao.GetAccount("id = ?", id)
}
func ListExportAccount() ([]bo.AccountExport, error) {
accounts, err := dao.ListAccount(nil, nil)
if err != nil {
return nil, errors.New(constant.SysError)
}
var accountExports []bo.AccountExport
for _, item := range accounts {
accountExport := bo.AccountExport{
Id: *item.Id,
Username: *item.Username,
Pass: *item.Pass,
ConPass: *item.ConPass,
Quota: *item.Quota,
Download: *item.Download,
Upload: *item.Upload,
ExpireTime: *item.ExpireTime,
DeviceNo: *item.DeviceNo,
KickUtilTime: *item.KickUtilTime,
Role: *item.Role,
Deleted: *item.Deleted,
CreateTime: *item.CreateTime,
UpdateTime: *item.UpdateTime,
LoginAt: *item.LoginAt,
ConAt: *item.ConAt,
Remark: *item.Remark,
}
accountExports = append(accountExports, accountExport)
}
return accountExports, nil
}
func ReleaseKickAccount(id int64) error {
return dao.UpdateAccount([]int64{id}, map[string]interface{}{"kick_util_time": 0})
}
func UpsertAccount(accounts []entity.Account) error {
return dao.UpsertAccount(accounts)
}
func GetAccountInfo(c *gin.Context) (vo.AccountInfoVo, error) {
myClaims, err := ParseToken(GetToken(c))
if err != nil {
return vo.AccountInfoVo{}, err
}
if myClaims.AccountBo.Deleted != 0 {
return vo.AccountInfoVo{}, errors.New("this account has been disabled")
}
return vo.AccountInfoVo{
Id: myClaims.AccountBo.Id,
Username: myClaims.AccountBo.Username,
Roles: myClaims.AccountBo.Roles,
}, nil
}
+197
View File
@@ -0,0 +1,197 @@
package service
import (
"errors"
"fmt"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"hy2xs-admin/dao"
"hy2xs-admin/model/bo"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/entity"
"os"
"strconv"
"strings"
)
func UpdateConfig(key string, value string) error {
if key == constant.Hysteria2Enable {
if value == "1" {
hysteria2Config, err := GetHysteria2Config()
if err != nil {
return err
}
if hysteria2Config.Listen == nil || *hysteria2Config.Listen == "" {
logrus.Errorf("hysteria2 config is empty")
return errors.New("hysteria2 config is empty")
}
// 启动Hysteria2
if err = StartHysteria2(); err != nil {
return err
}
} else {
if err := StopHysteria2(); err != nil {
return err
}
}
}
return dao.UpdateConfig([]string{key}, map[string]interface{}{"value": value})
}
func GetConfig(key string) (entity.Config, error) {
return dao.GetConfig("key = ?", key)
}
func ListConfig(keys []string) ([]entity.Config, error) {
return dao.ListConfig("key in ?", keys)
}
func ListConfigNotIn(keys []string) ([]entity.Config, error) {
return dao.ListConfig("key not in ?", keys)
}
func GetHysteria2Config() (bo.Hysteria2ServerConfig, error) {
var serverConfig bo.Hysteria2ServerConfig
config, err := dao.GetConfig("key = ?", constant.Hysteria2Config)
if err != nil {
return serverConfig, err
}
if config.Value == nil || strings.TrimSpace(*config.Value) == "" {
content, readErr := os.ReadFile(constant.Hysteria2ConfigPath)
if readErr != nil {
return serverConfig, readErr
}
if err = yaml.Unmarshal(content, &serverConfig); err != nil {
return serverConfig, err
}
return serverConfig, nil
}
if err = yaml.Unmarshal([]byte(*config.Value), &serverConfig); err != nil {
return serverConfig, err
}
return serverConfig, nil
}
func UpdateHysteria2Config(hysteria2ServerConfig bo.Hysteria2ServerConfig) error {
// 默认值
config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.JwtSecret})
if err != nil {
return err
}
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")
return errors.New(constant.SysError)
}
authHttpUrl, err := GetAuthHttpUrl()
if err != nil {
return err
}
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
yamlConfig, err := yaml.Marshal(&hysteria2ServerConfig)
if err != nil {
return err
}
return dao.UpdateConfig([]string{constant.Hysteria2Config}, map[string]interface{}{"value": string(yamlConfig)})
}
func SetHysteria2Config(hysteria2ServerConfig bo.Hysteria2ServerConfig) error {
config, err := yaml.Marshal(&hysteria2ServerConfig)
if err != nil {
return err
}
return dao.UpdateConfig([]string{constant.Hysteria2Config}, map[string]interface{}{"value": string(config)})
}
func UpsertConfig(configs []entity.Config) error {
return dao.UpsertConfig(configs)
}
func GetHysteria2ApiPort() (int64, error) {
hysteria2Config, err := GetHysteria2Config()
if err != nil {
return 0, err
}
if hysteria2Config.TrafficStats == nil || hysteria2Config.TrafficStats.Listen == nil {
errMsg := "hysteria2 Traffic Stats API (HTTP) Listen is nil"
logrus.Errorf(errMsg)
return 0, errors.New(errMsg)
}
apiPort, err := strconv.ParseInt(strings.Split(*hysteria2Config.TrafficStats.Listen, ":")[1], 10, 64)
if err != nil {
errMsg := fmt.Sprintf("apiPort: %s is invalid", *hysteria2Config.TrafficStats.Listen)
logrus.Errorf(errMsg)
return 0, errors.New(errMsg)
}
return apiPort, nil
}
func GetPortAndCert() (int64, string, string, error) {
configs, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.HUICrtPath, constant.HUIKeyPath})
if err != nil {
return 0, "", "", err
}
port := ""
crtPath := ""
keyPath := ""
for _, config := range configs {
value := *config.Value
if *config.Key == constant.HUIWebPort {
port = value
} else if *config.Key == constant.HUICrtPath {
crtPath = value
} else if *config.Key == constant.HUIKeyPath {
keyPath = value
}
}
portInt, err := strconv.ParseInt(port, 10, 64)
if err != nil {
logrus.Errorf("port: %s is invalid", port)
return 0, "", "", errors.New(fmt.Sprintf("port: %s is invalid", port))
}
return portInt, crtPath, keyPath, nil
}
func GetAuthHttpUrl() (string, error) {
port, crtPath, keyPath, err := GetPortAndCert()
if err != nil {
return "", err
}
protocol := "http"
if crtPath != "" && keyPath != "" {
protocol = "https"
}
config, err := dao.GetConfig("key = ?", constant.HUIWebContext)
if err != nil {
return "", err
}
webContext := ""
if config.Value != nil && *config.Value != "/" && strings.HasPrefix(*config.Value, "/") {
webContext = *config.Value
}
return fmt.Sprintf("%s://127.0.0.1:%d%s/hui/hysteria2/auth", protocol, port, webContext), nil
}
+140
View File
@@ -0,0 +1,140 @@
package service
import (
"github.com/sirupsen/logrus"
"hy2xs-admin/dao"
"hy2xs-admin/model/bo"
"hy2xs-admin/model/constant"
"hy2xs-admin/proxy"
"hy2xs-admin/util"
"strconv"
"sync"
"time"
)
var trafficMutex sync.Mutex
var kickMutex sync.Mutex
func CronHandleAccount() {
go func() {
hysteriaEnable, err := dao.GetConfig("key = ?", constant.Hysteria2Enable)
if err != nil {
return
}
if hysteriaEnable.Value != nil && *hysteriaEnable.Value == "1" {
apiPort, err := GetHysteria2ApiPort()
if err != nil {
return
}
jwtSecretConfig, err := dao.GetConfig("key = ?", constant.JwtSecret)
if err != nil {
return
}
// 保存流量数据
go saveAccountTraffic(apiPort, *jwtSecretConfig.Value)
// 踢下线
go kickAccount(apiPort, *jwtSecretConfig.Value)
}
}()
}
func CronResetTraffic() {
accounts, err := dao.ListAccount(nil, nil)
if err != nil {
return
}
var ids []int64
for _, item := range accounts {
ids = append(ids, *item.Id)
}
idsList := util.SplitArr(ids, 100)
for _, item := range idsList {
if err := dao.UpdateAccount(item, map[string]interface{}{"download": 0, "upload": 0}); err != nil {
continue
}
}
}
func saveAccountTraffic(apiPort int64, jwtSecret string) {
if !trafficMutex.TryLock() {
return
}
defer trafficMutex.Unlock()
hysteria2TrafficTime, err := dao.GetConfig("key = ?", constant.Hysteria2TrafficTime)
if err != nil {
return
}
hysteria2TrafficTimeFloat, err := strconv.ParseFloat(*hysteria2TrafficTime.Value, 64)
if err != nil {
logrus.Errorf("hysteria2TrafficTime string conv int64 err: %v", err)
return
}
users, err := proxy.NewHysteria2Api(apiPort).ListUsers(true, jwtSecret)
if err != nil {
return
}
if len(users) > 0 {
userLists := util.SplitMap(users, 10)
var wg sync.WaitGroup
for _, userList := range userLists {
wg.Add(1)
go func(userList map[string]bo.Hysteria2UserTraffic) {
defer wg.Done()
for username, traffic := range userList {
if err = dao.UpdateAccountTraffic(username, int64(float64(traffic.Rx)*hysteria2TrafficTimeFloat), int64(float64(traffic.Tx)*hysteria2TrafficTimeFloat)); err != nil {
continue
}
}
}(userList)
}
wg.Wait()
}
}
func kickAccount(apiPort int64, jwtSecret string) {
if !kickMutex.TryLock() {
return
}
defer kickMutex.Unlock()
users, err := proxy.NewHysteria2Api(apiPort).OnlineUsers(jwtSecret)
if err != nil {
return
}
if len(users) > 0 {
i := 0
usernames := make([]string, len(users))
for k := range users {
usernames[i] = k
i++
}
usernameLists := util.SplitArr(usernames, 10)
var wg sync.WaitGroup
for _, usernameList := range usernameLists {
wg.Add(1)
go func(usernameList []string) {
defer wg.Done()
now := time.Now().UnixMilli()
accounts, err := dao.ListAccount("username in ? and (deleted = 1 or (quota > 0 and quota < download + upload)) or ? > expire_time or ? < kick_util_time", usernameList, now, now)
if err != nil {
return
}
kickUsernames := make([]string, len(accounts))
j := 0
for _, item := range accounts {
kickUsernames[j] = *item.Username
j++
}
if err = proxy.NewHysteria2Api(apiPort).KickUsers(kickUsernames, jwtSecret); err != nil {
return
}
}(usernameList)
}
wg.Wait()
}
}
+234
View File
@@ -0,0 +1,234 @@
package service
import (
"errors"
"fmt"
"hy2xs-admin/dao"
"hy2xs-admin/model/constant"
"hy2xs-admin/util"
"strings"
)
var (
netManager string
ingressInterface string
Add = "add"
Delete = "delete"
Table = "hui_porthopping"
Comment = "hui_hysteria_porthopping"
)
func InitForward() {
if nft, err := util.Exec("command -v nft"); err == nil && strings.TrimSpace(nft) != "" {
netManager = "nft"
} else if iptables, err := util.Exec("command -v iptables"); err == nil && strings.TrimSpace(iptables) != "" {
netManager = "iptables"
}
if ii, err := util.Exec("ls /sys/class/net | grep -E '^en|^eth'"); err == nil && strings.TrimSpace(ii) != "" {
iiList := strings.Split(ii, "\n")
ingressInterface = strings.TrimSpace(iiList[0])
}
}
func InitTableAndChain() error {
if netManager == "nft" {
_, err := util.Exec(fmt.Sprintf("nft add table inet %s", Table))
if err != nil {
return err
}
_, err = util.Exec(fmt.Sprintf("nft add chain inet %s prerouting { type nat hook prerouting priority dstnat\\; policy accept\\; }", Table))
if err != nil {
return err
}
}
return nil
}
func InitPortHopping() error {
if err := RemoveByComment(); err != nil {
return err
}
hysteria2Config, err := GetHysteria2Config()
if err != nil {
return err
}
// set port forward
hysteria2ConfigPortHopping, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigPortHopping)
if err != nil {
return err
}
if *hysteria2ConfigPortHopping.Value != "" {
listen := strings.Split(*hysteria2Config.Listen, ":")
if len(listen) == 2 {
portHoppings := strings.Split(*hysteria2ConfigPortHopping.Value, ",")
for _, item := range portHoppings {
if err := portForward(item, listen[1], Add); err != nil {
return err
}
}
}
}
return nil
}
func portForward(rules string, target string, option string) error {
switch netManager {
case "nft":
switch option {
case Add, Delete:
return nftForward(rules, target, option)
default:
return errors.New("unsupported command option")
}
case "iptables":
switch option {
case Add:
return iptablesForward(rules, target, "-A")
case Delete:
return iptablesForward(rules, target, "-D")
default:
return errors.New("unsupported command option")
}
default:
return errors.New("port hopping not supported on this system")
}
}
func RemoveByComment() error {
switch netManager {
case "nft":
return ntfRemoveByComment(Comment)
case "iptables":
return iptablesRemoveByComment(Comment)
default:
return errors.New("port hopping not supported on this system")
}
}
func nftForward(rules string, target string, option string) error {
if ingressInterface == "" {
return fmt.Errorf("no network interface detected")
}
// nft list ruleset
// 创建表:nft add table inet hui_hysteria_porthopping
// 创建链:nft add chain inet hui_hysteria_porthopping prerouting { type nat hook prerouting priority dstnat\; policy accept\; }
// 添加规则:nft add rule inet hui_hysteria_porthopping prerouting iifname enp1s0 udp dport {30000-40000} counter redirect to :444 comment hui_hysteria_porthopping
_, err := util.Exec(fmt.Sprintf("nft %s rule inet %s prerouting iifname %s udp dport {%s} counter redirect to :%s comment %s", option, Table, ingressInterface, rules, target, Comment))
if err != nil {
return err
}
return nil
}
func ntfRemoveByComment(comment string) error {
rules, err := nftRules()
if err != nil {
return err
}
for _, rule := range rules {
if strings.Contains(rule, comment) {
parts := strings.Fields(rule)
handle := parts[len(parts)-1]
_, err := util.Exec(fmt.Sprintf("nft delete rule inet %s prerouting handle %s", Table, strings.TrimSpace(handle)))
if err != nil {
return err
}
}
}
return nil
}
func nftRules() ([]string, error) {
listOutput, err := util.Exec(fmt.Sprintf("nft list ruleset | grep -q %s && echo 'found' || echo 'not found'", Comment))
if err != nil {
return nil, err
}
if strings.TrimSpace(listOutput) == "not found" {
return []string{}, nil
}
output, err := util.Exec(fmt.Sprintf("nft --handle list chain inet %s prerouting", Table))
if err != nil {
return nil, err
}
rules := strings.Split(output, "\n")
return rules, nil
}
func iptablesForward(rules string, target string, option string) error {
if ingressInterface == "" {
return fmt.Errorf("no network interface detected")
}
rulePairs := strings.Split(rules, ",")
for _, pair := range rulePairs {
ports := ""
portRange := strings.Split(pair, "-")
if len(portRange) == 1 {
ports = strings.TrimSpace(portRange[0])
} else if len(portRange) == 2 {
startPort := strings.TrimSpace(portRange[0])
endPort := strings.TrimSpace(portRange[1])
ports = startPort + ":" + endPort
} else {
return fmt.Errorf("invalid port range format: %s", pair)
}
if len(ports) != 0 {
if err := iptablesAddRule(option, ports, target); err != nil {
return err
}
}
}
return nil
}
func iptablesAddRule(option, ports, target string) error {
protocols := [2]string{"iptables", "ip6tables"}
for _, protocol := range protocols {
// iptables -t nat -A PREROUTING -i enp1s0 -p udp --dport 30000:40000 -j REDIRECT --to-port 444 -m comment --comment hui_hysteria_porthopping
_, err := util.Exec(fmt.Sprintf("%s -t nat %s PREROUTING -i %s -p udp --dport %s -j REDIRECT --to-port %s -m comment --comment %s", protocol, option, ingressInterface, ports, target, Comment))
if err != nil {
return err
}
}
return nil
}
func iptablesRemoveByComment(comment string) error {
protocols := [2]string{"iptables", "ip6tables"}
for _, protocol := range protocols {
rules, err := iptablesRules(protocol)
if err != nil {
return err
}
for _, rule := range rules {
if strings.Contains(rule, comment) {
parts := strings.Fields(rule)
handle := parts[0]
_, err := util.Exec(fmt.Sprintf("%s -t nat -D PREROUTING %s", protocol, strings.TrimSpace(handle)))
if err != nil {
return err
}
}
}
}
return nil
}
func iptablesRules(protocol string) ([]string, error) {
// iptables -t nat -L PREROUTING -v --line-numbers
output, err := util.Exec(fmt.Sprintf("%s -t nat -L PREROUTING -v --line-numbers", protocol))
if err != nil {
return nil, err
}
rules := strings.Split(output, "\n")
return rules, nil
}
+149
View File
@@ -0,0 +1,149 @@
package service
import (
"errors"
"fmt"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"hy2xs-admin/dao"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/vo"
"hy2xs-admin/util"
"os"
)
func InitHysteria2() error {
if !util.Exists(util.GetHysteria2BinPath()) {
return errors.New("systemd-managed hysteria binary not found")
}
config, err := dao.GetConfig("key = ?", constant.Hysteria2Enable)
if err != nil {
return err
}
if *config.Value == "1" {
logrus.Infof("hysteria2 lifecycle is managed by systemd in HY2XS production package")
}
return nil
}
func setHysteria2ConfigYAML() error {
serverConfig, err := GetHysteria2Config()
if err != nil {
return err
}
if serverConfig.Listen == nil || *serverConfig.Listen == "" {
return errors.New("hysteria2 config is empty")
}
authHttpUrl, err := GetAuthHttpUrl()
if err != nil {
return err
}
if serverConfig.Auth == nil || serverConfig.Auth.HTTP == nil || serverConfig.Auth.HTTP.URL == nil {
if err := UpdateHysteria2Config(serverConfig); err != nil {
return err
}
serverConfig, err = GetHysteria2Config()
if err != nil {
return err
}
}
// update auth http url
if *serverConfig.Auth.HTTP.URL != authHttpUrl {
serverConfig.Auth.HTTP.URL = &authHttpUrl
if err := UpdateHysteria2Config(serverConfig); err != nil {
return err
}
}
hysteria2Config, err := yaml.Marshal(&serverConfig)
if err != nil {
logrus.Errorf("marshal hysteria2 config err: %v", err)
return errors.New("marshal hysteria2 config err")
}
file, err := os.OpenFile(constant.Hysteria2ConfigPath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
if err != nil {
logrus.Errorf("create hysteria2 server config file err: %v", err)
return errors.New("create hysteria2 server config file err")
}
_, err = file.WriteString(string(hysteria2Config))
if err != nil {
logrus.Errorf("write hysteria2 config.json file err: %v", err)
return errors.New("hysteria2 config.json file write err")
}
return nil
}
func Hysteria2IsRunning() bool {
_, err := util.Exec("systemctl is-active --quiet hysteria-server")
return err == nil
}
func StartHysteria2() error {
if err := setHysteria2ConfigYAML(); err != nil {
return err
}
return util.Systemctl("restart", "hysteria-server")
}
func StopHysteria2() error {
return util.Systemctl("stop", "hysteria-server")
}
func RestartHysteria2() error {
if err := StopHysteria2(); err != nil {
return err
}
if err := StartHysteria2(); err != nil {
return err
}
return nil
}
func ReleaseHysteria2() error {
return nil
}
func Hysteria2AcmePath() (vo.Hysteria2AcmePathVo, error) {
hysteria2AcmePathVo := vo.Hysteria2AcmePathVo{}
hysteria2Config, err := GetHysteria2Config()
if err != nil {
return hysteria2AcmePathVo, err
}
if hysteria2Config.TLS != nil &&
hysteria2Config.TLS.Cert != nil && *hysteria2Config.TLS.Cert != "" &&
hysteria2Config.TLS.Key != nil && *hysteria2Config.TLS.Key != "" {
if util.Exists(*hysteria2Config.TLS.Cert) && util.Exists(*hysteria2Config.TLS.Key) {
hysteria2AcmePathVo.CrtPath = *hysteria2Config.TLS.Cert
hysteria2AcmePathVo.KeyPath = *hysteria2Config.TLS.Key
return hysteria2AcmePathVo, nil
}
return hysteria2AcmePathVo, errors.New("cert not found")
} else if hysteria2Config.ACME != nil &&
hysteria2Config.ACME.Domains != nil &&
len(hysteria2Config.ACME.Domains) > 0 &&
hysteria2Config.ACME.CA != nil &&
*hysteria2Config.ACME.CA != "" &&
hysteria2Config.ACME.Dir != nil &&
*hysteria2Config.ACME.Dir != "" {
acmeDir := *hysteria2Config.ACME.Dir
for _, domain := range hysteria2Config.ACME.Domains {
crtPath, err := util.FindFile(acmeDir, fmt.Sprintf("%s.crt", domain))
if err != nil {
continue
}
keyPath, err := util.FindFile(acmeDir, fmt.Sprintf("%s.key", domain))
if err != nil {
continue
}
hysteria2AcmePathVo.CrtPath = crtPath
hysteria2AcmePathVo.KeyPath = keyPath
return hysteria2AcmePathVo, nil
}
}
return vo.Hysteria2AcmePathVo{}, errors.New("cert not found")
}
+282
View File
@@ -0,0 +1,282 @@
package service
import (
"errors"
"fmt"
"gopkg.in/yaml.v3"
"hy2xs-admin/dao"
"hy2xs-admin/model/bo"
"hy2xs-admin/model/constant"
"hy2xs-admin/proxy"
"net/url"
"strings"
"time"
)
func Hysteria2Auth(conPass string) (int64, string, error) {
if !Hysteria2IsRunning() {
return 0, "", errors.New("hysteria2 is not running")
}
now := time.Now().UnixMilli()
account, err := dao.GetAccount("con_pass = ? and deleted = 0 and (quota < 0 or quota > download + upload) and ? < expire_time and ? > kick_util_time", conPass, now, now)
if err != nil {
return 0, "", err
}
// 限制设备数
onlineUsers, err := Hysteria2Online()
if err != nil {
return 0, "", err
}
device, exist := onlineUsers[*account.Username]
if exist && *account.DeviceNo <= device {
return 0, "", errors.New("device limited")
}
return *account.Id, *account.Username, nil
}
func Hysteria2Online() (map[string]int64, error) {
if !Hysteria2IsRunning() {
return map[string]int64{}, nil
}
apiPort, err := GetHysteria2ApiPort()
if err != nil {
return nil, errors.New("get hysteria2 apiPort err")
}
jwtSecretConfig, err := dao.GetConfig("key = ?", constant.JwtSecret)
if err != nil {
return nil, err
}
onlineUsers, err := proxy.NewHysteria2Api(apiPort).OnlineUsers(*jwtSecretConfig.Value)
if err != nil {
return nil, err
}
return onlineUsers, nil
}
func Hysteria2Kick(ids []int64, kickUtilTime int64) error {
if !Hysteria2IsRunning() {
return errors.New("hysteria2 is not running")
}
if err := dao.UpdateAccount(ids, map[string]interface{}{"kick_util_time": kickUtilTime}); err != nil {
return err
}
accounts, err := dao.ListAccount("id in ?", ids)
if err != nil {
return err
}
var keys []string
for _, item := range accounts {
keys = append(keys, *item.Username)
}
apiPort, err := GetHysteria2ApiPort()
if err != nil {
return errors.New("get hysteria2 apiPort err")
}
jwtSecretConfig, err := dao.GetConfig("key = ?", constant.JwtSecret)
if err != nil {
return err
}
if err = proxy.NewHysteria2Api(apiPort).KickUsers(keys, *jwtSecretConfig.Value); err != nil {
return err
}
return nil
}
func Hysteria2SubscribeUrl(accountId int64, protocol string, host string) (string, error) {
account, err := dao.GetAccount("id = ?", accountId)
if err != nil {
return "", err
}
config, err := dao.GetConfig("key = ?", constant.HUIWebContext)
if err != nil {
return "", err
}
webContext := ""
if config.Value != nil && *config.Value != "/" && strings.HasPrefix(*config.Value, "/") {
webContext = *config.Value
}
return fmt.Sprintf("%s//%s%s/hui/%s", protocol, host, webContext, url.QueryEscape(*account.ConPass)), nil
}
func Hysteria2Subscribe(conPass string, clientType string, host string) (string, string, error) {
hysteria2Config, err := GetHysteria2Config()
if err != nil {
return "", "", err
}
if hysteria2Config.Listen == nil || *hysteria2Config.Listen == "" {
return "", "", errors.New("hysteria2 config is empty")
}
account, err := dao.GetAccount("con_pass = ?", conPass)
if err != nil {
return "", "", err
}
hysteria2Name := "hysteria2"
hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark)
if err != nil {
return "", "", err
}
if *hysteria2ConfigRemark.Value != "" {
hysteria2Name = *hysteria2ConfigRemark.Value
}
hysteria2ConfigPortHopping, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigPortHopping)
if err != nil {
return "", "", err
}
userInfo := ""
configStr := ""
if clientType == constant.Shadowrocket || clientType == constant.Clash {
userInfo = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d",
*account.Upload,
*account.Download,
*account.Quota,
*account.ExpireTime/1000)
hysteria2 := bo.Hysteria2{
Name: hysteria2Name,
Type: "hysteria2",
Server: strings.Split(host, ":")[0],
Port: strings.Split(*hysteria2Config.Listen, ":")[1],
Ports: *hysteria2ConfigPortHopping.Value,
Password: conPass,
}
if hysteria2Config.Bandwidth != nil {
if hysteria2Config.Bandwidth.Up != nil &&
*hysteria2Config.Bandwidth.Up != "" {
hysteria2.Up = *hysteria2Config.Bandwidth.Up
}
if hysteria2Config.Bandwidth.Down != nil &&
*hysteria2Config.Bandwidth.Down != "" {
hysteria2.Down = *hysteria2Config.Bandwidth.Down
}
}
if hysteria2Config.Obfs != nil &&
hysteria2Config.Obfs.Type != nil &&
*hysteria2Config.Obfs.Type == "salamander" &&
hysteria2Config.Obfs.Salamander != nil &&
hysteria2Config.Obfs.Salamander.Password != nil &&
*hysteria2Config.Obfs.Salamander.Password != "" {
if clientType == constant.Shadowrocket {
hysteria2.Obfs = *hysteria2Config.Obfs.Salamander.Password
} else {
hysteria2.Obfs = "salamander"
hysteria2.ObfsPassword = *hysteria2Config.Obfs.Salamander.Password
}
}
if hysteria2Config.ACME != nil &&
hysteria2Config.ACME.Domains != nil &&
len(hysteria2Config.ACME.Domains) > 0 {
hysteria2.Sni = hysteria2Config.ACME.Domains[0]
}
hysteria2.SkipCertVerify = false
proxyGroup := bo.ProxyGroup{
Name: "PROXY",
Type: "select",
Proxies: []string{hysteria2Name},
}
clashConfig := bo.ClashConfig{
ProxyGroups: []bo.ProxyGroup{
proxyGroup,
},
Proxies: []interface{}{hysteria2},
}
clashConfigYaml, err := yaml.Marshal(&clashConfig)
if err != nil {
return "", "", err
}
configStr = string(clashConfigYaml)
if clientType == constant.Clash {
clashExtension, err := GetConfig(constant.ClashExtension)
if err != nil {
return "", "", err
}
if clashExtension.Value != nil && *clashExtension.Value != "" {
configStr = fmt.Sprintf("%s%s", configStr, *clashExtension.Value)
}
}
} else if clientType == constant.V2rayN {
hysteria2Url, err := Hysteria2Url(*account.Id, strings.Split(host, ":")[0])
if err != nil {
return "", "", err
}
configStr = hysteria2Url
}
return userInfo, configStr, nil
}
func Hysteria2Url(accountId int64, hostname string) (string, error) {
hysteria2Config, err := GetHysteria2Config()
if err != nil {
return "", err
}
if hysteria2Config.Listen == nil || *hysteria2Config.Listen == "" {
return "", errors.New("hysteria2 config is empty")
}
account, err := dao.GetAccount("id = ?", accountId)
if err != nil {
return "", err
}
urlConfig := ""
if hysteria2Config.Obfs != nil &&
hysteria2Config.Obfs.Type != nil &&
*hysteria2Config.Obfs.Type == "salamander" &&
hysteria2Config.Obfs.Salamander != nil &&
hysteria2Config.Obfs.Salamander.Password != nil &&
*hysteria2Config.Obfs.Salamander.Password != "" {
urlConfig += fmt.Sprintf("&obfs=salamander&obfs-password=%s", *hysteria2Config.Obfs.Salamander.Password)
}
if hysteria2Config.ACME != nil &&
hysteria2Config.ACME.Domains != nil &&
len(hysteria2Config.ACME.Domains) > 0 {
urlConfig += fmt.Sprintf("&sni=%s", hysteria2Config.ACME.Domains[0])
// shadowrocket
urlConfig += fmt.Sprintf("&peer=%s", hysteria2Config.ACME.Domains[0])
}
urlConfig += "&insecure=0"
if hysteria2Config.Bandwidth != nil &&
hysteria2Config.Bandwidth.Down != nil &&
*hysteria2Config.Bandwidth.Down != "" {
// shadowrocket
urlConfig += fmt.Sprintf("&downmbps=%s", url.PathEscape(*hysteria2Config.Bandwidth.Down))
}
hysteria2ConfigPortHopping, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigPortHopping)
if err != nil {
return "", err
}
if *hysteria2ConfigPortHopping.Value != "" {
// shadowrocket
urlConfig += fmt.Sprintf("&mport=%s", *hysteria2ConfigPortHopping.Value)
}
hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark)
if err != nil {
return "", err
}
if *hysteria2ConfigRemark.Value != "" {
urlConfig += fmt.Sprintf("#%s", *hysteria2ConfigRemark.Value)
}
if urlConfig != "" {
urlConfig = "/?" + strings.TrimPrefix(urlConfig, "&")
}
return fmt.Sprintf("hysteria2://%s@%s%s", *account.ConPass, hostname, *hysteria2Config.Listen) + urlConfig, nil
}
+60
View File
@@ -0,0 +1,60 @@
package service
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"hy2xs-admin/dao"
"hy2xs-admin/model/bo"
"hy2xs-admin/model/constant"
"strings"
"time"
)
const TokenExpireDuration = time.Hour * 24
type MyClaims struct {
AccountBo bo.AccountBo `json:"account"`
jwt.StandardClaims
}
func GenToken(accountBo bo.AccountBo) (string, error) {
c := MyClaims{
AccountBo: accountBo,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
Issuer: "hy2xs-admin",
},
}
config, err := dao.GetConfig("key = ?", constant.JwtSecret)
if err != nil {
return "", errors.New(constant.SysError)
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
return token.SignedString([]byte(*config.Value))
}
func ParseToken(tokenString string) (*MyClaims, error) {
config, err := dao.GetConfig("key = ?", constant.JwtSecret)
if err != nil {
return nil, errors.New(constant.SysError)
}
token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (i interface{}, err error) {
return []byte(*config.Value), nil
})
if err != nil {
return nil, errors.New(constant.IllegalTokenError)
}
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New(constant.TokenExpiredError)
}
func GetToken(c *gin.Context) string {
tokenStr := c.Request.Header.Get("Authorization")
if tokenStr == "" {
return ""
}
return strings.SplitN(tokenStr, " ", 2)[1]
}
+64
View File
@@ -0,0 +1,64 @@
package service
import (
"errors"
"fmt"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/vo"
"hy2xs-admin/util"
"regexp"
"strings"
)
func MonitorSystem() (vo.SystemMonitorVo, error) {
cpuPercent, err := util.GetCpuPercent()
if err != nil {
return vo.SystemMonitorVo{}, errors.New("cpu query failed")
}
memPercent, err := util.GetMemPercent()
if err != nil {
return vo.SystemMonitorVo{}, errors.New("mem query failed")
}
diskPercent, err := util.GetDiskPercent()
if err != nil {
return vo.SystemMonitorVo{}, errors.New("disk query failed")
}
return vo.SystemMonitorVo{
HUIVersion: constant.Version,
CpuPercent: cpuPercent,
MemPercent: memPercent,
DiskPercent: diskPercent,
}, nil
}
func MonitorHysteria2() (vo.Hysteria2MonitorVo, error) {
var hysteria2MonitorVo vo.Hysteria2MonitorVo
onlineUsers, err := Hysteria2Online()
if err != nil {
return hysteria2MonitorVo, err
}
if len(onlineUsers) > 0 {
hysteria2MonitorVo.UserTotal = int64(len(onlineUsers))
var deviceTotal int64 = 0
for _, value := range onlineUsers {
deviceTotal += value
}
hysteria2MonitorVo.DeviceTotal = deviceTotal
}
hysteria2MonitorVo.Version = "-"
content, err := util.Exec(fmt.Sprintf("%s version", util.GetHysteria2BinPath()))
if err == nil {
pattern := `v\d+\.\d+\.\d+`
re := regexp.MustCompile(pattern)
matches := re.FindAllString(strings.TrimSpace(content), -1)
if len(matches) > 0 {
hysteria2MonitorVo.Version = matches[0]
}
}
running := Hysteria2IsRunning()
hysteria2MonitorVo.Running = running
return hysteria2MonitorVo, nil
}
+69
View File
@@ -0,0 +1,69 @@
package service
import (
"context"
"errors"
"fmt"
"github.com/sirupsen/logrus"
"hy2xs-admin/util"
"net/http"
"time"
)
var server *http.Server
func InitServer(addr string, handler http.Handler) {
server = &http.Server{
Addr: addr,
Handler: handler,
}
}
func StartServer(crtPath string, keyPath string) error {
if crtPath != "" && keyPath != "" {
return server.ListenAndServeTLS(crtPath, keyPath)
}
return server.ListenAndServe()
}
func StopServer() error {
if err := StopHysteria2(); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logrus.Errorf("failed to shutdown server: %v", err)
return errors.New("failed to shutdown server")
}
return nil
}
func GetServerPortAndCert() (int64, string, string, error) {
port, crtPath, keyPath, err := GetPortAndCert()
if err != nil {
return 0, "", "", err
}
if !util.IsPortAvailable(uint(port), "tcp") {
errMsg := fmt.Sprintf("port %d is taken", port)
logrus.Errorf(errMsg)
return 0, "", "", errors.New(errMsg)
}
if crtPath != "" && !util.Exists(crtPath) {
errMsg := fmt.Sprintf("crt path: %s does not exist", crtPath)
logrus.Errorf(errMsg)
return 0, "", "", errors.New(errMsg)
}
if keyPath != "" && !util.Exists(keyPath) {
errMsg := fmt.Sprintf("key path: %s does not exist", keyPath)
logrus.Errorf(errMsg)
return 0, "", "", errors.New(errMsg)
}
return port, crtPath, keyPath, nil
}