Files
HY2XS_flamy/apps/service/hysteria2_api.go
T

191 lines
4.9 KiB
Go

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()
}