Merge branch 'MHSanaei:main' into main

This commit is contained in:
Дмитрий Олегович Саенко 2025-09-25 15:09:05 +03:00 committed by GitHub
commit f31351e631
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 402 additions and 138 deletions

View file

@ -1,7 +1,9 @@
name: Release 3X-UI for Docker name: Release 3X-UI for Docker
permissions: permissions:
contents: read contents: read
packages: write packages: write
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
@ -13,48 +15,48 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
submodules: true submodules: true
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
hsanaeii/3x-ui
ghcr.io/mhsanaei/3x-ui
tags: |
type=ref,event=branch
type=ref,event=tag
type=pep440,pattern={{version}}
- name: Set up QEMU - name: Docker meta
uses: docker/setup-qemu-action@v3 id: meta
uses: docker/metadata-action@v5
with:
images: |
hsanaeii/3x-ui
ghcr.io/mhsanaei/3x-ui
tags: |
type=ref,event=branch
type=ref,event=tag
type=semver,pattern={{version}}
- name: Set up Docker Buildx - name: Set up QEMU
uses: docker/setup-buildx-action@v3 uses: docker/setup-qemu-action@v3
with:
install: true
- name: Login to Docker Hub - name: Set up Docker Buildx
uses: docker/login-action@v3 uses: docker/setup-buildx-action@v3
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} install: true
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GHCR - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io username: ${{ secrets.DOCKER_HUB_USERNAME }}
username: ${{ github.repository_owner }} password: ${{ secrets.DOCKER_HUB_TOKEN }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image - name: Login to GHCR
uses: docker/build-push-action@v6 uses: docker/login-action@v3
with: with:
context: . registry: ghcr.io
push: true username: ${{ github.actor }}
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386 password: ${{ secrets.GITHUB_TOKEN }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} - name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View file

@ -1 +1 @@
2.8.3 2.8.4

2
go.mod
View file

@ -95,7 +95,7 @@ require (
golang.org/x/tools v0.37.0 // indirect golang.org/x/tools v0.37.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
google.golang.org/protobuf v1.36.9 // indirect google.golang.org/protobuf v1.36.9 // indirect
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
lukechampine.com/blake3 v1.4.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect

2
go.sum
View file

@ -236,6 +236,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w= google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=

View file

@ -56,6 +56,9 @@ install_base() {
opensuse-tumbleweed) opensuse-tumbleweed)
zypper refresh && zypper -q install -y wget curl tar timezone zypper refresh && zypper -q install -y wget curl tar timezone
;; ;;
alpine)
apk update && apk add wget curl tar tzdata
;;
*) *)
apt-get update && apt-get install -y -q wget curl tar tzdata apt-get update && apt-get install -y -q wget curl tar tzdata
;; ;;
@ -177,7 +180,11 @@ install_x-ui() {
# Stop x-ui service and remove old resources # Stop x-ui service and remove old resources
if [[ -e /usr/local/x-ui/ ]]; then if [[ -e /usr/local/x-ui/ ]]; then
systemctl stop x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm /usr/local/x-ui/ -rf rm /usr/local/x-ui/ -rf
fi fi
@ -201,10 +208,18 @@ install_x-ui() {
chmod +x /usr/bin/x-ui chmod +x /usr/bin/x-ui
config_after_install config_after_install
cp -f x-ui.service /etc/systemd/system/ if [[ $release == "alpine" ]]; then
systemctl daemon-reload wget -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
systemctl enable x-ui chmod +x /etc/init.d/x-ui
systemctl start x-ui rc-update add x-ui
rc-service x-ui start
else
cp -f x-ui.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable x-ui
systemctl start x-ui
fi
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..." echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
echo -e "" echo -e ""
echo -e "┌───────────────────────────────────────────────────────┐ echo -e "┌───────────────────────────────────────────────────────┐

View file

@ -98,8 +98,14 @@ func (s *Server) initRouter() (*gin.Engine, error) {
} }
// Set base_path based on LinksPath for template rendering // Set base_path based on LinksPath for template rendering
// Ensure LinksPath ends with "/" for proper asset URL generation
basePath := LinksPath
if basePath != "/" && !strings.HasSuffix(basePath, "/") {
basePath += "/"
}
logger.Debug("sub: Setting base_path to:", basePath)
engine.Use(func(c *gin.Context) { engine.Use(func(c *gin.Context) {
c.Set("base_path", LinksPath) c.Set("base_path", basePath)
}) })
Encrypt, err := s.settingService.GetSubEncrypt() Encrypt, err := s.settingService.GetSubEncrypt()
@ -179,22 +185,48 @@ func (s *Server) initRouter() (*gin.Engine, error) {
linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets" linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
} }
// Mount assets in multiple paths to handle different URL patterns
var assetsFS http.FileSystem
if _, err := os.Stat("web/assets"); err == nil { if _, err := os.Stat("web/assets"); err == nil {
engine.StaticFS("/assets", http.FS(os.DirFS("web/assets"))) assetsFS = http.FS(os.DirFS("web/assets"))
if linksPathForAssets != "/assets" {
engine.StaticFS(linksPathForAssets, http.FS(os.DirFS("web/assets")))
}
} else { } else {
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil { if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
engine.StaticFS("/assets", http.FS(subFS)) assetsFS = http.FS(subFS)
if linksPathForAssets != "/assets" {
engine.StaticFS(linksPathForAssets, http.FS(subFS))
}
} else { } else {
logger.Error("sub: failed to mount embedded assets:", err) logger.Error("sub: failed to mount embedded assets:", err)
} }
} }
if assetsFS != nil {
engine.StaticFS("/assets", assetsFS)
if linksPathForAssets != "/assets" {
engine.StaticFS(linksPathForAssets, assetsFS)
}
// Add middleware to handle dynamic asset paths with subid
if LinksPath != "/" {
engine.Use(func(c *gin.Context) {
path := c.Request.URL.Path
// Check if this is an asset request with subid pattern: /sub/path/{subid}/assets/...
pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
// Extract the asset path after /assets/
assetsIndex := strings.Index(path, "/assets/")
if assetsIndex != -1 {
assetPath := path[assetsIndex+8:] // +8 to skip "/assets/"
if assetPath != "" {
// Serve the asset file
c.FileFromFS(assetPath, assetsFS)
c.Abort()
return
}
}
}
c.Next()
})
}
}
g := engine.Group("/") g := engine.Group("/")
s.sub = NewSUBController( s.sub = NewSUBController(

View file

@ -87,7 +87,20 @@ func (a *SUBController) subs(c *gin.Context) {
if !a.jsonEnabled { if !a.jsonEnabled {
subJsonURL = "" subJsonURL = ""
} }
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL) // Get base_path from context (set by middleware)
basePath, exists := c.Get("base_path")
if !exists {
basePath = "/"
}
// Add subId to base_path for asset URLs
basePathStr := basePath.(string)
if basePathStr == "/" {
basePathStr = "/" + subId + "/"
} else {
// Remove trailing slash if exists, add subId, then add trailing slash
basePathStr = strings.TrimRight(basePathStr, "/") + "/" + subId + "/"
}
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, basePathStr)
c.HTML(200, "subpage.html", gin.H{ c.HTML(200, "subpage.html", gin.H{
"title": "subscription.title", "title": "subscription.title",
"cur_ver": config.GetVersion(), "cur_ver": config.GetVersion(),

View file

@ -1148,7 +1148,7 @@ func (s *SubService) joinPathWithID(basePath, subId string) string {
// BuildPageData parses header and prepares the template view model. // BuildPageData parses header and prepares the template view model.
// BuildPageData constructs page data for rendering the subscription information page. // BuildPageData constructs page data for rendering the subscription information page.
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string) PageData { func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string, basePath string) PageData {
download := common.FormatTraffic(traffic.Down) download := common.FormatTraffic(traffic.Down)
upload := common.FormatTraffic(traffic.Up) upload := common.FormatTraffic(traffic.Up)
total := "∞" total := "∞"
@ -1167,7 +1167,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
return PageData{ return PageData{
Host: hostHeader, Host: hostHeader,
BasePath: "/", // kept as "/"; templates now use context base_path injected from router BasePath: basePath,
SId: subId, SId: subId,
Download: download, Download: download,
Upload: upload, Upload: upload,

View file

@ -142,7 +142,10 @@
}, },
npvtunUrl() { npvtunUrl() {
return this.app.subUrl; return this.app.subUrl;
} },
happUrl() {
return `happ://add/${encodeURIComponent(this.app.subUrl)}`;
}
}, },
methods: { methods: {
renderLink, renderLink,

View file

@ -1,7 +1,10 @@
package controller package controller
import ( import (
"net/http"
"github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/mhsanaei/3x-ui/v2/web/session"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -21,11 +24,21 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
return a return a
} }
// checkAPIAuth is a middleware that returns 404 for unauthenticated API requests
// to hide the existence of API endpoints from unauthorized users
func (a *APIController) checkAPIAuth(c *gin.Context) {
if !session.IsLogin(c) {
c.AbortWithStatus(http.StatusNotFound)
return
}
c.Next()
}
// initRouter sets up the API routes for inbounds, server, and other endpoints. // initRouter sets up the API routes for inbounds, server, and other endpoints.
func (a *APIController) initRouter(g *gin.RouterGroup) { func (a *APIController) initRouter(g *gin.RouterGroup) {
// Main API group // Main API group
api := g.Group("/panel/api") api := g.Group("/panel/api")
api.Use(a.checkLogin) api.Use(a.checkAPIAuth)
// Inbounds API // Inbounds API
inbounds := api.Group("/inbounds") inbounds := api.Group("/inbounds")

View file

@ -39,8 +39,9 @@ func NewIndexController(g *gin.RouterGroup) *IndexController {
// initRouter sets up the routes for index, login, logout, and two-factor authentication. // initRouter sets up the routes for index, login, logout, and two-factor authentication.
func (a *IndexController) initRouter(g *gin.RouterGroup) { func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index) g.GET("/", a.index)
g.POST("/login", a.login)
g.GET("/logout", a.logout) g.GET("/logout", a.logout)
g.POST("/login", a.login)
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable) g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
} }

View file

@ -8,8 +8,6 @@ import (
type XUIController struct { type XUIController struct {
BaseController BaseController
inboundController *InboundController
serverController *ServerController
settingController *SettingController settingController *SettingController
xraySettingController *XraySettingController xraySettingController *XraySettingController
} }
@ -31,8 +29,6 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
g.GET("/settings", a.settings) g.GET("/settings", a.settings)
g.GET("/xray", a.xraySettings) g.GET("/xray", a.xraySettings)
a.inboundController = NewInboundController(g)
a.serverController = NewServerController(g)
a.settingController = NewSettingController(g) a.settingController = NewSettingController(g)
a.xraySettingController = NewXraySettingController(g) a.xraySettingController = NewXraySettingController(g)
} }

View file

@ -4,7 +4,7 @@
{{ template "page/body_start" .}} {{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'"> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
<transition name="list" appear> <transition name="list" appear>
<a-layout-content class="under min-h-0"> <a-layout-content class="under min-h-0">
<div class="waves-header"> <div class="waves-header">
<div class="waves-inner-header"></div> <div class="waves-inner-header"></div>
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" <svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
@ -20,7 +20,7 @@
</g> </g>
</svg> </svg>
</div> </div>
<a-row type="flex" justify="center" align="middle" class="h-100 overflow-y-auto overflow-x-hidden"> <a-row type="flex" justify="center" align="middle" class="h-100 overflow-y-auto overflow-x-hidden">
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem"> <a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem">
<template v-if="!loadingStates.fetched"> <template v-if="!loadingStates.fetched">
<div class="text-center"> <div class="text-center">
@ -35,8 +35,8 @@
<a-space direction="vertical" :size="10"> <a-space direction="vertical" :size="10">
<a-theme-switch-login></a-theme-switch-login> <a-theme-switch-login></a-theme-switch-login>
<span>{{ i18n "pages.settings.language" }}</span> <span>{{ i18n "pages.settings.language" }}</span>
<a-select ref="selectLang" class="w-100" v-model="lang" <a-select ref="selectLang" class="w-100" v-model="lang" @change="LanguageManager.setLanguage(lang)"
@change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages"> <a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
<span role="img" aria-label="l.name" v-text="l.icon"></span> <span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span> &nbsp;&nbsp;<span v-text="l.name"></span>
@ -68,7 +68,7 @@
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-input-password autocomplete="password" name="password" v-model.trim="user.password" <a-input-password autocomplete="current-password" name="password" v-model.trim="user.password"
placeholder='{{ i18n "password" }}' required> placeholder='{{ i18n "password" }}' required>
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon> <a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
</a-input-password> </a-input-password>
@ -81,7 +81,8 @@
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-row justify="center" class="centered"> <a-row justify="center" class="centered">
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem" :style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'"> <div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem"
:style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning" <a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit"> :icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]] [[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]

View file

@ -218,6 +218,8 @@
<a-menu-item key="android-npvtunnel" <a-menu-item key="android-npvtunnel"
@click="copy(app.subUrl)">NPV @click="copy(app.subUrl)">NPV
Tunnel</a-menu-item> Tunnel</a-menu-item>
<a-menu-item key="android-happ"
@click="open('happ://add/' + encodeURIComponent(app.subUrl))">Happ</a-menu-item>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
</a-col> </a-col>
@ -244,6 +246,8 @@
@click="copy(npvtunUrl)">NPV @click="copy(npvtunUrl)">NPV
Tunnel Tunnel
</a-menu-item> </a-menu-item>
<a-menu-item key="ios-happ"
@click="open(happUrl)">Happ</a-menu-item>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
</a-col> </a-col>

View file

@ -35,6 +35,25 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
return nil, err return nil, err
} }
// Enrich client stats with UUID/SubId from inbound settings
for _, inbound := range inbounds {
clients, _ := s.GetClients(inbound)
if len(clients) == 0 || len(inbound.ClientStats) == 0 {
continue
}
// Build a map email -> client
cMap := make(map[string]model.Client, len(clients))
for _, c := range clients {
cMap[strings.ToLower(c.Email)] = c
}
for i := range inbound.ClientStats {
email := strings.ToLower(inbound.ClientStats[i].Email)
if c, ok := cMap[email]; ok {
inbound.ClientStats[i].UUID = c.ID
inbound.ClientStats[i].SubId = c.SubID
}
}
}
return inbounds, nil return inbounds, nil
} }
@ -47,6 +66,24 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
return nil, err return nil, err
} }
// Enrich client stats with UUID/SubId from inbound settings
for _, inbound := range inbounds {
clients, _ := s.GetClients(inbound)
if len(clients) == 0 || len(inbound.ClientStats) == 0 {
continue
}
cMap := make(map[string]model.Client, len(clients))
for _, c := range clients {
cMap[strings.ToLower(c.Email)] = c
}
for i := range inbound.ClientStats {
email := strings.ToLower(inbound.ClientStats[i].Email)
if c, ok := cMap[email]; ok {
inbound.ClientStats[i].UUID = c.ID
inbound.ClientStats[i].SubId = c.SubID
}
}
}
return inbounds, nil return inbounds, nil
} }

View file

@ -95,10 +95,9 @@ type Server struct {
httpServer *http.Server httpServer *http.Server
listener net.Listener listener net.Listener
index *controller.IndexController index *controller.IndexController
server *controller.ServerController panel *controller.XUIController
panel *controller.XUIController api *controller.APIController
api *controller.APIController
xrayService service.XrayService xrayService service.XrayService
settingService service.SettingService settingService service.SettingService
@ -264,10 +263,14 @@ func (s *Server) initRouter() (*gin.Engine, error) {
g := engine.Group(basePath) g := engine.Group(basePath)
s.index = controller.NewIndexController(g) s.index = controller.NewIndexController(g)
s.server = controller.NewServerController(g)
s.panel = controller.NewXUIController(g) s.panel = controller.NewXUIController(g)
s.api = controller.NewAPIController(g) s.api = controller.NewAPIController(g)
// Add a catch-all route to handle undefined paths and return 404
engine.NoRoute(func(c *gin.Context) {
c.AbortWithStatus(http.StatusNotFound)
})
return engine, nil return engine, nil
} }

13
x-ui.rc Normal file
View file

@ -0,0 +1,13 @@
#!/sbin/openrc-run
command="/usr/local/x-ui/x-ui"
command_background=true
pidfile="/run/x-ui.pid"
description="x-ui Service"
procname="x-ui"
depend() {
need net
}
start_pre(){
cd /usr/local/x-ui
}

251
x-ui.sh
View file

@ -153,11 +153,19 @@ uninstall() {
fi fi
return 0 return 0
fi fi
systemctl stop x-ui
systemctl disable x-ui if [[ $release == "alpine" ]]; then
rm /etc/systemd/system/x-ui.service -f rc-service x-ui stop
systemctl daemon-reload rc-update del x-ui
systemctl reset-failed rm /etc/init.d/x-ui -f
else
systemctl stop x-ui
systemctl disable x-ui
rm /etc/systemd/system/x-ui.service -f
systemctl daemon-reload
systemctl reset-failed
fi
rm /etc/x-ui/ -rf rm /etc/x-ui/ -rf
rm /usr/local/x-ui/ -rf rm /usr/local/x-ui/ -rf
@ -286,7 +294,11 @@ start() {
echo "" echo ""
LOGI "Panel is running, No need to start again, If you need to restart, please select restart" LOGI "Panel is running, No need to start again, If you need to restart, please select restart"
else else
systemctl start x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui start
else
systemctl start x-ui
fi
sleep 2 sleep 2
check_status check_status
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
@ -307,7 +319,11 @@ stop() {
echo "" echo ""
LOGI "Panel stopped, No need to stop again!" LOGI "Panel stopped, No need to stop again!"
else else
systemctl stop x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
sleep 2 sleep 2
check_status check_status
if [[ $? == 1 ]]; then if [[ $? == 1 ]]; then
@ -323,7 +339,11 @@ stop() {
} }
restart() { restart() {
systemctl restart x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui restart
else
systemctl restart x-ui
fi
sleep 2 sleep 2
check_status check_status
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
@ -337,14 +357,22 @@ restart() {
} }
status() { status() {
systemctl status x-ui -l if [[ $release == "alpine" ]]; then
rc-service x-ui status
else
systemctl status x-ui -l
fi
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
before_show_menu before_show_menu
fi fi
} }
enable() { enable() {
systemctl enable x-ui if [[ $release == "alpine" ]]; then
rc-update add x-ui
else
systemctl enable x-ui
fi
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
LOGI "x-ui Set to boot automatically on startup successfully" LOGI "x-ui Set to boot automatically on startup successfully"
else else
@ -357,7 +385,11 @@ enable() {
} }
disable() { disable() {
systemctl disable x-ui if [[ $release == "alpine" ]]; then
rc-update del x-ui
else
systemctl disable x-ui
fi
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
LOGI "x-ui Autostart Cancelled successfully" LOGI "x-ui Autostart Cancelled successfully"
else else
@ -370,32 +402,54 @@ disable() {
} }
show_log() { show_log() {
echo -e "${green}\t1.${plain} Debug Log" if [[ $release == "alpine" ]]; then
echo -e "${green}\t2.${plain} Clear All logs" echo -e "${green}\t1.${plain} Debug Log"
echo -e "${green}\t0.${plain} Back to Main Menu" echo -e "${green}\t0.${plain} Back to Main Menu"
read -rp "Choose an option: " choice read -rp "Choose an option: " choice
case "$choice" in case "$choice" in
0) 0)
show_menu show_menu
;; ;;
1) 1)
journalctl -u x-ui -e --no-pager -f -p debug grep -F 'x-ui[' /var/log/messages
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
before_show_menu before_show_menu
fi fi
;; ;;
2) *)
sudo journalctl --rotate echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
sudo journalctl --vacuum-time=1s show_log
echo "All Logs cleared." ;;
restart esac
;; else
*) echo -e "${green}\t1.${plain} Debug Log"
echo -e "${red}Invalid option. Please select a valid number.${plain}\n" echo -e "${green}\t2.${plain} Clear All logs"
show_log echo -e "${green}\t0.${plain} Back to Main Menu"
;; read -rp "Choose an option: " choice
esac
case "$choice" in
0)
show_menu
;;
1)
journalctl -u x-ui -e --no-pager -f -p debug
if [[ $# == 0 ]]; then
before_show_menu
fi
;;
2)
sudo journalctl --rotate
sudo journalctl --vacuum-time=1s
echo "All Logs cleared."
restart
;;
*)
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
show_log
;;
esac
fi
} }
bbr_menu() { bbr_menu() {
@ -464,6 +518,9 @@ enable_bbr() {
arch | manjaro | parch) arch | manjaro | parch)
pacman -Sy --noconfirm ca-certificates pacman -Sy --noconfirm ca-certificates
;; ;;
alpine)
apk add ca-certificates
;;
*) *)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1 exit 1
@ -500,23 +557,42 @@ update_shell() {
# 0: running, 1: not running, 2: not installed # 0: running, 1: not running, 2: not installed
check_status() { check_status() {
if [[ ! -f /etc/systemd/system/x-ui.service ]]; then if [[ $release == "alpine" ]]; then
return 2 if [[ ! -f /etc/init.d/x-ui ]]; then
fi return 2
temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1) fi
if [[ "${temp}" == "running" ]]; then if [[ $(rc-service x-ui status | grep -F 'status: started' -c) == 1 ]]; then
return 0 return 0
else
return 1
fi
else else
return 1 if [[ ! -f /etc/systemd/system/x-ui.service ]]; then
return 2
fi
temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1)
if [[ "${temp}" == "running" ]]; then
return 0
else
return 1
fi
fi fi
} }
check_enabled() { check_enabled() {
temp=$(systemctl is-enabled x-ui) if [[ $release == "alpine" ]]; then
if [[ "${temp}" == "enabled" ]]; then if [[ $(rc-update show | grep -F 'x-ui' | grep default -c) == 1 ]]; then
return 0 return 0
else
return 1
fi
else else
return 1 temp=$(systemctl is-enabled x-ui)
if [[ "${temp}" == "enabled" ]]; then
return 0
else
return 1
fi
fi fi
} }
@ -798,7 +874,11 @@ update_geo() {
show_menu show_menu
;; ;;
1) 1)
systemctl stop x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip.dat geosite.dat rm -f geoip.dat geosite.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
@ -806,7 +886,11 @@ update_geo() {
restart restart
;; ;;
2) 2)
systemctl stop x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip_IR.dat geosite_IR.dat rm -f geoip_IR.dat geosite_IR.dat
wget -O geoip_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat wget -O geoip_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat wget -O geosite_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
@ -814,7 +898,11 @@ update_geo() {
restart restart
;; ;;
3) 3)
systemctl stop x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip_RU.dat geosite_RU.dat rm -f geoip_RU.dat geosite_RU.dat
wget -O geoip_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat wget -O geoip_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O geosite_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat wget -O geosite_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
@ -985,6 +1073,9 @@ ssl_cert_issue() {
arch | manjaro | parch) arch | manjaro | parch)
pacman -Sy --noconfirm socat pacman -Sy --noconfirm socat
;; ;;
alpine)
apk add socat
;;
*) *)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1 exit 1
@ -1335,7 +1426,11 @@ iplimit_main() {
read -rp "Please enter new Ban Duration in Minutes [default 30]: " NUM read -rp "Please enter new Ban Duration in Minutes [default 30]: " NUM
if [[ $NUM =~ ^[0-9]+$ ]]; then if [[ $NUM =~ ^[0-9]+$ ]]; then
create_iplimit_jails ${NUM} create_iplimit_jails ${NUM}
systemctl restart fail2ban if [[ $release == "alpine" ]]; then
rc-service fail2ban restart
else
systemctl restart fail2ban
fi
else else
echo -e "${red}${NUM} is not a number! Please, try again.${plain}" echo -e "${red}${NUM} is not a number! Please, try again.${plain}"
fi fi
@ -1388,7 +1483,11 @@ iplimit_main() {
iplimit_main iplimit_main
;; ;;
9) 9)
systemctl restart fail2ban if [[ $release == "alpine" ]]; then
rc-service fail2ban restart
else
systemctl restart fail2ban
fi
iplimit_main iplimit_main
;; ;;
10) 10)
@ -1436,6 +1535,9 @@ install_iplimit() {
arch | manjaro | parch) arch | manjaro | parch)
pacman -Syu --noconfirm fail2ban pacman -Syu --noconfirm fail2ban
;; ;;
alpine)
apk add fail2ban
;;
*) *)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1 exit 1
@ -1472,12 +1574,21 @@ install_iplimit() {
create_iplimit_jails create_iplimit_jails
# Launching fail2ban # Launching fail2ban
if ! systemctl is-active --quiet fail2ban; then if [[ $release == "alpine" ]]; then
systemctl start fail2ban if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then
rc-service fail2ban start
else
rc-service fail2ban restart
fi
rc-update add fail2ban
else else
systemctl restart fail2ban if ! systemctl is-active --quiet fail2ban; then
systemctl start fail2ban
else
systemctl restart fail2ban
fi
systemctl enable fail2ban
fi fi
systemctl enable fail2ban
echo -e "${green}IP Limit installed and configured successfully!${plain}\n" echo -e "${green}IP Limit installed and configured successfully!${plain}\n"
before_show_menu before_show_menu
@ -1493,13 +1604,21 @@ remove_iplimit() {
rm -f /etc/fail2ban/filter.d/3x-ipl.conf rm -f /etc/fail2ban/filter.d/3x-ipl.conf
rm -f /etc/fail2ban/action.d/3x-ipl.conf rm -f /etc/fail2ban/action.d/3x-ipl.conf
rm -f /etc/fail2ban/jail.d/3x-ipl.conf rm -f /etc/fail2ban/jail.d/3x-ipl.conf
systemctl restart fail2ban if [[ $release == "alpine" ]]; then
rc-service fail2ban restart
else
systemctl restart fail2ban
fi
echo -e "${green}IP Limit removed successfully!${plain}\n" echo -e "${green}IP Limit removed successfully!${plain}\n"
before_show_menu before_show_menu
;; ;;
2) 2)
rm -rf /etc/fail2ban rm -rf /etc/fail2ban
systemctl stop fail2ban if [[ $release == "alpine" ]]; then
rc-service fail2ban stop
else
systemctl stop fail2ban
fi
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)
apt-get remove -y fail2ban apt-get remove -y fail2ban
@ -1517,6 +1636,9 @@ remove_iplimit() {
arch | manjaro | parch) arch | manjaro | parch)
pacman -Rns --noconfirm fail2ban pacman -Rns --noconfirm fail2ban
;; ;;
alpine)
apk del fail2ban
;;
*) *)
echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n" echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n"
exit 1 exit 1
@ -1540,9 +1662,16 @@ show_banlog() {
echo -e "${green}Checking ban logs...${plain}\n" echo -e "${green}Checking ban logs...${plain}\n"
if ! systemctl is-active --quiet fail2ban; then if [[ $release == "alpine" ]]; then
echo -e "${red}Fail2ban service is not running!${plain}\n" if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then
return 1 echo -e "${red}Fail2ban service is not running!${plain}\n"
return 1
fi
else
if ! systemctl is-active --quiet fail2ban; then
echo -e "${red}Fail2ban service is not running!${plain}\n"
return 1
fi
fi fi
if [[ -f "$system_log" ]]; then if [[ -f "$system_log" ]]; then