package service import ( "errors" "github.com/sirupsen/logrus" "hy2xs-admin/dao" "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() secretDigest, digestErr := PeerSecretDigest(conPass) if digestErr != nil { return 0, "", digestErr } peer, err := dao.GetPeer(`secret_digest = ? and disabled = 0 and (quota_bytes < 0 or quota_bytes > download_bytes + upload_bytes) and (expires_at = 0 or ? < expires_at) and ? > banned_until`, secretDigest, now, now) if err != nil { return 0, "", err } // Ограничение количества устройств onlineUsers, err := Hysteria2Online() if err != nil { logrus.WithError(err).Warn("hysteria2 online users unavailable; skip device-limit check") return *peer.Id, *peer.AuthId, nil } device, exist := onlineUsers[*peer.AuthId] if exist && *peer.MaxDevices <= device { return 0, "", errors.New("device limited") } return *peer.Id, *peer.AuthId, 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.UpdatePeer(ids, map[string]interface{}{"banned_until": kickUtilTime}); err != nil { return err } peers, err := dao.ListPeer("id in ?", ids) if err != nil { return err } var keys []string for _, item := range peers { keys = append(keys, *item.AuthId) } 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 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 } peer, err := dao.GetPeer("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] } secret := "" if peer.SecretEncrypted != nil { decrypted, decErr := DecryptPeerSecret(*peer.SecretEncrypted) if decErr != nil { return "", decErr } secret = decrypted } return buildHysteria2Url(secret, 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() }