Merge branch 'main' into feature/multi-server-support

This commit is contained in:
Sanaei 2025-09-09 20:53:50 +02:00 committed by GitHub
commit 747af376f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 320 additions and 184 deletions

View file

@ -146,3 +146,79 @@ jobs:
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
overwrite: true overwrite: true
prerelease: true prerelease: true
# =================================
# Windows Build
# =================================
build-windows:
name: Build for Windows
permissions:
contents: write
strategy:
matrix:
platform:
- amd64
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Build 3X-UI for Windows
shell: pwsh
run: |
$env:CGO_ENABLED="1"
$env:GOOS="windows"
$env:GOARCH="amd64"
go build -ldflags "-w -s" -o xui-release.exe -v main.go
mkdir x-ui
Copy-Item xui-release.exe x-ui\
mkdir x-ui\bin
cd x-ui\bin
# Download Xray for Windows
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.6.8/"
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
Remove-Item "Xray-windows-64.zip"
Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat"
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat"
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat"
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat"
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat"
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat"
Rename-Item xray.exe xray-windows-amd64.exe
cd ..
Copy-Item -Path ..\windows_files\* -Destination . -Recurse
cd ..
- name: Package to Zip
shell: pwsh
run: |
Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
- name: Upload files to Artifacts
uses: actions/upload-artifact@v4
with:
name: x-ui-windows-amd64
path: ./x-ui-windows-amd64.zip
- name: Upload files to GH release
uses: svenstaro/upload-release-action@v2
if: |
(github.event_name == 'release' && github.event.action == 'published') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-windows-amd64.zip
asset_name: x-ui-windows-amd64.zip
overwrite: true
prerelease: true

View file

@ -12,11 +12,11 @@ type Protocol string
const ( const (
VMESS Protocol = "vmess" VMESS Protocol = "vmess"
VLESS Protocol = "vless" VLESS Protocol = "vless"
DOKODEMO Protocol = "dokodemo-door" Tunnel Protocol = "tunnel"
HTTP Protocol = "http" HTTP Protocol = "http"
Trojan Protocol = "trojan" Trojan Protocol = "trojan"
Shadowsocks Protocol = "shadowsocks" Shadowsocks Protocol = "shadowsocks"
Socks Protocol = "socks" Mixed Protocol = "mixed"
WireGuard Protocol = "wireguard" WireGuard Protocol = "wireguard"
) )

View file

@ -13,7 +13,7 @@
"inbounds": [ "inbounds": [
{ {
"port": 10808, "port": 10808,
"protocol": "socks", "protocol": "mixed",
"settings": { "settings": {
"auth": "noauth", "auth": "noauth",
"udp": true, "udp": true,
@ -28,7 +28,7 @@
], ],
"enabled": true "enabled": true
}, },
"tag": "socks" "tag": "mixed"
}, },
{ {
"port": 10809, "port": 10809,

View file

@ -49,8 +49,8 @@ class DBInbound {
return this.protocol === Protocols.SHADOWSOCKS; return this.protocol === Protocols.SHADOWSOCKS;
} }
get isSocks() { get isMixed() {
return this.protocol === Protocols.SOCKS; return this.protocol === Protocols.MIXED;
} }
get isHTTP() { get isHTTP() {

View file

@ -3,8 +3,8 @@ const Protocols = {
VLESS: 'vless', VLESS: 'vless',
TROJAN: 'trojan', TROJAN: 'trojan',
SHADOWSOCKS: 'shadowsocks', SHADOWSOCKS: 'shadowsocks',
DOKODEMO: 'dokodemo-door', TUNNEL: 'tunnel',
SOCKS: 'socks', MIXED: 'mixed',
HTTP: 'http', HTTP: 'http',
WIREGUARD: 'wireguard', WIREGUARD: 'wireguard',
}; };
@ -729,7 +729,7 @@ class RealityStreamSettings extends XrayCommonClass {
constructor( constructor(
show = false, show = false,
xver = 0, xver = 0,
dest = 'google.com:443', target = 'google.com:443',
serverNames = 'google.com,www.google.com', serverNames = 'google.com,www.google.com',
privateKey = '', privateKey = '',
minClientVer = '', minClientVer = '',
@ -742,7 +742,7 @@ class RealityStreamSettings extends XrayCommonClass {
super(); super();
this.show = show; this.show = show;
this.xver = xver; this.xver = xver;
this.dest = dest; this.target = target;
this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames; this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames;
this.privateKey = privateKey; this.privateKey = privateKey;
this.minClientVer = minClientVer; this.minClientVer = minClientVer;
@ -767,7 +767,7 @@ class RealityStreamSettings extends XrayCommonClass {
return new RealityStreamSettings( return new RealityStreamSettings(
json.show, json.show,
json.xver, json.xver,
json.dest, json.target,
json.serverNames, json.serverNames,
json.privateKey, json.privateKey,
json.minClientVer, json.minClientVer,
@ -783,7 +783,7 @@ class RealityStreamSettings extends XrayCommonClass {
return { return {
show: this.show, show: this.show,
xver: this.xver, xver: this.xver,
dest: this.dest, target: this.target,
serverNames: this.serverNames.split(","), serverNames: this.serverNames.split(","),
privateKey: this.privateKey, privateKey: this.privateKey,
minClientVer: this.minClientVer, minClientVer: this.minClientVer,
@ -1712,8 +1712,8 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.VLESS: return new Inbound.VLESSSettings(protocol); case Protocols.VLESS: return new Inbound.VLESSSettings(protocol);
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol); case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol); case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
case Protocols.DOKODEMO: return new Inbound.DokodemoSettings(protocol); case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol);
case Protocols.SOCKS: return new Inbound.SocksSettings(protocol); case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol); case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol); case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
default: return null; default: return null;
@ -1726,8 +1726,8 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json); case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json);
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json); case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json); case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
case Protocols.DOKODEMO: return Inbound.DokodemoSettings.fromJson(json); case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json);
case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json); case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json); case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json); case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
default: return null; default: return null;
@ -2327,7 +2327,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
}; };
Inbound.DokodemoSettings = class extends Inbound.Settings { Inbound.TunnelSettings = class extends Inbound.Settings {
constructor( constructor(
protocol, protocol,
address, address,
@ -2345,8 +2345,8 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.DokodemoSettings( return new Inbound.TunnelSettings(
Protocols.DOKODEMO, Protocols.TUNNEL,
json.address, json.address,
json.port, json.port,
XrayCommonClass.toHeaders(json.portMap), XrayCommonClass.toHeaders(json.portMap),
@ -2366,8 +2366,8 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
} }
}; };
Inbound.SocksSettings = class extends Inbound.Settings { Inbound.MixedSettings = class extends Inbound.Settings {
constructor(protocol, auth = 'password', accounts = [new Inbound.SocksSettings.SocksAccount()], udp = false, ip = '127.0.0.1') { constructor(protocol, auth = 'password', accounts = [new Inbound.MixedSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
super(protocol); super(protocol);
this.auth = auth; this.auth = auth;
this.accounts = accounts; this.accounts = accounts;
@ -2387,11 +2387,11 @@ Inbound.SocksSettings = class extends Inbound.Settings {
let accounts; let accounts;
if (json.auth === 'password') { if (json.auth === 'password') {
accounts = json.accounts.map( accounts = json.accounts.map(
account => Inbound.SocksSettings.SocksAccount.fromJson(account) account => Inbound.MixedSettings.SocksAccount.fromJson(account)
) )
} }
return new Inbound.SocksSettings( return new Inbound.MixedSettings(
Protocols.SOCKS, Protocols.MIXED,
json.auth, json.auth,
accounts, accounts,
json.udp, json.udp,
@ -2408,7 +2408,7 @@ Inbound.SocksSettings = class extends Inbound.Settings {
}; };
} }
}; };
Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass { Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) { constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
super(); super();
this.user = user; this.user = user;
@ -2416,7 +2416,7 @@ Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass {
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.SocksSettings.SocksAccount(json.user, json.pass); return new Inbound.MixedSettings.SocksAccount(json.user, json.pass);
} }
}; };

View file

@ -6,7 +6,7 @@ const Protocols = {
VLESS: "vless", VLESS: "vless",
Trojan: "trojan", Trojan: "trojan",
Shadowsocks: "shadowsocks", Shadowsocks: "shadowsocks",
Socks: "socks", Mixed: "mixed",
HTTP: "http", HTTP: "http",
Wireguard: "wireguard" Wireguard: "wireguard"
}; };
@ -643,7 +643,7 @@ class Outbound extends CommonClass {
Protocols.Trojan, Protocols.Trojan,
Protocols.Shadowsocks, Protocols.Shadowsocks,
Protocols.HTTP, Protocols.HTTP,
Protocols.Socks Protocols.Mixed
].includes(this.protocol); ].includes(this.protocol);
} }
@ -652,7 +652,7 @@ class Outbound extends CommonClass {
} }
hasServers() { hasServers() {
return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Socks, Protocols.HTTP].includes(this.protocol); return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Mixed, Protocols.HTTP].includes(this.protocol);
} }
hasAddressPort() { hasAddressPort() {
@ -662,13 +662,13 @@ class Outbound extends CommonClass {
Protocols.VLESS, Protocols.VLESS,
Protocols.Trojan, Protocols.Trojan,
Protocols.Shadowsocks, Protocols.Shadowsocks,
Protocols.Socks, Protocols.Mixed,
Protocols.HTTP Protocols.HTTP
].includes(this.protocol); ].includes(this.protocol);
} }
hasUsername() { hasUsername() {
return [Protocols.Socks, Protocols.HTTP].includes(this.protocol); return [Protocols.Mixed, Protocols.HTTP].includes(this.protocol);
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@ -847,7 +847,7 @@ Outbound.Settings = class extends CommonClass {
case Protocols.VLESS: return new Outbound.VLESSSettings(); case Protocols.VLESS: return new Outbound.VLESSSettings();
case Protocols.Trojan: return new Outbound.TrojanSettings(); case Protocols.Trojan: return new Outbound.TrojanSettings();
case Protocols.Shadowsocks: return new Outbound.ShadowsocksSettings(); case Protocols.Shadowsocks: return new Outbound.ShadowsocksSettings();
case Protocols.Socks: return new Outbound.SocksSettings(); case Protocols.Mixed: return new Outbound.MixedSettings();
case Protocols.HTTP: return new Outbound.HttpSettings(); case Protocols.HTTP: return new Outbound.HttpSettings();
case Protocols.Wireguard: return new Outbound.WireguardSettings(); case Protocols.Wireguard: return new Outbound.WireguardSettings();
default: return null; default: return null;
@ -863,7 +863,7 @@ Outbound.Settings = class extends CommonClass {
case Protocols.VLESS: return Outbound.VLESSSettings.fromJson(json); case Protocols.VLESS: return Outbound.VLESSSettings.fromJson(json);
case Protocols.Trojan: return Outbound.TrojanSettings.fromJson(json); case Protocols.Trojan: return Outbound.TrojanSettings.fromJson(json);
case Protocols.Shadowsocks: return Outbound.ShadowsocksSettings.fromJson(json); case Protocols.Shadowsocks: return Outbound.ShadowsocksSettings.fromJson(json);
case Protocols.Socks: return Outbound.SocksSettings.fromJson(json); case Protocols.Mixed: return Outbound.MixedSettings.fromJson(json);
case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json); case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);
case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json); case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json);
default: return null; default: return null;
@ -1141,7 +1141,7 @@ Outbound.ShadowsocksSettings = class extends CommonClass {
} }
}; };
Outbound.SocksSettings = class extends CommonClass { Outbound.MixedSettings = class extends CommonClass {
constructor(address, port, user, pass) { constructor(address, port, user, pass) {
super(); super();
this.address = address; this.address = address;
@ -1153,7 +1153,7 @@ Outbound.SocksSettings = class extends CommonClass {
static fromJson(json = {}) { static fromJson(json = {}) {
let servers = json.servers; let servers = json.servers;
if (ObjectUtil.isArrEmpty(servers)) servers = [{ users: [{}] }]; if (ObjectUtil.isArrEmpty(servers)) servers = [{ users: [{}] }];
return new Outbound.SocksSettings( return new Outbound.MixedSettings(
servers[0].address, servers[0].address,
servers[0].port, servers[0].port,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user, ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,

View file

@ -9,6 +9,7 @@ import (
type APIController struct { type APIController struct {
BaseController BaseController
inboundController *InboundController inboundController *InboundController
serverController *ServerController
Tgbot service.Tgbot Tgbot service.Tgbot
} }
@ -19,43 +20,22 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
} }
func (a *APIController) initRouter(g *gin.RouterGroup) { func (a *APIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/panel/api/inbounds") // Main API group
g.Use(a.checkLogin) api := g.Group("/panel/api")
api.Use(a.checkLogin)
a.inboundController = NewInboundController(g) // Inbounds API
inbounds := api.Group("/inbounds")
a.inboundController = NewInboundController(inbounds)
inboundRoutes := []struct { // Server API
Method string server := api.Group("/server")
Path string a.serverController = NewServerController(server)
Handler gin.HandlerFunc
}{
{"GET", "/createbackup", a.createBackup},
{"GET", "/list", a.inboundController.getInbounds},
{"GET", "/get/:id", a.inboundController.getInbound},
{"GET", "/getClientTraffics/:email", a.inboundController.getClientTraffics},
{"GET", "/getClientTrafficsById/:id", a.inboundController.getClientTrafficsById},
{"POST", "/add", a.inboundController.addInbound},
{"POST", "/del/:id", a.inboundController.delInbound},
{"POST", "/update/:id", a.inboundController.updateInbound},
{"POST", "/clientIps/:email", a.inboundController.getClientIps},
{"POST", "/clearClientIps/:email", a.inboundController.clearClientIps},
{"POST", "/addClient", a.inboundController.addInboundClient},
{"POST", "/:id/delClient/:clientId", a.inboundController.delInboundClient},
{"POST", "/updateClient/:clientId", a.inboundController.updateInboundClient},
{"POST", "/:id/resetClientTraffic/:email", a.inboundController.resetClientTraffic},
{"POST", "/resetAllTraffics", a.inboundController.resetAllTraffics},
{"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics},
{"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients},
{"POST", "/onlines", a.inboundController.onlines},
{"POST", "/lastOnline", a.inboundController.lastOnline},
{"POST", "/updateClientTraffic/:email", a.inboundController.updateClientTraffic},
}
for _, route := range inboundRoutes { // Extra routes
g.Handle(route.Method, route.Path, route.Handler) api.GET("/backuptotgbot", a.BackuptoTgbot)
}
} }
func (a *APIController) createBackup(c *gin.Context) { func (a *APIController) BackuptoTgbot(c *gin.Context) {
a.Tgbot.SendBackupToAdmins() a.Tgbot.SendBackupToAdmins()
} }

View file

@ -25,34 +25,28 @@ func NewInboundController(g *gin.RouterGroup) *InboundController {
} }
func (a *InboundController) initRouter(g *gin.RouterGroup) { func (a *InboundController) initRouter(g *gin.RouterGroup) {
g = g.Group("/inbound")
g.POST("/list", a.getInbounds) g.GET("/list", a.getInbounds)
g.GET("/get/:id", a.getInbound)
g.GET("/getClientTraffics/:email", a.getClientTraffics)
g.GET("/getClientTrafficsById/:id", a.getClientTrafficsById)
g.POST("/add", a.addInbound) g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound) g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound) g.POST("/update/:id", a.updateInbound)
g.POST("/clientIps/:email", a.getClientIps) g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps) g.POST("/clearClientIps/:email", a.clearClientIps)
g.POST("/addClient", a.addInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic) g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics) g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics) g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients) g.POST("/delDepletedClients/:id", a.delDepletedClients)
g.POST("/import", a.importInbound) g.POST("/import", a.importInbound)
g.POST("/onlines", a.onlines) g.POST("/onlines", a.onlines)
g.POST("/lastOnline", a.lastOnline)
// Routes for UI g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
g.POST("/addClient", a.addInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/updateClient/:clientId", a.updateInboundClient)
// Routes for API (for slave servers)
apiGroup := g.Group("/api")
apiGroup.Use(middleware.ApiAuth())
{
apiGroup.POST("/addClient", a.addInboundClient)
apiGroup.POST("/:id/delClient/:clientId", a.delInboundClient)
apiGroup.POST("/updateClient/:clientId", a.updateInboundClient)
}
} }
func (a *InboundController) getInbounds(c *gin.Context) { func (a *InboundController) getInbounds(c *gin.Context) {

View file

@ -37,11 +37,17 @@ func NewServerController(g *gin.RouterGroup) *ServerController {
} }
func (a *ServerController) initRouter(g *gin.RouterGroup) { func (a *ServerController) initRouter(g *gin.RouterGroup) {
g = g.Group("/server")
g.Use(a.checkLogin) g.GET("/status", a.status)
g.POST("/status", a.status) g.GET("/getXrayVersion", a.getXrayVersion)
g.POST("/getXrayVersion", a.getXrayVersion) g.GET("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb)
g.GET("/getNewUUID", a.getNewUUID)
g.GET("/getNewX25519Cert", a.getNewX25519Cert)
g.GET("/getNewmldsa65", a.getNewmldsa65)
g.GET("/getNewmlkem768", a.getNewmlkem768)
g.GET("/getNewVlessEnc", a.getNewVlessEnc)
g.POST("/stopXrayService", a.stopXrayService) g.POST("/stopXrayService", a.stopXrayService)
g.POST("/restartXrayService", a.restartXrayService) g.POST("/restartXrayService", a.restartXrayService)
g.POST("/installXray/:version", a.installXray) g.POST("/installXray/:version", a.installXray)
@ -49,13 +55,8 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/updateGeofile/:fileName", a.updateGeofile) g.POST("/updateGeofile/:fileName", a.updateGeofile)
g.POST("/logs/:count", a.getLogs) g.POST("/logs/:count", a.getLogs)
g.POST("/xraylogs/:count", a.getXrayLogs) g.POST("/xraylogs/:count", a.getXrayLogs)
g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb)
g.POST("/importDB", a.importDB) g.POST("/importDB", a.importDB)
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
g.POST("/getNewmldsa65", a.getNewmldsa65)
g.POST("/getNewEchCert", a.getNewEchCert) g.POST("/getNewEchCert", a.getNewEchCert)
g.POST("/getNewVlessEnc", a.getNewVlessEnc)
} }
func (a *ServerController) refreshStatus() { func (a *ServerController) refreshStatus() {
@ -276,3 +277,22 @@ func (a *ServerController) getNewVlessEnc(c *gin.Context) {
} }
jsonObj(c, out, nil) jsonObj(c, out, nil)
} }
func (a *ServerController) getNewUUID(c *gin.Context) {
uuidResp, err := a.serverService.GetNewUUID()
if err != nil {
jsonMsg(c, "Failed to generate UUID", err)
return
}
jsonObj(c, uuidResp, nil)
}
func (a *ServerController) getNewmlkem768(c *gin.Context) {
out, err := a.serverService.GetNewmlkem768()
if err != nil {
jsonMsg(c, "Failed to generate mlkem768 keys", err)
return
}
jsonObj(c, out, nil)
}

View file

@ -8,6 +8,7 @@ type XUIController struct {
BaseController BaseController
inboundController *InboundController inboundController *InboundController
serverController *ServerController
settingController *SettingController settingController *SettingController
xraySettingController *XraySettingController xraySettingController *XraySettingController
} }
@ -29,6 +30,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
g.GET("/xray", a.xraySettings) g.GET("/xray", a.xraySettings)
a.inboundController = NewInboundController(g) 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

@ -83,14 +83,14 @@
{{template "form/shadowsocks"}} {{template "form/shadowsocks"}}
</template> </template>
<!-- dokodemo-door --> <!-- tunnel -->
<template v-if="inbound.protocol === Protocols.DOKODEMO"> <template v-if="inbound.protocol === Protocols.TUNNEL">
{{template "form/dokodemo"}} {{template "form/tunnel"}}
</template> </template>
<!-- socks --> <!-- mixed -->
<template v-if="inbound.protocol === Protocols.SOCKS"> <template v-if="inbound.protocol === Protocols.MIXED">
{{template "form/socks"}} {{template "form/mixed"}}
</template> </template>
<!-- http --> <!-- http -->

View file

@ -241,9 +241,9 @@
</template> </template>
</template> </template>
<!-- Servers (trojan/shadowsocks/socks/http) settings --> <!-- Servers (trojan/shadowsocks/mixed/http) settings -->
<template v-if="outbound.hasServers()"> <template v-if="outbound.hasServers()">
<!-- http / socks --> <!-- http / mixed -->
<template v-if="outbound.hasUsername()"> <template v-if="outbound.hasUsername()">
<a-form-item label='{{ i18n "username" }}'> <a-form-item label='{{ i18n "username" }}'>
<a-input v-model.trim="outbound.settings.user"></a-input> <a-input v-model.trim="outbound.settings.user"></a-input>
@ -441,6 +441,9 @@
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option> <a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="ECH Config List">
<a-input v-model.trim="outbound.stream.tls.echConfigList"></a-input>
</a-form-item>
<a-form-item label="Allow Insecure"> <a-form-item label="Allow Insecure">
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch> <a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
</a-form-item> </a-form-item>

View file

@ -1,4 +1,4 @@
{{define "form/dokodemo"}} {{define "form/tunnel"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'> <a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
<a-input v-model.trim="inbound.settings.address"></a-input> <a-input v-model.trim="inbound.settings.address"></a-input>

View file

@ -1,4 +1,4 @@
{{define "form/socks"}} {{define "form/mixed"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'> <a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
<a-switch v-model="inbound.settings.udp"></a-switch> <a-switch v-model="inbound.settings.udp"></a-switch>
@ -15,7 +15,7 @@
<td width="45%">{{ i18n "username" }}</td> <td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td> <td width="45%">{{ i18n "password" }}</td>
<td> <td>
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())"></a-button> <a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"></a-button>
</td> </td>
</tr> </tr>
</table> </table>

View file

@ -22,7 +22,6 @@
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Authentication"> <a-form-item label="Authentication">
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="undefined">None</a-select-option>
<a-select-option value="X25519, not Post-Quantum">X25519 (not Post-Quantum)</a-select-option> <a-select-option value="X25519, not Post-Quantum">X25519 (not Post-Quantum)</a-select-option>
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768 (Post-Quantum)</a-select-option> <a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768 (Post-Quantum)</a-select-option>
</a-select> </a-select>
@ -31,17 +30,17 @@
<a-input v-model.trim="inbound.settings.decryption"></a-input> <a-input v-model.trim="inbound.settings.decryption"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="encryption"> <a-form-item label="encryption">
<a-input v-model="inbound.settings.encryption" disabled></a-input> <a-input v-model="inbound.settings.encryption"></a-input>
</a-form-item> </a-form-item>
<a-form-item label=" "> <a-form-item label=" ">
<a-space> <a-space>
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New keys</a-button> <a-button type="primary" icon="import" @click="getNewVlessEnc">Get New keys</a-button>
<a-button danger @click="clearKeys">Clear</a-button> <a-button danger @click="clearVlessEnc">Clear</a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-form> </a-form>
</template> </template>
<template v-if="inbound.isTcp && !inbound.settings.encryption"> <template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button> <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>

View file

@ -12,8 +12,8 @@
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Dest (Target)'> <a-form-item label='Target'>
<a-input v-model.trim="inbound.stream.reality.dest"></a-input> <a-input v-model.trim="inbound.stream.reality.target"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='SNI'> <a-form-item label='SNI'>
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input> <a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
@ -48,7 +48,10 @@
<a-textarea v-model="inbound.stream.reality.privateKey"></a-textarea> <a-textarea v-model="inbound.stream.reality.privateKey"></a-textarea>
</a-form-item> </a-form-item>
<a-form-item label=" "> <a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button> <a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
<a-button danger @click="clearX25519Cert">Clear</a-button>
</a-space>
</a-form-item> </a-form-item>
<a-form-item label="mldsa65 Seed"> <a-form-item label="mldsa65 Seed">
<a-textarea v-model="inbound.stream.reality.mldsa65Seed"></a-textarea> <a-textarea v-model="inbound.stream.reality.mldsa65Seed"></a-textarea>
@ -57,7 +60,10 @@
<a-textarea v-model="inbound.stream.reality.settings.mldsa65Verify"></a-textarea> <a-textarea v-model="inbound.stream.reality.settings.mldsa65Verify"></a-textarea>
</a-form-item> </a-form-item>
<a-form-item label=" "> <a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewmldsa65">Get New Seed</a-button> <a-button type="primary" icon="import" @click="getNewmldsa65">Get New Seed</a-button>
<a-button danger @click="clearMldsa65">Clear</a-button>
</a-space>
</a-form-item> </a-form-item>
</template> </template>
{{end}} {{end}}

View file

@ -5,13 +5,13 @@
<a-form-item label='{{ i18n "security" }}'> <a-form-item label='{{ i18n "security" }}'>
<a-radio-group v-model="inbound.stream.security" button-style="solid"> <a-radio-group v-model="inbound.stream.security" button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button> <a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
<a-radio-button v-if="inbound.canEnableReality() && !inbound.settings.encryption" value="reality">Reality</a-radio-button> <a-radio-button v-if="inbound.canEnableReality()" value="reality">Reality</a-radio-button>
<a-radio-button v-if="!inbound.settings.encryption" value="tls">TLS</a-radio-button> <a-radio-button value="tls">TLS</a-radio-button>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<!-- tls settings --> <!-- tls settings -->
<template v-if="inbound.stream.isTls && !inbound.settings.encryption"> <template v-if="inbound.stream.isTls">
<a-form-item label="SNI" placeholder="Server Name Indication"> <a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="inbound.stream.tls.sni"></a-input> <a-input v-model.trim="inbound.stream.tls.sni"></a-input>
</a-form-item> </a-form-item>
@ -116,12 +116,15 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label=" "> <a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button> <a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button>
<a-button danger @click="clearEchCert">Clear</a-button>
</a-space>
</a-form-item> </a-form-item>
</template> </template>
<!-- reality settings --> <!-- reality settings -->
<template v-if="inbound.stream.isReality && !inbound.settings.encryption"> <template v-if="inbound.stream.isReality">
{{template "form/realitySettings"}} {{template "form/realitySettings"}}
</template> </template>
</a-form> </a-form>

View file

@ -830,7 +830,7 @@
}, },
async getDBInbounds() { async getDBInbounds() {
this.refreshing = true; this.refreshing = true;
const msg = await HttpUtil.post('/panel/inbound/list'); const msg = await HttpUtil.get('/panel/api/inbounds/list');
if (!msg.success) { if (!msg.success) {
this.refreshing = false; this.refreshing = false;
return; return;
@ -845,7 +845,7 @@
}, 500); }, 500);
}, },
async getOnlineUsers() { async getOnlineUsers() {
const msg = await HttpUtil.post('/panel/inbound/onlines'); const msg = await HttpUtil.post('/panel/api/inbounds/onlines');
if (!msg.success) { if (!msg.success) {
return; return;
} }
@ -1099,7 +1099,7 @@
streamSettings: baseInbound.stream.toString(), streamSettings: baseInbound.stream.toString(),
sniffing: baseInbound.sniffing.toString(), sniffing: baseInbound.sniffing.toString(),
}; };
await this.submit('/panel/inbound/add', data, inModal); await this.submit('/panel/api/inbounds/add', data, inModal);
}, },
openAddInbound() { openAddInbound() {
inModal.show({ inModal.show({
@ -1148,7 +1148,7 @@
} }
data.sniffing = inbound.sniffing.toString(); data.sniffing = inbound.sniffing.toString();
await this.submit('/panel/inbound/add', data, inModal); await this.submit('/panel/api/inbounds/add', data, inModal);
}, },
async updateInbound(inbound, dbInbound) { async updateInbound(inbound, dbInbound) {
const data = { const data = {
@ -1171,7 +1171,7 @@
} }
data.sniffing = inbound.sniffing.toString(); data.sniffing = inbound.sniffing.toString();
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal); await this.submit(`/panel/api/inbounds/update/${dbInbound.id}`, data, inModal);
}, },
openAddClient(dbInboundId) { openAddClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@ -1226,14 +1226,14 @@
id: dbInboundId, id: dbInboundId,
settings: '{"clients": [' + clients.toString() + ']}', settings: '{"clients": [' + clients.toString() + ']}',
}; };
await this.submit(`/panel/inbound/addClient`, data, modal); await this.submit(`/panel/api/inbounds/addClient`, data, modal);
}, },
async updateClient(client, dbInboundId, clientId) { async updateClient(client, dbInboundId, clientId) {
const data = { const data = {
id: dbInboundId, id: dbInboundId,
settings: '{"clients": [' + client.toString() + ']}', settings: '{"clients": [' + client.toString() + ']}',
}; };
await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal); await this.submit(`/panel/api/inbounds/updateClient/${clientId}`, data, clientModal);
}, },
resetTraffic(dbInboundId) { resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@ -1258,7 +1258,7 @@
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/del/' + dbInboundId), onOk: () => this.submit('/panel/api/inbounds/del/' + dbInboundId),
}); });
}, },
delClient(dbInboundId, client,confirmation = true) { delClient(dbInboundId, client,confirmation = true) {
@ -1271,10 +1271,10 @@
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`), onOk: () => this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`),
}); });
} else { } else {
this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`); this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`);
} }
}, },
getSubGroupClients(dbInbounds, currentClient) { getSubGroupClients(dbInbounds, currentClient) {
@ -1353,7 +1353,7 @@
switchEnable(dbInboundId,state) { switchEnable(dbInboundId,state) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
dbInbound.enable = state; dbInbound.enable = state;
this.submit(`/panel/inbound/update/${dbInboundId}`, dbInbound); this.submit(`/panel/api/inbounds/update/${dbInboundId}`, dbInbound);
}, },
async switchEnableClient(dbInboundId, client) { async switchEnableClient(dbInboundId, client) {
this.loading() this.loading()
@ -1383,10 +1383,10 @@
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email), onOk: () => this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email),
}) })
} else { } else {
this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email); this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email);
} }
}, },
resetAllTraffic() { resetAllTraffic() {
@ -1396,7 +1396,7 @@
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/resetAllTraffics'), onOk: () => this.submit('/panel/api/inbounds/resetAllTraffics'),
}); });
}, },
resetAllClientTraffics(dbInboundId) { resetAllClientTraffics(dbInboundId) {
@ -1406,7 +1406,7 @@
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/resetAllClientTraffics/' + dbInboundId), onOk: () => this.submit('/panel/api/inbounds/resetAllClientTraffics/' + dbInboundId),
}) })
}, },
delDepletedClients(dbInboundId) { delDepletedClients(dbInboundId) {
@ -1416,7 +1416,7 @@
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/delDepletedClients/' + dbInboundId), onOk: () => this.submit('/panel/api/inbounds/delDepletedClients/' + dbInboundId),
}) })
}, },
isExpiry(dbInbound, index) { isExpiry(dbInbound, index) {
@ -1542,7 +1542,7 @@
value: '', value: '',
okText: '{{ i18n "pages.inbounds.import" }}', okText: '{{ i18n "pages.inbounds.import" }}',
confirm: async (dbInboundText) => { confirm: async (dbInboundText) => {
await this.submit('/panel/inbound/import', {data: dbInboundText}, promptModal); await this.submit('/panel/api/inbounds/import', {data: dbInboundText}, promptModal);
}, },
}); });
}, },

View file

@ -746,7 +746,7 @@ ${dateTime}
}, },
async getStatus() { async getStatus() {
try { try {
const msg = await HttpUtil.post('/server/status'); const msg = await HttpUtil.get('/panel/api/server/status');
if (msg.success) { if (msg.success) {
if (!this.loadingStates.fetched) { if (!this.loadingStates.fetched) {
this.loadingStates.fetched = true; this.loadingStates.fetched = true;
@ -763,7 +763,7 @@ ${dateTime}
}, },
async openSelectV2rayVersion() { async openSelectV2rayVersion() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/getXrayVersion'); const msg = await HttpUtil.get('/panel/api/server/getXrayVersion');
this.loading(false); this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
@ -780,7 +780,7 @@ ${dateTime}
onOk: async () => { onOk: async () => {
versionModal.hide(); versionModal.hide();
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}'); this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
await HttpUtil.post(`/server/installXray/${version}`); await HttpUtil.post(`/panel/api/server/installXray/${version}`);
this.loading(false); this.loading(false);
}, },
}); });
@ -799,8 +799,8 @@ ${dateTime}
versionModal.hide(); versionModal.hide();
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}'); this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
const url = isSingleFile const url = isSingleFile
? `/server/updateGeofile/${fileName}` ? `/panel/api/server/updateGeofile/${fileName}`
: `/server/updateGeofile`; : `/panel/api/server/updateGeofile`;
await HttpUtil.post(url); await HttpUtil.post(url);
this.loading(false); this.loading(false);
}, },
@ -808,7 +808,7 @@ ${dateTime}
}, },
async stopXrayService() { async stopXrayService() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/stopXrayService'); const msg = await HttpUtil.post('/panel/api/server/stopXrayService');
this.loading(false); this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
@ -816,7 +816,7 @@ ${dateTime}
}, },
async restartXrayService() { async restartXrayService() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService'); const msg = await HttpUtil.post('/panel/api/server/restartXrayService');
this.loading(false); this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
@ -824,7 +824,7 @@ ${dateTime}
}, },
async openLogs(){ async openLogs(){
logModal.loading = true; logModal.loading = true;
const msg = await HttpUtil.post('server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog}); const msg = await HttpUtil.post('/panel/api/server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
if (!msg.success) { if (!msg.success) {
return; return;
} }
@ -834,7 +834,7 @@ ${dateTime}
}, },
async openXrayLogs(){ async openXrayLogs(){
xraylogModal.loading = true; xraylogModal.loading = true;
const msg = await HttpUtil.post('server/xraylogs/'+xraylogModal.rows,{filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy}); const msg = await HttpUtil.post('/panel/api/server/xraylogs/'+xraylogModal.rows,{filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy});
if (!msg.success) { if (!msg.success) {
return; return;
} }
@ -844,7 +844,7 @@ ${dateTime}
}, },
async openConfig() { async openConfig() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson'); const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
this.loading(false); this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
@ -855,7 +855,7 @@ ${dateTime}
backupModal.show(); backupModal.show();
}, },
exportDatabase() { exportDatabase() {
window.location = basePath + 'server/getDb'; window.location = basePath + 'panel/api/server/getDb';
}, },
importDatabase() { importDatabase() {
const fileInput = document.createElement('input'); const fileInput = document.createElement('input');
@ -868,7 +868,7 @@ ${dateTime}
formData.append('db', dbFile); formData.append('db', dbFile);
backupModal.hide(); backupModal.hide();
this.loading(true); this.loading(true);
const uploadMsg = await HttpUtil.post('server/importDB', formData, { const uploadMsg = await HttpUtil.post('/panel/api/server/importDB', formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
} }

View file

@ -121,7 +121,7 @@
}, },
methods: { methods: {
async getDBClientIps(email) { async getDBClientIps(email) {
const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`); const msg = await HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`);
if (!msg.success) { if (!msg.success) {
document.getElementById("clientIPs").value = msg.obj; document.getElementById("clientIPs").value = msg.obj;
return; return;
@ -139,7 +139,7 @@
}, },
async clearDBClientIps(email) { async clearDBClientIps(email) {
try { try {
const msg = await HttpUtil.post(`/panel/inbound/clearClientIps/${email}`); const msg = await HttpUtil.post(`/panel/api/inbounds/clearClientIps/${email}`);
if (!msg.success) { if (!msg.success) {
return; return;
} }
@ -156,7 +156,7 @@
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
iconElement.disabled = true; iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email); const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + email);
if (msg.success) { if (msg.success) {
this.clientModal.clientStats.up = 0; this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0; this.clientModal.clientStats.down = 0;

View file

@ -354,7 +354,7 @@
<code>[[ link.link ]]</code> <code>[[ link.link ]]</code>
</tr-info-row> </tr-info-row>
</template> </template>
<table v-if="inbound.protocol == Protocols.DOKODEMO" class="tr-info-table"> <table v-if="inbound.protocol == Protocols.TUNNEL" class="tr-info-table">
<tr> <tr>
<th>{{ i18n "pages.inbounds.targetAddress" }}</th> <th>{{ i18n "pages.inbounds.targetAddress" }}</th>
<th>{{ i18n "pages.inbounds.destinationPort" }}</th> <th>{{ i18n "pages.inbounds.destinationPort" }}</th>
@ -376,7 +376,7 @@
</td> </td>
</tr> </tr>
</table> </table>
<table v-if="dbInbound.isSocks" class="tr-info-table"> <table v-if="dbInbound.isMixed" class="tr-info-table">
<tr> <tr>
<th>{{ i18n "password" }} Auth</th> <th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th> <th>{{ i18n "pages.inbounds.enable" }} udp</th>
@ -492,7 +492,7 @@
</a-modal> </a-modal>
<script> <script>
function refreshIPs(email) { function refreshIPs(email) {
return HttpUtil.post(`/panel/inbound/clientIps/${email}`).then((msg) => { return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
if (msg.success) { if (msg.success) {
try { try {
return JSON.parse(msg.obj).join(', '); return JSON.parse(msg.obj).join(', ');
@ -613,7 +613,7 @@
}); });
}, },
clearClientIps() { clearClientIps() {
HttpUtil.post(`/panel/inbound/clearClientIps/${this.infoModal.clientStats.email}`) HttpUtil.post(`/panel/api/inbounds/clearClientIps/${this.infoModal.clientStats.email}`)
.then((msg) => { .then((msg) => {
if (!msg.success) { if (!msg.success) {
return; return;

View file

@ -132,7 +132,7 @@
}, },
async getNewX25519Cert() { async getNewX25519Cert() {
inModal.loading(true); inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewX25519Cert'); const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
inModal.loading(false); inModal.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
@ -140,9 +140,13 @@
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey; inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey; inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
}, },
clearX25519Cert() {
this.inbound.stream.reality.privateKey = '';
this.inbound.stream.reality.settings.publicKey = '';
},
async getNewmldsa65() { async getNewmldsa65() {
inModal.loading(true); inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewmldsa65'); const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
inModal.loading(false); inModal.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
@ -150,9 +154,13 @@
inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed; inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed;
inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify; inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify;
}, },
clearMldsa65() {
this.inbound.stream.reality.mldsa65Seed = '';
this.inbound.stream.reality.settings.mldsa65Verify = '';
},
async getNewEchCert() { async getNewEchCert() {
inModal.loading(true); inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni }); const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni });
inModal.loading(false); inModal.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
@ -160,9 +168,13 @@
inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys; inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;
inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList; inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList;
}, },
clearEchCert() {
this.inbound.stream.tls.echServerKeys = '';
this.inbound.stream.tls.settings.echConfigList = '';
},
async getNewVlessEnc() { async getNewVlessEnc() {
inModal.loading(true); inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewVlessEnc'); const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
inModal.loading(false); inModal.loading(false);
if (!msg.success) { if (!msg.success) {
@ -181,7 +193,7 @@
inModal.inbound.settings.decryption = block.decryption; inModal.inbound.settings.decryption = block.decryption;
inModal.inbound.settings.encryption = block.encryption; inModal.inbound.settings.encryption = block.encryption;
}, },
clearKeys() { clearVlessEnc() {
this.inbound.settings.decryption = 'none'; this.inbound.settings.decryption = 'none';
this.inbound.settings.encryption = 'none'; this.inbound.settings.encryption = 'none';
this.inbound.settings.selectedAuth = undefined; this.inbound.settings.selectedAuth = undefined;

View file

@ -151,7 +151,7 @@
methods: { methods: {
async getStatus() { async getStatus() {
try { try {
const msg = await HttpUtil.post('/server/status'); const msg = await HttpUtil.get('/panel/api/server/status');
if (msg.success) { if (msg.success) {
this.serverStatus = msg.obj; this.serverStatus = msg.obj;
} }

View file

@ -420,7 +420,7 @@
}, },
async restartXray() { async restartXray() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post("server/restartXrayService"); const msg = await HttpUtil.post("/panel/api/server/restartXrayService");
this.loading(false); this.loading(false);
if (msg.success) { if (msg.success) {
await PromiseUtil.sleep(500); await PromiseUtil.sleep(500);
@ -572,7 +572,7 @@
serverObj = o.settings.vnext; serverObj = o.settings.vnext;
break; break;
case Protocols.HTTP: case Protocols.HTTP:
case Protocols.Socks: case Protocols.Mixed:
case Protocols.Shadowsocks: case Protocols.Shadowsocks:
case Protocols.Trojan: case Protocols.Trojan:
serverObj = o.settings.servers; serverObj = o.settings.servers;

View file

@ -19,7 +19,7 @@
"tag": "api", "tag": "api",
"listen": "127.0.0.1", "listen": "127.0.0.1",
"port": 62789, "port": 62789,
"protocol": "dokodemo-door", "protocol": "tunnel",
"settings": { "settings": {
"address": "127.0.0.1" "address": "127.0.0.1"
} }

View file

@ -24,6 +24,7 @@ import (
"x-ui/util/sys" "x-ui/util/sys"
"x-ui/xray" "x-ui/xray"
"github.com/google/uuid"
"github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/host" "github.com/shirou/gopsutil/v4/host"
@ -872,12 +873,6 @@ func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
}, nil }, nil
} }
type AuthBlock struct {
Label string `json:"label"`
Decryption string `json:"decryption"`
Encryption string `json:"encryption"`
}
func (s *ServerService) GetNewVlessEnc() (any, error) { func (s *ServerService) GetNewVlessEnc() (any, error) {
cmd := exec.Command(xray.GetBinaryPath(), "vlessenc") cmd := exec.Command(xray.GetBinaryPath(), "vlessenc")
var out bytes.Buffer var out bytes.Buffer
@ -887,37 +882,70 @@ func (s *ServerService) GetNewVlessEnc() (any, error) {
} }
lines := strings.Split(out.String(), "\n") lines := strings.Split(out.String(), "\n")
var auths []map[string]string
var blocks []AuthBlock var current map[string]string
var current *AuthBlock
for _, line := range lines { for _, line := range lines {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if strings.HasPrefix(line, "Authentication:") { if strings.HasPrefix(line, "Authentication:") {
if current != nil { if current != nil {
blocks = append(blocks, *current) auths = append(auths, current)
}
current = map[string]string{
"label": strings.TrimSpace(strings.TrimPrefix(line, "Authentication:")),
} }
current = &AuthBlock{Label: strings.TrimSpace(strings.TrimPrefix(line, "Authentication:"))}
} else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) { } else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) {
parts := strings.SplitN(line, ":", 2) parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 && current != nil { if len(parts) == 2 && current != nil {
key := strings.Trim(parts[0], `" `) key := strings.Trim(parts[0], `" `)
val := strings.Trim(parts[1], `" `) val := strings.Trim(parts[1], `" `)
switch key { current[key] = val
case "decryption":
current.Decryption = val
case "encryption":
current.Encryption = val
}
} }
} }
} }
if current != nil { if current != nil {
blocks = append(blocks, *current) auths = append(auths, current)
} }
return map[string]any{ return map[string]any{
"auths": blocks, "auths": auths,
}, nil }, nil
} }
func (s *ServerService) GetNewUUID() (map[string]string, error) {
newUUID, err := uuid.NewRandom()
if err != nil {
return nil, fmt.Errorf("failed to generate UUID: %w", err)
}
return map[string]string{
"uuid": newUUID.String(),
}, nil
}
func (s *ServerService) GetNewmlkem768() (any, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "mlkem768")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
SeedLine := strings.Split(lines[0], ":")
ClientLine := strings.Split(lines[1], ":")
seed := strings.TrimSpace(SeedLine[1])
client := strings.TrimSpace(ClientLine[1])
keyPair := map[string]any{
"seed": seed,
"client": client,
}
return keyPair, nil
}

View file

@ -2129,8 +2129,8 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
} }
excludedProtocols := map[model.Protocol]bool{ excludedProtocols := map[model.Protocol]bool{
model.DOKODEMO: true, model.Tunnel: true,
model.Socks: true, model.Mixed: true,
model.WireGuard: true, model.WireGuard: true,
model.HTTP: true, model.HTTP: true,
} }

View file

@ -176,7 +176,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{basePath + "panel/API/"}))) engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{basePath + "panel/api/"})))
assetsBasePath := basePath + "assets/" assetsBasePath := basePath + "assets/"
store := cookie.NewStore(secret) store := cookie.NewStore(secret)

Binary file not shown.

13
windows_files/readme.txt Normal file
View file

@ -0,0 +1,13 @@
you can't install fail2ban on windows
we don't have bash menu for windows
if you forgot your password you need to check your database with https://sqlitebrowser.org/
the app need to be open all the time
default setting:
http://localhost:2053/
user: admin
pass: admin
port: 2053
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout localhost.key -out localhost.crt