Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormhsanaei <ho3ein.sanaei@gmail.com>2025-09-14 02:22:42 +0300
committermhsanaei <ho3ein.sanaei@gmail.com>2025-09-14 19:56:31 +0300
commit10025ffa66c011fd756af780772260d833460795 (patch)
tree0cf2d3924f3a8c26d47c4d9fe20d2438b7c4b6fc /sub/subService.go
parent5ee62b25ca9a3bf0ce683adbba5b1b64ddea074e (diff)
Subscription
Diffstat (limited to 'sub/subService.go')
-rw-r--r--sub/subService.go203
1 files changed, 196 insertions, 7 deletions
diff --git a/sub/subService.go b/sub/subService.go
index e6e25e3a..485048fd 100644
--- a/sub/subService.go
+++ b/sub/subService.go
@@ -3,10 +3,15 @@ package sub
import (
"encoding/base64"
"fmt"
+ "net"
"net/url"
+ "strconv"
"strings"
"time"
+ "github.com/gin-gonic/gin"
+ "github.com/goccy/go-json"
+
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
@@ -14,8 +19,6 @@ import (
"x-ui/util/random"
"x-ui/web/service"
"x-ui/xray"
-
- "github.com/goccy/go-json"
)
type SubService struct {
@@ -34,19 +37,20 @@ func NewSubService(showInfo bool, remarkModel string) *SubService {
}
}
-func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
+func (s *SubService) GetSubs(subId string, host string) ([]string, string, int64, error) {
s.address = host
var result []string
var header string
var traffic xray.ClientTraffic
+ var lastOnline int64
var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId)
if err != nil {
- return nil, "", err
+ return nil, "", 0, err
}
if len(inbounds) == 0 {
- return nil, "", common.NewError("No inbounds found with ", subId)
+ return nil, "", 0, common.NewError("No inbounds found with ", subId)
}
s.datepicker, err = s.settingService.GetDatepicker()
@@ -73,7 +77,11 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
if client.Enable && client.SubID == subId {
link := s.getLink(inbound, client.Email)
result = append(result, link)
- clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
+ ct := s.getClientTraffics(inbound.ClientStats, client.Email)
+ clientTraffics = append(clientTraffics, ct)
+ if ct.LastOnline > lastOnline {
+ lastOnline = ct.LastOnline
+ }
}
}
}
@@ -101,7 +109,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
}
}
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
- return result, header, nil
+ return result, header, lastOnline, nil
}
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
@@ -1001,3 +1009,184 @@ func searchHost(headers any) string {
return ""
}
+
+// PageData is a view model for subscription.html
+type PageData struct {
+ Host string
+ BasePath string
+ SId string
+ Download string
+ Upload string
+ Total string
+ Used string
+ Remained string
+ Expire int64
+ LastOnline int64
+ Datepicker string
+ DownloadByte int64
+ UploadByte int64
+ TotalByte int64
+ SubUrl string
+ SubJsonUrl string
+ Result []string
+}
+
+// ResolveRequest extracts scheme and host info from request/headers consistently.
+func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) {
+ // scheme
+ scheme = "http"
+ if c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") {
+ scheme = "https"
+ }
+
+ // base host (no port)
+ if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil && h != "" {
+ host = h
+ }
+ if host == "" {
+ host = c.GetHeader("X-Real-IP")
+ }
+ if host == "" {
+ var err error
+ host, _, err = net.SplitHostPort(c.Request.Host)
+ if err != nil {
+ host = c.Request.Host
+ }
+ }
+
+ // host:port for URLs
+ hostWithPort = c.GetHeader("X-Forwarded-Host")
+ if hostWithPort == "" {
+ hostWithPort = c.Request.Host
+ }
+ if hostWithPort == "" {
+ hostWithPort = host
+ }
+
+ // header display host
+ hostHeader = c.GetHeader("X-Forwarded-Host")
+ if hostHeader == "" {
+ hostHeader = c.GetHeader("X-Real-IP")
+ }
+ if hostHeader == "" {
+ hostHeader = host
+ }
+ return
+}
+
+// BuildURLs constructs absolute subscription and json URLs.
+func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {
+ if strings.HasSuffix(subPath, "/") {
+ subURL = scheme + "://" + hostWithPort + subPath + subId
+ } else {
+ subURL = scheme + "://" + hostWithPort + strings.TrimRight(subPath, "/") + "/" + subId
+ }
+ if strings.HasSuffix(subJsonPath, "/") {
+ subJsonURL = scheme + "://" + hostWithPort + subJsonPath + subId
+ } else {
+ subJsonURL = scheme + "://" + hostWithPort + strings.TrimRight(subJsonPath, "/") + "/" + subId
+ }
+ return
+}
+
+// BuildPageData parses header and prepares the template view model.
+func (s *SubService) BuildPageData(subId, hostHeader, header string, lastOnline int64, subs []string, subURL, subJsonURL string) PageData {
+ // Parse header values
+ var uploadByte, downloadByte, totalByte, expire int64
+ parts := strings.Split(header, ";")
+ for _, p := range parts {
+ kv := strings.Split(strings.TrimSpace(p), "=")
+ if len(kv) != 2 {
+ continue
+ }
+ key := strings.ToLower(strings.TrimSpace(kv[0]))
+ val := strings.TrimSpace(kv[1])
+ switch key {
+ case "upload":
+ if v, err := parseInt64(val); err == nil {
+ uploadByte = v
+ }
+ case "download":
+ if v, err := parseInt64(val); err == nil {
+ downloadByte = v
+ }
+ case "total":
+ if v, err := parseInt64(val); err == nil {
+ totalByte = v
+ }
+ case "expire":
+ if v, err := parseInt64(val); err == nil {
+ expire = v
+ }
+ }
+ }
+
+ download := common.FormatTraffic(downloadByte)
+ upload := common.FormatTraffic(uploadByte)
+ total := "∞"
+ used := common.FormatTraffic(uploadByte + downloadByte)
+ remained := ""
+ if totalByte > 0 {
+ total = common.FormatTraffic(totalByte)
+ left := totalByte - (uploadByte + downloadByte)
+ if left < 0 {
+ left = 0
+ }
+ remained = common.FormatTraffic(left)
+ }
+
+ datepicker := s.datepicker
+ if datepicker == "" {
+ datepicker = "gregorian"
+ }
+
+ return PageData{
+ Host: hostHeader,
+ BasePath: "/",
+ SId: subId,
+ Download: download,
+ Upload: upload,
+ Total: total,
+ Used: used,
+ Remained: remained,
+ Expire: expire,
+ LastOnline: lastOnline,
+ Datepicker: datepicker,
+ DownloadByte: downloadByte,
+ UploadByte: uploadByte,
+ TotalByte: totalByte,
+ SubUrl: subURL,
+ SubJsonUrl: subJsonURL,
+ Result: subs,
+ }
+}
+
+func getHostFromXFH(s string) (string, error) {
+ if strings.Contains(s, ":") {
+ realHost, _, err := net.SplitHostPort(s)
+ if err != nil {
+ return "", err
+ }
+ return realHost, nil
+ }
+ return s, nil
+}
+
+func parseInt64(s string) (int64, error) {
+ // handle potential quotes
+ s = strings.Trim(s, "\"'")
+ n, err := strconv.ParseInt(s, 10, 64)
+ return n, err
+}
+
+// ApplyCommonHeaders sets standard subscription headers on the response writer.
+func (s *SubService) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
+ c.Writer.Header().Set("Subscription-Userinfo", header)
+ c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
+ c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))
+}
+
+// ApplyBase64ContentHeader adds the full subscription content as base64 header for convenience.
+func (s *SubService) ApplyBase64ContentHeader(c *gin.Context, content string) {
+ c.Writer.Header().Set("Subscription-Content-Base64", base64.StdEncoding.EncodeToString([]byte(content)))
+}