Files
trihuy-russian/sub/subService.go
T

1491 lines
42 KiB
Go
Raw Normal View History

2023-05-22 18:06:34 +03:30
package sub
2023-04-09 23:13:18 +03:30
import (
"encoding/base64"
"fmt"
2026-04-26 23:04:47 +02:00
"maps"
2025-09-14 01:22:42 +02:00
"net"
2023-04-09 23:13:18 +03:30
"net/url"
2026-04-26 23:04:47 +02:00
"slices"
2023-04-09 23:13:18 +03:30
"strings"
2023-07-18 03:19:01 +03:30
"time"
2024-03-11 01:01:24 +03:30
2025-09-14 01:22:42 +02:00
"github.com/gin-gonic/gin"
"github.com/goccy/go-json"
2025-09-19 10:05:43 +02:00
"github.com/mhsanaei/3x-ui/v2/database"
"github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/util/common"
"github.com/mhsanaei/3x-ui/v2/util/random"
"github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/mhsanaei/3x-ui/v2/xray"
2023-04-09 23:13:18 +03:30
)
2025-09-20 09:35:50 +02:00
// SubService provides business logic for generating subscription links and managing subscription data.
2023-04-09 23:13:18 +03:30
type SubService struct {
address string
2023-08-26 15:11:12 +03:30
showInfo bool
2023-12-08 20:31:17 +01:00
remarkModel string
2024-01-02 09:32:21 +01:00
datepicker string
2023-05-22 18:06:34 +03:30
inboundService service.InboundService
2023-12-05 23:09:08 +01:00
settingService service.SettingService
2023-04-09 23:13:18 +03:30
}
2025-09-20 09:35:50 +02:00
// NewSubService creates a new subscription service with the given configuration.
2024-02-21 14:17:52 +03:30
func NewSubService(showInfo bool, remarkModel string) *SubService {
return &SubService{
showInfo: showInfo,
remarkModel: remarkModel,
}
}
2025-09-20 09:35:50 +02:00
// GetSubs retrieves subscription links for a given subscription ID and host.
2025-09-14 23:08:09 +02:00
func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.ClientTraffic, error) {
2023-04-09 23:13:18 +03:30
s.address = host
var result []string
2023-04-18 21:34:06 +03:30
var traffic xray.ClientTraffic
2025-09-14 01:22:42 +02:00
var lastOnline int64
2026-05-04 23:27:57 +02:00
var hasEnabledClient bool
2023-04-18 21:34:06 +03:30
var clientTraffics []xray.ClientTraffic
2023-04-09 23:13:18 +03:30
inbounds, err := s.getInboundsBySubId(subId)
if err != nil {
2025-09-14 23:08:09 +02:00
return nil, 0, traffic, err
2023-04-09 23:13:18 +03:30
}
2024-02-21 14:17:52 +03:30
2024-03-21 09:51:12 +03:00
if len(inbounds) == 0 {
2025-09-14 23:08:09 +02:00
return nil, 0, traffic, common.NewError("No inbounds found with ", subId)
2024-03-21 09:51:12 +03:00
}
2024-02-21 14:17:52 +03:30
s.datepicker, err = s.settingService.GetDatepicker()
2023-12-08 20:31:17 +01:00
if err != nil {
2024-02-21 14:17:52 +03:30
s.datepicker = "gregorian"
2023-12-08 20:31:17 +01:00
}
2023-04-09 23:13:18 +03:30
for _, inbound := range inbounds {
2023-05-22 18:06:34 +03:30
clients, err := s.inboundService.GetClients(inbound)
2023-04-09 23:13:18 +03:30
if err != nil {
2024-02-21 14:17:52 +03:30
logger.Error("SubService - GetClients: Unable to get clients from inbound")
2023-04-09 23:13:18 +03:30
}
if clients == nil {
continue
}
2023-05-23 03:15:34 +03:30
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
2024-02-21 14:17:52 +03:30
listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
2023-05-23 03:15:34 +03:30
if err == nil {
2024-02-21 14:17:52 +03:30
inbound.Listen = listen
inbound.Port = port
inbound.StreamSettings = streamSettings
2023-05-23 03:15:34 +03:30
}
}
2023-04-09 23:13:18 +03:30
for _, client := range clients {
if client.SubID == subId {
2026-05-04 23:27:57 +02:00
if client.Enable {
hasEnabledClient = true
}
2023-08-26 15:11:12 +03:30
link := s.getLink(inbound, client.Email)
2023-04-09 23:13:18 +03:30
result = append(result, link)
2025-09-14 01:22:42 +02:00
ct := s.getClientTraffics(inbound.ClientStats, client.Email)
clientTraffics = append(clientTraffics, ct)
if ct.LastOnline > lastOnline {
lastOnline = ct.LastOnline
}
2023-04-18 21:34:06 +03:30
}
}
}
2024-02-21 14:17:52 +03:30
// Prepare statistics
2023-04-18 21:34:06 +03:30
for index, clientTraffic := range clientTraffics {
if index == 0 {
traffic.Up = clientTraffic.Up
traffic.Down = clientTraffic.Down
traffic.Total = clientTraffic.Total
if clientTraffic.ExpiryTime > 0 {
traffic.ExpiryTime = clientTraffic.ExpiryTime
}
} else {
traffic.Up += clientTraffic.Up
traffic.Down += clientTraffic.Down
if traffic.Total == 0 || clientTraffic.Total == 0 {
traffic.Total = 0
} else {
traffic.Total += clientTraffic.Total
}
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
traffic.ExpiryTime = 0
2023-04-09 23:13:18 +03:30
}
}
}
2026-05-04 23:27:57 +02:00
traffic.Enable = hasEnabledClient
2025-09-14 23:08:09 +02:00
return result, lastOnline, traffic, nil
2023-04-09 23:13:18 +03:30
}
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
db := database.GetDB()
var inbounds []*model.Inbound
// allow "hysteria2" so imports stored with the literal v2 protocol
// string still surface here (#4081)
2023-12-08 18:45:21 +01:00
err := db.Model(model.Inbound{}).Preload("ClientStats").Where(`id in (
SELECT DISTINCT inbounds.id
FROM inbounds,
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
2023-12-08 18:45:21 +01:00
WHERE
protocol in ('vmess','vless','trojan','shadowsocks','hysteria','hysteria2')
2023-12-08 18:45:21 +01:00
AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ?
)`, subId, true).Find(&inbounds).Error
2023-05-17 01:07:35 +03:30
if err != nil {
2023-04-09 23:13:18 +03:30
return nil, err
}
return inbounds, nil
}
2023-04-18 21:34:06 +03:30
func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic {
for _, traffic := range traffics {
if traffic.Email == email {
return traffic
}
}
return xray.ClientTraffic{}
}
2024-02-21 14:17:52 +03:30
func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) {
2023-05-23 03:15:34 +03:30
db := database.GetDB()
var inbound *model.Inbound
err := db.Model(model.Inbound{}).
Where("JSON_TYPE(settings, '$.fallbacks') = 'array'").
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
Find(&inbound).Error
if err != nil {
2024-02-21 14:17:52 +03:30
return "", 0, "", err
2023-05-23 03:15:34 +03:30
}
2024-02-21 14:17:52 +03:30
2025-03-12 20:13:51 +01:00
var stream map[string]any
2024-02-21 14:17:52 +03:30
json.Unmarshal([]byte(streamSettings), &stream)
2025-03-12 20:13:51 +01:00
var masterStream map[string]any
2024-02-21 14:17:52 +03:30
json.Unmarshal([]byte(inbound.StreamSettings), &masterStream)
stream["security"] = masterStream["security"]
stream["tlsSettings"] = masterStream["tlsSettings"]
stream["externalProxy"] = masterStream["externalProxy"]
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
return inbound.Listen, inbound.Port, string(modifiedStream), nil
2023-05-23 03:15:34 +03:30
}
2023-08-26 15:11:12 +03:30
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
2023-04-09 23:13:18 +03:30
switch inbound.Protocol {
case "vmess":
2023-08-26 15:11:12 +03:30
return s.genVmessLink(inbound, email)
2023-04-09 23:13:18 +03:30
case "vless":
2023-08-26 15:11:12 +03:30
return s.genVlessLink(inbound, email)
2023-04-09 23:13:18 +03:30
case "trojan":
2023-08-26 15:11:12 +03:30
return s.genTrojanLink(inbound, email)
2023-05-06 20:21:14 +03:30
case "shadowsocks":
2023-08-26 15:11:12 +03:30
return s.genShadowsocksLink(inbound, email)
case "hysteria", "hysteria2":
2026-04-20 16:05:27 +02:00
return s.genHysteriaLink(inbound, email)
2023-04-09 23:13:18 +03:30
}
return ""
}
2026-04-26 23:04:47 +02:00
// Protocol link generators are intentionally ordered as:
// vmess -> vless -> trojan -> shadowsocks -> hysteria.
2023-08-26 15:11:12 +03:30
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
if inbound.Protocol != model.VMESS {
2023-04-09 23:13:18 +03:30
return ""
}
2026-04-26 23:04:47 +02:00
address := s.resolveInboundAddress(inbound)
2025-03-12 20:13:51 +01:00
obj := map[string]any{
2023-04-28 00:15:06 +03:30
"v": "2",
"add": address,
2023-04-28 00:15:06 +03:30
"port": inbound.Port,
"type": "none",
}
2026-04-26 23:04:47 +02:00
stream := unmarshalStreamSettings(inbound.StreamSettings)
2023-04-09 23:13:18 +03:30
network, _ := stream["network"].(string)
2026-04-26 23:04:47 +02:00
applyVmessNetworkParams(stream, network, obj)
if finalmask, ok := stream["finalmask"].(map[string]any); ok {
applyFinalMaskObj(finalmask, obj)
2023-04-09 23:13:18 +03:30
}
security, _ := stream["security"].(string)
2023-04-28 00:15:06 +03:30
obj["tls"] = security
2023-04-09 23:13:18 +03:30
if security == "tls" {
2026-04-26 23:04:47 +02:00
applyVmessTLSParams(stream, obj)
2023-04-09 23:13:18 +03:30
}
2023-05-22 18:06:34 +03:30
clients, _ := s.inboundService.GetClients(inbound)
2026-04-26 23:04:47 +02:00
clientIndex := findClientIndex(clients, email)
2023-04-28 00:15:06 +03:30
obj["id"] = clients[clientIndex].ID
obj["scy"] = clients[clientIndex].Security
2023-04-09 23:13:18 +03:30
2025-03-12 20:13:51 +01:00
externalProxies, _ := stream["externalProxy"].([]any)
2023-12-08 18:45:21 +01:00
if len(externalProxies) > 0 {
2026-04-26 23:04:47 +02:00
return s.buildVmessExternalProxyLinks(externalProxies, obj, inbound, email)
2023-05-22 18:06:34 +03:30
}
2023-08-26 15:11:12 +03:30
obj["ps"] = s.genRemark(inbound, email, "")
2026-04-26 23:04:47 +02:00
return buildVmessLink(obj)
2023-04-09 23:13:18 +03:30
}
2023-08-26 15:11:12 +03:30
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
2023-04-09 23:13:18 +03:30
if inbound.Protocol != model.VLESS {
return ""
}
2026-04-26 23:04:47 +02:00
address := s.resolveInboundAddress(inbound)
stream := unmarshalStreamSettings(inbound.StreamSettings)
2023-05-22 18:06:34 +03:30
clients, _ := s.inboundService.GetClients(inbound)
2026-04-26 23:04:47 +02:00
clientIndex := findClientIndex(clients, email)
2023-04-09 23:13:18 +03:30
uuid := clients[clientIndex].ID
port := inbound.Port
streamNetwork := stream["network"].(string)
params := make(map[string]string)
params["type"] = streamNetwork
2025-09-20 11:11:30 +02:00
// Add encryption parameter for VLESS from inbound settings
var settings map[string]any
json.Unmarshal([]byte(inbound.Settings), &settings)
if encryption, ok := settings["encryption"].(string); ok {
params["encryption"] = encryption
}
2026-04-26 23:04:47 +02:00
applyShareNetworkParams(stream, streamNetwork, params)
if finalmask, ok := stream["finalmask"].(map[string]any); ok {
applyFinalMaskParams(finalmask, params)
2023-04-09 23:13:18 +03:30
}
security, _ := stream["security"].(string)
2026-04-26 23:04:47 +02:00
switch security {
case "tls":
applyShareTLSParams(stream, params)
2023-04-09 23:13:18 +03:30
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
2026-04-26 23:04:47 +02:00
case "reality":
applyShareRealityParams(stream, params)
2023-04-11 15:40:45 +03:30
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
2026-04-26 23:04:47 +02:00
default:
2023-06-14 17:06:56 +03:30
params["security"] = "none"
}
2025-03-12 20:13:51 +01:00
externalProxies, _ := stream["externalProxy"].([]any)
2023-04-09 23:13:18 +03:30
2023-12-08 18:45:21 +01:00
if len(externalProxies) > 0 {
2026-04-26 23:04:47 +02:00
return s.buildExternalProxyURLLinks(
externalProxies,
params,
security,
func(dest string, port int) string {
return fmt.Sprintf("vless://%s@%s:%d", uuid, dest, port)
},
func(ep map[string]any) string {
return s.genRemark(inbound, email, ep["remark"].(string))
},
)
2023-05-22 18:06:34 +03:30
}
2023-08-26 15:11:12 +03:30
2023-12-08 18:45:21 +01:00
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
2026-04-26 23:04:47 +02:00
return buildLinkWithParams(link, params, s.genRemark(inbound, email, ""))
2023-04-09 23:13:18 +03:30
}
2023-08-26 15:11:12 +03:30
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
2023-04-09 23:13:18 +03:30
if inbound.Protocol != model.Trojan {
return ""
}
2026-04-26 23:04:47 +02:00
address := s.resolveInboundAddress(inbound)
stream := unmarshalStreamSettings(inbound.StreamSettings)
2023-05-22 18:06:34 +03:30
clients, _ := s.inboundService.GetClients(inbound)
2026-04-26 23:04:47 +02:00
clientIndex := findClientIndex(clients, email)
2023-04-09 23:13:18 +03:30
password := clients[clientIndex].Password
port := inbound.Port
streamNetwork := stream["network"].(string)
params := make(map[string]string)
params["type"] = streamNetwork
2026-04-26 23:04:47 +02:00
applyShareNetworkParams(stream, streamNetwork, params)
if finalmask, ok := stream["finalmask"].(map[string]any); ok {
applyFinalMaskParams(finalmask, params)
2023-04-09 23:13:18 +03:30
}
security, _ := stream["security"].(string)
2026-04-26 23:04:47 +02:00
switch security {
case "tls":
applyShareTLSParams(stream, params)
case "reality":
applyShareRealityParams(stream, params)
2023-04-12 10:44:07 +02:00
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
2023-04-09 23:13:18 +03:30
}
2026-04-26 23:04:47 +02:00
default:
2023-06-14 17:06:56 +03:30
params["security"] = "none"
}
2025-03-12 20:13:51 +01:00
externalProxies, _ := stream["externalProxy"].([]any)
2023-04-09 23:13:18 +03:30
2023-12-08 18:45:21 +01:00
if len(externalProxies) > 0 {
2026-04-26 23:04:47 +02:00
return s.buildExternalProxyURLLinks(
externalProxies,
params,
security,
func(dest string, port int) string {
return fmt.Sprintf("trojan://%s@%s:%d", password, dest, port)
},
func(ep map[string]any) string {
return s.genRemark(inbound, email, ep["remark"].(string))
},
)
2023-05-22 18:06:34 +03:30
}
2023-12-08 18:45:21 +01:00
link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
2026-04-26 23:04:47 +02:00
return buildLinkWithParams(link, params, s.genRemark(inbound, email, ""))
2023-04-09 23:13:18 +03:30
}
2023-08-26 15:11:12 +03:30
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
2023-05-06 20:21:14 +03:30
if inbound.Protocol != model.Shadowsocks {
return ""
}
2026-04-26 23:04:47 +02:00
address := s.resolveInboundAddress(inbound)
stream := unmarshalStreamSettings(inbound.StreamSettings)
2023-05-22 18:06:34 +03:30
clients, _ := s.inboundService.GetClients(inbound)
2023-05-06 20:21:14 +03:30
2025-03-12 20:13:51 +01:00
var settings map[string]any
2023-05-06 20:21:14 +03:30
json.Unmarshal([]byte(inbound.Settings), &settings)
inboundPassword := settings["password"].(string)
method := settings["method"].(string)
2026-04-26 23:04:47 +02:00
clientIndex := findClientIndex(clients, email)
2023-07-18 03:19:01 +03:30
streamNetwork := stream["network"].(string)
params := make(map[string]string)
params["type"] = streamNetwork
2026-04-26 23:04:47 +02:00
applyShareNetworkParams(stream, streamNetwork, params)
if finalmask, ok := stream["finalmask"].(map[string]any); ok {
applyFinalMaskParams(finalmask, params)
2023-07-18 03:19:01 +03:30
}
2023-12-08 18:45:21 +01:00
security, _ := stream["security"].(string)
if security == "tls" {
2026-04-26 23:04:47 +02:00
applyShareTLSParams(stream, params)
2023-12-08 18:45:21 +01:00
}
2023-07-27 11:58:12 +03:30
encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password)
if method[0] == '2' {
encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
}
2023-12-08 18:45:21 +01:00
2025-03-12 20:13:51 +01:00
externalProxies, _ := stream["externalProxy"].([]any)
2023-12-08 18:45:21 +01:00
if len(externalProxies) > 0 {
2026-04-26 23:04:47 +02:00
proxyParams := cloneStringMap(params)
proxyParams["security"] = security
return s.buildExternalProxyURLLinks(
externalProxies,
proxyParams,
security,
func(dest string, port int) string {
return fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), dest, port)
},
func(ep map[string]any) string {
return s.genRemark(inbound, email, ep["remark"].(string))
},
)
2023-12-08 18:45:21 +01:00
}
2023-07-18 03:19:01 +03:30
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
2026-04-26 23:04:47 +02:00
return buildLinkWithParams(link, params, s.genRemark(inbound, email, ""))
2023-08-26 15:11:12 +03:30
}
2023-07-18 03:19:01 +03:30
2026-04-20 16:05:27 +02:00
func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) string {
if !model.IsHysteria(inbound.Protocol) {
2026-04-20 16:05:27 +02:00
return ""
}
2026-04-26 23:04:47 +02:00
var stream map[string]any
2026-04-20 16:05:27 +02:00
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
clientIndex = i
break
}
}
auth := clients[clientIndex].Auth
params := make(map[string]string)
params["security"] = "tls"
2026-04-26 23:04:47 +02:00
tlsSetting, _ := stream["tlsSettings"].(map[string]any)
alpns, _ := tlsSetting["alpn"].([]any)
2026-04-20 16:05:27 +02:00
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["insecure"] = "1"
}
}
}
// salamander obfs (Hysteria2). The panel-side link generator already
// emits these; keep the subscription output in sync so a client has
// the obfs password to match the server.
2026-04-26 23:04:47 +02:00
if finalmask, ok := stream["finalmask"].(map[string]any); ok {
applyFinalMaskParams(finalmask, params)
if udpMasks, ok := finalmask["udp"].([]any); ok {
for _, m := range udpMasks {
2026-04-26 23:04:47 +02:00
mask, _ := m.(map[string]any)
if mask == nil || mask["type"] != "salamander" {
continue
}
2026-04-26 23:04:47 +02:00
settings, _ := mask["settings"].(map[string]any)
if pw, ok := settings["password"].(string); ok && pw != "" {
params["obfs"] = "salamander"
params["obfs-password"] = pw
break
}
}
}
}
2026-04-26 23:04:47 +02:00
var settings map[string]any
2026-04-20 16:05:27 +02:00
json.Unmarshal([]byte(inbound.Settings), &settings)
version, _ := settings["version"].(float64)
protocol := "hysteria2"
if int(version) == 1 {
protocol = "hysteria"
}
// Fan out one link per External Proxy entry if any. Previously this
// generator ignored `externalProxy` entirely, so the link kept the
// server's own IP/port even when the admin configured an alternate
// endpoint (e.g. a CDN hostname + port that forwards to the node).
// Matches the behaviour of genVlessLink / genTrojanLink / ….
2026-04-26 23:04:47 +02:00
externalProxies, _ := stream["externalProxy"].([]any)
if len(externalProxies) > 0 {
links := make([]string, 0, len(externalProxies))
for _, externalProxy := range externalProxies {
2026-04-26 23:04:47 +02:00
ep, ok := externalProxy.(map[string]any)
if !ok {
continue
}
dest, _ := ep["dest"].(string)
portF, okPort := ep["port"].(float64)
if dest == "" || !okPort {
continue
}
epRemark, _ := ep["remark"].(string)
link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, dest, int(portF))
u, _ := url.Parse(link)
q := u.Query()
for k, v := range params {
q.Add(k, v)
}
u.RawQuery = q.Encode()
u.Fragment = s.genRemark(inbound, email, epRemark)
links = append(links, u.String())
}
return strings.Join(links, "\n")
}
// No external proxy configured — fall back to the request host.
link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, s.address, inbound.Port)
2026-04-20 16:05:27 +02:00
url, _ := url.Parse(link)
q := url.Query()
for k, v := range params {
q.Add(k, v)
}
url.RawQuery = q.Encode()
url.Fragment = s.genRemark(inbound, email, "")
return url.String()
}
2026-04-26 23:04:47 +02:00
func (s *SubService) resolveInboundAddress(inbound *model.Inbound) string {
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
return s.address
}
return inbound.Listen
}
func findClientIndex(clients []model.Client, email string) int {
for i, client := range clients {
if client.Email == email {
return i
}
}
return -1
}
func unmarshalStreamSettings(streamSettings string) map[string]any {
var stream map[string]any
json.Unmarshal([]byte(streamSettings), &stream)
return stream
}
func applyPathAndHostParams(settings map[string]any, params map[string]string) {
params["path"] = settings["path"].(string)
if host, ok := settings["host"].(string); ok && len(host) > 0 {
params["host"] = host
} else {
headers, _ := settings["headers"].(map[string]any)
params["host"] = searchHost(headers)
}
}
func applyPathAndHostObj(settings map[string]any, obj map[string]any) {
obj["path"] = settings["path"].(string)
if host, ok := settings["host"].(string); ok && len(host) > 0 {
obj["host"] = host
} else {
headers, _ := settings["headers"].(map[string]any)
obj["host"] = searchHost(headers)
}
}
func applyShareNetworkParams(stream map[string]any, streamNetwork string, params map[string]string) {
switch streamNetwork {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]any)
header, _ := tcp["header"].(map[string]any)
typeStr, _ := header["type"].(string)
if typeStr == "http" {
request := header["request"].(map[string]any)
requestPath, _ := request["path"].([]any)
params["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]any)
params["host"] = searchHost(headers)
params["headerType"] = "http"
}
case "kcp":
applyKcpShareParams(stream, params)
case "ws":
ws, _ := stream["wsSettings"].(map[string]any)
applyPathAndHostParams(ws, params)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]any)
params["serviceName"] = grpc["serviceName"].(string)
params["authority"], _ = grpc["authority"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
case "httpupgrade":
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any)
applyPathAndHostParams(httpupgrade, params)
case "xhttp":
xhttp, _ := stream["xhttpSettings"].(map[string]any)
applyPathAndHostParams(xhttp, params)
params["mode"], _ = xhttp["mode"].(string)
applyXhttpPaddingParams(xhttp, params)
}
}
func applyXhttpPaddingObj(xhttp map[string]any, obj map[string]any) {
// VMess base64 JSON supports arbitrary keys; copy the padding
// settings through so clients can match the server's xhttp
// xPaddingBytes range and, when the admin opted into obfs
// mode, the custom key / header / placement / method.
if xpb, ok := xhttp["xPaddingBytes"].(string); ok && len(xpb) > 0 {
obj["x_padding_bytes"] = xpb
}
if obfs, ok := xhttp["xPaddingObfsMode"].(bool); ok && obfs {
obj["xPaddingObfsMode"] = true
for _, field := range []string{"xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"} {
if v, ok := xhttp[field].(string); ok && len(v) > 0 {
obj[field] = v
}
}
}
}
func applyVmessNetworkParams(stream map[string]any, network string, obj map[string]any) {
obj["net"] = network
switch network {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]any)
header, _ := tcp["header"].(map[string]any)
typeStr, _ := header["type"].(string)
obj["type"] = typeStr
if typeStr == "http" {
request := header["request"].(map[string]any)
requestPath, _ := request["path"].([]any)
obj["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]any)
obj["host"] = searchHost(headers)
}
case "kcp":
applyKcpShareObj(stream, obj)
case "ws":
ws, _ := stream["wsSettings"].(map[string]any)
applyPathAndHostObj(ws, obj)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]any)
obj["path"] = grpc["serviceName"].(string)
obj["authority"] = grpc["authority"].(string)
if grpc["multiMode"].(bool) {
obj["type"] = "multi"
}
case "httpupgrade":
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any)
applyPathAndHostObj(httpupgrade, obj)
case "xhttp":
xhttp, _ := stream["xhttpSettings"].(map[string]any)
applyPathAndHostObj(xhttp, obj)
obj["mode"], _ = xhttp["mode"].(string)
applyXhttpPaddingObj(xhttp, obj)
}
}
func applyShareTLSParams(stream map[string]any, params map[string]string) {
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]any)
alpns, _ := tlsSetting["alpn"].([]any)
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
}
}
func applyVmessTLSParams(stream map[string]any, obj map[string]any) {
tlsSetting, _ := stream["tlsSettings"].(map[string]any)
alpns, _ := tlsSetting["alpn"].([]any)
if len(alpns) > 0 {
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
obj["alpn"] = strings.Join(alpn, ",")
}
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
obj["sni"], _ = sniValue.(string)
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
obj["fp"], _ = fpValue.(string)
}
}
}
func applyShareRealityParams(stream map[string]any, params map[string]string) {
params["security"] = "reality"
realitySetting, _ := stream["realitySettings"].(map[string]any)
realitySettings, _ := searchKey(realitySetting, "settings")
if realitySetting != nil {
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
sNames, _ := sniValue.([]any)
params["sni"] = sNames[random.Num(len(sNames))].(string)
}
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
params["pbk"], _ = pbkValue.(string)
}
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
shortIds, _ := sidValue.([]any)
params["sid"] = shortIds[random.Num(len(shortIds))].(string)
}
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
params["fp"] = fp
}
}
if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok {
if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 {
params["pqv"] = pqv
}
}
params["spx"] = "/" + random.Seq(15)
}
}
func buildVmessLink(obj map[string]any) string {
jsonStr, _ := json.MarshalIndent(obj, "", " ")
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
}
func cloneVmessShareObj(baseObj map[string]any, newSecurity string) map[string]any {
newObj := map[string]any{}
for key, value := range baseObj {
if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp")) {
newObj[key] = value
}
}
return newObj
}
func (s *SubService) buildVmessExternalProxyLinks(externalProxies []any, baseObj map[string]any, inbound *model.Inbound, email string) string {
var links strings.Builder
for index, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]any)
newSecurity, _ := ep["forceTls"].(string)
newObj := cloneVmessShareObj(baseObj, newSecurity)
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string))
newObj["add"] = ep["dest"].(string)
newObj["port"] = int(ep["port"].(float64))
if newSecurity != "same" {
newObj["tls"] = newSecurity
}
if index > 0 {
links.WriteString("\n")
}
links.WriteString(buildVmessLink(newObj))
}
return links.String()
}
func buildLinkWithParams(link string, params map[string]string, fragment string) string {
parsedURL, _ := url.Parse(link)
q := parsedURL.Query()
for k, v := range params {
q.Add(k, v)
}
parsedURL.RawQuery = q.Encode()
parsedURL.Fragment = fragment
return parsedURL.String()
}
func buildLinkWithParamsAndSecurity(link string, params map[string]string, fragment, security string, omitTLSFields bool) string {
parsedURL, _ := url.Parse(link)
q := parsedURL.Query()
for k, v := range params {
if k == "security" {
v = security
}
if omitTLSFields && (k == "alpn" || k == "sni" || k == "fp") {
continue
}
q.Add(k, v)
}
parsedURL.RawQuery = q.Encode()
parsedURL.Fragment = fragment
return parsedURL.String()
}
func (s *SubService) buildExternalProxyURLLinks(
externalProxies []any,
params map[string]string,
baseSecurity string,
makeLink func(dest string, port int) string,
makeRemark func(ep map[string]any) string,
) string {
links := make([]string, 0, len(externalProxies))
for _, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]any)
newSecurity, _ := ep["forceTls"].(string)
dest, _ := ep["dest"].(string)
port := int(ep["port"].(float64))
securityToApply := baseSecurity
if newSecurity != "same" {
securityToApply = newSecurity
}
links = append(
links,
buildLinkWithParamsAndSecurity(
makeLink(dest, port),
params,
makeRemark(ep),
securityToApply,
newSecurity == "none",
),
)
}
return strings.Join(links, "\n")
}
func cloneStringMap(source map[string]string) map[string]string {
cloned := make(map[string]string, len(source))
maps.Copy(cloned, source)
return cloned
}
2023-08-26 15:11:12 +03:30
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
2023-12-08 20:31:17 +01:00
separationChar := string(s.remarkModel[0])
orderChars := s.remarkModel[1:]
orders := map[byte]string{
'i': "",
'e': "",
'o': "",
}
2023-08-26 15:11:12 +03:30
if len(email) > 0 {
2023-12-08 20:31:17 +01:00
orders['e'] = email
}
if len(inbound.Remark) > 0 {
orders['i'] = inbound.Remark
}
if len(extra) > 0 {
2023-12-10 20:43:48 +03:30
orders['o'] = extra
2023-12-08 20:31:17 +01:00
}
var remark []string
for i := 0; i < len(orderChars); i++ {
char := orderChars[i]
order, exists := orders[char]
if exists && order != "" {
remark = append(remark, order)
2023-08-26 15:11:12 +03:30
}
}
2023-08-26 15:11:12 +03:30
if s.showInfo {
statsExist := false
var stats xray.ClientTraffic
for _, clientStat := range inbound.ClientStats {
if clientStat.Email == email {
stats = clientStat
statsExist = true
break
}
}
// Get remained days
if statsExist {
if !stats.Enable {
2023-12-08 20:31:17 +01:00
return fmt.Sprintf("⛔️N/A%s%s", separationChar, strings.Join(remark, separationChar))
2023-08-26 15:11:12 +03:30
}
if vol := stats.Total - (stats.Up + stats.Down); vol > 0 {
remark = append(remark, fmt.Sprintf("%s%s", common.FormatTraffic(vol), "📊"))
}
now := time.Now().Unix()
switch exp := stats.ExpiryTime / 1000; {
case exp > 0:
2024-07-01 19:11:40 +02:00
remainingSeconds := exp - now
days := remainingSeconds / 86400
hours := (remainingSeconds % 86400) / 3600
minutes := (remainingSeconds % 3600) / 60
if days > 0 {
if hours > 0 {
remark = append(remark, fmt.Sprintf("%dD,%dH⏳", days, hours))
} else {
remark = append(remark, fmt.Sprintf("%dD⏳", days))
}
} else if hours > 0 {
remark = append(remark, fmt.Sprintf("%dH⏳", hours))
} else {
remark = append(remark, fmt.Sprintf("%dM⏳", minutes))
}
2023-08-26 15:11:12 +03:30
case exp < 0:
days := exp / -86400
hours := (exp % -86400) / 3600
minutes := (exp % -3600) / 60
2024-07-01 19:11:40 +02:00
if days > 0 {
if hours > 0 {
remark = append(remark, fmt.Sprintf("%dD,%dH⏳", days, hours))
} else {
remark = append(remark, fmt.Sprintf("%dD⏳", days))
}
} else if hours > 0 {
remark = append(remark, fmt.Sprintf("%dH⏳", hours))
} else {
remark = append(remark, fmt.Sprintf("%dM⏳", minutes))
}
2023-08-26 15:11:12 +03:30
}
}
}
2023-12-08 20:31:17 +01:00
return strings.Join(remark, separationChar)
2023-05-06 20:21:14 +03:30
}
2025-03-12 20:13:51 +01:00
func searchKey(data any, key string) (any, bool) {
2023-04-09 23:13:18 +03:30
switch val := data.(type) {
2025-03-12 20:13:51 +01:00
case map[string]any:
2023-04-09 23:13:18 +03:30
for k, v := range val {
if k == key {
return v, true
}
if result, ok := searchKey(v, key); ok {
return result, true
}
}
2025-03-12 20:13:51 +01:00
case []any:
2023-04-09 23:13:18 +03:30
for _, v := range val {
if result, ok := searchKey(v, key); ok {
return result, true
}
}
}
return nil, false
}
// applyXhttpPaddingParams copies the xPadding* fields from an xhttpSettings
// map into the URL query params of a vless:// / trojan:// / ss:// link.
//
// Before this helper existed, only path / host / mode were propagated,
// so a server configured with a non-default xPaddingBytes (e.g. 80-600)
// or with xPaddingObfsMode=true + custom xPaddingKey / xPaddingHeader
// would silently diverge from the client: the client kept defaults,
// hit the server, and was rejected by its padding validation
// ("invalid padding" in the inbound log) — the client-visible symptom
// was "xhttp doesn't connect" on OpenWRT / sing-box.
//
// Two encodings are written so every popular client can read at least one:
//
// - x_padding_bytes=<range> — flat param, understood by sing-box and its
// derivatives (Podkop, OpenWRT sing-box, Karing, NekoBox, …).
// - extra=<url-encoded-json> — full xhttp settings blob, which is how
// xray-core clients (v2rayNG, Happ, Furious, Exclave, …) pick up the
// obfs-mode key / header / placement / method.
//
// Anything that doesn't map to a non-empty value is skipped, so simple
// inbounds (no custom padding) produce exactly the same URL as before.
func applyXhttpPaddingParams(xhttp map[string]any, params map[string]string) {
if xhttp == nil {
return
}
if xpb, ok := xhttp["xPaddingBytes"].(string); ok && len(xpb) > 0 {
params["x_padding_bytes"] = xpb
}
extra := map[string]any{}
if xpb, ok := xhttp["xPaddingBytes"].(string); ok && len(xpb) > 0 {
extra["xPaddingBytes"] = xpb
}
if obfs, ok := xhttp["xPaddingObfsMode"].(bool); ok && obfs {
extra["xPaddingObfsMode"] = true
// The obfs-mode-only fields: only populate the ones the admin
// actually set, so xray-core falls back to its own defaults for
// the rest instead of seeing spurious empty strings.
for _, field := range []string{"xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"} {
if v, ok := xhttp[field].(string); ok && len(v) > 0 {
extra[field] = v
}
}
}
if len(extra) > 0 {
if b, err := json.Marshal(extra); err == nil {
params["extra"] = string(b)
}
}
}
2026-04-26 23:04:47 +02:00
var kcpMaskToHeaderType = map[string]string{
"header-dns": "dns",
"header-dtls": "dtls",
"header-srtp": "srtp",
"header-utp": "utp",
"header-wechat": "wechat-video",
"header-wireguard": "wireguard",
}
var validFinalMaskUDPTypes = map[string]struct{}{
"salamander": {},
"mkcp-aes128gcm": {},
"header-dns": {},
"header-dtls": {},
"header-srtp": {},
"header-utp": {},
"header-wechat": {},
"header-wireguard": {},
"mkcp-original": {},
"xdns": {},
"xicmp": {},
"noise": {},
2026-04-27 02:29:13 +02:00
"header-custom": {},
}
var validFinalMaskTCPTypes = map[string]struct{}{
"header-custom": {},
"fragment": {},
"sudoku": {},
2026-04-26 23:04:47 +02:00
}
// applyKcpShareParams reconstructs legacy KCP share-link fields from either
// the historical kcpSettings.header/seed shape or the current finalmask model.
// This keeps subscription output compatible while avoiding panics when older
// keys are absent from modern inbounds.
func applyKcpShareParams(stream map[string]any, params map[string]string) {
extractKcpShareFields(stream).applyToParams(params)
}
func applyKcpShareObj(stream map[string]any, obj map[string]any) {
extractKcpShareFields(stream).applyToObj(obj)
}
type kcpShareFields struct {
headerType string
seed string
mtu int
tti int
}
func (f kcpShareFields) applyToParams(params map[string]string) {
2026-04-27 15:02:43 +02:00
if f.headerType != "" && f.headerType != "none" {
params["headerType"] = f.headerType
}
2026-04-26 23:04:47 +02:00
setStringParam(params, "seed", f.seed)
setIntParam(params, "mtu", f.mtu)
setIntParam(params, "tti", f.tti)
}
func (f kcpShareFields) applyToObj(obj map[string]any) {
2026-04-27 15:02:43 +02:00
if f.headerType != "" && f.headerType != "none" {
obj["type"] = f.headerType
}
2026-04-26 23:04:47 +02:00
setStringField(obj, "path", f.seed)
setIntField(obj, "mtu", f.mtu)
setIntField(obj, "tti", f.tti)
}
func extractKcpShareFields(stream map[string]any) kcpShareFields {
fields := kcpShareFields{headerType: "none"}
if kcp, ok := stream["kcpSettings"].(map[string]any); ok {
if header, ok := kcp["header"].(map[string]any); ok {
if value, ok := header["type"].(string); ok && value != "" {
fields.headerType = value
}
}
if value, ok := kcp["seed"].(string); ok && value != "" {
fields.seed = value
}
if value, ok := readPositiveInt(kcp["mtu"]); ok {
fields.mtu = value
}
if value, ok := readPositiveInt(kcp["tti"]); ok {
fields.tti = value
}
}
for _, rawMask := range normalizedFinalMaskUDPMasks(stream["finalmask"]) {
mask, _ := rawMask.(map[string]any)
if mask == nil {
continue
}
maskType, _ := mask["type"].(string)
if mapped, ok := kcpMaskToHeaderType[maskType]; ok {
fields.headerType = mapped
continue
}
switch maskType {
case "mkcp-original":
fields.seed = ""
case "mkcp-aes128gcm":
fields.seed = ""
settings, _ := mask["settings"].(map[string]any)
if value, ok := settings["password"].(string); ok && value != "" {
fields.seed = value
}
}
}
return fields
}
func readPositiveInt(value any) (int, bool) {
switch number := value.(type) {
case int:
return number, number > 0
case int32:
return int(number), number > 0
case int64:
return int(number), number > 0
case float32:
parsed := int(number)
return parsed, parsed > 0
case float64:
parsed := int(number)
return parsed, parsed > 0
default:
return 0, false
}
}
func setStringParam(params map[string]string, key, value string) {
if value == "" {
delete(params, key)
return
}
params[key] = value
}
func setIntParam(params map[string]string, key string, value int) {
if value <= 0 {
delete(params, key)
return
}
params[key] = fmt.Sprintf("%d", value)
}
func setStringField(obj map[string]any, key, value string) {
if value == "" {
delete(obj, key)
return
}
obj[key] = value
}
func setIntField(obj map[string]any, key string, value int) {
if value <= 0 {
delete(obj, key)
return
}
obj[key] = value
}
// applyFinalMaskParams exports the finalmask payload as the compact
// `fm=<json>` share-link field used by v2rayN-compatible clients.
func applyFinalMaskParams(finalmask map[string]any, params map[string]string) {
if fm, ok := marshalFinalMask(finalmask); ok {
params["fm"] = fm
}
}
func applyFinalMaskObj(finalmask map[string]any, obj map[string]any) {
if fm, ok := marshalFinalMask(finalmask); ok {
obj["fm"] = fm
}
}
func marshalFinalMask(finalmask map[string]any) (string, bool) {
normalized := normalizeFinalMask(finalmask)
if !hasFinalMaskContent(normalized) {
return "", false
}
b, err := json.Marshal(normalized)
if err != nil || len(b) == 0 || string(b) == "null" {
return "", false
}
return string(b), true
}
func normalizeFinalMask(finalmask map[string]any) map[string]any {
2026-04-27 02:29:13 +02:00
tcpMasks := normalizedFinalMaskTCPMasks(finalmask)
2026-04-26 23:04:47 +02:00
udpMasks := normalizedFinalMaskUDPMasks(finalmask)
2026-04-27 02:29:13 +02:00
quicParams, hasQuicParams := finalmask["quicParams"].(map[string]any)
if len(tcpMasks) == 0 && len(udpMasks) == 0 && !hasQuicParams {
return nil
}
result := map[string]any{}
if len(tcpMasks) > 0 {
result["tcp"] = tcpMasks
}
if len(udpMasks) > 0 {
result["udp"] = udpMasks
}
if hasQuicParams && len(quicParams) > 0 {
result["quicParams"] = quicParams
}
return result
}
func normalizedFinalMaskTCPMasks(value any) []any {
finalmask, _ := value.(map[string]any)
if finalmask == nil {
return nil
}
rawMasks, _ := finalmask["tcp"].([]any)
if len(rawMasks) == 0 {
return nil
}
normalized := make([]any, 0, len(rawMasks))
for _, rawMask := range rawMasks {
mask, _ := rawMask.(map[string]any)
if mask == nil {
continue
}
maskType, _ := mask["type"].(string)
if _, ok := validFinalMaskTCPTypes[maskType]; !ok || maskType == "" {
continue
}
normalizedMask := map[string]any{"type": maskType}
if settings, ok := mask["settings"].(map[string]any); ok && len(settings) > 0 {
normalizedMask["settings"] = settings
}
normalized = append(normalized, normalizedMask)
}
if len(normalized) == 0 {
2026-04-26 23:04:47 +02:00
return nil
}
2026-04-27 02:29:13 +02:00
return normalized
2026-04-26 23:04:47 +02:00
}
func normalizedFinalMaskUDPMasks(value any) []any {
finalmask, _ := value.(map[string]any)
if finalmask == nil {
return nil
}
rawMasks, _ := finalmask["udp"].([]any)
if len(rawMasks) == 0 {
return nil
}
normalized := make([]any, 0, len(rawMasks))
for _, rawMask := range rawMasks {
mask, _ := rawMask.(map[string]any)
if mask == nil {
continue
}
maskType, _ := mask["type"].(string)
if _, ok := validFinalMaskUDPTypes[maskType]; !ok || maskType == "" {
continue
}
normalizedMask := map[string]any{"type": maskType}
if settings, ok := mask["settings"].(map[string]any); ok && len(settings) > 0 {
normalizedMask["settings"] = settings
}
normalized = append(normalized, normalizedMask)
}
if len(normalized) == 0 {
return nil
}
return normalized
}
func hasFinalMaskContent(value any) bool {
switch v := value.(type) {
case nil:
return false
case string:
return len(v) > 0
case map[string]any:
for _, item := range v {
if hasFinalMaskContent(item) {
return true
}
}
return false
case []any:
return slices.ContainsFunc(v, hasFinalMaskContent)
default:
return true
}
}
2025-03-12 20:13:51 +01:00
func searchHost(headers any) string {
data, _ := headers.(map[string]any)
2023-04-09 23:13:18 +03:30
for k, v := range data {
if strings.EqualFold(k, "host") {
switch v.(type) {
2025-03-12 20:13:51 +01:00
case []any:
hosts, _ := v.([]any)
2023-04-25 14:39:09 +03:30
if len(hosts) > 0 {
return hosts[0].(string)
} else {
return ""
}
2025-03-12 20:13:51 +01:00
case any:
2023-04-09 23:13:18 +03:30
return v.(string)
}
}
}
return ""
}
2025-09-14 01:22:42 +02:00
2025-09-18 12:20:21 +02:00
// PageData is a view model for subpage.html
2025-09-20 09:35:50 +02:00
// PageData contains data for rendering the subscription information page.
2025-09-14 01:22:42 +02:00
type PageData struct {
Host string
BasePath string
SId string
2026-05-04 23:27:57 +02:00
Enabled bool
2025-09-14 01:22:42 +02:00
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
2026-04-20 04:26:13 +08:00
SubClashUrl string
2025-09-14 01:22:42 +02:00
Result []string
}
// ResolveRequest extracts scheme and host info from request/headers consistently.
2025-09-20 09:35:50 +02:00
// ResolveRequest extracts scheme, host, and header information from an HTTP request.
2025-09-14 01:22:42 +02:00
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
}
2025-09-21 21:20:37 +02:00
// BuildURLs constructs absolute subscription and JSON subscription URLs for a given subscription ID.
// It prioritizes configured URIs, then individual settings, and finally falls back to request-derived components.
2026-04-20 04:26:13 +08:00
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subClashPath, subId string) (subURL, subJsonURL, subClashURL string) {
2025-09-21 21:20:37 +02:00
if subId == "" {
2026-04-20 04:26:13 +08:00
return "", "", ""
2025-09-14 01:22:42 +02:00
}
2025-09-21 21:20:37 +02:00
configuredSubURI, _ := s.settingService.GetSubURI()
configuredSubJsonURI, _ := s.settingService.GetSubJsonURI()
2026-04-20 04:26:13 +08:00
configuredSubClashURI, _ := s.settingService.GetSubClashURI()
2025-09-21 21:20:37 +02:00
var baseScheme, baseHostWithPort string
2026-04-20 04:26:13 +08:00
if configuredSubURI == "" || configuredSubJsonURI == "" || configuredSubClashURI == "" {
2025-09-21 21:20:37 +02:00
baseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort)
2025-09-14 01:22:42 +02:00
}
2025-09-21 21:20:37 +02:00
subURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId)
subJsonURL = s.buildSingleURL(configuredSubJsonURI, baseScheme, baseHostWithPort, subJsonPath, subId)
2026-04-20 04:26:13 +08:00
subClashURL = s.buildSingleURL(configuredSubClashURI, baseScheme, baseHostWithPort, subClashPath, subId)
2025-09-21 21:20:37 +02:00
2026-04-20 04:26:13 +08:00
return subURL, subJsonURL, subClashURL
2025-09-21 21:20:37 +02:00
}
// getBaseSchemeAndHost determines the base scheme and host from settings or falls back to request values
func (s *SubService) getBaseSchemeAndHost(requestScheme, requestHostWithPort string) (string, string) {
subDomain, err := s.settingService.GetSubDomain()
if err != nil || subDomain == "" {
return requestScheme, requestHostWithPort
}
// Get port and TLS settings
subPort, _ := s.settingService.GetSubPort()
subKeyFile, _ := s.settingService.GetSubKeyFile()
subCertFile, _ := s.settingService.GetSubCertFile()
// Determine scheme from TLS configuration
scheme := "http"
if subKeyFile != "" && subCertFile != "" {
scheme = "https"
}
// Build host:port, always include port for clarity
hostWithPort := fmt.Sprintf("%s:%d", subDomain, subPort)
return scheme, hostWithPort
}
// buildSingleURL constructs a single URL using configured URI or base components
func (s *SubService) buildSingleURL(configuredURI, baseScheme, baseHostWithPort, basePath, subId string) string {
if configuredURI != "" {
return s.joinPathWithID(configuredURI, subId)
}
baseURL := fmt.Sprintf("%s://%s", baseScheme, baseHostWithPort)
return s.joinPathWithID(baseURL+basePath, subId)
}
// joinPathWithID safely joins a base path with a subscription ID
func (s *SubService) joinPathWithID(basePath, subId string) string {
if strings.HasSuffix(basePath, "/") {
return basePath + subId
}
return basePath + "/" + subId
2025-09-14 01:22:42 +02:00
}
// BuildPageData parses header and prepares the template view model.
2025-09-20 09:35:50 +02:00
// BuildPageData constructs page data for rendering the subscription information page.
2026-04-20 04:26:13 +08:00
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL, subClashURL string, basePath string) PageData {
2025-09-14 23:08:09 +02:00
download := common.FormatTraffic(traffic.Down)
upload := common.FormatTraffic(traffic.Up)
2025-09-14 01:22:42 +02:00
total := "∞"
2025-09-14 23:08:09 +02:00
used := common.FormatTraffic(traffic.Up + traffic.Down)
2025-09-14 01:22:42 +02:00
remained := ""
2025-09-14 23:08:09 +02:00
if traffic.Total > 0 {
total = common.FormatTraffic(traffic.Total)
2025-09-21 21:20:37 +02:00
left := max(traffic.Total-(traffic.Up+traffic.Down), 0)
2025-09-14 01:22:42 +02:00
remained = common.FormatTraffic(left)
}
datepicker := s.datepicker
if datepicker == "" {
datepicker = "gregorian"
}
return PageData{
Host: hostHeader,
2025-09-24 19:51:01 +02:00
BasePath: basePath,
2025-09-14 01:22:42 +02:00
SId: subId,
2026-05-04 23:27:57 +02:00
Enabled: traffic.Enable,
2025-09-14 01:22:42 +02:00
Download: download,
Upload: upload,
Total: total,
Used: used,
Remained: remained,
2025-09-14 23:08:09 +02:00
Expire: traffic.ExpiryTime / 1000,
2025-09-14 01:22:42 +02:00
LastOnline: lastOnline,
Datepicker: datepicker,
2025-09-14 23:08:09 +02:00
DownloadByte: traffic.Down,
UploadByte: traffic.Up,
TotalByte: traffic.Total,
2025-09-14 01:22:42 +02:00
SubUrl: subURL,
SubJsonUrl: subJsonURL,
2026-04-20 04:26:13 +08:00
SubClashUrl: subClashURL,
2025-09-14 01:22:42 +02:00
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
}