From 47a48316cac550a9f2e87c45f3c6c47a2b3c8850 Mon Sep 17 00:00:00 2001 From: Hossin Asaadi Date: Sat, 12 Nov 2022 08:25:29 -0500 Subject: [PATCH] add fully function Multi User With ExpireDate & Traffic --- database/db.go | 10 ++++- database/model/model.go | 4 +- web/assets/js/model/models.js | 7 +--- web/html/xui/form/protocol/vmess.html | 8 ++-- web/html/xui/inbound_modal.html | 47 +++++++++++++++++++--- web/job/check_inbound_job.go | 10 ++++- web/job/xray_traffic_job.go | 22 +++++----- web/service/inbound.go | 58 +++++++++++++++++---------- web/service/xray.go | 42 ++++++++++++++++++- xray/client_traffic.go | 11 +++-- 10 files changed, 166 insertions(+), 53 deletions(-) diff --git a/database/db.go b/database/db.go index ef5bf2f3..92fca27a 100644 --- a/database/db.go +++ b/database/db.go @@ -8,6 +8,7 @@ import ( "os" "path" "x-ui/config" + "x-ui/xray" "x-ui/database/model" ) @@ -43,6 +44,9 @@ func initSetting() error { func initInboundClientIps() error { return db.AutoMigrate(&model.InboundClientIps{}) } +func initClientTraffic() error { + return db.AutoMigrate(&xray.ClientTraffic{}) +} func InitDB(dbPath string) error { dir := path.Dir(dbPath) @@ -83,7 +87,11 @@ func InitDB(dbPath string) error { if err != nil { return err } - + err = initClientTraffic() + if err != nil { + return err + } + return nil } diff --git a/database/model/model.go b/database/model/model.go index 158e33ef..30e348be 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -32,7 +32,7 @@ type Inbound struct { Remark string `json:"remark" form:"remark"` Enable bool `json:"enable" form:"enable"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` - ClientStats string `json:"clientStats" form:"clientStats"` + ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // config part Listen string `json:"listen" form:"listen"` @@ -76,6 +76,6 @@ type Client struct { Email string `json:"email"` LimitIP int `json:"limitIp"` Security string `json:"security"` - Total int64 `json:"total" form:"total"` + TotalGB int64 `json:"totalGB" form:"totalGB"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` } \ No newline at end of file diff --git a/web/assets/js/model/models.js b/web/assets/js/model/models.js index ba79d8a2..768bf4de 100644 --- a/web/assets/js/model/models.js +++ b/web/assets/js/model/models.js @@ -125,10 +125,7 @@ class DBInbound { if (!ObjectUtil.isEmpty(this.sniffing)) { sniffing = JSON.parse(this.sniffing); } - let clientStats = {}; - if (!ObjectUtil.isEmpty(this.clientStats)) { - clientStats = JSON.parse(this.clientStats); - } + const config = { port: this.port, listen: this.listen, @@ -137,7 +134,7 @@ class DBInbound { streamSettings: streamSettings, tag: this.tag, sniffing: sniffing, - clientStats: clientStats, + clientStats: this.clientStats, }; return Inbound.fromJson(config); } diff --git a/web/html/xui/form/protocol/vmess.html b/web/html/xui/form/protocol/vmess.html index cc2fd908..223ffea2 100644 --- a/web/html/xui/form/protocol/vmess.html +++ b/web/html/xui/form/protocol/vmess.html @@ -2,8 +2,8 @@ - - Account is Expired And Disabled + + Account is (Expired|Traffic Ended) And Disabled @@ -56,7 +56,7 @@ - + @@ -73,7 +73,7 @@ [[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]] - + used : [[ sizeFormat(getUpStats(vmess.email) + getDownStats(vmess.email)) ]] diff --git a/web/html/xui/inbound_modal.html b/web/html/xui/inbound_modal.html index 61dbba69..9f9cd92a 100644 --- a/web/html/xui/inbound_modal.html +++ b/web/html/xui/inbound_modal.html @@ -116,14 +116,51 @@ return this.inbound.isExpiry(index) }, getUpStats(email) { - console.log(email,this.inbound.clientStats[email]) - if(this.inbound.clientStats[email]) - return this.inbound.clientStats[email]["Up"] + clientStats = this.inbound.clientStats + if(clientStats.length > 0) + { + for (const key in clientStats) { + if (Object.hasOwnProperty.call(clientStats, key)) { + if(clientStats[key]['email'] == email) + return clientStats[key]['up'] + + } + } + } }, getDownStats(email) { - if(this.inbound.clientStats[email]) - return this.inbound.clientStats[email]["Down"] + clientStats = this.inbound.clientStats + if(clientStats.length > 0) + { + for (const key in clientStats) { + if (Object.hasOwnProperty.call(clientStats, key)) { + if(clientStats[key]['email'] == email) + return clientStats[key]['down'] + + } + } + } + }, + isClientEnable(email) { + clientStats = this.inbound.clientStats + if(clientStats.length > 0) + { + for (const key in clientStats) { + if (Object.hasOwnProperty.call(clientStats, key)) { + if(clientStats[key]['email'] == email) + return clientStats[key]['enable'] + + } + } + } + }, + + getHeaderText(email) { + if(email == "") + return "Add Client" + + return email + (this.isClientEnable(email) == true ? ' Active' : ' Deactive') }, diff --git a/web/job/check_inbound_job.go b/web/job/check_inbound_job.go index 14883023..2b24afb0 100644 --- a/web/job/check_inbound_job.go +++ b/web/job/check_inbound_job.go @@ -15,7 +15,15 @@ func NewCheckInboundJob() *CheckInboundJob { } func (j *CheckInboundJob) Run() { - count, err := j.inboundService.DisableInvalidInbounds() + count, err := j.inboundService.DisableInvalidClients() + if err != nil { + logger.Warning("disable invalid Client err:", err) + } else if count > 0 { + logger.Debugf("disabled %v Client", count) + j.xrayService.SetToNeedRestart() + } + + count, err = j.inboundService.DisableInvalidInbounds() if err != nil { logger.Warning("disable invalid inbounds err:", err) } else if count > 0 { diff --git a/web/job/xray_traffic_job.go b/web/job/xray_traffic_job.go index 1d1012d2..fa0fa636 100644 --- a/web/job/xray_traffic_job.go +++ b/web/job/xray_traffic_job.go @@ -18,16 +18,6 @@ func (j *XrayTrafficJob) Run() { if !j.xrayService.IsXrayRunning() { return } - traffics, err := j.xrayService.GetXrayTraffic() - if err != nil { - logger.Warning("get xray traffic failed:", err) - return - } - err = j.inboundService.AddTraffic(traffics) - if err != nil { - logger.Warning("add traffic failed:", err) - } - // get Client Traffic @@ -40,5 +30,17 @@ func (j *XrayTrafficJob) Run() { if err != nil { logger.Warning("add client traffic failed:", err) } + + traffics, err := j.xrayService.GetXrayTraffic() + if err != nil { + logger.Warning("get xray traffic failed:", err) + return + } + err = j.inboundService.AddTraffic(traffics) + if err != nil { + logger.Warning("add traffic failed:", err) + } + + } diff --git a/web/service/inbound.go b/web/service/inbound.go index 7baaf096..08965899 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -3,8 +3,8 @@ package service import ( "fmt" "time" - "encoding/json" "x-ui/database" + "encoding/json" "x-ui/database/model" "x-ui/util/common" "x-ui/xray" @@ -18,7 +18,7 @@ type InboundService struct { func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound - err := db.Model(model.Inbound{}).Where("user_id = ?", userId).Find(&inbounds).Error + err := db.Model(model.Inbound{}).Preload("ClientStats").Where("user_id = ?", userId).Find(&inbounds).Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } @@ -28,7 +28,7 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound - err := db.Model(model.Inbound{}).Find(&inbounds).Error + err := db.Model(model.Inbound{}).Preload("ClientStats").Find(&inbounds).Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } @@ -172,7 +172,7 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e return nil } db := database.GetDB() - db = db.Model(model.Inbound{}) + db = db.Model(xray.ClientTraffic{}) tx := db.Begin() defer func() { if err != nil { @@ -184,24 +184,30 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e for _, traffic := range traffics { inbound := &model.Inbound{} - err := tx.Where("settings like ?", "%" + traffic.Email + "%").First(inbound).Error - clientStats := map[string]*xray.ClientTraffic{} - json.Unmarshal([]byte(inbound.ClientStats), &clientStats) - - if _, ok := clientStats[traffic.Email]; ok { - clientStats[traffic.Email].Up = clientStats[traffic.Email].Up + traffic.Up - clientStats[traffic.Email].Down = clientStats[traffic.Email].Down + traffic.Down - }else{ - clientStats[traffic.Email] = traffic + err := db.Model(model.Inbound{}).Where("settings like ?", "%" + traffic.Email + "%").First(inbound).Error + traffic.InboundId = inbound.Id + if err != nil { + return err } - jsonClientStats, err := json.Marshal(clientStats) - - // if clientStats[traffic.Email] - err = tx.Where("settings like ?", "%" + traffic.Email + "%"). - Update("client_stats", jsonClientStats). - Error - - + // get settings clients + settings := map[string][]model.Client{} + json.Unmarshal([]byte(inbound.Settings), &settings) + clients := settings["clients"] + for _, client := range clients { + if traffic.Email == client.Email { + traffic.ExpiryTime = client.ExpiryTime + traffic.Total = client.TotalGB + } + } + if tx.Where("inbound_id = ?", inbound.Id).Where("email = ?", traffic.Email). + UpdateColumn("enable", true). + UpdateColumn("expiry_time", traffic.ExpiryTime). + UpdateColumn("total",traffic.Total). + UpdateColumn("up", gorm.Expr("up + ?", traffic.Up)). + UpdateColumn("down", gorm.Expr("down + ?", traffic.Down)).RowsAffected == 0 { + err = tx.Create(traffic).Error + } + if err != nil { return err } @@ -220,6 +226,16 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) { count := result.RowsAffected return count, err } +func (s *InboundService) DisableInvalidClients() (int64, error) { + db := database.GetDB() + now := time.Now().Unix() * 1000 + result := db.Model(xray.ClientTraffic{}). + Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true). + Update("enable", false) + err := result.Error + count := result.RowsAffected + return count, err +} func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) { db := database.GetDB() diff --git a/web/service/xray.go b/web/service/xray.go index b410181a..e664d064 100644 --- a/web/service/xray.go +++ b/web/service/xray.go @@ -4,9 +4,11 @@ import ( "encoding/json" "errors" "sync" + "time" + "x-ui/logger" "x-ui/xray" - + "x-ui/database/model" "go.uber.org/atomic" ) @@ -51,6 +53,9 @@ func (s *XrayService) GetXrayVersion() string { } return p.GetVersion() } +func RemoveIndex(s []model.Client, index int) []model.Client { + return append(s[:index], s[index+1:]...) +} func (s *XrayService) GetXrayConfig() (*xray.Config, error) { templateConfig, err := s.settingService.GetXrayConfigTemplate() @@ -72,6 +77,41 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { if !inbound.Enable { continue } + // get settings clients + settings := map[string][]model.Client{} + json.Unmarshal([]byte(inbound.Settings), &settings) + clients := settings["clients"] + + + + // check users active or not + now := time.Now().Unix() * 1000 + + clientStats := inbound.ClientStats + for _, clientTraffic := range clientStats { + + for index, client := range clients { + if client.Email == clientTraffic.Email { + totalUsage := clientTraffic.Up + clientTraffic.Down + if totalUsage > client.TotalGB || (client.ExpiryTime > 0 && client.ExpiryTime <= now){ + clients = RemoveIndex(clients,index) + logger.Debug("Remove Inbound User",client.Email) + + } + + } + } + + + } + settings["clients"] = clients + modifiedSettings, err := json.Marshal(settings) + if err != nil { + return nil, err + } + + inbound.Settings = string(modifiedSettings) + inboundConfig := inbound.GenXrayInboundConfig() xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig) } diff --git a/xray/client_traffic.go b/xray/client_traffic.go index 0c930375..4df6a502 100644 --- a/xray/client_traffic.go +++ b/xray/client_traffic.go @@ -1,7 +1,12 @@ package xray type ClientTraffic struct { - Email string - Up int64 - Down int64 + Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + InboundId int `json:"inboundId" form:"inboundId"` + Enable bool `json:"enable" form:"enable"` + Email string `json:"email" form:"email" gorm:"unique"` + Up int64 `json:"up" form:"up"` + Down int64 `json:"down" form:"down"` + ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` + Total int64 `json:"total" form:"total"` }