2023-05-22 14:36:34 +00:00
|
|
|
|
package sub
|
2023-04-09 19:43:18 +00:00
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
|
"fmt"
|
2025-09-13 23:22:42 +00:00
|
|
|
|
"net"
|
2023-04-09 19:43:18 +00:00
|
|
|
|
"net/url"
|
|
|
|
|
|
"strings"
|
2023-07-17 23:49:01 +00:00
|
|
|
|
"time"
|
2024-03-10 21:31:24 +00:00
|
|
|
|
|
2025-09-13 23:22:42 +00:00
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
"github.com/goccy/go-json"
|
|
|
|
|
|
|
2025-09-19 08:05:43 +00: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 19:43:18 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-09-20 07:35:50 +00:00
|
|
|
|
// SubService provides business logic for generating subscription links and managing subscription data.
|
2023-04-09 19:43:18 +00:00
|
|
|
|
type SubService struct {
|
|
|
|
|
|
address string
|
2023-08-26 11:41:12 +00:00
|
|
|
|
showInfo bool
|
2023-12-08 19:31:17 +00:00
|
|
|
|
remarkModel string
|
2024-01-02 08:32:21 +00:00
|
|
|
|
datepicker string
|
2023-05-22 14:36:34 +00:00
|
|
|
|
inboundService service.InboundService
|
2023-12-05 22:09:08 +00:00
|
|
|
|
settingService service.SettingService
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-20 07:35:50 +00:00
|
|
|
|
// NewSubService creates a new subscription service with the given configuration.
|
2024-02-21 10:47:52 +00:00
|
|
|
|
func NewSubService(showInfo bool, remarkModel string) *SubService {
|
|
|
|
|
|
return &SubService{
|
|
|
|
|
|
showInfo: showInfo,
|
|
|
|
|
|
remarkModel: remarkModel,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-20 07:35:50 +00:00
|
|
|
|
// GetSubs retrieves subscription links for a given subscription ID and host.
|
2025-09-14 21:08:09 +00:00
|
|
|
|
func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.ClientTraffic, error) {
|
2023-04-09 19:43:18 +00:00
|
|
|
|
s.address = host
|
|
|
|
|
|
var result []string
|
2023-04-18 18:04:06 +00:00
|
|
|
|
var traffic xray.ClientTraffic
|
2025-09-13 23:22:42 +00:00
|
|
|
|
var lastOnline int64
|
2023-04-18 18:04:06 +00:00
|
|
|
|
var clientTraffics []xray.ClientTraffic
|
2023-04-09 19:43:18 +00:00
|
|
|
|
inbounds, err := s.getInboundsBySubId(subId)
|
|
|
|
|
|
if err != nil {
|
2025-09-14 21:08:09 +00:00
|
|
|
|
return nil, 0, traffic, err
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
2024-02-21 10:47:52 +00:00
|
|
|
|
|
2024-03-21 06:51:12 +00:00
|
|
|
|
if len(inbounds) == 0 {
|
2025-09-14 21:08:09 +00:00
|
|
|
|
return nil, 0, traffic, common.NewError("No inbounds found with ", subId)
|
2024-03-21 06:51:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-21 10:47:52 +00:00
|
|
|
|
s.datepicker, err = s.settingService.GetDatepicker()
|
2023-12-08 19:31:17 +00:00
|
|
|
|
if err != nil {
|
2024-02-21 10:47:52 +00:00
|
|
|
|
s.datepicker = "gregorian"
|
2023-12-08 19:31:17 +00:00
|
|
|
|
}
|
2023-04-09 19:43:18 +00:00
|
|
|
|
for _, inbound := range inbounds {
|
2023-05-22 14:36:34 +00:00
|
|
|
|
clients, err := s.inboundService.GetClients(inbound)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
if err != nil {
|
2024-02-21 10:47:52 +00:00
|
|
|
|
logger.Error("SubService - GetClients: Unable to get clients from inbound")
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
if clients == nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2023-05-22 23:45:34 +00:00
|
|
|
|
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
|
2024-02-21 10:47:52 +00:00
|
|
|
|
listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
|
2023-05-22 23:45:34 +00:00
|
|
|
|
if err == nil {
|
2024-02-21 10:47:52 +00:00
|
|
|
|
inbound.Listen = listen
|
|
|
|
|
|
inbound.Port = port
|
|
|
|
|
|
inbound.StreamSettings = streamSettings
|
2023-05-22 23:45:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2023-04-09 19:43:18 +00:00
|
|
|
|
for _, client := range clients {
|
2023-05-12 14:59:02 +00:00
|
|
|
|
if client.Enable && client.SubID == subId {
|
2023-08-26 11:41:12 +00:00
|
|
|
|
link := s.getLink(inbound, client.Email)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
result = append(result, link)
|
2025-09-13 23:22:42 +00:00
|
|
|
|
ct := s.getClientTraffics(inbound.ClientStats, client.Email)
|
|
|
|
|
|
clientTraffics = append(clientTraffics, ct)
|
|
|
|
|
|
if ct.LastOnline > lastOnline {
|
|
|
|
|
|
lastOnline = ct.LastOnline
|
|
|
|
|
|
}
|
2023-04-18 18:04:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-02-21 10:47:52 +00:00
|
|
|
|
|
|
|
|
|
|
// Prepare statistics
|
2023-04-18 18:04:06 +00:00
|
|
|
|
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 19:43:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-14 21:08:09 +00:00
|
|
|
|
return result, lastOnline, traffic, nil
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
|
|
|
|
|
db := database.GetDB()
|
|
|
|
|
|
var inbounds []*model.Inbound
|
2023-12-08 17:45:21 +00: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
|
|
|
|
|
|
WHERE
|
|
|
|
|
|
protocol in ('vmess','vless','trojan','shadowsocks')
|
|
|
|
|
|
AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ?
|
|
|
|
|
|
)`, subId, true).Find(&inbounds).Error
|
2023-05-16 21:37:35 +00:00
|
|
|
|
if err != nil {
|
2023-04-09 19:43:18 +00:00
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return inbounds, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-04-18 18:04:06 +00:00
|
|
|
|
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 10:47:52 +00:00
|
|
|
|
func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) {
|
2023-05-22 23:45:34 +00:00
|
|
|
|
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 10:47:52 +00:00
|
|
|
|
return "", 0, "", err
|
2023-05-22 23:45:34 +00:00
|
|
|
|
}
|
2024-02-21 10:47:52 +00:00
|
|
|
|
|
2025-03-12 19:13:51 +00:00
|
|
|
|
var stream map[string]any
|
2024-02-21 10:47:52 +00:00
|
|
|
|
json.Unmarshal([]byte(streamSettings), &stream)
|
2025-03-12 19:13:51 +00:00
|
|
|
|
var masterStream map[string]any
|
2024-02-21 10:47:52 +00:00
|
|
|
|
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-22 23:45:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-26 11:41:12 +00:00
|
|
|
|
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
2023-04-09 19:43:18 +00:00
|
|
|
|
switch inbound.Protocol {
|
|
|
|
|
|
case "vmess":
|
2023-08-26 11:41:12 +00:00
|
|
|
|
return s.genVmessLink(inbound, email)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
case "vless":
|
2023-08-26 11:41:12 +00:00
|
|
|
|
return s.genVlessLink(inbound, email)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
case "trojan":
|
2023-08-26 11:41:12 +00:00
|
|
|
|
return s.genTrojanLink(inbound, email)
|
2023-05-06 16:51:14 +00:00
|
|
|
|
case "shadowsocks":
|
2023-08-26 11:41:12 +00:00
|
|
|
|
return s.genShadowsocksLink(inbound, email)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-26 11:41:12 +00:00
|
|
|
|
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
2024-08-10 22:47:44 +00:00
|
|
|
|
if inbound.Protocol != model.VMESS {
|
2023-04-09 19:43:18 +00:00
|
|
|
|
return ""
|
|
|
|
|
|
}
|
2026-01-03 03:39:30 +00:00
|
|
|
|
var address string
|
|
|
|
|
|
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
|
|
|
|
|
address = s.address
|
|
|
|
|
|
} else {
|
|
|
|
|
|
address = inbound.Listen
|
|
|
|
|
|
}
|
2025-03-12 19:13:51 +00:00
|
|
|
|
obj := map[string]any{
|
2023-04-27 20:45:06 +00:00
|
|
|
|
"v": "2",
|
2026-01-03 03:39:30 +00:00
|
|
|
|
"add": address,
|
2023-04-27 20:45:06 +00:00
|
|
|
|
"port": inbound.Port,
|
|
|
|
|
|
"type": "none",
|
|
|
|
|
|
}
|
2025-03-12 19:13:51 +00:00
|
|
|
|
var stream map[string]any
|
2023-04-09 19:43:18 +00:00
|
|
|
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
|
|
|
|
|
network, _ := stream["network"].(string)
|
2023-04-27 20:45:06 +00:00
|
|
|
|
obj["net"] = network
|
2023-04-09 19:43:18 +00:00
|
|
|
|
switch network {
|
|
|
|
|
|
case "tcp":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
tcp, _ := stream["tcpSettings"].(map[string]any)
|
|
|
|
|
|
header, _ := tcp["header"].(map[string]any)
|
2023-04-27 20:45:06 +00:00
|
|
|
|
typeStr, _ := header["type"].(string)
|
|
|
|
|
|
obj["type"] = typeStr
|
2023-04-09 19:43:18 +00:00
|
|
|
|
if typeStr == "http" {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
request := header["request"].(map[string]any)
|
|
|
|
|
|
requestPath, _ := request["path"].([]any)
|
2023-04-27 20:45:06 +00:00
|
|
|
|
obj["path"] = requestPath[0].(string)
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := request["headers"].(map[string]any)
|
2023-04-27 20:45:06 +00:00
|
|
|
|
obj["host"] = searchHost(headers)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
case "kcp":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
kcp, _ := stream["kcpSettings"].(map[string]any)
|
|
|
|
|
|
header, _ := kcp["header"].(map[string]any)
|
2023-04-27 20:45:06 +00:00
|
|
|
|
obj["type"], _ = header["type"].(string)
|
|
|
|
|
|
obj["path"], _ = kcp["seed"].(string)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
case "ws":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
ws, _ := stream["wsSettings"].(map[string]any)
|
2023-04-27 20:45:06 +00:00
|
|
|
|
obj["path"] = ws["path"].(string)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
if host, ok := ws["host"].(string); ok && len(host) > 0 {
|
|
|
|
|
|
obj["host"] = host
|
|
|
|
|
|
} else {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := ws["headers"].(map[string]any)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
obj["host"] = searchHost(headers)
|
2024-04-01 12:32:02 +00:00
|
|
|
|
}
|
2023-04-09 19:43:18 +00:00
|
|
|
|
case "grpc":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
grpc, _ := stream["grpcSettings"].(map[string]any)
|
2023-04-27 20:45:06 +00:00
|
|
|
|
obj["path"] = grpc["serviceName"].(string)
|
2024-03-11 09:30:00 +00:00
|
|
|
|
obj["authority"] = grpc["authority"].(string)
|
2023-04-27 20:45:06 +00:00
|
|
|
|
if grpc["multiMode"].(bool) {
|
|
|
|
|
|
obj["type"] = "multi"
|
|
|
|
|
|
}
|
2024-03-11 07:36:33 +00:00
|
|
|
|
case "httpupgrade":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any)
|
2024-03-11 07:36:33 +00:00
|
|
|
|
obj["path"] = httpupgrade["path"].(string)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
|
|
|
|
|
|
obj["host"] = host
|
|
|
|
|
|
} else {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := httpupgrade["headers"].(map[string]any)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
obj["host"] = searchHost(headers)
|
|
|
|
|
|
}
|
2024-12-03 21:24:34 +00:00
|
|
|
|
case "xhttp":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
xhttp, _ := stream["xhttpSettings"].(map[string]any)
|
2024-12-03 21:24:34 +00:00
|
|
|
|
obj["path"] = xhttp["path"].(string)
|
|
|
|
|
|
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
|
2024-06-18 10:49:20 +00:00
|
|
|
|
obj["host"] = host
|
|
|
|
|
|
} else {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := xhttp["headers"].(map[string]any)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
obj["host"] = searchHost(headers)
|
2024-04-02 20:11:06 +00:00
|
|
|
|
}
|
2024-12-03 21:24:34 +00:00
|
|
|
|
obj["mode"] = xhttp["mode"].(string)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
security, _ := stream["security"].(string)
|
2023-04-27 20:45:06 +00:00
|
|
|
|
obj["tls"] = security
|
2023-04-09 19:43:18 +00:00
|
|
|
|
if security == "tls" {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
tlsSetting, _ := stream["tlsSettings"].(map[string]any)
|
|
|
|
|
|
alpns, _ := tlsSetting["alpn"].([]any)
|
2023-04-27 20:45:06 +00:00
|
|
|
|
if len(alpns) > 0 {
|
|
|
|
|
|
var alpn []string
|
|
|
|
|
|
for _, a := range alpns {
|
|
|
|
|
|
alpn = append(alpn, a.(string))
|
|
|
|
|
|
}
|
|
|
|
|
|
obj["alpn"] = strings.Join(alpn, ",")
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
2023-12-08 17:45:21 +00:00
|
|
|
|
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
|
|
|
|
|
|
obj["sni"], _ = sniValue.(string)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-04-09 19:43:18 +00:00
|
|
|
|
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
|
|
|
|
|
if tlsSetting != nil {
|
|
|
|
|
|
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
2023-04-27 20:45:06 +00:00
|
|
|
|
obj["fp"], _ = fpValue.(string)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
2023-04-27 20:45:06 +00:00
|
|
|
|
obj["allowInsecure"], _ = insecure.(bool)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-05-22 14:36:34 +00:00
|
|
|
|
clients, _ := s.inboundService.GetClients(inbound)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
clientIndex := -1
|
|
|
|
|
|
for i, client := range clients {
|
|
|
|
|
|
if client.Email == email {
|
|
|
|
|
|
clientIndex = i
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2023-04-27 20:45:06 +00:00
|
|
|
|
obj["id"] = clients[clientIndex].ID
|
2024-08-10 22:47:44 +00:00
|
|
|
|
obj["scy"] = clients[clientIndex].Security
|
2023-04-09 19:43:18 +00:00
|
|
|
|
|
2025-03-12 19:13:51 +00:00
|
|
|
|
externalProxies, _ := stream["externalProxy"].([]any)
|
2023-12-08 17:45:21 +00:00
|
|
|
|
|
|
|
|
|
|
if len(externalProxies) > 0 {
|
2023-05-22 14:36:34 +00:00
|
|
|
|
links := ""
|
2023-12-08 17:45:21 +00:00
|
|
|
|
for index, externalProxy := range externalProxies {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
ep, _ := externalProxy.(map[string]any)
|
2023-12-08 17:45:21 +00:00
|
|
|
|
newSecurity, _ := ep["forceTls"].(string)
|
2025-03-12 19:13:51 +00:00
|
|
|
|
newObj := map[string]any{}
|
2023-12-08 17:45:21 +00:00
|
|
|
|
for key, value := range obj {
|
|
|
|
|
|
if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp" || key == "allowInsecure")) {
|
|
|
|
|
|
newObj[key] = value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
2023-05-22 14:36:34 +00:00
|
|
|
|
if index > 0 {
|
|
|
|
|
|
links += "\n"
|
|
|
|
|
|
}
|
2023-12-08 17:45:21 +00:00
|
|
|
|
jsonStr, _ := json.MarshalIndent(newObj, "", " ")
|
2023-05-22 14:36:34 +00:00
|
|
|
|
links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
|
|
|
|
|
}
|
|
|
|
|
|
return links
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-26 11:41:12 +00:00
|
|
|
|
obj["ps"] = s.genRemark(inbound, email, "")
|
|
|
|
|
|
|
2023-04-09 19:43:18 +00:00
|
|
|
|
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
|
|
|
|
|
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-26 11:41:12 +00:00
|
|
|
|
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
2026-01-03 03:39:30 +00:00
|
|
|
|
var address string
|
|
|
|
|
|
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
|
|
|
|
|
address = s.address
|
|
|
|
|
|
} else {
|
|
|
|
|
|
address = inbound.Listen
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-04-09 19:43:18 +00:00
|
|
|
|
if inbound.Protocol != model.VLESS {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
2025-03-12 19:13:51 +00:00
|
|
|
|
var stream map[string]any
|
2023-04-09 19:43:18 +00:00
|
|
|
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
2023-05-22 14:36:34 +00:00
|
|
|
|
clients, _ := s.inboundService.GetClients(inbound)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
clientIndex := -1
|
|
|
|
|
|
for i, client := range clients {
|
|
|
|
|
|
if client.Email == email {
|
|
|
|
|
|
clientIndex = i
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
uuid := clients[clientIndex].ID
|
|
|
|
|
|
port := inbound.Port
|
|
|
|
|
|
streamNetwork := stream["network"].(string)
|
|
|
|
|
|
params := make(map[string]string)
|
|
|
|
|
|
params["type"] = streamNetwork
|
|
|
|
|
|
|
2025-09-20 09:11:30 +00: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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-04-09 19:43:18 +00:00
|
|
|
|
switch streamNetwork {
|
|
|
|
|
|
case "tcp":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
tcp, _ := stream["tcpSettings"].(map[string]any)
|
|
|
|
|
|
header, _ := tcp["header"].(map[string]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
typeStr, _ := header["type"].(string)
|
|
|
|
|
|
if typeStr == "http" {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
request := header["request"].(map[string]any)
|
|
|
|
|
|
requestPath, _ := request["path"].([]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
params["path"] = requestPath[0].(string)
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := request["headers"].(map[string]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
params["host"] = searchHost(headers)
|
|
|
|
|
|
params["headerType"] = "http"
|
|
|
|
|
|
}
|
|
|
|
|
|
case "kcp":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
kcp, _ := stream["kcpSettings"].(map[string]any)
|
|
|
|
|
|
header, _ := kcp["header"].(map[string]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
params["headerType"] = header["type"].(string)
|
|
|
|
|
|
params["seed"] = kcp["seed"].(string)
|
|
|
|
|
|
case "ws":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
ws, _ := stream["wsSettings"].(map[string]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
params["path"] = ws["path"].(string)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
if host, ok := ws["host"].(string); ok && len(host) > 0 {
|
|
|
|
|
|
params["host"] = host
|
|
|
|
|
|
} else {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := ws["headers"].(map[string]any)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
params["host"] = searchHost(headers)
|
2024-04-01 12:32:02 +00:00
|
|
|
|
}
|
2023-04-09 19:43:18 +00:00
|
|
|
|
case "grpc":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
grpc, _ := stream["grpcSettings"].(map[string]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
params["serviceName"] = grpc["serviceName"].(string)
|
2024-04-01 07:39:45 +00:00
|
|
|
|
params["authority"], _ = grpc["authority"].(string)
|
2023-04-27 20:45:06 +00:00
|
|
|
|
if grpc["multiMode"].(bool) {
|
|
|
|
|
|
params["mode"] = "multi"
|
|
|
|
|
|
}
|
2024-03-11 07:36:33 +00:00
|
|
|
|
case "httpupgrade":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any)
|
2024-03-11 07:36:33 +00:00
|
|
|
|
params["path"] = httpupgrade["path"].(string)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
|
|
|
|
|
|
params["host"] = host
|
|
|
|
|
|
} else {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := httpupgrade["headers"].(map[string]any)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
params["host"] = searchHost(headers)
|
|
|
|
|
|
}
|
2024-12-03 21:24:34 +00:00
|
|
|
|
case "xhttp":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
xhttp, _ := stream["xhttpSettings"].(map[string]any)
|
2024-12-03 21:24:34 +00:00
|
|
|
|
params["path"] = xhttp["path"].(string)
|
|
|
|
|
|
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
|
2024-06-18 10:49:20 +00:00
|
|
|
|
params["host"] = host
|
|
|
|
|
|
} else {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := xhttp["headers"].(map[string]any)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
params["host"] = searchHost(headers)
|
2024-04-02 20:11:06 +00:00
|
|
|
|
}
|
2024-12-03 21:24:34 +00:00
|
|
|
|
params["mode"] = xhttp["mode"].(string)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
security, _ := stream["security"].(string)
|
|
|
|
|
|
if security == "tls" {
|
|
|
|
|
|
params["security"] = "tls"
|
2025-03-12 19:13:51 +00:00
|
|
|
|
tlsSetting, _ := stream["tlsSettings"].(map[string]any)
|
|
|
|
|
|
alpns, _ := tlsSetting["alpn"].([]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
var alpn []string
|
|
|
|
|
|
for _, a := range alpns {
|
|
|
|
|
|
alpn = append(alpn, a.(string))
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(alpn) > 0 {
|
|
|
|
|
|
params["alpn"] = strings.Join(alpn, ",")
|
|
|
|
|
|
}
|
2023-12-08 17:45:21 +00:00
|
|
|
|
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
|
|
|
|
|
|
params["sni"], _ = sniValue.(string)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-04-09 19:43:18 +00:00
|
|
|
|
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["allowInsecure"] = "1"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
|
|
|
|
|
params["flow"] = clients[clientIndex].Flow
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-04-11 12:10:45 +00:00
|
|
|
|
if security == "reality" {
|
|
|
|
|
|
params["security"] = "reality"
|
2025-03-12 19:13:51 +00:00
|
|
|
|
realitySetting, _ := stream["realitySettings"].(map[string]any)
|
2023-04-19 08:25:31 +00:00
|
|
|
|
realitySettings, _ := searchKey(realitySetting, "settings")
|
|
|
|
|
|
if realitySetting != nil {
|
|
|
|
|
|
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
sNames, _ := sniValue.([]any)
|
2024-03-11 10:04:15 +00:00
|
|
|
|
params["sni"] = sNames[random.Num(len(sNames))].(string)
|
2023-04-11 12:10:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
|
|
|
|
|
|
params["pbk"], _ = pbkValue.(string)
|
|
|
|
|
|
}
|
2023-04-19 08:25:31 +00:00
|
|
|
|
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
shortIds, _ := sidValue.([]any)
|
2024-03-11 10:04:15 +00:00
|
|
|
|
params["sid"] = shortIds[random.Num(len(shortIds))].(string)
|
2023-04-11 12:10:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
|
2023-04-19 08:25:31 +00:00
|
|
|
|
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
|
|
|
|
|
|
params["fp"] = fp
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-04 08:24:21 +00:00
|
|
|
|
if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok {
|
|
|
|
|
|
if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 {
|
|
|
|
|
|
params["pqv"] = pqv
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-03-11 10:04:15 +00:00
|
|
|
|
params["spx"] = "/" + random.Seq(15)
|
2023-04-11 12:10:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
|
|
|
|
|
params["flow"] = clients[clientIndex].Flow
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-29 11:50:25 +00:00
|
|
|
|
if security != "tls" && security != "reality" {
|
2023-06-14 13:36:56 +00:00
|
|
|
|
params["security"] = "none"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-12 19:13:51 +00:00
|
|
|
|
externalProxies, _ := stream["externalProxy"].([]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
|
2023-12-08 17:45:21 +00:00
|
|
|
|
if len(externalProxies) > 0 {
|
2026-01-05 04:54:56 +00:00
|
|
|
|
links := make([]string, 0, len(externalProxies))
|
|
|
|
|
|
for _, externalProxy := range externalProxies {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
ep, _ := externalProxy.(map[string]any)
|
2023-12-08 17:45:21 +00:00
|
|
|
|
newSecurity, _ := ep["forceTls"].(string)
|
|
|
|
|
|
dest, _ := ep["dest"].(string)
|
|
|
|
|
|
port := int(ep["port"].(float64))
|
|
|
|
|
|
link := fmt.Sprintf("vless://%s@%s:%d", uuid, dest, port)
|
|
|
|
|
|
|
|
|
|
|
|
if newSecurity != "same" {
|
|
|
|
|
|
params["security"] = newSecurity
|
|
|
|
|
|
} else {
|
|
|
|
|
|
params["security"] = security
|
|
|
|
|
|
}
|
|
|
|
|
|
url, _ := url.Parse(link)
|
|
|
|
|
|
q := url.Query()
|
2023-04-09 19:43:18 +00:00
|
|
|
|
|
2023-12-08 17:45:21 +00:00
|
|
|
|
for k, v := range params {
|
|
|
|
|
|
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
|
|
|
|
|
|
q.Add(k, v)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Set the new query values on the URL
|
|
|
|
|
|
url.RawQuery = q.Encode()
|
|
|
|
|
|
|
|
|
|
|
|
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
2023-04-09 19:43:18 +00:00
|
|
|
|
|
2026-01-05 04:54:56 +00:00
|
|
|
|
links = append(links, url.String())
|
2023-05-22 14:36:34 +00:00
|
|
|
|
}
|
2026-01-05 04:54:56 +00:00
|
|
|
|
return strings.Join(links, "\n")
|
2023-05-22 14:36:34 +00:00
|
|
|
|
}
|
2023-08-26 11:41:12 +00:00
|
|
|
|
|
2023-12-08 17:45:21 +00:00
|
|
|
|
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
|
|
|
|
|
|
url, _ := url.Parse(link)
|
|
|
|
|
|
q := url.Query()
|
|
|
|
|
|
|
|
|
|
|
|
for k, v := range params {
|
|
|
|
|
|
q.Add(k, v)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Set the new query values on the URL
|
|
|
|
|
|
url.RawQuery = q.Encode()
|
|
|
|
|
|
|
# Pull Request: Connection Reporting System & Improvements for Restricted Networks
## Description
This PR introduces a comprehensive **Connection Reporting System** designed to improve the reliability and monitoring of connections, specifically tailored for environments with restricted internet access (e.g., active censorship, GFW).
### Key Changes
1. **New Reporting API (`/report`)**:
* Added `ReportController` and `ReportService` to handle incoming connection reports.
* Endpoint receives data such as `Latency`, `Success` status, `Protocol`, and Client Interface details.
* Data is persisted to the database via the new `ConnectionReport` model.
2. **Subscription Link Updates**:
* Modified `subService` to append a `reportUrl` parameter to generated subscription links (VLESS, VMess, etc.).
* This allows compatible clients to automatically discover the reporting endpoint and send feedback.
3. **Database Integration**:
* Added `ConnectionReport` schema to `database/model` and registered it in `database/db.go` for auto-migration.
## Why is this helpful for Restricted Internet Locations?
In regions with heavy internet censorship, connection stability is volatile.
* **Dynamic Reporting Endpoint**: The `reportUrl` parameter embedded in the subscription link explicitly tells the client *where* to send connection data.
* **Bypassing Blocking**: By decoupling the reporting URL from the node address, clients can ensure diagnostic data reaches the panel even if specific node IPs are being interfered with (assuming the panel itself is reachable).
* **Real-time Network Intelligence**: This mechanism enables the panel to aggregate "ground truth" data from clients inside the restricted network (e.g., latency, accessibility of specific protocols), allowing admins to react faster to blocking events.
* **Protocol Performance Tracking**: Allows comparison of different protocols (Reality vs. VLESS+TLS vs. Trojan) based on real-world latency and success rates from actual users.
* **Rapid Troubleshooting**: Administrators can see connection quality trends and rotate IPs/domains proactively when success rates drop, minimizing downtime for users.
## Technical Details
* **API Endpoint**: `POST /report`
* **Payload Format**: JSON containing `SystemInfo` (Interface), `ConnectionQuality` (Latency, Success), and `ProtocolInfo`.
* **Security**: Reports are tied to valid client request contexts (implementation detail: ensure endpoint is rate-limited or authenticated if necessary, though currently designed for open reporting from valid sub links).
## How to Test
1. Update the panel.
2. Generate a subscription link.
3. Observe the `reportUrl` parameter in the link.
4. Simulate a client POST to the report URL and verify the entry in the `ConnectionReports` table.
2026-02-04 10:00:00 +00:00
|
|
|
|
// Set the new query values on the URL
|
|
|
|
|
|
url.RawQuery = q.Encode()
|
|
|
|
|
|
|
2023-08-26 11:41:12 +00:00
|
|
|
|
url.Fragment = s.genRemark(inbound, email, "")
|
# Pull Request: Connection Reporting System & Improvements for Restricted Networks
## Description
This PR introduces a comprehensive **Connection Reporting System** designed to improve the reliability and monitoring of connections, specifically tailored for environments with restricted internet access (e.g., active censorship, GFW).
### Key Changes
1. **New Reporting API (`/report`)**:
* Added `ReportController` and `ReportService` to handle incoming connection reports.
* Endpoint receives data such as `Latency`, `Success` status, `Protocol`, and Client Interface details.
* Data is persisted to the database via the new `ConnectionReport` model.
2. **Subscription Link Updates**:
* Modified `subService` to append a `reportUrl` parameter to generated subscription links (VLESS, VMess, etc.).
* This allows compatible clients to automatically discover the reporting endpoint and send feedback.
3. **Database Integration**:
* Added `ConnectionReport` schema to `database/model` and registered it in `database/db.go` for auto-migration.
## Why is this helpful for Restricted Internet Locations?
In regions with heavy internet censorship, connection stability is volatile.
* **Dynamic Reporting Endpoint**: The `reportUrl` parameter embedded in the subscription link explicitly tells the client *where* to send connection data.
* **Bypassing Blocking**: By decoupling the reporting URL from the node address, clients can ensure diagnostic data reaches the panel even if specific node IPs are being interfered with (assuming the panel itself is reachable).
* **Real-time Network Intelligence**: This mechanism enables the panel to aggregate "ground truth" data from clients inside the restricted network (e.g., latency, accessibility of specific protocols), allowing admins to react faster to blocking events.
* **Protocol Performance Tracking**: Allows comparison of different protocols (Reality vs. VLESS+TLS vs. Trojan) based on real-world latency and success rates from actual users.
* **Rapid Troubleshooting**: Administrators can see connection quality trends and rotate IPs/domains proactively when success rates drop, minimizing downtime for users.
## Technical Details
* **API Endpoint**: `POST /report`
* **Payload Format**: JSON containing `SystemInfo` (Interface), `ConnectionQuality` (Latency, Success), and `ProtocolInfo`.
* **Security**: Reports are tied to valid client request contexts (implementation detail: ensure endpoint is rate-limited or authenticated if necessary, though currently designed for open reporting from valid sub links).
## How to Test
1. Update the panel.
2. Generate a subscription link.
3. Observe the `reportUrl` parameter in the link.
4. Simulate a client POST to the report URL and verify the entry in the `ConnectionReports` table.
2026-02-04 10:00:00 +00:00
|
|
|
|
|
|
|
|
|
|
return s.appendReportUrl(url).String()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *SubService) appendReportUrl(u *url.URL) *url.URL {
|
|
|
|
|
|
// Construct report URL: https://<panel_address>/<base_path>/report
|
|
|
|
|
|
// Assuming s.address is the domain/IP.
|
|
|
|
|
|
// We need to know the protocol (http/https). For now, infer or use simple heuristic.
|
|
|
|
|
|
// Or better: pass the full "reportUrl" if we can derive it.
|
|
|
|
|
|
|
|
|
|
|
|
// Since we don't have the full context here easily, let's construct it based on s.address
|
|
|
|
|
|
// Caveat: port might be missing if on 80/443.
|
|
|
|
|
|
|
|
|
|
|
|
reportUrl := fmt.Sprintf("https://%s/report", s.address)
|
|
|
|
|
|
|
|
|
|
|
|
// Append as query param 'reportUrl' to the fragment or the query string?
|
|
|
|
|
|
// Standard Xray/V2ray config doesn't use this. The client needs to parse it.
|
|
|
|
|
|
// Putting it in the query string is safer for link parsers.
|
|
|
|
|
|
|
|
|
|
|
|
q := u.Query()
|
|
|
|
|
|
q.Set("reportUrl", reportUrl)
|
|
|
|
|
|
u.RawQuery = q.Encode()
|
|
|
|
|
|
return u
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-26 11:41:12 +00:00
|
|
|
|
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
|
2026-01-03 03:39:30 +00:00
|
|
|
|
var address string
|
|
|
|
|
|
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
|
|
|
|
|
address = s.address
|
|
|
|
|
|
} else {
|
|
|
|
|
|
address = inbound.Listen
|
|
|
|
|
|
}
|
2023-04-09 19:43:18 +00:00
|
|
|
|
if inbound.Protocol != model.Trojan {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
2025-03-12 19:13:51 +00:00
|
|
|
|
var stream map[string]any
|
2023-04-09 19:43:18 +00:00
|
|
|
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
2023-05-22 14:36:34 +00:00
|
|
|
|
clients, _ := s.inboundService.GetClients(inbound)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
clientIndex := -1
|
|
|
|
|
|
for i, client := range clients {
|
|
|
|
|
|
if client.Email == email {
|
|
|
|
|
|
clientIndex = i
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
password := clients[clientIndex].Password
|
|
|
|
|
|
port := inbound.Port
|
|
|
|
|
|
streamNetwork := stream["network"].(string)
|
|
|
|
|
|
params := make(map[string]string)
|
|
|
|
|
|
params["type"] = streamNetwork
|
|
|
|
|
|
|
|
|
|
|
|
switch streamNetwork {
|
|
|
|
|
|
case "tcp":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
tcp, _ := stream["tcpSettings"].(map[string]any)
|
|
|
|
|
|
header, _ := tcp["header"].(map[string]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
typeStr, _ := header["type"].(string)
|
|
|
|
|
|
if typeStr == "http" {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
request := header["request"].(map[string]any)
|
|
|
|
|
|
requestPath, _ := request["path"].([]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
params["path"] = requestPath[0].(string)
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := request["headers"].(map[string]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
params["host"] = searchHost(headers)
|
|
|
|
|
|
params["headerType"] = "http"
|
|
|
|
|
|
}
|
|
|
|
|
|
case "kcp":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
kcp, _ := stream["kcpSettings"].(map[string]any)
|
|
|
|
|
|
header, _ := kcp["header"].(map[string]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
params["headerType"] = header["type"].(string)
|
|
|
|
|
|
params["seed"] = kcp["seed"].(string)
|
|
|
|
|
|
case "ws":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
ws, _ := stream["wsSettings"].(map[string]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
params["path"] = ws["path"].(string)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
if host, ok := ws["host"].(string); ok && len(host) > 0 {
|
|
|
|
|
|
params["host"] = host
|
|
|
|
|
|
} else {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := ws["headers"].(map[string]any)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
params["host"] = searchHost(headers)
|
2024-04-01 12:32:02 +00:00
|
|
|
|
}
|
2023-04-09 19:43:18 +00:00
|
|
|
|
case "grpc":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
grpc, _ := stream["grpcSettings"].(map[string]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
params["serviceName"] = grpc["serviceName"].(string)
|
2024-04-01 07:39:45 +00:00
|
|
|
|
params["authority"], _ = grpc["authority"].(string)
|
2023-04-27 20:45:06 +00:00
|
|
|
|
if grpc["multiMode"].(bool) {
|
|
|
|
|
|
params["mode"] = "multi"
|
|
|
|
|
|
}
|
2024-03-11 07:36:33 +00:00
|
|
|
|
case "httpupgrade":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any)
|
2024-03-11 07:36:33 +00:00
|
|
|
|
params["path"] = httpupgrade["path"].(string)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
|
|
|
|
|
|
params["host"] = host
|
|
|
|
|
|
} else {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := httpupgrade["headers"].(map[string]any)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
params["host"] = searchHost(headers)
|
|
|
|
|
|
}
|
2024-12-03 21:24:34 +00:00
|
|
|
|
case "xhttp":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
xhttp, _ := stream["xhttpSettings"].(map[string]any)
|
2024-12-03 21:24:34 +00:00
|
|
|
|
params["path"] = xhttp["path"].(string)
|
|
|
|
|
|
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
|
2024-06-18 10:49:20 +00:00
|
|
|
|
params["host"] = host
|
|
|
|
|
|
} else {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := xhttp["headers"].(map[string]any)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
params["host"] = searchHost(headers)
|
2024-04-02 20:11:06 +00:00
|
|
|
|
}
|
2024-12-03 21:24:34 +00:00
|
|
|
|
params["mode"] = xhttp["mode"].(string)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
security, _ := stream["security"].(string)
|
|
|
|
|
|
if security == "tls" {
|
|
|
|
|
|
params["security"] = "tls"
|
2025-03-12 19:13:51 +00:00
|
|
|
|
tlsSetting, _ := stream["tlsSettings"].(map[string]any)
|
|
|
|
|
|
alpns, _ := tlsSetting["alpn"].([]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
var alpn []string
|
|
|
|
|
|
for _, a := range alpns {
|
|
|
|
|
|
alpn = append(alpn, a.(string))
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(alpn) > 0 {
|
|
|
|
|
|
params["alpn"] = strings.Join(alpn, ",")
|
|
|
|
|
|
}
|
2023-12-08 17:45:21 +00:00
|
|
|
|
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
|
|
|
|
|
|
params["sni"], _ = sniValue.(string)
|
|
|
|
|
|
}
|
2024-02-21 10:47:52 +00:00
|
|
|
|
|
2023-04-09 19:43:18 +00:00
|
|
|
|
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["allowInsecure"] = "1"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2023-04-11 19:00:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if security == "reality" {
|
|
|
|
|
|
params["security"] = "reality"
|
2025-03-12 19:13:51 +00:00
|
|
|
|
realitySetting, _ := stream["realitySettings"].(map[string]any)
|
2023-04-19 08:25:31 +00:00
|
|
|
|
realitySettings, _ := searchKey(realitySetting, "settings")
|
|
|
|
|
|
if realitySetting != nil {
|
|
|
|
|
|
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
sNames, _ := sniValue.([]any)
|
2024-03-11 10:04:15 +00:00
|
|
|
|
params["sni"] = sNames[random.Num(len(sNames))].(string)
|
2023-04-11 19:00:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
|
|
|
|
|
|
params["pbk"], _ = pbkValue.(string)
|
|
|
|
|
|
}
|
2023-06-07 09:15:58 +00:00
|
|
|
|
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
shortIds, _ := sidValue.([]any)
|
2024-03-11 10:04:15 +00:00
|
|
|
|
params["sid"] = shortIds[random.Num(len(shortIds))].(string)
|
2023-04-11 19:00:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
|
2023-04-19 08:25:31 +00:00
|
|
|
|
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
|
|
|
|
|
|
params["fp"] = fp
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-04 08:24:21 +00:00
|
|
|
|
if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok {
|
|
|
|
|
|
if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 {
|
|
|
|
|
|
params["pqv"] = pqv
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-03-11 10:04:15 +00:00
|
|
|
|
params["spx"] = "/" + random.Seq(15)
|
2023-04-11 19:00:24 +00:00
|
|
|
|
}
|
2023-04-12 08:44:07 +00:00
|
|
|
|
|
|
|
|
|
|
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
|
|
|
|
|
params["flow"] = clients[clientIndex].Flow
|
2023-04-09 19:43:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-29 11:50:25 +00:00
|
|
|
|
if security != "tls" && security != "reality" {
|
2023-06-14 13:36:56 +00:00
|
|
|
|
params["security"] = "none"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-12 19:13:51 +00:00
|
|
|
|
externalProxies, _ := stream["externalProxy"].([]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
|
2023-12-08 17:45:21 +00:00
|
|
|
|
if len(externalProxies) > 0 {
|
|
|
|
|
|
links := ""
|
|
|
|
|
|
for index, externalProxy := range externalProxies {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
ep, _ := externalProxy.(map[string]any)
|
2023-12-08 17:45:21 +00:00
|
|
|
|
newSecurity, _ := ep["forceTls"].(string)
|
|
|
|
|
|
dest, _ := ep["dest"].(string)
|
|
|
|
|
|
port := int(ep["port"].(float64))
|
|
|
|
|
|
link := fmt.Sprintf("trojan://%s@%s:%d", password, dest, port)
|
|
|
|
|
|
|
|
|
|
|
|
if newSecurity != "same" {
|
|
|
|
|
|
params["security"] = newSecurity
|
|
|
|
|
|
} else {
|
|
|
|
|
|
params["security"] = security
|
|
|
|
|
|
}
|
|
|
|
|
|
url, _ := url.Parse(link)
|
|
|
|
|
|
q := url.Query()
|
2023-04-09 19:43:18 +00:00
|
|
|
|
|
2023-12-08 17:45:21 +00:00
|
|
|
|
for k, v := range params {
|
|
|
|
|
|
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
|
|
|
|
|
|
q.Add(k, v)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2023-04-09 19:43:18 +00:00
|
|
|
|
|
2023-12-08 17:45:21 +00:00
|
|
|
|
// Set the new query values on the URL
|
|
|
|
|
|
url.RawQuery = q.Encode()
|
|
|
|
|
|
|
|
|
|
|
|
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
2023-04-09 19:43:18 +00:00
|
|
|
|
|
2023-05-22 14:36:34 +00:00
|
|
|
|
if index > 0 {
|
|
|
|
|
|
links += "\n"
|
|
|
|
|
|
}
|
|
|
|
|
|
links += url.String()
|
|
|
|
|
|
}
|
|
|
|
|
|
return links
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-08 17:45:21 +00:00
|
|
|
|
link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
|
|
|
|
|
|
|
|
|
|
|
|
url, _ := url.Parse(link)
|
|
|
|
|
|
q := url.Query()
|
|
|
|
|
|
|
|
|
|
|
|
for k, v := range params {
|
|
|
|
|
|
q.Add(k, v)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Set the new query values on the URL
|
|
|
|
|
|
url.RawQuery = q.Encode()
|
|
|
|
|
|
|
2023-08-26 11:41:12 +00:00
|
|
|
|
url.Fragment = s.genRemark(inbound, email, "")
|
2023-04-09 19:43:18 +00:00
|
|
|
|
return url.String()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-26 11:41:12 +00:00
|
|
|
|
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
|
2026-01-03 03:39:30 +00:00
|
|
|
|
var address string
|
|
|
|
|
|
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
|
|
|
|
|
address = s.address
|
|
|
|
|
|
} else {
|
|
|
|
|
|
address = inbound.Listen
|
|
|
|
|
|
}
|
2023-05-06 16:51:14 +00:00
|
|
|
|
if inbound.Protocol != model.Shadowsocks {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
2025-03-12 19:13:51 +00:00
|
|
|
|
var stream map[string]any
|
2023-07-17 23:49:01 +00:00
|
|
|
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
2023-05-22 14:36:34 +00:00
|
|
|
|
clients, _ := s.inboundService.GetClients(inbound)
|
2023-05-06 16:51:14 +00:00
|
|
|
|
|
2025-03-12 19:13:51 +00:00
|
|
|
|
var settings map[string]any
|
2023-05-06 16:51:14 +00:00
|
|
|
|
json.Unmarshal([]byte(inbound.Settings), &settings)
|
|
|
|
|
|
inboundPassword := settings["password"].(string)
|
|
|
|
|
|
method := settings["method"].(string)
|
|
|
|
|
|
clientIndex := -1
|
|
|
|
|
|
for i, client := range clients {
|
|
|
|
|
|
if client.Email == email {
|
|
|
|
|
|
clientIndex = i
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2023-07-17 23:49:01 +00:00
|
|
|
|
streamNetwork := stream["network"].(string)
|
|
|
|
|
|
params := make(map[string]string)
|
|
|
|
|
|
params["type"] = streamNetwork
|
|
|
|
|
|
|
|
|
|
|
|
switch streamNetwork {
|
|
|
|
|
|
case "tcp":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
tcp, _ := stream["tcpSettings"].(map[string]any)
|
|
|
|
|
|
header, _ := tcp["header"].(map[string]any)
|
2023-07-17 23:49:01 +00:00
|
|
|
|
typeStr, _ := header["type"].(string)
|
|
|
|
|
|
if typeStr == "http" {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
request := header["request"].(map[string]any)
|
|
|
|
|
|
requestPath, _ := request["path"].([]any)
|
2023-07-17 23:49:01 +00:00
|
|
|
|
params["path"] = requestPath[0].(string)
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := request["headers"].(map[string]any)
|
2023-07-17 23:49:01 +00:00
|
|
|
|
params["host"] = searchHost(headers)
|
|
|
|
|
|
params["headerType"] = "http"
|
|
|
|
|
|
}
|
|
|
|
|
|
case "kcp":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
kcp, _ := stream["kcpSettings"].(map[string]any)
|
|
|
|
|
|
header, _ := kcp["header"].(map[string]any)
|
2023-07-17 23:49:01 +00:00
|
|
|
|
params["headerType"] = header["type"].(string)
|
|
|
|
|
|
params["seed"] = kcp["seed"].(string)
|
|
|
|
|
|
case "ws":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
ws, _ := stream["wsSettings"].(map[string]any)
|
2023-07-17 23:49:01 +00:00
|
|
|
|
params["path"] = ws["path"].(string)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
if host, ok := ws["host"].(string); ok && len(host) > 0 {
|
|
|
|
|
|
params["host"] = host
|
|
|
|
|
|
} else {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := ws["headers"].(map[string]any)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
params["host"] = searchHost(headers)
|
2024-04-01 12:32:02 +00:00
|
|
|
|
}
|
2023-07-17 23:49:01 +00:00
|
|
|
|
case "grpc":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
grpc, _ := stream["grpcSettings"].(map[string]any)
|
2023-07-17 23:49:01 +00:00
|
|
|
|
params["serviceName"] = grpc["serviceName"].(string)
|
2024-04-01 07:39:45 +00:00
|
|
|
|
params["authority"], _ = grpc["authority"].(string)
|
2023-07-17 23:49:01 +00:00
|
|
|
|
if grpc["multiMode"].(bool) {
|
|
|
|
|
|
params["mode"] = "multi"
|
|
|
|
|
|
}
|
2024-03-11 07:36:33 +00:00
|
|
|
|
case "httpupgrade":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any)
|
2024-03-11 07:36:33 +00:00
|
|
|
|
params["path"] = httpupgrade["path"].(string)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
|
|
|
|
|
|
params["host"] = host
|
|
|
|
|
|
} else {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := httpupgrade["headers"].(map[string]any)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
params["host"] = searchHost(headers)
|
|
|
|
|
|
}
|
2024-12-03 21:24:34 +00:00
|
|
|
|
case "xhttp":
|
2025-03-12 19:13:51 +00:00
|
|
|
|
xhttp, _ := stream["xhttpSettings"].(map[string]any)
|
2024-12-03 21:24:34 +00:00
|
|
|
|
params["path"] = xhttp["path"].(string)
|
|
|
|
|
|
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
|
2024-06-18 10:49:20 +00:00
|
|
|
|
params["host"] = host
|
|
|
|
|
|
} else {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
headers, _ := xhttp["headers"].(map[string]any)
|
2024-06-18 10:49:20 +00:00
|
|
|
|
params["host"] = searchHost(headers)
|
2024-04-02 20:11:06 +00:00
|
|
|
|
}
|
2024-12-03 21:24:34 +00:00
|
|
|
|
params["mode"] = xhttp["mode"].(string)
|
2023-07-17 23:49:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-08 17:45:21 +00:00
|
|
|
|
security, _ := stream["security"].(string)
|
|
|
|
|
|
if security == "tls" {
|
|
|
|
|
|
params["security"] = "tls"
|
2025-03-12 19:13:51 +00:00
|
|
|
|
tlsSetting, _ := stream["tlsSettings"].(map[string]any)
|
|
|
|
|
|
alpns, _ := tlsSetting["alpn"].([]any)
|
2023-12-08 17:45:21 +00: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["allowInsecure"] = "1"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-07-27 08:28:12 +00:00
|
|
|
|
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 17:45:21 +00:00
|
|
|
|
|
2025-03-12 19:13:51 +00:00
|
|
|
|
externalProxies, _ := stream["externalProxy"].([]any)
|
2023-12-08 17:45:21 +00:00
|
|
|
|
|
|
|
|
|
|
if len(externalProxies) > 0 {
|
|
|
|
|
|
links := ""
|
|
|
|
|
|
for index, externalProxy := range externalProxies {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
ep, _ := externalProxy.(map[string]any)
|
2023-12-08 17:45:21 +00:00
|
|
|
|
newSecurity, _ := ep["forceTls"].(string)
|
|
|
|
|
|
dest, _ := ep["dest"].(string)
|
|
|
|
|
|
port := int(ep["port"].(float64))
|
|
|
|
|
|
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), dest, port)
|
|
|
|
|
|
|
|
|
|
|
|
if newSecurity != "same" {
|
|
|
|
|
|
params["security"] = newSecurity
|
|
|
|
|
|
} else {
|
|
|
|
|
|
params["security"] = security
|
|
|
|
|
|
}
|
|
|
|
|
|
url, _ := url.Parse(link)
|
|
|
|
|
|
q := url.Query()
|
|
|
|
|
|
|
|
|
|
|
|
for k, v := range params {
|
|
|
|
|
|
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
|
|
|
|
|
|
q.Add(k, v)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Set the new query values on the URL
|
|
|
|
|
|
url.RawQuery = q.Encode()
|
|
|
|
|
|
|
|
|
|
|
|
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
|
|
|
|
|
|
|
|
|
|
|
if index > 0 {
|
|
|
|
|
|
links += "\n"
|
|
|
|
|
|
}
|
|
|
|
|
|
links += url.String()
|
|
|
|
|
|
}
|
|
|
|
|
|
return links
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-07-17 23:49:01 +00:00
|
|
|
|
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
|
|
|
|
|
|
url, _ := url.Parse(link)
|
|
|
|
|
|
q := url.Query()
|
|
|
|
|
|
|
|
|
|
|
|
for k, v := range params {
|
|
|
|
|
|
q.Add(k, v)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Set the new query values on the URL
|
|
|
|
|
|
url.RawQuery = q.Encode()
|
2023-12-08 17:45:21 +00:00
|
|
|
|
|
2023-08-26 11:41:12 +00:00
|
|
|
|
url.Fragment = s.genRemark(inbound, email, "")
|
|
|
|
|
|
return url.String()
|
|
|
|
|
|
}
|
2023-07-17 23:49:01 +00:00
|
|
|
|
|
2023-08-26 11:41:12 +00:00
|
|
|
|
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
|
2023-12-08 19:31:17 +00:00
|
|
|
|
separationChar := string(s.remarkModel[0])
|
|
|
|
|
|
orderChars := s.remarkModel[1:]
|
|
|
|
|
|
orders := map[byte]string{
|
|
|
|
|
|
'i': "",
|
|
|
|
|
|
'e': "",
|
|
|
|
|
|
'o': "",
|
|
|
|
|
|
}
|
2023-08-26 11:41:12 +00:00
|
|
|
|
if len(email) > 0 {
|
2023-12-08 19:31:17 +00:00
|
|
|
|
orders['e'] = email
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(inbound.Remark) > 0 {
|
|
|
|
|
|
orders['i'] = inbound.Remark
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(extra) > 0 {
|
2023-12-10 17:13:48 +00:00
|
|
|
|
orders['o'] = extra
|
2023-12-08 19:31:17 +00: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 11:41:12 +00:00
|
|
|
|
}
|
2023-08-01 20:58:51 +00:00
|
|
|
|
}
|
2023-08-26 11:41:12 +00:00
|
|
|
|
|
|
|
|
|
|
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 19:31:17 +00:00
|
|
|
|
return fmt.Sprintf("⛔️N/A%s%s", separationChar, strings.Join(remark, separationChar))
|
2023-08-26 11:41:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
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 17:11:40 +00: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 11:41:12 +00:00
|
|
|
|
case exp < 0:
|
2024-07-17 14:39:53 +00:00
|
|
|
|
days := exp / -86400
|
|
|
|
|
|
hours := (exp % -86400) / 3600
|
|
|
|
|
|
minutes := (exp % -3600) / 60
|
2024-07-01 17:11:40 +00: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 11:41:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2023-12-08 19:31:17 +00:00
|
|
|
|
return strings.Join(remark, separationChar)
|
2023-05-06 16:51:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-12 19:13:51 +00:00
|
|
|
|
func searchKey(data any, key string) (any, bool) {
|
2023-04-09 19:43:18 +00:00
|
|
|
|
switch val := data.(type) {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
case map[string]any:
|
2023-04-09 19:43:18 +00:00
|
|
|
|
for k, v := range val {
|
|
|
|
|
|
if k == key {
|
|
|
|
|
|
return v, true
|
|
|
|
|
|
}
|
|
|
|
|
|
if result, ok := searchKey(v, key); ok {
|
|
|
|
|
|
return result, true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-12 19:13:51 +00:00
|
|
|
|
case []any:
|
2023-04-09 19:43:18 +00:00
|
|
|
|
for _, v := range val {
|
|
|
|
|
|
if result, ok := searchKey(v, key); ok {
|
|
|
|
|
|
return result, true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil, false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-12 19:13:51 +00:00
|
|
|
|
func searchHost(headers any) string {
|
|
|
|
|
|
data, _ := headers.(map[string]any)
|
2023-04-09 19:43:18 +00:00
|
|
|
|
for k, v := range data {
|
|
|
|
|
|
if strings.EqualFold(k, "host") {
|
|
|
|
|
|
switch v.(type) {
|
2025-03-12 19:13:51 +00:00
|
|
|
|
case []any:
|
|
|
|
|
|
hosts, _ := v.([]any)
|
2023-04-25 11:09:09 +00:00
|
|
|
|
if len(hosts) > 0 {
|
|
|
|
|
|
return hosts[0].(string)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
2025-03-12 19:13:51 +00:00
|
|
|
|
case any:
|
2023-04-09 19:43:18 +00:00
|
|
|
|
return v.(string)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
2025-09-13 23:22:42 +00:00
|
|
|
|
|
2025-09-18 10:20:21 +00:00
|
|
|
|
// PageData is a view model for subpage.html
|
2025-09-20 07:35:50 +00:00
|
|
|
|
// PageData contains data for rendering the subscription information page.
|
2025-09-13 23:22:42 +00:00
|
|
|
|
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.
|
2025-09-20 07:35:50 +00:00
|
|
|
|
// ResolveRequest extracts scheme, host, and header information from an HTTP request.
|
2025-09-13 23:22:42 +00: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 19:20:37 +00: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.
|
2025-09-13 23:22:42 +00:00
|
|
|
|
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {
|
2025-09-21 19:20:37 +00:00
|
|
|
|
// Input validation
|
|
|
|
|
|
if subId == "" {
|
|
|
|
|
|
return "", ""
|
2025-09-13 23:22:42 +00:00
|
|
|
|
}
|
2025-09-21 19:20:37 +00:00
|
|
|
|
|
|
|
|
|
|
// Get configured URIs first (highest priority)
|
|
|
|
|
|
configuredSubURI, _ := s.settingService.GetSubURI()
|
|
|
|
|
|
configuredSubJsonURI, _ := s.settingService.GetSubJsonURI()
|
|
|
|
|
|
|
|
|
|
|
|
// Determine base scheme and host (cached to avoid duplicate calls)
|
|
|
|
|
|
var baseScheme, baseHostWithPort string
|
|
|
|
|
|
if configuredSubURI == "" || configuredSubJsonURI == "" {
|
|
|
|
|
|
baseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort)
|
2025-09-13 23:22:42 +00:00
|
|
|
|
}
|
2025-09-21 19:20:37 +00:00
|
|
|
|
|
|
|
|
|
|
// Build subscription URL
|
|
|
|
|
|
subURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId)
|
|
|
|
|
|
|
|
|
|
|
|
// Build JSON subscription URL
|
|
|
|
|
|
subJsonURL = s.buildSingleURL(configuredSubJsonURI, baseScheme, baseHostWithPort, subJsonPath, subId)
|
|
|
|
|
|
|
|
|
|
|
|
return subURL, subJsonURL
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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-13 23:22:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BuildPageData parses header and prepares the template view model.
|
2025-09-20 07:35:50 +00:00
|
|
|
|
// BuildPageData constructs page data for rendering the subscription information page.
|
2025-09-24 17:51:01 +00:00
|
|
|
|
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string, basePath string) PageData {
|
2025-09-14 21:08:09 +00:00
|
|
|
|
download := common.FormatTraffic(traffic.Down)
|
|
|
|
|
|
upload := common.FormatTraffic(traffic.Up)
|
2025-09-13 23:22:42 +00:00
|
|
|
|
total := "∞"
|
2025-09-14 21:08:09 +00:00
|
|
|
|
used := common.FormatTraffic(traffic.Up + traffic.Down)
|
2025-09-13 23:22:42 +00:00
|
|
|
|
remained := ""
|
2025-09-14 21:08:09 +00:00
|
|
|
|
if traffic.Total > 0 {
|
|
|
|
|
|
total = common.FormatTraffic(traffic.Total)
|
2025-09-21 19:20:37 +00:00
|
|
|
|
left := max(traffic.Total-(traffic.Up+traffic.Down), 0)
|
2025-09-13 23:22:42 +00:00
|
|
|
|
remained = common.FormatTraffic(left)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
datepicker := s.datepicker
|
|
|
|
|
|
if datepicker == "" {
|
|
|
|
|
|
datepicker = "gregorian"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return PageData{
|
|
|
|
|
|
Host: hostHeader,
|
2025-09-24 17:51:01 +00:00
|
|
|
|
BasePath: basePath,
|
2025-09-13 23:22:42 +00:00
|
|
|
|
SId: subId,
|
|
|
|
|
|
Download: download,
|
|
|
|
|
|
Upload: upload,
|
|
|
|
|
|
Total: total,
|
|
|
|
|
|
Used: used,
|
|
|
|
|
|
Remained: remained,
|
2025-09-14 21:08:09 +00:00
|
|
|
|
Expire: traffic.ExpiryTime / 1000,
|
2025-09-13 23:22:42 +00:00
|
|
|
|
LastOnline: lastOnline,
|
|
|
|
|
|
Datepicker: datepicker,
|
2025-09-14 21:08:09 +00:00
|
|
|
|
DownloadByte: traffic.Down,
|
|
|
|
|
|
UploadByte: traffic.Up,
|
|
|
|
|
|
TotalByte: traffic.Total,
|
2025-09-13 23:22:42 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|