310 lines
8.2 KiB
Go
310 lines
8.2 KiB
Go
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()
|
|
}
|