package service import ( "errors" "fmt" "gopkg.in/yaml.v3" "hy2xs-admin/dao" "hy2xs-admin/model/bo" "hy2xs-admin/model/constant" "hy2xs-admin/proxy" "net" "net/url" "os" "strconv" "strings" "time" ) func resolvePublicEndpoint() (string, int, error) { host := strings.TrimSpace(os.Getenv("HY2XS_PUBLIC_HOST")) if host == "" || host == "0.0.0.0" { return "", 0, errors.New("HY2XS_PUBLIC_HOST must be set to public domain or IPv4") } portRaw := strings.TrimSpace(os.Getenv("HY2XS_PUBLIC_PORT")) if portRaw == "" { return "", 0, errors.New("HY2XS_PUBLIC_PORT is required") } port, err := strconv.Atoi(portRaw) if err != nil || port < 1 || port > 65535 { return "", 0, errors.New("HY2XS_PUBLIC_PORT must be a valid TCP port") } return host, port, nil } 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") } trafficSecretConfig, err := dao.GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret) if err != nil { return nil, err } onlineUsers, err := proxy.NewHysteria2Api(apiPort).OnlineUsers(*trafficSecretConfig.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") } trafficSecretConfig, err := dao.GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret) if err != nil { return err } if err = proxy.NewHysteria2Api(apiPort).KickUsers(keys, *trafficSecretConfig.Value); err != nil { return err } return nil } func Hysteria2SubscribeUrl(accountId int64, protocol string) (string, error) { account, err := dao.GetAccount("id = ?", accountId) if err != nil { return "", err } publicHost, publicPort, err := resolvePublicEndpoint() 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:%d%s/hui/%s", protocol, publicHost, publicPort, webContext, url.QueryEscape(*account.ConPass)), nil } func Hysteria2Subscribe(conPass string, clientType 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 } publicHost, publicPort, err := resolvePublicEndpoint() 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 } 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: publicHost, Port: strconv.Itoa(publicPort), 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) if err != nil { return "", "", err } configStr = hysteria2Url } return userInfo, configStr, nil } func Hysteria2Url(accountId int64) (string, error) { hysteria2Config, err := GetHysteria2Config() if err != nil { return "", err } if hysteria2Config.Listen == nil || *hysteria2Config.Listen == "" { return "", errors.New("hysteria2 config is empty") } hostname, port, err := resolvePublicEndpoint() if err != nil { return "", err } account, err := dao.GetAccount("id = ?", accountId) if err != nil { return "", err } hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark) if err != nil { return "", err } remark := "" if hysteria2ConfigRemark.Value != nil { remark = *hysteria2ConfigRemark.Value } obfsType := "" obfsPassword := "" if hysteria2Config.Obfs != nil && hysteria2Config.Obfs.Type != nil && hysteria2Config.Obfs.Salamander != nil && hysteria2Config.Obfs.Salamander.Password != nil { obfsType = *hysteria2Config.Obfs.Type obfsPassword = *hysteria2Config.Obfs.Salamander.Password } sni := "" if hysteria2Config.ACME != nil && len(hysteria2Config.ACME.Domains) > 0 { sni = hysteria2Config.ACME.Domains[0] } return buildHysteria2Url(*account.ConPass, hostname, port, obfsType, obfsPassword, sni, remark), nil } func buildHysteria2Url(conPass string, hostname string, port int, obfsType string, obfsPassword string, sni string, remark string) string { query := url.Values{} if obfsType == "salamander" && obfsPassword != "" { query.Set("obfs", "salamander") query.Set("obfs-password", obfsPassword) } if sni != "" { query.Set("sni", sni) } query.Set("insecure", "0") u := url.URL{ Scheme: "hysteria2", User: url.User(conPass), Host: net.JoinHostPort(hostname, strconv.Itoa(port)), Path: "/", RawQuery: query.Encode(), } if strings.TrimSpace(remark) != "" { u.Fragment = remark } return u.String() }