Compare commits

...

55 Commits

Author SHA1 Message Date
刘祥超
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
121 changed files with 4402 additions and 689 deletions

View File

@@ -2,6 +2,7 @@ package main
import (
"bytes"
"flag"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/apps"
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
@@ -24,7 +25,7 @@ func main() {
var app = apps.NewAppCmd().
Version(teaconst.Version).
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]").
Option("-h", "show this help").
Option("-v", "show version").
@@ -38,7 +39,7 @@ func main() {
Option("demo", "switch to demo mode").
Option("dev", "switch to 'dev' 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() {
nodes.NewAdminNode().Daemon()
@@ -138,7 +139,12 @@ func main() {
}
})
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 ...")
var ticker = time.NewTicker(1 * time.Second)
go func() {

View File

@@ -1,7 +1,7 @@
FROM alpine:latest
LABEL maintainer="iwind.liu@gmail.com"
ENV TZ "Asia/Shanghai"
ENV VERSION 0.6.1
ENV VERSION 0.6.4
ENV ROOT_DIR /usr/local/goedge
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"

11
go.mod
View File

@@ -8,14 +8,14 @@ require (
github.com/TeaOSLab/EdgeCommon v0.0.0-00010101000000-000000000000
github.com/cespare/xxhash v1.1.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/miekg/dns v1.1.43
github.com/shirou/gopsutil/v3 v3.22.5
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tealeg/xlsx/v3 v3.2.3
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119
golang.org/x/sys v0.2.0
golang.org/x/sys v0.5.0
google.golang.org/grpc v1.45.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -26,18 +26,15 @@ require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/btree v1.0.0 // 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/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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rogpeppe/fastuuid v1.2.0 // indirect
github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/net v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/text v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20220317150908-0efb43f6373e // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect

30
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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -75,13 +74,15 @@ 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/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-20220304043459-0dd944a5b475 h1:EseyfFaQOjWanGiby9KMw7PjDBMg/95tLDgIw/ns0Cw=
github.com/iwind/TeaGo v0.0.0-20220304043459-0dd944a5b475/go.mod h1:HRHK0zoC/og3c9/hKosD9yYVMTnnzm3PgXUdhRYHaLc=
github.com/iwind/TeaGo v0.0.0-20230207032553-d6dcde0cd518 h1:zuWjQ57zc67ZSTHpxNK95JYoa9Ph/JRSnapsTY/hlhQ=
github.com/iwind/TeaGo v0.0.0-20230207032553-d6dcde0cd518/go.mod h1:fi/Pq+/5m2HZoseM+39dMF57ANXRt6w4PkGu3NXPc5s=
github.com/iwind/TeaGo v0.0.0-20230303070415-9d0689db6456 h1:xv3AVaxuwjThkBDptAfsFSmuHQIrRrvt8BRaekWnsvs=
github.com/iwind/TeaGo v0.0.0-20230303070415-9d0689db6456/go.mod h1:fi/Pq+/5m2HZoseM+39dMF57ANXRt6w4PkGu3NXPc5s=
github.com/iwind/TeaGo v0.0.0-20230304012706-c1f4a4e27470 h1:TuRxvKRv9PxKVijWOkUnZm5TeanQqWGUJyPx9u6cra4=
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/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.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.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -93,11 +94,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/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-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 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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -112,7 +109,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/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug=
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/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=
@@ -132,7 +128,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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
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/go.mod h1:0hGmAEoZ48SS1ZAE6eqZJkJVXgOMY+8a33vjXa8S8HA=
@@ -171,8 +166,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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -200,15 +195,15 @@ 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-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.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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.3/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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -263,7 +258,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.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-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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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))
}
// 重启
// RunRestart 重启
func (this *AppCmd) RunRestart() {
this.runStop()
time.Sleep(1 * time.Second)

View File

@@ -155,3 +155,15 @@ func (this *APIConfig) WriteFile(path string) error {
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
const (
Version = "0.6.2"
Version = "0.6.4.1"
APINodeVersion = "0.6.2"
APINodeVersion = "0.6.4.1"
ProductName = "Edge Admin"
ProcessName = "edge-admin"
@@ -18,5 +18,5 @@ const (
CookieSID = "edgesid"
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

@@ -12,7 +12,6 @@ import (
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/sessions"
"github.com/iwind/TeaGo/types"
"github.com/iwind/gosock/pkg/gosock"
"gopkg.in/yaml.v3"
@@ -21,6 +20,8 @@ import (
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
)
@@ -85,10 +86,15 @@ func (this *AdminNode) Run() {
this.startAPINode()
// 启动Web服务
sessionManager, err := NewSessionManager()
if err != nil {
log.Fatal("start session failed: " + err.Error())
return
}
TeaGo.NewServer(false).
AccessLog(false).
EndAll().
Session(sessions.NewFileSessionManager(86400, secret), teaconst.CookieSID).
Session(sessionManager, teaconst.CookieSID).
ReadHeaderTimeout(3 * time.Second).
ReadTimeout(1200 * time.Second).
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)
os.Exit(0)

View File

@@ -0,0 +1,96 @@
// 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"
)
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())
}
func (this *RPCClient) LoginSessionRPC() pb.LoginSessionServiceClient {
return pb.NewLoginSessionServiceClient(this.pickConn())
}
func (this *RPCClient) NodeTaskRPC() pb.NodeTaskServiceClient {
return pb.NewNodeTaskServiceClient(this.pickConn())
}

View File

@@ -87,6 +87,7 @@ func (this *CheckUpdatesTask) Loop() error {
var apiURL = teaconst.UpdatesURL
apiURL = strings.ReplaceAll(apiURL, "${os}", runtime.GOOS)
apiURL = strings.ReplaceAll(apiURL, "${arch}", runtime.GOARCH)
apiURL = strings.ReplaceAll(apiURL, "${version}", teaconst.Version)
resp, err := http.Get(apiURL)
if err != nil {
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

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

View File

@@ -3,12 +3,15 @@ package api
import (
"encoding/json"
"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/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
stringutil "github.com/iwind/TeaGo/utils/string"
timeutil "github.com/iwind/TeaGo/utils/time"
"time"
)
@@ -44,7 +47,7 @@ func (this *IndexAction) RunGet(params struct{}) {
for _, node := range nodesResp.ApiNodes {
// 状态
status := &nodeconfigs.NodeStatus{}
var status = &nodeconfigs.NodeStatus{}
if len(node.StatusJSON) > 0 {
err = json.Unmarshal(node.StatusJSON, &status)
if err != nil {
@@ -55,7 +58,7 @@ func (this *IndexAction) RunGet(params struct{}) {
}
// Rest地址
restAccessAddrs := []string{}
var restAccessAddrs = []string{}
if node.RestIsOn {
if len(node.RestHTTPJSON) > 0 {
httpConfig := &serverconfigs.HTTPProtocolConfig{}
@@ -86,6 +89,9 @@ func (this *IndexAction) RunGet(params struct{}) {
}
}
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{
"id": node.Id,
"isOn": node.IsOn,
@@ -94,14 +100,17 @@ func (this *IndexAction) RunGet(params struct{}) {
"restAccessAddrs": restAccessAddrs,
"isPrimary": node.IsPrimary,
"status": maps.Map{
"isActive": status.IsActive,
"updatedAt": status.UpdatedAt,
"hostname": status.Hostname,
"cpuUsage": status.CPUUsage,
"cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100),
"memUsage": status.MemoryUsage,
"memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100),
"buildVersion": status.BuildVersion,
"isActive": status.IsActive,
"updatedAt": status.UpdatedAt,
"hostname": status.Hostname,
"cpuUsage": status.CPUUsage,
"cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100),
"memUsage": status.MemoryUsage,
"memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100),
"buildVersion": status.BuildVersion,
"latestVersion": teaconst.APINodeVersion,
"shouldUpgrade": shouldUpgrade,
"canUpgrade": shouldUpgrade && canUpgrade,
},
})
}

View File

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

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

View File

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

View File

@@ -142,8 +142,17 @@ func (this *NodesAction) RunGet(params struct {
this.ErrorPage(err)
return
}
ipAddresses := []maps.Map{}
var ipAddresses = []maps.Map{}
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{
"id": addr.Id,
"name": addr.Name,
@@ -151,6 +160,7 @@ func (this *NodesAction) RunGet(params struct {
"canAccess": addr.CanAccess,
"isUp": addr.IsUp,
"isOn": addr.IsOn,
"clusters": addrClusterMaps,
})
}

View File

@@ -82,6 +82,7 @@ func (this *IndexAction) RunPost(params struct {
HttpAccessLogEnableResponseHeaders bool
HttpAccessLogCommonRequestHeadersOnly bool
HttpAccessLogEnableCookies bool
HttpAccessLogEnableServerNotFound bool
LogRecordServerError bool
@@ -138,6 +139,7 @@ func (this *IndexAction) RunPost(params struct {
config.HTTPAccessLog.EnableResponseHeaders = params.HttpAccessLogEnableResponseHeaders
config.HTTPAccessLog.CommonRequestHeadersOnly = params.HttpAccessLogCommonRequestHeadersOnly
config.HTTPAccessLog.EnableCookies = params.HttpAccessLogEnableCookies
config.HTTPAccessLog.EnableServerNotFound = params.HttpAccessLogEnableServerNotFound
// 日志
config.Log.RecordServerError = params.LogRecordServerError

View File

@@ -68,7 +68,7 @@ func (this *ClusterHelper) BeforeAction(actionPtr actions.ActionWrapper) (goNext
actionutils.SetTabbar(action, tabbar)
// 左侧菜单
secondMenuItem := action.Data.GetString("secondMenuItem")
var secondMenuItem = action.Data.GetString("secondMenuItem")
switch selectedTabbar {
case "setting":
var menuItems = this.createSettingMenu(cluster, clusterInfo, secondMenuItem)

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

View File

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

View File

@@ -144,8 +144,17 @@ func (this *NodesAction) RunGet(params struct {
this.ErrorPage(err)
return
}
ipAddresses := []maps.Map{}
var ipAddresses = []maps.Map{}
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{
"id": addr.Id,
"name": addr.Name,
@@ -153,6 +162,7 @@ func (this *NodesAction) RunGet(params struct {
"canAccess": addr.CanAccess,
"isUp": addr.IsUp,
"isOn": addr.IsOn,
"clusters": addrClusterMaps,
})
}
@@ -175,7 +185,7 @@ func (this *NodesAction) RunGet(params struct {
}
// DNS
dnsRouteNames := []string{}
var dnsRouteNames = []string{}
for _, route := range node.DnsRoutes {
dnsRouteNames = append(dnsRouteNames, route.Name)
}

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
import (
"encoding/json"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
@@ -14,10 +13,8 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
stringutil "github.com/iwind/TeaGo/utils/string"
"github.com/xlzd/gotp"
"time"
)
@@ -27,7 +24,8 @@ type IndexAction struct {
// 首页(登录页)
var TokenSalt = stringutil.Rand(32)
// TokenKey 加密用的密钥
var TokenKey = stringutil.Rand(32)
func (this *IndexAction) RunGet(params struct {
From string
@@ -59,7 +57,7 @@ func (this *IndexAction) RunGet(params struct {
this.Data["menu"] = "signIn"
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
uiConfig, err := configloaders.LoadAdminUIConfig()
@@ -93,9 +91,10 @@ func (this *IndexAction) RunPost(params struct {
Password string
OtpCode string
Remember bool
Must *actions.Must
Auth *helpers.UserShouldAuth
CSRF *actionutils.CSRF
Must *actions.Must
Auth *helpers.UserShouldAuth
CSRF *actionutils.CSRF
}) {
params.Must.
Field("username", params.Username).
@@ -112,7 +111,7 @@ func (this *IndexAction) RunPost(params struct {
this.Fail("请通过登录页面登录")
}
var timestampString = params.Token[32:]
if stringutil.Md5(TokenSalt+timestampString) != params.Token[:32] {
if stringutil.Md5(TokenKey+timestampString) != params.Token[:32] {
this.FailField("refresh", "登录页面已过期,请刷新后重试")
}
var timestamp = types.Int64(timestampString)
@@ -123,6 +122,7 @@ func (this *IndexAction) RunPost(params struct {
rpcClient, err := rpc.SharedRPC()
if err != nil {
this.Fail("服务器出了点小问题:" + err.Error())
return
}
resp, err := rpcClient.AdminRPC().LoginAdmin(rpcClient.Context(0), &pb.LoginAdminRequest{
Username: params.Username,
@@ -136,6 +136,7 @@ func (this *IndexAction) RunPost(params struct {
}
actionutils.Fail(this, err)
return
}
if !resp.IsOk {
@@ -145,31 +146,37 @@ func (this *IndexAction) RunPost(params struct {
}
this.Fail("请输入正确的用户名密码")
return
}
var adminId = resp.AdminId
// 检查OTP
otpLoginResp, err := this.RPC().LoginRPC().FindEnabledLogin(this.AdminContext(), &pb.FindEnabledLoginRequest{
AdminId: resp.AdminId,
Type: "otp",
})
// 检查是否支持OTP
checkOTPResp, err := this.RPC().AdminRPC().CheckAdminOTPWithUsername(this.AdminContext(), &pb.CheckAdminOTPWithUsernameRequest{Username: params.Username})
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)
var requireOTP = checkOTPResp.RequireOTP
this.Data["requireOTP"] = requireOTP
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 {
this.ErrorPage(err)
return
}
secret := loginParams.GetString("secret")
if gotp.NewDefaultTOTP(secret).Now() != params.OtpCode {
this.Fail("请输入正确的OTP动态密码")
}
this.Success()
return
}
var adminId = resp.AdminId
// 写入SESSION
params.Auth.StoreAdmin(adminId, params.Remember)
// 记录日志

View File

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

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

@@ -5,6 +5,7 @@ import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"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/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
@@ -20,8 +21,18 @@ func (this *CreatePopupAction) Init() {
}
func (this *CreatePopupAction) RunGet(params struct {
NodeId int64
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.Show()
@@ -33,6 +44,7 @@ func (this *CreatePopupAction) RunPost(params struct {
Name string
IsUp bool
ThresholdsJSON []byte
ClusterIds []int64
Must *actions.Must
}) {
@@ -57,6 +69,14 @@ func (this *CreatePopupAction) RunPost(params struct {
_ = 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{
"name": params.Name,
"canAccess": params.CanAccess,
@@ -65,6 +85,7 @@ func (this *CreatePopupAction) RunPost(params struct {
"isOn": true,
"isUp": params.IsUp,
"thresholds": thresholds,
"clusters": clusterMaps,
}
this.Success()
}

View File

@@ -11,14 +11,28 @@ import (
// UpdateNodeIPAddresses 保存一组IP地址
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)
if err != nil {
return err
}
for _, addr := range addresses {
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 {
resultAddrIds = append(resultAddrIds, addrId)
@@ -36,6 +50,7 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
CanAccess: addr.GetBool("canAccess"),
IsOn: isOn,
IsUp: addr.GetBool("isUp"),
ClusterIds: addrClusterIds,
})
if err != nil {
return err
@@ -47,12 +62,13 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
if len(result) == 1 {
// 单个创建
createResp, err := parentAction.RPC().NodeIPAddressRPC().CreateNodeIPAddress(parentAction.AdminContext(), &pb.CreateNodeIPAddressRequest{
NodeId: nodeId,
Role: role,
Name: addr.GetString("name"),
Ip: result[0],
CanAccess: addr.GetBool("canAccess"),
IsUp: addr.GetBool("isUp"),
NodeId: nodeId,
Role: role,
Name: addr.GetString("name"),
Ip: result[0],
CanAccess: addr.GetBool("canAccess"),
IsUp: addr.GetBool("isUp"),
NodeClusterIds: addrClusterIds,
})
if err != nil {
return err
@@ -62,13 +78,14 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
} else if len(result) > 1 {
// 批量创建
createResp, err := parentAction.RPC().NodeIPAddressRPC().CreateNodeIPAddresses(parentAction.AdminContext(), &pb.CreateNodeIPAddressesRequest{
NodeId: nodeId,
Role: role,
Name: addr.GetString("name"),
IpList: result,
CanAccess: addr.GetBool("canAccess"),
IsUp: addr.GetBool("isUp"),
GroupValue: ipStrings,
NodeId: nodeId,
Role: role,
Name: addr.GetString("name"),
IpList: result,
CanAccess: addr.GetBool("canAccess"),
IsUp: addr.GetBool("isUp"),
GroupValue: ipStrings,
NodeClusterIds: addrClusterIds,
})
if err != nil {
return err
@@ -140,3 +157,53 @@ func InitNodeIPAddressThresholds(parentAction *actionutils.ParentAction, addrId
}
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"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"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/rpc/pb"
"github.com/iwind/TeaGo/actions"
@@ -21,9 +22,18 @@ func (this *UpdatePopupAction) Init() {
}
func (this *UpdatePopupAction) RunGet(params struct {
NodeId int64
AddressId int64
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.Show()
@@ -37,6 +47,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
IsOn bool
IsUp bool
ThresholdsJSON []byte
ClusterIds []int64
Must *actions.Must
}) {
@@ -81,6 +92,14 @@ func (this *UpdatePopupAction) RunPost(params struct {
_ = 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{
"name": params.Name,
"ip": params.IP,
@@ -89,6 +108,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
"isOn": params.IsOn,
"isUp": isUp,
"thresholds": thresholds,
"clusters": clusterMaps,
}
this.Success()

View File

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

View File

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

View File

@@ -255,21 +255,7 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
"isOn": serverConfig.ReverseProxyRef != nil && serverConfig.ReverseProxyRef.IsOn,
})
if teaconst.IsPlus {
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 = filterMenuItems(serverConfig, menuItems, serverIdString, secondMenuItem)
menuItems = append(menuItems, maps.Map{
"name": "-",
@@ -406,7 +392,7 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
"isOn": serverConfig.Web != nil && serverConfig.Web.RequestLimit != nil && serverConfig.Web.RequestLimit.IsOn,
})
menuItems = filterMenuItems(serverConfig, menuItems, serverIdString, secondMenuItem)
menuItems = filterMenuItems2(serverConfig, menuItems, serverIdString, secondMenuItem)
menuItems = append(menuItems, maps.Map{
"name": "-",

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
package setup
import "github.com/iwind/TeaGo"
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
@@ -15,6 +18,8 @@ func init() {
Post("/status", new(StatusAction)).
Post("/detectDB", new(DetectDBAction)).
Post("/checkLocalIP", new(CheckLocalIPAction)).
GetPost("/mysql/installPopup", new(mysql.InstallPopupAction)).
Post("/mysql/installLogs", new(mysql.InstallLogsAction)).
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,612 @@
// 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"
timeutil "github.com/iwind/TeaGo/utils/time"
"io"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"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 '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 {
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
`
}
// 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=always
RestartSec=1s
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")
}

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/types"
"net"
"regexp"
"strings"
)
@@ -61,7 +62,24 @@ func (this *ValidateApiAction) RunPost(params struct {
}
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.

View File

@@ -68,6 +68,7 @@ func (this *UpdateAction) RunGet(params struct {
"mobile": user.Mobile,
"isOn": user.IsOn,
"countAccessKeys": countAccessKeys,
"bandwidthAlgo": user.BandwidthAlgo,
// 实名认证
"hasNewIndividualIdentity": hasNewIndividualIdentity,
@@ -87,17 +88,18 @@ func (this *UpdateAction) RunGet(params struct {
}
func (this *UpdateAction) RunPost(params struct {
UserId int64
Username string
Pass1 string
Pass2 string
Fullname string
Mobile string
Tel string
Email string
Remark string
IsOn bool
ClusterId int64
UserId int64
Username string
Pass1 string
Pass2 string
Fullname string
Mobile string
Tel string
Email string
Remark string
IsOn bool
ClusterId int64
BandwidthAlgo string
// OTP
OtpOn bool
@@ -159,6 +161,7 @@ func (this *UpdateAction) RunPost(params struct {
Remark: params.Remark,
IsOn: params.IsOn,
NodeClusterId: params.ClusterId,
BandwidthAlgo: params.BandwidthAlgo,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -109,6 +109,7 @@ func (this *UserAction) RunGet(params struct {
"isVerified": user.IsVerified,
"registeredIP": user.RegisteredIP,
"registeredRegion": registeredRegion,
"bandwidthAlgo": user.BandwidthAlgo,
// 实名认证
"hasNewIndividualIdentity": hasNewIndividualIdentity,

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
Vue.component("js-page", {
props: ["v-max"],
data: function () {
let max = this.vMax
if (max == null) {
max = 0
}
return {
max: max,
page: 1
}
},
methods: {
updateMax: function (max) {
this.max = max
},
selectPage: function(page) {
this.page = page
this.$emit("change", page)
}
},
template:`<div>
<div class="page" v-if="max > 1">
<a href="" v-for="i in max" :class="{active: i == page}" @click.prevent="selectPage(i)">{{i}}</a>
</div>
</div>`
})

View File

@@ -0,0 +1,108 @@
Vue.component("url-patterns-box", {
props: ["value"],
data: function () {
let patterns = []
if (this.value != null) {
patterns = this.value
}
return {
patterns: patterns,
isAdding: false,
addingPattern: {"type": "wildcard", "pattern": ""},
editingIndex: -1
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.patternInput.focus()
})
},
edit: function (index) {
this.isAdding = true
this.editingIndex = index
this.addingPattern = {
type: this.patterns[index].type,
pattern: this.patterns[index].pattern
}
},
confirm: function () {
let pattern = this.addingPattern.pattern.trim()
if (pattern.length == 0) {
let that = this
teaweb.warn("请输入URL", function () {
that.$refs.patternInput.focus()
})
return
}
if (this.editingIndex < 0) {
this.patterns.push({
type: this.addingPattern.type,
pattern: this.addingPattern.pattern
})
} else {
this.patterns[this.editingIndex].type = this.addingPattern.type
this.patterns[this.editingIndex].pattern = this.addingPattern.pattern
}
this.notifyChange()
this.cancel()
},
remove: function (index) {
this.patterns.$remove(index)
this.cancel()
this.notifyChange()
},
cancel: function () {
this.isAdding = false
this.addingPattern = {"type": "wildcard", "pattern": ""}
this.editingIndex = -1
},
patternTypeName: function (patternType) {
switch (patternType) {
case "wildcard":
return "通配符"
case "regexp":
return "正则"
}
return ""
},
notifyChange: function () {
this.$emit("input", this.patterns)
}
},
template: `<div>
<div v-show="patterns.length > 0">
<div v-for="(pattern, index) in patterns" class="ui label basic small" :class="{blue: index == editingIndex, disabled: isAdding && index != editingIndex}" style="margin-bottom: 0.8em">
<span class="grey" style="font-weight: normal">[{{patternTypeName(pattern.type)}}]</span> <span >{{pattern.pattern}}</span> &nbsp;
<a href="" title="修改" @click.prevent="edit(index)"><i class="icon pencil tiny"></i></a>
<a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
</div>
</div>
<div v-show="isAdding" style="margin-top: 0.5em">
<div class="ui fields inline">
<div class="ui field">
<select class="ui dropdown auto-width" v-model="addingPattern.type">
<option value="wildcard">通配符</option>
<option value="regexp">正则表达式</option>
</select>
</div>
<div class="ui field">
<input type="text" :placeholder="(addingPattern.type == 'wildcard') ? '可以使用星号(*)通配符,不区分大小写' : '可以使用正则表达式,不区分大小写'" v-model="addingPattern.pattern" size="36" ref="patternInput" @keyup.enter="confirm()" @keypress.enter.prevent="1" spellcheck="false"/>
</div>
<div class="ui field" style="padding-left: 0">
<tip-icon content="通配符示例:<br/>单个路径开头:/hello/world/*<br/>单个路径结尾:*/hello/world<br/>包含某个路径:*/article/*<br/>某个域名下的所有URL*example.com/*" v-if="addingPattern.type == 'wildcard'"></tip-icon>
<tip-icon content="正则表达式示例:<br/>单个路径开头:^/hello/world<br/>单个路径结尾:/hello/world$<br/>包含某个路径:/article/<br/>匹配某个数字路径:/article/(\\d+)<br/>某个域名下的所有URL^(http|https)://example.com/" v-if="addingPattern.type == 'regexp'"></tip-icon>
</div>
<div class="ui field">
<button class="ui button tiny" type="button" @click.prevent="confirm">确定</button><a href="" title="取消" @click.prevent="cancel"><i class="icon remove small"></i></a>
</div>
</div>
</div>
<div v-if=!isAdding style="margin-top: 0.5em">
<button class="ui button tiny basic" type="button" @click.prevent="add">+</button>
</div>
</div>`
})

View File

@@ -1,5 +1,5 @@
Vue.component("values-box", {
props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty"],
props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"],
data: function () {
let values = this.values;
if (values == null) {
@@ -44,6 +44,22 @@ Vue.component("values-box", {
}
}
// validate
if (typeof(this.validator) == "function") {
let resp = this.validator.call(this, this.value)
if (typeof resp == "object") {
if (typeof resp.isOk == "boolean" && !resp.isOk) {
if (typeof resp.message == "string") {
let that = this
teaweb.warn(resp.message, function () {
that.$refs.value.focus();
})
}
return
}
}
}
if (this.isUpdating) {
Vue.set(this.realValues, this.index, this.value);
} else {

View File

@@ -0,0 +1,53 @@
Vue.component("node-ip-address-clusters-selector", {
props: ["vClusters"],
mounted: function () {
this.checkClusters()
},
data: function () {
let clusters = this.vClusters
if (clusters == null) {
clusters = []
}
return {
clusters: clusters,
hasCheckedCluster: false,
clustersVisible: false
}
},
methods: {
checkClusters: function () {
let that = this
let b = false
this.clusters.forEach(function (cluster) {
if (cluster.isChecked) {
b = true
}
})
this.hasCheckedCluster = b
return b
},
changeCluster: function (cluster) {
cluster.isChecked = !cluster.isChecked
this.checkClusters()
},
showClusters: function () {
this.clustersVisible = !this.clustersVisible
}
},
template: `<div>
<span v-if="!hasCheckedCluster">默认用于所有集群 &nbsp; <a href="" @click.prevent="showClusters">修改 <i class="icon angle" :class="{down: !clustersVisible, up:clustersVisible}"></i></a></span>
<div v-if="hasCheckedCluster">
<span v-for="cluster in clusters" class="ui label basic small" v-if="cluster.isChecked">{{cluster.name}}</span> &nbsp; <a href="" @click.prevent="showClusters">修改 <i class="icon angle" :class="{down: !clustersVisible, up:clustersVisible}"></i></a>
<p class="comment">当前IP仅在所选集群中有效。</p>
</div>
<div v-show="clustersVisible">
<div class="ui divider"></div>
<checkbox v-for="cluster in clusters" :v-value="cluster.id" :value="cluster.isChecked ? cluster.id : 0" style="margin-right: 1em" @input="changeCluster(cluster)" name="clusterIds">
{{cluster.name}}
</checkbox>
</div>
</div>`
})

View File

@@ -1,10 +1,16 @@
// 节点IP地址管理标签形式
Vue.component("node-ip-addresses-box", {
props: ["v-ip-addresses", "role"],
props: ["v-ip-addresses", "role", "v-node-id"],
data: function () {
let nodeId = this.vNodeId
if (nodeId == null) {
nodeId = 0
}
return {
ipAddresses: (this.vIpAddresses == null) ? [] : this.vIpAddresses,
supportThresholds: this.role != "ns"
supportThresholds: this.role != "ns",
nodeId: nodeId
}
},
methods: {
@@ -13,7 +19,7 @@ Vue.component("node-ip-addresses-box", {
window.UPDATING_NODE_IP_ADDRESS = null
let that = this;
teaweb.popup("/nodes/ipAddresses/createPopup?supportThresholds=" + (this.supportThresholds ? 1 : 0), {
teaweb.popup("/nodes/ipAddresses/createPopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), {
callback: function (resp) {
that.ipAddresses.push(resp.data.ipAddress);
},
@@ -24,10 +30,10 @@ Vue.component("node-ip-addresses-box", {
// 修改地址
updateIPAddress: function (index, address) {
window.UPDATING_NODE_IP_ADDRESS = address
window.UPDATING_NODE_IP_ADDRESS = teaweb.clone(address)
let that = this;
teaweb.popup("/nodes/ipAddresses/updatePopup?supportThresholds=" + (this.supportThresholds ? 1 : 0), {
teaweb.popup("/nodes/ipAddresses/updatePopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), {
callback: function (resp) {
Vue.set(that.ipAddresses, index, resp.data.ipAddress);
},
@@ -58,6 +64,11 @@ Vue.component("node-ip-addresses-box", {
<span class="small red" v-if="!address.isUp" title="已下线">[down]</span>
<span class="small" v-if="address.thresholds != null && address.thresholds.length > 0">[{{address.thresholds.length}}个阈值]</span>
&nbsp;
<span v-if="address.clusters != null && address.clusters.length > 0">
&nbsp; <span class="small grey">专属集群:[</span><span v-for="(cluster, index) in address.clusters" class="small grey">{{cluster.name}}<span v-if="index < address.clusters.length - 1"></span></span><span class="small grey">]</span>
&nbsp;
</span>
<a href="" title="修改" @click.prevent="updateIPAddress(index, address)"><i class="icon pencil small"></i></a>
<a href="" title="删除" @click.prevent="removeIPAddress(index)"><i class="icon remove"></i></a>
</div>

View File

@@ -48,7 +48,7 @@ Vue.component("firewall-syn-flood-config-box", {
<input type="hidden" name="synFloodJSON" :value="JSON.stringify(config)"/>
<a href="" @click.prevent="edit">
<span v-if="config.isOn">
已启用 / <span>空连接次数:{{config.minAttempts}}次/分钟</span> / 封禁时{{config.timeoutSeconds}}秒 <span v-if="config.ignoreLocal">/ 忽略局域网访问</span>
已启用 / <span>空连接次数:{{config.minAttempts}}次/分钟</span> / 封禁时{{config.timeoutSeconds}}秒 <span v-if="config.ignoreLocal">/ 忽略局域网访问</span>
</span>
<span v-else>未启用</span>
<i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i>
@@ -73,7 +73,7 @@ Vue.component("firewall-syn-flood-config-box", {
</td>
</tr>
<tr>
<td>封禁时</td>
<td>封禁时</td>
<td>
<div class="ui input right labeled">
<input type="text" v-model="timeoutSeconds" style="width: 5em" maxlength="8"/>

View File

@@ -16,7 +16,7 @@ Vue.component("firewall-syn-flood-config-viewer", {
},
template: `<div>
<span v-if="config.isOn">
已启用 / <span>空连接次数:{{config.minAttempts}}次/分钟</span> / 封禁时{{config.timeoutSeconds}}秒 <span v-if="config.ignoreLocal">/ 忽略局域网访问</span>
已启用 / <span>空连接次数:{{config.minAttempts}}次/分钟</span> / 封禁时{{config.timeoutSeconds}}秒 <span v-if="config.ignoreLocal">/ 忽略局域网访问</span>
</span>
<span v-else>未启用</span>
</div>`

View File

@@ -56,18 +56,22 @@ Vue.component("http-cache-config-box", {
},
template: `<div>
<input type="hidden" name="cacheJSON" :value="JSON.stringify(cacheConfig)"/>
<table class="ui table definition selectable" v-show="!vIsGroup">
<tr>
<td class="title">全局缓存策略</td>
<td>
<div v-if="vCachePolicy != null">{{vCachePolicy.name}} <link-icon :href="'/servers/components/cache/policy?cachePolicyId=' + vCachePolicy.id"></link-icon>
<p class="comment">使用当前服务所在集群的设置。</p>
</div>
<span v-else class="red">当前集群没有设置缓存策略,当前配置无法生效。</span>
</td>
</tr>
</table>
<table class="ui table definition selectable">
<prior-checkbox :v-config="cacheConfig" v-if="vIsLocation || vIsGroup"></prior-checkbox>
<tbody v-show="(!vIsLocation && !vIsGroup) || cacheConfig.isPrior">
<tr v-show="!vIsGroup">
<td>缓存策略</td>
<td>
<div v-if="vCachePolicy != null">{{vCachePolicy.name}} <link-icon :href="'/servers/components/cache/policy?cachePolicyId=' + vCachePolicy.id"></link-icon>
<p class="comment">使用当前服务所在集群的设置。</p>
</div>
<span v-else class="red">当前集群没有设置缓存策略,当前配置无法生效。</span>
</td>
</tr>
<tr>
<td class="title">启用缓存</td>
<td>
@@ -90,7 +94,7 @@ Vue.component("http-cache-config-box", {
<td>使用默认缓存条件</td>
<td>
<checkbox v-model="enablePolicyRefs"></checkbox>
<p class="comment">选中后使用系统中已经定义的默认缓存条件。</p>
<p class="comment">选中后使用系统全局缓存策略中已经定义的默认缓存条件。</p>
</td>
</tr>
<tr>

View File

@@ -190,7 +190,10 @@ Vue.component("http-cache-ref-box", {
<tr v-if="condCategory == 'simple'">
<td class="color-border">{{condComponent.paramsTitle}} *</td>
<td>
<component :is="condComponent.component" :v-cond="ref.simpleCond"></component>
<component :is="condComponent.component" :v-cond="ref.simpleCond" v-if="condComponent.type != 'params'"></component>
<table class="ui table" v-if="condComponent.type == 'params'">
<component :is="condComponent.component" :v-cond="ref.simpleCond"></component>
</table>
</td>
</tr>
<tr v-if="condCategory == 'simple' && condComponent.caseInsensitive">

View File

@@ -75,7 +75,7 @@ Vue.component("http-cache-refs-config-box", {
})
},
updateRef: function (index, cacheRef) {
window.UPDATING_CACHE_REF = cacheRef
window.UPDATING_CACHE_REF = teaweb.clone(cacheRef)
let height = window.innerHeight
if (height > 500) {

View File

@@ -4,7 +4,7 @@ Vue.component("http-cond-url-extension", {
data: function () {
let cond = {
isRequest: true,
param: "${requestPathExtension}",
param: "${requestPathLowerExtension}",
operator: "in",
value: "[]"
}
@@ -95,7 +95,7 @@ Vue.component("http-cond-url-not-extension", {
data: function () {
let cond = {
isRequest: true,
param: "${requestPathExtension}",
param: "${requestPathLowerExtension}",
operator: "not in",
value: "[]"
}
@@ -718,21 +718,19 @@ Vue.component("http-cond-params", {
},
template: `<tbody>
<tr>
<td>参数值</td>
<td style="width: 8em">参数值</td>
<td>
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
<div>
<div class="ui fields inline">
<div class="ui field">
<input type="text" placeholder="\${xxx}" v-model="cond.param"/>
</div>
<div class="ui field">
<select class="ui dropdown" style="width: 7em; color: grey" v-model="variable" @change="changeVariable">
<option value="">[常用参数]</option>
<option v-for="v in variables" :value="v.code">{{v.code}} - {{v.name}}</option>
</select>
</div>
</div>
<div class="ui field">
<input type="text" placeholder="\${xxx}" v-model="cond.param"/>
</div>
<div class="ui field">
<select class="ui dropdown" style="width: 16em; color: grey" v-model="variable" @change="changeVariable">
<option value="">[常用参数]</option>
<option v-for="v in variables" :value="v.code">{{v.code}} - {{v.name}}</option>
</select>
</div>
</div>
<p class="comment">其中可以使用变量,类似于<code-label>\${requestPath}</code-label>,也可以是多个变量的组合。</p>
</td>
@@ -835,5 +833,6 @@ Vue.component("http-cond-params", {
<p class="comment">选中后表示对比时忽略参数值的大小写。</p>
</td>
</tr>
</tbody>`
</tbody>
`
})

View File

@@ -73,6 +73,7 @@ Vue.component("http-firewall-actions-box", {
// 动作参数
blockTimeout: "",
blockTimeoutMax: "",
blockScope: "global",
captchaLife: "",
@@ -122,6 +123,14 @@ Vue.component("http-firewall-actions-box", {
this.actionOptions["timeout"] = v
}
},
blockTimeoutMax: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["timeoutMax"] = 0
} else {
this.actionOptions["timeoutMax"] = v
}
},
blockScope: function (v) {
this.actionOptions["scope"] = v
},
@@ -237,6 +246,7 @@ Vue.component("http-firewall-actions-box", {
// 动作参数
this.blockTimeout = ""
this.blockTimeoutMax = ""
this.blockScope = "global"
this.captchaLife = ""
@@ -298,9 +308,13 @@ Vue.component("http-firewall-actions-box", {
switch (config.code) {
case "block":
this.blockTimeout = ""
this.blockTimeoutMax = ""
if (config.options.timeout != null || config.options.timeout > 0) {
this.blockTimeout = config.options.timeout.toString()
}
if (config.options.timeoutMax != null || config.options.timeoutMax > 0) {
this.blockTimeoutMax = config.options.timeoutMax.toString()
}
if (config.options.scope != null && config.options.scope.length > 0) {
this.blockScope = config.options.scope
} else {
@@ -584,7 +598,7 @@ Vue.component("http-firewall-actions-box", {
{{config.name}} <span class="small">({{config.code.toUpperCase()}})</span>
<!-- block -->
<span v-if="config.code == 'block' && config.options.timeout > 0">有效期{{config.options.timeout}}秒</span>
<span v-if="config.code == 'block' && config.options.timeout > 0">封禁时长{{config.options.timeout}}<span v-if="config.options.timeoutMax > config.options.timeout">-{{config.options.timeoutMax}}</span>秒</span>
<!-- captcha -->
<span v-if="config.code == 'captcha' && config.options.life > 0">:有效期{{config.options.life}}秒
@@ -643,7 +657,18 @@ Vue.component("http-firewall-actions-box", {
<!-- block -->
<tr v-if="actionCode == 'block'">
<td>封锁时间</td>
<td>封禁范围</td>
<td>
<select class="ui dropdown auto-width" v-model="blockScope">
<option value="service">当前服务</option>
<option value="global">所有服务</option>
</select>
<p class="comment" v-if="blockScope == 'service'">只封禁用户对当前网站服务的访问,其他服务不受影响。</p>
<p class="comment" v-if="blockScope =='global'">封禁用户对所有网站服务的访问。</p>
</td>
</tr>
<tr v-if="actionCode == 'block'">
<td>封禁时长</td>
<td>
<div class="ui input right labeled">
<input type="text" style="width: 5em" maxlength="9" v-model="blockTimeout" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
@@ -652,14 +677,13 @@ Vue.component("http-firewall-actions-box", {
</td>
</tr>
<tr v-if="actionCode == 'block'">
<td>封锁范围</td>
<td>最大封禁时长</td>
<td>
<select class="ui dropdown auto-width" v-model="blockScope">
<option value="service">当前服务</option>
<option value="global">所有服务</option>
</select>
<p class="comment" v-if="blockScope == 'service'">只封锁用户对当前网站服务的访问,其他服务不受影响。</p>
<p class="comment" v-if="blockScope =='global'">封锁用户对所有网站服务的访问。</p>
<div class="ui input right labeled">
<input type="text" style="width: 5em" maxlength="9" v-model="blockTimeoutMax" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
<span class="ui label">秒</span>
</div>
<p class="comment">选填项。如果同时填写了封禁时长和最大封禁时长,则会在两者之间随机选择一个数字作为最终的封禁时长。</p>
</td>
</tr>

View File

@@ -8,7 +8,7 @@ Vue.component("http-firewall-block-options-viewer", {
template: `<div>
<span v-if="options == null">默认设置</span>
<div v-else>
状态码:{{options.statusCode}} / 提示内容:<span v-if="options.body != null && options.body.length > 0">[{{options.body.length}}字符]</span><span v-else class="disabled">[无]</span> / 超时时间:{{options.timeout}}秒
状态码:{{options.statusCode}} / 提示内容:<span v-if="options.body != null && options.body.length > 0">[{{options.body.length}}字符]</span><span v-else class="disabled">[无]</span> / 超时时间:{{options.timeout}}秒 <span v-if="options.timeoutMax > options.timeout">/ 最大封禁时长:{{options.timeoutMax}}秒</span>
</div>
</div>
`

View File

@@ -5,6 +5,7 @@ Vue.component("http-firewall-block-options", {
blockOptions: this.vBlockOptions,
statusCode: this.vBlockOptions.statusCode,
timeout: this.vBlockOptions.timeout,
timeoutMax: this.vBlockOptions.timeoutMax,
isEditing: false
}
},
@@ -24,6 +25,14 @@ Vue.component("http-firewall-block-options", {
} else {
this.blockOptions.timeout = timeout
}
},
timeoutMax: function (v) {
let timeoutMax = parseInt(v)
if (isNaN(timeoutMax)) {
this.blockOptions.timeoutMax = 0
} else {
this.blockOptions.timeoutMax = timeoutMax
}
}
},
methods: {
@@ -33,7 +42,9 @@ Vue.component("http-firewall-block-options", {
},
template: `<div>
<input type="hidden" name="blockOptionsJSON" :value="JSON.stringify(blockOptions)"/>
<a href="" @click.prevent="edit">状态码:{{statusCode}} / 提示内容:<span v-if="blockOptions.body != null && blockOptions.body.length > 0">[{{blockOptions.body.length}}字符]</span><span v-else class="disabled">[无]</span> / 超时时间:{{timeout}}秒 <i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i></a>
<a href="" @click.prevent="edit">状态码:{{statusCode}} / 提示内容:<span v-if="blockOptions.body != null && blockOptions.body.length > 0">[{{blockOptions.body.length}}字符]</span><span v-else class="disabled">[无]</span> <span v-if="timeout > 0"> / 封禁时长:{{timeout}}秒</span>
<span v-if="timeoutMax > timeout"> / 最大封禁时长:{{timeoutMax}}秒</span>
<i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i></a>
<table class="ui table" v-show="isEditing">
<tr>
<td class="title">状态码</td>
@@ -48,13 +59,23 @@ Vue.component("http-firewall-block-options", {
</td>
</tr>
<tr>
<td>超时时间</td>
<td>封禁时长</td>
<td>
<div class="ui input right labeled">
<input type="text" v-model="timeout" style="width: 5em" maxlength="6"/>
<span class="ui label">秒</span>
</div>
<p class="comment">触发阻止动作时,封客户端IP的时间。</p>
<p class="comment">触发阻止动作时,封客户端IP的时间。</p>
</td>
</tr>
<tr>
<td>最大封禁时长</td>
<td>
<div class="ui input right labeled">
<input type="text" v-model="timeoutMax" style="width: 5em" maxlength="6"/>
<span class="ui label">秒</span>
</div>
<p class="comment">如果最大封禁时长大于封禁时长({{timeout}}秒),那么表示每次封禁的时候,将会在这两个时长数字之间随机选取一个数字作为最终的封禁时长。</p>
</td>
</tr>
</table>

View File

@@ -6,36 +6,60 @@ Vue.component("http-firewall-config-box", {
firewall = {
isPrior: false,
isOn: false,
firewallPolicyId: 0
firewallPolicyId: 0,
ignoreGlobalRules: false
}
}
return {
firewall: firewall
firewall: firewall,
moreOptionsVisible: false,
execGlobalRules: !firewall.ignoreGlobalRules
}
},
watch: {
execGlobalRules: function (v) {
this.firewall.ignoreGlobalRules = !v
}
},
methods: {
changeOptionsVisible: function (v) {
this.moreOptionsVisible = v
}
},
template: `<div>
<input type="hidden" name="firewallJSON" :value="JSON.stringify(firewall)"/>
<table class="ui table selectable definition" v-show="!vIsGroup">
<tr>
<td class="title">全局WAF策略</td>
<td>
<div v-if="vFirewallPolicy != null">{{vFirewallPolicy.name}} <span v-if="vFirewallPolicy.modeInfo != null">&nbsp; <span :class="{green: vFirewallPolicy.modeInfo.code == 'defend', blue: vFirewallPolicy.modeInfo.code == 'observe', grey: vFirewallPolicy.modeInfo.code == 'bypass'}">[{{vFirewallPolicy.modeInfo.name}}]</span>&nbsp;</span> <link-icon :href="'/servers/components/waf/policy?firewallPolicyId=' + vFirewallPolicy.id"></link-icon>
<p class="comment">当前服务所在集群的设置。</p>
</div>
<span v-else class="red">当前集群没有设置WAF策略当前配置无法生效。</span>
</td>
</tr>
</table>
<table class="ui table selectable definition">
<prior-checkbox :v-config="firewall" v-if="vIsLocation || vIsGroup"></prior-checkbox>
<tbody v-show="(!vIsLocation && !vIsGroup) || firewall.isPrior">
<tr v-show="!vIsGroup">
<td>WAF策略</td>
<td>
<div v-if="vFirewallPolicy != null">{{vFirewallPolicy.name}} <span v-if="vFirewallPolicy.modeInfo != null">&nbsp; <span :class="{green: vFirewallPolicy.modeInfo.code == 'defend', blue: vFirewallPolicy.modeInfo.code == 'observe', grey: vFirewallPolicy.modeInfo.code == 'bypass'}">[{{vFirewallPolicy.modeInfo.name}}]</span>&nbsp;</span> <link-icon :href="'/servers/components/waf/policy?firewallPolicyId=' + vFirewallPolicy.id"></link-icon>
<p class="comment">使用当前服务所在集群的设置。</p>
</div>
<span v-else class="red">当前集群没有设置WAF策略当前配置无法生效。</span>
</td>
</tr>
<tr>
<td class="title">启用WAF</td>
<td>
<div class="ui checkbox">
<input type="checkbox" v-model="firewall.isOn"/>
<label></label>
</div>
<p class="comment">启用WAF之后各项WAF设置才会生效。</p>
<checkbox v-model="firewall.isOn"></checkbox>
<p class="comment">选中后表示启用当前网站服务的WAF功能。</p>
</td>
</tr>
</tbody>
<more-options-tbody @change="changeOptionsVisible"></more-options-tbody>
<tbody v-show="moreOptionsVisible">
<tr>
<td>启用系统全局规则</td>
<td>
<checkbox v-model="execGlobalRules"></checkbox>
<p class="comment">选中后表示使用系统全局WAF策略中定义的规则。</p>
</td>
</tr>
</tbody>

View File

@@ -21,6 +21,9 @@ Vue.component("http-firewall-rule-label", {
}
return operatorName
},
isEmptyString: function (v) {
return typeof v == "string" && v.length == 0
}
},
template: `<div>
@@ -40,8 +43,9 @@ Vue.component("http-firewall-rule-label", {
<span v-else>
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span>
<span :class="{dash:rule.isCaseInsensitive}" :title="rule.isCaseInsensitive ? '大小写不敏感':''" v-if="!rule.isComposed">{{operatorName(rule.operator)}}</span>
{{rule.value}}
<span :class="{dash:!rule.isComposed && rule.isCaseInsensitive}" :title="(!rule.isComposed && rule.isCaseInsensitive) ? '大小写不敏感':''">{{operatorName(rule.operator)}}</span>
<span v-if="!isEmptyString(rule.value)">{{rule.value}}</span>
<span v-else class="disabled" style="font-weight: normal" title="空字符串">[空]</span>
</span>
<!-- description -->

View File

@@ -47,6 +47,9 @@ Vue.component("http-firewall-rules-box", {
}
return operatorName
},
isEmptyString: function (v) {
return typeof v == "string" && v.length == 0
}
},
template: `<div>
@@ -67,7 +70,9 @@ Vue.component("http-firewall-rules-box", {
</span>
<span v-else>
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span> <span :class="{dash:rule.isCaseInsensitive}" :title="rule.isCaseInsensitive ? '大小写不敏感':''">{{operatorName(rule.operator)}}</span> {{rule.value}}
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span> <span :class="{dash:(!rule.isComposed && rule.isCaseInsensitive)}" :title="(!rule.isComposed && rule.isCaseInsensitive) ? '大小写不敏感':''">{{operatorName(rule.operator)}}</span>
<span v-if="!isEmptyString(rule.value)">{{rule.value}}</span>
<span v-else class="disabled" style="font-weight: normal" title="空字符串">[空]</span>
</span>
<!-- description -->

View File

@@ -93,6 +93,7 @@ Vue.component("http-firewall-checkpoint-cc", {
let period = 60
let threshold = 1000
let ignoreCommonFiles = false
let enableFingerprint = true
let options = {}
if (window.parent.UPDATING_RULE != null) {
@@ -117,6 +118,9 @@ Vue.component("http-firewall-checkpoint-cc", {
if (options.ignoreCommonFiles != null && typeof (options.ignoreCommonFiles) == "boolean") {
ignoreCommonFiles = options.ignoreCommonFiles
}
if (options.enableFingerprint != null && typeof (options.enableFingerprint) == "boolean") {
enableFingerprint = options.enableFingerprint
}
let that = this
setTimeout(function () {
@@ -128,6 +132,7 @@ Vue.component("http-firewall-checkpoint-cc", {
period: period,
threshold: threshold,
ignoreCommonFiles: ignoreCommonFiles,
enableFingerprint: enableFingerprint,
options: {},
value: threshold
}
@@ -141,6 +146,9 @@ Vue.component("http-firewall-checkpoint-cc", {
},
ignoreCommonFiles: function () {
this.change()
},
enableFingerprint: function () {
this.change()
}
},
methods: {
@@ -165,6 +173,11 @@ Vue.component("http-firewall-checkpoint-cc", {
ignoreCommonFiles = false
}
let enableFingerprint = this.enableFingerprint
if (typeof enableFingerprint != "boolean") {
enableFingerprint = true
}
this.vCheckpoint.options = [
{
code: "keys",
@@ -181,6 +194,10 @@ Vue.component("http-firewall-checkpoint-cc", {
{
code: "ignoreCommonFiles",
value: ignoreCommonFiles
},
{
code: "enableFingerprint",
value: enableFingerprint
}
]
},
@@ -218,6 +235,13 @@ Vue.component("http-firewall-checkpoint-cc", {
<p class="comment" v-if="thresholdTooLow()"><span class="red">对于网站类应用来说,当前阈值设置的太低,有可能会影响用户正常访问。</span></p>
</td>
</tr>
<tr>
<td>检查请求来源指纹</td>
<td>
<checkbox v-model="enableFingerprint"></checkbox>
<p class="comment">在接收到HTTPS请求时尝试检查请求来源的指纹用来检测代理服务和爬虫攻击。</p>
</td>
</tr>
<tr>
<td>忽略常见文件</td>
<td>

View File

@@ -43,6 +43,12 @@ Vue.component("http-location-labels", {
<!-- 反向代理 -->
<http-location-labels-label v-if="refIsOn(location.reverseProxyRef, location.reverseProxy)" :v-href="url('/reverseProxy')">源站</http-location-labels-label>
<!-- UAM -->
<http-location-labels-label v-if="location.web != null && location.web.uam != null && location.web.uam.isPrior"><span :class="{disabled: !location.web.uam.isOn, red:location.web.uam.isOn}">5秒盾</span></http-location-labels-label>
<!-- CC -->
<http-location-labels-label v-if="location.web != null && location.web.cc != null && location.web.cc.isPrior"><span :class="{disabled: !location.web.cc.isOn, red:location.web.cc.isOn}">CC防护</span></http-location-labels-label>
<!-- WAF -->
<!-- TODO -->

View File

@@ -27,8 +27,16 @@ Vue.component("http-referers-config-box", {
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
},
changeAllowDomains: function (domains) {
if (typeof (domains) == "object") {
this.config.allowDomains = domains
this.$forceUpdate()
}
},
changeDenyDomains: function (domains) {
if (typeof (domains) == "object") {
this.config.denyDomains = domains
this.$forceUpdate()
}
}
},
template: `<div>

View File

@@ -1,32 +0,0 @@
// UAM模式配置
Vue.component("uam-config-box", {
props: ["v-uam-config", "v-is-location", "v-is-group"],
data: function () {
let config = this.vUamConfig
if (config == null) {
config = {
isPrior: false,
isOn: false
}
}
return {
config: config
}
},
template: `<div>
<input type="hidden" name="uamJSON" :value="JSON.stringify(config)"/>
<table class="ui table definition selectable">
<prior-checkbox :v-config="config" v-if="vIsLocation || vIsGroup"></prior-checkbox>
<tbody v-show="((!vIsLocation && !vIsGroup) || config.isPrior)">
<tr>
<td class="title">启用5秒盾</td>
<td>
<checkbox v-model="config.isOn"></checkbox>
<p class="comment"><plus-label></plus-label>启用后,访问网站时,自动检查浏览器环境,阻止非正常访问。</p>
</td>
</tr>
</tbody>
</table>
<div class="margin"></div>
</div>`
})

View File

@@ -334,7 +334,7 @@ window.teaweb = {
popupTip: function (html) {
Swal.fire({
html: '<div style="line-height: 1.7;text-align: left "><i class="icon question circle"></i>' + html + "</div>",
width: "30em",
width: "34em",
padding: "4em",
showConfirmButton: false,
showCloseButton: true,
@@ -991,7 +991,45 @@ window.teaweb = {
}
return color
},
DefaultChartColor: "#9DD3E8"
DefaultChartColor: "#9DD3E8",
validateIP: function (ip) {
if (typeof ip != "string") {
return false
}
if (ip.length == 0) {
return false
}
// IPv6
if (ip.indexOf(":") >= 0) {
let pieces = ip.split(":")
if (pieces.length > 8) {
return false
}
let isOk = true
pieces.forEach(function (piece) {
if (!/^[\da-fA-F]{0,4}$/.test(piece)) {
isOk = false
}
})
return isOk
}
if (!ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) {
return false
}
let pieces = ip.split(".")
let isOk = true
pieces.forEach(function (v) {
let v1 = parseInt(v)
if (v1 > 255) {
isOk = false
}
})
return isOk
}
}
String.prototype.quoteIP = function () {

View File

@@ -7,5 +7,4 @@
<a class="item" @click.prevent="showQQGroupQrcode()" title="点击弹出加群二维码">QQ讨论群 &nbsp;<i class="icon qrcode"></i> </a>
<a class="item" href="https://goedge.cn/community/telegram" target="_blank" title="点击跳转到加群页面">Telegram群 &nbsp;<i class="icon paper plane"></i></a>
<a class="item right" href="https://goedge.cn/commercial" target="_blank" v-if="!teaIsPlus">企业版</a>
<a class="item right" href="https://goedge.cn/docs/Appendix/Donation.md" target="_blank" v-if="teaIsPlus">捐助作者</a>
</div>

View File

@@ -23,7 +23,12 @@
</thead>
<tr v-for="node in nodes">
<td><a :href="'/api/node?nodeId=' + node.id">{{node.name}}</a>
<grey-label v-if="node.isPrimary">主节点</grey-label>
<div v-if="node.isPrimary">
<grey-label v-if="node.isPrimary">主节点</grey-label>
</div>
<div v-if="node.status != null && node.status.shouldUpgrade">
<span class="red small">v{{node.status.buildVersion}} -&gt; v{{node.status.latestVersion}}<br/><a href="" v-if="node.status.canUpgrade" @click.prevent="upgradeNode(node.id)">[远程升级]</a> </span>
</div>
</td>
<td>
<div v-if="node.accessAddrs != null && node.accessAddrs.length > 0">

View File

@@ -1,7 +1,7 @@
Tea.context(function () {
// 创建节点
this.createNode = function () {
teaweb.popup("/api/node/createPopup", {
teaweb.popup(".node.createPopup", {
width: "50em",
height: "30em",
callback: function () {
@@ -16,11 +16,20 @@ Tea.context(function () {
this.deleteNode = function (nodeId) {
let that = this
teaweb.confirm("确定要删除此节点吗?", function () {
that.$post("/api/delete")
that.$post(".delete")
.params({
nodeId: nodeId
})
.refresh()
})
}
// 升级节点
this.upgradeNode = function (nodeId) {
teaweb.popup(".node.upgradePopup?nodeId=" + nodeId, {
onClose: function () {
teaweb.reload()
}
})
}
})

View File

@@ -0,0 +1,35 @@
{$layout "layout_popup"}
<h3>远程升级</h3>
<form class="ui form" data-tea-action="$" data-tea-success="success" data-tea-timeout="3600" data-tea-before="before" data-tea-done="done">
<csrf-token></csrf-token>
<input type="hidden" name="nodeId" :value="nodeId"/>
<table class="ui table definition selectable">
<tr v-show="nodeName.length > 0">
<td class="title">API节点</td>
<td>{{nodeName}}</td>
</tr>
<tr v-show="currentVersion.length > 0">
<td>当前版本</td>
<td>v{{currentVersion}}</td>
</tr>
<tr v-show="latestVersion.length > 0">
<td>目标版本</td>
<td>v{{latestVersion}}</td>
</tr>
<tr>
<td class="title">升级结果</td>
<td>
<span v-if="currentVersion == latestVersion" class="green">已经升级到最新版本</span>
<span :class="{red: !resultIsOk}" v-if="currentVersion != latestVersion">
<span v-if="!isRequesting">{{result}}</span>
<span v-if="isRequesting">升级中...</span>
</span>
</td>
</tr>
</table>
<submit-btn v-show="canUpgrade && !isRequesting && !isUpgrading">开始升级</submit-btn>
</form>

View File

@@ -0,0 +1,39 @@
Tea.context(function () {
this.$delay(function () {
this.checkLoop()
})
this.success = function () {
}
this.isRequesting = false
this.before = function () {
this.isRequesting = true
}
this.done = function () {
this.isRequesting = false
}
this.checkLoop = function () {
if (this.currentVersion == this.latestVersion) {
return
}
this.$post(".upgradeCheck")
.params({
nodeId: this.nodeId
})
.success(function (resp) {
if (resp.data.isOk) {
teaweb.reload()
}
})
.done(function () {
this.$delay(function () {
this.checkLoop()
}, 3000)
})
}
})

View File

@@ -22,7 +22,7 @@
<td>
<div v-if="node.ipAddresses.length > 0">
<div>
<div v-for="(address, index) in node.ipAddresses" class="ui label tiny basic">
<div v-for="(address, index) in node.ipAddresses" class="ui label small basic">
<span v-if="address.ip.indexOf(':') > -1" class="grey">[IPv6]</span> {{address.ip}}
<span class="small red" v-if="address.originIP != null && address.originIP.length > 0 && address.originIP != address.ip">(原:{{address.originIP}}</span>
<span class="small grey" v-if="address.name.length > 0">{{address.name}}<span v-if="!address.canAccess">,不可访问</span></span>
@@ -30,6 +30,10 @@
<span class="small red" v-if="!address.isOn">[off]</span>
<span class="small red" v-if="!address.isUp">[down]</span>
<span class="small" v-if="address.thresholds != null && address.thresholds.length > 0">[阈值]</span>
<span v-if="address.clusters.length > 0">
&nbsp; <span class="small grey">专属集群:[</span><span v-for="(cluster, index) in address.clusters" class="small grey">{{cluster.name}}<span v-if="index < address.clusters.length - 1"></span></span><span class="small grey">]</span>
</span>
</div>
</div>
</div>
@@ -51,6 +55,7 @@
<table class="ui table celled">
<thead class="full-width">
<tr>
<th>集群</th>
<th>记录名</th>
<th>记录类型</th>
<th>线路</th>
@@ -59,6 +64,7 @@
</thead>
<tr v-for="record in node.records">
<td>{{record.clusterName}}</td>
<td>{{record.name}}</td>
<td>{{record.type}}</td>
<td>

View File

@@ -26,7 +26,7 @@
<tr>
<td>IP地址 *</td>
<td>
<node-ip-addresses-box :v-ip-addresses="ipAddresses"></node-ip-addresses-box>
<node-ip-addresses-box :v-ip-addresses="ipAddresses" :v-node-id="node.id"></node-ip-addresses-box>
<p class="comment">用于访问节点和域名解析等。</p>
</td>
</tr>

View File

@@ -94,6 +94,11 @@
<div class="ui label tiny basic">{{addr.ip}}
<span class="small" v-if="addr.name.length > 0">{{addr.name}}<span v-if="!addr.canAccess">,不可访问</span></span>
<span class="small" v-if="addr.name.length == 0 && !addr.canAccess">(不可访问)</span>
<!-- 专属集群 -->
<div v-if="addr.clusters != null && addr.clusters.length > 0" style="margin-top: 0.3em; font-weight: normal">
<span class="small grey">[</span><span v-for="(cluster, index) in addr.clusters" class="small grey">{{cluster.name}}<span v-if="index < addr.clusters.length - 1"></span></span><span class="small grey">]</span>
</div>
</div>
</div>
</div>

View File

@@ -77,6 +77,13 @@
<p class="comment">选中后表示访问日志中记录Cookie内容。</p>
</td>
</tr>
<tr>
<td>记录找不到网站日志</td>
<td>
<checkbox name="httpAccessLogEnableServerNotFound" v-model="config.httpAccessLog.enableServerNotFound"></checkbox>
<p class="comment">选中后,表示如果访客访问的域名对应的网站不存在也会记录日志。</p>
</td>
</tr>
</table>
<h4>运行日志</h4>

View File

@@ -2,4 +2,11 @@ pre.log-box {
margin: 0;
padding: 0;
}
.search-keyword-label a {
opacity: 1!important;
}
.search-keyword-label span.small {
font-size: 0.8em;
color: #4183c4 !important;
}
/*# sourceMappingURL=index.css.map */

View File

@@ -1 +1 @@
{"version":3,"sources":["index.less"],"names":[],"mappings":"AAAA,GAAG;EACF,SAAA;EACA,UAAA","file":"index.css"}
{"version":3,"sources":["index.less"],"names":[],"mappings":"AAAA,GAAG;EACF,SAAA;EACA,UAAA;;AAGD,qBACC;EACC,oBAAA;;AAFF,qBAKC,KAAI;EACH,gBAAA;EACA,cAAA","file":"index.css"}

View File

@@ -68,6 +68,10 @@
<p class="comment" v-if="logs.length == 0">暂时还没有<span v-if="type == 'unread'">未读</span><span v-if="type == 'needFix'">需修复</span>日志。</p>
<div v-if="countLogs > 0 && searchedKeyword.length > 0" class="search-keyword-label">
<div class="ui label basic small">正在搜索关键词"{{searchedKeyword}}",共{{countLogs}}条记录 &nbsp; <a href="" @click.prevent="deleteLogs"><span class="small">[清除日志]</span></a> </div>
</div>
<table class="ui table selectable celled" v-if="logs.length > 0">
<thead>
<tr>

View File

@@ -82,4 +82,23 @@ Tea.context(function () {
})
})
}
this.deleteLogs = function () {
teaweb.confirm("确定要删除当前关键词\"" + this.searchedKeyword + "\"匹配的" + this.countLogs + "个运行日志?", function () {
this.$post(".deleteAll")
.params({
dayFrom: this.dayFrom,
dayTo: this.dayTo,
keyword: this.keyword,
level: this.level,
type: this.type,
tag: this.tag,
clusterId: this.clusterId,
nodeId: this.nodeId
})
.success(function () {
teaweb.reload()
})
})
}
})

View File

@@ -1,4 +1,15 @@
pre.log-box {
margin: 0;
padding: 0;
}
}
.search-keyword-label {
a {
opacity: 1!important;
}
span.small {
font-size: 0.8em;
color: #4183c4!important;
}
}

View File

@@ -88,6 +88,11 @@
<div class="ui label tiny basic">{{addr.ip}}
<span class="small" v-if="addr.name.length > 0">{{addr.name}}<span v-if="!addr.canAccess">,不可访问</span></span>
<span class="small" v-if="addr.name.length == 0 && !addr.canAccess">(不可访问)</span>
<!-- 专属集群 -->
<div v-if="addr.clusters != null && addr.clusters.length > 0" style="margin-top: 0.3em; font-weight: normal">
<span class="small grey">[</span><span v-for="(cluster, index) in addr.clusters" class="small grey">{{cluster.name}}<span v-if="index < addr.clusters.length - 1"></span></span><span class="small grey">]</span>
</div>
</div>
</div>
</div>

View File

@@ -41,12 +41,6 @@
<input type="password" v-model="password" placeholder="请输入密码" maxlength="200" @input="changePassword()" ref="passwordRef"/>
</div>
</div>
<div class="ui field" v-show="showOTP">
<div class="ui left icon input">
<i class="ui barcode icon"></i>
<input type="text" name="otpCode" placeholder="请输入OTP动态密码" maxlength="6"/>
</div>
</div>
<div class="ui field" v-if="rememberLogin">
<a href="" @click.prevent="showMoreOptions()">更多选项 <i class="icon angle" :class="{down:!moreOptionsVisible, up:moreOptionsVisible}"></i> </a>
</div>

View File

@@ -9,8 +9,6 @@ Tea.context(function () {
this.password = "123456"
}
this.showOTP = false
this.isSubmitting = false
this.$delay(function () {
@@ -19,13 +17,7 @@ Tea.context(function () {
});
this.changeUsername = function () {
this.$post("/checkOTP")
.params({
username: this.username
})
.success(function (resp) {
this.showOTP = resp.data.requireOTP
})
}
this.changePassword = function () {
@@ -46,7 +38,11 @@ Tea.context(function () {
this.isSubmitting = false;
};
this.submitSuccess = function () {
this.submitSuccess = function (resp) {
if (resp.data.requireOTP) {
window.location = "/index/otp?sid=" + resp.data.sid + "&remember=" + (resp.data.remember ? 1 : 0) + "&from=" + window.encodeURIComponent(this.from)
return
}
if (this.from.length == 0) {
window.location = "/dashboard";
} else {

View File

@@ -0,0 +1,42 @@
.form-box {
position: fixed;
top: 2em;
bottom: 0;
left: 0;
right: 0;
}
form {
position: fixed;
width: 21em;
top: 50%;
left: 50%;
margin-left: -10em;
margin-top: -16em;
}
form .header {
text-align: center;
font-size: 1em !important;
}
form p {
font-size: 0.8em;
margin-top: 0.3em;
margin-bottom: 0;
font-weight: normal;
padding: 0;
}
form .comment {
margin-top: 0.5em;
padding: 0.5em;
color: gray;
}
form .cancel-login {
text-align: center;
padding-top: 1em;
}
@media screen and (max-width: 512px) {
form {
width: 80%;
margin-left: -40%;
}
}
/*# sourceMappingURL=otp.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["otp.less"],"names":[],"mappings":"AAAA;EACI,eAAA;EACA,QAAA;EACA,SAAA;EACA,OAAA;EACA,QAAA;;AAGJ;EACI,eAAA;EACA,WAAA;EACA,QAAA;EACA,SAAA;EACA,kBAAA;EACA,iBAAA;;AANJ,IAQC;EACC,kBAAA;EACA,yBAAA;;AAVF,IAaC;EACC,gBAAA;EACA,iBAAA;EACA,gBAAA;EACA,mBAAA;EACA,UAAA;;AAlBF,IAqBC;EACC,iBAAA;EACA,cAAA;EACA,WAAA;;AAxBF,IA2BC;EACC,kBAAA;EACA,gBAAA;;AAIF,mBAAqC;EACjC;IACI,UAAA;IACA,iBAAA","file":"otp.css"}

View File

@@ -0,0 +1,55 @@
<!doctype html>
<html lang="zh">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
{$if eq .faviconFileId 0}
<link rel="shortcut icon" href="/images/favicon.png"/>
{$else}
<link rel="shortcut icon" href="/ui/image/{$ .faviconFileId}"/>
{$end}
<title>登录{$.systemName} - 二次验证</title>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
{$TEA.VUE}
{$TEA.SEMANTIC}
<script type="text/javascript" src="/js/md5.min.js"></script>
<script type="text/javascript" src="/js/utils.js"></script>
<script type="text/javascript" src="/js/sweetalert2/dist/sweetalert2.all.min.js"></script>
<script type="text/javascript" src="/js/components.js"></script>
</head>
<body>
<div>
{$template "/menu"}
<div class="form-box">
<form method="post" class="ui form" data-tea-action="$" data-tea-before="submitBefore"
data-tea-done="submitDone" data-tea-success="submitSuccess" autocomplete="off">
<input type="hidden" name="sid" :value="sid"/>
<input type="hidden" name="remember" :value="remember ? 1 : 0"/>
<div class="ui segment stacked">
<div class="ui header">
登录{$.systemName}
</div>
<div class="ui field">
为了保护你的账户安全需要进行OTP二次身份验证。
</div>
<div class="ui field">
<div class="ui left icon input">
<i class="ui barcode icon"></i>
<input type="text" name="otpCode" placeholder="请输入OTP动态密码" maxlength="6"/>
</div>
</div>
<button class="ui button primary fluid" type="submit" v-if="!isSubmitting">验证</button>
<button class="ui button primary fluid disabled" type="submit" v-if="isSubmitting">验证中...</button>
<div class="ui field cancel-login">
<a :href="'/?from=' + encodedFrom">&laquo; 取消登录</a>
</div>
</div>
</form>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,31 @@
Tea.context(function () {
this.isSubmitting = false
this.encodedFrom = window.encodeURIComponent(this.from)
this.$delay(function () {
this.$find("form input[name='otpCode']").focus()
});
// 更多选项
this.moreOptionsVisible = false;
this.showMoreOptions = function () {
this.moreOptionsVisible = !this.moreOptionsVisible;
};
this.submitBefore = function () {
this.isSubmitting = true;
};
this.submitDone = function () {
this.isSubmitting = false;
};
this.submitSuccess = function (resp) {
if (this.from.length == 0) {
window.location = "/dashboard";
} else {
window.location = this.from;
}
};
});

View File

@@ -0,0 +1,47 @@
.form-box {
position: fixed;
top: 2em;
bottom: 0;
left: 0;
right: 0;
}
form {
position: fixed;
width: 21em;
top: 50%;
left: 50%;
margin-left: -10em;
margin-top: -16em;
.header {
text-align: center;
font-size: 1em !important;
}
p {
font-size: 0.8em;
margin-top: 0.3em;
margin-bottom: 0;
font-weight: normal;
padding: 0;
}
.comment {
margin-top: 0.5em;
padding: 0.5em;
color: gray;
}
.cancel-login {
text-align: center;
padding-top: 1em;
}
}
@media screen and (max-width: 512px) {
form {
width: 80%;
margin-left: -40%;
}
}

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