Compare commits

...

133 Commits

Author SHA1 Message Date
刘祥超
4f7fe247b4 更新components.js 2023-04-09 22:32:30 +08:00
刘祥超
cee20bd7d2 上传证书私钥时可以选择pem文件 2023-04-09 21:29:44 +08:00
刘祥超
f99ed0c4d9 优化grpc参数 2023-04-09 21:05:49 +08:00
刘祥超
4ae6523164 修复一个单词拼写错误 2023-04-09 20:09:12 +08:00
刘祥超
2ece764dcb 优化<combo-box>组件上下键盘键移动速度 2023-04-09 20:08:38 +08:00
刘祥超
f3bdd98af5 优化API升级处理提示文字 2023-04-09 17:29:29 +08:00
刘祥超
3c3c511c5b 版本号更改为1.0.0 2023-04-09 17:24:04 +08:00
刘祥超
45167b87e5 提交components.js 2023-04-09 17:23:51 +08:00
刘祥超
f7d5755744 提升Cookie安全性 2023-04-09 17:10:53 +08:00
刘祥超
e2adafd16b 服务配置菜单项增加配置代号 2023-04-09 15:59:37 +08:00
刘祥超
9d178b238d 审计日志列表增加级别筛选 2023-04-06 10:06:22 +08:00
刘祥超
431054a1be 自动检查管理员弱密码并提醒 2023-04-04 17:26:28 +08:00
刘祥超
5bf6428253 调整表单中注释颜色 2023-04-04 15:22:57 +08:00
刘祥超
6a4a03267b 优化文字提示 2023-04-03 10:03:48 +08:00
刘祥超
6598e16974 优化MySQL系统服务参数 2023-04-03 10:01:35 +08:00
刘祥超
b1943a4cec 优化MySQL系统服务参数 2023-04-02 18:05:54 +08:00
刘祥超
5f70d5afd3 优化文字提示 2023-04-02 10:17:56 +08:00
刘祥超
7dead36212 更新components.js 2023-04-02 10:17:47 +08:00
刘祥超
1f68a7830b 集群服务设置增加“支持低版本HTTP”选项 2023-04-01 09:51:44 +08:00
刘祥超
42c5b7a181 修复在未初始化缓存设置时添加缓存条件产生的panic错误 2023-03-31 17:42:33 +08:00
刘祥超
6596b47e54 证书即将到期提示 2023-03-28 16:52:21 +08:00
刘祥超
02e4a3e244 安装MySQL时指定 innodb_buffer_pool_size 为系统内存的一半 2023-03-28 11:42:19 +08:00
刘祥超
d9af90c76b 优化代码 2023-03-27 17:37:48 +08:00
刘祥超
a54405f24f 自动安装mysql时调整innodb_sort_buffer_size参数值 2023-03-27 17:22:13 +08:00
刘祥超
2b39c5d517 优化代码 2023-03-27 16:31:52 +08:00
刘祥超
a09d295948 优化<combo-box>组件 2023-03-27 11:28:06 +08:00
刘祥超
2c0b0be8c4 优化<combo-box>组件 2023-03-27 11:01:28 +08:00
刘祥超
b289877273 README中增加企业版链接 2023-03-26 20:50:45 +08:00
刘祥超
869d54b9a8 如果管理系统同时设置了HTTP和HTTPS端口,那么访问HTTP登录页时自动跳转到HTTPS地址 2023-03-26 15:54:14 +08:00
刘祥超
df48ae8316 优化缓存策略相关文字提示 2023-03-26 12:23:12 +08:00
刘祥超
e42cf2a420 创建服务和修改服务HTTPS设置时也支持批量上传证书 2023-03-26 12:00:41 +08:00
刘祥超
bd899b649d 创建服务时如果选择了所属用户,则证书列表中默认显示该用户的证书 2023-03-26 10:36:47 +08:00
刘祥超
2bc43ee2a5 实现自动匹配证书和批量选择证书功能 2023-03-25 20:50:27 +08:00
刘祥超
48e2907426 开源版本默认每次只能上传20个证书文件 2023-03-24 15:29:51 +08:00
刘祥超
4e33cce128 批量上传时如果证书文件和私钥文件不一致,也会提示错误 2023-03-24 15:13:28 +08:00
刘祥超
6658028f90 增加批量上传证书功能 2023-03-24 15:05:31 +08:00
刘祥超
3fe67bb179 优化系统设置--Web服务文字提示。 2023-03-24 10:53:40 +08:00
刘祥超
ce9a2d0cc3 证书内容输入框支持拖动文件上传 2023-03-24 10:17:05 +08:00
刘祥超
45b7c7af15 优化界面显示 2023-03-23 15:47:11 +08:00
刘祥超
3116aaeccb 节点列表节点IP超出4个后可以自动隐藏和显示 2023-03-23 14:50:12 +08:00
刘祥超
bfd5517c6c 优化创建网站服务界面 2023-03-22 17:52:22 +08:00
刘祥超
c83598a4f9 优化创建网站服务界面 2023-03-21 16:56:45 +08:00
刘祥超
3dc983871f 优化CNAME查询程序 2023-03-21 11:41:40 +08:00
刘祥超
ac6fcefffd 修改<columns-grid>组件文件名 2023-03-21 11:00:35 +08:00
刘祥超
094080cc9f 优化界面文字 2023-03-20 21:32:18 +08:00
刘祥超
d2dff968fb 自动安装MySQL时自动生成所需的动态库软链接 2023-03-20 21:32:10 +08:00
刘祥超
496f82a01f 日志数据库节点详情中密码使用星号(*)代替 2023-03-20 17:00:54 +08:00
刘祥超
9293c3e861 优化界面 2023-03-20 12:10:08 +08:00
刘祥超
da7d42edd5 修复<file-textarea>组件在firefox上无法复制内容的Bug 2023-03-20 09:43:21 +08:00
刘祥超
87b96d6526 SSH认证添加私钥时可以从私钥文件中直接拖入 2023-03-19 17:49:07 +08:00
刘祥超
a371821ff8 创建SSH认证私钥时校验私钥内容 2023-03-19 17:48:43 +08:00
刘祥超
748cb6eb8f 更新相关库 2023-03-19 17:46:28 +08:00
刘祥超
05bee642e9 配置初始化时增加context参数 2023-03-18 22:12:22 +08:00
刘祥超
081be04592 优化文字提示 2023-03-18 16:08:25 +08:00
刘祥超
f77518d086 生成节点DNS解析时区分节点是否已安装 2023-03-18 16:05:46 +08:00
刘祥超
35e6202b3c 创建节点时自动从节点名称中提取节点IP 2023-03-18 11:36:10 +08:00
刘祥超
1d84ea6ab9 更新components.js 2023-03-18 11:35:42 +08:00
刘祥超
df461d81d8 版本号更改为0.6.5 2023-03-17 15:53:01 +08:00
刘祥超
821d5aa595 优化节点列表连接数显示 2023-03-17 15:25:07 +08:00
刘祥超
c25bd71592 节点列表带宽保存两位小数数字 2023-03-17 15:20:53 +08:00
刘祥超
7e9224680e 当HTTP和HTTPS端口冲突时提示用户 2023-03-17 11:11:54 +08:00
刘祥超
bc89116d3d 编译脚本中暂时不删除.css.map和.js.map文件 2023-03-17 10:20:11 +08:00
刘祥超
aabe75ee13 优化文字提示 2023-03-17 10:19:38 +08:00
刘祥超
8903adc523 优化文字提示 2023-03-16 10:09:17 +08:00
刘祥超
61d8c8cd39 优化节点列表统计项宽度 2023-03-16 09:48:45 +08:00
刘祥超
2e1d991e0c 在开源版本看板增加检查是否为Plus的钩子 2023-03-16 09:37:16 +08:00
刘祥超
5a57167832 更新Dockerfile中的版本号 2023-03-16 09:27:36 +08:00
刘祥超
c3df181dcc 版本号改为0.6.4.2 2023-03-16 08:55:12 +08:00
刘祥超
699ea47ac5 更新components.js 2023-03-16 08:55:01 +08:00
刘祥超
fba69f3109 节点列表增加连接数列 2023-03-15 17:57:32 +08:00
刘祥超
063583dda1 调整节点任务和DNS同步任务弹窗高度 2023-03-15 16:24:14 +08:00
刘祥超
a31ca5cfb6 提高消息弹窗高度 2023-03-15 15:48:35 +08:00
刘祥超
c0a13305ad 管理系统增加两种背景风格 2023-03-15 15:35:11 +08:00
刘祥超
9d8cbf87dc URL跳转之域名跳转增加跳转后域名校验 2023-03-15 15:07:55 +08:00
刘祥超
009f7da26b 优化看板中的统计图表 2023-03-15 14:39:45 +08:00
刘祥超
f1d0359dc4 优化代码 2023-03-13 16:24:37 +08:00
刘祥超
473baa8c5b 版本号改为0.6.5 2023-03-13 16:16:26 +08:00
刘祥超
1a5dda6c72 Update Dockerfile 2023-03-13 12:12:44 +08:00
刘祥超
8a78dcb06e 版本号改为0.6.4.1 2023-03-13 10:36:56 +08:00
刘祥超
185768a80f 更新Dockerfile中的版本号 2023-03-13 09:15:23 +08:00
刘祥超
619a2817ce 检查版本更新时增加当前版本参数 2023-03-12 21:24:08 +08:00
刘祥超
12a33ee9fc 自动安装的MySQL binlog过期时间改成7天 2023-03-12 20:58:13 +08:00
刘祥超
26302ca930 重启edge-admin时确保同目录下的edge-api也能重启 2023-03-11 22:02:29 +08:00
刘祥超
7e85555ba7 安装过程中可以选择自动在本机安装MySQL 2023-03-11 18:52:40 +08:00
刘祥超
2546676f6a 删除页脚捐助作者链接 2023-03-11 14:44:19 +08:00
刘祥超
93b7edf5c4 安装过程中节点主机地址允许填写域名 2023-03-10 17:00:53 +08:00
刘祥超
571e432263 WAF cc防护增加“检查请求来源指纹”选项 2023-03-10 10:34:50 +08:00
刘祥超
580b158567 优化tips弹窗 2023-03-09 17:40:49 +08:00
刘祥超
395aa665d7 可以在运行日志页面中删除使用关键词匹配的运行日志 2023-03-09 16:20:09 +08:00
刘祥超
a80e54e0b3 优化<url-patterns-box>组件 2023-03-09 12:09:28 +08:00
刘祥超
8ccd41b551 优化路由规则标签、菜单、删除UAM组件 2023-03-09 11:42:15 +08:00
刘祥超
9d6a3a8a0d 优化<url-patterns-box>组件 2023-03-07 17:18:13 +08:00
刘祥超
a9d1b4b863 集群服务设置增加“记录找不到网站日志”选项 2023-03-07 10:30:11 +08:00
刘祥超
459d664a60 更新go.mod 2023-03-06 21:51:10 +08:00
刘祥超
5f10b0156c 5秒盾增加例外URL和限制URL 2023-03-06 21:48:29 +08:00
刘祥超
348c07f847 优化网站服务WAF设置界面 2023-03-06 16:40:42 +08:00
刘祥超
934b1894c4 使用edge-admin upgrade升级时可以通过--url参数指定升级包URL 2023-03-05 19:52:48 +08:00
刘祥超
a660f4af93 上传components.js 2023-03-05 19:51:46 +08:00
刘祥超
12c0d39b13 优化缓存条件设置界面 2023-03-05 16:48:01 +08:00
刘祥超
bc6de68006 远程升级API节点限制版本号必须不小于0.6.4 2023-03-05 15:32:27 +08:00
刘祥超
52bb753594 实现API节点远程升级 2023-03-05 12:05:18 +08:00
刘祥超
ed6b763d06 远程升级API节点(部分实现) 2023-03-04 21:04:30 +08:00
刘祥超
07e421afea 更新components.js 2023-03-04 09:34:17 +08:00
刘祥超
71352841bf 修复无法启动安装的Bug 2023-03-04 09:34:10 +08:00
刘祥超
91e8fcbb24 使用edge-admin upgrade升级系统时URL请求增加User-Agent 2023-03-03 10:44:04 +08:00
刘祥超
5b67a85624 WAF阻止动作也增加最大封禁时长 2023-03-01 19:18:22 +08:00
刘祥超
b49efa0d5a WAF拦截动作可以设置最大封禁时间,从而实现封禁时间随机 2023-03-01 18:59:42 +08:00
刘祥超
abeb585a0d 优化代码 2023-03-01 16:58:38 +08:00
刘祥超
cf621f1cc9 WAF支持忽略全局WAF规则 2023-03-01 16:46:49 +08:00
刘祥超
389a494e00 优化界面 2023-03-01 15:30:32 +08:00
刘祥超
a9c55dc23b 在节点列表中IP地址中显示对应的专属集群 2023-03-01 15:23:36 +08:00
刘祥超
3d35b7e71b 节点IP地址可以设置专属集群 2023-03-01 11:38:21 +08:00
刘祥超
6f78146711 可以修改单个用户的带宽算法 2023-02-27 10:46:48 +08:00
刘祥超
8afa47c351 删除不必要的文件 2023-02-22 19:25:38 +08:00
刘祥超
20838cfc3e 增加<js-page>组件 2023-02-22 17:34:32 +08:00
刘祥超
d00acd6d2f 上传日期相关公共函数 2023-02-18 20:45:05 +08:00
刘祥超
0b58a36779 优化界面 2023-02-17 10:30:25 +08:00
刘祥超
54bc98e9c1 优化组件显示 2023-02-13 09:44:10 +08:00
刘祥超
836daf2ad9 Dockerfile版本号修改为0.6.3 2023-02-10 18:17:57 +08:00
刘祥超
9a8cd9bd87 优化缓存请求条件界面 2023-02-10 11:15:06 +08:00
刘祥超
c555d91503 文件扩展名相关变量使用${requestPathLowerExtension} 2023-02-10 10:44:29 +08:00
刘祥超
e0e7c1bcc4 修复表单能够上传的数据过小问题 2023-02-07 11:40:28 +08:00
刘祥超
4bf733beec 在管理员登录后才验证OTP 2023-02-04 16:44:33 +08:00
刘祥超
e93f23c943 使用数据库存储SESSION 2023-02-04 15:18:26 +08:00
刘祥超
5384f4d9f2 优化WAF规则大小写敏感显示 2023-02-02 16:14:07 +08:00
刘祥超
3c0a97c3cc 修复防盗链设置中域名无法修改的Bug 2023-02-01 18:16:41 +08:00
刘祥超
129db6cf4e 修复无法显示IPv6最近日志的Bug 2023-02-01 10:36:30 +08:00
刘祥超
c6bfa5652f 版本号修改为0.6.4 2023-01-14 17:19:03 +08:00
刘祥超
780472d83e 更新components.js 2023-01-14 17:18:34 +08:00
刘祥超
0d02e3f15a 优化WAF创建规则界面中参数选项显示 2023-01-13 18:51:14 +08:00
刘祥超
bf82f22d0f WAF规则中如果对比值为空,则显示空字样 2023-01-13 15:58:35 +08:00
刘祥超
e18f182ce6 版本号修改为0.6.3 2023-01-11 15:45:03 +08:00
刘祥超
761c26b587 Update Dockerfile 2023-01-10 21:32:20 +08:00
248 changed files with 7170 additions and 1499 deletions

View File

@@ -56,5 +56,8 @@
## 联系我们 ## 联系我们
有什么问题和建议都可以加入QQ群 `659832182` 或者 [Telegram群](https://t.me/+5kVCMGxQhZxiODY9) 。 有什么问题和建议都可以加入QQ群 `659832182` 或者 [Telegram群](https://t.me/+5kVCMGxQhZxiODY9) 。
## 企业版
* [GoEdge企业版](https://goedge.cn/commercial) - 功能更强大的CDN系统
## 感谢 ## 感谢
* 感谢 [Gitee](https://gitee.com/) 提供国内源代码托管平台 * 感谢 [Gitee](https://gitee.com/) 提供国内源代码托管平台

View File

@@ -105,8 +105,8 @@ function build() {
find "$DIST" -name ".DS_Store" -delete find "$DIST" -name ".DS_Store" -delete
find "$DIST" -name ".gitignore" -delete find "$DIST" -name ".gitignore" -delete
find "$DIST" -name "*.less" -delete find "$DIST" -name "*.less" -delete
find "$DIST" -name "*.css.map" -delete #find "$DIST" -name "*.css.map" -delete
find "$DIST" -name "*.js.map" -delete #find "$DIST" -name "*.js.map" -delete
# zip # zip
echo "zip files ..." echo "zip files ..."

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"flag"
"fmt" "fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/apps" "github.com/TeaOSLab/EdgeAdmin/internal/apps"
"github.com/TeaOSLab/EdgeAdmin/internal/configs" "github.com/TeaOSLab/EdgeAdmin/internal/configs"
@@ -24,7 +25,7 @@ func main() {
var app = apps.NewAppCmd(). var app = apps.NewAppCmd().
Version(teaconst.Version). Version(teaconst.Version).
Product(teaconst.ProductName). Product(teaconst.ProductName).
Usage(teaconst.ProcessName+" [-v|start|stop|restart|service|daemon|reset|recover|demo|upgrade]"). Usage(teaconst.ProcessName+" [-h|-v|start|stop|restart|service|daemon|reset|recover|demo|upgrade]").
Usage(teaconst.ProcessName+" [dev|prod]"). Usage(teaconst.ProcessName+" [dev|prod]").
Option("-h", "show this help"). Option("-h", "show this help").
Option("-v", "show version"). Option("-v", "show version").
@@ -38,7 +39,7 @@ func main() {
Option("demo", "switch to demo mode"). Option("demo", "switch to demo mode").
Option("dev", "switch to 'dev' mode"). Option("dev", "switch to 'dev' mode").
Option("prod", "switch to 'prod' mode"). Option("prod", "switch to 'prod' mode").
Option("upgrade", "upgrade from official site") Option("upgrade [--url=URL]", "upgrade from official site or an url")
app.On("daemon", func() { app.On("daemon", func() {
nodes.NewAdminNode().Daemon() nodes.NewAdminNode().Daemon()
@@ -138,7 +139,12 @@ func main() {
} }
}) })
app.On("upgrade", func() { app.On("upgrade", func() {
var manager = utils.NewUpgradeManager("admin") var downloadURL = ""
var flagSet = flag.NewFlagSet("", flag.ContinueOnError)
flagSet.StringVar(&downloadURL, "url", "", "new version download url")
_ = flagSet.Parse(os.Args[2:])
var manager = utils.NewUpgradeManager("admin", downloadURL)
log.Println("checking latest version ...") log.Println("checking latest version ...")
var ticker = time.NewTicker(1 * time.Second) var ticker = time.NewTicker(1 * time.Second)
go func() { go func() {

View File

@@ -1,7 +1,7 @@
FROM alpine:latest FROM alpine:latest
LABEL maintainer="iwind.liu@gmail.com" LABEL maintainer="iwind.liu@gmail.com"
ENV TZ "Asia/Shanghai" ENV TZ "Asia/Shanghai"
ENV VERSION 0.6.1 ENV VERSION 0.6.4.2
ENV ROOT_DIR /usr/local/goedge ENV ROOT_DIR /usr/local/goedge
ENV TAR_FILE edge-admin-linux-amd64-plus-v${VERSION}.zip ENV TAR_FILE edge-admin-linux-amd64-plus-v${VERSION}.zip
ENV TAR_URL "https://dl.goedge.cn/edge/v${VERSION}/edge-admin-linux-amd64-plus-v${VERSION}.zip" ENV TAR_URL "https://dl.goedge.cn/edge/v${VERSION}/edge-admin-linux-amd64-plus-v${VERSION}.zip"

12
go.mod
View File

@@ -8,14 +8,15 @@ require (
github.com/TeaOSLab/EdgeCommon v0.0.0-00010101000000-000000000000 github.com/TeaOSLab/EdgeCommon v0.0.0-00010101000000-000000000000
github.com/cespare/xxhash v1.1.0 github.com/cespare/xxhash v1.1.0
github.com/go-sql-driver/mysql v1.5.0 github.com/go-sql-driver/mysql v1.5.0
github.com/iwind/TeaGo v0.0.0-20220304043459-0dd944a5b475 github.com/iwind/TeaGo v0.0.0-20230304012706-c1f4a4e27470
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4 github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4
github.com/miekg/dns v1.1.43 github.com/miekg/dns v1.1.43
github.com/shirou/gopsutil/v3 v3.22.5 github.com/shirou/gopsutil/v3 v3.22.5
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tealeg/xlsx/v3 v3.2.3 github.com/tealeg/xlsx/v3 v3.2.3
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119 github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119
golang.org/x/sys v0.2.0 golang.org/x/crypto v0.7.0
golang.org/x/sys v0.6.0
google.golang.org/grpc v1.45.0 google.golang.org/grpc v1.45.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -26,18 +27,15 @@ require (
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/btree v1.0.0 // indirect github.com/google/btree v1.0.0 // indirect
github.com/google/go-cmp v0.5.8 // indirect github.com/google/go-cmp v0.5.8 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/pretty v0.2.1 // indirect github.com/kr/pretty v0.2.1 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rogpeppe/fastuuid v1.2.0 // indirect github.com/rogpeppe/fastuuid v1.2.0 // indirect
github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa // indirect github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/net v0.2.0 // indirect golang.org/x/net v0.8.0 // indirect
golang.org/x/text v0.4.0 // indirect golang.org/x/text v0.8.0 // indirect
google.golang.org/genproto v0.0.0-20220317150908-0efb43f6373e // indirect google.golang.org/genproto v0.0.0-20220317150908-0efb43f6373e // indirect
google.golang.org/protobuf v1.27.1 // indirect google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect

29
go.sum
View File

@@ -21,7 +21,6 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200624174652-8d2f3be8b2d9/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200624174652-8d2f3be8b2d9/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -75,13 +74,11 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iwind/TeaGo v0.0.0-20210411134150-ddf57e240c2f/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc= github.com/iwind/TeaGo v0.0.0-20210411134150-ddf57e240c2f/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
github.com/iwind/TeaGo v0.0.0-20220304043459-0dd944a5b475 h1:EseyfFaQOjWanGiby9KMw7PjDBMg/95tLDgIw/ns0Cw= github.com/iwind/TeaGo v0.0.0-20230304012706-c1f4a4e27470 h1:TuRxvKRv9PxKVijWOkUnZm5TeanQqWGUJyPx9u6cra4=
github.com/iwind/TeaGo v0.0.0-20220304043459-0dd944a5b475/go.mod h1:HRHK0zoC/og3c9/hKosD9yYVMTnnzm3PgXUdhRYHaLc= github.com/iwind/TeaGo v0.0.0-20230304012706-c1f4a4e27470/go.mod h1:fi/Pq+/5m2HZoseM+39dMF57ANXRt6w4PkGu3NXPc5s=
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4 h1:VWGsCqTzObdlbf7UUE3oceIpcEKi4C/YBUszQXk118A= github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4 h1:VWGsCqTzObdlbf7UUE3oceIpcEKi4C/YBUszQXk118A=
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4/go.mod h1:H5Q7SXwbx3a97ecJkaS2sD77gspzE7HFUafBO0peEyA= github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4/go.mod h1:H5Q7SXwbx3a97ecJkaS2sD77gspzE7HFUafBO0peEyA=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -93,11 +90,7 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -112,7 +105,6 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug= github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug=
github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
@@ -132,7 +124,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tealeg/xlsx/v3 v3.2.3 h1:MXnVh+9Y8cUglowItTy2HL3Kv6z+q/0aNjeKuTsVqZQ= github.com/tealeg/xlsx/v3 v3.2.3 h1:MXnVh+9Y8cUglowItTy2HL3Kv6z+q/0aNjeKuTsVqZQ=
github.com/tealeg/xlsx/v3 v3.2.3/go.mod h1:0hGmAEoZ48SS1ZAE6eqZJkJVXgOMY+8a33vjXa8S8HA= github.com/tealeg/xlsx/v3 v3.2.3/go.mod h1:0hGmAEoZ48SS1ZAE6eqZJkJVXgOMY+8a33vjXa8S8HA=
@@ -147,6 +138,8 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
@@ -171,8 +164,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -200,15 +193,16 @@ golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -263,7 +257,6 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -209,7 +209,7 @@ func (this *AppCmd) runStop() {
fmt.Println(this.product+" stopped ok, pid:", types.String(pid)) fmt.Println(this.product+" stopped ok, pid:", types.String(pid))
} }
// 重启 // RunRestart 重启
func (this *AppCmd) RunRestart() { func (this *AppCmd) RunRestart() {
this.runStop() this.runStop()
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)

View File

@@ -21,9 +21,9 @@ type APIConfig struct {
// LoadAPIConfig 加载API配置 // LoadAPIConfig 加载API配置
func LoadAPIConfig() (*APIConfig, error) { func LoadAPIConfig() (*APIConfig, error) {
// 候选文件 // 候选文件
localFile := Tea.ConfigFile("api.yaml") var localFile = Tea.ConfigFile("api.yaml")
isFromLocal := false var isFromLocal = false
paths := []string{localFile} var paths = []string{localFile}
homeDir, homeErr := os.UserHomeDir() homeDir, homeErr := os.UserHomeDir()
if homeErr == nil { if homeErr == nil {
paths = append(paths, homeDir+"/."+teaconst.ProcessName+"/api.yaml") paths = append(paths, homeDir+"/."+teaconst.ProcessName+"/api.yaml")
@@ -45,7 +45,7 @@ func LoadAPIConfig() (*APIConfig, error) {
return nil, err return nil, err
} }
config := &APIConfig{} var config = &APIConfig{}
err = yaml.Unmarshal(data, config) err = yaml.Unmarshal(data, config)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -155,3 +155,15 @@ func (this *APIConfig) WriteFile(path string) error {
return nil return nil
} }
// Clone 克隆当前配置
func (this *APIConfig) Clone() *APIConfig {
return &APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
}{},
NodeId: this.NodeId,
Secret: this.Secret,
}
}

View File

@@ -1,42 +0,0 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package configs
import (
"encoding/json"
"github.com/iwind/TeaGo/Tea"
"os"
)
var plusConfigFile = "plus.cache.json"
type PlusConfig struct {
IsPlus bool `json:"isPlus"`
Components []string `json:"components"`
DayTo string `json:"dayTo"`
}
func ReadPlusConfig() *PlusConfig {
data, err := os.ReadFile(Tea.ConfigFile(plusConfigFile))
if err != nil {
return &PlusConfig{IsPlus: false}
}
var config = &PlusConfig{IsPlus: false}
err = json.Unmarshal(data, config)
if err != nil {
return config
}
return config
}
func WritePlusConfig(config *PlusConfig) error {
configJSON, err := json.Marshal(config)
if err != nil {
return err
}
err = os.WriteFile(Tea.ConfigFile(plusConfigFile), configJSON, 0777)
if err != nil {
return err
}
return nil
}

View File

@@ -1,9 +1,9 @@
package teaconst package teaconst
const ( const (
Version = "0.6.2" Version = "1.0.0"
APINodeVersion = "0.6.2" APINodeVersion = "1.0.0"
ProductName = "Edge Admin" ProductName = "Edge Admin"
ProcessName = "edge-admin" ProcessName = "edge-admin"
@@ -18,5 +18,5 @@ const (
CookieSID = "edgesid" CookieSID = "edgesid"
SystemdServiceName = "edge-admin" SystemdServiceName = "edge-admin"
UpdatesURL = "https://goedge.cn/api/boot/versions?os=${os}&arch=${arch}" UpdatesURL = "https://goedge.cn/api/boot/versions?os=${os}&arch=${arch}&version=${version}"
) )

View File

@@ -2,6 +2,11 @@
package teaconst package teaconst
import (
"os"
"strings"
)
var ( var (
IsRecoverMode = false IsRecoverMode = false
@@ -10,4 +15,18 @@ var (
NewVersionCode = "" // 有新的版本 NewVersionCode = "" // 有新的版本
NewVersionDownloadURL = "" // 新版本下载地址 NewVersionDownloadURL = "" // 新版本下载地址
IsMain = checkMain()
) )
// 检查是否为主程序
func checkMain() bool {
if len(os.Args) == 1 ||
(len(os.Args) >= 2 && os.Args[1] == "pprof") {
return true
}
exe, _ := os.Executable()
return strings.HasSuffix(exe, ".test") ||
strings.HasSuffix(exe, ".test.exe") ||
strings.Contains(exe, "___")
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/iwind/TeaGo/logs" "github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/rands" "github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/sessions"
"github.com/iwind/TeaGo/types" "github.com/iwind/TeaGo/types"
"github.com/iwind/gosock/pkg/gosock" "github.com/iwind/gosock/pkg/gosock"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -21,6 +20,8 @@ import (
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path/filepath"
"strings"
"syscall" "syscall"
"time" "time"
) )
@@ -85,10 +86,15 @@ func (this *AdminNode) Run() {
this.startAPINode() this.startAPINode()
// 启动Web服务 // 启动Web服务
sessionManager, err := NewSessionManager()
if err != nil {
log.Fatal("start session failed: " + err.Error())
return
}
TeaGo.NewServer(false). TeaGo.NewServer(false).
AccessLog(false). AccessLog(false).
EndAll(). EndAll().
Session(sessions.NewFileSessionManager(86400, secret), teaconst.CookieSID). Session(sessionManager, teaconst.CookieSID).
ReadHeaderTimeout(3 * time.Second). ReadHeaderTimeout(3 * time.Second).
ReadTimeout(1200 * time.Second). ReadTimeout(1200 * time.Second).
Start() Start()
@@ -360,6 +366,16 @@ func (this *AdminNode) listenSock() error {
} }
} }
// 停止当前目录下的API节点
var apiSock = gosock.NewTmpSock("edge-api")
apiReply, err := apiSock.Send(&gosock.Command{Code: "info"})
if err == nil {
adminExe, _ := os.Executable()
if len(adminExe) > 0 && apiReply != nil && strings.HasPrefix(maps.NewMap(apiReply.Params).GetString("path"), filepath.Dir(filepath.Dir(adminExe))) {
_, _ = apiSock.Send(&gosock.Command{Code: "stop"})
}
}
// 退出主进程 // 退出主进程
events.Notify(events.EventQuit) events.Notify(events.EventQuit)
os.Exit(0) os.Exit(0)

View File

@@ -0,0 +1,97 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package nodes
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/logs"
"strings"
)
// SessionManager SESSION管理
type SessionManager struct {
life uint
}
func NewSessionManager() (*SessionManager, error) {
return &SessionManager{}, nil
}
func (this *SessionManager) Init(config *actions.SessionConfig) {
this.life = config.Life
}
func (this *SessionManager) Read(sid string) map[string]string {
// 忽略OTP
if strings.HasSuffix(sid, "_otp") {
return map[string]string{}
}
var result = map[string]string{}
rpcClient, err := rpc.SharedRPC()
if err != nil {
return map[string]string{}
}
resp, err := rpcClient.LoginSessionRPC().FindLoginSession(rpcClient.Context(0), &pb.FindLoginSessionRequest{Sid: sid})
if err != nil {
logs.Println("SESSION", "read '"+sid+"' failed: "+err.Error())
return result
}
var session = resp.LoginSession
if session == nil || len(session.ValuesJSON) == 0 {
return result
}
err = json.Unmarshal(session.ValuesJSON, &result)
if err != nil {
logs.Println("SESSION", "decode '"+sid+"' values failed: "+err.Error())
}
return result
}
func (this *SessionManager) WriteItem(sid string, key string, value string) bool {
// 忽略OTP
if strings.HasSuffix(sid, "_otp") {
return false
}
rpcClient, err := rpc.SharedRPC()
if err != nil {
return false
}
_, err = rpcClient.LoginSessionRPC().WriteLoginSessionValue(rpcClient.Context(0), &pb.WriteLoginSessionValueRequest{
Sid: sid,
Key: key,
Value: value,
})
if err != nil {
logs.Println("SESSION", "write sid:'"+sid+"' key:'"+key+"' failed: "+err.Error())
}
return true
}
func (this *SessionManager) Delete(sid string) bool {
// 忽略OTP
if strings.HasSuffix(sid, "_otp") {
return false
}
rpcClient, err := rpc.SharedRPC()
if err != nil {
return false
}
_, err = rpcClient.LoginSessionRPC().DeleteLoginSession(rpcClient.Context(0), &pb.DeleteLoginSessionRequest{Sid: sid})
if err != nil {
logs.Println("SESSION", "delete '"+sid+"' failed: "+err.Error())
}
return true
}

View File

@@ -407,6 +407,10 @@ func (this *RPCClient) LoginRPC() pb.LoginServiceClient {
return pb.NewLoginServiceClient(this.pickConn()) return pb.NewLoginServiceClient(this.pickConn())
} }
func (this *RPCClient) LoginSessionRPC() pb.LoginSessionServiceClient {
return pb.NewLoginSessionServiceClient(this.pickConn())
}
func (this *RPCClient) NodeTaskRPC() pb.NodeTaskServiceClient { func (this *RPCClient) NodeTaskRPC() pb.NodeTaskServiceClient {
return pb.NewNodeTaskServiceClient(this.pickConn()) return pb.NewNodeTaskServiceClient(this.pickConn())
} }
@@ -540,8 +544,11 @@ func (this *RPCClient) init() error {
} }
var conn *grpc.ClientConn var conn *grpc.ClientConn
var callOptions = grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(128*1024*1024), var callOptions = grpc.WithDefaultCallOptions(
grpc.UseCompressor(gzip.Name)) grpc.MaxCallRecvMsgSize(128<<20),
grpc.MaxCallSendMsgSize(128<<20),
grpc.UseCompressor(gzip.Name),
)
if u.Scheme == "http" { if u.Scheme == "http" {
conn, err = grpc.Dial(apiHost, grpc.WithTransportCredentials(insecure.NewCredentials()), callOptions) conn, err = grpc.Dial(apiHost, grpc.WithTransportCredentials(insecure.NewCredentials()), callOptions)
} else if u.Scheme == "https" { } else if u.Scheme == "https" {

View File

@@ -87,6 +87,7 @@ func (this *CheckUpdatesTask) Loop() error {
var apiURL = teaconst.UpdatesURL var apiURL = teaconst.UpdatesURL
apiURL = strings.ReplaceAll(apiURL, "${os}", runtime.GOOS) apiURL = strings.ReplaceAll(apiURL, "${os}", runtime.GOOS)
apiURL = strings.ReplaceAll(apiURL, "${arch}", runtime.GOARCH) apiURL = strings.ReplaceAll(apiURL, "${arch}", runtime.GOARCH)
apiURL = strings.ReplaceAll(apiURL, "${version}", teaconst.Version)
resp, err := http.Get(apiURL) resp, err := http.Get(apiURL)
if err != nil { if err != nil {
return errors.New("read api failed: " + err.Error()) return errors.New("read api failed: " + err.Error())

View File

@@ -0,0 +1,30 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package apinodeutils
var SharedManager = NewManager()
type Manager struct {
upgraderMap map[int64]*Upgrader
}
func NewManager() *Manager {
return &Manager{
upgraderMap: map[int64]*Upgrader{},
}
}
func (this *Manager) AddUpgrader(upgrader *Upgrader) {
this.upgraderMap[upgrader.apiNodeId] = upgrader
}
func (this *Manager) FindUpgrader(apiNodeId int64) *Upgrader {
return this.upgraderMap[apiNodeId]
}
func (this *Manager) RemoveUpgrader(upgrader *Upgrader) {
if upgrader == nil {
return
}
delete(this.upgraderMap, upgrader.apiNodeId)
}

View File

@@ -0,0 +1,201 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package apinodeutils
import (
"compress/gzip"
"crypto/md5"
"errors"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/types"
stringutil "github.com/iwind/TeaGo/utils/string"
"io"
"os"
"path/filepath"
"runtime"
)
type Progress struct {
Percent float64
}
type Upgrader struct {
progress *Progress
apiExe string
apiNodeId int64
}
func NewUpgrader(apiNodeId int64) *Upgrader {
return &Upgrader{
apiExe: apiExe(),
progress: &Progress{Percent: 0},
apiNodeId: apiNodeId,
}
}
func (this *Upgrader) Upgrade() error {
sharedClient, err := rpc.SharedRPC()
if err != nil {
return err
}
apiNodeResp, err := sharedClient.APINodeRPC().FindEnabledAPINode(sharedClient.Context(0), &pb.FindEnabledAPINodeRequest{ApiNodeId: this.apiNodeId})
if err != nil {
return err
}
var apiNode = apiNodeResp.ApiNode
if apiNode == nil {
return errors.New("could not find api node with id '" + types.String(this.apiNodeId) + "'")
}
apiConfig, err := configs.LoadAPIConfig()
if err != nil {
return err
}
var newAPIConfig = apiConfig.Clone()
newAPIConfig.RPC.Endpoints = apiNode.AccessAddrs
rpcClient, err := rpc.NewRPCClient(newAPIConfig, false)
if err != nil {
return err
}
versionResp, err := rpcClient.APINodeRPC().FindCurrentAPINodeVersion(sharedClient.Context(0), &pb.FindCurrentAPINodeVersionRequest{})
if err != nil {
return err
}
if !Tea.IsTesting() /** 开发环境下允许突破此限制方便测试 **/ &&
(stringutil.VersionCompare(versionResp.Version, "0.6.4" /** 从0.6.4开始支持 **/) < 0 || versionResp.Os != runtime.GOOS || versionResp.Arch != runtime.GOARCH) {
return errors.New("could not upgrade api node v" + versionResp.Version + "/" + versionResp.Os + "/" + versionResp.Arch)
}
// 检查本地文件版本
canUpgrade, reason := CanUpgrade(versionResp.Version, versionResp.Os, versionResp.Arch)
if !canUpgrade {
return errors.New(reason)
}
localVersion, err := localVersion()
if err != nil {
return errors.New("lookup version failed: " + err.Error())
}
// 检查要升级的文件
var gzFile = this.apiExe + "." + localVersion + ".gz"
gzReader, err := os.Open(gzFile)
if err != nil {
if !os.IsNotExist(err) {
return err
}
err = func() error {
// 压缩文件
exeReader, err := os.Open(this.apiExe)
if err != nil {
return err
}
defer func() {
_ = exeReader.Close()
}()
var tmpGzFile = gzFile + ".tmp"
gzFileWriter, err := os.OpenFile(tmpGzFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
return err
}
var gzWriter = gzip.NewWriter(gzFileWriter)
defer func() {
_ = gzWriter.Close()
_ = gzFileWriter.Close()
_ = os.Rename(tmpGzFile, gzFile)
}()
_, err = io.Copy(gzWriter, exeReader)
if err != nil {
return err
}
return nil
}()
if err != nil {
return err
}
gzReader, err = os.Open(gzFile)
if err != nil {
return err
}
}
defer func() {
_ = gzReader.Close()
}()
// 开始上传
var hash = md5.New()
var buf = make([]byte, 128*4096)
var isFirst = true
stat, err := gzReader.Stat()
if err != nil {
return err
}
var totalSize = stat.Size()
if totalSize == 0 {
_ = gzReader.Close()
_ = os.Remove(gzFile)
return errors.New("invalid gz file")
}
var uploadedSize int64 = 0
for {
n, err := gzReader.Read(buf)
if n > 0 {
// 计算Hash
hash.Write(buf[:n])
// 上传
_, uploadErr := rpcClient.APINodeRPC().UploadAPINodeFile(rpcClient.Context(0), &pb.UploadAPINodeFileRequest{
Filename: filepath.Base(this.apiExe),
Sum: "",
ChunkData: buf[:n],
IsFirstChunk: isFirst,
IsLastChunk: false,
})
if uploadErr != nil {
return uploadErr
}
// 进度
uploadedSize += int64(n)
this.progress = &Progress{Percent: float64(uploadedSize*100) / float64(totalSize)}
}
if isFirst {
isFirst = false
}
if err != nil {
if err != io.EOF {
return err
}
if err == io.EOF {
_, uploadErr := rpcClient.APINodeRPC().UploadAPINodeFile(rpcClient.Context(0), &pb.UploadAPINodeFileRequest{
Filename: filepath.Base(this.apiExe),
Sum: fmt.Sprintf("%x", hash.Sum(nil)),
ChunkData: buf[:n],
IsFirstChunk: isFirst,
IsLastChunk: true,
})
if uploadErr != nil {
return uploadErr
}
break
}
}
}
return nil
}
func (this *Upgrader) Progress() *Progress {
return this.progress
}

View File

@@ -0,0 +1,22 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package apinodeutils_test
import (
"github.com/TeaOSLab/EdgeAdmin/internal/utils/apinodeutils"
_ "github.com/iwind/TeaGo/bootstrap"
"runtime"
"testing"
)
func TestUpgrader_CanUpgrade(t *testing.T) {
t.Log(apinodeutils.CanUpgrade("0.6.3", runtime.GOOS, runtime.GOARCH))
}
func TestUpgrader_Upgrade(t *testing.T) {
var upgrader = apinodeutils.NewUpgrader(1)
err := upgrader.Upgrade()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,80 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package apinodeutils
import (
"bytes"
"errors"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/iwind/TeaGo/Tea"
stringutil "github.com/iwind/TeaGo/utils/string"
"os"
"os/exec"
"regexp"
"runtime"
"strings"
)
func CanUpgrade(apiVersion string, osName string, arch string) (canUpgrade bool, reason string) {
if len(apiVersion) == 0 {
return false, "current api version should not be empty"
}
if stringutil.VersionCompare(apiVersion, "0.6.4") < 0 {
return false, "api node version must greater than or equal to 0.6.4"
}
if osName != runtime.GOOS {
return false, "os not match: " + osName
}
if arch != runtime.GOARCH {
return false, "arch not match: " + arch
}
stat, err := os.Stat(apiExe())
if err != nil {
return false, "stat error: " + err.Error()
}
if stat.IsDir() {
return false, "is directory"
}
localVersion, err := localVersion()
if err != nil {
return false, "lookup version failed: " + err.Error()
}
if localVersion != teaconst.APINodeVersion {
return false, "not newest api node"
}
if stringutil.VersionCompare(localVersion, apiVersion) <= 0 {
return false, "need not upgrade, local '" + localVersion + "' vs remote '" + apiVersion + "'"
}
return true, ""
}
func localVersion() (string, error) {
var cmd = exec.Command(apiExe(), "-V")
var output = &bytes.Buffer{}
cmd.Stdout = output
err := cmd.Run()
if err != nil {
return "", err
}
var localVersion = strings.TrimSpace(output.String())
// 检查版本号
var reg = regexp.MustCompile(`^[\d.]+$`)
if !reg.MatchString(localVersion) {
return "", errors.New("lookup version failed: " + localVersion)
}
return localVersion, nil
}
func apiExe() string {
return Tea.Root + "/edge-api/bin/edge-api"
}

View File

@@ -0,0 +1,12 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dateutils
// SplitYmd 分隔Ymd格式的日期
// Ymd => Y-m-d
func SplitYmd(day string) string {
if len(day) != 8 {
return day
}
return day[:4] + "-" + day[4:6] + "-" + day[6:]
}

View File

@@ -1,26 +1,40 @@
package utils package utils
import ( import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeCommon/pkg/configutils" "github.com/TeaOSLab/EdgeCommon/pkg/configutils"
"github.com/iwind/TeaGo/logs"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
// LookupCNAME 获取CNAME var sharedDNSClient *dns.Client
func LookupCNAME(host string) (string, error) { var sharedDNSConfig *dns.ClientConfig
config, err := dns.ClientConfigFromFile("/etc/resolv.conf")
if err != nil { func init() {
return "", err if !teaconst.IsMain {
return
} }
c := new(dns.Client) config, err := dns.ClientConfigFromFile("/etc/resolv.conf")
m := new(dns.Msg) if err != nil {
logs.Println("ERROR: configure dns client failed: " + err.Error())
return
}
sharedDNSConfig = config
sharedDNSClient = &dns.Client{}
}
// LookupCNAME 获取CNAME
func LookupCNAME(host string) (string, error) {
var m = new(dns.Msg)
m.SetQuestion(host+".", dns.TypeCNAME) m.SetQuestion(host+".", dns.TypeCNAME)
m.RecursionDesired = true m.RecursionDesired = true
var lastErr error var lastErr error
for _, serverAddr := range config.Servers { for _, serverAddr := range sharedDNSConfig.Servers {
r, _, err := c.Exchange(m, configutils.QuoteIP(serverAddr)+":"+config.Port) r, _, err := sharedDNSClient.Exchange(m, configutils.QuoteIP(serverAddr)+":"+sharedDNSConfig.Port)
if err != nil { if err != nil {
lastErr = err lastErr = err
continue continue

View File

@@ -0,0 +1,12 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package utils_test
import (
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"testing"
)
func TestLookupCNAME(t *testing.T) {
t.Log(utils.LookupCNAME("www.yun4s.cn"))
}

View File

@@ -128,7 +128,7 @@ After=network-online.target
[Service] [Service]
Type=simple Type=simple
Restart=always Restart=always
RestartSec=1s RestartSec=5s
ExecStart=` + exePath + ` daemon ExecStart=` + exePath + ` daemon
ExecStop=` + exePath + ` stop ExecStop=` + exePath + ` stop
ExecReload=` + exePath + ` reload ExecReload=` + exePath + ` reload

View File

@@ -52,11 +52,14 @@ type UpgradeManager struct {
writer *UpgradeFileWriter writer *UpgradeFileWriter
body io.ReadCloser body io.ReadCloser
isCancelled bool isCancelled bool
downloadURL string
} }
func NewUpgradeManager(component string) *UpgradeManager { func NewUpgradeManager(component string, downloadURL string) *UpgradeManager {
return &UpgradeManager{ return &UpgradeManager{
component: component, component: component,
downloadURL: downloadURL,
client: &http.Client{ client: &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
@@ -96,8 +99,8 @@ func (this *UpgradeManager) Start() error {
} }
// 检查新版本 // 检查新版本
var downloadURL = "" var downloadURL = this.downloadURL
{ if len(downloadURL) == 0 {
var url = teaconst.UpdatesURL var url = teaconst.UpdatesURL
var osName = runtime.GOOS var osName = runtime.GOOS
if Tea.IsTesting() && osName == "darwin" { if Tea.IsTesting() && osName == "darwin" {
@@ -105,10 +108,12 @@ func (this *UpgradeManager) Start() error {
} }
url = strings.ReplaceAll(url, "${os}", osName) url = strings.ReplaceAll(url, "${os}", osName)
url = strings.ReplaceAll(url, "${arch}", runtime.GOARCH) url = strings.ReplaceAll(url, "${arch}", runtime.GOARCH)
url = strings.ReplaceAll(url, "${version}", teaconst.Version)
req, err := http.NewRequest(http.MethodGet, url, nil) req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil { if err != nil {
return errors.New("create url request failed: " + err.Error()) return errors.New("create url request failed: " + err.Error())
} }
req.Header.Set("User-Agent", "Edge-Admin/"+teaconst.Version)
resp, err := this.client.Do(req) resp, err := this.client.Do(req)
if err != nil { if err != nil {
@@ -169,6 +174,7 @@ func (this *UpgradeManager) Start() error {
if err != nil { if err != nil {
return errors.New("create download request failed: " + err.Error()) return errors.New("create download request failed: " + err.Error())
} }
req.Header.Set("User-Agent", "Edge-Admin/"+teaconst.Version)
resp, err := this.client.Do(req) resp, err := this.client.Do(req)
if err != nil { if err != nil {

View File

@@ -9,7 +9,7 @@ import (
) )
func TestNewUpgradeManager(t *testing.T) { func TestNewUpgradeManager(t *testing.T) {
var manager = utils.NewUpgradeManager("admin") var manager = utils.NewUpgradeManager("admin", "")
var ticker = time.NewTicker(2 * time.Second) var ticker = time.NewTicker(2 * time.Second)
go func() { go func() {

View File

@@ -102,7 +102,7 @@ func FailPage(action actions.ActionWrapper, err error) {
var html = `<!DOCTYPE html> var html = `<!DOCTYPE html>
<html> <html>
<head> <head>
<title>有系统错误需要处理</title> <title>正在处理...</title>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<style type="text/css"> <style type="text/css">
hr { border-top: 1px #ccc solid; } hr { border-top: 1px #ccc solid; }
@@ -116,7 +116,7 @@ func FailPage(action actions.ActionWrapper, err error) {
html += "<div class=\"red\">API节点正在启动请耐心等待完成" html += "<div class=\"red\">API节点正在启动请耐心等待完成"
if len(apiNodeProgress) > 0 { if len(apiNodeProgress) > 0 {
html += "" + apiNodeProgress html += "" + apiNodeProgress + "(刷新当前页面查看最新状态)"
} }
html += "</div>" html += "</div>"

View File

@@ -45,6 +45,7 @@ func (this *AdminAction) RunGet(params struct {
"isOn": admin.IsOn, "isOn": admin.IsOn,
"isSuper": admin.IsSuper, "isSuper": admin.IsSuper,
"canLogin": admin.CanLogin, "canLogin": admin.CanLogin,
"hasWeakPassword": admin.HasWeakPassword,
"countAccessKeys": countAccessKeys, "countAccessKeys": countAccessKeys,
} }

View File

@@ -15,34 +15,46 @@ func (this *IndexAction) Init() {
this.Nav("", "", "") this.Nav("", "", "")
} }
func (this *IndexAction) RunGet(params struct{}) { func (this *IndexAction) RunGet(params struct {
countResp, err := this.RPC().AdminRPC().CountAllEnabledAdmins(this.AdminContext(), &pb.CountAllEnabledAdminsRequest{}) Keyword string
if err != nil { HasWeakPassword bool
this.ErrorPage(err) }) {
return this.Data["keyword"] = params.Keyword
} this.Data["hasWeakPassword"] = params.HasWeakPassword
page := this.NewPage(countResp.Count)
this.Data["page"] = page.AsHTML()
adminsResp, err := this.RPC().AdminRPC().ListEnabledAdmins(this.AdminContext(), &pb.ListEnabledAdminsRequest{ countResp, err := this.RPC().AdminRPC().CountAllEnabledAdmins(this.AdminContext(), &pb.CountAllEnabledAdminsRequest{
Offset: page.Offset, Keyword: params.Keyword,
Size: page.Size, HasWeakPassword: params.HasWeakPassword,
}) })
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
adminMaps := []maps.Map{} var page = this.NewPage(countResp.Count)
this.Data["page"] = page.AsHTML()
adminsResp, err := this.RPC().AdminRPC().ListEnabledAdmins(this.AdminContext(), &pb.ListEnabledAdminsRequest{
Keyword: params.Keyword,
HasWeakPassword: params.HasWeakPassword,
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
return
}
var adminMaps = []maps.Map{}
for _, admin := range adminsResp.Admins { for _, admin := range adminsResp.Admins {
adminMaps = append(adminMaps, maps.Map{ adminMaps = append(adminMaps, maps.Map{
"id": admin.Id, "id": admin.Id,
"isOn": admin.IsOn, "isOn": admin.IsOn,
"isSuper": admin.IsSuper, "isSuper": admin.IsSuper,
"username": admin.Username, "username": admin.Username,
"fullname": admin.Fullname, "fullname": admin.Fullname,
"createdTime": timeutil.FormatTime("Y-m-d H:i:s", admin.CreatedAt), "createdTime": timeutil.FormatTime("Y-m-d H:i:s", admin.CreatedAt),
"otpLoginIsOn": admin.OtpLogin != nil && admin.OtpLogin.IsOn, "otpLoginIsOn": admin.OtpLogin != nil && admin.OtpLogin.IsOn,
"canLogin": admin.CanLogin, "canLogin": admin.CanLogin,
"hasWeakPassword": admin.HasWeakPassword,
}) })
} }
this.Data["admins"] = adminMaps this.Data["admins"] = adminMaps

View File

@@ -3,12 +3,15 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/apinodeutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/logs" "github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
stringutil "github.com/iwind/TeaGo/utils/string"
timeutil "github.com/iwind/TeaGo/utils/time" timeutil "github.com/iwind/TeaGo/utils/time"
"time" "time"
) )
@@ -44,7 +47,7 @@ func (this *IndexAction) RunGet(params struct{}) {
for _, node := range nodesResp.ApiNodes { for _, node := range nodesResp.ApiNodes {
// 状态 // 状态
status := &nodeconfigs.NodeStatus{} var status = &nodeconfigs.NodeStatus{}
if len(node.StatusJSON) > 0 { if len(node.StatusJSON) > 0 {
err = json.Unmarshal(node.StatusJSON, &status) err = json.Unmarshal(node.StatusJSON, &status)
if err != nil { if err != nil {
@@ -55,7 +58,7 @@ func (this *IndexAction) RunGet(params struct{}) {
} }
// Rest地址 // Rest地址
restAccessAddrs := []string{} var restAccessAddrs = []string{}
if node.RestIsOn { if node.RestIsOn {
if len(node.RestHTTPJSON) > 0 { if len(node.RestHTTPJSON) > 0 {
httpConfig := &serverconfigs.HTTPProtocolConfig{} httpConfig := &serverconfigs.HTTPProtocolConfig{}
@@ -79,13 +82,16 @@ func (this *IndexAction) RunGet(params struct{}) {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
_ = httpsConfig.Init() _ = httpsConfig.Init(nil)
if httpsConfig.IsOn && len(httpsConfig.Listen) > 0 { if httpsConfig.IsOn && len(httpsConfig.Listen) > 0 {
restAccessAddrs = append(restAccessAddrs, httpsConfig.FullAddresses()...) restAccessAddrs = append(restAccessAddrs, httpsConfig.FullAddresses()...)
} }
} }
} }
var shouldUpgrade = status.IsActive && len(status.BuildVersion) > 0 && stringutil.VersionCompare(teaconst.APINodeVersion, status.BuildVersion) > 0
canUpgrade, _ := apinodeutils.CanUpgrade(status.BuildVersion, status.OS, status.Arch)
nodeMaps = append(nodeMaps, maps.Map{ nodeMaps = append(nodeMaps, maps.Map{
"id": node.Id, "id": node.Id,
"isOn": node.IsOn, "isOn": node.IsOn,
@@ -94,14 +100,17 @@ func (this *IndexAction) RunGet(params struct{}) {
"restAccessAddrs": restAccessAddrs, "restAccessAddrs": restAccessAddrs,
"isPrimary": node.IsPrimary, "isPrimary": node.IsPrimary,
"status": maps.Map{ "status": maps.Map{
"isActive": status.IsActive, "isActive": status.IsActive,
"updatedAt": status.UpdatedAt, "updatedAt": status.UpdatedAt,
"hostname": status.Hostname, "hostname": status.Hostname,
"cpuUsage": status.CPUUsage, "cpuUsage": status.CPUUsage,
"cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100), "cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100),
"memUsage": status.MemoryUsage, "memUsage": status.MemoryUsage,
"memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100), "memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100),
"buildVersion": status.BuildVersion, "buildVersion": status.BuildVersion,
"latestVersion": teaconst.APINodeVersion,
"shouldUpgrade": shouldUpgrade,
"canUpgrade": shouldUpgrade && canUpgrade,
}, },
}) })
} }

View File

@@ -65,7 +65,10 @@ func (this *IndexAction) RunGet(params struct {
// 证书信息 // 证书信息
var certs = []*sslconfigs.SSLCertConfig{} var certs = []*sslconfigs.SSLCertConfig{}
if httpsConfig.SSLPolicyRef != nil && httpsConfig.SSLPolicyRef.SSLPolicyId > 0 { if httpsConfig.SSLPolicyRef != nil && httpsConfig.SSLPolicyRef.SSLPolicyId > 0 {
sslPolicyConfigResp, err := this.RPC().SSLPolicyRPC().FindEnabledSSLPolicyConfig(this.AdminContext(), &pb.FindEnabledSSLPolicyConfigRequest{SslPolicyId: httpsConfig.SSLPolicyRef.SSLPolicyId}) sslPolicyConfigResp, err := this.RPC().SSLPolicyRPC().FindEnabledSSLPolicyConfig(this.AdminContext(), &pb.FindEnabledSSLPolicyConfigRequest{
SslPolicyId: httpsConfig.SSLPolicyRef.SSLPolicyId,
IgnoreData: true,
})
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return

View File

@@ -24,7 +24,10 @@ func init() {
GetPost("/update", new(UpdateAction)). GetPost("/update", new(UpdateAction)).
Get("/install", new(InstallAction)). Get("/install", new(InstallAction)).
Get("/logs", new(LogsAction)). Get("/logs", new(LogsAction)).
GetPost("/upgradePopup", new(UpgradePopupAction)).
Post("/upgradeCheck", new(UpgradeCheckAction)).
//
EndAll() EndAll()
}) })
} }

View File

@@ -85,7 +85,10 @@ func (this *UpdateAction) RunGet(params struct {
var certs = []*sslconfigs.SSLCertConfig{} var certs = []*sslconfigs.SSLCertConfig{}
var sslPolicyId = int64(0) var sslPolicyId = int64(0)
if httpsConfig.SSLPolicyRef != nil && httpsConfig.SSLPolicyRef.SSLPolicyId > 0 { if httpsConfig.SSLPolicyRef != nil && httpsConfig.SSLPolicyRef.SSLPolicyId > 0 {
sslPolicyConfigResp, err := this.RPC().SSLPolicyRPC().FindEnabledSSLPolicyConfig(this.AdminContext(), &pb.FindEnabledSSLPolicyConfigRequest{SslPolicyId: httpsConfig.SSLPolicyRef.SSLPolicyId}) sslPolicyConfigResp, err := this.RPC().SSLPolicyRPC().FindEnabledSSLPolicyConfig(this.AdminContext(), &pb.FindEnabledSSLPolicyConfigRequest{
SslPolicyId: httpsConfig.SSLPolicyRef.SSLPolicyId,
IgnoreData: true,
})
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return

View File

@@ -0,0 +1,67 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
// UpgradeCheckAction 检查升级结果
type UpgradeCheckAction struct {
actionutils.ParentAction
}
func (this *UpgradeCheckAction) Init() {
this.Nav("", "", "")
}
func (this *UpgradeCheckAction) RunPost(params struct {
NodeId int64
}) {
this.Data["isOk"] = false
nodeResp, err := this.RPC().APINodeRPC().FindEnabledAPINode(this.AdminContext(), &pb.FindEnabledAPINodeRequest{ApiNodeId: params.NodeId})
if err != nil {
this.Success()
return
}
var node = nodeResp.ApiNode
if node == nil || len(node.AccessAddrs) == 0 {
this.Success()
return
}
apiConfig, err := configs.LoadAPIConfig()
if err != nil {
this.Success()
return
}
var newAPIConfig = apiConfig.Clone()
newAPIConfig.RPC.Endpoints = node.AccessAddrs
rpcClient, err := rpc.NewRPCClient(newAPIConfig, false)
if err != nil {
this.Success()
return
}
versionResp, err := rpcClient.APINodeRPC().FindCurrentAPINodeVersion(rpcClient.Context(0), &pb.FindCurrentAPINodeVersionRequest{})
if err != nil {
this.Success()
return
}
if versionResp.Version != teaconst.Version {
this.Success()
return
}
this.Data["isOk"] = true
this.Success()
}

View File

@@ -0,0 +1,124 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package node
import (
"encoding/json"
"errors"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/apinodeutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"strings"
)
type UpgradePopupAction struct {
actionutils.ParentAction
}
func (this *UpgradePopupAction) Init() {
this.Nav("", "", "")
}
func (this *UpgradePopupAction) RunGet(params struct {
NodeId int64
}) {
this.Data["nodeId"] = params.NodeId
this.Data["nodeName"] = ""
this.Data["currentVersion"] = ""
this.Data["latestVersion"] = ""
this.Data["result"] = ""
this.Data["resultIsOk"] = true
this.Data["canUpgrade"] = false
this.Data["isUpgrading"] = false
nodeResp, err := this.RPC().APINodeRPC().FindEnabledAPINode(this.AdminContext(), &pb.FindEnabledAPINodeRequest{ApiNodeId: params.NodeId})
if err != nil {
this.ErrorPage(err)
return
}
var node = nodeResp.ApiNode
if node == nil {
this.Data["result"] = "要升级的节点不存在"
this.Data["resultIsOk"] = false
this.Show()
return
}
this.Data["nodeName"] = node.Name + " / [" + strings.Join(node.AccessAddrs, ", ") + "]"
// 节点状态
var status = &nodeconfigs.NodeStatus{}
if len(node.StatusJSON) > 0 {
err = json.Unmarshal(node.StatusJSON, &status)
if err != nil {
this.ErrorPage(errors.New("decode status failed: " + err.Error()))
return
}
this.Data["currentVersion"] = status.BuildVersion
} else {
this.Data["result"] = "无法检测到节点当前版本"
this.Data["resultIsOk"] = false
this.Show()
return
}
this.Data["latestVersion"] = teaconst.APINodeVersion
if status.IsActive && len(status.BuildVersion) > 0 {
canUpgrade, reason := apinodeutils.CanUpgrade(status.BuildVersion, status.OS, status.Arch)
if !canUpgrade {
this.Data["result"] = reason
this.Data["resultIsOk"] = false
this.Show()
return
}
this.Data["canUpgrade"] = true
this.Data["result"] = "等待升级"
this.Data["resultIsOk"] = true
} else {
this.Data["result"] = "当前节点非连接状态无法远程升级"
this.Data["resultIsOk"] = false
this.Show()
return
}
// 是否正在升级
var oldUpgrader = apinodeutils.SharedManager.FindUpgrader(params.NodeId)
if oldUpgrader != nil {
this.Data["result"] = "正在升级中..."
this.Data["resultIsOk"] = false
this.Data["isUpgrading"] = true
}
this.Show()
}
func (this *UpgradePopupAction) RunPost(params struct {
NodeId int64
Must *actions.Must
CSRF *actionutils.CSRF
}) {
var manager = apinodeutils.SharedManager
var oldUpgrader = manager.FindUpgrader(params.NodeId)
if oldUpgrader != nil {
this.Fail("正在升级中,无需重复提交 ...")
return
}
var upgrader = apinodeutils.NewUpgrader(params.NodeId)
manager.AddUpgrader(upgrader)
defer func() {
manager.RemoveUpgrader(upgrader)
}()
err := upgrader.Upgrade()
if err != nil {
this.Fail("升级失败:" + err.Error())
return
}
this.Success()
}

View File

@@ -89,7 +89,7 @@ func (this *DetailAction) RunGet(params struct {
return return
} }
var ipAddresses = ipAddressesResp.NodeIPAddresses var ipAddresses = ipAddressesResp.NodeIPAddresses
ipAddressMaps := []maps.Map{} var ipAddressMaps = []maps.Map{}
for _, addr := range ipAddressesResp.NodeIPAddresses { for _, addr := range ipAddressesResp.NodeIPAddresses {
thresholds, err := ipaddressutils.InitNodeIPAddressThresholds(this.Parent(), addr.Id) thresholds, err := ipaddressutils.InitNodeIPAddressThresholds(this.Parent(), addr.Id)
if err != nil { if err != nil {
@@ -103,6 +103,15 @@ func (this *DetailAction) RunGet(params struct {
addr.Ip = addr.BackupIP addr.Ip = addr.BackupIP
} }
// 专属集群
var addrClusterMaps = []maps.Map{}
for _, addrCluster := range addr.NodeClusters {
addrClusterMaps = append(addrClusterMaps, maps.Map{
"id": addrCluster.Id,
"name": addrCluster.Name,
})
}
ipAddressMaps = append(ipAddressMaps, maps.Map{ ipAddressMaps = append(ipAddressMaps, maps.Map{
"id": addr.Id, "id": addr.Id,
"name": addr.Name, "name": addr.Name,
@@ -111,6 +120,7 @@ func (this *DetailAction) RunGet(params struct {
"canAccess": addr.CanAccess, "canAccess": addr.CanAccess,
"isOn": addr.IsOn, "isOn": addr.IsOn,
"isUp": addr.IsUp, "isUp": addr.IsUp,
"clusters": addrClusterMaps,
"thresholds": thresholds, "thresholds": thresholds,
}) })
} }
@@ -152,16 +162,31 @@ func (this *DetailAction) RunGet(params struct {
if !addr.CanAccess || !addr.IsUp || !addr.IsOn { if !addr.CanAccess || !addr.IsUp || !addr.IsOn {
continue continue
} }
// 过滤集群
if len(addr.NodeClusters) > 0 {
var inCluster = false
for _, addrCluster := range addr.NodeClusters {
if addrCluster.Id == cluster.Id {
inCluster = true
}
}
if !inCluster {
continue
}
}
for _, route := range dnsInfo.Routes { for _, route := range dnsInfo.Routes {
var recordType = "A" var recordType = "A"
if utils.IsIPv6(addr.Ip) { if utils.IsIPv6(addr.Ip) {
recordType = "AAAA" recordType = "AAAA"
} }
recordMaps = append(recordMaps, maps.Map{ recordMaps = append(recordMaps, maps.Map{
"name": dnsInfo.NodeClusterDNSName + "." + domainName, "name": dnsInfo.NodeClusterDNSName + "." + domainName,
"type": recordType, "type": recordType,
"route": route.Name, "route": route.Name,
"value": addr.Ip, "value": addr.Ip,
"clusterName": cluster.Name,
}) })
} }
} }
@@ -179,8 +204,8 @@ func (this *DetailAction) RunGet(params struct {
} }
} }
grantMap := maps.Map{} var grantMap = maps.Map{}
grantId := loginParams.GetInt64("grantId") var grantId = loginParams.GetInt64("grantId")
if grantId > 0 { if grantId > 0 {
grantResp, err := this.RPC().NodeGrantRPC().FindEnabledNodeGrant(this.AdminContext(), &pb.FindEnabledNodeGrantRequest{NodeGrantId: grantId}) grantResp, err := this.RPC().NodeGrantRPC().FindEnabledNodeGrant(this.AdminContext(), &pb.FindEnabledNodeGrantRequest{NodeGrantId: grantId})
if err != nil { if err != nil {

View File

@@ -64,12 +64,22 @@ func (this *UpdateAction) RunGet(params struct {
} }
var ipAddressMaps = []maps.Map{} var ipAddressMaps = []maps.Map{}
for _, addr := range ipAddressesResp.NodeIPAddresses { for _, addr := range ipAddressesResp.NodeIPAddresses {
// 阈值
thresholds, err := ipaddressutils.InitNodeIPAddressThresholds(this.Parent(), addr.Id) thresholds, err := ipaddressutils.InitNodeIPAddressThresholds(this.Parent(), addr.Id)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
// 专属集群
var clusterMaps = []maps.Map{}
for _, addrCluster := range addr.NodeClusters {
clusterMaps = append(clusterMaps, maps.Map{
"id": addrCluster.Id,
"name": addrCluster.Name,
})
}
ipAddressMaps = append(ipAddressMaps, maps.Map{ ipAddressMaps = append(ipAddressMaps, maps.Map{
"id": addr.Id, "id": addr.Id,
"name": addr.Name, "name": addr.Name,
@@ -78,6 +88,7 @@ func (this *UpdateAction) RunGet(params struct {
"isOn": addr.IsOn, "isOn": addr.IsOn,
"isUp": addr.IsUp, "isUp": addr.IsUp,
"thresholds": thresholds, "thresholds": thresholds,
"clusters": clusterMaps,
}) })
} }

View File

@@ -32,11 +32,12 @@ func (this *NodesAction) RunGet(params struct {
Keyword string Keyword string
Level int32 Level int32
CpuOrder string CpuOrder string
MemoryOrder string MemoryOrder string
TrafficInOrder string TrafficInOrder string
TrafficOutOrder string TrafficOutOrder string
LoadOrder string LoadOrder string
ConnectionsOrder string
}) { }) {
this.Data["groupId"] = params.GroupId this.Data["groupId"] = params.GroupId
this.Data["regionId"] = params.RegionId this.Data["regionId"] = params.RegionId
@@ -44,7 +45,7 @@ func (this *NodesAction) RunGet(params struct {
this.Data["activeState"] = params.ActiveState this.Data["activeState"] = params.ActiveState
this.Data["keyword"] = params.Keyword this.Data["keyword"] = params.Keyword
this.Data["level"] = params.Level this.Data["level"] = params.Level
this.Data["hasOrder"] = len(params.CpuOrder) > 0 || len(params.MemoryOrder) > 0 || len(params.TrafficInOrder) > 0 || len(params.TrafficOutOrder) > 0 || len(params.LoadOrder) > 0 this.Data["hasOrder"] = len(params.CpuOrder) > 0 || len(params.MemoryOrder) > 0 || len(params.TrafficInOrder) > 0 || len(params.TrafficOutOrder) > 0 || len(params.LoadOrder) > 0 || len(params.ConnectionsOrder) > 0
// 集群是否已经设置了线路 // 集群是否已经设置了线路
clusterDNSResp, err := this.RPC().NodeClusterRPC().FindEnabledNodeClusterDNS(this.AdminContext(), &pb.FindEnabledNodeClusterDNSRequest{NodeClusterId: params.ClusterId}) clusterDNSResp, err := this.RPC().NodeClusterRPC().FindEnabledNodeClusterDNS(this.AdminContext(), &pb.FindEnabledNodeClusterDNSRequest{NodeClusterId: params.ClusterId})
@@ -112,6 +113,10 @@ func (this *NodesAction) RunGet(params struct {
req.LoadAsc = true req.LoadAsc = true
} else if params.LoadOrder == "desc" { } else if params.LoadOrder == "desc" {
req.LoadDesc = true req.LoadDesc = true
} else if params.ConnectionsOrder == "asc" {
req.ConnectionsAsc = true
} else if params.ConnectionsOrder == "desc" {
req.ConnectionsDesc = true
} }
nodesResp, err := this.RPC().NodeRPC().ListEnabledNodesMatch(this.AdminContext(), req) nodesResp, err := this.RPC().NodeRPC().ListEnabledNodesMatch(this.AdminContext(), req)
if err != nil { if err != nil {
@@ -121,8 +126,8 @@ func (this *NodesAction) RunGet(params struct {
var nodeMaps = []maps.Map{} var nodeMaps = []maps.Map{}
for _, node := range nodesResp.Nodes { for _, node := range nodesResp.Nodes {
// 状态 // 状态
isSynced := false var isSynced = false
status := &nodeconfigs.NodeStatus{} var status = &nodeconfigs.NodeStatus{}
if len(node.StatusJSON) > 0 { if len(node.StatusJSON) > 0 {
err = json.Unmarshal(node.StatusJSON, &status) err = json.Unmarshal(node.StatusJSON, &status)
if err != nil { if err != nil {
@@ -142,8 +147,17 @@ func (this *NodesAction) RunGet(params struct {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
ipAddresses := []maps.Map{} var ipAddresses = []maps.Map{}
for _, addr := range ipAddressesResp.NodeIPAddresses { for _, addr := range ipAddressesResp.NodeIPAddresses {
// 专属集群
var addrClusterMaps = []maps.Map{}
for _, addrCluster := range addr.NodeClusters {
addrClusterMaps = append(addrClusterMaps, maps.Map{
"id": addrCluster.Id,
"name": addrCluster.Name,
})
}
ipAddresses = append(ipAddresses, maps.Map{ ipAddresses = append(ipAddresses, maps.Map{
"id": addr.Id, "id": addr.Id,
"name": addr.Name, "name": addr.Name,
@@ -151,6 +165,7 @@ func (this *NodesAction) RunGet(params struct {
"canAccess": addr.CanAccess, "canAccess": addr.CanAccess,
"isUp": addr.IsUp, "isUp": addr.IsUp,
"isOn": addr.IsOn, "isOn": addr.IsOn,
"clusters": addrClusterMaps,
}) })
} }
@@ -201,16 +216,17 @@ func (this *NodesAction) RunGet(params struct {
"error": node.InstallStatus.Error, "error": node.InstallStatus.Error,
}, },
"status": maps.Map{ "status": maps.Map{
"isActive": status.IsActive, "isActive": status.IsActive,
"updatedAt": status.UpdatedAt, "updatedAt": status.UpdatedAt,
"hostname": status.Hostname, "hostname": status.Hostname,
"cpuUsage": status.CPUUsage, "cpuUsage": status.CPUUsage,
"cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100), "cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100),
"memUsage": status.MemoryUsage, "memUsage": status.MemoryUsage,
"memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100), "memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100),
"trafficInBytes": status.TrafficInBytes, "trafficInBytes": status.TrafficInBytes,
"trafficOutBytes": status.TrafficOutBytes, "trafficOutBytes": status.TrafficOutBytes,
"load1m": numberutils.FormatFloat2(status.Load1m), "load1m": numberutils.FormatFloat2(status.Load1m),
"countConnections": status.ConnectionCount,
}, },
"cluster": maps.Map{ "cluster": maps.Map{
"id": node.NodeCluster.Id, "id": node.NodeCluster.Id,

View File

@@ -78,10 +78,13 @@ func (this *IndexAction) RunPost(params struct {
HttpAllAllowNodeIP bool HttpAllAllowNodeIP bool
HttpAllDefaultDomain string HttpAllDefaultDomain string
HttpAllSupportsLowVersionHTTP bool
HttpAccessLogEnableRequestHeaders bool HttpAccessLogEnableRequestHeaders bool
HttpAccessLogEnableResponseHeaders bool HttpAccessLogEnableResponseHeaders bool
HttpAccessLogCommonRequestHeadersOnly bool HttpAccessLogCommonRequestHeadersOnly bool
HttpAccessLogEnableCookies bool HttpAccessLogEnableCookies bool
HttpAccessLogEnableServerNotFound bool
LogRecordServerError bool LogRecordServerError bool
@@ -133,11 +136,15 @@ func (this *IndexAction) RunPost(params struct {
config.HTTPAll.AllowNodeIP = params.HttpAllAllowNodeIP config.HTTPAll.AllowNodeIP = params.HttpAllAllowNodeIP
config.HTTPAll.DefaultDomain = params.HttpAllDefaultDomain config.HTTPAll.DefaultDomain = params.HttpAllDefaultDomain
// HTTP All
config.HTTPAll.SupportsLowVersionHTTP = params.HttpAllSupportsLowVersionHTTP
// 访问日志 // 访问日志
config.HTTPAccessLog.EnableRequestHeaders = params.HttpAccessLogEnableRequestHeaders config.HTTPAccessLog.EnableRequestHeaders = params.HttpAccessLogEnableRequestHeaders
config.HTTPAccessLog.EnableResponseHeaders = params.HttpAccessLogEnableResponseHeaders config.HTTPAccessLog.EnableResponseHeaders = params.HttpAccessLogEnableResponseHeaders
config.HTTPAccessLog.CommonRequestHeadersOnly = params.HttpAccessLogCommonRequestHeadersOnly config.HTTPAccessLog.CommonRequestHeadersOnly = params.HttpAccessLogCommonRequestHeadersOnly
config.HTTPAccessLog.EnableCookies = params.HttpAccessLogEnableCookies config.HTTPAccessLog.EnableCookies = params.HttpAccessLogEnableCookies
config.HTTPAccessLog.EnableServerNotFound = params.HttpAccessLogEnableServerNotFound
// 日志 // 日志
config.Log.RecordServerError = params.LogRecordServerError config.Log.RecordServerError = params.LogRecordServerError

View File

@@ -68,7 +68,7 @@ func (this *ClusterHelper) BeforeAction(actionPtr actions.ActionWrapper) (goNext
actionutils.SetTabbar(action, tabbar) actionutils.SetTabbar(action, tabbar)
// 左侧菜单 // 左侧菜单
secondMenuItem := action.Data.GetString("secondMenuItem") var secondMenuItem = action.Data.GetString("secondMenuItem")
switch selectedTabbar { switch selectedTabbar {
case "setting": case "setting":
var menuItems = this.createSettingMenu(cluster, clusterInfo, secondMenuItem) var menuItems = this.createSettingMenu(cluster, clusterInfo, secondMenuItem)
@@ -157,6 +157,7 @@ func (this *ClusterHelper) createSettingMenu(cluster *pb.NodeCluster, info *pb.F
"name": "服务设置", "name": "服务设置",
"url": "/clusters/cluster/settings/global-server-config?clusterId=" + clusterId, "url": "/clusters/cluster/settings/global-server-config?clusterId=" + clusterId,
"isActive": selectedItem == "globalServerConfig", "isActive": selectedItem == "globalServerConfig",
"isOn": true,
}) })
items = append(items, maps.Map{ items = append(items, maps.Map{

View File

@@ -6,6 +6,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/grants/grantutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/grants/grantutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"golang.org/x/crypto/ssh"
) )
type CreateAction struct { type CreateAction struct {
@@ -50,6 +51,18 @@ func (this *CreateAction) RunPost(params struct {
if len(params.PrivateKey) == 0 { if len(params.PrivateKey) == 0 {
this.FailField("privateKey", "请输入RSA私钥") this.FailField("privateKey", "请输入RSA私钥")
} }
// 验证私钥
var err error
if len(params.Passphrase) > 0 {
_, err = ssh.ParsePrivateKeyWithPassphrase([]byte(params.PrivateKey), []byte(params.Passphrase))
} else {
_, err = ssh.ParsePrivateKey([]byte(params.PrivateKey))
}
if err != nil {
this.Fail("私钥验证失败,请检查格式:" + err.Error())
return
}
default: default:
this.Fail("请选择正确的认证方式") this.Fail("请选择正确的认证方式")
} }

View File

@@ -7,6 +7,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
"golang.org/x/crypto/ssh"
) )
type CreatePopupAction struct { type CreatePopupAction struct {
@@ -51,6 +52,18 @@ func (this *CreatePopupAction) RunPost(params struct {
if len(params.PrivateKey) == 0 { if len(params.PrivateKey) == 0 {
this.FailField("privateKey", "请输入RSA私钥") this.FailField("privateKey", "请输入RSA私钥")
} }
// 验证私钥
var err error
if len(params.Passphrase) > 0 {
_, err = ssh.ParsePrivateKeyWithPassphrase([]byte(params.PrivateKey), []byte(params.Passphrase))
} else {
_, err = ssh.ParsePrivateKey([]byte(params.PrivateKey))
}
if err != nil {
this.Fail("私钥验证失败,请检查格式:" + err.Error())
return
}
default: default:
this.Fail("请选择正确的认证方式") this.Fail("请选择正确的认证方式")
} }

View File

@@ -7,6 +7,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
"golang.org/x/crypto/ssh"
) )
type UpdateAction struct { type UpdateAction struct {
@@ -83,6 +84,18 @@ func (this *UpdateAction) RunPost(params struct {
if len(params.PrivateKey) == 0 { if len(params.PrivateKey) == 0 {
this.FailField("privateKey", "请输入RSA私钥") this.FailField("privateKey", "请输入RSA私钥")
} }
// 验证私钥
var err error
if len(params.Passphrase) > 0 {
_, err = ssh.ParsePrivateKeyWithPassphrase([]byte(params.PrivateKey), []byte(params.Passphrase))
} else {
_, err = ssh.ParsePrivateKey([]byte(params.PrivateKey))
}
if err != nil {
this.Fail("私钥验证失败,请检查格式:" + err.Error())
return
}
default: default:
this.Fail("请选择正确的认证方式") this.Fail("请选择正确的认证方式")
} }

View File

@@ -7,6 +7,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
"golang.org/x/crypto/ssh"
) )
type UpdatePopupAction struct { type UpdatePopupAction struct {
@@ -83,6 +84,18 @@ func (this *UpdatePopupAction) RunPost(params struct {
if len(params.PrivateKey) == 0 { if len(params.PrivateKey) == 0 {
this.FailField("privateKey", "请输入RSA私钥") this.FailField("privateKey", "请输入RSA私钥")
} }
// 验证私钥
var err error
if len(params.Passphrase) > 0 {
_, err = ssh.ParsePrivateKeyWithPassphrase([]byte(params.PrivateKey), []byte(params.Passphrase))
} else {
_, err = ssh.ParsePrivateKey([]byte(params.PrivateKey))
}
if err != nil {
this.Fail("私钥验证失败,请检查格式:" + err.Error())
return
}
default: default:
this.Fail("请选择正确的认证方式") this.Fail("请选择正确的认证方式")
} }

View File

@@ -0,0 +1,60 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package logs
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type DeleteAllAction struct {
actionutils.ParentAction
}
func (this *DeleteAllAction) RunPost(params struct {
DayFrom string
DayTo string
Keyword string
Level string
Type string // unread, needFix
Tag string
ClusterId int64
NodeId int64
}) {
defer this.CreateLogInfo("批量删除节点运行日志")
// 目前仅允许通过关键词删除,防止误删
if len(params.Keyword) == 0 {
this.Fail("目前仅允许通过关键词删除")
return
}
var fixedState configutils.BoolState = 0
var allServers = false
if params.Type == "needFix" {
fixedState = configutils.BoolStateNo
allServers = true
}
_, err := this.RPC().NodeLogRPC().DeleteNodeLogs(this.AdminContext(), &pb.DeleteNodeLogsRequest{
NodeClusterId: params.ClusterId,
NodeId: params.NodeId,
Role: nodeconfigs.NodeRoleNode,
DayFrom: params.DayFrom,
DayTo: params.DayTo,
Keyword: params.Keyword,
Level: params.Level,
IsUnread: params.Type == "unread",
Tag: params.Tag,
FixedState: int32(fixedState),
AllServers: allServers,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -39,6 +39,7 @@ func (this *IndexAction) RunGet(params struct {
this.Data["dayFrom"] = params.DayFrom this.Data["dayFrom"] = params.DayFrom
this.Data["dayTo"] = params.DayTo this.Data["dayTo"] = params.DayTo
this.Data["keyword"] = params.Keyword this.Data["keyword"] = params.Keyword
this.Data["searchedKeyword"] = params.Keyword
this.Data["level"] = params.Level this.Data["level"] = params.Level
this.Data["type"] = params.Type this.Data["type"] = params.Type
this.Data["tag"] = params.Tag this.Data["tag"] = params.Tag
@@ -97,6 +98,8 @@ func (this *IndexAction) RunGet(params struct {
return return
} }
var count = countResp.Count var count = countResp.Count
this.Data["countLogs"] = count
var page = this.NewPage(count) var page = this.NewPage(count)
this.Data["page"] = page.AsHTML() this.Data["page"] = page.AsHTML()

View File

@@ -18,6 +18,7 @@ func init() {
Post("/readAllLogs", new(ReadAllLogsAction)). Post("/readAllLogs", new(ReadAllLogsAction)).
Post("/fix", new(FixAction)). Post("/fix", new(FixAction)).
Post("/fixAll", new(FixAllAction)). Post("/fixAll", new(FixAllAction)).
Post("/deleteAll", new(DeleteAllAction)).
EndAll() EndAll()
}) })
} }

View File

@@ -33,11 +33,12 @@ func (this *NodesAction) RunGet(params struct {
Keyword string Keyword string
Level int32 Level int32
CpuOrder string CpuOrder string
MemoryOrder string MemoryOrder string
TrafficInOrder string TrafficInOrder string
TrafficOutOrder string TrafficOutOrder string
LoadOrder string LoadOrder string
ConnectionsOrder string
}) { }) {
this.Data["groupId"] = params.GroupId this.Data["groupId"] = params.GroupId
this.Data["regionId"] = params.RegionId this.Data["regionId"] = params.RegionId
@@ -46,7 +47,7 @@ func (this *NodesAction) RunGet(params struct {
this.Data["keyword"] = params.Keyword this.Data["keyword"] = params.Keyword
this.Data["level"] = params.Level this.Data["level"] = params.Level
this.Data["clusterId"] = params.ClusterId this.Data["clusterId"] = params.ClusterId
this.Data["hasOrder"] = len(params.CpuOrder) > 0 || len(params.MemoryOrder) > 0 || len(params.TrafficInOrder) > 0 || len(params.TrafficOutOrder) > 0 || len(params.LoadOrder) > 0 this.Data["hasOrder"] = len(params.CpuOrder) > 0 || len(params.MemoryOrder) > 0 || len(params.TrafficInOrder) > 0 || len(params.TrafficOutOrder) > 0 || len(params.LoadOrder) > 0 || len(params.ConnectionsOrder) > 0
// 集群是否已经设置了线路 // 集群是否已经设置了线路
clusterDNSResp, err := this.RPC().NodeClusterRPC().FindEnabledNodeClusterDNS(this.AdminContext(), &pb.FindEnabledNodeClusterDNSRequest{NodeClusterId: params.ClusterId}) clusterDNSResp, err := this.RPC().NodeClusterRPC().FindEnabledNodeClusterDNS(this.AdminContext(), &pb.FindEnabledNodeClusterDNSRequest{NodeClusterId: params.ClusterId})
@@ -114,6 +115,10 @@ func (this *NodesAction) RunGet(params struct {
req.LoadAsc = true req.LoadAsc = true
} else if params.LoadOrder == "desc" { } else if params.LoadOrder == "desc" {
req.LoadDesc = true req.LoadDesc = true
} else if params.ConnectionsOrder == "asc" {
req.ConnectionsAsc = true
} else if params.ConnectionsOrder == "desc" {
req.ConnectionsDesc = true
} }
nodesResp, err := this.RPC().NodeRPC().ListEnabledNodesMatch(this.AdminContext(), req) nodesResp, err := this.RPC().NodeRPC().ListEnabledNodesMatch(this.AdminContext(), req)
if err != nil { if err != nil {
@@ -123,8 +128,8 @@ func (this *NodesAction) RunGet(params struct {
var nodeMaps = []maps.Map{} var nodeMaps = []maps.Map{}
for _, node := range nodesResp.Nodes { for _, node := range nodesResp.Nodes {
// 状态 // 状态
isSynced := false var isSynced = false
status := &nodeconfigs.NodeStatus{} var status = &nodeconfigs.NodeStatus{}
if len(node.StatusJSON) > 0 { if len(node.StatusJSON) > 0 {
err = json.Unmarshal(node.StatusJSON, &status) err = json.Unmarshal(node.StatusJSON, &status)
if err != nil { if err != nil {
@@ -144,8 +149,17 @@ func (this *NodesAction) RunGet(params struct {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
ipAddresses := []maps.Map{} var ipAddresses = []maps.Map{}
for _, addr := range ipAddressesResp.NodeIPAddresses { for _, addr := range ipAddressesResp.NodeIPAddresses {
// 专属集群
var addrClusterMaps = []maps.Map{}
for _, addrCluster := range addr.NodeClusters {
addrClusterMaps = append(addrClusterMaps, maps.Map{
"id": addrCluster.Id,
"name": addrCluster.Name,
})
}
ipAddresses = append(ipAddresses, maps.Map{ ipAddresses = append(ipAddresses, maps.Map{
"id": addr.Id, "id": addr.Id,
"name": addr.Name, "name": addr.Name,
@@ -153,6 +167,7 @@ func (this *NodesAction) RunGet(params struct {
"canAccess": addr.CanAccess, "canAccess": addr.CanAccess,
"isUp": addr.IsUp, "isUp": addr.IsUp,
"isOn": addr.IsOn, "isOn": addr.IsOn,
"clusters": addrClusterMaps,
}) })
} }
@@ -175,7 +190,7 @@ func (this *NodesAction) RunGet(params struct {
} }
// DNS // DNS
dnsRouteNames := []string{} var dnsRouteNames = []string{}
for _, route := range node.DnsRoutes { for _, route := range node.DnsRoutes {
dnsRouteNames = append(dnsRouteNames, route.Name) dnsRouteNames = append(dnsRouteNames, route.Name)
} }
@@ -203,16 +218,17 @@ func (this *NodesAction) RunGet(params struct {
"error": node.InstallStatus.Error, "error": node.InstallStatus.Error,
}, },
"status": maps.Map{ "status": maps.Map{
"isActive": status.IsActive, "isActive": status.IsActive,
"updatedAt": status.UpdatedAt, "updatedAt": status.UpdatedAt,
"hostname": status.Hostname, "hostname": status.Hostname,
"cpuUsage": status.CPUUsage, "cpuUsage": status.CPUUsage,
"cpuUsageText": numberutils.FormatFloat2(status.CPUUsage * 100), "cpuUsageText": numberutils.FormatFloat2(status.CPUUsage * 100),
"memUsage": status.MemoryUsage, "memUsage": status.MemoryUsage,
"memUsageText": numberutils.FormatFloat2(status.MemoryUsage * 100), "memUsageText": numberutils.FormatFloat2(status.MemoryUsage * 100),
"trafficInBytes": status.TrafficInBytes, "trafficInBytes": status.TrafficInBytes,
"trafficOutBytes": status.TrafficOutBytes, "trafficOutBytes": status.TrafficOutBytes,
"load1m": numberutils.FormatFloat2(status.Load1m), "load1m": numberutils.FormatFloat2(status.Load1m),
"countConnections": status.ConnectionCount,
}, },
"cluster": maps.Map{ "cluster": maps.Map{
"id": node.NodeCluster.Id, "id": node.NodeCluster.Id,

View File

@@ -30,7 +30,7 @@ func (this *IndexAction) RunGet(params struct{}) {
helpers.NotifyIPItemsCountChanges() helpers.NotifyIPItemsCountChanges()
helpers.NotifyNodeLogsCountChange() helpers.NotifyNodeLogsCountChange()
if teaconst.IsPlus { if this.checkPlus() {
this.RedirectURL("/dashboard/boards") this.RedirectURL("/dashboard/boards")
return return
} }
@@ -276,5 +276,13 @@ func (this *IndexAction) RunPost(params struct{}) {
} }
} }
// 弱密码提示
countWeakAdminsResp, err := this.RPC().AdminRPC().CountAllEnabledAdmins(this.AdminContext(), &pb.CountAllEnabledAdminsRequest{HasWeakPassword: true})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["countWeakAdmins"] = countWeakAdminsResp.Count
this.Success() this.Success()
} }

View File

@@ -0,0 +1,8 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build !plus
package dashboard
func (this *IndexAction) checkPlus() bool {
return false
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/db/dbnodeutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/db/dbnodeutils"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
"strings"
) )
type NodeAction struct { type NodeAction struct {
@@ -33,7 +34,7 @@ func (this *NodeAction) RunGet(params struct {
"host": node.Host, "host": node.Host,
"port": node.Port, "port": node.Port,
"username": node.Username, "username": node.Username,
"password": node.Password, "password": strings.Repeat("*", len(node.Password)),
"description": node.Description, "description": node.Description,
} }

View File

@@ -25,7 +25,7 @@ func (this *ClusterAction) RunGet(params struct {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
cluster := clusterResp.NodeCluster var cluster = clusterResp.NodeCluster
if cluster == nil { if cluster == nil {
this.NotFound("nodeCluster", params.ClusterId) this.NotFound("nodeCluster", params.ClusterId)
return return
@@ -42,7 +42,7 @@ func (this *ClusterAction) RunGet(params struct {
return return
} }
var defaultRoute = dnsResp.DefaultRoute var defaultRoute = dnsResp.DefaultRoute
domainName := "" var domainName = ""
var dnsMap = maps.Map{ var dnsMap = maps.Map{
"dnsName": dnsResp.Name, "dnsName": dnsResp.Name,
"domainId": 0, "domainId": 0,
@@ -70,19 +70,42 @@ func (this *ClusterAction) RunGet(params struct {
this.Data["dnsInfo"] = dnsMap this.Data["dnsInfo"] = dnsMap
// 节点DNS解析记录 // 未安装的节点
nodesResp, err := this.RPC().NodeRPC().FindAllEnabledNodesDNSWithNodeClusterId(this.AdminContext(), &pb.FindAllEnabledNodesDNSWithNodeClusterIdRequest{NodeClusterId: params.ClusterId}) notInstalledNodesResp, err := this.RPC().NodeRPC().FindAllEnabledNodesDNSWithNodeClusterId(this.AdminContext(), &pb.FindAllEnabledNodesDNSWithNodeClusterIdRequest{
NodeClusterId: params.ClusterId,
IsInstalled: false,
})
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
var nodeMaps = []maps.Map{} var allNodes = notInstalledNodesResp.Nodes
// 节点DNS解析记录
nodesResp, err := this.RPC().NodeRPC().FindAllEnabledNodesDNSWithNodeClusterId(this.AdminContext(), &pb.FindAllEnabledNodesDNSWithNodeClusterIdRequest{
NodeClusterId: params.ClusterId,
IsInstalled: true,
})
if err != nil {
this.ErrorPage(err)
return
}
var installedNodeIdsMap = map[int64]bool{}
for _, node := range nodesResp.Nodes { for _, node := range nodesResp.Nodes {
installedNodeIdsMap[node.Id] = true
}
allNodes = append(allNodes, nodesResp.Nodes...)
var nodeMaps = []maps.Map{}
for _, node := range allNodes {
var isInstalled = installedNodeIdsMap[node.Id]
if len(node.Routes) > 0 { if len(node.Routes) > 0 {
for _, route := range node.Routes { for _, route := range node.Routes {
// 检查是否已解析 // 检查是否已解析
var isResolved = false var isResolved = false
if cluster.DnsDomainId > 0 && len(cluster.DnsName) > 0 && len(node.IpAddr) > 0 { if isInstalled && cluster.DnsDomainId > 0 && len(cluster.DnsName) > 0 && len(node.IpAddr) > 0 {
var recordType = "A" var recordType = "A"
if utils.IsIPv6(node.IpAddr) { if utils.IsIPv6(node.IpAddr) {
recordType = "AAAA" recordType = "AAAA"
@@ -110,14 +133,15 @@ func (this *ClusterAction) RunGet(params struct {
"name": route.Name, "name": route.Name,
"code": route.Code, "code": route.Code,
}, },
"clusterId": node.NodeClusterId, "clusterId": node.NodeClusterId,
"isResolved": isResolved, "isResolved": isResolved,
"isInstalled": isInstalled,
}) })
} }
} else { } else {
// 默认线路 // 默认线路
var isResolved = false var isResolved = false
if len(defaultRoute) > 0 { if isInstalled && len(defaultRoute) > 0 {
var recordType = "A" var recordType = "A"
if utils.IsIPv6(node.IpAddr) { if utils.IsIPv6(node.IpAddr) {
recordType = "AAAA" recordType = "AAAA"
@@ -144,8 +168,9 @@ func (this *ClusterAction) RunGet(params struct {
"name": "", "name": "",
"code": "", "code": "",
}, },
"clusterId": node.NodeClusterId, "clusterId": node.NodeClusterId,
"isResolved": isResolved, "isResolved": isResolved,
"isInstalled": isInstalled,
}) })
} }
} }

View File

@@ -18,6 +18,8 @@ func (this *NodesPopupAction) Init() {
func (this *NodesPopupAction) RunGet(params struct { func (this *NodesPopupAction) RunGet(params struct {
DomainId int64 DomainId int64
}) { }) {
this.Data["domainId"] = params.DomainId
// 域名信息 // 域名信息
domainResp, err := this.RPC().DNSDomainRPC().FindBasicDNSDomain(this.AdminContext(), &pb.FindBasicDNSDomainRequest{ domainResp, err := this.RPC().DNSDomainRPC().FindBasicDNSDomain(this.AdminContext(), &pb.FindBasicDNSDomainRequest{
DnsDomainId: params.DomainId, DnsDomainId: params.DomainId,
@@ -26,7 +28,7 @@ func (this *NodesPopupAction) RunGet(params struct {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
domain := domainResp.DnsDomain var domain = domainResp.DnsDomain
if domain == nil { if domain == nil {
this.NotFound("dnsDomain", params.DomainId) this.NotFound("dnsDomain", params.DomainId)
return return
@@ -35,7 +37,7 @@ func (this *NodesPopupAction) RunGet(params struct {
this.Data["domain"] = domain.Name this.Data["domain"] = domain.Name
// 集群 // 集群
clusterMaps := []maps.Map{} var clusterMaps = []maps.Map{}
clustersResp, err := this.RPC().NodeClusterRPC().FindAllEnabledNodeClustersWithDNSDomainId(this.AdminContext(), &pb.FindAllEnabledNodeClustersWithDNSDomainIdRequest{DnsDomainId: params.DomainId}) clustersResp, err := this.RPC().NodeClusterRPC().FindAllEnabledNodeClustersWithDNSDomainId(this.AdminContext(), &pb.FindAllEnabledNodeClustersWithDNSDomainIdRequest{DnsDomainId: params.DomainId})
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
@@ -43,18 +45,24 @@ func (this *NodesPopupAction) RunGet(params struct {
} }
for _, cluster := range clustersResp.NodeClusters { for _, cluster := range clustersResp.NodeClusters {
// 默认值
var defaultRoute = cluster.DnsDefaultRoute
// 节点DNS解析记录 // 节点DNS解析记录
nodesResp, err := this.RPC().NodeRPC().FindAllEnabledNodesDNSWithNodeClusterId(this.AdminContext(), &pb.FindAllEnabledNodesDNSWithNodeClusterIdRequest{NodeClusterId: cluster.Id}) nodesResp, err := this.RPC().NodeRPC().FindAllEnabledNodesDNSWithNodeClusterId(this.AdminContext(), &pb.FindAllEnabledNodesDNSWithNodeClusterIdRequest{
NodeClusterId: cluster.Id,
IsInstalled: true,
})
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
nodeMaps := []maps.Map{} var nodeMaps = []maps.Map{}
for _, node := range nodesResp.Nodes { for _, node := range nodesResp.Nodes {
if len(node.Routes) > 0 { if len(node.Routes) > 0 {
for _, route := range node.Routes { for _, route := range node.Routes {
// 检查是否有域名解析记录 // 检查是否有域名解析记录
isOk := false var isResolved = false
if len(route.Name) > 0 && len(node.IpAddr) > 0 && len(cluster.DnsName) > 0 { if len(route.Name) > 0 && len(node.IpAddr) > 0 && len(cluster.DnsName) > 0 {
var recordType = "A" var recordType = "A"
if utils.IsIPv6(node.IpAddr) { if utils.IsIPv6(node.IpAddr) {
@@ -71,7 +79,7 @@ func (this *NodesPopupAction) RunGet(params struct {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
isOk = checkResp.IsOk isResolved = checkResp.IsOk
} }
nodeMaps = append(nodeMaps, maps.Map{ nodeMaps = append(nodeMaps, maps.Map{
@@ -83,10 +91,30 @@ func (this *NodesPopupAction) RunGet(params struct {
"code": route.Code, "code": route.Code,
}, },
"clusterId": node.NodeClusterId, "clusterId": node.NodeClusterId,
"isOk": isOk, "isOk": isResolved,
}) })
} }
} else { } else {
// 默认线路
var isResolved = false
if len(defaultRoute) > 0 {
var recordType = "A"
if utils.IsIPv6(node.IpAddr) {
recordType = "AAAA"
}
checkResp, err := this.RPC().DNSDomainRPC().ExistDNSDomainRecord(this.AdminContext(), &pb.ExistDNSDomainRecordRequest{
DnsDomainId: cluster.DnsDomainId,
Name: cluster.DnsName,
Type: recordType,
Route: defaultRoute,
Value: node.IpAddr,
})
if err != nil {
this.ErrorPage(err)
return
}
isResolved = checkResp.IsOk
}
nodeMaps = append(nodeMaps, maps.Map{ nodeMaps = append(nodeMaps, maps.Map{
"id": node.Id, "id": node.Id,
"name": node.Name, "name": node.Name,
@@ -96,7 +124,7 @@ func (this *NodesPopupAction) RunGet(params struct {
"code": "", "code": "",
}, },
"clusterId": node.NodeClusterId, "clusterId": node.NodeClusterId,
"isOk": false, "isOk": isResolved,
}) })
} }
} }

View File

@@ -17,6 +17,8 @@ func (this *ServersPopupAction) Init() {
func (this *ServersPopupAction) RunGet(params struct { func (this *ServersPopupAction) RunGet(params struct {
DomainId int64 DomainId int64
}) { }) {
this.Data["domainId"] = params.DomainId
// 域名信息 // 域名信息
domainResp, err := this.RPC().DNSDomainRPC().FindBasicDNSDomain(this.AdminContext(), &pb.FindBasicDNSDomainRequest{ domainResp, err := this.RPC().DNSDomainRPC().FindBasicDNSDomain(this.AdminContext(), &pb.FindBasicDNSDomainRequest{
DnsDomainId: params.DomainId, DnsDomainId: params.DomainId,
@@ -25,7 +27,7 @@ func (this *ServersPopupAction) RunGet(params struct {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
domain := domainResp.DnsDomain var domain = domainResp.DnsDomain
if domain == nil { if domain == nil {
this.NotFound("dnsDomain", params.DomainId) this.NotFound("dnsDomain", params.DomainId)
return return
@@ -34,7 +36,7 @@ func (this *ServersPopupAction) RunGet(params struct {
this.Data["domain"] = domain.Name this.Data["domain"] = domain.Name
// 服务信息 // 服务信息
clusterMaps := []maps.Map{} var clusterMaps = []maps.Map{}
clustersResp, err := this.RPC().NodeClusterRPC().FindAllEnabledNodeClustersWithDNSDomainId(this.AdminContext(), &pb.FindAllEnabledNodeClustersWithDNSDomainIdRequest{DnsDomainId: params.DomainId}) clustersResp, err := this.RPC().NodeClusterRPC().FindAllEnabledNodeClustersWithDNSDomainId(this.AdminContext(), &pb.FindAllEnabledNodeClustersWithDNSDomainIdRequest{DnsDomainId: params.DomainId})
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
@@ -46,9 +48,9 @@ func (this *ServersPopupAction) RunGet(params struct {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
serverMaps := []maps.Map{} var serverMaps = []maps.Map{}
for _, server := range serversResp.Servers { for _, server := range serversResp.Servers {
isOk := false var isOk = false
if len(cluster.DnsName) > 0 && len(server.DnsName) > 0 { if len(cluster.DnsName) > 0 && len(server.DnsName) > 0 {
checkResp, err := this.RPC().DNSDomainRPC().ExistDNSDomainRecord(this.AdminContext(), &pb.ExistDNSDomainRecordRequest{ checkResp, err := this.RPC().DNSDomainRPC().ExistDNSDomainRecord(this.AdminContext(), &pb.ExistDNSDomainRecordRequest{
DnsDomainId: params.DomainId, DnsDomainId: params.DomainId,

View File

@@ -1,30 +0,0 @@
package index
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
)
// 检查是否需要OTP
type CheckOTPAction struct {
actionutils.ParentAction
}
func (this *CheckOTPAction) Init() {
this.Nav("", "", "")
}
func (this *CheckOTPAction) RunPost(params struct {
Username string
Must *actions.Must
}) {
checkResp, err := this.RPC().AdminRPC().CheckAdminOTPWithUsername(this.AdminContext(), &pb.CheckAdminOTPWithUsernameRequest{Username: params.Username})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["requireOTP"] = checkResp.RequireOTP
this.Success()
}

View File

@@ -1,7 +1,6 @@
package index package index
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders" "github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
@@ -10,14 +9,16 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/setup" "github.com/TeaOSLab/EdgeAdmin/internal/setup"
"github.com/TeaOSLab/EdgeAdmin/internal/utils" "github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/index/loginutils"
adminserverutils "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/server/admin-server-utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types" "github.com/iwind/TeaGo/types"
stringutil "github.com/iwind/TeaGo/utils/string" stringutil "github.com/iwind/TeaGo/utils/string"
"github.com/xlzd/gotp" "net"
"time" "time"
) )
@@ -27,13 +28,33 @@ type IndexAction struct {
// 首页(登录页) // 首页(登录页)
var TokenSalt = stringutil.Rand(32) // TokenKey 加密用的密钥
var TokenKey = stringutil.Rand(32)
func (this *IndexAction) RunGet(params struct { func (this *IndexAction) RunGet(params struct {
From string From string
Auth *helpers.UserShouldAuth Auth *helpers.UserShouldAuth
}) { }) {
// 是否自动从HTTP跳转到HTTPS
if this.Request.TLS == nil {
httpsPort, _ := adminserverutils.ReadServerHTTPS()
if httpsPort > 0 {
currentHost, _, err := net.SplitHostPort(this.Request.Host)
if err != nil {
currentHost = this.Request.Host
}
var newHost = configutils.QuoteIP(currentHost)
if httpsPort != 443 /** default https port **/ {
newHost += ":" + types.String(httpsPort)
}
this.RedirectURL("https://" + newHost + this.Request.RequestURI)
return
}
}
// DEMO模式 // DEMO模式
this.Data["isDemo"] = teaconst.IsDemoMode this.Data["isDemo"] = teaconst.IsDemoMode
@@ -59,7 +80,7 @@ func (this *IndexAction) RunGet(params struct {
this.Data["menu"] = "signIn" this.Data["menu"] = "signIn"
var timestamp = fmt.Sprintf("%d", time.Now().Unix()) var timestamp = fmt.Sprintf("%d", time.Now().Unix())
this.Data["token"] = stringutil.Md5(TokenSalt+timestamp) + timestamp this.Data["token"] = stringutil.Md5(TokenKey+timestamp) + timestamp
this.Data["from"] = params.From this.Data["from"] = params.From
uiConfig, err := configloaders.LoadAdminUIConfig() uiConfig, err := configloaders.LoadAdminUIConfig()
@@ -83,6 +104,9 @@ func (this *IndexAction) RunGet(params struct {
this.Data["rememberLogin"] = securityConfig.AllowRememberLogin this.Data["rememberLogin"] = securityConfig.AllowRememberLogin
} }
// 删除Cookie
loginutils.UnsetCookie(this.Object())
this.Show() this.Show()
} }
@@ -93,9 +117,10 @@ func (this *IndexAction) RunPost(params struct {
Password string Password string
OtpCode string OtpCode string
Remember bool Remember bool
Must *actions.Must
Auth *helpers.UserShouldAuth Must *actions.Must
CSRF *actionutils.CSRF Auth *helpers.UserShouldAuth
CSRF *actionutils.CSRF
}) { }) {
params.Must. params.Must.
Field("username", params.Username). Field("username", params.Username).
@@ -112,7 +137,7 @@ func (this *IndexAction) RunPost(params struct {
this.Fail("请通过登录页面登录") this.Fail("请通过登录页面登录")
} }
var timestampString = params.Token[32:] var timestampString = params.Token[32:]
if stringutil.Md5(TokenSalt+timestampString) != params.Token[:32] { if stringutil.Md5(TokenKey+timestampString) != params.Token[:32] {
this.FailField("refresh", "登录页面已过期,请刷新后重试") this.FailField("refresh", "登录页面已过期,请刷新后重试")
} }
var timestamp = types.Int64(timestampString) var timestamp = types.Int64(timestampString)
@@ -123,6 +148,7 @@ func (this *IndexAction) RunPost(params struct {
rpcClient, err := rpc.SharedRPC() rpcClient, err := rpc.SharedRPC()
if err != nil { if err != nil {
this.Fail("服务器出了点小问题:" + err.Error()) this.Fail("服务器出了点小问题:" + err.Error())
return
} }
resp, err := rpcClient.AdminRPC().LoginAdmin(rpcClient.Context(0), &pb.LoginAdminRequest{ resp, err := rpcClient.AdminRPC().LoginAdmin(rpcClient.Context(0), &pb.LoginAdminRequest{
Username: params.Username, Username: params.Username,
@@ -136,6 +162,7 @@ func (this *IndexAction) RunPost(params struct {
} }
actionutils.Fail(this, err) actionutils.Fail(this, err)
return
} }
if !resp.IsOk { if !resp.IsOk {
@@ -145,31 +172,37 @@ func (this *IndexAction) RunPost(params struct {
} }
this.Fail("请输入正确的用户名密码") this.Fail("请输入正确的用户名密码")
return
} }
var adminId = resp.AdminId
// 检查OTP // 检查是否支持OTP
otpLoginResp, err := this.RPC().LoginRPC().FindEnabledLogin(this.AdminContext(), &pb.FindEnabledLoginRequest{ checkOTPResp, err := this.RPC().AdminRPC().CheckAdminOTPWithUsername(this.AdminContext(), &pb.CheckAdminOTPWithUsernameRequest{Username: params.Username})
AdminId: resp.AdminId,
Type: "otp",
})
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
if otpLoginResp.Login != nil && otpLoginResp.Login.IsOn { var requireOTP = checkOTPResp.RequireOTP
var loginParams = maps.Map{} this.Data["requireOTP"] = requireOTP
err = json.Unmarshal(otpLoginResp.Login.ParamsJSON, &loginParams) if requireOTP {
this.Data["remember"] = params.Remember
var sid = this.Session().Sid
this.Data["sid"] = sid
_, err = this.RPC().LoginSessionRPC().WriteLoginSessionValue(this.AdminContext(), &pb.WriteLoginSessionValueRequest{
Sid: sid + "_otp",
Key: "adminId",
Value: types.String(adminId),
})
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
secret := loginParams.GetString("secret") this.Success()
if gotp.NewDefaultTOTP(secret).Now() != params.OtpCode { return
this.Fail("请输入正确的OTP动态密码")
}
} }
var adminId = resp.AdminId // 写入SESSION
params.Auth.StoreAdmin(adminId, params.Remember) params.Auth.StoreAdmin(adminId, params.Remember)
// 记录日志 // 记录日志

View File

@@ -7,9 +7,9 @@ import (
func init() { func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) { TeaGo.BeforeStart(func(server *TeaGo.Server) {
server. server.
Post("/checkOTP", new(CheckOTPAction)). Prefix("").
Prefix("/"). GetPost("/", new(IndexAction)).
GetPost("", new(IndexAction)). GetPost("/index/otp", new(OtpAction)).
EndAll() EndAll()
}) })
} }

View File

@@ -0,0 +1,60 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package loginutils
import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/iwind/TeaGo/actions"
stringutil "github.com/iwind/TeaGo/utils/string"
"net/http"
)
// CalculateClientFingerprint 计算客户端指纹
func CalculateClientFingerprint(action *actions.ActionObject) string {
return stringutil.Md5(action.RequestRemoteIP() + "@" + action.Request.UserAgent())
}
func SetCookie(action *actions.ActionObject, remember bool) {
if remember {
var cookie = &http.Cookie{
Name: teaconst.CookieSID,
Value: action.Session().Sid,
Path: "/",
MaxAge: 14 * 86400,
HttpOnly: true,
}
if action.Request.TLS != nil {
cookie.SameSite = http.SameSiteStrictMode
cookie.Secure = true
}
action.AddCookie(cookie)
} else {
var cookie = &http.Cookie{
Name: teaconst.CookieSID,
Value: action.Session().Sid,
Path: "/",
MaxAge: 0,
HttpOnly: true,
}
if action.Request.TLS != nil {
cookie.SameSite = http.SameSiteStrictMode
cookie.Secure = true
}
action.AddCookie(cookie)
}
}
func UnsetCookie(action *actions.ActionObject) {
cookie := &http.Cookie{
Name: teaconst.CookieSID,
Value: action.Session().Sid,
Path: "/",
MaxAge: -1,
HttpOnly: true,
}
if action.Request.TLS != nil {
cookie.SameSite = http.SameSiteStrictMode
cookie.Secure = true
}
action.AddCookie(cookie)
}

View File

@@ -0,0 +1,154 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package index
import (
"encoding/json"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/setup"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
stringutil "github.com/iwind/TeaGo/utils/string"
"github.com/xlzd/gotp"
"time"
)
type OtpAction struct {
actionutils.ParentAction
}
func (this *OtpAction) Init() {
this.Nav("", "", "")
}
func (this *OtpAction) RunGet(params struct {
From string
Sid string
Remember bool
}) {
// 检查系统是否已经配置过
if !setup.IsConfigured() {
this.RedirectURL("/setup")
return
}
//// 是否新安装
if setup.IsNewInstalled() {
this.RedirectURL("/setup/confirm")
return
}
this.Data["isUser"] = false
this.Data["menu"] = "signIn"
var timestamp = fmt.Sprintf("%d", time.Now().Unix())
this.Data["token"] = stringutil.Md5(TokenKey+timestamp) + timestamp
this.Data["from"] = params.From
this.Data["sid"] = params.Sid
uiConfig, err := configloaders.LoadAdminUIConfig()
if err != nil {
this.ErrorPage(err)
return
}
this.Data["systemName"] = uiConfig.AdminSystemName
this.Data["showVersion"] = uiConfig.ShowVersion
if len(uiConfig.Version) > 0 {
this.Data["version"] = uiConfig.Version
} else {
this.Data["version"] = teaconst.Version
}
this.Data["faviconFileId"] = uiConfig.FaviconFileId
this.Data["remember"] = params.Remember
this.Show()
}
func (this *OtpAction) RunPost(params struct {
Sid string
OtpCode string
Remember bool
Must *actions.Must
Auth *helpers.UserShouldAuth
}) {
if len(params.OtpCode) == 0 {
this.FailField("otpCode", "请输入正确的OTP动态密码")
return
}
var sid = params.Sid
if len(sid) == 0 || len(sid) > 64 {
this.Fail("参数错误请重新登录001")
return
}
sid += "_otp"
// 获取SESSION
sessionResp, err := this.RPC().LoginSessionRPC().FindLoginSession(this.AdminContext(), &pb.FindLoginSessionRequest{Sid: sid})
if err != nil {
this.ErrorPage(err)
return
}
var session = sessionResp.LoginSession
if session == nil || session.AdminId <= 0 {
this.Fail("参数错误请重新登录002")
return
}
var adminId = session.AdminId
// 检查OTP
otpLoginResp, err := this.RPC().LoginRPC().FindEnabledLogin(this.AdminContext(), &pb.FindEnabledLoginRequest{
AdminId: adminId,
Type: "otp",
})
if err != nil {
this.ErrorPage(err)
return
}
if otpLoginResp.Login != nil && otpLoginResp.Login.IsOn {
var loginParams = maps.Map{}
err = json.Unmarshal(otpLoginResp.Login.ParamsJSON, &loginParams)
if err != nil {
this.ErrorPage(err)
return
}
var secret = loginParams.GetString("secret")
if gotp.NewDefaultTOTP(secret).Now() != params.OtpCode {
this.FailField("otpCode", "请输入正确的OTP动态密码")
return
}
}
// 写入SESSION
params.Auth.StoreAdmin(adminId, params.Remember)
// 删除OTP SESSION
_, err = this.RPC().LoginSessionRPC().DeleteLoginSession(this.AdminContext(), &pb.DeleteLoginSessionRequest{Sid: sid})
if err != nil {
this.ErrorPage(err)
return
}
// 记录日志
rpcClient, err := rpc.SharedRPC()
if err != nil {
this.ErrorPage(err)
return
}
err = dao.SharedLogDAO.CreateAdminLog(rpcClient.Context(adminId), oplogs.LevelInfo, this.Request.URL.Path, "成功通过OTP验证登录系统", this.RequestRemoteIP())
if err != nil {
utils.PrintError(err)
}
this.Success()
}

View File

@@ -23,6 +23,7 @@ func (this *ExportExcelAction) RunGet(params struct {
DayTo string DayTo string
Keyword string Keyword string
UserType string UserType string
Level string
}) { }) {
logsResp, err := this.RPC().LogRPC().ListLogs(this.AdminContext(), &pb.ListLogsRequest{ logsResp, err := this.RPC().LogRPC().ListLogs(this.AdminContext(), &pb.ListLogsRequest{
Offset: 0, Offset: 0,
@@ -31,6 +32,7 @@ func (this *ExportExcelAction) RunGet(params struct {
DayTo: params.DayTo, DayTo: params.DayTo,
Keyword: params.Keyword, Keyword: params.Keyword,
UserType: params.UserType, UserType: params.UserType,
Level: params.Level,
}) })
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
@@ -56,6 +58,7 @@ func (this *ExportExcelAction) RunGet(params struct {
row.AddCell().SetString("区域") row.AddCell().SetString("区域")
row.AddCell().SetString("运营商") row.AddCell().SetString("运营商")
row.AddCell().SetString("页面地址") row.AddCell().SetString("页面地址")
row.AddCell().SetString("级别")
} }
// 数据 // 数据
@@ -95,6 +98,17 @@ func (this *ExportExcelAction) RunGet(params struct {
row.AddCell().SetString(regionName) row.AddCell().SetString(regionName)
row.AddCell().SetString(ispName) row.AddCell().SetString(ispName)
row.AddCell().SetString(log.Action) row.AddCell().SetString(log.Action)
var levelName = ""
switch log.Level {
case "info":
levelName = "信息"
case "warn", "warning":
levelName = "警告"
case "error":
levelName = "错误"
}
row.AddCell().SetString(levelName)
} }
this.AddHeader("Content-Type", "application/vnd.ms-excel") this.AddHeader("Content-Type", "application/vnd.ms-excel")

View File

@@ -21,6 +21,7 @@ func (this *IndexAction) RunGet(params struct {
DayTo string DayTo string
Keyword string Keyword string
UserType string UserType string
Level string
}) { }) {
// 读取配置 // 读取配置
config, err := configloaders.LoadLogConfig() config, err := configloaders.LoadLogConfig()
@@ -35,18 +36,36 @@ func (this *IndexAction) RunGet(params struct {
this.Data["keyword"] = params.Keyword this.Data["keyword"] = params.Keyword
this.Data["userType"] = params.UserType this.Data["userType"] = params.UserType
// 级别
this.Data["level"] = params.Level
this.Data["levelOptions"] = []maps.Map{
{
"code": "info",
"name": "信息",
},
{
"code": "warn",
"name": "警告",
},
{
"code": "error",
"name": "错误",
},
}
countResp, err := this.RPC().LogRPC().CountLogs(this.AdminContext(), &pb.CountLogRequest{ countResp, err := this.RPC().LogRPC().CountLogs(this.AdminContext(), &pb.CountLogRequest{
DayFrom: params.DayFrom, DayFrom: params.DayFrom,
DayTo: params.DayTo, DayTo: params.DayTo,
Keyword: params.Keyword, Keyword: params.Keyword,
UserType: params.UserType, UserType: params.UserType,
Level: params.Level,
}) })
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
count := countResp.Count var count = countResp.Count
page := this.NewPage(count) var page = this.NewPage(count)
this.Data["page"] = page.AsHTML() this.Data["page"] = page.AsHTML()
logsResp, err := this.RPC().LogRPC().ListLogs(this.AdminContext(), &pb.ListLogsRequest{ logsResp, err := this.RPC().LogRPC().ListLogs(this.AdminContext(), &pb.ListLogsRequest{
@@ -56,12 +75,13 @@ func (this *IndexAction) RunGet(params struct {
DayTo: params.DayTo, DayTo: params.DayTo,
Keyword: params.Keyword, Keyword: params.Keyword,
UserType: params.UserType, UserType: params.UserType,
Level: params.Level,
}) })
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
logMaps := []maps.Map{} var logMaps = []maps.Map{}
for _, log := range logsResp.Logs { for _, log := range logsResp.Logs {
regionName := "" regionName := ""
regionResp, err := this.RPC().IPLibraryRPC().LookupIPRegion(this.AdminContext(), &pb.LookupIPRegionRequest{Ip: log.Ip}) regionResp, err := this.RPC().IPLibraryRPC().LookupIPRegion(this.AdminContext(), &pb.LookupIPRegionRequest{Ip: log.Ip})

View File

@@ -5,6 +5,7 @@ import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/utils" "github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/nodes/ipAddresses/ipaddressutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
@@ -20,8 +21,18 @@ func (this *CreatePopupAction) Init() {
} }
func (this *CreatePopupAction) RunGet(params struct { func (this *CreatePopupAction) RunGet(params struct {
NodeId int64
SupportThresholds bool SupportThresholds bool
}) { }) {
// 专属集群
clusterMaps, err := ipaddressutils.FindNodeClusterMapsWithNodeId(this.Parent(), params.NodeId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["clusters"] = clusterMaps
// 阈值
this.Data["supportThresholds"] = params.SupportThresholds this.Data["supportThresholds"] = params.SupportThresholds
this.Show() this.Show()
@@ -33,6 +44,7 @@ func (this *CreatePopupAction) RunPost(params struct {
Name string Name string
IsUp bool IsUp bool
ThresholdsJSON []byte ThresholdsJSON []byte
ClusterIds []int64
Must *actions.Must Must *actions.Must
}) { }) {
@@ -57,6 +69,14 @@ func (this *CreatePopupAction) RunPost(params struct {
_ = json.Unmarshal(params.ThresholdsJSON, &thresholds) _ = json.Unmarshal(params.ThresholdsJSON, &thresholds)
} }
// 专属集群
// 目前只考虑CDN边缘集群
clusterMaps, err := ipaddressutils.FindNodeClusterMaps(this.Parent(), params.ClusterIds)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["ipAddress"] = maps.Map{ this.Data["ipAddress"] = maps.Map{
"name": params.Name, "name": params.Name,
"canAccess": params.CanAccess, "canAccess": params.CanAccess,
@@ -65,6 +85,7 @@ func (this *CreatePopupAction) RunPost(params struct {
"isOn": true, "isOn": true,
"isUp": params.IsUp, "isUp": params.IsUp,
"thresholds": thresholds, "thresholds": thresholds,
"clusters": clusterMaps,
} }
this.Success() this.Success()
} }

View File

@@ -11,14 +11,28 @@ import (
// UpdateNodeIPAddresses 保存一组IP地址 // UpdateNodeIPAddresses 保存一组IP地址
func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64, role nodeconfigs.NodeRole, ipAddressesJSON []byte) error { func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64, role nodeconfigs.NodeRole, ipAddressesJSON []byte) error {
addresses := []maps.Map{} var addresses = []maps.Map{}
err := json.Unmarshal(ipAddressesJSON, &addresses) err := json.Unmarshal(ipAddressesJSON, &addresses)
if err != nil { if err != nil {
return err return err
} }
for _, addr := range addresses { for _, addr := range addresses {
var resultAddrIds = []int64{} var resultAddrIds = []int64{}
addrId := addr.GetInt64("id") var addrId = addr.GetInt64("id")
// 专属集群
var addrClusterIds = []int64{}
var addrClusters = addr.GetSlice("clusters")
if len(addrClusters) > 0 {
for _, addrCluster := range addrClusters {
var m = maps.NewMap(addrCluster)
var clusterId = m.GetInt64("id")
if clusterId > 0 {
addrClusterIds = append(addrClusterIds, clusterId)
}
}
}
if addrId > 0 { if addrId > 0 {
resultAddrIds = append(resultAddrIds, addrId) resultAddrIds = append(resultAddrIds, addrId)
@@ -36,6 +50,7 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
CanAccess: addr.GetBool("canAccess"), CanAccess: addr.GetBool("canAccess"),
IsOn: isOn, IsOn: isOn,
IsUp: addr.GetBool("isUp"), IsUp: addr.GetBool("isUp"),
ClusterIds: addrClusterIds,
}) })
if err != nil { if err != nil {
return err return err
@@ -47,12 +62,13 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
if len(result) == 1 { if len(result) == 1 {
// 单个创建 // 单个创建
createResp, err := parentAction.RPC().NodeIPAddressRPC().CreateNodeIPAddress(parentAction.AdminContext(), &pb.CreateNodeIPAddressRequest{ createResp, err := parentAction.RPC().NodeIPAddressRPC().CreateNodeIPAddress(parentAction.AdminContext(), &pb.CreateNodeIPAddressRequest{
NodeId: nodeId, NodeId: nodeId,
Role: role, Role: role,
Name: addr.GetString("name"), Name: addr.GetString("name"),
Ip: result[0], Ip: result[0],
CanAccess: addr.GetBool("canAccess"), CanAccess: addr.GetBool("canAccess"),
IsUp: addr.GetBool("isUp"), IsUp: addr.GetBool("isUp"),
NodeClusterIds: addrClusterIds,
}) })
if err != nil { if err != nil {
return err return err
@@ -62,13 +78,14 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
} else if len(result) > 1 { } else if len(result) > 1 {
// 批量创建 // 批量创建
createResp, err := parentAction.RPC().NodeIPAddressRPC().CreateNodeIPAddresses(parentAction.AdminContext(), &pb.CreateNodeIPAddressesRequest{ createResp, err := parentAction.RPC().NodeIPAddressRPC().CreateNodeIPAddresses(parentAction.AdminContext(), &pb.CreateNodeIPAddressesRequest{
NodeId: nodeId, NodeId: nodeId,
Role: role, Role: role,
Name: addr.GetString("name"), Name: addr.GetString("name"),
IpList: result, IpList: result,
CanAccess: addr.GetBool("canAccess"), CanAccess: addr.GetBool("canAccess"),
IsUp: addr.GetBool("isUp"), IsUp: addr.GetBool("isUp"),
GroupValue: ipStrings, GroupValue: ipStrings,
NodeClusterIds: addrClusterIds,
}) })
if err != nil { if err != nil {
return err return err
@@ -140,3 +157,53 @@ func InitNodeIPAddressThresholds(parentAction *actionutils.ParentAction, addrId
} }
return thresholds, nil return thresholds, nil
} }
// FindNodeClusterMapsWithNodeId 根据节点读取集群信息
func FindNodeClusterMapsWithNodeId(parentAction *actionutils.ParentAction, nodeId int64) ([]maps.Map, error) {
var clusterMaps = []maps.Map{}
if nodeId > 0 { // CDN边缘节点
nodeResp, err := parentAction.RPC().NodeRPC().FindEnabledNode(parentAction.AdminContext(), &pb.FindEnabledNodeRequest{NodeId: nodeId})
if err != nil {
return nil, err
}
var node = nodeResp.Node
if node != nil {
var clusters = []*pb.NodeCluster{}
if node.NodeCluster != nil {
clusters = append(clusters, nodeResp.Node.NodeCluster)
}
if len(node.SecondaryNodeClusters) > 0 {
clusters = append(clusters, node.SecondaryNodeClusters...)
}
for _, cluster := range clusters {
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
"isChecked": false,
})
}
}
}
return clusterMaps, nil
}
// FindNodeClusterMaps 根据一组集群ID读取集群信息
func FindNodeClusterMaps(parentAction *actionutils.ParentAction, clusterIds []int64) ([]maps.Map, error) {
var clusterMaps = []maps.Map{}
if len(clusterIds) > 0 {
for _, clusterId := range clusterIds {
clusterResp, err := parentAction.RPC().NodeClusterRPC().FindEnabledNodeCluster(parentAction.AdminContext(), &pb.FindEnabledNodeClusterRequest{NodeClusterId: clusterId})
if err != nil {
return nil, err
}
var cluster = clusterResp.NodeCluster
if cluster != nil {
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
})
}
}
}
return clusterMaps, nil
}

View File

@@ -5,6 +5,7 @@ import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/utils" "github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/nodes/ipAddresses/ipaddressutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
@@ -21,9 +22,18 @@ func (this *UpdatePopupAction) Init() {
} }
func (this *UpdatePopupAction) RunGet(params struct { func (this *UpdatePopupAction) RunGet(params struct {
NodeId int64
AddressId int64 AddressId int64
SupportThresholds bool SupportThresholds bool
}) { }) {
// 专属集群
clusterMaps, err := ipaddressutils.FindNodeClusterMapsWithNodeId(this.Parent(), params.NodeId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["clusters"] = clusterMaps
this.Data["supportThresholds"] = params.SupportThresholds this.Data["supportThresholds"] = params.SupportThresholds
this.Show() this.Show()
@@ -37,6 +47,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
IsOn bool IsOn bool
IsUp bool IsUp bool
ThresholdsJSON []byte ThresholdsJSON []byte
ClusterIds []int64
Must *actions.Must Must *actions.Must
}) { }) {
@@ -81,6 +92,14 @@ func (this *UpdatePopupAction) RunPost(params struct {
_ = json.Unmarshal(params.ThresholdsJSON, &thresholds) _ = json.Unmarshal(params.ThresholdsJSON, &thresholds)
} }
// 专属集群
// 目前只考虑CDN边缘集群
clusterMaps, err := ipaddressutils.FindNodeClusterMaps(this.Parent(), params.ClusterIds)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["ipAddress"] = maps.Map{ this.Data["ipAddress"] = maps.Map{
"name": params.Name, "name": params.Name,
"ip": params.IP, "ip": params.IP,
@@ -89,6 +108,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
"isOn": params.IsOn, "isOn": params.IsOn,
"isUp": isUp, "isUp": isUp,
"thresholds": thresholds, "thresholds": thresholds,
"clusters": clusterMaps,
} }
this.Success() this.Success()

View File

@@ -21,6 +21,7 @@ func init() {
Data("leftMenuItem", "cert"). Data("leftMenuItem", "cert").
Get("", new(IndexAction)). Get("", new(IndexAction)).
GetPost("/uploadPopup", new(UploadPopupAction)). GetPost("/uploadPopup", new(UploadPopupAction)).
GetPost("/uploadBatchPopup", new(UploadBatchPopupAction)).
Post("/delete", new(DeleteAction)). Post("/delete", new(DeleteAction)).
GetPost("/updatePopup", new(UpdatePopupAction)). GetPost("/updatePopup", new(UpdatePopupAction)).
Get("/certPopup", new(CertPopupAction)). Get("/certPopup", new(CertPopupAction)).

View File

@@ -2,9 +2,11 @@ package certs
import ( import (
"encoding/json" "encoding/json"
"errors"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils" "github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/lists" "github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
@@ -23,15 +25,82 @@ func (this *SelectPopupAction) Init() {
} }
func (this *SelectPopupAction) RunGet(params struct { func (this *SelectPopupAction) RunGet(params struct {
ServerId int64 // 搜索的服务
UserId int64 // 搜索的用户名
SearchingDomains string // 搜索的域名
SearchingType string // 搜索类型match|all
ViewSize string ViewSize string
SelectedCertIds string SelectedCertIds string
Keyword string Keyword string
}) { }) {
// TODO 列出常用和最新的证书供用户选择 // 服务相关
if params.ServerId > 0 {
serverResp, err := this.RPC().ServerRPC().FindEnabledUserServerBasic(this.AdminContext(), &pb.FindEnabledUserServerBasicRequest{ServerId: params.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
var server = serverResp.Server
if server != nil {
if server.UserId > 0 {
params.UserId = server.UserId
}
// 读取所有ServerNames
serverNamesResp, err := this.RPC().ServerRPC().FindServerNames(this.AdminContext(), &pb.FindServerNamesRequest{ServerId: params.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
if len(serverNamesResp.ServerNamesJSON) > 0 {
var serverNames = []*serverconfigs.ServerNameConfig{}
err = json.Unmarshal(serverNamesResp.ServerNamesJSON, &serverNames)
if err != nil {
this.ErrorPage(err)
return
}
params.SearchingDomains = strings.Join(serverconfigs.PlainServerNames(serverNames), ",")
}
}
}
// 用户相关
this.Data["userId"] = params.UserId
// 域名搜索相关
var url = this.Request.URL.Path
var query = this.Request.URL.Query()
query.Del("searchingType")
this.Data["baseURL"] = url + "?" + query.Encode()
var searchingDomains = []string{}
if len(params.SearchingDomains) > 0 {
searchingDomains = strings.Split(params.SearchingDomains, ",")
}
const maxDomains = 2_000 // 限制搜索的域名数量
if len(searchingDomains) > maxDomains {
searchingDomains = searchingDomains[:maxDomains]
}
this.Data["searchingDomains"] = searchingDomains
this.Data["keyword"] = params.Keyword this.Data["keyword"] = params.Keyword
this.Data["selectedCertIds"] = params.SelectedCertIds this.Data["selectedCertIds"] = params.SelectedCertIds
var searchingType = params.SearchingType
if len(searchingType) == 0 {
if len(params.SearchingDomains) == 0 {
searchingType = "all"
} else {
searchingType = "match"
}
}
if searchingType != "all" && searchingType != "match" {
this.ErrorPage(errors.New("invalid searching type '" + searchingType + "'"))
return
}
this.Data["searchingType"] = searchingType
// 已经选择的证书 // 已经选择的证书
var selectedCertIds = []string{} var selectedCertIds = []string{}
if len(params.SelectedCertIds) > 0 { if len(params.SelectedCertIds) > 0 {
@@ -43,24 +112,68 @@ func (this *SelectPopupAction) RunGet(params struct {
} }
this.Data["viewSize"] = params.ViewSize this.Data["viewSize"] = params.ViewSize
countResp, err := this.RPC().SSLCertRPC().CountSSLCerts(this.AdminContext(), &pb.CountSSLCertRequest{ // 全部证书数量
countAllResp, err := this.RPC().SSLCertRPC().CountSSLCerts(this.AdminContext(), &pb.CountSSLCertRequest{
UserId: params.UserId,
Keyword: params.Keyword, Keyword: params.Keyword,
}) })
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
var totalAll = countAllResp.Count
this.Data["totalAll"] = totalAll
page := this.NewPage(countResp.Count) // 已匹配证书数量
var totalMatch int64 = 0
if len(searchingDomains) > 0 {
countMatchResp, err := this.RPC().SSLCertRPC().CountSSLCerts(this.AdminContext(), &pb.CountSSLCertRequest{
UserId: params.UserId,
Keyword: params.Keyword,
Domains: searchingDomains,
})
if err != nil {
this.ErrorPage(err)
return
}
totalMatch = countMatchResp.Count
}
this.Data["totalMatch"] = totalMatch
var totalCerts int64
if searchingType == "all" {
totalCerts = totalAll
} else if searchingType == "match" {
totalCerts = totalMatch
}
var page = this.NewPage(totalCerts)
this.Data["page"] = page.AsHTML() this.Data["page"] = page.AsHTML()
listResp, err := this.RPC().SSLCertRPC().ListSSLCerts(this.AdminContext(), &pb.ListSSLCertsRequest{ var listResp *pb.ListSSLCertsResponse
Keyword: params.Keyword, if searchingType == "all" {
Offset: page.Offset, listResp, err = this.RPC().SSLCertRPC().ListSSLCerts(this.AdminContext(), &pb.ListSSLCertsRequest{
Size: page.Size, UserId: params.UserId,
}) Keyword: params.Keyword,
Offset: page.Offset,
Size: page.Size,
})
} else if searchingType == "match" {
listResp, err = this.RPC().SSLCertRPC().ListSSLCerts(this.AdminContext(), &pb.ListSSLCertsRequest{
UserId: params.UserId,
Keyword: params.Keyword,
Domains: searchingDomains,
Offset: page.Offset,
Size: page.Size,
})
}
certConfigs := []*sslconfigs.SSLCertConfig{} if listResp == nil {
this.ErrorPage(errors.New("'listResp' should not be nil"))
return
}
var certConfigs = []*sslconfigs.SSLCertConfig{}
err = json.Unmarshal(listResp.SslCertsJSON, &certConfigs) err = json.Unmarshal(listResp.SslCertsJSON, &certConfigs)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
@@ -68,8 +181,8 @@ func (this *SelectPopupAction) RunGet(params struct {
} }
this.Data["certs"] = certConfigs this.Data["certs"] = certConfigs
certMaps := []maps.Map{} var certMaps = []maps.Map{}
nowTime := time.Now().Unix() var nowTime = time.Now().Unix()
for _, certConfig := range certConfigs { for _, certConfig := range certConfigs {
countServersResp, err := this.RPC().ServerRPC().CountAllEnabledServersWithSSLCertId(this.AdminContext(), &pb.CountAllEnabledServersWithSSLCertIdRequest{SslCertId: certConfig.Id}) countServersResp, err := this.RPC().ServerRPC().CountAllEnabledServersWithSSLCertId(this.AdminContext(), &pb.CountAllEnabledServersWithSSLCertIdRequest{SslCertId: certConfig.Id})
if err != nil { if err != nil {

View File

@@ -26,18 +26,20 @@ func (this *UpdatePopupAction) RunGet(params struct {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
certConfigJSON := certConfigResp.SslCertJSON var certConfigJSON = certConfigResp.SslCertJSON
if len(certConfigJSON) == 0 { if len(certConfigJSON) == 0 {
this.NotFound("cert", params.CertId) this.NotFound("cert", params.CertId)
return return
} }
certConfig := &sslconfigs.SSLCertConfig{} var certConfig = &sslconfigs.SSLCertConfig{}
err = json.Unmarshal(certConfigJSON, certConfig) err = json.Unmarshal(certConfigJSON, certConfig)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
certConfig.CertData = nil // cert & key 不需要在界面上显示
certConfig.KeyData = nil
this.Data["certConfig"] = certConfig this.Data["certConfig"] = certConfig
this.Show() this.Show()
@@ -118,7 +120,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
// 校验 // 校验
certConfig.IsCA = params.IsCA certConfig.IsCA = params.IsCA
err = certConfig.Init() err = certConfig.Init(nil)
if err != nil { if err != nil {
if params.IsCA { if params.IsCA {
this.Fail("证书校验错误:" + err.Error()) this.Fail("证书校验错误:" + err.Error())

View File

@@ -0,0 +1,225 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package certs
import (
"bytes"
"crypto/tls"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/types"
"io"
"mime/multipart"
"strings"
)
// UploadBatchPopupAction 批量上传证书
type UploadBatchPopupAction struct {
actionutils.ParentAction
}
func (this *UploadBatchPopupAction) Init() {
this.Nav("", "", "")
}
func (this *UploadBatchPopupAction) RunGet(params struct {
ServerId int64
UserId int64
}) {
// 读取服务用户
if params.ServerId > 0 {
serverResp, err := this.RPC().ServerRPC().FindEnabledUserServerBasic(this.AdminContext(), &pb.FindEnabledUserServerBasicRequest{ServerId: params.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
var server = serverResp.Server
if server != nil {
params.UserId = server.UserId
}
}
this.Data["userId"] = params.UserId
this.Data["maxFiles"] = this.maxFiles()
this.Show()
}
func (this *UploadBatchPopupAction) RunPost(params struct {
UserId int64
Must *actions.Must
CSRF *actionutils.CSRF
}) {
defer this.CreateLogInfo("批量上传证书")
var files = this.Request.MultipartForm.File["certFiles"]
if len(files) == 0 {
this.Fail("请选择要上传的证书和私钥文件")
return
}
// 限制每次上传的文件数量
var maxFiles = this.maxFiles()
if len(files) > maxFiles {
this.Fail("每次上传最多不能超过" + types.String(maxFiles) + "个文件")
return
}
type certInfo struct {
filename string
data []byte
}
var certDataList = []*certInfo{}
var keyDataList = [][]byte{}
var failMessages = []string{}
for _, file := range files {
func(file *multipart.FileHeader) {
fp, err := file.Open()
if err != nil {
failMessages = append(failMessages, "文件"+file.Filename+"读取失败:"+err.Error())
return
}
defer func() {
_ = fp.Close()
}()
data, err := io.ReadAll(fp)
if err != nil {
failMessages = append(failMessages, "文件"+file.Filename+"读取失败:"+err.Error())
return
}
if bytes.Contains(data, []byte("CERTIFICATE-")) {
certDataList = append(certDataList, &certInfo{
filename: file.Filename,
data: data,
})
} else if bytes.Contains(data, []byte("PRIVATE KEY-")) {
keyDataList = append(keyDataList, data)
} else {
failMessages = append(failMessages, "文件"+file.Filename+"读取失败:文件格式错误,无法识别是证书还是私钥")
return
}
}(file)
}
if len(failMessages) > 0 {
this.Fail("发生了错误:" + strings.Join(failMessages, ""))
return
}
// 对比证书和私钥数量是否一致
if len(certDataList) != len(keyDataList) {
this.Fail("证书文件数量(" + types.String(len(certDataList)) + ")和私钥文件数量(" + types.String(len(keyDataList)) + ")不一致")
return
}
// 自动匹配
var pairs = [][2][]byte{} // [] { cert, key }
var keyIndexMap = map[int]bool{} // 方便下面跳过已匹配的Key
for _, cert := range certDataList {
var found = false
for keyIndex, keyData := range keyDataList {
if keyIndexMap[keyIndex] {
continue
}
_, err := tls.X509KeyPair(cert.data, keyData)
if err == nil {
found = true
pairs = append(pairs, [2][]byte{cert.data, keyData})
keyIndexMap[keyIndex] = true
break
}
}
if !found {
this.Fail("找不到" + cert.filename + "对应的私钥")
return
}
}
// 组织 CertConfig
var pbCerts = []*pb.CreateSSLCertsRequestCert{}
var certConfigs = []*sslconfigs.SSLCertConfig{}
for _, pair := range pairs {
certData, keyData := pair[0], pair[1]
var certConfig = &sslconfigs.SSLCertConfig{
IsCA: false,
CertData: certData,
KeyData: keyData,
}
err := certConfig.Init(nil)
if err != nil {
this.Fail("证书验证失败:" + err.Error())
return
}
certConfigs = append(certConfigs, certConfig)
var certName = ""
if len(certConfig.DNSNames) > 0 {
certName = certConfig.DNSNames[0]
if len(certConfig.DNSNames) > 1 {
certName += "等" + types.String(len(certConfig.DNSNames)) + "个域名"
}
}
certConfig.Name = certName
pbCerts = append(pbCerts, &pb.CreateSSLCertsRequestCert{
IsOn: true,
Name: certName,
Description: "",
ServerName: "",
IsCA: false,
CertData: certData,
KeyData: keyData,
TimeBeginAt: certConfig.TimeBeginAt,
TimeEndAt: certConfig.TimeEndAt,
DnsNames: certConfig.DNSNames,
CommonNames: certConfig.CommonNames,
})
}
createResp, err := this.RPC().SSLCertRPC().CreateSSLCerts(this.AdminContext(), &pb.CreateSSLCertsRequest{
UserId: params.UserId,
SSLCerts: pbCerts,
})
if err != nil {
this.ErrorPage(err)
return
}
var certIds = createResp.SslCertIds
if len(certIds) != len(certConfigs) {
this.Fail("上传成功但API返回的证书ID数量错误请反馈给开发者")
return
}
// 返回数据
this.Data["count"] = len(pbCerts)
var certRefs = []*sslconfigs.SSLCertRef{}
for index, cert := range certConfigs {
// ID
cert.Id = certIds[index]
// 减少不必要的数据
cert.CertData = nil
cert.KeyData = nil
certRefs = append(certRefs, &sslconfigs.SSLCertRef{
IsOn: true,
CertId: cert.Id,
})
}
this.Data["certs"] = certConfigs
this.Data["certRefs"] = certRefs
this.Success()
}

View File

@@ -0,0 +1,8 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build !plus
package certs
func (this *UploadBatchPopupAction) maxFiles() int {
return 20
}

View File

@@ -18,11 +18,30 @@ func (this *UploadPopupAction) Init() {
this.Nav("", "", "") this.Nav("", "", "")
} }
func (this *UploadPopupAction) RunGet(params struct{}) { func (this *UploadPopupAction) RunGet(params struct {
ServerId int64
UserId int64
}) {
// 读取服务用户
if params.ServerId > 0 {
serverResp, err := this.RPC().ServerRPC().FindEnabledUserServerBasic(this.AdminContext(), &pb.FindEnabledUserServerBasicRequest{ServerId: params.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
var server = serverResp.Server
if server != nil {
params.UserId = server.UserId
}
}
this.Data["userId"] = params.UserId
this.Show() this.Show()
} }
func (this *UploadPopupAction) RunPost(params struct { func (this *UploadPopupAction) RunPost(params struct {
UserId int64
TextMode bool TextMode bool
Name string Name string
IsCA bool IsCA bool
@@ -85,7 +104,7 @@ func (this *UploadPopupAction) RunPost(params struct {
CertData: certData, CertData: certData,
KeyData: keyData, KeyData: keyData,
} }
err := certConfig.Init() err := certConfig.Init(nil)
if err != nil { if err != nil {
if params.IsCA { if params.IsCA {
this.Fail("证书校验错误:" + err.Error()) this.Fail("证书校验错误:" + err.Error())
@@ -107,6 +126,7 @@ func (this *UploadPopupAction) RunPost(params struct {
// 保存 // 保存
createResp, err := this.RPC().SSLCertRPC().CreateSSLCert(this.AdminContext(), &pb.CreateSSLCertRequest{ createResp, err := this.RPC().SSLCertRPC().CreateSSLCert(this.AdminContext(), &pb.CreateSSLCertRequest{
IsOn: params.IsOn, IsOn: params.IsOn,
UserId: params.UserId,
Name: params.Name, Name: params.Name,
Description: params.Description, Description: params.Description,
ServerName: "", ServerName: "",

View File

@@ -36,24 +36,6 @@ func (this *CreateAction) RunGet(params struct{}) {
} }
this.Data["countAuditing"] = countAuditingResp.Count this.Data["countAuditing"] = countAuditingResp.Count
// 所有集群
resp, err := this.RPC().NodeClusterRPC().FindAllEnabledNodeClusters(this.AdminContext(), &pb.FindAllEnabledNodeClustersRequest{})
if err != nil {
this.ErrorPage(err)
}
if err != nil {
this.ErrorPage(err)
return
}
clusterMaps := []maps.Map{}
for _, cluster := range resp.NodeClusters {
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
})
}
this.Data["clusters"] = clusterMaps
// 服务类型 // 服务类型
this.Data["serverTypes"] = serverconfigs.AllServerTypes() this.Data["serverTypes"] = serverconfigs.AllServerTypes()
@@ -95,10 +77,6 @@ func (this *CreateAction) RunPost(params struct {
Must *actions.Must Must *actions.Must
}) { }) {
params.Must.
Field("name", params.Name).
Require("请输入服务名称")
var clusterId = params.ClusterId var clusterId = params.ClusterId
// 用户 // 用户
@@ -129,7 +107,7 @@ func (this *CreateAction) RunPost(params struct {
switch params.ServerType { switch params.ServerType {
case serverconfigs.ServerTypeHTTPProxy, serverconfigs.ServerTypeHTTPWeb: case serverconfigs.ServerTypeHTTPProxy, serverconfigs.ServerTypeHTTPWeb:
listen := []*serverconfigs.NetworkAddressConfig{} var listen = []*serverconfigs.NetworkAddressConfig{}
err := json.Unmarshal([]byte(params.Addresses), &listen) err := json.Unmarshal([]byte(params.Addresses), &listen)
if err != nil { if err != nil {
this.Fail("端口地址解析失败:" + err.Error()) this.Fail("端口地址解析失败:" + err.Error())
@@ -166,7 +144,7 @@ func (this *CreateAction) RunPost(params struct {
this.Fail("DEMO模式下不能创建TCP反向代理") this.Fail("DEMO模式下不能创建TCP反向代理")
} }
listen := []*serverconfigs.NetworkAddressConfig{} var listen = []*serverconfigs.NetworkAddressConfig{}
err := json.Unmarshal([]byte(params.Addresses), &listen) err := json.Unmarshal([]byte(params.Addresses), &listen)
if err != nil { if err != nil {
this.Fail("端口地址解析失败:" + err.Error()) this.Fail("端口地址解析失败:" + err.Error())
@@ -197,13 +175,17 @@ func (this *CreateAction) RunPost(params struct {
tlsConfig.AddListen(addr) tlsConfig.AddListen(addr)
} }
} }
if len(params.Name) == 0 {
params.Name = "TCP负载均衡"
}
case serverconfigs.ServerTypeUDPProxy: case serverconfigs.ServerTypeUDPProxy:
// 在DEMO模式下不能创建 // 在DEMO模式下不能创建
if teaconst.IsDemoMode { if teaconst.IsDemoMode {
this.Fail("DEMO模式下不能创建UDP反向代理") this.Fail("DEMO模式下不能创建UDP反向代理")
} }
listen := []*serverconfigs.NetworkAddressConfig{} var listen = []*serverconfigs.NetworkAddressConfig{}
err := json.Unmarshal([]byte(params.Addresses), &listen) err := json.Unmarshal([]byte(params.Addresses), &listen)
if err != nil { if err != nil {
this.Fail("端口地址解析失败:" + err.Error()) this.Fail("端口地址解析失败:" + err.Error())
@@ -225,20 +207,24 @@ func (this *CreateAction) RunPost(params struct {
udpConfig.AddListen(addr) udpConfig.AddListen(addr)
} }
} }
if len(params.Name) == 0 {
params.Name = "UDP负载均衡"
}
default: default:
this.Fail("请选择正确的服务类型") this.Fail("请选择正确的服务类型")
} }
// 证书 // 证书
if len(params.CertIdsJSON) > 0 { if len(params.CertIdsJSON) > 0 {
certIds := []int64{} var certIds = []int64{}
err := json.Unmarshal(params.CertIdsJSON, &certIds) err := json.Unmarshal(params.CertIdsJSON, &certIds)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
if len(certIds) > 0 { if len(certIds) > 0 {
certRefs := []*sslconfigs.SSLCertRef{} var certRefs = []*sslconfigs.SSLCertRef{}
for _, certId := range certIds { for _, certId := range certIds {
certRefs = append(certRefs, &sslconfigs.SSLCertRef{ certRefs = append(certRefs, &sslconfigs.SSLCertRef{
IsOn: true, IsOn: true,
@@ -265,7 +251,7 @@ func (this *CreateAction) RunPost(params struct {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
sslPolicyId := sslPolicyIdResp.SslPolicyId var sslPolicyId = sslPolicyIdResp.SslPolicyId
httpsConfig.SSLPolicyRef = &sslconfigs.SSLPolicyRef{ httpsConfig.SSLPolicyRef = &sslconfigs.SSLPolicyRef{
IsOn: true, IsOn: true,
SSLPolicyId: sslPolicyId, SSLPolicyId: sslPolicyId,
@@ -282,8 +268,13 @@ func (this *CreateAction) RunPost(params struct {
} }
// 检查域名是否已经存在 // 检查域名是否已经存在
allServerNames := serverconfigs.PlainServerNames(serverNames) var allServerNames = serverconfigs.PlainServerNames(serverNames)
if len(allServerNames) > 0 { if len(allServerNames) > 0 {
// 指定默认名称
if len(params.Name) == 0 {
params.Name = allServerNames[0]
}
dupResp, err := this.RPC().ServerRPC().CheckServerNameDuplicationInNodeCluster(this.AdminContext(), &pb.CheckServerNameDuplicationInNodeClusterRequest{ dupResp, err := this.RPC().ServerRPC().CheckServerNameDuplicationInNodeCluster(this.AdminContext(), &pb.CheckServerNameDuplicationInNodeClusterRequest{
ServerNames: allServerNames, ServerNames: allServerNames,
NodeClusterId: clusterId, NodeClusterId: clusterId,
@@ -396,7 +387,7 @@ func (this *CreateAction) RunPost(params struct {
AdminId: this.AdminId(), AdminId: this.AdminId(),
Type: params.ServerType, Type: params.ServerType,
Name: params.Name, Name: params.Name,
ServerNamesJON: params.ServerNames, ServerNamesJSON: params.ServerNames,
Description: params.Description, Description: params.Description,
NodeClusterId: clusterId, NodeClusterId: clusterId,
IncludeNodesJSON: includeNodesJSON, IncludeNodesJSON: includeNodesJSON,

View File

@@ -65,14 +65,14 @@ func (this *SettingAction) RunPost(params struct {
// TODO 校验配置 // TODO 校验配置
reverseProxyConfig := &serverconfigs.ReverseProxyConfig{} var reverseProxyConfig = &serverconfigs.ReverseProxyConfig{}
err := json.Unmarshal(params.ReverseProxyJSON, reverseProxyConfig) err := json.Unmarshal(params.ReverseProxyJSON, reverseProxyConfig)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
err = reverseProxyConfig.Init() err = reverseProxyConfig.Init(nil)
if err != nil { if err != nil {
this.Fail("配置校验失败:" + err.Error()) this.Fail("配置校验失败:" + err.Error())
} }

View File

@@ -65,14 +65,14 @@ func (this *SettingAction) RunPost(params struct {
// TODO 校验配置 // TODO 校验配置
reverseProxyConfig := &serverconfigs.ReverseProxyConfig{} var reverseProxyConfig = &serverconfigs.ReverseProxyConfig{}
err := json.Unmarshal(params.ReverseProxyJSON, reverseProxyConfig) err := json.Unmarshal(params.ReverseProxyJSON, reverseProxyConfig)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
err = reverseProxyConfig.Init() err = reverseProxyConfig.Init(nil)
if err != nil { if err != nil {
this.Fail("配置校验失败:" + err.Error()) this.Fail("配置校验失败:" + err.Error())
} }

View File

@@ -72,7 +72,7 @@ func (this *SettingAction) RunPost(params struct {
return return
} }
err = reverseProxyConfig.Init() err = reverseProxyConfig.Init(nil)
if err != nil { if err != nil {
this.Fail("配置校验失败:" + err.Error()) this.Fail("配置校验失败:" + err.Error())
} }

View File

@@ -30,6 +30,9 @@ func (this *IndexAction) RunGet(params struct {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
this.Data["regions"] = ""
this.Data["isp"] = ""
if regionResp.IpRegion != nil { if regionResp.IpRegion != nil {
var regionName = regionResp.IpRegion.Summary var regionName = regionResp.IpRegion.Summary
@@ -39,10 +42,8 @@ func (this *IndexAction) RunGet(params struct {
regionName = regionName[:index] regionName = regionName[:index]
} }
this.Data["regions"] = regionName this.Data["regions"] = regionName
} else { this.Data["isp"] = regionResp.IpRegion.Isp
this.Data["regions"] = ""
} }
this.Data["isp"] = regionResp.IpRegion.Isp
// IP列表 // IP列表
ipListResp, err := this.RPC().IPListRPC().FindEnabledIPListContainsIP(this.AdminContext(), &pb.FindEnabledIPListContainsIPRequest{ ipListResp, err := this.RPC().IPListRPC().FindEnabledIPListContainsIP(this.AdminContext(), &pb.FindEnabledIPListContainsIPRequest{

View File

@@ -41,6 +41,10 @@ func (this *UpdateRefsAction) RunPost(params struct {
// 校验配置 // 校验配置
var cacheConfig = webConfig.Cache var cacheConfig = webConfig.Cache
if cacheConfig == nil {
this.Success()
return
}
var refs = []*serverconfigs.HTTPCacheRef{} var refs = []*serverconfigs.HTTPCacheRef{}
err = json.Unmarshal(params.RefsJSON, &refs) err = json.Unmarshal(params.RefsJSON, &refs)

View File

@@ -9,6 +9,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types" "github.com/iwind/TeaGo/types"
"regexp" "regexp"
@@ -30,7 +31,7 @@ func (this *IndexAction) RunGet(params struct {
if !isOk { if !isOk {
return return
} }
httpConfig := &serverconfigs.HTTPProtocolConfig{} var httpConfig = &serverconfigs.HTTPProtocolConfig{}
if len(server.HttpJSON) > 0 { if len(server.HttpJSON) > 0 {
err := json.Unmarshal(server.HttpJSON, httpConfig) err := json.Unmarshal(server.HttpJSON, httpConfig)
if err != nil { if err != nil {
@@ -40,6 +41,26 @@ func (this *IndexAction) RunGet(params struct {
} else { } else {
httpConfig.IsOn = true httpConfig.IsOn = true
} }
_ = httpConfig.Init()
var httpPorts = httpConfig.AllPorts()
// 检查http和https端口冲突
var conflictingPorts = []int{}
if len(server.HttpsJSON) > 0 {
var httpsConfig = &serverconfigs.HTTPSProtocolConfig{}
err := json.Unmarshal(server.HttpsJSON, httpsConfig)
if err != nil {
this.ErrorPage(err)
return
}
_ = httpsConfig.Init(nil)
for _, port := range httpsConfig.AllPorts() {
if lists.ContainsInt(httpPorts, port) {
conflictingPorts = append(conflictingPorts, port)
}
}
}
this.Data["conflictingPorts"] = conflictingPorts
this.Data["serverType"] = server.Type this.Data["serverType"] = server.Type
this.Data["httpConfig"] = maps.Map{ this.Data["httpConfig"] = maps.Map{

View File

@@ -10,6 +10,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types" "github.com/iwind/TeaGo/types"
"regexp" "regexp"
@@ -42,6 +43,27 @@ func (this *IndexAction) RunGet(params struct {
httpsConfig.IsOn = true httpsConfig.IsOn = true
} }
_ = httpsConfig.Init(nil)
var httpsPorts = httpsConfig.AllPorts()
// 检查http和https端口冲突
var conflictingPorts = []int{}
if len(server.HttpJSON) > 0 {
var httpConfig = &serverconfigs.HTTPProtocolConfig{}
err := json.Unmarshal(server.HttpJSON, httpConfig)
if err != nil {
this.ErrorPage(err)
return
}
_ = httpConfig.Init()
for _, port := range httpConfig.AllPorts() {
if lists.ContainsInt(httpsPorts, port) {
conflictingPorts = append(conflictingPorts, port)
}
}
}
this.Data["conflictingPorts"] = conflictingPorts
var sslPolicy *sslconfigs.SSLPolicy var sslPolicy *sslconfigs.SSLPolicy
if httpsConfig.SSLPolicyRef != nil && httpsConfig.SSLPolicyRef.SSLPolicyId > 0 { if httpsConfig.SSLPolicyRef != nil && httpsConfig.SSLPolicyRef.SSLPolicyId > 0 {
sslPolicyConfigResp, err := this.RPC().SSLPolicyRPC().FindEnabledSSLPolicyConfig(this.AdminContext(), &pb.FindEnabledSSLPolicyConfigRequest{ sslPolicyConfigResp, err := this.RPC().SSLPolicyRPC().FindEnabledSSLPolicyConfig(this.AdminContext(), &pb.FindEnabledSSLPolicyConfigRequest{
@@ -199,6 +221,7 @@ func (this *IndexAction) RunPost(params struct {
} }
} }
httpsConfig.SSLPolicy = nil
httpsConfig.SSLPolicyRef = &sslconfigs.SSLPolicyRef{ httpsConfig.SSLPolicyRef = &sslconfigs.SSLPolicyRef{
IsOn: true, IsOn: true,
SSLPolicyId: sslPolicyId, SSLPolicyId: sslPolicyId,

View File

@@ -22,7 +22,7 @@ func FindLocationConfig(parentAction *actionutils.ParentAction, locationId int64
return return
} }
err = locationConfig.Init() err = locationConfig.Init(nil)
if err != nil { if err != nil {
parentAction.ErrorPage(err) parentAction.ErrorPage(err)
return return

View File

@@ -56,14 +56,14 @@ func (this *SettingAction) RunPost(params struct {
// TODO 校验配置 // TODO 校验配置
reverseProxyConfig := &serverconfigs.ReverseProxyConfig{} var reverseProxyConfig = &serverconfigs.ReverseProxyConfig{}
err := json.Unmarshal(params.ReverseProxyJSON, reverseProxyConfig) err := json.Unmarshal(params.ReverseProxyJSON, reverseProxyConfig)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
} }
err = reverseProxyConfig.Init() err = reverseProxyConfig.Init(nil)
if err != nil { if err != nil {
this.Fail("配置校验失败:" + err.Error()) this.Fail("配置校验失败:" + err.Error())
} }

View File

@@ -3,6 +3,7 @@ package redirects
import ( import (
"encoding/json" "encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dns/domains/domainutils"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
@@ -134,6 +135,23 @@ func (this *CreatePopupAction) RunPost(params struct {
this.FailField("domainAfter", "请输入跳转后域名") this.FailField("domainAfter", "请输入跳转后域名")
return return
} }
// 检查用户输入的是否为域名
if !domainutils.ValidateDomainFormat(params.DomainAfter) {
// 是否为URL
u, err := url.Parse(params.DomainAfter)
if err == nil {
if len(u.Host) == 0 {
this.FailField("domainAfter", "跳转后域名输入不正确")
return
}
params.DomainAfter = u.Host
} else {
this.FailField("domainAfter", "跳转后域名输入不正确")
return
}
}
config.DomainAfter = params.DomainAfter config.DomainAfter = params.DomainAfter
config.DomainAfterScheme = params.DomainAfterScheme config.DomainAfterScheme = params.DomainAfterScheme
case serverconfigs.HTTPHostRedirectTypePort: case serverconfigs.HTTPHostRedirectTypePort:

View File

@@ -62,7 +62,7 @@ func (this *SettingAction) RunPost(params struct {
return return
} }
err = reverseProxyConfig.Init() err = reverseProxyConfig.Init(nil)
if err != nil { if err != nil {
this.Fail("配置校验失败:" + err.Error()) this.Fail("配置校验失败:" + err.Error())
} }

View File

@@ -43,7 +43,10 @@ func (this *IndexAction) RunGet(params struct {
// SSL配置 // SSL配置
var sslPolicy *sslconfigs.SSLPolicy var sslPolicy *sslconfigs.SSLPolicy
if tlsConfig.SSLPolicyRef != nil && tlsConfig.SSLPolicyRef.SSLPolicyId > 0 { if tlsConfig.SSLPolicyRef != nil && tlsConfig.SSLPolicyRef.SSLPolicyId > 0 {
sslPolicyConfigResp, err := this.RPC().SSLPolicyRPC().FindEnabledSSLPolicyConfig(this.AdminContext(), &pb.FindEnabledSSLPolicyConfigRequest{SslPolicyId: tlsConfig.SSLPolicyRef.SSLPolicyId}) sslPolicyConfigResp, err := this.RPC().SSLPolicyRPC().FindEnabledSSLPolicyConfig(this.AdminContext(), &pb.FindEnabledSSLPolicyConfigRequest{
SslPolicyId: tlsConfig.SSLPolicyRef.SSLPolicyId,
IgnoreData: true,
})
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return

View File

@@ -11,3 +11,7 @@ import (
func filterMenuItems(serverConfig *serverconfigs.ServerConfig, menuItems []maps.Map, serverIdString string, secondMenuItem string) []maps.Map { func filterMenuItems(serverConfig *serverconfigs.ServerConfig, menuItems []maps.Map, serverIdString string, secondMenuItem string) []maps.Map {
return menuItems return menuItems
} }
func filterMenuItems2(serverConfig *serverconfigs.ServerConfig, menuItems []maps.Map, serverIdString string, secondMenuItem string) []maps.Map {
return menuItems
}

View File

@@ -181,7 +181,7 @@ func (this *ServerHelper) createLogMenu(secondMenuItem string, serverIdString st
// 统计菜单 // 统计菜单
func (this *ServerHelper) createStatMenu(secondMenuItem string, serverIdString string, serverConfig *serverconfigs.ServerConfig) []maps.Map { func (this *ServerHelper) createStatMenu(secondMenuItem string, serverIdString string, serverConfig *serverconfigs.ServerConfig) []maps.Map {
menuItems := []maps.Map{} var menuItems = []maps.Map{}
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "流量统计", "name": "流量统计",
"url": "/servers/server/stat?serverId=" + serverIdString, "url": "/servers/server/stat?serverId=" + serverIdString,
@@ -212,7 +212,7 @@ func (this *ServerHelper) createStatMenu(secondMenuItem string, serverIdString s
// 设置菜单 // 设置菜单
func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdString string, serverConfig *serverconfigs.ServerConfig) (items []maps.Map) { func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdString string, serverConfig *serverconfigs.ServerConfig) (items []maps.Map) {
menuItems := []maps.Map{ var menuItems = []maps.Map{
{ {
"name": "基本信息", "name": "基本信息",
"url": "/servers/server/settings?serverId=" + serverIdString, "url": "/servers/server/settings?serverId=" + serverIdString,
@@ -249,27 +249,14 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
"isOff": serverConfig.HTTPS != nil && !serverConfig.HTTPS.IsOn, "isOff": serverConfig.HTTPS != nil && !serverConfig.HTTPS.IsOn,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "源站", "name": "源站",
"url": "/servers/server/settings/reverseProxy?serverId=" + serverIdString, "url": "/servers/server/settings/reverseProxy?serverId=" + serverIdString,
"isActive": secondMenuItem == "reverseProxy", "isActive": secondMenuItem == "reverseProxy",
"isOn": serverConfig.ReverseProxyRef != nil && serverConfig.ReverseProxyRef.IsOn, "isOn": serverConfig.ReverseProxyRef != nil && serverConfig.ReverseProxyRef.IsOn,
"configCode": serverconfigs.ConfigCodeReverseProxy,
}) })
if teaconst.IsPlus { menuItems = filterMenuItems(serverConfig, menuItems, serverIdString, secondMenuItem)
menuItems = append(menuItems, maps.Map{
"name": "-",
"url": "",
"isActive": false,
})
menuItems = append(menuItems, maps.Map{
"name": "5秒盾",
"url": "/servers/server/settings/uam?serverId=" + serverIdString,
"isActive": secondMenuItem == "uam",
"isOn": serverConfig.UAM != nil && serverConfig.UAM.IsOn,
"isImportant": serverConfig.UAM != nil && serverConfig.UAM.IsOn,
})
}
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "-", "name": "-",
@@ -277,10 +264,11 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
"isActive": false, "isActive": false,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "URL跳转", "name": "URL跳转",
"url": "/servers/server/settings/redirects?serverId=" + serverIdString, "url": "/servers/server/settings/redirects?serverId=" + serverIdString,
"isActive": secondMenuItem == "redirects", "isActive": secondMenuItem == "redirects",
"isOn": serverConfig.Web != nil && len(serverConfig.Web.HostRedirects) > 0, "isOn": serverConfig.Web != nil && len(serverConfig.Web.HostRedirects) > 0,
"configCode": serverconfigs.ConfigCodeHostRedirects,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "路由规则", "name": "路由规则",
@@ -301,58 +289,67 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
"isOn": serverConfig.Web != nil && serverConfig.Web.FirewallRef != nil && serverConfig.Web.FirewallRef.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.FirewallRef != nil && serverConfig.Web.FirewallRef.IsOn,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "缓存", "name": "缓存",
"url": "/servers/server/settings/cache?serverId=" + serverIdString, "url": "/servers/server/settings/cache?serverId=" + serverIdString,
"isActive": secondMenuItem == "cache", "isActive": secondMenuItem == "cache",
"isOn": serverConfig.Web != nil && serverConfig.Web.Cache != nil && serverConfig.Web.Cache.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.Cache != nil && serverConfig.Web.Cache.IsOn,
"configCode": serverconfigs.ConfigCodeCache,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "访问鉴权", "name": "访问鉴权",
"url": "/servers/server/settings/access?serverId=" + serverIdString, "url": "/servers/server/settings/access?serverId=" + serverIdString,
"isActive": secondMenuItem == "access", "isActive": secondMenuItem == "access",
"isOn": serverConfig.Web != nil && serverConfig.Web.Auth != nil && serverConfig.Web.Auth.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.Auth != nil && serverConfig.Web.Auth.IsOn,
"configCode": serverconfigs.ConfigCodeAuth,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "防盗链", "name": "防盗链",
"url": "/servers/server/settings/referers?serverId=" + serverIdString, "url": "/servers/server/settings/referers?serverId=" + serverIdString,
"isActive": secondMenuItem == "referer", "isActive": secondMenuItem == "referer",
"isOn": serverConfig.Web != nil && serverConfig.Web.Referers != nil && serverConfig.Web.Referers.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.Referers != nil && serverConfig.Web.Referers.IsOn,
"configCode": serverconfigs.ConfigCodeReferers,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "UA名单", "name": "UA名单",
"url": "/servers/server/settings/userAgent?serverId=" + serverIdString, "url": "/servers/server/settings/userAgent?serverId=" + serverIdString,
"isActive": secondMenuItem == "userAgent", "isActive": secondMenuItem == "userAgent",
"isOn": serverConfig.Web != nil && serverConfig.Web.UserAgent != nil && serverConfig.Web.UserAgent.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.UserAgent != nil && serverConfig.Web.UserAgent.IsOn,
"configCode": serverconfigs.ConfigCodeUserAgent,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "字符编码", "name": "字符编码",
"url": "/servers/server/settings/charset?serverId=" + serverIdString, "url": "/servers/server/settings/charset?serverId=" + serverIdString,
"isActive": secondMenuItem == "charset", "isActive": secondMenuItem == "charset",
"isOn": serverConfig.Web != nil && serverConfig.Web.Charset != nil && serverConfig.Web.Charset.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.Charset != nil && serverConfig.Web.Charset.IsOn,
"configCode": serverconfigs.ConfigCodeCharset,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "访问日志", "name": "访问日志",
"url": "/servers/server/settings/accessLog?serverId=" + serverIdString, "url": "/servers/server/settings/accessLog?serverId=" + serverIdString,
"isActive": secondMenuItem == "accessLog", "isActive": secondMenuItem == "accessLog",
"isOn": serverConfig.Web != nil && serverConfig.Web.AccessLogRef != nil && serverConfig.Web.AccessLogRef.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.AccessLogRef != nil && serverConfig.Web.AccessLogRef.IsOn,
"configCode": serverconfigs.ConfigCodeAccessLog,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "统计", "name": "统计",
"url": "/servers/server/settings/stat?serverId=" + serverIdString, "url": "/servers/server/settings/stat?serverId=" + serverIdString,
"isActive": secondMenuItem == "stat", "isActive": secondMenuItem == "stat",
"isOn": serverConfig.Web != nil && serverConfig.Web.StatRef != nil && serverConfig.Web.StatRef.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.StatRef != nil && serverConfig.Web.StatRef.IsOn,
"configCode": serverconfigs.ConfigCodeStat,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "内容压缩", "name": "内容压缩",
"url": "/servers/server/settings/compression?serverId=" + serverIdString, "url": "/servers/server/settings/compression?serverId=" + serverIdString,
"isActive": secondMenuItem == "compression", "isActive": secondMenuItem == "compression",
"isOn": serverConfig.Web != nil && serverConfig.Web.Compression != nil && serverConfig.Web.Compression.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.Compression != nil && serverConfig.Web.Compression.IsOn,
"configCode": serverconfigs.ConfigCodeCompression,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "自定义页面", "name": "自定义页面",
"url": "/servers/server/settings/pages?serverId=" + serverIdString, "url": "/servers/server/settings/pages?serverId=" + serverIdString,
"isActive": secondMenuItem == "pages", "isActive": secondMenuItem == "pages",
"isOn": serverConfig.Web != nil && (len(serverConfig.Web.Pages) > 0 || (serverConfig.Web.Shutdown != nil && serverConfig.Web.Shutdown.IsOn)), "isOn": serverConfig.Web != nil && (len(serverConfig.Web.Pages) > 0 || (serverConfig.Web.Shutdown != nil && serverConfig.Web.Shutdown.IsOn)),
"configCode": serverconfigs.ConfigCodePages,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "HTTP Header", "name": "HTTP Header",
@@ -361,23 +358,26 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
"isOn": this.hasHTTPHeaders(serverConfig.Web), "isOn": this.hasHTTPHeaders(serverConfig.Web),
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "Websocket", "name": "Websocket",
"url": "/servers/server/settings/websocket?serverId=" + serverIdString, "url": "/servers/server/settings/websocket?serverId=" + serverIdString,
"isActive": secondMenuItem == "websocket", "isActive": secondMenuItem == "websocket",
"isOn": serverConfig.Web != nil && serverConfig.Web.WebsocketRef != nil && serverConfig.Web.WebsocketRef.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.WebsocketRef != nil && serverConfig.Web.WebsocketRef.IsOn,
"configCode": serverconfigs.ConfigCodeWebsocket,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "WebP", "name": "WebP",
"url": "/servers/server/settings/webp?serverId=" + serverIdString, "url": "/servers/server/settings/webp?serverId=" + serverIdString,
"isActive": secondMenuItem == "webp", "isActive": secondMenuItem == "webp",
"isOn": serverConfig.Web != nil && serverConfig.Web.WebP != nil && serverConfig.Web.WebP.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.WebP != nil && serverConfig.Web.WebP.IsOn,
"configCode": serverconfigs.ConfigCodeWebp,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "静态分发", "name": "静态分发",
"url": "/servers/server/settings/web?serverId=" + serverIdString, "url": "/servers/server/settings/web?serverId=" + serverIdString,
"isActive": secondMenuItem == "web", "isActive": secondMenuItem == "web",
"isOn": serverConfig.Web != nil && serverConfig.Web.Root != nil && serverConfig.Web.Root.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.Root != nil && serverConfig.Web.Root.IsOn,
"configCode": serverconfigs.ConfigCodeRoot,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "Fastcgi", "name": "Fastcgi",
@@ -393,20 +393,22 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "访客IP地址", "name": "访客IP地址",
"url": "/servers/server/settings/remoteAddr?serverId=" + serverIdString, "url": "/servers/server/settings/remoteAddr?serverId=" + serverIdString,
"isActive": secondMenuItem == "remoteAddr", "isActive": secondMenuItem == "remoteAddr",
"isOn": serverConfig.Web != nil && serverConfig.Web.RemoteAddr != nil && serverConfig.Web.RemoteAddr.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.RemoteAddr != nil && serverConfig.Web.RemoteAddr.IsOn,
"configCode": serverconfigs.ConfigCodeRemoteAddr,
}) })
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "请求限制", "name": "请求限制",
"url": "/servers/server/settings/requestLimit?serverId=" + serverIdString, "url": "/servers/server/settings/requestLimit?serverId=" + serverIdString,
"isActive": secondMenuItem == "requestLimit", "isActive": secondMenuItem == "requestLimit",
"isOn": serverConfig.Web != nil && serverConfig.Web.RequestLimit != nil && serverConfig.Web.RequestLimit.IsOn, "isOn": serverConfig.Web != nil && serverConfig.Web.RequestLimit != nil && serverConfig.Web.RequestLimit.IsOn,
"configCode": serverconfigs.ConfigCodeRequestLimit,
}) })
menuItems = filterMenuItems(serverConfig, menuItems, serverIdString, secondMenuItem) menuItems = filterMenuItems2(serverConfig, menuItems, serverIdString, secondMenuItem)
menuItems = append(menuItems, maps.Map{ menuItems = append(menuItems, maps.Map{
"name": "-", "name": "-",

View File

@@ -0,0 +1,86 @@
package adminserverutils
import (
"errors"
"github.com/iwind/TeaGo"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/types"
"gopkg.in/yaml.v3"
"net"
"os"
"time"
)
var ServerConfigIsChanged = false
const configFilename = "server.yaml"
// LoadServerConfig 读取当前服务配置
func LoadServerConfig() (*TeaGo.ServerConfig, error) {
configFile := Tea.ConfigFile(configFilename)
data, err := os.ReadFile(configFile)
if err != nil {
return nil, err
}
var serverConfig = &TeaGo.ServerConfig{}
err = yaml.Unmarshal(data, serverConfig)
if err != nil {
return nil, err
}
return serverConfig, nil
}
// WriteServerConfig 保存当前服务配置
func WriteServerConfig(serverConfig *TeaGo.ServerConfig) error {
data, err := yaml.Marshal(serverConfig)
if err != nil {
return err
}
err = os.WriteFile(Tea.ConfigFile(configFilename), data, 0666)
if err != nil {
return err
}
ServerConfigIsChanged = true
return nil
}
// ReadServerHTTPS 检查HTTPS地址
func ReadServerHTTPS() (port int, err error) {
config, err := LoadServerConfig()
if err != nil {
return 0, err
}
if config == nil {
return 0, errors.New("could not load server config")
}
if config.Https.On && len(config.Https.Listen) > 0 {
for _, listen := range config.Https.Listen {
_, portString, splitErr := net.SplitHostPort(listen)
if splitErr == nil {
var portInt = types.Int(portString)
if portInt > 0 {
// 是否已经启动
checkErr := func() error {
conn, connErr := net.DialTimeout("tcp", ":"+portString, 1*time.Second)
if connErr != nil {
return connErr
}
_ = conn.Close()
return nil
}()
if checkErr != nil {
continue
}
port = portInt
err = nil
break
}
}
}
}
return
}

View File

@@ -2,6 +2,7 @@ package server
import ( import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
adminserverutils "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/server/admin-server-utils"
) )
type IndexAction struct { type IndexAction struct {
@@ -13,9 +14,9 @@ func (this *IndexAction) Init() {
} }
func (this *IndexAction) RunGet(params struct{}) { func (this *IndexAction) RunGet(params struct{}) {
this.Data["serverIsChanged"] = serverConfigIsChanged this.Data["serverIsChanged"] = adminserverutils.ServerConfigIsChanged
serverConfig, err := loadServerConfig() serverConfig, err := adminserverutils.LoadServerConfig()
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return

View File

@@ -3,6 +3,7 @@ package server
import ( import (
"github.com/TeaOSLab/EdgeAdmin/internal/utils" "github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
adminserverutils "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/server/admin-server-utils"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"net" "net"
) )
@@ -16,7 +17,7 @@ func (this *UpdateHTTPPopupAction) Init() {
} }
func (this *UpdateHTTPPopupAction) RunGet(params struct{}) { func (this *UpdateHTTPPopupAction) RunGet(params struct{}) {
serverConfig, err := loadServerConfig() serverConfig, err := adminserverutils.LoadServerConfig()
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
@@ -38,7 +39,7 @@ func (this *UpdateHTTPPopupAction) RunPost(params struct {
this.Fail("请输入绑定地址") this.Fail("请输入绑定地址")
} }
serverConfig, err := loadServerConfig() serverConfig, err := adminserverutils.LoadServerConfig()
if err != nil { if err != nil {
this.Fail("保存失败:" + err.Error()) this.Fail("保存失败:" + err.Error())
} }
@@ -58,7 +59,7 @@ func (this *UpdateHTTPPopupAction) RunPost(params struct {
} }
serverConfig.Http.Listen = listen serverConfig.Http.Listen = listen
err = writeServerConfig(serverConfig) err = adminserverutils.WriteServerConfig(serverConfig)
if err != nil { if err != nil {
this.Fail("保存失败:" + err.Error()) this.Fail("保存失败:" + err.Error())
} }

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/utils" "github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
adminserverutils "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/server/admin-server-utils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/Tea" "github.com/iwind/TeaGo/Tea"
@@ -21,7 +22,7 @@ func (this *UpdateHTTPSPopupAction) Init() {
} }
func (this *UpdateHTTPSPopupAction) RunGet(params struct{}) { func (this *UpdateHTTPSPopupAction) RunGet(params struct{}) {
serverConfig, err := loadServerConfig() serverConfig, err := adminserverutils.LoadServerConfig()
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return
@@ -47,7 +48,7 @@ func (this *UpdateHTTPSPopupAction) RunGet(params struct{}) {
CertData: certData, CertData: certData,
KeyData: keyData, KeyData: keyData,
} }
_ = certConfig.Init() _ = certConfig.Init(nil)
certConfig.CertData = nil certConfig.CertData = nil
certConfig.KeyData = nil certConfig.KeyData = nil
certConfigs = append(certConfigs, certConfig) certConfigs = append(certConfigs, certConfig)
@@ -70,7 +71,7 @@ func (this *UpdateHTTPSPopupAction) RunPost(params struct {
this.Fail("请输入绑定地址") this.Fail("请输入绑定地址")
} }
serverConfig, err := loadServerConfig() serverConfig, err := adminserverutils.LoadServerConfig()
if err != nil { if err != nil {
this.Fail("保存失败:" + err.Error()) this.Fail("保存失败:" + err.Error())
} }
@@ -133,7 +134,7 @@ func (this *UpdateHTTPSPopupAction) RunPost(params struct {
serverConfig.Https.Cert = "configs/https.cert.pem" serverConfig.Https.Cert = "configs/https.cert.pem"
} }
err = writeServerConfig(serverConfig) err = adminserverutils.WriteServerConfig(serverConfig)
if err != nil { if err != nil {
this.Fail("保存配置失败:" + err.Error()) this.Fail("保存配置失败:" + err.Error())
} }

View File

@@ -1,41 +0,0 @@
package server
import (
"github.com/iwind/TeaGo"
"github.com/iwind/TeaGo/Tea"
"gopkg.in/yaml.v3"
"os"
)
var serverConfigIsChanged = false
// 读取当前服务配置
func loadServerConfig() (*TeaGo.ServerConfig, error) {
configFile := Tea.ConfigFile("server.yaml")
data, err := os.ReadFile(configFile)
if err != nil {
return nil, err
}
serverConfig := &TeaGo.ServerConfig{}
err = yaml.Unmarshal(data, serverConfig)
if err != nil {
return nil, err
}
return serverConfig, nil
}
// 保存当前服务配置
func writeServerConfig(serverConfig *TeaGo.ServerConfig) error {
data, err := yaml.Marshal(serverConfig)
if err != nil {
return err
}
err = os.WriteFile(Tea.ConfigFile("server.yaml"), data, 0666)
if err != nil {
return err
}
serverConfigIsChanged = true
return nil
}

View File

@@ -58,6 +58,7 @@ func (this *IndexAction) RunPost(params struct {
var apiURL = teaconst.UpdatesURL var apiURL = teaconst.UpdatesURL
apiURL = strings.ReplaceAll(apiURL, "${os}", runtime.GOOS) apiURL = strings.ReplaceAll(apiURL, "${os}", runtime.GOOS)
apiURL = strings.ReplaceAll(apiURL, "${arch}", runtime.GOARCH) apiURL = strings.ReplaceAll(apiURL, "${arch}", runtime.GOARCH)
apiURL = strings.ReplaceAll(apiURL, "${version}", teaconst.Version)
resp, err := http.Get(apiURL) resp, err := http.Get(apiURL)
if err != nil { if err != nil {
this.Data["result"] = maps.Map{ this.Data["result"] = maps.Map{

View File

@@ -9,6 +9,7 @@ import (
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
"net" "net"
"os" "os"
"runtime"
"strings" "strings"
"time" "time"
) )
@@ -63,10 +64,11 @@ func (this *DetectDBAction) RunPost(params struct{}) {
} }
this.Data["localDB"] = maps.Map{ this.Data["localDB"] = maps.Map{
"host": localHost, "host": localHost,
"port": localPort, "port": localPort,
"username": localUsername, "username": localUsername,
"password": localPassword, "password": localPassword,
"canInstall": runtime.GOOS == "linux" && runtime.GOARCH == "amd64" && os.Getgid() == 0,
} }
this.Success() this.Success()

View File

@@ -1,6 +1,9 @@
package setup package setup
import "github.com/iwind/TeaGo" import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql"
"github.com/iwind/TeaGo"
)
func init() { func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) { TeaGo.BeforeStart(func(server *TeaGo.Server) {
@@ -15,6 +18,8 @@ func init() {
Post("/status", new(StatusAction)). Post("/status", new(StatusAction)).
Post("/detectDB", new(DetectDBAction)). Post("/detectDB", new(DetectDBAction)).
Post("/checkLocalIP", new(CheckLocalIPAction)). Post("/checkLocalIP", new(CheckLocalIPAction)).
GetPost("/mysql/installPopup", new(mysql.InstallPopupAction)).
Post("/mysql/installLogs", new(mysql.InstallLogsAction)).
EndAll() EndAll()
}) })
} }

View File

@@ -0,0 +1,17 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package mysql
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers/utils"
)
type InstallLogsAction struct {
actionutils.ParentAction
}
func (this *InstallLogsAction) RunPost(params struct{}) {
this.Data["logs"] = utils.SharedLogger.ReadAll()
this.Success()
}

View File

@@ -0,0 +1,47 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package mysql
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers/utils"
)
type InstallPopupAction struct {
actionutils.ParentAction
}
func (this *InstallPopupAction) RunGet(params struct{}) {
this.Show()
}
func (this *InstallPopupAction) RunPost(params struct{}) {
// 清空日志
utils.SharedLogger.Reset()
this.Data["isOk"] = false
var installer = mysqlinstallers.NewMySQLInstaller()
var targetDir = "/usr/local/mysql"
xzFile, err := installer.Download()
if err != nil {
this.Data["err"] = "download failed: " + err.Error()
this.Success()
return
}
err = installer.InstallFromFile(xzFile, targetDir)
if err != nil {
this.Data["err"] = "install from '" + xzFile + "' failed: " + err.Error()
this.Success()
return
}
this.Data["user"] = "root"
this.Data["password"] = installer.Password()
this.Data["dir"] = targetDir
this.Data["isOk"] = true
this.Success()
}

View File

@@ -0,0 +1,713 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package mysqlinstallers
import (
"bytes"
"crypto/rand"
"errors"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers/utils"
stringutil "github.com/iwind/TeaGo/utils/string"
timeutil "github.com/iwind/TeaGo/utils/time"
"io"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
)
type MySQLInstaller struct {
password string
}
func NewMySQLInstaller() *MySQLInstaller {
return &MySQLInstaller{}
}
func (this *MySQLInstaller) InstallFromFile(xzFilePath string, targetDir string) error {
// check whether mysql already running
this.log("checking mysqld ...")
var oldPid = utils.FindPidWithName("mysqld")
if oldPid > 0 {
return errors.New("there is already a running mysql server process, pid: '" + strconv.Itoa(oldPid) + "'")
}
// check target dir
this.log("checking target dir '" + targetDir + "' ...")
_, err := os.Stat(targetDir)
if err == nil {
// check target dir
matches, _ := filepath.Glob(targetDir + "/*")
if len(matches) > 0 {
return errors.New("target dir '" + targetDir + "' already exists and not empty")
} else {
err = os.Remove(targetDir)
if err != nil {
return errors.New("clean target dir '" + targetDir + "' failed: " + err.Error())
}
}
}
// check 'tar' command
this.log("checking 'tar' command ...")
var tarExe, _ = exec.LookPath("tar")
if len(tarExe) == 0 {
this.log("installing 'tar' command ...")
err = this.installTarCommand()
if err != nil {
this.log("WARN: failed to install 'tar' ...")
}
}
// check commands
this.log("checking system commands ...")
var cmdList = []string{"tar" /** again **/, "chown", "sh"}
for _, cmd := range cmdList {
cmdPath, err := exec.LookPath(cmd)
if err != nil || len(cmdPath) == 0 {
return errors.New("could not find '" + cmd + "' command in this system")
}
}
groupAddExe, err := this.lookupGroupAdd()
if err != nil {
return errors.New("could not find 'groupadd' command in this system")
}
userAddExe, err := this.lookupUserAdd()
if err != nil {
return errors.New("could not find 'useradd' command in this system")
}
// ubuntu apt
aptExe, err := exec.LookPath("apt")
if err == nil && len(aptExe) > 0 {
for _, lib := range []string{"libaio1", "libncurses5"} {
this.log("checking " + lib + " ...")
var cmd = utils.NewCmd("apt", "-y", "install", lib)
cmd.WithStderr()
err = cmd.Run()
if err != nil {
return errors.New("install " + lib + " failed: " + cmd.Stderr())
}
time.Sleep(1 * time.Second)
}
} else { // yum
yumExe, err := exec.LookPath("yum")
if err == nil && len(yumExe) > 0 {
for _, lib := range []string{"libaio", "ncurses-libs", "ncurses-compat-libs"} {
var cmd = utils.NewCmd("yum", "-y", "install", lib)
_ = cmd.Run()
time.Sleep(1 * time.Second)
}
}
// create symbolic links
{
var libFile = "/usr/lib64/libncurses.so.5"
_, err = os.Stat(libFile)
if err != nil && os.IsNotExist(err) {
var latestLibFile = this.findLatestVersionFile("/usr/lib64", "libncurses.so.")
if len(latestLibFile) > 0 {
this.log("link '" + latestLibFile + "' to '" + libFile + "'")
_ = os.Symlink(latestLibFile, libFile)
}
}
}
{
var libFile = "/usr/lib64/libtinfo.so.5"
_, err = os.Stat(libFile)
if err != nil && os.IsNotExist(err) {
var latestLibFile = this.findLatestVersionFile("/usr/lib64", "libtinfo.so.")
if len(latestLibFile) > 0 {
this.log("link '" + latestLibFile + "' to '" + libFile + "'")
_ = os.Symlink(latestLibFile, libFile)
}
}
}
}
// create 'mysql' user group
this.log("checking 'mysql' user group ...")
{
data, err := os.ReadFile("/etc/group")
if err != nil {
return errors.New("check user group failed: " + err.Error())
}
if !bytes.Contains(data, []byte("\nmysql:")) {
var cmd = utils.NewCmd(groupAddExe, "mysql")
cmd.WithStderr()
err = cmd.Run()
if err != nil {
return errors.New("add 'mysql' user group failed: " + cmd.Stderr())
}
}
}
// create 'mysql' user
this.log("checking 'mysql' user ...")
{
data, err := os.ReadFile("/etc/passwd")
if err != nil {
return errors.New("check user failed: " + err.Error())
}
if !bytes.Contains(data, []byte("\nmysql:")) {
var cmd *utils.Cmd
if strings.HasSuffix(userAddExe, "useradd") {
cmd = utils.NewCmd(userAddExe, "mysql", "-g", "mysql")
} else { // adduser
cmd = utils.NewCmd(userAddExe, "-S", "-G", "mysql", "mysql")
}
cmd.WithStderr()
err = cmd.Run()
if err != nil {
return errors.New("add 'mysql' user failed: " + cmd.Stderr())
}
}
}
// mkdir
{
var parentDir = filepath.Dir(targetDir)
stat, err := os.Stat(parentDir)
if err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(parentDir, 0777)
if err != nil {
return errors.New("try to create dir '" + parentDir + "' failed: " + err.Error())
}
} else {
return errors.New("check dir '" + parentDir + "' failed: " + err.Error())
}
} else {
if !stat.IsDir() {
return errors.New("'" + parentDir + "' should be a directory")
}
}
}
// check installer file .xz
this.log("checking installer file ...")
{
stat, err := os.Stat(xzFilePath)
if err != nil {
return errors.New("could not open the installer file: " + err.Error())
}
if stat.IsDir() {
return errors.New("'" + xzFilePath + "' not a valid file")
}
var basename = filepath.Base(xzFilePath)
if !strings.HasSuffix(basename, ".xz") {
return errors.New("installer file should has '.xz' extension")
}
}
// extract
this.log("extracting installer file ...")
var tmpDir = os.TempDir() + "/goedge-mysql-tmp"
{
_, err := os.Stat(tmpDir)
if err == nil {
err = os.RemoveAll(tmpDir)
if err != nil {
return errors.New("clean temporary directory '" + tmpDir + "' failed: " + err.Error())
}
}
err = os.Mkdir(tmpDir, 0777)
if err != nil {
return errors.New("create temporary directory '" + tmpDir + "' failed: " + err.Error())
}
}
{
var cmd = utils.NewCmd("tar", "-xJvf", xzFilePath, "-C", tmpDir)
cmd.WithStderr()
err = cmd.Run()
if err != nil {
return errors.New("extract installer file '" + xzFilePath + "' failed: " + cmd.Stderr())
}
}
// create datadir
matches, err := filepath.Glob(tmpDir + "/mysql-*")
if err != nil || len(matches) == 0 {
return errors.New("could not find mysql installer directory from '" + tmpDir + "'")
}
var baseDir = matches[0]
var dataDir = baseDir + "/data"
_, err = os.Stat(dataDir)
if err != nil {
if os.IsNotExist(err) {
err = os.Mkdir(dataDir, 0777)
if err != nil {
return errors.New("create data dir '" + dataDir + "' failed: " + err.Error())
}
} else {
return errors.New("check data dir '" + dataDir + "' failed: " + err.Error())
}
}
// chown datadir
{
var cmd = utils.NewCmd("chown", "mysql:mysql", dataDir)
cmd.WithStderr()
err = cmd.Run()
if err != nil {
return errors.New("chown data dir '" + dataDir + "' failed: " + err.Error())
}
}
// create my.cnf
var myCnfFile = "/etc/my.cnf"
_, err = os.Stat(myCnfFile)
if err == nil {
// backup it
err = os.Rename(myCnfFile, "/etc/my.cnf."+timeutil.Format("YmdHis"))
if err != nil {
return errors.New("backup '/etc/my.cnf' failed: " + err.Error())
}
}
// mysql server options https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html
var myCnfTemplate = this.createMyCnf(baseDir, dataDir)
err = os.WriteFile(myCnfFile, []byte(myCnfTemplate), 0666)
if err != nil {
return errors.New("write '" + myCnfFile + "' failed: " + err.Error())
}
// initialize
this.log("initializing mysql ...")
var generatedPassword = ""
{
var cmd = utils.NewCmd(baseDir+"/bin/mysqld", "--initialize", "--user=mysql")
cmd.WithStderr()
cmd.WithStdout()
err = cmd.Run()
if err != nil {
return errors.New("initialize failed: " + cmd.Stderr())
}
// read from stdout
var match = regexp.MustCompile(`temporary password is.+:\s*(.+)`).FindStringSubmatch(cmd.Stdout())
if len(match) == 0 {
// read from stderr
match = regexp.MustCompile(`temporary password is.+:\s*(.+)`).FindStringSubmatch(cmd.Stderr())
if len(match) == 0 {
return errors.New("initialize successfully, but could not find generated password, please report to developer")
}
}
generatedPassword = strings.TrimSpace(match[1])
// write password to file
var passwordFile = baseDir + "/generated-password.txt"
err = os.WriteFile(passwordFile, []byte(generatedPassword), 0666)
if err != nil {
return errors.New("write password failed: " + err.Error())
}
}
// move to right place
this.log("moving files to target dir ...")
err = os.Rename(baseDir, targetDir)
if err != nil {
return errors.New("move '" + baseDir + "' to '" + targetDir + "' failed: " + err.Error())
}
baseDir = targetDir
// change my.cnf
myCnfTemplate = this.createMyCnf(baseDir, baseDir+"/data")
err = os.WriteFile(myCnfFile, []byte(myCnfTemplate), 0666)
if err != nil {
return errors.New("create new '" + myCnfFile + "' failed: " + err.Error())
}
// start mysql
this.log("starting mysql ...")
{
var cmd = utils.NewCmd(baseDir+"/bin/mysqld_safe", "--user=mysql")
cmd.WithStderr()
err = cmd.Start()
if err != nil {
return errors.New("start failed '" + cmd.String() + "': " + cmd.Stderr())
}
// waiting for startup
for i := 0; i < 5; i++ {
_, err = net.Dial("tcp", "127.0.0.1:3306")
if err != nil {
time.Sleep(1 * time.Second)
} else {
break
}
}
time.Sleep(1 * time.Second)
}
// change password
newPassword, err := this.generatePassword()
if err != nil {
return errors.New("generate new password failed: " + err.Error())
}
this.log("changing mysql password ...")
var passwordSQL = "ALTER USER 'root'@'localhost' IDENTIFIED BY '" + newPassword + "';"
{
var cmd = utils.NewCmd("sh", "-c", baseDir+"/bin/mysql --user=root --password=\""+generatedPassword+"\" --execute=\""+passwordSQL+"\" --connect-expired-password")
cmd.WithStderr()
err = cmd.Run()
if err != nil {
return errors.New("change password failed: " + cmd.String() + ": " + cmd.Stderr())
}
}
this.password = newPassword
var passwordFile = baseDir + "/generated-password.txt"
err = os.WriteFile(passwordFile, []byte(this.password), 0666)
if err != nil {
return errors.New("write generated file failed: " + err.Error())
}
// remove temporary directory
_ = os.Remove(tmpDir)
// create link to 'mysql' client command
var clientExe = "/usr/local/bin/mysql"
_, err = os.Stat(clientExe)
if err != nil && os.IsNotExist(err) {
err = os.Symlink(baseDir+"/bin/mysql", clientExe)
if err == nil {
this.log("created symbolic link '" + clientExe + "' to '" + baseDir + "/bin/mysql'")
} else {
this.log("WARN: failed to create symbolic link '" + clientExe + "' to '" + baseDir + "/bin/mysql': " + err.Error())
}
}
// install service
// this is not required, so we ignore all errors
err = this.installService(baseDir)
if err != nil {
this.log("WARN: install service failed: " + err.Error())
}
this.log("finished")
return nil
}
func (this *MySQLInstaller) Download() (path string, err error) {
var client = &http.Client{}
// check latest version
this.log("checking mysql latest version ...")
var latestVersion = "8.0.31" // 默认版本
{
req, err := http.NewRequest(http.MethodGet, "https://dev.mysql.com/downloads/mysql/", nil)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", "curl/7.61.1")
resp, err := client.Do(req)
if err != nil {
return "", errors.New("check latest version failed: " + err.Error())
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode == http.StatusOK {
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", errors.New("read latest version failed: " + err.Error())
}
var reg = regexp.MustCompile(`<h1>MySQL Community Server ([\d.]+) </h1>`)
var matches = reg.FindSubmatch(data)
if len(matches) > 0 {
latestVersion = string(matches[1])
}
}
}
this.log("found version: v" + latestVersion)
// download
this.log("start downloading ...")
var downloadURL = "https://cdn.mysql.com/Downloads/MySQL-8.0/mysql-" + latestVersion + "-linux-glibc2.17-x86_64-minimal.tar.xz"
{
req, err := http.NewRequest(http.MethodGet, downloadURL, nil)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36")
resp, err := client.Do(req)
if err != nil {
return "", errors.New("check latest version failed: " + err.Error())
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return "", errors.New("check latest version failed: invalid response code: " + strconv.Itoa(resp.StatusCode))
}
path = filepath.Base(downloadURL)
fp, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
return "", errors.New("create download file '" + path + "' failed: " + err.Error())
}
var writer = utils.NewProgressWriter(fp, resp.ContentLength)
var ticker = time.NewTicker(1 * time.Second)
var done = make(chan bool, 1)
go func() {
var lastProgress float32 = -1
for {
select {
case <-ticker.C:
var progress = writer.Progress()
if lastProgress < 0 || progress-lastProgress > 0.1 || progress == 1 {
lastProgress = progress
this.log(fmt.Sprintf("%.2f%%", progress*100))
}
case <-done:
return
}
}
}()
_, err = io.Copy(writer, resp.Body)
if err != nil {
_ = fp.Close()
done <- true
return "", errors.New("download failed: " + err.Error())
}
err = fp.Close()
if err != nil {
done <- true
return "", errors.New("download failed: " + err.Error())
}
time.Sleep(1 * time.Second) // waiting for progress printing
done <- true
}
return path, nil
}
// Password get generated password
func (this *MySQLInstaller) Password() string {
return this.password
}
// create my.cnf content
func (this *MySQLInstaller) createMyCnf(baseDir string, dataDir string) string {
var memoryTotalG = 1
if runtime.GOOS == "linux" {
memoryTotalG = this.sysMemoryGB() / 2
if memoryTotalG <= 0 {
memoryTotalG = 1
}
}
return `
[mysqld]
port=3306
basedir="` + baseDir + `"
datadir="` + dataDir + `"
max_connections=256
innodb_flush_log_at_trx_commit=2
max_prepared_stmt_count=65535
binlog_cache_size=1M
binlog_stmt_cache_size=1M
thread_cache_size=32
binlog_expire_logs_seconds=604800
innodb_sort_buffer_size=8M
innodb_buffer_pool_size=` + strconv.Itoa(memoryTotalG) + "G"
}
// generate random password
func (this *MySQLInstaller) generatePassword() (string, error) {
var p = make([]byte, 16)
n, err := rand.Read(p)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", p[:n]), nil
}
// print log
func (this *MySQLInstaller) log(message string) {
utils.SharedLogger.Push("[" + timeutil.Format("H:i:s") + "]" + message)
}
// copy file
func (this *MySQLInstaller) installService(baseDir string) error {
_, err := exec.LookPath("systemctl")
if err != nil {
return err
}
this.log("registering systemd service ...")
var desc = `### BEGIN INIT INFO
# Provides: mysql
# Required-Start: $local_fs $network $remote_fs
# Should-Start: ypbind nscd ldap ntpd xntpd
# Required-Stop: $local_fs $network $remote_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: start and stop MySQL
# Description: MySQL is a very fast and reliable SQL database engine.
### END INIT INFO
[Unit]
Description=MySQL Service
Before=shutdown.target
After=network-online.target
[Service]
Type=simple
Restart=on-failure
RestartSec=5s
ExecStart=${BASE_DIR}/support-files/mysql.server start
ExecStop=${BASE_DIR}/support-files/mysql.server stop
ExecRestart=${BASE_DIR}/support-files/mysql.server restart
ExecStatus=${BASE_DIR}/support-files/mysql.server status
ExecReload=${BASE_DIR}/support-files/mysql.server reload
[Install]
WantedBy=multi-user.target`
desc = strings.ReplaceAll(desc, "${BASE_DIR}", baseDir)
err = os.WriteFile("/etc/systemd/system/mysqld.service", []byte(desc), 0666)
if err != nil {
return err
}
var cmd = utils.NewTimeoutCmd(5*time.Second, "systemctl", "enable", "mysqld.service")
cmd.WithStderr()
err = cmd.Run()
if err != nil {
return errors.New("enable mysqld.service failed: " + cmd.Stderr())
}
return nil
}
// install 'tar' command automatically
func (this *MySQLInstaller) installTarCommand() error {
// yum
yumExe, err := exec.LookPath("yum")
if err == nil && len(yumExe) > 0 {
var cmd = utils.NewTimeoutCmd(10*time.Second, yumExe, "-y", "install", "tar")
return cmd.Run()
}
// apt
aptExe, err := exec.LookPath("apt")
if err == nil && len(aptExe) > 0 {
var cmd = utils.NewTimeoutCmd(10*time.Second, aptExe, "-y", "install", "tar")
return cmd.Run()
}
return nil
}
func (this *MySQLInstaller) lookupGroupAdd() (string, error) {
for _, cmd := range []string{"groupadd", "addgroup"} {
path, err := exec.LookPath(cmd)
if err == nil && len(path) > 0 {
return path, nil
}
}
return "", errors.New("not found")
}
func (this *MySQLInstaller) lookupUserAdd() (string, error) {
for _, cmd := range []string{"useradd", "adduser"} {
path, err := exec.LookPath(cmd)
if err == nil && len(path) > 0 {
return path, nil
}
}
return "", errors.New("not found")
}
func (this *MySQLInstaller) findLatestVersionFile(dir string, prefix string) string {
files, err := filepath.Glob(filepath.Clean(dir + "/" + prefix + "*"))
if err != nil {
return ""
}
var resultFile = ""
var lastVersion = ""
var reg = regexp.MustCompile(`\.([\d.]+)`)
for _, file := range files {
var filename = filepath.Base(file)
var matches = reg.FindStringSubmatch(filename)
if len(matches) > 1 {
var version = matches[1]
if len(lastVersion) == 0 || stringutil.VersionCompare(lastVersion, version) < 0 {
lastVersion = version
resultFile = file
}
}
}
return resultFile
}
func (this *MySQLInstaller) sysMemoryGB() int {
if runtime.GOOS != "linux" {
return 0
}
meminfoData, err := os.ReadFile("/proc/meminfo")
if err != nil {
return 0
}
for _, line := range bytes.Split(meminfoData, []byte{'\n'}) {
line = bytes.TrimSpace(line)
if bytes.Contains(line, []byte{':'}) {
name, value, found := bytes.Cut(line, []byte{':'})
if found {
name = bytes.TrimSpace(name)
if bytes.Equal(name, []byte("MemTotal")) {
for _, unit := range []string{"gB", "mB", "kB"} {
if bytes.Contains(value, []byte(unit)) {
value = bytes.TrimSpace(bytes.ReplaceAll(value, []byte(unit), nil))
valueInt, err := strconv.Atoi(string(value))
if err != nil {
return 0
}
switch unit {
case "gB":
return valueInt
case "mB":
return valueInt / 1024
case "kB":
return valueInt / 1024 / 1024
}
return 0
}
}
}
}
}
}
return 0
}

View File

@@ -0,0 +1,162 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils
import (
"bytes"
"context"
"os"
"os/exec"
"strings"
"time"
)
type Cmd struct {
name string
args []string
env []string
dir string
ctx context.Context
timeout time.Duration
cancelFunc func()
captureStdout bool
captureStderr bool
stdout *bytes.Buffer
stderr *bytes.Buffer
rawCmd *exec.Cmd
}
func NewCmd(name string, args ...string) *Cmd {
return &Cmd{
name: name,
args: args,
}
}
func NewTimeoutCmd(timeout time.Duration, name string, args ...string) *Cmd {
return (&Cmd{
name: name,
args: args,
}).WithTimeout(timeout)
}
func (this *Cmd) WithTimeout(timeout time.Duration) *Cmd {
this.timeout = timeout
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
this.ctx = ctx
this.cancelFunc = cancelFunc
return this
}
func (this *Cmd) WithStdout() *Cmd {
this.captureStdout = true
return this
}
func (this *Cmd) WithStderr() *Cmd {
this.captureStderr = true
return this
}
func (this *Cmd) WithEnv(env []string) *Cmd {
this.env = env
return this
}
func (this *Cmd) WithDir(dir string) *Cmd {
this.dir = dir
return this
}
func (this *Cmd) Start() error {
var cmd = this.compose()
return cmd.Start()
}
func (this *Cmd) Wait() error {
var cmd = this.compose()
return cmd.Wait()
}
func (this *Cmd) Run() error {
if this.cancelFunc != nil {
defer this.cancelFunc()
}
var cmd = this.compose()
return cmd.Run()
}
func (this *Cmd) RawStdout() string {
if this.stdout != nil {
return this.stdout.String()
}
return ""
}
func (this *Cmd) Stdout() string {
return strings.TrimSpace(this.RawStdout())
}
func (this *Cmd) RawStderr() string {
if this.stderr != nil {
return this.stderr.String()
}
return ""
}
func (this *Cmd) Stderr() string {
return strings.TrimSpace(this.RawStderr())
}
func (this *Cmd) String() string {
if this.rawCmd != nil {
return this.rawCmd.String()
}
var newCmd = exec.Command(this.name, this.args...)
return newCmd.String()
}
func (this *Cmd) Process() *os.Process {
if this.rawCmd != nil {
return this.rawCmd.Process
}
return nil
}
func (this *Cmd) compose() *exec.Cmd {
if this.rawCmd != nil {
return this.rawCmd
}
if this.ctx != nil {
this.rawCmd = exec.CommandContext(this.ctx, this.name, this.args...)
} else {
this.rawCmd = exec.Command(this.name, this.args...)
}
if this.env != nil {
this.rawCmd.Env = this.env
}
if len(this.dir) > 0 {
this.rawCmd.Dir = this.dir
}
if this.captureStdout {
this.stdout = &bytes.Buffer{}
this.rawCmd.Stdout = this.stdout
}
if this.captureStderr {
this.stderr = &bytes.Buffer{}
this.rawCmd.Stderr = this.stderr
}
return this.rawCmd
}

View File

@@ -0,0 +1,46 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils
var SharedLogger = NewLogger()
type Logger struct {
c chan string
}
func NewLogger() *Logger {
return &Logger{
c: make(chan string, 1024),
}
}
func (this *Logger) Push(msg string) {
select {
case this.c <- msg:
default:
}
}
func (this *Logger) ReadAll() (msgList []string) {
msgList = []string{}
for {
select {
case msg := <-this.c:
msgList = append(msgList, msg)
default:
return
}
}
}
func (this *Logger) Reset() {
for {
select {
case <-this.c:
default:
return
}
}
}

View File

@@ -0,0 +1,37 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils
import (
"os"
"path/filepath"
"strconv"
"strings"
)
const (
ProcDir = "/proc"
)
func FindPidWithName(name string) int {
// process name
commFiles, err := filepath.Glob(ProcDir + "/*/comm")
if err != nil {
return 0
}
for _, commFile := range commFiles {
data, err := os.ReadFile(commFile)
if err != nil {
continue
}
if strings.TrimSpace(string(data)) == name {
var pieces = strings.Split(commFile, "/")
var pid = pieces[len(pieces)-2]
pidInt, _ := strconv.Atoi(pid)
return pidInt
}
}
return 0
}

View File

@@ -0,0 +1,31 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package utils
import "io"
type ProgressWriter struct {
rawWriter io.Writer
total int64
written int64
}
func NewProgressWriter(rawWriter io.Writer, total int64) *ProgressWriter {
return &ProgressWriter{
rawWriter: rawWriter,
total: total,
}
}
func (this *ProgressWriter) Write(p []byte) (n int, err error) {
n, err = this.rawWriter.Write(p)
this.written += int64(n)
return
}
func (this *ProgressWriter) Progress() float32 {
if this.total <= 0 {
return 0
}
return float32(float64(this.written) / float64(this.total))
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types" "github.com/iwind/TeaGo/types"
"net" "net"
"regexp"
"strings" "strings"
) )
@@ -61,7 +62,24 @@ func (this *ValidateApiAction) RunPost(params struct {
} }
if net.ParseIP(params.NewHost) == nil { if net.ParseIP(params.NewHost) == nil {
this.FailField("newHost", "请输入正确的API节点主机地址") // 检查是否为域名
var domainReg = regexp.MustCompile(`^[\w-]+$`) // 这里暂不支持 unicode 域名
var digitReg = regexp.MustCompile(`^\d+$`)
var isIP = true
for _, piece := range strings.Split(params.NewHost, ".") {
if !domainReg.MatchString(piece) {
this.FailField("newHost", "请输入正确的API节点主机地址")
return
}
if !digitReg.MatchString(piece) {
isIP = false
}
}
if isIP {
this.FailField("newHost", "请输入正确的API节点主机地址")
return
}
} }
params.Must. params.Must.

Some files were not shown because too many files have changed in this diff Show More