>6]+i[128|63&n]:n<55296||57344<=n?t+=i[224|n>>12]+i[128|n>>6&63]+i[128|63&n]:(o+=1,n=65536+((1023&n)<<10|1023&r.charCodeAt(o)),t+=i[240|n>>18]+i[128|n>>12&63]+i[128|n>>6&63]+i[128|63&n])}return t},isBuffer:function(e){return null!=e&&!!(e.constructor&&e.constructor.isBuffer&&e.constructor.isBuffer(e))},isRegExp:function(e){return"[object RegExp]"===Object.prototype.toString.call(e)},merge:function o(t,n,i){if(!n)return t;if("object"!=typeof n){if(Array.isArray(t))t.push(n);else{if("object"!=typeof t)return[t,n];(i.plainObjects||i.allowPrototypes||!a.call(Object.prototype,n))&&(t[n]=!0)}return t}if("object"!=typeof t)return[t].concat(n);var e=t;return Array.isArray(t)&&!Array.isArray(n)&&(e=l(t,i)),Array.isArray(t)&&Array.isArray(n)?(n.forEach(function(e,r){a.call(t,r)?t[r]&&"object"==typeof t[r]?t[r]=o(t[r],e,i):t.push(e):t[r]=e}),t):Object.keys(n).reduce(function(e,r){var t=n[r];return a.call(e,r)?e[r]=o(e[r],t,i):e[r]=t,e},e)}}},{}]},{},[2])(2)});
--------------------------------------------------------------------------------
/web/assets/vue@2.6.12/vue.common.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./vue.common.prod.js')
3 | } else {
4 | module.exports = require('./vue.common.dev.js')
5 | }
6 |
--------------------------------------------------------------------------------
/web/assets/vue@2.6.12/vue.runtime.common.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./vue.runtime.common.prod.js')
3 | } else {
4 | module.exports = require('./vue.runtime.common.dev.js')
5 | }
6 |
--------------------------------------------------------------------------------
/web/controller/api.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | )
6 | type APIController struct {
7 | BaseController
8 |
9 | inboundController *InboundController
10 | settingController *SettingController
11 | }
12 |
13 | func NewAPIController(g *gin.RouterGroup) *APIController {
14 | a := &APIController{}
15 | a.initRouter(g)
16 | return a
17 | }
18 |
19 | func (a *APIController) initRouter(g *gin.RouterGroup) {
20 | g = g.Group("/xui/API/inbounds")
21 | g.Use(a.checkLogin)
22 |
23 | g.GET("/", a.inbounds)
24 | g.GET("/get/:id", a.inbound)
25 | g.POST("/add", a.addInbound)
26 | g.POST("/del/:id", a.delInbound)
27 | g.POST("/update/:id", a.updateInbound)
28 |
29 |
30 | a.inboundController = NewInboundController(g)
31 | }
32 |
33 |
34 | func (a *APIController) inbounds(c *gin.Context) {
35 | a.inboundController.getInbounds(c)
36 | }
37 | func (a *APIController) inbound(c *gin.Context) {
38 | a.inboundController.getInbound(c)
39 | }
40 | func (a *APIController) addInbound(c *gin.Context) {
41 | a.inboundController.addInbound(c)
42 | }
43 | func (a *APIController) delInbound(c *gin.Context) {
44 | a.inboundController.delInbound(c)
45 | }
46 | func (a *APIController) updateInbound(c *gin.Context) {
47 | a.inboundController.updateInbound(c)
48 | }
49 |
--------------------------------------------------------------------------------
/web/controller/base.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | "x-ui/web/session"
7 | )
8 |
9 | type BaseController struct {
10 | }
11 |
12 | func (a *BaseController) checkLogin(c *gin.Context) {
13 | if !session.IsLogin(c) {
14 | if isAjax(c) {
15 | pureJsonMsg(c, false, I18n(c , "pages.login.loginAgain"))
16 | } else {
17 | c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
18 | }
19 | c.Abort()
20 | } else {
21 | c.Next()
22 | }
23 | }
24 |
25 |
26 | func I18n(c *gin.Context , name string) string{
27 | anyfunc, _ := c.Get("I18n")
28 | i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
29 |
30 | message, _ := i18n(name)
31 |
32 | return message;
33 | }
34 |
--------------------------------------------------------------------------------
/web/controller/inbound.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "fmt"
5 | "github.com/gin-gonic/gin"
6 | "strconv"
7 | "x-ui/database/model"
8 | "x-ui/logger"
9 | "x-ui/web/global"
10 | "x-ui/web/service"
11 | "x-ui/web/session"
12 | )
13 |
14 | type InboundController struct {
15 | inboundService service.InboundService
16 | xrayService service.XrayService
17 | }
18 |
19 | func NewInboundController(g *gin.RouterGroup) *InboundController {
20 | a := &InboundController{}
21 | a.initRouter(g)
22 | a.startTask()
23 | return a
24 | }
25 |
26 | func (a *InboundController) initRouter(g *gin.RouterGroup) {
27 | g = g.Group("/inbound")
28 |
29 | g.POST("/list", a.getInbounds)
30 | g.POST("/add", a.addInbound)
31 | g.POST("/del/:id", a.delInbound)
32 | g.POST("/update/:id", a.updateInbound)
33 |
34 | g.POST("/clientIps/:email", a.getClientIps)
35 | g.POST("/clearClientIps/:email", a.clearClientIps)
36 | g.POST("/resetClientTraffic/:email", a.resetClientTraffic)
37 |
38 |
39 | }
40 |
41 | func (a *InboundController) startTask() {
42 | webServer := global.GetWebServer()
43 | c := webServer.GetCron()
44 | c.AddFunc("@every 10s", func() {
45 | if a.xrayService.IsNeedRestartAndSetFalse() {
46 | err := a.xrayService.RestartXray(false)
47 | if err != nil {
48 | logger.Error("restart xray failed:", err)
49 | }
50 | }
51 | })
52 | }
53 |
54 | func (a *InboundController) getInbounds(c *gin.Context) {
55 | user := session.GetLoginUser(c)
56 | inbounds, err := a.inboundService.GetInbounds(user.Id)
57 | if err != nil {
58 | jsonMsg(c, I18n(c , "pages.inbounds.toasts.obtain"), err)
59 | return
60 | }
61 | jsonObj(c, inbounds, nil)
62 | }
63 | func (a *InboundController) getInbound(c *gin.Context) {
64 | id, err := strconv.Atoi(c.Param("id"))
65 | if err != nil {
66 | jsonMsg(c, I18n(c , "get"), err)
67 | return
68 | }
69 | inbound, err := a.inboundService.GetInbound(id)
70 | if err != nil {
71 | jsonMsg(c, I18n(c , "pages.inbounds.toasts.obtain"), err)
72 | return
73 | }
74 | jsonObj(c, inbound, nil)
75 | }
76 |
77 | func (a *InboundController) addInbound(c *gin.Context) {
78 | inbound := &model.Inbound{}
79 | err := c.ShouldBind(inbound)
80 | if err != nil {
81 | jsonMsg(c, I18n(c , "pages.inbounds.addTo"), err)
82 | return
83 | }
84 | user := session.GetLoginUser(c)
85 | inbound.UserId = user.Id
86 | inbound.Enable = true
87 | inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
88 | inbound, err = a.inboundService.AddInbound(inbound)
89 | jsonMsgObj(c, I18n(c , "pages.inbounds.addTo"), inbound, err)
90 | if err == nil {
91 | a.xrayService.SetToNeedRestart()
92 | }
93 | }
94 |
95 | func (a *InboundController) delInbound(c *gin.Context) {
96 | id, err := strconv.Atoi(c.Param("id"))
97 | if err != nil {
98 | jsonMsg(c, I18n(c , "delete"), err)
99 | return
100 | }
101 | err = a.inboundService.DelInbound(id)
102 | jsonMsgObj(c, I18n(c , "delete"), id, err)
103 | if err == nil {
104 | a.xrayService.SetToNeedRestart()
105 | }
106 | }
107 |
108 | func (a *InboundController) updateInbound(c *gin.Context) {
109 | id, err := strconv.Atoi(c.Param("id"))
110 | if err != nil {
111 | jsonMsg(c, I18n(c , "pages.inbounds.revise"), err)
112 | return
113 | }
114 | inbound := &model.Inbound{
115 | Id: id,
116 | }
117 | err = c.ShouldBind(inbound)
118 | if err != nil {
119 | jsonMsg(c, I18n(c , "pages.inbounds.revise"), err)
120 | return
121 | }
122 | inbound, err = a.inboundService.UpdateInbound(inbound)
123 | jsonMsgObj(c, I18n(c , "pages.inbounds.revise"), inbound, err)
124 | if err == nil {
125 | a.xrayService.SetToNeedRestart()
126 | }
127 | }
128 | func (a *InboundController) getClientIps(c *gin.Context) {
129 | email := c.Param("email")
130 |
131 | ips , err := a.inboundService.GetInboundClientIps(email)
132 | if err != nil {
133 | jsonObj(c, "No IP Record", nil)
134 | return
135 | }
136 | jsonObj(c, ips, nil)
137 | }
138 | func (a *InboundController) clearClientIps(c *gin.Context) {
139 | email := c.Param("email")
140 |
141 | err := a.inboundService.ClearClientIps(email)
142 | if err != nil {
143 | jsonMsg(c, "修改", err)
144 | return
145 | }
146 | jsonMsg(c, "Log Cleared", nil)
147 | }
148 | func (a *InboundController) resetClientTraffic(c *gin.Context) {
149 | email := c.Param("email")
150 |
151 | err := a.inboundService.ResetClientTraffic(email)
152 | if err != nil {
153 | jsonMsg(c, "something worng!", err)
154 | return
155 | }
156 | jsonMsg(c, "traffic reseted", nil)
157 | }
158 |
--------------------------------------------------------------------------------
/web/controller/index.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 | "time"
6 | "x-ui/logger"
7 | "x-ui/web/job"
8 | "x-ui/web/service"
9 | "x-ui/web/session"
10 |
11 | "github.com/gin-gonic/gin"
12 | )
13 |
14 | type LoginForm struct {
15 | Username string `json:"username" form:"username"`
16 | Password string `json:"password" form:"password"`
17 | }
18 |
19 | type IndexController struct {
20 | BaseController
21 |
22 | userService service.UserService
23 | }
24 |
25 | func NewIndexController(g *gin.RouterGroup) *IndexController {
26 | a := &IndexController{}
27 | a.initRouter(g)
28 | return a
29 | }
30 |
31 | func (a *IndexController) initRouter(g *gin.RouterGroup) {
32 | g.GET("/", a.index)
33 | g.POST("/login", a.login)
34 | g.GET("/logout", a.logout)
35 | }
36 |
37 | func (a *IndexController) index(c *gin.Context) {
38 | if session.IsLogin(c) {
39 | c.Redirect(http.StatusTemporaryRedirect, "xui/")
40 | return
41 | }
42 | html(c, "login.html", "pages.login.title", nil)
43 | }
44 |
45 | func (a *IndexController) login(c *gin.Context) {
46 | var form LoginForm
47 | err := c.ShouldBind(&form)
48 | if err != nil {
49 | pureJsonMsg(c, false, I18n(c , "pages.login.toasts.invalidFormData"))
50 | return
51 | }
52 | if form.Username == "" {
53 | pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyUsername"))
54 | return
55 | }
56 | if form.Password == "" {
57 | pureJsonMsg(c, false, I18n(c , "pages.login.toasts.emptyPassword"))
58 | return
59 | }
60 | user := a.userService.CheckUser(form.Username, form.Password)
61 | timeStr := time.Now().Format("2006-01-02 15:04:05")
62 | if user == nil {
63 | job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
64 | logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
65 | pureJsonMsg(c, false, I18n(c , "pages.login.toasts.wrongUsernameOrPassword"))
66 | return
67 | } else {
68 | logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
69 | job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
70 | }
71 |
72 | err = session.SetLoginUser(c, user)
73 | logger.Info("user", user.Id, "login success")
74 | jsonMsg(c, I18n(c , "pages.login.toasts.successLogin"), err)
75 | }
76 |
77 | func (a *IndexController) logout(c *gin.Context) {
78 | user := session.GetLoginUser(c)
79 | if user != nil {
80 | logger.Info("user", user.Id, "logout")
81 | }
82 | session.ClearSession(c)
83 | c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
84 | }
85 |
--------------------------------------------------------------------------------
/web/controller/server.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "time"
6 | "x-ui/web/global"
7 | "x-ui/web/service"
8 | )
9 |
10 | type ServerController struct {
11 | BaseController
12 |
13 | serverService service.ServerService
14 |
15 | lastStatus *service.Status
16 | lastGetStatusTime time.Time
17 |
18 | lastVersions []string
19 | lastGetVersionsTime time.Time
20 | }
21 |
22 | func NewServerController(g *gin.RouterGroup) *ServerController {
23 | a := &ServerController{
24 | lastGetStatusTime: time.Now(),
25 | }
26 | a.initRouter(g)
27 | a.startTask()
28 | return a
29 | }
30 |
31 | func (a *ServerController) initRouter(g *gin.RouterGroup) {
32 | g = g.Group("/server")
33 |
34 | g.Use(a.checkLogin)
35 | g.POST("/status", a.status)
36 | g.POST("/getXrayVersion", a.getXrayVersion)
37 | g.POST("/installXray/:version", a.installXray)
38 | }
39 |
40 | func (a *ServerController) refreshStatus() {
41 | a.lastStatus = a.serverService.GetStatus(a.lastStatus)
42 | }
43 |
44 | func (a *ServerController) startTask() {
45 | webServer := global.GetWebServer()
46 | c := webServer.GetCron()
47 | c.AddFunc("@every 2s", func() {
48 | now := time.Now()
49 | if now.Sub(a.lastGetStatusTime) > time.Minute*3 {
50 | return
51 | }
52 | a.refreshStatus()
53 | })
54 | }
55 |
56 | func (a *ServerController) status(c *gin.Context) {
57 | a.lastGetStatusTime = time.Now()
58 |
59 | jsonObj(c, a.lastStatus, nil)
60 | }
61 |
62 | func (a *ServerController) getXrayVersion(c *gin.Context) {
63 | now := time.Now()
64 | if now.Sub(a.lastGetVersionsTime) <= time.Minute {
65 | jsonObj(c, a.lastVersions, nil)
66 | return
67 | }
68 |
69 | versions, err := a.serverService.GetXrayVersions()
70 | if err != nil {
71 | jsonMsg(c, I18n(c , "getVersion"), err)
72 | return
73 | }
74 |
75 | a.lastVersions = versions
76 | a.lastGetVersionsTime = time.Now()
77 |
78 | jsonObj(c, versions, nil)
79 | }
80 |
81 | func (a *ServerController) installXray(c *gin.Context) {
82 | version := c.Param("version")
83 | err := a.serverService.UpdateXray(version)
84 | jsonMsg(c, I18n(c , "install") + " xray", err)
85 | }
86 |
--------------------------------------------------------------------------------
/web/controller/setting.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "errors"
5 | "github.com/gin-gonic/gin"
6 | "time"
7 | "x-ui/web/entity"
8 | "x-ui/web/service"
9 | "x-ui/web/session"
10 | )
11 |
12 | type updateUserForm struct {
13 | OldUsername string `json:"oldUsername" form:"oldUsername"`
14 | OldPassword string `json:"oldPassword" form:"oldPassword"`
15 | NewUsername string `json:"newUsername" form:"newUsername"`
16 | NewPassword string `json:"newPassword" form:"newPassword"`
17 | }
18 |
19 | type SettingController struct {
20 | settingService service.SettingService
21 | userService service.UserService
22 | panelService service.PanelService
23 | }
24 |
25 | func NewSettingController(g *gin.RouterGroup) *SettingController {
26 | a := &SettingController{}
27 | a.initRouter(g)
28 | return a
29 | }
30 |
31 | func (a *SettingController) initRouter(g *gin.RouterGroup) {
32 | g = g.Group("/setting")
33 |
34 | g.POST("/all", a.getAllSetting)
35 | g.POST("/update", a.updateSetting)
36 | g.POST("/updateUser", a.updateUser)
37 | g.POST("/restartPanel", a.restartPanel)
38 | }
39 |
40 | func (a *SettingController) getAllSetting(c *gin.Context) {
41 | allSetting, err := a.settingService.GetAllSetting()
42 | if err != nil {
43 | jsonMsg(c, I18n(c , "pages.setting.toasts.getSetting"), err)
44 | return
45 | }
46 | jsonObj(c, allSetting, nil)
47 | }
48 |
49 | func (a *SettingController) updateSetting(c *gin.Context) {
50 | allSetting := &entity.AllSetting{}
51 | err := c.ShouldBind(allSetting)
52 | if err != nil {
53 | jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
54 | return
55 | }
56 | err = a.settingService.UpdateAllSetting(allSetting)
57 | jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
58 | }
59 |
60 | func (a *SettingController) updateUser(c *gin.Context) {
61 | form := &updateUserForm{}
62 | err := c.ShouldBind(form)
63 | if err != nil {
64 | jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
65 | return
66 | }
67 | user := session.GetLoginUser(c)
68 | if user.Username != form.OldUsername || user.Password != form.OldPassword {
69 | jsonMsg(c, I18n(c , "pages.setting.toasts.modifyUser"), errors.New(I18n(c , "pages.setting.toasts.originalUserPassIncorrect")))
70 | return
71 | }
72 | if form.NewUsername == "" || form.NewPassword == "" {
73 | jsonMsg(c,I18n(c , "pages.setting.toasts.modifyUser"), errors.New(I18n(c , "pages.setting.toasts.userPassMustBeNotEmpty")))
74 | return
75 | }
76 | err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
77 | if err == nil {
78 | user.Username = form.NewUsername
79 | user.Password = form.NewPassword
80 | session.SetLoginUser(c, user)
81 | }
82 | jsonMsg(c, I18n(c , "pages.setting.toasts.modifyUser"), err)
83 | }
84 |
85 | func (a *SettingController) restartPanel(c *gin.Context) {
86 | err := a.panelService.RestartPanel(time.Second * 3)
87 | jsonMsg(c, I18n(c , "pages.setting.restartPanel"), err)
88 | }
89 |
--------------------------------------------------------------------------------
/web/controller/util.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net"
6 | "net/http"
7 | "strings"
8 | "x-ui/config"
9 | "x-ui/logger"
10 | "x-ui/web/entity"
11 | )
12 |
13 | func getUriId(c *gin.Context) int64 {
14 | s := struct {
15 | Id int64 `uri:"id"`
16 | }{}
17 |
18 | _ = c.BindUri(&s)
19 | return s.Id
20 | }
21 |
22 | func getRemoteIp(c *gin.Context) string {
23 | value := c.GetHeader("X-Forwarded-For")
24 | if value != "" {
25 | ips := strings.Split(value, ",")
26 | return ips[0]
27 | } else {
28 | addr := c.Request.RemoteAddr
29 | ip, _, _ := net.SplitHostPort(addr)
30 | return ip
31 | }
32 | }
33 |
34 | func jsonMsg(c *gin.Context, msg string, err error) {
35 | jsonMsgObj(c, msg, nil, err)
36 | }
37 |
38 | func jsonObj(c *gin.Context, obj interface{}, err error) {
39 | jsonMsgObj(c, "", obj, err)
40 | }
41 |
42 | func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
43 | m := entity.Msg{
44 | Obj: obj,
45 | }
46 | if err == nil {
47 | m.Success = true
48 | if msg != "" {
49 | m.Msg = msg + I18n(c , "success")
50 | }
51 | } else {
52 | m.Success = false
53 | m.Msg = msg + I18n(c , "fail") + ": " + err.Error()
54 | logger.Warning(msg + I18n(c , "fail") + ": ", err)
55 | }
56 | c.JSON(http.StatusOK, m)
57 | }
58 |
59 | func pureJsonMsg(c *gin.Context, success bool, msg string) {
60 | if success {
61 | c.JSON(http.StatusOK, entity.Msg{
62 | Success: true,
63 | Msg: msg,
64 | })
65 | } else {
66 | c.JSON(http.StatusOK, entity.Msg{
67 | Success: false,
68 | Msg: msg,
69 | })
70 | }
71 | }
72 |
73 | func html(c *gin.Context, name string, title string, data gin.H) {
74 | if data == nil {
75 | data = gin.H{}
76 | }
77 | data["title"] = title
78 | data["request_uri"] = c.Request.RequestURI
79 | data["base_path"] = c.GetString("base_path")
80 | c.HTML(http.StatusOK, name, getContext(data))
81 | }
82 |
83 | func getContext(h gin.H) gin.H {
84 | a := gin.H{
85 | "cur_ver": config.GetVersion(),
86 | }
87 | if h != nil {
88 | for key, value := range h {
89 | a[key] = value
90 | }
91 | }
92 | return a
93 | }
94 |
95 | func isAjax(c *gin.Context) bool {
96 | return c.GetHeader("X-Requested-With") == "XMLHttpRequest"
97 | }
98 |
--------------------------------------------------------------------------------
/web/controller/xui.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | )
6 |
7 | type XUIController struct {
8 | BaseController
9 |
10 | inboundController *InboundController
11 | settingController *SettingController
12 | }
13 |
14 | func NewXUIController(g *gin.RouterGroup) *XUIController {
15 | a := &XUIController{}
16 | a.initRouter(g)
17 | return a
18 | }
19 |
20 | func (a *XUIController) initRouter(g *gin.RouterGroup) {
21 | g = g.Group("/xui")
22 | g.Use(a.checkLogin)
23 |
24 | g.GET("/", a.index)
25 | g.GET("/inbounds", a.inbounds)
26 | g.GET("/setting", a.setting)
27 |
28 | a.inboundController = NewInboundController(g)
29 | a.settingController = NewSettingController(g)
30 | }
31 |
32 | func (a *XUIController) index(c *gin.Context) {
33 | html(c, "index.html", "pages.index.title", nil)
34 | }
35 |
36 | func (a *XUIController) inbounds(c *gin.Context) {
37 | html(c, "inbounds.html", "pages.inbounds.title", nil)
38 | }
39 |
40 | func (a *XUIController) setting(c *gin.Context) {
41 | html(c, "setting.html", "pages.setting.title", nil)
42 | }
43 |
--------------------------------------------------------------------------------
/web/entity/entity.go:
--------------------------------------------------------------------------------
1 | package entity
2 |
3 | import (
4 | "crypto/tls"
5 | "encoding/json"
6 | "net"
7 | "strings"
8 | "time"
9 | _ "time/tzdata"
10 | "x-ui/util/common"
11 | "x-ui/xray"
12 | )
13 |
14 | type Msg struct {
15 | Success bool `json:"success"`
16 | Msg string `json:"msg"`
17 | Obj interface{} `json:"obj"`
18 | }
19 |
20 | type Pager struct {
21 | Current int `json:"current"`
22 | PageSize int `json:"page_size"`
23 | Total int `json:"total"`
24 | OrderBy string `json:"order_by"`
25 | Desc bool `json:"desc"`
26 | Key string `json:"key"`
27 | List interface{} `json:"list"`
28 | }
29 |
30 | type AllSetting struct {
31 | WebListen string `json:"webListen" form:"webListen"`
32 | WebPort int `json:"webPort" form:"webPort"`
33 | WebCertFile string `json:"webCertFile" form:"webCertFile"`
34 | WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
35 | WebBasePath string `json:"webBasePath" form:"webBasePath"`
36 | TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
37 | TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
38 | TgBotChatId int `json:"tgBotChatId" form:"tgBotChatId"`
39 | TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
40 | XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
41 |
42 | TimeLocation string `json:"timeLocation" form:"timeLocation"`
43 | }
44 |
45 | func (s *AllSetting) CheckValid() error {
46 | if s.WebListen != "" {
47 | ip := net.ParseIP(s.WebListen)
48 | if ip == nil {
49 | return common.NewError("web listen is not valid ip:", s.WebListen)
50 | }
51 | }
52 |
53 | if s.WebPort <= 0 || s.WebPort > 65535 {
54 | return common.NewError("web port is not a valid port:", s.WebPort)
55 | }
56 |
57 | if s.WebCertFile != "" || s.WebKeyFile != "" {
58 | _, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile)
59 | if err != nil {
60 | return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.WebCertFile, s.WebKeyFile, err)
61 | }
62 | }
63 |
64 | if !strings.HasPrefix(s.WebBasePath, "/") {
65 | s.WebBasePath = "/" + s.WebBasePath
66 | }
67 | if !strings.HasSuffix(s.WebBasePath, "/") {
68 | s.WebBasePath += "/"
69 | }
70 |
71 | xrayConfig := &xray.Config{}
72 | err := json.Unmarshal([]byte(s.XrayTemplateConfig), xrayConfig)
73 | if err != nil {
74 | return common.NewError("xray template config invalid:", err)
75 | }
76 |
77 | _, err = time.LoadLocation(s.TimeLocation)
78 | if err != nil {
79 | return common.NewError("time location not exist:", s.TimeLocation)
80 | }
81 |
82 | return nil
83 | }
84 |
--------------------------------------------------------------------------------
/web/global/global.go:
--------------------------------------------------------------------------------
1 | package global
2 |
3 | import (
4 | "context"
5 | "github.com/robfig/cron/v3"
6 | _ "unsafe"
7 | )
8 |
9 | var webServer WebServer
10 |
11 | type WebServer interface {
12 | GetCron() *cron.Cron
13 | GetCtx() context.Context
14 | }
15 |
16 | func SetWebServer(s WebServer) {
17 | webServer = s
18 | }
19 |
20 | func GetWebServer() WebServer {
21 | return webServer
22 | }
23 |
--------------------------------------------------------------------------------
/web/html/common/head.html:
--------------------------------------------------------------------------------
1 | {{define "head"}}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 | {{ i18n .title}}
16 |
17 | {{end}}
--------------------------------------------------------------------------------
/web/html/common/js.html:
--------------------------------------------------------------------------------
1 | {{define "js"}}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
22 | {{end}}
--------------------------------------------------------------------------------
/web/html/common/prompt_modal.html:
--------------------------------------------------------------------------------
1 | {{define "promptModal"}}
2 |
5 |
10 |
11 |
12 |
67 | {{end}}
--------------------------------------------------------------------------------
/web/html/common/qrcode_modal.html:
--------------------------------------------------------------------------------
1 | {{define "qrcodeModal"}}
2 |
5 | click on QR Code to Copy
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
120 | {{end}}
--------------------------------------------------------------------------------
/web/html/common/text_modal.html:
--------------------------------------------------------------------------------
1 | {{define "textModal"}}
2 |
5 |
7 | {{ i18n "download" }} [[ txtModal.fileName ]]
8 |
9 |
11 |
12 |
13 |
58 | {{end}}
--------------------------------------------------------------------------------
/web/html/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{template "head" .}}
4 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {{ i18n "pages.login.title" }}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
51 |
52 |
53 |
54 |
55 |
57 |
58 |
59 |
60 |
61 | {{ i18n "login" }}
62 |
63 |
64 |
65 |
66 | Language :
67 |
68 |
69 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | {{template "js" .}}
93 |
122 |
123 |
--------------------------------------------------------------------------------
/web/html/xui/common_sider.html:
--------------------------------------------------------------------------------
1 | {{define "menuItems"}}
2 |
3 |
4 | {{ i18n "menu.dashboard"}}
5 |
6 |
7 |
8 | {{ i18n "menu.inbounds"}}
9 |
10 |
11 |
12 | {{ i18n "menu.setting"}}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{ i18n "menu.link"}}
22 |
23 |
24 |
25 | Github
26 |
27 |
28 |
29 |
30 | {{ i18n "menu.logout"}}
31 |
32 | {{end}}
33 |
34 |
35 | {{define "commonSider"}}
36 |
37 | key.startsWith('http') ? window.open(key) : location.href = key">
39 | {{template "menuItems" .}}
40 |
41 |
42 |
45 |
48 | key.startsWith('http') ? window.open(key) : location.href = key">
50 | {{template "menuItems" .}}
51 |
52 |
53 |
69 | {{end}}
70 |
--------------------------------------------------------------------------------
/web/html/xui/component/inbound_info.html:
--------------------------------------------------------------------------------
1 | {{define "inboundInfoStream"}}
2 | {{ i18n "transmission" }}: [[ inbound.network ]]
3 |
4 |
5 | host: [[ inbound.host ]]
6 | {{ i18n "host" }}: {{ i18n "none" }}
7 |
8 | path: [[ inbound.path ]]
9 | {{ i18n "path" }}: {{ i18n "none" }}
10 |
11 |
12 |
13 | quic {{ i18n "encryption" }}: [[ inbound.quicSecurity ]]
14 | quic {{ i18n "password" }}: [[ inbound.quicKey ]]
15 | quic {{ i18n "camouflage" }}: [[ inbound.quicType ]]
16 |
17 |
18 |
19 | kcp {{ i18n "encryption" }}: [[ inbound.kcpType ]]
20 | kcp {{ i18n "password" }}: [[ inbound.kcpSeed ]]
21 |
22 |
23 |
24 | grpc serviceName: [[ inbound.serviceName ]]
25 |
26 |
27 |
28 | tls: {{ i18n "turnOn" }}
29 | xtls: {{ i18n "turnOn" }}
30 |
31 |
32 | tls: {{ i18n "closure" }}
33 |
34 |
35 | tls {{ i18n "domainName" }}: [[ inbound.serverName ? inbound.serverName : '' ]]
36 |
37 |
38 | xtls {{ i18n "domainName" }}: [[ inbound.serverName ? inbound.serverName : '' ]]
39 |
40 | {{end}}
41 |
42 |
43 | {{define "component/inboundInfoComponent"}}
44 |
45 |
{{ i18n "protocol"}}: [[ dbInbound.protocol ]]
46 |
{{ i18n "pages.inbounds.address"}}: [[ dbInbound.address ]]
47 |
{{ i18n "pages.inbounds.port"}}: [[ dbInbound.port ]]
48 |
49 |
50 | uuid: [[ vmess.id ]]
51 | alterId: [[ vmess.alterId ]]
52 |
53 |
54 |
55 |
56 | uuid: [[ vless.id ]]
57 | flow: [[ vless.flow ]]
58 |
59 |
60 |
61 |
62 | {{ i18n "password"}}: [[ inbound.password ]]
63 |
64 |
65 |
66 | {{ i18n "encryption"}}: [[ inbound.method ]]
67 | {{ i18n "password"}}: [[ inbound.password ]]
68 |
69 |
70 |
71 | {{ i18n "username"}}: [[ inbound.username ]]
72 | {{ i18n "password"}}: [[ inbound.password ]]
73 |
74 |
75 |
76 | {{ i18n "username"}}: [[ inbound.username ]]
77 | {{ i18n "password"}}: [[ inbound.password ]]
78 |
79 |
80 |
81 | {{template "inboundInfoStream"}}
82 |
83 |
84 | {{end}}
85 |
86 | {{define "component/inboundInfo"}}
87 |
94 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/component/setting.html:
--------------------------------------------------------------------------------
1 | {{define "component/settingListItem"}}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | $emit('input', value)">
19 |
20 |
21 |
22 |
23 | {{end}}
24 |
25 | {{define "component/setting"}}
26 |
32 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/inbound.html:
--------------------------------------------------------------------------------
1 | {{define "form/inbound"}}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | [[ p ]]
13 |
14 |
15 |
16 |
17 | {{ i18n "monitor" }}
18 |
19 |
20 | {{ i18n "pages.inbounds.monitorDesc" }}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{ i18n "pages.inbounds.totalFlow" }}(GB)
33 |
34 |
35 | 0 {{ i18n "pages.inbounds.meansNoLimit" }}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | {{ i18n "pages.inbounds.expireDate" }}
45 |
46 |
47 | {{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}
48 |
49 |
50 |
51 |
52 |
54 |
55 |
56 |
57 |
58 |
59 | {{template "form/vmess"}}
60 |
61 |
62 |
63 |
64 | {{template "form/vless"}}
65 |
66 |
67 |
68 |
69 | {{template "form/trojan"}}
70 |
71 |
72 |
73 |
74 | {{template "form/shadowsocks"}}
75 |
76 |
77 |
78 |
79 | {{template "form/dokodemo"}}
80 |
81 |
82 |
83 |
84 | {{template "form/socks"}}
85 |
86 |
87 |
88 |
89 | {{template "form/http"}}
90 |
91 |
92 |
93 |
94 | {{template "form/streamSettings"}}
95 |
96 |
97 |
98 |
99 | {{template "form/tlsSettings"}}
100 |
101 |
102 |
103 |
104 | {{template "form/sniffing"}}
105 |
106 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/protocol/dokodemo.html:
--------------------------------------------------------------------------------
1 | {{define "form/dokodemo"}}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | tcp+udp
12 | tcp
13 | udp
14 |
15 |
16 |
17 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/protocol/http.html:
--------------------------------------------------------------------------------
1 | {{define "form/http"}}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/protocol/shadowsocks.html:
--------------------------------------------------------------------------------
1 | {{define "form/shadowsocks"}}
2 |
3 |
4 |
5 | [[ method ]]
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | tcp+udp
14 | tcp
15 | udp
16 |
17 |
18 |
19 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/protocol/socks.html:
--------------------------------------------------------------------------------
1 | {{define "form/socks"}}
2 |
3 |
4 |
5 | inbound.settings.auth = checked ? 'password' : 'noauth'">
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
21 |
22 |
23 |
24 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/protocol/trojan.html:
--------------------------------------------------------------------------------
1 | {{define "form/trojan"}}
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ i18n "none" }}
9 | [[ key ]]
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
19 | +
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | fallback[[ index + 1 ]]
29 | inbound.settings.delTrojanFallback(index)"
30 | style="color: rgb(255, 77, 79);cursor: pointer;"/>
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/sniffing.html:
--------------------------------------------------------------------------------
1 | {{define "form/sniffing"}}
2 |
3 |
4 |
5 | sniffing
6 |
7 |
8 | {{ i18n "pages.inbounds.noRecommendKeepDefault" }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_grpc.html:
--------------------------------------------------------------------------------
1 | {{define "form/streamGRPC"}}
2 |
3 |
4 |
5 |
6 |
7 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_http.html:
--------------------------------------------------------------------------------
1 | {{define "form/streamHTTP"}}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_kcp.html:
--------------------------------------------------------------------------------
1 | {{define "form/streamKCP"}}
2 |
3 |
4 |
5 | none(not camouflage)
6 | srtp(camouflage video call)
7 | utp(camouflage BT download)
8 | wechat-video(camouflage WeChat video)
9 | dtls(camouflage DTLS 1.2 packages)
10 | wireguard(camouflage wireguard packages)
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_quic.html:
--------------------------------------------------------------------------------
1 | {{define "form/streamQUIC"}}
2 |
3 |
4 |
5 | none
6 | aes-128-gcm
7 | chacha20-poly1305
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | none(not camouflage)
16 | srtp(camouflage video call)
17 | utp(camouflage BT download)
18 | wechat-video(camouflage WeChat video)
19 | dtls(camouflage DTLS 1.2 packages)
20 | wireguard(camouflage wireguard packages)
21 |
22 |
23 |
24 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_settings.html:
--------------------------------------------------------------------------------
1 | {{define "form/streamSettings"}}
2 |
3 |
4 |
5 |
6 | tcp
7 | kcp
8 | ws
9 | http
10 | quic
11 | grpc
12 |
13 |
14 |
15 |
16 |
17 |
18 | {{template "form/streamTCP"}}
19 |
20 |
21 |
22 |
23 | {{template "form/streamKCP"}}
24 |
25 |
26 |
27 |
28 | {{template "form/streamWS"}}
29 |
30 |
31 |
32 |
33 | {{template "form/streamHTTP"}}
34 |
35 |
36 |
37 |
38 | {{template "form/streamQUIC"}}
39 |
40 |
41 |
42 |
43 | {{template "form/streamGRPC"}}
44 |
45 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_tcp.html:
--------------------------------------------------------------------------------
1 | {{define "form/streamTCP"}}
2 |
3 |
4 |
5 |
6 |
7 |
8 | inbound.stream.tcp.type = checked ? 'http' : 'none'">
11 |
12 |
13 |
14 |
15 |
16 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
33 | +
34 |
35 |
36 |
37 |
39 |
41 |
42 |
44 | -
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
68 | +
69 |
70 |
71 |
72 |
74 |
76 |
77 |
79 | -
80 |
81 |
82 |
83 |
84 |
85 |
86 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_ws.html:
--------------------------------------------------------------------------------
1 | {{define "form/streamWS"}}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
15 | +
16 |
17 |
18 |
19 |
21 |
23 |
24 |
26 | -
27 |
28 |
29 |
30 |
31 |
32 |
33 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/form/tls_settings.html:
--------------------------------------------------------------------------------
1 | {{define "form/tlsSettings"}}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
25 | {{ i18n "pages.inbounds.certificatePath" }}
26 | {{ i18n "pages.inbounds.certificateContent" }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
41 |
42 |
43 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/inbound_info_modal.html:
--------------------------------------------------------------------------------
1 | {{define "inboundInfoModal"}}
2 | {{template "component/inboundInfo"}}
3 |
6 |
7 |
8 |
61 | {{end}}
--------------------------------------------------------------------------------
/web/html/xui/inbound_modal.html:
--------------------------------------------------------------------------------
1 | {{define "inboundModal"}}
2 |
5 | {{template "form/inbound"}}
6 |
7 |
197 | {{end}}
198 |
--------------------------------------------------------------------------------
/web/job/check_inbound_job.go:
--------------------------------------------------------------------------------
1 | package job
2 |
3 | import (
4 | "x-ui/logger"
5 | "x-ui/web/service"
6 | )
7 |
8 | type CheckInboundJob struct {
9 | xrayService service.XrayService
10 | inboundService service.InboundService
11 | }
12 |
13 | func NewCheckInboundJob() *CheckInboundJob {
14 | return new(CheckInboundJob)
15 | }
16 |
17 | func (j *CheckInboundJob) Run() {
18 | count, err := j.inboundService.DisableInvalidClients()
19 | if err != nil {
20 | logger.Warning("disable invalid Client err:", err)
21 | } else if count > 0 {
22 | logger.Debugf("disabled %v Client", count)
23 | j.xrayService.SetToNeedRestart()
24 | }
25 |
26 | count, err = j.inboundService.DisableInvalidInbounds()
27 | if err != nil {
28 | logger.Warning("disable invalid inbounds err:", err)
29 | } else if count > 0 {
30 | logger.Debugf("disabled %v inbounds", count)
31 | j.xrayService.SetToNeedRestart()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/web/job/check_xray_running_job.go:
--------------------------------------------------------------------------------
1 | package job
2 |
3 | import "x-ui/web/service"
4 |
5 | type CheckXrayRunningJob struct {
6 | xrayService service.XrayService
7 |
8 | checkTime int
9 | }
10 |
11 | func NewCheckXrayRunningJob() *CheckXrayRunningJob {
12 | return new(CheckXrayRunningJob)
13 | }
14 |
15 | func (j *CheckXrayRunningJob) Run() {
16 | if j.xrayService.IsXrayRunning() {
17 | j.checkTime = 0
18 | return
19 | }
20 | j.checkTime++
21 | if j.checkTime < 2 {
22 | return
23 | }
24 | j.xrayService.SetToNeedRestart()
25 | }
26 |
--------------------------------------------------------------------------------
/web/job/xray_traffic_job.go:
--------------------------------------------------------------------------------
1 | package job
2 |
3 | import (
4 | "x-ui/logger"
5 | "x-ui/web/service"
6 | )
7 |
8 | type XrayTrafficJob struct {
9 | xrayService service.XrayService
10 | inboundService service.InboundService
11 | }
12 |
13 | func NewXrayTrafficJob() *XrayTrafficJob {
14 | return new(XrayTrafficJob)
15 | }
16 |
17 | func (j *XrayTrafficJob) Run() {
18 | if !j.xrayService.IsXrayRunning() {
19 | return
20 | }
21 |
22 | traffics, clientTraffics, err := j.xrayService.GetXrayTraffic()
23 | if err != nil {
24 | logger.Warning("get xray traffic failed:", err)
25 | return
26 | }
27 | err = j.inboundService.AddTraffic(traffics)
28 | if err != nil {
29 | logger.Warning("add traffic failed:", err)
30 | }
31 |
32 | err = j.inboundService.AddClientTraffic(clientTraffics)
33 | if err != nil {
34 | logger.Warning("add client traffic failed:", err)
35 | }
36 |
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/web/network/auto_https_listener.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import "net"
4 |
5 | type AutoHttpsListener struct {
6 | net.Listener
7 | }
8 |
9 | func NewAutoHttpsListener(listener net.Listener) net.Listener {
10 | return &AutoHttpsListener{
11 | Listener: listener,
12 | }
13 | }
14 |
15 | func (l *AutoHttpsListener) Accept() (net.Conn, error) {
16 | conn, err := l.Listener.Accept()
17 | if err != nil {
18 | return nil, err
19 | }
20 | return NewAutoHttpsConn(conn), nil
21 | }
22 |
--------------------------------------------------------------------------------
/web/network/autp_https_conn.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "fmt"
7 | "net"
8 | "net/http"
9 | "sync"
10 | )
11 |
12 | type AutoHttpsConn struct {
13 | net.Conn
14 |
15 | firstBuf []byte
16 | bufStart int
17 |
18 | readRequestOnce sync.Once
19 | }
20 |
21 | func NewAutoHttpsConn(conn net.Conn) net.Conn {
22 | return &AutoHttpsConn{
23 | Conn: conn,
24 | }
25 | }
26 |
27 | func (c *AutoHttpsConn) readRequest() bool {
28 | c.firstBuf = make([]byte, 2048)
29 | n, err := c.Conn.Read(c.firstBuf)
30 | c.firstBuf = c.firstBuf[:n]
31 | if err != nil {
32 | return false
33 | }
34 | reader := bytes.NewReader(c.firstBuf)
35 | bufReader := bufio.NewReader(reader)
36 | request, err := http.ReadRequest(bufReader)
37 | if err != nil {
38 | return false
39 | }
40 | resp := http.Response{
41 | Header: http.Header{},
42 | }
43 | resp.StatusCode = http.StatusTemporaryRedirect
44 | location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI)
45 | resp.Header.Set("Location", location)
46 | resp.Write(c.Conn)
47 | c.Close()
48 | c.firstBuf = nil
49 | return true
50 | }
51 |
52 | func (c *AutoHttpsConn) Read(buf []byte) (int, error) {
53 | c.readRequestOnce.Do(func() {
54 | c.readRequest()
55 | })
56 |
57 | if c.firstBuf != nil {
58 | n := copy(buf, c.firstBuf[c.bufStart:])
59 | c.bufStart += n
60 | if c.bufStart >= len(c.firstBuf) {
61 | c.firstBuf = nil
62 | }
63 | return n, nil
64 | }
65 |
66 | return c.Conn.Read(buf)
67 | }
68 |
--------------------------------------------------------------------------------
/web/service/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "log": {
3 | "loglevel": "warning",
4 | "access": "./access.log"
5 | },
6 |
7 | "api": {
8 | "services": [
9 | "HandlerService",
10 | "LoggerService",
11 | "StatsService"
12 | ],
13 | "tag": "api"
14 | },
15 | "inbounds": [
16 | {
17 | "listen": "127.0.0.1",
18 | "port": 62789,
19 | "protocol": "dokodemo-door",
20 | "settings": {
21 | "address": "127.0.0.1"
22 | },
23 | "tag": "api"
24 | }
25 | ],
26 | "outbounds": [
27 | {
28 | "protocol": "freedom",
29 | "settings": {}
30 | },
31 | {
32 | "protocol": "blackhole",
33 | "settings": {},
34 | "tag": "blocked"
35 | }
36 | ],
37 | "policy": {
38 | "levels": {
39 | "0": {
40 | "statsUserUplink": true,
41 | "statsUserDownlink": true
42 | }
43 | },
44 | "system": {
45 | "statsInboundDownlink": true,
46 | "statsInboundUplink": true
47 | }
48 | },
49 | "routing": {
50 | "rules": [
51 | {
52 | "inboundTag": [
53 | "api"
54 | ],
55 | "outboundTag": "api",
56 | "type": "field"
57 | },
58 | {
59 | "ip": [
60 | "geoip:private"
61 | ],
62 | "outboundTag": "blocked",
63 | "type": "field"
64 | },
65 | {
66 | "outboundTag": "blocked",
67 | "protocol": [
68 | "bittorrent"
69 | ],
70 | "type": "field"
71 | }
72 | ]
73 | },
74 | "stats": {}
75 | }
76 |
--------------------------------------------------------------------------------
/web/service/panel.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "os"
5 | "syscall"
6 | "time"
7 | "x-ui/logger"
8 | )
9 |
10 | type PanelService struct {
11 | }
12 |
13 | func (s *PanelService) RestartPanel(delay time.Duration) error {
14 | p, err := os.FindProcess(syscall.Getpid())
15 | if err != nil {
16 | return err
17 | }
18 | go func() {
19 | time.Sleep(delay)
20 | err := p.Signal(syscall.SIGHUP)
21 | if err != nil {
22 | logger.Error("send signal SIGHUP failed:", err)
23 | }
24 | }()
25 | return nil
26 | }
27 |
--------------------------------------------------------------------------------
/web/service/server.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "archive/zip"
5 | "bytes"
6 | "encoding/json"
7 | "fmt"
8 | "github.com/shirou/gopsutil/cpu"
9 | "github.com/shirou/gopsutil/disk"
10 | "github.com/shirou/gopsutil/host"
11 | "github.com/shirou/gopsutil/load"
12 | "github.com/shirou/gopsutil/mem"
13 | "github.com/shirou/gopsutil/net"
14 | "io"
15 | "io/fs"
16 | "net/http"
17 | "os"
18 | "runtime"
19 | "time"
20 | "x-ui/logger"
21 | // "x-ui/util/sys"
22 | "x-ui/xray"
23 | )
24 |
25 | type ProcessState string
26 |
27 | const (
28 | Running ProcessState = "running"
29 | Stop ProcessState = "stop"
30 | Error ProcessState = "error"
31 | )
32 |
33 | type Status struct {
34 | T time.Time `json:"-"`
35 | Cpu float64 `json:"cpu"`
36 | Mem struct {
37 | Current uint64 `json:"current"`
38 | Total uint64 `json:"total"`
39 | } `json:"mem"`
40 | Swap struct {
41 | Current uint64 `json:"current"`
42 | Total uint64 `json:"total"`
43 | } `json:"swap"`
44 | Disk struct {
45 | Current uint64 `json:"current"`
46 | Total uint64 `json:"total"`
47 | } `json:"disk"`
48 | Xray struct {
49 | State ProcessState `json:"state"`
50 | ErrorMsg string `json:"errorMsg"`
51 | Version string `json:"version"`
52 | } `json:"xray"`
53 | Uptime uint64 `json:"uptime"`
54 | Loads []float64 `json:"loads"`
55 | TcpCount int `json:"tcpCount"`
56 | UdpCount int `json:"udpCount"`
57 | NetIO struct {
58 | Up uint64 `json:"up"`
59 | Down uint64 `json:"down"`
60 | } `json:"netIO"`
61 | NetTraffic struct {
62 | Sent uint64 `json:"sent"`
63 | Recv uint64 `json:"recv"`
64 | } `json:"netTraffic"`
65 | }
66 |
67 | type Release struct {
68 | TagName string `json:"tag_name"`
69 | }
70 |
71 | type ServerService struct {
72 | xrayService XrayService
73 | }
74 |
75 | func (s *ServerService) GetStatus(lastStatus *Status) *Status {
76 | now := time.Now()
77 | status := &Status{
78 | T: now,
79 | }
80 |
81 | percents, err := cpu.Percent(0, false)
82 | if err != nil {
83 | logger.Warning("get cpu percent failed:", err)
84 | } else {
85 | status.Cpu = percents[0]
86 | }
87 |
88 | upTime, err := host.Uptime()
89 | if err != nil {
90 | logger.Warning("get uptime failed:", err)
91 | } else {
92 | status.Uptime = upTime
93 | }
94 |
95 | memInfo, err := mem.VirtualMemory()
96 | if err != nil {
97 | logger.Warning("get virtual memory failed:", err)
98 | } else {
99 | status.Mem.Current = memInfo.Used
100 | status.Mem.Total = memInfo.Total
101 | }
102 |
103 | swapInfo, err := mem.SwapMemory()
104 | if err != nil {
105 | logger.Warning("get swap memory failed:", err)
106 | } else {
107 | status.Swap.Current = swapInfo.Used
108 | status.Swap.Total = swapInfo.Total
109 | }
110 |
111 | distInfo, err := disk.Usage("/")
112 | if err != nil {
113 | logger.Warning("get dist usage failed:", err)
114 | } else {
115 | status.Disk.Current = distInfo.Used
116 | status.Disk.Total = distInfo.Total
117 | }
118 |
119 | avgState, err := load.Avg()
120 | if err != nil {
121 | logger.Warning("get load avg failed:", err)
122 | } else {
123 | status.Loads = []float64{avgState.Load1, avgState.Load5, avgState.Load15}
124 | }
125 |
126 | ioStats, err := net.IOCounters(false)
127 | if err != nil {
128 | logger.Warning("get io counters failed:", err)
129 | } else if len(ioStats) > 0 {
130 | ioStat := ioStats[0]
131 | status.NetTraffic.Sent = ioStat.BytesSent
132 | status.NetTraffic.Recv = ioStat.BytesRecv
133 |
134 | if lastStatus != nil {
135 | duration := now.Sub(lastStatus.T)
136 | seconds := float64(duration) / float64(time.Second)
137 | up := uint64(float64(status.NetTraffic.Sent-lastStatus.NetTraffic.Sent) / seconds)
138 | down := uint64(float64(status.NetTraffic.Recv-lastStatus.NetTraffic.Recv) / seconds)
139 | status.NetIO.Up = up
140 | status.NetIO.Down = down
141 | }
142 | } else {
143 | logger.Warning("can not find io counters")
144 | }
145 |
146 | // status.TcpCount, err = sys.GetTCPCount()
147 | status.TcpCount = 1
148 | err = nil
149 | if err != nil {
150 | logger.Warning("get tcp connections failed:", err)
151 | }
152 |
153 | // status.UdpCount, err = sys.GetUDPCount()
154 | status.UdpCount = 1
155 | err = nil
156 | if err != nil {
157 | logger.Warning("get udp connections failed:", err)
158 | }
159 |
160 | if s.xrayService.IsXrayRunning() {
161 | status.Xray.State = Running
162 | status.Xray.ErrorMsg = ""
163 | } else {
164 | err := s.xrayService.GetXrayErr()
165 | if err != nil {
166 | status.Xray.State = Error
167 | } else {
168 | status.Xray.State = Stop
169 | }
170 | status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
171 | }
172 | status.Xray.Version = s.xrayService.GetXrayVersion()
173 |
174 | return status
175 | }
176 |
177 | func (s *ServerService) GetXrayVersions() ([]string, error) {
178 | url := "https://api.github.com/repos/hossinasaadi/Xray-core/releases"
179 | resp, err := http.Get(url)
180 | if err != nil {
181 | return nil, err
182 | }
183 |
184 | defer resp.Body.Close()
185 | buffer := bytes.NewBuffer(make([]byte, 8192))
186 | buffer.Reset()
187 | _, err = buffer.ReadFrom(resp.Body)
188 | if err != nil {
189 | return nil, err
190 | }
191 |
192 | releases := make([]Release, 0)
193 | err = json.Unmarshal(buffer.Bytes(), &releases)
194 | if err != nil {
195 | return nil, err
196 | }
197 | versions := make([]string, 0, len(releases))
198 | for _, release := range releases {
199 | versions = append(versions, release.TagName)
200 | }
201 | return versions, nil
202 | }
203 |
204 | func (s *ServerService) downloadXRay(version string) (string, error) {
205 | osName := runtime.GOOS
206 | arch := runtime.GOARCH
207 |
208 | switch osName {
209 | case "darwin":
210 | osName = "macos"
211 | }
212 |
213 | switch arch {
214 | case "amd64":
215 | arch = "64"
216 | case "arm64":
217 | arch = "arm64-v8a"
218 | }
219 |
220 | fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
221 | url := fmt.Sprintf("https://github.com/hossinasaadi/Xray-core/releases/download/%s/%s", version, fileName)
222 | resp, err := http.Get(url)
223 | if err != nil {
224 | return "", err
225 | }
226 | defer resp.Body.Close()
227 |
228 | os.Remove(fileName)
229 | file, err := os.Create(fileName)
230 | if err != nil {
231 | return "", err
232 | }
233 | defer file.Close()
234 |
235 | _, err = io.Copy(file, resp.Body)
236 | if err != nil {
237 | return "", err
238 | }
239 |
240 | return fileName, nil
241 | }
242 |
243 | func (s *ServerService) UpdateXray(version string) error {
244 | zipFileName, err := s.downloadXRay(version)
245 | if err != nil {
246 | return err
247 | }
248 |
249 | zipFile, err := os.Open(zipFileName)
250 | if err != nil {
251 | return err
252 | }
253 | defer func() {
254 | zipFile.Close()
255 | os.Remove(zipFileName)
256 | }()
257 |
258 | stat, err := zipFile.Stat()
259 | if err != nil {
260 | return err
261 | }
262 | reader, err := zip.NewReader(zipFile, stat.Size())
263 | if err != nil {
264 | return err
265 | }
266 |
267 | s.xrayService.StopXray()
268 | defer func() {
269 | err := s.xrayService.RestartXray(true)
270 | if err != nil {
271 | logger.Error("start xray failed:", err)
272 | }
273 | }()
274 |
275 | copyZipFile := func(zipName string, fileName string) error {
276 | zipFile, err := reader.Open(zipName)
277 | if err != nil {
278 | return err
279 | }
280 | os.Remove(fileName)
281 | file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm)
282 | if err != nil {
283 | return err
284 | }
285 | defer file.Close()
286 | _, err = io.Copy(file, zipFile)
287 | return err
288 | }
289 |
290 | err = copyZipFile("xray", xray.GetBinaryPath())
291 | if err != nil {
292 | return err
293 | }
294 | err = copyZipFile("geosite.dat", xray.GetGeositePath())
295 | if err != nil {
296 | return err
297 | }
298 | err = copyZipFile("geoip.dat", xray.GetGeoipPath())
299 | if err != nil {
300 | return err
301 | }
302 |
303 | return nil
304 |
305 | }
306 |
--------------------------------------------------------------------------------
/web/service/user.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "errors"
5 | "x-ui/database"
6 | "x-ui/database/model"
7 | "x-ui/logger"
8 |
9 | "gorm.io/gorm"
10 | )
11 |
12 | type UserService struct {
13 | }
14 |
15 | func (s *UserService) GetFirstUser() (*model.User, error) {
16 | db := database.GetDB()
17 |
18 | user := &model.User{}
19 | err := db.Model(model.User{}).
20 | First(user).
21 | Error
22 | if err != nil {
23 | return nil, err
24 | }
25 | return user, nil
26 | }
27 |
28 | func (s *UserService) CheckUser(username string, password string) *model.User {
29 | db := database.GetDB()
30 |
31 | user := &model.User{}
32 | err := db.Model(model.User{}).
33 | Where("username = ? and password = ?", username, password).
34 | First(user).
35 | Error
36 | if err == gorm.ErrRecordNotFound {
37 | return nil
38 | } else if err != nil {
39 | logger.Warning("check user err:", err)
40 | return nil
41 | }
42 | return user
43 | }
44 |
45 | func (s *UserService) UpdateUser(id int, username string, password string) error {
46 | db := database.GetDB()
47 | return db.Model(model.User{}).
48 | Where("id = ?", id).
49 | Update("username", username).
50 | Update("password", password).
51 | Error
52 | }
53 |
54 | func (s *UserService) UpdateFirstUser(username string, password string) error {
55 | if username == "" {
56 | return errors.New("username can not be empty")
57 | } else if password == "" {
58 | return errors.New("password can not be empty")
59 | }
60 | db := database.GetDB()
61 | user := &model.User{}
62 | err := db.Model(model.User{}).First(user).Error
63 | if database.IsNotFound(err) {
64 | user.Username = username
65 | user.Password = password
66 | return db.Model(model.User{}).Create(user).Error
67 | } else if err != nil {
68 | return err
69 | }
70 | user.Username = username
71 | user.Password = password
72 | return db.Save(user).Error
73 | }
74 |
--------------------------------------------------------------------------------
/web/service/xray.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "sync"
7 | "x-ui/logger"
8 | "x-ui/xray"
9 | "go.uber.org/atomic"
10 | )
11 |
12 | var p *xray.Process
13 | var lock sync.Mutex
14 | var isNeedXrayRestart atomic.Bool
15 | var result string
16 |
17 | type XrayService struct {
18 | inboundService InboundService
19 | settingService SettingService
20 | }
21 |
22 | func (s *XrayService) IsXrayRunning() bool {
23 | return p != nil && p.IsRunning()
24 | }
25 |
26 | func (s *XrayService) GetXrayErr() error {
27 | if p == nil {
28 | return nil
29 | }
30 | return p.GetErr()
31 | }
32 |
33 | func (s *XrayService) GetXrayResult() string {
34 | if result != "" {
35 | return result
36 | }
37 | if s.IsXrayRunning() {
38 | return ""
39 | }
40 | if p == nil {
41 | return ""
42 | }
43 | result = p.GetResult()
44 | return result
45 | }
46 |
47 | func (s *XrayService) GetXrayVersion() string {
48 | if p == nil {
49 | return "Unknown"
50 | }
51 | return p.GetVersion()
52 | }
53 | func RemoveIndex(s []interface{}, index int) []interface{} {
54 | return append(s[:index], s[index+1:]...)
55 | }
56 |
57 | func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
58 | templateConfig, err := s.settingService.GetXrayConfigTemplate()
59 | if err != nil {
60 | return nil, err
61 | }
62 |
63 | xrayConfig := &xray.Config{}
64 | err = json.Unmarshal([]byte(templateConfig), xrayConfig)
65 | if err != nil {
66 | return nil, err
67 | }
68 |
69 | s.inboundService.DisableInvalidClients()
70 |
71 | inbounds, err := s.inboundService.GetAllInbounds()
72 | if err != nil {
73 | return nil, err
74 | }
75 | for _, inbound := range inbounds {
76 | if !inbound.Enable {
77 | continue
78 | }
79 | // get settings clients
80 | settings := map[string]interface{}{}
81 | json.Unmarshal([]byte(inbound.Settings), &settings)
82 | clients, ok := settings["clients"].([]interface{})
83 | if ok {
84 | // check users active or not
85 |
86 | clientStats := inbound.ClientStats
87 | for _, clientTraffic := range clientStats {
88 |
89 | for index, client := range clients {
90 | c := client.(map[string]interface{})
91 | if c["email"] == clientTraffic.Email {
92 | if ! clientTraffic.Enable {
93 | clients = RemoveIndex(clients,index)
94 | logger.Info("Remove Inbound User",c["email"] ,"due the expire or traffic limit")
95 |
96 | }
97 |
98 | }
99 | }
100 |
101 |
102 | }
103 | settings["clients"] = clients
104 | modifiedSettings, err := json.Marshal(settings)
105 | if err != nil {
106 | return nil, err
107 | }
108 |
109 | inbound.Settings = string(modifiedSettings)
110 | }
111 | inboundConfig := inbound.GenXrayInboundConfig()
112 | xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
113 | }
114 | return xrayConfig, nil
115 | }
116 |
117 | func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
118 | if !s.IsXrayRunning() {
119 | return nil, nil, errors.New("xray is not running")
120 | }
121 | return p.GetTraffic(true)
122 | }
123 |
124 | func (s *XrayService) RestartXray(isForce bool) error {
125 | lock.Lock()
126 | defer lock.Unlock()
127 | logger.Debug("restart xray, force:", isForce)
128 |
129 | xrayConfig, err := s.GetXrayConfig()
130 | if err != nil {
131 | return err
132 | }
133 |
134 | if p != nil && p.IsRunning() {
135 | if !isForce && p.GetConfig().Equals(xrayConfig) {
136 | logger.Debug("not need to restart xray")
137 | return nil
138 | }
139 | p.Stop()
140 | }
141 |
142 | p = xray.NewProcess(xrayConfig)
143 | result = ""
144 | return p.Start()
145 | }
146 |
147 | func (s *XrayService) StopXray() error {
148 | lock.Lock()
149 | defer lock.Unlock()
150 | logger.Debug("stop xray")
151 | if s.IsXrayRunning() {
152 | return p.Stop()
153 | }
154 | return errors.New("xray is not running")
155 | }
156 |
157 | func (s *XrayService) SetToNeedRestart() {
158 | isNeedXrayRestart.Store(true)
159 | }
160 |
161 | func (s *XrayService) IsNeedRestartAndSetFalse() bool {
162 | return isNeedXrayRestart.CAS(true, false)
163 | }
164 |
--------------------------------------------------------------------------------
/web/session/session.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "encoding/gob"
5 | "github.com/gin-contrib/sessions"
6 | "github.com/gin-gonic/gin"
7 | "x-ui/database/model"
8 | )
9 |
10 | const (
11 | loginUser = "LOGIN_USER"
12 | )
13 |
14 | func init() {
15 | gob.Register(model.User{})
16 | }
17 |
18 | func SetLoginUser(c *gin.Context, user *model.User) error {
19 | s := sessions.Default(c)
20 | s.Set(loginUser, user)
21 | return s.Save()
22 | }
23 |
24 | func GetLoginUser(c *gin.Context) *model.User {
25 | s := sessions.Default(c)
26 | obj := s.Get(loginUser)
27 | if obj == nil {
28 | return nil
29 | }
30 | user := obj.(model.User)
31 | return &user
32 | }
33 |
34 | func IsLogin(c *gin.Context) bool {
35 | return GetLoginUser(c) != nil
36 | }
37 |
38 | func ClearSession(c *gin.Context) {
39 | s := sessions.Default(c)
40 | s.Clear()
41 | s.Options(sessions.Options{
42 | Path: "/",
43 | MaxAge: -1,
44 | })
45 | s.Save()
46 | }
47 |
--------------------------------------------------------------------------------
/web/translation/translate.en_US.toml:
--------------------------------------------------------------------------------
1 | "username" = "username"
2 | "password" = "password"
3 | "login" = "login"
4 | "confirm" = "confirm"
5 | "cancel" = "cancel"
6 | "close" = "close"
7 | "copy" = "copy"
8 | "copied" = "copied"
9 | "download" = "download"
10 | "remark" = "remark"
11 | "enable" = "enable"
12 | "protocol" = "protocol"
13 |
14 | "loading" = "Loading"
15 | "second" = "second"
16 | "minute" = "minute"
17 | "hour" = "hour"
18 | "day" = "day"
19 | "check" = "check"
20 | "indefinitely" = "indefinitely"
21 | "unlimited" = "unlimited"
22 | "none" = "none"
23 | "qrCode" = "QR Code"
24 | "edit" = "edit"
25 | "delete" = "delete"
26 | "reset" = "reset"
27 | "copySuccess" = "Copy successfully"
28 | "sure" = "Sure"
29 | "encryption" = "encryption"
30 | "transmission" = "transmission"
31 | "host" = "host"
32 | "path" = "path"
33 | "camouflage" = "camouflage"
34 | "turnOn" = "turn on"
35 | "closure" = "closure"
36 | "domainName" = "domain name"
37 | "additional" = "alter"
38 | "monitor" = "Listen IP"
39 | "certificate" = "certificat"
40 | "fail" = "fail"
41 | "success" = " success"
42 | "getVersion" = "get version"
43 | "install" = "install"
44 |
45 | [menu]
46 | "dashboard" = "System Status"
47 | "inbounds" = "Inbounds"
48 | "setting" = "Panel Setting"
49 | "logout" = "LogOut"
50 | "link" = "Other"
51 |
52 | [pages.login]
53 | "title" = "Login"
54 | "loginAgain" = "The login time limit has expired, please log in again"
55 |
56 | [pages.login.toasts]
57 | "invalidFormData" = "Input Data Format Is Invalid"
58 | "emptyUsername" = "please Enter Username"
59 | "emptyPassword" = "please Enter Password"
60 | "wrongUsernameOrPassword" = "invalid username or password"
61 | "successLogin" = "Login"
62 |
63 |
64 | [pages.index]
65 | "title" = "system status"
66 | "memory" = "memory"
67 | "hard" = "hard disk"
68 | "xrayStatus" = "xray Status"
69 | "xraySwitch" = "Switch Version"
70 | "xraySwitchClick" = "Click on the version you want to switch"
71 | "xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
72 | "operationHours" = "Operation Hours"
73 | "operationHoursDesc" = "The running time of the system since it was started"
74 | "systemLoad" = "System Load"
75 | "connectionCount" = "Connection Count"
76 | "connectionCountDesc" = "The total number of connections for all network cards"
77 | "upSpeed" = "Total upload speed for all network cards"
78 | "downSpeed" = "Total download speed for all network cards"
79 | "totalSent" = "Total upload traffic of all network cards since system startup"
80 | "totalReceive" = "Total download traffic of all network cards since system startup"
81 | "xraySwitchVersionDialog" = "switch xray version"
82 | "xraySwitchVersionDialogDesc" = "whether to switch the xray version to"
83 | "dontRefreshh" = "Installation is in progress, please do not refresh this page"
84 |
85 |
86 | [pages.inbounds]
87 | "title" = "Inbounds"
88 | "totalDownUp" = "Total uploads/downloads"
89 | "totalUsage" = "Total usage"
90 | "inboundCount" = "Number of inbound"
91 | "operate" = "operate"
92 | "enable" = "enable"
93 | "remark" = "remark"
94 | "protocol" = "protocol"
95 | "port" = "port"
96 | "traffic" = "traffic"
97 | "details" = "details"
98 | "transportConfig" = "transport config"
99 | "expireDate" = "expire date"
100 | "resetTraffic" = "reset traffic"
101 | "addInbound" = "addInbound"
102 | "addTo" = "Add To"
103 | "revise" = "Revise"
104 | "modifyInbound" = "Modify InBound"
105 | "deleteInbound" = "Delete Inbound"
106 | "deleteInboundContent" = "Are you sure you want to delete inbound?"
107 | "resetTrafficContent" = "Are you sure you want to reset traffic?"
108 | "copyLink" = "Copy Link"
109 | "address" = "address"
110 | "network" = "network"
111 | "destinationPort" = "destination port"
112 | "targetAddress" = "target address"
113 | "disableInsecureEncryption" = "Disable insecure encryption"
114 | "monitorDesc" = "Leave blank by default"
115 | "meansNoLimit" = "means no limit"
116 | "totalFlow" = "total flow"
117 | "leaveBlankToNeverExpire" = "Leave blank to never expire"
118 | "noRecommendKeepDefault" = "There are no special requirements to keep the default"
119 | "certificatePath" = "certificate file path"
120 | "certificateContent" = "certificate file content"
121 | "publicKeyPath" = "public key file path"
122 | "publicKeyContent" = "public key content"
123 | "keyPath" = "key file path"
124 | "keyContent" = "key content"
125 |
126 | [pages.inbounds.toasts]
127 | "obtain" = "Obtain"
128 |
129 | [pages.inbounds.stream.general]
130 | "requestHeader" = "request header"
131 | "name" = "name"
132 | "value" = "value"
133 |
134 | [pages.inbounds.stream.tcp]
135 | "requestVersion" = "request version"
136 | "requestMethod" = "request method"
137 | "requestPath" = "request path"
138 | "responseVersion" = "response version"
139 | "responseStatus" = "response status"
140 | "responseStatusDescription" = "response status description"
141 | "responseHeader" = "response header"
142 |
143 | [pages.inbounds.stream.quic]
144 | "encryption" = "encryption"
145 |
146 |
147 | [pages.setting]
148 | "title" = "Setting"
149 | "save" = "Save"
150 | "restartPanel" = "Restart Panel"
151 | "restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please go to the server to view the panel log information"
152 | "panelConfig" = "Panel Configuration"
153 | "userSetting" = "User Setting"
154 | "xrayConfiguration" = "xray Configuration"
155 | "TGReminder" = "TG Reminder Related Settings"
156 | "otherSetting" = "Other Setting"
157 | "panelListeningIP" = "Panel listening IP"
158 | "panelListeningIPDesc" = "Leave blank by default to monitor all IPs, restart the panel to take effect"
159 | "panelPort" = "Panel Port"
160 | "panelPortDesc" = "Restart the panel to take effect"
161 | "publicKeyPath" = "Panel certificate public key file path"
162 | "publicKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
163 | "privateKeyPath" = "Panel certificate key file path"
164 | "privateKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
165 | "panelUrlPath" = "panel url root path"
166 | "panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect"
167 | "oldUsername" = "Current Username"
168 | "currentPassword" = "Current Password"
169 | "newUsername" = "New Username"
170 | "newPassword" = "New Password"
171 | "xrayConfigTemplate" = "xray Configuration Template"
172 | "xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect"
173 | "telegramBotEnable" = "Enable telegram bot"
174 | "telegramBotEnableDesc" = "Restart the panel to take effect"
175 | "telegramToken" = "Telegram Token"
176 | "telegramTokenDesc" = "Restart the panel to take effect"
177 | "telegramChatId" = "Telegram ChatId"
178 | "telegramChatIdDesc" = "Restart the panel to take effect"
179 | "telegramNotifyTime" = "Telegram bot notification time"
180 | "telegramNotifyTimeDesc" = "Using Crontab timing format, restart the panel to take effect"
181 | "timeZonee" = "Time Zone"
182 | "timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect"
183 |
184 | [pages.setting.toasts]
185 | "modifySetting" = "modify setting"
186 | "getSetting" = "get setting"
187 | "modifyUser" = "modify user"
188 | "originalUserPassIncorrect" = "The original user name or original password is incorrect"
189 | "userPassMustBeNotEmpty" = "New username and new password cannot be empty"
--------------------------------------------------------------------------------
/web/translation/translate.zh_Hans.toml:
--------------------------------------------------------------------------------
1 | "username" = "用户名"
2 | "password" = "密码"
3 | "login" = "登录"
4 | "confirm" = "确定"
5 | "cancel" = "取消"
6 | "close" = "关闭"
7 | "copy" = "复制"
8 | "copied" = "已复制"
9 | "download" = "下载"
10 | "remark" = "备注"
11 | "enable" = "启用"
12 | "protocol" = "协议"
13 |
14 | "loading" = "加载中"
15 | "second" = "秒"
16 | "minute" = "分钟"
17 | "hour" = "小时"
18 | "day" = "天"
19 | "check" = "查看"
20 | "indefinitely" = "无限期"
21 | "unlimited" = "无限制"
22 | "none" = "无"
23 | "qrCode" = "二维码"
24 | "edit" = "编辑"
25 | "delete" = "删除"
26 | "reset" = "重置"
27 | "copySuccess" = "复制成功"
28 | "sure" = "确定"
29 | "encryption" = "加密"
30 | "transmission" = "传输"
31 | "host" = "主持人"
32 | "path" = "小路"
33 | "camouflage" = "伪装"
34 | "turnOn" = "开启"
35 | "closure" = "关闭"
36 | "domainName" = "域名"
37 | "additional" = "额外"
38 | "monitor" = "监听"
39 | "certificate" = "证书"
40 | "fail" = "失败"
41 | "success" = "成功"
42 | "getVersion" = "获取版本"
43 | "install" = "安装"
44 |
45 | [menu]
46 | "dashboard" = "系统状态"
47 | "inbounds" = "入站列表"
48 | "setting" = "面板设置"
49 | "logout" = "退出登录"
50 | "link" = "其他"
51 |
52 | [pages.login]
53 | "title" = "登录"
54 | "loginAgain" = "登录时效已过,请重新登录"
55 |
56 | [pages.login.toasts]
57 | "invalidFormData" = "数据格式错误"
58 | "emptyUsername" = "请输入用户名"
59 | "emptyPassword" = "请输入密码"
60 | "wrongUsernameOrPassword" = "用户名或密码错误"
61 | "successLogin" = "登录"
62 |
63 | [pages.index]
64 | "title" = "系统状态"
65 | "memory" = "内存"
66 | "hard" = "硬盘"
67 | "xrayStatus" = "xray 状态"
68 | "xraySwitch" = "切换版本"
69 | "xraySwitchClick" = "点击你想切换的版本"
70 | "xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
71 | "operationHours" = "运行时间"
72 | "operationHoursDesc" = "系统自启动以来的运行时间"
73 | "systemLoad" = "系统负载"
74 | "connectionCount" = "连接数"
75 | "connectionCountDesc" = "所有网卡的总连接数"
76 | "upSpeed" = "所有网卡的总上传速度"
77 | "downSpeed" = "所有网卡的总下载速度"
78 | "totalSent" = "系统启动以来所有网卡的总上传流量"
79 | "totalReceive" = "系统启动以来所有网卡的总下载流量"
80 | "xraySwitchVersionDialog" = "切换 xray 版本"
81 | "xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
82 | "dontRefreshh" = "安装中,请不要刷新此页面"
83 |
84 |
85 | [pages.inbounds]
86 | "title" = "入站列表"
87 | "totalDownUp" = "总上传 / 下载"
88 | "totalUsage" = "总用量"
89 | "inboundCount" = "入站数量"
90 | "operate" = "操作"
91 | "enable" = "启用"
92 | "remark" = "备注"
93 | "protocol" = "协议"
94 | "port" = "端口"
95 | "traffic" = "流量"
96 | "details" = "详细信息"
97 | "transportConfig" = "传输配置"
98 | "expireDate" = "到期时间"
99 | "resetTraffic" = "重置流量"
100 | "addInbound" = "添加入"
101 | "addTo" = "添加"
102 | "revise" = "修改"
103 | "modifyInbound" = "修改入站"
104 | "deleteInbound" = "删除入站"
105 | "deleteInboundContent" = "确定要删除入站吗?"
106 | "resetTrafficContent" = "确定要重置流量吗?"
107 | "copyLink" = "复制链接"
108 | "address" = "地址"
109 | "network" = "网络"
110 | "destinationPort" = "目标端口"
111 | "targetAddress" = "目标地址"
112 | "disableInsecureEncryption" = "禁用不安全加密"
113 | "monitorDesc" = "默认留空即可"
114 | "meansNoLimit" = "表示不限制"
115 | "totalFlow" = "总流量"
116 | "leaveBlankToNeverExpire" = "留空则永不到期"
117 | "noRecommendKeepDefault" = "没有特殊需求保持默认即可"
118 | "certificatePath" = "证书文件路径"
119 | "certificateContent" = "证书文件内容"
120 | "publicKeyPath" = "公钥文件路径"
121 | "publicKeyContent" = "公钥内容"
122 | "keyPath" = "密钥文件路径"
123 | "keyContent" = "密钥内容"
124 |
125 | [pages.inbounds.toasts]
126 | "obtain" = "获取"
127 |
128 | [pages.inbounds.stream.general]
129 | "requestHeader" = "请求头"
130 | "name" = "名称"
131 | "value" = "值"
132 |
133 | [pages.inbounds.stream.tcp]
134 | "requestVersion" = "请求版本"
135 | "requestMethod" = "请求方法"
136 | "requestPath" = "请求路径"
137 | "responseVersion" = "响应版本"
138 | "responseStatus" = "响应状态"
139 | "responseStatusDescription" = "响应状态说明"
140 | "responseHeader" = "响应头"
141 |
142 | [pages.inbounds.stream.quic]
143 | "encryption" = "加密"
144 |
145 |
146 | [pages.setting]
147 | "title" = "设置"
148 | "save" = "保存配置"
149 | "restartPanel" = "重启面板"
150 | "restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
151 | "panelConfig" = "面板配置"
152 | "userSetting" = "用户设置"
153 | "xrayConfiguration" = "xray 相关设置"
154 | "TGReminder" = "TG提醒相关设置"
155 | "otherSetting" = "其他设置"
156 | "panelListeningIP" = "面板监听 IP"
157 | "panelListeningIPDesc" = "默认留空监听所有 IP,重启面板生效"
158 | "panelPort" = "面板监听端口"
159 | "panelPortDesc" = "重启面板生效"
160 | "publicKeyPath" = "面板证书公钥文件路径"
161 | "publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效"
162 | "privateKeyPath" = "面板证书密钥文件路径"
163 | "privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效"
164 | "panelUrlPath" = "面板 url 根路径"
165 | "panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾,重启面板生效"
166 | "oldUsername" = "原用户名"
167 | "currentPassword" = "原密码"
168 | "newUsername" = "新用户名"
169 | "newPassword" = "新密码"
170 | "xrayConfigTemplate" = "xray 配置模版"
171 | "xrayConfigTemplateDesc" = "以该模版为基础生成最终的 xray 配置文件,重启面板生效"
172 | "telegramBotEnable" = "启用电报机器人"
173 | "telegramBotEnableDesc" = "重启面板生效"
174 | "telegramToken" = "电报机器人TOKEN"
175 | "telegramTokenDesc" = "重启面板生效"
176 | "telegramChatId" = "电报机器人ChatId"
177 | "telegramChatIdDesc" = "重启面板生效"
178 | "telegramNotifyTime" = "电报机器人通知时间"
179 | "telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
180 | "timeZonee" = "时区"
181 | "timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
182 |
183 | [pages.setting.toasts]
184 | "modifySetting" = "修改设置"
185 | "getSetting" = "获取设置"
186 | "modifyUser" = "修改用户"
187 | "originalUserPassIncorrect" = "原用户名或原密码错误"
188 | "userPassMustBeNotEmpty" = "新用户名和新密码不能为空"
189 |
190 |
--------------------------------------------------------------------------------
/web/translation/translate.zh_Hant.toml:
--------------------------------------------------------------------------------
1 | "username" = "用戶名"
2 | "password" = "密碼"
3 | "login" = "登錄"
4 | "confirm" = "確定"
5 | "cancel" = "取消"
6 | "close" = "關閉"
7 | "copy" = "複製"
8 | "copied" = "已複製"
9 | "download" = "下載"
10 | "remark" = "備註"
11 | "enable" = "啟用"
12 | "protocol" = "協議"
13 |
14 | [menu]
15 | "dashboard" = "系统状态"
16 | "inbounds" = "入站列表"
17 | "setting" = "面板设置"
18 | "logout" = "退出登录"
19 | "link" = "其他"
20 |
21 | [pages.login]
22 | "title" = "登錄"
23 |
24 | [pages.login.toasts]
25 | "invalidFormData" = "数据格式错误"
26 | "emptyUsername" = "请输入用户名"
27 | "emptyPassword" = "请输入密码"
28 | "wrongUsernameOrPassword" = "用户名或密码错误"
29 | "successLogin" = "登录"
30 |
31 | [pages.index]
32 | "title" = "系统状态"
33 |
34 | [pages.inbounds]
35 | "title" = "入站列表"
36 |
37 | [pages.inbounds.stream.general]
38 | "requestHeader" = "request header"
39 | "name" = "name"
40 | "value" = "value"
41 |
42 | [pages.inbounds.stream.tcp]
43 | "requestVersion" = "request version"
44 | "requestMethod" = "request method"
45 | "requestPath" = "request path"
46 | "responseVersion" = "response version"
47 | "responseStatus" = "response status"
48 | "responseStatusDescription" = "response status description"
49 | "responseHeader" = "response header"
50 |
51 | [pages.inbounds.stream.quic]
52 | "encryption" = "encryption"
53 |
54 | [pages.setting]
55 | "title" = "设置"
--------------------------------------------------------------------------------
/x-ui.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=x-ui Service
3 | After=network.target
4 | Wants=network.target
5 |
6 | [Service]
7 | Environment="XRAY_VMESS_AEAD_FORCED=false"
8 | Type=simple
9 | WorkingDirectory=/usr/local/x-ui/
10 | ExecStart=/usr/local/x-ui/x-ui
11 |
12 | [Install]
13 | WantedBy=multi-user.target
--------------------------------------------------------------------------------
/xray/client_traffic.go:
--------------------------------------------------------------------------------
1 | package xray
2 |
3 | type ClientTraffic struct {
4 | Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
5 | InboundId int `json:"inboundId" form:"inboundId"`
6 | Enable bool `json:"enable" form:"enable"`
7 | Email string `json:"email" form:"email" gorm:"unique"`
8 | Up int64 `json:"up" form:"up"`
9 | Down int64 `json:"down" form:"down"`
10 | ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
11 | Total int64 `json:"total" form:"total"`
12 | }
13 |
--------------------------------------------------------------------------------
/xray/config.go:
--------------------------------------------------------------------------------
1 | package xray
2 |
3 | import (
4 | "bytes"
5 | "x-ui/util/json_util"
6 | )
7 |
8 | type Config struct {
9 | LogConfig json_util.RawMessage `json:"log"`
10 | RouterConfig json_util.RawMessage `json:"routing"`
11 | DNSConfig json_util.RawMessage `json:"dns"`
12 | InboundConfigs []InboundConfig `json:"inbounds"`
13 | OutboundConfigs json_util.RawMessage `json:"outbounds"`
14 | Transport json_util.RawMessage `json:"transport"`
15 | Policy json_util.RawMessage `json:"policy"`
16 | API json_util.RawMessage `json:"api"`
17 | Stats json_util.RawMessage `json:"stats"`
18 | Reverse json_util.RawMessage `json:"reverse"`
19 | FakeDNS json_util.RawMessage `json:"fakeDns"`
20 | }
21 |
22 | func (c *Config) Equals(other *Config) bool {
23 | if len(c.InboundConfigs) != len(other.InboundConfigs) {
24 | return false
25 | }
26 | for i, inbound := range c.InboundConfigs {
27 | if !inbound.Equals(&other.InboundConfigs[i]) {
28 | return false
29 | }
30 | }
31 | if !bytes.Equal(c.LogConfig, other.LogConfig) {
32 | return false
33 | }
34 | if !bytes.Equal(c.RouterConfig, other.RouterConfig) {
35 | return false
36 | }
37 | if !bytes.Equal(c.DNSConfig, other.DNSConfig) {
38 | return false
39 | }
40 | if !bytes.Equal(c.OutboundConfigs, other.OutboundConfigs) {
41 | return false
42 | }
43 | if !bytes.Equal(c.Transport, other.Transport) {
44 | return false
45 | }
46 | if !bytes.Equal(c.Policy, other.Policy) {
47 | return false
48 | }
49 | if !bytes.Equal(c.API, other.API) {
50 | return false
51 | }
52 | if !bytes.Equal(c.Stats, other.Stats) {
53 | return false
54 | }
55 | if !bytes.Equal(c.Reverse, other.Reverse) {
56 | return false
57 | }
58 | if !bytes.Equal(c.FakeDNS, other.FakeDNS) {
59 | return false
60 | }
61 | return true
62 | }
63 |
--------------------------------------------------------------------------------
/xray/inbound.go:
--------------------------------------------------------------------------------
1 | package xray
2 |
3 | import (
4 | "bytes"
5 | "x-ui/util/json_util"
6 | )
7 |
8 | type InboundConfig struct {
9 | Listen json_util.RawMessage `json:"listen"` // listen 不能为空字符串
10 | Port int `json:"port"`
11 | Protocol string `json:"protocol"`
12 | Settings json_util.RawMessage `json:"settings"`
13 | StreamSettings json_util.RawMessage `json:"streamSettings"`
14 | Tag string `json:"tag"`
15 | Sniffing json_util.RawMessage `json:"sniffing"`
16 | }
17 |
18 | func (c *InboundConfig) Equals(other *InboundConfig) bool {
19 | if !bytes.Equal(c.Listen, other.Listen) {
20 | return false
21 | }
22 | if c.Port != other.Port {
23 | return false
24 | }
25 | if c.Protocol != other.Protocol {
26 | return false
27 | }
28 | if !bytes.Equal(c.Settings, other.Settings) {
29 | return false
30 | }
31 | if !bytes.Equal(c.StreamSettings, other.StreamSettings) {
32 | return false
33 | }
34 | if c.Tag != other.Tag {
35 | return false
36 | }
37 | if !bytes.Equal(c.Sniffing, other.Sniffing) {
38 | return false
39 | }
40 | return true
41 | }
42 |
--------------------------------------------------------------------------------
/xray/process.go:
--------------------------------------------------------------------------------
1 | package xray
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "context"
7 | "encoding/json"
8 | "errors"
9 | "fmt"
10 | "io/fs"
11 | "os"
12 | "os/exec"
13 | "regexp"
14 | "runtime"
15 | "strings"
16 | "time"
17 | "x-ui/util/common"
18 |
19 | "github.com/Workiva/go-datastructures/queue"
20 | statsservice "github.com/xtls/xray-core/app/stats/command"
21 | "google.golang.org/grpc"
22 | )
23 |
24 | var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
25 | var ClientTrafficRegex = regexp.MustCompile("(user)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
26 |
27 | func GetBinaryName() string {
28 | return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH)
29 | }
30 |
31 | func GetBinaryPath() string {
32 | return "bin/" + GetBinaryName()
33 | }
34 |
35 | func GetConfigPath() string {
36 | return "bin/config.json"
37 | }
38 |
39 | func GetGeositePath() string {
40 | return "bin/geosite.dat"
41 | }
42 |
43 | func GetGeoipPath() string {
44 | return "bin/geoip.dat"
45 | }
46 |
47 | func stopProcess(p *Process) {
48 | p.Stop()
49 | }
50 |
51 | type Process struct {
52 | *process
53 | }
54 |
55 | func NewProcess(xrayConfig *Config) *Process {
56 | p := &Process{newProcess(xrayConfig)}
57 | runtime.SetFinalizer(p, stopProcess)
58 | return p
59 | }
60 |
61 | type process struct {
62 | cmd *exec.Cmd
63 |
64 | version string
65 | apiPort int
66 |
67 | config *Config
68 | lines *queue.Queue
69 | exitErr error
70 | }
71 |
72 | func newProcess(config *Config) *process {
73 | return &process{
74 | version: "Unknown",
75 | config: config,
76 | lines: queue.New(100),
77 | }
78 | }
79 |
80 | func (p *process) IsRunning() bool {
81 | if p.cmd == nil || p.cmd.Process == nil {
82 | return false
83 | }
84 | if p.cmd.ProcessState == nil {
85 | return true
86 | }
87 | return false
88 | }
89 |
90 | func (p *process) GetErr() error {
91 | return p.exitErr
92 | }
93 |
94 | func (p *process) GetResult() string {
95 | if p.lines.Empty() && p.exitErr != nil {
96 | return p.exitErr.Error()
97 | }
98 | items, _ := p.lines.TakeUntil(func(item interface{}) bool {
99 | return true
100 | })
101 | lines := make([]string, 0, len(items))
102 | for _, item := range items {
103 | lines = append(lines, item.(string))
104 | }
105 | return strings.Join(lines, "\n")
106 | }
107 |
108 | func (p *process) GetVersion() string {
109 | return p.version
110 | }
111 |
112 | func (p *Process) GetAPIPort() int {
113 | return p.apiPort
114 | }
115 |
116 | func (p *Process) GetConfig() *Config {
117 | return p.config
118 | }
119 |
120 | func (p *process) refreshAPIPort() {
121 | for _, inbound := range p.config.InboundConfigs {
122 | if inbound.Tag == "api" {
123 | p.apiPort = inbound.Port
124 | break
125 | }
126 | }
127 | }
128 |
129 | func (p *process) refreshVersion() {
130 | cmd := exec.Command(GetBinaryPath(), "-version")
131 | data, err := cmd.Output()
132 | if err != nil {
133 | p.version = "Unknown"
134 | } else {
135 | datas := bytes.Split(data, []byte(" "))
136 | if len(datas) <= 1 {
137 | p.version = "Unknown"
138 | } else {
139 | p.version = string(datas[1])
140 | }
141 | }
142 | }
143 |
144 | func (p *process) Start() (err error) {
145 | if p.IsRunning() {
146 | return errors.New("xray is already running")
147 | }
148 |
149 | defer func() {
150 | if err != nil {
151 | p.exitErr = err
152 | }
153 | }()
154 |
155 | data, err := json.MarshalIndent(p.config, "", " ")
156 | if err != nil {
157 | return common.NewErrorf("生成 xray 配置文件失败: %v", err)
158 | }
159 | configPath := GetConfigPath()
160 | err = os.WriteFile(configPath, data, fs.ModePerm)
161 | if err != nil {
162 | return common.NewErrorf("写入配置文件失败: %v", err)
163 | }
164 |
165 | cmd := exec.Command(GetBinaryPath(), "-c", configPath, "-restrictedIPsPath", "./bin/blockedIPs")
166 | p.cmd = cmd
167 |
168 | stdReader, err := cmd.StdoutPipe()
169 | if err != nil {
170 | return err
171 | }
172 | errReader, err := cmd.StderrPipe()
173 | if err != nil {
174 | return err
175 | }
176 |
177 | go func() {
178 | defer func() {
179 | common.Recover("")
180 | stdReader.Close()
181 | }()
182 | reader := bufio.NewReaderSize(stdReader, 8192)
183 | for {
184 | line, _, err := reader.ReadLine()
185 | if err != nil {
186 | return
187 | }
188 | if p.lines.Len() >= 100 {
189 | p.lines.Get(1)
190 | }
191 | p.lines.Put(string(line))
192 | }
193 | }()
194 |
195 | go func() {
196 | defer func() {
197 | common.Recover("")
198 | errReader.Close()
199 | }()
200 | reader := bufio.NewReaderSize(errReader, 8192)
201 | for {
202 | line, _, err := reader.ReadLine()
203 | if err != nil {
204 | return
205 | }
206 | if p.lines.Len() >= 100 {
207 | p.lines.Get(1)
208 | }
209 | p.lines.Put(string(line))
210 | }
211 | }()
212 |
213 | go func() {
214 | err := cmd.Run()
215 | if err != nil {
216 | p.exitErr = err
217 | }
218 | }()
219 |
220 | p.refreshVersion()
221 | p.refreshAPIPort()
222 |
223 | return nil
224 | }
225 |
226 | func (p *process) Stop() error {
227 | if !p.IsRunning() {
228 | return errors.New("xray is not running")
229 | }
230 | return p.cmd.Process.Kill()
231 | }
232 |
233 | func (p *process) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
234 | if p.apiPort == 0 {
235 | return nil, nil, common.NewError("xray api port wrong:", p.apiPort)
236 | }
237 | conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%v", p.apiPort), grpc.WithInsecure())
238 | if err != nil {
239 | return nil, nil, err
240 | }
241 | defer conn.Close()
242 |
243 | client := statsservice.NewStatsServiceClient(conn)
244 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
245 | defer cancel()
246 | request := &statsservice.QueryStatsRequest{
247 | Reset_: reset,
248 | }
249 | resp, err := client.QueryStats(ctx, request)
250 | if err != nil {
251 | return nil, nil, err
252 | }
253 | tagTrafficMap := map[string]*Traffic{}
254 | emailTrafficMap := map[string]*ClientTraffic{}
255 |
256 | clientTraffics := make([]*ClientTraffic, 0)
257 | traffics := make([]*Traffic, 0)
258 | for _, stat := range resp.GetStat() {
259 | matchs := trafficRegex.FindStringSubmatch(stat.Name)
260 | if len(matchs) < 3 {
261 |
262 | matchs := ClientTrafficRegex.FindStringSubmatch(stat.Name)
263 | if len(matchs) < 3 {
264 | continue
265 | }else {
266 |
267 | isUser := matchs[1] == "user"
268 | email := matchs[2]
269 | isDown := matchs[3] == "downlink"
270 | if ! isUser {
271 | continue
272 | }
273 | traffic, ok := emailTrafficMap[email]
274 | if !ok {
275 | traffic = &ClientTraffic{
276 | Email: email,
277 | }
278 | emailTrafficMap[email] = traffic
279 | clientTraffics = append(clientTraffics, traffic)
280 | }
281 | if isDown {
282 | traffic.Down = stat.Value
283 | } else {
284 | traffic.Up = stat.Value
285 | }
286 |
287 | }
288 | continue
289 | }
290 | isInbound := matchs[1] == "inbound"
291 | tag := matchs[2]
292 | isDown := matchs[3] == "downlink"
293 | if tag == "api" {
294 | continue
295 | }
296 | traffic, ok := tagTrafficMap[tag]
297 | if !ok {
298 | traffic = &Traffic{
299 | IsInbound: isInbound,
300 | Tag: tag,
301 | }
302 | tagTrafficMap[tag] = traffic
303 | traffics = append(traffics, traffic)
304 | }
305 | if isDown {
306 | traffic.Down = stat.Value
307 | } else {
308 | traffic.Up = stat.Value
309 | }
310 | }
311 |
312 | return traffics, clientTraffics, nil
313 | }
314 |
--------------------------------------------------------------------------------
/xray/traffic.go:
--------------------------------------------------------------------------------
1 | package xray
2 |
3 | type Traffic struct {
4 | IsInbound bool
5 | Tag string
6 | Up int64
7 | Down int64
8 | }
9 |
--------------------------------------------------------------------------------
/xray_core_config/xray core config.txt:
--------------------------------------------------------------------------------
1 | {
2 | "log": {
3 | "loglevel": "warning",
4 | "access": "./access.log"
5 | },
6 |
7 | "api": {
8 | "services": [
9 | "HandlerService",
10 | "LoggerService",
11 | "StatsService"
12 | ],
13 | "tag": "api"
14 | },
15 | "inbounds": [
16 | {
17 | "listen": "127.0.0.1",
18 | "port": 62789,
19 | "protocol": "dokodemo-door",
20 | "settings": {
21 | "address": "127.0.0.1"
22 | },
23 | "tag": "api"
24 | }
25 | ],
26 | "outbounds": [
27 | {
28 | "protocol": "freedom",
29 | "settings": {}
30 | },
31 | {
32 | "protocol": "blackhole",
33 | "settings": {},
34 | "tag": "blocked"
35 | }
36 | ],
37 | "policy": {
38 | "levels": {
39 | "0": {
40 | "statsUserUplink": true,
41 | "statsUserDownlink": true
42 | }
43 | },
44 | "system": {
45 | "statsInboundDownlink": true,
46 | "statsInboundUplink": true
47 | }
48 | },
49 | "routing": {
50 | "domainStrategy": "IPIfNonMatch",
51 | "rules": [
52 | {
53 | "inboundTag": [
54 | "api"
55 | ],
56 | "outboundTag": "api",
57 | "type": "field"
58 | },
59 |
60 | {
61 | "domain": [
62 | "geosite:category-ads-all",
63 | "geosite:category-porn",
64 | "geosite:category-ir",
65 | "geosite:cn"
66 | ],
67 | "outboundTag": "blocked",
68 | "type": "field"
69 | },
70 |
71 | {
72 | "ip": [
73 | "geoip:cn",
74 | "geoip:ir",
75 | "geoip:private"
76 | ],
77 | "outboundTag": "blocked",
78 | "type": "field"
79 | },
80 | {
81 | "protocol": [
82 | "bittorrent"
83 | ],
84 | "outboundTag": "blocked",
85 | "type": "field"
86 | }
87 | ]
88 | },
89 | "stats": {}
90 | }
91 |
--------------------------------------------------------------------------------
/xray_core_config/xui panel config block ir ads pushiran.txt:
--------------------------------------------------------------------------------
1 | {
2 | "log": {
3 | "loglevel": "none",
4 | "access": "./access.log"
5 | },
6 |
7 | "api": {
8 | "services": [
9 | "HandlerService",
10 | "LoggerService",
11 | "StatsService"
12 | ],
13 | "tag": "api"
14 | },
15 | "inbounds": [
16 | {
17 | "listen": "127.0.0.1",
18 | "port": 62789,
19 | "protocol": "dokodemo-door",
20 | "settings": {
21 | "address": "127.0.0.1"
22 | },
23 | "tag": "api"
24 | }
25 | ],
26 | "outbounds": [
27 | {
28 | "protocol": "freedom",
29 | "settings": {},
30 | "tag": "direct"
31 | },
32 | {
33 | "protocol": "blackhole",
34 | "settings": {},
35 | "tag": "blocked"
36 | }
37 | ],
38 | "policy": {
39 | "levels": {
40 | "0": {
41 | "statsUserUplink": true,
42 | "statsUserDownlink": true
43 | }
44 | },
45 | "system": {
46 | "statsInboundDownlink": true,
47 | "statsInboundUplink": true
48 | }
49 | },
50 | "routing": {
51 | "domainStrategy": "IPIfNonMatch",
52 | "rules": [
53 | {
54 | "inboundTag": [
55 | "api"
56 | ],
57 | "outboundTag": "api",
58 | "type": "field"
59 | },
60 |
61 | {
62 | "domain": [
63 | "geosite:category-ads-all",
64 | "geosite:category-porn",
65 | "geosite:category-ir",
66 | "geosite:cn",
67 | "dl2.learnasan.ir",
68 | "pushiran.com",
69 | "rtellservers.com",
70 | "p86.ir",
71 | "adsnovin.net",
72 | "adsnovin.ir",
73 | "adsnovin.com",
74 | "vira-s1.com",
75 | "vira-tel.ir",
76 | "paydane.ir",
77 | "ccibp.ir",
78 | "adnamaa.ir",
79 | "raz-network.ir",
80 | "raaz.co",
81 | "pushekhtesasi.com",
82 | "pushnotificationws.com",
83 | "vira-tel.ir",
84 | "pushfa.com"
85 | ],
86 | "outboundTag": "blocked",
87 | "type": "field"
88 | },
89 |
90 | {
91 | "ip": [
92 | "geoip:cn",
93 | "geoip:ir",
94 | "geoip:private",
95 | "141.8.224.183",
96 | "52.213.114.86",
97 | "51.38.11.229",
98 | "141.105.69.168",
99 | "199.127.99.12",
100 | "141.105.69.162",
101 | "148.251.189.249",
102 | "176.31.82.42",
103 | "185.55.226.20",
104 | "185.94.97.54",
105 | "109.169.76.38",
106 | "206.191.152.58"
107 | ],
108 | "outboundTag": "blocked",
109 | "type": "field"
110 | },
111 | {
112 | "protocol": [
113 | "bittorrent"
114 | ],
115 | "outboundTag": "blocked",
116 | "type": "field"
117 | }
118 | ]
119 | },
120 | "stats": {}
121 | }
122 |
--------------------------------------------------------------------------------