diff --git a/internal/web/actions/default/servers/logs/index.go b/internal/web/actions/default/servers/logs/index.go index 41e646de..b08b8ed9 100644 --- a/internal/web/actions/default/servers/logs/index.go +++ b/internal/web/actions/default/servers/logs/index.go @@ -7,6 +7,7 @@ import ( "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/iwind/TeaGo/lists" + "github.com/iwind/TeaGo/maps" timeutil "github.com/iwind/TeaGo/utils/time" "regexp" "strings" @@ -22,12 +23,14 @@ func (this *IndexAction) Init() { } func (this *IndexAction) RunGet(params struct { - Day string - Keyword string - Ip string - Domain string - HasError int - HasWAF int + ClusterId int64 + NodeId int64 + Day string + Keyword string + Ip string + Domain string + HasError int + HasWAF int RequestId string ServerId int64 @@ -38,6 +41,8 @@ func (this *IndexAction) RunGet(params struct { params.Day = timeutil.Format("Y-m-d") } + this.Data["clusterId"] = params.ClusterId + this.Data["nodeId"] = params.NodeId this.Data["serverId"] = 0 this.Data["path"] = this.Request.URL.Path this.Data["day"] = params.Day @@ -66,6 +71,8 @@ func (this *IndexAction) RunGet(params struct { var before = time.Now() resp, err := this.RPC().HTTPAccessLogRPC().ListHTTPAccessLogs(this.AdminContext(), &pb.ListHTTPAccessLogsRequest{ RequestId: params.RequestId, + NodeClusterId: params.ClusterId, + NodeId: params.NodeId, ServerId: params.ServerId, HasError: params.HasError > 0, HasFirewallPolicy: params.HasWAF > 0, @@ -108,6 +115,8 @@ func (this *IndexAction) RunGet(params struct { this.Data["hasPrev"] = true prevResp, err := this.RPC().HTTPAccessLogRPC().ListHTTPAccessLogs(this.AdminContext(), &pb.ListHTTPAccessLogsRequest{ RequestId: params.RequestId, + NodeClusterId: params.ClusterId, + NodeId: params.NodeId, ServerId: params.ServerId, HasError: params.HasError > 0, HasFirewallPolicy: params.HasWAF > 0, @@ -144,5 +153,20 @@ func (this *IndexAction) RunGet(params struct { } this.Data["regions"] = regionMap + // 集群列表 + var clusterMaps = []maps.Map{} + clusterResp, err := this.RPC().NodeClusterRPC().FindAllEnabledNodeClusters(this.AdminContext(), &pb.FindAllEnabledNodeClustersRequest{}) + if err != nil { + this.ErrorPage(err) + return + } + for _, cluster := range clusterResp.NodeClusters { + clusterMaps = append(clusterMaps, maps.Map{ + "id": cluster.Id, + "name": cluster.Name, + }) + } + this.Data["clusters"] = clusterMaps + this.Show() } diff --git a/internal/web/actions/default/servers/logs/init.go b/internal/web/actions/default/servers/logs/init.go index e504e224..0760cfaa 100644 --- a/internal/web/actions/default/servers/logs/init.go +++ b/internal/web/actions/default/servers/logs/init.go @@ -17,6 +17,7 @@ func init() { Prefix("/servers/logs"). Get("", new(IndexAction)). GetPost("/settings", new(SettingsAction)). + Post("/nodeOptions", new(NodeOptionsAction)). EndAll() }) } diff --git a/internal/web/actions/default/servers/logs/nodeOptions.go b/internal/web/actions/default/servers/logs/nodeOptions.go new file mode 100644 index 00000000..84f33caf --- /dev/null +++ b/internal/web/actions/default/servers/logs/nodeOptions.go @@ -0,0 +1,34 @@ +// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package logs + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/maps" +) + +type NodeOptionsAction struct { + actionutils.ParentAction +} + +func (this *NodeOptionsAction) RunPost(params struct { + ClusterId int64 +}) { + resp, err := this.RPC().NodeRPC().FindAllEnabledNodesWithNodeClusterId(this.AdminContext(), &pb.FindAllEnabledNodesWithNodeClusterIdRequest{NodeClusterId: params.ClusterId}) + if err != nil { + this.ErrorPage(err) + return + } + + var nodeMaps = []maps.Map{} + for _, node := range resp.Nodes { + nodeMaps = append(nodeMaps, maps.Map{ + "id": node.Id, + "name": node.Name, + }) + } + this.Data["nodes"] = nodeMaps + + this.Success() +} diff --git a/web/public/js/components.js b/web/public/js/components.js index 65f839ab..3bebc773 100755 --- a/web/public/js/components.js +++ b/web/public/js/components.js @@ -2257,7 +2257,7 @@ Vue.component("http-firewall-rule-label", { }, template: `
-
+
{{rule.name}}[{{rule.param}}] @@ -2276,6 +2276,9 @@ Vue.component("http-firewall-rule-label", { {{rule.value}} + + ({{rule.description}}) + 规则错误
` @@ -7833,7 +7836,14 @@ Vue.component("http-remote-addr-config-box", { // 访问日志搜索框 Vue.component("http-access-log-search-box", { - props: ["v-ip", "v-domain", "v-keyword"], + props: ["v-ip", "v-domain", "v-keyword", "v-cluster-id", "v-node-id", "v-clusters"], + mounted: function () { + if (this.vClusterId >0) { + this.changeCluster({ + value: this.vClusterId + }) + } + }, data: function () { let ip = this.vIp if (ip == null) { @@ -7853,7 +7863,8 @@ Vue.component("http-access-log-search-box", { return { ip: ip, domain: domain, - keyword: keyword + keyword: keyword, + nodes: [] } }, methods: { @@ -7885,9 +7896,23 @@ Vue.component("http-access-log-search-box", { parent.submit() }, 500) } + }, + changeCluster: function (item) { + this.nodes = [] + if (item != null) { + let that = this + Tea.action("/servers/logs/nodeOptions") + .params({ + clusterId: item.value + }) + .post() + .success(function (resp) { + that.nodes = resp.data.nodes + }) + } } }, - template: `
+ template: `
@@ -7912,8 +7937,16 @@ Vue.component("http-access-log-search-box", {
+
+
+
+ +
+
+ +
- +
` @@ -8412,7 +8445,8 @@ Vue.component("http-firewall-block-options", { return { blockOptions: this.vBlockOptions, statusCode: this.vBlockOptions.statusCode, - timeout: this.vBlockOptions.timeout + timeout: this.vBlockOptions.timeout, + isEditing: false } }, watch: { @@ -8433,9 +8467,15 @@ Vue.component("http-firewall-block-options", { } } }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + } + }, template: `
- - + + 状态码:{{statusCode}} / 提示内容:[{{blockOptions.body.length}}字符][无] / 超时时间:{{timeout}}秒 +
状态码 @@ -8520,6 +8560,9 @@ Vue.component("http-firewall-rules-box", { | {{paramFilter.code}} {{rule.operator}} {{rule.value}} + + ({{rule.description}}) + @@ -9824,6 +9867,99 @@ Vue.component("traffic-limit-config-box", { ` }) +Vue.component("firewall-syn-flood-config-box", { + props: ["v-syn-flood-config"], + data: function () { + let config = this.vSynFloodConfig + if (config == null) { + config = { + isOn: false, + minAttempts: 10, + timeoutSeconds: 600, + ignoreLocal: true + } + } + return { + config: config, + isEditing: false, + minAttempts: config.minAttempts, + timeoutSeconds: config.timeoutSeconds + } + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + } + }, + watch: { + minAttempts: function (v) { + let count = parseInt(v) + if (isNaN(count)) { + count = 10 + } + if (count < 3) { + count = 3 + } + this.config.minAttempts = count + }, + timeoutSeconds: function (v) { + let seconds = parseInt(v) + if (isNaN(seconds)) { + seconds = 10 + } + if (seconds < 60) { + seconds = 60 + } + this.config.timeoutSeconds = seconds + } + }, + template: `
+ + + + 已启用 / 空连接次数:{{config.minAttempts}}次/分钟 / 封禁时间:{{config.timeoutSeconds}}秒 / 忽略局域网访问 + + 未启用 + + + + + + + + + + + + + + + + + + + + +
是否启用 + +

启用后,WAF将会尝试自动检测并阻止SYN Flood攻击。此功能需要节点已安装并启用Firewalld。

+
空连接次数 +
+ + 次/分钟 +
+

超过此数字的"空连接"将被视为SYN Flood攻击,为了防止误判,此数值默认不小于3。

+
封禁时间 +
+ + +
+
忽略局域网访问 + +
+
` +}) + // TODO 支持关键词搜索 // TODO 改成弹窗选择 Vue.component("admin-selector", { @@ -10091,6 +10227,9 @@ Vue.component("ip-list-table", { {{item.sourcePolicy.name}} » {{item.sourceGroup.name}} » {{item.sourceSet.name}} {{item.sourcePolicy.name}} » {{item.sourceGroup.name}} » {{item.sourceSet.name}} +
日志   @@ -11468,6 +11607,143 @@ Vue.component("request-variables-describer", { }) +Vue.component("combo-box", { + props: ["name", "title", "placeholder", "size", "v-items", "v-value"], + data: function () { + let items = this.vItems + if (items == null || !(items instanceof Array)) { + items = [] + } + + // 自动使用ID作为值 + items.forEach(function (v) { + if (v.value == null) { + v.value = v.id + } + }) + + // 当前选中项 + let selectedItem = null + if (this.vValue != null) { + let that = this + items.forEach(function (v) { + if (v.value == that.vValue) { + selectedItem = v + } + }) + } + + return { + allItems: items, + items: items.$copy(), + selectedItem: selectedItem, + keyword: "", + visible: false, + hideTimer: null, + hoverIndex: 0 + } + }, + methods: { + reset: function () { + this.selectedItem = null + this.change() + this.hoverIndex = 0 + + let that = this + setTimeout(function () { + if (that.$refs.searchBox) { + that.$refs.searchBox.focus() + } + }) + }, + changeKeyword: function () { + this.hoverIndex = 0 + let keyword = this.keyword + if (keyword.length == 0) { + this.items = this.allItems.$copy() + return + } + this.items = this.allItems.$copy().filter(function (v) { + return teaweb.match(v.name, keyword) + }) + }, + selectItem: function (item) { + this.selectedItem = item + this.change() + this.hoverIndex = 0 + this.keyword = "" + this.changeKeyword() + }, + confirm: function () { + if (this.items.length > this.hoverIndex) { + this.selectItem(this.items[this.hoverIndex]) + } + }, + show: function () { + this.visible = true + + // 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换 + }, + hide: function () { + let that = this + this.hideTimer = setTimeout(function () { + that.visible = false + }, 500) + }, + downItem: function () { + this.hoverIndex++ + if (this.hoverIndex > this.items.length - 1) { + this.hoverIndex = 0 + } + this.focusItem() + }, + upItem: function () { + this.hoverIndex-- + if (this.hoverIndex < 0) { + this.hoverIndex = 0 + } + this.focusItem() + }, + focusItem: function () { + if (this.hoverIndex < this.items.length) { + this.$refs.itemRef[this.hoverIndex].focus() + let that = this + setTimeout(function () { + that.$refs.searchBox.focus() + if (that.hideTimer != null) { + clearTimeout(that.hideTimer) + that.hideTimer = null + } + }) + } + }, + change: function () { + this.$emit("change", this.selectedItem) + } + }, + template: `
+ +
+ +
+ + +
+ + {{title}}:{{selectedItem.name}} + + +
+ + +
+ +
+
` +}) + Vue.component("time-duration-box", { props: ["v-name", "v-value", "v-count", "v-unit"], mounted: function () { diff --git a/web/public/js/components/common/combo-box.js b/web/public/js/components/common/combo-box.js new file mode 100644 index 00000000..902b9f9d --- /dev/null +++ b/web/public/js/components/common/combo-box.js @@ -0,0 +1,136 @@ +Vue.component("combo-box", { + props: ["name", "title", "placeholder", "size", "v-items", "v-value"], + data: function () { + let items = this.vItems + if (items == null || !(items instanceof Array)) { + items = [] + } + + // 自动使用ID作为值 + items.forEach(function (v) { + if (v.value == null) { + v.value = v.id + } + }) + + // 当前选中项 + let selectedItem = null + if (this.vValue != null) { + let that = this + items.forEach(function (v) { + if (v.value == that.vValue) { + selectedItem = v + } + }) + } + + return { + allItems: items, + items: items.$copy(), + selectedItem: selectedItem, + keyword: "", + visible: false, + hideTimer: null, + hoverIndex: 0 + } + }, + methods: { + reset: function () { + this.selectedItem = null + this.change() + this.hoverIndex = 0 + + let that = this + setTimeout(function () { + if (that.$refs.searchBox) { + that.$refs.searchBox.focus() + } + }) + }, + changeKeyword: function () { + this.hoverIndex = 0 + let keyword = this.keyword + if (keyword.length == 0) { + this.items = this.allItems.$copy() + return + } + this.items = this.allItems.$copy().filter(function (v) { + return teaweb.match(v.name, keyword) + }) + }, + selectItem: function (item) { + this.selectedItem = item + this.change() + this.hoverIndex = 0 + this.keyword = "" + this.changeKeyword() + }, + confirm: function () { + if (this.items.length > this.hoverIndex) { + this.selectItem(this.items[this.hoverIndex]) + } + }, + show: function () { + this.visible = true + + // 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换 + }, + hide: function () { + let that = this + this.hideTimer = setTimeout(function () { + that.visible = false + }, 500) + }, + downItem: function () { + this.hoverIndex++ + if (this.hoverIndex > this.items.length - 1) { + this.hoverIndex = 0 + } + this.focusItem() + }, + upItem: function () { + this.hoverIndex-- + if (this.hoverIndex < 0) { + this.hoverIndex = 0 + } + this.focusItem() + }, + focusItem: function () { + if (this.hoverIndex < this.items.length) { + this.$refs.itemRef[this.hoverIndex].focus() + let that = this + setTimeout(function () { + that.$refs.searchBox.focus() + if (that.hideTimer != null) { + clearTimeout(that.hideTimer) + that.hideTimer = null + } + }) + } + }, + change: function () { + this.$emit("change", this.selectedItem) + } + }, + template: `
+ +
+ +
+ + +
+ + {{title}}:{{selectedItem.name}} + + +
+ + +
+ +
+
` +}) \ No newline at end of file diff --git a/web/public/js/components/server/http-access-log-search-box.js b/web/public/js/components/server/http-access-log-search-box.js index 35a3be1e..6c59bf08 100644 --- a/web/public/js/components/server/http-access-log-search-box.js +++ b/web/public/js/components/server/http-access-log-search-box.js @@ -1,6 +1,13 @@ // 访问日志搜索框 Vue.component("http-access-log-search-box", { - props: ["v-ip", "v-domain", "v-keyword"], + props: ["v-ip", "v-domain", "v-keyword", "v-cluster-id", "v-node-id", "v-clusters"], + mounted: function () { + if (this.vClusterId >0) { + this.changeCluster({ + value: this.vClusterId + }) + } + }, data: function () { let ip = this.vIp if (ip == null) { @@ -20,7 +27,8 @@ Vue.component("http-access-log-search-box", { return { ip: ip, domain: domain, - keyword: keyword + keyword: keyword, + nodes: [] } }, methods: { @@ -52,9 +60,23 @@ Vue.component("http-access-log-search-box", { parent.submit() }, 500) } + }, + changeCluster: function (item) { + this.nodes = [] + if (item != null) { + let that = this + Tea.action("/servers/logs/nodeOptions") + .params({ + clusterId: item.value + }) + .post() + .success(function (resp) { + that.nodes = resp.data.nodes + }) + } } }, - template: `
+ template: `
@@ -79,8 +101,16 @@ Vue.component("http-access-log-search-box", {
+
+
+
+ +
+
+ +
- +
` diff --git a/web/views/@default/servers/logs/index.html b/web/views/@default/servers/logs/index.html index f334f4c9..670c476b 100644 --- a/web/views/@default/servers/logs/index.html +++ b/web/views/@default/servers/logs/index.html @@ -2,9 +2,9 @@ {$template "/datepicker"} - 所有日志 - 错误日志 - WAF日志 + 所有日志 + 错误日志 + WAF日志 设置 @@ -12,7 +12,7 @@ - +
@@ -31,10 +31,10 @@
- 上一页 + 上一页 上一页   |   - 下一页 + 下一页 下一页