diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 62bc6e7c..ca2be2f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5.0.0 with: - go-version: '1.21' + go-version: '1.22' - name: Install dependencies run: | @@ -116,12 +116,11 @@ jobs: - name: Package run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui - - name: Upload - uses: svenstaro/upload-release-action@2.7.0 + - name: Upload files to GH release + uses: MHSanaei/upload-release-action@2.8.0 with: repo_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ github.ref }} file: x-ui-linux-${{ matrix.platform }}.tar.gz asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz prerelease: true - overwrite: true diff --git a/Dockerfile b/Dockerfile index 5f4a36c3..010d9578 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # ======================================================== # Stage: Builder # ======================================================== -FROM golang:1.21-alpine AS builder +FROM golang:1.22-alpine AS builder WORKDIR /app ARG TARGETARCH diff --git a/README.md b/README.md index a1c902e2..36764e52 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,17 @@ certbot renew --dry-run ```sh ARCH=$(uname -m) -[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64" +case "${ARCH}" in + x86_64 | x64 | amd64) XUI_ARCH="amd64" ;; + i*86 | x86) XUI_ARCH="386" ;; + armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;; + armv7* | armv7) XUI_ARCH="armv7" ;; + armv6* | armv6) XUI_ARCH="armv6" ;; + armv5* | armv5) XUI_ARCH="armv5" ;; + *) XUI_ARCH="amd64" ;; +esac + + wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz ``` @@ -77,7 +87,16 @@ wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI ```sh ARCH=$(uname -m) -[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64" +case "${ARCH}" in + x86_64 | x64 | amd64) XUI_ARCH="amd64" ;; + i*86 | x86) XUI_ARCH="386" ;; + armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;; + armv7* | armv7) XUI_ARCH="armv7" ;; + armv6* | armv6) XUI_ARCH="armv6" ;; + armv5* | armv5) XUI_ARCH="armv5" ;; + *) XUI_ARCH="amd64" ;; +esac + cd /root/ rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz @@ -164,22 +183,26 @@ remove 3x-ui from docker - AlmaLinux 9+ - Rockylinux 9+ -## Compatible Architectures & Devices +## Supported Architectures and Devices -Supports a variety of different architectures and devices. Here are some of the main architectures that we support: +
+ Click for Supported Architectures and devices details -- **amd64**: This is the most common architecture for personal computers and servers. It supports most modern operating systems. +Our platform offers compatibility with a diverse range of architectures and devices, ensuring flexibility across various computing environments. The following are key architectures that we support: -- **x86 / i386**: This architecture is prevalent in desktop and laptop computers. It's widely supported by various operating systems and applications. (Ex: Most Windows, macOS, and Linux systems) +- **amd64**: This prevalent architecture is the standard for personal computers and servers, accommodating most modern operating systems seamlessly. -- **armv8 / arm64 / aarch64**: This is the architecture for modern mobile and embedded devices, including smartphones and tablets. (Ex: Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS,...) +- **x86 / i386**: Widely adopted in desktop and laptop computers, this architecture enjoys broad support from numerous operating systems and applications, including but not limited to Windows, macOS, and Linux systems. -- **armv7 / arm / arm32**: This is the architecture for older mobile and embedded devices. It is still widely used in many devices. (Ex: Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2,...) +- **armv8 / arm64 / aarch64**: Tailored for contemporary mobile and embedded devices, such as smartphones and tablets, this architecture is exemplified by devices like Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS, and more. -- **armv6 / arm / arm32**: This is the architecture for very old embedded devices. While not as common as before, there are still some devices using this architecture. (Ex: Raspberry Pi 1, Raspberry Pi Zero/Zero W,...) +- **armv7 / arm / arm32**: Serving as the architecture for older mobile and embedded devices, it remains widely utilized in devices like Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2, among others. + +- **armv6 / arm / arm32**: Geared towards very old embedded devices, this architecture, while less prevalent, is still in use. Devices such as Raspberry Pi 1, Raspberry Pi Zero/Zero W, rely on this architecture. + +- **armv5 / arm / arm32**: An older architecture primarily associated with early embedded systems, it is less common today but may still be found in legacy devices like early Raspberry Pi versions and some older smartphones. +
-- **armv5 / arm / arm32**: This is an older architecture primarily used in early embedded systems. While it's less common today, some legacy devices may still rely on this architecture. (Ex: Early versions of Raspberry Pi, some older smartphones) - ## Languages - English @@ -188,6 +211,7 @@ Supports a variety of different architectures and devices. Here are some of the - Russian - Vietnamese - Spanish +- Indonesian ## Features diff --git a/go.mod b/go.mod index 722df287..0d274bcb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module x-ui -go 1.21.4 +go 1.22.0 require ( github.com/Calidity/gin-sessions v1.3.1 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..92519f3e --- /dev/null +++ b/sub/subJsonService.go @@ -0,0 +1,355 @@ +package sub + +import ( + _ "embed" + "encoding/json" + "fmt" + "strings" + "x-ui/database/model" + "x-ui/logger" + "x-ui/util/json_util" + "x-ui/util/random" + "x-ui/web/service" + "x-ui/xray" +) + +//go:embed default.json +var defaultJson string + +type SubJsonService struct { + fragmanet string + + inboundService service.InboundService + SubService +} + +func NewSubJsonService(fragment string) *SubJsonService { + return &SubJsonService{ + fragmanet: fragment, + } +} + +func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) { + inbounds, err := s.SubService.getInboundsBySubId(subId) + if err != nil || len(inbounds) == 0 { + return "", "", err + } + + var header string + var traffic xray.ClientTraffic + var clientTraffics []xray.ClientTraffic + var configJson map[string]interface{} + var defaultOutbounds []json_util.RawMessage + + json.Unmarshal([]byte(defaultJson), &configJson) + if outboundSlices, ok := configJson["outbounds"].([]interface{}); ok { + for _, defaultOutbound := range outboundSlices { + jsonBytes, _ := json.Marshal(defaultOutbound) + defaultOutbounds = append(defaultOutbounds, jsonBytes) + } + } + + outbounds := []json_util.RawMessage{} + startIndex := 0 + // Prepare Inbounds + for _, inbound := range inbounds { + clients, err := s.inboundService.GetClients(inbound) + if err != nil { + logger.Error("SubJsonService - GetClients: Unable to get clients from inbound") + } + if clients == nil { + continue + } + if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' { + listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings) + if err == nil { + inbound.Listen = listen + inbound.Port = port + inbound.StreamSettings = streamSettings + } + } + + var subClients []model.Client + for _, client := range clients { + if client.Enable && client.SubID == subId { + subClients = append(subClients, client) + clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email)) + } + } + + outbound := s.getOutbound(inbound, subClients, host, startIndex) + if outbound != nil { + outbounds = append(outbounds, outbound...) + startIndex += len(outbound) + } + } + + if len(outbounds) == 0 { + return "", "", nil + } + + // Prepare statistics + for index, clientTraffic := range clientTraffics { + if index == 0 { + traffic.Up = clientTraffic.Up + traffic.Down = clientTraffic.Down + traffic.Total = clientTraffic.Total + if clientTraffic.ExpiryTime > 0 { + traffic.ExpiryTime = clientTraffic.ExpiryTime + } + } else { + traffic.Up += clientTraffic.Up + traffic.Down += clientTraffic.Down + if traffic.Total == 0 || clientTraffic.Total == 0 { + traffic.Total = 0 + } else { + traffic.Total += clientTraffic.Total + } + if clientTraffic.ExpiryTime != traffic.ExpiryTime { + traffic.ExpiryTime = 0 + } + } + } + + if s.fragmanet != "" { + outbounds = append(outbounds, json_util.RawMessage(s.fragmanet)) + } + + // Combile outbounds + outbounds = append(outbounds, defaultOutbounds...) + configJson["outbounds"] = outbounds + finalJson, _ := json.MarshalIndent(configJson, "", " ") + + header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) + return string(finalJson), header, nil +} + +func (s *SubJsonService) getOutbound(inbound *model.Inbound, clients []model.Client, host string, startIndex int) []json_util.RawMessage { + var newOutbounds []json_util.RawMessage + stream := s.streamData(inbound.StreamSettings) + + externalProxies, ok := stream["externalProxy"].([]interface{}) + if !ok || len(externalProxies) == 0 { + externalProxies = []interface{}{ + map[string]interface{}{ + "forceTls": "same", + "dest": host, + "port": float64(inbound.Port), + }, + } + } + + delete(stream, "externalProxy") + + config_index := startIndex + for _, ep := range externalProxies { + extPrxy := ep.(map[string]interface{}) + inbound.Listen = extPrxy["dest"].(string) + inbound.Port = int(extPrxy["port"].(float64)) + newStream := stream + switch extPrxy["forceTls"].(string) { + case "tls": + if newStream["security"] != "tls" { + newStream["security"] = "tls" + newStream["tslSettings"] = map[string]interface{}{} + } + case "none": + if newStream["security"] != "none" { + newStream["security"] = "none" + delete(newStream, "tslSettings") + } + } + streamSettings, _ := json.MarshalIndent(newStream, "", " ") + inbound.StreamSettings = string(streamSettings) + + for _, client := range clients { + inbound.Tag = fmt.Sprintf("proxy_%d", config_index) + switch inbound.Protocol { + case "vmess", "vless": + newOutbounds = append(newOutbounds, s.genVnext(inbound, client)) + case "trojan", "shadowsocks": + newOutbounds = append(newOutbounds, s.genServer(inbound, client)) + } + config_index += 1 + } + } + + return newOutbounds +} + +func (s *SubJsonService) streamData(stream string) map[string]interface{} { + var streamSettings map[string]interface{} + json.Unmarshal([]byte(stream), &streamSettings) + security, _ := streamSettings["security"].(string) + if security == "tls" { + streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]interface{})) + } else if security == "reality" { + streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]interface{})) + } + delete(streamSettings, "sockopt") + + if s.fragmanet != "" { + streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "TcpNoDelay": true}`) + } + + // remove proxy protocol + network, _ := streamSettings["network"].(string) + switch network { + case "tcp": + streamSettings["tcpSettings"] = s.removeAcceptProxy(streamSettings["tcpSettings"]) + case "ws": + streamSettings["wsSettings"] = s.removeAcceptProxy(streamSettings["wsSettings"]) + } + + return streamSettings +} + +func (s *SubJsonService) removeAcceptProxy(setting interface{}) map[string]interface{} { + netSettings, ok := setting.(map[string]interface{}) + if ok { + delete(netSettings, "acceptProxyProtocol") + } + return netSettings +} + +func (s *SubJsonService) tlsData(tData map[string]interface{}) map[string]interface{} { + tlsData := make(map[string]interface{}, 1) + tlsClientSettings := tData["settings"].(map[string]interface{}) + + tlsData["serverName"] = tData["serverName"] + tlsData["alpn"] = tData["alpn"] + if allowInsecure, ok := tlsClientSettings["allowInsecure"].(string); ok { + tlsData["allowInsecure"] = allowInsecure + } + if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok { + tlsData["fingerprint"] = fingerprint + } + return tlsData +} + +func (s *SubJsonService) realityData(rData map[string]interface{}) map[string]interface{} { + rltyData := make(map[string]interface{}, 1) + rltyClientSettings := rData["settings"].(map[string]interface{}) + + rltyData["show"] = false + rltyData["publicKey"] = rltyClientSettings["publicKey"] + rltyData["fingerprint"] = rltyClientSettings["fingerprint"] + + // Set random data + rltyData["spiderX"] = "/" + random.Seq(15) + shortIds, ok := rData["shortIds"].([]interface{}) + if ok && len(shortIds) > 0 { + rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string) + } else { + rltyData["shortId"] = "" + } + serverNames, ok := rData["serverNames"].([]interface{}) + if ok && len(serverNames) > 0 { + rltyData["serverName"] = serverNames[random.Num(len(serverNames))].(string) + } else { + rltyData["serverName"] = "" + } + + return rltyData +} + +func (s *SubJsonService) genVnext(inbound *model.Inbound, client model.Client) json_util.RawMessage { + outbound := Outbound{} + usersData := make([]UserVnext, 1) + + usersData[0].ID = client.ID + usersData[0].Level = 8 + if inbound.Protocol == model.VLESS { + usersData[0].Flow = client.Flow + usersData[0].Encryption = "none" + } + + vnextData := make([]VnextSetting, 1) + vnextData[0] = VnextSetting{ + Address: inbound.Listen, + Port: inbound.Port, + Users: usersData, + } + + outbound.Protocol = string(inbound.Protocol) + outbound.Tag = inbound.Tag + outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings) + outbound.Settings = OutboundSettings{ + Vnext: vnextData, + } + + result, _ := json.MarshalIndent(outbound, "", " ") + return result +} + +func (s *SubJsonService) genServer(inbound *model.Inbound, client model.Client) json_util.RawMessage { + outbound := Outbound{} + + serverData := make([]ServerSetting, 1) + serverData[0] = ServerSetting{ + Address: inbound.Listen, + Port: inbound.Port, + Level: 8, + Password: client.Password, + } + + if inbound.Protocol == model.Shadowsocks { + var inboundSettings map[string]interface{} + json.Unmarshal([]byte(inbound.Settings), &inboundSettings) + method, _ := inboundSettings["method"].(string) + serverData[0].Method = method + + // server password in multi-user 2022 protocols + if strings.HasPrefix(method, "2022") { + if serverPassword, ok := inboundSettings["password"].(string); ok { + serverData[0].Password = fmt.Sprintf("%s:%s", serverPassword, client.Password) + } + } + } + + outbound.Protocol = string(inbound.Protocol) + outbound.Tag = inbound.Tag + outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings) + outbound.Settings = OutboundSettings{ + Servers: serverData, + } + + result, _ := json.MarshalIndent(outbound, "", " ") + return result +} + +type Outbound struct { + Protocol string `json:"protocol"` + Tag string `json:"tag"` + StreamSettings json_util.RawMessage `json:"streamSettings"` + Mux map[string]interface{} `json:"mux,omitempty"` + ProxySettings map[string]interface{} `json:"proxySettings,omitempty"` + Settings OutboundSettings `json:"settings,omitempty"` +} + +type OutboundSettings struct { + Vnext []VnextSetting `json:"vnext,omitempty"` + Servers []ServerSetting `json:"servers,omitempty"` +} + +type VnextSetting struct { + Address string `json:"address"` + Port int `json:"port"` + Users []UserVnext `json:"users"` +} + +type UserVnext struct { + Encryption string `json:"encryption,omitempty"` + Flow string `json:"flow,omitempty"` + ID string `json:"id"` + Level int `json:"level"` +} + +type ServerSetting struct { + Password string `json:"password"` + Level int `json:"level"` + Address string `json:"address"` + Port int `json:"port"` + Flow string `json:"flow,omitempty"` + Method string `json:"method,omitempty"` +} diff --git a/sub/subService.go b/sub/subService.go index ddf9692b..06d1ed0a 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -25,47 +25,42 @@ type SubService struct { 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 } + s.datepicker, err = s.settingService.GetDatepicker() - if err != nil { - s.datepicker = "gregorian" - } + if err != nil { + s.datepicker = "gregorian" + } 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 { @@ -76,6 +71,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 @@ -97,11 +94,8 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string } } } - headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)) - updateInterval, _ := s.settingService.GetSubUpdates() - headers = append(headers, fmt.Sprintf("%d", updateInterval)) - headers = append(headers, subId) - return result, headers, nil + header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) + return result, header, nil } func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) { @@ -130,7 +124,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{}). @@ -138,9 +132,19 @@ func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) { Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest). 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 { @@ -578,6 +582,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { params["sni"], _ = sniValue.(string) } + tlsSettings, _ := searchKey(tlsSetting, "settings") if tlsSetting != nil { if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { diff --git a/web/assets/css/custom.css b/web/assets/css/custom.css index c41bcc16..8eb5037f 100644 --- a/web/assets/css/custom.css +++ b/web/assets/css/custom.css @@ -1050,6 +1050,12 @@ li.ant-select-dropdown-menu-item:empty:after { color: rgba(255, 255, 255, 0.25); } +.dark .ant-message-notice-content { + background-color: #222d42; + border: 1px solid #2c3950; + color: rgba(255, 255, 255, 0.65); +} + .ant-input-group.ant-input-group-compact-addon:not(:first-child):not( :last-child ), diff --git a/web/assets/favicon.ico b/web/assets/favicon.ico deleted file mode 100644 index 99b108ab..00000000 Binary files a/web/assets/favicon.ico and /dev/null differ diff --git a/web/assets/js/model/setting.js b/web/assets/js/model/setting.js index ddf1a0e1..637830e8 100644 --- a/web/assets/js/model/setting.js +++ b/web/assets/js/model/setting.js @@ -28,6 +28,7 @@ class AllSetting { this.subListen = ""; this.subPort = "2096"; this.subPath = "/sub/"; + this.subJsonPath = "/json/"; this.subDomain = ""; this.subCertFile = ""; this.subKeyFile = ""; @@ -35,6 +36,8 @@ class AllSetting { this.subEncrypt = true; this.subShowInfo = false; this.subURI = ''; + this.subJsonURI = ''; + this.subJsonFragment = ''; this.timeLocation = "Asia/Tehran"; diff --git a/web/assets/js/model/xray.js b/web/assets/js/model/xray.js index d7d1fa0d..791e8533 100644 --- a/web/assets/js/model/xray.js +++ b/web/assets/js/model/xray.js @@ -1146,10 +1146,6 @@ class Inbound extends XrayCommonClass { return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol); } - canSniffing() { - return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol); - } - reset() { this.port = RandomUtil.randomIntRange(10000, 60000); this.listen = ''; @@ -2299,7 +2295,7 @@ Inbound.WireguardSettings = class extends XrayCommonClass { } addPeer() { - this.peers.push(new Inbound.WireguardSettings.Peer()); + this.peers.push(new Inbound.WireguardSettings.Peer(null,null,'',['10.0.0.' + (this.peers.length+2)])); } delPeer(index) { @@ -2327,7 +2323,7 @@ Inbound.WireguardSettings = class extends XrayCommonClass { }; Inbound.WireguardSettings.Peer = class extends XrayCommonClass { - constructor(privateKey, publicKey, psk='', allowedIPs=['10.0.0.0/24'], keepAlive=0) { + constructor(privateKey, publicKey, psk='', allowedIPs=['10.0.0.2/32'], keepAlive=0) { super(); this.privateKey = privateKey this.publicKey = publicKey; @@ -2335,6 +2331,9 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass { [this.publicKey, this.privateKey] = Object.values(Wireguard.generateKeypair()) } this.psk = psk; + allowedIPs.forEach((a,index) => { + if (a.length>0 && !a.includes('/')) allowedIPs[index] += '/32'; + }) this.allowedIPs = allowedIPs; this.keepAlive = keepAlive; } @@ -2350,6 +2349,9 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass { } toJson() { + this.allowedIPs.forEach((a,index) => { + if (a.length>0 && !a.includes('/')) this.allowedIPs[index] += '/32'; + }); return { privateKey: this.privateKey, publicKey: this.publicKey, diff --git a/web/controller/xray_setting.go b/web/controller/xray_setting.go index 28f55b54..2dddb44b 100644 --- a/web/controller/xray_setting.go +++ b/web/controller/xray_setting.go @@ -81,7 +81,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 8ab06399..06850128 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -48,6 +48,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"` Datepicker string `json:"datepicker" form:"datepicker"` } @@ -105,6 +108,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/head.html b/web/html/common/head.html index e20cdc24..9d7919b6 100644 --- a/web/html/common/head.html +++ b/web/html/common/head.html @@ -7,8 +7,6 @@ - - -{{end}} +{{end}} \ No newline at end of file diff --git a/web/html/common/text_modal.html b/web/html/common/text_modal.html index 4fe2f175..68387be2 100644 --- a/web/html/common/text_modal.html +++ b/web/html/common/text_modal.html @@ -1,15 +1,16 @@ {{define "textModal"}} - - {{ i18n "download" }} [[ txtModal.fileName ]] - + :closable="true" + :class="themeSwitcher.currentTheme"> + + :autosize="{ minRows: 10, maxRows: 20}"> {{end}} \ No newline at end of file diff --git a/web/html/xui/dns_modal.html b/web/html/xui/dns_modal.html new file mode 100644 index 00000000..8b687ab2 --- /dev/null +++ b/web/html/xui/dns_modal.html @@ -0,0 +1,86 @@ +{{define "dnsModal"}} + + + + + + + + + + + + + + [[ l ]] + + + + + + +{{end}} diff --git a/web/html/xui/fakedns_modal.html b/web/html/xui/fakedns_modal.html new file mode 100644 index 00000000..c830f44d --- /dev/null +++ b/web/html/xui/fakedns_modal.html @@ -0,0 +1,57 @@ +{{define "fakednsModal"}} + + + + + + + + + + + +{{end}} diff --git a/web/html/xui/form/inbound.html b/web/html/xui/form/inbound.html index 6f3705ff..9453f0d7 100644 --- a/web/html/xui/form/inbound.html +++ b/web/html/xui/form/inbound.html @@ -114,7 +114,7 @@ -