This commit is contained in:
Hassan Dashtizadeh 2025-03-08 14:42:05 +01:00 committed by GitHub
commit 7b062acd0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 245 additions and 4 deletions

105
sub/html/sub.html Normal file
View file

@ -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>

View file

@ -3,11 +3,14 @@ package sub
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"embed"
"html/template"
"io" "io"
"io/fs"
"net" "net"
"net/http" "net/http"
"os"
"strconv" "strconv"
"x-ui/config" "x-ui/config"
"x-ui/logger" "x-ui/logger"
"x-ui/util/common" "x-ui/util/common"
@ -15,9 +18,13 @@ import (
"x-ui/web/network" "x-ui/web/network"
"x-ui/web/service" "x-ui/web/service"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
//go:embed html/*
var htmlFS embed.FS
type Server struct { type Server struct {
httpServer *http.Server httpServer *http.Server
listener net.Listener 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) { func (s *Server) initRouter() (*gin.Engine, error) {
if config.IsDebug() { if config.IsDebug() {
gin.SetMode(gin.DebugMode) gin.SetMode(gin.DebugMode)
@ -48,6 +97,33 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default() 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() subDomain, err := s.settingService.GetSubDomain()
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -2,7 +2,10 @@ package sub
import ( import (
"encoding/base64" "encoding/base64"
"fmt"
"math"
"net" "net"
"strconv"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -78,18 +81,47 @@ func (a *SUBController) subs(c *gin.Context) {
for _, sub := range subs { for _, sub := range subs {
result += sub + "\n" result += sub + "\n"
} }
resultSlice := strings.Split(strings.TrimSpace(result), "\n")
// Add headers // Add headers
c.Writer.Header().Set("Subscription-Userinfo", header) c.Writer.Header().Set("Subscription-Userinfo", header)
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval) c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
c.Writer.Header().Set("Profile-Title", subId) c.Writer.Header().Set("Profile-Title", subId)
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 {
if a.subEncrypt { if a.subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
} else { } else {
c.String(200, result) c.String(200, result)
} }
} }
}
} }
func (a *SUBController) subJsons(c *gin.Context) { func (a *SUBController) subJsons(c *gin.Context) {
@ -132,3 +164,31 @@ func getHostFromXFH(s string) (string, error) {
} }
return s, nil 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))])
}