소스 검색

[sub] json + fragment

Co-Authored-By: Alireza Ahmadi <[email protected]>
MHSanaei 1 년 전
부모
커밋
03b7a34793

+ 105 - 0
sub/default.json

@@ -0,0 +1,105 @@
+{
+  "dns": {
+    "tag": "dns_out",
+    "queryStrategy": "UseIP",
+    "servers": [
+      {
+        "address": "8.8.8.8",
+        "skipFallback": false
+      }
+    ]
+  },
+  "inbounds": [
+    {
+      "port": 10808,
+      "protocol": "socks",
+      "settings": {
+        "auth": "noauth",
+        "udp": true,
+        "userLevel": 8
+      },
+      "sniffing": {
+        "destOverride": [
+          "http",
+          "tls",
+          "fakedns"
+        ],
+        "enabled": true
+      },
+      "tag": "socks"
+    },
+    {
+      "port": 10809,
+      "protocol": "http",
+      "settings": {
+        "userLevel": 8
+      },
+      "tag": "http"
+    }
+  ],
+  "log": {
+    "loglevel": "warning"
+  },
+  "outbounds": [
+    {
+      "tag": "direct",
+      "protocol": "freedom",
+      "settings": {
+        "domainStrategy": "UseIP"
+      }
+    },
+    {
+      "tag": "block",
+      "protocol": "blackhole",
+      "settings": {
+        "response": {
+          "type": "http"
+        }
+      }
+    }
+  ],
+  "policy": {
+    "levels": {
+      "8": {
+        "connIdle": 300,
+        "downlinkOnly": 1,
+        "handshake": 4,
+        "uplinkOnly": 1
+      }
+    },
+    "system": {
+      "statsOutboundUplink": true,
+      "statsOutboundDownlink": true
+    }
+  },
+  "routing": {
+    "domainStrategy": "AsIs",
+    "rules": [
+      {
+        "type": "field",
+        "network": "tcp,udp",
+        "balancerTag": "all"
+      }
+    ],
+    "balancers": [
+      {
+        "tag": "all",
+        "selector": [
+          "proxy"
+        ],
+        "strategy": {
+          "type": "leastPing"
+        }
+      }
+    ]
+  },
+  "observatory": {
+    "probeInterval": "5m",
+    "probeURL": "https://api.github.com/_private/browser/stats",
+    "subjectSelector": [
+      "proxy"
+    ],
+    "EnableConcurrency": true
+  },
+  "stats": {}
+}

+ 36 - 6
sub/sub.go

@@ -47,23 +47,53 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 
 
 	engine := gin.Default()
 	engine := gin.Default()
 
 
-	subPath, err := s.settingService.GetSubPath()
+	subDomain, err := s.settingService.GetSubDomain()
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	subDomain, err := s.settingService.GetSubDomain()
+	if subDomain != "" {
+		engine.Use(middleware.DomainValidatorMiddleware(subDomain))
+	}
+
+	LinksPath, err := s.settingService.GetSubPath()
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	if subDomain != "" {
-		engine.Use(middleware.DomainValidatorMiddleware(subDomain))
+	JsonPath, err := s.settingService.GetSubJsonPath()
+	if err != nil {
+		return nil, err
+	}
+
+	Encrypt, err := s.settingService.GetSubEncrypt()
+	if err != nil {
+		return nil, err
+	}
+
+	ShowInfo, err := s.settingService.GetSubShowInfo()
+	if err != nil {
+		return nil, err
+	}
+
+	RemarkModel, err := s.settingService.GetRemarkModel()
+	if err != nil {
+		RemarkModel = "-ieo"
+	}
+
+	SubUpdates, err := s.settingService.GetSubUpdates()
+	if err != nil {
+		SubUpdates = "10"
+	}
+
+	SubJsonFragment, err := s.settingService.GetSubJsonFragment()
+	if err != nil {
+		SubJsonFragment = ""
 	}
 	}
 
 
-	g := engine.Group(subPath)
+	g := engine.Group("/")
 
 
-	s.sub = NewSUBController(g)
+	s.sub = NewSUBController(g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates, SubJsonFragment)
 
 
 	return engine, nil
 	return engine, nil
 }
 }

+ 55 - 14
sub/subController.go

@@ -3,34 +3,57 @@ package sub
 import (
 import (
 	"encoding/base64"
 	"encoding/base64"
 	"strings"
 	"strings"
-	"x-ui/web/service"
 
 
 	"github.com/gin-gonic/gin"
 	"github.com/gin-gonic/gin"
 )
 )
 
 
 type SUBController struct {
 type SUBController struct {
-	subService     SubService
-	settingService service.SettingService
+	subPath        string
+	subJsonPath    string
+	subEncrypt     bool
+	updateInterval string
+
+	subService     *SubService
+	subJsonService *SubJsonService
 }
 }
 
 
-func NewSUBController(g *gin.RouterGroup) *SUBController {
-	a := &SUBController{}
+func NewSUBController(
+	g *gin.RouterGroup,
+	subPath string,
+	jsonPath string,
+	encrypt bool,
+	showInfo bool,
+	rModel string,
+	update string,
+	jsonFragment string) *SUBController {
+
+	a := &SUBController{
+		subPath:        subPath,
+		subJsonPath:    jsonPath,
+		subEncrypt:     encrypt,
+		updateInterval: update,
+
+		subService:     NewSubService(showInfo, rModel),
+		subJsonService: NewSubJsonService(jsonFragment),
+	}
 	a.initRouter(g)
 	a.initRouter(g)
 	return a
 	return a
 }
 }
 
 
 func (a *SUBController) initRouter(g *gin.RouterGroup) {
 func (a *SUBController) initRouter(g *gin.RouterGroup) {
-	g = g.Group("/")
+	gLink := g.Group(a.subPath)
+	gJson := g.Group(a.subJsonPath)
 
 
-	g.GET("/:subid", a.subs)
+	gLink.GET(":subid", a.subs)
+
+	gJson.GET(":subid", a.subJsons)
 }
 }
 
 
 func (a *SUBController) subs(c *gin.Context) {
 func (a *SUBController) subs(c *gin.Context) {
-	subEncrypt, _ := a.settingService.GetSubEncrypt()
-	subShowInfo, _ := a.settingService.GetSubShowInfo()
+	println(c.Request.Header["User-Agent"][0])
 	subId := c.Param("subid")
 	subId := c.Param("subid")
 	host := strings.Split(c.Request.Host, ":")[0]
 	host := strings.Split(c.Request.Host, ":")[0]
-	subs, headers, err := a.subService.GetSubs(subId, host, subShowInfo)
+	subs, header, err := a.subService.GetSubs(subId, host)
 	if err != nil || len(subs) == 0 {
 	if err != nil || len(subs) == 0 {
 		c.String(400, "Error!")
 		c.String(400, "Error!")
 	} else {
 	} else {
@@ -40,14 +63,32 @@ func (a *SUBController) subs(c *gin.Context) {
 		}
 		}
 
 
 		// Add headers
 		// Add headers
-		c.Writer.Header().Set("Subscription-Userinfo", headers[0])
-		c.Writer.Header().Set("Profile-Update-Interval", headers[1])
-		c.Writer.Header().Set("Profile-Title", headers[2])
+		c.Writer.Header().Set("Subscription-Userinfo", header)
+		c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
+		c.Writer.Header().Set("Profile-Title", subId)
 
 
-		if subEncrypt {
+		if a.subEncrypt {
 			c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
 			c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
 		} else {
 		} else {
 			c.String(200, result)
 			c.String(200, result)
 		}
 		}
 	}
 	}
 }
 }
+
+func (a *SUBController) subJsons(c *gin.Context) {
+	println(c.Request.Header["User-Agent"][0])
+	subId := c.Param("subid")
+	host := strings.Split(c.Request.Host, ":")[0]
+	jsonSub, header, err := a.subJsonService.GetJson(subId, host)
+	if err != nil || len(jsonSub) == 0 {
+		c.String(400, "Error!")
+	} else {
+
+		// Add headers
+		c.Writer.Header().Set("Subscription-Userinfo", header)
+		c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
+		c.Writer.Header().Set("Profile-Title", subId)
+
+		c.String(200, jsonSub)
+	}
+}

+ 355 - 0
sub/subJsonService.go

@@ -0,0 +1,355 @@
+package sub
+
+import (
+	_ "embed"
+	"encoding/json"
+	"fmt"
+	"strings"
+	"x-ui/database/model"
+	"x-ui/logger"
+	"x-ui/util/json_util"
+	"x-ui/util/random"
+	"x-ui/web/service"
+	"x-ui/xray"
+)
+
+//go:embed default.json
+var defaultJson string
+
+type SubJsonService struct {
+	fragmanet string
+
+	inboundService service.InboundService
+	SubService
+}
+
+func NewSubJsonService(fragment string) *SubJsonService {
+	return &SubJsonService{
+		fragmanet: fragment,
+	}
+}
+
+func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
+	inbounds, err := s.SubService.getInboundsBySubId(subId)
+	if err != nil || len(inbounds) == 0 {
+		return "", "", err
+	}
+
+	var header string
+	var traffic xray.ClientTraffic
+	var clientTraffics []xray.ClientTraffic
+	var configJson map[string]interface{}
+	var defaultOutbounds []json_util.RawMessage
+
+	json.Unmarshal([]byte(defaultJson), &configJson)
+	if outboundSlices, ok := configJson["outbounds"].([]interface{}); ok {
+		for _, defaultOutbound := range outboundSlices {
+			jsonBytes, _ := json.Marshal(defaultOutbound)
+			defaultOutbounds = append(defaultOutbounds, jsonBytes)
+		}
+	}
+
+	outbounds := []json_util.RawMessage{}
+	startIndex := 0
+	// Prepare Inbounds
+	for _, inbound := range inbounds {
+		clients, err := s.inboundService.GetClients(inbound)
+		if err != nil {
+			logger.Error("SubJsonService - GetClients: Unable to get clients from inbound")
+		}
+		if clients == nil {
+			continue
+		}
+		if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
+			listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
+			if err == nil {
+				inbound.Listen = listen
+				inbound.Port = port
+				inbound.StreamSettings = streamSettings
+			}
+		}
+
+		var subClients []model.Client
+		for _, client := range clients {
+			if client.Enable && client.SubID == subId {
+				subClients = append(subClients, client)
+				clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email))
+			}
+		}
+
+		outbound := s.getOutbound(inbound, subClients, host, startIndex)
+		if outbound != nil {
+			outbounds = append(outbounds, outbound...)
+			startIndex += len(outbound)
+		}
+	}
+
+	if len(outbounds) == 0 {
+		return "", "", nil
+	}
+
+	// Prepare statistics
+	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
+			}
+		}
+	}
+
+	if s.fragmanet != "" {
+		outbounds = append(outbounds, json_util.RawMessage(s.fragmanet))
+	}
+
+	// Combile outbounds
+	outbounds = append(outbounds, defaultOutbounds...)
+	configJson["outbounds"] = outbounds
+	finalJson, _ := json.MarshalIndent(configJson, "", "  ")
+
+	header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
+	return string(finalJson), header, nil
+}
+
+func (s *SubJsonService) getOutbound(inbound *model.Inbound, clients []model.Client, host string, startIndex int) []json_util.RawMessage {
+	var newOutbounds []json_util.RawMessage
+	stream := s.streamData(inbound.StreamSettings)
+
+	externalProxies, ok := stream["externalProxy"].([]interface{})
+	if !ok || len(externalProxies) == 0 {
+		externalProxies = []interface{}{
+			map[string]interface{}{
+				"forceTls": "same",
+				"dest":     host,
+				"port":     float64(inbound.Port),
+			},
+		}
+	}
+
+	delete(stream, "externalProxy")
+
+	config_index := startIndex
+	for _, ep := range externalProxies {
+		extPrxy := ep.(map[string]interface{})
+		inbound.Listen = extPrxy["dest"].(string)
+		inbound.Port = int(extPrxy["port"].(float64))
+		newStream := stream
+		switch extPrxy["forceTls"].(string) {
+		case "tls":
+			if newStream["security"] != "tls" {
+				newStream["security"] = "tls"
+				newStream["tslSettings"] = map[string]interface{}{}
+			}
+		case "none":
+			if newStream["security"] != "none" {
+				newStream["security"] = "none"
+				delete(newStream, "tslSettings")
+			}
+		}
+		streamSettings, _ := json.MarshalIndent(newStream, "", "  ")
+		inbound.StreamSettings = string(streamSettings)
+
+		for _, client := range clients {
+			inbound.Tag = fmt.Sprintf("proxy_%d", config_index)
+			switch inbound.Protocol {
+			case "vmess", "vless":
+				newOutbounds = append(newOutbounds, s.genVnext(inbound, client))
+			case "trojan", "shadowsocks":
+				newOutbounds = append(newOutbounds, s.genServer(inbound, client))
+			}
+			config_index += 1
+		}
+	}
+
+	return newOutbounds
+}
+
+func (s *SubJsonService) streamData(stream string) map[string]interface{} {
+	var streamSettings map[string]interface{}
+	json.Unmarshal([]byte(stream), &streamSettings)
+	security, _ := streamSettings["security"].(string)
+	if security == "tls" {
+		streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]interface{}))
+	} else if security == "reality" {
+		streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]interface{}))
+	}
+	delete(streamSettings, "sockopt")
+
+	if s.fragmanet != "" {
+		streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "TcpNoDelay": true}`)
+	}
+
+	// remove proxy protocol
+	network, _ := streamSettings["network"].(string)
+	switch network {
+	case "tcp":
+		streamSettings["tcpSettings"] = s.removeAcceptProxy(streamSettings["tcpSettings"])
+	case "ws":
+		streamSettings["wsSettings"] = s.removeAcceptProxy(streamSettings["wsSettings"])
+	}
+
+	return streamSettings
+}
+
+func (s *SubJsonService) removeAcceptProxy(setting interface{}) map[string]interface{} {
+	netSettings, ok := setting.(map[string]interface{})
+	if ok {
+		delete(netSettings, "acceptProxyProtocol")
+	}
+	return netSettings
+}
+
+func (s *SubJsonService) tlsData(tData map[string]interface{}) map[string]interface{} {
+	tlsData := make(map[string]interface{}, 1)
+	tlsClientSettings := tData["settings"].(map[string]interface{})
+
+	tlsData["serverName"] = tData["serverName"]
+	tlsData["alpn"] = tData["alpn"]
+	if allowInsecure, ok := tlsClientSettings["allowInsecure"].(string); ok {
+		tlsData["allowInsecure"] = allowInsecure
+	}
+	if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
+		tlsData["fingerprint"] = fingerprint
+	}
+	return tlsData
+}
+
+func (s *SubJsonService) realityData(rData map[string]interface{}) map[string]interface{} {
+	rltyData := make(map[string]interface{}, 1)
+	rltyClientSettings := rData["settings"].(map[string]interface{})
+
+	rltyData["show"] = false
+	rltyData["publicKey"] = rltyClientSettings["publicKey"]
+	rltyData["fingerprint"] = rltyClientSettings["fingerprint"]
+
+	// Set random data
+	rltyData["spiderX"] = "/" + random.Seq(15)
+	shortIds, ok := rData["shortIds"].([]interface{})
+	if ok && len(shortIds) > 0 {
+		rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string)
+	} else {
+		rltyData["shortId"] = ""
+	}
+	serverNames, ok := rData["serverNames"].([]interface{})
+	if ok && len(serverNames) > 0 {
+		rltyData["serverName"] = serverNames[random.Num(len(serverNames))].(string)
+	} else {
+		rltyData["serverName"] = ""
+	}
+
+	return rltyData
+}
+
+func (s *SubJsonService) genVnext(inbound *model.Inbound, client model.Client) json_util.RawMessage {
+	outbound := Outbound{}
+	usersData := make([]UserVnext, 1)
+
+	usersData[0].ID = client.ID
+	usersData[0].Level = 8
+	if inbound.Protocol == model.VLESS {
+		usersData[0].Flow = client.Flow
+		usersData[0].Encryption = "none"
+	}
+
+	vnextData := make([]VnextSetting, 1)
+	vnextData[0] = VnextSetting{
+		Address: inbound.Listen,
+		Port:    inbound.Port,
+		Users:   usersData,
+	}
+
+	outbound.Protocol = string(inbound.Protocol)
+	outbound.Tag = inbound.Tag
+	outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings)
+	outbound.Settings = OutboundSettings{
+		Vnext: vnextData,
+	}
+
+	result, _ := json.MarshalIndent(outbound, "", "  ")
+	return result
+}
+
+func (s *SubJsonService) genServer(inbound *model.Inbound, client model.Client) json_util.RawMessage {
+	outbound := Outbound{}
+
+	serverData := make([]ServerSetting, 1)
+	serverData[0] = ServerSetting{
+		Address:  inbound.Listen,
+		Port:     inbound.Port,
+		Level:    8,
+		Password: client.Password,
+	}
+
+	if inbound.Protocol == model.Shadowsocks {
+		var inboundSettings map[string]interface{}
+		json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
+		method, _ := inboundSettings["method"].(string)
+		serverData[0].Method = method
+
+		// server password in multi-user 2022 protocols
+		if strings.HasPrefix(method, "2022") {
+			if serverPassword, ok := inboundSettings["password"].(string); ok {
+				serverData[0].Password = fmt.Sprintf("%s:%s", serverPassword, client.Password)
+			}
+		}
+	}
+
+	outbound.Protocol = string(inbound.Protocol)
+	outbound.Tag = inbound.Tag
+	outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings)
+	outbound.Settings = OutboundSettings{
+		Servers: serverData,
+	}
+
+	result, _ := json.MarshalIndent(outbound, "", "  ")
+	return result
+}
+
+type Outbound struct {
+	Protocol       string                 `json:"protocol"`
+	Tag            string                 `json:"tag"`
+	StreamSettings json_util.RawMessage   `json:"streamSettings"`
+	Mux            map[string]interface{} `json:"mux,omitempty"`
+	ProxySettings  map[string]interface{} `json:"proxySettings,omitempty"`
+	Settings       OutboundSettings       `json:"settings,omitempty"`
+}
+
+type OutboundSettings struct {
+	Vnext   []VnextSetting  `json:"vnext,omitempty"`
+	Servers []ServerSetting `json:"servers,omitempty"`
+}
+
+type VnextSetting struct {
+	Address string      `json:"address"`
+	Port    int         `json:"port"`
+	Users   []UserVnext `json:"users"`
+}
+
+type UserVnext struct {
+	Encryption string `json:"encryption,omitempty"`
+	Flow       string `json:"flow,omitempty"`
+	ID         string `json:"id"`
+	Level      int    `json:"level"`
+}
+
+type ServerSetting struct {
+	Password string `json:"password"`
+	Level    int    `json:"level"`
+	Address  string `json:"address"`
+	Port     int    `json:"port"`
+	Flow     string `json:"flow,omitempty"`
+	Method   string `json:"method,omitempty"`
+}

+ 36 - 31
sub/subService.go

@@ -25,47 +25,42 @@ type SubService struct {
 	settingService service.SettingService
 	settingService service.SettingService
 }
 }
 
 
-func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string, []string, error) {
+func NewSubService(showInfo bool, remarkModel string) *SubService {
+	return &SubService{
+		showInfo:    showInfo,
+		remarkModel: remarkModel,
+	}
+}
+
+func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
 	s.address = host
 	s.address = host
-	s.showInfo = showInfo
 	var result []string
 	var result []string
-	var headers []string
+	var header string
 	var traffic xray.ClientTraffic
 	var traffic xray.ClientTraffic
 	var clientTraffics []xray.ClientTraffic
 	var clientTraffics []xray.ClientTraffic
 	inbounds, err := s.getInboundsBySubId(subId)
 	inbounds, err := s.getInboundsBySubId(subId)
 	if err != nil {
 	if err != nil {
-		return nil, nil, err
+		return nil, "", err
 	}
 	}
-	s.remarkModel, err = s.settingService.GetRemarkModel()
+
+	s.datepicker, err = s.settingService.GetDatepicker()
 	if err != nil {
 	if err != nil {
-		s.remarkModel = "-ieo"
+		s.datepicker = "gregorian"
 	}
 	}
-	s.datepicker, err = s.settingService.GetDatepicker()
-    if err != nil {
-        s.datepicker = "gregorian"
-    }
 	for _, inbound := range inbounds {
 	for _, inbound := range inbounds {
 		clients, err := s.inboundService.GetClients(inbound)
 		clients, err := s.inboundService.GetClients(inbound)
 		if err != nil {
 		if err != nil {
-			logger.Error("SubService - GetSub: Unable to get clients from inbound")
+			logger.Error("SubService - GetClients: Unable to get clients from inbound")
 		}
 		}
 		if clients == nil {
 		if clients == nil {
 			continue
 			continue
 		}
 		}
 		if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
 		if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
-			fallbackMaster, err := s.getFallbackMaster(inbound.Listen)
+			listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
 			if err == nil {
 			if err == nil {
-				inbound.Listen = fallbackMaster.Listen
-				inbound.Port = fallbackMaster.Port
-				var stream map[string]interface{}
-				json.Unmarshal([]byte(inbound.StreamSettings), &stream)
-				var masterStream map[string]interface{}
-				json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
-				stream["security"] = masterStream["security"]
-				stream["tlsSettings"] = masterStream["tlsSettings"]
-				stream["externalProxy"] = masterStream["externalProxy"]
-				modifiedStream, _ := json.MarshalIndent(stream, "", "  ")
-				inbound.StreamSettings = string(modifiedStream)
+				inbound.Listen = listen
+				inbound.Port = port
+				inbound.StreamSettings = streamSettings
 			}
 			}
 		}
 		}
 		for _, client := range clients {
 		for _, client := range clients {
@@ -76,6 +71,8 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
 			}
 			}
 		}
 		}
 	}
 	}
+
+	// Prepare statistics
 	for index, clientTraffic := range clientTraffics {
 	for index, clientTraffic := range clientTraffics {
 		if index == 0 {
 		if index == 0 {
 			traffic.Up = clientTraffic.Up
 			traffic.Up = clientTraffic.Up
@@ -97,11 +94,8 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
 			}
 			}
 		}
 		}
 	}
 	}
-	headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
-	updateInterval, _ := s.settingService.GetSubUpdates()
-	headers = append(headers, fmt.Sprintf("%d", updateInterval))
-	headers = append(headers, subId)
-	return result, headers, nil
+	header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
+	return result, header, nil
 }
 }
 
 
 func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
 func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
@@ -130,7 +124,7 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
 	return xray.ClientTraffic{}
 	return xray.ClientTraffic{}
 }
 }
 
 
-func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
+func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) {
 	db := database.GetDB()
 	db := database.GetDB()
 	var inbound *model.Inbound
 	var inbound *model.Inbound
 	err := db.Model(model.Inbound{}).
 	err := db.Model(model.Inbound{}).
@@ -138,9 +132,19 @@ func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
 		Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
 		Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
 		Find(&inbound).Error
 		Find(&inbound).Error
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return "", 0, "", err
 	}
 	}
-	return inbound, nil
+
+	var stream map[string]interface{}
+	json.Unmarshal([]byte(streamSettings), &stream)
+	var masterStream map[string]interface{}
+	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
 }
 }
 
 
 func (s *SubService) getLink(inbound *model.Inbound, email string) string {
 func (s *SubService) getLink(inbound *model.Inbound, email string) string {
@@ -578,6 +582,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 		if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
 		if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
 			params["sni"], _ = sniValue.(string)
 			params["sni"], _ = sniValue.(string)
 		}
 		}
+
 		tlsSettings, _ := searchKey(tlsSetting, "settings")
 		tlsSettings, _ := searchKey(tlsSetting, "settings")
 		if tlsSetting != nil {
 		if tlsSetting != nil {
 			if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
 			if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {

+ 3 - 0
web/assets/js/model/setting.js

@@ -28,6 +28,7 @@ class AllSetting {
         this.subListen = "";
         this.subListen = "";
         this.subPort = "2096";
         this.subPort = "2096";
         this.subPath = "/sub/";
         this.subPath = "/sub/";
+        this.subJsonPath = "/json/";
         this.subDomain = "";
         this.subDomain = "";
         this.subCertFile = "";
         this.subCertFile = "";
         this.subKeyFile = "";
         this.subKeyFile = "";
@@ -35,6 +36,8 @@ class AllSetting {
         this.subEncrypt = true;
         this.subEncrypt = true;
         this.subShowInfo = false;
         this.subShowInfo = false;
         this.subURI = '';
         this.subURI = '';
+        this.subJsonURI = '';
+        this.subJsonFragment = '';
 
 
         this.timeLocation = "Asia/Tehran";
         this.timeLocation = "Asia/Tehran";
 
 

+ 0 - 1
web/controller/xray_setting.go

@@ -81,7 +81,6 @@ func (a *XraySettingController) warp(c *gin.Context) {
 		resp, err = a.XraySettingService.RegWarp(skey, pkey)
 		resp, err = a.XraySettingService.RegWarp(skey, pkey)
 	case "license":
 	case "license":
 		license := c.PostForm("license")
 		license := c.PostForm("license")
-		println(license)
 		resp, err = a.XraySettingService.SetWarpLicence(license)
 		resp, err = a.XraySettingService.SetWarpLicence(license)
 	}
 	}
 
 

+ 10 - 0
web/entity/entity.go

@@ -48,6 +48,9 @@ type AllSetting struct {
 	SubEncrypt       bool   `json:"subEncrypt" form:"subEncrypt"`
 	SubEncrypt       bool   `json:"subEncrypt" form:"subEncrypt"`
 	SubShowInfo      bool   `json:"subShowInfo" form:"subShowInfo"`
 	SubShowInfo      bool   `json:"subShowInfo" form:"subShowInfo"`
 	SubURI           string `json:"subURI" form:"subURI"`
 	SubURI           string `json:"subURI" form:"subURI"`
+	SubJsonPath      string `json:"subJsonPath" form:"subJsonPath"`
+	SubJsonURI       string `json:"subJsonURI" form:"subJsonURI"`
+	SubJsonFragment  string `json:"subJsonFragment" form:"subJsonFragment"`
 	Datepicker       string `json:"datepicker" form:"datepicker"`
 	Datepicker       string `json:"datepicker" form:"datepicker"`
 }
 }
 
 
@@ -105,6 +108,13 @@ func (s *AllSetting) CheckValid() error {
 		s.SubPath += "/"
 		s.SubPath += "/"
 	}
 	}
 
 
+	if !strings.HasPrefix(s.SubJsonPath, "/") {
+		s.SubJsonPath = "/" + s.SubJsonPath
+	}
+	if !strings.HasSuffix(s.SubJsonPath, "/") {
+		s.SubJsonPath += "/"
+	}
+
 	_, err := time.LoadLocation(s.TimeLocation)
 	_, err := time.LoadLocation(s.TimeLocation)
 	if err != nil {
 	if err != nil {
 		return common.NewError("time location not exist:", s.TimeLocation)
 		return common.NewError("time location not exist:", s.TimeLocation)

+ 19 - 5
web/html/common/qrcode_modal.html

@@ -8,13 +8,23 @@
         {{ i18n "pages.inbounds.clickOnQRcode" }}
         {{ i18n "pages.inbounds.clickOnQRcode" }}
     </a-tag>
     </a-tag>
     <template v-if="app.subSettings.enable && qrModal.subId">
     <template v-if="app.subSettings.enable && qrModal.subId">
-        <a-divider>Subscription</a-divider>
-        <div class="qr-bg"><canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas></div>
+        <a-divider>{{ i18n "pages.settings.subSettings"}}</a-divider>
+        <canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))"
+            id="qrCode-sub"
+            style="width: 100%; height: 100%; display: flex; border-radius: 1rem;">
+        </canvas>
+        <a-divider>{{ i18n "pages.settings.subSettings"}} Json</a-divider>
+        <canvas @click="copyToClipboard('qrCode-subJson',genSubJsonLink(qrModal.client.subId))"
+            id="qrCode-subJson"
+            style="width: 100%; height: 100%; display: flex; border-radius: 1rem;">
+        </canvas>
     </template>
     </template>
     <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
     <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
     <template v-for="(row, index) in qrModal.qrcodes">
     <template v-for="(row, index) in qrModal.qrcodes">
-        <a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
-        <div class="qr-bg"><canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas></div>
+        <a-tag color="blue" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
+        <canvas @click="copyToClipboard('qrCode-'+index, row.link)"
+            :id="'qrCode-'+index"
+            style="width: 100%; height: 100%; display: flex; border-radius: 1rem;"></canvas>
     </template>
     </template>
 </a-modal>
 </a-modal>
 
 
@@ -82,12 +92,16 @@
             },
             },
             genSubLink(subID) {
             genSubLink(subID) {
                 return app.subSettings.subURI+subID;
                 return app.subSettings.subURI+subID;
+            },
+            genSubJsonLink(subID) {
+                return app.subSettings.subJsonURI+subID;
             }
             }
         },
         },
         updated() {
         updated() {
             if (qrModal.client && qrModal.client.subId) {
             if (qrModal.client && qrModal.client.subId) {
                 qrModal.subId = qrModal.client.subId;
                 qrModal.subId = qrModal.client.subId;
                 this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
                 this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
+                this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
             }
             }
             qrModal.qrcodes.forEach((element, index) => {
             qrModal.qrcodes.forEach((element, index) => {
                 this.setQrCode("qrCode-" + index, element.link);
                 this.setQrCode("qrCode-" + index, element.link);
@@ -96,4 +110,4 @@
     });
     });
 
 
 </script>
 </script>
-{{end}}
+{{end}}

+ 16 - 1
web/html/xui/inbound_info_modal.html

@@ -166,7 +166,7 @@
         <template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
         <template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
             <a-divider>Subscription URL</a-divider>
             <a-divider>Subscription URL</a-divider>
             <a-row>
             <a-row>
-                <a-col :sx="24" :md="22"><a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
+                <a-col :sx="24" :md="22">SUB: <a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
                 <a-col :sx="24" :md="2" style="text-align: right;">
                 <a-col :sx="24" :md="2" style="text-align: right;">
                     <a-tooltip title='{{ i18n "copy" }}'>
                     <a-tooltip title='{{ i18n "copy" }}'>
                         <button class="ant-btn ant-btn-primary" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)">
                         <button class="ant-btn ant-btn-primary" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)">
@@ -175,6 +175,16 @@
                     </a-tooltip>
                     </a-tooltip>
                 </a-col>
                 </a-col>
             </a-row>
             </a-row>
+            <a-row>
+                <a-col :sx="24" :md="22">JSON: <a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[ infoModal.subJsonLink ]]</a></a-col>
+                <a-col :sx="24" :md="2" style="text-align: right; margin-top: 5px;">
+                    <a-tooltip title='{{ i18n "copy" }}'>
+                        <button class="ant-btn ant-btn-primary" id="copy-subJson-link" @click="copyToClipboard('copy-subJson-link', infoModal.subJsonLink)">
+                            <a-icon type="snippets"></a-icon>
+                        </button>
+                    </a-tooltip>
+                </a-col>
+            </a-row>
         </template>
         </template>
         <template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
         <template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
             <a-divider>Telegram ID</a-divider>
             <a-divider>Telegram ID</a-divider>
@@ -345,6 +355,7 @@
         index: null,
         index: null,
         isExpired: false,
         isExpired: false,
         subLink: '',
         subLink: '',
+        subJsonLink: '',
         show(dbInbound, index) {
         show(dbInbound, index) {
             this.index = index;
             this.index = index;
             this.inbound = dbInbound.toInbound();
             this.inbound = dbInbound.toInbound();
@@ -360,6 +371,7 @@
             if (this.clientSettings) {
             if (this.clientSettings) {
                 if (this.clientSettings.subId) {
                 if (this.clientSettings.subId) {
                     this.subLink = this.genSubLink(this.clientSettings.subId);
                     this.subLink = this.genSubLink(this.clientSettings.subId);
+                    this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId);
                 }
                 }
             }
             }
             this.visible = true;
             this.visible = true;
@@ -369,6 +381,9 @@
         },
         },
         genSubLink(subID) {
         genSubLink(subID) {
             return app.subSettings.subURI+subID;
             return app.subSettings.subURI+subID;
+        },
+        genSubJsonLink(subID) {
+            return app.subSettings.subJsonURI+subID;
         }
         }
     };
     };
 
 

+ 9 - 14
web/html/xui/inbounds.html

@@ -64,11 +64,6 @@
                     >
                     >
                     </a-alert>
                     </a-alert>
                 </transition>
                 </transition>
-                <transition name="list" appear>
-                    <a-tag v-if="false" color="red" style="margin-bottom: 10px">
-                        Please go to the panel settings as soon as possible to modify the username and password, otherwise there may be a risk of leaking account information
-                    </a-tag>
-                </transition>
                 <transition name="list" appear>
                 <transition name="list" appear>
                     <a-card hoverable>
                     <a-card hoverable>
                         <a-row>
                         <a-row>
@@ -576,7 +571,8 @@
             refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
             refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
             subSettings: {
             subSettings: {
                 enable : false,
                 enable : false,
-                subURI : ''
+                subURI : '',
+                subJsonURI : '',
             },
             },
             remarkModel: '-ieo',
             remarkModel: '-ieo',
             datepicker: 'gregorian',
             datepicker: 'gregorian',
@@ -623,7 +619,8 @@
                     this.tgBotEnable = tgBotEnable;
                     this.tgBotEnable = tgBotEnable;
                     this.subSettings = {
                     this.subSettings = {
                         enable : subEnable,
                         enable : subEnable,
-                        subURI: subURI
+                        subURI: subURI,
+                        subJsonURI: subJsonURI
                     };
                     };
                     this.pageSize = pageSize;
                     this.pageSize = pageSize;
                     this.remarkModel = remarkModel;
                     this.remarkModel = remarkModel;
@@ -997,7 +994,7 @@
             },
             },
             delInbound(dbInboundId) {
             delInbound(dbInboundId) {
                 this.$confirm({
                 this.$confirm({
-                    title: '{{ i18n "pages.inbounds.deleteInbound"}}',
+                    title: '{{ i18n "pages.inbounds.deleteInbound"}}' + ' #' + dbInboundId,
                     content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
                     content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
                     class: themeSwitcher.currentTheme,
                     class: themeSwitcher.currentTheme,
                     okText: '{{ i18n "delete"}}',
                     okText: '{{ i18n "delete"}}',
@@ -1010,7 +1007,7 @@
                 clientId = this.getClientId(dbInbound.protocol, client);
                 clientId = this.getClientId(dbInbound.protocol, client);
                 if (confirmation){
                 if (confirmation){
                     this.$confirm({
                     this.$confirm({
-                        title: '{{ i18n "pages.inbounds.deleteClient"}}',
+                        title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
                         content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
                         content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
                         class: themeSwitcher.currentTheme,
                         class: themeSwitcher.currentTheme,
                         okText: '{{ i18n "delete"}}',
                         okText: '{{ i18n "delete"}}',
@@ -1301,7 +1298,7 @@
             pagination(obj){
             pagination(obj){
                 if (this.pageSize > 0 && obj.length>this.pageSize) {
                 if (this.pageSize > 0 && obj.length>this.pageSize) {
                     // Set page options based on object size
                     // Set page options based on object size
-                    sizeOptions = []
+                    sizeOptions = [];
                     for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) {
                     for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) {
                         sizeOptions.push(i.toString());
                         sizeOptions.push(i.toString());
                     }
                     }
@@ -1314,8 +1311,8 @@
                         position: 'bottom',
                         position: 'bottom',
                         pageSize: this.pageSize,
                         pageSize: this.pageSize,
                         pageSizeOptions: sizeOptions
                         pageSizeOptions: sizeOptions
-                    }
-                    return p
+                    };
+                    return p;
                 }
                 }
                 return false
                 return false
             },
             },
@@ -1369,7 +1366,6 @@
             }
             }
         },
         },
     });
     });
-
 </script>
 </script>
 
 
 {{template "inboundModal"}}
 {{template "inboundModal"}}
@@ -1379,6 +1375,5 @@
 {{template "inboundInfoModal"}}
 {{template "inboundInfoModal"}}
 {{template "clientsModal"}}
 {{template "clientsModal"}}
 {{template "clientsBulkModal"}}
 {{template "clientsBulkModal"}}
-
 </body>
 </body>
 </html>
 </html>

+ 86 - 10
web/html/xui/settings.html

@@ -83,7 +83,17 @@
                     show-icon closable
                     show-icon closable
                     >
                     >
                     </a-alert>
                     </a-alert>
-                </transition>
+                    <a-alert type="error" v-if="confAlerts.length>0" style="margin-bottom: 10px"
+                        message='{{ i18n "secAlertTitle" }}'
+                        color="red"
+                        show-icon closable
+                        >
+                        <template slot="description">
+                            {{ i18n "secAlertConf" }}
+                            <li v-for="a in confAlerts">- [[ a ]]</li>
+                        </template>
+                        </a-alert>
+                    </transition>
                 <a-space direction="vertical">
                 <a-space direction="vertical">
                     <a-card hoverable style="margin-bottom: .5rem; overflow-x: hidden;">
                     <a-card hoverable style="margin-bottom: .5rem; overflow-x: hidden;">
                         <a-row style="display: flex; flex-wrap: wrap; align-items: center;">
                         <a-row style="display: flex; flex-wrap: wrap; align-items: center;">
@@ -93,9 +103,7 @@
                                     <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
                                     <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
                                 </a-space>
                                 </a-space>
                             </a-col>
                             </a-col>
-                            <a-col :xs="24" :sm="14">
-                                <template>
-                                    <div>
+                            <a-col :xs="24" :sm="16">
                                         <template>
                                         <template>
                                             <div>
                                             <div>
                                                 <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
                                                 <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
@@ -104,8 +112,6 @@
                                                 message='{{ i18n "pages.settings.infoDesc" }}'
                                                 message='{{ i18n "pages.settings.infoDesc" }}'
                                                 show-icon
                                                 show-icon
                                                 >
                                                 >
-                                            </div>
-                                        </template>
                                     </div>
                                     </div>
                                 </template>
                                 </template>
                             </a-col>
                             </a-col>
@@ -173,7 +179,6 @@
                                         <a-col :lg="24" :xl="12">
                                         <a-col :lg="24" :xl="12">
                                             <a-list-item-meta title="Language" />
                                             <a-list-item-meta title="Language" />
                                         </a-col>
                                         </a-col>
-
                                         <a-col :lg="24" :xl="12">
                                         <a-col :lg="24" :xl="12">
                                             <template>
                                             <template>
                                                 <a-select
                                                 <a-select
@@ -243,7 +248,6 @@
                                 <a-button type="primary" :loading="this.changeSecret" @click="updateSecret">{{ i18n "confirm" }}</a-button>
                                 <a-button type="primary" :loading="this.changeSecret" @click="updateSecret">{{ i18n "confirm" }}</a-button>
                             </a-form>
                             </a-form>
                         </a-tab-pane>
                         </a-tab-pane>
-
                         <a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
                         <a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
                             <a-list item-layout="horizontal">
                             <a-list item-layout="horizontal">
                                 <setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
                                 <setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
@@ -266,8 +270,7 @@
                                                     ref="selectBotLang"
                                                     ref="selectBotLang"
                                                     v-model="allSetting.tgLang"
                                                     v-model="allSetting.tgLang"
                                                     :dropdown-class-name="themeSwitcher.currentTheme"
                                                     :dropdown-class-name="themeSwitcher.currentTheme"
-                                                    style="width: 100%"
-                                                >
+                                                    style="width: 100%">
                                                     <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
                                                     <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
                                                         <span role="img" :aria-label="l.name" v-text="l.icon"></span>
                                                         <span role="img" :aria-label="l.name" v-text="l.icon"></span>
                                                         &nbsp;&nbsp;<span v-text="l.name"></span>
                                                         &nbsp;&nbsp;<span v-text="l.name"></span>
@@ -294,6 +297,17 @@
                                 <setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item>
                                 <setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item>
                             </a-list>
                             </a-list>
                         </a-tab-pane>
                         </a-tab-pane>
+                        <a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }} Json' v-if="allSetting.subEnable">
+                            <a-list item-layout="horizontal">
+                                <setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subJsonPath"></setting-list-item>
+                                <setting-list-item type="text" title='{{ i18n "pages.settings.subURI"}}' desc='{{ i18n "pages.settings.subURIDesc"}}' v-model="allSetting.subJsonURI" placeholder="(http|https)://domain[:port]/path/"></setting-list-item>
+                                <setting-list-item type="switch" title='{{ i18n "pages.settings.fragment"}}' desc='{{ i18n "pages.settings.fragmentDesc"}}' v-model="fragment"></setting-list-item>
+                                <template v-if="fragment">
+                                    <setting-list-item type="text" title='length' v-model="fragmentLength" placeholder="100-200"></setting-list-item>
+                                    <setting-list-item type="text" title='Interval' v-model="fragmentInterval" placeholder="10-20"></setting-list-item>
+                                </template>
+                            </a-list>
+                        </a-tab-pane>
                     </a-tabs>
                     </a-tabs>
                 </a-space>
                 </a-space>
             </a-spin>
             </a-spin>
@@ -324,6 +338,24 @@
             remarkSeparators: [' ','-','_','@',':','~','|',',','.','/'],
             remarkSeparators: [' ','-','_','@',':','~','|',',','.','/'],
             datepickerList: [{name:'Gregorian (Standard)', value: 'gregorian'}, {name:'Jalalian (شمسی)', value: 'jalalian'}],
             datepickerList: [{name:'Gregorian (Standard)', value: 'gregorian'}, {name:'Jalalian (شمسی)', value: 'jalalian'}],
             remarkSample: '',
             remarkSample: '',
+            defaultFragment: {
+                tag: "fragment",
+                protocol: "freedom",
+                settings: {
+                    domainStrategy: "AsIs",
+                    fragment: {
+                        packets: "tlshello",
+                        length: "100-200",
+                        interval: "10-20"
+                    }
+                },
+                streamSettings: {
+                    sockopt: {
+                        tcpKeepAliveIdle: 100,
+                        TcpNoDelay: true
+                    }
+                }
+            },
             get remarkModel() {
             get remarkModel() {
                 rm = this.allSetting.remarkModel;
                 rm = this.allSetting.remarkModel;
                 return rm.length>1 ? rm.substring(1).split('') : [];
                 return rm.length>1 ? rm.substring(1).split('') : [];
@@ -452,6 +484,50 @@
                 }
                 }
             },
             },
         },
         },
+        computed: {
+            fragment: {
+                get: function() { return this.allSetting?.subJsonFragment != ""; },
+                set: function (v) {
+                    this.allSetting.subJsonFragment = v ? JSON.stringify(this.defaultFragment) : "";
+                }
+            },
+            fragmentLength: {
+                get: function() { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.length : ""; },
+                set: function(v) {
+                    if (v != ""){
+                        newFragment = JSON.parse(this.allSetting.subJsonFragment);
+                        newFragment.settings.fragment.length = v;
+                        this.allSetting.subJsonFragment = JSON.stringify(newFragment);
+                    }
+                }
+            },
+            fragmentInterval: {
+                get: function() { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.interval : ""; },
+                set: function(v) {
+                    if (v != ""){
+                        newFragment = JSON.parse(this.allSetting.subJsonFragment);
+                        newFragment.settings.fragment.interval = v;
+                        this.allSetting.subJsonFragment = JSON.stringify(newFragment);
+                    }
+                }
+            },
+            confAlerts: {
+                get: function() {
+                    if (!this.allSetting) return [];
+                    var alerts = []
+                    if (this.allSetting.port == 54321) alerts.push('{{ i18n "pages.settings.panelPort"}}');
+                    panelPath = window.location.pathname.split('/').length<4
+                    if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "pages.settings.panelSettings"}} {{ i18n "pages.settings.panelUrlPath"}}');
+                    if (this.allSetting.subEnable) {
+                        subPath = this.allSetting.subURI.length >0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;
+                        if (subPath == '/sub/') alerts.push('{{ i18n "pages.settings.subSettings"}} {{ i18n "pages.settings.subPath"}}');
+                        subJsonPath = this.allSetting.subJsonURI.length >0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
+                        if (subJsonPath == '/json/') alerts.push('JSON {{ i18n "pages.settings.subPath"}}');
+                    }
+                    return alerts
+                }
+            }
+        }, 
         async mounted() {
         async mounted() {
             if (window.location.protocol !== "https:") {
             if (window.location.protocol !== "https:") {
                 this.showAlert = true;
                 this.showAlert = true;

+ 26 - 19
web/service/setting.go

@@ -57,6 +57,9 @@ var defaultValueMap = map[string]string{
 	"subEncrypt":         "true",
 	"subEncrypt":         "true",
 	"subShowInfo":        "true",
 	"subShowInfo":        "true",
 	"subURI":             "",
 	"subURI":             "",
+	"subJsonPath":        "/json/",
+	"subJsonURI":         "",
+	"subJsonFragment":    "",
 	"datepicker":         "gregorian",
 	"datepicker":         "gregorian",
 	"warp":               "",
 	"warp":               "",
 }
 }
@@ -387,17 +390,11 @@ func (s *SettingService) GetSubPort() (int, error) {
 }
 }
 
 
 func (s *SettingService) GetSubPath() (string, error) {
 func (s *SettingService) GetSubPath() (string, error) {
-	subPath, err := s.getString("subPath")
-	if err != nil {
-		return "", err
-	}
-	if !strings.HasPrefix(subPath, "/") {
-		subPath = "/" + subPath
-	}
-	if !strings.HasSuffix(subPath, "/") {
-		subPath += "/"
-	}
-	return subPath, nil
+	return s.getString("subPath")
+}
+
+func (s *SettingService) GetSubJsonPath() (string, error) {
+	return s.getString("subJsonPath")
 }
 }
 
 
 func (s *SettingService) GetSubDomain() (string, error) {
 func (s *SettingService) GetSubDomain() (string, error) {
@@ -412,8 +409,8 @@ func (s *SettingService) GetSubKeyFile() (string, error) {
 	return s.getString("subKeyFile")
 	return s.getString("subKeyFile")
 }
 }
 
 
-func (s *SettingService) GetSubUpdates() (int, error) {
-	return s.getInt("subUpdates")
+func (s *SettingService) GetSubUpdates() (string, error) {
+	return s.getString("subUpdates")
 }
 }
 
 
 func (s *SettingService) GetSubEncrypt() (bool, error) {
 func (s *SettingService) GetSubEncrypt() (bool, error) {
@@ -432,6 +429,14 @@ func (s *SettingService) GetSubURI() (string, error) {
 	return s.getString("subURI")
 	return s.getString("subURI")
 }
 }
 
 
+func (s *SettingService) GetSubJsonURI() (string, error) {
+	return s.getString("subJsonURI")
+}
+
+func (s *SettingService) GetSubJsonFragment() (string, error) {
+	return s.getString("subJsonFragment")
+}
+
 func (s *SettingService) GetDatepicker() (string, error) {
 func (s *SettingService) GetDatepicker() (string, error) {
 	return s.getString("datepicker")
 	return s.getString("datepicker")
 }
 }
@@ -484,6 +489,7 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
 		"tgBotEnable": func() (interface{}, error) { return s.GetTgbotenabled() },
 		"tgBotEnable": func() (interface{}, error) { return s.GetTgbotenabled() },
 		"subEnable":   func() (interface{}, error) { return s.GetSubEnable() },
 		"subEnable":   func() (interface{}, error) { return s.GetSubEnable() },
 		"subURI":      func() (interface{}, error) { return s.GetSubURI() },
 		"subURI":      func() (interface{}, error) { return s.GetSubURI() },
+		"subJsonURI":  func() (interface{}, error) { return s.GetSubJsonURI() },
 		"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
 		"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
 		"datepicker":  func() (interface{}, error) { return s.GetDatepicker() },
 		"datepicker":  func() (interface{}, error) { return s.GetDatepicker() },
 	}
 	}
@@ -498,10 +504,11 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
 		result[key] = value
 		result[key] = value
 	}
 	}
 
 
-	if result["subEnable"].(bool) && result["subURI"].(string) == "" {
+	if result["subEnable"].(bool) && (result["subURI"].(string) == "" || result["subJsonURI"].(string) == "") {
 		subURI := ""
 		subURI := ""
 		subPort, _ := s.GetSubPort()
 		subPort, _ := s.GetSubPort()
 		subPath, _ := s.GetSubPath()
 		subPath, _ := s.GetSubPath()
+		subJsonPath, _ := s.GetSubJsonPath()
 		subDomain, _ := s.GetSubDomain()
 		subDomain, _ := s.GetSubDomain()
 		subKeyFile, _ := s.GetSubKeyFile()
 		subKeyFile, _ := s.GetSubKeyFile()
 		subCertFile, _ := s.GetSubCertFile()
 		subCertFile, _ := s.GetSubCertFile()
@@ -522,12 +529,12 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
 		} else {
 		} else {
 			subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
 			subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
 		}
 		}
-		if subPath[0] == byte('/') {
-			subURI += subPath
-		} else {
-			subURI += "/" + subPath
+		if result["subURI"].(string) == "" {
+			result["subURI"] = subURI + subPath
+		}
+		if result["subJsonURI"].(string) == "" {
+			result["subJsonURI"] = subURI + subJsonPath
 		}
 		}
-		result["subURI"] = subURI
 	}
 	}
 
 
 	return result, nil
 	return result, nil

+ 3 - 0
web/translation/translate.en_US.toml

@@ -54,6 +54,7 @@
 "security" = "Security"
 "security" = "Security"
 "secAlertTitle" = "Security Alert"
 "secAlertTitle" = "Security Alert"
 "secAlertSsl" = "This connection is not secure. Please avoid entering sensitive information until TLS is activated for data protection."
 "secAlertSsl" = "This connection is not secure. Please avoid entering sensitive information until TLS is activated for data protection."
+"secAlertConf" = "Certain configurations have been identified as susceptible to attacks, prompting immediate action to reinforce security protocols and safeguard against potential security breaches."
 
 
 [menu]
 [menu]
 "dashboard" = "Overview"
 "dashboard" = "Overview"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "The remaining traffic and date will be displayed in the client apps."
 "subShowInfoDesc" = "The remaining traffic and date will be displayed in the client apps."
 "subURI" = "Reverse Proxy URI"
 "subURI" = "Reverse Proxy URI"
 "subURIDesc" = "The URI path of the subscription URL for use behind proxies."
 "subURIDesc" = "The URI path of the subscription URL for use behind proxies."
+"fragment" = "Fragmentation"
+"fragmentDesc" = "Enable fragmentation for TLS hello packet"
 
 
 [pages.xray]
 [pages.xray]
 "title" = "Xray Configs"
 "title" = "Xray Configs"

+ 3 - 0
web/translation/translate.es_ES.toml

@@ -54,6 +54,7 @@
 "security" = "Seguridad"
 "security" = "Seguridad"
 "secAlertTitle" = "Alerta de seguridad"
 "secAlertTitle" = "Alerta de seguridad"
 "secAlertSsl" = "Esta conexión no es segura. Evite ingresar información confidencial hasta que TLS esté activado para la protección de datos."
 "secAlertSsl" = "Esta conexión no es segura. Evite ingresar información confidencial hasta que TLS esté activado para la protección de datos."
+"secAlertConf" = "Se han identificado ciertas configuraciones como susceptibles a ataques, lo que genera acciones inmediatas para reforzar los protocolos de seguridad y proteger contra posibles violaciones de seguridad."
 
 
 [menu]
 [menu]
 "dashboard" = "Estado del Sistema"
 "dashboard" = "Estado del Sistema"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "Mostrar tráfico restante y fecha después del nombre de configuración."
 "subShowInfoDesc" = "Mostrar tráfico restante y fecha después del nombre de configuración."
 "subURI" = "URI de proxy inverso"
 "subURI" = "URI de proxy inverso"
 "subURIDesc" = "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy"
 "subURIDesc" = "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy"
+"fragment" = "Fragmentación"
+"fragmentDesc" = "Habilitar la fragmentación para el paquete de saludo TLS"
 
 
 [pages.xray]
 [pages.xray]
 "title" = "Xray Configuración"
 "title" = "Xray Configuración"

+ 3 - 0
web/translation/translate.fa_IR.toml

@@ -54,6 +54,7 @@
 "security" = "امنیت"
 "security" = "امنیت"
 "secAlertTitle" = "هشدار‌امنیتی"
 "secAlertTitle" = "هشدار‌امنیتی"
 "secAlertSsl" = "این‌اتصال‌امن نیست. لطفا‌ تازمانی‌که تی‌ال‌اس برای محافظت از‌ داده‌ها فعال نشده‌است، از وارد کردن اطلاعات حساس خودداری کنید"
 "secAlertSsl" = "این‌اتصال‌امن نیست. لطفا‌ تازمانی‌که تی‌ال‌اس برای محافظت از‌ داده‌ها فعال نشده‌است، از وارد کردن اطلاعات حساس خودداری کنید"
+"secAlertConf" = "پیکربندی‌های خاصی مستعد حملات سایبری شناسایی شده‌اند، اقدام فوری برای تقویت پروتکل‌های امنیتی و محافظت در برابر نقض‌های امنیتی لازم است"
 
 
 [menu]
 [menu]
 "dashboard" = "نمای کلی"
 "dashboard" = "نمای کلی"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد"
 "subShowInfoDesc" = "ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد"
 "subURI" = "پروکسی معکوس URI مسیر"
 "subURI" = "پروکسی معکوس URI مسیر"
 "subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر"
 "subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر"
+"fragment" = "تکه‌تکه شدن"
+"fragmentDesc" = "فعال کردن تکه تکه شدن برای بسته نخست تی‌ال‌اس"
 
 
 [pages.xray]
 [pages.xray]
 "title" = "پیکربندی ایکس‌ری"
 "title" = "پیکربندی ایکس‌ری"

+ 3 - 0
web/translation/translate.id_ID.toml

@@ -54,6 +54,7 @@
 "security" = "Keamanan"
 "security" = "Keamanan"
 "secAlertTitle" = "Peringatan keamanan"
 "secAlertTitle" = "Peringatan keamanan"
 "secAlertSsl" = "Koneksi ini tidak aman. Harap hindari memasukkan informasi sensitif sampai TLS diaktifkan untuk perlindungan data."
 "secAlertSsl" = "Koneksi ini tidak aman. Harap hindari memasukkan informasi sensitif sampai TLS diaktifkan untuk perlindungan data."
+"secAlertConf" = "Konfigurasi tertentu telah diidentifikasi rentan terhadap serangan, sehingga mendorong tindakan segera untuk memperkuat protokol keamanan dan melindungi dari potensi pelanggaran keamanan."
 
 
 [menu]
 [menu]
 "dashboard" = "Ikhtisar"
 "dashboard" = "Ikhtisar"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "Sisa traffic dan tanggal akan ditampilkan di aplikasi klien."
 "subShowInfoDesc" = "Sisa traffic dan tanggal akan ditampilkan di aplikasi klien."
 "subURI" = "URI Proxy Terbalik"
 "subURI" = "URI Proxy Terbalik"
 "subURIDesc" = "URI path URL langganan untuk penggunaan di belakang proxy."
 "subURIDesc" = "URI path URL langganan untuk penggunaan di belakang proxy."
+"fragment" = "Fragmentasi"
+"fragmentDesc" = "Aktifkan fragmentasi untuk paket hello TLS"
 
 
 [pages.xray]
 [pages.xray]
 "title" = "Konfigurasi Xray"
 "title" = "Konfigurasi Xray"

+ 3 - 0
web/translation/translate.ru_RU.toml

@@ -54,6 +54,7 @@
 "security" = "Безопасность"
 "security" = "Безопасность"
 "secAlertTitle" = "Предупреждение системы безопасности"
 "secAlertTitle" = "Предупреждение системы безопасности"
 "secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации до тех пор, пока не будет активирован TLS для защиты данных"
 "secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации до тех пор, пока не будет активирован TLS для защиты данных"
+"secAlertConf" = "Некоторые конфигурации были определены как уязвимые для атак, что требует немедленных действий по усилению протоколов безопасности и защите от потенциальных нарушений безопасности."
 
 
 [menu]
 [menu]
 "dashboard" = "Статус системы"
 "dashboard" = "Статус системы"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
 "subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
 "subURI" = "URI обратного прокси"
 "subURI" = "URI обратного прокси"
 "subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами"
 "subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами"
+"fragment" = "Фрагментация"
+"fragmentDesc" = "Включить фрагментацию для пакета приветствия TLS"
 
 
 [pages.xray]
 [pages.xray]
 "title" = "Настройки Xray"
 "title" = "Настройки Xray"

+ 3 - 0
web/translation/translate.vi_VN.toml

@@ -54,6 +54,7 @@
 "security" = "Bảo vệ"
 "security" = "Bảo vệ"
 "secAlertTitle" = "Cảnh báo an ninh-Tiếng Việt by Ohoang7"
 "secAlertTitle" = "Cảnh báo an ninh-Tiếng Việt by Ohoang7"
 "secAlertSsl" = "Kết nối này không an toàn; Vui lòng không nhập thông tin nhạy cảm cho đến khi TLS được kích hoạt để bảo vệ dữ liệu của Bạn"
 "secAlertSsl" = "Kết nối này không an toàn; Vui lòng không nhập thông tin nhạy cảm cho đến khi TLS được kích hoạt để bảo vệ dữ liệu của Bạn"
+"secAlertConf" = "Một số cấu hình nhất định đã được xác định là dễ bị tấn công, thúc đẩy hành động ngay lập tức để củng cố các giao thức bảo mật và bảo vệ chống lại các vi phạm bảo mật tiềm ẩn."
 
 
 [menu]
 [menu]
 "dashboard" = "Trạng thái hệ thống"
 "dashboard" = "Trạng thái hệ thống"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình"
 "subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình"
 "subURI" = "URI proxy trung gian"
 "subURI" = "URI proxy trung gian"
 "subURIDesc" = "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian"
 "subURIDesc" = "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian"
+"fragment" = "Sự phân mảnh"
+"fragmentDesc" = "Kích hoạt phân mảnh cho gói TLS hello"
 
 
 [pages.xray]
 [pages.xray]
 "title" = "Cài đặt Xray"
 "title" = "Cài đặt Xray"

+ 3 - 0
web/translation/translate.zh_Hans.toml

@@ -54,6 +54,7 @@
 "security" = "安全"
 "security" = "安全"
 "secAlertTitle" = "安全警报"
 "secAlertTitle" = "安全警报"
 "secAlertSsl" = "此连接不安全;在激活 TLS 进行数据保护之前,请勿输入敏感信息"
 "secAlertSsl" = "此连接不安全;在激活 TLS 进行数据保护之前,请勿输入敏感信息"
+"secAlertConf" = "某些配置已被确定为容易受到攻击,促使立即采取行动以加强安全协议并防范潜在的安全漏洞。"
 
 
 [menu]
 [menu]
 "dashboard" = "系统状态"
 "dashboard" = "系统状态"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
 "subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
 "subURI" = "反向代理 URI"
 "subURI" = "反向代理 URI"
 "subURIDesc" = "更改订阅 URL 的基本 URI 以在代理后面使用"
 "subURIDesc" = "更改订阅 URL 的基本 URI 以在代理后面使用"
+"fragment" = "碎片"
+"fragmentDesc" = "启用 TLS hello 数据包分段"
 
 
 [pages.xray]
 [pages.xray]
 "title" = "Xray 设置"
 "title" = "Xray 设置"

+ 2 - 1
xray/config.go

@@ -16,7 +16,8 @@ type Config struct {
 	API             json_util.RawMessage `json:"api"`
 	API             json_util.RawMessage `json:"api"`
 	Stats           json_util.RawMessage `json:"stats"`
 	Stats           json_util.RawMessage `json:"stats"`
 	Reverse         json_util.RawMessage `json:"reverse"`
 	Reverse         json_util.RawMessage `json:"reverse"`
-	FakeDNS         json_util.RawMessage `json:"fakeDns"`
+	FakeDNS         json_util.RawMessage `json:"fakedns"`
+	Observatory     json_util.RawMessage `json:"observatory"`
 }
 }
 
 
 func (c *Config) Equals(other *Config) bool {
 func (c *Config) Equals(other *Config) bool {