diff --git a/sub/default.json b/sub/default.json
new file mode 100644
index 00000000..ba13f6fb
--- /dev/null
+++ b/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": {}
+}
\ No newline at end of file
diff --git a/sub/sub.go b/sub/sub.go
index b642f7f2..2a4a37f4 100644
--- a/sub/sub.go
+++ b/sub/sub.go
@@ -47,11 +47,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default()
- subPath, err := s.settingService.GetSubPath()
- if err != nil {
- return nil, err
- }
-
subDomain, err := s.settingService.GetSubDomain()
if err != nil {
return nil, err
@@ -61,9 +56,44 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
}
- g := engine.Group(subPath)
+ LinksPath, err := s.settingService.GetSubPath()
+ if err != nil {
+ return nil, err
+ }
- s.sub = NewSUBController(g)
+ 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("/")
+
+ s.sub = NewSUBController(g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates, SubJsonFragment)
return engine, nil
}
diff --git a/sub/subController.go b/sub/subController.go
index 5f7c69cf..e0b641df 100644
--- a/sub/subController.go
+++ b/sub/subController.go
@@ -3,34 +3,57 @@ package sub
import (
"encoding/base64"
"strings"
- "x-ui/web/service"
"github.com/gin-gonic/gin"
)
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)
return a
}
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) {
- subEncrypt, _ := a.settingService.GetSubEncrypt()
- subShowInfo, _ := a.settingService.GetSubShowInfo()
+ println(c.Request.Header["User-Agent"][0])
subId := c.Param("subid")
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 {
c.String(400, "Error!")
} else {
@@ -40,14 +63,32 @@ func (a *SUBController) subs(c *gin.Context) {
}
// 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)))
} else {
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)
+ }
+}
diff --git a/sub/subJsonService.go b/sub/subJsonService.go
new file mode 100644
index 00000000..14db09e5
--- /dev/null
+++ b/sub/subJsonService.go
@@ -0,0 +1,359 @@
+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...)
+ var outboundStrings []json_util.RawMessage
+ for _, outbound := range outbounds {
+ outboundStrings = append(outboundStrings, outbound)
+ }
+ configJson["outbounds"] = outboundStrings
+ 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"`
+}
diff --git a/sub/subService.go b/sub/subService.go
index 5b75935a..ccf27b04 100644
--- a/sub/subService.go
+++ b/sub/subService.go
@@ -18,50 +18,46 @@ import (
)
type SubService struct {
- address string
- showInfo bool
- remarkModel string
+ address string
+ showInfo bool
+ remarkModel string
+
inboundService service.InboundService
- 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.showInfo = showInfo
var result []string
- var headers []string
+ var header string
var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId)
if err != nil {
- return nil, nil, err
- }
- s.remarkModel, err = s.settingService.GetRemarkModel()
- if err != nil {
- s.remarkModel = "-ieo"
+ return nil, "", err
}
+
+ // Prepare Inbounds
for _, inbound := range inbounds {
clients, err := s.inboundService.GetClients(inbound)
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 {
continue
}
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 {
- 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 {
@@ -72,6 +68,8 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
}
}
}
+
+ // Prepare statistics
for index, clientTraffic := range clientTraffics {
if index == 0 {
traffic.Up = clientTraffic.Up
@@ -93,11 +91,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) {
@@ -126,7 +121,7 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
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()
var inbound *model.Inbound
err := db.Model(model.Inbound{}).
@@ -134,9 +129,19 @@ func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
Find(&inbound).Error
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 {
diff --git a/web/assets/js/model/setting.js b/web/assets/js/model/setting.js
index 61b7b532..286535d7 100644
--- a/web/assets/js/model/setting.js
+++ b/web/assets/js/model/setting.js
@@ -24,6 +24,7 @@ class AllSetting {
this.subListen = "";
this.subPort = "2096";
this.subPath = "/sub/";
+ this.subJsonPath = "/json/";
this.subDomain = "";
this.subCertFile = "";
this.subKeyFile = "";
@@ -31,6 +32,8 @@ class AllSetting {
this.subEncrypt = true;
this.subShowInfo = false;
this.subURI = '';
+ this.subJsonURI = '';
+ this.subJsonFragment = '';
this.timeLocation = "Asia/Tehran";
diff --git a/web/controller/xray_setting.go b/web/controller/xray_setting.go
index 09e9115f..9555b0d7 100644
--- a/web/controller/xray_setting.go
+++ b/web/controller/xray_setting.go
@@ -78,7 +78,6 @@ func (a *XraySettingController) warp(c *gin.Context) {
resp, err = a.XraySettingService.RegWarp(skey, pkey)
case "license":
license := c.PostForm("license")
- println(license)
resp, err = a.XraySettingService.SetWarpLicence(license)
}
diff --git a/web/entity/entity.go b/web/entity/entity.go
index 80735c2f..0bcbeb58 100644
--- a/web/entity/entity.go
+++ b/web/entity/entity.go
@@ -46,6 +46,9 @@ type AllSetting struct {
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
SubURI string `json:"subURI" form:"subURI"`
+ SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
+ SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
+ SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
}
func (s *AllSetting) CheckValid() error {
@@ -103,6 +106,13 @@ func (s *AllSetting) CheckValid() error {
s.SubPath += "/"
}
+ if !strings.HasPrefix(s.SubJsonPath, "/") {
+ s.SubJsonPath = "/" + s.SubJsonPath
+ }
+ if !strings.HasSuffix(s.SubJsonPath, "/") {
+ s.SubJsonPath += "/"
+ }
+
_, err := time.LoadLocation(s.TimeLocation)
if err != nil {
return common.NewError("time location not exist:", s.TimeLocation)
diff --git a/web/html/common/qrcode_modal.html b/web/html/common/qrcode_modal.html
index b72ea7be..7441bd5b 100644
--- a/web/html/common/qrcode_modal.html
+++ b/web/html/common/qrcode_modal.html
@@ -5,10 +5,16 @@
width="300px" :class="themeSwitcher.currentTheme">
{{ i18n "pages.inbounds.clickOnQRcode" }}
- Subscription
+ {{ i18n "pages.settings.subSettings"}}
+ style="width: 100%; height: 100%; display: flex; border-radius: 1rem;">
+
+ {{ i18n "pages.settings.subSettings"}} Json
+
{{ i18n "pages.inbounds.client" }}
@@ -83,12 +89,16 @@
},
genSubLink(subID) {
return app.subSettings.subURI+subID+'?name='+subID;
+ },
+ genSubJsonLink(subID) {
+ return app.subSettings.subJsonURI+subID;
}
},
updated() {
if (qrModal.client && qrModal.client.subId) {
qrModal.subId = qrModal.client.subId;
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
+ this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
}
qrModal.qrcodes.forEach((element, index) => {
this.setQrCode("qrCode-" + index, element.link);
diff --git a/web/html/xui/inbound_info_modal.html b/web/html/xui/inbound_info_modal.html
index fee2bc78..ecd38039 100644
--- a/web/html/xui/inbound_info_modal.html
+++ b/web/html/xui/inbound_info_modal.html
@@ -162,7 +162,7 @@
Subscription URL
- [[ infoModal.subLink ]]
+ SUB: [[ infoModal.subLink ]]
+
+ JSON: [[ infoModal.subJsonLink ]]
+
+
+
+
+
+
Telegram ID
@@ -341,6 +351,7 @@
index: null,
isExpired: false,
subLink: '',
+ subJsonLink: '',
tgLink: '',
show(dbInbound, index) {
this.index = index;
@@ -357,6 +368,7 @@
if (this.clientSettings) {
if (this.clientSettings.subId) {
this.subLink = this.genSubLink(this.clientSettings.subId);
+ this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId);
}
if (this.clientSettings.tgId) {
this.tgLink = "https://t.me/" + this.clientSettings.tgId;
@@ -369,6 +381,9 @@
},
genSubLink(subID) {
return app.subSettings.subURI+subID+'?name='+subID;
+ },
+ genSubJsonLink(subID) {
+ return app.subSettings.subJsonURI+subID+'?name='+subID;;
}
};
diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html
index e8cb6166..755d0c98 100644
--- a/web/html/xui/inbounds.html
+++ b/web/html/xui/inbounds.html
@@ -564,7 +564,8 @@
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
subSettings: {
enable : false,
- subURI : ''
+ subURI : '',
+ subJsonURI : '',
},
remarkModel: '-ieo',
tgBotEnable: false,
@@ -609,7 +610,8 @@
this.tgBotEnable = tgBotEnable;
this.subSettings = {
enable : subEnable,
- subURI: subURI
+ subURI: subURI,
+ subJsonURI: subJsonURI
};
this.pageSize = pageSize;
this.remarkModel = remarkModel;
@@ -646,8 +648,12 @@
clientCount = clients.length;
if (dbInbound.enable) {
clients.forEach(client => {
- client.enable ? active.push(client.email) : deactive.push(client.email);
- if(this.isClientOnline(client.email)) online.push(client.email);
+ if (client.enable) {
+ active.push(client.email);
+ if(this.isClientOnline(client.email)) online.push(client.email);
+ } else {
+ deactive.push(client.email);
+ }
});
clientStats.forEach(client => {
if (!client.enable) {
diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html
index 086ad792..f87b87ac 100644
--- a/web/html/xui/settings.html
+++ b/web/html/xui/settings.html
@@ -211,6 +211,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -240,6 +251,24 @@
remarkModels: {i:'Inbound',e:'Email',o:'Other'},
remarkSeparators: [' ','-','_','@',':','~','|',',','.','/'],
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() {
rm = this.allSetting.remarkModel;
return rm.length>1 ? rm.substring(1).split('') : [];
@@ -321,6 +350,32 @@
}
},
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 [];
@@ -330,6 +385,8 @@
if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "pages.settings.panelConfig"}} {{ i18n "pages.settings.panelUrlPath"}}');
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
}
}
diff --git a/web/service/setting.go b/web/service/setting.go
index 427ee17b..9f68217b 100644
--- a/web/service/setting.go
+++ b/web/service/setting.go
@@ -55,6 +55,9 @@ var defaultValueMap = map[string]string{
"subEncrypt": "true",
"subShowInfo": "false",
"subURI": "",
+ "subJsonPath": "/json/",
+ "subJsonURI": "",
+ "subJsonFragment": "",
"warp": "",
}
@@ -353,17 +356,11 @@ func (s *SettingService) GetSubPort() (int, 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) {
@@ -378,8 +375,8 @@ func (s *SettingService) GetSubKeyFile() (string, error) {
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) {
@@ -398,6 +395,14 @@ func (s *SettingService) GetSubURI() (string, error) {
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) GetWarp() (string, error) {
return s.getString("warp")
}
@@ -446,6 +451,7 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
"tgBotEnable": func() (interface{}, error) { return s.GetTgbotenabled() },
"subEnable": func() (interface{}, error) { return s.GetSubEnable() },
"subURI": func() (interface{}, error) { return s.GetSubURI() },
+ "subJsonURI": func() (interface{}, error) { return s.GetSubJsonURI() },
"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
}
@@ -459,10 +465,11 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
result[key] = value
}
- if result["subEnable"].(bool) && result["subURI"].(string) == "" {
+ if result["subEnable"].(bool) && (result["subURI"].(string) == "" || result["subJsonURI"].(string) == "") {
subURI := ""
subPort, _ := s.GetSubPort()
subPath, _ := s.GetSubPath()
+ subJsonPath, _ := s.GetSubJsonPath()
subDomain, _ := s.GetSubDomain()
subKeyFile, _ := s.GetSubKeyFile()
subCertFile, _ := s.GetSubCertFile()
@@ -483,12 +490,12 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
} else {
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
diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml
index fb00e6c8..90e6d782 100644
--- a/web/translation/translate.en_US.toml
+++ b/web/translation/translate.en_US.toml
@@ -294,6 +294,8 @@
"subShowInfoDesc" = "The remaining traffic and date will be displayed in the client apps."
"subURI" = "Reverse Proxy URI"
"subURIDesc" = "The subscription service will use the URI that has been set up behind reverse proxies."
+"fragment" = "Fragmentation"
+"fragmentDesc" = "Enable fragmentation for TLS hello packet"
[pages.settings.toasts]
"modifySettings" = "Modify Settings"
diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml
index 5b590ff0..c698e45a 100644
--- a/web/translation/translate.fa_IR.toml
+++ b/web/translation/translate.fa_IR.toml
@@ -293,6 +293,8 @@
"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در برنامههای کاربری نمایش میدهد"
"subURI" = "پراکسی معکوس URI"
"subURIDesc" = "سابسکریپشن از لینکی که در پشت پراکسیهای معکوس تنظیم شده، استفاده خواهدکرد"
+"fragment" = "تکهتکه شدن"
+"fragmentDesc" = "فعال کردن تکه تکه شدن برای بسته نخست تیالاس"
[pages.settings.toasts]
"modifySettings" = "ویرایش تنظیمات"
diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml
index f533dfe4..2bcfd5a6 100644
--- a/web/translation/translate.ru_RU.toml
+++ b/web/translation/translate.ru_RU.toml
@@ -294,6 +294,8 @@
"subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
"subURI" = "URI обратного прокси"
"subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами"
+"fragment" = "Фрагментация"
+"fragmentDesc" = "Включить фрагментацию для пакета приветствия TLS"
[pages.settings.toasts]
"modifySettings" = "Изменение настроек"
diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml
index 2b68c6d3..8384c9fe 100644
--- a/web/translation/translate.vi_VN.toml
+++ b/web/translation/translate.vi_VN.toml
@@ -294,6 +294,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"
"subURI" = "URI proxy ngược"
"subURIDesc" = "Thay đổi URI cơ sở của URL đăng ký để sử dụng ở phía sau proxy"
+"fragment" = "Sự phân mảnh"
+"fragmentDesc" = "Kích hoạt phân mảnh cho gói TLS hello"
[pages.settings.toasts]
"modifySettings" = "Sửa đổi cài đặt"
diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml
index ca0f6d60..999aee9c 100644
--- a/web/translation/translate.zh_Hans.toml
+++ b/web/translation/translate.zh_Hans.toml
@@ -294,6 +294,8 @@
"subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
"subURI" = "反向代理 URI"
"subURIDesc" = "更改订阅 URL 的基本 URI 以在代理后面使用"
+"fragment" = "碎片"
+"fragmentDesc" = "启用 TLS hello 数据包分段"
[pages.settings.toasts]
"modifySettings" = "修改设置"