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 @@ +<!DOCTYPE html> +<html lang="fa" dir="rtl"> + +<head> + <meta charset="UTF-8"> + <meta name="renderer" content="webkit"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>{{ .sId }} - Sub Info</title> + <script src="https://unpkg.com/tailwindcss-cdn@3.4.10/tailwindcss-with-all-plugins.js"></script> + <link href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@33.003/Vazirmatn-Variable-font-face.css" + rel="stylesheet" type="text/css" /> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css" + integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A==" + crossorigin="anonymous" /> + <script src="https://cdn.jsdelivr.net/npm/jalaali-js/dist/jalaali.min.js"></script> + <script src="https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js" + integrity="sha256-25ncr0CpJhgbzkUiR3wu/Fkk9sSykRG2qX+upHfJUos=" crossorigin="anonymous"></script> +</head> + +<body class="flex items-center justify-center min-h-screen bg-gray-900 text-white font-[Vazirmatn] mr-4 ml-4"> +<div class="container text-center bg-gray-800 p-8 rounded-lg shadow-lg"> + <h1 class="text-2xl font-bold mb-4">اطلاعات سابسکریپشن</h1> + <canvas id="qrcode" class="rounded-md inline mt-2 mb-3"></canvas> + <div class="text-lg mb-2"><i class="fa-regular fa-id-badge"></i> شناسه اشتراک : {{ .sId }}</div> + <div class="mb-2"><i class="fa-solid fa-circle-info"></i> وضعیت اشتراک : <span id="status"></span></div> + <div class="mb-2"><i class=" fa-solid fa-download"></i> دانلود : {{ .download }}</div> + <div class="mb-2"><i class=" fa-solid fa-upload"></i> آپلود : {{ .upload }}</div> + <div class="mb-2"><i class="fa-regular fa-calendar"></i> تاریخ پایان : <span id="timestamp"></span></div> + <div class="mb-4"><i class="fa-regular fa-star"></i> حجم کلی : <span id="total"></span></div> + <div class="bg-gray-700 rounded-lg shadow-lg p-4 font-mono flow-text break-words"> + {{ range .result }} + <div class="text-gray-400 text-sm"> + <button onclick="copyToClipboard('{{ . }}')"><i class="fa-regular fa-copy"></i></button> + {{ . }} + </div> + {{ end }} + </div> + <script> + // Get the HTML element to display the timestamp. + const humanDateElement = document.getElementById('timestamp'); + // Convert timestamp to a human-readable date + const timestamp = parseInt("{{ .expire }}", 10) * 1000; // Parse and convert to milliseconds + const date = new Date(timestamp); + // Extract the Gregorian year, month, and day + const gregorianYear = date.getFullYear(); + const gregorianMonth = date.getMonth() + 1; // Months are 0-indexed + const gregorianDay = date.getDate(); + // Convert Gregorian to Jalali + const jalaliDate = jalaali.toJalaali(gregorianYear, gregorianMonth, gregorianDay); + // Format the Jalali date + const formattedJalaliDate = `${jalaliDate.jy}/${jalaliDate.jm}/${jalaliDate.jd}`; + // Display the Jalali date in the HTML + if ('{{ .expire }}' === '0') { + humanDateElement.textContent = 'بدون انقضا'; + } else { + humanDateElement.textContent = formattedJalaliDate; + } + // Get the HTML element to display the status. + const statusElement = document.getElementById('status'); + if (timestamp >= Date.now() && '{{ .downloadByte}}' + '{{ .uploadByte }}' <= '{{ .totalByte }}') { + statusElement.textContent = 'فعال'; + } else { + if ('{{ .totalByte }}' === '0') { + statusElement.textContent = 'نامحدود'; + } else { + statusElement.textContent = 'غیرفعال'; + } + } + if ('{{ .totalByte }}' === '0') { + document.getElementById('total').textContent = '∞'; + } else { + document.getElementById('total').textContent = '{{ .total }}'; + } + + function copyToClipboard(text) { + navigator.clipboard.writeText(text); + } + + (function () { + var qr = new QRious({ + element: document.getElementById('qrcode'), + value: '{{ .subUrl }}', + size: 250 + }); + })(); + </script> + <div class="mt-4 flex justify-center gap-4"> + <button class="flex items-center gap-2 px-3 py-3 bg-zinc-900 text-white rounded-lg hover:bg-zinc-600" + onclick="window.location.href='v2box://install-sub?url={{ .subUrl }}&name={{ .sId }}';"> + <span>افزودن به V2Box</span> + </button> + <button class="flex items-center gap-2 px-3 py-3 bg-violet-900 text-white rounded-lg hover:bg-violet-600" + onclick="window.location.href='streisand://import/{{ .subUrl }}';"> + <span>افزودن به Streisand</span> + </button> + <button class="flex items-center gap-2 px-3 py-3 bg-gray-100 text-black rounded-lg hover:bg-gray-500" + onclick="window.location.href='v2rayng://install-config?url={{ .subUrl }}';"> + <span>افزودن به V2RayNG</span> + </button> + </div> +</div> +</div> +</body> + +</html> 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))]) +}