diff --git a/internal/web/actions/default/servers/components/waf/policy.go b/internal/web/actions/default/servers/components/waf/policy.go index ef4fecc0..937dfc9f 100644 --- a/internal/web/actions/default/servers/components/waf/policy.go +++ b/internal/web/actions/default/servers/components/waf/policy.go @@ -94,6 +94,7 @@ func (this *PolicyAction) RunGet(params struct { "modeInfo": firewallconfigs.FindFirewallMode(firewallPolicy.Mode), "groups": internalGroups, "blockOptions": firewallPolicy.BlockOptions, + "captchaOptions": firewallPolicy.CaptchaOptions, "useLocalFirewall": firewallPolicy.UseLocalFirewall, "synFlood": firewallPolicy.SYNFlood, "log": firewallPolicy.Log, diff --git a/internal/web/actions/default/servers/components/waf/update.go b/internal/web/actions/default/servers/components/waf/update.go index 435b334c..aec363a3 100644 --- a/internal/web/actions/default/servers/components/waf/update.go +++ b/internal/web/actions/default/servers/components/waf/update.go @@ -70,6 +70,7 @@ func (this *UpdateAction) RunGet(params struct { "isOn": firewallPolicy.IsOn, "mode": firewallPolicy.Mode, "blockOptions": firewallPolicy.BlockOptions, + "captchaOptions": firewallPolicy.CaptchaOptions, "useLocalFirewall": firewallPolicy.UseLocalFirewall, "synFloodConfig": firewallPolicy.SYNFlood, "log": firewallPolicy.Log, @@ -98,16 +99,17 @@ func (this *UpdateAction) RunGet(params struct { } func (this *UpdateAction) RunPost(params struct { - FirewallPolicyId int64 - Name string - GroupCodes []string - BlockOptionsJSON []byte - Description string - IsOn bool - Mode string - UseLocalFirewall bool - SynFloodJSON []byte - LogJSON []byte + FirewallPolicyId int64 + Name string + GroupCodes []string + BlockOptionsJSON []byte + CaptchaOptionsJSON []byte + Description string + IsOn bool + Mode string + UseLocalFirewall bool + SynFloodJSON []byte + LogJSON []byte Must *actions.Must }) { @@ -118,13 +120,20 @@ func (this *UpdateAction) RunPost(params struct { Field("name", params.Name). Require("请输入策略名称") - // 校验JSON + // 校验拦截选项JSON var blockOptions = &firewallconfigs.HTTPFirewallBlockAction{} err := json.Unmarshal(params.BlockOptionsJSON, blockOptions) if err != nil { this.Fail("拦截动作参数校验失败:" + err.Error()) } + // 校验验证码选项JSON + var captchaOptions = &firewallconfigs.HTTPFirewallCaptchaAction{} + err = json.Unmarshal(params.CaptchaOptionsJSON, captchaOptions) + if err != nil { + this.Fail("验证码动作参数校验失败:" + err.Error()) + } + _, err = this.RPC().HTTPFirewallPolicyRPC().UpdateHTTPFirewallPolicy(this.AdminContext(), &pb.UpdateHTTPFirewallPolicyRequest{ HttpFirewallPolicyId: params.FirewallPolicyId, IsOn: params.IsOn, @@ -132,6 +141,7 @@ func (this *UpdateAction) RunPost(params struct { Description: params.Description, FirewallGroupCodes: params.GroupCodes, BlockOptionsJSON: params.BlockOptionsJSON, + CaptchaOptionsJSON: params.CaptchaOptionsJSON, Mode: params.Mode, UseLocalFirewall: params.UseLocalFirewall, SynFloodJSON: params.SynFloodJSON, diff --git a/web/public/js/components.js b/web/public/js/components.js index 73ade29a..6ec94f99 100644 --- a/web/public/js/components.js +++ b/web/public/js/components.js @@ -1863,7 +1863,9 @@ Vue.component("traffic-map-box",{props:["v-stats","v-is-attack"],mounted:functio :有效期{{config.options.timeout}}秒 - :有效期{{config.options.life}}秒 + :有效期{{config.options.life}}秒 + / 最多失败{{config.options.maxFails}}次 + :有效期{{config.options.life}}秒 @@ -2599,7 +2601,10 @@ Vue.component("traffic-map-box",{props:["v-stats","v-is-attack"],mounted:functio - 耗时:{{formatCost(accessLog.requestTime)}} ms   ({{accessLog.humanTime}})   -`}),Vue.component("http-access-log-config-box",{props:["v-access-log-config","v-fields","v-default-field-codes","v-is-location","v-is-group"],data:function(){let t=this,i=(setTimeout(function(){t.changeFields()},100),{isPrior:!1,isOn:!1,fields:[1,2,6,7],status1:!0,status2:!0,status3:!0,status4:!0,status5:!0,firewallOnly:!1,enableClientClosed:!1});return null!=this.vAccessLogConfig&&(i=this.vAccessLogConfig),this.vFields.forEach(function(e){null==t.vAccessLogConfig?e.isChecked=t.vDefaultFieldCodes.$contains(e.code):e.isChecked=i.fields.$contains(e.code)}),{accessLog:i,hasRequestBodyField:this.vFields.$contains(8)}},methods:{changeFields:function(){this.accessLog.fields=this.vFields.filter(function(e){return e.isChecked}).map(function(e){return e.code}),this.hasRequestBodyField=this.accessLog.fields.$contains(8)}},template:`
+
`}),Vue.component("http-firewall-block-options-viewer",{props:["v-block-options"],data:function(){return{blockOptions:this.vBlockOptions,statusCode:this.vBlockOptions.statusCode,timeout:this.vBlockOptions.timeout}},watch:{statusCode:function(e){e=parseInt(e);isNaN(e)?this.blockOptions.statusCode=403:this.blockOptions.statusCode=e},timeout:function(e){e=parseInt(e);isNaN(e)?this.blockOptions.timeout=0:this.blockOptions.timeout=e}},methods:{edit:function(){this.isEditing=!this.isEditing}},template:`
+ 状态码:{{statusCode}} / 提示内容:[{{blockOptions.body.length}}字符][无] / 超时时间:{{timeout}}秒 +
+`}),Vue.component("http-access-log-config-box",{props:["v-access-log-config","v-fields","v-default-field-codes","v-is-location","v-is-group"],data:function(){let t=this,i=(setTimeout(function(){t.changeFields()},100),{isPrior:!1,isOn:!1,fields:[1,2,6,7],status1:!0,status2:!0,status3:!0,status4:!0,status5:!0,firewallOnly:!1,enableClientClosed:!1});return null!=this.vAccessLogConfig&&(i=this.vAccessLogConfig),this.vFields.forEach(function(e){null==t.vAccessLogConfig?e.isChecked=t.vDefaultFieldCodes.$contains(e.code):e.isChecked=i.fields.$contains(e.code)}),{accessLog:i,hasRequestBodyField:this.vFields.$contains(8)}},methods:{changeFields:function(){this.accessLog.fields=this.vFields.filter(function(e){return e.isChecked}).map(function(e){return e.code}),this.hasRequestBodyField=this.accessLog.fields.$contains(8)}},template:`
@@ -2851,7 +2856,8 @@ Vue.component("traffic-map-box",{props:["v-stats","v-is-attack"],mounted:functio {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   -`}),Vue.component("reverse-proxy-box",{props:["v-reverse-proxy-ref","v-reverse-proxy-config","v-is-location","v-is-group","v-family"],data:function(){let e=this.vReverseProxyRef,t=(null==e&&(e={isPrior:!1,isOn:!1,reverseProxyId:0}),this.vReverseProxyConfig),i=(null==(t=null==t?{requestPath:"",stripPrefix:"",requestURI:"",requestHost:"",requestHostType:0,addHeaders:[],connTimeout:{count:0,unit:"second"},readTimeout:{count:0,unit:"second"},idleTimeout:{count:0,unit:"second"},maxConns:0,maxIdleConns:0,followRedirects:!1}:t).addHeaders&&(t.addHeaders=[]),null==t.connTimeout&&(t.connTimeout={count:0,unit:"second"}),null==t.readTimeout&&(t.readTimeout={count:0,unit:"second"}),null==t.idleTimeout&&(t.idleTimeout={count:0,unit:"second"}),null==t.proxyProtocol&&Vue.set(t,"proxyProtocol",{isOn:!1,version:1}),[{name:"X-Real-IP",isChecked:!1},{name:"X-Forwarded-For",isChecked:!1},{name:"X-Forwarded-By",isChecked:!1},{name:"X-Forwarded-Host",isChecked:!1},{name:"X-Forwarded-Proto",isChecked:!1}]);return i.forEach(function(e){e.isChecked=t.addHeaders.$contains(e.name)}),{reverseProxyRef:e,reverseProxyConfig:t,advancedVisible:!1,family:this.vFamily,forwardHeaders:i}},watch:{"reverseProxyConfig.requestHostType":function(e){let t=parseInt(e);isNaN(t)&&(t=0),this.reverseProxyConfig.requestHostType=t},"reverseProxyConfig.connTimeout.count":function(e){let t=parseInt(e);(isNaN(t)||t<0)&&(t=0),this.reverseProxyConfig.connTimeout.count=t},"reverseProxyConfig.readTimeout.count":function(e){let t=parseInt(e);(isNaN(t)||t<0)&&(t=0),this.reverseProxyConfig.readTimeout.count=t},"reverseProxyConfig.idleTimeout.count":function(e){let t=parseInt(e);(isNaN(t)||t<0)&&(t=0),this.reverseProxyConfig.idleTimeout.count=t},"reverseProxyConfig.maxConns":function(e){let t=parseInt(e);(isNaN(t)||t<0)&&(t=0),this.reverseProxyConfig.maxConns=t},"reverseProxyConfig.maxIdleConns":function(e){let t=parseInt(e);(isNaN(t)||t<0)&&(t=0),this.reverseProxyConfig.maxIdleConns=t},"reverseProxyConfig.proxyProtocol.version":function(e){let t=parseInt(e);isNaN(t)&&(t=1),this.reverseProxyConfig.proxyProtocol.version=t}},methods:{isOn:function(){return(!this.vIsLocation&&!this.vIsGroup||this.reverseProxyRef.isPrior)&&this.reverseProxyRef.isOn},changeAdvancedVisible:function(e){this.advancedVisible=e},changeAddHeader:function(){this.reverseProxyConfig.addHeaders=this.forwardHeaders.filter(function(e){return e.isChecked}).map(function(e){return e.name})}},template:`
+
`}),Vue.component("http-firewall-captcha-options-viewer",{props:["v-captcha-options"],mounted:function(){this.updateSummary()},data:function(){let e=this.vCaptchaOptions;return{options:e=null==e?{life:0,maxFails:0,failBlockTimeout:0,failBlockScopeAll:!1,uiIsOn:!1,uiTitle:"",uiPrompt:"",uiButtonTitle:"",uiShowRequestId:!1,uiCss:"",uiFooter:"",uiBody:"",cookieId:"",lang:""}:e,summary:""}},methods:{updateSummary:function(){let e=[];0{{summary}} +`}),Vue.component("reverse-proxy-box",{props:["v-reverse-proxy-ref","v-reverse-proxy-config","v-is-location","v-is-group","v-family"],data:function(){let e=this.vReverseProxyRef,t=(null==e&&(e={isPrior:!1,isOn:!1,reverseProxyId:0}),this.vReverseProxyConfig),i=(null==(t=null==t?{requestPath:"",stripPrefix:"",requestURI:"",requestHost:"",requestHostType:0,addHeaders:[],connTimeout:{count:0,unit:"second"},readTimeout:{count:0,unit:"second"},idleTimeout:{count:0,unit:"second"},maxConns:0,maxIdleConns:0,followRedirects:!1}:t).addHeaders&&(t.addHeaders=[]),null==t.connTimeout&&(t.connTimeout={count:0,unit:"second"}),null==t.readTimeout&&(t.readTimeout={count:0,unit:"second"}),null==t.idleTimeout&&(t.idleTimeout={count:0,unit:"second"}),null==t.proxyProtocol&&Vue.set(t,"proxyProtocol",{isOn:!1,version:1}),[{name:"X-Real-IP",isChecked:!1},{name:"X-Forwarded-For",isChecked:!1},{name:"X-Forwarded-By",isChecked:!1},{name:"X-Forwarded-Host",isChecked:!1},{name:"X-Forwarded-Proto",isChecked:!1}]);return i.forEach(function(e){e.isChecked=t.addHeaders.$contains(e.name)}),{reverseProxyRef:e,reverseProxyConfig:t,advancedVisible:!1,family:this.vFamily,forwardHeaders:i}},watch:{"reverseProxyConfig.requestHostType":function(e){let t=parseInt(e);isNaN(t)&&(t=0),this.reverseProxyConfig.requestHostType=t},"reverseProxyConfig.connTimeout.count":function(e){let t=parseInt(e);(isNaN(t)||t<0)&&(t=0),this.reverseProxyConfig.connTimeout.count=t},"reverseProxyConfig.readTimeout.count":function(e){let t=parseInt(e);(isNaN(t)||t<0)&&(t=0),this.reverseProxyConfig.readTimeout.count=t},"reverseProxyConfig.idleTimeout.count":function(e){let t=parseInt(e);(isNaN(t)||t<0)&&(t=0),this.reverseProxyConfig.idleTimeout.count=t},"reverseProxyConfig.maxConns":function(e){let t=parseInt(e);(isNaN(t)||t<0)&&(t=0),this.reverseProxyConfig.maxConns=t},"reverseProxyConfig.maxIdleConns":function(e){let t=parseInt(e);(isNaN(t)||t<0)&&(t=0),this.reverseProxyConfig.maxIdleConns=t},"reverseProxyConfig.proxyProtocol.version":function(e){let t=parseInt(e);isNaN(t)&&(t=1),this.reverseProxyConfig.proxyProtocol.version=t}},methods:{isOn:function(){return(!this.vIsLocation&&!this.vIsGroup||this.reverseProxyRef.isPrior)&&this.reverseProxyRef.isOn},changeAdvancedVisible:function(e){this.advancedVisible=e},changeAddHeader:function(){this.reverseProxyConfig.addHeaders=this.forwardHeaders.filter(function(e){return e.isChecked}).map(function(e){return e.name})}},template:`
@@ -3734,7 +3740,115 @@ Vue.component("traffic-map-box",{props:["v-stats","v-is-attack"],mounted:functio
-
`}),Vue.component("firewall-syn-flood-config-box",{props:["v-syn-flood-config"],data:function(){let e=this.vSynFloodConfig;return{config:e=null==e?{isOn:!1,minAttempts:10,timeoutSeconds:600,ignoreLocal:!0}:e,isEditing:!1,minAttempts:e.minAttempts,timeoutSeconds:e.timeoutSeconds}},methods:{edit:function(){this.isEditing=!this.isEditing}},watch:{minAttempts:function(e){let t=parseInt(e);(t=isNaN(t)?10:t)<5&&(t=5),this.config.minAttempts=t},timeoutSeconds:function(e){let t=parseInt(e);(t=isNaN(t)?10:t)<60&&(t=60),this.config.timeoutSeconds=t}},template:`
+
`}),Vue.component("http-firewall-captcha-options",{props:["v-captcha-options"],mounted:function(){this.updateSummary()},data:function(){let e=this.vCaptchaOptions;return(e=null==e?{countLetters:0,life:0,maxFails:0,failBlockTimeout:0,failBlockScopeAll:!1,uiIsOn:!1,uiTitle:"",uiPrompt:"",uiButtonTitle:"",uiShowRequestId:!1,uiCss:"",uiFooter:"",uiBody:"",cookieId:"",lang:""}:e).countLetters<=0&&(e.countLetters=6),{options:e,isEditing:!1,summary:""}},watch:{"options.countLetters":function(e){let t=parseInt(e,10);isNaN(t)||t<0?t=0:10 + + {{summary}} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
有效时间 +
+ + +
+

验证通过后在这个时间内不再验证,默认600秒。

+
最多失败次数 +
+ + +
+

如果为空或者为0,表示不限制。

+
失败拦截时间 +
+ + +
+

在达到最多失败次数(大于0)时,自动拦截的时间;如果为0表示不自动拦截。

+
失败全局封禁 + +

是否在失败时全局封禁,默认为只封禁对单个网站服务的访问。

+
验证码中数字个数 + +
定制UI
页面标题 + +
按钮标题 + +

类似于提交验证

+
显示请求ID + +

在界面上显示请求ID,方便用户报告问题。

+
CSS样式 + +
页头提示 + +

类似于请输入上面的验证码,支持HTML。

+
页尾提示 + +

支持HTML。

+
页面模板 + +

模板中必须包含\${body}表示验证码表单!整个页面的模板,支持HTML,其中必须使用\${body}变量代表验证码表单,否则将无法正常显示验证码。

+
+
+ +`}),Vue.component("firewall-syn-flood-config-box",{props:["v-syn-flood-config"],data:function(){let e=this.vSynFloodConfig;return{config:e=null==e?{isOn:!1,minAttempts:10,timeoutSeconds:600,ignoreLocal:!0}:e,isEditing:!1,minAttempts:e.minAttempts,timeoutSeconds:e.timeoutSeconds}},methods:{edit:function(){this.isEditing=!this.isEditing}},watch:{minAttempts:function(e){let t=parseInt(e);(t=isNaN(t)?10:t)<5&&(t=5),this.config.minAttempts=t},timeoutSeconds:function(e){let t=parseInt(e);(t=isNaN(t)?10:t)<60&&(t=60),this.config.timeoutSeconds=t}},template:`
diff --git a/web/public/js/components.src.js b/web/public/js/components.src.js index 208f8057..5d718e2b 100755 --- a/web/public/js/components.src.js +++ b/web/public/js/components.src.js @@ -6221,7 +6221,9 @@ Vue.component("http-firewall-actions-box", { :有效期{{config.options.timeout}}秒 - :有效期{{config.options.life}}秒 + :有效期{{config.options.life}}秒 + / 最多失败{{config.options.maxFails}}次 + :有效期{{config.options.life}}秒 @@ -7651,6 +7653,44 @@ Vue.component("http-access-log-box", {
` }) +Vue.component("http-firewall-block-options-viewer", { + props: ["v-block-options"], + data: function () { + return { + blockOptions: this.vBlockOptions, + statusCode: this.vBlockOptions.statusCode, + timeout: this.vBlockOptions.timeout + } + }, + watch: { + statusCode: function (v) { + let statusCode = parseInt(v) + if (isNaN(statusCode)) { + this.blockOptions.statusCode = 403 + } else { + this.blockOptions.statusCode = statusCode + } + }, + timeout: function (v) { + let timeout = parseInt(v) + if (isNaN(timeout)) { + this.blockOptions.timeout = 0 + } else { + this.blockOptions.timeout = timeout + } + } + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + } + }, + template: `
+ 状态码:{{statusCode}} / 提示内容:[{{blockOptions.body.length}}字符][无] / 超时时间:{{timeout}}秒 +
+` +}) + Vue.component("http-access-log-config-box", { props: ["v-access-log-config", "v-fields", "v-default-field-codes", "v-is-location", "v-is-group"], data: function () { @@ -8170,6 +8210,65 @@ Vue.component("ssl-certs-view", { ` }) +Vue.component("http-firewall-captcha-options-viewer", { + props: ["v-captcha-options"], + mounted: function () { + this.updateSummary() + }, + data: function () { + let options = this.vCaptchaOptions + if (options == null) { + options = { + life: 0, + maxFails: 0, + failBlockTimeout: 0, + failBlockScopeAll: false, + uiIsOn: false, + uiTitle: "", + uiPrompt: "", + uiButtonTitle: "", + uiShowRequestId: false, + uiCss: "", + uiFooter: "", + uiBody: "", + cookieId: "", + lang: "" + } + } + return { + options: options, + summary: "" + } + }, + methods: { + updateSummary: function () { + let summaryList = [] + if (this.options.life > 0) { + summaryList.push("有效时间" + this.options.life + "秒") + } + if (this.options.maxFails > 0) { + summaryList.push("最多失败" + this.options.maxFails + "次") + } + if (this.options.failBlockTimeout > 0) { + summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") + } + if (this.options.failBlockScopeAll) { + summaryList.push("全局封禁") + } + if (this.options.uiIsOn) { + summaryList.push("定制UI") + } + if (summaryList.length == 0) { + this.summary = "默认配置" + } else { + this.summary = summaryList.join(" / ") + } + } + }, + template: `
{{summary}}
+` +}) + Vue.component("reverse-proxy-box", { props: ["v-reverse-proxy-ref", "v-reverse-proxy-config", "v-is-location", "v-is-group", "v-family"], data: function () { @@ -10684,6 +10783,226 @@ Vue.component("traffic-limit-config-box", { ` }) +Vue.component("http-firewall-captcha-options", { + props: ["v-captcha-options"], + mounted: function () { + this.updateSummary() + }, + data: function () { + let options = this.vCaptchaOptions + if (options == null) { + options = { + countLetters: 0, + life: 0, + maxFails: 0, + failBlockTimeout: 0, + failBlockScopeAll: false, + uiIsOn: false, + uiTitle: "", + uiPrompt: "", + uiButtonTitle: "", + uiShowRequestId: false, + uiCss: "", + uiFooter: "", + uiBody: "", + cookieId: "", + lang: "" + } + } + if (options.countLetters <= 0) { + options.countLetters = 6 + } + return { + options: options, + isEditing: false, + summary: "" + } + }, + watch: { + "options.countLetters": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } else if (i < 0) { + i = 0 + } else if (i > 10) { + i = 10 + } + this.options.countLetters = i + }, + "options.life": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.life = i + this.updateSummary() + }, + "options.maxFails": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.maxFails = i + this.updateSummary() + }, + "options.failBlockTimeout": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.failBlockTimeout = i + this.updateSummary() + }, + "options.failBlockScopeAll": function (v) { + this.updateSummary() + }, + "options.uiIsOn": function (v) { + this.updateSummary() + } + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + }, + updateSummary: function () { + let summaryList = [] + if (this.options.life > 0) { + summaryList.push("有效时间" + this.options.life + "秒") + } + if (this.options.maxFails > 0) { + summaryList.push("最多失败" + this.options.maxFails + "次") + } + if (this.options.failBlockTimeout > 0) { + summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") + } + if (this.options.failBlockScopeAll) { + summaryList.push("全局封禁") + } + if (this.options.uiIsOn) { + summaryList.push("定制UI") + } + if (summaryList.length == 0) { + this.summary = "默认配置" + } else { + this.summary = summaryList.join(" / ") + } + }, + confirm: function () { + this.isEditing = false + } + }, + template: `
+ + {{summary}} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
有效时间 +
+ + +
+

验证通过后在这个时间内不再验证,默认600秒。

+
最多失败次数 +
+ + +
+

如果为空或者为0,表示不限制。

+
失败拦截时间 +
+ + +
+

在达到最多失败次数(大于0)时,自动拦截的时间;如果为0表示不自动拦截。

+
失败全局封禁 + +

是否在失败时全局封禁,默认为只封禁对单个网站服务的访问。

+
验证码中数字个数 + +
定制UI
页面标题 + +
按钮标题 + +

类似于提交验证

+
显示请求ID + +

在界面上显示请求ID,方便用户报告问题。

+
CSS样式 + +
页头提示 + +

类似于请输入上面的验证码,支持HTML。

+
页尾提示 + +

支持HTML。

+
页面模板 + +

模板中必须包含\${body}表示验证码表单!整个页面的模板,支持HTML,其中必须使用\${body}变量代表验证码表单,否则将无法正常显示验证码。

+
+
+
+` +}) + Vue.component("firewall-syn-flood-config-box", { props: ["v-syn-flood-config"], data: function () { diff --git a/web/public/js/components/server/http-firewall-actions-box.js b/web/public/js/components/server/http-firewall-actions-box.js index c4c4541a..879663cf 100644 --- a/web/public/js/components/server/http-firewall-actions-box.js +++ b/web/public/js/components/server/http-firewall-actions-box.js @@ -535,7 +535,9 @@ Vue.component("http-firewall-actions-box", { :有效期{{config.options.timeout}}秒 - :有效期{{config.options.life}}秒 + :有效期{{config.options.life}}秒 + / 最多失败{{config.options.maxFails}}次 + :有效期{{config.options.life}}秒 diff --git a/web/public/js/components/server/http-firewall-block-options-viewer.js b/web/public/js/components/server/http-firewall-block-options-viewer.js new file mode 100644 index 00000000..6ce56cd8 --- /dev/null +++ b/web/public/js/components/server/http-firewall-block-options-viewer.js @@ -0,0 +1,37 @@ +Vue.component("http-firewall-block-options-viewer", { + props: ["v-block-options"], + data: function () { + return { + blockOptions: this.vBlockOptions, + statusCode: this.vBlockOptions.statusCode, + timeout: this.vBlockOptions.timeout + } + }, + watch: { + statusCode: function (v) { + let statusCode = parseInt(v) + if (isNaN(statusCode)) { + this.blockOptions.statusCode = 403 + } else { + this.blockOptions.statusCode = statusCode + } + }, + timeout: function (v) { + let timeout = parseInt(v) + if (isNaN(timeout)) { + this.blockOptions.timeout = 0 + } else { + this.blockOptions.timeout = timeout + } + } + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + } + }, + template: `
+ 状态码:{{statusCode}} / 提示内容:[{{blockOptions.body.length}}字符][无] / 超时时间:{{timeout}}秒 +
+` +}) \ No newline at end of file diff --git a/web/public/js/components/server/http-firewall-captcha-options-viewer.js b/web/public/js/components/server/http-firewall-captcha-options-viewer.js new file mode 100644 index 00000000..e6550f4a --- /dev/null +++ b/web/public/js/components/server/http-firewall-captcha-options-viewer.js @@ -0,0 +1,58 @@ +Vue.component("http-firewall-captcha-options-viewer", { + props: ["v-captcha-options"], + mounted: function () { + this.updateSummary() + }, + data: function () { + let options = this.vCaptchaOptions + if (options == null) { + options = { + life: 0, + maxFails: 0, + failBlockTimeout: 0, + failBlockScopeAll: false, + uiIsOn: false, + uiTitle: "", + uiPrompt: "", + uiButtonTitle: "", + uiShowRequestId: false, + uiCss: "", + uiFooter: "", + uiBody: "", + cookieId: "", + lang: "" + } + } + return { + options: options, + summary: "" + } + }, + methods: { + updateSummary: function () { + let summaryList = [] + if (this.options.life > 0) { + summaryList.push("有效时间" + this.options.life + "秒") + } + if (this.options.maxFails > 0) { + summaryList.push("最多失败" + this.options.maxFails + "次") + } + if (this.options.failBlockTimeout > 0) { + summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") + } + if (this.options.failBlockScopeAll) { + summaryList.push("全局封禁") + } + if (this.options.uiIsOn) { + summaryList.push("定制UI") + } + if (summaryList.length == 0) { + this.summary = "默认配置" + } else { + this.summary = summaryList.join(" / ") + } + } + }, + template: `
{{summary}}
+` +}) \ No newline at end of file diff --git a/web/public/js/components/server/http-firewall-captcha-options.js b/web/public/js/components/server/http-firewall-captcha-options.js new file mode 100644 index 00000000..703af040 --- /dev/null +++ b/web/public/js/components/server/http-firewall-captcha-options.js @@ -0,0 +1,219 @@ +Vue.component("http-firewall-captcha-options", { + props: ["v-captcha-options"], + mounted: function () { + this.updateSummary() + }, + data: function () { + let options = this.vCaptchaOptions + if (options == null) { + options = { + countLetters: 0, + life: 0, + maxFails: 0, + failBlockTimeout: 0, + failBlockScopeAll: false, + uiIsOn: false, + uiTitle: "", + uiPrompt: "", + uiButtonTitle: "", + uiShowRequestId: false, + uiCss: "", + uiFooter: "", + uiBody: "", + cookieId: "", + lang: "" + } + } + if (options.countLetters <= 0) { + options.countLetters = 6 + } + return { + options: options, + isEditing: false, + summary: "" + } + }, + watch: { + "options.countLetters": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } else if (i < 0) { + i = 0 + } else if (i > 10) { + i = 10 + } + this.options.countLetters = i + }, + "options.life": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.life = i + this.updateSummary() + }, + "options.maxFails": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.maxFails = i + this.updateSummary() + }, + "options.failBlockTimeout": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.failBlockTimeout = i + this.updateSummary() + }, + "options.failBlockScopeAll": function (v) { + this.updateSummary() + }, + "options.uiIsOn": function (v) { + this.updateSummary() + } + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + }, + updateSummary: function () { + let summaryList = [] + if (this.options.life > 0) { + summaryList.push("有效时间" + this.options.life + "秒") + } + if (this.options.maxFails > 0) { + summaryList.push("最多失败" + this.options.maxFails + "次") + } + if (this.options.failBlockTimeout > 0) { + summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") + } + if (this.options.failBlockScopeAll) { + summaryList.push("全局封禁") + } + if (this.options.uiIsOn) { + summaryList.push("定制UI") + } + if (summaryList.length == 0) { + this.summary = "默认配置" + } else { + this.summary = summaryList.join(" / ") + } + }, + confirm: function () { + this.isEditing = false + } + }, + template: `
+ + {{summary}} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
有效时间 +
+ + +
+

验证通过后在这个时间内不再验证,默认600秒。

+
最多失败次数 +
+ + +
+

如果为空或者为0,表示不限制。

+
失败拦截时间 +
+ + +
+

在达到最多失败次数(大于0)时,自动拦截的时间;如果为0表示不自动拦截。

+
失败全局封禁 + +

是否在失败时全局封禁,默认为只封禁对单个网站服务的访问。

+
验证码中数字个数 + +
定制UI
页面标题 + +
按钮标题 + +

类似于提交验证

+
显示请求ID + +

在界面上显示请求ID,方便用户报告问题。

+
CSS样式 + +
页头提示 + +

类似于请输入上面的验证码,支持HTML。

+
页尾提示 + +

支持HTML。

+
页面模板 + +

模板中必须包含\${body}表示验证码表单!整个页面的模板,支持HTML,其中必须使用\${body}变量代表验证码表单,否则将无法正常显示验证码。

+
+
+
+` +}) \ No newline at end of file diff --git a/web/views/@default/servers/components/waf/policy.html b/web/views/@default/servers/components/waf/policy.html index fcaf873f..a4bdf45a 100644 --- a/web/views/@default/servers/components/waf/policy.html +++ b/web/views/@default/servers/components/waf/policy.html @@ -29,26 +29,13 @@ 阻止动作设置 - 还没有设置。 -
- - - - - - - - - - - - - -
状态码{{firewallPolicy.blockOptions.statusCode}}
提示内容{{firewallPolicy.blockOptions.body}}
超时时间 - 使用默认时间。 - {{firewallPolicy.blockOptions.timeout}}秒 -
-
+ + + + + 验证码动作设置 + + diff --git a/web/views/@default/servers/components/waf/update.html b/web/views/@default/servers/components/waf/update.html index 994cba9a..e4f12582 100644 --- a/web/views/@default/servers/components/waf/update.html +++ b/web/views/@default/servers/components/waf/update.html @@ -37,6 +37,12 @@ + + 验证码动作配置 + + + + 使用系统防火墙