Compare commits

..

75 Commits

Author SHA1 Message Date
刘祥超
e3674fa2c1 优化文字 2021-12-20 09:35:36 +08:00
刘祥超
a14cbe1319 访问日志限制字段 2021-12-20 09:35:25 +08:00
刘祥超
c03c35de88 访问日志限制字段 2021-12-19 20:39:18 +08:00
刘祥超
dc6c649af8 优化文字提示等 2021-12-19 18:56:09 +08:00
刘祥超
b0d2bdb0ba 修复HSTS无法设置有效期的Bug 2021-12-18 17:23:55 +08:00
刘祥超
9239dd9a8b 调整单次通知更新任务的数量为500(原来200) 2021-12-17 14:25:40 +08:00
刘祥超
07a368a0bd 修改相关域名、文字等 2021-12-17 14:17:48 +08:00
刘祥超
b8d6d1c249 访问日志中缓存状态增加STALE 2021-12-17 11:55:12 +08:00
刘祥超
0f1d6a1ad2 实现stale cache配置 2021-12-16 17:27:09 +08:00
刘祥超
37c6928ffc 节点IP显示原始IP(如果已经切换到备用IP的话) 2021-12-16 15:18:21 +08:00
刘祥超
7478b6dbe0 优化代码 2021-12-15 20:46:42 +08:00
刘祥超
e3cd6e1441 增加在线检查最新版本功能 2021-12-15 20:13:10 +08:00
刘祥超
8081d968b6 Update components.js 2021-12-15 09:56:18 +08:00
刘祥超
a226eee6ef HTTP Header:实现请求方法、域名、状态码等限制,实现内容替换功能 2021-12-14 21:26:32 +08:00
刘祥超
8b5a21e593 访问日志查询过慢的时候,提示建议增加新的日志节点 2021-12-14 15:50:21 +08:00
刘祥超
2c71f6c6be 实现访问日志队列 2021-12-14 12:45:09 +08:00
刘祥超
1a50b01edf 优化代码 2021-12-13 10:53:49 +08:00
刘祥超
05f08eeb4c 优化坐标轴单位 2021-12-12 21:13:55 +08:00
刘祥超
fdc3ebb5c5 WAF添加规则:调整界面/增加正则表达式测试功能 2021-12-12 20:56:25 +08:00
刘祥超
8f06bccd48 WAF策略:可以修改分组代号/导入时可以根据名称合并/导出时可以导出停用的分组 2021-12-12 20:24:15 +08:00
刘祥超
16b4eb67d4 Update components.js 2021-12-12 17:14:46 +08:00
刘祥超
ffa545cb41 服务分组可以设置请求限制 2021-12-12 17:07:16 +08:00
刘祥超
f67454d51c 路由规则增加专属域名设置 2021-12-12 16:38:52 +08:00
刘祥超
8ae54c56db 增加排除扩展名条件 2021-12-12 16:14:56 +08:00
刘祥超
9a17adcc6f 请求条件增加不区分大小写选项 2021-12-12 16:11:25 +08:00
刘祥超
4dd6903025 优化界面 2021-12-12 11:58:16 +08:00
刘祥超
085770d0ad 实现请求连接数等限制/容量组件增加EB支持 2021-12-12 11:46:09 +08:00
刘祥超
fbaba7c37d 修复公共黑名单/白名单无法搜索的Bug 2021-12-10 11:12:49 +08:00
刘祥超
d5cea208d2 支持设置单节点最大线程数、单节点TCP最大连接数 2021-12-09 18:49:35 +08:00
刘祥超
36397adca4 修复服务统计图表颜色显示问题/Expires设置增加提示文字 2021-12-08 21:40:01 +08:00
刘祥超
4971e25d44 WAF看板最新拦截记录增加区域信息 2021-12-08 20:28:26 +08:00
刘祥超
150357441d 修复域名为空时点击审核页面为空的Bug 2021-12-08 20:20:10 +08:00
刘祥超
c4ee663285 重新生成components.js 2021-12-08 19:13:54 +08:00
刘祥超
8c95b4a9b9 多处访问日志增加单页显示条数选择 2021-12-08 19:13:34 +08:00
刘祥超
32ba919851 增加部分访问日志条数10->20 2021-12-08 17:49:30 +08:00
刘祥超
eb37345e85 可以在缓存条件里设置Expires Header 2021-12-08 17:41:12 +08:00
刘祥超
681e454917 修复编译时components.js可能没有更新的Bug 2021-12-08 10:03:27 +08:00
刘祥超
7c7b82dee4 优化地图标签 2021-12-08 09:37:46 +08:00
刘祥超
2ae47af8f0 增加批量增加节点IP接口 2021-12-07 18:22:46 +08:00
刘祥超
b72d91d0d4 访问日志实现记录和显示requestBody 2021-12-07 15:03:48 +08:00
刘祥超
56010e7203 缓存默认支持所有请求方法 2021-12-07 10:47:46 +08:00
刘祥超
f658698f7b 缓存支持请求方法设置 2021-12-07 10:45:49 +08:00
刘祥超
b708b9c6df 将缓存默认key改为${scheme}://${host}${requestPath}${isArgs}${args} 2021-12-07 10:04:50 +08:00
刘祥超
c2908e17fa SSH认证支持sudo 2021-12-06 19:24:30 +08:00
刘祥超
302daab824 服务看板增加区域地图 2021-12-06 09:16:23 +08:00
刘祥超
ec49f238d6 优化界面 2021-12-06 08:55:47 +08:00
刘祥超
3e43a5d866 上传components.js/优化地图样式 2021-12-05 20:59:07 +08:00
刘祥超
a99d5e68e9 商业版WAF看板增加地图 2021-12-05 19:38:14 +08:00
刘祥超
28514276ec 商业版首页增加地图/调低各个图表的高度,以便同时可以显示更多的图表 2021-12-05 18:59:20 +08:00
刘祥超
b611427c17 创建新服务时默认开启配置可以选择统计 2021-12-03 14:55:14 +08:00
刘祥超
ce7a4ead04 首页看板显示未审核的服务数、本周流量、昨日流量 2021-12-03 14:54:15 +08:00
刘祥超
5fe15a85fd 优化IPBox交互/优化TOA文字提示 2021-12-03 11:00:20 +08:00
刘祥超
ae412909f6 优化服务设置界面顶部菜单 2021-12-02 17:41:51 +08:00
刘祥超
c6ed579797 IPBox把IP加入黑名单可以选择过期时间/可以从已经添加的名单中删除/已经添加的名单中显示过期时间 2021-12-02 17:11:44 +08:00
刘祥超
b2a525268e WAF规则集中增加是否忽略局域网IP选项 2021-12-02 16:09:15 +08:00
刘祥超
eb78b4881c 多个提示页面增加请求ID 2021-12-02 14:45:51 +08:00
刘祥超
4ce6e5a9f6 访问日志弹窗中加入请求ID/优化Header添加/修改文字提示 2021-12-02 11:49:36 +08:00
刘祥超
15d7e75555 优化URL跳转文字提示 2021-12-02 10:39:22 +08:00
刘祥超
6453cc6ccc 缓存配置增加是否支持Cache-Control: max-age=... 2021-12-02 10:18:22 +08:00
刘祥超
d882a2eb63 缓存配置增加Age Header配置 2021-12-02 09:54:31 +08:00
刘祥超
065ac4aa25 增加generate.sh脚本用来生成文件 2021-12-02 09:46:46 +08:00
刘祥超
dea54fc55e 增加是否记录499选项 2021-12-01 21:13:15 +08:00
刘祥超
8f425bd9c7 当用户提交待审核域名时,给管理员发送消息 2021-12-01 17:19:50 +08:00
刘祥超
9d909d73b8 审核中服务增加提交审核时间/已通过域名标绿 2021-12-01 17:06:05 +08:00
刘祥超
b417d50a28 优化节点日志:可以批量设置服务错误日志为已修复等 2021-11-30 16:43:44 +08:00
刘祥超
aa93a2f702 优化编译脚本 2021-11-30 11:05:58 +08:00
刘祥超
ba7125e773 优化文字 2021-11-30 11:05:24 +08:00
刘祥超
46a7eaa4bb 商业版认证增加申请页面 2021-11-30 10:02:31 +08:00
刘祥超
887439a6fe 优化代码 2021-11-29 20:35:47 +08:00
刘祥超
0e2b07d06d 优化提示文字 2021-11-29 16:41:32 +08:00
刘祥超
b7d4bde11b 修改部分菜单名 2021-11-29 12:00:23 +08:00
刘祥超
9707360948 完善套餐 2021-11-28 20:11:48 +08:00
刘祥超
103a8eb092 修改版本号为0.3.7 2021-11-28 14:28:53 +08:00
刘祥超
dcba4f9376 节点同步不在图标上提示IP名单相关更新 2021-11-27 17:06:36 +08:00
刘祥超
12aaa6fcb1 更新生成的components.js 2021-11-27 17:06:03 +08:00
268 changed files with 5371 additions and 1663 deletions

View File

@@ -20,6 +20,18 @@ function build() {
TAG="community"
fi
# checking environment
echo "checking required commands ..."
commands=("zip" "unzip" "go" "find" "sed")
for cmd in "${commands[@]}"; do
if [ `which ${cmd}` ]; then
echo "checking ${cmd}: ok"
else
echo "checking ${cmd}: not found"
return
fi
done
VERSION=$(lookup-version $ROOT/../internal/const/const.go)
ZIP="${NAME}-${OS}-${ARCH}-${TAG}-v${VERSION}.zip"
@@ -38,6 +50,10 @@ function build() {
echo "=============================="
cd -
# generate files
echo "generating files ..."
go run -tags $TAG $ROOT/../cmd/edge-admin/main.go generate
# create dir & copy files
echo "copying ..."
if [ ! -d $DIST ]; then
@@ -51,6 +67,19 @@ function build() {
rm -f $DIST/web/tmp/*
cp $ROOT/configs/server.template.yaml $DIST/configs/
# change _plus.[ext] to .[ext]
if [ "${TAG}" = "plus" ]; then
echo "converting filenames ..."
exts=("html" "js" "css")
for ext in "${exts[@]}"; do
pattern="*_plus."${ext}
find $DIST/web/views -type f -name $pattern | \
while read filename; do
mv ${filename} "${filename/_plus."${ext}"/."${ext}"}"
done
done
fi
EDGE_API_ZIP_FILE=$ROOT"/../../EdgeAPI/dist/edge-api-${OS}-${ARCH}-${TAG}-v${APINodeVersion}.zip"
cp $EDGE_API_ZIP_FILE $DIST/
cd $DIST/
@@ -58,10 +87,6 @@ function build() {
rm -f $(basename $EDGE_API_ZIP_FILE)
cd -
# generate files
echo "generating files ..."
go run -tags $TAG $ROOT/../cmd/edge-admin/main.go generate
# build
echo "building "${NAME}" ..."
env GOOS=$OS GOARCH=$ARCH go build -tags $TAG -ldflags="-s -w" -o $DIST/bin/${NAME} $ROOT/../cmd/edge-admin/main.go

3
build/generate.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
go run -tags=community ../cmd/edge-admin/main.go generate

View File

@@ -11,7 +11,9 @@ import (
var plusConfigFile = "plus.cache.json"
type PlusConfig struct {
IsPlus bool `json:"isPlus"`
IsPlus bool `json:"isPlus"`
Components []string `json:"components"`
DayTo string `json:"dayTo"`
}
func ReadPlusConfig() *PlusConfig {

View File

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

View File

@@ -1,6 +1,7 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build community
// +build community
package teaconst
// IsPlus 是否为企业版
var IsPlus = false

View File

@@ -18,7 +18,7 @@ func TestRPCClient_NodeRPC(t *testing.T) {
if err != nil {
t.Fatal(err)
}
rpc, err := NewRPCClient(config)
rpc, err := NewRPCClient(config, true)
if err != nil {
t.Fatal(err)
}
@@ -41,7 +41,7 @@ func TestRPC_Dial_HTTP(t *testing.T) {
},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",
})
}, true)
if err != nil {
t.Fatal(err)
}
@@ -62,7 +62,7 @@ func TestRPC_Dial_HTTP_2(t *testing.T) {
},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",
})
}, true)
if err != nil {
t.Fatal(err)
}
@@ -83,7 +83,7 @@ func TestRPC_Dial_HTTPS(t *testing.T) {
},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",
})
}, true)
if err != nil {
t.Fatal(err)
}

View File

@@ -51,7 +51,7 @@ func (this *SyncClusterTask) loop() error {
}
ctx := rpcClient.Context(0)
tasksResp, err := rpcClient.NodeTaskRPC().FindNotifyingNodeTasks(ctx, &pb.FindNotifyingNodeTasksRequest{Size: 100})
tasksResp, err := rpcClient.NodeTaskRPC().FindNotifyingNodeTasks(ctx, &pb.FindNotifyingNodeTasksRequest{Size: 500})
if err != nil {
return err
}

View File

@@ -1,14 +1,17 @@
package utils
import (
"bytes"
"encoding/binary"
"errors"
"github.com/iwind/TeaGo/types"
"math/big"
"net"
"regexp"
"strings"
)
// 将IP转换为整型
// IP2Long 将IP转换为整型
func IP2Long(ip string) uint64 {
s := net.ParseIP(ip)
if len(s) != 16 {
@@ -23,7 +26,7 @@ func IP2Long(ip string) uint64 {
return uint64(binary.BigEndian.Uint32(s.To4()))
}
// 判断是否为IPv4
// IsIPv4 判断是否为IPv4
func IsIPv4(ip string) bool {
if !regexp.MustCompile(`^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$`).MatchString(ip) {
return false
@@ -34,10 +37,100 @@ func IsIPv4(ip string) bool {
return true
}
// 判断是否为IPv6
// IsIPv6 判断是否为IPv6
func IsIPv6(ip string) bool {
if !strings.Contains(ip, ":") {
return false
}
return len(net.ParseIP(ip)) == net.IPv6len
}
// ExtractIP 分解IP
// 只支持D段掩码的CIDR
// 最多只记录255个值
func ExtractIP(ipStrings string) ([]string, error) {
ipStrings = strings.ReplaceAll(ipStrings, " ", "")
// CIDR
if strings.Contains(ipStrings, "/") {
_, cidrNet, err := net.ParseCIDR(ipStrings)
if err != nil {
return nil, err
}
var index = strings.Index(ipStrings, "/")
var ipFrom = ipStrings[:index]
var bits = types.Int(ipStrings[index+1:])
if bits < 24 {
return nil, errors.New("CIDR bits should be greater than 24")
}
var ipv4 = net.ParseIP(ipFrom).To4()
if len(ipv4) == 0 {
return nil, errors.New("support IPv4 only")
}
var result = []string{}
ipv4[3] = 0 // 从0开始
for i := 0; i <= 255; i++ {
if cidrNet.Contains(ipv4) {
result = append(result, ipv4.String())
}
ipv4 = NextIP(ipv4)
}
return result, nil
}
// IP Range
if strings.Contains(ipStrings, "-") {
var index = strings.Index(ipStrings, "-")
var ipFromString = ipStrings[:index]
var ipToString = ipStrings[index+1:]
var ipFrom = net.ParseIP(ipFromString).To4()
if len(ipFrom) == 0 {
return nil, errors.New("invalid ip '" + ipFromString + "'")
}
var ipTo = net.ParseIP(ipToString).To4()
if len(ipTo) == 0 {
return nil, errors.New("invalid ip '" + ipToString + "'")
}
if bytes.Compare(ipFrom, ipTo) > 0 {
ipFrom, ipTo = ipTo, ipFrom
}
var result = []string{}
for i := 0; i < 255; i++ {
if bytes.Compare(ipFrom, ipTo) > 0 {
break
}
result = append(result, ipFrom.String())
ipFrom = NextIP(ipFrom)
}
return result, nil
}
return []string{ipStrings}, nil
}
// NextIP IP增加1
func NextIP(prevIP net.IP) net.IP {
var ip = make(net.IP, len(prevIP))
copy(ip, prevIP)
var index = len(ip) - 1
for {
if ip[index] == 255 {
ip[index] = 0
index--
if index < 0 {
break
}
} else {
ip[index]++
break
}
}
return ip
}

View File

@@ -1,6 +1,7 @@
package utils
import (
"net"
"testing"
)
@@ -83,3 +84,27 @@ func TestIsIPv6(t *testing.T) {
}
}
}
func TestExtractIP(t *testing.T) {
t.Log(ExtractIP("192.168.1.100"))
}
func TestExtractIP_CIDR(t *testing.T) {
t.Log(ExtractIP("192.168.2.100/24"))
}
func TestExtractIP_Range(t *testing.T) {
t.Log(ExtractIP("192.168.2.100 - 192.168.4.2"))
}
func TestNextIP(t *testing.T) {
for _, ip := range []string{"192.168.1.1", "0.0.0.0", "255.255.255.255", "192.168.2.255", "192.168.255.255"} {
t.Log(ip+":", NextIP(net.ParseIP(ip).To4()))
}
}
func TestNextIP_Copy(t *testing.T) {
var ip = net.ParseIP("192.168.1.100")
var nextIP = NextIP(ip)
t.Log(ip, nextIP)
}

View File

@@ -13,38 +13,30 @@ func FormatInt(value int) string {
return strconv.Itoa(value)
}
func FormatBytes(bytes int64) string {
if bytes < 1024 {
return FormatInt64(bytes) + "B"
} else if bytes < 1024*1024 {
return fmt.Sprintf("%.2fKB", float64(bytes)/1024)
} else if bytes < 1024*1024*1024 {
return fmt.Sprintf("%.2fMB", float64(bytes)/1024/1024)
} else if bytes < 1024*1024*1024*1024 {
return fmt.Sprintf("%.2fGB", float64(bytes)/1024/1024/1024)
} else if bytes < 1024*1024*1024*1024*1024 {
return fmt.Sprintf("%.2fTB", float64(bytes)/1024/1024/1024/1024)
} else if bytes < 1024*1024*1024*1024*1024*1024 {
return fmt.Sprintf("%.2fPB", float64(bytes)/1024/1024/1024/1024/1024)
} else {
return fmt.Sprintf("%.2fEB", float64(bytes)/1024/1024/1024/1024/1024/1024)
func Pow1024(n int) int64 {
if n <= 0 {
return 1
}
if n == 1 {
return 1024
}
return Pow1024(n-1) * 1024
}
func FormatBits(bits int64) string {
if bits < 1000 {
return FormatInt64(bits) + "B"
} else if bits < 1000*1000 {
return fmt.Sprintf("%.2fKB", float64(bits)/1000)
} else if bits < 1000*1000*1000 {
return fmt.Sprintf("%.2fMB", float64(bits)/1000/1000)
} else if bits < 1000*1000*1000*1000 {
return fmt.Sprintf("%.2fGB", float64(bits)/1000/1000/1000)
} else if bits < 1000*1000*1000*1000*1000 {
return fmt.Sprintf("%.2fTB", float64(bits)/1000/1000/1000/1000)
} else if bits < 1000*1000*1000*1000*1000*1000 {
return fmt.Sprintf("%.2fPB", float64(bits)/1000/1000/1000/1000/1000)
func FormatBytes(bytes int64) string {
if bytes < Pow1024(1) {
return FormatInt64(bytes) + "B"
} else if bytes < Pow1024(2) {
return fmt.Sprintf("%.2fKB", float64(bytes)/float64(Pow1024(1)))
} else if bytes < Pow1024(3) {
return fmt.Sprintf("%.2fMB", float64(bytes)/float64(Pow1024(2)))
} else if bytes < Pow1024(4) {
return fmt.Sprintf("%.2fGB", float64(bytes)/float64(Pow1024(3)))
} else if bytes < Pow1024(5) {
return fmt.Sprintf("%.2fTB", float64(bytes)/float64(Pow1024(4)))
} else if bytes < Pow1024(6) {
return fmt.Sprintf("%.2fPB", float64(bytes)/float64(Pow1024(5)))
} else {
return fmt.Sprintf("%.2fEB", float64(bits)/1000/1000/1000/1000/1000/1000)
return fmt.Sprintf("%.2fEB", float64(bytes)/float64(Pow1024(6)))
}
}

View File

@@ -0,0 +1,16 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package numberutils
import "testing"
func TestFormatBytes(t *testing.T) {
t.Log(FormatBytes(1))
t.Log(FormatBytes(1000))
t.Log(FormatBytes(1_000_000))
t.Log(FormatBytes(1_000_000_000))
t.Log(FormatBytes(1_000_000_000_000))
t.Log(FormatBytes(1_000_000_000_000_000))
t.Log(FormatBytes(1_000_000_000_000_000_000))
t.Log(FormatBytes(9_000_000_000_000_000_000))
}

View File

@@ -3,6 +3,7 @@ package cluster
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/grants/grantutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
@@ -167,9 +168,13 @@ func (this *CreateNodeAction) RunPost(params struct {
nodeId := createResp.NodeId
// IP地址
var resultIPAddresses = []string{}
for _, addr := range ipAddresses {
var resultAddrIds = []int64{}
addrId := addr.GetInt64("id")
if addrId > 0 {
resultAddrIds = append(resultAddrIds, addrId)
_, err = this.RPC().NodeIPAddressRPC().UpdateNodeIPAddressNodeId(this.AdminContext(), &pb.UpdateNodeIPAddressNodeIdRequest{
NodeIPAddressId: addrId,
NodeId: nodeId,
@@ -178,20 +183,50 @@ func (this *CreateNodeAction) RunPost(params struct {
this.ErrorPage(err)
return
}
resultIPAddresses = append(resultIPAddresses, addr.GetString("ip"))
} else {
createResp, err := this.RPC().NodeIPAddressRPC().CreateNodeIPAddress(this.AdminContext(), &pb.CreateNodeIPAddressRequest{
NodeId: nodeId,
Role: nodeconfigs.NodeRoleNode,
Name: addr.GetString("name"),
Ip: addr.GetString("ip"),
CanAccess: addr.GetBool("canAccess"),
IsUp: addr.GetBool("isUp"),
})
var ipStrings = addr.GetString("ip")
result, err := utils.ExtractIP(ipStrings)
if err != nil {
this.ErrorPage(err)
return
this.Fail("节点创建成功但是保存IP失败" + err.Error())
}
resultIPAddresses = append(resultIPAddresses, result...)
if len(result) == 1 {
// 单个创建
createResp, err := this.RPC().NodeIPAddressRPC().CreateNodeIPAddress(this.AdminContext(), &pb.CreateNodeIPAddressRequest{
NodeId: nodeId,
Role: nodeconfigs.NodeRoleNode,
Name: addr.GetString("name"),
Ip: result[0],
CanAccess: addr.GetBool("canAccess"),
IsUp: addr.GetBool("isUp"),
})
if err != nil {
this.ErrorPage(err)
return
}
addrId = createResp.NodeIPAddressId
resultAddrIds = append(resultAddrIds, addrId)
} else if len(result) > 1 {
// 批量创建
createResp, err := this.RPC().NodeIPAddressRPC().CreateNodeIPAddresses(this.AdminContext(), &pb.CreateNodeIPAddressesRequest{
NodeId: nodeId,
Role: nodeconfigs.NodeRoleNode,
Name: addr.GetString("name"),
IpList: result,
CanAccess: addr.GetBool("canAccess"),
IsUp: addr.GetBool("isUp"),
GroupValue: ipStrings,
})
if err != nil {
this.ErrorPage(err)
return
}
resultAddrIds = append(resultAddrIds, createResp.NodeIPAddressIds...)
}
addrId = createResp.NodeIPAddressId
}
// 阈值
@@ -202,13 +237,16 @@ func (this *CreateNodeAction) RunPost(params struct {
this.ErrorPage(err)
return
}
_, err = this.RPC().NodeIPAddressThresholdRPC().UpdateAllNodeIPAddressThresholds(this.AdminContext(), &pb.UpdateAllNodeIPAddressThresholdsRequest{
NodeIPAddressId: addrId,
NodeIPAddressThresholdsJSON: thresholdsJSON,
})
if err != nil {
this.ErrorPage(err)
return
for _, addrId := range resultAddrIds {
_, err = this.RPC().NodeIPAddressThresholdRPC().UpdateAllNodeIPAddressThresholds(this.AdminContext(), &pb.UpdateAllNodeIPAddressThresholdsRequest{
NodeIPAddressId: addrId,
NodeIPAddressThresholdsJSON: thresholdsJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
}
}
}
@@ -224,11 +262,6 @@ func (this *CreateNodeAction) RunPost(params struct {
return
}
if nodeResp.Node != nil {
var addresses = []string{}
for _, addrMap := range ipAddresses {
addresses = append(addresses, addrMap.GetString("ip"))
}
var grantMap maps.Map = nil
grantId := params.GrantId
if grantId > 0 {
@@ -253,7 +286,7 @@ func (this *CreateNodeAction) RunPost(params struct {
"name": nodeResp.Node.Name,
"uniqueId": nodeResp.Node.UniqueId,
"secret": nodeResp.Node.Secret,
"addresses": addresses,
"addresses": resultIPAddresses,
"login": maps.Map{
"id": 0,
"name": "SSH",

View File

@@ -86,9 +86,16 @@ func (this *DetailAction) RunGet(params struct {
return
}
// 是否有备用IP
var originIP = addr.Ip
if len(addr.BackupIP) > 0 {
addr.Ip = addr.BackupIP
}
ipAddressMaps = append(ipAddressMaps, maps.Map{
"id": addr.Id,
"name": addr.Name,
"originIP": originIP,
"ip": addr.Ip,
"canAccess": addr.CanAccess,
"isOn": addr.IsOn,

View File

@@ -8,6 +8,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
)
type IndexAction struct {
@@ -65,22 +66,32 @@ func (this *IndexAction) RunGet(params struct {
this.Data["timeZoneLocation"] = nodeconfigs.FindTimeZoneLocation(cluster.TimeZone)
this.Data["cluster"] = maps.Map{
"id": cluster.Id,
"name": cluster.Name,
"installDir": cluster.InstallDir,
"timeZone": cluster.TimeZone,
"id": cluster.Id,
"name": cluster.Name,
"installDir": cluster.InstallDir,
"timeZone": cluster.TimeZone,
"nodeMaxThreads": cluster.NodeMaxThreads,
"nodeTCPMaxConnections": cluster.NodeTCPMaxConnections,
}
// 默认值
this.Data["defaultNodeMaxThreads"] = nodeconfigs.DefaultMaxThreads
this.Data["defaultNodeMaxThreadsMin"] = nodeconfigs.DefaultMaxThreadsMin
this.Data["defaultNodeMaxThreadsMax"] = nodeconfigs.DefaultMaxThreadsMax
this.Data["defaultNodeTCPMaxConnections"] = nodeconfigs.DefaultTCPMaxConnections
this.Show()
}
// RunPost 保存设置
func (this *IndexAction) RunPost(params struct {
ClusterId int64
Name string
GrantId int64
InstallDir string
TimeZone string
ClusterId int64
Name string
GrantId int64
InstallDir string
TimeZone string
NodeMaxThreads int32
NodeTCPMaxConnections int32
Must *actions.Must
}) {
@@ -91,12 +102,21 @@ func (this *IndexAction) RunPost(params struct {
Field("name", params.Name).
Require("请输入集群名称")
if params.NodeMaxThreads > 0 {
params.Must.
Field("nodeMaxThreads", params.NodeMaxThreads).
Gte(int64(nodeconfigs.DefaultMaxThreadsMin), "单节点最大线程数最小值不能小于"+types.String(nodeconfigs.DefaultMaxThreadsMin)).
Lte(int64(nodeconfigs.DefaultMaxThreadsMax), "单节点最大线程数最大值不能大于"+types.String(nodeconfigs.DefaultMaxThreadsMax))
}
_, err := this.RPC().NodeClusterRPC().UpdateNodeCluster(this.AdminContext(), &pb.UpdateNodeClusterRequest{
NodeClusterId: params.ClusterId,
Name: params.Name,
NodeGrantId: params.GrantId,
InstallDir: params.InstallDir,
TimeZone: params.TimeZone,
NodeClusterId: params.ClusterId,
Name: params.Name,
NodeGrantId: params.GrantId,
InstallDir: params.InstallDir,
TimeZone: params.TimeZone,
NodeMaxThreads: params.NodeMaxThreads,
NodeTCPMaxConnections: params.NodeTCPMaxConnections,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -30,6 +30,7 @@ func (this *CreateAction) RunPost(params struct {
PrivateKey string
Passphrase string
Description string
Su bool
Must *actions.Must
}) {
@@ -61,6 +62,7 @@ func (this *CreateAction) RunPost(params struct {
PrivateKey: params.PrivateKey,
Passphrase: params.Passphrase,
Description: params.Description,
Su: params.Su,
NodeId: 0,
})
if err != nil {

View File

@@ -31,6 +31,7 @@ func (this *CreatePopupAction) RunPost(params struct {
PrivateKey string
Passphrase string
Description string
Su bool
Must *actions.Must
}) {
@@ -62,6 +63,7 @@ func (this *CreatePopupAction) RunPost(params struct {
PrivateKey: params.PrivateKey,
Passphrase: params.Passphrase,
Description: params.Description,
Su: params.Su,
NodeId: 0,
})
if err != nil {
@@ -74,6 +76,7 @@ func (this *CreatePopupAction) RunPost(params struct {
"name": params.Name,
"method": params.Method,
"methodName": grantutils.FindGrantMethodName(params.Method),
"username": params.Username,
}
// 创建日志

View File

@@ -60,6 +60,7 @@ func (this *UpdateAction) RunPost(params struct {
PrivateKey string
Passphrase string
Description string
Su bool
Must *actions.Must
}) {
@@ -97,6 +98,7 @@ func (this *UpdateAction) RunPost(params struct {
PrivateKey: params.PrivateKey,
Passphrase: params.Passphrase,
Description: params.Description,
Su: params.Su,
NodeId: 0,
})
if err != nil {

View File

@@ -44,6 +44,7 @@ func (this *UpdatePopupAction) RunGet(params struct {
"description": grant.Description,
"privateKey": grant.PrivateKey,
"passphrase": grant.Passphrase,
"su": grant.Su,
}
this.Show()
@@ -59,6 +60,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
PrivateKey string
Passphrase string
Description string
Su bool
Must *actions.Must
}) {
@@ -96,6 +98,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
Passphrase: params.Passphrase,
Description: params.Description,
NodeId: params.NodeId,
Su: params.Su,
})
if err != nil {
this.ErrorPage(err)
@@ -108,6 +111,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
"name": params.Name,
"method": params.Method,
"methodName": grantutils.FindGrantMethodName(params.Method),
"username": params.Username,
}
this.Success()

View File

@@ -10,7 +10,9 @@ type CheckAction struct {
}
func (this *CheckAction) RunPost(params struct{}) {
resp, err := this.RPC().NodeTaskRPC().ExistsNodeTasks(this.AdminContext(), &pb.ExistsNodeTasksRequest{})
resp, err := this.RPC().NodeTaskRPC().ExistsNodeTasks(this.AdminContext(), &pb.ExistsNodeTasksRequest{
ExcludeTypes: []string{"ipItemChanged"},
})
if err != nil {
this.ErrorPage(err)
return

View File

@@ -3,6 +3,7 @@ package ipAddresses
import (
"encoding/json"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/iwind/TeaGo/actions"
@@ -39,9 +40,15 @@ func (this *CreatePopupAction) RunPost(params struct {
Field("ip", params.IP).
Require("请输入IP地址")
ip := net.ParseIP(params.IP)
if len(ip) == 0 {
this.FailField("ip", "请输入正确的IP")
result, err := utils.ExtractIP(params.IP)
if err != nil {
this.Fail("IP格式错误'" + params.IP + "'")
}
for _, ip := range result {
if len(net.ParseIP(ip)) == 0 {
this.FailField("ip", "请输入正确的IP")
}
}
// 阈值设置

View File

@@ -2,6 +2,7 @@ package ipaddressutils
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
@@ -16,8 +17,11 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
return err
}
for _, addr := range addresses {
var resultAddrIds = []int64{}
addrId := addr.GetInt64("id")
if addrId > 0 {
resultAddrIds = append(resultAddrIds, addrId)
var isOn = false
if !addr.Has("isOn") { // 兼容老版本
isOn = true
@@ -37,18 +41,40 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
return err
}
} else {
createResp, err := parentAction.RPC().NodeIPAddressRPC().CreateNodeIPAddress(parentAction.AdminContext(), &pb.CreateNodeIPAddressRequest{
NodeId: nodeId,
Role: role,
Name: addr.GetString("name"),
Ip: addr.GetString("ip"),
CanAccess: addr.GetBool("canAccess"),
IsUp: addr.GetBool("isUp"),
})
if err != nil {
return err
var ipStrings = addr.GetString("ip")
result, _ := utils.ExtractIP(ipStrings)
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"),
})
if err != nil {
return err
}
addrId = createResp.NodeIPAddressId
resultAddrIds = append(resultAddrIds, addrId)
} 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,
})
if err != nil {
return err
}
resultAddrIds = append(resultAddrIds, createResp.NodeIPAddressIds...)
}
addrId = createResp.NodeIPAddressId
}
// 保存阈值
@@ -58,12 +84,25 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
if err != nil {
return err
}
_, err = parentAction.RPC().NodeIPAddressThresholdRPC().UpdateAllNodeIPAddressThresholds(parentAction.AdminContext(), &pb.UpdateAllNodeIPAddressThresholdsRequest{
NodeIPAddressId: addrId,
NodeIPAddressThresholdsJSON: thresholdsJSON,
})
if err != nil {
return err
for _, addrId := range resultAddrIds {
_, err = parentAction.RPC().NodeIPAddressThresholdRPC().UpdateAllNodeIPAddressThresholds(parentAction.AdminContext(), &pb.UpdateAllNodeIPAddressThresholdsRequest{
NodeIPAddressId: addrId,
NodeIPAddressThresholdsJSON: thresholdsJSON,
})
if err != nil {
return err
}
}
} else {
for _, addrId := range resultAddrIds {
_, err = parentAction.RPC().NodeIPAddressThresholdRPC().UpdateAllNodeIPAddressThresholds(parentAction.AdminContext(), &pb.UpdateAllNodeIPAddressThresholdsRequest{
NodeIPAddressId: addrId,
NodeIPAddressThresholdsJSON: []byte("[]"),
})
if err != nil {
return err
}
}
}
}

View File

@@ -3,6 +3,7 @@ package ipAddresses
import (
"encoding/json"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
@@ -57,9 +58,22 @@ func (this *UpdatePopupAction) RunPost(params struct {
}
}
ip := net.ParseIP(params.IP)
if len(ip) == 0 {
this.Fail("请输入正确的IP")
if params.AddressId > 0 {
ip := net.ParseIP(params.IP)
if len(ip) == 0 {
this.Fail("请输入正确的IP")
}
} else {
result, err := utils.ExtractIP(params.IP)
if err != nil {
this.Fail("IP格式错误'" + params.IP + "'")
}
for _, ip := range result {
if len(net.ParseIP(ip)) == 0 {
this.FailField("ip", "请输入正确的IP")
}
}
}
var thresholds = []*nodeconfigs.IPAddressThresholdConfig{}

View File

@@ -3,6 +3,7 @@ package cluster
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
@@ -87,14 +88,31 @@ func (this *CreateNodeAction) RunPost(params struct {
NodeId: nodeId,
})
} else {
_, err = this.RPC().NodeIPAddressRPC().CreateNodeIPAddress(this.AdminContext(), &pb.CreateNodeIPAddressRequest{
NodeId: nodeId,
Role: nodeconfigs.NodeRoleDNS,
Name: addrMap.GetString("name"),
Ip: addrMap.GetString("ip"),
CanAccess: addrMap.GetBool("canAccess"),
IsUp: addrMap.GetBool("isUp"),
})
var ipStrings = addrMap.GetString("ip")
result, _ := utils.ExtractIP(ipStrings)
if len(result) == 1 {
// 单个创建
_, err = this.RPC().NodeIPAddressRPC().CreateNodeIPAddress(this.AdminContext(), &pb.CreateNodeIPAddressRequest{
NodeId: nodeId,
Role: nodeconfigs.NodeRoleDNS,
Name: addrMap.GetString("name"),
Ip: result[0],
CanAccess: addrMap.GetBool("canAccess"),
IsUp: addrMap.GetBool("isUp"),
})
} else if len(result) > 1 {
// 批量创建
_, err = this.RPC().NodeIPAddressRPC().CreateNodeIPAddresses(this.AdminContext(), &pb.CreateNodeIPAddressesRequest{
NodeId: nodeId,
Role: nodeconfigs.NodeRoleDNS,
Name: addrMap.GetString("name"),
IpList: result,
CanAccess: addrMap.GetBool("canAccess"),
IsUp: addrMap.GetBool("isUp"),
GroupValue: ipStrings,
})
}
}
if err != nil {
this.ErrorPage(err)

View File

@@ -30,6 +30,7 @@ func (this *CreateGroupPopupAction) RunPost(params struct {
Type string
Name string
Code string
Description string
IsOn bool
@@ -52,6 +53,7 @@ func (this *CreateGroupPopupAction) RunPost(params struct {
createResp, err := this.RPC().HTTPFirewallRuleGroupRPC().CreateHTTPFirewallRuleGroup(this.AdminContext(), &pb.CreateHTTPFirewallRuleGroupRequest{
IsOn: params.IsOn,
Name: params.Name,
Code: params.Code,
Description: params.Description,
})
if err != nil {

View File

@@ -73,6 +73,7 @@ func (this *CreateSetPopupAction) RunPost(params struct {
RulesJSON []byte
Connector string
ActionsJSON []byte
IgnoreLocal bool
Must *actions.Must
}) {
@@ -124,6 +125,7 @@ func (this *CreateSetPopupAction) RunPost(params struct {
RuleRefs: nil,
Rules: rules,
Actions: actionConfigs,
IgnoreLocal: params.IgnoreLocal,
}
setConfigJSON, err := json.Marshal(setConfig)

View File

@@ -32,24 +32,36 @@ func (this *ExportAction) RunGet(params struct {
return
}
inboundGroups := []*firewallconfigs.HTTPFirewallRuleGroup{}
outboundGroups := []*firewallconfigs.HTTPFirewallRuleGroup{}
enabledInboundGroups := []*firewallconfigs.HTTPFirewallRuleGroup{}
enabledOutboundGroups := []*firewallconfigs.HTTPFirewallRuleGroup{}
disabledInboundGroups := []*firewallconfigs.HTTPFirewallRuleGroup{}
disabledOutboundGroups := []*firewallconfigs.HTTPFirewallRuleGroup{}
if policy.Inbound != nil {
for _, g := range policy.Inbound.Groups {
if g.IsOn {
inboundGroups = append(inboundGroups, g)
enabledInboundGroups = append(enabledInboundGroups, g)
} else {
disabledInboundGroups = append(disabledInboundGroups, g)
}
}
}
if policy.Outbound != nil {
for _, g := range policy.Outbound.Groups {
if g.IsOn {
outboundGroups = append(outboundGroups, g)
enabledOutboundGroups = append(enabledOutboundGroups, g)
} else {
disabledOutboundGroups = append(disabledOutboundGroups, g)
}
}
}
this.Data["inboundGroups"] = inboundGroups
this.Data["outboundGroups"] = outboundGroups
this.Data["enabledInboundGroups"] = enabledInboundGroups
this.Data["enabledOutboundGroups"] = enabledOutboundGroups
this.Data["disabledInboundGroups"] = disabledInboundGroups
this.Data["disabledOutboundGroups"] = disabledOutboundGroups
this.Show()
}
@@ -116,5 +128,6 @@ func (this *ExportAction) RunPost(params struct {
ttlcache.DefaultCache.Write(key, configJSON, time.Now().Unix()+600)
this.Data["key"] = key
this.Data["id"] = params.FirewallPolicyId
this.Success()
}

View File

@@ -3,6 +3,7 @@ package waf
import (
"github.com/TeaOSLab/EdgeAdmin/internal/ttlcache"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/types"
"strconv"
)
@@ -15,7 +16,8 @@ func (this *ExportDownloadAction) Init() {
}
func (this *ExportDownloadAction) RunGet(params struct {
Key string
Key string
PolicyId int64
}) {
item := ttlcache.DefaultCache.Read(params.Key)
if item == nil || item.Value == nil {
@@ -27,7 +29,7 @@ func (this *ExportDownloadAction) RunGet(params struct {
data, ok := item.Value.([]byte)
if ok {
this.AddHeader("Content-Disposition", "attachment; filename=\"WAF.json\";")
this.AddHeader("Content-Disposition", "attachment; filename=\"WAF-"+types.String(params.PolicyId)+".json\";")
this.AddHeader("Content-Length", strconv.Itoa(len(data)))
this.Write(data)
} else {

View File

@@ -43,7 +43,8 @@ func (this *GroupsAction) RunGet(params struct {
"isOn": g.IsOn,
"description": g.Description,
"countSets": len(g.Sets),
"canDelete": len(g.Code) == 0,
"isTemplate": g.IsTemplate,
"canDelete": !g.IsTemplate,
})
}
}
@@ -60,7 +61,8 @@ func (this *GroupsAction) RunGet(params struct {
"isOn": g.IsOn,
"description": g.Description,
"countSets": len(g.Sets),
"canDelete": len(g.Code) == 0,
"isTemplate": g.IsTemplate,
"canDelete": !g.IsTemplate,
})
}
}

View File

@@ -41,6 +41,7 @@ func init() {
GetPost("/updateSetPopup", new(UpdateSetPopupAction)).
Post("/count", new(CountAction)).
Get("/selectPopup", new(SelectPopupAction)).
Post("/testRegexp", new(TestRegexpAction)).
// IP管理
GetPost("/ipadmin", new(ipadmin.IndexAction)).

View File

@@ -0,0 +1,48 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package waf
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
"regexp"
"strings"
)
type TestRegexpAction struct {
actionutils.ParentAction
}
func (this *TestRegexpAction) RunPost(params struct {
Regexp string
IsCaseInsensitive bool
Body string
}) {
var exp = params.Regexp
if params.IsCaseInsensitive && !strings.HasPrefix(params.Regexp, "(?i)") {
exp = "(?i)" + exp
}
reg, err := regexp.Compile(exp)
if err != nil {
this.Data["result"] = maps.Map{
"isOk": false,
"message": "解析正则出错:" + err.Error(),
}
this.Success()
}
if reg.MatchString(params.Body) {
this.Data["result"] = maps.Map{
"isOk": true,
"message": "匹配成功",
}
this.Success()
}
this.Data["result"] = maps.Map{
"isOk": false,
"message": "匹配失败",
}
this.Success()
}

View File

@@ -35,6 +35,7 @@ func (this *UpdateGroupPopupAction) RunGet(params struct {
"name": groupConfig.Name,
"description": groupConfig.Description,
"isOn": groupConfig.IsOn,
"code": groupConfig.Code,
}
this.Show()
@@ -43,6 +44,7 @@ func (this *UpdateGroupPopupAction) RunGet(params struct {
func (this *UpdateGroupPopupAction) RunPost(params struct {
GroupId int64
Name string
Code string
Description string
IsOn bool
@@ -59,6 +61,7 @@ func (this *UpdateGroupPopupAction) RunPost(params struct {
FirewallRuleGroupId: params.GroupId,
IsOn: params.IsOn,
Name: params.Name,
Code: params.Code,
Description: params.Description,
})
if err != nil {

View File

@@ -97,6 +97,7 @@ func (this *UpdateSetPopupAction) RunPost(params struct {
RulesJSON []byte
Connector string
ActionsJSON []byte
IgnoreLocal bool
Must *actions.Must
}) {
@@ -144,6 +145,7 @@ func (this *UpdateSetPopupAction) RunPost(params struct {
setConfig.Connector = params.Connector
setConfig.Rules = rules
setConfig.Actions = actionConfigs
setConfig.IgnoreLocal = params.IgnoreLocal
setConfigJSON, err := json.Marshal(setConfig)
if err != nil {

View File

@@ -89,6 +89,7 @@ func (this *CreateAction) RunPost(params struct {
CacheIsOn bool
WafIsOn bool
RemoteAddrIsOn bool
StatIsOn bool
WebRoot string
@@ -466,7 +467,7 @@ func (this *CreateAction) RunPost(params struct {
AccessLogJSON: []byte(`{
"isPrior": false,
"isOn": true,
"fields": [],
"fields": [1, 2, 6, 7],
"status1": true,
"status2": true,
"status3": true,
@@ -582,6 +583,27 @@ func (this *CreateAction) RunPost(params struct {
this.ErrorPage(err)
return
}
// 统计
if params.StatIsOn {
var statConfig = &serverconfigs.HTTPStatRef{
IsPrior: false,
IsOn: true,
}
statJSON, err := json.Marshal(statConfig)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().HTTPWebRPC().UpdateHTTPWebStat(this.AdminContext(), &pb.UpdateHTTPWebStatRequest{
HttpWebId: webConfig.Id,
StatJSON: statJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
}
}
}

View File

@@ -12,9 +12,9 @@ type FixLogAction struct {
}
func (this *FixLogAction) RunPost(params struct {
LogId int64
LogIds []int64
}) {
_, err := this.RPC().NodeLogRPC().FixNodeLog(this.AdminContext(), &pb.FixNodeLogRequest{NodeLogId: params.LogId})
_, err := this.RPC().NodeLogRPC().FixNodeLogs(this.AdminContext(), &pb.FixNodeLogsRequest{NodeLogIds: params.LogIds})
if err != nil {
this.ErrorPage(err)
return

View File

@@ -144,6 +144,15 @@ func InitGroup(parent *actionutils.ParentAction, groupId int64, menuItem string)
"isActive": menuItem == "remoteAddr",
"isOn": configInfoResp.HasRemoteAddrConfig,
})
if teaconst.IsPlus {
leftMenuItems = append(leftMenuItems, maps.Map{
"name": "请求限制",
"url": urlPrefix + "/requestLimit?groupId=" + types.String(groupId),
"isActive": menuItem == "requestLimit",
"isOn": configInfoResp.HasRequestLimitConfig,
})
}
parent.Data["leftMenuItems"] = leftMenuItems
}

View File

@@ -0,0 +1,62 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package requestlimit
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/groups/group/servergrouputils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "setting", "index")
this.SecondMenu("requestLimit")
}
func (this *IndexAction) RunGet(params struct {
GroupId int64
}) {
_, err := servergrouputils.InitGroup(this.Parent(), params.GroupId, "requestLimit")
if err != nil {
this.ErrorPage(err)
return
}
webConfig, err := dao.SharedHTTPWebDAO.FindWebConfigWithServerGroupId(this.AdminContext(), params.GroupId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["webId"] = webConfig.Id
this.Data["requestLimitConfig"] = webConfig.RequestLimit
this.Show()
}
func (this *IndexAction) RunPost(params struct {
WebId int64
RequestLimitJSON []byte
Must *actions.Must
CSRF *actionutils.CSRF
}) {
defer this.CreateLogInfo("修改Web %d 请求限制", params.WebId)
_, err := this.RPC().HTTPWebRPC().UpdateHTTPWebRequestLimit(this.AdminContext(), &pb.UpdateHTTPWebRequestLimitRequest{
HttpWebId: params.WebId,
RequestLimitJSON: params.RequestLimitJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,19 @@
package requestlimit
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeServer)).
Data("teaMenu", "servers").
Data("teaSubMenu", "group").
Prefix("/servers/groups/group/settings/requestLimit").
GetPost("", new(IndexAction)).
EndAll()
})
}

View File

@@ -199,6 +199,12 @@ func (this *IndexAction) RunGet(params struct {
}
}
// 提交审核时间
var auditingTime = ""
if server.AuditingAt > 0 {
auditingTime = timeutil.FormatTime("Y-m-d", server.AuditingAt)
}
serverMaps = append(serverMaps, maps.Map{
"id": server.Id,
"isOn": server.IsOn,
@@ -215,6 +221,7 @@ func (this *IndexAction) RunGet(params struct {
"isAuditing": server.IsAuditing,
"auditingIsOk": auditingIsOk,
"user": userMap,
"auditingTime": auditingTime,
})
}
this.Data["servers"] = serverMaps
@@ -258,7 +265,7 @@ func (this *IndexAction) RunGet(params struct {
NodeId: 0,
Role: nodeconfigs.NodeRoleNode,
Offset: 0,
Size: 10,
Size: 20,
Level: "",
FixedState: int32(configutils.BoolStateNo),
AllServers: true,
@@ -279,7 +286,7 @@ func (this *IndexAction) RunGet(params struct {
var server = serverResp.Server
if server == nil {
// 设置为已修复
_, err = this.RPC().NodeLogRPC().FixNodeLog(this.AdminContext(), &pb.FixNodeLogRequest{NodeLogId: errorLog.Id})
_, err = this.RPC().NodeLogRPC().FixNodeLogs(this.AdminContext(), &pb.FixNodeLogsRequest{NodeLogIds: []int64{errorLog.Id}})
if err != nil {
this.ErrorPage(err)
return
@@ -297,7 +304,7 @@ func (this *IndexAction) RunGet(params struct {
var node = nodeResp.Node
if node == nil || node.NodeCluster == nil {
// 设置为已修复
_, err = this.RPC().NodeLogRPC().FixNodeLog(this.AdminContext(), &pb.FixNodeLogRequest{NodeLogId: errorLog.Id})
_, err = this.RPC().NodeLogRPC().FixNodeLogs(this.AdminContext(), &pb.FixNodeLogsRequest{NodeLogIds: []int64{errorLog.Id}})
if err != nil {
this.ErrorPage(err)
return

View File

@@ -14,19 +14,30 @@ type AddIPAction struct {
}
func (this *AddIPAction) RunPost(params struct {
ListId int64
Ip string
ListId int64
Ip string
ExpiredAt int64
}) {
var itemId int64 = 0
defer func() {
this.CreateLogInfo("在名单 %d 中创建IP %d", params.ListId, itemId)
}()
var ipType = "ipv4"
if strings.Contains(params.Ip, ":") {
ipType = "ipv6"
}
_, err := this.RPC().IPItemRPC().CreateIPItem(this.AdminContext(), &pb.CreateIPItemRequest{
if params.ExpiredAt <= 0 {
params.ExpiredAt = time.Now().Unix() + 86400
}
createResp, err := this.RPC().IPItemRPC().CreateIPItem(this.AdminContext(), &pb.CreateIPItemRequest{
IpListId: params.ListId,
IpFrom: params.Ip,
IpTo: "",
ExpiredAt: time.Now().Unix() + 86400, // TODO 可以自定义时间
ExpiredAt: params.ExpiredAt,
Reason: "从IPBox中加入名单",
Type: ipType,
EventLevel: "critical",
@@ -36,5 +47,7 @@ func (this *AddIPAction) RunPost(params struct {
return
}
itemId = createResp.IpItemId
this.Success()
}

View File

@@ -0,0 +1,27 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package ipbox
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type DeleteFromListAction struct {
actionutils.ParentAction
}
func (this *DeleteFromListAction) RunPost(params struct {
ListId int64
ItemId int64
}) {
defer this.CreateLogInfo("从IP名单 %d 中删除IP %d", params.ListId, params.ItemId)
_, err := this.RPC().IPItemRPC().DeleteIPItem(this.AdminContext(), &pb.DeleteIPItemRequest{IpItemId: params.ItemId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -37,17 +37,44 @@ func (this *IndexAction) RunGet(params struct {
this.Data["isp"] = regionResp.IpRegion.Isp
// IP列表
ipListResp, err := this.RPC().IPListRPC().FindEnabledIPListContainsIP(this.AdminContext(), &pb.FindEnabledIPListContainsIPRequest{Ip: params.Ip})
ipListResp, err := this.RPC().IPListRPC().FindEnabledIPListContainsIP(this.AdminContext(), &pb.FindEnabledIPListContainsIPRequest{
Ip: params.Ip,
})
if err != nil {
this.ErrorPage(err)
return
}
var ipListMaps = []maps.Map{}
for _, ipList := range ipListResp.IpLists {
itemsResp, err := this.RPC().IPItemRPC().ListIPItemsWithListId(this.AdminContext(), &pb.ListIPItemsWithListIdRequest{
IpListId: ipList.Id,
Keyword: "",
IpFrom: params.Ip,
IpTo: "",
Offset: 0,
Size: 1,
})
if err != nil {
this.ErrorPage(err)
return
}
var items = itemsResp.IpItems
if len(items) == 0 {
continue
}
var item = items[0]
var expiredTime = ""
if item.ExpiredAt > 0 {
expiredTime = timeutil.FormatTime("Y-m-d H:i:s", item.ExpiredAt)
}
ipListMaps = append(ipListMaps, maps.Map{
"id": ipList.Id,
"name": ipList.Name,
"type": ipList.Type,
"id": ipList.Id,
"name": ipList.Name,
"type": ipList.Type,
"itemExpiredTime": expiredTime,
"itemId": item.Id,
})
}
this.Data["ipLists"] = ipListMaps
@@ -58,7 +85,7 @@ func (this *IndexAction) RunGet(params struct {
IsPublic: true,
Keyword: "",
Offset: 0,
Size: 10, // TODO 将来考虑到支持更多的黑名单
Size: 20, // TODO 将来考虑到支持更多的黑名单
})
if err != nil {
this.ErrorPage(err)

View File

@@ -15,6 +15,7 @@ func init() {
Prefix("/servers/ipbox").
Get("", new(IndexAction)).
Post("/addIP", new(AddIPAction)).
Post("/deleteFromList", new(DeleteFromListAction)).
EndAll()
})
}

View File

@@ -3,12 +3,14 @@
package logs
import (
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/lists"
timeutil "github.com/iwind/TeaGo/utils/time"
"regexp"
"strings"
"time"
)
type IndexAction struct {
@@ -29,6 +31,8 @@ func (this *IndexAction) RunGet(params struct {
RequestId string
ServerId int64
PageSize int64
}) {
if len(params.Day) == 0 {
params.Day = timeutil.Format("Y-m-d")
@@ -43,16 +47,23 @@ func (this *IndexAction) RunGet(params struct {
this.Data["accessLogs"] = []interface{}{}
this.Data["hasError"] = params.HasError
this.Data["hasWAF"] = params.HasWAF
this.Data["pageSize"] = params.PageSize
this.Data["isSlowQuery"] = false
this.Data["slowQueryCost"] = ""
day := params.Day
ipList := []string{}
if len(day) > 0 && regexp.MustCompile(`\d{4}-\d{2}-\d{2}`).MatchString(day) {
day = strings.ReplaceAll(day, "-", "")
size := int64(10)
size := params.PageSize
if size < 1 {
size = 20
}
this.Data["hasError"] = params.HasError
var before = time.Now()
resp, err := this.RPC().HTTPAccessLogRPC().ListHTTPAccessLogs(this.AdminContext(), &pb.ListHTTPAccessLogsRequest{
RequestId: params.RequestId,
ServerId: params.ServerId,
@@ -69,6 +80,12 @@ func (this *IndexAction) RunGet(params struct {
return
}
var cost = time.Since(before).Seconds()
if cost > 5 {
this.Data["slowQueryCost"] = fmt.Sprintf("%.2f", cost)
this.Data["isSlowQuery"] = true
}
if len(resp.HttpAccessLogs) == 0 {
this.Data["accessLogs"] = []interface{}{}
} else {

View File

@@ -16,6 +16,7 @@ func init() {
Data("teaSubMenu", "log").
Prefix("/servers/logs").
Get("", new(IndexAction)).
GetPost("/settings", new(SettingsAction)).
EndAll()
})
}

View File

@@ -0,0 +1,94 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package logs
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/iwind/TeaGo/actions"
)
type SettingsAction struct {
actionutils.ParentAction
}
func (this *SettingsAction) Init() {
this.Nav("", "", "settings")
}
func (this *SettingsAction) RunGet(params struct{}) {
settingsResp, err := this.RPC().SysSettingRPC().ReadSysSetting(this.AdminContext(), &pb.ReadSysSettingRequest{Code: systemconfigs.SettingCodeAccessLogQueue})
if err != nil {
this.ErrorPage(err)
return
}
var config = &serverconfigs.AccessLogQueueConfig{
MaxLength: 0,
CountPerSecond: 0,
Percent: 100,
}
if len(settingsResp.ValueJSON) > 0 {
err = json.Unmarshal(settingsResp.ValueJSON, config)
if err != nil {
this.ErrorPage(err)
return
}
} else {
configJSON, err := json.Marshal(config)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().SysSettingRPC().UpdateSysSetting(this.AdminContext(), &pb.UpdateSysSettingRequest{
Code: systemconfigs.SettingCodeAccessLogQueue,
ValueJSON: configJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
}
this.Data["config"] = config
this.Show()
}
func (this *SettingsAction) RunPost(params struct {
Percent int
CountPerSecond int
MaxLength int
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.
Field("percent", params.Percent).
Gte(0, "请输入大于0的整数").
Lte(100, "请输入小于100的整数")
var config = &serverconfigs.AccessLogQueueConfig{
MaxLength: params.MaxLength,
CountPerSecond: params.CountPerSecond,
Percent: params.Percent,
}
configJSON, err := json.Marshal(config)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().SysSettingRPC().UpdateSysSetting(this.AdminContext(), &pb.UpdateSysSettingRequest{
Code: systemconfigs.SettingCodeAccessLogQueue,
ValueJSON: configJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -1,7 +1,9 @@
package boards
import (
"fmt"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
@@ -120,6 +122,22 @@ func (this *IndexAction) RunPost(params struct {
this.Data["topDomainStats"] = statMaps
}
// 地区排行
{
var countryMaps = []maps.Map{}
for _, stat := range resp.TopCountryStats {
countryMaps = append(countryMaps, maps.Map{
"name": stat.CountryName,
"bytes": stat.Bytes,
"formattedBytes": numberutils.FormatBytes(stat.Bytes),
"countRequests": stat.CountRequests,
"countAttackRequests": stat.CountAttackRequests,
"percent": fmt.Sprintf("%.2f", stat.Percent),
})
}
this.Data["topCountryStats"] = countryMaps
}
// 指标
{
var chartMaps = []maps.Map{}
@@ -157,5 +175,6 @@ func (this *IndexAction) RunPost(params struct {
}
this.Data["metricCharts"] = chartMaps
}
this.Success()
}

View File

@@ -28,6 +28,8 @@ func (this *HistoryAction) RunGet(params struct {
RequestId string
HasError int
PageSize int
}) {
if len(params.Day) == 0 {
params.Day = timeutil.Format("Y-m-d")
@@ -41,13 +43,17 @@ func (this *HistoryAction) RunGet(params struct {
this.Data["accessLogs"] = []interface{}{}
this.Data["hasError"] = params.HasError
this.Data["hasWAF"] = params.HasWAF
this.Data["pageSize"] = params.PageSize
day := params.Day
ipList := []string{}
if len(day) > 0 && regexp.MustCompile(`\d{4}-\d{2}-\d{2}`).MatchString(day) {
day = strings.ReplaceAll(day, "-", "")
size := int64(10)
size := int64(params.PageSize)
if size < 1 {
size = 20
}
this.Data["hasError"] = params.HasError

View File

@@ -24,8 +24,15 @@ func (this *TodayAction) RunGet(params struct {
Keyword string
Ip string
Domain string
PageSize int
}) {
size := int64(10)
this.Data["pageSize"] = params.PageSize
size := int64(params.PageSize)
if size < 1 {
size = 20
}
this.Data["path"] = this.Request.URL.Path
this.Data["hasError"] = params.HasError

View File

@@ -5,6 +5,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
"net/http"
"strings"
)
type ViewPopupAction struct {
@@ -98,5 +99,19 @@ func (this *ViewPopupAction) RunGet(params struct {
}
this.Data["region"] = regionMap
// 请求内容
this.Data["requestBody"] = string(accessLog.RequestBody)
this.Data["requestContentType"] = "text/plain"
requestContentType, ok := accessLog.Header["Content-Type"]
if ok {
if len(requestContentType.Values) > 0 {
var contentType = requestContentType.Values[0]
if strings.HasSuffix(contentType, "/json") || strings.Contains(contentType, "/json;") {
this.Data["requestContentType"] = "application/json"
}
}
}
this.Show()
}

View File

@@ -10,12 +10,13 @@ import (
)
type CondJSComponent struct {
Type string `json:"type"`
Name string `json:"name"`
Description string `json:"description"`
Component string `json:"component"`
ParamsTitle string `json:"paramsTitle"`
IsRequest bool `json:"isRequest"`
Type string `json:"type"`
Name string `json:"name"`
Description string `json:"description"`
Component string `json:"component"`
ParamsTitle string `json:"paramsTitle"`
IsRequest bool `json:"isRequest"`
CaseInsensitive bool `json:"caseInsensitive"`
}
// ReadAllAvailableCondTypes 读取所有可用的条件

View File

@@ -19,8 +19,10 @@ func (this *CreateSetPopupAction) Init() {
func (this *CreateSetPopupAction) RunGet(params struct {
HeaderPolicyId int64
Type string
}) {
this.Data["headerPolicyId"] = params.HeaderPolicyId
this.Data["type"] = params.Type
this.Show()
}
@@ -30,6 +32,14 @@ func (this *CreateSetPopupAction) RunPost(params struct {
Name string
Value string
StatusListJSON []byte
MethodsJSON []byte
DomainsJSON []byte
ShouldAppend bool
DisableRedirect bool
ShouldReplace bool
ReplaceValuesJSON []byte
Must *actions.Must
}) {
// 日志
@@ -51,10 +61,57 @@ func (this *CreateSetPopupAction) RunPost(params struct {
return
}
// status list
var statusList = []int32{}
if len(params.StatusListJSON) > 0 {
err = json.Unmarshal(params.StatusListJSON, &statusList)
if err != nil {
this.ErrorPage(err)
return
}
}
// methods
var methods = []string{}
if len(params.MethodsJSON) > 0 {
err = json.Unmarshal(params.MethodsJSON, &methods)
if err != nil {
this.ErrorPage(err)
return
}
}
// domains
var domains = []string{}
if len(params.DomainsJSON) > 0 {
err = json.Unmarshal(params.DomainsJSON, &domains)
if err != nil {
this.ErrorPage(err)
return
}
}
// replace values
var replaceValues = []*shared.HTTPHeaderReplaceValue{}
if len(params.ReplaceValuesJSON) > 0 {
err = json.Unmarshal(params.ReplaceValuesJSON, &replaceValues)
if err != nil {
this.ErrorPage(err)
return
}
}
// 创建Header
createHeaderResp, err := this.RPC().HTTPHeaderRPC().CreateHTTPHeader(this.AdminContext(), &pb.CreateHTTPHeaderRequest{
Name: params.Name,
Value: params.Value,
Name: params.Name,
Value: params.Value,
Status: statusList,
Methods: methods,
Domains: domains,
ShouldAppend: params.ShouldAppend,
DisableRedirect: params.DisableRedirect,
ShouldReplace: params.ShouldReplace,
ReplaceValuesJSON: params.ReplaceValuesJSON,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -35,26 +35,6 @@ func (this *DeleteAction) RunPost(params struct {
}
switch params.Type {
case "addHeader":
result := []*shared.HTTPHeaderRef{}
for _, h := range policyConfig.AddHeaderRefs {
if h.HeaderId != params.HeaderId {
result = append(result, h)
}
}
resultJSON, err := json.Marshal(result)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().HTTPHeaderPolicyRPC().UpdateHTTPHeaderPolicyAddingHeaders(this.AdminContext(), &pb.UpdateHTTPHeaderPolicyAddingHeadersRequest{
HeaderPolicyId: params.HeaderPolicyId,
HeadersJSON: resultJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
case "setHeader":
result := []*shared.HTTPHeaderRef{}
for _, h := range policyConfig.SetHeaderRefs {
@@ -75,46 +55,6 @@ func (this *DeleteAction) RunPost(params struct {
this.ErrorPage(err)
return
}
case "replace":
result := []*shared.HTTPHeaderRef{}
for _, h := range policyConfig.ReplaceHeaderRefs {
if h.HeaderId != params.HeaderId {
result = append(result, h)
}
}
resultJSON, err := json.Marshal(result)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().HTTPHeaderPolicyRPC().UpdateHTTPHeaderPolicyReplacingHeaders(this.AdminContext(), &pb.UpdateHTTPHeaderPolicyReplacingHeadersRequest{
HeaderPolicyId: params.HeaderPolicyId,
HeadersJSON: resultJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
case "addTrailer":
result := []*shared.HTTPHeaderRef{}
for _, h := range policyConfig.AddTrailerRefs {
if h.HeaderId != params.HeaderId {
result = append(result, h)
}
}
resultJSON, err := json.Marshal(result)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().HTTPHeaderPolicyRPC().UpdateHTTPHeaderPolicyAddingTrailers(this.AdminContext(), &pb.UpdateHTTPHeaderPolicyAddingTrailersRequest{
HeaderPolicyId: params.HeaderPolicyId,
HeadersJSON: resultJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
}
this.Success()

View File

@@ -20,9 +20,11 @@ func (this *UpdateSetPopupAction) Init() {
func (this *UpdateSetPopupAction) RunGet(params struct {
HeaderPolicyId int64
HeaderId int64
Type string
}) {
this.Data["headerPolicyId"] = params.HeaderPolicyId
this.Data["headerId"] = params.HeaderId
this.Data["type"] = params.Type
headerResp, err := this.RPC().HTTPHeaderRPC().FindEnabledHTTPHeaderConfig(this.AdminContext(), &pb.FindEnabledHTTPHeaderConfigRequest{HeaderId: params.HeaderId})
if err != nil {
@@ -45,6 +47,14 @@ func (this *UpdateSetPopupAction) RunPost(params struct {
Name string
Value string
StatusListJSON []byte
MethodsJSON []byte
DomainsJSON []byte
ShouldAppend bool
DisableRedirect bool
ShouldReplace bool
ReplaceValuesJSON []byte
Must *actions.Must
}) {
// 日志
@@ -54,10 +64,57 @@ func (this *UpdateSetPopupAction) RunPost(params struct {
Field("name", params.Name).
Require("请输入Header名称")
// status list
var statusList = []int32{}
if len(params.StatusListJSON) > 0 {
err := json.Unmarshal(params.StatusListJSON, &statusList)
if err != nil {
this.ErrorPage(err)
return
}
}
// methods
var methods = []string{}
if len(params.MethodsJSON) > 0 {
err := json.Unmarshal(params.MethodsJSON, &methods)
if err != nil {
this.ErrorPage(err)
return
}
}
// domains
var domains = []string{}
if len(params.DomainsJSON) > 0 {
err := json.Unmarshal(params.DomainsJSON, &domains)
if err != nil {
this.ErrorPage(err)
return
}
}
// replace values
var replaceValues = []*shared.HTTPHeaderReplaceValue{}
if len(params.ReplaceValuesJSON) > 0 {
err := json.Unmarshal(params.ReplaceValuesJSON, &replaceValues)
if err != nil {
this.ErrorPage(err)
return
}
}
_, err := this.RPC().HTTPHeaderRPC().UpdateHTTPHeader(this.AdminContext(), &pb.UpdateHTTPHeaderRequest{
HeaderId: params.HeaderId,
Name: params.Name,
Value: params.Value,
HeaderId: params.HeaderId,
Name: params.Name,
Value: params.Value,
Status: statusList,
Methods: methods,
Domains: domains,
ShouldAppend: params.ShouldAppend,
DisableRedirect: params.DisableRedirect,
ShouldReplace: params.ShouldReplace,
ReplaceValuesJSON: params.ReplaceValuesJSON,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -51,8 +51,12 @@ func (this *CreateAction) RunPost(params struct {
IsReverse bool
CondsJSON []byte
DomainsJSON []byte
Must *actions.Must
}) {
defer this.CreateLogInfo("创建路由规则:%s", params.Pattern)
params.Must.
Field("pattern", params.Pattern).
Require("请输入路径匹配规则")
@@ -85,6 +89,21 @@ func (this *CreateAction) RunPost(params struct {
params.Pattern = "/" + strings.TrimLeft(params.Pattern, "/")
}
// 域名
var domains = []string{}
if len(params.DomainsJSON) > 0 {
err := json.Unmarshal(params.DomainsJSON, &domains)
if err != nil {
this.ErrorPage(err)
return
}
// 去除可能误加的斜杠
for index, domain := range domains {
domains[index] = strings.TrimSuffix(domain, "/")
}
}
location := &serverconfigs.HTTPLocationConfig{}
location.SetPattern(params.Pattern, params.PatternType, params.IsCaseInsensitive, params.IsReverse)
resultPattern := location.Pattern
@@ -96,6 +115,7 @@ func (this *CreateAction) RunPost(params struct {
Pattern: resultPattern,
IsBreak: params.IsBreak,
CondsJSON: params.CondsJSON,
Domains: domains,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -31,6 +31,7 @@ func (this *IndexAction) RunGet(params struct {
this.Data["isReverse"] = locationConfig.IsReverse()
this.Data["isCaseInsensitive"] = locationConfig.IsCaseInsensitive()
this.Data["conds"] = locationConfig.Conds
this.Data["domains"] = locationConfig.Domains
this.Show()
}
@@ -50,6 +51,8 @@ func (this *IndexAction) RunPost(params struct {
CondsJSON []byte
DomainsJSON []byte
Must *actions.Must
}) {
defer this.CreateLogInfo("修改路由规则 %d 设置", params.LocationId)
@@ -86,6 +89,21 @@ func (this *IndexAction) RunPost(params struct {
}
}
// 域名
var domains = []string{}
if len(params.DomainsJSON) > 0 {
err := json.Unmarshal(params.DomainsJSON, &domains)
if err != nil {
this.ErrorPage(err)
return
}
// 去除可能误加的斜杠
for index, domain := range domains {
domains[index] = strings.TrimSuffix(domain, "/")
}
}
location := &serverconfigs.HTTPLocationConfig{}
location.SetPattern(params.Pattern, params.PatternType, params.IsCaseInsensitive, params.IsReverse)
resultPattern := location.Pattern
@@ -98,6 +116,7 @@ func (this *IndexAction) RunPost(params struct {
IsBreak: params.IsBreak,
IsOn: params.IsOn,
CondsJSON: params.CondsJSON,
Domains: domains,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -0,0 +1,69 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package requestlimit
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/types"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "setting", "index")
this.SecondMenu("requestLimit")
}
func (this *IndexAction) RunGet(params struct {
ServerId int64
}) {
// 服务分组设置
groupResp, err := this.RPC().ServerGroupRPC().FindEnabledServerGroupConfigInfo(this.AdminContext(), &pb.FindEnabledServerGroupConfigInfoRequest{
ServerId: params.ServerId,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["hasGroupConfig"] = groupResp.HasRequestLimitConfig
this.Data["groupSettingURL"] = "/servers/groups/group/settings/requestLimit?groupId=" + types.String(groupResp.ServerGroupId)
this.Data["serverId"] = params.ServerId
webConfig, err := dao.SharedHTTPWebDAO.FindWebConfigWithServerId(this.AdminContext(), params.ServerId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["webId"] = webConfig.Id
this.Data["requestLimitConfig"] = webConfig.RequestLimit
this.Show()
}
func (this *IndexAction) RunPost(params struct {
WebId int64
RequestLimitJSON []byte
Must *actions.Must
CSRF *actionutils.CSRF
}) {
defer this.CreateLogInfo("修改Web %d 请求限制", params.WebId)
_, err := this.RPC().HTTPWebRPC().UpdateHTTPWebRequestLimit(this.AdminContext(), &pb.UpdateHTTPWebRequestLimitRequest{
HttpWebId: params.WebId,
RequestLimitJSON: params.RequestLimitJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,19 @@
package requestlimit
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/serverutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeServer)).
Helper(serverutils.NewServerHelper()).
Prefix("/servers/server/settings/requestLimit").
GetPost("", new(IndexAction)).
EndAll()
})
}

View File

@@ -30,8 +30,29 @@ func (this *IndexAction) RunGet(params struct {
return
}
// 已审核域名
var passedDomains = []string{}
if len(serverNamesResp.ServerNamesJSON) > 0 {
var passedServerNameConfigs = []*serverconfigs.ServerNameConfig{}
err = json.Unmarshal(serverNamesResp.ServerNamesJSON, &passedServerNameConfigs)
if err == nil {
passedDomains = serverconfigs.PlainServerNames(passedServerNameConfigs)
if passedDomains == nil {
passedDomains = []string{}
}
}
}
this.Data["passedDomains"] = passedDomains
// 提交审核时间
var auditingTime = ""
if serverNamesResp.AuditingAt > 0 {
auditingTime = timeutil.FormatTime("Y-m-d", serverNamesResp.AuditingAt)
}
serverNamesConfig := []*serverconfigs.ServerNameConfig{}
this.Data["isAuditing"] = serverNamesResp.IsAuditing
this.Data["auditingTime"] = auditingTime
this.Data["auditingResult"] = maps.Map{
"isOk": true,
}

View File

@@ -68,6 +68,11 @@ func (this *ServerHelper) createLeftMenu(action *actions.ActionObject) {
return
}
// 初始化数据
if !action.Data.Has("server") {
action.Data["server"] = maps.Map{"id": server.Id, "name": server.Name}
}
// 服务管理
serverConfig := &serverconfigs.ServerConfig{}
err = json.Unmarshal(server.Config, serverConfig)
@@ -120,7 +125,17 @@ func (this *ServerHelper) createLeftMenu(action *actions.ActionObject) {
case "stat":
action.Data["leftMenuItems"] = this.createStatMenu(types.String(secondMenuItem), serverIdString, serverConfig)
case "setting":
action.Data["leftMenuItems"] = this.createSettingsMenu(types.String(secondMenuItem), serverIdString, serverConfig)
var menuItems = this.createSettingsMenu(types.String(secondMenuItem), serverIdString, serverConfig)
action.Data["leftMenuItems"] = menuItems
// 当前菜单
action.Data["leftMenuActiveItem"] = nil
for _, item := range menuItems {
if item.GetBool("isActive") {
action.Data["leftMenuActiveItem"] = item
break
}
}
case "delete":
action.Data["leftMenuItems"] = this.createDeleteMenu(types.String(secondMenuItem), serverIdString, serverConfig)
}
@@ -349,6 +364,13 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
"isOn": serverConfig.Web != nil && serverConfig.Web.RemoteAddr != nil && serverConfig.Web.RemoteAddr.IsOn,
})
menuItems = append(menuItems, maps.Map{
"name": "请求限制",
"url": "/servers/server/settings/requestLimit?serverId=" + serverIdString,
"isActive": secondMenuItem == "requestLimit",
"isOn": serverConfig.Web != nil && serverConfig.Web.RequestLimit != nil && serverConfig.Web.RequestLimit.IsOn,
})
if teaconst.IsPlus {
menuItems = append(menuItems, maps.Map{
"name": "流量限制",

View File

@@ -1,52 +0,0 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package server
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
)
type ActivateAction struct {
actionutils.ParentAction
}
func (this *ActivateAction) Init() {
this.Nav("", "", "activate")
}
func (this *ActivateAction) RunGet(params struct{}) {
this.Show()
}
func (this *ActivateAction) RunPost(params struct {
Key string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
if len(params.Key) == 0 {
this.FailField("key", "请输入激活码")
}
resp, err := this.RPC().AuthorityKeyRPC().ValidateAuthorityKey(this.AdminContext(), &pb.ValidateAuthorityKeyRequest{Key: params.Key})
if err != nil {
this.ErrorPage(err)
return
}
if resp.IsOk {
_, err := this.RPC().AuthorityKeyRPC().UpdateAuthorityKey(this.AdminContext(), &pb.UpdateAuthorityKeyRequest{
Value: params.Key,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
} else {
this.FailField("key", "无法激活:"+resp.Error)
}
}

View File

@@ -1,76 +0,0 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package server
import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
"time"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "index")
}
func (this *IndexAction) RunGet(params struct{}) {
keyResp, err := this.RPC().AuthorityKeyRPC().ReadAuthorityKey(this.AdminContext(), &pb.ReadAuthorityKeyRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var keyMap maps.Map = nil
teaconst.IsPlus = false
var key = keyResp.AuthorityKey
if key != nil {
if len(key.MacAddresses) == 0 {
key.MacAddresses = []string{}
}
isActive := len(key.DayTo) > 0 && key.DayTo >= timeutil.Format("Y-m-d")
if isActive {
teaconst.IsPlus = true
}
isExpiring := isActive && key.DayTo < timeutil.Format("Y-m-d", time.Now().AddDate(0, 0, 7))
keyMap = maps.Map{
"dayFrom": key.DayFrom,
"dayTo": key.DayTo,
"macAddresses": key.MacAddresses,
"hostname": key.Hostname,
"company": key.Company,
"nodes": key.Nodes,
"isExpired": !isActive,
"isExpiring": isExpiring,
"updatedTime": timeutil.FormatTime("Y-m-d H:i:s", keyResp.AuthorityKey.UpdatedAt),
}
}
this.Data["key"] = keyMap
// 检查是否有认证节点,如果没有认证节点,则自动生成一个
countResp, err := this.RPC().AuthorityNodeRPC().CountAllEnabledAuthorityNodes(this.AdminContext(), &pb.CountAllEnabledAuthorityNodesRequest{})
if err != nil {
this.ErrorPage(err)
return
}
if countResp.Count == 0 {
_, err = this.RPC().AuthorityNodeRPC().CreateAuthorityNode(this.AdminContext(), &pb.CreateAuthorityNodeRequest{
Name: "默认节点",
Description: "系统自动生成的默认节点",
IsOn: true,
})
if err != nil {
this.ErrorPage(err)
return
}
}
this.Show()
}

View File

@@ -1,28 +0,0 @@
package nodes
import (
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type DeleteAction struct {
actionutils.ParentAction
}
func (this *DeleteAction) RunPost(params struct {
NodeId int64
}) {
// TODO 检查权限
_, err := this.RPC().AuthorityNodeRPC().DeleteAuthorityNode(this.AdminContext(), &pb.DeleteAuthorityNodeRequest{AuthorityNodeId: params.NodeId})
if err != nil {
this.ErrorPage(err)
return
}
// 创建日志
defer this.CreateLog(oplogs.LevelInfo, "删除认证节点 %d", params.NodeId)
this.Success()
}

View File

@@ -1,15 +0,0 @@
package nodes
import (
"github.com/iwind/TeaGo/actions"
)
type Helper struct {
}
func NewHelper() *Helper {
return &Helper{}
}
func (this *Helper) BeforeAction(action *actions.ActionObject) {
}

View File

@@ -1,75 +0,0 @@
package nodes
import (
"encoding/json"
"fmt"
"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/logs"
"github.com/iwind/TeaGo/maps"
"time"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "node", "node")
}
func (this *IndexAction) RunGet(params struct{}) {
countResp, err := this.RPC().AuthorityNodeRPC().CountAllEnabledAuthorityNodes(this.AdminContext(), &pb.CountAllEnabledAuthorityNodesRequest{})
if err != nil {
this.ErrorPage(err)
return
}
count := countResp.Count
page := this.NewPage(count)
this.Data["page"] = page.AsHTML()
nodeMaps := []maps.Map{}
if count > 0 {
nodesResp, err := this.RPC().AuthorityNodeRPC().ListEnabledAuthorityNodes(this.AdminContext(), &pb.ListEnabledAuthorityNodesRequest{
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
return
}
for _, node := range nodesResp.AuthorityNodes {
// 状态
status := &nodeconfigs.NodeStatus{}
if len(node.StatusJSON) > 0 {
err = json.Unmarshal(node.StatusJSON, &status)
if err != nil {
logs.Error(err)
continue
}
status.IsActive = status.IsActive && time.Now().Unix()-status.UpdatedAt <= 60 // N秒之内认为活跃
}
nodeMaps = append(nodeMaps, maps.Map{
"id": node.Id,
"isOn": node.IsOn,
"name": node.Name,
"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,
},
})
}
}
this.Data["nodes"] = nodeMaps
this.Show()
}

View File

@@ -1,23 +0,0 @@
package nodes
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/authority/nodes/node"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/settingutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)).
Helper(NewHelper()).
Helper(settingutils.NewAdvancedHelper("authority")).
Prefix("/settings/authority/nodes").
Get("", new(IndexAction)).
GetPost("/node/createPopup", new(node.CreatePopupAction)).
Post("/delete", new(DeleteAction)).
EndAll()
})
}

View File

@@ -1,47 +0,0 @@
package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
)
type CreatePopupAction struct {
actionutils.ParentAction
}
func (this *CreatePopupAction) Init() {
this.Nav("", "node", "create")
}
func (this *CreatePopupAction) RunGet(params struct{}) {
this.Show()
}
func (this *CreatePopupAction) RunPost(params struct {
Name string
Description string
IsOn bool
Must *actions.Must
}) {
params.Must.
Field("name", params.Name).
Require("请输入认证节点名称")
createResp, err := this.RPC().AuthorityNodeRPC().CreateAuthorityNode(this.AdminContext(), &pb.CreateAuthorityNodeRequest{
Name: params.Name,
Description: params.Description,
IsOn: params.IsOn,
})
if err != nil {
this.ErrorPage(err)
return
}
// 创建日志
defer this.CreateLog(oplogs.LevelInfo, "创建认证节点 %d", createResp.AuthorityNodeId)
this.Success()
}

View File

@@ -1,21 +0,0 @@
package node
import (
"github.com/iwind/TeaGo/actions"
"net/http"
)
type Helper struct {
}
func NewHelper() *Helper {
return &Helper{}
}
func (this *Helper) BeforeAction(action *actions.ActionObject) (goNext bool) {
if action.Request.Method != http.MethodGet {
return true
}
return true
}

View File

@@ -1,39 +0,0 @@
package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "index")
}
func (this *IndexAction) RunGet(params struct {
NodeId int64
}) {
nodeResp, err := this.RPC().AuthorityNodeRPC().FindEnabledAuthorityNode(this.AdminContext(), &pb.FindEnabledAuthorityNodeRequest{AuthorityNodeId: params.NodeId})
if err != nil {
this.ErrorPage(err)
return
}
node := nodeResp.AuthorityNode
if node == nil {
this.NotFound("authorityNode", params.NodeId)
return
}
this.Data["node"] = maps.Map{
"id": node.Id,
"name": node.Name,
"description": node.Description,
"isOn": node.IsOn,
}
this.Show()
}

View File

@@ -1,26 +0,0 @@
package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/settingutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)).
Helper(settingutils.NewAdvancedHelper("authority")).
Prefix("/settings/authority/nodes/node").
// 节点相关
Helper(NewHelper()).
Get("", new(IndexAction)).
Get("/logs", new(LogsAction)).
GetPost("/update", new(UpdateAction)).
Get("/install", new(InstallAction)).
EndAll()
})
}

View File

@@ -1,57 +0,0 @@
package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
"strings"
)
type InstallAction struct {
actionutils.ParentAction
}
func (this *InstallAction) Init() {
this.Nav("", "", "install")
}
func (this *InstallAction) RunGet(params struct {
NodeId int64
}) {
// 认证节点信息
nodeResp, err := this.RPC().AuthorityNodeRPC().FindEnabledAuthorityNode(this.AdminContext(), &pb.FindEnabledAuthorityNodeRequest{AuthorityNodeId: params.NodeId})
if err != nil {
this.ErrorPage(err)
return
}
node := nodeResp.AuthorityNode
if node == nil {
this.NotFound("authorityNode", params.NodeId)
return
}
this.Data["node"] = maps.Map{
"id": node.Id,
"name": node.Name,
"uniqueId": node.UniqueId,
"secret": node.Secret,
}
// API节点列表
apiNodesResp, err := this.RPC().APINodeRPC().FindAllEnabledAPINodes(this.AdminContext(), &pb.FindAllEnabledAPINodesRequest{})
if err != nil {
this.ErrorPage(err)
return
}
apiNodes := apiNodesResp.ApiNodes
apiEndpoints := []string{}
for _, apiNode := range apiNodes {
if !apiNode.IsOn {
continue
}
apiEndpoints = append(apiEndpoints, apiNode.AccessAddrs...)
}
this.Data["apiEndpoints"] = "\"" + strings.Join(apiEndpoints, "\", \"") + "\""
this.Show()
}

View File

@@ -1,92 +0,0 @@
package node
import (
"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/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type LogsAction struct {
actionutils.ParentAction
}
func (this *LogsAction) Init() {
this.Nav("", "node", "log")
this.SecondMenu("nodes")
}
func (this *LogsAction) RunGet(params struct {
NodeId int64
DayFrom string
DayTo string
Keyword string
Level string
}) {
this.Data["nodeId"] = params.NodeId
this.Data["dayFrom"] = params.DayFrom
this.Data["dayTo"] = params.DayTo
this.Data["keyword"] = params.Keyword
this.Data["level"] = params.Level
authorityNodeResp, err := this.RPC().AuthorityNodeRPC().FindEnabledAuthorityNode(this.AdminContext(), &pb.FindEnabledAuthorityNodeRequest{AuthorityNodeId: params.NodeId})
if err != nil {
this.ErrorPage(err)
return
}
authorityNode := authorityNodeResp.AuthorityNode
if authorityNode == nil {
this.NotFound("authorityNode", params.NodeId)
return
}
this.Data["node"] = maps.Map{
"id": authorityNode.Id,
"name": authorityNode.Name,
}
countResp, err := this.RPC().NodeLogRPC().CountNodeLogs(this.AdminContext(), &pb.CountNodeLogsRequest{
Role: nodeconfigs.NodeRoleAuthority,
NodeId: params.NodeId,
DayFrom: params.DayFrom,
DayTo: params.DayTo,
Keyword: params.Keyword,
Level: params.Level,
})
if err != nil {
this.ErrorPage(err)
return
}
count := countResp.Count
page := this.NewPage(count, 20)
logsResp, err := this.RPC().NodeLogRPC().ListNodeLogs(this.AdminContext(), &pb.ListNodeLogsRequest{
NodeId: params.NodeId,
Role: nodeconfigs.NodeRoleAuthority,
DayFrom: params.DayFrom,
DayTo: params.DayTo,
Keyword: params.Keyword,
Level: params.Level,
Offset: page.Offset,
Size: page.Size,
})
logs := []maps.Map{}
for _, log := range logsResp.NodeLogs {
logs = append(logs, maps.Map{
"tag": log.Tag,
"description": log.Description,
"createdTime": timeutil.FormatTime("Y-m-d H:i:s", log.CreatedAt),
"level": log.Level,
"isToday": timeutil.FormatTime("Y-m-d", log.CreatedAt) == timeutil.Format("Y-m-d"),
})
}
this.Data["logs"] = logs
this.Data["page"] = page.AsHTML()
this.Show()
}

View File

@@ -1,73 +0,0 @@
package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type UpdateAction struct {
actionutils.ParentAction
}
func (this *UpdateAction) Init() {
this.Nav("", "", "update")
}
func (this *UpdateAction) RunGet(params struct {
NodeId int64
}) {
nodeResp, err := this.RPC().AuthorityNodeRPC().FindEnabledAuthorityNode(this.AdminContext(), &pb.FindEnabledAuthorityNodeRequest{
AuthorityNodeId: params.NodeId,
})
if err != nil {
this.ErrorPage(err)
return
}
node := nodeResp.AuthorityNode
if node == nil {
this.WriteString("要操作的节点不存在")
return
}
this.Data["node"] = maps.Map{
"id": node.Id,
"name": node.Name,
"description": node.Description,
"isOn": node.IsOn,
}
this.Show()
}
// 保存基础设置
func (this *UpdateAction) RunPost(params struct {
NodeId int64
Name string
Description string
IsOn bool
Must *actions.Must
}) {
params.Must.
Field("name", params.Name).
Require("请输入认证节点名称")
_, err := this.RPC().AuthorityNodeRPC().UpdateAuthorityNode(this.AdminContext(), &pb.UpdateAuthorityNodeRequest{
AuthorityNodeId: params.NodeId,
Name: params.Name,
Description: params.Description,
IsOn: params.IsOn,
})
if err != nil {
this.ErrorPage(err)
return
}
// 创建日志
defer this.CreateLog(oplogs.LevelInfo, "修改认证节点 %d", params.NodeId)
this.Success()
}

View File

@@ -1,8 +1,10 @@
//go:build community
// +build community
package settingutils
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
)
@@ -33,17 +35,8 @@ func (this *AdvancedHelper) BeforeAction(actionPtr actions.ActionWrapper) (goNex
if configloaders.AllowModule(adminId, configloaders.AdminModuleCodeSetting) {
tabbar.Add("数据库", "", "/settings/database", "", this.tab == "database")
tabbar.Add("API节点", "", "/api", "", this.tab == "apiNodes")
if teaconst.IsPlus {
tabbar.Add("用户节点", "", "/settings/userNodes", "", this.tab == "userNodes")
}
tabbar.Add("日志数据库", "", "/db", "", this.tab == "dbNodes")
if teaconst.IsPlus {
tabbar.Add("监控节点", "", "/settings/monitorNodes", "", this.tab == "monitorNodes")
}
tabbar.Add("迁移", "", "/settings/transfer", "", this.tab == "transfer")
if teaconst.BuildPlus {
tabbar.Add("商业版认证", "", "/settings/authority", "", this.tab == "authority")
}
//tabbar.Add("备份", "", "/settings/backup", "", this.tab == "backup")
}

View File

@@ -37,6 +37,7 @@ func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool)
}
tabbar.Add("安全设置", "", "/settings/security", "", this.tab == "security")
tabbar.Add("IP库", "", "/settings/ip-library", "", this.tab == "ipLibrary")
tabbar.Add("检查更新", "", "/settings/updates", "", this.tab == "updates")
}
tabbar.Add("个人资料", "", "/settings/profile", "", this.tab == "profile")
tabbar.Add("登录设置", "", "/settings/login", "", this.tab == "login")

View File

@@ -0,0 +1,115 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package updates
import (
"encoding/json"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
stringutil "github.com/iwind/TeaGo/utils/string"
"io/ioutil"
"net/http"
"strings"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "updates", "")
}
func (this *IndexAction) RunGet(params struct{}) {
this.Data["version"] = teaconst.Version
this.Show()
}
func (this *IndexAction) RunPost(params struct {
}) {
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
var apiURL = teaconst.UpdatesURL
apiURL = strings.ReplaceAll(apiURL, "${os}", "linux") //runtime.GOOS)
apiURL = strings.ReplaceAll(apiURL, "${arch}", "amd64") // runtime.GOARCH)
resp, err := http.Get(apiURL)
if err != nil {
this.Data["result"] = maps.Map{
"isOk": false,
"message": "读取更新信息失败:" + err.Error(),
}
this.Success()
}
defer func() {
_ = resp.Body.Close()
}()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
this.Data["result"] = maps.Map{
"isOk": false,
"message": "读取更新信息失败:" + err.Error(),
}
this.Success()
}
var apiResponse = &Response{}
err = json.Unmarshal(data, apiResponse)
if err != nil {
this.Data["result"] = maps.Map{
"isOk": false,
"message": "解析更新信息失败:" + err.Error(),
}
this.Success()
}
if apiResponse.Code != 200 {
this.Data["result"] = maps.Map{
"isOk": false,
"message": "解析更新信息失败:" + apiResponse.Message,
}
this.Success()
}
var m = maps.NewMap(apiResponse.Data)
var dlHost = m.GetString("host")
var versions = m.GetSlice("versions")
if len(versions) > 0 {
for _, version := range versions {
var vMap = maps.NewMap(version)
if vMap.GetString("code") == "admin" {
var latestVersion = vMap.GetString("version")
if stringutil.VersionCompare(teaconst.Version, latestVersion) < 0 {
this.Data["result"] = maps.Map{
"isOk": true,
"message": "有最新的版本v" + types.String(latestVersion) + "可以更新",
"hasNew": true,
"dlURL": dlHost + vMap.GetString("url"),
}
this.Success()
} else {
this.Data["result"] = maps.Map{
"isOk": true,
"message": "你已安装最新版本,无需更新",
}
this.Success()
}
}
}
}
this.Data["result"] = maps.Map{
"isOk": false,
"message": "找不到更新信息",
}
this.Success()
this.Success()
}

View File

@@ -1,4 +1,4 @@
package server
package updates
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
@@ -11,10 +11,9 @@ func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)).
Helper(settingutils.NewAdvancedHelper("authority")).
Prefix("/settings/authority").
Get("", new(IndexAction)).
GetPost("/activate", new(ActivateAction)).
Helper(settingutils.NewHelper("updates")).
Prefix("/settings/updates").
GetPost("", new(IndexAction)).
EndAll()
})
}

View File

@@ -0,0 +1,147 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
//go:build community
// +build community
package helpers
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/iwind/TeaGo/maps"
)
func FindAllMenuMaps(nodeLogsType string, countUnreadNodeLogs int64) []maps.Map {
return []maps.Map{
{
"code": "dashboard",
"module": configloaders.AdminModuleCodeDashboard,
"name": "数据看板",
"icon": "dashboard",
},
{
"code": "servers",
"module": configloaders.AdminModuleCodeServer,
"name": "网站服务",
"subtitle": "服务列表",
"icon": "clone outsize",
"subItems": []maps.Map{
{
"name": "服务分组",
"url": "/servers/groups",
"code": "group",
},
{
"name": "证书管理",
"url": "/servers/certs",
"code": "cert",
},
{
"name": "访问日志",
"url": "/servers/logs",
"code": "log",
},
{
"name": "缓存策略",
"url": "/servers/components/cache",
"code": "cache",
},
{
"name": "WAF策略",
"url": "/servers/components/waf",
"code": "waf",
},
{
"name": "IP名单",
"url": "/servers/iplists",
"code": "iplist",
},
{
"name": "统计指标",
"url": "/servers/metrics",
"code": "metric",
},
{
"name": "通用设置",
"url": "/servers/components",
"code": "global",
},
},
},
{
"code": "clusters",
"module": configloaders.AdminModuleCodeNode,
"name": "边缘节点",
"subtitle": "集群列表",
"icon": "cloud",
"subItems": []maps.Map{
{
"name": "运行日志",
"url": "/clusters/logs?type=" + nodeLogsType,
"code": "log",
"badge": countUnreadNodeLogs,
},
{
"name": "SSH认证",
"url": "/clusters/grants",
"code": "grant",
},
{
"name": "区域设置",
"url": "/clusters/regions",
"code": "region",
},
},
},
{
"code": "dns",
"module": configloaders.AdminModuleCodeDNS,
"name": "域名解析",
"subtitle": "集群列表",
"icon": "globe",
"subItems": []maps.Map{
{
"name": "问题修复",
"url": "/dns/issues",
"code": "issue",
},
{
"name": "DNS服务商",
"url": "/dns/providers",
"code": "provider",
},
},
},
{
"code": "users",
"module": configloaders.AdminModuleCodeUser,
"name": "平台用户",
"icon": "users",
},
{
"code": "admins",
"module": configloaders.AdminModuleCodeAdmin,
"name": "系统用户",
"subtitle": "用户列表",
"icon": "user secret",
},
{
"code": "log",
"module": configloaders.AdminModuleCodeLog,
"name": "日志审计",
"icon": "history",
},
{
"code": "settings",
"module": configloaders.AdminModuleCodeSetting,
"name": "系统设置",
"subtitle": "基本设置",
"icon": "setting",
"subItems": []maps.Map{
{
"name": "高级设置",
"url": "/settings/advanced",
"code": "advanced",
},
},
},
}
}

View File

@@ -191,8 +191,8 @@ func (this *userMustAuth) modules(actionPtr actions.ActionWrapper, adminId int64
var countNodeLogs = countNodeLogsResp.Count
if countNodeLogs > 0 {
countUnreadNodeLogs = countNodeLogs
if countUnreadNodeLogs >= 1000 {
countUnreadNodeLogs = 999
if countUnreadNodeLogs >= 100 {
countUnreadNodeLogs = 99
}
nodeLogsType = "unread"
}
@@ -200,255 +200,8 @@ func (this *userMustAuth) modules(actionPtr actions.ActionWrapper, adminId int64
}
}
allMaps := []maps.Map{
{
"code": "dashboard",
"module": configloaders.AdminModuleCodeDashboard,
"name": "数据看板",
"icon": "dashboard",
},
{
"code": "servers",
"module": configloaders.AdminModuleCodeServer,
"name": "网站服务",
"subtitle": "服务列表",
"icon": "clone outsize",
"subItems": []maps.Map{
{
"name": "服务分组",
"url": "/servers/groups",
"code": "group",
},
{
"name": "证书管理",
"url": "/servers/certs",
"code": "cert",
},
{
"name": "访问日志",
"url": "/servers/logs",
"code": "log",
},
{
"name": "缓存策略",
"url": "/servers/components/cache",
"code": "cache",
},
{
"name": "WAF策略",
"url": "/servers/components/waf",
"code": "waf",
},
{
"name": "日志策略",
"url": "/servers/accesslogs",
"code": "accesslog",
"isOn": teaconst.IsPlus,
},
{
"name": "IP名单",
"url": "/servers/iplists",
"code": "iplist",
},
{
"name": "统计指标",
"url": "/servers/metrics",
"code": "metric",
},
{
"name": "通用设置",
"url": "/servers/components",
"code": "global",
},
},
},
{
"code": "clusters",
"module": configloaders.AdminModuleCodeNode,
"name": "边缘节点",
"subtitle": "集群列表",
"icon": "cloud",
"subItems": []maps.Map{
{
"name": "运行日志",
"url": "/clusters/logs?type=" + nodeLogsType,
"code": "log",
"badge": countUnreadNodeLogs,
},
{
"name": "IP地址",
"url": "/clusters/ip-addrs",
"code": "ipAddr",
"isOn": teaconst.IsPlus,
},
{
"name": "区域监控",
"url": "/clusters/monitors",
"code": "monitor",
"isOn": teaconst.IsPlus,
},
{
"name": "SSH认证",
"url": "/clusters/grants",
"code": "grant",
},
{
"name": "区域设置",
"url": "/clusters/regions",
"code": "region",
},
},
},
{
"code": "dns",
"module": configloaders.AdminModuleCodeDNS,
"name": "域名解析",
"subtitle": "集群列表",
"icon": "globe",
"subItems": []maps.Map{
{
"name": "问题修复",
"url": "/dns/issues",
"code": "issue",
},
{
"name": "DNS服务商",
"url": "/dns/providers",
"code": "provider",
},
},
},
{
"code": "ns",
"module": configloaders.AdminModuleCodeNS,
"name": "智能DNS",
"icon": "cubes",
"isOn": teaconst.IsPlus,
"subItems": []maps.Map{
{
"name": "域名管理",
"url": "/ns/domains",
"code": "domain",
},
{
"name": "集群管理",
"url": "/ns/clusters",
"code": "cluster",
},
{
"name": "线路管理",
"url": "/ns/routes",
"code": "route",
},
{
"name": "访问日志",
"url": "/ns/clusters/accessLogs",
"code": "accessLog",
},
{
"name": "运行日志",
"url": "/ns/clusters/logs",
"code": "log",
},
{
"name": "全局配置",
"url": "/ns/settings",
"code": "setting",
},
{
"name": "解析测试",
"url": "/ns/test",
"code": "test",
},
},
},
{
"code": "users",
"module": configloaders.AdminModuleCodeUser,
"name": "平台用户",
"icon": "users",
},
{
"code": "finance",
"module": configloaders.AdminModuleCodeFinance,
"name": "财务管理",
"icon": "yen sign",
"isOn": teaconst.IsPlus,
"subItems": []maps.Map{
{
"name": "用户账户",
"url": "/finance/accounts",
"code": "accounts",
"isOn": teaconst.IsPlus,
},
{
"name": "操作记录",
"url": "/finance/logs",
"code": "logs",
"isOn": teaconst.IsPlus,
},
{
"name": "收支报表",
"url": "/finance/income",
"code": "income",
"isOn": teaconst.IsPlus,
},
},
},
{
"code": "plans",
"module": configloaders.AdminModuleCodePlan,
"name": "套餐管理",
"icon": "puzzle piece",
"isOn": teaconst.IsPlus,
"subItems": []maps.Map{
{
"name": "已购套餐",
"url": "/plans/userPlans",
"code": "userPlans",
"isOn": teaconst.IsPlus,
},
},
},
{
"code": "admins",
"module": configloaders.AdminModuleCodeAdmin,
"name": "系统用户",
"subtitle": "用户列表",
"icon": "user secret",
"subItems": []maps.Map{
{
"name": "通知媒介",
"url": "/admins/recipients",
"code": "recipients",
"isOn": teaconst.IsPlus,
},
},
},
{
"code": "log",
"module": configloaders.AdminModuleCodeLog,
"name": "日志审计",
"icon": "history",
},
{
"code": "settings",
"module": configloaders.AdminModuleCodeSetting,
"name": "系统设置",
"subtitle": "基本设置",
"icon": "setting",
"subItems": []maps.Map{
{
"name": "高级设置",
"url": "/settings/advanced",
"code": "advanced",
},
},
},
}
result := []maps.Map{}
for _, m := range allMaps {
for _, m := range FindAllMenuMaps(nodeLogsType, countUnreadNodeLogs) {
if m.GetString("code") == "finance" && !configloaders.ShowFinance() {
continue
}

View File

@@ -88,6 +88,7 @@ import (
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/server/settings/pages"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/server/settings/redirects"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/server/settings/remoteAddr"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/server/settings/requestLimit"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/server/settings/reverseProxy"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/server/settings/rewrite"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/server/settings/serverNames"
@@ -108,9 +109,6 @@ import (
// 设置相关
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/authority"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/authority/nodes"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/authority/nodes/node"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/backup"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/database"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/ip-library"
@@ -120,6 +118,7 @@ import (
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/server"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/transfer"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/ui"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/updates"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/upgrade"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/user-ui"

File diff suppressed because it is too large Load Diff

View File

@@ -135,6 +135,7 @@ Vue.component("datetime-input", {
this.timestamp = Math.floor(date.getTime() / 1000)
},
leadingZero: function (s, l) {
s = s.toString()
if (l <= s.length) {
return s
}
@@ -142,6 +143,18 @@ Vue.component("datetime-input", {
s = "0" + s
}
return s
},
resultTimestamp: function () {
return this.timestamp
},
nextDays: function (days) {
let date = new Date()
date.setTime(date.getTime() + days * 86400 * 1000)
this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2)
this.hour = this.leadingZero(date.getHours(), 2)
this.minute = this.leadingZero(date.getMinutes(), 2)
this.second = this.leadingZero(date.getSeconds(), 2)
this.change()
}
},
template: `<div>
@@ -156,5 +169,6 @@ Vue.component("datetime-input", {
<div class="ui field">:</div>
<div class="ui field" :class="{error: hasSecondError}"><input type="text" v-model="second" maxlength="2" style="width:4em" placeholder="秒" @input="change"/></div>
</div>
<p class="comment">常用时间:<a href="" @click.prevent="nextDays(1)"> &nbsp;1天&nbsp; </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(3)"> &nbsp;3天&nbsp; </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(7)"> &nbsp;一周&nbsp; </a> <span class="disabled">|</span> <a href="" @click.prevent="nextDays(30)"> &nbsp;30天&nbsp; </a> </p>
</div>`
})

View File

@@ -0,0 +1,33 @@
Vue.component("page-size-selector", {
data: function () {
let query = window.location.search
let pageSize = 10
if (query.length > 0) {
query = query.substr(1)
let params = query.split("&")
params.forEach(function (v) {
let pieces = v.split("=")
if (pieces.length == 2 && pieces[0] == "pageSize") {
let pageSizeString = pieces[1]
if (pageSizeString.match(/^\d+$/)) {
pageSize = parseInt(pageSizeString, 10)
if (isNaN(pageSize) || pageSize < 1) {
pageSize = 10
}
}
}
})
}
return {
pageSize: pageSize
}
},
watch: {
pageSize: function () {
window.ChangePageSize(this.pageSize)
}
},
template: `<select class="ui dropdown" style="height:34px;padding-top:0;padding-bottom:0;margin-left:1em;color:#666" v-model="pageSize">
\t<option value="10">[每页]</option><option value="10" selected="selected">10条</option><option value="20">20条</option><option value="30">30条</option><option value="40">40条</option><option value="50">50条</option><option value="60">60条</option><option value="70">70条</option><option value="80">80条</option><option value="90">90条</option><option value="100">100条</option>
</select>`
})

View File

@@ -0,0 +1,33 @@
// 将变量转换为中文
Vue.component("request-variables-describer", {
data: function () {
return {
vars:[]
}
},
methods: {
update: function (variablesString) {
this.vars = []
let that = this
variablesString.replace(/\${.+?}/g, function (v) {
let def = that.findVar(v)
if (def == null) {
return v
}
that.vars.push(def)
})
},
findVar: function (name) {
let def = null
window.REQUEST_VARIABLES.forEach(function (v) {
if (v.code == name) {
def = v
}
})
return def
}
},
template: `<span>
<span v-for="(v, index) in vars"><code-label :title="v.description">{{v.code}}</code-label> - {{v.name}}<span v-if="index < vars.length-1"></span></span>
</span>`
})

View File

@@ -1,5 +1,5 @@
Vue.component("size-capacity-box", {
props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength"],
props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"],
data: function () {
let v = this.vValue
if (v == null) {
@@ -22,11 +22,17 @@ Vue.component("size-capacity-box", {
vMaxlength = 10
}
let supportedUnits = this.vSupportedUnits
if (supportedUnits == null) {
supportedUnits = []
}
return {
capacity: v,
countString: (v.count >= 0) ? v.count.toString() : "",
vSize: vSize,
vMaxlength: vMaxlength
vMaxlength: vMaxlength,
supportedUnits: supportedUnits
}
},
watch: {
@@ -56,12 +62,13 @@ Vue.component("size-capacity-box", {
</div>
<div class="ui field">
<select class="ui dropdown" v-model="capacity.unit" @change="change">
<option value="byte">字节</option>
<option value="kb">KB</option>
<option value="mb">MB</option>
<option value="gb">GB</option>
<option value="tb">TB</option>
<option value="pb">PB</option>
<option value="byte" v-if="supportedUnits.length == 0 || supportedUnits.$contains('byte')">字节</option>
<option value="kb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('kb')">KB</option>
<option value="mb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('mb')">MB</option>
<option value="gb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('gb')">GB</option>
<option value="tb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('tb')">TB</option>
<option value="pb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('pb')">PB</option>
<option value="eb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('eb')">EB</option>
</select>
</div>
</div>`

View File

@@ -1,7 +1,7 @@
let sourceCodeBoxIndex = 0
Vue.component("source-code-box", {
props: ["name", "type", "id", "read-only"],
props: ["name", "type", "id", "read-only", "width", "height"],
mounted: function () {
let readOnly = this.readOnly
if (typeof readOnly != "boolean") {
@@ -15,27 +15,46 @@ Vue.component("source-code-box", {
} else if (valueBox.innerText != null) {
value = valueBox.innerText
}
let boxEditor = CodeMirror.fromTextArea(box, {
theme: "idea",
lineNumbers: true,
value: "",
readOnly: readOnly,
showCursorWhenSelecting: true,
height: "auto",
//scrollbarStyle: null,
viewportMargin: Infinity,
lineWrapping: true,
highlightFormatting: false,
indentUnit: 4,
indentWithTabs: true
})
boxEditor.setValue(value)
let info = CodeMirror.findModeByMIME(this.type)
if (info != null) {
boxEditor.setOption("mode", info.mode)
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js"
CodeMirror.autoLoadMode(boxEditor, info.mode)
this.createEditor(box, value, readOnly)
},
methods: {
createEditor: function (box, value, readOnly) {
let boxEditor = CodeMirror.fromTextArea(box, {
theme: "idea",
lineNumbers: true,
value: "",
readOnly: readOnly,
showCursorWhenSelecting: true,
height: "auto",
//scrollbarStyle: null,
viewportMargin: Infinity,
lineWrapping: true,
highlightFormatting: false,
indentUnit: 4,
indentWithTabs: true
})
boxEditor.setValue(value)
let width = this.width
let height = this.height
if (width != null && height != null) {
width = parseInt(width)
height = parseInt(height)
if (!isNaN(width) && !isNaN(height)) {
if (width <= 0) {
width = box.parentNode.offsetWidth
}
boxEditor.setSize(width, height)
}
}
let info = CodeMirror.findModeByMIME(this.type)
if (info != null) {
boxEditor.setOption("mode", info.mode)
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js"
CodeMirror.autoLoadMode(boxEditor, info.mode)
}
}
},
data: function () {

View File

@@ -50,6 +50,7 @@ Vue.component("time-duration-box", {
<option value="minute">分钟</option>
<option value="hour">小时</option>
<option value="day">天</option>
<option value="week">周</option>
</select>
</div>
</div>`

View File

@@ -1,14 +1,17 @@
Vue.component("ip-box", {
props: [],
props: ["v-ip"],
methods: {
popup: function () {
let e = this.$refs.container
let text = e.innerText
if (text == null) {
text = e.textContent
let ip = this.vIp
if (ip == null || ip.length == 0) {
let e = this.$refs.container
ip = e.innerText
if (ip == null) {
ip = e.textContent
}
}
teaweb.popup("/servers/ipbox?ip=" + text, {
teaweb.popup("/servers/ipbox?ip=" + ip, {
width: "50em",
height: "30em"
})

View File

@@ -0,0 +1,260 @@
Vue.component("traffic-map-box", {
props: ["v-stats", "v-is-attack"],
mounted: function () {
this.render()
},
data: function () {
let maxPercent = 0
let isAttack = this.vIsAttack
this.vStats.forEach(function (v) {
let percent = parseFloat(v.percent)
if (percent > maxPercent) {
maxPercent = percent
}
v.formattedCountRequests = teaweb.formatCount(v.countRequests) + "次"
v.formattedCountAttackRequests = teaweb.formatCount(v.countAttackRequests) + "次"
})
if (maxPercent < 100) {
maxPercent *= 1.2 // 不要让某一项100%
}
let screenIsNarrow = window.innerWidth < 512
return {
isAttack: isAttack,
stats: this.vStats,
chart: null,
minOpacity: 0.2,
maxPercent: maxPercent,
selectedCountryName: "",
screenIsNarrow: screenIsNarrow
}
},
methods: {
render: function () {
this.chart = teaweb.initChart(document.getElementById("traffic-map-box"));
let that = this
this.chart.setOption({
backgroundColor: "white",
grid: {
top: 0,
bottom: 0,
left: 0,
right: 0
},
roam: false,
tooltip: {
trigger: "item"
},
series: [{
type: "map",
map: "world",
zoom: 1.3,
selectedMode: false,
itemStyle: {
areaColor: "#E9F0F9",
borderColor: "#DDD"
},
label: {
show: false,
fontSize: "10px",
color: "#fff",
backgroundColor: "#8B9BD3",
padding: [2, 2, 2, 2]
},
emphasis: {
itemStyle: {
areaColor: "#8B9BD3",
opacity: 1.0
},
label: {
show: true,
fontSize: "10px",
color: "#fff",
backgroundColor: "#8B9BD3",
padding: [2, 2, 2, 2]
}
},
//select: {itemStyle:{ areaColor: "#8B9BD3", opacity: 0.8 }},
tooltip: {
formatter: function (args) {
let name = args.name
let stat = null
that.stats.forEach(function (v) {
if (v.name == name) {
stat = v
}
})
if (stat != null) {
return name + "<br/>流量:" + stat.formattedBytes + "<br/>流量占比:" + stat.percent + "%<br/>请求数:" + stat.formattedCountRequests + "<br/>攻击数:" + stat.formattedCountAttackRequests
}
return name
}
},
data: this.stats.map(function (v) {
let opacity = parseFloat(v.percent) / that.maxPercent
if (opacity < that.minOpacity) {
opacity = that.minOpacity
}
let fullOpacity = opacity * 3
if (fullOpacity > 1) {
fullOpacity = 1
}
let isAttack = that.vIsAttack
let bgColor = "#276AC6"
if (isAttack) {
bgColor = "#B03A5B"
}
return {
name: v.name,
value: v.bytes,
percent: parseFloat(v.percent),
itemStyle: {
areaColor: bgColor,
opacity: opacity
},
emphasis: {
itemStyle: {
areaColor: bgColor,
opacity: fullOpacity
},
label: {
show: true,
formatter: function (args) {
return args.name
}
}
},
label: {
show: false,
formatter: function (args) {
if (args.name == that.selectedCountryName) {
return args.name
}
return ""
},
fontSize: "10px",
color: "#fff",
backgroundColor: "#8B9BD3",
padding: [2, 2, 2, 2]
}
}
}),
nameMap: window.WorldCountriesMap
}]
})
this.chart.resize()
},
selectCountry: function (countryName) {
if (this.chart == null) {
return
}
let option = this.chart.getOption()
let that = this
option.series[0].data.forEach(function (v) {
let opacity = v.percent / that.maxPercent
if (opacity < that.minOpacity) {
opacity = that.minOpacity
}
if (v.name == countryName) {
if (v.isSelected) {
v.itemStyle.opacity = opacity
v.isSelected = false
v.label.show = false
that.selectedCountryName = ""
return
}
v.isSelected = true
that.selectedCountryName = countryName
opacity *= 3
if (opacity > 1) {
opacity = 1
}
// 至少是0.5,让用户能够看清
if (opacity < 0.5) {
opacity = 0.5
}
v.itemStyle.opacity = opacity
v.label.show = true
} else {
v.itemStyle.opacity = opacity
v.isSelected = false
v.label.show = false
}
})
this.chart.setOption(option)
},
select: function (args) {
this.selectCountry(args.countryName)
}
},
template: `<div>
<table style="width: 100%; border: 0; padding: 0; margin: 0">
<tbody>
<tr>
<td>
<div class="traffic-map-box" id="traffic-map-box"></div>
</td>
<td style="width: 14em" v-if="!screenIsNarrow">
<traffic-map-box-table :v-stats="stats" :v-is-attack="isAttack" @select="select"></traffic-map-box-table>
</td>
</tr>
</tbody>
<tbody v-if="screenIsNarrow">
<tr>
<td colspan="2">
<traffic-map-box-table :v-stats="stats" :v-is-attack="isAttack" :v-screen-is-narrow="true" @select="select"></traffic-map-box-table>
</td>
</tr>
</tbody>
</table>
</div>`
})
Vue.component("traffic-map-box-table", {
props: ["v-stats", "v-is-attack", "v-screen-is-narrow"],
data: function () {
return {
stats: this.vStats,
isAttack: this.vIsAttack
}
},
methods: {
select: function (countryName) {
this.$emit("select", {countryName: countryName})
}
},
template: `<div style="overflow-y: auto" :style="{'max-height':vScreenIsNarrow ? 'auto' : '16em'}" class="narrow-scrollbar">
<table class="ui table selectable">
<thead>
<tr>
<th colspan="2">国家/地区排行&nbsp; <tip-icon content="只有开启了统计的服务才会有记录。"></tip-icon></th>
</tr>
</thead>
<tbody v-if="stats.length == 0">
<tr>
<td colspan="2">暂无数据</td>
</tr>
</tbody>
<tbody>
<tr v-for="(stat, index) in stats.slice(0, 10)">
<td @click.prevent="select(stat.name)" style="cursor: pointer" colspan="2">
<div class="ui progress bar" :class="{red: vIsAttack, blue:!vIsAttack}" style="margin-bottom: 0.3em">
<div class="bar" style="min-width: 0; height: 4px;" :style="{width: stat.percent + '%'}"></div>
</div>
<div>{{stat.name}}</div>
<div><span class="grey">{{stat.percent}}% </span>
<span class="small grey" v-if="isAttack">{{stat.formattedCountAttackRequests}}</span>
<span class="small grey" v-if="!isAttack">{{stat.formattedBytes}}</span></div>
</td>
</tr>
</tbody>
</table>
</div>`
})

View File

@@ -90,6 +90,11 @@ Vue.component("message-row", {
<div v-if="message.type == 'SSLCertACMETaskFailed'" style="margin-top: 0.8em">
<a href="" @click.prevent="viewCert(params.certId)" target="_top">查看证书</a> &nbsp;|&nbsp; <a :href="'/servers/certs/acme'" v-if="params != null && params.acmeTaskId > 0" target="_top">查看任务&raquo;</a>
</div>
<!-- 网站域名审核 -->
<div v-if="message.type == 'serverNamesRequireAuditing'" style="margin-top: 0.8em">
<a :href="'/servers/server/settings/serverNames?serverId=' + params.serverId" target="_top">去审核</a></a>
</div>
</td>
</tr>
</table>

View File

@@ -51,7 +51,7 @@ Vue.component("http-access-log-box", {
template: `<div style="word-break: break-all" :style="{'color': (accessLog.status >= 400) ? '#dc143c' : ''}" ref="box">
<a v-if="accessLog.node != null && accessLog.node.nodeCluster != null" :href="'/clusters/cluster/node?nodeId=' + accessLog.node.id + '&clusterId=' + accessLog.node.nodeCluster.id" title="点击查看节点详情" target="_top"><span class="grey">[{{accessLog.node.name}}<span v-if="!accessLog.node.name.endsWith('节点')">节点</span>]</span></a>
<a :href="'/servers/server/log?serverId=' + accessLog.serverId" title="点击到网站服务" v-if="vShowServerLink"><span class="grey">[服务]</span></a>
<span v-if="accessLog.region != null && accessLog.region.length > 0" class="grey">[{{accessLog.region}}]</span> <ip-box><keyword :v-word="vKeyword">{{accessLog.remoteAddr}}</keyword></ip-box> [{{accessLog.timeLocal}}] <em>&quot;<keyword :v-word="vKeyword">{{accessLog.requestMethod}}</keyword> {{accessLog.scheme}}://<keyword :v-word="vKeyword">{{accessLog.host}}</keyword><keyword :v-word="vKeyword">{{accessLog.requestURI}}</keyword> <a :href="accessLog.scheme + '://' + accessLog.host + accessLog.requestURI" target="_blank" title="新窗口打开" class="disabled"><i class="external icon tiny"></i> </a> {{accessLog.proto}}&quot; </em> <keyword :v-word="vKeyword">{{accessLog.status}}</keyword> <code-label v-if="accessLog.attrs != null && accessLog.attrs['cache.status'] == 'HIT'">cache hit</code-label> <code-label v-if="accessLog.firewallActions != null && accessLog.firewallActions.length > 0">waf {{accessLog.firewallActions}}</code-label> <span v-if="accessLog.tags != null && accessLog.tags.length > 0">- <code-label v-for="tag in accessLog.tags" :key="tag">{{tag}}</code-label></span> - 耗时:{{formatCost(accessLog.requestTime)}} ms <span v-if="accessLog.humanTime != null && accessLog.humanTime.length > 0" class="grey small">&nbsp; ({{accessLog.humanTime}})</span>
<span v-if="accessLog.region != null && accessLog.region.length > 0" class="grey"><ip-box :v-ip="accessLog.remoteAddr">[{{accessLog.region}}]</ip-box></span> <ip-box><keyword :v-word="vKeyword">{{accessLog.remoteAddr}}</keyword></ip-box> [{{accessLog.timeLocal}}] <em>&quot;<keyword :v-word="vKeyword">{{accessLog.requestMethod}}</keyword> {{accessLog.scheme}}://<keyword :v-word="vKeyword">{{accessLog.host}}</keyword><keyword :v-word="vKeyword">{{accessLog.requestURI}}</keyword> <a :href="accessLog.scheme + '://' + accessLog.host + accessLog.requestURI" target="_blank" title="新窗口打开" class="disabled"><i class="external icon tiny"></i> </a> {{accessLog.proto}}&quot; </em> <keyword :v-word="vKeyword">{{accessLog.status}}</keyword> <code-label v-if="accessLog.attrs != null && (accessLog.attrs['cache.status'] == 'HIT' || accessLog.attrs['cache.status'] == 'STALE')">cache {{accessLog.attrs['cache.status'].toLowerCase()}}</code-label> <code-label v-if="accessLog.firewallActions != null && accessLog.firewallActions.length > 0">waf {{accessLog.firewallActions}}</code-label> <span v-if="accessLog.tags != null && accessLog.tags.length > 0">- <code-label v-for="tag in accessLog.tags" :key="tag">{{tag}}</code-label></span> - 耗时:{{formatCost(accessLog.requestTime)}} ms <span v-if="accessLog.humanTime != null && accessLog.humanTime.length > 0" class="grey small">&nbsp; ({{accessLog.humanTime}})</span>
&nbsp; <a href="" @click.prevent="showLog" title="查看详情"><i class="icon expand"></i></a>
</div>`
})

View File

@@ -11,14 +11,15 @@ Vue.component("http-access-log-config-box", {
let accessLog = {
isPrior: false,
isOn: false,
fields: [],
fields: [1, 2, 6, 7],
status1: true,
status2: true,
status3: true,
status4: true,
status5: true,
firewallOnly: false
firewallOnly: false,
enableClientClosed: false
}
if (this.vAccessLogConfig != null) {
accessLog = this.vAccessLogConfig
@@ -33,7 +34,8 @@ Vue.component("http-access-log-config-box", {
})
return {
accessLog: accessLog
accessLog: accessLog,
hasRequestBodyField: this.vFields.$contains(8)
}
},
methods: {
@@ -43,6 +45,7 @@ Vue.component("http-access-log-config-box", {
}).map(function (v) {
return v.code
})
this.hasRequestBodyField = this.accessLog.fields.$contains(8)
}
},
template: `<div>
@@ -63,12 +66,19 @@ Vue.component("http-access-log-config-box", {
</tbody>
<tbody v-show="((!vIsLocation && !vIsGroup) || accessLog.isPrior) && accessLog.isOn">
<tr>
<td>要存储的访问日志字段</td>
<td>基础信息</td>
<td><p class="comment" style="padding-top: 0">默认记录客户端IP、请求URL等基础信息。</p></td>
</tr>
<tr>
<td>高级信息</td>
<td>
<div class="ui checkbox" v-for="field in vFields" style="width:10em;margin-bottom:0.8em">
<input type="checkbox" v-model="field.isChecked" @change="changeFields"/>
<label>{{field.name}}</label>
<div class="ui checkbox" v-for="(field, index) in vFields" style="width:10em;margin-bottom:0.8em">
<input type="checkbox" v-model="field.isChecked" @change="changeFields" :id="'access-log-field-' + index"/>
<label :for="'access-log-field-' + index">{{field.name}}</label>
</div>
<p class="comment">在基础信息之外要存储的信息。
<span class="red" v-if="hasRequestBodyField">记录"请求Body"将会显著消耗更多的系统资源建议仅在调试时启用最大记录尺寸为2MB。</span>
</p>
</td>
</tr>
<tr>
@@ -96,6 +106,16 @@ Vue.component("http-access-log-config-box", {
</div>
</td>
</tr>
<tr>
<td>记录客户端中断日志</td>
<td>
<div class="ui checkbox">
<input type="checkbox" v-model="accessLog.enableClientClosed"/>
<label></label>
</div>
<p class="comment">以<code-label>499</code-label>的状态码记录客户端主动中断日志。</p>
</td>
</tr>
</tbody>
</table>

View File

@@ -7,6 +7,8 @@ Vue.component("http-cache-config-box", {
isPrior: false,
isOn: false,
addStatusHeader: true,
addAgeHeader: false,
enableCacheControlMaxAge: false,
cacheRefs: [],
purgeIsOn: false,
purgeKey: ""
@@ -24,6 +26,9 @@ Vue.component("http-cache-config-box", {
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.cacheConfig.isPrior) && this.cacheConfig.isOn
},
isPlus: function () {
return Tea.Vue.teaIsPlus
},
generatePurgeKey: function () {
let r = Math.random().toString() + Math.random().toString()
let s = r.replace(/0\./g, "")
@@ -36,6 +41,9 @@ Vue.component("http-cache-config-box", {
},
showMoreOptions: function () {
this.moreOptionsVisible = !this.moreOptionsVisible
},
changeStale: function (stale) {
this.cacheConfig.stale = stale
}
},
template: `<div>
@@ -71,21 +79,35 @@ Vue.component("http-cache-config-box", {
</tbody>
<tbody v-show="isOn() && moreOptionsVisible">
<tr>
<td>自动添加X-Cache Header</td>
<td>添加X-Cache Header</td>
<td>
<checkbox v-model="cacheConfig.addStatusHeader"></checkbox>
<p class="comment">选中后自动在响应Header中增加<code-label>X-Cache: BYPASS|MISS|HIT</code-label>。</p>
<p class="comment">选中后自动在响应Header中增加<code-label>X-Cache: BYPASS|MISS|HIT|PURGE</code-label>。</p>
</td>
</tr>
<tr>
<td>允许PURGE</td>
<td>添加Age Header</td>
<td>
<checkbox v-model="cacheConfig.addAgeHeader"></checkbox>
<p class="comment">选中后自动在响应Header中增加<code-label>Age: [有效时间秒数]</code-label>。</p>
</td>
</tr>
<tr>
<td>支持源站控制有效时间</td>
<td>
<checkbox v-model="cacheConfig.enableCacheControlMaxAge"></checkbox>
<p class="comment">选中后表示支持源站在Header中设置的<code-label>Cache-Control: max-age=[有效时间秒数]</code-label>。</p>
</td>
</tr>
<tr>
<td class="color-border">允许PURGE</td>
<td>
<checkbox v-model="cacheConfig.purgeIsOn"></checkbox>
<p class="comment">允许使用PURGE方法清除某个URL缓存。</p>
</td>
</tr>
<tr v-show="cacheConfig.purgeIsOn">
<td>PURGE Key *</td>
<td class="color-border">PURGE Key *</td>
<td>
<input type="text" maxlength="200" v-model="cacheConfig.purgeKey"/>
<p class="comment"><a href="" @click.prevent="generatePurgeKey">[随机生成]</a>。需要在PURGE方法调用时加入<code-label>Edge-Purge-Key: {{cacheConfig.purgeKey}}</code-label> Header。只能包含字符、数字、下划线。</p>
@@ -94,7 +116,12 @@ Vue.component("http-cache-config-box", {
</tbody>
</table>
<div v-show="isOn()">
<div v-if="isOn() && moreOptionsVisible && isPlus()">
<h4>过时缓存策略</h4>
<http-cache-stale-config :v-cache-stale-config="cacheConfig.stale" @change="changeStale"></http-cache-stale-config>
</div>
<div v-show="isOn()" style="margin-top: 1em">
<h4>缓存条件</h4>
<http-cache-refs-config-box :v-cache-config="cacheConfig" :v-cache-refs="cacheConfig.cacheRefs" ></http-cache-refs-config-box>
</div>

View File

@@ -1,13 +1,16 @@
// 单个缓存条件设置
Vue.component("http-cache-ref-box", {
props: ["v-cache-ref", "v-is-reverse"],
mounted: function () {
this.$refs.variablesDescriber.update(this.ref.key)
},
data: function () {
let ref = this.vCacheRef
if (ref == null) {
ref = {
isOn: true,
cachePolicyId: 0,
key: "${scheme}://${host}${requestURI}",
key: "${scheme}://${host}${requestPath}${isArgs}${args}",
life: {count: 2, unit: "hour"},
status: [200],
maxSize: {count: 32, unit: "mb"},
@@ -17,9 +20,24 @@ Vue.component("http-cache-ref-box", {
enableRequestCachePragma: false,
conds: null,
allowChunkedEncoding: true,
isReverse: this.vIsReverse
isReverse: this.vIsReverse,
methods: [],
expiresTime: {
isPrior: false,
isOn: false,
overwrite: true,
autoCalculate: true,
duration: {count: -1, "unit": "hour"}
}
}
}
if (ref.key == null) {
ref.key = ""
}
if (ref.methods == null) {
ref.methods = []
}
if (ref.life == null) {
ref.life = {count: 2, unit: "hour"}
}
@@ -60,6 +78,17 @@ Vue.component("http-cache-ref-box", {
result.push(statusNumber)
})
this.ref.status = result
},
changeMethods: function (methods) {
this.ref.methods = methods.map(function (v) {
return v.toUpperCase()
})
},
changeKey: function (key) {
this.$refs.variablesDescriber.update(key)
},
changeExpiresTime: function (expiresTime) {
this.ref.expiresTime = expiresTime
}
},
template: `<tbody>
@@ -80,13 +109,26 @@ Vue.component("http-cache-ref-box", {
<tr v-show="!vIsReverse">
<td>缓存Key *</td>
<td>
<input type="text" v-model="ref.key"/>
<p class="comment">用来区分不同缓存内容的唯一Key。</p>
<input type="text" v-model="ref.key" @input="changeKey(ref.key)"/>
<p class="comment">用来区分不同缓存内容的唯一Key。<request-variables-describer ref="variablesDescriber"></request-variables-describer>。</p>
</td>
</tr>
<tr v-show="!vIsReverse">
<td colspan="2"><more-options-indicator @change="changeOptionsVisible"></more-options-indicator></td>
</tr>
<tr v-show="moreOptionsVisible && !vIsReverse">
<td>请求方法限制</td>
<td>
<values-box size="5" maxlength="10" :values="ref.methods" @change="changeMethods"></values-box>
<p class="comment">允许请求的缓存方法,默认支持所有的请求方法。</p>
</td>
</tr>
<tr v-show="moreOptionsVisible && !vIsReverse">
<td>客户端过期时间<em>Expires</em></td>
<td>
<http-expires-time-config-box :v-expires-time="ref.expiresTime" @change="changeExpiresTime"></http-expires-time-config-box>
</td>
</tr>
<tr v-show="moreOptionsVisible && !vIsReverse">
<td>可缓存的最大内容尺寸</td>
<td>

View File

@@ -49,6 +49,8 @@ Vue.component("http-cache-refs-box", {
<span v-if="cacheRef.maxSize != null && cacheRef.maxSize.count > 0">- {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}}</span>
</grey-label>
<grey-label v-else-if="cacheRef.maxSize != null && cacheRef.maxSize.count > 0">0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}}</grey-label>
<grey-label v-if="cacheRef.methods != null && cacheRef.methods.length > 0">{{cacheRef.methods.join(", ")}}</grey-label>
<grey-label v-if="cacheRef.expiresTime != null && cacheRef.expiresTime.isPrior && cacheRef.expiresTime.isOn">Expires</grey-label>
<grey-label v-if="cacheRef.status != null && cacheRef.status.length > 0 && (cacheRef.status.length > 1 || cacheRef.status[0] != 200)">状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}}</grey-label>
</td>
<td>

View File

@@ -172,6 +172,8 @@ Vue.component("http-cache-refs-config-box", {
<span v-if="cacheRef.maxSize != null && cacheRef.maxSize.count > 0">- {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}}</span>
</grey-label>
<grey-label v-else-if="cacheRef.maxSize != null && cacheRef.maxSize.count > 0">0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}}</grey-label>
<grey-label v-if="cacheRef.methods != null && cacheRef.methods.length > 0">{{cacheRef.methods.join(", ")}}</grey-label>
<grey-label v-if="cacheRef.expiresTime != null && cacheRef.expiresTime.isPrior && cacheRef.expiresTime.isOn">Expires</grey-label>
<grey-label v-if="cacheRef.status != null && cacheRef.status.length > 0 && (cacheRef.status.length > 1 || cacheRef.status[0] != 200)">状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}}</grey-label>
</td>
<td>
@@ -189,7 +191,7 @@ Vue.component("http-cache-refs-config-box", {
</tr>
</tbody>
</table>
<p class="comment" v-if="refs.length > 1">所有条件匹配顺序为从上到下,可以拖动左侧的<i class="icon bars"></i>排序。</p>
<p class="comment" v-if="refs.length > 1">所有条件匹配顺序为从上到下,可以拖动左侧的<i class="icon bars"></i>排序。服务设置的优先级比全局缓存策略设置的优先级要高。</p>
<button class="ui button tiny" @click.prevent="addRef(false)">+添加缓存设置</button> &nbsp; &nbsp; <a href="" @click.prevent="addRef(true)">+添加不缓存设置</a>
</div>

View File

@@ -0,0 +1,61 @@
Vue.component("http-cache-stale-config", {
props: ["v-cache-stale-config"],
data: function () {
let config = this.vCacheStaleConfig
if (config == null) {
config = {
isPrior: false,
isOn: false,
status: [],
supportStaleIfErrorHeader: true,
life: {
count: 1,
unit: "day"
}
}
}
return {
config: config
}
},
watch: {
config: {
deep: true,
handler: function () {
this.$emit("change", this.config)
}
}
},
methods: {},
template: `<table class="ui table definition selectable">
<tbody>
<tr>
<td class="title">启用过时缓存</td>
<td>
<checkbox v-model="config.isOn"></checkbox>
<p class="comment">选中后,在更新缓存失败后会尝试读取过时的缓存。</p>
</td>
</tr>
<tr v-show="config.isOn">
<td>有效期</td>
<td>
<time-duration-box :v-value="config.life"></time-duration-box>
<p class="comment">缓存在过期之后,仍然保留的时间。</p>
</td>
</tr>
<tr v-show="config.isOn">
<td>状态码</td>
<td><http-status-box :v-status-list="config.status"></http-status-box>
<p class="comment">在这些状态码出现时使用过时缓存,默认支持<code-label>50x</code-label>状态码。</p>
</td>
</tr>
<tr v-show="config.isOn">
<td>支持stale-if-error</td>
<td>
<checkbox v-model="config.supportStaleIfErrorHeader"></checkbox>
<p class="comment">选中后支持在Cache-Control中通过<code-label>stale-if-error</code-label>指定过时缓存有效期。</p>
</td>
</tr>
</tbody>
</table>`
})

View File

@@ -89,6 +89,97 @@ Vue.component("http-cond-url-extension", {
</div>`
})
// URL扩展名条件
Vue.component("http-cond-url-not-extension", {
props: ["v-cond"],
data: function () {
let cond = {
isRequest: true,
param: "${requestPathExtension}",
operator: "not in",
value: "[]"
}
if (this.vCond != null && this.vCond.param == cond.param) {
cond.value = this.vCond.value
}
let extensions = []
try {
extensions = JSON.parse(cond.value)
} catch (e) {
}
return {
cond: cond,
extensions: extensions, // TODO 可以拖动排序
isAdding: false,
addingExt: ""
}
},
watch: {
extensions: function () {
this.cond.value = JSON.stringify(this.extensions)
}
},
methods: {
addExt: function () {
this.isAdding = !this.isAdding
if (this.isAdding) {
let that = this
setTimeout(function () {
that.$refs.addingExt.focus()
}, 100)
}
},
cancelAdding: function () {
this.isAdding = false
this.addingExt = ""
},
confirmAdding: function () {
// TODO 做更详细的校验
// TODO 如果有重复的则提示之
if (this.addingExt.length == 0) {
return
}
if (this.addingExt[0] != ".") {
this.addingExt = "." + this.addingExt
}
this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase()
this.extensions.push(this.addingExt)
// 清除状态
this.cancelAdding()
},
removeExt: function (index) {
this.extensions.$remove(index)
}
},
template: `<div>
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
<div v-if="extensions.length > 0">
<div class="ui label small" v-for="(ext, index) in extensions">{{ext}} <a href="" title="删除" @click.prevent="removeExt(index)"><i class="icon remove"></i></a></div>
<div class="ui divider"></div>
</div>
<div class="ui fields inline" v-if="isAdding">
<div class="ui field">
<input type="text" size="6" maxlength="100" v-model="addingExt" ref="addingExt" placeholder=".xxx" @keyup.enter="confirmAdding" @keypress.enter.prevent="1" />
</div>
<div class="ui field">
<button class="ui button tiny" type="button" @click.prevent="confirmAdding">确认</button>
<a href="" title="取消" @click.prevent="cancelAdding"><i class="icon remove"></i></a>
</div>
</div>
<div style="margin-top: 1em">
<button class="ui button tiny" type="button" @click.prevent="addExt()">+添加扩展名</button>
</div>
<p class="comment">扩展名需要包含点(.)符号,例如<span class="ui label tiny">.jpg</span>、<span class="ui label tiny">.png</span>之类。</p>
</div>`
})
// 根据URL前缀
Vue.component("http-cond-url-prefix", {
props: ["v-cond"],
@@ -97,7 +188,8 @@ Vue.component("http-cond-url-prefix", {
isRequest: true,
param: "${requestPath}",
operator: "prefix",
value: ""
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof (this.vCond.value) == "string") {
cond.value = this.vCond.value
@@ -106,6 +198,11 @@ Vue.component("http-cond-url-prefix", {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `<div>
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
<input type="text" v-model="cond.value"/>
@@ -121,7 +218,8 @@ Vue.component("http-cond-url-not-prefix", {
param: "${requestPath}",
operator: "prefix",
value: "",
isReverse: true
isReverse: true,
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
@@ -130,6 +228,11 @@ Vue.component("http-cond-url-not-prefix", {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `<div>
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
<input type="text" v-model="cond.value"/>
@@ -145,7 +248,8 @@ Vue.component("http-cond-url-eq", {
isRequest: true,
param: "${requestPath}",
operator: "eq",
value: ""
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
@@ -154,6 +258,11 @@ Vue.component("http-cond-url-eq", {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `<div>
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
<input type="text" v-model="cond.value"/>
@@ -169,7 +278,8 @@ Vue.component("http-cond-url-not-eq", {
param: "${requestPath}",
operator: "eq",
value: "",
isReverse: true
isReverse: true,
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
@@ -178,6 +288,11 @@ Vue.component("http-cond-url-not-eq", {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `<div>
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
<input type="text" v-model="cond.value"/>
@@ -193,7 +308,8 @@ Vue.component("http-cond-url-regexp", {
isRequest: true,
param: "${requestPath}",
operator: "regexp",
value: ""
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
@@ -202,6 +318,11 @@ Vue.component("http-cond-url-regexp", {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `<div>
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
<input type="text" v-model="cond.value"/>
@@ -217,7 +338,8 @@ Vue.component("http-cond-url-not-regexp", {
isRequest: true,
param: "${requestPath}",
operator: "not regexp",
value: ""
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
@@ -226,6 +348,11 @@ Vue.component("http-cond-url-not-regexp", {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `<div>
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
<input type="text" v-model="cond.value"/>
@@ -233,6 +360,67 @@ Vue.component("http-cond-url-not-regexp", {
</div>`
})
// User-Agent正则匹配
Vue.component("http-cond-user-agent-regexp", {
props: ["v-cond"],
data: function () {
let cond = {
isRequest: true,
param: "${userAgent}",
operator: "regexp",
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `<div>
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
<input type="text" v-model="cond.value"/>
<p class="comment">匹配User-Agent的正则表达式比如<code-label>Android|iPhone</code-label>。</p>
</div>`
})
// User-Agent正则不匹配
Vue.component("http-cond-user-agent-not-regexp", {
props: ["v-cond"],
data: function () {
let cond = {
isRequest: true,
param: "${userAgent}",
operator: "not regexp",
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `<div>
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
<input type="text" v-model="cond.value"/>
<p class="comment">匹配User-Agent的正则表达式比如<code-label>Android|iPhone</code-label>,如果匹配,则排除此条件。</p>
</div>`
})
// 根据MimeType
Vue.component("http-cond-mime-type", {
props: ["v-cond"],
@@ -376,7 +564,8 @@ Vue.component("http-cond-params", {
isRequest: true,
param: "",
operator: window.REQUEST_COND_OPERATORS[0].op,
value: ""
value: "",
isCaseInsensitive: false
}
if (this.vCond != null) {
cond = this.vCond
@@ -582,5 +771,15 @@ Vue.component("http-cond-params", {
</div>
</td>
</tr>
<tr v-if="['regexp', 'not regexp', 'eq', 'not', 'prefix', 'suffix', 'contains', 'not contains', 'in', 'not in'].$contains(cond.operator)">
<td>不区分大小写</td>
<td>
<div class="ui checkbox">
<input type="checkbox" v-model="cond.isCaseInsensitive"/>
<label></label>
</div>
<p class="comment">选中后表示对比时忽略参数值的大小写。</p>
</td>
</tr>
</tbody>`
})

View File

@@ -0,0 +1,70 @@
Vue.component("http-expires-time-config-box", {
props: ["v-expires-time"],
data: function () {
let expiresTime = this.vExpiresTime
if (expiresTime == null) {
expiresTime = {
isPrior: false,
isOn: false,
overwrite: true,
autoCalculate: true,
duration: {count: -1, "unit": "hour"}
}
}
return {
expiresTime: expiresTime
}
},
watch: {
"expiresTime.isPrior": function () {
this.notifyChange()
},
"expiresTime.isOn": function () {
this.notifyChange()
},
"expiresTime.overwrite": function () {
this.notifyChange()
},
"expiresTime.autoCalculate": function () {
this.notifyChange()
}
},
methods: {
notifyChange: function () {
this.$emit("change", this.expiresTime)
}
},
template: `<div>
<table class="ui table">
<prior-checkbox :v-config="expiresTime"></prior-checkbox>
<tbody v-show="expiresTime.isPrior">
<tr>
<td class="title">是否启用</td>
<td><checkbox v-model="expiresTime.isOn"></checkbox>
<p class="comment">启用后将会在响应的Header中添加<code-label>Expires</code-label>字段,浏览器据此会将内容缓存在客户端;同时,在管理后台执行清理缓存时,也将无法清理客户端已有的缓存。</p>
</td>
</tr>
<tr v-show="expiresTime.isPrior && expiresTime.isOn">
<td>覆盖源站设置</td>
<td>
<checkbox v-model="expiresTime.overwrite"></checkbox>
<p class="comment">选中后会覆盖源站Header中已有的<code-label>Expires</code-label>字段。</p>
</td>
</tr>
<tr v-show="expiresTime.isPrior && expiresTime.isOn">
<td>自动计算时间</td>
<td><checkbox v-model="expiresTime.autoCalculate"></checkbox>
<p class="comment">根据已设置的缓存有效期进行计算。</p>
</td>
</tr>
<tr v-show="expiresTime.isPrior && expiresTime.isOn && !expiresTime.autoCalculate">
<td>强制缓存时间</td>
<td>
<time-duration-box :v-value="expiresTime.duration" @change="notifyChange"></time-duration-box>
<p class="comment">从客户端访问的时间开始要缓存的时长。</p>
</td>
</tr>
</tbody>
</table>
</div>`
})

View File

@@ -48,8 +48,10 @@ Vue.component("http-firewall-actions-box", {
var defaultPageBody = `<!DOCTYPE html>
<html>
<title>403 Forbidden</title>
<body>
403 Forbidden
<h1>403 Forbidden</h1>
<address>Request ID: \${requestId}.</address>
</body>
</html>`

View File

@@ -1,10 +1,10 @@
Vue.component("http-header-policy-box", {
props: ["v-request-header-policy", "v-request-header-ref", "v-response-header-policy", "v-response-header-ref", "v-params", "v-is-location", "v-is-group", "v-has-group-request-config", "v-has-group-response-config", "v-group-setting-url"],
data: function () {
let type = "request"
let type = "response"
let hash = window.location.hash
if (hash == "#response") {
type = "response"
if (hash == "#request") {
type = "request"
}
// ref
@@ -53,7 +53,7 @@ Vue.component("http-header-policy-box", {
responseDeletingHeaders = responsePolicy.deleteHeaders
}
}
return {
type: type,
typeName: (type == "request") ? "请求" : "响应",
@@ -72,23 +72,23 @@ Vue.component("http-header-policy-box", {
window.location.reload()
},
addSettingHeader: function (policyId) {
teaweb.popup("/servers/server/settings/headers/createSetPopup?" + this.vParams + "&headerPolicyId=" + policyId, {
teaweb.popup("/servers/server/settings/headers/createSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + this.type, {
callback: function () {
window.location.reload()
teaweb.successRefresh("保存成功")
}
})
},
addDeletingHeader: function (policyId, type) {
teaweb.popup("/servers/server/settings/headers/createDeletePopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + type, {
callback: function () {
window.location.reload()
teaweb.successRefresh("保存成功")
}
})
},
updateSettingPopup: function (policyId, headerId) {
teaweb.popup("/servers/server/settings/headers/updateSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&headerId=" + headerId, {
teaweb.popup("/servers/server/settings/headers/updateSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&headerId=" + headerId+ "&type=" + this.type, {
callback: function () {
window.location.reload()
teaweb.successRefresh("保存成功")
}
})
},
@@ -118,8 +118,8 @@ Vue.component("http-header-policy-box", {
},
template: `<div>
<div class="ui menu tabular small">
<a class="item" :class="{active:type == 'request'}" @click.prevent="selectType('request')">请求Header<span v-if="requestSettingHeaders.length > 0">({{requestSettingHeaders.length}})</span></a>
<a class="item" :class="{active:type == 'response'}" @click.prevent="selectType('response')">响应Header<span v-if="responseSettingHeaders.length > 0">({{responseSettingHeaders.length}})</span></a>
<a class="item" :class="{active:type == 'request'}" @click.prevent="selectType('request')">请求Header<span v-if="requestSettingHeaders.length > 0">({{requestSettingHeaders.length}})</span></a>
</div>
<div class="margin"></div>
@@ -152,7 +152,17 @@ Vue.component("http-header-policy-box", {
</tr>
</thead>
<tr v-for="header in requestSettingHeaders">
<td class="five wide">{{header.name}}</td>
<td class="five wide">
{{header.name}}
<div>
<span v-if="header.status != null && header.status.codes != null && !header.status.always"><grey-label v-for="code in header.status.codes" :key="code">{{code}}</grey-label></span>
<span v-if="header.methods != null && header.methods.length > 0"><grey-label v-for="method in header.methods" :key="method">{{method}}</grey-label></span>
<span v-if="header.domains != null && header.domains.length > 0"><grey-label v-for="domain in header.domains" :key="domain">{{domain}}</grey-label></span>
<grey-label v-if="header.shouldAppend">附加</grey-label>
<grey-label v-if="header.disableRedirect">跳转禁用</grey-label>
<grey-label v-if="header.shouldReplace && header.replaceValues != null && header.replaceValues.length > 0">替换</grey-label>
</div>
</td>
<td>{{header.value}}</td>
<td><a href="" @click.prevent="updateSettingPopup(vRequestHeaderPolicy.id, header.id)">修改</a> &nbsp; <a href="" @click.prevent="deleteHeader(vRequestHeaderPolicy.id, 'setHeader', header.id)">删除</a> </td>
</tr>
@@ -201,7 +211,17 @@ Vue.component("http-header-policy-box", {
</tr>
</thead>
<tr v-for="header in responseSettingHeaders">
<td class="five wide">{{header.name}}</td>
<td class="five wide">
{{header.name}}
<div>
<span v-if="header.status != null && header.status.codes != null && !header.status.always"><grey-label v-for="code in header.status.codes" :key="code">{{code}}</grey-label></span>
<span v-if="header.methods != null && header.methods.length > 0"><grey-label v-for="method in header.methods" :key="method">{{method}}</grey-label></span>
<span v-if="header.domains != null && header.domains.length > 0"><grey-label v-for="domain in header.domains" :key="domain">{{domain}}</grey-label></span>
<grey-label v-if="header.shouldAppend">附加</grey-label>
<grey-label v-if="header.disableRedirect">跳转禁用</grey-label>
<grey-label v-if="header.shouldReplace && header.replaceValues != null && header.replaceValues.length > 0">替换</grey-label>
</div>
</td>
<td>{{header.value}}</td>
<td><a href="" @click.prevent="updateSettingPopup(vResponseHeaderPolicy.id, header.id)">修改</a> &nbsp; <a href="" @click.prevent="deleteHeader(vResponseHeaderPolicy.id, 'setHeader', header.id)">删除</a> </td>
</tr>

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