mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-13 19:49:12 +00:00
Merge branch 'MHSanaei:main' into main
This commit is contained in:
commit
f31351e631
18 changed files with 402 additions and 138 deletions
82
.github/workflows/docker.yml
vendored
82
.github/workflows/docker.yml
vendored
|
@ -1,7 +1,9 @@
|
|||
name: Release 3X-UI for Docker
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
|
@ -13,48 +15,48 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
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}}
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- 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=semver,pattern={{version}}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
install: true
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- 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 }}
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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 }}
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.8.3
|
||||
2.8.4
|
2
go.mod
2
go.mod
|
@ -95,7 +95,7 @@ require (
|
|||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // 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
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -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=
|
||||
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-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/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
|
|
25
install.sh
25
install.sh
|
@ -56,6 +56,9 @@ install_base() {
|
|||
opensuse-tumbleweed)
|
||||
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
|
||||
;;
|
||||
|
@ -177,7 +180,11 @@ install_x-ui() {
|
|||
|
||||
# Stop x-ui service and remove old resources
|
||||
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
|
||||
fi
|
||||
|
||||
|
@ -201,10 +208,18 @@ install_x-ui() {
|
|||
chmod +x /usr/bin/x-ui
|
||||
config_after_install
|
||||
|
||||
cp -f x-ui.service /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl start x-ui
|
||||
if [[ $release == "alpine" ]]; then
|
||||
wget -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
|
||||
chmod +x /etc/init.d/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 ""
|
||||
echo -e "┌───────────────────────────────────────────────────────┐
|
||||
|
|
50
sub/sub.go
50
sub/sub.go
|
@ -98,8 +98,14 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
}
|
||||
|
||||
// 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) {
|
||||
c.Set("base_path", LinksPath)
|
||||
c.Set("base_path", basePath)
|
||||
})
|
||||
|
||||
Encrypt, err := s.settingService.GetSubEncrypt()
|
||||
|
@ -179,22 +185,48 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
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 {
|
||||
engine.StaticFS("/assets", http.FS(os.DirFS("web/assets")))
|
||||
if linksPathForAssets != "/assets" {
|
||||
engine.StaticFS(linksPathForAssets, http.FS(os.DirFS("web/assets")))
|
||||
}
|
||||
assetsFS = http.FS(os.DirFS("web/assets"))
|
||||
} else {
|
||||
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
||||
engine.StaticFS("/assets", http.FS(subFS))
|
||||
if linksPathForAssets != "/assets" {
|
||||
engine.StaticFS(linksPathForAssets, http.FS(subFS))
|
||||
}
|
||||
assetsFS = http.FS(subFS)
|
||||
} else {
|
||||
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("/")
|
||||
|
||||
s.sub = NewSUBController(
|
||||
|
|
|
@ -87,7 +87,20 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||
if !a.jsonEnabled {
|
||||
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{
|
||||
"title": "subscription.title",
|
||||
"cur_ver": config.GetVersion(),
|
||||
|
|
|
@ -1148,7 +1148,7 @@ func (s *SubService) joinPathWithID(basePath, subId string) string {
|
|||
|
||||
// BuildPageData parses header and prepares the template view model.
|
||||
// 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)
|
||||
upload := common.FormatTraffic(traffic.Up)
|
||||
total := "∞"
|
||||
|
@ -1167,7 +1167,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
|
|||
|
||||
return PageData{
|
||||
Host: hostHeader,
|
||||
BasePath: "/", // kept as "/"; templates now use context base_path injected from router
|
||||
BasePath: basePath,
|
||||
SId: subId,
|
||||
Download: download,
|
||||
Upload: upload,
|
||||
|
|
|
@ -142,7 +142,10 @@
|
|||
},
|
||||
npvtunUrl() {
|
||||
return this.app.subUrl;
|
||||
}
|
||||
},
|
||||
happUrl() {
|
||||
return `happ://add/${encodeURIComponent(this.app.subUrl)}`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
renderLink,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
@ -21,11 +24,21 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
|
|||
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.
|
||||
func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||
// Main API group
|
||||
api := g.Group("/panel/api")
|
||||
api.Use(a.checkLogin)
|
||||
api.Use(a.checkAPIAuth)
|
||||
|
||||
// Inbounds API
|
||||
inbounds := api.Group("/inbounds")
|
||||
|
|
|
@ -39,8 +39,9 @@ func NewIndexController(g *gin.RouterGroup) *IndexController {
|
|||
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
|
||||
func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/", a.index)
|
||||
g.POST("/login", a.login)
|
||||
g.GET("/logout", a.logout)
|
||||
|
||||
g.POST("/login", a.login)
|
||||
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,6 @@ import (
|
|||
type XUIController struct {
|
||||
BaseController
|
||||
|
||||
inboundController *InboundController
|
||||
serverController *ServerController
|
||||
settingController *SettingController
|
||||
xraySettingController *XraySettingController
|
||||
}
|
||||
|
@ -31,8 +29,6 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
|||
g.GET("/settings", a.settings)
|
||||
g.GET("/xray", a.xraySettings)
|
||||
|
||||
a.inboundController = NewInboundController(g)
|
||||
a.serverController = NewServerController(g)
|
||||
a.settingController = NewSettingController(g)
|
||||
a.xraySettingController = NewXraySettingController(g)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
|
||||
<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-inner-header"></div>
|
||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
|
@ -20,7 +20,7 @@
|
|||
</g>
|
||||
</svg>
|
||||
</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">
|
||||
<template v-if="!loadingStates.fetched">
|
||||
<div class="text-center">
|
||||
|
@ -35,8 +35,8 @@
|
|||
<a-space direction="vertical" :size="10">
|
||||
<a-theme-switch-login></a-theme-switch-login>
|
||||
<span>{{ i18n "pages.settings.language" }}</span>
|
||||
<a-select ref="selectLang" class="w-100" v-model="lang"
|
||||
@change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select ref="selectLang" class="w-100" v-model="lang" @change="LanguageManager.setLanguage(lang)"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<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 v-text="l.name"></span>
|
||||
|
@ -68,7 +68,7 @@
|
|||
</a-input>
|
||||
</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>
|
||||
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
||||
</a-input-password>
|
||||
|
@ -81,7 +81,8 @@
|
|||
</a-form-item>
|
||||
<a-form-item>
|
||||
<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"
|
||||
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
||||
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
||||
|
|
|
@ -218,6 +218,8 @@
|
|||
<a-menu-item key="android-npvtunnel"
|
||||
@click="copy(app.subUrl)">NPV
|
||||
Tunnel</a-menu-item>
|
||||
<a-menu-item key="android-happ"
|
||||
@click="open('happ://add/' + encodeURIComponent(app.subUrl))">Happ</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
|
@ -244,6 +246,8 @@
|
|||
@click="copy(npvtunUrl)">NPV
|
||||
Tunnel
|
||||
</a-menu-item>
|
||||
<a-menu-item key="ios-happ"
|
||||
@click="open(happUrl)">Happ</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
|
|
|
@ -35,6 +35,25 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
|||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -47,6 +66,24 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
|
|||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
13
web/web.go
13
web/web.go
|
@ -95,10 +95,9 @@ type Server struct {
|
|||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
|
||||
index *controller.IndexController
|
||||
server *controller.ServerController
|
||||
panel *controller.XUIController
|
||||
api *controller.APIController
|
||||
index *controller.IndexController
|
||||
panel *controller.XUIController
|
||||
api *controller.APIController
|
||||
|
||||
xrayService service.XrayService
|
||||
settingService service.SettingService
|
||||
|
@ -264,10 +263,14 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
g := engine.Group(basePath)
|
||||
|
||||
s.index = controller.NewIndexController(g)
|
||||
s.server = controller.NewServerController(g)
|
||||
s.panel = controller.NewXUIController(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
|
||||
}
|
||||
|
||||
|
|
13
x-ui.rc
Normal file
13
x-ui.rc
Normal 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
251
x-ui.sh
|
@ -153,11 +153,19 @@ uninstall() {
|
|||
fi
|
||||
return 0
|
||||
fi
|
||||
systemctl stop x-ui
|
||||
systemctl disable x-ui
|
||||
rm /etc/systemd/system/x-ui.service -f
|
||||
systemctl daemon-reload
|
||||
systemctl reset-failed
|
||||
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui stop
|
||||
rc-update del x-ui
|
||||
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 /usr/local/x-ui/ -rf
|
||||
|
||||
|
@ -286,7 +294,11 @@ start() {
|
|||
echo ""
|
||||
LOGI "Panel is running, No need to start again, If you need to restart, please select restart"
|
||||
else
|
||||
systemctl start x-ui
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui start
|
||||
else
|
||||
systemctl start x-ui
|
||||
fi
|
||||
sleep 2
|
||||
check_status
|
||||
if [[ $? == 0 ]]; then
|
||||
|
@ -307,7 +319,11 @@ stop() {
|
|||
echo ""
|
||||
LOGI "Panel stopped, No need to stop again!"
|
||||
else
|
||||
systemctl stop x-ui
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui stop
|
||||
else
|
||||
systemctl stop x-ui
|
||||
fi
|
||||
sleep 2
|
||||
check_status
|
||||
if [[ $? == 1 ]]; then
|
||||
|
@ -323,7 +339,11 @@ stop() {
|
|||
}
|
||||
|
||||
restart() {
|
||||
systemctl restart x-ui
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui restart
|
||||
else
|
||||
systemctl restart x-ui
|
||||
fi
|
||||
sleep 2
|
||||
check_status
|
||||
if [[ $? == 0 ]]; then
|
||||
|
@ -337,14 +357,22 @@ restart() {
|
|||
}
|
||||
|
||||
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
|
||||
before_show_menu
|
||||
fi
|
||||
}
|
||||
|
||||
enable() {
|
||||
systemctl enable x-ui
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-update add x-ui
|
||||
else
|
||||
systemctl enable x-ui
|
||||
fi
|
||||
if [[ $? == 0 ]]; then
|
||||
LOGI "x-ui Set to boot automatically on startup successfully"
|
||||
else
|
||||
|
@ -357,7 +385,11 @@ enable() {
|
|||
}
|
||||
|
||||
disable() {
|
||||
systemctl disable x-ui
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-update del x-ui
|
||||
else
|
||||
systemctl disable x-ui
|
||||
fi
|
||||
if [[ $? == 0 ]]; then
|
||||
LOGI "x-ui Autostart Cancelled successfully"
|
||||
else
|
||||
|
@ -370,32 +402,54 @@ disable() {
|
|||
}
|
||||
|
||||
show_log() {
|
||||
echo -e "${green}\t1.${plain} Debug Log"
|
||||
echo -e "${green}\t2.${plain} Clear All logs"
|
||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||
read -rp "Choose an option: " choice
|
||||
if [[ $release == "alpine" ]]; then
|
||||
echo -e "${green}\t1.${plain} Debug Log"
|
||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||
read -rp "Choose an option: " choice
|
||||
|
||||
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
|
||||
case "$choice" in
|
||||
0)
|
||||
show_menu
|
||||
;;
|
||||
1)
|
||||
grep -F 'x-ui[' /var/log/messages
|
||||
if [[ $# == 0 ]]; then
|
||||
before_show_menu
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
|
||||
show_log
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo -e "${green}\t1.${plain} Debug Log"
|
||||
echo -e "${green}\t2.${plain} Clear All logs"
|
||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||
read -rp "Choose an option: " choice
|
||||
|
||||
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() {
|
||||
|
@ -464,6 +518,9 @@ enable_bbr() {
|
|||
arch | manjaro | parch)
|
||||
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"
|
||||
exit 1
|
||||
|
@ -500,23 +557,42 @@ update_shell() {
|
|||
|
||||
# 0: running, 1: not running, 2: not installed
|
||||
check_status() {
|
||||
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
|
||||
if [[ $release == "alpine" ]]; then
|
||||
if [[ ! -f /etc/init.d/x-ui ]]; then
|
||||
return 2
|
||||
fi
|
||||
if [[ $(rc-service x-ui status | grep -F 'status: started' -c) == 1 ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
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
|
||||
}
|
||||
|
||||
check_enabled() {
|
||||
temp=$(systemctl is-enabled x-ui)
|
||||
if [[ "${temp}" == "enabled" ]]; then
|
||||
return 0
|
||||
if [[ $release == "alpine" ]]; then
|
||||
if [[ $(rc-update show | grep -F 'x-ui' | grep default -c) == 1 ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
return 1
|
||||
temp=$(systemctl is-enabled x-ui)
|
||||
if [[ "${temp}" == "enabled" ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
@ -798,7 +874,11 @@ update_geo() {
|
|||
show_menu
|
||||
;;
|
||||
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
|
||||
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
|
||||
|
@ -806,7 +886,11 @@ update_geo() {
|
|||
restart
|
||||
;;
|
||||
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
|
||||
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
|
||||
|
@ -814,7 +898,11 @@ update_geo() {
|
|||
restart
|
||||
;;
|
||||
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
|
||||
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
|
||||
|
@ -985,6 +1073,9 @@ ssl_cert_issue() {
|
|||
arch | manjaro | parch)
|
||||
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"
|
||||
exit 1
|
||||
|
@ -1335,7 +1426,11 @@ iplimit_main() {
|
|||
read -rp "Please enter new Ban Duration in Minutes [default 30]: " NUM
|
||||
if [[ $NUM =~ ^[0-9]+$ ]]; then
|
||||
create_iplimit_jails ${NUM}
|
||||
systemctl restart fail2ban
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service fail2ban restart
|
||||
else
|
||||
systemctl restart fail2ban
|
||||
fi
|
||||
else
|
||||
echo -e "${red}${NUM} is not a number! Please, try again.${plain}"
|
||||
fi
|
||||
|
@ -1388,7 +1483,11 @@ iplimit_main() {
|
|||
iplimit_main
|
||||
;;
|
||||
9)
|
||||
systemctl restart fail2ban
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service fail2ban restart
|
||||
else
|
||||
systemctl restart fail2ban
|
||||
fi
|
||||
iplimit_main
|
||||
;;
|
||||
10)
|
||||
|
@ -1436,6 +1535,9 @@ install_iplimit() {
|
|||
arch | manjaro | parch)
|
||||
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"
|
||||
exit 1
|
||||
|
@ -1472,12 +1574,21 @@ install_iplimit() {
|
|||
create_iplimit_jails
|
||||
|
||||
# Launching fail2ban
|
||||
if ! systemctl is-active --quiet fail2ban; then
|
||||
systemctl start fail2ban
|
||||
if [[ $release == "alpine" ]]; then
|
||||
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
|
||||
systemctl restart fail2ban
|
||||
if ! systemctl is-active --quiet fail2ban; then
|
||||
systemctl start fail2ban
|
||||
else
|
||||
systemctl restart fail2ban
|
||||
fi
|
||||
systemctl enable fail2ban
|
||||
fi
|
||||
systemctl enable fail2ban
|
||||
|
||||
echo -e "${green}IP Limit installed and configured successfully!${plain}\n"
|
||||
before_show_menu
|
||||
|
@ -1493,13 +1604,21 @@ remove_iplimit() {
|
|||
rm -f /etc/fail2ban/filter.d/3x-ipl.conf
|
||||
rm -f /etc/fail2ban/action.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"
|
||||
before_show_menu
|
||||
;;
|
||||
2)
|
||||
rm -rf /etc/fail2ban
|
||||
systemctl stop fail2ban
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service fail2ban stop
|
||||
else
|
||||
systemctl stop fail2ban
|
||||
fi
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
apt-get remove -y fail2ban
|
||||
|
@ -1517,6 +1636,9 @@ remove_iplimit() {
|
|||
arch | manjaro | parch)
|
||||
pacman -Rns --noconfirm fail2ban
|
||||
;;
|
||||
alpine)
|
||||
apk del fail2ban
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n"
|
||||
exit 1
|
||||
|
@ -1540,9 +1662,16 @@ show_banlog() {
|
|||
|
||||
echo -e "${green}Checking ban logs...${plain}\n"
|
||||
|
||||
if ! systemctl is-active --quiet fail2ban; then
|
||||
echo -e "${red}Fail2ban service is not running!${plain}\n"
|
||||
return 1
|
||||
if [[ $release == "alpine" ]]; then
|
||||
if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then
|
||||
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
|
||||
|
||||
if [[ -f "$system_log" ]]; then
|
||||
|
|
Loading…
Reference in a new issue