diff --git a/go.mod b/go.mod index dc50288f..03d9c768 100644 --- a/go.mod +++ b/go.mod @@ -23,8 +23,11 @@ require ( require ( github.com/BurntSushi/toml v1.2.1 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect github.com/bytedance/sonic v1.8.7 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect + github.com/gaukas/godicttls v0.0.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -37,7 +40,9 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.5 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/pretty v0.1.0 // indirect github.com/leodido/go-urn v1.2.3 // indirect github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect github.com/mattn/go-isatty v0.0.18 // indirect @@ -46,11 +51,16 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pires/go-proxyproto v0.7.0 // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect + github.com/refraction-networking/utls v1.3.2 // indirect + github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect + github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect github.com/shoenig/go-m1cpu v0.1.5 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/numcpus v0.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect + github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect + github.com/xtls/reality v0.0.0-20230331223127-176a94313eda // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.8.0 // indirect @@ -58,5 +68,6 @@ require ( golang.org/x/sys v0.7.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e90a5b4c..c3fa9862 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/Workiva/go-datastructures v1.1.0 h1:hu20UpgZneBhQ3ZvwiOGlqJSKIosin2Rd5wAKUHEO/k= github.com/Workiva/go-datastructures v1.1.0/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= @@ -17,9 +18,12 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0= +github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk= +github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo= github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo= @@ -78,9 +82,15 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA= github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= @@ -123,13 +133,16 @@ github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc8 github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E= github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0= github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8= +github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= +github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk= github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw= github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo= github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U= +github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o= github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8= github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ= @@ -141,6 +154,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -161,7 +175,9 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= +github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= github.com/xtls/reality v0.0.0-20230331223127-176a94313eda h1:psRJD2RrZbnI0OWyHvXfgYCPqlRM5q5SPDcjDoDBWhE= +github.com/xtls/reality v0.0.0-20230331223127-176a94313eda/go.mod h1:rkuAY1S9F8eI8gDiPDYvACE8e2uwkyg8qoOTuwWov7Y= github.com/xtls/xray-core v1.8.1 h1:iSTTqXj82ZdwC1ah+eV331X4JTcnrDz+WuKuB/EB3P4= github.com/xtls/xray-core v1.8.1/go.mod h1:AXxSso0MZwUE4NhRocCfHCg73BtJ+T2dSpQVo1Cg9VM= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -237,13 +253,15 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.5.1 h1:hYyrLkAWE71bcarJDPdZNTLWtr8XrSjOWyjUYI6xdL4= diff --git a/web/controller/inbound.go b/web/controller/inbound.go index 1f5c0bd8..8ae55433 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -147,13 +147,15 @@ func (a *InboundController) addInboundClient(c *gin.Context) { return } - err = a.inboundService.AddInboundClient(data) + needRestart := false + + needRestart, err = a.inboundService.AddInboundClient(data) if err != nil { jsonMsg(c, "Something went wrong!", err) return } jsonMsg(c, "Client(s) added", nil) - if err == nil { + if err == nil && needRestart { a.xrayService.SetToNeedRestart() } } @@ -166,13 +168,15 @@ func (a *InboundController) delInboundClient(c *gin.Context) { } clientId := c.Param("clientId") - err = a.inboundService.DelInboundClient(id, clientId) + needRestart := false + + needRestart, err = a.inboundService.DelInboundClient(id, clientId) if err != nil { jsonMsg(c, "Something went wrong!", err) return } jsonMsg(c, "Client deleted", nil) - if err == nil { + if err == nil && needRestart { a.xrayService.SetToNeedRestart() } } @@ -187,13 +191,15 @@ func (a *InboundController) updateInboundClient(c *gin.Context) { return } - err = a.inboundService.UpdateInboundClient(inbound, clientId) + needRestart := false + + needRestart, err = a.inboundService.UpdateInboundClient(inbound, clientId) if err != nil { jsonMsg(c, "Something went wrong!", err) return } jsonMsg(c, "Client updated", nil) - if err == nil { + if err == nil && needRestart { a.xrayService.SetToNeedRestart() } } @@ -206,13 +212,15 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) { } email := c.Param("email") - err = a.inboundService.ResetClientTraffic(id, email) + needRestart := false + + needRestart, err = a.inboundService.ResetClientTraffic(id, email) if err != nil { jsonMsg(c, "Something went wrong!", err) return } jsonMsg(c, "traffic reseted", nil) - if err == nil { + if err == nil && needRestart { a.xrayService.SetToNeedRestart() } } @@ -222,6 +230,8 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) { if err != nil { jsonMsg(c, "Something went wrong!", err) return + } else { + a.xrayService.SetToNeedRestart() } jsonMsg(c, "All traffics reseted", nil) } @@ -237,6 +247,8 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) { if err != nil { jsonMsg(c, "Something went wrong!", err) return + } else { + a.xrayService.SetToNeedRestart() } jsonMsg(c, "All traffics of client reseted", nil) } diff --git a/web/job/check_inbound_job.go b/web/job/check_inbound_job.go index 2b24afb0..cb8bd331 100644 --- a/web/job/check_inbound_job.go +++ b/web/job/check_inbound_job.go @@ -15,19 +15,21 @@ func NewCheckInboundJob() *CheckInboundJob { } func (j *CheckInboundJob) Run() { - count, err := j.inboundService.DisableInvalidClients() + needRestart, count, err := j.inboundService.DisableInvalidClients() if err != nil { - logger.Warning("disable invalid Client err:", err) + logger.Warning("Error in disabling invalid clients:", err) } else if count > 0 { - logger.Debugf("disabled %v Client", count) - j.xrayService.SetToNeedRestart() + logger.Debugf("%v clients disabled", count) + if needRestart { + j.xrayService.SetToNeedRestart() + } } count, err = j.inboundService.DisableInvalidInbounds() if err != nil { - logger.Warning("disable invalid inbounds err:", err) + logger.Warning("Error in disabling invalid inbounds:", err) } else if count > 0 { - logger.Debugf("disabled %v inbounds", count) + logger.Debugf("%v inbounds disabled", count) j.xrayService.SetToNeedRestart() } } diff --git a/web/service/inbound.go b/web/service/inbound.go index 3c39a230..fbbb05a4 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -15,6 +15,7 @@ import ( ) type InboundService struct { + xrayApi xray.XrayAPI } func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { @@ -248,36 +249,36 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, return inbound, db.Save(oldInbound).Error } -func (s *InboundService) AddInboundClient(data *model.Inbound) error { +func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) { clients, err := s.GetClients(data) if err != nil { - return err + return false, err } var settings map[string]interface{} err = json.Unmarshal([]byte(data.Settings), &settings) if err != nil { - return err + return false, err } interfaceClients := settings["clients"].([]interface{}) existEmail, err := s.checkEmailsExistForClients(clients) if err != nil { - return err + return false, err } if existEmail != "" { - return common.NewError("Duplicate email:", existEmail) + return false, common.NewError("Duplicate email:", existEmail) } oldInbound, err := s.GetInbound(data.Id) if err != nil { - return err + return false, err } var oldSettings map[string]interface{} err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) if err != nil { - return err + return false, err } oldClients := oldSettings["clients"].([]interface{}) @@ -287,30 +288,48 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) error { newSettings, err := json.MarshalIndent(oldSettings, "", " ") if err != nil { - return err + return false, err } oldInbound.Settings = string(newSettings) + needRestart := false + s.xrayApi.Init(p.GetAPIPort()) for _, client := range clients { if len(client.Email) > 0 { s.AddClientStat(data.Id, &client) + err = s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]interface{}{ + "email": client.Email, + "id": client.ID, + "alterId": client.AlterIds, + "flow": client.Flow, + "password": client.Password, + }) + if err == nil { + logger.Debug("Client added by api:", client.Email) + } else { + needRestart = true + } + } else { + needRestart = true } } + s.xrayApi.Close() + db := database.GetDB() - return db.Save(oldInbound).Error + return needRestart, db.Save(oldInbound).Error } -func (s *InboundService) DelInboundClient(inboundId int, clientId string) error { +func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool, error) { oldInbound, err := s.GetInbound(inboundId) if err != nil { logger.Error("Load Old Data Error") - return err + return false, err } var settings map[string]interface{} err = json.Unmarshal([]byte(oldInbound.Settings), &settings) if err != nil { - return err + return false, err } email := "" @@ -337,7 +356,7 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error settings["clients"] = newClients newSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { - return err + return false, err } oldInbound.Settings = string(newSettings) @@ -346,33 +365,43 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error err = s.DelClientStat(db, email) if err != nil { logger.Error("Delete stats Data Error") - return err + return false, err } - return db.Save(oldInbound).Error + needRestart := true + s.xrayApi.Init(p.GetAPIPort()) + if len(email) > 0 { + err = s.xrayApi.RemoveUser(oldInbound.Tag, email) + if err == nil { + logger.Debug("Client deleted by api:", email) + needRestart = false + } + } + s.xrayApi.Close() + return needRestart, db.Save(oldInbound).Error } -func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) error { +func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) (bool, error) { clients, err := s.GetClients(data) if err != nil { - return err + return false, err } var settings map[string]interface{} err = json.Unmarshal([]byte(data.Settings), &settings) if err != nil { - return err + return false, err } inerfaceClients := settings["clients"].([]interface{}) oldInbound, err := s.GetInbound(data.Id) if err != nil { - return err + return false, err } oldClients, err := s.GetClients(oldInbound) if err != nil { - return err + return false, err } oldEmail := "" @@ -396,17 +425,17 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin if len(clients[0].Email) > 0 && clients[0].Email != oldEmail { existEmail, err := s.checkEmailsExistForClients(clients) if err != nil { - return err + return false, err } if existEmail != "" { - return common.NewError("Duplicate email:", existEmail) + return false, common.NewError("Duplicate email:", existEmail) } } var oldSettings map[string]interface{} err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) if err != nil { - return err + return false, err } settingsClients := oldSettings["clients"].([]interface{}) settingsClients[clientIndex] = inerfaceClients[0] @@ -414,7 +443,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin newSettings, err := json.MarshalIndent(oldSettings, "", " ") if err != nil { - return err + return false, err } oldInbound.Settings = string(newSettings) @@ -424,7 +453,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin if len(oldEmail) > 0 { err = s.UpdateClientStat(oldEmail, &clients[0]) if err != nil { - return err + return false, err } } else { s.AddClientStat(data.Id, &clients[0]) @@ -432,10 +461,32 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin } else { err = s.DelClientStat(db, oldEmail) if err != nil { - return err + return false, err } } - return db.Save(oldInbound).Error + needRestart := true + s.xrayApi.Init(p.GetAPIPort()) + if len(oldEmail) > 0 { + s.xrayApi.RemoveUser(oldInbound.Tag, oldEmail) + if clients[0].Enable { + err = s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]interface{}{ + "email": clients[0].Email, + "id": clients[0].ID, + "alterId": clients[0].AlterIds, + "flow": clients[0].Flow, + "password": clients[0].Password, + }) + if err == nil { + logger.Debug("Client edited by api:", clients[0].Email) + needRestart = false + } + } else { + logger.Debug("Client disabled by api:", clients[0].Email) + needRestart = false + } + } + s.xrayApi.Close() + return needRestart, db.Save(oldInbound).Error } func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error { @@ -461,6 +512,7 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error { return err } + func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) { if len(traffics) == 0 { return nil @@ -573,15 +625,42 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) { return count, err } -func (s *InboundService) DisableInvalidClients() (int64, error) { +func (s *InboundService) DisableInvalidClients() (bool, int64, error) { db := database.GetDB() now := time.Now().Unix() * 1000 + needRestart := false + + if p != nil { + var results []struct { + Tag string + Email string + } + + err := db.Table("inbounds"). + Select("inbounds.tag, client_traffics.email"). + Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id"). + Where("((client_traffics.total > 0 AND client_traffics.up + client_traffics.down >= client_traffics.total) OR (client_traffics.expiry_time > 0 AND client_traffics.expiry_time <= ?)) AND client_traffics.enable = ?", now, true). + Scan(&results).Error + if err != nil { + return false, 0, err + } + s.xrayApi.Init(p.GetAPIPort()) + for _, result := range results { + err = s.xrayApi.RemoveUser(result.Tag, result.Email) + if err == nil { + logger.Debug("Client deleted by api:", result.Email) + } else { + needRestart = true + } + } + s.xrayApi.Close() + } 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 + return needRestart, count, err } func (s *InboundService) MigrationRemoveOrphanedTraffics() { @@ -635,19 +714,55 @@ func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error { return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error } -func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error { - db := database.GetDB() - - result := db.Model(xray.ClientTraffic{}). - Where("inbound_id = ? and email = ?", id, clientEmail). - Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0}) - - err := result.Error +func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, error) { + needRestart := false + traffic, err := s.GetClientTrafficByEmail(clientEmail) if err != nil { - return err + return false, err } - return nil + + if !traffic.Enable { + inbound, err := s.GetInbound(id) + if err != nil { + return false, err + } + clients, err := s.GetClients(inbound) + if err != nil { + return false, err + } + for _, client := range clients { + if client.Email == clientEmail { + s.xrayApi.Init(p.GetAPIPort()) + err = s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]interface{}{ + "email": client.Email, + "id": client.ID, + "alterId": client.AlterIds, + "flow": client.Flow, + "password": client.Password, + }) + if err == nil { + logger.Debug("Client enabled due to reset traffic:", clientEmail) + } else { + needRestart = true + } + s.xrayApi.Close() + break + } + } + } + + traffic.Up = 0 + traffic.Down = 0 + traffic.Enable = true + + db := database.GetDB() + err = db.Save(traffic).Error + if err != nil { + return false, err + } + + return needRestart, nil } func (s *InboundService) ResetAllClientTraffics(id int) error { diff --git a/web/service/xray.go b/web/service/xray.go index bcc886fe..0ac1c2f2 100644 --- a/web/service/xray.go +++ b/web/service/xray.go @@ -18,6 +18,7 @@ var result string type XrayService struct { inboundService InboundService settingService SettingService + xrayAPI xray.XrayAPI } func (s *XrayService) IsXrayRunning() bool { @@ -143,7 +144,9 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, if !s.IsXrayRunning() { return nil, nil, errors.New("xray is not running") } - return p.GetTraffic(true) + s.xrayAPI.Init(p.GetAPIPort()) + defer s.xrayAPI.Close() + return s.xrayAPI.GetTraffic(true) } func (s *XrayService) RestartXray(isForce bool) error { @@ -166,7 +169,11 @@ func (s *XrayService) RestartXray(isForce bool) error { p = xray.NewProcess(xrayConfig) result = "" - return p.Start() + err = p.Start() + if err != nil { + return err + } + return nil } func (s *XrayService) StopXray() error { diff --git a/xray/api.go b/xray/api.go new file mode 100644 index 00000000..68ac8be3 --- /dev/null +++ b/xray/api.go @@ -0,0 +1,181 @@ +package xray + +import ( + "context" + "fmt" + "regexp" + "time" + "x-ui/util/common" + + "github.com/xtls/xray-core/app/proxyman/command" + statsService "github.com/xtls/xray-core/app/stats/command" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/proxy/shadowsocks" + "github.com/xtls/xray-core/proxy/trojan" + "github.com/xtls/xray-core/proxy/vless" + "github.com/xtls/xray-core/proxy/vmess" + "google.golang.org/grpc" +) + +type XrayAPI struct { + HandlerServiceClient *command.HandlerServiceClient + StatsServiceClient *statsService.StatsServiceClient + grpcClient *grpc.ClientConn + isConnected bool +} + +func (x *XrayAPI) Init(apiPort int) (err error) { + if apiPort == 0 { + return common.NewError("xray api port wrong:", apiPort) + } + x.grpcClient, err = grpc.Dial(fmt.Sprintf("127.0.0.1:%v", apiPort), grpc.WithInsecure()) + if err != nil { + return err + } + x.isConnected = true + + hsClient := command.NewHandlerServiceClient(x.grpcClient) + ssClient := statsService.NewStatsServiceClient(x.grpcClient) + + x.HandlerServiceClient = &hsClient + x.StatsServiceClient = &ssClient + + return +} + +func (x *XrayAPI) Close() { + x.grpcClient.Close() + x.HandlerServiceClient = nil + x.StatsServiceClient = nil + x.isConnected = false +} + +func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]interface{}) error { + var account *serial.TypedMessage + switch Protocol { + case "vmess": + account = serial.ToTypedMessage(&vmess.Account{ + Id: user["id"].(string), + AlterId: uint32(user["alterId"].(uint16)), + }) + case "vless": + account = serial.ToTypedMessage(&vless.Account{ + Id: user["id"].(string), + Flow: user["flow"].(string), + }) + case "trojan": + account = serial.ToTypedMessage(&trojan.Account{ + Password: user["password"].(string), + }) + case "shadowsocks": + account = serial.ToTypedMessage(&shadowsocks.Account{ + Password: user["password"].(string), + }) + default: + return nil + } + + client := *x.HandlerServiceClient + + _, err := client.AlterInbound(context.Background(), &command.AlterInboundRequest{ + Tag: inboundTag, + Operation: serial.ToTypedMessage(&command.AddUserOperation{ + User: &protocol.User{ + Email: user["email"].(string), + Account: account, + }, + }), + }) + return err +} + +func (x *XrayAPI) RemoveUser(inboundTag string, email string) error { + client := *x.HandlerServiceClient + _, err := client.AlterInbound(context.Background(), &command.AlterInboundRequest{ + Tag: inboundTag, + Operation: serial.ToTypedMessage(&command.RemoveUserOperation{ + Email: email, + }), + }) + return err +} + +func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) { + if x.grpcClient == nil { + return nil, nil, common.NewError("xray api is not initialized") + } + var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)") + var ClientTrafficRegex = regexp.MustCompile("(user)>>>([^>]+)>>>traffic>>>(downlink|uplink)") + + client := *x.StatsServiceClient + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + request := &statsService.QueryStatsRequest{ + Reset_: reset, + } + resp, err := client.QueryStats(ctx, request) + if err != nil { + return nil, nil, err + } + tagTrafficMap := map[string]*Traffic{} + emailTrafficMap := map[string]*ClientTraffic{} + + clientTraffics := make([]*ClientTraffic, 0) + traffics := make([]*Traffic, 0) + for _, stat := range resp.GetStat() { + matchs := trafficRegex.FindStringSubmatch(stat.Name) + if len(matchs) < 3 { + + matchs := ClientTrafficRegex.FindStringSubmatch(stat.Name) + if len(matchs) < 3 { + continue + } else { + + isUser := matchs[1] == "user" + email := matchs[2] + isDown := matchs[3] == "downlink" + if !isUser { + continue + } + traffic, ok := emailTrafficMap[email] + if !ok { + traffic = &ClientTraffic{ + Email: email, + } + emailTrafficMap[email] = traffic + clientTraffics = append(clientTraffics, traffic) + } + if isDown { + traffic.Down = stat.Value + } else { + traffic.Up = stat.Value + } + + } + continue + } + isInbound := matchs[1] == "inbound" + tag := matchs[2] + isDown := matchs[3] == "downlink" + if tag == "api" { + continue + } + traffic, ok := tagTrafficMap[tag] + if !ok { + traffic = &Traffic{ + IsInbound: isInbound, + Tag: tag, + } + tagTrafficMap[tag] = traffic + traffics = append(traffics, traffic) + } + if isDown { + traffic.Down = stat.Value + } else { + traffic.Up = stat.Value + } + } + + return traffics, clientTraffics, nil +} diff --git a/xray/process.go b/xray/process.go index be90331f..afc083c6 100644 --- a/xray/process.go +++ b/xray/process.go @@ -3,28 +3,20 @@ package xray import ( "bufio" "bytes" - "context" "encoding/json" "errors" "fmt" "io/fs" "os" "os/exec" - "regexp" "runtime" "strings" - "time" "x-ui/config" "x-ui/util/common" "github.com/Workiva/go-datastructures/queue" - statsservice "github.com/xtls/xray-core/app/stats/command" - "google.golang.org/grpc" ) -var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)") -var ClientTrafficRegex = regexp.MustCompile("(user)>>>([^>]+)>>>traffic>>>(downlink|uplink)") - func GetBinaryName() string { return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH) } @@ -230,85 +222,3 @@ func (p *process) Stop() error { } return p.cmd.Process.Kill() } - -func (p *process) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) { - if p.apiPort == 0 { - return nil, nil, common.NewError("xray api port wrong:", p.apiPort) - } - conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%v", p.apiPort), grpc.WithInsecure()) - if err != nil { - return nil, nil, err - } - defer conn.Close() - - client := statsservice.NewStatsServiceClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - request := &statsservice.QueryStatsRequest{ - Reset_: reset, - } - resp, err := client.QueryStats(ctx, request) - if err != nil { - return nil, nil, err - } - tagTrafficMap := map[string]*Traffic{} - emailTrafficMap := map[string]*ClientTraffic{} - - clientTraffics := make([]*ClientTraffic, 0) - traffics := make([]*Traffic, 0) - for _, stat := range resp.GetStat() { - matchs := trafficRegex.FindStringSubmatch(stat.Name) - if len(matchs) < 3 { - - matchs := ClientTrafficRegex.FindStringSubmatch(stat.Name) - if len(matchs) < 3 { - continue - } else { - - isUser := matchs[1] == "user" - email := matchs[2] - isDown := matchs[3] == "downlink" - if !isUser { - continue - } - traffic, ok := emailTrafficMap[email] - if !ok { - traffic = &ClientTraffic{ - Email: email, - } - emailTrafficMap[email] = traffic - clientTraffics = append(clientTraffics, traffic) - } - if isDown { - traffic.Down = stat.Value - } else { - traffic.Up = stat.Value - } - - } - continue - } - isInbound := matchs[1] == "inbound" - tag := matchs[2] - isDown := matchs[3] == "downlink" - if tag == "api" { - continue - } - traffic, ok := tagTrafficMap[tag] - if !ok { - traffic = &Traffic{ - IsInbound: isInbound, - Tag: tag, - } - tagTrafficMap[tag] = traffic - traffics = append(traffics, traffic) - } - if isDown { - traffic.Down = stat.Value - } else { - traffic.Up = stat.Value - } - } - - return traffics, clientTraffics, nil -}