diff --git a/database/model/model.go b/database/model/model.go index 6225df52..c10cbdc7 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -117,6 +117,7 @@ type Client struct { Enable bool `json:"enable" form:"enable"` // Whether the client is enabled TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications SubID string `json:"subId" form:"subId"` // Subscription identifier + SubHost string `json:"subHost,omitempty"` // Optional host/IP override for exported client links Comment string `json:"comment" form:"comment"` // Client comment Reset int `json:"reset" form:"reset"` // Reset period in days CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp diff --git a/sub/subJsonService.go b/sub/subJsonService.go index 72c5c6f1..5e7257db 100644 --- a/sub/subJsonService.go +++ b/sub/subJsonService.go @@ -76,6 +76,8 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err if err != nil || len(inbounds) == 0 { return "", "", err } + requestHost := strings.TrimSpace(host) + defaultClientHost := s.SubService.getDefaultClientHost() var header string var traffic xray.ClientTraffic @@ -103,7 +105,8 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err for _, client := range clients { if client.Enable && client.SubID == subId { clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email)) - newConfigs := s.getConfig(inbound, client, host) + clientHost := s.SubService.ResolveClientHostWithDefault(inbound, client, requestHost, defaultClientHost) + newConfigs := s.getConfig(inbound, client, clientHost) configArray = append(configArray, newConfigs...) } } diff --git a/sub/subService.go b/sub/subService.go index 818f193b..9283f2cb 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -22,10 +22,8 @@ import ( // SubService provides business logic for generating subscription links and managing subscription data. type SubService struct { - address string showInfo bool remarkModel string - datepicker string inboundService service.InboundService settingService service.SettingService } @@ -40,7 +38,8 @@ func NewSubService(showInfo bool, remarkModel string) *SubService { // GetSubs retrieves subscription links for a given subscription ID and host. func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.ClientTraffic, error) { - s.address = host + requestHost := strings.TrimSpace(host) + defaultClientHost := s.getDefaultClientHost() var result []string var traffic xray.ClientTraffic var lastOnline int64 @@ -54,10 +53,6 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C return nil, 0, traffic, common.NewError("No inbounds found with ", subId) } - s.datepicker, err = s.settingService.GetDatepicker() - if err != nil { - s.datepicker = "gregorian" - } for _, inbound := range inbounds { clients, err := s.inboundService.GetClients(inbound) if err != nil { @@ -76,7 +71,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C } for _, client := range clients { if client.Enable && client.SubID == subId { - link := s.getLink(inbound, client.Email) + link := s.getLink(inbound, client.Email, requestHost, defaultClientHost) result = append(result, link) ct := s.getClientTraffics(inbound.ClientStats, client.Email) clientTraffics = append(clientTraffics, ct) @@ -161,33 +156,87 @@ func (s *SubService) getFallbackMaster(dest string, streamSettings string) (stri return inbound.Listen, inbound.Port, string(modifiedStream), nil } -func (s *SubService) getLink(inbound *model.Inbound, email string) string { +func (s *SubService) getDefaultClientHost() string { + defaultHost, err := s.settingService.GetSubDefaultHost() + if err != nil { + return "" + } + return strings.TrimSpace(defaultHost) +} + +func isWildcardListen(listen string) bool { + switch strings.ToLower(strings.TrimSpace(listen)) { + case "", "0.0.0.0", "::", "::0": + return true + default: + return false + } +} + +func (s *SubService) getInboundSubHost(inbound *model.Inbound) string { + var settings map[string]any + if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil { + return "" + } + subHost, _ := settings["subHost"].(string) + return strings.TrimSpace(subHost) +} + +func (s *SubService) resolveAddress(inbound *model.Inbound, client model.Client, requestHost string, defaultClientHost string) string { + if host := strings.TrimSpace(client.SubHost); host != "" { + return host + } + if host := s.getInboundSubHost(inbound); host != "" { + return host + } + if host := strings.TrimSpace(defaultClientHost); host != "" { + return host + } + if !isWildcardListen(inbound.Listen) { + return inbound.Listen + } + return requestHost +} + +func (s *SubService) ResolveClientHost(inbound *model.Inbound, client model.Client, requestHost string) string { + return s.ResolveClientHostWithDefault(inbound, client, requestHost, s.getDefaultClientHost()) +} + +func (s *SubService) ResolveClientHostWithDefault(inbound *model.Inbound, client model.Client, requestHost string, defaultClientHost string) string { + host := strings.TrimSpace(requestHost) + return s.resolveAddress(inbound, client, host, defaultClientHost) +} + +func findClientByEmail(clients []model.Client, email string) (model.Client, int) { + for i, client := range clients { + if client.Email == email { + return client, i + } + } + return model.Client{}, -1 +} + +func (s *SubService) getLink(inbound *model.Inbound, email string, requestHost string, defaultClientHost string) string { switch inbound.Protocol { case "vmess": - return s.genVmessLink(inbound, email) + return s.genVmessLink(inbound, email, requestHost, defaultClientHost) case "vless": - return s.genVlessLink(inbound, email) + return s.genVlessLink(inbound, email, requestHost, defaultClientHost) case "trojan": - return s.genTrojanLink(inbound, email) + return s.genTrojanLink(inbound, email, requestHost, defaultClientHost) case "shadowsocks": - return s.genShadowsocksLink(inbound, email) + return s.genShadowsocksLink(inbound, email, requestHost, defaultClientHost) } return "" } -func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { +func (s *SubService) genVmessLink(inbound *model.Inbound, email string, requestHost string, defaultClientHost string) string { if inbound.Protocol != model.VMESS { return "" } - var address string - if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { - address = s.address - } else { - address = inbound.Listen - } obj := map[string]any{ "v": "2", - "add": address, + "add": "", "port": inbound.Port, "type": "none", } @@ -274,15 +323,13 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { } clients, _ := s.inboundService.GetClients(inbound) - clientIndex := -1 - for i, client := range clients { - if client.Email == email { - clientIndex = i - break - } + client, clientIndex := findClientByEmail(clients, email) + if clientIndex < 0 { + return "" } - obj["id"] = clients[clientIndex].ID - obj["scy"] = clients[clientIndex].Security + obj["add"] = s.resolveAddress(inbound, client, requestHost, defaultClientHost) + obj["id"] = client.ID + obj["scy"] = client.Security externalProxies, _ := stream["externalProxy"].([]any) @@ -319,28 +366,18 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) } -func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { - var address string - if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { - address = s.address - } else { - address = inbound.Listen - } - +func (s *SubService) genVlessLink(inbound *model.Inbound, email string, requestHost string, defaultClientHost string) string { if inbound.Protocol != model.VLESS { return "" } var stream map[string]any 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 - } + client, clientIndex := findClientByEmail(clients, email) + if clientIndex < 0 { + return "" } - uuid := clients[clientIndex].ID + uuid := client.ID port := inbound.Port streamNetwork := stream["network"].(string) params := make(map[string]string) @@ -430,8 +467,8 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { } } - if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { - params["flow"] = clients[clientIndex].Flow + if streamNetwork == "tcp" && len(client.Flow) > 0 { + params["flow"] = client.Flow } } @@ -464,8 +501,8 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { params["spx"] = "/" + random.Seq(15) } - if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { - params["flow"] = clients[clientIndex].Flow + if streamNetwork == "tcp" && len(client.Flow) > 0 { + params["flow"] = client.Flow } } @@ -508,6 +545,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { return strings.Join(links, "\n") } + address := s.resolveAddress(inbound, client, requestHost, defaultClientHost) link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port) url, _ := url.Parse(link) q := url.Query() @@ -523,27 +561,18 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { return url.String() } -func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string { - var address string - if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { - address = s.address - } else { - address = inbound.Listen - } +func (s *SubService) genTrojanLink(inbound *model.Inbound, email string, requestHost string, defaultClientHost string) string { if inbound.Protocol != model.Trojan { return "" } var stream map[string]any 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 - } + client, clientIndex := findClientByEmail(clients, email) + if clientIndex < 0 { + return "" } - password := clients[clientIndex].Password + password := client.Password port := inbound.Port streamNetwork := stream["network"].(string) params := make(map[string]string) @@ -656,8 +685,8 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string params["spx"] = "/" + random.Seq(15) } - if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { - params["flow"] = clients[clientIndex].Flow + if streamNetwork == "tcp" && len(client.Flow) > 0 { + params["flow"] = client.Flow } } @@ -703,6 +732,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string return links } + address := s.resolveAddress(inbound, client, requestHost, defaultClientHost) link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port) url, _ := url.Parse(link) @@ -719,13 +749,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string return url.String() } -func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string { - var address string - if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { - address = s.address - } else { - address = inbound.Listen - } +func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, requestHost string, defaultClientHost string) string { if inbound.Protocol != model.Shadowsocks { return "" } @@ -737,12 +761,9 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st json.Unmarshal([]byte(inbound.Settings), &settings) inboundPassword := settings["password"].(string) method := settings["method"].(string) - clientIndex := -1 - for i, client := range clients { - if client.Email == email { - clientIndex = i - break - } + client, clientIndex := findClientByEmail(clients, email) + if clientIndex < 0 { + return "" } streamNetwork := stream["network"].(string) params := make(map[string]string) @@ -827,9 +848,9 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st } } - encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password) + encPart := fmt.Sprintf("%s:%s", method, client.Password) if method[0] == '2' { - encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password) + encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, client.Password) } externalProxies, _ := stream["externalProxy"].([]any) @@ -870,6 +891,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st return links } + address := s.resolveAddress(inbound, client, requestHost, defaultClientHost) link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port) url, _ := url.Parse(link) q := url.Query() @@ -1161,8 +1183,8 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray remained = common.FormatTraffic(left) } - datepicker := s.datepicker - if datepicker == "" { + datepicker, err := s.settingService.GetDatepicker() + if err != nil || datepicker == "" { datepicker = "gregorian" } diff --git a/web/assets/js/model/dbinbound.js b/web/assets/js/model/dbinbound.js index befc618e..8fbe6d57 100644 --- a/web/assets/js/model/dbinbound.js +++ b/web/assets/js/model/dbinbound.js @@ -144,8 +144,8 @@ class DBInbound { } } - genInboundLinks(remarkModel) { + genInboundLinks(remarkModel, defaultHost = '') { const inbound = this.toInbound(); - return inbound.genInboundLinks(this.remark, remarkModel); + return inbound.genInboundLinks(this.remark, remarkModel, defaultHost); } -} \ No newline at end of file +} diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index b6059cf7..17ed0128 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -1725,10 +1725,25 @@ class Inbound extends XrayCommonClass { } } - genAllLinks(remark = '', remarkModel = '-ieo', client) { + resolveLinkHost(client, defaultHost = '') { + if (client?.subHost && client.subHost.trim().length > 0) { + return client.subHost.trim(); + } + if (this.settings?.subHost && this.settings.subHost.trim().length > 0) { + return this.settings.subHost.trim(); + } + if (defaultHost && defaultHost.trim().length > 0) { + return defaultHost.trim(); + } + return !ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0" && this.listen !== "::" && this.listen !== "::0" + ? this.listen + : location.hostname; + } + + genAllLinks(remark = '', remarkModel = '-ieo', client, defaultHost = '') { let result = []; let email = client ? client.email : ''; - let addr = !ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0" ? this.listen : location.hostname; + let addr = this.resolveLinkHost(client, defaultHost); let port = this.port; const separationChar = remarkModel.charAt(0); const orderChars = remarkModel.slice(1); @@ -1756,12 +1771,12 @@ class Inbound extends XrayCommonClass { return result; } - genInboundLinks(remark = '', remarkModel = '-ieo') { - let addr = !ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0" ? this.listen : location.hostname; + genInboundLinks(remark = '', remarkModel = '-ieo', defaultHost = '') { + let addr = this.resolveLinkHost(null, defaultHost); if (this.clients) { let links = []; this.clients.forEach((client) => { - this.genAllLinks(remark, remarkModel, client).forEach(l => { + this.genAllLinks(remark, remarkModel, client, defaultHost).forEach(l => { links.push(l.link); }) }); @@ -1811,9 +1826,10 @@ class Inbound extends XrayCommonClass { } Inbound.Settings = class extends XrayCommonClass { - constructor(protocol) { + constructor(protocol, subHost = '') { super(); this.protocol = protocol; + this.subHost = subHost; } static getSettings(protocol) { @@ -1877,15 +1893,18 @@ Inbound.VmessSettings = class extends Inbound.Settings { } static fromJson(json = {}) { - return new Inbound.VmessSettings( + const obj = new Inbound.VmessSettings( Protocols.VMESS, json.clients.map(client => Inbound.VmessSettings.VMESS.fromJson(client)), ); + obj.subHost = json.subHost || ''; + return obj; } toJson() { return { clients: Inbound.VmessSettings.toJsonArray(this.vmesses), + subHost: this.subHost || undefined, }; } }; @@ -1901,6 +1920,7 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass { enable = true, tgId = '', subId = RandomUtil.randomLowerAndNum(16), + subHost = '', comment = '', reset = 0, created_at = undefined, @@ -1916,6 +1936,7 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass { this.enable = enable; this.tgId = tgId; this.subId = subId; + this.subHost = subHost; this.comment = comment; this.reset = reset; this.created_at = created_at; @@ -1933,6 +1954,7 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass { json.enable, json.tgId, json.subId, + json.subHost, json.comment, json.reset, json.created_at, @@ -2009,6 +2031,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings { json.selectedAuth, testseed ); + obj.subHost = json.subHost || ''; return obj; } @@ -2032,6 +2055,9 @@ Inbound.VLESSSettings = class extends Inbound.Settings { if (this.selectedAuth) { json.selectedAuth = this.selectedAuth; } + if (this.subHost) { + json.subHost = this.subHost; + } // Only include testseed if at least one client has a flow set const hasFlow = this.vlesses && this.vlesses.some(vless => vless.flow && vless.flow !== ''); @@ -2056,6 +2082,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { enable = true, tgId = '', subId = RandomUtil.randomLowerAndNum(16), + subHost = '', comment = '', reset = 0, created_at = undefined, @@ -2071,6 +2098,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { this.enable = enable; this.tgId = tgId; this.subId = subId; + this.subHost = subHost; this.comment = comment; this.reset = reset; this.created_at = created_at; @@ -2088,6 +2116,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { json.enable, json.tgId, json.subId, + json.subHost, json.comment, json.reset, json.created_at, @@ -2177,16 +2206,19 @@ Inbound.TrojanSettings = class extends Inbound.Settings { } static fromJson(json = {}) { - return new Inbound.TrojanSettings( + const obj = new Inbound.TrojanSettings( Protocols.TROJAN, json.clients.map(client => Inbound.TrojanSettings.Trojan.fromJson(client)), Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),); + obj.subHost = json.subHost || ''; + return obj; } toJson() { return { clients: Inbound.TrojanSettings.toJsonArray(this.trojans), - fallbacks: Inbound.TrojanSettings.toJsonArray(this.fallbacks) + fallbacks: Inbound.TrojanSettings.toJsonArray(this.fallbacks), + subHost: this.subHost || undefined, }; } }; @@ -2201,6 +2233,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass { enable = true, tgId = '', subId = RandomUtil.randomLowerAndNum(16), + subHost = '', comment = '', reset = 0, created_at = undefined, @@ -2215,6 +2248,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass { this.enable = enable; this.tgId = tgId; this.subId = subId; + this.subHost = subHost; this.comment = comment; this.reset = reset; this.created_at = created_at; @@ -2231,6 +2265,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass { enable: this.enable, tgId: this.tgId, subId: this.subId, + subHost: this.subHost, comment: this.comment, reset: this.reset, created_at: this.created_at, @@ -2248,6 +2283,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass { json.enable, json.tgId, json.subId, + json.subHost, json.comment, json.reset, json.created_at, @@ -2338,7 +2374,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings { } static fromJson(json = {}) { - return new Inbound.ShadowsocksSettings( + const obj = new Inbound.ShadowsocksSettings( Protocols.SHADOWSOCKS, json.method, json.password, @@ -2346,6 +2382,8 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings { json.clients.map(client => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)), json.ivCheck, ); + obj.subHost = json.subHost || ''; + return obj; } toJson() { @@ -2355,6 +2393,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings { network: this.network, clients: Inbound.ShadowsocksSettings.toJsonArray(this.shadowsockses), ivCheck: this.ivCheck, + subHost: this.subHost || undefined, }; } }; @@ -2370,6 +2409,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { enable = true, tgId = '', subId = RandomUtil.randomLowerAndNum(16), + subHost = '', comment = '', reset = 0, created_at = undefined, @@ -2385,6 +2425,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { this.enable = enable; this.tgId = tgId; this.subId = subId; + this.subHost = subHost; this.comment = comment; this.reset = reset; this.created_at = created_at; @@ -2402,6 +2443,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { enable: this.enable, tgId: this.tgId, subId: this.subId, + subHost: this.subHost, comment: this.comment, reset: this.reset, created_at: this.created_at, @@ -2420,6 +2462,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { json.enable, json.tgId, json.subId, + json.subHost, json.comment, json.reset, json.created_at, diff --git a/web/assets/js/model/setting.js b/web/assets/js/model/setting.js index af80a63e..822be08d 100644 --- a/web/assets/js/model/setting.js +++ b/web/assets/js/model/setting.js @@ -39,6 +39,7 @@ class AllSetting { this.subPath = "/sub/"; this.subJsonPath = "/json/"; this.subDomain = ""; + this.subDefaultHost = ""; this.externalTrafficInformEnable = false; this.externalTrafficInformURI = ""; this.subCertFile = ""; @@ -86,4 +87,4 @@ class AllSetting { equals(other) { return ObjectUtil.equals(this, other); } -} \ No newline at end of file +} diff --git a/web/entity/entity.go b/web/entity/entity.go index 40294925..0a3ad2e4 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -66,6 +66,7 @@ type AllSetting struct { SubPort int `json:"subPort" form:"subPort"` // Subscription server port SubPath string `json:"subPath" form:"subPath"` // Base path for subscription URLs SubDomain string `json:"subDomain" form:"subDomain"` // Domain for subscription server validation + SubDefaultHost string `json:"subDefaultHost" form:"subDefaultHost"` // Default host/IP used in exported client links SubCertFile string `json:"subCertFile" form:"subCertFile"` // SSL certificate file for subscription server SubKeyFile string `json:"subKeyFile" form:"subKeyFile"` // SSL private key file for subscription server SubUpdates int `json:"subUpdates" form:"subUpdates"` // Subscription update interval in minutes diff --git a/web/html/form/client.html b/web/html/form/client.html index 908f28d2..2edac542 100644 --- a/web/html/form/client.html +++ b/web/html/form/client.html @@ -56,6 +56,18 @@ + + + +