Browse Source

[feature] separate subscription service

Co-Authored-By: Alireza Ahmadi <[email protected]>
MHSanaei 1 year ago
parent
commit
769590d779

+ 171 - 0
sub/sub.go

@@ -0,0 +1,171 @@
+package sub
+
+import (
+	"context"
+	"crypto/tls"
+	"io"
+	"net"
+	"net/http"
+	"strconv"
+	"strings"
+	"x-ui/config"
+	"x-ui/logger"
+	"x-ui/util/common"
+	"x-ui/web/network"
+	"x-ui/web/service"
+
+	"github.com/gin-gonic/gin"
+)
+
+type Server struct {
+	httpServer *http.Server
+	listener   net.Listener
+
+	sub            *SUBController
+	settingService service.SettingService
+
+	ctx    context.Context
+	cancel context.CancelFunc
+}
+
+func NewServer() *Server {
+	ctx, cancel := context.WithCancel(context.Background())
+	return &Server{
+		ctx:    ctx,
+		cancel: cancel,
+	}
+}
+
+func (s *Server) initRouter() (*gin.Engine, error) {
+	if config.IsDebug() {
+		gin.SetMode(gin.DebugMode)
+	} else {
+		gin.DefaultWriter = io.Discard
+		gin.DefaultErrorWriter = io.Discard
+		gin.SetMode(gin.ReleaseMode)
+	}
+
+	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
+	}
+
+	if subDomain != "" {
+		validateDomain := func(c *gin.Context) {
+			host := strings.Split(c.Request.Host, ":")[0]
+
+			if host != subDomain {
+				c.AbortWithStatus(http.StatusForbidden)
+				return
+			}
+
+			c.Next()
+		}
+
+		engine.Use(validateDomain)
+	}
+
+	g := engine.Group(subPath)
+
+	s.sub = NewSUBController(g)
+
+	return engine, nil
+}
+
+func (s *Server) Start() (err error) {
+	//This is an anonymous function, no function name
+	defer func() {
+		if err != nil {
+			s.Stop()
+		}
+	}()
+
+	subEnable, err := s.settingService.GetSubEnable()
+	if err != nil {
+		return err
+	}
+	if !subEnable {
+		return nil
+	}
+
+	engine, err := s.initRouter()
+	if err != nil {
+		return err
+	}
+
+	certFile, err := s.settingService.GetSubCertFile()
+	if err != nil {
+		return err
+	}
+	keyFile, err := s.settingService.GetSubKeyFile()
+	if err != nil {
+		return err
+	}
+	listen, err := s.settingService.GetSubListen()
+	if err != nil {
+		return err
+	}
+	port, err := s.settingService.GetSubPort()
+	if err != nil {
+		return err
+	}
+	listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
+	listener, err := net.Listen("tcp", listenAddr)
+	if err != nil {
+		return err
+	}
+	if certFile != "" || keyFile != "" {
+		cert, err := tls.LoadX509KeyPair(certFile, keyFile)
+		if err != nil {
+			listener.Close()
+			return err
+		}
+		c := &tls.Config{
+			Certificates: []tls.Certificate{cert},
+		}
+		listener = network.NewAutoHttpsListener(listener)
+		listener = tls.NewListener(listener, c)
+	}
+
+	if certFile != "" || keyFile != "" {
+		logger.Info("Sub server run https on", listener.Addr())
+	} else {
+		logger.Info("Sub server run http on", listener.Addr())
+	}
+	s.listener = listener
+
+	s.httpServer = &http.Server{
+		Handler: engine,
+	}
+
+	go func() {
+		s.httpServer.Serve(listener)
+	}()
+
+	return nil
+}
+
+func (s *Server) Stop() error {
+	s.cancel()
+
+	var err1 error
+	var err2 error
+	if s.httpServer != nil {
+		err1 = s.httpServer.Shutdown(s.ctx)
+	}
+	if s.listener != nil {
+		err2 = s.listener.Close()
+	}
+	return common.Combine(err1, err2)
+}
+
+func (s *Server) GetCtx() context.Context {
+	return s.ctx
+}

+ 3 - 6
web/controller/sub.go → sub/subController.go

@@ -1,17 +1,14 @@
-package controller
+package sub
 
 import (
 	"encoding/base64"
 	"strings"
-	"x-ui/web/service"
 
 	"github.com/gin-gonic/gin"
 )
 
 type SUBController struct {
-	BaseController
-
-	subService service.SubService
+	subService SubService
 }
 
 func NewSUBController(g *gin.RouterGroup) *SUBController {
@@ -21,7 +18,7 @@ func NewSUBController(g *gin.RouterGroup) *SUBController {
 }
 
 func (a *SUBController) initRouter(g *gin.RouterGroup) {
-	g = g.Group("/sub")
+	g = g.Group("/")
 
 	g.GET("/:subid", a.subs)
 }

+ 67 - 8
web/service/sub.go → sub/subService.go

@@ -1,4 +1,4 @@
-package service
+package sub
 
 import (
 	"encoding/base64"
@@ -8,6 +8,7 @@ import (
 	"x-ui/database"
 	"x-ui/database/model"
 	"x-ui/logger"
+	"x-ui/web/service"
 	"x-ui/xray"
 
 	"github.com/goccy/go-json"
@@ -15,7 +16,8 @@ import (
 
 type SubService struct {
 	address        string
-	inboundService InboundService
+	inboundService service.InboundService
+	settingServics service.SettingService
 }
 
 func (s *SubService) GetSubs(subId string, host string) ([]string, []string, error) {
@@ -29,7 +31,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, []string, err
 		return nil, nil, err
 	}
 	for _, inbound := range inbounds {
-		clients, err := s.inboundService.getClients(inbound)
+		clients, err := s.inboundService.GetClients(inbound)
 		if err != nil {
 			logger.Error("SubService - GetSub: Unable to get clients from inbound")
 		}
@@ -66,7 +68,8 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, []string, err
 		}
 	}
 	headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
-	headers = append(headers, "12")
+	updateInterval, _ := s.settingServics.GetSubUpdates()
+	headers = append(headers, fmt.Sprintf("%d", updateInterval))
 	headers = append(headers, subId)
 	return result, headers, nil
 }
@@ -163,6 +166,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
 	}
 
 	security, _ := stream["security"].(string)
+	var domains []interface{}
 	obj["tls"] = security
 	if security == "tls" {
 		tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@@ -185,6 +189,9 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
 			if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
 				obj["allowInsecure"], _ = insecure.(bool)
 			}
+			if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
+				domains, _ = domainSettings.([]interface{})
+			}
 		}
 		serverName, _ := tlsSetting["serverName"].(string)
 		if serverName != "" {
@@ -192,7 +199,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
 		}
 	}
 
-	clients, _ := s.inboundService.getClients(inbound)
+	clients, _ := s.inboundService.GetClients(inbound)
 	clientIndex := -1
 	for i, client := range clients {
 		if client.Email == email {
@@ -203,6 +210,21 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
 	obj["id"] = clients[clientIndex].ID
 	obj["aid"] = clients[clientIndex].AlterIds
 
+	if len(domains) > 0 {
+		links := ""
+		for index, d := range domains {
+			domain := d.(map[string]interface{})
+			obj["ps"] = remark + "-" + domain["remark"].(string)
+			obj["add"] = domain["domain"].(string)
+			if index > 0 {
+				links += "\n"
+			}
+			jsonStr, _ := json.MarshalIndent(obj, "", "  ")
+			links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
+		}
+		return links
+	}
+
 	jsonStr, _ := json.MarshalIndent(obj, "", "  ")
 	return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
 }
@@ -214,7 +236,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 	}
 	var stream map[string]interface{}
 	json.Unmarshal([]byte(inbound.StreamSettings), &stream)
-	clients, _ := s.inboundService.getClients(inbound)
+	clients, _ := s.inboundService.GetClients(inbound)
 	clientIndex := -1
 	for i, client := range clients {
 		if client.Email == email {
@@ -270,6 +292,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 	}
 
 	security, _ := stream["security"].(string)
+	var domains []interface{}
 	if security == "tls" {
 		params["security"] = "tls"
 		tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@@ -294,6 +317,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 					params["allowInsecure"] = "1"
 				}
 			}
+			if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
+				domains, _ = domainSettings.([]interface{})
+			}
 		}
 
 		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@@ -393,6 +419,20 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 	url.RawQuery = q.Encode()
 
 	remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
+
+	if len(domains) > 0 {
+		links := ""
+		for index, d := range domains {
+			domain := d.(map[string]interface{})
+			url.Fragment = remark + "-" + domain["remark"].(string)
+			url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
+			if index > 0 {
+				links += "\n"
+			}
+			links += url.String()
+		}
+		return links
+	}
 	url.Fragment = remark
 	return url.String()
 }
@@ -404,7 +444,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 	}
 	var stream map[string]interface{}
 	json.Unmarshal([]byte(inbound.StreamSettings), &stream)
-	clients, _ := s.inboundService.getClients(inbound)
+	clients, _ := s.inboundService.GetClients(inbound)
 	clientIndex := -1
 	for i, client := range clients {
 		if client.Email == email {
@@ -460,6 +500,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 	}
 
 	security, _ := stream["security"].(string)
+	var domains []interface{}
 	if security == "tls" {
 		params["security"] = "tls"
 		tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@@ -484,6 +525,9 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 					params["allowInsecure"] = "1"
 				}
 			}
+			if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
+				domains, _ = domainSettings.([]interface{})
+			}
 		}
 
 		serverName, _ := tlsSetting["serverName"].(string)
@@ -580,6 +624,21 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 	url.RawQuery = q.Encode()
 
 	remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
+
+	if len(domains) > 0 {
+		links := ""
+		for index, d := range domains {
+			domain := d.(map[string]interface{})
+			url.Fragment = remark + "-" + domain["remark"].(string)
+			url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
+			if index > 0 {
+				links += "\n"
+			}
+			links += url.String()
+		}
+		return links
+	}
+
 	url.Fragment = remark
 	return url.String()
 }
@@ -589,7 +648,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
 	if inbound.Protocol != model.Shadowsocks {
 		return ""
 	}
-	clients, _ := s.inboundService.getClients(inbound)
+	clients, _ := s.inboundService.GetClients(inbound)
 
 	var settings map[string]interface{}
 	json.Unmarshal([]byte(inbound.Settings), &settings)

+ 8 - 0
web/assets/js/model/models.js

@@ -184,6 +184,14 @@ class AllSetting {
         this.tgLang = "en-US";
         this.xrayTemplateConfig = "";
         this.secretEnable = false;
+        this.subEnable = false;
+        this.subListen = "";
+        this.subPort = "2096";
+        this.subPath = "sub/";
+        this.subDomain = "";
+        this.subCertFile = "";
+        this.subKeyFile = "";
+        this.subUpdates = 0;
 
         this.timeLocation = "Asia/Tehran";
 

+ 45 - 0
web/controller/setting.go

@@ -85,11 +85,56 @@ func (a *SettingController) getDefaultSettings(c *gin.Context) {
 		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
 		return
 	}
+	tgBotEnable, err := a.settingService.GetTgbotenabled()
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
+		return
+	}
+	subEnable, err := a.settingService.GetSubEnable()
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
+		return
+	}
+	subPort, err := a.settingService.GetSubPort()
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
+		return
+	}
+	subPath, err := a.settingService.GetSubPath()
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
+		return
+	}
+	subDomain, err := a.settingService.GetSubDomain()
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
+		return
+	}
+	subKeyFile, err := a.settingService.GetSubKeyFile()
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
+		return
+	}
+	subCertFile, err := a.settingService.GetSubCertFile()
+	if err != nil {
+		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
+		return
+	}
+	subTLS := false
+	if subKeyFile != "" || subCertFile != "" {
+		subTLS = true
+	}
 	result := map[string]interface{}{
 		"expireDiff":  expireDiff,
 		"trafficDiff": trafficDiff,
 		"defaultCert": defaultCert,
 		"defaultKey":  defaultKey,
+		"tgBotEnable": tgBotEnable,
+		"subEnable":   subEnable,
+		"subPort":     subPort,
+		"subPath":     subPath,
+		"subDomain":   subDomain,
+		"subTLS":      subTLS,
 	}
 	jsonObj(c, result, nil)
 }

+ 30 - 0
web/entity/entity.go

@@ -45,6 +45,14 @@ type AllSetting struct {
 	XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
 	TimeLocation       string `json:"timeLocation" form:"timeLocation"`
 	SecretEnable       bool   `json:"secretEnable" form:"secretEnable"`
+	SubEnable          bool   `json:"subEnable" form:"subEnable"`
+	SubListen          string `json:"subListen" form:"subListen"`
+	SubPort            int    `json:"subPort" form:"subPort"`
+	SubPath            string `json:"subPath" form:"subPath"`
+	SubDomain          string `json:"subDomain" form:"subDomain"`
+	SubCertFile        string `json:"subCertFile" form:"subCertFile"`
+	SubKeyFile         string `json:"subKeyFile" form:"subKeyFile"`
+	SubUpdates         int    `json:"subUpdates" form:"subUpdates"`
 }
 
 func (s *AllSetting) CheckValid() error {
@@ -55,10 +63,25 @@ func (s *AllSetting) CheckValid() error {
 		}
 	}
 
+	if s.SubListen != "" {
+		ip := net.ParseIP(s.SubListen)
+		if ip == nil {
+			return common.NewError("Sub listen is not valid ip:", s.SubListen)
+		}
+	}
+
 	if s.WebPort <= 0 || s.WebPort > 65535 {
 		return common.NewError("web port is not a valid port:", s.WebPort)
 	}
 
+	if s.SubPort <= 0 || s.SubPort > 65535 {
+		return common.NewError("Sub port is not a valid port:", s.SubPort)
+	}
+
+	if s.SubPort == s.WebPort {
+		return common.NewError("Sub and Web could not use same port:", s.SubPort)
+	}
+
 	if s.WebCertFile != "" || s.WebKeyFile != "" {
 		_, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile)
 		if err != nil {
@@ -66,6 +89,13 @@ func (s *AllSetting) CheckValid() error {
 		}
 	}
 
+	if s.SubCertFile != "" || s.SubKeyFile != "" {
+		_, err := tls.LoadX509KeyPair(s.SubCertFile, s.SubKeyFile)
+		if err != nil {
+			return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.SubCertFile, s.SubKeyFile, err)
+		}
+	}
+
 	if !strings.HasPrefix(s.WebBasePath, "/") {
 		s.WebBasePath = "/" + s.WebBasePath
 	}

+ 13 - 0
web/global/global.go

@@ -8,12 +8,17 @@ import (
 )
 
 var webServer WebServer
+var subServer SubServer
 
 type WebServer interface {
 	GetCron() *cron.Cron
 	GetCtx() context.Context
 }
 
+type SubServer interface {
+	GetCtx() context.Context
+}
+
 func SetWebServer(s WebServer) {
 	webServer = s
 }
@@ -21,3 +26,11 @@ func SetWebServer(s WebServer) {
 func GetWebServer() WebServer {
 	return webServer
 }
+
+func SetSubServer(s SubServer) {
+	subServer = s
+}
+
+func GetSubServer() SubServer {
+	return subServer
+}

+ 4 - 2
web/html/xui/client_bulk_modal.html

@@ -33,7 +33,7 @@
             <span slot="label">{{ i18n "pages.client.clientCount" }}</span>
             <a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
         </a-form-item>
-        <a-form-item>
+        <a-form-item v-if="app.subSettings.enable">
                 <span slot="label">
                     Subscription
                     <a-tooltip>
@@ -45,7 +45,7 @@
                 </span>
                 <a-input v-model.trim="clientsBulkModal.subId"></a-input>
         </a-form-item>
-        <a-form-item>
+        <a-form-item v-if="app.tgBotEnable">
             <span slot="label">
                 Telegram ID
                 <a-tooltip>
@@ -204,6 +204,7 @@
                 case Protocols.VMESS: return clientSettings.vmesses;
                 case Protocols.VLESS: return clientSettings.vlesses;
                 case Protocols.TROJAN: return clientSettings.trojans;
+                case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
                 default: return null;
             }
         },
@@ -212,6 +213,7 @@
                 case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
                 case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
                 case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
+                case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks();
                 default: return null;
             }
         },

+ 2 - 2
web/html/xui/form/client.html

@@ -34,7 +34,7 @@
         <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
         <a-input v-model.trim="client.id" style="width: 300px;"></a-input>
     </a-form-item>
-	<a-form-item v-if="client.email">
+	<a-form-item v-if="client.email && app.subSettings.enable">
         <span slot="label">
             Subscription
             <a-tooltip>
@@ -47,7 +47,7 @@
         <a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
         <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
     </a-form-item>
-    <a-form-item v-if="client.email">
+    <a-form-item v-if="client.email && app.tgBotEnable" >
         <span slot="label">
             Telegram ID
             <a-tooltip>

+ 2 - 2
web/html/xui/form/protocol/shadowsocks.html

@@ -18,7 +18,7 @@
                 <a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
                 <a-input v-model.trim="client.password" style="width: 250px;"></a-input>
             </a-form-item>
-            <a-form-item v-if="client.email">
+            <a-form-item v-if="client.email && app.subSettings.enable">
                 <span slot="label">
                     Subscription
                     <a-tooltip>
@@ -31,7 +31,7 @@
                 <a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
                 <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
             </a-form-item>
-            <a-form-item v-if="client.email">
+            <a-form-item v-if="client.email && app.tgBotEnable">
                 <span slot="label">
                     Telegram ID
                     <a-tooltip>

+ 2 - 2
web/html/xui/form/protocol/trojan.html

@@ -18,7 +18,7 @@
                 <a-icon @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
                 <a-input v-model.trim="client.password" style="width: 150px;"></a-input>
             </a-form-item>
-            <a-form-item v-if="client.email">
+            <a-form-item v-if="client.email && app.subSettings.enable">
                 <span slot="label">
                     Subscription
                     <a-tooltip>
@@ -31,7 +31,7 @@
                 <a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
                 <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
             </a-form-item>
-            <a-form-item v-if="client.email">
+            <a-form-item v-if="client.email && app.tgBotEnable">
                 <span slot="label">
                     Telegram ID
                     <a-tooltip>

+ 2 - 2
web/html/xui/form/protocol/vless.html

@@ -18,7 +18,7 @@
                 <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
                 <a-input v-model.trim="client.id" style="width: 300px;"></a-input>
             </a-form-item>
-            <a-form-item v-if="client.email">
+            <a-form-item v-if="client.email && app.subSettings.enable">
                 <span slot="label">
                     Subscription
                     <a-tooltip>
@@ -31,7 +31,7 @@
                 <a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
                 <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
             </a-form-item>
-            <a-form-item v-if="client.email">
+            <a-form-item v-if="client.email && app.tgBotEnable">
                 <span slot="label">
                     Telegram ID
                     <a-tooltip>

+ 2 - 2
web/html/xui/form/protocol/vmess.html

@@ -23,7 +23,7 @@
                 <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
                 <a-input v-model.trim="client.id" style="width: 300px;"></a-input>
             </a-form-item>
-            <a-form-item v-if="client.email">
+            <a-form-item v-if="client.email && app.subSettings.enable">
                 <span slot="label">
                     Subscription
                     <a-tooltip>
@@ -36,7 +36,7 @@
                 <a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
                 <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
             </a-form-item>
-            <a-form-item v-if="client.email">
+            <a-form-item v-if="client.email && app.tgBotEnable">
                 <span slot="label">
                     Telegram ID
                     <a-tooltip>

+ 23 - 5
web/html/xui/inbounds.html

@@ -343,7 +343,15 @@
             clientCount: {},
             isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
             refreshing: false,
-            refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000
+            refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
+            subSettings: {
+                enable : false,
+                port: 0,
+                path: '',
+                domain: '',
+                tls: false
+            },
+            tgBotEnable: false
         },
         methods: {
             loading(spinning = true) {
@@ -365,10 +373,20 @@
                 if (!msg.success) {
                     return;
                 }
-                this.expireDiff = msg.obj.expireDiff * 86400000;
-                this.trafficDiff = msg.obj.trafficDiff * 1073741824;
-                this.defaultCert = msg.obj.defaultCert;
-                this.defaultKey = msg.obj.defaultKey;
+                with(msg.obj){
+                    this.expireDiff = expireDiff * 86400000;
+                    this.trafficDiff = trafficDiff * 1073741824;
+                    this.defaultCert = defaultCert;
+                    this.defaultKey = defaultKey;
+                    this.tgBotEnable = tgBotEnable;
+                    this.subSettings = {
+                        enable : subEnable,
+                        port: subPort,
+                        path: subPath,
+                        domain: subDomain,
+                        tls: subTLS
+                    };
+                }
             },
             setInbounds(dbInbounds) {
                 this.inbounds.splice(0);

+ 18 - 0
web/html/xui/settings.html

@@ -363,6 +363,24 @@
                                 </a-list-item>
                             </a-list>
                         </a-tab-pane>
+                        <a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }}'>
+                            <a-row :xs="24" :sm="24" :lg="12">
+                                <h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 20px 20px; text-align: center;">
+                                    <a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
+                                    {{ i18n "pages.settings.infoDesc" }}
+                                </h2>
+                            </a-row>
+                            <a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
+                                <setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
+                                <setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
+                                <setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort"></setting-list-item>
+                                <setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item>
+                                <setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
+                                <setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item>
+                                <setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></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-tab-pane>
                     </a-tabs>
                 </a-space>
             </a-spin>

+ 14 - 14
web/service/inbound.go

@@ -51,7 +51,7 @@ func (s *InboundService) checkPortExist(port int, ignoreId int) (bool, error) {
 	return count > 0, nil
 }
 
-func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, error) {
+func (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, error) {
 	settings := map[string][]model.Client{}
 	json.Unmarshal([]byte(inbound.Settings), &settings)
 	if settings == nil {
@@ -110,7 +110,7 @@ func (s *InboundService) checkEmailsExistForClients(clients []model.Client) (str
 }
 
 func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (string, error) {
-	clients, err := s.getClients(inbound)
+	clients, err := s.GetClients(inbound)
 	if err != nil {
 		return "", err
 	}
@@ -150,7 +150,7 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, err
 		return inbound, common.NewError("Duplicate email:", existEmail)
 	}
 
-	clients, err := s.getClients(inbound)
+	clients, err := s.GetClients(inbound)
 	if err != nil {
 		return inbound, err
 	}
@@ -208,7 +208,7 @@ func (s *InboundService) DelInbound(id int) error {
 	if err != nil {
 		return err
 	}
-	clients, err := s.getClients(inbound)
+	clients, err := s.GetClients(inbound)
 	if err != nil {
 		return err
 	}
@@ -263,7 +263,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
 }
 
 func (s *InboundService) AddInboundClient(data *model.Inbound) error {
-	clients, err := s.getClients(data)
+	clients, err := s.GetClients(data)
 	if err != nil {
 		return err
 	}
@@ -372,7 +372,7 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error
 }
 
 func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) error {
-	clients, err := s.getClients(data)
+	clients, err := s.GetClients(data)
 	if err != nil {
 		return err
 	}
@@ -390,7 +390,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
 		return err
 	}
 
-	oldClients, err := s.getClients(oldInbound)
+	oldClients, err := s.GetClients(oldInbound)
 	if err != nil {
 		return err
 	}
@@ -712,7 +712,7 @@ func (s *InboundService) GetClientByEmail(clientEmail string) (*xray.ClientTraff
 		return nil, nil, common.NewError("Inbound Not Found For Email:", clientEmail)
 	}
 
-	clients, err := s.getClients(inbound)
+	clients, err := s.GetClients(inbound)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -737,7 +737,7 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) err
 
 	clientEmail := traffic.Email
 
-	oldClients, err := s.getClients(inbound)
+	oldClients, err := s.GetClients(inbound)
 	if err != nil {
 		return err
 	}
@@ -791,7 +791,7 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, er
 		return false, common.NewError("Inbound Not Found For Email:", clientEmail)
 	}
 
-	oldClients, err := s.getClients(inbound)
+	oldClients, err := s.GetClients(inbound)
 	if err != nil {
 		return false, err
 	}
@@ -847,7 +847,7 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
 		return common.NewError("Inbound Not Found For Email:", clientEmail)
 	}
 
-	oldClients, err := s.getClients(inbound)
+	oldClients, err := s.GetClients(inbound)
 	if err != nil {
 		return err
 	}
@@ -901,7 +901,7 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
 		return common.NewError("Inbound Not Found For Email:", clientEmail)
 	}
 
-	oldClients, err := s.getClients(inbound)
+	oldClients, err := s.GetClients(inbound)
 	if err != nil {
 		return err
 	}
@@ -1100,7 +1100,7 @@ func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTr
 	}
 	var emails []string
 	for _, inbound := range inbounds {
-		clients, err := s.getClients(inbound)
+		clients, err := s.GetClients(inbound)
 		if err != nil {
 			logger.Error("Unable to get clients from inbound")
 		}
@@ -1250,7 +1250,7 @@ func (s *InboundService) MigrationRequirements() {
 			inbounds[inbound_index].Settings = string(modifiedSettings)
 		}
 		// Add client traffic row for all clients which has email
-		modelClients, err := s.getClients(inbounds[inbound_index])
+		modelClients, err := s.GetClients(inbounds[inbound_index])
 		if err != nil {
 			return
 		}

+ 50 - 0
web/service/setting.go

@@ -41,6 +41,14 @@ var defaultValueMap = map[string]string{
 	"tgCpu":              "0",
 	"tgLang":             "en-US",
 	"secretEnable":       "false",
+	"subEnable":          "false",
+	"subListen":          "",
+	"subPort":            "2096",
+	"subPath":            "sub/",
+	"subDomain":          "",
+	"subCertFile":        "",
+	"subKeyFile":         "",
+	"subUpdates":         "12",
 }
 
 type SettingService struct {
@@ -336,6 +344,48 @@ func (s *SettingService) GetTimeLocation() (*time.Location, error) {
 	return location, nil
 }
 
+func (s *SettingService) GetSubEnable() (bool, error) {
+	return s.getBool("subEnable")
+}
+
+func (s *SettingService) GetSubListen() (string, error) {
+	return s.getString("subListen")
+}
+
+func (s *SettingService) GetSubPort() (int, error) {
+	return s.getInt("subPort")
+}
+
+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
+}
+
+func (s *SettingService) GetSubDomain() (string, error) {
+	return s.getString("subDomain")
+}
+
+func (s *SettingService) GetSubCertFile() (string, error) {
+	return s.getString("subCertFile")
+}
+
+func (s *SettingService) GetSubKeyFile() (string, error) {
+	return s.getString("subKeyFile")
+}
+
+func (s *SettingService) GetSubUpdates() (int, error) {
+	return s.getInt("subUpdates")
+}
+
 func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
 	if err := allSetting.CheckValid(); err != nil {
 		return err

+ 0 - 2
web/web.go

@@ -83,7 +83,6 @@ type Server struct {
 	server *controller.ServerController
 	panel  *controller.XUIController
 	api    *controller.APIController
-	sub    *controller.SUBController
 
 	xrayService    service.XrayService
 	settingService service.SettingService
@@ -242,7 +241,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 	s.server = controller.NewServerController(g)
 	s.panel = controller.NewXUIController(g)
 	s.api = controller.NewAPIController(g)
-	s.sub = controller.NewSUBController(g)
 
 	return engine, nil
 }