mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-04-19 21:42:24 +00:00
Merge 915fd27d61
into c6d27a4463
This commit is contained in:
commit
7b062acd0e
3 changed files with 245 additions and 4 deletions
105
sub/html/sub.html
Normal file
105
sub/html/sub.html
Normal 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>
|
78
sub/sub.go
78
sub/sub.go
|
@ -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
|
||||||
|
|
|
@ -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))])
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue