diff --git a/internal/rpc/rpc_client.go b/internal/rpc/rpc_client.go index 8a3bed0b..211d6531 100644 --- a/internal/rpc/rpc_client.go +++ b/internal/rpc/rpc_client.go @@ -207,6 +207,10 @@ func (this *RPCClient) HTTPFirewallRuleSetRPC() pb.HTTPFirewallRuleSetServiceCli return pb.NewHTTPFirewallRuleSetServiceClient(this.pickConn()) } +func (this *RPCClient) FirewallRPC() pb.FirewallServiceClient { + return pb.NewFirewallServiceClient(this.pickConn()) +} + func (this *RPCClient) HTTPLocationRPC() pb.HTTPLocationServiceClient { return pb.NewHTTPLocationServiceClient(this.pickConn()) } diff --git a/internal/web/actions/default/dashboard/boards/waf.go b/internal/web/actions/default/dashboard/boards/waf.go index fcf111f9..9e00c9a5 100644 --- a/internal/web/actions/default/dashboard/boards/waf.go +++ b/internal/web/actions/default/dashboard/boards/waf.go @@ -5,6 +5,8 @@ package boards 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" ) type WafAction struct { @@ -21,5 +23,54 @@ func (this *WafAction) RunGet(params struct{}) { return } + resp, err := this.RPC().FirewallRPC().ComposeFirewallGlobalBoard(this.AdminContext(), &pb.ComposeFirewallGlobalBoardRequest{}) + if err != nil { + this.ErrorPage(err) + return + } + this.Data["board"] = maps.Map{ + "countDailyLogs": resp.CountDailyLogs, + "countDailyBlocks": resp.CountDailyBlocks, + "countDailyCaptcha": resp.CountDailyCaptcha, + "countWeeklyBlocks": resp.CountWeeklyBlocks, + } + + { + var statMaps = []maps.Map{} + for _, stat := range resp.HourlyStats { + statMaps = append(statMaps, maps.Map{ + "hour": stat.Hour, + "countLogs": stat.CountLogs, + "countCaptcha": stat.CountCaptcha, + "countBlocks": stat.CountBlocks, + }) + } + this.Data["hourlyStats"] = statMaps + } + + { + var statMaps = []maps.Map{} + for _, stat := range resp.DailyStats { + statMaps = append(statMaps, maps.Map{ + "day": stat.Day, + "countLogs": stat.CountLogs, + "countCaptcha": stat.CountCaptcha, + "countBlocks": stat.CountBlocks, + }) + } + this.Data["dailyStats"] = statMaps + } + + { + var statMaps = []maps.Map{} + for _, stat := range resp.HttpFirewallRuleGroups { + statMaps = append(statMaps, maps.Map{ + "name": stat.HttpFirewallRuleGroup.Name, + "count": stat.Count, + }) + } + this.Data["groupStats"] = statMaps + } + this.Show() } diff --git a/internal/web/actions/default/dashboard/boards/wafLogs.go b/internal/web/actions/default/dashboard/boards/wafLogs.go new file mode 100644 index 00000000..fec2cfec --- /dev/null +++ b/internal/web/actions/default/dashboard/boards/wafLogs.go @@ -0,0 +1,28 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package boards + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + timeutil "github.com/iwind/TeaGo/utils/time" +) + +type WafLogsAction struct { + actionutils.ParentAction +} + +func (this *WafLogsAction) RunPost(params struct{}) { + resp, err := this.RPC().HTTPAccessLogRPC().ListHTTPAccessLogs(this.AdminContext(), &pb.ListHTTPAccessLogsRequest{ + HasFirewallPolicy: true, + Reverse: false, + Day: timeutil.Format("Ymd"), + Size: 5, + }) + if err != nil { + this.ErrorPage(err) + return + } + this.Data["accessLogs"] = resp.HttpAccessLogs + this.Success() +} diff --git a/internal/web/actions/default/dashboard/init.go b/internal/web/actions/default/dashboard/init.go index 07ef2959..9321f294 100644 --- a/internal/web/actions/default/dashboard/init.go +++ b/internal/web/actions/default/dashboard/init.go @@ -18,6 +18,7 @@ func init() { Prefix("/dashboard/boards"). Get("", new(boards.IndexAction)). Get("/waf", new(boards.WafAction)). + Post("/wafLogs", new(boards.WafLogsAction)). Get("/dns", new(boards.DnsAction)). Get("/user", new(boards.UserAction)). diff --git a/web/public/js/utils.js b/web/public/js/utils.js index d73a1b10..1ea25164 100644 --- a/web/public/js/utils.js +++ b/web/public/js/utils.js @@ -134,6 +134,24 @@ window.teaweb = { formatNumber: function (x) { return x.toString().replace(/\B(?= 1000 * 1000 * 1000) { + unit = "B" + divider = 1000 * 1000 * 1000 + } else if (x >= 1000 * 1000) { + unit = "M" + divider = 1000 * 1000 + } else if (x >= 1000) { + unit = "K" + divider = 1000 + } + if (unit.length == 0) { + return x.toString() + } + return (Math.round(x * 100 / divider) / 100) + unit + }, bytesAxis: function (stats, countFunc) { let max = Math.max.apply(this, stats.map(countFunc)) let divider = 1 @@ -362,12 +380,34 @@ window.teaweb = { }, renderBarChart: function (options) { let chartId = options.id + if (chartId == null || chartId.length == 0) { + throw new Error("'options.id' should not be empty") + } + let name = options.name let values = options.values + if (values == null || !(values instanceof Array)) { + throw new Error("'options.values' should be array") + } + let xFunc = options.x + if (typeof (xFunc) != "function") { + throw new Error("'options.x' should be a function") + } + let tooltipFunc = options.tooltip + if (typeof(tooltipFunc) != "function") { + throw new Error("'options.tooltip' should be a function") + } + let axis = options.axis + if (axis == null) { + axis = {unit: "", count: 1} + } let valueFunc = options.value + if (typeof (valueFunc) != "function") { + throw new Error("'options.value' should be a function") + } let click = options.click let chartBox = document.getElementById(chartId) diff --git a/web/views/@default/dashboard/boards/waf.css b/web/views/@default/dashboard/boards/waf.css index 7a6beaea..896d8fc2 100644 --- a/web/views/@default/dashboard/boards/waf.css +++ b/web/views/@default/dashboard/boards/waf.css @@ -30,4 +30,8 @@ .chart-box { height: 20em; } +.color-span { + font-size: 0.8em; + padding: 4px; +} /*# sourceMappingURL=waf.css.map */ \ No newline at end of file diff --git a/web/views/@default/dashboard/boards/waf.css.map b/web/views/@default/dashboard/boards/waf.css.map index e252fba4..a341a494 100644 --- a/web/views/@default/dashboard/boards/waf.css.map +++ b/web/views/@default/dashboard/boards/waf.css.map @@ -1 +1 @@ -{"version":3,"sources":["waf.less"],"names":[],"mappings":"AAAA,GAAG,QACF;EACC,kBAAA;EACA,UAAA;EACA,UAAA;;AAIF;EACC,0BAAA;EACA,2BAAA;;AAFD,KAIC;EACC,kBAAA;EACA,4BAAA;;AANF,KAIC,QAIC,IAAG;EACF,iBAAA;;AATH,KAIC,QAIC,IAAG,MAGF;EACC,cAAA;EACA,mBAAA;;AAbJ,KAkBC,QAAO;EACN,eAAA;;AAnBF,KAsBC,GACC;EACC,aAAA;;AAxBH,KA4BC,QAAO,MACN;EACC,eAAA;;AAKH;EACC,YAAA","file":"waf.css"} \ No newline at end of file +{"version":3,"sources":["waf.less"],"names":[],"mappings":"AAAA,GAAG,QACF;EACC,kBAAA;EACA,UAAA;EACA,UAAA;;AAIF;EACC,0BAAA;EACA,2BAAA;;AAFD,KAIC;EACC,kBAAA;EACA,4BAAA;;AANF,KAIC,QAIC,IAAG;EACF,iBAAA;;AATH,KAIC,QAIC,IAAG,MAGF;EACC,cAAA;EACA,mBAAA;;AAbJ,KAkBC,QAAO;EACN,eAAA;;AAnBF,KAsBC,GACC;EACC,aAAA;;AAxBH,KA4BC,QAAO,MACN;EACC,eAAA;;AAKH;EACC,YAAA;;AAGD;EACC,gBAAA;EACA,YAAA","file":"waf.css"} \ No newline at end of file diff --git a/web/views/@default/dashboard/boards/waf.html b/web/views/@default/dashboard/boards/waf.html index 6ad5ba32..7ef0a3f1 100644 --- a/web/views/@default/dashboard/boards/waf.html +++ b/web/views/@default/dashboard/boards/waf.html @@ -1,3 +1,56 @@ {$layout} {$template "menu"} -{$template "/echarts"} \ No newline at end of file +{$template "/echarts"} + +
+
+

今日拦截

+
{{board.countDailyBlocks}}
+
+ +
+

今日验证码验证

+
{{board.countDailyCaptcha}}
+
+ +
+

今日记录

+
{{board.countDailyLogs}}
+
+ +
+

本周拦截

+
{{board.countWeeklyBlocks}}
+
+
+ + +
+
+

最新记录

+ + + + +
+
+
+ + + + +
+
+
+ + +

拦截类型分布

+
\ No newline at end of file diff --git a/web/views/@default/dashboard/boards/waf.js b/web/views/@default/dashboard/boards/waf.js new file mode 100644 index 00000000..65b81cef --- /dev/null +++ b/web/views/@default/dashboard/boards/waf.js @@ -0,0 +1,166 @@ +Tea.context(function () { + this.$delay(function () { + let that = this + + this.board.countDailyBlocks = teaweb.formatCount(this.board.countDailyBlocks) + this.board.countDailyCaptcha = teaweb.formatCount(this.board.countDailyCaptcha) + this.board.countDailyLogs = teaweb.formatCount(this.board.countDailyLogs) + this.board.countWeeklyBlocks = teaweb.formatCount(this.board.countWeeklyBlocks) + + this.reloadHourlyChart() + this.reloadGroupChart() + this.reloadAccessLogs() + }) + + this.requestsTab = "hourly" + + this.selectRequestsTab = function (tab) { + this.requestsTab = tab + this.$delay(function () { + switch (tab) { + case "hourly": + this.reloadHourlyChart() + break + case "daily": + this.reloadDailyChart() + break + } + }) + } + + this.reloadHourlyChart = function () { + let axis = teaweb.countAxis(this.hourlyStats, function (v) { + return [v.countLogs, v.countCaptcha, v.countBlocks].$max() + }) + let that = this + this.reloadLineChart("hourly-chart", "按小时统计", this.hourlyStats, function (v) { + return v.hour.substring(8, 10) + }, function (args) { + let index = args.dataIndex + let hour = that.hourlyStats[index].hour.substring(0, 4) + "-" + that.hourlyStats[index].hour.substring(4, 6) + "-" + that.hourlyStats[index].hour.substring(6, 8) + " " + that.hourlyStats[index].hour.substring(8) + return hour + "时
拦截: " + + teaweb.formatNumber(that.hourlyStats[index].countBlocks) + "
验证码: " + teaweb.formatNumber(that.hourlyStats[index].countCaptcha) + "
记录: " + teaweb.formatNumber(that.hourlyStats[index].countLogs) + }, axis) + } + + this.reloadDailyChart = function () { + let axis = teaweb.countAxis(this.dailyStats, function (v) { + return [v.countLogs, v.countCaptcha, v.countBlocks].$max() + }) + let that = this + this.reloadLineChart("daily-chart", "按天统计", this.dailyStats, function (v) { + return v.day.substring(4, 6) + "月" + v.day.substring(6, 8) + "日" + }, function (args) { + let index = args.dataIndex + let day = that.dailyStats[index].day.substring(0, 4) + "-" + that.dailyStats[index].day.substring(4, 6) + "-" + that.dailyStats[index].day.substring(6, 8) + return day + "
拦截: " + + teaweb.formatNumber(that.dailyStats[index].countBlocks) + "
验证码: " + teaweb.formatNumber(that.dailyStats[index].countCaptcha) + "
记录: " + teaweb.formatNumber(that.dailyStats[index].countLogs) + }, axis) + } + + this.reloadLineChart = function (chartId, name, stats, xFunc, tooltipFunc, axis) { + let chartBox = document.getElementById(chartId) + if (chartBox == null) { + return + } + let chart = echarts.init(chartBox) + let option = { + xAxis: { + data: stats.map(xFunc) + }, + yAxis: { + axisLabel: { + formatter: function (value) { + return value + axis.unit + } + } + }, + tooltip: { + show: true, + trigger: "item", + formatter: tooltipFunc + }, + grid: { + left: 42, + top: 10, + right: 20, + bottom: 20 + }, + series: [ + { + name: name, + type: "line", + data: stats.map(function (v) { + return v.countLogs / axis.divider; + }), + itemStyle: { + color: "#879BD7" + }, + areaStyle: {}, + stack: "总量" + }, + { + name: name, + type: "line", + data: stats.map(function (v) { + return v.countCaptcha / axis.divider; + }), + itemStyle: { + color: "#FBD88A" + }, + areaStyle: {}, + stack: "总量" + }, + { + name: name, + type: "line", + data: stats.map(function (v) { + return v.countBlocks / axis.divider; + }), + itemStyle: { + color: "#F39494" + }, + areaStyle: {}, + stack: "总量" + } + ], + animation: true + } + chart.setOption(option) + chart.resize() + } + + this.reloadGroupChart = function () { + let axis = teaweb.countAxis(this.groupStats, function (v) { + return v.count + }) + teaweb.renderBarChart({ + id: "group-chart", + values: this.groupStats, + x: function (v) { + return v.name + }, + value: function (v) { + return v.count / axis.divider + }, + tooltip: function (args, stats) { + let index = args.dataIndex + return stats[index].name + ": " + stats[index].count + }, + axis: axis + }) + } + + this.accessLogs = [] + this.reloadAccessLogs = function () { + this.$post(".wafLogs") + .success(function (resp) { + if (resp.data.accessLogs != null) { + this.accessLogs = resp.data.accessLogs + } + }) + .done(function () { + this.$delay(this.reloadAccessLogs, 10000) + }) + } +}) \ No newline at end of file diff --git a/web/views/@default/dashboard/boards/waf.less b/web/views/@default/dashboard/boards/waf.less index fcb67336..f5151713 100644 --- a/web/views/@default/dashboard/boards/waf.less +++ b/web/views/@default/dashboard/boards/waf.less @@ -43,4 +43,9 @@ .chart-box { height: 20em; +} + +.color-span { + font-size: 0.8em; + padding: 4px; } \ No newline at end of file diff --git a/web/views/@default/servers/server/stat/waf.js b/web/views/@default/servers/server/stat/waf.js index 63e374e1..0bcf3508 100644 --- a/web/views/@default/servers/server/stat/waf.js +++ b/web/views/@default/servers/server/stat/waf.js @@ -1,198 +1,182 @@ Tea.context(function () { - this.$delay(function () { - let that = this + this.$delay(function () { + let that = this - this.totalDailyStats = this.logDailyStats.map(function (v, k) { - return { - day: v.day, - count: that.logDailyStats[k].count + that.blockDailyStats[k].count + that.captchaDailyStats[k].count - } - }) - let dailyUnit = this.processMaxUnit(this.totalDailyStats) - this.reloadLineChart("daily-chart", "规则分组", this.totalDailyStats, function (v) { - return v.day.substring(4, 6) + "-" + v.day.substring(6) - }, function (args) { + this.countDailyBlock = teaweb.formatCount(this.countDailyBlock) + this.countDailyCaptcha = teaweb.formatCount(this.countDailyCaptcha) + this.countDailyLog = teaweb.formatCount(this.countDailyLog) + this.countWeeklyBlock = teaweb.formatCount(this.countWeeklyBlock) + this.countMonthlyBlock = teaweb.formatCount(this.countMonthlyBlock) - return that.logDailyStats[args.dataIndex].day.substring(4, 6) + "-" + that.logDailyStats[args.dataIndex].day.substring(6) + ": 拦截: " - + teaweb.formatNumber(that.blockDailyStats[args.dataIndex].count) + ", 验证码: " + teaweb.formatNumber(that.captchaDailyStats[args.dataIndex].count) + ", 记录: " + teaweb.formatNumber(that.logDailyStats[args.dataIndex].count) - }, dailyUnit) + this.totalDailyStats = this.logDailyStats.map(function (v, k) { + return { + day: v.day, + count: that.logDailyStats[k].count + that.blockDailyStats[k].count + that.captchaDailyStats[k].count + } + }) + let dailyAxis = teaweb.countAxis(this.totalDailyStats, function (v) { + return v.count + }) + this.reloadLineChart("daily-chart", "规则分组", this.totalDailyStats, function (v) { + return v.day.substring(4, 6) + "-" + v.day.substring(6) + }, function (args) { + return that.logDailyStats[args.dataIndex].day.substring(4, 6) + "-" + that.logDailyStats[args.dataIndex].day.substring(6) + ": 拦截: " + + teaweb.formatNumber(that.blockDailyStats[args.dataIndex].count) + ", 验证码: " + teaweb.formatNumber(that.captchaDailyStats[args.dataIndex].count) + ", 记录: " + teaweb.formatNumber(that.logDailyStats[args.dataIndex].count) + }, dailyAxis) - let groupUnit = this.processMaxUnit(this.groupStats) - let total = this.groupStats.$sum(function (k, v) { - return v.rawCount - }) - this.reloadBarChart("group-chart", "规则分组", this.groupStats, function (v) { - return v.group.name - }, function (args) { - let percent = "" - if (total > 0) { - percent = ", 占比: " + (Math.round(that.groupStats[args.dataIndex].rawCount * 100 * 100 / total) / 100) + "%" - } - return that.groupStats[args.dataIndex].group.name + ": " + teaweb.formatNumber(that.groupStats[args.dataIndex].rawCount) + percent - }, groupUnit) + let groupAxis = teaweb.countAxis(this.groupStats, function (v) { + return v.count + }) + let total = this.groupStats.$sum(function (k, v) { + return v.count + }) + this.reloadBarChart("group-chart", "规则分组", this.groupStats, function (v) { + return v.group.name + }, function (args) { + let percent = "" + if (total > 0) { + percent = ", 占比: " + (Math.round(that.groupStats[args.dataIndex].count * 100 * 100 / total) / 100) + "%" + } + return that.groupStats[args.dataIndex].group.name + ": " + teaweb.formatNumber(that.groupStats[args.dataIndex].count) + percent + }, groupAxis) - window.addEventListener("resize", function () { - that.resizeChart("daily-chart") - that.resizeChart("group-chart") - }) - }) + window.addEventListener("resize", function () { + that.resizeChart("daily-chart") + that.resizeChart("group-chart") + }) + }) - this.reloadLineChart = function (chartId, name, stats, xFunc, tooltipFunc, unit) { - let chartBox = document.getElementById(chartId) - if (chartBox == null) { - return - } - let that = this - let chart = echarts.init(chartBox) - let option = { - xAxis: { - data: stats.map(xFunc) - }, - yAxis: { - axisLabel: { - formatter: function (value) { - return value + unit - } - } - }, - tooltip: { - show: true, - trigger: "item", - formatter: tooltipFunc - }, - grid: { - left: 40, - top: 10, - right: 20, - bottom: 20 - }, - series: [ - { - name: name, - type: "line", - data: this.totalDailyStats.map(function (v, index) { - return that.totalDailyStats[index].count; - }), - areaStyle: {}, - itemStyle: { - color: "#9DD3E8" - } - }, - { - name: name, - type: "line", - data: this.logDailyStats.map(function (v) { - return v.count; - }), - itemStyle: { - color: "#879BD7" - } - }, - { - name: name, - type: "line", - data: this.blockDailyStats.map(function (v) { - return v.count; - }), - itemStyle: { - color: "#F39494" - } - }, - { - name: name, - type: "line", - data: this.captchaDailyStats.map(function (v) { - return v.count; - }), - itemStyle: { - color: "#FBD88A" - } - } - ], - animation: true - } - chart.setOption(option) - chart.resize() - } + this.reloadLineChart = function (chartId, name, stats, xFunc, tooltipFunc, axis) { + let chartBox = document.getElementById(chartId) + if (chartBox == null) { + return + } + let that = this + let chart = echarts.init(chartBox) + let option = { + xAxis: { + data: stats.map(xFunc) + }, + yAxis: { + axisLabel: { + formatter: function (value) { + return value + axis.unit + } + } + }, + tooltip: { + show: true, + trigger: "item", + formatter: tooltipFunc + }, + grid: { + left: 40, + top: 10, + right: 20, + bottom: 20 + }, + series: [ + { + name: name, + type: "line", + data: this.totalDailyStats.map(function (v, index) { + return that.totalDailyStats[index].count / axis.divider; + }), + areaStyle: {}, + itemStyle: { + color: "#9DD3E8" + } + }, + { + name: name, + type: "line", + data: this.logDailyStats.map(function (v) { + return v.count / axis.divider; + }), + itemStyle: { + color: "#879BD7" + } + }, + { + name: name, + type: "line", + data: this.blockDailyStats.map(function (v) { + return v.count / axis.divider; + }), + itemStyle: { + color: "#F39494" + } + }, + { + name: name, + type: "line", + data: this.captchaDailyStats.map(function (v) { + return v.count / axis.divider; + }), + itemStyle: { + color: "#FBD88A" + } + } + ], + animation: true + } + chart.setOption(option) + chart.resize() + } - this.reloadBarChart = function (chartId, name, stats, xFunc, tooltipFunc, unit) { - let chartBox = document.getElementById(chartId) - if (chartBox == null) { - return - } - let chart = echarts.init(chartBox) - let option = { - xAxis: { - data: stats.map(xFunc) - }, - yAxis: { - axisLabel: { - formatter: function (value) { - return value + unit - } - } - }, - tooltip: { - show: true, - trigger: "item", - formatter: tooltipFunc - }, - grid: { - left: 40, - top: 10, - right: 20, - bottom: 20 - }, - series: [ - { - name: name, - type: "bar", - data: stats.map(function (v) { - return v.count; - }), - itemStyle: { - color: "#9DD3E8" - }, - barWidth: "20em" - } - ], - animation: true - } - chart.setOption(option) - chart.resize() - } + this.reloadBarChart = function (chartId, name, stats, xFunc, tooltipFunc, axis) { + let chartBox = document.getElementById(chartId) + if (chartBox == null) { + return + } + let chart = echarts.init(chartBox) + let option = { + xAxis: { + data: stats.map(xFunc) + }, + yAxis: { + axisLabel: { + formatter: function (value) { + return value + axis.unit + } + } + }, + tooltip: { + show: true, + trigger: "item", + formatter: tooltipFunc + }, + grid: { + left: 40, + top: 10, + right: 20, + bottom: 20 + }, + series: [ + { + name: name, + type: "bar", + data: stats.map(function (v) { + return v.count / axis.divider; + }), + itemStyle: { + color: "#9DD3E8" + }, + barWidth: "20em" + } + ], + animation: true + } + chart.setOption(option) + chart.resize() + } - this.resizeChart = function (chartId) { - let chartBox = document.getElementById(chartId) - if (chartBox == null) { - return - } - let chart = echarts.init(chartBox) - chart.resize() - } - - this.processMaxUnit = function (stats) { - let max = stats.$map(function (k, v) { - return v.count - }).$max() - let divider = 0 - let unit = "" - if (max >= 1000 * 1000 * 1000) { - unit = "B" - divider = 1000 * 1000 * 1000 - } else if (max >= 1000 * 1000) { - unit = "M" - divider = 1000 * 1000 - } else if (max >= 1000) { - unit = "K" - divider = 1000 - } - stats.forEach(function (v) { - v.rawCount = v.count - if (divider > 0) { - v.count /= divider - } - }) - return unit - } + this.resizeChart = function (chartId) { + let chartBox = document.getElementById(chartId) + if (chartBox == null) { + return + } + let chart = echarts.init(chartBox) + chart.resize() + } }) \ No newline at end of file