diff --git a/sub/html/sub.html b/sub/html/sub.html new file mode 100644 index 00000000..a1b61d82 --- /dev/null +++ b/sub/html/sub.html @@ -0,0 +1,105 @@ + + + + + + + + {{ .sId }} - Sub Info + + + + + + + + +
+

اطلاعات سابسکریپشن

+ +
شناسه اشتراک : {{ .sId }}
+
وضعیت اشتراک :
+
دانلود : {{ .download }}
+
آپلود : {{ .upload }}
+
تاریخ پایان :
+
حجم کلی :
+
+ {{ range .result }} +
+ + {{ . }} +
+ {{ end }} +
+ +
+ + + +
+
+ + + + diff --git a/sub/sub.go b/sub/sub.go index db582e8d..4ff7b8da 100644 --- a/sub/sub.go +++ b/sub/sub.go @@ -3,11 +3,14 @@ package sub import ( "context" "crypto/tls" + "embed" + "html/template" "io" + "io/fs" "net" "net/http" + "os" "strconv" - "x-ui/config" "x-ui/logger" "x-ui/util/common" @@ -15,9 +18,13 @@ import ( "x-ui/web/network" "x-ui/web/service" + "github.com/gin-contrib/gzip" "github.com/gin-gonic/gin" ) +//go:embed html/* +var htmlFS embed.FS + type Server struct { httpServer *http.Server listener net.Listener @@ -37,6 +44,48 @@ func NewServer() *Server { } } +func (s *Server) getHtmlFiles() ([]string, error) { + files := make([]string, 0) + dir, _ := os.Getwd() + err := fs.WalkDir(os.DirFS(dir), "sub/html", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + files = append(files, path) + return nil + }) + if err != nil { + return nil, err + } + return files, nil +} + +func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, error) { + t := template.New("").Funcs(funcMap) + err := fs.WalkDir(htmlFS, "html", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + newT, err := t.ParseFS(htmlFS, path+"/*.html") + if err != nil { + // ignore + return nil + } + t = newT + } + return nil + }) + if err != nil { + return nil, err + } + return t, nil +} + func (s *Server) initRouter() (*gin.Engine, error) { if config.IsDebug() { gin.SetMode(gin.DebugMode) @@ -48,6 +97,33 @@ func (s *Server) initRouter() (*gin.Engine, error) { engine := gin.Default() + basePath, err := s.settingService.GetBasePath() + if err != nil { + return nil, err + } + engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{basePath + "panel/API/"}))) + + engine.Use(func(c *gin.Context) { + c.Set("base_path", basePath) + }) + + // set static files and template + if config.IsDebug() { + // for development + files, err := s.getHtmlFiles() + if err != nil { + return nil, err + } + engine.LoadHTMLFiles(files...) + } else { + // for production + template, err := s.getHtmlTemplate(engine.FuncMap) + if err != nil { + return nil, err + } + engine.SetHTMLTemplate(template) + } + subDomain, err := s.settingService.GetSubDomain() if err != nil { return nil, err diff --git a/sub/subController.go b/sub/subController.go index 9afbc8da..22c43f18 100644 --- a/sub/subController.go +++ b/sub/subController.go @@ -2,7 +2,10 @@ package sub import ( "encoding/base64" + "fmt" + "math" "net" + "strconv" "strings" "github.com/gin-gonic/gin" @@ -78,16 +81,45 @@ func (a *SUBController) subs(c *gin.Context) { for _, sub := range subs { result += sub + "\n" } + resultSlice := strings.Split(strings.TrimSpace(result), "\n") // Add headers c.Writer.Header().Set("Subscription-Userinfo", header) c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval) c.Writer.Header().Set("Profile-Title", subId) - if a.subEncrypt { - c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) + acceptHeader := c.GetHeader("Accept") + headerMap := parseHeaderString(header) + expireValue := headerMap["expire"] + upValue := formatBytes(headerMap["upload"], 2) + downValue := formatBytes(headerMap["download"], 2) + totalValue := formatBytes(headerMap["total"], 2) + + currentURL := "https://" + c.Request.Host + c.Request.RequestURI + + if strings.Contains(acceptHeader, "text/html") { + if a.subEncrypt { + c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) + } else { + c.HTML(200, "sub.html", gin.H{ + "result": resultSlice, + "total": totalValue, + "expire": expireValue, + "upload": upValue, + "download": downValue, + "totalByte": headerMap["total"], + "uploadByte": headerMap["upload"], + "downloadByte": headerMap["download"], + "sId": subId, + "subUrl": currentURL, + }) + } } else { - c.String(200, result) + if a.subEncrypt { + c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) + } else { + c.String(200, result) + } } } } @@ -132,3 +164,31 @@ func getHostFromXFH(s string) (string, error) { } return s, nil } + +func parseHeaderString(header string) map[string]string { + headerMap := make(map[string]string) + pairs := strings.Split(header, ";") + for _, pair := range pairs { + kv := strings.Split(strings.TrimSpace(pair), "=") + if len(kv) == 2 { + headerMap[kv[0]] = kv[1] + } + } + return headerMap +} + +func formatBytes(sizeStr string, precision int) string { + // Convert the string input to a float64 + size, _ := strconv.ParseFloat(sizeStr, 64) + + if size == 0 { + return "0 B" + } + + // Calculate base and suffix + base := math.Log(size) / math.Log(1024) + suffixes := []string{"B", "K", "M", "G", "T"} + + value := math.Pow(1024, base-math.Floor(base)) + return fmt.Sprintf("%.*f %s", precision, value, suffixes[int(math.Floor(base))]) +}