mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-04-14 19:45:47 +00:00
feat(sub): integrate Clash YAML endpoint into subscription system
- Add Clash route handler in SUBController - Update BuildURLs to include Clash URL - Pass Clash settings through subscription pipeline Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9478e1a3e4
commit
9127fda70b
3 changed files with 58 additions and 20 deletions
13
sub/sub.go
13
sub/sub.go
|
|
@ -91,12 +91,21 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Determine if JSON subscription endpoint is enabled
|
||||
ClashPath, err := s.settingService.GetSubClashPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subJsonEnable, err := s.settingService.GetSubJsonEnable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subClashEnable, err := s.settingService.GetSubClashEnable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set base_path based on LinksPath for template rendering
|
||||
// Ensure LinksPath ends with "/" for proper asset URL generation
|
||||
basePath := LinksPath
|
||||
|
|
@ -255,7 +264,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
g := engine.Group("/")
|
||||
|
||||
s.sub = NewSUBController(
|
||||
g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||
g, LinksPath, JsonPath, ClashPath, subJsonEnable, subClashEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl,
|
||||
SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,12 +21,15 @@ type SUBController struct {
|
|||
subRoutingRules string
|
||||
subPath string
|
||||
subJsonPath string
|
||||
subClashPath string
|
||||
jsonEnabled bool
|
||||
clashEnabled bool
|
||||
subEncrypt bool
|
||||
updateInterval string
|
||||
|
||||
subService *SubService
|
||||
subJsonService *SubJsonService
|
||||
subService *SubService
|
||||
subJsonService *SubJsonService
|
||||
subClashService *SubClashService
|
||||
}
|
||||
|
||||
// NewSUBController creates a new subscription controller with the given configuration.
|
||||
|
|
@ -34,7 +37,9 @@ func NewSUBController(
|
|||
g *gin.RouterGroup,
|
||||
subPath string,
|
||||
jsonPath string,
|
||||
clashPath string,
|
||||
jsonEnabled bool,
|
||||
clashEnabled bool,
|
||||
encrypt bool,
|
||||
showInfo bool,
|
||||
rModel string,
|
||||
|
|
@ -60,12 +65,15 @@ func NewSUBController(
|
|||
subRoutingRules: subRoutingRules,
|
||||
subPath: subPath,
|
||||
subJsonPath: jsonPath,
|
||||
subClashPath: clashPath,
|
||||
jsonEnabled: jsonEnabled,
|
||||
clashEnabled: clashEnabled,
|
||||
subEncrypt: encrypt,
|
||||
updateInterval: update,
|
||||
|
||||
subService: sub,
|
||||
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
|
||||
subService: sub,
|
||||
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
|
||||
subClashService: NewSubClashService(sub),
|
||||
}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
|
|
@ -80,6 +88,10 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
|||
gJson := g.Group(a.subJsonPath)
|
||||
gJson.GET(":subid", a.subJsons)
|
||||
}
|
||||
if a.clashEnabled {
|
||||
gClash := g.Group(a.subClashPath)
|
||||
gClash.GET(":subid", a.subClashs)
|
||||
}
|
||||
}
|
||||
|
||||
// subs handles HTTP requests for subscription links, returning either HTML page or base64-encoded subscription data.
|
||||
|
|
@ -99,10 +111,13 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||
accept := c.GetHeader("Accept")
|
||||
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
|
||||
// Build page data in service
|
||||
subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId)
|
||||
subURL, subJsonURL, subClashURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, a.subClashPath, subId)
|
||||
if !a.jsonEnabled {
|
||||
subJsonURL = ""
|
||||
}
|
||||
if !a.clashEnabled {
|
||||
subClashURL = ""
|
||||
}
|
||||
// Get base_path from context (set by middleware)
|
||||
basePath, exists := c.Get("base_path")
|
||||
if !exists {
|
||||
|
|
@ -116,7 +131,7 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||
// Remove trailing slash if exists, add subId, then add trailing slash
|
||||
basePathStr = strings.TrimRight(basePathStr, "/") + "/" + subId + "/"
|
||||
}
|
||||
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, basePathStr)
|
||||
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, subClashURL, basePathStr)
|
||||
c.HTML(200, "subpage.html", gin.H{
|
||||
"title": "subscription.title",
|
||||
"cur_ver": config.GetVersion(),
|
||||
|
|
@ -136,6 +151,7 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||
"totalByte": page.TotalByte,
|
||||
"subUrl": page.SubUrl,
|
||||
"subJsonUrl": page.SubJsonUrl,
|
||||
"subClashUrl": page.SubClashUrl,
|
||||
"result": page.Result,
|
||||
})
|
||||
return
|
||||
|
|
@ -165,7 +181,6 @@ func (a *SUBController) subJsons(c *gin.Context) {
|
|||
if err != nil || len(jsonSub) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
// Add headers
|
||||
profileUrl := a.subProfileUrl
|
||||
if profileUrl == "" {
|
||||
profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI)
|
||||
|
|
@ -176,6 +191,22 @@ func (a *SUBController) subJsons(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *SUBController) subClashs(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
scheme, host, hostWithPort, _ := a.subService.ResolveRequest(c)
|
||||
clashSub, header, err := a.subClashService.GetClash(subId, host)
|
||||
if err != nil || len(clashSub) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
profileUrl := a.subProfileUrl
|
||||
if profileUrl == "" {
|
||||
profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI)
|
||||
}
|
||||
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, profileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules)
|
||||
c.Data(200, "application/yaml; charset=utf-8", []byte(clashSub))
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.
|
||||
func (a *SUBController) ApplyCommonHeaders(
|
||||
c *gin.Context,
|
||||
|
|
|
|||
|
|
@ -1031,6 +1031,7 @@ type PageData struct {
|
|||
TotalByte int64
|
||||
SubUrl string
|
||||
SubJsonUrl string
|
||||
SubClashUrl string
|
||||
Result []string
|
||||
}
|
||||
|
||||
|
|
@ -1080,29 +1081,25 @@ func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string,
|
|||
|
||||
// BuildURLs constructs absolute subscription and JSON subscription URLs for a given subscription ID.
|
||||
// It prioritizes configured URIs, then individual settings, and finally falls back to request-derived components.
|
||||
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {
|
||||
// Input validation
|
||||
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subClashPath, subId string) (subURL, subJsonURL, subClashURL string) {
|
||||
if subId == "" {
|
||||
return "", ""
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
// Get configured URIs first (highest priority)
|
||||
configuredSubURI, _ := s.settingService.GetSubURI()
|
||||
configuredSubJsonURI, _ := s.settingService.GetSubJsonURI()
|
||||
configuredSubClashURI, _ := s.settingService.GetSubClashURI()
|
||||
|
||||
// Determine base scheme and host (cached to avoid duplicate calls)
|
||||
var baseScheme, baseHostWithPort string
|
||||
if configuredSubURI == "" || configuredSubJsonURI == "" {
|
||||
if configuredSubURI == "" || configuredSubJsonURI == "" || configuredSubClashURI == "" {
|
||||
baseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort)
|
||||
}
|
||||
|
||||
// Build subscription URL
|
||||
subURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId)
|
||||
|
||||
// Build JSON subscription URL
|
||||
subJsonURL = s.buildSingleURL(configuredSubJsonURI, baseScheme, baseHostWithPort, subJsonPath, subId)
|
||||
subClashURL = s.buildSingleURL(configuredSubClashURI, baseScheme, baseHostWithPort, subClashPath, subId)
|
||||
|
||||
return subURL, subJsonURL
|
||||
return subURL, subJsonURL, subClashURL
|
||||
}
|
||||
|
||||
// getBaseSchemeAndHost determines the base scheme and host from settings or falls back to request values
|
||||
|
|
@ -1149,7 +1146,7 @@ func (s *SubService) joinPathWithID(basePath, subId string) string {
|
|||
|
||||
// BuildPageData parses header and prepares the template view model.
|
||||
// BuildPageData constructs page data for rendering the subscription information page.
|
||||
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string, basePath string) PageData {
|
||||
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL, subClashURL string, basePath string) PageData {
|
||||
download := common.FormatTraffic(traffic.Down)
|
||||
upload := common.FormatTraffic(traffic.Up)
|
||||
total := "∞"
|
||||
|
|
@ -1183,6 +1180,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
|
|||
TotalByte: traffic.Total,
|
||||
SubUrl: subURL,
|
||||
SubJsonUrl: subJsonURL,
|
||||
SubClashUrl: subClashURL,
|
||||
Result: subs,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue