diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f7d7d117a..1dd0e5eb34 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -144,4 +144,4 @@ jobs: upload_url: ${{ needs.release.outputs.upload_url }} asset_path: x-ui-linux-s390x.tar.gz asset_name: x-ui-linux-s390x.tar.gz - asset_content_type: application/gzip + asset_content_type: application/gzip \ No newline at end of file diff --git a/README.md b/README.md index cc1b9d9a97..6a93135a2d 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,10 @@ - 流量统计,限制流量,限制到期时间 - 可自定义 xray 配置模板 - 支持 https 访问面板(自备域名 + ssl 证书) +- 支持一键SSL证书申请且自动续签 - 更多高级配置项,详见面板 + # 安装&升级 ``` bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh) @@ -56,6 +58,44 @@ docker run -itd --network=host \ ```shell docker build -t x-ui . ``` +## SSL证书申请 +>此功能与教程由[FranzKafkaYu](https://github.com/FranzKafkaYu)提供 + +脚本内置SSL证书申请功能,使用该脚本申请证书,需满足以下条件: +- 知晓Cloudflare 注册邮箱 +- 知晓Cloudflare Global API Key +- 域名已通过cloudflare进行解析到当前服务器 + +获取Cloudflare Global API Key的方法: + ![](media/bda84fbc2ede834deaba1c173a932223.png) + ![](media/d13ffd6a73f938d1037d0708e31433bf.png) + +使用时只需输入`域名`, `邮箱`, `API KEY`即可,示意图如下: + ![](media/2022-04-04_141259.png) + +注意事项: +- 该脚本使用DNS API进行证书申请 +- 默认使用Let'sEncrypt作为CA方 +- 证书安装目录为/root/cert目录 +- 本脚本申请证书均为泛域名证书 + +## Tg机器人使用 +>此功能与教程由[FranzKafkaYu](https://github.com/FranzKafkaYu)提供 + +X-UI支持通过Tg机器人实现每日流量通知,面板登录提醒等功能,使用Tg机器人,需要自行申请 +具体申请教程可以参考[博客链接](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html) +使用说明:在面板后台或通过脚本设置机器人相关参数,具体包括 +- Tg机器人Token +- Tg机器人ChatId +- Tg机器人周期运行时间,采用crontab语法 + +参考示例: + 每小时定时通知 + ![](media/2022-04-17_110907.png) + 每分钟的第30s通知 + ![](media/2022-04-17_111321.png) + 效果示意图: + ![](media/2022-04-17_111705.png) ## 建议系统 - CentOS 7+ diff --git a/go.mod b/go.mod index ac32e09f84..4e8d00d25a 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/gin-contrib/sessions v0.0.3 github.com/gin-gonic/gin v1.7.1 github.com/go-ole/go-ole v1.2.5 // indirect + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect github.com/nicksnyder/go-i18n/v2 v2.1.2 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/robfig/cron/v3 v3.0.1 diff --git a/go.sum b/go.sum index 8bb9795858..c8ed240f17 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= diff --git a/install.sh b/install.sh index e1e1f2f516..d6ffd104e3 100755 --- a/install.sh +++ b/install.sh @@ -83,15 +83,15 @@ install_base() { #This function will be called when user installed x-ui out of sercurity config_after_install() { - echo -e "${yellow}出于安全考虑,安装完成后需要强制修改端口与账户密码${plain}" - read -p "请设置您的账户名:" config_account - echo -e "${yellow}您的账户名将设定为:${config_account}${plain}" - read -p "请设置您的账户密码:" config_password - echo -e "${yellow}您的账户密码将设定为:${config_password}${plain}" - read -p "请设置面板访问端口:" config_port - echo -e "${yellow}您的面板访问端口将设定为:${config_port}${plain}" - read -p "确认设定完成?[y/n]": config_confirm + echo -e "${yellow}出于安全考虑,安装/更新完成后需要强制修改端口与账户密码${plain}" + read -p "确认是否继续?[y/n]": config_confirm if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then + read -p "请设置您的账户名:" config_account + echo -e "${yellow}您的账户名将设定为:${config_account}${plain}" + read -p "请设置您的账户密码:" config_password + echo -e "${yellow}您的账户密码将设定为:${config_password}${plain}" + read -p "请设置面板访问端口:" config_port + echo -e "${yellow}您的面板访问端口将设定为:${config_port}${plain}" echo -e "${yellow}确认设定,设定中${plain}" /usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password} echo -e "${yellow}账户密码设定完成${plain}" diff --git a/main.go b/main.go index b993342dea..fb75ffd55a 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "flag" "fmt" - "github.com/op/go-logging" "log" "os" "os/signal" @@ -16,6 +15,8 @@ import ( "x-ui/web" "x-ui/web/global" "x-ui/web/service" + + "github.com/op/go-logging" ) func runWebServer() { @@ -50,6 +51,7 @@ func runWebServer() { } sigCh := make(chan os.Signal, 1) + //信号量捕获处理 signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL) for { sig := <-sigCh @@ -90,6 +92,90 @@ func resetSetting() { } } +func showSetting(show bool) { + if show { + settingService := service.SettingService{} + port, err := settingService.GetPort() + if err != nil { + fmt.Println("get current port fialed,error info:", err) + } + userService := service.UserService{} + userModel, err := userService.GetFirstUser() + if err != nil { + fmt.Println("get current user info failed,error info:", err) + } + username := userModel.Username + userpasswd := userModel.Password + if (username == "") || (userpasswd == "") { + fmt.Println("current username or password is empty") + } + fmt.Println("current pannel settings as follows:") + fmt.Println("username:", username) + fmt.Println("userpasswd:", userpasswd) + fmt.Println("port:", port) + } +} + +func updateTgbotEnableSts(status bool) { + settingService := service.SettingService{} + currentTgSts, err := settingService.GetTgbotenabled() + if err != nil { + fmt.Println(err) + return + } + logger.Infof("current enabletgbot status[%v],need update to status[%v]", currentTgSts, status) + if currentTgSts != status { + err := settingService.SetTgbotenabled(status) + if err != nil { + fmt.Println(err) + return + } else { + logger.Infof("SetTgbotenabled[%v] success", status) + } + } + return +} + +func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string) { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println(err) + return + } + + settingService := service.SettingService{} + + if tgBotToken != "" { + err := settingService.SetTgBotToken(tgBotToken) + if err != nil { + fmt.Println(err) + return + } else { + logger.Info("updateTgbotSetting tgBotToken success") + } + } + + if tgBotRuntime != "" { + err := settingService.SetTgbotRuntime(tgBotRuntime) + if err != nil { + fmt.Println(err) + return + } else { + logger.Infof("updateTgbotSetting tgBotRuntime[%s] success", tgBotRuntime) + } + } + + if tgBotChatid != 0 { + err := settingService.SetTgBotChatId(tgBotChatid) + if err != nil { + fmt.Println(err) + return + } else { + logger.Info("updateTgbotSetting tgBotChatid success") + } + } +} + func updateSetting(port int, username string, password string) { err := database.InitDB(config.GetDBPath()) if err != nil { @@ -137,11 +223,21 @@ func main() { var port int var username string var password string + var tgbottoken string + var tgbotchatid int + var enabletgbot bool + var tgbotRuntime string var reset bool - settingCmd.BoolVar(&reset, "reset", false, "reset all setting") + var show bool + settingCmd.BoolVar(&reset, "reset", false, "reset all settings") + settingCmd.BoolVar(&show, "show", false, "show current settings") settingCmd.IntVar(&port, "port", 0, "set panel port") settingCmd.StringVar(&username, "username", "", "set login username") settingCmd.StringVar(&password, "password", "", "set login password") + settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token") + settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time") + settingCmd.IntVar(&tgbotchatid, "tgbotchatid", 0, "set telegrame bot chat id") + settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify") oldUsage := flag.Usage flag.Usage = func() { @@ -188,6 +284,13 @@ func main() { } else { updateSetting(port, username, password) } + if show { + showSetting(show) + } + updateTgbotEnableSts(enabletgbot) + if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") { + updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime) + } default: fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands") fmt.Println() diff --git a/media/2022-04-04_141259.png b/media/2022-04-04_141259.png new file mode 100644 index 0000000000..aaa4664431 Binary files /dev/null and b/media/2022-04-04_141259.png differ diff --git a/media/2022-04-17_110907.png b/media/2022-04-17_110907.png new file mode 100644 index 0000000000..a4dd4ea2ea Binary files /dev/null and b/media/2022-04-17_110907.png differ diff --git a/media/2022-04-17_111321.png b/media/2022-04-17_111321.png new file mode 100644 index 0000000000..3f8bc34cec Binary files /dev/null and b/media/2022-04-17_111321.png differ diff --git a/media/2022-04-17_111705.png b/media/2022-04-17_111705.png new file mode 100644 index 0000000000..0ffcb8001f Binary files /dev/null and b/media/2022-04-17_111705.png differ diff --git a/media/2022-04-17_111910.png b/media/2022-04-17_111910.png new file mode 100644 index 0000000000..a8396b3545 Binary files /dev/null and b/media/2022-04-17_111910.png differ diff --git a/media/bda84fbc2ede834deaba1c173a932223.png b/media/bda84fbc2ede834deaba1c173a932223.png new file mode 100644 index 0000000000..9cc3c380d0 Binary files /dev/null and b/media/bda84fbc2ede834deaba1c173a932223.png differ diff --git a/media/d13ffd6a73f938d1037d0708e31433bf.png b/media/d13ffd6a73f938d1037d0708e31433bf.png new file mode 100644 index 0000000000..b08277aaf5 Binary files /dev/null and b/media/d13ffd6a73f938d1037d0708e31433bf.png differ diff --git a/util/common/format.go b/util/common/format.go new file mode 100644 index 0000000000..145669084c --- /dev/null +++ b/util/common/format.go @@ -0,0 +1,21 @@ +package common + +import ( + "fmt" +) + +func FormatTraffic(trafficBytes int64) (size string) { + if trafficBytes < 1024 { + return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1)) + } else if trafficBytes < (1024 * 1024) { + return fmt.Sprintf("%.2fKB", float64(trafficBytes)/float64(1024)) + } else if trafficBytes < (1024 * 1024 * 1024) { + return fmt.Sprintf("%.2fMB", float64(trafficBytes)/float64(1024*1024)) + } else if trafficBytes < (1024 * 1024 * 1024 * 1024) { + return fmt.Sprintf("%.2fGB", float64(trafficBytes)/float64(1024*1024*1024)) + } else if trafficBytes < (1024 * 1024 * 1024 * 1024 * 1024) { + return fmt.Sprintf("%.2fTB", float64(trafficBytes)/float64(1024*1024*1024*1024)) + } else { + return fmt.Sprintf("%.2fEB", float64(trafficBytes)/float64(1024*1024*1024*1024*1024)) + } +} diff --git a/util/common/stringUtil.go b/util/common/stringUtil.go new file mode 100644 index 0000000000..5f1f93fd4b --- /dev/null +++ b/util/common/stringUtil.go @@ -0,0 +1,9 @@ +package common + +import "sort" + +func IsSubString(target string, str_array []string) bool { + sort.Strings(str_array) + index := sort.SearchStrings(str_array, target) + return index < len(str_array) && str_array[index] == target +} diff --git a/web/assets/js/model/models.js b/web/assets/js/model/models.js index 4d26f1f08a..e00e05655b 100644 --- a/web/assets/js/model/models.js +++ b/web/assets/js/model/models.js @@ -163,7 +163,9 @@ class AllSetting { this.webCertFile = ""; this.webKeyFile = ""; this.webBasePath = "/"; - + this.tgBotToken = ""; + this.tgBotChatId = 0; + this.tgRunTime = ""; this.xrayTemplateConfig = ""; this.timeLocation = "Asia/Shanghai"; diff --git a/web/controller/index.go b/web/controller/index.go index 20887072d4..fd21f6b494 100644 --- a/web/controller/index.go +++ b/web/controller/index.go @@ -1,11 +1,14 @@ package controller import ( - "github.com/gin-gonic/gin" "net/http" + "time" "x-ui/logger" + "x-ui/web/job" "x-ui/web/service" "x-ui/web/session" + + "github.com/gin-gonic/gin" ) type LoginForm struct { @@ -59,6 +62,10 @@ func (a *IndexController) login(c *gin.Context) { logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password) pureJsonMsg(c, false, "用户名或密码错误") return + } else { + timeStr := time.Now().Format("2006-01-02 15:04:05") + logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c)) + job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr) } err = session.SetLoginUser(c, user) diff --git a/web/entity/entity.go b/web/entity/entity.go index 99a8af8883..9f7bfa04a8 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -27,12 +27,14 @@ type Pager struct { } type AllSetting struct { - WebListen string `json:"webListen" form:"webListen"` - WebPort int `json:"webPort" form:"webPort"` - WebCertFile string `json:"webCertFile" form:"webCertFile"` - WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` - WebBasePath string `json:"webBasePath" form:"webBasePath"` - + WebListen string `json:"webListen" form:"webListen"` + WebPort int `json:"webPort" form:"webPort"` + WebCertFile string `json:"webCertFile" form:"webCertFile"` + WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` + WebBasePath string `json:"webBasePath" form:"webBasePath"` + TgBotToken string `json:"tgBotToken" form:"tgBotToken"` + TgBotChatId int `json:"tgBotChatId" form:"tgBotChatId"` + TgRunTime string `json:"tgRunTime" form:"tgRunTime"` XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"` TimeLocation string `json:"timeLocation" form:"timeLocation"` diff --git a/web/html/xui/setting.html b/web/html/xui/setting.html index 104ab43ab7..1082f5a7a8 100644 --- a/web/html/xui/setting.html +++ b/web/html/xui/setting.html @@ -71,7 +71,14 @@ - + + + + + + + + diff --git a/web/job/stats_notify_job.go b/web/job/stats_notify_job.go new file mode 100644 index 0000000000..c245211768 --- /dev/null +++ b/web/job/stats_notify_job.go @@ -0,0 +1,121 @@ +package job + +import ( + "fmt" + "net" + "os" + "x-ui/logger" + "x-ui/util/common" + "x-ui/web/service" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +type StatsNotifyJob struct { + enable bool + xrayService service.XrayService + inboundService service.InboundService + settingService service.SettingService +} + +func NewStatsNotifyJob() *StatsNotifyJob { + return new(StatsNotifyJob) +} + +func (j *StatsNotifyJob) SendMsgToTgbot(msg string) { + //Telegram bot basic info + tgBottoken, err := j.settingService.GetTgBotToken() + if err != nil { + logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err) + return + } + tgBotid, err := j.settingService.GetTgBotChatId() + if err != nil { + logger.Warning("sendMsgToTgbot failed,GetTgBotChatId fail:", err) + return + } + + bot, err := tgbotapi.NewBotAPI(tgBottoken) + if err != nil { + fmt.Println("get tgbot error:", err) + return + } + bot.Debug = true + fmt.Printf("Authorized on account %s", bot.Self.UserName) + info := tgbotapi.NewMessage(int64(tgBotid), msg) + //msg.ReplyToMessageID = int(tgBotid) + bot.Send(info) +} + +//Here run is a interface method of Job interface +func (j *StatsNotifyJob) Run() { + if !j.xrayService.IsXrayRunning() { + return + } + var info string + //get hostname + name, err := os.Hostname() + if err != nil { + fmt.Println("get hostname error:", err) + return + } + info = fmt.Sprintf("主机名称:%s\r\n", name) + //get ip address + var ip string + netInterfaces, err := net.Interfaces() + if err != nil { + fmt.Println("net.Interfaces failed, err:", err.Error()) + return + } + + for i := 0; i < len(netInterfaces); i++ { + if (netInterfaces[i].Flags & net.FlagUp) != 0 { + addrs, _ := netInterfaces[i].Addrs() + + for _, address := range addrs { + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + ip = ipnet.IP.String() + break + } else { + ip = ipnet.IP.String() + break + } + } + } + } + } + info += fmt.Sprintf("IP地址:%s\r\n \r\n", ip) + + //get traffic + inbouds, err := j.inboundService.GetAllInbounds() + if err != nil { + logger.Warning("StatsNotifyJob run failed:", err) + return + } + //NOTE:If there no any sessions here,need to notify here + //TODO:分节点推送,自动转化格式 + for _, inbound := range inbouds { + info += fmt.Sprintf("节点名称:%s\r\n端口:%d\r\n上行流量↑:%s\r\n下行流量↓:%s\r\n总流量:%s\r\n \r\n", inbound.Remark, inbound.Port, common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down), common.FormatTraffic((inbound.Up + inbound.Down))) + } + j.SendMsgToTgbot(info) +} + +func (j *StatsNotifyJob) UserLoginNotify(username string, ip string, time string) { + if username == "" || ip == "" || time == "" { + logger.Warning("UserLoginNotify failed,invalid info") + return + } + var msg string + //get hostname + name, err := os.Hostname() + if err != nil { + fmt.Println("get hostname error:", err) + return + } + msg = fmt.Sprintf("面板登录提醒\r\n主机名称:%s\r\n", name) + msg += fmt.Sprintf("时间:%s\r\n", time) + msg += fmt.Sprintf("用户:%s\r\n", username) + msg += fmt.Sprintf("IP:%s\r\n", ip) + j.SendMsgToTgbot(msg) +} diff --git a/web/service/inbound.go b/web/service/inbound.go index c7f3310ca2..726b7ba0a5 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -2,12 +2,13 @@ package service import ( "fmt" - "gorm.io/gorm" "time" "x-ui/database" "x-ui/database/model" "x-ui/util/common" "x-ui/xray" + + "gorm.io/gorm" ) type InboundService struct { diff --git a/web/service/setting.go b/web/service/setting.go index dd57d0c56a..88f1675999 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -29,6 +29,10 @@ var defaultValueMap = map[string]string{ "secret": random.Seq(32), "webBasePath": "/", "timeLocation": "Asia/Shanghai", + "tgBotEnable": "false", + "tgBotToken": "", + "tgBotChatId": "0", + "tgRunTime": "", } type SettingService struct { @@ -156,6 +160,18 @@ func (s *SettingService) setString(key string, value string) error { return s.saveSetting(key, value) } +func (s *SettingService) getBool(key string) (bool, error) { + str, err := s.getString(key) + if err != nil { + return false, err + } + return strconv.ParseBool(str) +} + +func (s *SettingService) setBool(key string, value bool) error { + return s.setString(key, strconv.FormatBool(value)) +} + func (s *SettingService) getInt(key string) (int, error) { str, err := s.getString(key) if err != nil { @@ -176,6 +192,38 @@ func (s *SettingService) GetListen() (string, error) { return s.getString("webListen") } +func (s *SettingService) GetTgBotToken() (string, error) { + return s.getString("tgBotToken") +} + +func (s *SettingService) SetTgBotToken(token string) error { + return s.setString("tgBotToken", token) +} + +func (s *SettingService) GetTgBotChatId() (int, error) { + return s.getInt("tgBotChatId") +} + +func (s *SettingService) SetTgBotChatId(chatId int) error { + return s.setInt("tgBotChatId", chatId) +} + +func (s *SettingService) SetTgbotenabled(value bool) error { + return s.setBool("tgBotEnable", value) +} + +func (s *SettingService) GetTgbotenabled() (bool, error) { + return s.getBool("tgBotEnable") +} + +func (s *SettingService) SetTgbotRuntime(time string) error { + return s.setString("tgRunTime", time) +} + +func (s *SettingService) GetTgbotRuntime() (string, error) { + return s.getString("tgRunTime") +} + func (s *SettingService) GetPort() (int, error) { return s.getInt("webPort") } diff --git a/web/service/user.go b/web/service/user.go index b24267a029..e4e7572d35 100644 --- a/web/service/user.go +++ b/web/service/user.go @@ -2,10 +2,11 @@ package service import ( "errors" - "gorm.io/gorm" "x-ui/database" "x-ui/database/model" "x-ui/logger" + + "gorm.io/gorm" ) type UserService struct { diff --git a/web/service/xray.go b/web/service/xray.go index f67d367746..7cfb909c31 100644 --- a/web/service/xray.go +++ b/web/service/xray.go @@ -3,10 +3,11 @@ package service import ( "encoding/json" "errors" - "go.uber.org/atomic" "sync" "x-ui/logger" "x-ui/xray" + + "go.uber.org/atomic" ) var p *xray.Process diff --git a/web/web.go b/web/web.go index 491855b23a..972292f0a3 100644 --- a/web/web.go +++ b/web/web.go @@ -4,13 +4,6 @@ import ( "context" "crypto/tls" "embed" - "github.com/BurntSushi/toml" - "github.com/gin-contrib/sessions" - "github.com/gin-contrib/sessions/cookie" - "github.com/gin-gonic/gin" - "github.com/nicksnyder/go-i18n/v2/i18n" - "github.com/robfig/cron/v3" - "golang.org/x/text/language" "html/template" "io" "io/fs" @@ -27,6 +20,14 @@ import ( "x-ui/web/job" "x-ui/web/network" "x-ui/web/service" + + "github.com/BurntSushi/toml" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" + "github.com/nicksnyder/go-i18n/v2/i18n" + "github.com/robfig/cron/v3" + "golang.org/x/text/language" ) //go:embed assets/* @@ -294,9 +295,28 @@ func (s *Server) startTask() { // 每 30 秒检查一次 inbound 流量超出和到期的情况 s.cron.AddJob("@every 30s", job.NewCheckInboundJob()) + // 每一天提示一次流量情况,上海时间8点30 + var entry cron.EntryID + isTgbotenabled, err := s.settingService.GetTgbotenabled() + if (err == nil) && (isTgbotenabled) { + runtime, err := s.settingService.GetTgbotRuntime() + if err != nil || runtime == "" { + logger.Errorf("Add NewStatsNotifyJob error[%s],Runtime[%s] invalid,wil run default", err, runtime) + runtime = "@daily" + } + logger.Infof("Tg notify enabled,run at %s", runtime) + entry, err = s.cron.AddJob(runtime, job.NewStatsNotifyJob()) + if err != nil { + logger.Warning("Add NewStatsNotifyJob error", err) + return + } + } else { + s.cron.Remove(entry) + } } func (s *Server) Start() (err error) { + //这是一个匿名函数,没没有函数名 defer func() { if err != nil { s.Stop() @@ -348,6 +368,7 @@ func (s *Server) Start() (err error) { listener = network.NewAutoHttpsListener(listener) listener = tls.NewListener(listener, c) } + if certFile != "" || keyFile != "" { logger.Info("web server run https on", listener.Addr()) } else { diff --git a/x-ui.sh b/x-ui.sh index d433ffb886..42724dc823 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -18,7 +18,7 @@ function LOGI() { echo -e "${green}[INF] $* ${plain}" } # check root -[[ $EUID -ne 0 ]] && LOGE "错误: 必须使用root用户运行此脚本!\n" && exit 1 +[[ $EUID -ne 0 ]] && LOGE "错误: 必须使用root用户运行此脚本!\n" && exit 1 # check os if [[ -f /etc/redhat-release ]]; then @@ -121,7 +121,7 @@ update() { } uninstall() { - confirm "确定要卸载面板吗,xray 也会卸载?" "n" + confirm "确定要卸载面板吗,xray 也会卸载?" "n" if [[ $? != 0 ]]; then if [[ $# == 0 ]]; then show_menu @@ -171,6 +171,15 @@ reset_config() { confirm_restart } +check_config() { + info=$(/usr/local/x-ui/x-ui setting -show true) + if [[ $? != 0 ]]; then + LOGE "get current settings error,please check logs" + show_menu + fi + LOGI "${info}" +} + set_port() { echo && echo -n -e "输入端口号[1-65535]: " && read port if [[ -z "${port}" ]]; then @@ -399,6 +408,70 @@ show_xray_status() { fi } +set_telegram_bot() { + echo -E "" + LOGI "设置Telegram Bot需要知晓Bot的Token与ChatId" + LOGI "使用方法请参考博客https://coderfan.net" + confirm "我已确认以上内容[y/n]" "y" + if [ $? -ne 0 ]; then + show_menu + else + read -p "please input your tg bot token here:" TG_BOT_TOKEN + LOGI "你设置的电报机器人Token:$TG_BOT_TOKEN" + read -p "please input your tg chat id here:" TG_BOT_CHATID + LOGI "你设置的电报机器人ChatId:$TG_BOT_CHATID" + read -p "please input your tg bot runtime here:" TG_BOT_RUNTIME + LOGI "你设置的电报机器人运行周期:$TG_BOT_RUNTIME" + info=$(/usr/local/x-ui/x-ui setting -tgbottoken ${TG_BOT_TOKEN} -tgbotchatid ${TG_BOT_CHATID} -tgbotRuntime "$TG_BOT_RUNTIME") + if [ $? != 0 ]; then + LOGE "$info" + LOGE "设置TelegramBot失败" + exit 1 + else + LOGI "设置TelegramBot成功" + show_menu + fi + fi +} + +enable_telegram_bot() { + echo -E "" + LOGI "该功能会开启Telegram Bot通知" + LOGI "通知内容包括:" + LOGI "1.流量使用情况" + LOGI "2.节点到期提醒,待实现(规划中)" + LOGI "3.面板登录提醒,待完善(规划中)" + confirm "我已确认以上内容[y/n]" "y" + if [ $? -eq 0 ]; then + info=$(/usr/local/x-ui/x-ui setting -enabletgbot=true) + if [ $? == 0 ]; then + LOGI "开启成功,重启X-UI生效,重启中...." + restart + else + LOGE "开启失败,即将退出..." + exit 1 + fi + else + show_menu + fi +} + +disable_telegram_bot() { + confirm "确认是否关闭Tgbot[y/n]" "n" + if [ $? -eq 0 ]; then + info=$(/usr/local/x-ui/x-ui setting -enabletgbot=false) + if [ $? == 0 ]; then + LOGI "关闭成功,重启X-UI生效,重启中...." + restart + else + LOGE "关闭失败,请检查日志..." + exit 1 + fi + else + show_menu + fi +} + ssl_cert_issue() { echo -E "" LOGD "******使用说明******" @@ -450,8 +523,8 @@ ssl_cert_issue() { LOGI "证书签发成功,安装中..." fi ~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} --ca-file /root/cert/ca.cer \ - --cert-file /root/cert/${CF_Domain}.cer --key-file /root/cert/${CF_Domain}.key \ - --fullchain-file /root/cert/fullchain.cer + --cert-file /root/cert/${CF_Domain}.cer --key-file /root/cert/${CF_Domain}.key \ + --fullchain-file /root/cert/fullchain.cer if [ $? -ne 0 ]; then LOGE "证书安装失败,脚本退出" exit 1 @@ -504,21 +577,25 @@ show_menu() { ${green}4.${plain} 重置用户名密码 ${green}5.${plain} 重置面板设置 ${green}6.${plain} 设置面板端口 + ${green}7.${plain} 当前面板设置 ———————————————— - ${green}7.${plain} 启动 x-ui - ${green}8.${plain} 停止 x-ui - ${green}9.${plain} 重启 x-ui - ${green}10.${plain} 查看 x-ui 状态 - ${green}11.${plain} 查看 x-ui 日志 + ${green}8.${plain} 启动 x-ui + ${green}9.${plain} 停止 x-ui + ${green}10.${plain} 重启 x-ui + ${green}11.${plain} 查看 x-ui 状态 + ${green}12.${plain} 查看 x-ui 日志 ———————————————— - ${green}12.${plain} 设置 x-ui 开机自启 - ${green}13.${plain} 取消 x-ui 开机自启 + ${green}13.${plain} 设置 x-ui 开机自启 + ${green}14.${plain} 取消 x-ui 开机自启 ———————————————— - ${green}14.${plain} 一键安装 bbr (最新内核) - ${green}15.${plain} 一键申请SSL证书(acme申请) + ${green}15.${plain} 一键安装 bbr (最新内核) + ${green}16.${plain} 一键申请SSL证书(acme申请) + ${green}17.${plain} 开启Telegram通知(TgBot) + ${green}18.${plain} 关闭Telegram通知(TgBot) + ${green}19.${plain} 设置TelegramBot " show_status - echo && read -p "请输入选择 [0-14]: " num + echo && read -p "请输入选择 [0-19]: " num case "${num}" in 0) @@ -543,34 +620,46 @@ show_menu() { check_install && set_port ;; 7) - check_install && start + check_install && check_config ;; 8) - check_install && stop + check_install && start ;; 9) - check_install && restart + check_install && stop ;; 10) - check_install && status + check_install && restart ;; 11) - check_install && show_log + check_install && status ;; 12) - check_install && enable + check_install && show_log ;; 13) - check_install && disable + check_install && enable ;; 14) - install_bbr + check_install && disable ;; 15) + install_bbr + ;; + 16) ssl_cert_issue ;; + 17) + enable_telegram_bot + ;; + 18) + disable_telegram_bot + ;; + 19) + set_telegram_bot + ;; *) - LOGE "请输入正确的数字 [0-14]" + LOGE "请输入正确的数字 [0-19]" ;; esac } diff --git a/xray/process.go b/xray/process.go index ca91f5855f..f3f4dbaf02 100644 --- a/xray/process.go +++ b/xray/process.go @@ -7,9 +7,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/Workiva/go-datastructures/queue" - statsservice "github.com/xtls/xray-core/app/stats/command" - "google.golang.org/grpc" "io/fs" "os" "os/exec" @@ -18,6 +15,10 @@ import ( "strings" "time" "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)")