diff --git a/web/controller/sub.go b/web/controller/sub.go
new file mode 100644
index 00000000..e7ade37b
--- /dev/null
+++ b/web/controller/sub.go
@@ -0,0 +1,41 @@
+package controller
+
+import (
+ "encoding/base64"
+ "x-ui/web/service"
+
+ "github.com/gin-gonic/gin"
+)
+
+type SUBController struct {
+ BaseController
+
+ subService service.SubService
+}
+
+func NewSUBController(g *gin.RouterGroup) *SUBController {
+ a := &SUBController{}
+ a.initRouter(g)
+ return a
+}
+
+func (a *SUBController) initRouter(g *gin.RouterGroup) {
+ g = g.Group("/sub")
+
+ g.GET("/:subid", a.subs)
+}
+
+func (a *SUBController) subs(c *gin.Context) {
+ subId := c.Param("subid")
+ host := c.Request.Host
+ subs, err := a.subService.GetSubs(subId, host)
+ if err != nil {
+ c.String(400, "Error!")
+ } else {
+ result := ""
+ for _, sub := range subs {
+ result += sub + "\n"
+ }
+ c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
+ }
+}
diff --git a/web/html/xui/form/client.html b/web/html/xui/form/client.html
index b03d999f..6b1b0f8f 100644
--- a/web/html/xui/form/client.html
+++ b/web/html/xui/form/client.html
@@ -27,6 +27,12 @@
+
+
+
+
+
+
{{ i18n "none" }}
diff --git a/web/service/sub.go b/web/service/sub.go
new file mode 100644
index 00000000..875a5b53
--- /dev/null
+++ b/web/service/sub.go
@@ -0,0 +1,505 @@
+package service
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/url"
+ "strings"
+ "x-ui/database"
+ "x-ui/database/model"
+ "x-ui/logger"
+
+ "github.com/goccy/go-json"
+ "gorm.io/gorm"
+)
+
+type SubService struct {
+ address string
+ inboundService InboundService
+}
+
+func (s *SubService) GetSubs(subId string, host string) ([]string, error) {
+ s.address = host
+ var result []string
+ inbounds, err := s.getInboundsBySubId(subId)
+ if err != nil {
+ return nil, err
+ }
+ for _, inbound := range inbounds {
+ clients, err := s.inboundService.getClients(inbound)
+ if err != nil {
+ logger.Error("SubService - GetSub: Unable to get clients from inbound")
+ }
+ if clients == nil {
+ continue
+ }
+ for _, client := range clients {
+ if client.SubID == subId {
+ link := s.getLink(inbound, client.Email)
+ result = append(result, link)
+ }
+ }
+ }
+ return result, nil
+}
+
+func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
+ db := database.GetDB()
+ var inbounds []*model.Inbound
+ err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error
+ if err != nil && err != gorm.ErrRecordNotFound {
+ return nil, err
+ }
+ return inbounds, nil
+}
+
+func (s *SubService) getLink(inbound *model.Inbound, email string) string {
+ switch inbound.Protocol {
+ case "vmess":
+ return s.genVmessLink(inbound, email)
+ case "vless":
+ return s.genVlessLink(inbound, email)
+ case "trojan":
+ return s.genTrojanLink(inbound, email)
+ }
+ return ""
+}
+
+func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
+ address := s.address
+ if inbound.Protocol != model.VMess {
+ return ""
+ }
+ var stream map[string]interface{}
+ json.Unmarshal([]byte(inbound.StreamSettings), &stream)
+ network, _ := stream["network"].(string)
+ typeStr := "none"
+ host := ""
+ path := ""
+ sni := ""
+ fp := ""
+ var alpn []string
+ allowInsecure := false
+ switch network {
+ case "tcp":
+ tcp, _ := stream["tcpSettings"].(map[string]interface{})
+ header, _ := tcp["header"].(map[string]interface{})
+ typeStr, _ = header["type"].(string)
+ if typeStr == "http" {
+ request := header["request"].(map[string]interface{})
+ requestPath, _ := request["path"].([]interface{})
+ path = requestPath[0].(string)
+ headers, _ := request["headers"].(map[string]interface{})
+ host = searchHost(headers)
+ }
+ case "kcp":
+ kcp, _ := stream["kcpSettings"].(map[string]interface{})
+ header, _ := kcp["header"].(map[string]interface{})
+ typeStr, _ = header["type"].(string)
+ path, _ = kcp["seed"].(string)
+ case "ws":
+ ws, _ := stream["wsSettings"].(map[string]interface{})
+ path = ws["path"].(string)
+ headers, _ := ws["headers"].(map[string]interface{})
+ host = searchHost(headers)
+ case "http":
+ network = "h2"
+ http, _ := stream["httpSettings"].(map[string]interface{})
+ path, _ = http["path"].(string)
+ host = searchHost(http)
+ case "quic":
+ quic, _ := stream["quicSettings"].(map[string]interface{})
+ header := quic["header"].(map[string]interface{})
+ typeStr, _ = header["type"].(string)
+ host, _ = quic["security"].(string)
+ path, _ = quic["key"].(string)
+ case "grpc":
+ grpc, _ := stream["grpcSettings"].(map[string]interface{})
+ path = grpc["serviceName"].(string)
+ }
+
+ security, _ := stream["security"].(string)
+ if security == "tls" {
+ tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
+ alpns, _ := tlsSetting["alpn"].([]interface{})
+ for _, a := range alpns {
+ alpn = append(alpn, a.(string))
+ }
+ tlsSettings, _ := searchKey(tlsSetting, "settings")
+ if tlsSetting != nil {
+ if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
+ sni, _ = sniValue.(string)
+ }
+ if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
+ fp, _ = fpValue.(string)
+ }
+ if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
+ allowInsecure, _ = insecure.(bool)
+ }
+ }
+ serverName, _ := tlsSetting["serverName"].(string)
+ if serverName != "" {
+ address = serverName
+ }
+ }
+
+ clients, _ := s.inboundService.getClients(inbound)
+ clientIndex := -1
+ for i, client := range clients {
+ if client.Email == email {
+ clientIndex = i
+ break
+ }
+ }
+
+ obj := map[string]interface{}{
+ "v": "2",
+ "ps": email,
+ "add": address,
+ "port": inbound.Port,
+ "id": clients[clientIndex].ID,
+ "aid": clients[clientIndex].AlterIds,
+ "net": network,
+ "type": typeStr,
+ "host": host,
+ "path": path,
+ "tls": security,
+ "sni": sni,
+ "fp": fp,
+ "alpn": strings.Join(alpn, ","),
+ "allowInsecure": allowInsecure,
+ }
+ jsonStr, _ := json.MarshalIndent(obj, "", " ")
+ return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
+}
+
+func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
+ address := s.address
+ if inbound.Protocol != model.VLESS {
+ return ""
+ }
+ var stream map[string]interface{}
+ json.Unmarshal([]byte(inbound.StreamSettings), &stream)
+ clients, _ := s.inboundService.getClients(inbound)
+ clientIndex := -1
+ for i, client := range clients {
+ if client.Email == email {
+ clientIndex = i
+ break
+ }
+ }
+ uuid := clients[clientIndex].ID
+ port := inbound.Port
+ streamNetwork := stream["network"].(string)
+ params := make(map[string]string)
+ params["type"] = streamNetwork
+
+ switch streamNetwork {
+ case "tcp":
+ tcp, _ := stream["tcpSettings"].(map[string]interface{})
+ header, _ := tcp["header"].(map[string]interface{})
+ typeStr, _ := header["type"].(string)
+ if typeStr == "http" {
+ request := header["request"].(map[string]interface{})
+ requestPath, _ := request["path"].([]interface{})
+ params["path"] = requestPath[0].(string)
+ headers, _ := request["headers"].(map[string]interface{})
+ params["host"] = searchHost(headers)
+ params["headerType"] = "http"
+ }
+ case "kcp":
+ kcp, _ := stream["kcpSettings"].(map[string]interface{})
+ header, _ := kcp["header"].(map[string]interface{})
+ params["headerType"] = header["type"].(string)
+ params["seed"] = kcp["seed"].(string)
+ case "ws":
+ ws, _ := stream["wsSettings"].(map[string]interface{})
+ params["path"] = ws["path"].(string)
+ headers, _ := ws["headers"].(map[string]interface{})
+ params["host"] = searchHost(headers)
+ case "http":
+ http, _ := stream["httpSettings"].(map[string]interface{})
+ params["path"] = http["path"].(string)
+ params["host"] = searchHost(http)
+ case "quic":
+ quic, _ := stream["quicSettings"].(map[string]interface{})
+ params["quicSecurity"] = quic["security"].(string)
+ params["key"] = quic["key"].(string)
+ header := quic["header"].(map[string]interface{})
+ params["headerType"] = header["type"].(string)
+ case "grpc":
+ grpc, _ := stream["grpcSettings"].(map[string]interface{})
+ params["serviceName"] = grpc["serviceName"].(string)
+ }
+
+ security, _ := stream["security"].(string)
+ if security == "tls" {
+ params["security"] = "tls"
+ tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
+ alpns, _ := tlsSetting["alpn"].([]interface{})
+ var alpn []string
+ for _, a := range alpns {
+ alpn = append(alpn, a.(string))
+ }
+ if len(alpn) > 0 {
+ params["alpn"] = strings.Join(alpn, ",")
+ }
+ tlsSettings, _ := searchKey(tlsSetting, "settings")
+ if tlsSetting != nil {
+ if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
+ params["sni"], _ = sniValue.(string)
+ }
+ if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
+ params["fp"], _ = fpValue.(string)
+ }
+ if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
+ if insecure.(bool) {
+ params["allowInsecure"] = "1"
+ }
+ }
+ }
+
+ if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
+ params["flow"] = clients[clientIndex].Flow
+ }
+
+ serverName, _ := tlsSetting["serverName"].(string)
+ if serverName != "" {
+ address = serverName
+ }
+ }
+
+ if security == "xtls" {
+ params["security"] = "xtls"
+ xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
+ alpns, _ := xtlsSetting["alpn"].([]interface{})
+ var alpn []string
+ for _, a := range alpns {
+ alpn = append(alpn, a.(string))
+ }
+ if len(alpn) > 0 {
+ params["alpn"] = strings.Join(alpn, ",")
+ }
+
+ xtlsSettings, _ := searchKey(xtlsSetting, "settings")
+ if xtlsSetting != nil {
+ if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
+ params["sni"], _ = sniValue.(string)
+ }
+ if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
+ params["fp"], _ = fpValue.(string)
+ }
+ if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
+ if insecure.(bool) {
+ params["allowInsecure"] = "1"
+ }
+ }
+ }
+
+ if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
+ params["flow"] = clients[clientIndex].Flow
+ }
+
+ serverName, _ := xtlsSetting["serverName"].(string)
+ if serverName != "" {
+ address = serverName
+ }
+ }
+
+ link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
+ url, _ := url.Parse(link)
+ q := url.Query()
+
+ for k, v := range params {
+ q.Add(k, v)
+ }
+
+ // Set the new query values on the URL
+ url.RawQuery = q.Encode()
+
+ url.Fragment = email
+ return url.String()
+}
+
+func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
+ address := s.address
+ if inbound.Protocol != model.Trojan {
+ return ""
+ }
+ var stream map[string]interface{}
+ json.Unmarshal([]byte(inbound.StreamSettings), &stream)
+ clients, _ := s.inboundService.getClients(inbound)
+ clientIndex := -1
+ for i, client := range clients {
+ if client.Email == email {
+ clientIndex = i
+ break
+ }
+ }
+ password := clients[clientIndex].Password
+ port := inbound.Port
+ streamNetwork := stream["network"].(string)
+ params := make(map[string]string)
+ params["type"] = streamNetwork
+
+ switch streamNetwork {
+ case "tcp":
+ tcp, _ := stream["tcpSettings"].(map[string]interface{})
+ header, _ := tcp["header"].(map[string]interface{})
+ typeStr, _ := header["type"].(string)
+ if typeStr == "http" {
+ request := header["request"].(map[string]interface{})
+ requestPath, _ := request["path"].([]interface{})
+ params["path"] = requestPath[0].(string)
+ headers, _ := request["headers"].(map[string]interface{})
+ params["host"] = searchHost(headers)
+ params["headerType"] = "http"
+ }
+ case "kcp":
+ kcp, _ := stream["kcpSettings"].(map[string]interface{})
+ header, _ := kcp["header"].(map[string]interface{})
+ params["headerType"] = header["type"].(string)
+ params["seed"] = kcp["seed"].(string)
+ case "ws":
+ ws, _ := stream["wsSettings"].(map[string]interface{})
+ params["path"] = ws["path"].(string)
+ headers, _ := ws["headers"].(map[string]interface{})
+ params["host"] = searchHost(headers)
+ case "http":
+ http, _ := stream["httpSettings"].(map[string]interface{})
+ params["path"] = http["path"].(string)
+ params["host"] = searchHost(http)
+ case "quic":
+ quic, _ := stream["quicSettings"].(map[string]interface{})
+ params["quicSecurity"] = quic["security"].(string)
+ params["key"] = quic["key"].(string)
+ header := quic["header"].(map[string]interface{})
+ params["headerType"] = header["type"].(string)
+ case "grpc":
+ grpc, _ := stream["grpcSettings"].(map[string]interface{})
+ params["serviceName"] = grpc["serviceName"].(string)
+ }
+
+ security, _ := stream["security"].(string)
+ if security == "tls" {
+ params["security"] = "tls"
+ tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
+ alpns, _ := tlsSetting["alpn"].([]interface{})
+ var alpn []string
+ for _, a := range alpns {
+ alpn = append(alpn, a.(string))
+ }
+ if len(alpn) > 0 {
+ params["alpn"] = strings.Join(alpn, ",")
+ }
+ tlsSettings, _ := searchKey(tlsSetting, "settings")
+ if tlsSetting != nil {
+ if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
+ params["sni"], _ = sniValue.(string)
+ }
+ if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
+ params["fp"], _ = fpValue.(string)
+ }
+ if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
+ if insecure.(bool) {
+ params["allowInsecure"] = "1"
+ }
+ }
+ }
+
+ serverName, _ := tlsSetting["serverName"].(string)
+ if serverName != "" {
+ address = serverName
+ }
+ }
+
+ if security == "xtls" {
+ params["security"] = "xtls"
+ xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
+ alpns, _ := xtlsSetting["alpn"].([]interface{})
+ var alpn []string
+ for _, a := range alpns {
+ alpn = append(alpn, a.(string))
+ }
+ if len(alpn) > 0 {
+ params["alpn"] = strings.Join(alpn, ",")
+ }
+
+ xtlsSettings, _ := searchKey(xtlsSetting, "settings")
+ if xtlsSetting != nil {
+ if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
+ params["sni"], _ = sniValue.(string)
+ }
+ if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
+ params["fp"], _ = fpValue.(string)
+ }
+ if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
+ if insecure.(bool) {
+ params["allowInsecure"] = "1"
+ }
+ }
+ }
+
+ if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
+ params["flow"] = clients[clientIndex].Flow
+ }
+
+ serverName, _ := xtlsSetting["serverName"].(string)
+ if serverName != "" {
+ address = serverName
+ }
+ }
+
+ link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
+
+ url, _ := url.Parse(link)
+ q := url.Query()
+
+ for k, v := range params {
+ q.Add(k, v)
+ }
+
+ // Set the new query values on the URL
+ url.RawQuery = q.Encode()
+
+ url.Fragment = email
+ return url.String()
+}
+
+func searchKey(data interface{}, key string) (interface{}, bool) {
+ switch val := data.(type) {
+ case map[string]interface{}:
+ for k, v := range val {
+ if k == key {
+ return v, true
+ }
+ if result, ok := searchKey(v, key); ok {
+ return result, true
+ }
+ }
+ case []interface{}:
+ for _, v := range val {
+ if result, ok := searchKey(v, key); ok {
+ return result, true
+ }
+ }
+ }
+ return nil, false
+}
+
+func searchHost(headers interface{}) string {
+ data, _ := headers.(map[string]interface{})
+ for k, v := range data {
+ if strings.EqualFold(k, "host") {
+ switch v.(type) {
+ case []interface{}:
+ hosts, _ := v.([]interface{})
+ return hosts[0].(string)
+ case interface{}:
+ return v.(string)
+ }
+ }
+ }
+
+ return ""
+}
diff --git a/web/web.go b/web/web.go
index 4880ff19..a86a0419 100644
--- a/web/web.go
+++ b/web/web.go
@@ -85,6 +85,7 @@ type Server struct {
server *controller.ServerController
xui *controller.XUIController
api *controller.APIController
+ sub *controller.SUBController
xrayService service.XrayService
settingService service.SettingService
@@ -208,6 +209,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
s.server = controller.NewServerController(g)
s.xui = controller.NewXUIController(g)
s.api = controller.NewAPIController(g)
+ s.sub = controller.NewSUBController(g)
return engine, nil
}