Files
trihuy-russian/sub/subController.go
T

308 lines
9.6 KiB
Go
Raw Normal View History

2023-05-22 18:06:34 +03:30
package sub
2023-04-09 23:13:18 +03:30
import (
2026-05-09 17:38:48 +02:00
"bytes"
2023-04-09 23:13:18 +03:30
"encoding/base64"
2026-05-09 17:38:48 +02:00
"encoding/json"
2025-09-14 23:08:09 +02:00
"fmt"
2026-05-09 17:38:48 +02:00
"net/http"
"os"
"strconv"
"strings"
2025-09-18 22:06:01 +02:00
2026-05-10 02:13:42 +02:00
webpkg "github.com/mhsanaei/3x-ui/v3/web"
"github.com/mhsanaei/3x-ui/v3/web/service"
2023-04-09 23:13:18 +03:30
"github.com/gin-gonic/gin"
)
2025-09-20 09:35:50 +02:00
// SUBController handles HTTP requests for subscription links and JSON configurations.
2023-04-09 23:13:18 +03:30
type SUBController struct {
subTitle string
subSupportUrl string
subProfileUrl string
subAnnounce string
subEnableRouting bool
subRoutingRules string
subPath string
subJsonPath string
2026-04-20 04:26:13 +08:00
subClashPath string
jsonEnabled bool
2026-04-20 04:26:13 +08:00
clashEnabled bool
subEncrypt bool
updateInterval string
2024-02-21 14:17:52 +03:30
2026-04-20 04:26:13 +08:00
subService *SubService
subJsonService *SubJsonService
subClashService *SubClashService
2026-05-09 17:38:48 +02:00
settingService service.SettingService
2023-04-09 23:13:18 +03:30
}
2025-09-20 09:35:50 +02:00
// NewSUBController creates a new subscription controller with the given configuration.
2024-02-21 14:17:52 +03:30
func NewSUBController(
g *gin.RouterGroup,
subPath string,
jsonPath string,
2026-04-20 04:26:13 +08:00
clashPath string,
2025-09-18 13:56:04 +02:00
jsonEnabled bool,
2026-04-20 04:26:13 +08:00
clashEnabled bool,
2024-02-21 14:17:52 +03:30
encrypt bool,
showInfo bool,
rModel string,
update string,
2024-03-11 01:01:24 +03:30
jsonFragment string,
2024-08-29 11:27:43 +02:00
jsonNoise string,
2024-03-12 19:44:51 +03:30
jsonMux string,
jsonRules string,
subTitle string,
subSupportUrl string,
subProfileUrl string,
subAnnounce string,
subEnableRouting bool,
subRoutingRules string,
2024-03-11 01:01:24 +03:30
) *SUBController {
2024-03-11 16:14:24 +03:30
sub := NewSubService(showInfo, rModel)
2024-02-21 14:17:52 +03:30
a := &SUBController{
subTitle: subTitle,
subSupportUrl: subSupportUrl,
subProfileUrl: subProfileUrl,
subAnnounce: subAnnounce,
subEnableRouting: subEnableRouting,
subRoutingRules: subRoutingRules,
subPath: subPath,
subJsonPath: jsonPath,
2026-04-20 04:26:13 +08:00
subClashPath: clashPath,
jsonEnabled: jsonEnabled,
2026-04-20 04:26:13 +08:00
clashEnabled: clashEnabled,
subEncrypt: encrypt,
updateInterval: update,
2026-04-20 04:26:13 +08:00
subService: sub,
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
subClashService: NewSubClashService(sub),
2024-02-21 14:17:52 +03:30
}
2023-04-09 23:13:18 +03:30
a.initRouter(g)
return a
}
2025-09-20 09:35:50 +02:00
// initRouter registers HTTP routes for subscription links and JSON endpoints
// on the provided router group.
2023-04-09 23:13:18 +03:30
func (a *SUBController) initRouter(g *gin.RouterGroup) {
2024-02-21 14:17:52 +03:30
gLink := g.Group(a.subPath)
gLink.GET(":subid", a.subs)
2025-09-18 13:56:04 +02:00
if a.jsonEnabled {
gJson := g.Group(a.subJsonPath)
gJson.GET(":subid", a.subJsons)
}
2026-04-20 04:26:13 +08:00
if a.clashEnabled {
gClash := g.Group(a.subClashPath)
gClash.GET(":subid", a.subClashs)
}
2023-04-09 23:13:18 +03:30
}
2025-09-20 09:35:50 +02:00
// subs handles HTTP requests for subscription links, returning either HTML page or base64-encoded subscription data.
2023-04-09 23:13:18 +03:30
func (a *SUBController) subs(c *gin.Context) {
subId := c.Param("subid")
2025-09-14 01:22:42 +02:00
scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c)
2025-09-14 23:08:09 +02:00
subs, lastOnline, traffic, err := a.subService.GetSubs(subId, host)
2023-04-18 21:34:06 +03:30
if err != nil || len(subs) == 0 {
2023-04-09 23:13:18 +03:30
c.String(400, "Error!")
} else {
result := ""
for _, sub := range subs {
result += sub + "\n"
}
2023-04-18 21:34:06 +03:30
2025-09-14 01:22:42 +02:00
// If the request expects HTML (e.g., browser) or explicitly asked (?html=1 or ?view=html), render the info page here
accept := c.GetHeader("Accept")
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
2026-04-20 04:26:13 +08:00
subURL, subJsonURL, subClashURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, a.subClashPath, subId)
2025-09-18 13:56:04 +02:00
if !a.jsonEnabled {
subJsonURL = ""
}
2026-04-20 04:26:13 +08:00
if !a.clashEnabled {
subClashURL = ""
}
2025-09-24 19:51:01 +02:00
basePath, exists := c.Get("base_path")
if !exists {
basePath = "/"
}
basePathStr := basePath.(string)
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, subClashURL, basePathStr, a.subTitle, a.subSupportUrl)
2026-05-09 17:38:48 +02:00
a.serveSubPage(c, basePathStr, page)
2025-09-14 01:22:42 +02:00
return
}
2023-04-18 21:34:06 +03:30
2025-09-14 19:44:26 +02:00
// Add headers
2025-09-14 23:08:09 +02:00
header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
profileUrl := a.subProfileUrl
2026-02-09 23:45:25 +03:00
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)
2025-09-14 19:44:26 +02:00
2024-02-21 14:17:52 +03:30
if a.subEncrypt {
2023-08-26 16:54:01 +03:30
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
} else {
c.String(200, result)
}
2023-04-09 23:13:18 +03:30
}
}
2024-02-21 14:17:52 +03:30
2026-05-09 17:38:48 +02:00
// serveSubPage renders web/dist/subpage.html for the current subscription
// request. The Vite-built SPA reads window.__SUB_PAGE_DATA__ on mount —
// we inject that here, along with window.X_UI_BASE_PATH so the
2026-05-09 17:38:48 +02:00
// page's static asset references resolve correctly when the panel runs
// behind a URL prefix.
func (a *SUBController) serveSubPage(c *gin.Context, basePath string, page PageData) {
var body []byte
if diskBody, diskErr := os.ReadFile("web/dist/subpage.html"); diskErr == nil {
body = diskBody
} else {
dist := webpkg.EmbeddedDist()
readBody, err := dist.ReadFile("dist/subpage.html")
if err != nil {
c.String(http.StatusInternalServerError, "missing embedded subpage")
return
}
body = readBody
2026-05-09 17:38:48 +02:00
}
// Vite emits absolute asset URLs (`/assets/...`); when the panel is
// installed under a custom URL prefix, rewrite them so the bundle
// loads from `<basePath>assets/...` where the static handler is
// actually mounted.
if basePath != "/" && basePath != "" {
body = bytes.ReplaceAll(body, []byte(`src="/assets/`), []byte(`src="`+basePath+`assets/`))
body = bytes.ReplaceAll(body, []byte(`href="/assets/`), []byte(`href="`+basePath+`assets/`))
}
// JSON-marshal the view-model so the SPA can read it as a plain
// object on mount. PageData fields are already in the shape the Vue
// component expects, plus a `links` array carrying the rendered
// share URLs.
// The panel's "Calendar Type" setting decides whether the SubPage
// renders dates in Gregorian or Jalali — surface it here so the SPA
// can match the rest of the panel without a round-trip.
datepicker, _ := a.settingService.GetDatepicker()
if datepicker == "" {
datepicker = "gregorian"
}
subData := map[string]any{
"sId": page.SId,
"enabled": page.Enabled,
"download": page.Download,
"upload": page.Upload,
"total": page.Total,
"used": page.Used,
"remained": page.Remained,
"expire": page.Expire,
"lastOnline": page.LastOnline,
"downloadByte": page.DownloadByte,
"uploadByte": page.UploadByte,
"totalByte": page.TotalByte,
"subUrl": page.SubUrl,
"subJsonUrl": page.SubJsonUrl,
"subClashUrl": page.SubClashUrl,
"links": page.Result,
"datepicker": datepicker,
}
subDataJSON, err := json.Marshal(subData)
if err != nil {
subDataJSON = []byte("{}")
}
// Defense-in-depth string-escape for the basePath embed — admin-
// controlled but cheap to harden.
jsEscape := strings.NewReplacer(
`\`, `\\`,
`"`, `\"`,
"\n", `\n`,
"\r", `\r`,
"<", `<`,
">", `>`,
"&", `&`,
)
escapedBase := jsEscape.Replace(basePath)
inject := []byte(`<script>window.X_UI_BASE_PATH="` + escapedBase + `";` +
2026-05-09 17:38:48 +02:00
`window.__SUB_PAGE_DATA__=` + string(subDataJSON) + `;</script></head>`)
out := bytes.Replace(body, []byte("</head>"), inject, 1)
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
c.Header("Pragma", "no-cache")
c.Header("Expires", "0")
c.Data(http.StatusOK, "text/html; charset=utf-8", out)
}
2025-09-20 09:35:50 +02:00
// subJsons handles HTTP requests for JSON subscription configurations.
2024-02-21 14:17:52 +03:30
func (a *SUBController) subJsons(c *gin.Context) {
subId := c.Param("subid")
2026-02-09 23:45:25 +03:00
scheme, host, hostWithPort, _ := a.subService.ResolveRequest(c)
2024-02-21 14:17:52 +03:30
jsonSub, header, err := a.subJsonService.GetJson(subId, host)
if err != nil || len(jsonSub) == 0 {
c.String(400, "Error!")
} else {
2026-02-09 23:45:25 +03:00
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)
2024-02-21 14:17:52 +03:30
c.String(200, jsonSub)
}
}
2026-04-20 04:26:13 +08:00
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))
}
}
2025-09-20 09:35:50 +02:00
// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.
func (a *SUBController) ApplyCommonHeaders(
c *gin.Context,
header,
updateInterval,
profileTitle string,
profileSupportUrl string,
profileUrl string,
profileAnnounce string,
profileEnableRouting bool,
profileRoutingRules string,
) {
2025-09-14 19:44:26 +02:00
c.Writer.Header().Set("Subscription-Userinfo", header)
c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
//Basics
if profileTitle != "" {
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))
}
if profileSupportUrl != "" {
c.Writer.Header().Set("Support-Url", profileSupportUrl)
}
if profileUrl != "" {
c.Writer.Header().Set("Profile-Web-Page-Url", profileUrl)
}
if profileAnnounce != "" {
c.Writer.Header().Set("Announce", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileAnnounce)))
}
//Advanced (Happ)
c.Writer.Header().Set("Routing-Enable", strconv.FormatBool(profileEnableRouting))
if profileRoutingRules != "" {
c.Writer.Header().Set("Routing", profileRoutingRules)
}
2025-09-14 19:44:26 +02:00
}