From a2db446cf3b0c7372e467afc403b21f982f4290b Mon Sep 17 00:00:00 2001 From: Hossin Asaadi Date: Sat, 29 Oct 2022 11:34:05 -0400 Subject: [PATCH 01/28] creade client ip db model --- database/db.go | 7 +++++++ database/model/model.go | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/database/db.go b/database/db.go index 3273874a..ef5bf2f3 100644 --- a/database/db.go +++ b/database/db.go @@ -40,6 +40,9 @@ func initInbound() error { func initSetting() error { return db.AutoMigrate(&model.Setting{}) } +func initInboundClientIps() error { + return db.AutoMigrate(&model.InboundClientIps{}) +} func InitDB(dbPath string) error { dir := path.Dir(dbPath) @@ -76,6 +79,10 @@ func InitDB(dbPath string) error { if err != nil { return err } + err = initInboundClientIps() + if err != nil { + return err + } return nil } diff --git a/database/model/model.go b/database/model/model.go index bc194445..b7142157 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -42,6 +42,11 @@ type Inbound struct { Tag string `json:"tag" form:"tag" gorm:"unique"` Sniffing string `json:"sniffing" form:"sniffing"` } +type InboundClientIps struct { + Id int `json:"id" gorm:"primaryKey;autoIncrement"` + ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"` + Ips string `json:"ips" form:"ips"` +} func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { listen := i.Listen From d864d20f34567251b65bae74ae5ae9a79eafaacb Mon Sep 17 00:00:00 2001 From: Hossin Asaadi Date: Sat, 29 Oct 2022 11:42:15 -0400 Subject: [PATCH 02/28] add email and ip limit to vmess ui --- web/assets/js/model/xray.js | 8 +++++++- web/html/xui/form/protocol/vmess.html | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/web/assets/js/model/xray.js b/web/assets/js/model/xray.js index ef5c4995..ecdf9e27 100644 --- a/web/assets/js/model/xray.js +++ b/web/assets/js/model/xray.js @@ -1157,16 +1157,22 @@ Inbound.VmessSettings = class extends Inbound.Settings { } }; Inbound.VmessSettings.Vmess = class extends XrayCommonClass { - constructor(id=RandomUtil.randomUUID(), alterId=0) { + constructor(id=RandomUtil.randomUUID(), alterId=0, email='', limitIp=0) { super(); this.id = id; this.alterId = alterId; + this.email = email; + this.limitIp = limitIp; + } static fromJson(json={}) { return new Inbound.VmessSettings.Vmess( json.id, json.alterId, + json.email, + json.limitIp, + ); } }; diff --git a/web/html/xui/form/protocol/vmess.html b/web/html/xui/form/protocol/vmess.html index 280e6a6a..ce23d4a3 100644 --- a/web/html/xui/form/protocol/vmess.html +++ b/web/html/xui/form/protocol/vmess.html @@ -1,5 +1,23 @@ {{define "form/vmess"}} + + + + + + + Ip Count Limit + + + + + + + + + From 20e7b9e02b02886d8855da00e53a15e56c16c34f Mon Sep 17 00:00:00 2001 From: Hossin Asaadi Date: Sat, 29 Oct 2022 11:49:27 -0400 Subject: [PATCH 03/28] add client ip limit job --- web/job/check_clinet_ip_job.go | 232 +++++++++++++++++++++++++++++++++ web/web.go | 4 + 2 files changed, 236 insertions(+) create mode 100644 web/job/check_clinet_ip_job.go diff --git a/web/job/check_clinet_ip_job.go b/web/job/check_clinet_ip_job.go new file mode 100644 index 00000000..2e922a19 --- /dev/null +++ b/web/job/check_clinet_ip_job.go @@ -0,0 +1,232 @@ +package job + +import ( + "x-ui/logger" + "x-ui/web/service" + "x-ui/database" + "x-ui/database/model" + "os" + ss "strings" + "regexp" + "encoding/json" + "gorm.io/gorm" + "strconv" + +) + +type CheckClientIpJob struct { + xrayService service.XrayService + inboundService service.InboundService +} + + +func NewCheckClientIpJob() *CheckClientIpJob { + return new(CheckClientIpJob) +} + +func (j *CheckClientIpJob) Run() { + processLogFile() +} + +func processLogFile() { + accessLogPath := GetAccessLogPath() + if(accessLogPath == "") { + logger.Warning("xray log not init in config.json") + return + } + + data, err := os.ReadFile(accessLogPath) + InboundClientIps := make(map[string][]string) + checkError(err) + + // clean log + if err := os.Truncate(GetAccessLogPath(), 0); err != nil { + checkError(err) + } + + lines := ss.Split(string(data), "\n") + for _, line := range lines { + ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`) + emailRegx, _ := regexp.Compile(`email:.+`) + + matchesIp := ipRegx.FindString(line) + if(len(matchesIp) > 0) { + ip := string(matchesIp) + if( ip == "127.0.0.1" || ip == "1.1.1.1") { + continue + } + + matchesEmail := emailRegx.FindString(line) + if(matchesEmail == "") { + continue + } + matchesEmail = ss.Split(matchesEmail, "email: ")[1] + + if(InboundClientIps[matchesEmail] != nil) { + if(contains(InboundClientIps[matchesEmail],ip)){ + continue + } + InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail],ip) + + + + }else{ + InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail],ip) + } + } + + } + for clientEmail, ips := range InboundClientIps { + inboundClientIps,err := GetInboundClientIps(clientEmail) + if(err != nil){ + addInboundClientIps(clientEmail,ips) + + }else{ + updateInboundClientIps(inboundClientIps,clientEmail,ips) + } + + } + + +} +func GetAccessLogPath() string { + + config, err := os.ReadFile("bin/config.json") + checkError(err) + + jsonConfig := map[string]interface{}{} + err = json.Unmarshal([]byte(config), &jsonConfig) + if err != nil { + logger.Warning(err) + } + if(jsonConfig["log"] != nil) { + jsonLog := jsonConfig["log"].(map[string]interface{}) + if(jsonLog["access"] != nil) { + + accessLogPath := jsonLog["access"].(string) + + return accessLogPath + } + } + return "" + +} +func checkError(e error) { + if e != nil { + logger.Warning("client ip job err:", e) + } +} +func contains(s []string, str string) bool { + for _, v := range s { + if v == str { + return true + } + } + + return false +} +// https://codereview.stackexchange.com/a/192954 +func Unique(slice []string) []string { + // create a map with all the values as key + uniqMap := make(map[string]struct{}) + for _, v := range slice { + uniqMap[v] = struct{}{} + } + + // turn the map keys into a slice + uniqSlice := make([]string, 0, len(uniqMap)) + for v := range uniqMap { + uniqSlice = append(uniqSlice, v) + } + return uniqSlice +} + +func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) { + db := database.GetDB() + InboundClientIps := &model.InboundClientIps{} + err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error + if err != nil { + return nil, err + } + return InboundClientIps, nil +} +func addInboundClientIps(clientEmail string,ips []string) error { + inboundClientIps := &model.InboundClientIps{} + jsonIps, err := json.Marshal(ips) + checkError(err) + + inboundClientIps.ClientEmail = clientEmail + inboundClientIps.Ips = string(jsonIps) + + + db := database.GetDB() + tx := db.Begin() + + defer func() { + if err == nil { + tx.Commit() + } else { + tx.Rollback() + } + }() + + err = tx.Save(inboundClientIps).Error + if err != nil { + return err + } + return nil +} +func updateInboundClientIps(inboundClientIps *model.InboundClientIps,clientEmail string,ips []string) error { + + var oldIps []string + err := json.Unmarshal([]byte(inboundClientIps.Ips), &oldIps) + mergedIps := Unique(append(oldIps, ips...)) + + jsonIps, err := json.Marshal(mergedIps) + checkError(err) + + inboundClientIps.ClientEmail = clientEmail + inboundClientIps.Ips = string(jsonIps) + + // check inbound limitation + inbound, _ := GetInboundByEmail(clientEmail) + + limitIpRegx, _ := regexp.Compile(`"limitIp": .+`) + + limitIpMactch := limitIpRegx.FindString(inbound.Settings) + limitIpMactch = ss.Split(limitIpMactch, `"limitIp": `)[1] + limitIp, err := strconv.Atoi(limitIpMactch) + + + if(limitIp < len(mergedIps) && limitIp != 0 && inbound.Enable) { + + DisableInbound(inbound.Id) + } + + db := database.GetDB() + err = db.Save(inboundClientIps).Error + if err != nil { + return err + } + return nil +} + +func GetInboundByEmail(clientEmail string) (*model.Inbound, error) { + db := database.GetDB() + var inbounds *model.Inbound + err := db.Model(model.Inbound{}).Where("settings LIKE ?", "%" + clientEmail + "%").Find(&inbounds).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + return inbounds, nil +} +func DisableInbound(id int) error{ + db := database.GetDB() + result := db.Model(model.Inbound{}). + Where("id = ? and enable = ?", id, true). + Update("enable", false) + err := result.Error + logger.Warning("disable inbound with id:",id) + + return err +} diff --git a/web/web.go b/web/web.go index 972292f0..60a69da1 100644 --- a/web/web.go +++ b/web/web.go @@ -295,6 +295,10 @@ func (s *Server) startTask() { // 每 30 秒检查一次 inbound 流量超出和到期的情况 s.cron.AddJob("@every 30s", job.NewCheckInboundJob()) + + // check client ips from log file every 1 min + s.cron.AddJob("@every 1m", job.NewCheckClientIpJob()) + // 每一天提示一次流量情况,上海时间8点30 var entry cron.EntryID isTgbotenabled, err := s.settingService.GetTgbotenabled() From 2722d2920bbb3a6d1cdf2572f0eccf2431de75db Mon Sep 17 00:00:00 2001 From: Hossin Asaadi Date: Sun, 30 Oct 2022 11:48:07 -0400 Subject: [PATCH 04/28] restart xray service after disable inbound --- web/job/check_clinet_ip_job.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/job/check_clinet_ip_job.go b/web/job/check_clinet_ip_job.go index 2e922a19..7e1cc4f7 100644 --- a/web/job/check_clinet_ip_job.go +++ b/web/job/check_clinet_ip_job.go @@ -18,10 +18,11 @@ type CheckClientIpJob struct { xrayService service.XrayService inboundService service.InboundService } - +var job *CheckClientIpJob func NewCheckClientIpJob() *CheckClientIpJob { - return new(CheckClientIpJob) + job = new(CheckClientIpJob) + return job } func (j *CheckClientIpJob) Run() { @@ -228,5 +229,9 @@ func DisableInbound(id int) error{ err := result.Error logger.Warning("disable inbound with id:",id) + if err == nil { + job.xrayService.SetToNeedRestart() + } + return err } From e0abaf5044305a056d44dda8ab1b0333f0c9c2d6 Mon Sep 17 00:00:00 2001 From: Hossin Asaadi Date: Sun, 30 Oct 2022 12:05:31 -0400 Subject: [PATCH 05/28] add ip log, clear ip log button in edit modal --- .gitignore | 2 ++ web/controller/inbound.go | 25 +++++++++++++++++++++++++ web/html/xui/form/protocol/vmess.html | 17 +++++++++++++++++ web/html/xui/inbound_modal.html | 27 +++++++++++++++++++++++++-- web/service/inbound.go | 24 ++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 1b5f0069..419da08b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ x-ui-*.tar.gz /release.sh .sync* main +release/ +access.log diff --git a/web/controller/inbound.go b/web/controller/inbound.go index 3cb3b6b6..c9922b3f 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -30,6 +30,11 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) { g.POST("/add", a.addInbound) g.POST("/del/:id", a.delInbound) g.POST("/update/:id", a.updateInbound) + + g.POST("/clientIps/:email", a.getClientIps) + g.POST("/clearClientIps/:email", a.clearClientIps) + + } func (a *InboundController) startTask() { @@ -106,3 +111,23 @@ func (a *InboundController) updateInbound(c *gin.Context) { a.xrayService.SetToNeedRestart() } } +func (a *InboundController) getClientIps(c *gin.Context) { + email := c.Param("email") + + ips , err := a.inboundService.GetInboundClientIps(email) + if err != nil { + jsonMsg(c, "修改", err) + return + } + jsonObj(c, ips, nil) +} +func (a *InboundController) clearClientIps(c *gin.Context) { + email := c.Param("email") + + err := a.inboundService.ClearClientIps(email) + if err != nil { + jsonMsg(c, "修改", err) + return + } + jsonMsg(c, "Log Cleared", nil) +} \ No newline at end of file diff --git a/web/html/xui/form/protocol/vmess.html b/web/html/xui/form/protocol/vmess.html index ce23d4a3..c161563f 100644 --- a/web/html/xui/form/protocol/vmess.html +++ b/web/html/xui/form/protocol/vmess.html @@ -17,6 +17,23 @@ + + + client ip log + + + + + + + + + + clear log + + diff --git a/web/html/xui/inbound_modal.html b/web/html/xui/inbound_modal.html index 050b4a00..1bf06a7c 100644 --- a/web/html/xui/inbound_modal.html +++ b/web/html/xui/inbound_modal.html @@ -14,6 +14,7 @@ confirm: null, inbound: new Inbound(), dbInbound: new DBInbound(), + clientIps: "", ok() { ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound); }, @@ -64,6 +65,9 @@ }, get dbInbound() { return inModal.dbInbound; + }, + get clientIps() { + return inModal.clientIps; } }, methods: { @@ -71,8 +75,27 @@ if (oldValue === 'kcp') { this.inModal.inbound.tls = false; } - } - } + }, + async getDBClientIps(email) { + + const msg = await HttpUtil.post('/xui/inbound/clientIps/'+ email); + if (!msg.success) { + return; + } + ips = JSON.parse(msg.obj) + ips = ips.join(",") + this.inModal.clientIps = ips + }, + async clearDBClientIps(email) { + const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email); + if (!msg.success) { + return; + } + this.inModal.clientIps = "" + }, + + }, + }); diff --git a/web/service/inbound.go b/web/service/inbound.go index 726b7ba0..d440cbce 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -176,3 +176,27 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) { count := result.RowsAffected return count, err } + +func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) { + db := database.GetDB() + InboundClientIps := &model.InboundClientIps{} + err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error + if err != nil { + return "", err + } + return InboundClientIps.Ips, nil +} +func (s *InboundService) ClearClientIps(clientEmail string) (error) { + db := database.GetDB() + + result := db.Model(model.InboundClientIps{}). + Where("client_email = ?", clientEmail). + Update("ips", "") + err := result.Error + + + if err != nil { + return err + } + return nil +} From beacad316857a8267a1cff13f2a042c87dfb357e Mon Sep 17 00:00:00 2001 From: Hossin Asaadi Date: Sun, 30 Oct 2022 14:24:33 -0400 Subject: [PATCH 06/28] remove merging new ips and old ones in ip log --- web/job/check_clinet_ip_job.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/web/job/check_clinet_ip_job.go b/web/job/check_clinet_ip_job.go index 7e1cc4f7..4e1d17fb 100644 --- a/web/job/check_clinet_ip_job.go +++ b/web/job/check_clinet_ip_job.go @@ -179,11 +179,7 @@ func addInboundClientIps(clientEmail string,ips []string) error { } func updateInboundClientIps(inboundClientIps *model.InboundClientIps,clientEmail string,ips []string) error { - var oldIps []string - err := json.Unmarshal([]byte(inboundClientIps.Ips), &oldIps) - mergedIps := Unique(append(oldIps, ips...)) - - jsonIps, err := json.Marshal(mergedIps) + jsonIps, err := json.Marshal(ips) checkError(err) inboundClientIps.ClientEmail = clientEmail @@ -199,7 +195,7 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps,clientEmail limitIp, err := strconv.Atoi(limitIpMactch) - if(limitIp < len(mergedIps) && limitIp != 0 && inbound.Enable) { + if(limitIp < len(ips) && limitIp != 0 && inbound.Enable) { DisableInbound(inbound.Id) } From 58327d5062e62ba9c28f5e42a0fcf6f2dee4a71b Mon Sep 17 00:00:00 2001 From: Hossin Asaadi Date: Sun, 30 Oct 2022 14:43:17 -0400 Subject: [PATCH 07/28] replace text and add condtion to field --- web/html/xui/form/protocol/vmess.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/html/xui/form/protocol/vmess.html b/web/html/xui/form/protocol/vmess.html index c161563f..381caa5b 100644 --- a/web/html/xui/form/protocol/vmess.html +++ b/web/html/xui/form/protocol/vmess.html @@ -6,7 +6,7 @@ - Ip Count Limit + IP Count Limit