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 }}
+
وضعیت اشتراک :
+
دانلود : {{ .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))])
+}