feat(panDomain): update subscription logic to use V2 handler for improved functionality

This commit is contained in:
Chang lue Tsen 2025-08-16 06:18:32 -04:00
parent 2dfb1030e1
commit 81a46d1f13
4 changed files with 238 additions and 244 deletions

View File

@ -10,41 +10,41 @@ import (
"github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/internal/types"
) )
func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { //func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) { // return func(c *gin.Context) {
var req types.SubscribeRequest // var req types.SubscribeRequest
if c.Request.Header.Get("token") != "" { // if c.Request.Header.Get("token") != "" {
req.Token = c.Request.Header.Get("token") // req.Token = c.Request.Header.Get("token")
} else { // } else {
req.Token = c.Query("token") // req.Token = c.Query("token")
} // }
req.UA = c.Request.Header.Get("User-Agent") // req.UA = c.Request.Header.Get("User-Agent")
req.Flag = c.Query("flag") // req.Flag = c.Query("flag")
//
// intercept browser // // intercept browser
ua := c.GetHeader("User-Agent") // ua := c.GetHeader("User-Agent")
if ua == "" { // if ua == "" {
c.String(http.StatusForbidden, "Access denied") // c.String(http.StatusForbidden, "Access denied")
return // return
} // }
browserKeywords := []string{"chrome", "firefox", "safari", "edge", "opera", "micromessenger"} // browserKeywords := []string{"chrome", "firefox", "safari", "edge", "opera", "micromessenger"}
for _, keyword := range browserKeywords { // for _, keyword := range browserKeywords {
lcUA := strings.ToLower(ua) // lcUA := strings.ToLower(ua)
if strings.Contains(lcUA, keyword) { // if strings.Contains(lcUA, keyword) {
c.String(http.StatusForbidden, "Access denied") // c.String(http.StatusForbidden, "Access denied")
return // return
} // }
} // }
//
l := subscribe.NewSubscribeLogic(c, svcCtx) // l := subscribe.NewSubscribeLogic(c, svcCtx)
resp, err := l.Generate(&req) // resp, err := l.V2(&req)
if err != nil { // if err != nil {
return // return
} // }
c.Header("subscription-userinfo", resp.Header) // c.Header("subscription-userinfo", resp.Header)
c.String(200, "%s", string(resp.Config)) // c.String(200, "%s", string(resp.Config))
} // }
} //}
func V2SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { func V2SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) { return func(c *gin.Context) {

View File

@ -1,16 +1,9 @@
package subscribe package subscribe
import ( import (
"fmt"
"net/url"
"strings" "strings"
"time" "time"
"github.com/perfect-panel/server/pkg/adapter"
"github.com/perfect-panel/server/pkg/adapter/shadowrocket"
"github.com/perfect-panel/server/pkg/adapter/surfboard"
"github.com/perfect-panel/server/pkg/adapter/surge"
"github.com/perfect-panel/server/internal/model/server" "github.com/perfect-panel/server/internal/model/server"
"github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/model/user"
@ -39,38 +32,38 @@ func NewSubscribeLogic(ctx *gin.Context, svc *svc.ServiceContext) *SubscribeLogi
} }
} }
func (l *SubscribeLogic) Generate(req *types.SubscribeRequest) (*types.SubscribeResponse, error) { //func (l *SubscribeLogic) GenerateBak(req *types.SubscribeRequest) (*types.SubscribeResponse, error) {
userSub, err := l.getUserSubscribe(req.Token) // userSub, err := l.getUserSubscribe(req.Token)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
//
var subscribeStatus = false // var subscribeStatus = false
defer func() { // defer func() {
l.logSubscribeActivity(subscribeStatus, userSub, req) // l.logSubscribeActivity(subscribeStatus, userSub, req)
}() // }()
//
servers, err := l.getServers(userSub) // servers, err := l.getServers(userSub)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
//
rules, err := l.getRules() // rules, err := l.getRules()
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
//
resp, headerInfo, err := l.buildClientConfig(req, userSub, servers, rules) // resp, headerInfo, err := l.buildClientConfig(req, userSub, servers, rules)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
//
subscribeStatus = true // subscribeStatus = true
return &types.SubscribeResponse{ // return &types.SubscribeResponse{
Config: resp, // Config: resp,
Header: headerInfo, // Header: headerInfo,
}, nil // }, nil
} //}
func (l *SubscribeLogic) getUserSubscribe(token string) (*user.Subscribe, error) { func (l *SubscribeLogic) getUserSubscribe(token string) (*user.Subscribe, error) {
userSub, err := l.svc.UserModel.FindOneSubscribeByToken(l.ctx.Request.Context(), token) userSub, err := l.svc.UserModel.FindOneSubscribeByToken(l.ctx.Request.Context(), token)
@ -171,174 +164,174 @@ func (l *SubscribeLogic) getFirstHostLine() string {
return host return host
} }
func (l *SubscribeLogic) getRules() ([]*server.RuleGroup, error) { //func (l *SubscribeLogic) getRules() ([]*server.RuleGroup, error) {
rules, err := l.svc.ServerModel.QueryAllRuleGroup(l.ctx) // rules, err := l.svc.ServerModel.QueryAllRuleGroup(l.ctx)
if err != nil { // if err != nil {
l.Errorw("[Generate Subscribe]find rule group error: %v", logger.Field("error", err.Error())) // l.Errorw("[Generate Subscribe]find rule group error: %v", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find rule group error: %v", err.Error()) // return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find rule group error: %v", err.Error())
} // }
return rules, nil // return rules, nil
} //}
//
//func (l *SubscribeLogic) buildClientConfig(req *types.SubscribeRequest, userSub *user.Subscribe, servers []*server.Server, rules []*server.RuleGroup) ([]byte, string, error) {
// tags := make(map[string][]*server.Server)
//
// serverTags, err := l.svc.ServerModel.FindServerTags(l.ctx)
// if err != nil {
// l.Errorw("[Generate Subscribe]find server tags error: %v", logger.Field("error", err.Error()))
// return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server tags error: %v", err.Error())
// }
// // Deduplicate tags
// serverTags = tool.RemoveDuplicateElements(serverTags...)
// for _, tag := range serverTags {
// s, err := l.svc.ServerModel.FindServersByTag(l.ctx.Request.Context(), tag)
// if err != nil {
// l.Errorw("[Generate Subscribe]find servers by tag error: %v", logger.Field("error", err.Error()))
// continue
// }
// if len(s) > 0 {
// tags[tag] = s
// }
// }
//
// proxyManager := adapter.NewAdapter(&adapter.Config{
// Nodes: servers,
// Rules: rules,
// Tags: tags,
// })
// clientType := l.getClientType(req)
// var resp []byte
//
// l.Logger.Info(fmt.Sprintf("[Generate Subscribe] %s", clientType), logger.Field("ua", req.UA), logger.Field("flag", req.Flag))
//
// switch clientType {
// case "clash":
// resp, err = proxyManager.BuildClash(userSub.UUID)
// if err != nil {
// l.Errorw("[Generate Subscribe] build clash error", logger.Field("error", err.Error()))
// return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "build clash error: %v", err.Error())
// }
// l.setClashHeaders()
// case "sing-box":
// resp, err = proxyManager.BuildSingbox(userSub.UUID)
// if err != nil {
// l.Errorw("[Generate Subscribe] build sing-box error", logger.Field("error", err.Error()))
// return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "build sing-box error: %v", err.Error())
// }
// case "quantumult":
// resp = []byte(proxyManager.BuildQuantumultX(userSub.UUID))
// case "shadowrocket":
// resp = proxyManager.BuildShadowrocket(userSub.UUID, shadowrocket.UserInfo{
// Upload: userSub.Upload,
// Download: userSub.Download,
// TotalTraffic: userSub.Traffic,
// ExpiredDate: userSub.ExpireTime,
// })
// case "loon":
// resp = proxyManager.BuildLoon(userSub.UUID)
// l.setLoonHeaders()
// case "surfboard":
// subsURL := l.getSubscribeURL(userSub.Token, "surfboard")
// resp = proxyManager.BuildSurfboard(l.svc.Config.Site.SiteName, surfboard.UserInfo{
// Upload: userSub.Upload,
// Download: userSub.Download,
// TotalTraffic: userSub.Traffic,
// ExpiredDate: userSub.ExpireTime,
// UUID: userSub.UUID,
// SubscribeURL: subsURL,
// })
// l.setSurfboardHeaders()
// case "v2rayn":
// resp = proxyManager.BuildV2rayN(userSub.UUID)
// case "surge":
// subsURL := l.getSubscribeURL(userSub.Token, "surge")
// resp = proxyManager.BuildSurge(l.svc.Config.Site.SiteName, surge.UserInfo{
// UUID: userSub.UUID,
// Upload: userSub.Upload,
// Download: userSub.Download,
// TotalTraffic: userSub.Traffic,
// ExpiredDate: userSub.ExpireTime,
// SubscribeURL: subsURL,
// })
// l.setSurgeHeaders()
// default:
// resp = proxyManager.BuildGeneral(userSub.UUID)
// }
//
// headerInfo := fmt.Sprintf("upload=%d;download=%d;total=%d;expire=%d",
// userSub.Upload, userSub.Download, userSub.Traffic, userSub.ExpireTime.Unix())
//
// return resp, headerInfo, nil
//}
//
//func (l *SubscribeLogic) setClashHeaders() {
// l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s", url.QueryEscape(l.svc.Config.Site.SiteName)))
// l.ctx.Header("Profile-Update-Interval", "24")
// l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
//}
//
//func (l *SubscribeLogic) setSurfboardHeaders() {
// l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", url.QueryEscape(l.svc.Config.Site.SiteName)))
// l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
//}
//
//func (l *SubscribeLogic) setSurgeHeaders() {
// l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", url.QueryEscape(l.svc.Config.Site.SiteName)))
// l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
//}
//
//func (l *SubscribeLogic) setLoonHeaders() {
// l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", url.QueryEscape(l.svc.Config.Site.SiteName)))
// l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
//}
func (l *SubscribeLogic) buildClientConfig(req *types.SubscribeRequest, userSub *user.Subscribe, servers []*server.Server, rules []*server.RuleGroup) ([]byte, string, error) { //func (l *SubscribeLogic) getSubscribeURL(token, flag string) string {
tags := make(map[string][]*server.Server) // if l.svc.Config.Subscribe.PanDomain {
// return fmt.Sprintf("https://%s", l.ctx.Request.Host)
// }
//
// if l.svc.Config.Subscribe.SubscribeDomain != "" {
// domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n")
// return fmt.Sprintf("https://%s%s?token=%s&flag=%s", domains[0], l.svc.Config.Subscribe.SubscribePath, token, flag)
// }
//
// return fmt.Sprintf("https://%s%s?token=%s&flag=surfboard", l.ctx.Request.Host, l.svc.Config.Subscribe.SubscribePath, token)
//}
serverTags, err := l.svc.ServerModel.FindServerTags(l.ctx) //func (l *SubscribeLogic) getClientType(req *types.SubscribeRequest) string {
if err != nil { // clientTypeMap := map[string]string{
l.Errorw("[Generate Subscribe]find server tags error: %v", logger.Field("error", err.Error())) // "clash": "clash",
return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server tags error: %v", err.Error()) // "meta": "clash",
} // "sing-box": "sing-box",
// Deduplicate tags // "hiddify": "sing-box",
serverTags = tool.RemoveDuplicateElements(serverTags...) // "surge": "surge",
for _, tag := range serverTags { // "quantumult": "quantumult",
s, err := l.svc.ServerModel.FindServersByTag(l.ctx.Request.Context(), tag) // "shadowrocket": "shadowrocket",
if err != nil { // "loon": "loon",
l.Errorw("[Generate Subscribe]find servers by tag error: %v", logger.Field("error", err.Error())) // "surfboard": "surfboard",
continue // "v2rayn": "v2rayn",
} // }
if len(s) > 0 { //
tags[tag] = s // findClient := func(s string) string {
} // s = strings.ToLower(strings.TrimSpace(s))
} // if s == "" {
// return ""
proxyManager := adapter.NewAdapter(&adapter.Config{ // }
Nodes: servers, //
Rules: rules, // for key, clientType := range clientTypeMap {
Tags: tags, // if strings.Contains(s, key) {
}) // return clientType
clientType := l.getClientType(req) // }
var resp []byte // }
//
l.Logger.Info(fmt.Sprintf("[Generate Subscribe] %s", clientType), logger.Field("ua", req.UA), logger.Field("flag", req.Flag)) // return ""
// }
switch clientType { //
case "clash": // // 优先检查Flag参数
resp, err = proxyManager.BuildClash(userSub.UUID) // if typ := findClient(req.Flag); typ != "" {
if err != nil { // return typ
l.Errorw("[Generate Subscribe] build clash error", logger.Field("error", err.Error())) // }
return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "build clash error: %v", err.Error()) //
} // // 其次检查UA参数
l.setClashHeaders() // return findClient(req.UA)
case "sing-box": //}
resp, err = proxyManager.BuildSingbox(userSub.UUID)
if err != nil {
l.Errorw("[Generate Subscribe] build sing-box error", logger.Field("error", err.Error()))
return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "build sing-box error: %v", err.Error())
}
case "quantumult":
resp = []byte(proxyManager.BuildQuantumultX(userSub.UUID))
case "shadowrocket":
resp = proxyManager.BuildShadowrocket(userSub.UUID, shadowrocket.UserInfo{
Upload: userSub.Upload,
Download: userSub.Download,
TotalTraffic: userSub.Traffic,
ExpiredDate: userSub.ExpireTime,
})
case "loon":
resp = proxyManager.BuildLoon(userSub.UUID)
l.setLoonHeaders()
case "surfboard":
subsURL := l.getSubscribeURL(userSub.Token, "surfboard")
resp = proxyManager.BuildSurfboard(l.svc.Config.Site.SiteName, surfboard.UserInfo{
Upload: userSub.Upload,
Download: userSub.Download,
TotalTraffic: userSub.Traffic,
ExpiredDate: userSub.ExpireTime,
UUID: userSub.UUID,
SubscribeURL: subsURL,
})
l.setSurfboardHeaders()
case "v2rayn":
resp = proxyManager.BuildV2rayN(userSub.UUID)
case "surge":
subsURL := l.getSubscribeURL(userSub.Token, "surge")
resp = proxyManager.BuildSurge(l.svc.Config.Site.SiteName, surge.UserInfo{
UUID: userSub.UUID,
Upload: userSub.Upload,
Download: userSub.Download,
TotalTraffic: userSub.Traffic,
ExpiredDate: userSub.ExpireTime,
SubscribeURL: subsURL,
})
l.setSurgeHeaders()
default:
resp = proxyManager.BuildGeneral(userSub.UUID)
}
headerInfo := fmt.Sprintf("upload=%d;download=%d;total=%d;expire=%d",
userSub.Upload, userSub.Download, userSub.Traffic, userSub.ExpireTime.Unix())
return resp, headerInfo, nil
}
func (l *SubscribeLogic) setClashHeaders() {
l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s", url.QueryEscape(l.svc.Config.Site.SiteName)))
l.ctx.Header("Profile-Update-Interval", "24")
l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
}
func (l *SubscribeLogic) setSurfboardHeaders() {
l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", url.QueryEscape(l.svc.Config.Site.SiteName)))
l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
}
func (l *SubscribeLogic) setSurgeHeaders() {
l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", url.QueryEscape(l.svc.Config.Site.SiteName)))
l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
}
func (l *SubscribeLogic) setLoonHeaders() {
l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", url.QueryEscape(l.svc.Config.Site.SiteName)))
l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
}
func (l *SubscribeLogic) getSubscribeURL(token, flag string) string {
if l.svc.Config.Subscribe.PanDomain {
return fmt.Sprintf("https://%s", l.ctx.Request.Host)
}
if l.svc.Config.Subscribe.SubscribeDomain != "" {
domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n")
return fmt.Sprintf("https://%s%s?token=%s&flag=%s", domains[0], l.svc.Config.Subscribe.SubscribePath, token, flag)
}
return fmt.Sprintf("https://%s%s?token=%s&flag=surfboard", l.ctx.Request.Host, l.svc.Config.Subscribe.SubscribePath, token)
}
func (l *SubscribeLogic) getClientType(req *types.SubscribeRequest) string {
clientTypeMap := map[string]string{
"clash": "clash",
"meta": "clash",
"sing-box": "sing-box",
"hiddify": "sing-box",
"surge": "surge",
"quantumult": "quantumult",
"shadowrocket": "shadowrocket",
"loon": "loon",
"surfboard": "surfboard",
"v2rayn": "v2rayn",
}
findClient := func(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
if s == "" {
return ""
}
for key, clientType := range clientTypeMap {
if strings.Contains(s, key) {
return clientType
}
}
return ""
}
// 优先检查Flag参数
if typ := findClient(req.Flag); typ != "" {
return typ
}
// 其次检查UA参数
return findClient(req.UA)
}

View File

@ -110,6 +110,7 @@ func (l *SubscribeLogic) V2(req *types.SubscribeRequest) (resp *types.SubscribeR
userSubscribe.Upload, userSubscribe.Download, userSubscribe.Traffic, userSubscribe.ExpireTime.Unix(), userSubscribe.Upload, userSubscribe.Download, userSubscribe.Traffic, userSubscribe.ExpireTime.Unix(),
), ),
} }
subscribeStatus = true
return return
} }

View File

@ -41,7 +41,7 @@ func PanDomainMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
UA: c.Request.Header.Get("User-Agent"), UA: c.Request.Header.Get("User-Agent"),
} }
l := subscribe.NewSubscribeLogic(c, svc) l := subscribe.NewSubscribeLogic(c, svc)
resp, err := l.Generate(&request) resp, err := l.V2(&request)
if err != nil { if err != nil {
return return
} }