mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-04-16 12:35:54 +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
|
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()
|
subJsonEnable, err := s.settingService.GetSubJsonEnable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subClashEnable, err := s.settingService.GetSubClashEnable()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Set base_path based on LinksPath for template rendering
|
// Set base_path based on LinksPath for template rendering
|
||||||
// Ensure LinksPath ends with "/" for proper asset URL generation
|
// Ensure LinksPath ends with "/" for proper asset URL generation
|
||||||
basePath := LinksPath
|
basePath := LinksPath
|
||||||
|
|
@ -255,7 +264,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
g := engine.Group("/")
|
g := engine.Group("/")
|
||||||
|
|
||||||
s.sub = NewSUBController(
|
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,
|
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl,
|
||||||
SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules)
|
SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,15 @@ type SUBController struct {
|
||||||
subRoutingRules string
|
subRoutingRules string
|
||||||
subPath string
|
subPath string
|
||||||
subJsonPath string
|
subJsonPath string
|
||||||
|
subClashPath string
|
||||||
jsonEnabled bool
|
jsonEnabled bool
|
||||||
|
clashEnabled bool
|
||||||
subEncrypt bool
|
subEncrypt bool
|
||||||
updateInterval string
|
updateInterval string
|
||||||
|
|
||||||
subService *SubService
|
subService *SubService
|
||||||
subJsonService *SubJsonService
|
subJsonService *SubJsonService
|
||||||
|
subClashService *SubClashService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSUBController creates a new subscription controller with the given configuration.
|
// NewSUBController creates a new subscription controller with the given configuration.
|
||||||
|
|
@ -34,7 +37,9 @@ func NewSUBController(
|
||||||
g *gin.RouterGroup,
|
g *gin.RouterGroup,
|
||||||
subPath string,
|
subPath string,
|
||||||
jsonPath string,
|
jsonPath string,
|
||||||
|
clashPath string,
|
||||||
jsonEnabled bool,
|
jsonEnabled bool,
|
||||||
|
clashEnabled bool,
|
||||||
encrypt bool,
|
encrypt bool,
|
||||||
showInfo bool,
|
showInfo bool,
|
||||||
rModel string,
|
rModel string,
|
||||||
|
|
@ -60,12 +65,15 @@ func NewSUBController(
|
||||||
subRoutingRules: subRoutingRules,
|
subRoutingRules: subRoutingRules,
|
||||||
subPath: subPath,
|
subPath: subPath,
|
||||||
subJsonPath: jsonPath,
|
subJsonPath: jsonPath,
|
||||||
|
subClashPath: clashPath,
|
||||||
jsonEnabled: jsonEnabled,
|
jsonEnabled: jsonEnabled,
|
||||||
|
clashEnabled: clashEnabled,
|
||||||
subEncrypt: encrypt,
|
subEncrypt: encrypt,
|
||||||
updateInterval: update,
|
updateInterval: update,
|
||||||
|
|
||||||
subService: sub,
|
subService: sub,
|
||||||
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
|
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
|
||||||
|
subClashService: NewSubClashService(sub),
|
||||||
}
|
}
|
||||||
a.initRouter(g)
|
a.initRouter(g)
|
||||||
return a
|
return a
|
||||||
|
|
@ -80,6 +88,10 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||||
gJson := g.Group(a.subJsonPath)
|
gJson := g.Group(a.subJsonPath)
|
||||||
gJson.GET(":subid", a.subJsons)
|
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.
|
// 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")
|
accept := c.GetHeader("Accept")
|
||||||
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
|
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
|
||||||
// Build page data in service
|
// 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 {
|
if !a.jsonEnabled {
|
||||||
subJsonURL = ""
|
subJsonURL = ""
|
||||||
}
|
}
|
||||||
|
if !a.clashEnabled {
|
||||||
|
subClashURL = ""
|
||||||
|
}
|
||||||
// Get base_path from context (set by middleware)
|
// Get base_path from context (set by middleware)
|
||||||
basePath, exists := c.Get("base_path")
|
basePath, exists := c.Get("base_path")
|
||||||
if !exists {
|
if !exists {
|
||||||
|
|
@ -116,7 +131,7 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||||
// Remove trailing slash if exists, add subId, then add trailing slash
|
// Remove trailing slash if exists, add subId, then add trailing slash
|
||||||
basePathStr = strings.TrimRight(basePathStr, "/") + "/" + subId + "/"
|
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{
|
c.HTML(200, "subpage.html", gin.H{
|
||||||
"title": "subscription.title",
|
"title": "subscription.title",
|
||||||
"cur_ver": config.GetVersion(),
|
"cur_ver": config.GetVersion(),
|
||||||
|
|
@ -136,6 +151,7 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||||
"totalByte": page.TotalByte,
|
"totalByte": page.TotalByte,
|
||||||
"subUrl": page.SubUrl,
|
"subUrl": page.SubUrl,
|
||||||
"subJsonUrl": page.SubJsonUrl,
|
"subJsonUrl": page.SubJsonUrl,
|
||||||
|
"subClashUrl": page.SubClashUrl,
|
||||||
"result": page.Result,
|
"result": page.Result,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -165,7 +181,6 @@ func (a *SUBController) subJsons(c *gin.Context) {
|
||||||
if err != nil || len(jsonSub) == 0 {
|
if err != nil || len(jsonSub) == 0 {
|
||||||
c.String(400, "Error!")
|
c.String(400, "Error!")
|
||||||
} else {
|
} else {
|
||||||
// Add headers
|
|
||||||
profileUrl := a.subProfileUrl
|
profileUrl := a.subProfileUrl
|
||||||
if profileUrl == "" {
|
if profileUrl == "" {
|
||||||
profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI)
|
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.
|
// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.
|
||||||
func (a *SUBController) ApplyCommonHeaders(
|
func (a *SUBController) ApplyCommonHeaders(
|
||||||
c *gin.Context,
|
c *gin.Context,
|
||||||
|
|
|
||||||
|
|
@ -1031,6 +1031,7 @@ type PageData struct {
|
||||||
TotalByte int64
|
TotalByte int64
|
||||||
SubUrl string
|
SubUrl string
|
||||||
SubJsonUrl string
|
SubJsonUrl string
|
||||||
|
SubClashUrl string
|
||||||
Result []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.
|
// 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.
|
// 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) {
|
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subClashPath, subId string) (subURL, subJsonURL, subClashURL string) {
|
||||||
// Input validation
|
|
||||||
if subId == "" {
|
if subId == "" {
|
||||||
return "", ""
|
return "", "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get configured URIs first (highest priority)
|
|
||||||
configuredSubURI, _ := s.settingService.GetSubURI()
|
configuredSubURI, _ := s.settingService.GetSubURI()
|
||||||
configuredSubJsonURI, _ := s.settingService.GetSubJsonURI()
|
configuredSubJsonURI, _ := s.settingService.GetSubJsonURI()
|
||||||
|
configuredSubClashURI, _ := s.settingService.GetSubClashURI()
|
||||||
|
|
||||||
// Determine base scheme and host (cached to avoid duplicate calls)
|
|
||||||
var baseScheme, baseHostWithPort string
|
var baseScheme, baseHostWithPort string
|
||||||
if configuredSubURI == "" || configuredSubJsonURI == "" {
|
if configuredSubURI == "" || configuredSubJsonURI == "" || configuredSubClashURI == "" {
|
||||||
baseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort)
|
baseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build subscription URL
|
|
||||||
subURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId)
|
subURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId)
|
||||||
|
|
||||||
// Build JSON subscription URL
|
|
||||||
subJsonURL = s.buildSingleURL(configuredSubJsonURI, baseScheme, baseHostWithPort, subJsonPath, subId)
|
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
|
// 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 parses header and prepares the template view model.
|
||||||
// BuildPageData constructs page data for rendering the subscription information page.
|
// 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)
|
download := common.FormatTraffic(traffic.Down)
|
||||||
upload := common.FormatTraffic(traffic.Up)
|
upload := common.FormatTraffic(traffic.Up)
|
||||||
total := "∞"
|
total := "∞"
|
||||||
|
|
@ -1183,6 +1180,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
|
||||||
TotalByte: traffic.Total,
|
TotalByte: traffic.Total,
|
||||||
SubUrl: subURL,
|
SubUrl: subURL,
|
||||||
SubJsonUrl: subJsonURL,
|
SubJsonUrl: subJsonURL,
|
||||||
|
SubClashUrl: subClashURL,
|
||||||
Result: subs,
|
Result: subs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue