Compare commits

...

33 Commits
1.0.0 ... 1.1.1

Author SHA1 Message Date
Alireza Ahmadi
ba083ecc7e v1.1.1 2023-04-26 11:23:16 +02:00
Alireza Ahmadi
365ec1a704 [bug] fix expirytime #267 2023-04-26 11:20:56 +02:00
Alireza Ahmadi
63969d5fd6 Remove unused codes 2023-04-26 11:20:22 +02:00
Alireza Ahmadi
0449a35409 Update readme 2023-04-25 20:22:07 +02:00
Alireza Ahmadi
453594ee9e v1.1.0 2023-04-25 19:46:51 +02:00
Alireza Ahmadi
a2d8bec80a [bug] vision-udp443 only for client 2023-04-25 19:30:10 +02:00
Alireza Ahmadi
b0ca8a8e6c Translation 2023-04-25 18:16:33 +02:00
Alireza Ahmadi
99c9d777c0 add hostname to page title 2023-04-25 17:38:58 +02:00
Alireza Ahmadi
97d109c900 [api] support for delete depleted clients 2023-04-25 16:48:18 +02:00
Alireza Ahmadi
35ad91a9e0 [feature] delete depleted clients 2023-04-25 16:24:13 +02:00
Alireza Ahmadi
8bc16b020b [migration] add fix for omitted traffics 2023-04-25 16:22:42 +02:00
Alireza Ahmadi
6db9bd0ad9 fix switch enable disable client 2023-04-25 13:38:26 +02:00
Alireza Ahmadi
5baa397d1c [feature] reset traffics of all client 2023-04-25 12:26:56 +02:00
Alireza Ahmadi
76e1243da3 [feature] add general action menu 2023-04-25 12:22:50 +02:00
Alireza Ahmadi
7b7a0f9aa7 [sub] fix bug in http link without host 2023-04-25 10:44:39 +02:00
Alireza Ahmadi
134e2236a6 [feature] add login session timeout 2023-04-25 08:41:11 +02:00
Alireza Ahmadi
aab672976e update by client id #218 2023-04-24 20:30:01 +02:00
Alireza Ahmadi
f7aee9fe18 v1.0.2 2023-04-24 17:52:33 +02:00
Alireza Ahmadi
a19b58675b Add migration to install script 2023-04-24 11:16:28 +02:00
Alireza Ahmadi
7a229b27f5 Better client delete + api #218 2023-04-24 11:10:59 +02:00
Alireza Ahmadi
abf68446e5 Optimize database #258 2023-04-24 10:40:37 +02:00
Alireza Ahmadi
b4b7ec565e simplify API: del client #218 2023-04-22 11:21:44 +02:00
Alireza Ahmadi
4b1b920bf4 Add database migration 2023-04-22 11:19:22 +02:00
Alireza Ahmadi
11bff57f23 [feature] add getClientTraffics api 2023-04-20 19:16:06 +02:00
Alireza Ahmadi
4838b0f6f0 v1.0.1 2023-04-20 14:30:58 +02:00
Alireza Ahmadi
bb8807129a [client] add reset traffic in edit #244 2023-04-20 13:59:32 +02:00
Alireza Ahmadi
cb050bfe34 [sub] fix userinfo header #217 2023-04-20 12:53:50 +02:00
Alireza Ahmadi
76a996cc2d main.go enhancements 2023-04-20 10:55:58 +02:00
Alireza Ahmadi
7fec1dd5cf [reality] fix fingerprint issue #236 2023-04-20 10:50:20 +02:00
Alireza Ahmadi
a24e9089b6 small visual enhancements #237 2023-04-20 10:48:57 +02:00
Alireza Ahmadi
b6474eaef6 fix setting numbers #237 2023-04-20 10:47:24 +02:00
Alireza Ahmadi
76f90b0a46 fix enabletgbot cli 2023-04-20 10:45:21 +02:00
Alireza Ahmadi
42abffdaef [docker] fix build for amd64 2023-04-20 10:36:46 +02:00
32 changed files with 590 additions and 282 deletions

View File

@@ -10,8 +10,11 @@ else
FNAME="amd64";
fi
mkdir -p build/bin
cd build/bin
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-${ARCH}.zip"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -o build/bin/geoip.dat
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -o build/bin/geosite.dat
mv xray "build/bin/xray-linux-${FNAME}"
mv main build/x-ui
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
cd ../../

View File

@@ -1,9 +1,9 @@
FROM golang:1.20-alpine AS builder
WORKDIR /app
ARG TARGETARCH
RUN apk --no-cache --update add build-base gcc wget
RUN apk --no-cache --update add build-base gcc wget unzip
COPY . .
RUN env CGO_ENABLED=1 go build main.go
RUN env CGO_ENABLED=1 go build -o build/x-ui main.go
RUN ./DockerInitFiles.sh "$TARGETARCH"
FROM alpine

View File

@@ -54,11 +54,12 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
| `POST` | `"/del/:id"` | Delete Inbound |
| `POST` | `"/update/:id"` | Update Inbound |
| `POST` | `"/addClient/"` | Add Client to inbound |
| `POST` | `"/delClient/:email"` | Delete Client |
| `POST` | `"/updateClient/:index"` | Update Client |
| `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic |
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by UID/Password as clientId |
| `POST` | `"/updateClient/:clientId"` | Update Client by UID/Password as clientId |
| `POST` | `"/getClientTraffics/:email"` | Get Client's Traffic |
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset inbound clients traffics (-1: all) |
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
# Environment Variables

View File

@@ -1 +1 @@
1.0.0
1.1.1

View File

@@ -79,6 +79,7 @@ install_base() {
#This function will be called when user installed x-ui out of sercurity
config_after_install() {
/usr/local/x-ui/x-ui migrate
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
read -p "Do you want to continue with the modification [y/n]? ": config_confirm
if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then

24
main.go
View File

@@ -51,8 +51,8 @@ func runWebServer() {
}
sigCh := make(chan os.Signal, 1)
//信号量捕获处理
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
// Trap shutdown signals
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
for {
sig := <-sigCh
@@ -133,7 +133,6 @@ func updateTgbotEnableSts(status bool) {
logger.Infof("SetTgbotenabled[%v] success", status)
}
}
return
}
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
@@ -204,6 +203,19 @@ func updateSetting(port int, username string, password string) {
}
}
func migrateDb() {
inboundService := service.InboundService{}
err := database.InitDB(config.GetDBPath())
if err != nil {
log.Fatal(err)
}
fmt.Println("Start migrating database...")
inboundService.MigrationRequirements()
inboundService.RemoveOrphanedTraffics()
fmt.Println("Migration done!")
}
func main() {
if len(os.Args) < 2 {
runWebServer()
@@ -246,6 +258,7 @@ func main() {
fmt.Println("Commands:")
fmt.Println(" run run web panel")
fmt.Println(" v2-ui migrate form v2-ui")
fmt.Println(" migrate migrate form other/old x-ui")
fmt.Println(" setting set settings")
}
@@ -263,6 +276,8 @@ func main() {
return
}
runWebServer()
case "migrate":
migrateDb()
case "v2-ui":
err := v2uiCmd.Parse(os.Args[2:])
if err != nil {
@@ -290,6 +305,9 @@ func main() {
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
}
if enabletgbot {
updateTgbotEnableSts(enabletgbot)
}
default:
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
fmt.Println()

View File

@@ -170,6 +170,7 @@ class AllSetting {
this.webCertFile = "";
this.webKeyFile = "";
this.webBasePath = "/";
this.sessionMaxAge = "";
this.expireDiff = "";
this.trafficDiff = "";
this.tgBotEnable = false;

View File

@@ -647,7 +647,7 @@ class RealityStreamSettings extends XrayCommonClass {
}
RealityStreamSettings.Settings = class extends XrayCommonClass {
constructor(publicKey = '', fingerprint = '', serverName = '', spiderX= '/') {
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '', spiderX= '/') {
super();
this.publicKey = publicKey;
this.fingerprint = fingerprint;
@@ -1243,15 +1243,13 @@ class Inbound extends XrayCommonClass {
if (this.reality) {
params.set("security", "reality");
params.set("pbk", this.stream.reality.settings.publicKey);
params.set("fp", this.stream.reality.settings.fingerprint);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(",")[0]);
}
if (this.stream.reality.shortIds.length > 0) {
params.set("sid", this.stream.reality.shortIds.split(",")[0]);
}
if (!ObjectUtil.isEmpty(this.stream.reality.fingerprint)) {
params.set("fp", this.stream.reality.settings.fingerprint);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
}
@@ -1355,15 +1353,13 @@ class Inbound extends XrayCommonClass {
if (this.reality) {
params.set("security", "reality");
params.set("pbk", this.stream.reality.settings.publicKey);
params.set("fp", this.stream.reality.settings.fingerprint);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(",")[0]);
}
if (this.stream.reality.shortIds.length > 0) {
params.set("sid", this.stream.reality.shortIds.split(",")[0]);
}
if (!ObjectUtil.isEmpty(this.stream.reality.fingerprint)) {
params.set("fp", this.stream.reality.settings.fingerprint);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
}

View File

@@ -19,15 +19,17 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.inbounds)
g.GET("/get/:id", a.inbound)
g.GET("/getClientTraffics/:email", a.getClientTraffics)
g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound)
g.POST("/addClient/", a.addInboundClient)
g.POST("/delClient/:email", a.delInboundClient)
g.POST("/updateClient/:index", a.updateInboundClient)
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("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
a.inboundController = NewInboundController(g)
}
@@ -38,6 +40,9 @@ func (a *APIController) inbounds(c *gin.Context) {
func (a *APIController) inbound(c *gin.Context) {
a.inboundController.getInbound(c)
}
func (a *APIController) getClientTraffics(c *gin.Context) {
a.inboundController.getClientTraffics(c)
}
func (a *APIController) addInbound(c *gin.Context) {
a.inboundController.addInbound(c)
}
@@ -65,3 +70,6 @@ func (a *APIController) resetAllTraffics(c *gin.Context) {
func (a *APIController) resetAllClientTraffics(c *gin.Context) {
a.inboundController.resetAllClientTraffics(c)
}
func (a *APIController) delDepletedClients(c *gin.Context) {
a.inboundController.delDepletedClients(c)
}

View File

@@ -32,11 +32,12 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound)
g.POST("/addClient", a.addInboundClient)
g.POST("/delClient/:email", a.delInboundClient)
g.POST("/updateClient/:index", a.updateInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
}
@@ -75,6 +76,15 @@ func (a *InboundController) getInbound(c *gin.Context) {
}
jsonObj(c, inbound, nil)
}
func (a *InboundController) getClientTraffics(c *gin.Context) {
email := c.Param("email")
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
if err != nil {
jsonMsg(c, "Error getting traffics", err)
return
}
jsonObj(c, clientTraffics, nil)
}
func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{}
@@ -138,7 +148,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
err = a.inboundService.AddInboundClient(data)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client(s) added", nil)
@@ -148,17 +158,16 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
}
func (a *InboundController) delInboundClient(c *gin.Context) {
email := c.Param("email")
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
clientId := c.Param("clientId")
err = a.inboundService.DelInboundClient(inbound, email)
err = a.inboundService.DelInboundClient(id, clientId)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client deleted", nil)
@@ -168,22 +177,18 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
}
func (a *InboundController) updateInboundClient(c *gin.Context) {
index, err := strconv.Atoi(c.Param("index"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
clientId := c.Param("clientId")
inbound := &model.Inbound{}
err = c.ShouldBind(inbound)
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
err = a.inboundService.UpdateInboundClient(inbound, index)
err = a.inboundService.UpdateInboundClient(inbound, clientId)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client updated", nil)
@@ -202,7 +207,7 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
err = a.inboundService.ResetClientTraffic(id, email)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "traffic reseted", nil)
@@ -214,7 +219,7 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
func (a *InboundController) resetAllTraffics(c *gin.Context) {
err := a.inboundService.ResetAllTraffics()
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "All traffics reseted", nil)
@@ -229,8 +234,22 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
err = a.inboundService.ResetAllClientTraffics(id)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "All traffics of client reseted", nil)
}
func (a *InboundController) delDepletedClients(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
err = a.inboundService.DelDepletedClients(id)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "All delpeted clients are deleted", nil)
}

View File

@@ -18,8 +18,9 @@ type LoginForm struct {
type IndexController struct {
BaseController
userService service.UserService
tgbot service.Tgbot
settingService service.SettingService
userService service.UserService
tgbot service.Tgbot
}
func NewIndexController(g *gin.RouterGroup) *IndexController {
@@ -69,6 +70,16 @@ func (a *IndexController) login(c *gin.Context) {
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
}
sessionMaxAge, err := a.settingService.GetSessionMaxAge()
if err != nil {
logger.Infof("Unable to get session's max age from DB")
}
err = session.SetMaxAge(c, sessionMaxAge*60)
if err != nil {
logger.Infof("Unable to set session's max age")
}
err = session.SetLoginUser(c, user)
logger.Info("user", user.Id, "login success")
jsonMsg(c, I18n(c, "pages.login.toasts.successLogin"), err)

View File

@@ -39,7 +39,7 @@ func (a *SUBController) subs(c *gin.Context) {
}
// Add subscription-userinfo
c.Writer.Header().Set("subscription-userinfo", header)
c.Writer.Header().Set("Subscription-Userinfo", header)
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
}

View File

@@ -1,24 +1,16 @@
package controller
import (
"github.com/gin-gonic/gin"
"net"
"net/http"
"strings"
"x-ui/config"
"x-ui/logger"
"x-ui/web/entity"
"github.com/gin-gonic/gin"
)
func getUriId(c *gin.Context) int64 {
s := struct {
Id int64 `uri:"id"`
}{}
_ = c.BindUri(&s)
return s.Id
}
func getRemoteIp(c *gin.Context) string {
value := c.GetHeader("X-Forwarded-For")
if value != "" {
@@ -75,6 +67,7 @@ func html(c *gin.Context, name string, title string, data gin.H) {
data = gin.H{}
}
data["title"] = title
data["host"] = strings.Split(c.Request.Host, ":")[0]
data["request_uri"] = c.Request.RequestURI
data["base_path"] = c.GetString("base_path")
c.HTML(http.StatusOK, name, getContext(data))
@@ -84,10 +77,8 @@ func getContext(h gin.H) gin.H {
a := gin.H{
"cur_ver": config.GetVersion(),
}
if h != nil {
for key, value := range h {
a[key] = value
}
for key, value := range h {
a[key] = value
}
return a
}

View File

@@ -32,6 +32,7 @@ type AllSetting struct {
WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
WebBasePath string `json:"webBasePath" form:"webBasePath"`
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`

View File

@@ -13,6 +13,6 @@
display: none;
}
</style>
<title>{{ i18n .title}}</title>
<title>{{ .host }}-{{ i18n .title}}</title>
</head>
{{end}}

View File

@@ -17,12 +17,13 @@
inbound: new Inbound(),
clients: [],
clientStats: [],
oldClientId: "",
index: null,
isExpired: false,
delayedStart: false,
ok() {
if(clientModal.isEdit){
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.index);
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
} else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
}
@@ -38,12 +39,13 @@
this.index = index === null ? this.clients.length : index;
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
this.delayedStart = false;
if (!isEdit){
this.addClient(this.inbound.protocol, this.clients);
} else {
if (isEdit){
if (this.clients[index].expiryTime < 0){
this.delayedStart = true;
}
this.oldClientId = this.dbInbound.protocol == "trojan" ? this.clients[index].password : this.clients[index].id;
} else {
this.addClient(this.inbound.protocol, this.clients);
}
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm;
@@ -122,6 +124,24 @@
}
client.email = string;
},
resetClientTraffic(email,dbInboundId,iconElement) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ email);
if (msg.success) {
this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0;
}
iconElement.disabled = false;
},
})
},
},
});
</script>

View File

@@ -9,7 +9,7 @@
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
</template>
<template v-else-if="type === 'number'">
<a-input-number :value="value" @input="$emit('input', $event.target.value)"></a-input-number>
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)" :min="min"></a-input>
</template>
<template v-else-if="type === 'textarea'">
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
@@ -25,7 +25,7 @@
{{define "component/setting"}}
<script>
Vue.component('setting-list-item', {
props: ["type", "title", "desc", "value"],
props: ["type", "title", "desc", "value", "min"],
template: `{{template "component/settingListItem"}}`,
});
</script>

View File

@@ -103,6 +103,10 @@
[[ sizeFormat(clientStats.down) ]]
([[ sizeFormat(clientStats.up + clientStats.down) ]])
</a-tag>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" v-if="client.email.length > 0"></a-icon>
</a-tooltip>
</td>
</tr>
<tr>

View File

@@ -1,24 +1,17 @@
{{define "form/streamSettings"}}
<!-- select stream network -->
<a-form layout="inline">
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "transmission" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.network" @change="streamNetworkChange"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="kcp">kcp</a-select-option>
<a-select-option value="ws">ws</a-select-option>
<a-select-option value="http">http</a-select-option>
<a-select-option value="quic">quic</a-select-option>
<a-select-option value="grpc">grpc</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</table>
<a-form-item label="{{ i18n "transmission" }}">
<a-select v-model="inbound.stream.network" @change="streamNetworkChange"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="kcp">kcp</a-select-option>
<a-select-option value="ws">ws</a-select-option>
<a-select-option value="http">http</a-select-option>
<a-select-option value="quic">quic</a-select-option>
<a-select-option value="grpc">grpc</a-select-option>
</a-select>
</a-form-item>
</a-form>
<!-- tcp -->

View File

@@ -176,7 +176,6 @@
<td>
<a-form-item >
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 250px">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>

View File

@@ -51,7 +51,7 @@
</td>
<td v-else-if="inbound.reality">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
</td>
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
</td>

View File

@@ -67,8 +67,27 @@
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
<a-button type="primary" icon="reload" @click="resetAllTraffic">{{ i18n "pages.inbounds.resetAllTraffic" }}</a-button>
<a-dropdown :trigger="['click']">
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="siderDrawer.theme">
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }}
</a-menu-item>
<a-menu-item key="resetInbounds">
<a-icon type="reload"></a-icon>
{{ i18n "pages.inbounds.resetAllTraffic" }}
</a-menu-item>
<a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
</a-menu-item>
<a-menu-item key="delDepletedClients">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</a-menu>
</a-dropdown>
</div>
<a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
@@ -100,12 +119,16 @@
</a-menu-item>
<a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetAllClientTraffics"}}
{{ i18n "pages.inbounds.resetInboundClientTraffics"}}
</a-menu-item>
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}}
</a-menu-item>
<a-menu-item key="delDepletedClients">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</template>
<template v-else>
<a-menu-item key="showInfo">
@@ -387,6 +410,22 @@
});
}
},
generalActions(action){
switch (action.key) {
case "export":
this.exportAllLinks();
break;
case "resetInbounds":
this.resetAllTraffic();
break;
case "resetClients":
this.resetAllClientTraffics(-1);
break;
case "delDepletedClients":
this.delDepletedClients(-1)
break;
}
},
clickAction(action, dbInbound) {
switch (action.key) {
case "qrcode":
@@ -419,6 +458,9 @@
case "delete":
this.delInbound(dbInbound.id);
break;
case "delDepletedClients":
this.delDepletedClients(dbInbound.id)
break;
}
},
openAddInbound() {
@@ -558,9 +600,9 @@
okText: '{{ i18n "pages.client.submitEdit"}}',
dbInbound: dbInbound,
index: index,
confirm: async (client, dbInboundId, index) => {
confirm: async (client, dbInboundId, clientId) => {
clientModal.loading();
await this.updateClient(client, dbInboundId, index);
await this.updateClient(client, dbInboundId, clientId);
clientModal.close();
},
isEdit: true
@@ -577,12 +619,12 @@
};
await this.submit(`/xui/inbound/addClient`, data);
},
async updateClient(client, dbInboundId, index) {
async updateClient(client, dbInboundId, clientId) {
const data = {
id: dbInboundId,
settings: '{"clients": [' + client.toString() +']}',
};
await this.submit(`/xui/inbound/updateClient/${index}`, data);
await this.submit(`/xui/inbound/updateClient/${clientId}`, data);
},
resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -612,22 +654,14 @@
},
delClient(dbInboundId,client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
newDbInbound = new DBInbound(dbInbound);
inbound = newDbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(clients, client);
clients.splice(index, 1);
const data = {
id: dbInboundId,
settings: inbound.settings.toString(),
};
clientId = dbInbound.protocol == "trojan" ? client.password : client.id;
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/delClient/' + client.email, data),
onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`),
});
},
getClients(protocol, clientSettings) {
@@ -656,7 +690,8 @@
clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(clients, client);
clients[index].enable = !clients[index].enable;
await this.updateClient(clients[index],dbInboundId, index);
clientId = dbInbound.protocol == "trojan" ? clients[index].password : clients[index].id;
await this.updateClient(clients[index],dbInboundId, clientId);
this.loading(false);
},
async submit(url, data) {
@@ -696,14 +731,24 @@
},
resetAllClientTraffics(dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
title: dbInboundId>0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: dbInboundId>0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
})
},
delDepletedClients(dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/delDepletedClients/' + dbInboundId),
})
},
isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index)
},

View File

@@ -44,6 +44,7 @@
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.sessionMaxAge" }}' desc='{{ i18n "pages.setting.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.expireTimeDiff" }}' desc='{{ i18n "pages.setting.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.trafficDiff" }}' desc='{{ i18n "pages.setting.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
<a-list-item>

View File

@@ -3,6 +3,7 @@ package service
import (
"encoding/json"
"fmt"
"strings"
"time"
"x-ui/database"
"x-ui/database/model"
@@ -300,26 +301,54 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) error {
return db.Save(oldInbound).Error
}
func (s *InboundService) DelInboundClient(inbound *model.Inbound, email string) error {
db := database.GetDB()
err := s.DelClientStat(db, email)
if err != nil {
logger.Error("Delete stats Data Error")
return err
}
oldInbound, err := s.GetInbound(inbound.Id)
func (s *InboundService) DelInboundClient(inboundId int, clientId string) error {
oldInbound, err := s.GetInbound(inboundId)
if err != nil {
logger.Error("Load Old Data Error")
return err
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
if err != nil {
return err
}
oldInbound.Settings = inbound.Settings
email := ""
client_key := "id"
if oldInbound.Protocol == "trojan" {
client_key = "password"
}
inerfaceClients := settings["clients"].([]interface{})
var newClients []interface{}
for _, client := range inerfaceClients {
c := client.(map[string]interface{})
c_id := c[client_key].(string)
if c_id == clientId {
email = c["email"].(string)
} else {
newClients = append(newClients, client)
}
}
settings["clients"] = newClients
newSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
oldInbound.Settings = string(newSettings)
db := database.GetDB()
err = s.DelClientStat(db, email)
if err != nil {
logger.Error("Delete stats Data Error")
return err
}
return db.Save(oldInbound).Error
}
func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) error {
func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) error {
clients, err := s.getClients(data)
if err != nil {
return err
@@ -343,7 +372,23 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
return err
}
if len(clients[0].Email) > 0 && clients[0].Email != oldClients[index].Email {
oldEmail := ""
clientIndex := 0
for index, oldClient := range oldClients {
oldClientId := ""
if oldInbound.Protocol == "trojan" {
oldClientId = oldClient.Password
} else {
oldClientId = oldClient.ID
}
if clientId == oldClientId {
oldEmail = oldClient.Email
clientIndex = index
break
}
}
if len(clients[0].Email) > 0 && clients[0].Email != oldEmail {
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil {
return err
@@ -358,10 +403,8 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
if err != nil {
return err
}
settingsClients := oldSettings["clients"].([]interface{})
settingsClients[index] = inerfaceClients[0]
settingsClients[clientIndex] = inerfaceClients[0]
oldSettings["clients"] = settingsClients
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
@@ -373,8 +416,8 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
db := database.GetDB()
if len(clients[0].Email) > 0 {
if len(oldClients[index].Email) > 0 {
err = s.UpdateClientStat(oldClients[index].Email, &clients[0])
if len(oldEmail) > 0 {
err = s.UpdateClientStat(oldEmail, &clients[0])
if err != nil {
return err
}
@@ -382,7 +425,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
s.AddClientStat(data.Id, &clients[0])
}
} else {
err = s.DelClientStat(db, oldClients[index].Email)
err = s.DelClientStat(db, oldEmail)
if err != nil {
return err
}
@@ -390,45 +433,35 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
return db.Save(oldInbound).Error
}
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error {
if len(traffics) == 0 {
return nil
}
db := database.GetDB()
db = db.Model(model.Inbound{})
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
for _, traffic := range traffics {
if traffic.IsInbound {
err = tx.Where("tag = ?", traffic.Tag).
UpdateColumns(map[string]interface{}{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down)}).Error
if err != nil {
return
// Update traffics in a single transaction
err := database.GetDB().Transaction(func(tx *gorm.DB) error {
for _, traffic := range traffics {
if traffic.IsInbound {
update := tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
Updates(map[string]interface{}{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down),
})
if update.Error != nil {
return update.Error
}
}
}
}
return
return nil
})
return err
}
func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) {
if len(traffics) == 0 {
return nil
}
traffics, err = s.adjustTraffics(traffics)
if err != nil {
return err
}
db := database.GetDB()
db = db.Model(xray.ClientTraffic{})
tx := db.Begin()
defer func() {
@@ -439,7 +472,32 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
}
}()
err = tx.Save(traffics).Error
emails := make([]string, 0, len(traffics))
for _, traffic := range traffics {
emails = append(emails, traffic.Email)
}
dbClientTraffics := make([]*xray.ClientTraffic, 0, len(traffics))
err = db.Model(xray.ClientTraffic{}).Where("email IN (?)", emails).Find(&dbClientTraffics).Error
if err != nil {
return err
}
dbClientTraffics, err = s.adjustTraffics(tx, dbClientTraffics)
if err != nil {
return err
}
for dbTraffic_index := range dbClientTraffics {
for traffic_index := range traffics {
if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {
dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up
dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down
break
}
}
}
err = tx.Save(dbClientTraffics).Error
if err != nil {
logger.Warning("AddClientTraffic update data ", err)
}
@@ -447,81 +505,56 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
return nil
}
func (s *InboundService) adjustTraffics(traffics []*xray.ClientTraffic) (full_traffics []*xray.ClientTraffic, err error) {
db := database.GetDB()
dbInbound := db.Model(model.Inbound{})
txInbound := dbInbound.Begin()
defer func() {
if err != nil {
txInbound.Rollback()
} else {
txInbound.Commit()
func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.ClientTraffic) ([]*xray.ClientTraffic, error) {
inboundIds := make([]int, 0, len(dbClientTraffics))
for _, dbClientTraffic := range dbClientTraffics {
if dbClientTraffic.ExpiryTime < 0 {
inboundIds = append(inboundIds, dbClientTraffic.InboundId)
}
}()
for _, traffic := range traffics {
inbound := &model.Inbound{}
client_traffic := &xray.ClientTraffic{}
err := db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(client_traffic).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err, traffic.Email)
}
continue
}
client_traffic.Up += traffic.Up
client_traffic.Down += traffic.Down
err = txInbound.Where("id=?", client_traffic.InboundId).First(inbound).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err, traffic.Email)
}
continue
}
// get clients
clients, err := s.getClients(inbound)
needUpdate := false
if err == nil {
for client_index, client := range clients {
if traffic.Email == client.Email {
if client.ExpiryTime < 0 {
clients[client_index].ExpiryTime = (time.Now().Unix() * 1000) - client.ExpiryTime
needUpdate = true
}
client_traffic.ExpiryTime = client.ExpiryTime
client_traffic.Total = client.TotalGB
break
}
}
}
if needUpdate {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbound.Settings), &settings)
// Convert clients to []interface to update clients in settings
var clientsInterface []interface{}
for _, c := range clients {
clientsInterface = append(clientsInterface, interface{}(c))
}
settings["clients"] = clientsInterface
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err
}
err = txInbound.Where("id=?", inbound.Id).Update("settings", string(modifiedSettings)).Error
if err != nil {
return nil, err
}
}
full_traffics = append(full_traffics, client_traffic)
}
return full_traffics, nil
if len(inboundIds) > 0 {
var inbounds []*model.Inbound
err := tx.Model(model.Inbound{}).Where("id IN (?)", inboundIds).Find(&inbounds).Error
if err != nil {
return nil, err
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
if ok {
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
for traffic_index := range dbClientTraffics {
if dbClientTraffics[traffic_index].ExpiryTime < 0 && c["email"] == dbClientTraffics[traffic_index].Email {
oldExpiryTime := c["expiryTime"].(float64)
newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime)
c["expiryTime"] = newExpiryTime
dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime
break
}
}
newClients = append(newClients, interface{}(c))
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err
}
inbounds[inbound_index].Settings = string(modifiedSettings)
}
}
err = tx.Save(inbounds).Error
if err != nil {
logger.Warning("AddClientTraffic update inbounds ", err)
logger.Error(inbounds)
}
}
return dbClientTraffics, nil
}
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
@@ -611,8 +644,15 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
func (s *InboundService) ResetAllClientTraffics(id int) error {
db := database.GetDB()
whereText := "inbound_id "
if id == -1 {
whereText += " > ?"
} else {
whereText += " = ?"
}
result := db.Model(xray.ClientTraffic{}).
Where("inbound_id = ?", id).
Where(whereText, id).
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
err := result.Error
@@ -638,6 +678,84 @@ func (s *InboundService) ResetAllTraffics() error {
return nil
}
func (s *InboundService) DelDepletedClients(id int) (err error) {
db := database.GetDB()
tx := db.Begin()
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
whereText := "inbound_id "
if id < 0 {
whereText += "> ?"
} else {
whereText += "= ?"
}
depletedClients := []xray.ClientTraffic{}
err = db.Model(xray.ClientTraffic{}).Where(whereText+" and enable = ?", id, false).Select("inbound_id, GROUP_CONCAT(email) as email").Group("inbound_id").Find(&depletedClients).Error
if err != nil {
return err
}
for _, depletedClient := range depletedClients {
emails := strings.Split(depletedClient.Email, ",")
oldInbound, err := s.GetInbound(depletedClient.InboundId)
if err != nil {
return err
}
var oldSettings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil {
return err
}
oldClients := oldSettings["clients"].([]interface{})
var newClients []interface{}
for _, client := range oldClients {
deplete := false
c := client.(map[string]interface{})
for _, email := range emails {
if email == c["email"].(string) {
deplete = true
break
}
}
if !deplete {
newClients = append(newClients, client)
}
}
if len(newClients) > 0 {
oldSettings["clients"] = newClients
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
if err != nil {
return err
}
oldInbound.Settings = string(newSettings)
err = tx.Save(oldInbound).Error
if err != nil {
return err
}
} else {
// Delete inbound if no client remains
s.DelInbound(depletedClient.InboundId)
}
}
err = tx.Where(whereText+" and enable = ?", id, false).Delete(xray.ClientTraffic{}).Error
if err != nil {
return err
}
return nil
}
func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTraffic, error) {
db := database.GetDB()
var inbounds []*model.Inbound
@@ -668,18 +786,18 @@ func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTr
return traffics, err
}
func (s *InboundService) GetClientTrafficByEmail(email string) (traffic []*xray.ClientTraffic, err error) {
func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.ClientTraffic, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("email like ?", "%"+email+"%").Find(&traffics).Error
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err)
return nil, err
}
}
return traffics, err
return traffics[0], err
}
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
@@ -730,3 +848,57 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
}
return inbounds, nil
}
func (s *InboundService) MigrationRequirements() {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
if ok {
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
// Add email='' if it is not exists
if _, ok := c["email"]; !ok {
c["email"] = ""
}
// Remove "flow": "xtls-rprx-direct"
if _, ok := c["flow"]; ok {
if c["flow"] == "xtls-rprx-direct" {
c["flow"] = ""
}
}
newClients = append(newClients, interface{}(c))
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return
}
inbounds[inbound_index].Settings = string(modifiedSettings)
}
modelClients, err := s.getClients(inbounds[inbound_index])
if err != nil {
return
}
for _, modelClient := range modelClients {
if len(modelClient.Email) > 0 {
var count int64
db.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count)
if count == 0 {
s.AddClientStat(inbounds[inbound_index].Id, &modelClient)
}
}
}
}
db.Save(inbounds)
}

View File

@@ -28,6 +28,7 @@ var defaultValueMap = map[string]string{
"webKeyFile": "",
"secret": random.Seq(32),
"webBasePath": "/",
"sessionMaxAge": "0",
"expireDiff": "0",
"trafficDiff": "0",
"timeLocation": "Asia/Tehran",
@@ -234,18 +235,10 @@ func (s *SettingService) GetTgBotBackup() (bool, error) {
return s.getBool("tgBotBackup")
}
func (s *SettingService) SetTgBotBackup(value bool) error {
return s.setBool("tgBotBackup", value)
}
func (s *SettingService) GetTgCpu() (int, error) {
return s.getInt("tgCpu")
}
func (s *SettingService) SetTgCpu(value int) error {
return s.setInt("tgCpu", value)
}
func (s *SettingService) GetPort() (int, error) {
return s.getInt("webPort")
}
@@ -266,16 +259,12 @@ func (s *SettingService) GetExpireDiff() (int, error) {
return s.getInt("expireDiff")
}
func (s *SettingService) SetExpireDiff(value int) error {
return s.setInt("expireDiff", value)
}
func (s *SettingService) GetTrafficDiff() (int, error) {
return s.getInt("trafficDiff")
}
func (s *SettingService) SetgetTrafficDiff(value int) error {
return s.setInt("trafficDiff", value)
func (s *SettingService) GetSessionMaxAge() (int, error) {
return s.getInt("sessionMaxAge")
}
func (s *SettingService) GetSecret() ([]byte, error) {

View File

@@ -66,13 +66,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
}
}
}
header = fmt.Sprintf("upload=%d;download=%d", traffic.Up, traffic.Down)
if traffic.Total > 0 {
header = header + fmt.Sprintf(";total=%d", traffic.Total)
}
if traffic.ExpiryTime > 0 {
header = header + fmt.Sprintf(";expire=%d", traffic.ExpiryTime)
}
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
return result, header, nil
}
@@ -538,7 +532,11 @@ func searchHost(headers interface{}) string {
switch v.(type) {
case []interface{}:
hosts, _ := v.([]interface{})
return hosts[0].(string)
if len(hosts) > 0 {
return hosts[0].(string)
} else {
return ""
}
case interface{}:
return v.(string)
}

View File

@@ -404,38 +404,36 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
}
func (t *Tgbot) searchClient(chatId int64, email string) {
traffics, err := t.inboundService.GetClientTrafficByEmail(email)
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if len(traffics) == 0 {
if traffic == nil {
msg := "No result!"
t.SendMsgToTgbot(chatId, msg)
return
}
for _, traffic := range traffics {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
}
func (t *Tgbot) searchInbound(chatId int64, remark string) {

View File

@@ -69,7 +69,6 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
}
s.inboundService.DisableInvalidClients()
s.inboundService.RemoveOrphanedTraffics()
inbounds, err := s.inboundService.GetAllInbounds()
if err != nil {
@@ -119,12 +118,15 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "alterId" {
delete(c, key)
}
if c["flow"] == "xtls-rprx-vision-udp443" {
c["flow"] = "xtls-rprx-vision"
}
}
final_clients = append(final_clients, interface{}(c))
}
settings["clients"] = final_clients
modifiedSettings, err := json.Marshal(settings)
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err
}

View File

@@ -2,9 +2,10 @@ package session
import (
"encoding/gob"
"x-ui/database/model"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"x-ui/database/model"
)
const (
@@ -21,6 +22,15 @@ func SetLoginUser(c *gin.Context, user *model.User) error {
return s.Save()
}
func SetMaxAge(c *gin.Context, maxAge int) error {
s := sessions.Default(c)
s.Options(sessions.Options{
Path: "/",
MaxAge: maxAge,
})
return s.Save()
}
func GetLoginUser(c *gin.Context) *model.User {
s := sessions.Default(c)
obj := s.Get(loginUser)

View File

@@ -106,6 +106,7 @@
"expireDate" = "Expire date"
"resetTraffic" = "Reset traffic"
"addInbound" = "Add Inbound"
"generalActions" = "General Actions"
"addTo" = "Create"
"revise" = "Update"
"modifyInbound" = "Modify InBound"
@@ -138,9 +139,15 @@
"resetAllTraffic" = "Reset All Inbounds Traffic"
"resetAllTrafficTitle" = "Reset all inbounds traffic"
"resetAllTrafficContent" = "Are you sure to reset all inbounds traffic ?"
"resetAllClientTraffics" = "Reset Clients Traffic"
"resetInboundClientTraffics" = "Reset Clients Traffic"
"resetInboundClientTrafficTitle" = "Reset all clients traffic"
"resetInboundClientTrafficContent" = "Are you sure to reset all traffics of this inbound's clients ?"
"resetAllClientTraffics" = "Reset All Clients Traffic"
"resetAllClientTrafficTitle" = "Reset all clients traffic"
"resetAllClientTrafficContent" = "Are you sure to reset all traffics of this inbound's clients ?"
"resetAllClientTrafficContent" = "Are you sure to reset all traffics of all clients ?"
"delDepletedClients" = "Delete depleted clients"
"delDepletedClientsTitle" = "Delete depleted clients"
"delDepletedClientsContent" = "Are you sure to delete all depleted clients ?"
"Email" = "Email"
"EmailDesc" = "The Email Must Be Completely Unique"
"setDefaultCert" = "Set cert from panel"
@@ -199,7 +206,7 @@
"publicKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
"privateKeyPath" = "Panel certificate private key file path"
"privateKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
"panelUrlPath" = "panel url root path"
"panelUrlPath" = "Panel url root path"
"panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect"
"oldUsername" = "Current Username"
"currentPassword" = "Current Password"
@@ -229,6 +236,8 @@
"telegramNotifyTimeDesc" = "Using Crontab timing format. Restart the panel to take effect"
"tgNotifyBackup" = "Database backup"
"tgNotifyBackupDesc" = "Sending database backup file with report notification. Restart the panel to take effect"
"sessionMaxAge" = "Session maximum age"
"sessionMaxAgeDesc" = "The time that you can stay login (unit: minute)"
"expireTimeDiff" = "Exhaustion time threshold"
"expireTimeDiffDesc" = "Detect exhaustion before expiration (unit:day)"
"trafficDiff" = "Exhaustion traffic threshold"

View File

@@ -106,6 +106,7 @@
"expireDate" = "تاریخ انقضا"
"resetTraffic" = "ریست ترافیک"
"addInbound" = "اضافه کردن سرویس"
"generalActions" = "عملیات کلی"
"addTo" = "اضافه کردن"
"revise" = "ویرایش"
"modifyInbound" = "ویرایش سرویس"
@@ -138,9 +139,15 @@
"resetAllTraffic" = "ریست ترافیک کل سرویس ها"
"resetAllTrafficTitle" = "ریست ترافیک کل سرویس ها"
"resetAllTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک سرویس ها را ریست کنید؟"
"resetInboundClientTraffics" = "ریست ترافیک کاربران"
"resetInboundClientTrafficTitle" = "ریست ترافیک کل کاربران"
"resetInboundClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
"resetAllClientTraffics" = "ریست ترافیک کاربران"
"resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران"
"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران را ریست کنید؟"
"delDepletedClients" = "حذف کاربران منقضی"
"delDepletedClientsTitle" = "حذف کاربران منقضی"
"delDepletedClientsContent" = "آیا مطمئن هستید مه میخواهید تمامی کاربران منقضی شده را حذف کنید؟"
"Email" = "ایمیل"
"EmailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
"setDefaultCert" = "استفاده از گواهی پنل"
@@ -229,6 +236,8 @@
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"sessionMaxAge" = "بیشینه زمان جلسه وب"
"sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)"
"expireTimeDiff" = "آستانه زمان باقی مانده"
"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
"trafficDiff" = "آستانه ترافیک باقی مانده"

View File

@@ -106,6 +106,7 @@
"expireDate" = "到期时间"
"resetTraffic" = "重置流量"
"addInbound" = "添加入"
"generalActions" = "通用操作"
"addTo" = "添加"
"revise" = "修改"
"modifyInbound" = "修改入站"
@@ -138,9 +139,15 @@
"resetAllTraffic" = "重置所有入站流量"
"resetAllTrafficTitle" = "重置所有入站流量"
"resetAllTrafficContent" = "您确定要重置所有入站流量吗?"
"resetAllClientTraffics" = "重置客户端流量"
"resetInboundClientTraffics" = "重置客户端流量"
"resetInboundClientTrafficTitle" = "重置所有客户端流量"
"resetInboundClientTrafficContent" = "您确定要重置此入站客户端的所有流量吗?"
"resetAllClientTraffics" = "重置所有客户端流量"
"resetAllClientTrafficTitle" = "重置所有客户端流量"
"resetAllClientTrafficContent" = "确定要重置此入站客户端的所有流量吗?"
"resetAllClientTrafficContent" = "确定要重置所有客户端的所有流量吗?"
"delDepletedClients" = "删除耗尽的客户端"
"delDepletedClientsTitle" = "删除耗尽的客户"
"delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?"
"Email" = "电子邮件"
"EmailDesc" = "电子邮件必须完全唯"
"setDefaultCert" = "从面板设置证书"
@@ -229,6 +236,8 @@
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
"sessionMaxAge" = "会话最大年龄"
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
"expireTimeDiff" = "耗尽时间阈值"
"expireTimeDiffDesc" = "到期前检测耗尽(单位:天)"
"trafficDiff" = "耗尽流量阈值"