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 () { if (this.$el.offsetWidth < 300) { let that = this setTimeout(function () { that.render() }, 100) return } 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 + "
流量:" + stat.formattedBytes + "
流量占比:" + stat.percent + "%
请求数:" + stat.formattedCountRequests + "
攻击数:" + 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: `
` }) 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: `
国家/地区排行 
暂无数据
{{stat.name}}
{{stat.percent}}% {{stat.formattedCountAttackRequests}} ({{stat.formattedBytes}})
` }) Vue.component("ddos-protection-ports-config-box", { props: ["v-ports"], data: function () { let ports = this.vPorts if (ports == null) { ports = [] } return { ports: ports, isAdding: false, addingPort: { port: "", description: "" } } }, methods: { add: function () { this.isAdding = true let that = this setTimeout(function () { that.$refs.addingPortInput.focus() }) }, confirm: function () { let portString = this.addingPort.port if (portString.length == 0) { this.warn("请输入端口号") return } if (!/^\d+$/.test(portString)) { this.warn("请输入正确的端口号") return } let port = parseInt(portString, 10) if (port <= 0) { this.warn("请输入正确的端口号") return } if (port > 65535) { this.warn("请输入正确的端口号") return } let exists = false this.ports.forEach(function (v) { if (v.port == port) { exists = true } }) if (exists) { this.warn("端口号已经存在") return } this.ports.push({ port: port, description: this.addingPort.description }) this.notifyChange() this.cancel() }, cancel: function () { this.isAdding = false this.addingPort = { port: "", description: "" } }, remove: function (index) { this.ports.$remove(index) this.notifyChange() }, warn: function (message) { let that = this teaweb.warn(message, function () { that.$refs.addingPortInput.focus() }) }, notifyChange: function () { this.$emit("change", this.ports) } }, template: `
{{portConfig.port}} ({{portConfig.description}})
端口
备注
 取消
` }) // 显示节点的多个集群 Vue.component("node-clusters-labels", { props: ["v-primary-cluster", "v-secondary-clusters", "size"], data: function () { let cluster = this.vPrimaryCluster let secondaryClusters = this.vSecondaryClusters if (secondaryClusters == null) { secondaryClusters = [] } let labelSize = this.size if (labelSize == null) { labelSize = "small" } return { cluster: cluster, secondaryClusters: secondaryClusters, labelSize: labelSize } }, template: `
{{cluster.name}} {{cluster.name}} {{c.name}} {{c.name}}
` }) // 单个集群选择 Vue.component("cluster-selector", { props: ["v-cluster-id"], mounted: function () { let that = this Tea.action("/clusters/options") .post() .success(function (resp) { that.clusters = resp.data.clusters }) }, data: function () { let clusterId = this.vClusterId if (clusterId == null) { clusterId = 0 } return { clusters: [], clusterId: clusterId } }, template: `
` }) Vue.component("node-ddos-protection-config-box", { props: ["v-ddos-protection-config", "v-default-configs", "v-is-node", "v-cluster-is-on"], data: function () { let config = this.vDdosProtectionConfig if (config == null) { config = { tcp: { isPrior: false, isOn: false, maxConnections: 0, maxConnectionsPerIP: 0, newConnectionsRate: 0, newConnectionsRateBlockTimeout: 0, newConnectionsSecondlyRate: 0, newConnectionSecondlyRateBlockTimeout: 0, allowIPList: [], ports: [] } } } // initialize if (config.tcp == null) { config.tcp = { isPrior: false, isOn: false, maxConnections: 0, maxConnectionsPerIP: 0, newConnectionsRate: 0, newConnectionsRateBlockTimeout: 0, newConnectionsSecondlyRate: 0, newConnectionSecondlyRateBlockTimeout: 0, allowIPList: [], ports: [] } } return { config: config, defaultConfigs: this.vDefaultConfigs, isNode: this.vIsNode, isAddingPort: false } }, methods: { changeTCPPorts: function (ports) { this.config.tcp.ports = ports }, changeTCPAllowIPList: function (ipList) { this.config.tcp.allowIPList = ipList } }, template: `

功能说明:此功能为试验性质,目前仅能防御简单的DDoS攻击,试验期间建议仅在被攻击时启用,仅支持已安装nftables v0.9以上的Linux系统。

当前节点所在集群已设置DDoS防护。

TCP设置

启用DDoS防护
单节点TCP最大连接数

单个节点可以接受的TCP最大连接数。如果为0,则默认为{{defaultConfigs.tcpMaxConnections}}。

单IP TCP最大连接数

单个IP可以连接到节点的TCP最大连接数。如果为0,则默认为{{defaultConfigs.tcpMaxConnectionsPerIP}};最小值为{{defaultConfigs.tcpMinConnectionsPerIP}}。

单IP TCP新连接速率(分钟)
个新连接/每分钟
屏蔽

单个IP每分钟可以创建TCP新连接的数量。如果为0,则默认为{{defaultConfigs.tcpNewConnectionsMinutelyRate}};最小值为{{defaultConfigs.tcpNewConnectionsMinMinutelyRate}}。如果没有填写屏蔽时间,则只丢弃数据包。

单IP TCP新连接速率(秒钟)
个新连接/每秒钟
屏蔽

单个IP每秒钟可以创建TCP新连接的数量。如果为0,则默认为{{defaultConfigs.tcpNewConnectionsSecondlyRate}};最小值为{{defaultConfigs.tcpNewConnectionsMinSecondlyRate}}。如果没有填写屏蔽时间,则只丢弃数据包。

TCP端口列表

在这些端口上使用当前配置。默认为80和443两个端口。

IP白名单

在白名单中的IP不受当前设置的限制。

` }) Vue.component("ddos-protection-ip-list-config-box", { props: ["v-ip-list"], data: function () { let list = this.vIpList if (list == null) { list = [] } return { list: list, isAdding: false, addingIP: { ip: "", description: "" } } }, methods: { add: function () { this.isAdding = true let that = this setTimeout(function () { that.$refs.addingIPInput.focus() }) }, confirm: function () { let ip = this.addingIP.ip if (ip.length == 0) { this.warn("请输入IP") return } let exists = false this.list.forEach(function (v) { if (v.ip == ip) { exists = true } }) if (exists) { this.warn("IP '" + ip + "'已经存在") return } let that = this Tea.Vue.$post("/ui/validateIPs") .params({ ips: [ip] }) .success(function () { that.list.push({ ip: ip, description: that.addingIP.description }) that.notifyChange() that.cancel() }) .fail(function () { that.warn("请输入正确的IP") }) }, cancel: function () { this.isAdding = false this.addingIP = { ip: "", description: "" } }, remove: function (index) { this.list.$remove(index) this.notifyChange() }, warn: function (message) { let that = this teaweb.warn(message, function () { that.$refs.addingIPInput.focus() }) }, notifyChange: function () { this.$emit("change", this.list) } }, template: `
{{ipConfig.ip}} ({{ipConfig.description}})
IP
备注
 取消
` }) Vue.component("node-cluster-combo-box", { props: ["v-cluster-id"], data: function () { let that = this Tea.action("/clusters/options") .post() .success(function (resp) { that.clusters = resp.data.clusters }) return { clusters: [] } }, methods: { change: function (item) { if (item == null) { this.$emit("change", 0) } else { this.$emit("change", item.value) } } }, template: `
` }) // 一个节点的多个集群选择器 Vue.component("node-clusters-selector", { props: ["v-primary-cluster", "v-secondary-clusters"], data: function () { let primaryCluster = this.vPrimaryCluster let secondaryClusters = this.vSecondaryClusters if (secondaryClusters == null) { secondaryClusters = [] } return { primaryClusterId: (primaryCluster == null) ? 0 : primaryCluster.id, secondaryClusterIds: secondaryClusters.map(function (v) { return v.id }), primaryCluster: primaryCluster, secondaryClusters: secondaryClusters } }, methods: { addPrimary: function () { let that = this let selectedClusterIds = [this.primaryClusterId].concat(this.secondaryClusterIds) teaweb.popup("/clusters/selectPopup?selectedClusterIds=" + selectedClusterIds.join(",") + "&mode=single", { height: "30em", width: "50em", callback: function (resp) { if (resp.data.cluster != null) { that.primaryCluster = resp.data.cluster that.primaryClusterId = that.primaryCluster.id that.notifyChange() } } }) }, removePrimary: function () { this.primaryClusterId = 0 this.primaryCluster = null this.notifyChange() }, addSecondary: function () { let that = this let selectedClusterIds = [this.primaryClusterId].concat(this.secondaryClusterIds) teaweb.popup("/clusters/selectPopup?selectedClusterIds=" + selectedClusterIds.join(",") + "&mode=multiple", { height: "30em", width: "50em", callback: function (resp) { if (resp.data.cluster != null) { that.secondaryClusterIds.push(resp.data.cluster.id) that.secondaryClusters.push(resp.data.cluster) that.notifyChange() } } }) }, removeSecondary: function (index) { this.secondaryClusterIds.$remove(index) this.secondaryClusters.$remove(index) this.notifyChange() }, notifyChange: function () { this.$emit("change", { clusterId: this.primaryClusterId }) } }, template: `
主集群
{{primaryCluster.name}}  

多个集群配置有冲突时,优先使用主集群配置。

从集群
{{cluster.name}}  
` }) Vue.component("message-media-selector", { props: ["v-media-type"], mounted: function () { let that = this Tea.action("/admins/recipients/mediaOptions") .post() .success(function (resp) { that.medias = resp.data.medias // 初始化简介 if (that.mediaType.length > 0) { let media = that.medias.$find(function (_, media) { return media.type == that.mediaType }) if (media != null) { that.description = media.description } } }) }, data: function () { let mediaType = this.vMediaType if (mediaType == null) { mediaType = "" } return { medias: [], description: "", mediaType: mediaType } }, watch: { mediaType: function (v) { let media = this.medias.$find(function (_, media) { return media.type == v }) if (media == null) { this.description = "" } else { this.description = media.description } this.$emit("change", media) }, }, template: `

` }) // 消息接收人设置 Vue.component("message-receivers-box", { props: ["v-node-cluster-id"], mounted: function () { let that = this Tea.action("/clusters/cluster/settings/message/selectedReceivers") .params({ clusterId: this.clusterId }) .post() .success(function (resp) { that.receivers = resp.data.receivers }) }, data: function () { let clusterId = this.vNodeClusterId if (clusterId == null) { clusterId = 0 } return { clusterId: clusterId, receivers: [] } }, methods: { addReceiver: function () { let that = this let recipientIdStrings = [] let groupIdStrings = [] this.receivers.forEach(function (v) { if (v.type == "recipient") { recipientIdStrings.push(v.id.toString()) } else if (v.type == "group") { groupIdStrings.push(v.id.toString()) } }) teaweb.popup("/clusters/cluster/settings/message/selectReceiverPopup?recipientIds=" + recipientIdStrings.join(",") + "&groupIds=" + groupIdStrings.join(","), { callback: function (resp) { that.receivers.push(resp.data) } }) }, removeReceiver: function (index) { this.receivers.$remove(index) } }, template: `
分组:{{receiver.name}} ({{receiver.subName}})  
` }) Vue.component("message-recipient-group-selector", { props: ["v-groups"], data: function () { let groups = this.vGroups if (groups == null) { groups = [] } let groupIds = [] if (groups.length > 0) { groupIds = groups.map(function (v) { return v.id.toString() }).join(",") } return { groups: groups, groupIds: groupIds } }, methods: { addGroup: function () { let that = this teaweb.popup("/admins/recipients/groups/selectPopup?groupIds=" + this.groupIds, { callback: function (resp) { that.groups.push(resp.data.group) that.update() } }) }, removeGroup: function (index) { this.groups.$remove(index) this.update() }, update: function () { let groupIds = [] if (this.groups.length > 0) { this.groups.forEach(function (v) { groupIds.push(v.id) }) } this.groupIds = groupIds.join(",") } }, template: `
{{group.name}}  
` }) Vue.component("message-media-instance-selector", { props: ["v-instance-id"], mounted: function () { let that = this Tea.action("/admins/recipients/instances/options") .post() .success(function (resp) { that.instances = resp.data.instances // 初始化简介 if (that.instanceId > 0) { let instance = that.instances.$find(function (_, instance) { return instance.id == that.instanceId }) if (instance != null) { that.description = instance.description that.update(instance.id) } } }) }, data: function () { let instanceId = this.vInstanceId if (instanceId == null) { instanceId = 0 } return { instances: [], description: "", instanceId: instanceId } }, watch: { instanceId: function (v) { this.update(v) } }, methods: { update: function (v) { let instance = this.instances.$find(function (_, instance) { return instance.id == v }) if (instance == null) { this.description = "" } else { this.description = instance.description } this.$emit("change", instance) } }, template: `

` }) Vue.component("message-row", { props: ["v-message", "v-can-close"], data: function () { let paramsJSON = this.vMessage.params let params = null if (paramsJSON != null && paramsJSON.length > 0) { params = JSON.parse(paramsJSON) } return { message: this.vMessage, params: params, isClosing: false } }, methods: { viewCert: function (certId) { teaweb.popup("/servers/certs/certPopup?certId=" + certId, { height: "28em", width: "48em" }) }, readMessage: function (messageId) { let that = this Tea.action("/messages/readPage") .params({"messageIds": [messageId]}) .post() .success(function () { // 刷新父级页面Badge if (window.parent.Tea != null && window.parent.Tea.Vue != null) { window.parent.Tea.Vue.checkMessagesOnce() } // 刷新当前页面 if (that.vCanClose && typeof (NotifyPopup) != "undefined") { that.isClosing = true setTimeout(function () { NotifyPopup({}) }, 1000) } else { teaweb.reload() } }) } }, template: `
{{message.datetime}} | 集群:{{message.cluster.name}} DNS集群:{{message.cluster.name}} | 节点:{{message.node.name}} DNS节点:{{message.node.name}}
{{message.body}}
` }) Vue.component("ns-domain-group-selector", { props: ["v-domain-group-id"], data: function () { let groupId = this.vDomainGroupId if (groupId == null) { groupId = 0 } return { userId: 0, groupId: groupId } }, methods: { change: function (group) { if (group != null) { this.$emit("change", group.id) } else { this.$emit("change", 0) } }, reload: function (userId) { this.userId = userId this.$refs.comboBox.clear() this.$refs.comboBox.setDataURL("/ns/domains/groups/options?userId=" + userId) this.$refs.comboBox.reloadData() } }, template: `
` }) // 选择多个线路 Vue.component("ns-routes-selector", { props: ["v-routes", "name"], mounted: function () { let that = this Tea.action("/ns/routes/options") .post() .success(function (resp) { that.routes = resp.data.routes // provinces let provinces = {} if (resp.data.provinces != null && resp.data.provinces.length > 0) { for (const province of resp.data.provinces) { let countryCode = province.countryCode if (typeof provinces[countryCode] == "undefined") { provinces[countryCode] = [] } provinces[countryCode].push({ name: province.name, code: province.code }) } } that.provinces = provinces }) }, data: function () { let selectedRoutes = this.vRoutes if (selectedRoutes == null) { selectedRoutes = [] } let inputName = this.name if (typeof inputName != "string" || inputName.length == 0) { inputName = "routeCodes" } return { routeCode: "default", inputName: inputName, routes: [], provinces: {}, // country code => [ province1, province2, ... ] provinceRouteCode: "", isAdding: false, routeType: "default", selectedRoutes: selectedRoutes, } }, watch: { routeType: function (v) { this.routeCode = "" let that = this this.routes.forEach(function (route) { if (route.type == v && that.routeCode.length == 0) { that.routeCode = route.code } }) } }, methods: { add: function () { this.isAdding = true this.routeType = "default" this.routeCode = "default" this.provinceRouteCode = "" this.$emit("add") }, cancel: function () { this.isAdding = false this.$emit("cancel") }, confirm: function () { if (this.routeCode.length == 0) { return } let that = this // route let selectedRoute = null for (const route of this.routes) { if (route.code == this.routeCode) { selectedRoute = route break } } if (selectedRoute != null) { // province route if (this.provinceRouteCode.length > 0 && this.provinces[this.routeCode] != null) { for (const province of this.provinces[this.routeCode]) { if (province.code == this.provinceRouteCode) { selectedRoute = { name: selectedRoute.name + "-" + province.name, code: province.code } break } } } that.selectedRoutes.push(selectedRoute) } this.$emit("change", this.selectedRoutes) this.cancel() }, remove: function (index) { this.selectedRoutes.$remove(index) this.$emit("change", this.selectedRoutes) } } , template: `
{{route.name}}  
选择类型 *
选择线路 *
选择省/州
  取消
` }) // 递归DNS设置 Vue.component("ns-recursion-config-box", { props: ["v-recursion-config"], data: function () { let recursion = this.vRecursionConfig if (recursion == null) { recursion = { isOn: false, hosts: [], allowDomains: [], denyDomains: [], useLocalHosts: false } } if (recursion.hosts == null) { recursion.hosts = [] } if (recursion.allowDomains == null) { recursion.allowDomains = [] } if (recursion.denyDomains == null) { recursion.denyDomains = [] } return { config: recursion, hostIsAdding: false, host: "", updatingHost: null } }, methods: { changeHosts: function (hosts) { this.config.hosts = hosts }, changeAllowDomains: function (domains) { this.config.allowDomains = domains }, changeDenyDomains: function (domains) { this.config.denyDomains = domains }, removeHost: function (index) { this.config.hosts.$remove(index) }, addHost: function () { this.updatingHost = null this.host = "" this.hostIsAdding = !this.hostIsAdding if (this.hostIsAdding) { var that = this setTimeout(function () { let hostRef = that.$refs.hostRef if (hostRef != null) { hostRef.focus() } }, 200) } }, updateHost: function (host) { this.updatingHost = host this.host = host.host this.hostIsAdding = !this.hostIsAdding if (this.hostIsAdding) { var that = this setTimeout(function () { let hostRef = that.$refs.hostRef if (hostRef != null) { hostRef.focus() } }, 200) } }, confirmHost: function () { if (this.host.length == 0) { teaweb.warn("请输入DNS地址") return } // TODO 校验Host // TODO 可以输入端口号 // TODO 可以选择协议 this.hostIsAdding = false if (this.updatingHost == null) { this.config.hosts.push({ host: this.host }) } else { this.updatingHost.host = this.host } }, cancelHost: function () { this.hostIsAdding = false } }, template: `
启用

启用后,如果找不到某个域名的解析记录,则向上一级DNS查找。

从节点本机读取
上级DNS主机

选中后,节点会试图从/etc/resolv.conf文件中读取DNS配置。

上级DNS主机地址 *
{{host.host}}  
 
允许的域名

支持星号通配符,比如*.example.org

不允许的域名

支持星号通配符,比如*.example.org。优先级比允许的域名高。

` }) Vue.component("ns-access-log-ref-box", { props: ["v-access-log-ref", "v-is-parent"], data: function () { let config = this.vAccessLogRef if (config == null) { config = { isOn: false, isPrior: false, logMissingDomains: false } } if (typeof (config.logMissingDomains) == "undefined") { config.logMissingDomains = false } return { config: config } }, template: `
启用
只记录失败查询

选中后,表示只记录查询失败的日志。

包含未添加的域名

选中后,表示日志中包含对没有在系统里创建的域名访问。

` }) Vue.component("ns-records-health-check-config-box", { props:["value"], data: function () { let config = this.value if (config == null) { config = { isOn: false, port: 80, timeoutSeconds: 5, countUp: 1, countDown: 3 } } return { config: config, portString: config.port.toString(), timeoutSecondsString: config.timeoutSeconds.toString(), countUpString: config.countUp.toString(), countDownString: config.countDown.toString() } }, watch: { portString: function (value) { let port = parseInt(value.toString()) if (isNaN(port) || port > 65535 || port < 1) { this.config.port = 80 } else { this.config.port = port } }, timeoutSecondsString: function (value) { let timeoutSeconds = parseInt(value.toString()) if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) { this.config.timeoutSeconds = 5 } else { this.config.timeoutSeconds = timeoutSeconds } }, countUpString: function (value) { let countUp = parseInt(value.toString()) if (isNaN(countUp) || countUp > 1000 || countUp < 1) { this.config.countUp = 1 } else { this.config.countUp = countUp } }, countDownString: function (value) { let countDown = parseInt(value.toString()) if (isNaN(countDown) || countDown > 1000 || countDown < 1) { this.config.countDown = 3 } else { this.config.countDown = countDown } } }, template: `
启用健康检查

选中后,表示启用当前域名下A/AAAA记录的健康检查;启用此设置后,你仍需设置单个A/AAAA记录的健康检查。

默认检测端口

通过尝试连接A/AAAA记录中的IP加此端口来确定当前记录是否健康。

默认超时时间
默认连续上线次数

连续检测{{config.countUp}}N次成功后,认为当前记录是在线的。

默认连续下线次数

连续检测{{config.countDown}}N次失败后,认为当前记录是离线的。

` }) Vue.component("ns-node-ddos-protection-config-box", { props: ["v-ddos-protection-config", "v-default-configs", "v-is-node", "v-cluster-is-on"], data: function () { let config = this.vDdosProtectionConfig if (config == null) { config = { tcp: { isPrior: false, isOn: false, maxConnections: 0, maxConnectionsPerIP: 0, newConnectionsRate: 0, newConnectionsRateBlockTimeout: 0, newConnectionsSecondlyRate: 0, newConnectionSecondlyRateBlockTimeout: 0, allowIPList: [], ports: [] } } } // initialize if (config.tcp == null) { config.tcp = { isPrior: false, isOn: false, maxConnections: 0, maxConnectionsPerIP: 0, newConnectionsRate: 0, newConnectionsRateBlockTimeout: 0, newConnectionsSecondlyRate: 0, newConnectionSecondlyRateBlockTimeout: 0, allowIPList: [], ports: [] } } return { config: config, defaultConfigs: this.vDefaultConfigs, isNode: this.vIsNode, isAddingPort: false } }, methods: { changeTCPPorts: function (ports) { this.config.tcp.ports = ports }, changeTCPAllowIPList: function (ipList) { this.config.tcp.allowIPList = ipList } }, template: `

功能说明:此功能为试验性质,目前仅能防御简单的DDoS攻击,试验期间建议仅在被攻击时启用,仅支持已安装nftables v0.9以上的Linux系统。

当前节点所在集群已设置DDoS防护。

TCP设置

启用
单节点TCP最大连接数

单个节点可以接受的TCP最大连接数。如果为0,则默认为{{defaultConfigs.tcpMaxConnections}}。

单IP TCP最大连接数

单个IP可以连接到节点的TCP最大连接数。如果为0,则默认为{{defaultConfigs.tcpMaxConnectionsPerIP}};最小值为{{defaultConfigs.tcpMinConnectionsPerIP}}。

单IP TCP新连接速率(分钟)
个新连接/每分钟
屏蔽

单个IP每分钟可以创建TCP新连接的数量。如果为0,则默认为{{defaultConfigs.tcpNewConnectionsMinutelyRate}};最小值为{{defaultConfigs.tcpNewConnectionsMinMinutelyRate}}。如果没有填写屏蔽时间,则只丢弃数据包。

单IP TCP新连接速率(秒钟)
个新连接/每秒钟
屏蔽

单个IP每秒钟可以创建TCP新连接的数量。如果为0,则默认为{{defaultConfigs.tcpNewConnectionsSecondlyRate}};最小值为{{defaultConfigs.tcpNewConnectionsMinSecondlyRate}}。如果没有填写屏蔽时间,则只丢弃数据包。

TCP端口列表

在这些端口上使用当前配置。默认为53端口。

IP白名单

在白名单中的IP不受当前设置的限制。

` }) Vue.component("ns-route-ranges-box", { props: ["v-ranges"], data: function () { let ranges = this.vRanges if (ranges == null) { ranges = [] } return { ranges: ranges, isAdding: false, isAddingBatch: false, // 类型 rangeType: "ipRange", isReverse: false, // IP范围 ipRangeFrom: "", ipRangeTo: "", batchIPRange: "", // CIDR ipCIDR: "", batchIPCIDR: "", // region regions: [], regionType: "country", regionConnector: "OR" } }, methods: { addIPRange: function () { this.isAdding = true let that = this setTimeout(function () { that.$refs.ipRangeFrom.focus() }, 100) }, addCIDR: function () { this.isAdding = true let that = this setTimeout(function () { that.$refs.ipCIDR.focus() }, 100) }, addRegions: function () { this.isAdding = true }, addRegion: function (regionType) { this.regionType = regionType }, remove: function (index) { this.ranges.$remove(index) }, cancelIPRange: function () { this.isAdding = false this.ipRangeFrom = "" this.ipRangeTo = "" this.isReverse = false }, cancelIPCIDR: function () { this.isAdding = false this.ipCIDR = "" this.isReverse = false }, cancelRegions: function () { this.isAdding = false this.regions = [] this.regionType = "country" this.regionConnector = "OR" this.isReverse = false }, confirmIPRange: function () { // 校验IP let that = this this.ipRangeFrom = this.ipRangeFrom.trim() if (!this.validateIP(this.ipRangeFrom)) { teaweb.warn("开始IP填写错误", function () { that.$refs.ipRangeFrom.focus() }) return } this.ipRangeTo = this.ipRangeTo.trim() if (!this.validateIP(this.ipRangeTo)) { teaweb.warn("结束IP填写错误", function () { that.$refs.ipRangeTo.focus() }) return } this.ranges.push({ type: "ipRange", params: { ipFrom: this.ipRangeFrom, ipTo: this.ipRangeTo, isReverse: this.isReverse } }) this.cancelIPRange() }, confirmIPCIDR: function () { let that = this if (this.ipCIDR.length == 0) { teaweb.warn("请填写CIDR", function () { that.$refs.ipCIDR.focus() }) return } if (!this.validateCIDR(this.ipCIDR)) { teaweb.warn("请输入正确的CIDR", function () { that.$refs.ipCIDR.focus() }) return } this.ranges.push({ type: "cidr", params: { cidr: this.ipCIDR, isReverse: this.isReverse } }) this.cancelIPCIDR() }, confirmRegions: function () { if (this.regions.length == 0) { this.cancelRegions() return } this.ranges.push({ type: "region", connector: this.regionConnector, params: { regions: this.regions, isReverse: this.isReverse } }) this.cancelRegions() }, addBatchIPRange: function () { this.isAddingBatch = true let that = this setTimeout(function () { that.$refs.batchIPRange.focus() }, 100) }, addBatchCIDR: function () { this.isAddingBatch = true let that = this setTimeout(function () { that.$refs.batchIPCIDR.focus() }, 100) }, cancelBatchIPRange: function () { this.isAddingBatch = false this.batchIPRange = "" this.isReverse = false }, cancelBatchIPCIDR: function () { this.isAddingBatch = false this.batchIPCIDR = "" this.isReverse = false }, confirmBatchIPRange: function () { let that = this let rangesText = this.batchIPRange if (rangesText.length == 0) { teaweb.warn("请填写要加入的IP范围", function () { that.$refs.batchIPRange.focus() }) return } let validRanges = [] let invalidLine = "" rangesText.split("\n").forEach(function (line) { line = line.trim() if (line.length == 0) { return } line = line.replace(",", ",") let pieces = line.split(",") if (pieces.length != 2) { invalidLine = line return } let ipFrom = pieces[0].trim() let ipTo = pieces[1].trim() if (!that.validateIP(ipFrom) || !that.validateIP(ipTo)) { invalidLine = line return } validRanges.push({ type: "ipRange", params: { ipFrom: ipFrom, ipTo: ipTo, isReverse: that.isReverse } }) }) if (invalidLine.length > 0) { teaweb.warn("'" + invalidLine + "'格式错误", function () { that.$refs.batchIPRange.focus() }) return } validRanges.forEach(function (v) { that.ranges.push(v) }) this.cancelBatchIPRange() }, confirmBatchIPCIDR: function () { let that = this let rangesText = this.batchIPCIDR if (rangesText.length == 0) { teaweb.warn("请填写要加入的CIDR", function () { that.$refs.batchIPCIDR.focus() }) return } let validRanges = [] let invalidLine = "" rangesText.split("\n").forEach(function (line) { let cidr = line.trim() if (cidr.length == 0) { return } if (!that.validateCIDR(cidr)) { invalidLine = line return } validRanges.push({ type: "cidr", params: { cidr: cidr, isReverse: that.isReverse } }) }) if (invalidLine.length > 0) { teaweb.warn("'" + invalidLine + "'格式错误", function () { that.$refs.batchIPCIDR.focus() }) return } validRanges.forEach(function (v) { that.ranges.push(v) }) this.cancelBatchIPCIDR() }, selectRegionCountry: function (country) { if (country == null) { return } this.regions.push({ type: "country", id: country.id, name: country.name }) this.$refs.regionCountryComboBox.clear() }, selectRegionProvince: function (province) { if (province == null) { return } this.regions.push({ type: "province", id: province.id, name: province.name }) this.$refs.regionProvinceComboBox.clear() }, selectRegionCity: function (city) { if (city == null) { return } this.regions.push({ type: "city", id: city.id, name: city.name }) this.$refs.regionCityComboBox.clear() }, selectRegionProvider: function (provider) { if (provider == null) { return } this.regions.push({ type: "provider", id: provider.id, name: provider.name }) this.$refs.regionProviderComboBox.clear() }, removeRegion: function (index) { this.regions.$remove(index) }, validateIP: function (ip) { if (ip.length == 0) { return } // IPv6 if (ip.indexOf(":") >= 0) { let pieces = ip.split(":") if (pieces.length > 8) { return false } let isOk = true pieces.forEach(function (piece) { if (!/^[\da-fA-F]{0,4}$/.test(piece)) { isOk = false } }) return isOk } if (!ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) { return false } let pieces = ip.split(".") let isOk = true pieces.forEach(function (v) { let v1 = parseInt(v) if (v1 > 255) { isOk = false } }) return isOk }, validateCIDR: function (cidr) { let pieces = cidr.split("/") if (pieces.length != 2) { return false } let ip = pieces[0] if (!this.validateIP(ip)) { return false } let mask = pieces[1] if (!/^\d{1,3}$/.test(mask)) { return false } mask = parseInt(mask, 10) if (cidr.indexOf(":") >= 0) { // IPv6 return mask <= 128 } return mask <= 32 }, updateRangeType: function (rangeType) { this.rangeType = rangeType } }, template: `
[排除] IP范围: CIDR: {{range.params.ipFrom}} - {{range.params.ipTo}} {{range.params.cidr}} 国家/地区 省份 城市 ISP :{{region.name}}      
开始IP *
结束IP *
排除

选中后表示线路中排除当前条件。

 
IP范围列表 *

每行一条,格式为开始IP,结束IP,比如192.168.1.100,192.168.1.200

排除

选中后表示线路中排除当前条件。

 
 
CIDR *

类似于192.168.2.1/24

排除

选中后表示线路中排除当前条件。

 
IP范围列表 *

每行一条,格式为IP/MASK,比如192.168.2.1/24

排除

选中后表示线路中排除当前条件。

 
 
已添加 国家/地区 省份 城市 ISP :{{region.name}}    
添加新国家/地区省份城市ISP *
       
区域之间关系

匹配所选任一区域即认为匹配成功。

匹配所有所选区域才认为匹配成功。

排除

选中后表示线路中排除当前条件。

 
 
` }) Vue.component("ns-record-health-check-config-box", { props:["value", "v-parent-config"], data: function () { let config = this.value if (config == null) { config = { isOn: false, port: 0, timeoutSeconds: 0, countUp: 0, countDown: 0 } } let parentConfig = this.vParentConfig return { config: config, portString: config.port.toString(), timeoutSecondsString: config.timeoutSeconds.toString(), countUpString: config.countUp.toString(), countDownString: config.countDown.toString(), portIsEditing: config.port > 0, timeoutSecondsIsEditing: config.timeoutSeconds > 0, countUpIsEditing: config.countUp > 0, countDownIsEditing: config.countDown > 0, parentConfig: parentConfig } }, watch: { portString: function (value) { let port = parseInt(value.toString()) if (isNaN(port) || port > 65535 || port < 1) { this.config.port = 0 } else { this.config.port = port } }, timeoutSecondsString: function (value) { let timeoutSeconds = parseInt(value.toString()) if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) { this.config.timeoutSeconds = 0 } else { this.config.timeoutSeconds = timeoutSeconds } }, countUpString: function (value) { let countUp = parseInt(value.toString()) if (isNaN(countUp) || countUp > 1000 || countUp < 1) { this.config.countUp = 0 } else { this.config.countUp = countUp } }, countDownString: function (value) { let countDown = parseInt(value.toString()) if (isNaN(countDown) || countDown > 1000 || countDown < 1) { this.config.countDown = 0 } else { this.config.countDown = countDown } } }, template: `
启用当前记录健康检查
检测端口 默认{{parentConfig.port}}   [修改]

通过尝试连接A/AAAA记录中的IP加此端口来确定当前记录是否健康。

超时时间 默认{{parentConfig.timeoutSeconds}}秒   [修改]
默认连续上线次数 默认{{parentConfig.countUp}}次   [修改]

连续检测{{config.countUp}}N次成功后,认为当前记录是在线的。

默认连续下线次数 默认{{parentConfig.countDown}}次   [修改]

连续检测{{config.countDown}}N次失败后,认为当前记录是离线的。

` }) Vue.component("ns-create-records-table", { props: ["v-types"], data: function () { let types = this.vTypes if (types == null) { types = [] } return { types: types, records: [ { name: "", type: "A", value: "", routeCodes: [], ttl: 600, index: 0 } ], lastIndex: 0, isAddingRoutes: false // 是否正在添加线路 } }, methods: { add: function () { this.records.push({ name: "", type: "A", value: "", routeCodes: [], ttl: 600, index: ++this.lastIndex }) let that = this setTimeout(function () { that.$refs.nameInputs.$last().focus() }, 100) }, remove: function (index) { this.records.$remove(index) }, addRoutes: function () { this.isAddingRoutes = true }, cancelRoutes: function () { let that = this setTimeout(function () { that.isAddingRoutes = false }, 1000) }, changeRoutes: function (record, routes) { if (routes == null) { record.routeCodes = [] } else { record.routeCodes = routes.map(function (route) { return route.code }) } } }, template: `
记录名 记录类型 线路 记录值 TTL 操作
`, }) // 选择单一线路 Vue.component("ns-route-selector", { props: ["v-route-code"], mounted: function () { let that = this Tea.action("/ns/routes/options") .post() .success(function (resp) { that.routes = resp.data.routes }) }, data: function () { let routeCode = this.vRouteCode if (routeCode == null) { routeCode = "" } return { routeCode: routeCode, routes: [] } }, template: `
` }) Vue.component("ns-user-selector", { props: ["v-user-id"], data: function () { return {} }, methods: { change: function (userId) { this.$emit("change", userId) } }, template: `
` }) Vue.component("ns-access-log-box", { props: ["v-access-log", "v-keyword"], data: function () { let accessLog = this.vAccessLog let isFailure = false if (accessLog.isRecursive) { if (accessLog.recordValue == null || accessLog.recordValue.length == 0) { isFailure = true } } else { if (accessLog.recordType == "SOA" || accessLog.recordType == "NS") { if (accessLog.recordValue == null || accessLog.recordValue.length == 0) { isFailure = true } } // 没有找到记录的不需要高亮显示,防止管理员看到红色就心理恐慌 } return { accessLog: accessLog, isFailure: isFailure } }, methods: { showLog: function () { let that = this let requestId = this.accessLog.requestId this.$parent.$children.forEach(function (v) { if (v.deselect != null) { v.deselect() } }) this.select() teaweb.popup("/ns/clusters/accessLogs/viewPopup?requestId=" + requestId, { width: "50em", height: "24em", onClose: function () { that.deselect() } }) }, select: function () { this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)" }, deselect: function () { this.$refs.box.parentNode.style.cssText = "" } }, template: `
[{{accessLog.region}}] {{accessLog.remoteAddr}} [{{accessLog.timeLocal}}] [{{accessLog.networking}}] {{accessLog.questionType}} {{accessLog.questionName}} -> {{accessLog.recordType}} {{accessLog.recordValue}}  [没有记录]
线路: {{route.name}} 递归DNS
错误:[{{accessLog.error}}]
` }) Vue.component("ns-cluster-selector", { props: ["v-cluster-id"], mounted: function () { let that = this Tea.action("/ns/clusters/options") .post() .success(function (resp) { that.clusters = resp.data.clusters }) }, data: function () { let clusterId = this.vClusterId if (clusterId == null) { clusterId = 0 } return { clusters: [], clusterId: clusterId } }, template: `
` }) Vue.component("ns-cluster-combo-box", { props: ["v-cluster-id", "name"], data: function () { let that = this Tea.action("/ns/clusters/options") .post() .success(function (resp) { that.clusters = resp.data.clusters }) let inputName = "clusterId" if (this.name != null && this.name.length > 0) { inputName = this.name } return { clusters: [], inputName: inputName } }, methods: { change: function (item) { if (item == null) { this.$emit("change", 0) } else { this.$emit("change", item.value) } } }, template: `
` }) Vue.component("plan-user-selector", { props: ["v-user-id"], data: function () { return {} }, methods: { change: function (userId) { this.$emit("change", userId) } }, template: `
` }) // 显示流量限制说明 Vue.component("plan-limit-view", { props: ["value", "v-single-mode"], data: function () { let config = this.value let hasLimit = false if (!this.vSingleMode) { if (config.trafficLimit != null && config.trafficLimit.isOn && ((config.trafficLimit.dailySize != null && config.trafficLimit.dailySize.count > 0) || (config.trafficLimit.monthlySize != null && config.trafficLimit.monthlySize.count > 0))) { hasLimit = true } else if (config.dailyRequests > 0 || config.monthlyRequests > 0) { hasLimit = true } } return { config: config, hasLimit: hasLimit } }, methods: { formatNumber: function (n) { return teaweb.formatNumber(n) }, composeCapacity: function (capacity) { return teaweb.convertSizeCapacityToString(capacity) } }, template: `
日流量限制:{{composeCapacity(config.trafficLimit.dailySize)}}
月流量限制:{{composeCapacity(config.trafficLimit.monthlySize)}}
单日请求数限制:{{formatNumber(config.dailyRequests)}}
单月请求数限制:{{formatNumber(config.monthlyRequests)}}
单日Websocket限制:{{formatNumber(config.dailyWebsocketConnections)}}
单月Websocket限制:{{formatNumber(config.monthlyWebsocketConnections)}}
文件上传限制:{{composeCapacity(config.maxUploadSize)}}
` }) Vue.component("plan-price-view", { props: ["v-plan"], data: function () { return { plan: this.vPlan } }, template: `
按时间周期计费
月度:¥{{plan.monthlyPrice}}元
季度:¥{{plan.seasonallyPrice}}元
年度:¥{{plan.yearlyPrice}}元
按流量计费
基础价格:¥{{plan.trafficPrice.base}}元/GiB
按{{plan.bandwidthPrice.percentile}}th带宽计费
{{range.minMB}} - {{range.maxMB}}MiB{{range.totalPrice}}元{{range.pricePerMB}}元/MiB
` }) // 套餐价格配置 Vue.component("plan-price-config-box", { props: ["v-price-type", "v-monthly-price", "v-seasonally-price", "v-yearly-price", "v-traffic-price", "v-bandwidth-price", "v-disable-period"], data: function () { let priceType = this.vPriceType if (priceType == null) { priceType = "bandwidth" } // 按时间周期计费 let monthlyPriceNumber = 0 let monthlyPrice = this.vMonthlyPrice if (monthlyPrice == null || monthlyPrice <= 0) { monthlyPrice = "" } else { monthlyPrice = monthlyPrice.toString() monthlyPriceNumber = parseFloat(monthlyPrice) if (isNaN(monthlyPriceNumber)) { monthlyPriceNumber = 0 } } let seasonallyPriceNumber = 0 let seasonallyPrice = this.vSeasonallyPrice if (seasonallyPrice == null || seasonallyPrice <= 0) { seasonallyPrice = "" } else { seasonallyPrice = seasonallyPrice.toString() seasonallyPriceNumber = parseFloat(seasonallyPrice) if (isNaN(seasonallyPriceNumber)) { seasonallyPriceNumber = 0 } } let yearlyPriceNumber = 0 let yearlyPrice = this.vYearlyPrice if (yearlyPrice == null || yearlyPrice <= 0) { yearlyPrice = "" } else { yearlyPrice = yearlyPrice.toString() yearlyPriceNumber = parseFloat(yearlyPrice) if (isNaN(yearlyPriceNumber)) { yearlyPriceNumber = 0 } } // 按流量计费 let trafficPrice = this.vTrafficPrice let trafficPriceBaseNumber = 0 if (trafficPrice != null) { trafficPriceBaseNumber = trafficPrice.base } else { trafficPrice = { base: 0 } } let trafficPriceBase = "" if (trafficPriceBaseNumber > 0) { trafficPriceBase = trafficPriceBaseNumber.toString() } // 按带宽计费 let bandwidthPrice = this.vBandwidthPrice if (bandwidthPrice == null) { bandwidthPrice = { percentile: 95, ranges: [] } } else if (bandwidthPrice.ranges == null) { bandwidthPrice.ranges = [] } return { priceType: priceType, monthlyPrice: monthlyPrice, seasonallyPrice: seasonallyPrice, yearlyPrice: yearlyPrice, monthlyPriceNumber: monthlyPriceNumber, seasonallyPriceNumber: seasonallyPriceNumber, yearlyPriceNumber: yearlyPriceNumber, trafficPriceBase: trafficPriceBase, trafficPrice: trafficPrice, bandwidthPrice: bandwidthPrice, bandwidthPercentile: bandwidthPrice.percentile } }, methods: { changeBandwidthPriceRanges: function (ranges) { this.bandwidthPrice.ranges = ranges } }, watch: { monthlyPrice: function (v) { let price = parseFloat(v) if (isNaN(price)) { price = 0 } this.monthlyPriceNumber = price }, seasonallyPrice: function (v) { let price = parseFloat(v) if (isNaN(price)) { price = 0 } this.seasonallyPriceNumber = price }, yearlyPrice: function (v) { let price = parseFloat(v) if (isNaN(price)) { price = 0 } this.yearlyPriceNumber = price }, trafficPriceBase: function (v) { let price = parseFloat(v) if (isNaN(price)) { price = 0 } this.trafficPrice.base = price }, bandwidthPercentile: function (v) { let percentile = parseInt(v) if (isNaN(percentile) || percentile <= 0) { percentile = 95 } else if (percentile > 100) { percentile = 100 } this.bandwidthPrice.percentile = percentile } }, template: `
 按带宽    按流量    按时间周期
月度价格

如果为0表示免费。

季度价格

如果为0表示免费。

年度价格

如果为0表示免费。

基础流量费用 *
元/GB
带宽百分位 *
th
带宽价格
` }) Vue.component("plan-price-traffic-config-box", { props: ["v-plan-price-traffic-config"], data: function () { let config = this.vPlanPriceTrafficConfig if (config == null) { config = { base: 0, ranges: [], supportRegions: false } } if (config.ranges == null) { config.ranges = [] } return { config: config, priceBase: config.base, isEditing: false } }, watch: { priceBase: function (v) { let f = parseFloat(v) if (isNaN(f) || f < 0) { this.config.base = 0 } else { this.config.base = f } } }, methods: { edit: function () { this.isEditing = !this.isEditing } }, template: `
基础流量价格:{{config.base}}元/GB没有设置   |   阶梯价格:{{config.ranges.length}}段没有设置   |  支持区域流量计费
修改
基础流量费用
元/GB

没有定义流量阶梯价格时,使用此价格。

流量阶梯价格
支持按区域流量计费

选中后,表示可以根据节点所在区域设置不同的流量价格;并且开启此项后才可以使用流量包。

` }) Vue.component("plan-bandwidth-limit-view", { props: ["value"], template: `
带宽限制:
` }) Vue.component("plan-bandwidth-ranges", { props: ["value"], data: function () { let ranges = this.value if (ranges == null) { ranges = [] } return { ranges: ranges, isAdding: false, minMB: "", minMBUnit: "mb", maxMB: "", maxMBUnit: "mb", pricePerMB: "", totalPrice: "", addingRange: { minMB: 0, maxMB: 0, pricePerMB: 0, totalPrice: 0 } } }, methods: { add: function () { this.isAdding = !this.isAdding let that = this setTimeout(function () { that.$refs.minMB.focus() }) }, cancelAdding: function () { this.isAdding = false }, confirm: function () { if (this.addingRange.minMB < 0) { teaweb.warn("带宽下限需要大于0") return } if (this.addingRange.maxMB < 0) { teaweb.warn("带宽上限需要大于0") return } if (this.addingRange.pricePerMB <= 0) { teaweb.warn("请设置单位价格或者总价格") return } this.isAdding = false this.minMB = "" this.maxMB = "" this.pricePerMB = "" this.totalPrice = "" this.ranges.push(this.addingRange) this.ranges.$sort(function (v1, v2) { if (v1.minMB < v2.minMB) { return -1 } if (v1.minMB == v2.minMB) { if (v2.maxMB == 0 || v1.maxMB < v2.maxMB) { return -1 } return 0 } return 1 }) this.change() this.addingRange = { minMB: 0, maxMB: 0, pricePerMB: 0, totalPrice: 0 } }, remove: function (index) { this.ranges.$remove(index) this.change() }, change: function () { this.$emit("change", this.ranges) }, formatMB: function (mb) { return teaweb.formatBits(mb * 1024 * 1024) }, changeMinMB: function (v) { let minMB = parseFloat(v.toString()) if (isNaN(minMB) || minMB < 0) { minMB = 0 } switch (this.minMBUnit) { case "gb": minMB *= 1024 break case "tb": minMB *= 1024 * 1024 break } this.addingRange.minMB = minMB }, changeMaxMB: function (v) { let maxMB = parseFloat(v.toString()) if (isNaN(maxMB) || maxMB < 0) { maxMB = 0 } switch (this.maxMBUnit) { case "gb": maxMB *= 1024 break case "tb": maxMB *= 1024 * 1024 break } this.addingRange.maxMB = maxMB } }, watch: { minMB: function (v) { this.changeMinMB(v) }, minMBUnit: function () { this.changeMinMB(this.minMB) }, maxMB: function (v) { this.changeMaxMB(v) }, maxMBUnit: function () { this.changeMaxMB(this.maxMB) }, pricePerMB: function (v) { let pricePerMB = parseFloat(v.toString()) if (isNaN(pricePerMB) || pricePerMB < 0) { pricePerMB = 0 } this.addingRange.pricePerMB = pricePerMB }, totalPrice: function (v) { let totalPrice = parseFloat(v.toString()) if (isNaN(totalPrice) || totalPrice < 0) { totalPrice = 0 } this.addingRange.totalPrice = totalPrice } }, template: `
{{formatMB(range.minMB)}} - {{formatMB(range.maxMB)}}   价格:{{range.totalPrice}}元{{range.pricePerMB}}元/Mbps  
带宽下限 *
带宽上限 *

如果填0,表示上不封顶。

单位价格
元/Mbps

和总价格二选一。如果设置了单位价格,那么"总价格 = 单位价格 x 带宽/Mbps"。

总价格

固定的总价格,和单位价格二选一。

 
` }) Vue.component("plan-price-bandwidth-config-box", { props: ["v-plan-price-bandwidth-config"], data: function () { let config = this.vPlanPriceBandwidthConfig if (config == null) { config = { percentile: 95, base: 0, ranges: [], supportRegions: false } } if (config.ranges == null) { config.ranges = [] } return { config: config, bandwidthPercentile: config.percentile, priceBase: config.base, isEditing: false } }, watch: { priceBase: function (v) { let f = parseFloat(v) if (isNaN(f) || f < 0) { this.config.base = 0 } else { this.config.base = f } }, bandwidthPercentile: function (v) { let i = parseInt(v) if (isNaN(i) || i < 0) { this.config.percentile = 0 } else { this.config.percentile = i } } }, methods: { edit: function () { this.isEditing = !this.isEditing } }, template: `
带宽百分位:{{config.percentile}}th没有设置   |   基础带宽价格:{{config.base}}元/Mbps没有设置   |   阶梯价格:{{config.ranges.length}}段没有设置   |  支持区域带宽计费  |  使用平均带宽算法
修改
带宽百分位 *
th

带宽计费位置,在1-100之间。

基础带宽费用
元/Mbps

没有定义带宽阶梯价格时,使用此价格。

带宽阶梯价格
支持按区域带宽计费

选中后,表示可以根据节点所在区域设置不同的带宽价格。

带宽算法

按在计时时间段内(5分钟)最高带宽峰值计算,比如5分钟内最高的某个时间点带宽为100Mbps,那么就认为此时间段内的峰值带宽为100Mbps。修改此选项会同时影响到用量统计图表。

按在计时时间段内(5分钟)平均带宽计算,即此时间段内的总流量除以时间段的秒数,比如5分钟(300秒)内总流量600MiB,那么带宽即为600MiB * 8bit/300s = 16Mbps;通常平均带宽算法要比峰值带宽要少很多。修改此选项会同时影响到用量统计图表。

` }) Vue.component("plan-traffic-ranges", { props: ["value"], data: function () { let ranges = this.value if (ranges == null) { ranges = [] } return { ranges: ranges, isAdding: false, minGB: "", minGBUnit: "gb", maxGB: "", maxGBUnit: "gb", pricePerGB: "", totalPrice: "", addingRange: { minGB: 0, maxGB: 0, pricePerGB: 0, totalPrice: 0 } } }, methods: { add: function () { this.isAdding = !this.isAdding let that = this setTimeout(function () { that.$refs.minGB.focus() }) }, cancelAdding: function () { this.isAdding = false }, confirm: function () { if (this.addingRange.minGB < 0) { teaweb.warn("流量下限需要大于0") return } if (this.addingRange.maxGB < 0) { teaweb.warn("流量上限需要大于0") return } if (this.addingRange.pricePerGB <= 0 && this.addingRange.totalPrice <= 0) { teaweb.warn("请设置单位价格或者总价格") return; } this.isAdding = false this.minGB = "" this.maxGB = "" this.pricePerGB = "" this.totalPrice = "" this.ranges.push(this.addingRange) this.ranges.$sort(function (v1, v2) { if (v1.minGB < v2.minGB) { return -1 } if (v1.minGB == v2.minGB) { if (v2.maxGB == 0 || v1.maxGB < v2.maxGB) { return -1 } return 0 } return 1 }) this.change() this.addingRange = { minGB: 0, maxGB: 0, pricePerGB: 0, totalPrice: 0 } }, remove: function (index) { this.ranges.$remove(index) this.change() }, change: function () { this.$emit("change", this.ranges) }, formatGB: function (gb) { return teaweb.formatBytes(gb * 1024 * 1024 * 1024) }, changeMinGB: function (v) { let minGB = parseFloat(v.toString()) if (isNaN(minGB) || minGB < 0) { minGB = 0 } switch (this.minGBUnit) { case "tb": minGB *= 1024 break case "pb": minGB *= 1024 * 1024 break case "eb": minGB *= 1024 * 1024 * 1024 break } this.addingRange.minGB = minGB }, changeMaxGB: function (v) { let maxGB = parseFloat(v.toString()) if (isNaN(maxGB) || maxGB < 0) { maxGB = 0 } switch (this.maxGBUnit) { case "tb": maxGB *= 1024 break case "pb": maxGB *= 1024 * 1024 break case "eb": maxGB *= 1024 * 1024 * 1024 break } this.addingRange.maxGB = maxGB } }, watch: { minGB: function (v) { this.changeMinGB(v) }, minGBUnit: function (v) { this.changeMinGB(this.minGB) }, maxGB: function (v) { this.changeMaxGB(v) }, maxGBUnit: function (v) { this.changeMaxGB(this.maxGB) }, pricePerGB: function (v) { let pricePerGB = parseFloat(v.toString()) if (isNaN(pricePerGB) || pricePerGB < 0) { pricePerGB = 0 } this.addingRange.pricePerGB = pricePerGB }, totalPrice: function (v) { let totalPrice = parseFloat(v.toString()) if (isNaN(totalPrice) || totalPrice < 0) { totalPrice = 0 } this.addingRange.totalPrice = totalPrice } }, template: `
{{formatGB(range.minGB)}} - {{formatGB(range.maxGB)}}   价格:{{range.totalPrice}}元{{range.pricePerGB}}元/GB  
流量下限 *
流量上限 *

如果填0,表示上不封顶。

单位价格
元/GB

和总价格二选一。如果设置了单位价格,那么"总价格 = 单位价格 x 流量/GB"。

总价格

固定的总价格,和单位价格二选一。

 
` }) Vue.component("http-stat-config-box", { props: ["v-stat-config", "v-is-location", "v-is-group"], data: function () { let stat = this.vStatConfig if (stat == null) { stat = { isPrior: false, isOn: false } } return { stat: stat } }, template: `
启用统计
` }) Vue.component("http-firewall-page-options-viewer", { props: ["v-page-options"], data: function () { return { options: this.vPageOptions } }, template: `
默认设置
状态码:{{options.status}} / 提示内容:[{{options.body.length}}字符]
` }) Vue.component("http-request-conds-box", { props: ["v-conds"], data: function () { let conds = this.vConds if (conds == null) { conds = { isOn: true, connector: "or", groups: [] } } if (conds.groups == null) { conds.groups = [] } return { conds: conds, components: window.REQUEST_COND_COMPONENTS } }, methods: { change: function () { this.$emit("change", this.conds) }, addGroup: function () { window.UPDATING_COND_GROUP = null let that = this teaweb.popup("/servers/server/settings/conds/addGroupPopup", { height: "30em", callback: function (resp) { that.conds.groups.push(resp.data.group) that.change() } }) }, updateGroup: function (groupIndex, group) { window.UPDATING_COND_GROUP = group let that = this teaweb.popup("/servers/server/settings/conds/addGroupPopup", { height: "30em", callback: function (resp) { Vue.set(that.conds.groups, groupIndex, resp.data.group) that.change() } }) }, removeGroup: function (groupIndex) { let that = this teaweb.confirm("确定要删除这一组条件吗?", function () { that.conds.groups.$remove(groupIndex) that.change() }) }, typeName: function (cond) { let c = this.components.$find(function (k, v) { return v.type == cond.type }) if (c != null) { return c.name; } return cond.param + " " + cond.operator } }, template: `
分组{{groupIndex+1}} {{cond.param}} {{cond.operator}} {{typeName(cond)}}: {{cond.value}} {{group.connector}}  
分组之间关系

只要满足其中一个条件分组即可。 需要满足所有条件分组。

` }) Vue.component("ssl-config-box", { props: [ "v-ssl-policy", "v-protocol", "v-server-id", "v-support-http3" ], created: function () { let that = this setTimeout(function () { that.sortableCipherSuites() }, 100) }, data: function () { let policy = this.vSslPolicy if (policy == null) { policy = { id: 0, isOn: true, certRefs: [], certs: [], clientCARefs: [], clientCACerts: [], clientAuthType: 0, minVersion: "TLS 1.1", hsts: null, cipherSuitesIsOn: false, cipherSuites: [], http2Enabled: true, http3Enabled: false, ocspIsOn: false } } else { if (policy.certRefs == null) { policy.certRefs = [] } if (policy.certs == null) { policy.certs = [] } if (policy.clientCARefs == null) { policy.clientCARefs = [] } if (policy.clientCACerts == null) { policy.clientCACerts = [] } if (policy.cipherSuites == null) { policy.cipherSuites = [] } } let hsts = policy.hsts let hstsMaxAgeString = "31536000" if (hsts == null) { hsts = { isOn: false, maxAge: 31536000, includeSubDomains: false, preload: false, domains: [] } } if (hsts.maxAge != null) { hstsMaxAgeString = hsts.maxAge.toString() } return { policy: policy, // hsts hsts: hsts, hstsOptionsVisible: false, hstsDomainAdding: false, hstsMaxAgeString: hstsMaxAgeString, addingHstsDomain: "", hstsDomainEditingIndex: -1, // 相关数据 allVersions: window.SSL_ALL_VERSIONS, allCipherSuites: window.SSL_ALL_CIPHER_SUITES.$copy(), modernCipherSuites: window.SSL_MODERN_CIPHER_SUITES, intermediateCipherSuites: window.SSL_INTERMEDIATE_CIPHER_SUITES, allClientAuthTypes: window.SSL_ALL_CLIENT_AUTH_TYPES, cipherSuitesVisible: false, // 高级选项 moreOptionsVisible: false } }, watch: { hsts: { deep: true, handler: function () { this.policy.hsts = this.hsts } } }, methods: { // 删除证书 removeCert: function (index) { let that = this teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前服务不再使用此证书。", function () { that.policy.certRefs.$remove(index) that.policy.certs.$remove(index) }) }, // 选择证书 selectCert: function () { let that = this let selectedCertIds = [] if (this.policy != null && this.policy.certs.length > 0) { this.policy.certs.forEach(function (cert) { selectedCertIds.push(cert.id.toString()) }) } let serverId = this.vServerId if (serverId == null) { serverId = 0 } teaweb.popup("/servers/certs/selectPopup?selectedCertIds=" + selectedCertIds + "&serverId=" + serverId, { width: "50em", height: "30em", callback: function (resp) { if (resp.data.cert != null && resp.data.certRef != null) { that.policy.certRefs.push(resp.data.certRef) that.policy.certs.push(resp.data.cert) } if (resp.data.certs != null && resp.data.certRefs != null) { that.policy.certRefs.$pushAll(resp.data.certRefs) that.policy.certs.$pushAll(resp.data.certs) } that.$forceUpdate() } }) }, // 上传证书 uploadCert: function () { let that = this let serverId = this.vServerId if (typeof serverId != "number" && typeof serverId != "string") { serverId = 0 } teaweb.popup("/servers/certs/uploadPopup?serverId=" + serverId, { height: "30em", callback: function (resp) { teaweb.success("上传成功", function () { that.policy.certRefs.push(resp.data.certRef) that.policy.certs.push(resp.data.cert) }) } }) }, // 批量上传 uploadBatch: function () { let that = this let serverId = this.vServerId if (typeof serverId != "number" && typeof serverId != "string") { serverId = 0 } teaweb.popup("/servers/certs/uploadBatchPopup?serverId=" + serverId, { callback: function (resp) { if (resp.data.cert != null) { that.policy.certRefs.push(resp.data.certRef) that.policy.certs.push(resp.data.cert) } if (resp.data.certs != null) { that.policy.certRefs.$pushAll(resp.data.certRefs) that.policy.certs.$pushAll(resp.data.certs) } that.$forceUpdate() } }) }, // 申请证书 requestCert: function () { // 已经在证书中的域名 let excludeServerNames = [] if (this.policy != null && this.policy.certs.length > 0) { this.policy.certs.forEach(function (cert) { excludeServerNames.$pushAll(cert.dnsNames) }) } let that = this teaweb.popup("/servers/server/settings/https/requestCertPopup?serverId=" + this.vServerId + "&excludeServerNames=" + excludeServerNames.join(","), { callback: function () { that.policy.certRefs.push(resp.data.certRef) that.policy.certs.push(resp.data.cert) } }) }, // 更多选项 changeOptionsVisible: function () { this.moreOptionsVisible = !this.moreOptionsVisible }, // 格式化时间 formatTime: function (timestamp) { return new Date(timestamp * 1000).format("Y-m-d") }, // 格式化加密套件 formatCipherSuite: function (cipherSuite) { return cipherSuite.replace(/(AES|3DES)/, "$1") }, // 添加单个套件 addCipherSuite: function (cipherSuite) { if (!this.policy.cipherSuites.$contains(cipherSuite)) { this.policy.cipherSuites.push(cipherSuite) } this.allCipherSuites.$removeValue(cipherSuite) }, // 删除单个套件 removeCipherSuite: function (cipherSuite) { let that = this teaweb.confirm("确定要删除此套件吗?", function () { that.policy.cipherSuites.$removeValue(cipherSuite) that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$findAll(function (k, v) { return !that.policy.cipherSuites.$contains(v) }) }) }, // 清除所选套件 clearCipherSuites: function () { let that = this teaweb.confirm("确定要清除所有已选套件吗?", function () { that.policy.cipherSuites = [] that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$copy() }) }, // 批量添加套件 addBatchCipherSuites: function (suites) { var that = this teaweb.confirm("确定要批量添加套件?", function () { suites.$each(function (k, v) { if (that.policy.cipherSuites.$contains(v)) { return } that.policy.cipherSuites.push(v) }) }) }, /** * 套件拖动排序 */ sortableCipherSuites: function () { var box = document.querySelector(".cipher-suites-box") Sortable.create(box, { draggable: ".label", handle: ".icon.handle", onStart: function () { }, onUpdate: function (event) { } }) }, // 显示所有套件 showAllCipherSuites: function () { this.cipherSuitesVisible = !this.cipherSuitesVisible }, // 显示HSTS更多选项 showMoreHSTS: function () { this.hstsOptionsVisible = !this.hstsOptionsVisible; if (this.hstsOptionsVisible) { this.changeHSTSMaxAge() } }, // 监控HSTS有效期修改 changeHSTSMaxAge: function () { var v = parseInt(this.hstsMaxAgeString) if (isNaN(v) || v < 0) { this.hsts.maxAge = 0 this.hsts.days = "-" return } this.hsts.maxAge = v this.hsts.days = v / 86400 if (this.hsts.days == 0) { this.hsts.days = "-" } }, // 设置HSTS有效期 setHSTSMaxAge: function (maxAge) { this.hstsMaxAgeString = maxAge.toString() this.changeHSTSMaxAge() }, // 添加HSTS域名 addHstsDomain: function () { this.hstsDomainAdding = true this.hstsDomainEditingIndex = -1 let that = this setTimeout(function () { that.$refs.addingHstsDomain.focus() }, 100) }, // 修改HSTS域名 editHstsDomain: function (index) { this.hstsDomainEditingIndex = index this.addingHstsDomain = this.hsts.domains[index] this.hstsDomainAdding = true let that = this setTimeout(function () { that.$refs.addingHstsDomain.focus() }, 100) }, // 确认HSTS域名添加 confirmAddHstsDomain: function () { this.addingHstsDomain = this.addingHstsDomain.trim() if (this.addingHstsDomain.length == 0) { return; } if (this.hstsDomainEditingIndex > -1) { this.hsts.domains[this.hstsDomainEditingIndex] = this.addingHstsDomain } else { this.hsts.domains.push(this.addingHstsDomain) } this.cancelHstsDomainAdding() }, // 取消HSTS域名添加 cancelHstsDomainAdding: function () { this.hstsDomainAdding = false this.addingHstsDomain = "" this.hstsDomainEditingIndex = -1 }, // 删除HSTS域名 removeHstsDomain: function (index) { this.cancelHstsDomainAdding() this.hsts.domains.$remove(index) }, // 选择客户端CA证书 selectClientCACert: function () { let that = this teaweb.popup("/servers/certs/selectPopup?isCA=1", { width: "50em", height: "30em", callback: function (resp) { if (resp.data.cert != null && resp.data.certRef != null) { that.policy.clientCARefs.push(resp.data.certRef) that.policy.clientCACerts.push(resp.data.cert) } if (resp.data.certs != null && resp.data.certRefs != null) { that.policy.clientCARefs.$pushAll(resp.data.certRefs) that.policy.clientCACerts.$pushAll(resp.data.certs) } that.$forceUpdate() } }) }, // 上传CA证书 uploadClientCACert: function () { let that = this teaweb.popup("/servers/certs/uploadPopup?isCA=1", { height: "28em", callback: function (resp) { teaweb.success("上传成功", function () { that.policy.clientCARefs.push(resp.data.certRef) that.policy.clientCACerts.push(resp.data.cert) }) } }) }, // 删除客户端CA证书 removeClientCACert: function (index) { let that = this teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前服务不再使用此证书。", function () { that.policy.clientCARefs.$remove(index) that.policy.clientCACerts.$remove(index) }) } }, template: `

SSL/TLS相关配置

启用HTTP/2
启用HTTP/3
选择证书
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}  
选择或上传证书后HTTPSTLS服务才能生效。
  |       |  
TLS最低版本
加密算法套件(CipherSuites)
已添加套件({{policy.cipherSuites.length}}):
 

点击可选套件添加。

开启HSTS

开启后,会自动在响应Header中加入 Strict-Transport-Security: ... max-age={{hsts.maxAge}} ; includeSubDomains ; preload 修改

HSTS有效时间(max-age)
{{hsts.days}}天

[1年/365天]     [6个月/182.5天]     [1个月/30天]

HSTS包含子域名(includeSubDomains)
HSTS预加载(preload)
HSTS生效的域名
{{domain}}  
  取消

如果没有设置域名的话,则默认支持所有的域名。

OCSP Stapling

选中表示启用OCSP Stapling。

客户端认证方式
客户端认证CA证书
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}  
 

用来校验客户端证书以增强安全性,通常不需要设置。

` }) // Action列表 Vue.component("http-firewall-actions-view", { props: ["v-actions"], template: `
{{action.name}} ({{action.code.toUpperCase()}})
[{{action.options.status}}] [分组] [网站] [网站和策略]
` }) // 显示WAF规则的标签 Vue.component("http-firewall-rule-label", { props: ["v-rule"], data: function () { return { rule: this.vRule } }, methods: { showErr: function (err) { teaweb.popupTip("规则校验错误,请修正:" + teaweb.encodeHTML(err) + "") }, calculateParamName: function (param) { let paramName = "" if (param != null) { window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { paramName = checkpoint.name } }) } return paramName }, calculateParamDescription: function (param) { let paramName = "" let paramDescription = "" if (param != null) { window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { paramName = checkpoint.name paramDescription = checkpoint.description } }) } return paramName + ": " + paramDescription }, operatorName: function (operatorCode) { let operatorName = operatorCode if (typeof (window.WAF_RULE_OPERATORS) != null) { window.WAF_RULE_OPERATORS.forEach(function (v) { if (v.code == operatorCode) { operatorName = v.name } }) } return operatorName }, operatorDescription: function (operatorCode) { let operatorName = operatorCode let operatorDescription = "" if (typeof (window.WAF_RULE_OPERATORS) != null) { window.WAF_RULE_OPERATORS.forEach(function (v) { if (v.code == operatorCode) { operatorName = v.name operatorDescription = v.description } }) } return operatorName + ": " + operatorDescription }, operatorDataType: function (operatorCode) { let operatorDataType = "none" if (typeof (window.WAF_RULE_OPERATORS) != null) { window.WAF_RULE_OPERATORS.forEach(function (v) { if (v.code == operatorCode) { operatorDataType = v.dataType } }) } return operatorDataType }, isEmptyString: function (v) { return typeof v == "string" && v.length == 0 } }, template: `
{{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} {{rule.checkpointOptions.period}}秒内请求数 允许{{rule.checkpointOptions.allowDomains}} 禁止{{rule.checkpointOptions.denyDomains}} | {{paramFilter.code}} <{{operatorName(rule.operator)}}> {{rule.value}} [空] ({{rule.description}}) 规则错误
` }) // 缓存条件列表 Vue.component("http-cache-refs-box", { props: ["v-cache-refs"], data: function () { let refs = this.vCacheRefs if (refs == null) { refs = [] } return { refs: refs } }, methods: { timeUnitName: function (unit) { switch (unit) { case "ms": return "毫秒" case "second": return "秒" case "minute": return "分钟" case "hour": return "小时" case "day": return "天" case "week": return "周 " } return unit } }, template: `

暂时还没有缓存条件。

缓存条件 缓存时间
忽略URI参数 {{cacheRef.minSize.count}}{{cacheRef.minSize.unit}} - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} 0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} {{cacheRef.methods.join(", ")}} Expires 状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}} 分片缓存 Range回源 If-None-Match If-Modified-Since 支持异步 {{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}} 不缓存
` }) Vue.component("ssl-certs-box", { props: [ "v-certs", // 证书列表 "v-cert", // 单个证书 "v-protocol", // 协议:https|tls "v-view-size", // 弹窗尺寸:normal, mini "v-single-mode", // 单证书模式 "v-description", // 描述文字 "v-domains", // 搜索的域名列表或者函数 "v-user-id" // 用户ID ], data: function () { let certs = this.vCerts if (certs == null) { certs = [] } if (this.vCert != null) { certs.push(this.vCert) } let description = this.vDescription if (description == null || typeof (description) != "string") { description = "" } return { certs: certs, description: description } }, methods: { certIds: function () { return this.certs.map(function (v) { return v.id }) }, // 删除证书 removeCert: function (index) { let that = this teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前服务不再使用此证书。", function () { that.certs.$remove(index) }) }, // 选择证书 selectCert: function () { let that = this let width = "54em" let height = "32em" let viewSize = this.vViewSize if (viewSize == null) { viewSize = "normal" } if (viewSize == "mini") { width = "35em" height = "20em" } let searchingDomains = [] if (this.vDomains != null) { if (typeof this.vDomains == "function") { let resultDomains = this.vDomains() if (resultDomains != null && typeof resultDomains == "object" && (resultDomains instanceof Array)) { searchingDomains = resultDomains } } else if (typeof this.vDomains == "object" && (this.vDomains instanceof Array)) { searchingDomains = this.vDomains } if (searchingDomains.length > 10000) { searchingDomains = searchingDomains.slice(0, 10000) } } let selectedCertIds = this.certs.map(function (cert) { return cert.id }) let userId = this.vUserId if (userId == null) { userId = 0 } teaweb.popup("/servers/certs/selectPopup?viewSize=" + viewSize + "&searchingDomains=" + window.encodeURIComponent(searchingDomains.join(",")) + "&selectedCertIds=" + selectedCertIds.join(",") + "&userId=" + userId, { width: width, height: height, callback: function (resp) { if (resp.data.cert != null) { that.certs.push(resp.data.cert) } if (resp.data.certs != null) { that.certs.$pushAll(resp.data.certs) } that.$forceUpdate() } }) }, // 上传证书 uploadCert: function () { let that = this let userId = this.vUserId if (typeof userId != "number" && typeof userId != "string") { userId = 0 } teaweb.popup("/servers/certs/uploadPopup?userId=" + userId, { height: "28em", callback: function (resp) { teaweb.success("上传成功", function () { if (resp.data.cert != null) { that.certs.push(resp.data.cert) } if (resp.data.certs != null) { that.certs.$pushAll(resp.data.certs) } that.$forceUpdate() }) } }) }, // 批量上传 uploadBatch: function () { let that = this let userId = this.vUserId if (typeof userId != "number" && typeof userId != "string") { userId = 0 } teaweb.popup("/servers/certs/uploadBatchPopup?userId=" + userId, { callback: function (resp) { if (resp.data.cert != null) { that.certs.push(resp.data.cert) } if (resp.data.certs != null) { that.certs.$pushAll(resp.data.certs) } that.$forceUpdate() } }) }, // 格式化时间 formatTime: function (timestamp) { return new Date(timestamp * 1000).format("Y-m-d") }, // 判断是否显示选择|上传按钮 buttonsVisible: function () { return this.vSingleMode == null || !this.vSingleMode || this.certs == null || this.certs.length == 0 } }, template: `
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}  
选择或上传证书后HTTPSTLS服务才能生效。 {{description}}
  |      
` }) Vue.component("http-host-redirect-box", { props: ["v-redirects"], mounted: function () { let that = this sortTable(function (ids) { let newRedirects = [] ids.forEach(function (id) { that.redirects.forEach(function (redirect) { if (redirect.id == id) { newRedirects.push(redirect) } }) }) that.updateRedirects(newRedirects) }) }, data: function () { let redirects = this.vRedirects if (redirects == null) { redirects = [] } let id = 0 redirects.forEach(function (v) { id++ v.id = id }) return { redirects: redirects, statusOptions: [ {"code": 301, "text": "Moved Permanently"}, {"code": 308, "text": "Permanent Redirect"}, {"code": 302, "text": "Found"}, {"code": 303, "text": "See Other"}, {"code": 307, "text": "Temporary Redirect"} ], id: id } }, methods: { add: function () { let that = this window.UPDATING_REDIRECT = null teaweb.popup("/servers/server/settings/redirects/createPopup", { width: "50em", height: "36em", callback: function (resp) { that.id++ resp.data.redirect.id = that.id that.redirects.push(resp.data.redirect) that.change() } }) }, update: function (index, redirect) { let that = this window.UPDATING_REDIRECT = redirect teaweb.popup("/servers/server/settings/redirects/createPopup", { width: "50em", height: "36em", callback: function (resp) { resp.data.redirect.id = redirect.id Vue.set(that.redirects, index, resp.data.redirect) that.change() } }) }, remove: function (index) { let that = this teaweb.confirm("确定要删除这条跳转规则吗?", function () { that.redirects.$remove(index) that.change() }) }, change: function () { let that = this setTimeout(function (){ that.$emit("change", that.redirects) }, 100) }, updateRedirects: function (newRedirects) { this.redirects = newRedirects this.change() } }, template: `
[创建]

暂时还没有URL跳转规则。

跳转前 跳转后 HTTP状态码 状态 操作
{{redirect.beforeURL}}
URL跳转 匹配前缀 正则匹配 精准匹配 排除:{{domain}} 仅限:{{domain}}
所有域名 {{redirect.domainsBefore[0]}} {{redirect.domainsBefore[0]}}等{{redirect.domainsBefore.length}}个域名
域名跳转 {{redirect.domainAfterScheme}} 忽略端口
所有端口 {{redirect.portsBefore.join(", ")}} {{redirect.portsBefore.slice(0, 5).join(", ")}}等{{redirect.portsBefore.length}}个端口
端口跳转 {{redirect.portAfterScheme}}
匹配条件
-> {{redirect.afterURL}} {{redirect.domainAfter}} {{redirect.portAfter}} {{redirect.status}} 默认 修改   删除

所有规则匹配顺序为从上到下,可以拖动左侧的排序。

` }) // 单个缓存条件设置 Vue.component("http-cache-ref-box", { props: ["v-cache-ref", "v-is-reverse"], mounted: function () { this.$refs.variablesDescriber.update(this.ref.key) if (this.ref.simpleCond != null) { this.condType = this.ref.simpleCond.type this.changeCondType(this.ref.simpleCond.type, true) this.condCategory = "simple" } else if (this.ref.conds != null && this.ref.conds.groups != null) { this.condCategory = "complex" } this.changeCondCategory(this.condCategory) }, data: function () { let ref = this.vCacheRef if (ref == null) { ref = { isOn: true, cachePolicyId: 0, key: "${scheme}://${host}${requestPath}${isArgs}${args}", life: {count: 1, unit: "day"}, status: [200], maxSize: {count: 128, unit: "mb"}, minSize: {count: 0, unit: "kb"}, skipCacheControlValues: ["private", "no-cache", "no-store"], skipSetCookie: true, enableRequestCachePragma: false, conds: null, // 复杂条件 simpleCond: null, // 简单条件 allowChunkedEncoding: true, allowPartialContent: true, forcePartialContent: false, enableIfNoneMatch: false, enableIfModifiedSince: false, enableReadingOriginAsync: false, 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"} } if (ref.maxSize == null) { ref.maxSize = {count: 32, unit: "mb"} } if (ref.minSize == null) { ref.minSize = {count: 0, unit: "kb"} } let condType = "url-extension" let condComponent = window.REQUEST_COND_COMPONENTS.$find(function (k, v) { return v.type == "url-extension" }) return { ref: ref, keyIgnoreArgs: typeof ref.key == "string" && ref.key.indexOf("${args}") < 0, moreOptionsVisible: false, condCategory: "simple", // 条件分类:simple|complex condType: condType, condComponent: condComponent, condIsCaseInsensitive: (ref.simpleCond != null) ? ref.simpleCond.isCaseInsensitive : true, components: window.REQUEST_COND_COMPONENTS } }, watch: { keyIgnoreArgs: function (b) { if (typeof this.ref.key != "string") { return } if (b) { this.ref.key = this.ref.key.replace("${isArgs}${args}", "") return; } if (this.ref.key.indexOf("${isArgs}") < 0) { this.ref.key = this.ref.key + "${isArgs}" } if (this.ref.key.indexOf("${args}") < 0) { this.ref.key = this.ref.key + "${args}" } } }, methods: { changeOptionsVisible: function (v) { this.moreOptionsVisible = v }, changeLife: function (v) { this.ref.life = v }, changeMaxSize: function (v) { this.ref.maxSize = v }, changeMinSize: function (v) { this.ref.minSize = v }, changeConds: function (v) { this.ref.conds = v this.ref.simpleCond = null }, changeStatusList: function (list) { let result = [] list.forEach(function (status) { let statusNumber = parseInt(status) if (isNaN(statusNumber) || statusNumber < 100 || statusNumber > 999) { return } 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 }, // 切换条件类型 changeCondCategory: function (condCategory) { this.condCategory = condCategory // resize window let dialog = window.parent.document.querySelector("*[role='dialog']") if (dialog == null) { return } switch (condCategory) { case "simple": dialog.style.width = "45em" break case "complex": let width = window.parent.innerWidth if (width > 1024) { width = 1024 } dialog.style.width = width + "px" if (this.ref.conds != null) { this.ref.conds.isOn = true } break } }, changeCondType: function (condType, isInit) { if (!isInit && this.ref.simpleCond != null) { this.ref.simpleCond.value = null } let def = this.components.$find(function (k, component) { return component.type == condType }) if (def != null) { this.condComponent = def } } }, template: ` 缓存对象 *

切换到复杂条件 »

{{condComponent.paramsTitle}} *
不区分大小写

选中后表示对比时忽略参数值的大小写。

匹配条件分组 *

« 切换到简单条件

缓存有效期 * 忽略URI参数

选中后,表示缓存Key中不包含URI参数(即问号(?))后面的内容。

缓存Key *

用来区分不同缓存内容的唯一Key。

请求方法限制

允许请求的缓存方法,默认支持所有的请求方法。

客户端过期时间(Expires) 可缓存的最大内容尺寸

内容尺寸如果高于此值则不缓存。

可缓存的最小内容尺寸

内容尺寸如果低于此值则不缓存。

支持缓存分片内容

选中后,支持缓存源站返回的某个分片的内容,该内容通过206 Partial Content状态码返回。

强制返回分片内容

选中后,表示无论客户端是否发送Range报头,都会优先尝试返回已缓存的分片内容;如果你的应用有不支持分片内容的客户端(比如有些下载软件不支持206 Partial Content),请务必关闭此功能。

强制Range回源

选中后,表示把所有包含Range报头的请求都转发到源站,而不是尝试从缓存中读取。

状态码列表

允许缓存的HTTP状态码列表。

跳过的Cache-Control值

当响应的Cache-Control为这些值时不缓存响应内容,而且不区分大小写。

跳过Set-Cookie

选中后,当响应的报头中有Set-Cookie时不缓存响应内容,防止动态内容被缓存。

支持请求no-cache刷新

选中后,当请求的报头中含有Pragma: no-cache或Cache-Control: no-cache时,会跳过缓存直接读取源内容,一般仅用于调试。

允许If-None-Match回源

特殊情况下才需要开启,可能会降低缓存命中率。

允许If-Modified-Since回源

特殊情况下才需要开启,可能会降低缓存命中率。

允许异步读取源站

试验功能。允许客户端中断连接后,仍然继续尝试从源站读取内容并缓存。

支持分段内容

选中后,Gzip等压缩后的Chunked内容可以直接缓存,无需检查内容长度。

` }) // 请求限制 Vue.component("http-request-limit-config-box", { props: ["v-request-limit-config", "v-is-group", "v-is-location"], data: function () { let config = this.vRequestLimitConfig if (config == null) { config = { isPrior: false, isOn: false, maxConns: 0, maxConnsPerIP: 0, maxBodySize: { count: -1, unit: "kb" }, outBandwidthPerConn: { count: -1, unit: "kb" } } } return { config: config, maxConns: config.maxConns, maxConnsPerIP: config.maxConnsPerIP } }, watch: { maxConns: function (v) { let conns = parseInt(v, 10) if (isNaN(conns)) { this.config.maxConns = 0 return } if (conns < 0) { this.config.maxConns = 0 } else { this.config.maxConns = conns } }, maxConnsPerIP: function (v) { let conns = parseInt(v, 10) if (isNaN(conns)) { this.config.maxConnsPerIP = 0 return } if (conns < 0) { this.config.maxConnsPerIP = 0 } else { this.config.maxConnsPerIP = conns } } }, methods: { isOn: function () { return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn } }, template: `
启用请求限制
最大并发连接数

当前服务最大并发连接数,超出此限制则响应用户429代码。为0表示不限制。

单IP最大并发连接数

单IP最大连接数,统计单个IP总连接数时不区分服务,超出此限制则响应用户429代码。为0表示不限制。当前设置的并发连接数过低,可能会影响正常用户访问,建议不小于3。

单连接带宽限制

客户端单个请求每秒可以读取的下行流量。

单请求最大尺寸

单个请求能发送的最大内容尺寸。

` }) Vue.component("http-header-replace-values", { props: ["v-replace-values"], data: function () { let values = this.vReplaceValues if (values == null) { values = [] } return { values: values, isAdding: false, addingValue: {"pattern": "", "replacement": "", "isCaseInsensitive": false, "isRegexp": false} } }, methods: { add: function () { this.isAdding = true let that = this setTimeout(function () { that.$refs.pattern.focus() }) }, remove: function (index) { this.values.$remove(index) }, confirm: function () { let that = this if (this.addingValue.pattern.length == 0) { teaweb.warn("替换前内容不能为空", function () { that.$refs.pattern.focus() }) return } this.values.push(this.addingValue) this.cancel() }, cancel: function () { this.isAdding = false this.addingValue = {"pattern": "", "replacement": "", "isCaseInsensitive": false, "isRegexp": false} } }, template: `
{{value.pattern}} => {{value.replacement}}[空]
替换前内容 *
替换后内容
是否忽略大小写
 
` }) // 浏览条件列表 Vue.component("http-request-conds-view", { props: ["v-conds"], data: function () { let conds = this.vConds if (conds == null) { conds = { isOn: true, connector: "or", groups: [] } } if (conds.groups == null) { conds.groups = [] } let that = this conds.groups.forEach(function (group) { group.conds.forEach(function (cond) { cond.typeName = that.typeName(cond) }) }) return { initConds: conds } }, computed: { // 之所以使用computed,是因为需要动态更新 conds: function () { return this.initConds } }, methods: { typeName: function (cond) { let c = window.REQUEST_COND_COMPONENTS.$find(function (k, v) { return v.type == cond.type }) if (c != null) { return c.name; } return cond.param + " " + cond.operator }, updateConds: function (conds) { this.initConds = conds }, notifyChange: function () { let that = this if (this.initConds.groups != null) { this.initConds.groups.forEach(function (group) { group.conds.forEach(function (cond) { cond.typeName = that.typeName(cond) }) }) this.$forceUpdate() } } }, template: `
{{cond.param}} {{cond.operator}} {{cond.typeName}}: {{cond.value}} {{group.connector}}  
{{group.description}}
` }) Vue.component("http-firewall-config-box", { props: ["v-firewall-config", "v-is-location", "v-is-group", "v-firewall-policy"], data: function () { let firewall = this.vFirewallConfig if (firewall == null) { firewall = { isPrior: false, isOn: false, firewallPolicyId: 0, ignoreGlobalRules: false, defaultCaptchaType: "none" } } if (firewall.defaultCaptchaType == null || firewall.defaultCaptchaType.length == 0) { firewall.defaultCaptchaType = "none" } let allCaptchaTypes = window.WAF_CAPTCHA_TYPES.$copy() // geetest let geeTestIsOn = false if (this.vFirewallPolicy != null && this.vFirewallPolicy.captchaAction != null && this.vFirewallPolicy.captchaAction.geeTestConfig != null) { geeTestIsOn = this.vFirewallPolicy.captchaAction.geeTestConfig.isOn } // 如果没有启用geetest,则还原 if (!geeTestIsOn && firewall.defaultCaptchaType == "geetest") { firewall.defaultCaptchaType = "none" } return { firewall: firewall, moreOptionsVisible: false, execGlobalRules: !firewall.ignoreGlobalRules, captchaTypes: allCaptchaTypes, geeTestIsOn: geeTestIsOn } }, watch: { execGlobalRules: function (v) { this.firewall.ignoreGlobalRules = !v } }, methods: { changeOptionsVisible: function (v) { this.moreOptionsVisible = v } }, template: `
全局WAF策略
{{vFirewallPolicy.name}}   [{{vFirewallPolicy.modeInfo.name}}] 

当前网站所在集群的设置。

当前集群没有设置WAF策略,当前配置无法生效。
启用Web防火墙

选中后,表示启用当前网站的WAF功能。

人机识别验证方式

使用系统默认的设置。

{{captchaType.description}}

启用系统全局规则

选中后,表示使用系统全局WAF策略中定义的规则。

` }) // 指标图表 Vue.component("metric-chart", { props: ["v-chart", "v-stats", "v-item", "v-column" /** in column? **/], mounted: function () { this.load() }, data: function () { let stats = this.vStats if (stats == null) { stats = [] } if (stats.length > 0) { let sum = stats.$sum(function (k, v) { return v.value }) if (sum < stats[0].total) { if (this.vChart.type == "pie") { stats.push({ keys: ["其他"], value: stats[0].total - sum, total: stats[0].total, time: stats[0].time }) } } } if (this.vChart.maxItems > 0) { stats = stats.slice(0, this.vChart.maxItems) } else { stats = stats.slice(0, 10) } stats.$rsort(function (v1, v2) { return v1.value - v2.value }) let widthPercent = 100 if (this.vChart.widthDiv > 0) { widthPercent = 100 / this.vChart.widthDiv } return { chart: this.vChart, stats: stats, item: this.vItem, width: widthPercent + "%", chartId: "metric-chart-" + this.vChart.id, valueTypeName: (this.vItem != null && this.vItem.valueTypeName != null && this.vItem.valueTypeName.length > 0) ? this.vItem.valueTypeName : "" } }, methods: { load: function () { var el = document.getElementById(this.chartId) if (el == null || el.offsetWidth == 0 || el.offsetHeight == 0) { setTimeout(this.load, 100) } else { this.render(el) } }, render: function (el) { let chart = echarts.init(el) window.addEventListener("resize", function () { chart.resize() }) switch (this.chart.type) { case "pie": this.renderPie(chart) break case "bar": this.renderBar(chart) break case "timeBar": this.renderTimeBar(chart) break case "timeLine": this.renderTimeLine(chart) break case "table": this.renderTable(chart) break } }, renderPie: function (chart) { let values = this.stats.map(function (v) { return { name: v.keys[0], value: v.value } }) let that = this chart.setOption({ tooltip: { show: true, trigger: "item", formatter: function (data) { let stat = that.stats[data.dataIndex] let percent = 0 if (stat.total > 0) { percent = Math.round((stat.value * 100 / stat.total) * 100) / 100 } let value = stat.value switch (that.item.valueType) { case "byte": value = teaweb.formatBytes(value) break case "count": value = teaweb.formatNumber(value) break } return stat.keys[0] + "
" + that.valueTypeName + ": " + value + "
占比:" + percent + "%" } }, series: [ { name: name, type: "pie", data: values, areaStyle: {}, color: ["#9DD3E8", "#B2DB9E", "#F39494", "#FBD88A", "#879BD7"] } ] }) }, renderTimeBar: function (chart) { this.stats.$sort(function (v1, v2) { return (v1.time < v2.time) ? -1 : 1 }) let values = this.stats.map(function (v) { return v.value }) let axis = {unit: "", divider: 1} switch (this.item.valueType) { case "count": axis = teaweb.countAxis(values, function (v) { return v }) break case "byte": axis = teaweb.bytesAxis(values, function (v) { return v }) break } let that = this chart.setOption({ xAxis: { data: this.stats.map(function (v) { return that.formatTime(v.time) }) }, yAxis: { axisLabel: { formatter: function (value) { return value + axis.unit } } }, tooltip: { show: true, trigger: "item", formatter: function (data) { let stat = that.stats[data.dataIndex] let value = stat.value switch (that.item.valueType) { case "byte": value = teaweb.formatBytes(value) break } return that.formatTime(stat.time) + ": " + value } }, grid: { left: 50, top: 10, right: 20, bottom: 25 }, series: [ { name: name, type: "bar", data: values.map(function (v) { return v / axis.divider }), itemStyle: { color: teaweb.DefaultChartColor }, areaStyle: {}, barWidth: "10em" } ] }) }, renderTimeLine: function (chart) { this.stats.$sort(function (v1, v2) { return (v1.time < v2.time) ? -1 : 1 }) let values = this.stats.map(function (v) { return v.value }) let axis = {unit: "", divider: 1} switch (this.item.valueType) { case "count": axis = teaweb.countAxis(values, function (v) { return v }) break case "byte": axis = teaweb.bytesAxis(values, function (v) { return v }) break } let that = this chart.setOption({ xAxis: { data: this.stats.map(function (v) { return that.formatTime(v.time) }) }, yAxis: { axisLabel: { formatter: function (value) { return value + axis.unit } } }, tooltip: { show: true, trigger: "item", formatter: function (data) { let stat = that.stats[data.dataIndex] let value = stat.value switch (that.item.valueType) { case "byte": value = teaweb.formatBytes(value) break } return that.formatTime(stat.time) + ": " + value } }, grid: { left: 50, top: 10, right: 20, bottom: 25 }, series: [ { name: name, type: "line", data: values.map(function (v) { return v / axis.divider }), itemStyle: { color: teaweb.DefaultChartColor }, areaStyle: {} } ] }) }, renderBar: function (chart) { let values = this.stats.map(function (v) { return v.value }) let axis = {unit: "", divider: 1} switch (this.item.valueType) { case "count": axis = teaweb.countAxis(values, function (v) { return v }) break case "byte": axis = teaweb.bytesAxis(values, function (v) { return v }) break } let bottom = 24 let rotate = 0 let result = teaweb.xRotation(chart, this.stats.map(function (v) { return v.keys[0] })) if (result != null) { bottom = result[0] rotate = result[1] } let that = this chart.setOption({ xAxis: { data: this.stats.map(function (v) { return v.keys[0] }), axisLabel: { interval: 0, rotate: rotate } }, tooltip: { show: true, trigger: "item", formatter: function (data) { let stat = that.stats[data.dataIndex] let percent = 0 if (stat.total > 0) { percent = Math.round((stat.value * 100 / stat.total) * 100) / 100 } let value = stat.value switch (that.item.valueType) { case "byte": value = teaweb.formatBytes(value) break case "count": value = teaweb.formatNumber(value) break } return stat.keys[0] + "
" + that.valueTypeName + ":" + value + "
占比:" + percent + "%" } }, yAxis: { axisLabel: { formatter: function (value) { return value + axis.unit } } }, grid: { left: 40, top: 10, right: 20, bottom: bottom }, series: [ { name: name, type: "bar", data: values.map(function (v) { return v / axis.divider }), itemStyle: { color: teaweb.DefaultChartColor }, areaStyle: {}, barWidth: "10em" } ] }) if (this.item.keys != null) { // IP相关操作 if (this.item.keys.$contains("${remoteAddr}")) { let that = this chart.on("click", function (args) { let index = that.item.keys.$indexesOf("${remoteAddr}")[0] let value = that.stats[args.dataIndex].keys[index] teaweb.popup("/servers/ipbox?ip=" + value, { width: "50em", height: "30em" }) }) } } }, renderTable: function (chart) { let table = `` let that = this this.stats.forEach(function (v) { let value = v.value switch (that.item.valueType) { case "byte": value = teaweb.formatBytes(value) break } table += "" let percent = 0 if (v.total > 0) { percent = Math.round((v.value * 100 / v.total) * 100) / 100 } table += "" table += "" }) table += `
对象 数值 占比
" + v.keys[0] + "" + value + "
" + percent + "%
` document.getElementById(this.chartId).innerHTML = table }, formatTime: function (time) { if (time == null) { return "" } switch (this.item.periodUnit) { case "month": return time.substring(0, 4) + "-" + time.substring(4, 6) case "week": return time.substring(0, 4) + "-" + time.substring(4, 6) case "day": return time.substring(0, 4) + "-" + time.substring(4, 6) + "-" + time.substring(6, 8) case "hour": return time.substring(0, 4) + "-" + time.substring(4, 6) + "-" + time.substring(6, 8) + " " + time.substring(8, 10) case "minute": return time.substring(0, 4) + "-" + time.substring(4, 6) + "-" + time.substring(6, 8) + " " + time.substring(8, 10) + ":" + time.substring(10, 12) } return time } }, template: `

{{chart.name}} ({{valueTypeName}})

` }) Vue.component("metric-board", { template: `
` }) Vue.component("http-cache-config-box", { props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy", "v-web-id"], data: function () { let cacheConfig = this.vCacheConfig if (cacheConfig == null) { cacheConfig = { isPrior: false, isOn: false, addStatusHeader: true, addAgeHeader: false, enableCacheControlMaxAge: false, cacheRefs: [], purgeIsOn: false, purgeKey: "", disablePolicyRefs: false } } if (cacheConfig.cacheRefs == null) { cacheConfig.cacheRefs = [] } let maxBytes = null if (this.vCachePolicy != null && this.vCachePolicy.maxBytes != null) { maxBytes = this.vCachePolicy.maxBytes } // key if (cacheConfig.key == null) { // use Vue.set to activate vue events Vue.set(cacheConfig, "key", { isOn: false, scheme: "https", host: "" }) } return { cacheConfig: cacheConfig, moreOptionsVisible: false, enablePolicyRefs: !cacheConfig.disablePolicyRefs, maxBytes: maxBytes, searchBoxVisible: false, searchKeyword: "", keyOptionsVisible: false } }, watch: { enablePolicyRefs: function (v) { this.cacheConfig.disablePolicyRefs = !v }, searchKeyword: function (v) { this.$refs.cacheRefsConfigBoxRef.search(v) } }, methods: { 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, "") .replace(/\./g, "") let result = "" for (let i = 0; i < s.length; i++) { result += String.fromCharCode(parseInt(s.substring(i, i + 1)) + ((Math.random() < 0.5) ? "a" : "A").charCodeAt(0)) } this.cacheConfig.purgeKey = result }, showMoreOptions: function () { this.moreOptionsVisible = !this.moreOptionsVisible }, changeStale: function (stale) { this.cacheConfig.stale = stale }, showSearchBox: function () { this.searchBoxVisible = !this.searchBoxVisible if (this.searchBoxVisible) { let that = this setTimeout(function () { that.$refs.searchBox.focus() }) } else { this.searchKeyword = "" } } }, template: `
全局缓存策略
{{vCachePolicy.name}}

使用当前网站所在集群的设置。

当前集群没有设置缓存策略,当前配置无法生效。
启用缓存
缓存主域名
默认   [修改]
使用主域名:{{cacheConfig.key.scheme}}://{{cacheConfig.key.host}}   [修改]
启用主域名

启用主域名后,所有缓存键值中的协议和域名部分都会修改为主域名,用来实现缓存不区分域名。

主域名 *

此域名必须是当前网站已绑定域名,在刷新缓存时也需要使用此域名。

收起选项更多选项
使用默认缓存条件

选中后使用系统全局缓存策略中已经定义的默认缓存条件。

添加X-Cache报头

选中后自动在响应Header中增加X-Cache: BYPASS|MISS|HIT|PURGE;在浏览器端查看X-Cache值时请先禁用浏览器缓存,避免影响观察。

添加Age Header

选中后自动在响应Header中增加Age: [存活时间秒数]

支持源站控制有效时间

选中后表示支持源站在Header中设置的Cache-Control: max-age=[有效时间秒数]

允许PURGE

允许使用PURGE方法清除某个URL缓存。

PURGE Key *

[随机生成]。需要在PURGE方法调用时加入X-Edge-Purge-Key: {{cacheConfig.purgeKey}} Header。只能包含字符、数字、下划线。

过时缓存策略

缓存条件   [添加]   [搜索]

` }) // 通用Header长度 let defaultGeneralHeaders = ["Cache-Control", "Connection", "Date", "Pragma", "Trailer", "Transfer-Encoding", "Upgrade", "Via", "Warning"] Vue.component("http-cond-general-header-length", { props: ["v-checkpoint"], data: function () { let headers = null let length = null if (window.parent.UPDATING_RULE != null) { let options = window.parent.UPDATING_RULE.checkpointOptions if (options.headers != null && Array.$isArray(options.headers)) { headers = options.headers } if (options.length != null) { length = options.length } } if (headers == null) { headers = defaultGeneralHeaders } if (length == null) { length = 128 } let that = this setTimeout(function () { that.change() }, 100) return { headers: headers, length: length } }, watch: { length: function (v) { let len = parseInt(v) if (isNaN(len)) { len = 0 } if (len < 0) { len = 0 } this.length = len this.change() } }, methods: { change: function () { this.vCheckpoint.options = [ { code: "headers", value: this.headers }, { code: "length", value: this.length } ] } }, template: `
通用Header列表

需要检查的Header列表。

Header值超出长度
字节

超出此长度认为匹配成功,0表示不限制。

` }) // CC Vue.component("http-firewall-checkpoint-cc", { props: ["v-checkpoint"], data: function () { let keys = [] let period = 60 let threshold = 1000 let ignoreCommonFiles = true let enableFingerprint = true let options = {} if (window.parent.UPDATING_RULE != null) { options = window.parent.UPDATING_RULE.checkpointOptions } if (options == null) { options = {} } if (options.keys != null) { keys = options.keys } if (keys.length == 0) { keys = ["${remoteAddr}", "${requestPath}"] } if (options.period != null) { period = options.period } if (options.threshold != null) { threshold = options.threshold } if (options.ignoreCommonFiles != null && typeof (options.ignoreCommonFiles) == "boolean") { ignoreCommonFiles = options.ignoreCommonFiles } if (options.enableFingerprint != null && typeof (options.enableFingerprint) == "boolean") { enableFingerprint = options.enableFingerprint } let that = this setTimeout(function () { that.change() }, 100) return { keys: keys, period: period, threshold: threshold, ignoreCommonFiles: ignoreCommonFiles, enableFingerprint: enableFingerprint, options: {}, value: threshold } }, watch: { period: function () { this.change() }, threshold: function () { this.change() }, ignoreCommonFiles: function () { this.change() }, enableFingerprint: function () { this.change() } }, methods: { changeKeys: function (keys) { this.keys = keys this.change() }, change: function () { let period = parseInt(this.period.toString()) if (isNaN(period) || period <= 0) { period = 60 } let threshold = parseInt(this.threshold.toString()) if (isNaN(threshold) || threshold <= 0) { threshold = 1000 } this.value = threshold let ignoreCommonFiles = this.ignoreCommonFiles if (typeof ignoreCommonFiles != "boolean") { ignoreCommonFiles = false } let enableFingerprint = this.enableFingerprint if (typeof enableFingerprint != "boolean") { enableFingerprint = true } this.vCheckpoint.options = [ { code: "keys", value: this.keys }, { code: "period", value: period, }, { code: "threshold", value: threshold }, { code: "ignoreCommonFiles", value: ignoreCommonFiles }, { code: "enableFingerprint", value: enableFingerprint } ] }, thresholdTooLow: function () { let threshold = parseInt(this.threshold.toString()) if (isNaN(threshold) || threshold <= 0) { threshold = 1000 } return threshold > 0 && threshold < 5 } }, template: `
统计对象组合 *
统计周期 *
阈值 *

对于网站类应用来说,当前阈值设置的太低,有可能会影响用户正常访问。

检查请求来源指纹

在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。

忽略常用文件

忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。

` }) // 防盗链 Vue.component("http-firewall-checkpoint-referer-block", { props: ["v-checkpoint"], data: function () { let allowEmpty = true let allowSameDomain = true let allowDomains = [] let denyDomains = [] let checkOrigin = true let options = {} if (window.parent.UPDATING_RULE != null) { options = window.parent.UPDATING_RULE.checkpointOptions } if (options == null) { options = {} } if (typeof (options.allowEmpty) == "boolean") { allowEmpty = options.allowEmpty } if (typeof (options.allowSameDomain) == "boolean") { allowSameDomain = options.allowSameDomain } if (options.allowDomains != null && typeof (options.allowDomains) == "object") { allowDomains = options.allowDomains } if (options.denyDomains != null && typeof (options.denyDomains) == "object") { denyDomains = options.denyDomains } if (typeof options.checkOrigin == "boolean") { checkOrigin = options.checkOrigin } let that = this setTimeout(function () { that.change() }, 100) return { allowEmpty: allowEmpty, allowSameDomain: allowSameDomain, allowDomains: allowDomains, denyDomains: denyDomains, checkOrigin: checkOrigin, options: {}, value: 0 } }, watch: { allowEmpty: function () { this.change() }, allowSameDomain: function () { this.change() }, checkOrigin: function () { this.change() } }, methods: { changeAllowDomains: function (values) { this.allowDomains = values this.change() }, changeDenyDomains: function (values) { this.denyDomains = values this.change() }, change: function () { this.vCheckpoint.options = [ { code: "allowEmpty", value: this.allowEmpty }, { code: "allowSameDomain", value: this.allowSameDomain, }, { code: "allowDomains", value: this.allowDomains }, { code: "denyDomains", value: this.denyDomains }, { code: "checkOrigin", value: this.checkOrigin } ] } }, template: `
来源域名允许为空

允许不带来源的访问。

来源域名允许一致

允许来源域名和当前访问的域名一致,相当于在站内访问。

允许的来源域名

允许的来源域名列表,比如example.com(顶级域名)、*.example.com(example.com的所有二级域名)。单个星号*表示允许所有域名。

禁止的来源域名

禁止的来源域名列表,比如example.org(顶级域名)、*.example.org(example.org的所有二级域名);除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。

同时检查Origin

如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。

` }) Vue.component("http-access-log-partitions-box", { props: ["v-partition", "v-day", "v-query"], mounted: function () { let that = this Tea.action("/servers/logs/partitionData") .params({ day: this.vDay }) .success(function (resp) { that.partitions = [] resp.data.partitions.reverse().forEach(function (v) { that.partitions.push({ code: v, isDisabled: false, hasLogs: false }) }) if (that.partitions.length > 0) { if (that.vPartition == null || that.vPartition < 0) { that.selectedPartition = that.partitions[0].code } if (that.partitions.length > 1) { that.checkLogs() } } }) .post() }, data: function () { return { partitions: [], selectedPartition: this.vPartition, checkingPartition: 0 } }, methods: { url: function (p) { let u = window.location.toString() u = u.replace(/\?partition=-?\d+/, "?") u = u.replace(/\?requestId=-?\d+/, "?") u = u.replace(/&partition=-?\d+/, "") u = u.replace(/&requestId=-?\d+/, "") if (u.indexOf("?") > 0) { u += "&partition=" + p } else { u += "?partition=" + p } return u }, disable: function (partition) { this.partitions.forEach(function (p) { if (p.code == partition) { p.isDisabled = true } }) }, checkLogs: function () { let that = this let index = this.checkingPartition let params = { partition: index } let query = this.vQuery if (query == null || query.length == 0) { return } query.split("&").forEach(function (v) { let param = v.split("=") params[param[0]] = decodeURIComponent(param[1]) }) Tea.action("/servers/logs/hasLogs") .params(params) .post() .success(function (response) { if (response.data.hasLogs) { // 因为是倒序,所以这里需要使用总长度减去index that.partitions[that.partitions.length - 1 - index].hasLogs = true } index++ if (index >= that.partitions.length) { return } that.checkingPartition = index that.checkLogs() }) } }, template: `
` }) Vue.component("http-cache-refs-config-box", { props: ["v-cache-refs", "v-cache-config", "v-cache-policy-id", "v-web-id", "v-max-bytes"], mounted: function () { let that = this sortTable(function (ids) { let newRefs = [] ids.forEach(function (id) { that.refs.forEach(function (ref) { if (ref.id == id) { newRefs.push(ref) } }) }) that.updateRefs(newRefs) that.change() }) }, data: function () { let refs = this.vCacheRefs if (refs == null) { refs = [] } let maxBytes = this.vMaxBytes let id = 0 refs.forEach(function (ref) { // preset variables id++ ref.id = id ref.visible = true // check max size if (ref.maxSize != null && maxBytes != null && maxBytes.count > 0 && teaweb.compareSizeCapacity(ref.maxSize, maxBytes) > 0) { ref.overMaxSize = maxBytes } }) return { refs: refs, id: id // 用来对条件进行排序 } }, methods: { addRef: function (isReverse) { window.UPDATING_CACHE_REF = null let height = window.innerHeight if (height > 500) { height = 500 } let that = this teaweb.popup("/servers/server/settings/cache/createPopup?isReverse=" + (isReverse ? 1 : 0), { height: height + "px", callback: function (resp) { let newRef = resp.data.cacheRef if (newRef.conds == null) { return } that.id++ newRef.id = that.id if (newRef.isReverse) { let newRefs = [] let isAdded = false that.refs.forEach(function (v) { if (!v.isReverse && !isAdded) { newRefs.push(newRef) isAdded = true } newRefs.push(v) }) if (!isAdded) { newRefs.push(newRef) } that.updateRefs(newRefs) } else { that.refs.push(newRef) } // move to bottom var afterChangeCallback = function () { setTimeout(function () { let rightBox = document.querySelector(".right-box") if (rightBox != null) { rightBox.scrollTo(0, isReverse ? 0 : 100000) } }, 100) } that.change(afterChangeCallback) } }) }, updateRef: function (index, cacheRef) { window.UPDATING_CACHE_REF = teaweb.clone(cacheRef) let height = window.innerHeight if (height > 500) { height = 500 } let that = this teaweb.popup("/servers/server/settings/cache/createPopup", { height: height + "px", callback: function (resp) { resp.data.cacheRef.id = that.refs[index].id Vue.set(that.refs, index, resp.data.cacheRef) that.change() that.$refs.cacheRef[index].updateConds(resp.data.cacheRef.conds, resp.data.cacheRef.simpleCond) that.$refs.cacheRef[index].notifyChange() } }) }, disableRef: function (ref) { ref.isOn = false this.change() }, enableRef: function (ref) { ref.isOn = true this.change() }, removeRef: function (index) { let that = this teaweb.confirm("确定要删除此缓存设置吗?", function () { that.refs.$remove(index) that.change() }) }, updateRefs: function (newRefs) { this.refs = newRefs if (this.vCacheConfig != null) { this.vCacheConfig.cacheRefs = newRefs } }, timeUnitName: function (unit) { switch (unit) { case "ms": return "毫秒" case "second": return "秒" case "minute": return "分钟" case "hour": return "小时" case "day": return "天" case "week": return "周 " } return unit }, change: function (callback) { this.$forceUpdate() // 自动保存 if (this.vCachePolicyId != null && this.vCachePolicyId > 0) { // 缓存策略 Tea.action("/servers/components/cache/updateRefs") .params({ cachePolicyId: this.vCachePolicyId, refsJSON: JSON.stringify(this.refs) }) .post() } else if (this.vWebId != null && this.vWebId > 0) { // Server Web or Group Web Tea.action("/servers/server/settings/cache/updateRefs") .params({ webId: this.vWebId, refsJSON: JSON.stringify(this.refs) }) .success(function (resp) { if (resp.data.isUpdated) { teaweb.successToast("保存成功", null, function () { if (typeof callback == "function") { callback() } }) } }) .post() } }, search: function (keyword) { if (typeof keyword != "string") { keyword = "" } this.refs.forEach(function (ref) { if (keyword.length == 0) { ref.visible = true return } ref.visible = false // simple cond if (ref.simpleCond != null && typeof ref.simpleCond.value == "string" && teaweb.match(ref.simpleCond.value, keyword)) { ref.visible = true return } // composed conds if (ref.conds == null || ref.conds.groups == null || ref.conds.groups.length == 0) { return } ref.conds.groups.forEach(function (group) { if (group.conds != null) { group.conds.forEach(function (cond) { if (typeof cond.value == "string" && teaweb.match(cond.value, keyword)) { ref.visible = true } }) } }) }) this.$forceUpdate() } }, template: `

暂时还没有缓存条件。

缓存条件 缓存时间 操作
忽略URI参数 {{cacheRef.minSize.count}}{{cacheRef.minSize.unit}} - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit.toUpperCase()}} 0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit.toUpperCase()}} 系统限制{{cacheRef.overMaxSize.count}}{{cacheRef.overMaxSize.unit.toUpperCase()}} {{cacheRef.methods.join(", ")}} Expires 状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}} 分片缓存 Range回源 If-None-Match If-Modified-Since 支持异步 {{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}} 不缓存 修改   暂停恢复   删除

所有条件匹配顺序为从上到下,可以拖动左侧的排序。服务设置的优先级比全局缓存策略设置的优先级要高。

    +添加不缓存条件
` }) Vue.component("origin-list-box", { props: ["v-primary-origins", "v-backup-origins", "v-server-type", "v-params"], data: function () { return { primaryOrigins: this.vPrimaryOrigins, backupOrigins: this.vBackupOrigins } }, methods: { createPrimaryOrigin: function () { teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, { width: "45em", height: "27em", callback: function (resp) { teaweb.success("保存成功", function () { window.location.reload() }) } }) }, createBackupOrigin: function () { teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, { width: "45em", height: "27em", callback: function (resp) { teaweb.success("保存成功", function () { window.location.reload() }) } }) }, updateOrigin: function (originId, originType) { teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, { width: "45em", height: "27em", callback: function (resp) { teaweb.success("保存成功", function () { window.location.reload() }) } }) }, deleteOrigin: function (originId, originAddr, originType) { let that = this teaweb.confirm("确定要删除此源站(" + originAddr + ")吗?", function () { Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType) .post() .success(function () { teaweb.success("删除成功", function () { window.location.reload() }) }) }) }, updateOriginIsOn: function (originId, originAddr, isOn) { let message let resultMessage if (isOn) { message = "确定要启用此源站(" + originAddr + ")吗?" resultMessage = "启用成功" } else { message = "确定要停用此源站(" + originAddr + ")吗?" resultMessage = "停用成功" } let that = this teaweb.confirm(message, function () { Tea.action("/servers/server/settings/origins/updateIsOn?" + that.vParams + "&originId=" + originId + "&isOn=" + (isOn ? 1 : 0)) .post() .success(function () { teaweb.success(resultMessage, function () { window.location.reload() }) }) }) } }, template: `

主要源站 [添加主要源站]

暂时还没有主要源站。

备用源站 [添加备用源站]

暂时还没有备用源站。

` }) Vue.component("origin-list-table", { props: ["v-origins", "v-origin-type"], data: function () { let hasMatchedDomains = false let origins = this.vOrigins if (origins != null && origins.length > 0) { origins.forEach(function (origin) { if (origin.domains != null && origin.domains.length > 0) { hasMatchedDomains = true } }) } return { hasMatchedDomains: hasMatchedDomains } }, methods: { deleteOrigin: function (originId, originAddr) { this.$emit("deleteOrigin", originId, originAddr, this.vOriginType) }, updateOrigin: function (originId) { this.$emit("updateOrigin", originId, this.vOriginType) }, updateOriginIsOn: function (originId, originAddr, isOn) { this.$emit("updateOriginIsOn", originId, originAddr, isOn) } }, template: `
源站地址 权重 状态 操作
{{origin.addr}}  
对象存储 {{origin.name}} 证书 主机名: {{origin.host}} 端口跟随 HTTP/2 匹配: {{domain}} 匹配: 所有域名
{{origin.weight}} 修改   停用启用   删除
` }) Vue.component("http-cors-header-config-box", { props: ["value"], data: function () { let config = this.value if (config == null) { config = { isOn: false, allowMethods: [], allowOrigin: "", allowCredentials: true, exposeHeaders: [], maxAge: 0, requestHeaders: [], requestMethod: "", optionsMethodOnly: false } } if (config.allowMethods == null) { config.allowMethods = [] } if (config.exposeHeaders == null) { config.exposeHeaders = [] } let maxAgeSecondsString = config.maxAge.toString() if (maxAgeSecondsString == "0") { maxAgeSecondsString = "" } return { config: config, maxAgeSecondsString: maxAgeSecondsString, moreOptionsVisible: false } }, watch: { maxAgeSecondsString: function (v) { let seconds = parseInt(v) if (isNaN(seconds)) { seconds = 0 } this.config.maxAge = seconds } }, methods: { changeMoreOptions: function (visible) { this.moreOptionsVisible = visible }, addDefaultAllowMethods: function () { let that = this let defaultMethods = ["PUT", "GET", "POST", "DELETE", "HEAD", "OPTIONS", "PATCH"] defaultMethods.forEach(function (method) { if (!that.config.allowMethods.$contains(method)) { that.config.allowMethods.push(method) } }) } }, template: `
启用CORS自适应跨域

启用后,自动在响应报头中增加对应的Access-Control-*相关内容。

允许的请求方法列表

[添加默认]Access-Control-Allow-Methods值设置。所访问资源允许使用的方法列表,不设置则表示默认为PUTGETPOSTDELETEHEADOPTIONSPATCH

预检结果缓存时间

Access-Control-Max-Age值设置。预检结果缓存时间,0或者不填表示使用浏览器默认设置。注意每个浏览器有不同的缓存时间上限。

允许服务器暴露的报头

Access-Control-Expose-Headers值设置。允许服务器暴露的报头,请注意报头的大小写。

实际请求方法

Access-Control-Request-Method值设置。实际请求服务器时使用的方法,比如POST

仅OPTIONS有效

选中后,表示当前CORS设置仅在OPTIONS方法请求时有效。

` }) Vue.component("http-firewall-policy-selector", { props: ["v-http-firewall-policy"], mounted: function () { let that = this Tea.action("/servers/components/waf/count") .post() .success(function (resp) { that.count = resp.data.count }) }, data: function () { let firewallPolicy = this.vHttpFirewallPolicy return { count: 0, firewallPolicy: firewallPolicy } }, methods: { remove: function () { this.firewallPolicy = null }, select: function () { let that = this teaweb.popup("/servers/components/waf/selectPopup", { height: "26em", callback: function (resp) { that.firewallPolicy = resp.data.firewallPolicy } }) }, create: function () { let that = this teaweb.popup("/servers/components/waf/createPopup", { height: "26em", callback: function (resp) { that.firewallPolicy = resp.data.firewallPolicy } }) } }, template: `
{{firewallPolicy.name}}    
[选择已有策略]     [创建新策略]
` }) // 压缩配置 Vue.component("http-optimization-config-box", { props: ["v-optimization-config", "v-is-location", "v-is-group"], data: function () { let config = this.vOptimizationConfig return { config: config, htmlMoreOptions: false, javascriptMoreOptions: false, cssMoreOptions: false } }, methods: { isOn: function () { return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn } }, template: `
HTML优化

可以自动优化HTML中包含的空白、注释、空标签等。只有文件可以缓存时才会被优化。

HTML例外URL

如果填写了例外URL,表示这些URL跳过不做处理。

HTML限制URL

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

Javascript优化

可以自动缩短Javascript中变量、函数名称等。只有文件可以缓存时才会被优化。

Javascript例外URL

如果填写了例外URL,表示这些URL跳过不做处理。

Javascript限制URL

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

CSS优化

可以自动去除CSS中包含的空白。只有文件可以缓存时才会被优化。

CSS例外URL

如果填写了例外URL,表示这些URL跳过不做处理。

CSS限制URL

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

` }) Vue.component("http-websocket-box", { props: ["v-websocket-ref", "v-websocket-config", "v-is-location", "v-is-group"], data: function () { let websocketRef = this.vWebsocketRef if (websocketRef == null) { websocketRef = { isPrior: false, isOn: false, websocketId: 0 } } let websocketConfig = this.vWebsocketConfig if (websocketConfig == null) { websocketConfig = { id: 0, isOn: false, handshakeTimeout: { count: 30, unit: "second" }, allowAllOrigins: true, allowedOrigins: [], requestSameOrigin: true, requestOrigin: "" } } else { if (websocketConfig.handshakeTimeout == null) { websocketConfig.handshakeTimeout = { count: 30, unit: "second", } } if (websocketConfig.allowedOrigins == null) { websocketConfig.allowedOrigins = [] } } return { websocketRef: websocketRef, websocketConfig: websocketConfig, handshakeTimeoutCountString: websocketConfig.handshakeTimeout.count.toString(), advancedVisible: false } }, watch: { handshakeTimeoutCountString: function (v) { let count = parseInt(v) if (!isNaN(count) && count >= 0) { this.websocketConfig.handshakeTimeout.count = count } else { this.websocketConfig.handshakeTimeout.count = 0 } } }, methods: { isOn: function () { return ((!this.vIsLocation && !this.vIsGroup) || this.websocketRef.isPrior) && this.websocketRef.isOn }, changeAdvancedVisible: function (v) { this.advancedVisible = v }, createOrigin: function () { let that = this teaweb.popup("/servers/server/settings/websocket/createOrigin", { height: "12.5em", callback: function (resp) { that.websocketConfig.allowedOrigins.push(resp.data.origin) } }) }, removeOrigin: function (index) { this.websocketConfig.allowedOrigins.$remove(index) } }, template: `
启用Websocket
允许所有来源域(Origin)

选中表示允许所有的来源域。

允许的来源域列表(Origin)
{{origin}}

只允许在列表中的来源域名访问Websocket服务。

传递请求来源域

选中后,表示把接收到的请求中的Origin字段传递到源站。

指定传递的来源域

指定向源站传递的Origin字段值。

握手超时时间(Handshake)

0表示使用默认的时间设置。

` }) Vue.component("http-rewrite-rule-list", { props: ["v-web-id", "v-rewrite-rules"], mounted: function () { setTimeout(this.sort, 1000) }, data: function () { let rewriteRules = this.vRewriteRules if (rewriteRules == null) { rewriteRules = [] } return { rewriteRules: rewriteRules } }, methods: { updateRewriteRule: function (rewriteRuleId) { teaweb.popup("/servers/server/settings/rewrite/updatePopup?webId=" + this.vWebId + "&rewriteRuleId=" + rewriteRuleId, { height: "26em", callback: function () { window.location.reload() } }) }, deleteRewriteRule: function (rewriteRuleId) { let that = this teaweb.confirm("确定要删除此重写规则吗?", function () { Tea.action("/servers/server/settings/rewrite/delete") .params({ webId: that.vWebId, rewriteRuleId: rewriteRuleId }) .post() .refresh() }) }, // 排序 sort: function () { if (this.rewriteRules.length == 0) { return } let that = this sortTable(function (rowIds) { Tea.action("/servers/server/settings/rewrite/sort") .post() .params({ webId: that.vWebId, rewriteRuleIds: rowIds }) .success(function () { teaweb.success("保存成功") }) }) } }, template: `

暂时还没有重写规则。

匹配规则 转发目标 转发方式 状态 操作
{{rule.pattern}}
BREAK {{rule.redirectStatus}} Host: {{rule.proxyHost}}
{{rule.replace}} 隐式 显示 修改   删除

拖动左侧的图标可以对重写规则进行排序。

` }) Vue.component("http-rewrite-labels-label", { props: ["v-class"], template: `` }) Vue.component("server-name-box", { props: ["v-server-names"], data: function () { let serverNames = this.vServerNames; if (serverNames == null) { serverNames = [] } return { serverNames: serverNames, isSearching: false, keyword: "" } }, methods: { addServerName: function () { window.UPDATING_SERVER_NAME = null let that = this teaweb.popup("/servers/addServerNamePopup", { callback: function (resp) { var serverName = resp.data.serverName that.serverNames.push(serverName) } }); }, removeServerName: function (index) { this.serverNames.$remove(index) }, updateServerName: function (index, serverName) { window.UPDATING_SERVER_NAME = teaweb.clone(serverName) let that = this teaweb.popup("/servers/addServerNamePopup", { callback: function (resp) { var serverName = resp.data.serverName Vue.set(that.serverNames, index, serverName) } }); }, showSearchBox: function () { this.isSearching = !this.isSearching if (this.isSearching) { let that = this setTimeout(function () { that.$refs.keywordRef.focus() }, 200) } else { this.keyword = "" } }, allServerNames: function () { if (this.serverNames == null) { return [] } let result = [] this.serverNames.forEach(function (serverName) { if (serverName.subNames != null && serverName.subNames.length > 0) { serverName.subNames.forEach(function (subName) { if (subName != null && subName.length > 0) { if (!result.$contains(subName)) { result.push(subName) } } }) } else if (serverName.name != null && serverName.name.length > 0) { if (!result.$contains(serverName.name)) { result.push(serverName.name) } } }) return result } }, watch: { keyword: function (v) { this.serverNames.forEach(function (serverName) { if (v.length == 0) { serverName.isShowing = true return } if (serverName.subNames == null || serverName.subNames.length == 0) { if (!teaweb.match(serverName.name, v)) { serverName.isShowing = false } } else { let found = false serverName.subNames.forEach(function (subName) { if (teaweb.match(subName, v)) { found = true } }) serverName.isShowing = found } }) } }, template: `
{{serverName.type}} {{serverName.name}} {{serverName.subNames[0]}}等{{serverName.subNames.length}}个域名
|
` }) // UAM模式配置 Vue.component("uam-config-box", { props: ["v-uam-config", "v-is-location", "v-is-group"], data: function () { let config = this.vUamConfig if (config == null) { config = { isPrior: false, isOn: false, addToWhiteList: true, onlyURLPatterns: [], exceptURLPatterns: [], minQPSPerIP: 0 } } if (config.onlyURLPatterns == null) { config.onlyURLPatterns = [] } if (config.exceptURLPatterns == null) { config.exceptURLPatterns = [] } return { config: config, moreOptionsVisible: false, minQPSPerIP: config.minQPSPerIP } }, watch: { minQPSPerIP: function (v) { let qps = parseInt(v.toString()) if (isNaN(qps) || qps < 0) { qps = 0 } this.config.minQPSPerIP = qps } }, methods: { showMoreOptions: function () { this.moreOptionsVisible = !this.moreOptionsVisible }, changeConds: function (conds) { this.config.conds = conds } }, template: `
启用5秒盾

启用后,访问网站时,自动检查浏览器环境,阻止非正常访问。

单IP最低QPS
请求数/秒

当某个IP在1分钟内平均QPS达到此值时,才会触发5秒盾;如果设置为0,表示任何访问都会触发。

加入IP白名单

选中后,表示验证通过后,将访问者IP加入到临时白名单中,此IP下次访问时不再校验5秒盾;此白名单只对5秒盾有效,不影响其他规则。此选项主要用于可能无法正常使用Cookie的网站。

例外URL

如果填写了例外URL,表示这些URL跳过5秒盾不做处理。

限制URL

如果填写了限制URL,表示只对这些URL进行5秒盾处理;如果不填则表示支持所有的URL。

匹配条件
` }) 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: `
启用过时缓存

选中后,在更新缓存失败后会尝试读取过时的缓存。

有效期

缓存在过期之后,仍然保留的时间。

状态码

在这些状态码出现时使用过时缓存,默认支持50x状态码。

支持stale-if-error

选中后,支持在Cache-Control中通过stale-if-error指定过时缓存有效期。

` }) Vue.component("firewall-syn-flood-config-viewer", { 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 } }, template: `
已启用 / 空连接次数:{{config.minAttempts}}次/分钟 / 封禁时长:{{config.timeoutSeconds}}秒 / 忽略局域网访问 未启用
` }) // 域名列表 Vue.component("domains-box", { props: ["v-domains", "name", "v-support-wildcard"], data: function () { let domains = this.vDomains if (domains == null) { domains = [] } let realName = "domainsJSON" if (this.name != null && typeof this.name == "string") { realName = this.name } let supportWildcard = true if (typeof this.vSupportWildcard == "boolean") { supportWildcard = this.vSupportWildcard } return { domains: domains, mode: "single", // single | batch batchDomains: "", isAdding: false, addingDomain: "", isEditing: false, editingIndex: -1, realName: realName, supportWildcard: supportWildcard } }, watch: { vSupportWildcard: function (v) { if (typeof v == "boolean") { this.supportWildcard = v } }, mode: function (mode) { let that = this setTimeout(function () { if (mode == "single") { if (that.$refs.addingDomain != null) { that.$refs.addingDomain.focus() } } else if (mode == "batch") { if (that.$refs.batchDomains != null) { that.$refs.batchDomains.focus() } } }, 100) } }, methods: { add: function () { this.isAdding = true let that = this setTimeout(function () { that.$refs.addingDomain.focus() }, 100) }, confirm: function () { if (this.mode == "batch") { this.confirmBatch() return } let that = this // 删除其中的空格 this.addingDomain = this.addingDomain.replace(/\s/g, "") if (this.addingDomain.length == 0) { teaweb.warn("请输入要添加的域名", function () { that.$refs.addingDomain.focus() }) return } // 基本校验 if (this.supportWildcard) { if (this.addingDomain[0] == "~") { let expr = this.addingDomain.substring(1) try { new RegExp(expr) } catch (e) { teaweb.warn("正则表达式错误:" + e.message, function () { that.$refs.addingDomain.focus() }) return } } } else { if (/[*~^]/.test(this.addingDomain)) { teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () { that.$refs.addingDomain.focus() }) return } } if (this.isEditing && this.editingIndex >= 0) { this.domains[this.editingIndex] = this.addingDomain } else { // 分割逗号(,)、顿号(、) if (this.addingDomain.match("[,、,;]")) { let domainList = this.addingDomain.split(new RegExp("[,、,;]")) domainList.forEach(function (v) { if (v.length > 0) { that.domains.push(v) } }) } else { this.domains.push(this.addingDomain) } } this.cancel() this.change() }, confirmBatch: function () { let domains = this.batchDomains.split("\n") let realDomains = [] let that = this let hasProblems = false domains.forEach(function (domain) { if (hasProblems) { return } if (domain.length == 0) { return } if (that.supportWildcard) { if (domain == "~") { let expr = domain.substring(1) try { new RegExp(expr) } catch (e) { hasProblems = true teaweb.warn("正则表达式错误:" + e.message, function () { that.$refs.batchDomains.focus() }) return } } } else { if (/[*~^]/.test(domain)) { hasProblems = true teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () { that.$refs.batchDomains.focus() }) return } } realDomains.push(domain) }) if (hasProblems) { return } if (realDomains.length == 0) { teaweb.warn("请输入要添加的域名", function () { that.$refs.batchDomains.focus() }) return } realDomains.forEach(function (domain) { that.domains.push(domain) }) this.cancel() this.change() }, edit: function (index) { this.addingDomain = this.domains[index] this.isEditing = true this.editingIndex = index let that = this setTimeout(function () { that.$refs.addingDomain.focus() }, 50) }, remove: function (index) { this.domains.$remove(index) this.change() }, cancel: function () { this.isAdding = false this.mode = "single" this.batchDomains = "" this.isEditing = false this.editingIndex = -1 this.addingDomain = "" }, change: function () { this.$emit("change", this.domains) } }, template: `
[正则] [后缀] [泛域名] {{domain}}        
 

支持普通域名(example.com)、泛域名(*.example.com、域名后缀(以点号开头,如.example.com)和正则表达式(以波浪号开头,如~.*.example.com;如果域名后有端口,请加上端口号。

只支持普通域名(example.comwww.example.com)。

` }) Vue.component("http-firewall-province-selector", { props: ["v-type", "v-provinces"], data: function () { let provinces = this.vProvinces if (provinces == null) { provinces = [] } return { listType: this.vType, provinces: provinces } }, methods: { addProvince: function () { let selectedProvinceIds = this.provinces.map(function (province) { return province.id }) let that = this teaweb.popup("/servers/server/settings/waf/ipadmin/selectProvincesPopup?type=" + this.listType + "&selectedProvinceIds=" + selectedProvinceIds.join(","), { width: "50em", height: "26em", callback: function (resp) { that.provinces = resp.data.selectedProvinces that.$forceUpdate() that.notifyChange() } }) }, removeProvince: function (index) { this.provinces.$remove(index) this.notifyChange() }, resetProvinces: function () { this.provinces = [] this.notifyChange() }, notifyChange: function () { this.$emit("change", { "provinces": this.provinces }) } }, template: `
暂时没有选择允许封禁的省份。
{{province.name}}
 
` }) Vue.component("http-referers-config-box", { props: ["v-referers-config", "v-is-location", "v-is-group"], data: function () { let config = this.vReferersConfig if (config == null) { config = { isPrior: false, isOn: false, allowEmpty: true, allowSameDomain: true, allowDomains: [], denyDomains: [], checkOrigin: true } } if (config.allowDomains == null) { config.allowDomains = [] } if (config.denyDomains == null) { config.denyDomains = [] } return { config: config, moreOptionsVisible: false } }, methods: { isOn: function () { return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn }, changeAllowDomains: function (domains) { if (typeof (domains) == "object") { this.config.allowDomains = domains this.$forceUpdate() } }, changeDenyDomains: function (domains) { if (typeof (domains) == "object") { this.config.denyDomains = domains this.$forceUpdate() } }, showMoreOptions: function () { this.moreOptionsVisible = !this.moreOptionsVisible } }, template: `
启用防盗链

选中后表示开启防盗链。

允许直接访问网站

允许用户直接访问网站,用户第一次访问网站时来源域名通常为空。

来源域名允许一致

允许来源域名和当前访问的域名一致,相当于在站内访问。

允许的来源域名 >

允许的其他来源域名列表,比如example.com*.example.com。单个星号*表示允许所有域名。

禁止的来源域名

禁止的来源域名列表,比如example.org*.example.org;除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。

同时检查Origin

如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。

例外URL

如果填写了例外URL,表示这些URL跳过不做处理。

限制URL

如果填写了限制URL,表示只对这些URL进行处理;如果不填则表示支持所有的URL。

` }) Vue.component("server-traffic-limit-status-viewer", { props: ["value"], data: function () { let targetTypeName = "流量" if (this.value != null) { targetTypeName = this.targetTypeToName(this.value.targetType) } return { status: this.value, targetTypeName: targetTypeName } }, methods: { targetTypeToName: function (targetType) { switch (targetType) { case "traffic": return "流量" case "request": return "请求数" case "websocketConnections": return "Websocket连接数" } return "流量" } }, template: ` 已达到套餐当日{{targetTypeName}}限制 已达到套餐当月{{targetTypeName}}限制 已达到套餐总体{{targetTypeName}}限制 ` }) Vue.component("http-redirect-to-https-box", { props: ["v-redirect-to-https-config", "v-is-location"], data: function () { let redirectToHttpsConfig = this.vRedirectToHttpsConfig if (redirectToHttpsConfig == null) { redirectToHttpsConfig = { isPrior: false, isOn: false, host: "", port: 0, status: 0, onlyDomains: [], exceptDomains: [] } } else { if (redirectToHttpsConfig.onlyDomains == null) { redirectToHttpsConfig.onlyDomains = [] } if (redirectToHttpsConfig.exceptDomains == null) { redirectToHttpsConfig.exceptDomains = [] } } return { redirectToHttpsConfig: redirectToHttpsConfig, portString: (redirectToHttpsConfig.port > 0) ? redirectToHttpsConfig.port.toString() : "", moreOptionsVisible: false, statusOptions: [ {"code": 301, "text": "Moved Permanently"}, {"code": 308, "text": "Permanent Redirect"}, {"code": 302, "text": "Found"}, {"code": 303, "text": "See Other"}, {"code": 307, "text": "Temporary Redirect"} ] } }, watch: { "redirectToHttpsConfig.status": function () { this.redirectToHttpsConfig.status = parseInt(this.redirectToHttpsConfig.status) }, portString: function (v) { let port = parseInt(v) if (!isNaN(port)) { this.redirectToHttpsConfig.port = port } else { this.redirectToHttpsConfig.port = 0 } } }, methods: { changeMoreOptions: function (isVisible) { this.moreOptionsVisible = isVisible }, changeOnlyDomains: function (values) { this.redirectToHttpsConfig.onlyDomains = values this.$forceUpdate() }, changeExceptDomains: function (values) { this.redirectToHttpsConfig.exceptDomains = values this.$forceUpdate() } }, template: `
自动跳转到HTTPS

开启后,所有HTTP的请求都会自动跳转到对应的HTTPS URL上,

状态码
域名或IP地址

默认和用户正在访问的域名或IP地址一致。

端口

默认端口为443。

开启后,所有HTTP的请求都会自动跳转到对应的HTTPS URL上,

状态码
跳转后域名或IP地址

默认和用户正在访问的域名或IP地址一致,不填写就表示使用当前的域名。

端口

默认端口为443。

允许的域名

如果填写了允许的域名,那么只有这些域名可以自动跳转。

排除的域名

如果填写了排除的域名,那么这些域名将不跳转。

` }) // 动作选择 Vue.component("http-firewall-actions-box", { props: ["v-actions", "v-firewall-policy", "v-action-configs", "v-group-type"], mounted: function () { let that = this Tea.action("/servers/iplists/levelOptions") .success(function (resp) { that.ipListLevels = resp.data.levels }) .post() this.loadJS(function () { let box = document.getElementById("actions-box") Sortable.create(box, { draggable: ".label", handle: ".icon.handle", onStart: function () { that.cancel() }, onUpdate: function (event) { let labels = box.getElementsByClassName("label") let newConfigs = [] for (let i = 0; i < labels.length; i++) { let index = parseInt(labels[i].getAttribute("data-index")) newConfigs.push(that.configs[index]) } that.configs = newConfigs } }) }) }, data: function () { if (this.vFirewallPolicy.inbound == null) { this.vFirewallPolicy.inbound = {} } if (this.vFirewallPolicy.inbound.groups == null) { this.vFirewallPolicy.inbound.groups = [] } if (this.vFirewallPolicy.outbound == null) { this.vFirewallPolicy.outbound = {} } if (this.vFirewallPolicy.outbound.groups == null) { this.vFirewallPolicy.outbound.groups = [] } let id = 0 let configs = [] if (this.vActionConfigs != null) { configs = this.vActionConfigs configs.forEach(function (v) { v.id = (id++) }) } var defaultPageBody = ` \t403 Forbidden \t

403 Forbidden

Connection: \${remoteAddr} (Client) -> \${serverAddr} (Server)
Request ID: \${requestId}
` return { id: id, actions: this.vActions, configs: configs, isAdding: false, editingIndex: -1, action: null, actionCode: "", actionOptions: {}, // IPList相关 ipListLevels: [], // 动作参数 allowScope: "global", blockTimeout: "", blockTimeoutMax: "", blockScope: "global", captchaLife: "", captchaMaxFails: "", captchaFailBlockTimeout: "", get302Life: "", post307Life: "", recordIPType: "black", recordIPLevel: "critical", recordIPTimeout: "", recordIPListId: 0, recordIPListName: "", tagTags: [], pageUseDefault: true, pageStatus: 403, pageBody: defaultPageBody, defaultPageBody: defaultPageBody, redirectStatus: 307, redirectURL: "", goGroupName: "", goGroupId: 0, goGroup: null, goSetId: 0, goSetName: "", jsCookieLife: "", jsCookieMaxFails: "", jsCookieFailBlockTimeout: "", statusOptions: [ {"code": 301, "text": "Moved Permanently"}, {"code": 308, "text": "Permanent Redirect"}, {"code": 302, "text": "Found"}, {"code": 303, "text": "See Other"}, {"code": 307, "text": "Temporary Redirect"} ] } }, watch: { actionCode: function (code) { this.action = this.actions.$find(function (k, v) { return v.code == code }) this.actionOptions = {} }, allowScope: function (v) { this.actionOptions["scope"] = v }, blockTimeout: function (v) { v = parseInt(v) if (isNaN(v)) { this.actionOptions["timeout"] = 0 } else { this.actionOptions["timeout"] = v } }, blockTimeoutMax: function (v) { v = parseInt(v) if (isNaN(v)) { this.actionOptions["timeoutMax"] = 0 } else { this.actionOptions["timeoutMax"] = v } }, blockScope: function (v) { this.actionOptions["scope"] = v }, captchaLife: function (v) { v = parseInt(v) if (isNaN(v)) { this.actionOptions["life"] = 0 } else { this.actionOptions["life"] = v } }, captchaMaxFails: function (v) { v = parseInt(v) if (isNaN(v)) { this.actionOptions["maxFails"] = 0 } else { this.actionOptions["maxFails"] = v } }, captchaFailBlockTimeout: function (v) { v = parseInt(v) if (isNaN(v)) { this.actionOptions["failBlockTimeout"] = 0 } else { this.actionOptions["failBlockTimeout"] = v } }, get302Life: function (v) { v = parseInt(v) if (isNaN(v)) { this.actionOptions["life"] = 0 } else { this.actionOptions["life"] = v } }, post307Life: function (v) { v = parseInt(v) if (isNaN(v)) { this.actionOptions["life"] = 0 } else { this.actionOptions["life"] = v } }, recordIPType: function (v) { this.recordIPListId = 0 }, recordIPTimeout: function (v) { v = parseInt(v) if (isNaN(v)) { this.actionOptions["timeout"] = 0 } else { this.actionOptions["timeout"] = v } }, goGroupId: function (groupId) { let group = this.vFirewallPolicy.inbound.groups.$find(function (k, v) { return v.id == groupId }) this.goGroup = group if (group == null) { // search outbound groups group = this.vFirewallPolicy.outbound.groups.$find(function (k, v) { return v.id == groupId }) if (group == null) { this.goGroupName = "" } else { this.goGroup = group this.goGroupName = group.name } } else { this.goGroupName = group.name } this.goSetId = 0 this.goSetName = "" }, goSetId: function (setId) { if (this.goGroup == null) { return } let set = this.goGroup.sets.$find(function (k, v) { return v.id == setId }) if (set == null) { this.goSetId = 0 this.goSetName = "" } else { this.goSetName = set.name } }, jsCookieLife: function (v) { v = parseInt(v) if (isNaN(v)) { this.actionOptions["life"] = 0 } else { this.actionOptions["life"] = v } }, jsCookieMaxFails: function (v) { v = parseInt(v) if (isNaN(v)) { this.actionOptions["maxFails"] = 0 } else { this.actionOptions["maxFails"] = v } }, jsCookieFailBlockTimeout: function (v) { v = parseInt(v) if (isNaN(v)) { this.actionOptions["failBlockTimeout"] = 0 } else { this.actionOptions["failBlockTimeout"] = v } }, }, methods: { add: function () { this.action = null this.actionCode = "page" this.isAdding = true this.actionOptions = {} // 动作参数 this.allowScope = "global" this.blockTimeout = "" this.blockTimeoutMax = "" this.blockScope = "global" this.captchaLife = "" this.captchaMaxFails = "" this.captchaFailBlockTimeout = "" this.jsCookieLife = "" this.jsCookieMaxFails = "" this.jsCookieFailBlockTimeout = "" this.get302Life = "" this.post307Life = "" this.recordIPLevel = "critical" this.recordIPType = "black" this.recordIPTimeout = "" this.recordIPListId = 0 this.recordIPListName = "" this.tagTags = [] this.pageUseDefault = true this.pageStatus = 403 this.pageBody = this.defaultPageBody this.redirectStatus = 307 this.redirectURL = "" this.goGroupName = "" this.goGroupId = 0 this.goGroup = null this.goSetId = 0 this.goSetName = "" let that = this this.action = this.vActions.$find(function (k, v) { return v.code == that.actionCode }) // 滚到界面底部 this.scroll() }, remove: function (index) { this.isAdding = false this.editingIndex = -1 this.configs.$remove(index) }, update: function (index, config) { if (this.isAdding && this.editingIndex == index) { this.cancel() return } this.add() this.isAdding = true this.editingIndex = index this.actionCode = config.code this.action = this.actions.$find(function (k, v) { return v.code == config.code }) switch (config.code) { case "block": this.blockTimeout = "" this.blockTimeoutMax = "" if (config.options.timeout != null || config.options.timeout > 0) { this.blockTimeout = config.options.timeout.toString() } if (config.options.timeoutMax != null || config.options.timeoutMax > 0) { this.blockTimeoutMax = config.options.timeoutMax.toString() } if (config.options.scope != null && config.options.scope.length > 0) { this.blockScope = config.options.scope } else { this.blockScope = "global" // 兼容先前版本遗留的默认值 } break case "allow": if (config.options != null && config.options.scope != null && config.options.scope.length > 0) { this.allowScope = config.options.scope } else { this.allowScope = "global" } break case "log": break case "captcha": this.captchaLife = "" if (config.options.life != null || config.options.life > 0) { this.captchaLife = config.options.life.toString() } this.captchaMaxFails = "" if (config.options.maxFails != null || config.options.maxFails > 0) { this.captchaMaxFails = config.options.maxFails.toString() } this.captchaFailBlockTimeout = "" if (config.options.failBlockTimeout != null || config.options.failBlockTimeout > 0) { this.captchaFailBlockTimeout = config.options.failBlockTimeout.toString() } break case "js_cookie": this.jsCookieLife = "" if (config.options.life != null || config.options.life > 0) { this.jsCookieLife = config.options.life.toString() } this.jsCookieMaxFails = "" if (config.options.maxFails != null || config.options.maxFails > 0) { this.jsCookieMaxFails = config.options.maxFails.toString() } this.jsCookieFailBlockTimeout = "" if (config.options.failBlockTimeout != null || config.options.failBlockTimeout > 0) { this.jsCookieFailBlockTimeout = config.options.failBlockTimeout.toString() } break case "notify": break case "get_302": this.get302Life = "" if (config.options.life != null || config.options.life > 0) { this.get302Life = config.options.life.toString() } break case "post_307": this.post307Life = "" if (config.options.life != null || config.options.life > 0) { this.post307Life = config.options.life.toString() } break; case "record_ip": if (config.options != null) { this.recordIPLevel = config.options.level this.recordIPType = config.options.type if (config.options.timeout > 0) { this.recordIPTimeout = config.options.timeout.toString() } let that = this // VUE需要在函数执行完之后才会调用watch函数,这样会导致设置的值被覆盖,所以这里使用setTimeout setTimeout(function () { that.recordIPListId = config.options.ipListId that.recordIPListName = config.options.ipListName }) } break case "tag": this.tagTags = [] if (config.options.tags != null) { this.tagTags = config.options.tags } break case "page": this.pageUseDefault = true this.pageStatus = 403 this.pageBody = this.defaultPageBody if (typeof config.options.useDefault === "boolean") { this.pageUseDefault = config.options.useDefault } else { this.pageUseDefault = false } if (config.options.status != null) { this.pageStatus = config.options.status } if (config.options.body != null) { this.pageBody = config.options.body } break case "redirect": this.redirectStatus = 307 this.redirectURL = "" if (config.options.status != null) { this.redirectStatus = config.options.status } if (config.options.url != null) { this.redirectURL = config.options.url } break case "go_group": if (config.options != null) { this.goGroupName = config.options.groupName this.goGroupId = config.options.groupId this.goGroup = this.vFirewallPolicy.inbound.groups.$find(function (k, v) { return v.id == config.options.groupId }) } break case "go_set": if (config.options != null) { this.goGroupName = config.options.groupName this.goGroupId = config.options.groupId this.goGroup = this.vFirewallPolicy.inbound.groups.$find(function (k, v) { return v.id == config.options.groupId }) // VUE需要在函数执行完之后才会调用watch函数,这样会导致设置的值被覆盖,所以这里使用setTimeout let that = this setTimeout(function () { that.goSetId = config.options.setId if (that.goGroup != null) { let set = that.goGroup.sets.$find(function (k, v) { return v.id == config.options.setId }) if (set != null) { that.goSetName = set.name } } }) } break } // 滚到界面底部 this.scroll() }, cancel: function () { this.isAdding = false this.editingIndex = -1 }, confirm: function () { if (this.action == null) { return } if (this.actionOptions == null) { this.actionOptions = {} } // record_ip if (this.actionCode == "record_ip") { let timeout = parseInt(this.recordIPTimeout) if (isNaN(timeout)) { timeout = 0 } if (this.recordIPListId < 0) { return } // recordIPListId can be 0 this.actionOptions = { type: this.recordIPType, level: this.recordIPLevel, timeout: timeout, ipListId: this.recordIPListId, ipListName: this.recordIPListName } } else if (this.actionCode == "tag") { // tag if (this.tagTags == null || this.tagTags.length == 0) { return } this.actionOptions = { tags: this.tagTags } } else if (this.actionCode == "page") { let pageStatus = this.pageStatus.toString() if (!pageStatus.match(/^\d{3}$/)) { pageStatus = 403 } else { pageStatus = parseInt(pageStatus) } this.actionOptions = { useDefault: this.pageUseDefault, status: pageStatus, body: this.pageBody } } else if (this.actionCode == "redirect") { let redirectStatus = this.redirectStatus.toString() if (!redirectStatus.match(/^\d{3}$/)) { redirectStatus = 307 } else { redirectStatus = parseInt(redirectStatus) } if (this.redirectURL.length == 0) { teaweb.warn("请输入跳转到URL") return } this.actionOptions = { status: redirectStatus, url: this.redirectURL } } else if (this.actionCode == "go_group") { // go_group let groupId = this.goGroupId if (typeof (groupId) == "string") { groupId = parseInt(groupId) if (isNaN(groupId)) { groupId = 0 } } if (groupId <= 0) { return } this.actionOptions = { groupId: groupId.toString(), groupName: this.goGroupName } } else if (this.actionCode == "go_set") { // go_set let groupId = this.goGroupId if (typeof (groupId) == "string") { groupId = parseInt(groupId) if (isNaN(groupId)) { groupId = 0 } } let setId = this.goSetId if (typeof (setId) == "string") { setId = parseInt(setId) if (isNaN(setId)) { setId = 0 } } if (setId <= 0) { return } this.actionOptions = { groupId: groupId.toString(), groupName: this.goGroupName, setId: setId.toString(), setName: this.goSetName } } let options = {} for (let k in this.actionOptions) { if (this.actionOptions.hasOwnProperty(k)) { options[k] = this.actionOptions[k] } } if (this.editingIndex > -1) { this.configs[this.editingIndex] = { id: this.configs[this.editingIndex].id, code: this.actionCode, name: this.action.name, options: options } } else { this.configs.push({ id: (this.id++), code: this.actionCode, name: this.action.name, options: options }) } this.cancel() }, removeRecordIPList: function () { this.recordIPListId = 0 }, selectRecordIPList: function () { let that = this teaweb.popup("/servers/iplists/selectPopup?type=" + this.recordIPType, { width: "50em", height: "30em", callback: function (resp) { that.recordIPListId = resp.data.list.id that.recordIPListName = resp.data.list.name } }) }, changeTags: function (tags) { this.tagTags = tags }, loadJS: function (callback) { if (typeof Sortable != "undefined") { callback() return } // 引入js let jsFile = document.createElement("script") jsFile.setAttribute("src", "/js/sortable.min.js") jsFile.addEventListener("load", function () { callback() }) document.head.appendChild(jsFile) }, scroll: function () { setTimeout(function () { let mainDiv = document.getElementsByClassName("main") if (mainDiv.length > 0) { mainDiv[0].scrollTo(0, 1000) } }, 10) } }, template: `
{{config.name}} ({{config.code.toUpperCase()}}) [分组] [网站] [网站和策略] :封禁时长{{config.options.timeout}}-{{config.options.timeoutMax}} :有效期{{config.options.life}}秒 / 最多失败{{config.options.maxFails}}次 :有效期{{config.options.life}}秒 / 最多失败{{config.options.maxFails}}次 :有效期{{config.options.life}}秒 :有效期{{config.options.life}}秒 {{config.options.ipListName}} :{{config.options.tags.join(", ")}} :[{{config.options.status}}]  [默认页面] :{{config.options.url}} :{{config.options.groupName}} :{{config.options.groupName}} / {{config.options.setName}}   [所有网站] [当前网站]      
动作类型 *

{{action.description}}

有效范围

跳过当前分组其他规则集,继续执行其他分组的规则集。

跳过当前网站所有的规则集。

跳过当前网站和网站对应WAF策略所有的规则集。

封禁范围

只封禁用户对当前网站的访问,其他服务不受影响。

封禁用户对所有网站的访问。

封禁时长
最大封禁时长

选填项。如果同时填写了封禁时长和最大封禁时长,则会在两者之间随机选择一个数字作为最终的封禁时长。

有效时间

验证通过后在这个时间内不再验证;如果为空或者为0表示默认。

最多失败次数

建议填入一个不小于5的数字,以减少误判几率。允许用户失败尝试的最多次数,超过这个次数将被自动加入黑名单;如果为空或者为0表示默认。

失败拦截时间

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

有效时间

验证通过后在这个时间内不再验证;如果为空或者为0表示默认。

最多失败次数

允许用户失败尝试的最多次数,超过这个次数将被自动加入黑名单;如果为空或者为0表示默认。

失败拦截时间

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

有效时间

验证通过后在这个时间内不再验证。

有效时间

验证通过后在这个时间内不再验证。

IP名单类型 *
选择IP名单
{{recordIPListName}}

如不选择,则自动添加到当前策略的IP名单中。

级别
超时时间

0表示不超时。

标签 *
使用默认提示
状态码 *
网页内容
状态码 *
跳转到URL
下一个分组 *
下一个分组 *
下一个规则集 *
 

系统总是会先执行记录日志、标签等不会修改请求的动作,再执行阻止、验证码等可能改变请求的动作。

` }) // 认证设置 Vue.component("http-auth-config-box", { props: ["v-auth-config", "v-is-location"], data: function () { let authConfig = this.vAuthConfig if (authConfig == null) { authConfig = { isPrior: false, isOn: false } } if (authConfig.policyRefs == null) { authConfig.policyRefs = [] } return { authConfig: authConfig } }, methods: { isOn: function () { return (!this.vIsLocation || this.authConfig.isPrior) && this.authConfig.isOn }, add: function () { let that = this teaweb.popup("/servers/server/settings/access/createPopup", { callback: function (resp) { that.authConfig.policyRefs.push(resp.data.policyRef) that.change() }, height: "28em" }) }, update: function (index, policyId) { let that = this teaweb.popup("/servers/server/settings/access/updatePopup?policyId=" + policyId, { callback: function (resp) { teaweb.success("保存成功", function () { teaweb.reload() }) }, height: "28em" }) }, remove: function (index) { this.authConfig.policyRefs.$remove(index) this.change() }, methodName: function (methodType) { switch (methodType) { case "basicAuth": return "BasicAuth" case "subRequest": return "子请求" case "typeA": return "URL鉴权A" case "typeB": return "URL鉴权B" case "typeC": return "URL鉴权C" case "typeD": return "URL鉴权D" } return "" }, change: function () { let that = this setTimeout(function () { // 延时通知,是为了让表单有机会变更数据 that.$emit("change", this.authConfig) }, 100) } }, template: `
启用鉴权

鉴权方式

名称 鉴权方法 参数 状态 操作
{{ref.authPolicy.name}} {{methodName(ref.authPolicy.type)}} {{ref.authPolicy.params.users.length}}个用户 [{{ref.authPolicy.params.method}}] {{ref.authPolicy.params.url}} {{ref.authPolicy.params.signParamName}}/有效期{{ref.authPolicy.params.life}}秒 有效期{{ref.authPolicy.params.life}}秒 有效期{{ref.authPolicy.params.life}}秒 {{ref.authPolicy.params.signParamName}}/{{ref.authPolicy.params.timestampParamName}}/有效期{{ref.authPolicy.params.life}}秒
扩展名:{{ext}} 域名:{{domain}}
修改   删除
` }) Vue.component("user-selector", { props: ["v-user-id", "data-url"], data: function () { let userId = this.vUserId if (userId == null) { userId = 0 } let dataURL = this.dataUrl if (dataURL == null || dataURL.length == 0) { dataURL = "/servers/users/options" } return { users: [], userId: userId, dataURL: dataURL } }, methods: { change: function(item) { if (item != null) { this.$emit("change", item.id) } else { this.$emit("change", 0) } }, clear: function () { this.$refs.comboBox.clear() } }, template: `
` }) 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 = "response" let hash = window.location.hash if (hash == "#request") { type = "request" } // ref let requestHeaderRef = this.vRequestHeaderRef if (requestHeaderRef == null) { requestHeaderRef = { isPrior: false, isOn: true, headerPolicyId: 0 } } let responseHeaderRef = this.vResponseHeaderRef if (responseHeaderRef == null) { responseHeaderRef = { isPrior: false, isOn: true, headerPolicyId: 0 } } // 请求相关 let requestSettingHeaders = [] let requestDeletingHeaders = [] let requestNonStandardHeaders = [] let requestPolicy = this.vRequestHeaderPolicy if (requestPolicy != null) { if (requestPolicy.setHeaders != null) { requestSettingHeaders = requestPolicy.setHeaders } if (requestPolicy.deleteHeaders != null) { requestDeletingHeaders = requestPolicy.deleteHeaders } if (requestPolicy.nonStandardHeaders != null) { requestNonStandardHeaders = requestPolicy.nonStandardHeaders } } // 响应相关 let responseSettingHeaders = [] let responseDeletingHeaders = [] let responseNonStandardHeaders = [] let responsePolicy = this.vResponseHeaderPolicy if (responsePolicy != null) { if (responsePolicy.setHeaders != null) { responseSettingHeaders = responsePolicy.setHeaders } if (responsePolicy.deleteHeaders != null) { responseDeletingHeaders = responsePolicy.deleteHeaders } if (responsePolicy.nonStandardHeaders != null) { responseNonStandardHeaders = responsePolicy.nonStandardHeaders } } let responseCORS = { isOn: false } if (responsePolicy.cors != null) { responseCORS = responsePolicy.cors } return { type: type, typeName: (type == "request") ? "请求" : "响应", requestHeaderRef: requestHeaderRef, responseHeaderRef: responseHeaderRef, requestSettingHeaders: requestSettingHeaders, requestDeletingHeaders: requestDeletingHeaders, requestNonStandardHeaders: requestNonStandardHeaders, responseSettingHeaders: responseSettingHeaders, responseDeletingHeaders: responseDeletingHeaders, responseNonStandardHeaders: responseNonStandardHeaders, responseCORS: responseCORS } }, methods: { selectType: function (type) { this.type = type window.location.hash = "#" + type window.location.reload() }, addSettingHeader: function (policyId) { teaweb.popup("/servers/server/settings/headers/createSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + this.type, { height: "22em", callback: function () { teaweb.successRefresh("保存成功") } }) }, addDeletingHeader: function (policyId, type) { teaweb.popup("/servers/server/settings/headers/createDeletePopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + type, { callback: function () { teaweb.successRefresh("保存成功") } }) }, addNonStandardHeader: function (policyId, type) { teaweb.popup("/servers/server/settings/headers/createNonStandardPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + type, { callback: function () { teaweb.successRefresh("保存成功") } }) }, updateSettingPopup: function (policyId, headerId) { teaweb.popup("/servers/server/settings/headers/updateSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&headerId=" + headerId + "&type=" + this.type, { height: "22em", callback: function () { teaweb.successRefresh("保存成功") } }) }, deleteDeletingHeader: function (policyId, headerName) { teaweb.confirm("确定要删除'" + headerName + "'吗?", function () { Tea.action("/servers/server/settings/headers/deleteDeletingHeader") .params({ headerPolicyId: policyId, headerName: headerName }) .post() .refresh() }) }, deleteNonStandardHeader: function (policyId, headerName) { teaweb.confirm("确定要删除'" + headerName + "'吗?", function () { Tea.action("/servers/server/settings/headers/deleteNonStandardHeader") .params({ headerPolicyId: policyId, headerName: headerName }) .post() .refresh() }) }, deleteHeader: function (policyId, type, headerId) { teaweb.confirm("确定要删除此报头吗?", function () { this.$post("/servers/server/settings/headers/delete") .params({ headerPolicyId: policyId, type: type, headerId: headerId }) .refresh() } ) }, updateCORS: function (policyId) { teaweb.popup("/servers/server/settings/headers/updateCORSPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + this.type, { height: "30em", callback: function () { teaweb.successRefresh("保存成功") } }) } }, template: `
由于已经在当前网站分组中进行了对应的配置,在这里的配置将不会生效。

设置请求报头   [添加新报头]

暂时还没有自定义报头。

名称 操作
{{header.name}}
{{code}} {{method}} {{domain}} 附加 跳转禁用 替换
{{header.value}} 修改   删除

其他设置

删除报头
{{headerName}}
非标报头
{{headerName}}
由于已经在当前网站分组中进行了对应的配置,在这里的配置将不会生效。

设置响应报头   [添加新报头]

将会覆盖已有的同名报头。

暂时还没有自定义报头。

名称 操作
{{header.name}}
{{code}} {{method}} {{domain}} 附加 跳转禁用 替换
建议使用当前页面下方的"CORS自适应跨域"功能代替Access-Control-*-*相关报头。
{{header.value}} 修改   删除

其他设置

删除报头
{{headerName}}  
非标报头
{{headerName}}  
CORS自适应跨域 已启用未启用   [修改]

启用后,服务器可以服务器会自动生成Access-Control-*-*相关的报头。

` }) // 通用设置 Vue.component("http-common-config-box", { props: ["v-common-config"], data: function () { let config = this.vCommonConfig if (config == null) { config = { mergeSlashes: false } } return { config: config } }, template: `
合并重复的路径分隔符

合并URL中重复的路径分隔符为一个,比如//hello/world中的//

` }) Vue.component("http-cache-policy-selector", { props: ["v-cache-policy"], mounted: function () { let that = this Tea.action("/servers/components/cache/count") .post() .success(function (resp) { that.count = resp.data.count }) }, data: function () { let cachePolicy = this.vCachePolicy return { count: 0, cachePolicy: cachePolicy } }, methods: { remove: function () { this.cachePolicy = null }, select: function () { let that = this teaweb.popup("/servers/components/cache/selectPopup", { width: "42em", height: "26em", callback: function (resp) { that.cachePolicy = resp.data.cachePolicy } }) }, create: function () { let that = this teaweb.popup("/servers/components/cache/createPopup", { height: "26em", callback: function (resp) { that.cachePolicy = resp.data.cachePolicy } }) } }, template: `
{{cachePolicy.name}}    
[选择已有策略]     [创建新策略]
` }) Vue.component("http-pages-and-shutdown-box", { props: ["v-enable-global-pages", "v-pages", "v-shutdown-config", "v-is-location"], data: function () { let pages = [] if (this.vPages != null) { pages = this.vPages } let shutdownConfig = { isPrior: false, isOn: false, bodyType: "html", url: "", body: "", status: 0 } if (this.vShutdownConfig != null) { if (this.vShutdownConfig.body == null) { this.vShutdownConfig.body = "" } if (this.vShutdownConfig.bodyType == null) { this.vShutdownConfig.bodyType = "html" } shutdownConfig = this.vShutdownConfig } let shutdownStatus = "" if (shutdownConfig.status > 0) { shutdownStatus = shutdownConfig.status.toString() } return { pages: pages, shutdownConfig: shutdownConfig, shutdownStatus: shutdownStatus, enableGlobalPages: this.vEnableGlobalPages } }, watch: { shutdownStatus: function (status) { let statusInt = parseInt(status) if (!isNaN(statusInt) && statusInt > 0 && statusInt < 1000) { this.shutdownConfig.status = statusInt } else { this.shutdownConfig.status = 0 } } }, methods: { addPage: function () { let that = this teaweb.popup("/servers/server/settings/pages/createPopup", { height: "30em", callback: function (resp) { that.pages.push(resp.data.page) that.notifyChange() } }) }, updatePage: function (pageIndex, pageId) { let that = this teaweb.popup("/servers/server/settings/pages/updatePopup?pageId=" + pageId, { height: "30em", callback: function (resp) { Vue.set(that.pages, pageIndex, resp.data.page) that.notifyChange() } }) }, removePage: function (pageIndex) { let that = this teaweb.confirm("确定要删除此自定义页面吗?", function () { that.pages.$remove(pageIndex) that.notifyChange() }) }, addShutdownHTMLTemplate: function () { this.shutdownConfig.body = ` \t升级中 \t \t

网站升级中

为了给您提供更好的服务,我们正在升级网站,请稍后重新访问。

Connection: \${remoteAddr} (Client) -> \${serverAddr} (Server)
Request ID: \${requestId}
` }, notifyChange: function () { let parent = this.$el.parentNode while (true) { if (parent == null) { break } if (parent.tagName == "FORM") { break } parent = parent.parentNode } if (parent != null) { setTimeout(function () { Tea.runActionOn(parent) }, 100) } } }, template: `

自定义页面

根据响应状态码返回一些自定义页面,比如404,500等错误页面。

响应状态码 页面类型 新状态码 例外URL 限制URL 操作
{{page.status[0]}} {{page.status}}
{{page.url}}
读取URL
{{page.url}}
跳转URL {{page.newStatus}}
[HTML内容]
{{page.newStatus}}
{{page.newStatus}} 保持
{{urlPattern.pattern}}
-
{{urlPattern.pattern}}
-
修改   删除

临时关闭页面

开启临时关闭页面时,所有请求都会直接显示此页面。可用于临时升级网站或者禁止用户访问某个网页。

启用临时关闭网站

选中后,表示临时关闭当前网站,并显示自定义内容。

显示内容类型 *
显示页面URL *

将从此URL中读取内容。

跳转到URL *

将会跳转到此URL。

显示页面HTML *

[使用模板]。填写页面的HTML内容,支持请求变量。

状态码

其他设置

启用系统自定义页面

选中后,表示如果当前网站没有自定义页面,则尝试使用系统对应的自定义页面。

` }) Vue.component("http-firewall-page-options", { props: ["v-page-options"], data: function () { var defaultPageBody = ` 403 Forbidden

403 Forbidden By WAF

Connection: \${remoteAddr} (Client) -> \${serverAddr} (Server)
Request ID: \${requestId}
` return { pageOptions: this.vPageOptions, status: this.vPageOptions.status, body: this.vPageOptions.body, defaultPageBody: defaultPageBody, isEditing: false } }, watch: { status: function (v) { if (typeof v === "string" && v.length != 3) { return } let statusCode = parseInt(v) if (isNaN(statusCode)) { this.pageOptions.status = 403 } else { this.pageOptions.status = statusCode } }, body: function (v) { this.pageOptions.body = v } }, methods: { edit: function () { this.isEditing = !this.isEditing } }, template: `
状态码:{{status}} / 提示内容:[{{pageOptions.body.length}}字符][无]
状态码 *
网页内容

[使用模板]

` }) Vue.component("http-firewall-js-cookie-options", { props: ["v-js-cookie-options"], mounted: function () { this.updateSummary() }, data: function () { let options = this.vJsCookieOptions if (options == null) { options = { life: 0, maxFails: 0, failBlockTimeout: 0, failBlockScopeAll: false, scope: "service" } } return { options: options, isEditing: false, summary: "" } }, watch: { "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() } }, 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 (summaryList.length == 0) { this.summary = "默认配置" } else { this.summary = summaryList.join(" / ") } }, confirm: function () { this.isEditing = false } }, template: `
{{summary}}
有效时间

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

最多失败次数

建议填入一个不小于5的数字,以减少误判几率。允许用户失败尝试的最多次数,超过这个次数将被自动加入黑名单。如果为空或者为0,表示不限制。

失败拦截时间

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

失败全局封禁

选中后,表示允许系统尝试全局封禁某个IP,以提升封禁性能。

` }) // 压缩配置 Vue.component("http-compression-config-box", { props: ["v-compression-config", "v-is-location", "v-is-group"], mounted: function () { let that = this sortLoad(function () { that.initSortableTypes() }) }, data: function () { let config = this.vCompressionConfig if (config == null) { config = { isPrior: false, isOn: false, useDefaultTypes: true, types: ["brotli", "gzip", "zstd", "deflate"], level: 0, decompressData: false, gzipRef: null, deflateRef: null, brotliRef: null, minLength: {count: 1, "unit": "kb"}, maxLength: {count: 32, "unit": "mb"}, mimeTypes: ["text/*", "application/javascript", "application/json", "application/atom+xml", "application/rss+xml", "application/xhtml+xml", "font/*", "image/svg+xml"], extensions: [".js", ".json", ".html", ".htm", ".xml", ".css", ".woff2", ".txt"], exceptExtensions: [".apk", ".ipa"], conds: null, enablePartialContent: false, onlyURLPatterns: [], exceptURLPatterns: [] } } if (config.types == null) { config.types = [] } if (config.mimeTypes == null) { config.mimeTypes = [] } if (config.extensions == null) { config.extensions = [] } let allTypes = [ { name: "Gzip", code: "gzip", isOn: true }, { name: "Deflate", code: "deflate", isOn: true }, { name: "Brotli", code: "brotli", isOn: true }, { name: "ZSTD", code: "zstd", isOn: true } ] let configTypes = [] config.types.forEach(function (typeCode) { allTypes.forEach(function (t) { if (typeCode == t.code) { t.isOn = true configTypes.push(t) } }) }) allTypes.forEach(function (t) { if (!config.types.$contains(t.code)) { t.isOn = false configTypes.push(t) } }) return { config: config, moreOptionsVisible: false, allTypes: configTypes } }, methods: { isOn: function () { return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn }, changeExtensions: function (values) { values.forEach(function (v, k) { if (v.length > 0 && v[0] != ".") { values[k] = "." + v } }) this.config.extensions = values }, changeExceptExtensions: function (values) { values.forEach(function (v, k) { if (v.length > 0 && v[0] != ".") { values[k] = "." + v } }) this.config.exceptExtensions = values }, changeMimeTypes: function (values) { this.config.mimeTypes = values }, changeAdvancedVisible: function () { this.moreOptionsVisible = !this.moreOptionsVisible }, changeConds: function (conds) { this.config.conds = conds }, changeType: function () { this.config.types = [] let that = this this.allTypes.forEach(function (v) { if (v.isOn) { that.config.types.push(v.code) } }) }, initSortableTypes: function () { let box = document.querySelector("#compression-types-box") let that = this Sortable.create(box, { draggable: ".checkbox", handle: ".icon.handle", onStart: function () { }, onUpdate: function (event) { let checkboxes = box.querySelectorAll(".checkbox") let codes = [] checkboxes.forEach(function (checkbox) { let code = checkbox.getAttribute("data-code") codes.push(code) }) that.config.types = codes } }) } }, template: `
启用内容压缩
支持的扩展名

含有这些扩展名的URL将会被压缩,不区分大小写。

例外扩展名

含有这些扩展名的URL将不会被压缩,不区分大小写。

支持的MimeType

响应的Content-Type里包含这些MimeType的内容将会被压缩。

压缩算法

选择支持的压缩算法和优先顺序,拖动图表排序。

支持已压缩内容

支持对已压缩内容尝试重新使用新的算法压缩;不选中表示保留当前的压缩格式。

内容最小长度

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

内容最大长度

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

支持Partial
Content

支持对分片内容(PartialContent)的压缩;除非客户端有特殊要求,一般不需要启用。

例外URL

如果填写了例外URL,表示这些URL跳过不做处理。

限制URL

如果填写了限制URL,表示只对这些URL进行压缩处理;如果不填则表示支持所有的URL。

匹配条件
` }) // HTTP CC防护配置 Vue.component("http-cc-config-box", { props: ["v-cc-config", "v-is-location", "v-is-group"], data: function () { let config = this.vCcConfig if (config == null) { config = { isPrior: false, isOn: false, enableFingerprint: true, enableGET302: true, onlyURLPatterns: [], exceptURLPatterns: [], useDefaultThresholds: true, ignoreCommonFiles: true } } if (config.thresholds == null || config.thresholds.length == 0) { config.thresholds = [ { maxRequests: 0 }, { maxRequests: 0 }, { maxRequests: 0 } ] } if (typeof config.enableFingerprint != "boolean") { config.enableFingerprint = true } if (typeof config.enableGET302 != "boolean") { config.enableGET302 = true } if (config.onlyURLPatterns == null) { config.onlyURLPatterns = [] } if (config.exceptURLPatterns == null) { config.exceptURLPatterns = [] } return { config: config, moreOptionsVisible: false, minQPSPerIP: config.minQPSPerIP, useCustomThresholds: !config.useDefaultThresholds, thresholdMaxRequests0: this.maxRequestsStringAtThresholdIndex(config, 0), thresholdMaxRequests1: this.maxRequestsStringAtThresholdIndex(config, 1), thresholdMaxRequests2: this.maxRequestsStringAtThresholdIndex(config, 2) } }, watch: { minQPSPerIP: function (v) { let qps = parseInt(v.toString()) if (isNaN(qps) || qps < 0) { qps = 0 } this.config.minQPSPerIP = qps }, thresholdMaxRequests0: function (v) { this.setThresholdMaxRequests(0, v) }, thresholdMaxRequests1: function (v) { this.setThresholdMaxRequests(1, v) }, thresholdMaxRequests2: function (v) { this.setThresholdMaxRequests(2, v) }, useCustomThresholds: function (b) { this.config.useDefaultThresholds = !b } }, methods: { maxRequestsStringAtThresholdIndex: function (config, index) { if (config.thresholds == null) { return "" } if (index < config.thresholds.length) { let s = config.thresholds[index].maxRequests.toString() if (s == "0") { s = "" } return s } return "" }, setThresholdMaxRequests: function (index, v) { let maxRequests = parseInt(v) if (isNaN(maxRequests) || maxRequests < 0) { maxRequests = 0 } if (index < this.config.thresholds.length) { this.config.thresholds[index].maxRequests = maxRequests } }, showMoreOptions: function () { this.moreOptionsVisible = !this.moreOptionsVisible } }, template: `
启用CC无感防护

启用后,自动检测并拦截CC攻击。

例外URL

如果填写了例外URL,表示这些URL跳过CC防护不做处理。

限制URL

如果填写了限制URL,表示只对这些URL进行CC防护处理;如果不填则表示支持所有的URL。

忽略常用文件

忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。

检查请求来源指纹

在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。

启用GET302校验

选中后,表示自动通过GET302方法来校验客户端。

单IP最低QPS
请求数/秒

当某个IP在1分钟内平均QPS达到此值时,才会开始检测;如果设置为0,表示任何访问都会检测。(注意这里设置的是检测开启阈值,不是拦截阈值,拦截阈值在当前表单下方可以设置)

使用自定义拦截阈值
自定义拦截阈值设置
单IP每5秒最多 请求
单IP每60秒 请求
单IP每300秒 请求
` }) Vue.component("firewall-event-level-options", { props: ["v-value"], mounted: function () { let that = this Tea.action("/ui/eventLevelOptions") .post() .success(function (resp) { that.levels = resp.data.eventLevels that.change() }) }, data: function () { let value = this.vValue if (value == null || value.length == 0) { value = "" // 不要给默认值,因为黑白名单等默认值均有不同 } return { levels: [], description: "", level: value } }, methods: { change: function () { this.$emit("change") let that = this let l = this.levels.$find(function (k, v) { return v.code == that.level }) if (l != null) { this.description = l.description } else { this.description = "" } } }, template: `

{{description}}

` }) Vue.component("prior-checkbox", { props: ["v-config", "description"], data: function () { let description = this.description if (description == null) { description = "打开后可以覆盖父级或子级配置" } return { isPrior: this.vConfig.isPrior, realDescription: description } }, watch: { isPrior: function (v) { this.vConfig.isPrior = v } }, template: ` 打开独立配置

[已打开] {{realDescription}}。

` }) Vue.component("http-charsets-box", { props: ["v-usual-charsets", "v-all-charsets", "v-charset-config", "v-is-location", "v-is-group"], data: function () { let charsetConfig = this.vCharsetConfig if (charsetConfig == null) { charsetConfig = { isPrior: false, isOn: false, charset: "", isUpper: false, force: false } } return { charsetConfig: charsetConfig, advancedVisible: false } }, methods: { changeAdvancedVisible: function (v) { this.advancedVisible = v } }, template: `
启用字符编码
选择字符编码
强制替换

选中后,表示强制覆盖已经设置的字符集;不选中,表示如果源站已经设置了字符集,则保留不修改。

字符编码大写

选中后将指定的字符编码转换为大写,比如默认为utf-8,选中后将改为UTF-8

` }) 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: `
启用

启用后,将会在响应的Header中添加Expires字段,浏览器据此会将内容缓存在客户端;同时,在管理后台执行清理缓存时,也将无法清理客户端已有的缓存。

覆盖源站设置

选中后,会覆盖源站Header中已有的Expires字段。

自动计算时间

根据已设置的缓存有效期进行计算。

强制缓存时间

从客户端访问的时间开始要缓存的时长。

` }) Vue.component("http-access-log-box", { props: ["v-access-log", "v-keyword", "v-show-server-link"], data: function () { let accessLog = this.vAccessLog if (accessLog.header != null && accessLog.header.Upgrade != null && accessLog.header.Upgrade.values != null && accessLog.header.Upgrade.values.$contains("websocket")) { if (accessLog.scheme == "http") { accessLog.scheme = "ws" } else if (accessLog.scheme == "https") { accessLog.scheme = "wss" } } // 对TAG去重 if (accessLog.tags != null && accessLog.tags.length > 0) { let tagMap = {} accessLog.tags = accessLog.tags.$filter(function (k, tag) { let b = (typeof (tagMap[tag]) == "undefined") tagMap[tag] = true return b }) } // 域名 accessLog.unicodeHost = "" if (accessLog.host != null && accessLog.host.startsWith("xn--")) { // port let portIndex = accessLog.host.indexOf(":") if (portIndex > 0) { accessLog.unicodeHost = punycode.ToUnicode(accessLog.host.substring(0, portIndex)) } else { accessLog.unicodeHost = punycode.ToUnicode(accessLog.host) } } return { accessLog: accessLog } }, methods: { formatCost: function (seconds) { if (seconds == null) { return "0" } let s = (seconds * 1000).toString(); let pieces = s.split("."); if (pieces.length < 2) { return s; } return pieces[0] + "." + pieces[1].substring(0, 3); }, showLog: function () { let that = this let requestId = this.accessLog.requestId this.$parent.$children.forEach(function (v) { if (v.deselect != null) { v.deselect() } }) this.select() teaweb.popup("/servers/server/log/viewPopup?requestId=" + requestId, { width: "50em", height: "28em", onClose: function () { that.deselect() } }) }, select: function () { this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)" }, deselect: function () { this.$refs.box.parentNode.style.cssText = "" }, mismatch: function () { teaweb.warn("当前访问没有匹配到任何网站") } }, template: `
[{{accessLog.node.name}}节点] [网站] [网站] [{{accessLog.region}}] {{accessLog.remoteAddr}} [{{accessLog.timeLocal}}] "{{accessLog.requestMethod}} {{accessLog.scheme}}://{{accessLog.host}}{{accessLog.requestURI}} {{accessLog.proto}}" {{accessLog.status}} {{accessLog.unicodeHost}} cache {{accessLog.attrs['cache.status'].toLowerCase()}} waf {{accessLog.firewallActions}} - {{tag}} WAF - {{accessLog.wafInfo.group.name}} - {{accessLog.wafInfo.set.name}} - 耗时:{{formatCost(accessLog.requestTime)}} ms   ({{accessLog.humanTime}})  
` }) // Javascript Punycode converter derived from example in RFC3492. // This implementation is created by some@domain.name and released into public domain // 代码来自:https://stackoverflow.com/questions/183485/converting-punycode-with-dash-character-to-unicode var punycode = new function Punycode() { // This object converts to and from puny-code used in IDN // // punycode.ToASCII ( domain ) // // Returns a puny coded representation of "domain". // It only converts the part of the domain name that // has non ASCII characters. I.e. it dosent matter if // you call it with a domain that already is in ASCII. // // punycode.ToUnicode (domain) // // Converts a puny-coded domain name to unicode. // It only converts the puny-coded parts of the domain name. // I.e. it dosent matter if you call it on a string // that already has been converted to unicode. // // this.utf16 = { // The utf16-class is necessary to convert from javascripts internal character representation to unicode and back. decode: function (input) { var output = [], i = 0, len = input.length, value, extra; while (i < len) { value = input.charCodeAt(i++); if ((value & 0xF800) === 0xD800) { extra = input.charCodeAt(i++); if (((value & 0xFC00) !== 0xD800) || ((extra & 0xFC00) !== 0xDC00)) { throw new RangeError("UTF-16(decode): Illegal UTF-16 sequence"); } value = ((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000; } output.push(value); } return output; }, encode: function (input) { var output = [], i = 0, len = input.length, value; while (i < len) { value = input[i++]; if ((value & 0xF800) === 0xD800) { throw new RangeError("UTF-16(encode): Illegal UTF-16 value"); } if (value > 0xFFFF) { value -= 0x10000; output.push(String.fromCharCode(((value >>> 10) & 0x3FF) | 0xD800)); value = 0xDC00 | (value & 0x3FF); } output.push(String.fromCharCode(value)); } return output.join(""); } } //Default parameters var initial_n = 0x80; var initial_bias = 72; var delimiter = "\x2D"; var base = 36; var damp = 700; var tmin = 1; var tmax = 26; var skew = 38; var maxint = 0x7FFFFFFF; // decode_digit(cp) returns the numeric value of a basic code // point (for use in representing integers) in the range 0 to // base-1, or base if cp is does not represent a value. function decode_digit(cp) { return cp - 48 < 10 ? cp - 22 : cp - 65 < 26 ? cp - 65 : cp - 97 < 26 ? cp - 97 : base; } // encode_digit(d,flag) returns the basic code point whose value // (when used for representing integers) is d, which needs to be in // the range 0 to base-1. The lowercase form is used unless flag is // nonzero, in which case the uppercase form is used. The behavior // is undefined if flag is nonzero and digit d has no uppercase form. function encode_digit(d, flag) { return d + 22 + 75 * (d < 26) - ((flag != 0) << 5); // 0..25 map to ASCII a..z or A..Z // 26..35 map to ASCII 0..9 } //** Bias adaptation function ** function adapt(delta, numpoints, firsttime) { var k; delta = firsttime ? Math.floor(delta / damp) : (delta >> 1); delta += Math.floor(delta / numpoints); for (k = 0; delta > (((base - tmin) * tmax) >> 1); k += base) { delta = Math.floor(delta / (base - tmin)); } return Math.floor(k + (base - tmin + 1) * delta / (delta + skew)); } // encode_basic(bcp,flag) forces a basic code point to lowercase if flag is zero, // uppercase if flag is nonzero, and returns the resulting code point. // The code point is unchanged if it is caseless. // The behavior is undefined if bcp is not a basic code point. function encode_basic(bcp, flag) { bcp -= (bcp - 97 < 26) << 5; return bcp + ((!flag && (bcp - 65 < 26)) << 5); } // Main decode this.decode = function (input, preserveCase) { // Dont use utf16 var output = []; var case_flags = []; var input_length = input.length; var n, out, i, bias, basic, j, ic, oldi, w, k, digit, t, len; // Initialize the state: n = initial_n; i = 0; bias = initial_bias; // Handle the basic code points: Let basic be the number of input code // points before the last delimiter, or 0 if there is none, then // copy the first basic code points to the output. basic = input.lastIndexOf(delimiter); if (basic < 0) basic = 0; for (j = 0; j < basic; ++j) { if (preserveCase) case_flags[output.length] = (input.charCodeAt(j) - 65 < 26); if (input.charCodeAt(j) >= 0x80) { throw new RangeError("Illegal input >= 0x80"); } output.push(input.charCodeAt(j)); } // Main decoding loop: Start just after the last delimiter if any // basic code points were copied; start at the beginning otherwise. for (ic = basic > 0 ? basic + 1 : 0; ic < input_length;) { // ic is the index of the next character to be consumed, // Decode a generalized variable-length integer into delta, // which gets added to i. The overflow checking is easier // if we increase i as we go, then subtract off its starting // value at the end to obtain delta. for (oldi = i, w = 1, k = base; ; k += base) { if (ic >= input_length) { throw RangeError("punycode_bad_input(1)"); } digit = decode_digit(input.charCodeAt(ic++)); if (digit >= base) { throw RangeError("punycode_bad_input(2)"); } if (digit > Math.floor((maxint - i) / w)) { throw RangeError("punycode_overflow(1)"); } i += digit * w; t = k <= bias ? tmin : k >= bias + tmax ? tmax : k - bias; if (digit < t) { break; } if (w > Math.floor(maxint / (base - t))) { throw RangeError("punycode_overflow(2)"); } w *= (base - t); } out = output.length + 1; bias = adapt(i - oldi, out, oldi === 0); // i was supposed to wrap around from out to 0, // incrementing n each time, so we'll fix that now: if (Math.floor(i / out) > maxint - n) { throw RangeError("punycode_overflow(3)"); } n += Math.floor(i / out); i %= out; // Insert n at position i of the output: // Case of last character determines uppercase flag: if (preserveCase) { case_flags.splice(i, 0, input.charCodeAt(ic - 1) - 65 < 26); } output.splice(i, 0, n); i++; } if (preserveCase) { for (i = 0, len = output.length; i < len; i++) { if (case_flags[i]) { output[i] = (String.fromCharCode(output[i]).toUpperCase()).charCodeAt(0); } } } return this.utf16.encode(output); }; //** Main encode function ** this.encode = function (input, preserveCase) { //** Bias adaptation function ** var n, delta, h, b, bias, j, m, q, k, t, ijv, case_flags; if (preserveCase) { // Preserve case, step1 of 2: Get a list of the unaltered string case_flags = this.utf16.decode(input); } // Converts the input in UTF-16 to Unicode input = this.utf16.decode(input.toLowerCase()); var input_length = input.length; // Cache the length if (preserveCase) { // Preserve case, step2 of 2: Modify the list to true/false for (j = 0; j < input_length; j++) { case_flags[j] = input[j] != case_flags[j]; } } var output = []; // Initialize the state: n = initial_n; delta = 0; bias = initial_bias; // Handle the basic code points: for (j = 0; j < input_length; ++j) { if (input[j] < 0x80) { output.push( String.fromCharCode( case_flags ? encode_basic(input[j], case_flags[j]) : input[j] ) ); } } h = b = output.length; // h is the number of code points that have been handled, b is the // number of basic code points if (b > 0) output.push(delimiter); // Main encoding loop: // while (h < input_length) { // All non-basic code points < n have been // handled already. Find the next larger one: for (m = maxint, j = 0; j < input_length; ++j) { ijv = input[j]; if (ijv >= n && ijv < m) m = ijv; } // Increase delta enough to advance the decoder's // state to , but guard against overflow: if (m - n > Math.floor((maxint - delta) / (h + 1))) { throw RangeError("punycode_overflow (1)"); } delta += (m - n) * (h + 1); n = m; for (j = 0; j < input_length; ++j) { ijv = input[j]; if (ijv < n) { if (++delta > maxint) return Error("punycode_overflow(2)"); } if (ijv == n) { // Represent delta as a generalized variable-length integer: for (q = delta, k = base; ; k += base) { t = k <= bias ? tmin : k >= bias + tmax ? tmax : k - bias; if (q < t) break; output.push(String.fromCharCode(encode_digit(t + (q - t) % (base - t), 0))); q = Math.floor((q - t) / (base - t)); } output.push(String.fromCharCode(encode_digit(q, preserveCase && case_flags[j] ? 1 : 0))); bias = adapt(delta, h + 1, h == b); delta = 0; ++h; } } ++delta, ++n; } return output.join(""); } this.ToASCII = function (domain) { var domain_array = domain.split("."); var out = []; for (var i = 0; i < domain_array.length; ++i) { var s = domain_array[i]; out.push( s.match(/[^A-Za-z0-9-]/) ? "xn--" + punycode.encode(s) : s ); } return out.join("."); } this.ToUnicode = function (domain) { var domain_array = domain.split("."); var out = []; for (var i = 0; i < domain_array.length; ++i) { var s = domain_array[i]; out.push( s.match(/^xn--/) ? punycode.decode(s.slice(4)) : s ); } return out.join("."); } }(); Vue.component("http-firewall-block-options-viewer", { props: ["v-block-options"], data: function () { return { options: this.vBlockOptions } }, template: `
默认设置
状态码:{{options.statusCode}} / 提示内容:[{{options.body.length}}字符][无] / 超时时间:{{options.timeout}}秒 / 最大封禁时长:{{options.timeoutMax}}秒 / 尝试全局封禁
` }) 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 that = this // 初始化 setTimeout(function () { that.changeFields() }, 100) let accessLog = { isPrior: false, isOn: false, fields: [1, 2, 6, 7], status1: true, status2: true, status3: true, status4: true, status5: true, firewallOnly: false, enableClientClosed: false } if (this.vAccessLogConfig != null) { accessLog = this.vAccessLogConfig } this.vFields.forEach(function (v) { if (that.vAccessLogConfig == null) { // 初始化默认值 v.isChecked = that.vDefaultFieldCodes.$contains(v.code) } else { v.isChecked = accessLog.fields.$contains(v.code) } }) return { accessLog: accessLog, hasRequestBodyField: this.vFields.$contains(8), showAdvancedOptions: false } }, methods: { changeFields: function () { this.accessLog.fields = this.vFields.filter(function (v) { return v.isChecked }).map(function (v) { return v.code }) this.hasRequestBodyField = this.accessLog.fields.$contains(8) }, changeAdvanced: function (v) { this.showAdvancedOptions = v } }, template: `
启用访问日志
基础信息

默认记录客户端IP、请求URL等基础信息。

高级信息

在基础信息之外要存储的信息。 记录"请求Body"将会显著消耗更多的系统资源,建议仅在调试时启用,最大记录尺寸为2MiB。

要存储的访问日志状态码
记录客户端中断日志

499的状态码记录客户端主动中断日志。

WAF相关

只记录WAF相关日志

选中后只记录WAF相关的日志。通过此选项可有效减少访问日志数量,降低网络带宽和存储压力。

` }) // 基本认证用户配置 Vue.component("http-auth-basic-auth-user-box", { props: ["v-users"], data: function () { let users = this.vUsers if (users == null) { users = [] } return { users: users, isAdding: false, updatingIndex: -1, username: "", password: "" } }, methods: { add: function () { this.isAdding = true this.username = "" this.password = "" let that = this setTimeout(function () { that.$refs.username.focus() }, 100) }, cancel: function () { this.isAdding = false this.updatingIndex = -1 }, confirm: function () { let that = this if (this.username.length == 0) { teaweb.warn("请输入用户名", function () { that.$refs.username.focus() }) return } if (this.password.length == 0) { teaweb.warn("请输入密码", function () { that.$refs.password.focus() }) return } if (this.updatingIndex < 0) { this.users.push({ username: this.username, password: this.password }) } else { this.users[this.updatingIndex].username = this.username this.users[this.updatingIndex].password = this.password } this.cancel() }, update: function (index, user) { this.updatingIndex = index this.isAdding = true this.username = user.username this.password = user.password let that = this setTimeout(function () { that.$refs.username.focus() }, 100) }, remove: function (index) { this.users.$remove(index) } }, template: `
{{user.username}}
 
` }) Vue.component("http-location-labels", { props: ["v-location-config", "v-server-id"], data: function () { return { location: this.vLocationConfig } }, methods: { // 判断是否已启用某配置 configIsOn: function (config) { return config != null && config.isPrior && config.isOn }, refIsOn: function (ref, config) { return this.configIsOn(ref) && config != null && config.isOn }, len: function (arr) { return (arr == null) ? 0 : arr.length }, url: function (path) { return "/servers/server/settings/locations" + path + "?serverId=" + this.vServerId + "&locationId=" + this.location.id } }, template: `
{{location.name}}
{{domain}}
BREAK 自动跳转HTTPS 文档根目录 源站 5秒盾 CC防护 CACHE {{location.web.charset.charset}} Gzip:{{location.web.gzip.level}} 请求Header 响应Header Websocket 请求脚本 访客IP地址 请求限制
PAGE [状态码{{page.status[0]}}] -> {{page.url}}
临时关闭
REWRITE {{rewriteRule.pattern}} -> {{rewriteRule.replace}}
` }) Vue.component("http-location-labels-label", { props: ["v-class", "v-href"], template: `` }) Vue.component("http-gzip-box", { props: ["v-gzip-config", "v-gzip-ref", "v-is-location"], data: function () { let gzip = this.vGzipConfig if (gzip == null) { gzip = { isOn: true, level: 0, minLength: null, maxLength: null, conds: null } } return { gzip: gzip, advancedVisible: false } }, methods: { isOn: function () { return (!this.vIsLocation || this.vGzipRef.isPrior) && this.vGzipRef.isOn }, changeAdvancedVisible: function (v) { this.advancedVisible = v } }, template: `
启用Gzip压缩
压缩级别

级别越高,压缩比例越大。

Gzip内容最小长度

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

Gzip内容最大长度

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

匹配条件
` }) Vue.component("script-config-box", { props: ["id", "v-script-config", "comment", "v-auditing-status"], mounted: function () { let that = this setTimeout(function () { that.$forceUpdate() }, 100) }, data: function () { let config = this.vScriptConfig if (config == null) { config = { isPrior: false, isOn: false, code: "", auditingCode: "" } } let auditingStatus = null if (config.auditingCodeMD5 != null && config.auditingCodeMD5.length > 0 && config.auditingCode != null && config.auditingCode.length > 0) { config.code = config.auditingCode if (this.vAuditingStatus != null) { for (let i = 0; i < this.vAuditingStatus.length; i++) { let status = this.vAuditingStatus[i] if (status.md5 == config.auditingCodeMD5) { auditingStatus = status break } } } } if (config.code.length == 0) { config.code = "\n\n\n\n" } return { config: config, auditingStatus: auditingStatus } }, watch: { "config.isOn": function () { this.change() } }, methods: { change: function () { this.$emit("change", this.config) }, changeCode: function (code) { this.config.code = code this.change() }, isPlus: function () { if (Tea == null || Tea.Vue == null) { return false } return Tea.Vue.teaIsPlus } }, template: `
启用脚本设置
脚本代码

管理员审核结果:审核通过。 管理员审核结果:驳回     驳回理由:{{auditingStatus.rejectedReason}} 当前脚本将在审核后生效,请耐心等待审核结果。 去审核 »

管理员审核结果:审核通过。

{{config.code}}

{{comment}}

` }) Vue.component("http-firewall-js-cookie-options-viewer", { props: ["v-js-cookie-options"], mounted: function () { this.updateSummary() }, data: function () { let options = this.vJsCookieOptions if (options == null) { options = { life: 0, maxFails: 0, failBlockTimeout: 0, failBlockScopeAll: false, scope: "" } } 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 (summaryList.length == 0) { this.summary = "默认配置" } else { this.summary = summaryList.join(" / ") } } }, template: `
{{summary}}
` }) Vue.component("ssl-certs-view", { props: ["v-certs"], data: function () { let certs = this.vCerts if (certs == null) { certs = [] } return { certs: certs } }, methods: { // 格式化时间 formatTime: function (timestamp) { return new Date(timestamp * 1000).format("Y-m-d") }, // 查看详情 viewCert: function (certId) { teaweb.popup("/servers/certs/certPopup?certId=" + certId, { height: "28em", width: "48em" }) } }, template: `
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}  
` }) 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: "", captchaTypes: window.WAF_CAPTCHA_TYPES } }, 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("全局封禁") } let that = this let typeDef = this.captchaTypes.$find(function (k, v) { return v.code == that.options.captchaType }) if (typeDef != null) { summaryList.push("默认验证方式:" + typeDef.name) } if (this.options.captchaType == "default") { if (this.options.uiIsOn) { summaryList.push("定制UI") } } if (this.options.geeTestConfig != null && this.options.geeTestConfig.isOn) { summaryList.push("已配置极验") } 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 () { let reverseProxyRef = this.vReverseProxyRef if (reverseProxyRef == null) { reverseProxyRef = { isPrior: false, isOn: false, reverseProxyId: 0 } } let reverseProxyConfig = this.vReverseProxyConfig if (reverseProxyConfig == null) { reverseProxyConfig = { requestPath: "", stripPrefix: "", requestURI: "", requestHost: "", requestHostType: 0, requestHostExcludingPort: false, addHeaders: [], connTimeout: {count: 0, unit: "second"}, readTimeout: {count: 0, unit: "second"}, idleTimeout: {count: 0, unit: "second"}, maxConns: 0, maxIdleConns: 0, followRedirects: false, retry50X: false, retry40X: false } } if (reverseProxyConfig.addHeaders == null) { reverseProxyConfig.addHeaders = [] } if (reverseProxyConfig.connTimeout == null) { reverseProxyConfig.connTimeout = {count: 0, unit: "second"} } if (reverseProxyConfig.readTimeout == null) { reverseProxyConfig.readTimeout = {count: 0, unit: "second"} } if (reverseProxyConfig.idleTimeout == null) { reverseProxyConfig.idleTimeout = {count: 0, unit: "second"} } if (reverseProxyConfig.proxyProtocol == null) { // 如果直接赋值Vue将不会触发变更通知 Vue.set(reverseProxyConfig, "proxyProtocol", { isOn: false, version: 1 }) } let forwardHeaders = [ { name: "X-Real-IP", isChecked: false }, { name: "X-Forwarded-For", isChecked: false }, { name: "X-Forwarded-By", isChecked: false }, { name: "X-Forwarded-Host", isChecked: false }, { name: "X-Forwarded-Proto", isChecked: false } ] forwardHeaders.forEach(function (v) { v.isChecked = reverseProxyConfig.addHeaders.$contains(v.name) }) return { reverseProxyRef: reverseProxyRef, reverseProxyConfig: reverseProxyConfig, advancedVisible: false, family: this.vFamily, forwardHeaders: forwardHeaders } }, watch: { "reverseProxyConfig.requestHostType": function (v) { let requestHostType = parseInt(v) if (isNaN(requestHostType)) { requestHostType = 0 } this.reverseProxyConfig.requestHostType = requestHostType }, "reverseProxyConfig.connTimeout.count": function (v) { let count = parseInt(v) if (isNaN(count) || count < 0) { count = 0 } this.reverseProxyConfig.connTimeout.count = count }, "reverseProxyConfig.readTimeout.count": function (v) { let count = parseInt(v) if (isNaN(count) || count < 0) { count = 0 } this.reverseProxyConfig.readTimeout.count = count }, "reverseProxyConfig.idleTimeout.count": function (v) { let count = parseInt(v) if (isNaN(count) || count < 0) { count = 0 } this.reverseProxyConfig.idleTimeout.count = count }, "reverseProxyConfig.maxConns": function (v) { let maxConns = parseInt(v) if (isNaN(maxConns) || maxConns < 0) { maxConns = 0 } this.reverseProxyConfig.maxConns = maxConns }, "reverseProxyConfig.maxIdleConns": function (v) { let maxIdleConns = parseInt(v) if (isNaN(maxIdleConns) || maxIdleConns < 0) { maxIdleConns = 0 } this.reverseProxyConfig.maxIdleConns = maxIdleConns }, "reverseProxyConfig.proxyProtocol.version": function (v) { let version = parseInt(v) if (isNaN(version)) { version = 1 } this.reverseProxyConfig.proxyProtocol.version = version } }, methods: { isOn: function () { if (this.vIsLocation || this.vIsGroup) { return this.reverseProxyRef.isPrior && this.reverseProxyRef.isOn } return this.reverseProxyRef.isOn }, changeAdvancedVisible: function (v) { this.advancedVisible = v }, changeAddHeader: function () { this.reverseProxyConfig.addHeaders = this.forwardHeaders.filter(function (v) { return v.isChecked }).map(function (v) { return v.name }) } }, template: `
启用源站

选中后,所有源站设置才会生效。

回源主机名(Host) 跟随CDN域名   跟随源站   自定义

请求源站时的主机名(Host),用于修改源站接收到的域名 ,"跟随CDN域名"是指源站接收到的域名和当前CDN访问域名保持一致 ,"跟随源站"是指源站接收到的域名仍然是填写的源站地址中的信息,不随代理服务域名改变而改变 ,自定义Host内容中支持请求变量

回源主机名移除端口

选中后表示移除回源主机名中的端口部分。

回源跟随

选中后,自动读取源站跳转后的网页内容。

自动添加报头
{{header.name}}

选中后,会自动向源站请求添加这些报头,以便于源站获取客户端信息。

请求URI(RequestURI)

\${requestURI}为完整的请求URI,可以使用类似于"\${requestURI}?arg1=value1&arg2=value2"的形式添加你的参数。

去除URL前缀(StripPrefix)

可以把请求的路径部分前缀去除后再查找文件,比如把 /web/app/index.html 去除前缀 /web 后就变成 /app/index.html

自动刷新缓存区(AutoFlush)

开启后将自动刷新缓冲区数据到客户端,在类似于SSE(server-sent events)等场景下很有用。

自动重试50X

选中后,表示当源站返回状态码为50X(比如502、504等)时,自动重试其他源站。

自动重试40X

选中后,表示当源站返回状态码为40X(403或404)时,自动重试其他源站。

PROXY Protocol

选中后表示启用PROXY Protocol,每次连接源站时都会在头部写入客户端地址信息。

PROXY Protocol版本

发送类似于PROXY TCP4 192.168.1.1 192.168.1.10 32567 443的头部信息。

发送二进制格式的头部信息。

源站连接失败超时时间

连接源站失败的最大超时时间,0表示不限制。

源站读取超时时间

读取内容时的最大超时时间,0表示不限制。

源站最大并发连接数

源站可以接受到的最大并发连接数,0表示使用系统默认。

源站最大空闲连接数

当没有请求时,源站保持等待的最大空闲连接数量,0表示使用系统默认。

源站最大空闲超时时间

源站保持等待的空闲超时时间,0表示使用默认时间。

` }) Vue.component("http-firewall-param-filters-box", { props: ["v-filters"], data: function () { let filters = this.vFilters if (filters == null) { filters = [] } return { filters: filters, isAdding: false, options: [ {name: "MD5", code: "md5"}, {name: "URLEncode", code: "urlEncode"}, {name: "URLDecode", code: "urlDecode"}, {name: "BASE64Encode", code: "base64Encode"}, {name: "BASE64Decode", code: "base64Decode"}, {name: "UNICODE编码", code: "unicodeEncode"}, {name: "UNICODE解码", code: "unicodeDecode"}, {name: "HTML实体编码", code: "htmlEscape"}, {name: "HTML实体解码", code: "htmlUnescape"}, {name: "计算长度", code: "length"}, {name: "十六进制->十进制", "code": "hex2dec"}, {name: "十进制->十六进制", "code": "dec2hex"}, {name: "SHA1", "code": "sha1"}, {name: "SHA256", "code": "sha256"} ], addingCode: "" } }, methods: { add: function () { this.isAdding = true this.addingCode = "" }, confirm: function () { if (this.addingCode.length == 0) { return } let that = this this.filters.push(this.options.$find(function (k, v) { return (v.code == that.addingCode) })) this.isAdding = false }, cancel: function () { this.isAdding = false }, remove: function (index) { this.filters.$remove(index) } }, template: `
{{filter.name}}
 

可以对参数值进行特定的编解码处理。

` }) Vue.component("http-remote-addr-config-box", { props: ["v-remote-addr-config", "v-is-location", "v-is-group"], data: function () { let config = this.vRemoteAddrConfig if (config == null) { config = { isPrior: false, isOn: false, value: "${rawRemoteAddr}", type: "default", requestHeaderName: "" } } // type if (config.type == null || config.type.length == 0) { config.type = "default" switch (config.value) { case "${rawRemoteAddr}": config.type = "default" break case "${remoteAddrValue}": config.type = "default" break case "${remoteAddr}": config.type = "proxy" break default: if (config.value != null && config.value.length > 0) { config.type = "variable" } } } // value if (config.value == null || config.value.length == 0) { config.value = "${rawRemoteAddr}" } return { config: config, options: [ { name: "直接获取", description: "用户直接访问边缘节点,即 \"用户 --> 边缘节点\" 模式,这时候系统会试图从直接的连接中读取到客户端IP地址。", value: "${rawRemoteAddr}", type: "default" }, { name: "从上级代理中获取", description: "用户和边缘节点之间有别的代理服务转发,即 \"用户 --> [第三方代理服务] --> 边缘节点\",这时候只能从上级代理中获取传递的IP地址;上级代理传递的请求报头中必须包含 X-Forwarded-For 或 X-Real-IP 信息。", value: "${remoteAddr}", type: "proxy" }, { name: "从请求报头中读取", description: "从自定义请求报头读取客户端IP。", value: "", type: "requestHeader" }, { name: "[自定义变量]", description: "通过自定义变量来获取客户端真实的IP地址。", value: "", type: "variable" } ] } }, watch: { "config.requestHeaderName": function (value) { if (this.config.type == "requestHeader"){ this.config.value = "${header." + value.trim() + "}" } } }, methods: { isOn: function () { return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn }, changeOptionType: function () { let that = this switch(this.config.type) { case "default": this.config.value = "${rawRemoteAddr}" break case "proxy": this.config.value = "${remoteAddr}" break case "requestHeader": this.config.value = "" if (this.requestHeaderName != null && this.requestHeaderName.length > 0) { this.config.value = "${header." + this.requestHeaderName + "}" } setTimeout(function () { that.$refs.requestHeaderInput.focus() }) break case "variable": this.config.value = "${rawRemoteAddr}" setTimeout(function () { that.$refs.variableInput.focus() }) break } } }, template: `
启用访客IP设置

选中后,表示使用自定义的请求变量获取客户端IP。

获取IP方式 *

{{option.description}}

请求报头 *

请输入包含有客户端IP的请求报头,需要注意大小写,常见的有X-Forwarded-ForX-Real-IPX-Client-IP等。

读取IP变量值 *

通过此变量获取用户的IP地址。具体可用的请求变量列表可参考官方网站文档;比如通过报头传递IP的情形,可以使用\${header.你的自定义报头}(类似于\${header.X-Forwarded-For},需要注意大小写规范)。

` }) // 访问日志搜索框 Vue.component("http-access-log-search-box", { props: ["v-ip", "v-domain", "v-keyword", "v-cluster-id", "v-node-id"], data: function () { let ip = this.vIp if (ip == null) { ip = "" } let domain = this.vDomain if (domain == null) { domain = "" } let keyword = this.vKeyword if (keyword == null) { keyword = "" } return { ip: ip, domain: domain, keyword: keyword, clusterId: this.vClusterId } }, methods: { cleanIP: function () { this.ip = "" this.submit() }, cleanDomain: function () { this.domain = "" this.submit() }, cleanKeyword: function () { this.keyword = "" this.submit() }, submit: function () { let parent = this.$el.parentNode while (true) { if (parent == null) { break } if (parent.tagName == "FORM") { break } parent = parent.parentNode } if (parent != null) { setTimeout(function () { parent.submit() }, 500) } }, changeCluster: function (clusterId) { this.clusterId = clusterId } }, template: `
IP
域名
关键词
` }) Vue.component("server-config-copy-link", { props: ["v-server-id", "v-config-code"], data: function () { return { serverId: this.vServerId, configCode: this.vConfigCode } }, methods: { copy: function () { teaweb.popup("/servers/server/settings/copy?serverId=" + this.serverId + "&configCode=" + this.configCode, { height: "25em", callback: function () { teaweb.success("批量复制成功") } }) } }, template: `批量 ` }) // 显示指标对象名 Vue.component("metric-key-label", { props: ["v-key"], data: function () { return { keyDefs: window.METRIC_HTTP_KEYS } }, methods: { keyName: function (key) { let that = this let subKey = "" let def = this.keyDefs.$find(function (k, v) { if (v.code == key) { return true } if (key.startsWith("${arg.") && v.code.startsWith("${arg.")) { subKey = that.getSubKey("arg.", key) return true } if (key.startsWith("${header.") && v.code.startsWith("${header.")) { subKey = that.getSubKey("header.", key) return true } if (key.startsWith("${cookie.") && v.code.startsWith("${cookie.")) { subKey = that.getSubKey("cookie.", key) return true } return false }) if (def != null) { if (subKey.length > 0) { return def.name + ": " + subKey } return def.name } return key }, getSubKey: function (prefix, key) { prefix = "${" + prefix let index = key.indexOf(prefix) if (index >= 0) { key = key.substring(index + prefix.length) key = key.substring(0, key.length - 1) return key } return "" } }, template: `
{{keyName(this.vKey)}}
` }) // 指标对象 Vue.component("metric-keys-config-box", { props: ["v-keys"], data: function () { let keys = this.vKeys if (keys == null) { keys = [] } return { keys: keys, isAdding: false, key: "", subKey: "", keyDescription: "", keyDefs: window.METRIC_HTTP_KEYS } }, watch: { keys: function () { this.$emit("change", this.keys) } }, methods: { cancel: function () { this.key = "" this.subKey = "" this.keyDescription = "" this.isAdding = false }, confirm: function () { if (this.key.length == 0) { return } if (this.key.indexOf(".NAME") > 0) { if (this.subKey.length == 0) { teaweb.warn("请输入参数值") return } this.key = this.key.replace(".NAME", "." + this.subKey) } this.keys.push(this.key) this.cancel() }, add: function () { this.isAdding = true let that = this setTimeout(function () { if (that.$refs.key != null) { that.$refs.key.focus() } }, 100) }, remove: function (index) { this.keys.$remove(index) }, changeKey: function () { if (this.key.length == 0) { return } let that = this let def = this.keyDefs.$find(function (k, v) { return v.code == that.key }) if (def != null) { this.keyDescription = def.description } }, keyName: function (key) { let that = this let subKey = "" let def = this.keyDefs.$find(function (k, v) { if (v.code == key) { return true } if (key.startsWith("${arg.") && v.code.startsWith("${arg.")) { subKey = that.getSubKey("arg.", key) return true } if (key.startsWith("${header.") && v.code.startsWith("${header.")) { subKey = that.getSubKey("header.", key) return true } if (key.startsWith("${cookie.") && v.code.startsWith("${cookie.")) { subKey = that.getSubKey("cookie.", key) return true } return false }) if (def != null) { if (subKey.length > 0) { return def.name + ": " + subKey } return def.name } return key }, getSubKey: function (prefix, key) { prefix = "${" + prefix let index = key.indexOf(prefix) if (index >= 0) { key = key.substring(index + prefix.length) key = key.substring(0, key.length - 1) return key } return "" } }, template: `
{{keyName(key)}}  

{{keyDescription}}

` }) Vue.component("http-web-root-box", { props: ["v-root-config", "v-is-location", "v-is-group"], data: function () { let config = this.vRootConfig if (config == null) { config = { isPrior: false, isOn: false, dir: "", indexes: [], stripPrefix: "", decodePath: false, isBreak: false, exceptHiddenFiles: true, onlyURLPatterns: [], exceptURLPatterns: [] } } if (config.indexes == null) { config.indexes = [] } if (config.onlyURLPatterns == null) { config.onlyURLPatterns = [] } if (config.exceptURLPatterns == null) { config.exceptURLPatterns = [] } return { config: config, advancedVisible: false } }, methods: { changeAdvancedVisible: function (v) { this.advancedVisible = v }, addIndex: function () { let that = this teaweb.popup("/servers/server/settings/web/createIndex", { height: "10em", callback: function (resp) { that.config.indexes.push(resp.data.index) } }) }, removeIndex: function (i) { this.config.indexes.$remove(i) }, isOn: function () { return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn } }, template: `
启用静态资源分发
静态资源根目录

可以访问此根目录下的静态资源。

首页文件
{{index}}

在URL中只有目录没有文件名时默认查找的首页文件。

例外URL

如果填写了例外URL,表示不支持通过这些URL访问。

限制URL

如果填写了限制URL,表示仅支持通过这些URL访问。

排除隐藏文件

排除以点(.)符号开头的隐藏目录或文件,比如/.git/logs/HEAD

去除URL前缀

可以把请求的路径部分前缀去除后再查找文件,比如把 /web/app/index.html 去除前缀 /web 后就变成 /app/index.html

路径解码

是否对请求路径进行URL解码,比如把 /Web+App+Browser.html 解码成 /Web App Browser.html 再查找文件。

终止请求

在找不到要访问的文件的情况下是否终止请求并返回404,如果选择终止请求,则不再尝试反向代理等设置。

` }) Vue.component("http-webp-config-box", { props: ["v-webp-config", "v-is-location", "v-is-group", "v-require-cache"], data: function () { let config = this.vWebpConfig if (config == null) { config = { isPrior: false, isOn: false, minLength: {count: 0, "unit": "kb"}, maxLength: {count: 0, "unit": "kb"}, mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico"], extensions: [".png", ".jpeg", ".jpg", ".bmp", ".ico"], conds: null } } if (config.mimeTypes == null) { config.mimeTypes = [] } if (config.extensions == null) { config.extensions = [] } return { config: config, moreOptionsVisible: false } }, methods: { isOn: function () { return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn }, changeExtensions: function (values) { values.forEach(function (v, k) { if (v.length > 0 && v[0] != ".") { values[k] = "." + v } }) this.config.extensions = values }, changeMimeTypes: function (values) { this.config.mimeTypes = values }, changeAdvancedVisible: function () { this.moreOptionsVisible = !this.moreOptionsVisible }, changeConds: function (conds) { this.config.conds = conds } }, template: `
启用WebP压缩

选中后表示开启自动WebP压缩;图片的宽和高均不能超过16383像素;只有满足缓存条件的图片内容才会被转换

支持的扩展名

含有这些扩展名的URL将会被转成WebP,不区分大小写。

支持的MimeType

响应的Content-Type里包含这些MimeType的内容将会被转成WebP。

内容最小长度

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

内容最大长度

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

匹配条件
` }) Vue.component("origin-scheduling-view-box", { props: ["v-scheduling", "v-params"], data: function () { let scheduling = this.vScheduling if (scheduling == null) { scheduling = {} } return { scheduling: scheduling } }, methods: { update: function () { teaweb.popup("/servers/server/settings/reverseProxy/updateSchedulingPopup?" + this.vParams, { height: "21em", callback: function () { window.location.reload() }, }) } }, template: `
当前正在使用的算法 {{scheduling.name}}   [修改]

{{scheduling.description}}

` }) Vue.component("http-firewall-block-options", { props: ["v-block-options"], data: function () { return { options: this.vBlockOptions, statusCode: this.vBlockOptions.statusCode, timeout: this.vBlockOptions.timeout, timeoutMax: this.vBlockOptions.timeoutMax, isEditing: false } }, watch: { statusCode: function (v) { let statusCode = parseInt(v) if (isNaN(statusCode)) { this.options.statusCode = 403 } else { this.options.statusCode = statusCode } }, timeout: function (v) { let timeout = parseInt(v) if (isNaN(timeout)) { this.options.timeout = 0 } else { this.options.timeout = timeout } }, timeoutMax: function (v) { let timeoutMax = parseInt(v) if (isNaN(timeoutMax)) { this.options.timeoutMax = 0 } else { this.options.timeoutMax = timeoutMax } } }, methods: { edit: function () { this.isEditing = !this.isEditing } }, template: `
状态码:{{statusCode}} / 提示内容:[{{options.body.length}}字符][无] / 封禁时长:{{timeout}}秒 / 最大封禁时长:{{timeoutMax}}秒 / 尝试全局封禁
状态码
提示内容
封禁时长

触发阻止动作时,封禁客户端IP的时间。

最大封禁时长

如果最大封禁时长大于封禁时长({{timeout}}秒),那么表示每次封禁的时候,将会在这两个时长数字之间随机选取一个数字作为最终的封禁时长。

失败全局封禁

选中后,表示允许系统尝试全局封禁某个IP,以提升封禁性能。

` }) Vue.component("http-hls-config-box", { props: ["value", "v-is-location", "v-is-group"], data: function () { let config = this.value if (config == null) { config = { isPrior: false } } let encryptingConfig = config.encrypting if (encryptingConfig == null) { encryptingConfig = { isOn: false, onlyURLPatterns: [], exceptURLPatterns: [] } config.encrypting = encryptingConfig } return { config: config, encryptingConfig: encryptingConfig, encryptingMoreOptionsVisible: false } }, methods: { isOn: function () { return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) }, showEncryptingMoreOptions: function () { this.encryptingMoreOptionsVisible = !this.encryptingMoreOptionsVisible } }, template: `
启用HLS加密

启用后,系统会自动在.m3u8文件中加入#EXT-X-KEY:METHOD=AES-128...,并将其中的.ts文件内容进行加密。

例外URL

如果填写了例外URL,表示这些URL跳过不做处理。

限制URL

如果填写了限制URL,表示只对这些URL进行加密处理;如果不填则表示支持所有的URL。

` }) Vue.component("http-oss-bucket-params", { props: ["v-oss-config", "v-params", "name"], data: function () { let params = this.vParams if (params == null) { params = [] } let ossConfig = this.vOssConfig if (ossConfig == null) { ossConfig = { bucketParam: "input", bucketName: "", bucketArgName: "" } } else { // 兼容以往 if (ossConfig.bucketParam != null && ossConfig.bucketParam.length == 0) { ossConfig.bucketParam = "input" } if (ossConfig.options != null && ossConfig.options.bucketName != null && ossConfig.options.bucketName.length > 0) { ossConfig.bucketName = ossConfig.options.bucketName } } return { params: params, ossConfig: ossConfig } }, template: ` {{name}}名称获取方式 *

{{param.description.replace("\${optionName}", name)}}

{{name}}名称 *

{{name}}名称,类似于bucket-12345678

{{name}}参数名称 *

{{name}}参数名称,比如?myBucketName=BUCKET-NAME中的myBucketName

` }) Vue.component("http-request-scripts-config-box", { props: ["vRequestScriptsConfig", "v-auditing-status", "v-is-location"], data: function () { let config = this.vRequestScriptsConfig if (config == null) { config = {} } return { config: config } }, methods: { changeInitGroup: function (group) { this.config.initGroup = group this.$forceUpdate() }, changeRequestGroup: function (group) { this.config.requestGroup = group this.$forceUpdate() } }, template: `

请求初始化

在请求刚初始化时调用,此时自定义报头等尚未生效。

准备发送请求

在准备执行请求或者转发请求之前调用,此时自定义报头、源站等已准备好。

` }) Vue.component("http-request-cond-view", { props: ["v-cond"], data: function () { return { cond: this.vCond, components: window.REQUEST_COND_COMPONENTS } }, methods: { typeName: function (cond) { let c = this.components.$find(function (k, v) { return v.type == cond.type }) if (c != null) { return c.name; } return cond.param + " " + cond.operator }, updateConds: function (conds, simpleCond) { for (let k in simpleCond) { if (simpleCond.hasOwnProperty(k)) { this.cond[k] = simpleCond[k] } } }, notifyChange: function () { } }, template: `
{{cond.param}} {{cond.operator}} {{typeName(cond)}}: {{cond.value}}
` }) Vue.component("http-header-assistant", { props: ["v-type", "v-value"], mounted: function () { let that = this Tea.action("/servers/headers/options?type=" + this.vType) .post() .success(function (resp) { that.allHeaders = resp.data.headers }) }, data: function () { return { allHeaders: [], matchedHeaders: [], selectedHeaderName: "" } }, watch: { vValue: function (v) { if (v != this.selectedHeaderName) { this.selectedHeaderName = "" } if (v.length == 0) { this.matchedHeaders = [] return } this.matchedHeaders = this.allHeaders.filter(function (header) { return teaweb.match(header, v) }).slice(0, 10) } }, methods: { select: function (header) { this.$emit("select", header) this.selectedHeaderName = header } }, template: ` {{header}}     ` }) Vue.component("http-firewall-rules-box", { props: ["v-rules", "v-type"], data: function () { let rules = this.vRules if (rules == null) { rules = [] } return { rules: rules } }, methods: { addRule: function () { window.UPDATING_RULE = null let that = this teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { height: "30em", callback: function (resp) { that.rules.push(resp.data.rule) } }) }, updateRule: function (index, rule) { window.UPDATING_RULE = teaweb.clone(rule) let that = this teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { height: "30em", callback: function (resp) { Vue.set(that.rules, index, resp.data.rule) } }) }, removeRule: function (index) { let that = this teaweb.confirm("确定要删除此规则吗?", function () { that.rules.$remove(index) }) }, operatorName: function (operatorCode) { let operatorName = operatorCode if (typeof (window.WAF_RULE_OPERATORS) != null) { window.WAF_RULE_OPERATORS.forEach(function (v) { if (v.code == operatorCode) { operatorName = v.name } }) } return operatorName }, operatorDescription: function (operatorCode) { let operatorName = operatorCode let operatorDescription = "" if (typeof (window.WAF_RULE_OPERATORS) != null) { window.WAF_RULE_OPERATORS.forEach(function (v) { if (v.code == operatorCode) { operatorName = v.name operatorDescription = v.description } }) } return operatorName + ": " + operatorDescription }, operatorDataType: function (operatorCode) { let operatorDataType = "none" if (typeof (window.WAF_RULE_OPERATORS) != null) { window.WAF_RULE_OPERATORS.forEach(function (v) { if (v.code == operatorCode) { operatorDataType = v.dataType } }) } return operatorDataType }, calculateParamName: function (param) { let paramName = "" if (param != null) { window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { paramName = checkpoint.name } }) } return paramName }, calculateParamDescription: function (param) { let paramName = "" let paramDescription = "" if (param != null) { window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { paramName = checkpoint.name paramDescription = checkpoint.description } }) } return paramName + ": " + paramDescription }, isEmptyString: function (v) { return typeof v == "string" && v.length == 0 } }, template: `
{{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} {{rule.checkpointOptions.period}}秒内请求数 允许{{rule.checkpointOptions.allowDomains}} 禁止{{rule.checkpointOptions.denyDomains}} | {{paramFilter.code}} <{{operatorName(rule.operator)}}> {{rule.value}} [空] ({{rule.description}})
` }) Vue.component("http-fastcgi-box", { props: ["v-fastcgi-ref", "v-fastcgi-configs", "v-is-location"], data: function () { let fastcgiRef = this.vFastcgiRef if (fastcgiRef == null) { fastcgiRef = { isPrior: false, isOn: false, fastcgiIds: [] } } let fastcgiConfigs = this.vFastcgiConfigs if (fastcgiConfigs == null) { fastcgiConfigs = [] } else { fastcgiRef.fastcgiIds = fastcgiConfigs.map(function (v) { return v.id }) } return { fastcgiRef: fastcgiRef, fastcgiConfigs: fastcgiConfigs, advancedVisible: false } }, methods: { isOn: function () { return (!this.vIsLocation || this.fastcgiRef.isPrior) && this.fastcgiRef.isOn }, createFastcgi: function () { let that = this teaweb.popup("/servers/server/settings/fastcgi/createPopup", { height: "26em", callback: function (resp) { teaweb.success("添加成功", function () { that.fastcgiConfigs.push(resp.data.fastcgi) that.fastcgiRef.fastcgiIds.push(resp.data.fastcgi.id) }) } }) }, updateFastcgi: function (fastcgiId, index) { let that = this teaweb.popup("/servers/server/settings/fastcgi/updatePopup?fastcgiId=" + fastcgiId, { callback: function (resp) { teaweb.success("修改成功", function () { Vue.set(that.fastcgiConfigs, index, resp.data.fastcgi) }) } }) }, removeFastcgi: function (index) { this.fastcgiRef.fastcgiIds.$remove(index) this.fastcgiConfigs.$remove(index) } }, template: `
启用配置
Fastcgi服务
{{fastcgi.address}}    
` }) // 请求方法列表 Vue.component("http-methods-box", { props: ["v-methods"], data: function () { let methods = this.vMethods if (methods == null) { methods = [] } return { methods: methods, isAdding: false, addingMethod: "" } }, methods: { add: function () { this.isAdding = true let that = this setTimeout(function () { that.$refs.addingMethod.focus() }, 100) }, confirm: function () { let that = this // 删除其中的空格 this.addingMethod = this.addingMethod.replace(/\s/g, "").toUpperCase() if (this.addingMethod.length == 0) { teaweb.warn("请输入要添加的请求方法", function () { that.$refs.addingMethod.focus() }) return } // 是否已经存在 if (this.methods.$contains(this.addingMethod)) { teaweb.warn("此请求方法已经存在,无需重复添加", function () { that.$refs.addingMethod.focus() }) return } this.methods.push(this.addingMethod) this.cancel() }, remove: function (index) { this.methods.$remove(index) }, cancel: function () { this.isAdding = false this.addingMethod = "" } }, template: `
{{method}}  
 

格式为大写,比如GETPOST等。

` }) // URL扩展名条件 Vue.component("http-cond-url-extension", { props: ["v-cond"], data: function () { let cond = { isRequest: true, param: "${requestPathLowerExtension}", operator: "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 } let that = this this.addingExt.split(/[,;,;|]/).forEach(function (ext) { ext = ext.trim() if (ext.length > 0) { if (ext[0] != ".") { ext = "." + ext } ext = ext.replace(/\s+/g, "").toLowerCase() that.extensions.push(ext) } }) // 清除状态 this.cancelAdding() }, removeExt: function (index) { this.extensions.$remove(index) } }, template: `
{{ext}}

扩展名需要包含点(.)符号,例如.jpg.png之类;多个扩展名用逗号分割。

` }) // 排除URL扩展名条件 Vue.component("http-cond-url-not-extension", { props: ["v-cond"], data: function () { let cond = { isRequest: true, param: "${requestPathLowerExtension}", 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: `
{{ext}}

扩展名需要包含点(.)符号,例如.jpg.png之类。

` }) // 根据URL前缀 Vue.component("http-cond-url-prefix", { props: ["v-cond"], mounted: function () { this.$refs.valueInput.focus() }, data: function () { let cond = { isRequest: true, param: "${requestPath}", operator: "prefix", 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: `

URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static/images,不需要带域名。

` }) Vue.component("http-cond-url-not-prefix", { props: ["v-cond"], mounted: function () { this.$refs.valueInput.focus() }, data: function () { let cond = { isRequest: true, param: "${requestPath}", operator: "prefix", value: "", isReverse: true, 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: `

要排除的URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static/images,不需要带域名。

` }) // 首页 Vue.component("http-cond-url-eq-index", { props: ["v-cond"], data: function () { let cond = { isRequest: true, param: "${requestPath}", operator: "eq", 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: `

检查URL路径是为/,不需要带域名。

` }) // 全站 Vue.component("http-cond-url-all", { props: ["v-cond"], data: function () { let cond = { isRequest: true, param: "${requestPath}", operator: "prefix", 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: `

支持全站所有URL。

` }) // URL精准匹配 Vue.component("http-cond-url-eq", { props: ["v-cond"], mounted: function () { this.$refs.valueInput.focus() }, data: function () { let cond = { isRequest: true, param: "${requestPath}", operator: "eq", 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: `

完整的URL路径,通常以/开头,比如/static/ui.js,不需要带域名。

` }) Vue.component("http-cond-url-not-eq", { props: ["v-cond"], mounted: function () { this.$refs.valueInput.focus() }, data: function () { let cond = { isRequest: true, param: "${requestPath}", operator: "eq", value: "", isReverse: true, 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: `

要排除的完整的URL路径,通常以/开头,比如/static/ui.js,不需要带域名。

` }) // URL正则匹配 Vue.component("http-cond-url-regexp", { props: ["v-cond"], mounted: function () { this.$refs.valueInput.focus() }, data: function () { let cond = { isRequest: true, param: "${requestPath}", 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: `

匹配URL的正则表达式,比如^/static/(.*).js$,不需要带域名。

` }) // 排除URL正则匹配 Vue.component("http-cond-url-not-regexp", { props: ["v-cond"], mounted: function () { this.$refs.valueInput.focus() }, data: function () { let cond = { isRequest: true, param: "${requestPath}", 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: `

不要匹配URL的正则表达式,意即只要匹配成功则排除此条件,比如^/static/(.*).js$,不需要带域名。

` }) // URL通配符 Vue.component("http-cond-url-wildcard-match", { props: ["v-cond"], mounted: function () { this.$refs.valueInput.focus() }, data: function () { let cond = { isRequest: true, param: "${requestPath}", operator: "wildcard match", 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: `

匹配URL的通配符,用星号(*)表示任意字符,比如(/images/*.png/static/*,不需要带域名。

` }) // User-Agent正则匹配 Vue.component("http-cond-user-agent-regexp", { props: ["v-cond"], mounted: function () { this.$refs.valueInput.focus() }, 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: `

匹配User-Agent的正则表达式,比如Android|iPhone

` }) // User-Agent正则不匹配 Vue.component("http-cond-user-agent-not-regexp", { props: ["v-cond"], mounted: function () { this.$refs.valueInput.focus() }, 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: `

匹配User-Agent的正则表达式,比如Android|iPhone,如果匹配,则排除此条件。

` }) // 根据MimeType Vue.component("http-cond-mime-type", { props: ["v-cond"], data: function () { let cond = { isRequest: false, param: "${response.contentType}", operator: "mime type", value: "[]" } if (this.vCond != null && this.vCond.param == cond.param) { cond.value = this.vCond.value } return { cond: cond, mimeTypes: JSON.parse(cond.value), // TODO 可以拖动排序 isAdding: false, addingMimeType: "" } }, watch: { mimeTypes: function () { this.cond.value = JSON.stringify(this.mimeTypes) } }, methods: { addMimeType: function () { this.isAdding = !this.isAdding if (this.isAdding) { let that = this setTimeout(function () { that.$refs.addingMimeType.focus() }, 100) } }, cancelAdding: function () { this.isAdding = false this.addingMimeType = "" }, confirmAdding: function () { // TODO 做更详细的校验 // TODO 如果有重复的则提示之 if (this.addingMimeType.length == 0) { return } this.addingMimeType = this.addingMimeType.replace(/\s+/g, "") this.mimeTypes.push(this.addingMimeType) // 清除状态 this.cancelAdding() }, removeMimeType: function (index) { this.mimeTypes.$remove(index) } }, template: `
{{mimeType}}

服务器返回的内容的MimeType,比如text/htmlimage/*等。

` }) // 参数匹配 Vue.component("http-cond-params", { props: ["v-cond"], mounted: function () { let cond = this.vCond if (cond == null) { return } this.operator = cond.operator // stringValue if (["regexp", "not regexp", "eq", "not", "prefix", "suffix", "contains", "not contains", "eq ip", "gt ip", "gte ip", "lt ip", "lte ip", "ip range"].$contains(cond.operator)) { this.stringValue = cond.value return } // numberValue if (["eq int", "eq float", "gt", "gte", "lt", "lte", "mod 10", "ip mod 10", "mod 100", "ip mod 100"].$contains(cond.operator)) { this.numberValue = cond.value return } // modValue if (["mod", "ip mod"].$contains(cond.operator)) { let pieces = cond.value.split(",") this.modDivValue = pieces[0] if (pieces.length > 1) { this.modRemValue = pieces[1] } return } // stringValues let that = this if (["in", "not in", "file ext", "mime type"].$contains(cond.operator)) { try { let arr = JSON.parse(cond.value) if (arr != null && (arr instanceof Array)) { arr.forEach(function (v) { that.stringValues.push(v) }) } } catch (e) { } return } // versionValue if (["version range"].$contains(cond.operator)) { let pieces = cond.value.split(",") this.versionRangeMinValue = pieces[0] if (pieces.length > 1) { this.versionRangeMaxValue = pieces[1] } return } }, data: function () { let cond = { isRequest: true, param: "", operator: window.REQUEST_COND_OPERATORS[0].op, value: "", isCaseInsensitive: false } if (this.vCond != null) { cond = this.vCond } return { cond: cond, operators: window.REQUEST_COND_OPERATORS, operator: window.REQUEST_COND_OPERATORS[0].op, operatorDescription: window.REQUEST_COND_OPERATORS[0].description, variables: window.REQUEST_VARIABLES, variable: "", // 各种类型的值 stringValue: "", numberValue: "", modDivValue: "", modRemValue: "", stringValues: [], versionRangeMinValue: "", versionRangeMaxValue: "" } }, methods: { changeVariable: function () { let v = this.cond.param if (v == null) { v = "" } this.cond.param = v + this.variable }, changeOperator: function () { let that = this this.operators.forEach(function (v) { if (v.op == that.operator) { that.operatorDescription = v.description } }) this.cond.operator = this.operator // 移动光标 let box = document.getElementById("variables-value-box") if (box != null) { setTimeout(function () { let input = box.getElementsByTagName("INPUT") if (input.length > 0) { input[0].focus() } }, 100) } }, changeStringValues: function (v) { this.stringValues = v this.cond.value = JSON.stringify(v) } }, watch: { stringValue: function (v) { this.cond.value = v }, numberValue: function (v) { // TODO 校验数字 this.cond.value = v }, modDivValue: function (v) { if (v.length == 0) { return } let div = parseInt(v) if (isNaN(div)) { div = 1 } this.modDivValue = div this.cond.value = div + "," + this.modRemValue }, modRemValue: function (v) { if (v.length == 0) { return } let rem = parseInt(v) if (isNaN(rem)) { rem = 0 } this.modRemValue = rem this.cond.value = this.modDivValue + "," + rem }, versionRangeMinValue: function (v) { this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue }, versionRangeMaxValue: function (v) { this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue } }, template: ` 参数值

其中可以使用变量,类似于\${requestPath},也可以是多个变量的组合。

操作符

对比值

要匹配的正则表达式,比如^/static/(.+).js

要对比的数字。

参数值除以10的余数,在0-9之间。

参数值除以100的余数,在0-99之间。

除:
余:

和参数值一致的字符串。

和参数值不一致的字符串。

参数值的前缀。

参数值的后缀为此字符串。

参数值包含此字符串。

参数值不包含此字符串。

添加参数值列表。

添加参数值列表。

添加扩展名列表,比如pnghtml,不包括点。

添加MimeType列表,类似于text/htmlimage/*

-

要对比的IP。

参数中IP转换成整数后除以10的余数,在0-9之间。

参数中IP转换成整数后除以100的余数,在0-99之间。

不区分大小写

选中后表示对比时忽略参数值的大小写。

` }) // 请求方法列表 Vue.component("http-status-box", { props: ["v-status-list"], data: function () { let statusList = this.vStatusList if (statusList == null) { statusList = [] } return { statusList: statusList, isAdding: false, addingStatus: "" } }, methods: { add: function () { this.isAdding = true let that = this setTimeout(function () { that.$refs.addingStatus.focus() }, 100) }, confirm: function () { let that = this // 删除其中的空格 this.addingStatus = this.addingStatus.replace(/\s/g, "").toUpperCase() if (this.addingStatus.length == 0) { teaweb.warn("请输入要添加的状态码", function () { that.$refs.addingStatus.focus() }) return } // 是否已经存在 if (this.statusList.$contains(this.addingStatus)) { teaweb.warn("此状态码已经存在,无需重复添加", function () { that.$refs.addingStatus.focus() }) return } // 格式 if (!this.addingStatus.match(/^\d{3}$/)) { teaweb.warn("请输入正确的状态码", function () { that.$refs.addingStatus.focus() }) return } this.statusList.push(parseInt(this.addingStatus, 10)) this.cancel() }, remove: function (index) { this.statusList.$remove(index) }, cancel: function () { this.isAdding = false this.addingStatus = "" } }, template: `
{{status}}  
 

格式为三位数字,比如200404等。

` }) Vue.component("server-group-selector", { props: ["v-groups"], data: function () { let groups = this.vGroups if (groups == null) { groups = [] } return { groups: groups } }, methods: { selectGroup: function () { let that = this let groupIds = this.groups.map(function (v) { return v.id.toString() }).join(",") teaweb.popup("/servers/groups/selectPopup?selectedGroupIds=" + groupIds, { callback: function (resp) { that.groups.push(resp.data.group) } }) }, addGroup: function () { let that = this teaweb.popup("/servers/groups/createPopup", { callback: function (resp) { that.groups.push(resp.data.group) } }) }, removeGroup: function (index) { this.groups.$remove(index) }, groupIds: function () { return this.groups.map(function (v) { return v.id }) } }, template: `
{{group.name}}  
[选择分组]   [添加分组]
` }) Vue.component("script-group-config-box", { props: ["v-group", "v-auditing-status", "v-is-location"], data: function () { let group = this.vGroup if (group == null) { group = { isPrior: false, isOn: true, scripts: [] } } if (group.scripts == null) { group.scripts = [] } let script = null if (group.scripts.length > 0) { script = group.scripts[group.scripts.length - 1] } return { group: group, script: script } }, methods: { changeScript: function (script) { this.group.scripts = [script] // 目前只支持单个脚本 this.change() }, change: function () { this.$emit("change", this.group) } }, template: `
` }) // 指标周期设置 Vue.component("metric-period-config-box", { props: ["v-period", "v-period-unit"], data: function () { let period = this.vPeriod let periodUnit = this.vPeriodUnit if (period == null || period.toString().length == 0) { period = 1 } if (periodUnit == null || periodUnit.length == 0) { periodUnit = "day" } return { periodConfig: { period: period, unit: periodUnit } } }, watch: { "periodConfig.period": function (v) { v = parseInt(v) if (isNaN(v) || v <= 0) { v = 1 } this.periodConfig.period = v } }, template: `

在此周期内同一对象累积为同一数据。

` }) Vue.component("traffic-limit-config-box", { props: ["v-traffic-limit"], data: function () { let config = this.vTrafficLimit if (config == null) { config = { isOn: false, dailySize: { count: -1, unit: "gb" }, monthlySize: { count: -1, unit: "gb" }, totalSize: { count: -1, unit: "gb" }, noticePageBody: "" } } if (config.dailySize == null) { config.dailySize = { count: -1, unit: "gb" } } if (config.monthlySize == null) { config.monthlySize = { count: -1, unit: "gb" } } if (config.totalSize == null) { config.totalSize = { count: -1, unit: "gb" } } return { config: config } }, methods: { showBodyTemplate: function () { this.config.noticePageBody = ` Traffic Limit Exceeded Warning

Traffic Limit Exceeded Warning

The site traffic has exceeded the limit. Please contact with the site administrator.

Request ID: \${requestId}.
` } }, template: `
启用流量限制

注意:由于流量统计是每5分钟统计一次,所以超出流量限制后,对用户的提醒也会有所延迟。

日流量限制
月流量限制
网页提示内容

[使用模板]。当达到流量限制时网页显示的HTML内容,不填写则显示默认的提示内容,适用于网站类服务。

` }) Vue.component("http-firewall-captcha-options", { props: ["v-captcha-options"], mounted: function () { this.updateSummary() }, data: function () { let options = this.vCaptchaOptions if (options == null) { options = { captchaType: "default", countLetters: 0, life: 0, maxFails: 0, failBlockTimeout: 0, failBlockScopeAll: false, uiIsOn: false, uiTitle: "", uiPrompt: "", uiButtonTitle: "", uiShowRequestId: true, uiCss: "", uiFooter: "", uiBody: "", cookieId: "", lang: "", geeTestConfig: { isOn: false, captchaId: "", captchaKey: "" } } } if (options.countLetters <= 0) { options.countLetters = 6 } if (options.captchaType == null || options.captchaType.length == 0) { options.captchaType = "default" } return { options: options, isEditing: false, summary: "", uiBodyWarning: "", captchaTypes: window.WAF_CAPTCHA_TYPES } }, 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.captchaType": function (v) { this.updateSummary() }, "options.uiIsOn": function (v) { this.updateSummary() }, "options.uiBody": function (v) { if (/|\s).+\$\{body}.*<\/form>/s.test(v)) { this.uiBodyWarning = "页面模板中不能使用
标签包裹\${body}变量,否则将导致验证码表单无法提交。" } else { this.uiBodyWarning = "" } }, "options.geeTestConfig.isOn": 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("尝试全局封禁") } let that = this let typeDef = this.captchaTypes.$find(function (k, v) { return v.code == that.options.captchaType }) if (typeDef != null) { summaryList.push("默认验证方式:" + typeDef.name) } if (this.options.captchaType == "default") { if (this.options.uiIsOn) { summaryList.push("定制UI") } } if (this.options.geeTestConfig != null && this.options.geeTestConfig.isOn) { summaryList.push("已配置极验") } if (summaryList.length == 0) { this.summary = "默认配置" } else { this.summary = summaryList.join(" / ") } }, confirm: function () { this.isEditing = false } }, template: `
{{summary}}
默认验证方式

{{captchaDef.description}}

有效时间

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

最多失败次数

建议填入一个不小于5的数字,以减少误判几率。允许用户失败尝试的最多次数,超过这个次数将被自动加入黑名单。如果为空或者为0,表示不限制。

失败拦截时间

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

失败全局封禁

选中后,表示允许系统尝试全局封禁某个IP,以提升封禁性能。

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

类似于提交验证

显示请求ID

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

CSS样式
页头提示

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

页尾提示

支持HTML。

页面模板

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

允许用户使用极验

选中后,表示允许用户在WAF设置中选择极验。

极验-验证ID *

在极验控制台--业务管理中获取。

极验-验证Key *

在极验控制台--业务管理中获取。

` }) Vue.component("user-agent-config-box", { props: ["v-is-location", "v-is-group", "value"], data: function () { let config = this.value if (config == null) { config = { isPrior: false, isOn: false, filters: [] } } if (config.filters == null) { config.filters = [] } return { config: config, isAdding: false, addingFilter: { keywords: [], action: "deny" }, moreOptionsVisible: false, batchKeywords: "" } }, methods: { isOn: function () { return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn }, remove: function (index) { let that = this teaweb.confirm("确定要删除此名单吗?", function () { that.config.filters.$remove(index) }) }, add: function () { this.isAdding = true let that = this setTimeout(function () { that.$refs.batchKeywords.focus() }) }, confirm: function () { if (this.addingFilter.action == "deny") { this.config.filters.push(this.addingFilter) } else { let index = -1 this.config.filters.forEach(function (filter, filterIndex) { if (filter.action == "allow") { index = filterIndex } }) if (index < 0) { this.config.filters.unshift(this.addingFilter) } else { this.config.filters.$insert(index + 1, this.addingFilter) } } this.cancel() }, cancel: function () { this.isAdding = false this.addingFilter = { keywords: [], action: "deny" } this.batchKeywords = "" }, changeKeywords: function (keywords) { let arr = keywords.split(/\n/) let resultKeywords = [] arr.forEach(function (keyword){ keyword = keyword.trim() if (!resultKeywords.$contains(keyword)) { resultKeywords.push(keyword) } }) this.addingFilter.keywords = resultKeywords }, showMoreOptions: function () { this.moreOptionsVisible = !this.moreOptionsVisible } }, template: `
启用UA名单

选中后表示开启UserAgent名单。

UA名单
UA关键词 动作 操作
{{keyword}} [空] 允许不允许 删除
UA关键词

每行一个关键词;不区分大小写,比如Chrome;支持*通配符,比如*Firefox*;也支持空行关键词,表示空UserAgent。

动作
 
例外URL

如果填写了例外URL,表示这些URL跳过不做处理。

限制URL

如果填写了限制URL,表示只对这些URL进行处理;如果不填则表示支持所有的URL。

` }) Vue.component("http-pages-box", { props: ["v-pages"], data: function () { let pages = [] if (this.vPages != null) { pages = this.vPages } return { pages: pages } }, methods: { addPage: function () { let that = this teaweb.popup("/servers/server/settings/pages/createPopup", { height: "26em", callback: function (resp) { that.pages.push(resp.data.page) that.notifyChange() } }) }, updatePage: function (pageIndex, pageId) { let that = this teaweb.popup("/servers/server/settings/pages/updatePopup?pageId=" + pageId, { height: "26em", callback: function (resp) { Vue.set(that.pages, pageIndex, resp.data.page) that.notifyChange() } }) }, removePage: function (pageIndex) { let that = this teaweb.confirm("确定要移除此页面吗?", function () { that.pages.$remove(pageIndex) that.notifyChange() }) }, notifyChange: function () { let parent = this.$el.parentNode while (true) { if (parent == null) { break } if (parent.tagName == "FORM") { break } parent = parent.parentNode } if (parent != null) { setTimeout(function () { Tea.runActionOn(parent) }, 100) } } }, template: `
响应状态码 页面类型 新状态码 例外URL 限制URL 操作
{{page.status[0]}} {{page.status}}
{{page.url}}
读取URL
{{page.url}}
跳转URL {{page.newStatus}}
[HTML内容]
{{page.newStatus}}
{{page.newStatus}} 保持
{{urlPattern.pattern}}
-
{{urlPattern.pattern}}
-
修改   删除
` }) 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 < 5) { count = 5 } 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攻击。此功能需要节点已安装并启用nftables或Firewalld。

空连接次数
次/分钟

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

封禁时长
忽略局域网访问
` }) Vue.component("http-firewall-region-selector", { props: ["v-type", "v-countries"], data: function () { let countries = this.vCountries if (countries == null) { countries = [] } return { listType: this.vType, countries: countries } }, methods: { addCountry: function () { let selectedCountryIds = this.countries.map(function (country) { return country.id }) let that = this teaweb.popup("/servers/server/settings/waf/ipadmin/selectCountriesPopup?type=" + this.listType + "&selectedCountryIds=" + selectedCountryIds.join(","), { width: "52em", height: "30em", callback: function (resp) { that.countries = resp.data.selectedCountries that.$forceUpdate() that.notifyChange() } }) }, removeCountry: function (index) { this.countries.$remove(index) this.notifyChange() }, resetCountries: function () { this.countries = [] this.notifyChange() }, notifyChange: function () { this.$emit("change", { "countries": this.countries }) } }, template: `
暂时没有选择允许封禁的区域。
({{country.letter}}){{country.name}}
 
` }) // TODO 支持关键词搜索 // TODO 改成弹窗选择 Vue.component("admin-selector", { props: ["v-admin-id"], mounted: function () { let that = this Tea.action("/admins/options") .post() .success(function (resp) { that.admins = resp.data.admins }) }, data: function () { let adminId = this.vAdminId if (adminId == null) { adminId = 0 } return { admins: [], adminId: adminId } }, template: `
` }) // 绑定IP列表 Vue.component("ip-list-bind-box", { props: ["v-http-firewall-policy-id", "v-type"], mounted: function () { this.refresh() }, data: function () { return { policyId: this.vHttpFirewallPolicyId, type: this.vType, lists: [] } }, methods: { bind: function () { let that = this teaweb.popup("/servers/iplists/bindHTTPFirewallPopup?httpFirewallPolicyId=" + this.policyId + "&type=" + this.type, { width: "50em", height: "34em", callback: function () { }, onClose: function () { that.refresh() } }) }, remove: function (index, listId) { let that = this teaweb.confirm("确定要删除这个绑定的IP名单吗?", function () { Tea.action("/servers/iplists/unbindHTTPFirewall") .params({ httpFirewallPolicyId: that.policyId, listId: listId }) .post() .success(function (resp) { that.lists.$remove(index) }) }) }, refresh: function () { let that = this Tea.action("/servers/iplists/httpFirewall") .params({ httpFirewallPolicyId: this.policyId, type: this.vType }) .post() .success(function (resp) { that.lists = resp.data.lists }) } }, template: `
绑定+   已绑定:
` }) Vue.component("ip-list-table", { props: ["v-items", "v-keyword", "v-show-search-button", "v-total"/** total items >= items length **/], data: function () { let maxDeletes = 10000 if (this.vTotal != null && this.vTotal > 0 && this.vTotal < maxDeletes) { maxDeletes = this.vTotal } return { items: this.vItems, keyword: (this.vKeyword != null) ? this.vKeyword : "", selectedAll: false, hasSelectedItems: false, MaxDeletes: maxDeletes } }, methods: { updateItem: function (itemId) { this.$emit("update-item", itemId) }, deleteItem: function (itemId) { this.$emit("delete-item", itemId) }, viewLogs: function (itemId) { teaweb.popup("/servers/iplists/accessLogsPopup?itemId=" + itemId, { width: "50em", height: "30em" }) }, changeSelectedAll: function () { let boxes = this.$refs.itemCheckBox if (boxes == null) { return } let that = this boxes.forEach(function (box) { box.checked = that.selectedAll }) this.hasSelectedItems = this.selectedAll }, changeSelected: function (e) { let that = this that.hasSelectedItems = false let boxes = that.$refs.itemCheckBox if (boxes == null) { return } boxes.forEach(function (box) { if (box.checked) { that.hasSelectedItems = true } }) }, deleteAll: function () { let boxes = this.$refs.itemCheckBox if (boxes == null) { return } let itemIds = [] boxes.forEach(function (box) { if (box.checked) { itemIds.push(box.value) } }) if (itemIds.length == 0) { return } Tea.action("/servers/iplists/deleteItems") .post() .params({ itemIds: itemIds }) .success(function () { teaweb.successToast("批量删除成功", 1200, teaweb.reload) }) }, deleteCount: function () { let that = this teaweb.confirm("确定要批量删除当前列表中的" + this.MaxDeletes + "个IP吗?", function () { let query = window.location.search if (query.startsWith("?")) { query = query.substring(1) } Tea.action("/servers/iplists/deleteCount?" + query) .post() .params({count: that.MaxDeletes}) .success(function () { teaweb.successToast("批量删除成功", 1200, teaweb.reload) }) }) }, formatSeconds: function (seconds) { if (seconds < 60) { return seconds + "秒" } if (seconds < 3600) { return Math.ceil(seconds / 60) + "分钟" } if (seconds < 86400) { return Math.ceil(seconds / 3600) + "小时" } return Math.ceil(seconds / 86400) + "天" }, cancelChecked: function () { this.hasSelectedItems = false this.selectedAll = false let boxes = this.$refs.itemCheckBox if (boxes == null) { return } boxes.forEach(function (box) { box.checked = false }) } }, template: `
       
IP 类型 级别 过期时间 备注 操作
{{item.value}} {{item.ipFrom}}  New   - {{item.ipTo}} *
{{item.region}} | {{item.isp}}
{{item.isp}}
IPv4 IPv4 IPv6 所有IP {{item.eventLevelName}} -
{{item.expiredTime}}
已过期
{{formatSeconds(item.lifeSeconds)}} 已过期
不过期
{{item.reason}} - 日志   修改   删除
` }) Vue.component("ip-item-text", { props: ["v-item"], template: ` * {{vItem.value}} {{vItem.ipFrom}} - {{vItem.ipTo}}   级别:{{vItem.eventLevelName}} ` }) Vue.component("ip-box", { props: ["v-ip"], methods: { popup: function () { 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=" + ip, { width: "50em", height: "30em" }) } }, template: `` }) Vue.component("sms-sender", { props: ["value", "name"], mounted: function () { this.initType(this.config.type) }, data: function () { let value = this.value if (value == null) { value = { isOn: false, type: "webHook", webHookParams: { url: "", method: "POST" } } } return { config: value } }, watch: { "config.type": function (v) { this.initType(v) } }, methods: { initType: function (v) { // initialize params switch (v) { case "webHook": if (this.config.webHookParams == null) { this.config.webHookParams = { url: "", method: "POST" } } break } }, test: function () { window.TESTING_SMS_CONFIG = this.config teaweb.popup("/users/setting/smsTest", { height: "22em" }) } }, template: `
启用
发送渠道

通过WebHook的方式调用你的自定义发送短信接口。

WebHook URL地址 *

接收发送短信请求的URL,必须以http://https://开头。

WebHook请求方法

以在URL参数中加入mobile、body和code三个参数(YOUR_WEB_HOOK_URL?mobile=手机号&body=短信内容&code=验证码)的方式调用你的WebHook URL地址;状态码返回200表示成功。

通过POST表单发送mobile、body和code三个参数(mobile=手机号&body=短信内容&code=验证码)的方式调用你的WebHook URL地址;状态码返回200表示成功。

发送测试 [点此测试]
` }) Vue.component("email-sender", { props: ["value", "name"], data: function () { let value = this.value if (value == null) { value = { isOn: false, smtpHost: "", smtpPort: 0, username: "", password: "", fromEmail: "", fromName: "" } } let smtpPortString = value.smtpPort.toString() if (smtpPortString == "0") { smtpPortString = "" } return { config: value, smtpPortString: smtpPortString } }, watch: { smtpPortString: function (v) { let port = parseInt(v) if (!isNaN(port)) { this.config.smtpPort = port } } }, methods: { test: function () { window.TESTING_EMAIL_CONFIG = this.config teaweb.popup("/users/setting/emailTest", { height: "36em" }) } }, template: `
启用
SMTP地址 *

SMTP主机地址,比如smtp.qq.com,目前仅支持TLS协议,如不清楚,请查询对应邮件服务商文档。

SMTP端口 *

SMTP主机端口,比如587465,如不清楚,请查询对应邮件服务商文档。

用户名 *

通常为发件人邮箱地址。

密码 *

邮箱登录密码或授权码,如不清楚,请查询对应邮件服务商文档。。

发件人Email *

使用的发件人邮箱地址,通常和发件用户名一致。

发件人名称

使用的发件人名称,默认使用系统设置的产品名称

发送测试 [点此测试]
` }) Vue.component("api-node-selector", { props: [], data: function () { return {} }, template: `
暂未实现
` }) Vue.component("api-node-addresses-box", { props: ["v-addrs", "v-name"], data: function () { let addrs = this.vAddrs if (addrs == null) { addrs = [] } return { addrs: addrs } }, methods: { // 添加IP地址 addAddr: function () { let that = this; teaweb.popup("/settings/api/node/createAddrPopup", { height: "16em", callback: function (resp) { that.addrs.push(resp.data.addr); } }) }, // 修改地址 updateAddr: function (index, addr) { let that = this; window.UPDATING_ADDR = addr teaweb.popup("/settings/api/node/updateAddrPopup?addressId=", { callback: function (resp) { Vue.set(that.addrs, index, resp.data.addr); } }) }, // 删除IP地址 removeAddr: function (index) { this.addrs.$remove(index); } }, template: `
{{addr.protocol}}://{{addr.host.quoteIP()}}:{{addr.portRange}}
` }) // 给Table增加排序功能 function sortTable(callback) { // 引入js let jsFile = document.createElement("script") jsFile.setAttribute("src", "/js/sortable.min.js") jsFile.addEventListener("load", function () { // 初始化 let box = document.querySelector("#sortable-table") if (box == null) { return } Sortable.create(box, { draggable: "tbody", handle: ".icon.handle", onStart: function () { }, onUpdate: function (event) { let rows = box.querySelectorAll("tbody") let rowIds = [] rows.forEach(function (row) { rowIds.push(parseInt(row.getAttribute("v-id"))) }) callback(rowIds) } }) }) document.head.appendChild(jsFile) } function sortLoad(callback) { let jsFile = document.createElement("script") jsFile.setAttribute("src", "/js/sortable.min.js") jsFile.addEventListener("load", function () { if (typeof (callback) == "function") { callback() } }) document.head.appendChild(jsFile) } Vue.component("page-box", { data: function () { return { page: "" } }, created: function () { let that = this; setTimeout(function () { that.page = Tea.Vue.page; }) }, template: `
` }) Vue.component("network-addresses-box", { props: ["v-server-type", "v-addresses", "v-protocol", "v-name", "v-from", "v-support-range", "v-url"], data: function () { let addresses = this.vAddresses if (addresses == null) { addresses = [] } let protocol = this.vProtocol if (protocol == null) { protocol = "" } let name = this.vName if (name == null) { name = "addresses" } let from = this.vFrom if (from == null) { from = "" } return { addresses: addresses, protocol: protocol, name: name, from: from, isEditing: false } }, watch: { "vServerType": function () { this.addresses = [] }, "vAddresses": function () { if (this.vAddresses != null) { this.addresses = this.vAddresses } } }, methods: { addAddr: function () { this.isEditing = true let that = this window.UPDATING_ADDR = null let url = this.vUrl if (url == null) { url = "/servers/addPortPopup" } teaweb.popup(url + "?serverType=" + this.vServerType + "&protocol=" + this.protocol + "&from=" + this.from + "&supportRange=" + (this.supportRange() ? 1 : 0), { height: "18em", callback: function (resp) { var addr = resp.data.address if (that.addresses.$find(function (k, v) { return addr.host == v.host && addr.portRange == v.portRange && addr.protocol == v.protocol }) != null) { teaweb.warn("要添加的网络地址已经存在") return } that.addresses.push(addr) if (["https", "https4", "https6"].$contains(addr.protocol)) { this.tlsProtocolName = "HTTPS" } else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) { this.tlsProtocolName = "TLS" } // 发送事件 that.$emit("change", that.addresses) } }) }, removeAddr: function (index) { this.addresses.$remove(index); // 发送事件 this.$emit("change", this.addresses) }, updateAddr: function (index, addr) { let that = this window.UPDATING_ADDR = addr let url = this.vUrl if (url == null) { url = "/servers/addPortPopup" } teaweb.popup(url + "?serverType=" + this.vServerType + "&protocol=" + this.protocol + "&from=" + this.from + "&supportRange=" + (this.supportRange() ? 1 : 0), { height: "18em", callback: function (resp) { var addr = resp.data.address Vue.set(that.addresses, index, addr) if (["https", "https4", "https6"].$contains(addr.protocol)) { this.tlsProtocolName = "HTTPS" } else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) { this.tlsProtocolName = "TLS" } // 发送事件 that.$emit("change", that.addresses) } }) // 发送事件 this.$emit("change", this.addresses) }, supportRange: function () { return this.vSupportRange || (this.vServerType == "tcpProxy" || this.vServerType == "udpProxy") }, edit: function () { this.isEditing = true } }, template: `
{{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}}{{addr.portRange}}
    [修改]
{{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}}{{addr.portRange}}
[添加端口绑定]
` }) /** * 保存按钮 */ Vue.component("submit-btn", { template: '' }); // 可以展示更多条目的角图表 Vue.component("more-items-angle", { props: ["v-data-url", "v-url"], data: function () { return { visible: false } }, methods: { show: function () { this.visible = !this.visible if (this.visible) { this.showBox() } else { this.hideBox() } }, showBox: function () { let that = this this.visible = true Tea.action(this.vDataUrl) .params({ url: this.vUrl }) .post() .success(function (resp) { let groups = resp.data.groups let boxLeft = that.$el.offsetLeft + 120; let boxTop = that.$el.offsetTop + 70; let box = document.createElement("div") box.setAttribute("id", "more-items-box") box.style.cssText = "z-index: 100; position: absolute; left: " + boxLeft + "px; top: " + boxTop + "px; max-height: 30em; overflow: auto; border-bottom: 1px solid rgba(34,36,38,.15)" document.body.append(box) let menuHTML = "
    " groups.forEach(function (group) { menuHTML += "
    " + teaweb.encodeHTML(group.name) + "
    " group.items.forEach(function (item) { menuHTML += "" + teaweb.encodeHTML(item.name) + "" }) }) menuHTML += "
" box.innerHTML = menuHTML let listener = function (e) { if (e.target.tagName == "I") { return } if (!that.isInBox(box, e.target)) { document.removeEventListener("click", listener) that.hideBox() } } document.addEventListener("click", listener) }) }, hideBox: function () { let box = document.getElementById("more-items-box") if (box != null) { box.parentNode.removeChild(box) } this.visible = false }, isInBox: function (parent, child) { while (true) { if (child == null) { break } if (child.parentNode == parent) { return true } child = child.parentNode } return false } }, template: `切换` }) /** * 菜单项 */ Vue.component("menu-item", { props: ["href", "active", "code"], data: function () { let active = this.active if (typeof (active) == "undefined") { var itemCode = "" if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") { itemCode = window.TEA.ACTION.data.firstMenuItem } if (itemCode != null && itemCode.length > 0 && this.code != null && this.code.length > 0) { if (itemCode.indexOf(",") > 0) { active = itemCode.split(",").$contains(this.code) } else { active = (itemCode == this.code) } } } let href = (this.href == null) ? "" : this.href if (typeof (href) == "string" && href.length > 0 && href.startsWith(".")) { let qIndex = href.indexOf("?") if (qIndex >= 0) { href = Tea.url(href.substring(0, qIndex)) + href.substring(qIndex) } else { href = Tea.url(href) } } return { vHref: href, vActive: active } }, methods: { click: function (e) { this.$emit("click", e) } }, template: '\ \ ' }); // 使用Icon的链接方式 Vue.component("link-icon", { props: ["href", "title", "target", "size"], data: function () { let realSize = this.size if (realSize == null || realSize.length == 0) { realSize = "small" } return { vTitle: (this.title == null) ? "打开链接" : this.title, realSize: realSize } }, template: ` ` }) // 带有下划虚线的连接 Vue.component("link-red", { props: ["href", "title"], data: function () { let href = this.href if (href == null) { href = "" } return { vHref: href } }, methods: { clickPrevent: function () { emitClick(this, arguments) if (this.vHref.length > 0) { window.location = this.vHref } } }, template: `` }) // 会弹出窗口的链接 Vue.component("link-popup", { props: ["title"], methods: { clickPrevent: function () { emitClick(this, arguments) } }, template: `` }) Vue.component("popup-icon", { props: ["title", "href", "height"], methods: { clickPrevent: function () { if (this.href != null && this.href.length > 0) { teaweb.popup(this.href, { height: this.height }) } } }, template: ` ` }) // 小提示 Vue.component("tip-icon", { props: ["content"], methods: { showTip: function () { teaweb.popupTip(this.content) } }, template: `` }) // 提交点击事件 function emitClick(obj, arguments) { let event = "click" let newArgs = [event] for (let i = 0; i < arguments.length; i++) { newArgs.push(arguments[i]) } obj.$emit.apply(obj, newArgs) } Vue.component("countries-selector", { props: ["v-countries"], data: function () { let countries = this.vCountries if (countries == null) { countries = [] } let countryIds = countries.$map(function (k, v) { return v.id }) return { countries: countries, countryIds: countryIds } }, methods: { add: function () { let countryStringIds = this.countryIds.map(function (v) { return v.toString() }) let that = this teaweb.popup("/ui/selectCountriesPopup?countryIds=" + countryStringIds.join(","), { width: "48em", height: "23em", callback: function (resp) { that.countries = resp.data.countries that.change() } }) }, remove: function (index) { this.countries.$remove(index) this.change() }, change: function () { this.countryIds = this.countries.$map(function (k, v) { return v.id }) } }, template: `
{{country.name}}
` }) Vue.component("raquo-item", { template: `»` }) Vue.component("bandwidth-size-capacity-view", { props: ["v-value"], data: function () { let capacity = this.vValue if (capacity != null && capacity.count > 0 && typeof capacity.unit === "string") { capacity.unit = capacity.unit[0].toUpperCase() + capacity.unit.substring(1) + "ps" } return { capacity: capacity } }, template: ` {{capacity.count}}{{capacity.unit}} ` }) Vue.component("more-options-tbody", { data: function () { return { isVisible: false } }, methods: { show: function () { this.isVisible = !this.isVisible this.$emit("change", this.isVisible) } }, template: ` 更多选项收起选项 ` }) Vue.component("download-link", { props: ["v-element", "v-file", "v-value"], created: function () { let that = this setTimeout(function () { that.url = that.composeURL() }, 1000) }, data: function () { let filename = this.vFile if (filename == null || filename.length == 0) { filename = "unknown-file" } return { file: filename, url: this.composeURL() } }, methods: { composeURL: function () { let text = "" if (this.vValue != null) { text = this.vValue } else { let e = document.getElementById(this.vElement) if (e == null) { // 不提示错误,因为此时可能页面未加载完整 return } text = e.innerText if (text == null) { text = e.textContent } } return Tea.url("/ui/download", { file: this.file, text: text }) } }, template: ``, }) Vue.component("values-box", { props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"], data: function () { let values = this.values; if (values == null) { values = []; } if (this.vValues != null && typeof this.vValues == "object") { values = this.vValues } return { "realValues": values, "isUpdating": false, "isAdding": false, "index": 0, "value": "", isEditing: false } }, methods: { create: function () { this.isAdding = true; var that = this; setTimeout(function () { that.$refs.value.focus(); }, 200); }, update: function (index) { this.cancel() this.isUpdating = true; this.index = index; this.value = this.realValues[index]; var that = this; setTimeout(function () { that.$refs.value.focus(); }, 200); }, confirm: function () { if (this.value.length == 0) { if (typeof(this.vAllowEmpty) != "boolean" || !this.vAllowEmpty) { return } } // validate if (typeof(this.validator) == "function") { let resp = this.validator.call(this, this.value) if (typeof resp == "object") { if (typeof resp.isOk == "boolean" && !resp.isOk) { if (typeof resp.message == "string") { let that = this teaweb.warn(resp.message, function () { that.$refs.value.focus(); }) } return } } } if (this.isUpdating) { Vue.set(this.realValues, this.index, this.value); } else { this.realValues.push(this.value); } this.cancel() this.$emit("change", this.realValues) }, remove: function (index) { this.realValues.$remove(index) this.$emit("change", this.realValues) }, cancel: function () { this.isUpdating = false; this.isAdding = false; this.value = ""; }, updateAll: function (values) { this.realValues = values }, addValue: function (v) { this.realValues.push(v) }, startEditing: function () { this.isEditing = !this.isEditing }, allValues: function () { return this.realValues } }, template: `
{{value}} [空]
[修改]
{{value}} [空]  
` }); Vue.component("datetime-input", { props: ["v-name", "v-timestamp"], mounted: function () { let that = this teaweb.datepicker(this.$refs.dayInput, function (v) { that.day = v that.hour = "23" that.minute = "59" that.second = "59" that.change() }) }, data: function () { let timestamp = this.vTimestamp if (timestamp != null) { timestamp = parseInt(timestamp) if (isNaN(timestamp)) { timestamp = 0 } } else { timestamp = 0 } let day = "" let hour = "" let minute = "" let second = "" if (timestamp > 0) { let date = new Date() date.setTime(timestamp * 1000) let year = date.getFullYear().toString() let month = this.leadingZero((date.getMonth() + 1).toString(), 2) day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2) hour = this.leadingZero(date.getHours().toString(), 2) minute = this.leadingZero(date.getMinutes().toString(), 2) second = this.leadingZero(date.getSeconds().toString(), 2) } return { timestamp: timestamp, day: day, hour: hour, minute: minute, second: second, hasDayError: false, hasHourError: false, hasMinuteError: false, hasSecondError: false } }, methods: { change: function () { // day if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) { this.hasDayError = true return } let pieces = this.day.split("-") let year = parseInt(pieces[0]) let month = parseInt(pieces[1]) if (month < 1 || month > 12) { this.hasDayError = true return } let day = parseInt(pieces[2]) if (day < 1 || day > 32) { this.hasDayError = true return } this.hasDayError = false // hour if (!/^\d+$/.test(this.hour)) { this.hasHourError = true return } let hour = parseInt(this.hour) if (isNaN(hour)) { this.hasHourError = true return } if (hour < 0 || hour >= 24) { this.hasHourError = true return } this.hasHourError = false // minute if (!/^\d+$/.test(this.minute)) { this.hasMinuteError = true return } let minute = parseInt(this.minute) if (isNaN(minute)) { this.hasMinuteError = true return } if (minute < 0 || minute >= 60) { this.hasMinuteError = true return } this.hasMinuteError = false // second if (!/^\d+$/.test(this.second)) { this.hasSecondError = true return } let second = parseInt(this.second) if (isNaN(second)) { this.hasSecondError = true return } if (second < 0 || second >= 60) { this.hasSecondError = true return } this.hasSecondError = false let date = new Date(year, month - 1, day, hour, minute, second) this.timestamp = Math.floor(date.getTime() / 1000) }, leadingZero: function (s, l) { s = s.toString() if (l <= s.length) { return s } for (let i = 0; i < l - s.length; i++) { s = "0" + s } return s }, resultTimestamp: function () { return this.timestamp }, nextYear: function () { let date = new Date() date.setFullYear(date.getFullYear()+1) 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() }, 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() }, nextHours: function (hours) { let date = new Date() date.setTime(date.getTime() + hours * 3600 * 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: `
:
:

常用时间:  1小时  |  1天  |  3天  |  1周  |  30天  |  1年 

` }) // 启用状态标签 Vue.component("label-on", { props: ["v-is-on"], template: '
已启用已停用
' }) // 文字代码标签 Vue.component("code-label", { methods: { click: function (args) { this.$emit("click", args) } }, template: `` }) Vue.component("code-label-plain", { template: `` }) // tiny标签 Vue.component("tiny-label", { template: `` }) Vue.component("tiny-basic-label", { template: `` }) // 更小的标签 Vue.component("micro-basic-label", { template: `` }) // 灰色的Label Vue.component("grey-label", { props: ["color"], data: function () { let color = "grey" if (this.color != null && this.color.length > 0) { color = "red" } return { labelColor: color } }, template: `` }) // 可选标签 Vue.component("optional-label", { template: `(可选)` }) // Plus专属 Vue.component("plus-label", { template: `Plus专属功能。` }) // 提醒设置项为专业设置 Vue.component("pro-warning-label", { template: `注意:通常不需要修改;如要修改,请在专家指导下进行。` }) Vue.component("js-page", { props: ["v-max"], data: function () { let max = this.vMax if (max == null) { max = 0 } return { max: max, page: 1 } }, methods: { updateMax: function (max) { this.max = max }, selectPage: function(page) { this.page = page this.$emit("change", page) } }, template:`
` }) /** * 一级菜单 */ Vue.component("first-menu", { props: [], template: ' \
\ \
\
' }); /** * 更多选项 */ Vue.component("more-options-indicator", { props:[], data: function () { return { visible: false } }, methods: { changeVisible: function () { this.visible = !this.visible if (Tea.Vue != null) { Tea.Vue.moreOptionsVisible = this.visible } this.$emit("change", this.visible) this.$emit("input", this.visible) } }, template: '更多选项收起选项 ' }); 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: `` }) /** * 二级菜单 */ Vue.component("second-menu", { template: ' \
\ \
\
' }); Vue.component("loading-message", { template: `
 
` }) Vue.component("file-textarea", { props: ["value"], data: function () { let value = this.value if (typeof value != "string") { value = "" } return { realValue: value } }, mounted: function () { }, methods: { dragover: function () {}, drop: function (e) { let that = this e.dataTransfer.items[0].getAsFile().text().then(function (data) { that.setValue(data) }) }, setValue: function (value) { this.realValue = value }, focus: function () { this.$refs.textarea.focus() } }, template: `` }) Vue.component("more-options-angle", { data: function () { return { isVisible: false } }, methods: { show: function () { this.isVisible = !this.isVisible this.$emit("change", this.isVisible) } }, template: `更多选项收起选项` }) Vue.component("columns-grid", { props: [], mounted: function () { this.columns = this.calculateColumns() let that = this window.addEventListener("resize", function () { that.columns = that.calculateColumns() }) }, data: function () { return { columns: "four" } }, methods: { calculateColumns: function () { let w = window.innerWidth let columns = Math.floor(w / 250) if (columns == 0) { columns = 1 } let columnElements = this.$el.getElementsByClassName("column") if (columnElements.length == 0) { return } let maxColumns = columnElements.length if (columns > maxColumns) { columns = maxColumns } // 添加右侧边框 for (let index = 0; index < columnElements.length; index++) { let el = columnElements[index] el.className = el.className.replace("with-border", "") if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) { el.className += " with-border" } } switch (columns) { case 1: return "one" case 2: return "two" case 3: return "three" case 4: return "four" case 5: return "five" case 6: return "six" case 7: return "seven" case 8: return "eight" case 9: return "nine" case 10: return "ten" default: return "ten" } } }, template: `
` }) /** * 菜单项 */ Vue.component("inner-menu-item", { props: ["href", "active", "code"], data: function () { var active = this.active; if (typeof(active) =="undefined") { var itemCode = ""; if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") { itemCode = window.TEA.ACTION.data.firstMenuItem; } active = (itemCode == this.code); } return { vHref: (this.href == null) ? "" : this.href, vActive: active }; }, template: '\ [] \ ' }); Vue.component("bandwidth-size-capacity-box", { props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"], data: function () { let v = this.vValue if (v == null) { v = { count: this.vCount, unit: this.vUnit } } if (v.unit == null || v.unit.length == 0) { v.unit = "mb" } if (typeof (v["count"]) != "number") { v["count"] = -1 } let vSize = this.size if (vSize == null) { vSize = 6 } let vMaxlength = this.maxlength if (vMaxlength == null) { vMaxlength = 10 } let supportedUnits = this.vSupportedUnits if (supportedUnits == null) { supportedUnits = [] } return { capacity: v, countString: (v.count >= 0) ? v.count.toString() : "", vSize: vSize, vMaxlength: vMaxlength, supportedUnits: supportedUnits } }, watch: { "countString": function (newValue) { let value = newValue.trim() if (value.length == 0) { this.capacity.count = -1 this.change() return } let count = parseInt(value) if (!isNaN(count)) { this.capacity.count = count } this.change() } }, methods: { change: function () { this.$emit("change", this.capacity) } }, template: `
` }) Vue.component("health-check-config-box", { props: ["v-health-check-config", "v-check-domain-url", "v-is-plus"], data: function () { let healthCheckConfig = this.vHealthCheckConfig let urlProtocol = "http" let urlPort = "" let urlRequestURI = "/" let urlHost = "" if (healthCheckConfig == null) { healthCheckConfig = { isOn: false, url: "", interval: {count: 60, unit: "second"}, statusCodes: [200], timeout: {count: 10, unit: "second"}, countTries: 3, tryDelay: {count: 100, unit: "ms"}, autoDown: true, countUp: 1, countDown: 3, userAgent: "", onlyBasicRequest: true, accessLogIsOn: true } let that = this setTimeout(function () { that.changeURL() }, 500) } else { try { let url = new URL(healthCheckConfig.url) urlProtocol = url.protocol.substring(0, url.protocol.length - 1) // 域名 urlHost = url.host if (urlHost == "%24%7Bhost%7D") { urlHost = "${host}" } let colonIndex = urlHost.indexOf(":") if (colonIndex > 0) { urlHost = urlHost.substring(0, colonIndex) } urlPort = url.port urlRequestURI = url.pathname if (url.search.length > 0) { urlRequestURI += url.search } } catch (e) { } if (healthCheckConfig.statusCodes == null) { healthCheckConfig.statusCodes = [200] } if (healthCheckConfig.interval == null) { healthCheckConfig.interval = {count: 60, unit: "second"} } if (healthCheckConfig.timeout == null) { healthCheckConfig.timeout = {count: 10, unit: "second"} } if (healthCheckConfig.tryDelay == null) { healthCheckConfig.tryDelay = {count: 100, unit: "ms"} } if (healthCheckConfig.countUp == null || healthCheckConfig.countUp < 1) { healthCheckConfig.countUp = 1 } if (healthCheckConfig.countDown == null || healthCheckConfig.countDown < 1) { healthCheckConfig.countDown = 3 } } return { healthCheck: healthCheckConfig, advancedVisible: false, urlProtocol: urlProtocol, urlHost: urlHost, urlPort: urlPort, urlRequestURI: urlRequestURI, urlIsEditing: healthCheckConfig.url.length == 0, hostErr: "" } }, watch: { urlRequestURI: function () { if (this.urlRequestURI.length > 0 && this.urlRequestURI[0] != "/") { this.urlRequestURI = "/" + this.urlRequestURI } this.changeURL() }, urlPort: function (v) { let port = parseInt(v) if (!isNaN(port)) { this.urlPort = port.toString() } else { this.urlPort = "" } this.changeURL() }, urlProtocol: function () { this.changeURL() }, urlHost: function () { this.changeURL() this.hostErr = "" }, "healthCheck.countTries": function (v) { let count = parseInt(v) if (!isNaN(count)) { this.healthCheck.countTries = count } else { this.healthCheck.countTries = 0 } }, "healthCheck.countUp": function (v) { let count = parseInt(v) if (!isNaN(count)) { this.healthCheck.countUp = count } else { this.healthCheck.countUp = 0 } }, "healthCheck.countDown": function (v) { let count = parseInt(v) if (!isNaN(count)) { this.healthCheck.countDown = count } else { this.healthCheck.countDown = 0 } } }, methods: { showAdvanced: function () { this.advancedVisible = !this.advancedVisible }, changeURL: function () { let urlHost = this.urlHost if (urlHost.length == 0) { urlHost = "${host}" } this.healthCheck.url = this.urlProtocol + "://" + urlHost + ((this.urlPort.length > 0) ? ":" + this.urlPort : "") + this.urlRequestURI }, changeStatus: function (values) { this.healthCheck.statusCodes = values.$map(function (k, v) { let status = parseInt(v) if (isNaN(status)) { return 0 } else { return status } }) }, onChangeURLHost: function () { let checkDomainURL = this.vCheckDomainUrl if (checkDomainURL == null || checkDomainURL.length == 0) { return } let that = this Tea.action(checkDomainURL) .params({host: this.urlHost}) .success(function (resp) { if (!resp.data.isOk) { that.hostErr = "在当前集群中找不到此域名,可能会影响健康检查结果。" } else { that.hostErr = "" } }) .post() }, editURL: function () { this.urlIsEditing = !this.urlIsEditing } }, template: `
启用健康检查

通过访问节点上的网站URL来确定节点是否健康。

检测URL *
{{healthCheck.url}}   修改
协议
域名

{{hostErr}}已经部署到当前集群的一个域名;如果为空则使用节点IP作为域名。如果协议是https,这里必须填写一个已经设置了SSL证书的域名。

端口

域名或者IP的端口,可选项,默认为80/443。

RequestURI

请求的路径,可以带参数,可选项。

拼接后的检测URL:{{healthCheck.url}},其中\${host}指的是域名。

检测时间间隔

两次检查之间的间隔。

自动下线IP

选中后系统会根据健康检查的结果自动标记节点IP节点的上线/下线状态,并可能自动同步DNS设置。注意:免费版的只能整体上下线整个节点,商业版的可以下线单个IP。

连续上线次数

连续{{healthCheck.countUp}}次检查成功后自动恢复上线。

连续下线次数

连续{{healthCheck.countDown}}次检查失败后自动下线。

允许的状态码

允许检测URL返回的状态码列表。

超时时间

读取检测URL超时时间。

连续尝试次数

如果读取检测URL失败后需要再次尝试的次数。

每次尝试间隔

如果读取检测URL失败后再次尝试时的间隔时间。

终端信息(User-Agent)

发送到服务器的User-Agent值,不填写表示使用默认值。

只基础请求

只做基础的请求,不处理反向代理(不检查源站)、WAF等。

记录访问日志

记录健康检查的访问日志。

` }) // 将变量转换为中文 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: ` {{v.code}} - {{v.name}} ` }) Vue.component("combo-box", { // data-url 和 data-key 成对出现 props: [ "name", "title", "placeholder", "size", "v-items", "v-value", "data-url", // 数据源URL "data-key", // 数据源中数据的键名 "data-search", // 是否启用动态搜索,如果值为on或true,则表示启用 "width" ], mounted: function () { if (this.dataURL.length > 0) { this.search("") } // 设定菜单宽度 let searchBox = this.$refs.searchBox if (searchBox != null) { let inputWidth = searchBox.offsetWidth if (inputWidth != null && inputWidth > 0) { this.$refs.menu.style.width = inputWidth + "px" } else if (this.styleWidth.length > 0) { this.$refs.menu.style.width = this.styleWidth } } }, data: function () { let items = this.vItems if (items == null || !(items instanceof Array)) { items = [] } items = this.formatItems(items) // 当前选中项 let selectedItem = null if (this.vValue != null) { let that = this items.forEach(function (v) { if (v.value == that.vValue) { selectedItem = v } }) } let width = this.width if (width == null || width.length == 0) { width = "11em" } else { if (/\d+$/.test(width)) { width += "em" } } // data url let dataURL = "" if (typeof this.dataUrl == "string" && this.dataUrl.length > 0) { dataURL = this.dataUrl } return { allItems: items, // 原始的所有的items items: items.$copy(), // 候选的items selectedItem: selectedItem, // 选中的item keyword: "", visible: false, hideTimer: null, hoverIndex: 0, styleWidth: width, isInitial: true, dataURL: dataURL, urlRequestId: 0 // 记录URL请求ID,防止并行冲突 } }, methods: { search: function (keyword) { // 从URL中获取选项数据 let dataUrl = this.dataURL let dataKey = this.dataKey let that = this let requestId = Math.random() this.urlRequestId = requestId Tea.action(dataUrl) .params({ keyword: (keyword == null) ? "" : keyword }) .post() .success(function (resp) { if (requestId != that.urlRequestId) { return } if (resp.data != null) { if (typeof (resp.data[dataKey]) == "object") { let items = that.formatItems(resp.data[dataKey]) that.allItems = items that.items = items.$copy() if (that.isInitial) { that.isInitial = false if (that.vValue != null) { items.forEach(function (v) { if (v.value == that.vValue) { that.selectedItem = v } }) } } } } }) }, formatItems: function (items) { items.forEach(function (v) { if (v.value == null) { v.value = v.id } }) return items }, reset: function () { this.selectedItem = null this.change() this.hoverIndex = 0 let that = this setTimeout(function () { if (that.$refs.searchBox) { that.$refs.searchBox.focus() } }) }, clear: function () { this.selectedItem = null this.change() this.hoverIndex = 0 }, changeKeyword: function () { let shouldSearch = this.dataURL.length > 0 && (this.dataSearch == "on" || this.dataSearch == "true") this.hoverIndex = 0 let keyword = this.keyword if (keyword.length == 0) { if (shouldSearch) { this.search(keyword) } else { this.items = this.allItems.$copy() } return } if (shouldSearch) { this.search(keyword) } else { this.items = this.allItems.$copy().filter(function (v) { if (v.fullname != null && v.fullname.length > 0 && teaweb.match(v.fullname, keyword)) { return true } 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) let that = this setTimeout(function () { if (that.$refs.selectedLabel != null) { that.$refs.selectedLabel.focus() } }) }, submitForm: function (event) { if (event.target.tagName != "A") { return } let parentBox = this.$refs.selectedLabel.parentNode while (true) { parentBox = parentBox.parentNode if (parentBox == null || parentBox.tagName == "BODY") { return } if (parentBox.tagName == "FORM") { parentBox.submit() break } } }, setDataURL: function (dataURL) { this.dataURL = dataURL }, reloadData: function () { this.search("") } }, template: `
{{title}}:{{selectedItem.name}}
` }) Vue.component("search-box", { props: ["placeholder", "width"], data: function () { let width = this.width if (width == null) { width = "10em" } return { realWidth: width, realValue: "" } }, methods: { onInput: function () { this.$emit("input", { value: this.realValue}) this.$emit("change", { value: this.realValue}) }, clearValue: function () { this.realValue = "" this.focus() this.onInput() }, focus: function () { this.$refs.valueRef.focus() } }, template: `
` }) Vue.component("dot", { template: '' }) Vue.component("time-duration-box", { props: ["v-name", "v-value", "v-count", "v-unit", "placeholder", "v-min-unit", "maxlength"], mounted: function () { this.change() }, data: function () { let v = this.vValue if (v == null) { v = { count: this.vCount, unit: this.vUnit } } if (typeof (v["count"]) != "number") { v["count"] = -1 } let minUnit = this.vMinUnit let units = [ { code: "ms", name: "毫秒" }, { code: "second", name: "秒" }, { code: "minute", name: "分钟" }, { code: "hour", name: "小时" }, { code: "day", name: "天" } ] let minUnitIndex = -1 if (minUnit != null && typeof minUnit == "string" && minUnit.length > 0) { for (let i = 0; i < units.length; i++) { if (units[i].code == minUnit) { minUnitIndex = i break } } } if (minUnitIndex > -1) { units = units.slice(minUnitIndex) } let maxLength = parseInt(this.maxlength) if (typeof maxLength != "number") { maxLength = 10 } return { duration: v, countString: (v.count >= 0) ? v.count.toString() : "", units: units, realMaxLength: maxLength } }, watch: { "countString": function (newValue) { let value = newValue.trim() if (value.length == 0) { this.duration.count = -1 return } let count = parseInt(value) if (!isNaN(count)) { this.duration.count = count } this.change() } }, methods: { change: function () { this.$emit("change", this.duration) } }, template: `
` }) Vue.component("time-duration-text", { props: ["v-value"], methods: { unitName: function (unit) { switch (unit) { case "ms": return "毫秒" case "second": return "秒" case "minute": return "分钟" case "hour": return "小时" case "day": return "天" } } }, template: ` {{vValue.count}} {{unitName(vValue.unit)}} ` }) Vue.component("not-found-box", { props: ["message"], template: `

{{message}}

` }) // 警告消息 Vue.component("warning-message", { template: `
` }) let checkboxId = 0 Vue.component("checkbox", { props: ["name", "value", "v-value", "id", "checked"], data: function () { checkboxId++ let elementId = this.id if (elementId == null) { elementId = "checkbox" + checkboxId } let elementValue = this.vValue if (elementValue == null) { elementValue = "1" } let checkedValue = this.value if (checkedValue == null && this.checked == "checked") { checkedValue = elementValue } return { elementId: elementId, elementValue: elementValue, newValue: checkedValue } }, methods: { change: function () { this.$emit("input", this.newValue) }, check: function () { this.newValue = this.elementValue }, uncheck: function () { this.newValue = "" }, isChecked: function () { return (typeof (this.newValue) == "boolean" && this.newValue) || this.newValue == this.elementValue } }, watch: { value: function (v) { if (typeof v == "boolean") { this.newValue = v } } }, template: `
` }) Vue.component("network-addresses-view", { props: ["v-addresses"], template: `
{{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}}
` }) Vue.component("url-patterns-box", { props: ["value"], data: function () { let patterns = [] if (this.value != null) { patterns = this.value } return { patterns: patterns, isAdding: false, addingPattern: {"type": "wildcard", "pattern": ""}, editingIndex: -1, patternIsInvalid: false, windowIsSmall: window.innerWidth < 600 } }, methods: { add: function () { this.isAdding = true let that = this setTimeout(function () { that.$refs.patternInput.focus() }) }, edit: function (index) { this.isAdding = true this.editingIndex = index this.addingPattern = { type: this.patterns[index].type, pattern: this.patterns[index].pattern } }, confirm: function () { if (this.requireURL(this.addingPattern.type)) { let pattern = this.addingPattern.pattern.trim() if (pattern.length == 0) { let that = this teaweb.warn("请输入URL", function () { that.$refs.patternInput.focus() }) return } } if (this.editingIndex < 0) { this.patterns.push({ type: this.addingPattern.type, pattern: this.addingPattern.pattern }) } else { this.patterns[this.editingIndex].type = this.addingPattern.type this.patterns[this.editingIndex].pattern = this.addingPattern.pattern } this.notifyChange() this.cancel() }, remove: function (index) { this.patterns.$remove(index) this.cancel() this.notifyChange() }, cancel: function () { this.isAdding = false this.addingPattern = {"type": "wildcard", "pattern": ""} this.editingIndex = -1 }, patternTypeName: function (patternType) { switch (patternType) { case "wildcard": return "通配符" case "regexp": return "正则" case "images": return "常见图片文件" case "audios": return "常见音频文件" case "videos": return "常见视频文件" } return "" }, notifyChange: function () { this.$emit("input", this.patterns) }, changePattern: function () { this.patternIsInvalid = false let pattern = this.addingPattern.pattern switch (this.addingPattern.type) { case "wildcard": if (pattern.indexOf("?") >= 0) { this.patternIsInvalid = true } break case "regexp": if (pattern.indexOf("?") >= 0) { let pieces = pattern.split("?") for (let i = 0; i < pieces.length - 1; i++) { if (pieces[i].length == 0 || pieces[i][pieces[i].length - 1] != "\\") { this.patternIsInvalid = true } } } break } }, requireURL: function (patternType) { return patternType == "wildcard" || patternType == "regexp" } }, template: `
[{{patternTypeName(pattern.type)}}] {{pattern.pattern}}  

通配符正则表达式中不能包含问号(?)及问号以后的内容。

` }) Vue.component("size-capacity-view", { props:["v-default-text", "v-value"], methods: { composeCapacity: function (capacity) { return teaweb.convertSizeCapacityToString(capacity) } }, template: `
{{composeCapacity(vValue)}} {{vDefaultText}}
` }) // 信息提示窗口 Vue.component("tip-message-box", { props: ["code"], mounted: function () { let that = this Tea.action("/ui/showTip") .params({ code: this.code }) .success(function (resp) { that.visible = resp.data.visible }) .post() }, data: function () { return { visible: false } }, methods: { close: function () { this.visible = false Tea.action("/ui/hideTip") .params({ code: this.code }) .post() } }, template: `
` }) Vue.component("digit-input", { props: ["value", "maxlength", "size", "min", "max", "required", "placeholder"], mounted: function () { let that = this setTimeout(function () { that.check() }) }, data: function () { let realMaxLength = this.maxlength if (realMaxLength == null) { realMaxLength = 20 } let realSize = this.size if (realSize == null) { realSize = 6 } return { realValue: this.value, realMaxLength: realMaxLength, realSize: realSize, isValid: true } }, watch: { realValue: function (v) { this.notifyChange() } }, methods: { notifyChange: function () { let v = parseInt(this.realValue.toString(), 10) if (isNaN(v)) { v = 0 } this.check() this.$emit("input", v) }, check: function () { if (this.realValue == null) { return } let s = this.realValue.toString() if (!/^\d+$/.test(s)) { this.isValid = false return } let v = parseInt(s, 10) if (isNaN(v)) { this.isValid = false } else { if (this.required) { this.isValid = (this.min == null || this.min <= v) && (this.max == null || this.max >= v) } else { this.isValid = (v == 0 || (this.min == null || this.min <= v) && (this.max == null || this.max >= v)) } } } }, template: `` }) Vue.component("keyword", { props: ["v-word"], data: function () { let word = this.vWord if (word == null) { word = "" } else { word = word.replace(/\)/g, "\\)") word = word.replace(/\(/g, "\\(") word = word.replace(/\+/g, "\\+") word = word.replace(/\^/g, "\\^") word = word.replace(/\$/g, "\\$") word = word.replace(/\?/g, "\\?") word = word.replace(/\*/g, "\\*") word = word.replace(/\[/g, "\\[") word = word.replace(/{/g, "\\{") word = word.replace(/\./g, "\\.") } let slot = this.$slots["default"][0] let text = slot.text if (word.length > 0) { let that = this let m = [] // replacement => tmp let tmpIndex = 0 text = text.replaceAll(new RegExp("(" + word + ")", "ig"), function (replacement) { tmpIndex++ let s = "" + that.encodeHTML(replacement) + "" let tmpKey = "$TMP__KEY__" + tmpIndex.toString() + "$" m.push([tmpKey, s]) return tmpKey }) text = this.encodeHTML(text) m.forEach(function (r) { text = text.replace(r[0], r[1]) }) } else { text = this.encodeHTML(text) } return { word: word, text: text } }, methods: { encodeHTML: function (s) { s = s.replace(/&/g, "&") s = s.replace(//g, ">") s = s.replace(/"/g, """) return s } }, template: `` }) Vue.component("bits-var", { props: ["v-bits"], data: function () { let bits = this.vBits if (typeof bits != "number") { bits = 0 } let format = teaweb.splitFormat(teaweb.formatBits(bits)) return { format: format } }, template:` {{format[0]}}{{format[1]}} ` }) Vue.component("mask-warning", { template: `为了安全起见,此项数据保存后将不允许在界面查看完整明文,为避免忘记,请自行记录原始数据。` }) Vue.component("chart-columns-grid", { props: [], mounted: function () { this.columns = this.calculateColumns() let that = this window.addEventListener("resize", function () { that.columns = that.calculateColumns() }) }, updated: function () { let totalElements = this.$el.getElementsByClassName("column").length if (totalElements == this.totalElements) { return } this.totalElements = totalElements this.calculateColumns() }, data: function () { return { columns: "four", totalElements: 0 } }, methods: { calculateColumns: function () { let w = window.innerWidth let columns = Math.floor(w / 500) if (columns == 0) { columns = 1 } let columnElements = this.$el.getElementsByClassName("column") if (columnElements.length == 0) { return "one" } let maxColumns = columnElements.length if (columns > maxColumns) { columns = maxColumns } // 添加右侧边框 for (let index = 0; index < columnElements.length; index++) { let el = columnElements[index] el.className = el.className.replace("with-border", "") if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) { el.className += " with-border" } } switch (columns) { case 1: return "one" case 2: return "two" case 3: return "three" case 4: return "four" case 5: return "five" case 6: return "six" case 7: return "seven" case 8: return "eight" case 9: return "nine" case 10: return "ten" default: return "ten" } } }, template: `
` }) Vue.component("bytes-var", { props: ["v-bytes"], data: function () { let bytes = this.vBytes if (typeof bytes != "number") { bytes = 0 } let format = teaweb.splitFormat(teaweb.formatBytes(bytes)) return { format: format } }, template:` {{format[0]}}{{format[1]}} ` }) Vue.component("node-log-row", { props: ["v-log", "v-keyword"], data: function () { return { log: this.vLog, keyword: this.vKeyword } }, template: `
[{{log.createdTime}}][{{log.createdTime}}][{{log.tag}}]{{log.description}}   共{{log.count}}条 {{log.server.name}}
` }) Vue.component("provinces-selector", { props: ["v-provinces"], data: function () { let provinces = this.vProvinces if (provinces == null) { provinces = [] } let provinceIds = provinces.$map(function (k, v) { return v.id }) return { provinces: provinces, provinceIds: provinceIds } }, methods: { add: function () { let provinceStringIds = this.provinceIds.map(function (v) { return v.toString() }) let that = this teaweb.popup("/ui/selectProvincesPopup?provinceIds=" + provinceStringIds.join(","), { width: "48em", height: "23em", callback: function (resp) { that.provinces = resp.data.provinces that.change() } }) }, remove: function (index) { this.provinces.$remove(index) this.change() }, change: function () { this.provinceIds = this.provinces.$map(function (k, v) { return v.id }) } }, template: `
{{province.name}}
` }) Vue.component("csrf-token", { created: function () { this.refreshToken() }, mounted: function () { let that = this this.$refs.token.form.addEventListener("submit", function () { that.refreshToken() }) // 自动刷新 setInterval(function () { that.refreshToken() }, 10 * 60 * 1000) }, data: function () { return { token: "" } }, methods: { refreshToken: function () { let that = this Tea.action("/csrf/token") .get() .success(function (resp) { that.token = resp.data.token }) } }, template: `` }) Vue.component("labeled-input", { props: ["name", "size", "maxlength", "label", "value"], template: '
\ \ {{label}}\
' }); let radioId = 0 Vue.component("radio", { props: ["name", "value", "v-value", "id"], data: function () { radioId++ let elementId = this.id if (elementId == null) { elementId = "radio" + radioId } return { "elementId": elementId } }, methods: { change: function () { this.$emit("input", this.vValue) } }, template: `
` }) Vue.component("copy-to-clipboard", { props: ["v-target"], created: function () { if (typeof ClipboardJS == "undefined") { let jsFile = document.createElement("script") jsFile.setAttribute("src", "/js/clipboard.min.js") document.head.appendChild(jsFile) } }, methods: { copy: function () { new ClipboardJS('[data-clipboard-target]'); teaweb.successToast("已复制到剪切板") } }, template: `` }) // 节点角色名称 Vue.component("node-role-name", { props: ["v-role"], data: function () { let roleName = "" switch (this.vRole) { case "node": roleName = "边缘节点" break case "monitor": roleName = "监控节点" break case "api": roleName = "API节点" break case "user": roleName = "用户平台" break case "admin": roleName = "管理平台" break case "database": roleName = "数据库节点" break case "dns": roleName = "DNS节点" break case "report": roleName = "区域监控终端" break } return { roleName: roleName } }, template: `{{roleName}}` }) let sourceCodeBoxIndex = 0 Vue.component("source-code-box", { props: ["name", "type", "id", "read-only", "width", "height", "focus"], mounted: function () { let readOnly = this.readOnly if (typeof readOnly != "boolean") { readOnly = true } let box = document.getElementById("source-code-box-" + this.index) let valueBox = document.getElementById(this.valueBoxId) let value = "" if (valueBox.textContent != null) { value = valueBox.textContent } else if (valueBox.innerText != null) { value = valueBox.innerText } this.createEditor(box, value, readOnly) }, data: function () { let index = sourceCodeBoxIndex++ let valueBoxId = 'source-code-box-value-' + sourceCodeBoxIndex if (this.id != null) { valueBoxId = this.id } return { index: index, valueBoxId: valueBoxId } }, 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, }) let that = this boxEditor.on("change", function () { that.change(boxEditor.getValue()) }) boxEditor.setValue(value) if (this.focus) { boxEditor.focus() } 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) } } else if (height != null) { height = parseInt(height) if (!isNaN(height)) { boxEditor.setSize("100%", 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) } }, change: function (code) { this.$emit("change", code) } }, template: `
` }) Vue.component("size-capacity-box", { props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"], data: function () { let v = this.vValue if (v == null) { v = { count: this.vCount, unit: this.vUnit } } if (typeof (v["count"]) != "number") { v["count"] = -1 } let vSize = this.size if (vSize == null) { vSize = 6 } let vMaxlength = this.maxlength if (vMaxlength == null) { vMaxlength = 10 } let supportedUnits = this.vSupportedUnits if (supportedUnits == null) { supportedUnits = [] } return { capacity: v, countString: (v.count >= 0) ? v.count.toString() : "", vSize: vSize, vMaxlength: vMaxlength, supportedUnits: supportedUnits } }, watch: { "countString": function (newValue) { let value = newValue.trim() if (value.length == 0) { this.capacity.count = -1 this.change() return } let count = parseInt(value) if (!isNaN(count)) { this.capacity.count = count } this.change() } }, methods: { change: function () { this.$emit("change", this.capacity) } }, template: `
` }) /** * 二级菜单 */ Vue.component("inner-menu", { template: `
` }); Vue.component("datepicker", { props: ["value", "v-name", "name", "v-value", "v-bottom-left", "placeholder"], mounted: function () { let that = this teaweb.datepicker(this.$refs.dayInput, function (v) { that.day = v that.change() }, !!this.vBottomLeft) }, data: function () { let name = this.vName if (name == null) { name = this.name } if (name == null) { name = "day" } let day = this.vValue if (day == null) { day = this.value if (day == null) { day = "" } } let placeholder = "YYYY-MM-DD" if (this.placeholder != null) { placeholder = this.placeholder } return { realName: name, realPlaceholder: placeholder, day: day } }, watch: { value: function (v) { this.day = v let picker = this.$refs.dayInput.picker if (picker != null) { if (v != null && /^\d+-\d+-\d+$/.test(v)) { picker.setDate(v) } } } }, methods: { change: function () { this.$emit("input", this.day) // support v-model,事件触发需要在 change 之前 this.$emit("change", this.day) } }, template: `
` }) // 排序使用的箭头 Vue.component("sort-arrow", { props: ["name"], data: function () { let url = window.location.toString() let order = "" let iconTitle = "" let newArgs = [] if (window.location.search != null && window.location.search.length > 0) { let queryString = window.location.search.substring(1) let pieces = queryString.split("&") let that = this pieces.forEach(function (v) { let eqIndex = v.indexOf("=") if (eqIndex > 0) { let argName = v.substring(0, eqIndex) let argValue = v.substring(eqIndex + 1) if (argName == that.name) { order = argValue } else if (argName != "page" && argValue != "asc" && argValue != "desc") { newArgs.push(v) } } else { newArgs.push(v) } }) } if (order == "asc") { newArgs.push(this.name + "=desc") iconTitle = "当前正序排列" } else if (order == "desc") { newArgs.push(this.name + "=asc") iconTitle = "当前倒序排列" } else { newArgs.push(this.name + "=desc") iconTitle = "当前正序排列" } let qIndex = url.indexOf("?") if (qIndex > 0) { url = url.substring(0, qIndex) + "?" + newArgs.join("&") } else { url = url + "?" + newArgs.join("&") } return { order: order, url: url, iconTitle: iconTitle } }, template: `  ` }) Vue.component("user-link", { props: ["v-user", "v-keyword"], data: function () { let user = this.vUser if (user == null) { user = {id: 0, "username": "", "fullname": ""} } return { user: user } }, template: `
{{user.fullname}}{{user.username}} [已删除]
` }) // 监控节点分组选择 Vue.component("report-node-groups-selector", { props: ["v-group-ids"], mounted: function () { let that = this Tea.action("/clusters/monitors/groups/options") .post() .success(function (resp) { that.groups = resp.data.groups.map(function (group) { group.isChecked = that.groupIds.$contains(group.id) return group }) that.isLoaded = true }) }, data: function () { var groupIds = this.vGroupIds if (groupIds == null) { groupIds = [] } return { groups: [], groupIds: groupIds, isLoaded: false, allGroups: groupIds.length == 0 } }, methods: { check: function (group) { group.isChecked = !group.isChecked this.groupIds = [] let that = this this.groups.forEach(function (v) { if (v.isChecked) { that.groupIds.push(v.id) } }) this.change() }, change: function () { let that = this let groups = [] this.groupIds.forEach(function (groupId) { let group = that.groups.$find(function (k, v) { return v.id == groupId }) if (group == null) { return } groups.push({ id: group.id, name: group.name }) }) this.$emit("change", groups) } }, watch: { allGroups: function (b) { if (b) { this.groupIds = [] this.groups.forEach(function (v) { v.isChecked = false }) } this.change() } }, template: `
还没有分组。
` }) Vue.component("finance-user-selector", { props: ["v-user-id"], data: function () { return {} }, methods: { change: function (userId) { this.$emit("change", userId) } }, template: `
` }) Vue.component("node-cache-disk-dirs-box", { props: ["value", "name"], data: function () { let dirs = this.value if (dirs == null) { dirs = [] } return { dirs: dirs, isEditing: false, isAdding: false, addingPath: "" } }, methods: { add: function () { this.isAdding = true let that = this setTimeout(function () { that.$refs.addingPath.focus() }, 100) }, confirm: function () { let addingPath = this.addingPath.trim() if (addingPath.length == 0) { let that = this teaweb.warn("请输入要添加的缓存目录", function () { that.$refs.addingPath.focus() }) return } if (addingPath[0] != "/") { addingPath = "/" + addingPath } this.dirs.push({ path: addingPath }) this.cancel() }, cancel: function () { this.addingPath = "" this.isAdding = false this.isEditing = false }, remove: function (index) { let that = this teaweb.confirm("确定要删除此目录吗?", function () { that.dirs.$remove(index) }) } }, template: `
{{dir.path}}  
 
` }) Vue.component("node-ip-address-clusters-selector", { props: ["vClusters"], mounted: function () { this.checkClusters() }, data: function () { let clusters = this.vClusters if (clusters == null) { clusters = [] } return { clusters: clusters, hasCheckedCluster: false, clustersVisible: false } }, methods: { checkClusters: function () { let that = this let b = false this.clusters.forEach(function (cluster) { if (cluster.isChecked) { b = true } }) this.hasCheckedCluster = b return b }, changeCluster: function (cluster) { cluster.isChecked = !cluster.isChecked this.checkClusters() }, showClusters: function () { this.clustersVisible = !this.clustersVisible } }, template: `
默认用于所有集群   修改
{{cluster.name}}   修改

当前IP仅在所选集群中有效。

{{cluster.name}}
` }) // 节点登录推荐端口 Vue.component("node-login-suggest-ports", { data: function () { return { ports: [], availablePorts: [], autoSelected: false, isLoading: false } }, methods: { reload: function (host) { let that = this this.autoSelected = false this.isLoading = true Tea.action("/clusters/cluster/suggestLoginPorts") .params({ host: host }) .success(function (resp) { if (resp.data.availablePorts != null) { that.availablePorts = resp.data.availablePorts if (that.availablePorts.length > 0) { that.autoSelectPort(that.availablePorts[0]) that.autoSelected = true } } if (resp.data.ports != null) { that.ports = resp.data.ports if (that.ports.length > 0 && !that.autoSelected) { that.autoSelectPort(that.ports[0]) that.autoSelected = true } } }) .done(function () { that.isLoading = false }) .post() }, selectPort: function (port) { this.$emit("select", port) }, autoSelectPort: function (port) { this.$emit("auto-select", port) } }, template: ` 正在检查端口... 可能端口:{{port}}     常用端口:{{port}} 常用端口有22等。 (可以点击要使用的端口) ` }) Vue.component("node-group-selector", { props: ["v-cluster-id", "v-group"], data: function () { return { selectedGroup: this.vGroup } }, methods: { selectGroup: function () { let that = this teaweb.popup("/clusters/cluster/groups/selectPopup?clusterId=" + this.vClusterId, { callback: function (resp) { that.selectedGroup = resp.data.group } }) }, addGroup: function () { let that = this teaweb.popup("/clusters/cluster/groups/createPopup?clusterId=" + this.vClusterId, { callback: function (resp) { that.selectedGroup = resp.data.group } }) }, removeGroup: function () { this.selectedGroup = null } }, template: `
{{selectedGroup.name}}  
[选择分组]   [添加分组]
` }) // 节点IP地址管理(标签形式) Vue.component("node-ip-addresses-box", { props: ["v-ip-addresses", "role", "v-node-id"], data: function () { let nodeId = this.vNodeId if (nodeId == null) { nodeId = 0 } return { ipAddresses: (this.vIpAddresses == null) ? [] : this.vIpAddresses, supportThresholds: this.role != "ns", nodeId: nodeId } }, methods: { // 添加IP地址 addIPAddress: function () { window.UPDATING_NODE_IP_ADDRESS = null let that = this; teaweb.popup("/nodes/ipAddresses/createPopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), { callback: function (resp) { that.ipAddresses.push(resp.data.ipAddress); }, height: "24em", width: "44em" }) }, // 修改地址 updateIPAddress: function (index, address) { window.UPDATING_NODE_IP_ADDRESS = teaweb.clone(address) let that = this; teaweb.popup("/nodes/ipAddresses/updatePopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), { callback: function (resp) { Vue.set(that.ipAddresses, index, resp.data.ipAddress); }, height: "24em", width: "44em" }) }, // 删除IP地址 removeIPAddress: function (index) { this.ipAddresses.$remove(index); }, // 判断是否为IPv6 isIPv6: function (ip) { return ip.indexOf(":") > -1 } }, template: `
[IPv6] {{address.ip}} (备注:{{address.name}},不可访问 (不可访问) [off] [down] [{{address.thresholds.length}}个阈值]     专属集群:[{{cluster.name}}]  
` }) Vue.component("node-schedule-conds-box", { props: ["value", "v-params", "v-operators"], mounted: function () { this.formatConds(this.condsConfig.conds) this.$forceUpdate() }, data: function () { let condsConfig = this.value if (condsConfig == null) { condsConfig = { conds: [], connector: "and" } } if (condsConfig.conds == null) { condsConfig.conds = [] } let paramMap = {} this.vParams.forEach(function (param) { paramMap[param.code] = param }) let operatorMap = {} this.vOperators.forEach(function (operator) { operatorMap[operator.code] = operator.name }) return { condsConfig: condsConfig, params: this.vParams, paramMap: paramMap, operatorMap: operatorMap, operator: "", isAdding: false, paramCode: "", param: null, valueBandwidth: { count: 100, unit: "mb" }, valueTraffic: { count: 1, unit: "gb" }, valueCPU: 80, valueMemory: 90, valueLoad: 20, valueRate: 0 } }, watch: { paramCode: function (code) { if (code.length == 0) { this.param = null } else { this.param = this.params.$find(function (k, v) { return v.code == code }) } this.$emit("changeparam", this.param) } }, methods: { add: function () { this.isAdding = true }, confirm: function () { if (this.param == null) { teaweb.warn("请选择参数") return } if (this.param.operators != null && this.param.operators.length > 0 && this.operator.length == 0) { teaweb.warn("请选择操作符") return } if (this.param.operators == null || this.param.operators.length == 0) { this.operator = "" } let value = null switch (this.param.valueType) { case "bandwidth": { if (this.valueBandwidth.unit.length == 0) { teaweb.warn("请选择带宽单位") return } let count = parseInt(this.valueBandwidth.count.toString()) if (isNaN(count)) { count = 0 } if (count < 0) { count = 0 } value = { count: count, unit: this.valueBandwidth.unit } } break case "traffic": { if (this.valueTraffic.unit.length == 0) { teaweb.warn("请选择带宽单位") return } let count = parseInt(this.valueTraffic.count.toString()) if (isNaN(count)) { count = 0 } if (count < 0) { count = 0 } value = { count: count, unit: this.valueTraffic.unit } } break case "cpu": let cpu = parseInt(this.valueCPU.toString()) if (isNaN(cpu)) { cpu = 0 } if (cpu < 0) { cpu = 0 } if (cpu > 100) { cpu = 100 } value = cpu break case "memory": let memory = parseInt(this.valueMemory.toString()) if (isNaN(memory)) { memory = 0 } if (memory < 0) { memory = 0 } if (memory > 100) { memory = 100 } value = memory break case "load": let load = parseInt(this.valueLoad.toString()) if (isNaN(load)) { load = 0 } if (load < 0) { load = 0 } value = load break case "rate": let rate = parseInt(this.valueRate.toString()) if (isNaN(rate)) { rate = 0 } if (rate < 0) { rate = 0 } value = rate break } this.condsConfig.conds.push({ param: this.param.code, operator: this.operator, value: value }) this.formatConds(this.condsConfig.conds) this.cancel() }, cancel: function () { this.isAdding = false this.paramCode = "" this.param = null }, remove: function (index) { this.condsConfig.conds.$remove(index) }, formatConds: function (conds) { let that = this conds.forEach(function (cond) { switch (that.paramMap[cond.param].valueType) { case "bandwidth": cond.valueFormat = cond.value.count + cond.value.unit[0].toUpperCase() + cond.value.unit.substring(1) + "ps" return case "traffic": cond.valueFormat = cond.value.count + cond.value.unit.toUpperCase() return case "cpu": cond.valueFormat = cond.value + "%" return case "memory": cond.valueFormat = cond.value + "%" return case "load": cond.valueFormat = cond.value return case "rate": cond.valueFormat = cond.value + "/秒" return } }) } }, template: `
{{paramMap[cond.param].name}} {{operatorMap[cond.operator]}} {{cond.valueFormat}}     
参数

{{param.description}}

操作符
{{param.valueName}}
%
%
/秒
  取消
` }) Vue.component("node-schedule-action-box", { props: ["value", "v-actions"], data: function () { let actionConfig = this.value if (actionConfig == null) { actionConfig = { code: "", params: {} } } return { actions: this.vActions, currentAction: null, actionConfig: actionConfig } }, watch: { "actionConfig.code": function (actionCode) { if (actionCode.length == 0) { this.currentAction = null } else { this.currentAction = this.actions.$find(function (k, v) { return v.code == actionCode }) } this.actionConfig.params = {} } }, template: `

{{currentAction.description}}

接收通知的URL。

` }) // 节点IP阈值 Vue.component("node-ip-address-thresholds-view", { props: ["v-thresholds"], data: function () { let thresholds = this.vThresholds if (thresholds == null) { thresholds = [] } else { thresholds.forEach(function (v) { if (v.items == null) { v.items = [] } if (v.actions == null) { v.actions = [] } }) } return { thresholds: thresholds, allItems: window.IP_ADDR_THRESHOLD_ITEMS, allOperators: [ { "name": "小于等于", "code": "lte" }, { "name": "大于", "code": "gt" }, { "name": "不等于", "code": "neq" }, { "name": "小于", "code": "lt" }, { "name": "大于等于", "code": "gte" } ], allActions: window.IP_ADDR_THRESHOLD_ACTIONS } }, methods: { itemName: function (item) { let result = "" this.allItems.forEach(function (v) { if (v.code == item) { result = v.name } }) return result }, itemUnitName: function (itemCode) { let result = "" this.allItems.forEach(function (v) { if (v.code == itemCode) { result = v.unit } }) return result }, itemDurationUnitName: function (unit) { switch (unit) { case "minute": return "分钟" case "second": return "秒" case "hour": return "小时" case "day": return "天" } return unit }, itemOperatorName: function (operator) { let result = "" this.allOperators.forEach(function (v) { if (v.code == operator) { result = v.name } }) return result }, actionName: function (actionCode) { let result = "" this.allActions.forEach(function (v) { if (v.code == actionCode) { result = v.name } }) return result } }, template: `
[{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}] {{itemName(item.item)}} 成功 失败 [{{group.name}}   ] [{{itemOperatorName(item.operator)}}] {{item.value}}{{itemUnitName(item.item)}}   AND   -> {{actionName(action.action)}} 到{{action.options.ips.join(", ")}} ({{action.options.url}})   AND  
` }) // 节点IP阈值 Vue.component("node-ip-address-thresholds-box", { props: ["v-thresholds"], data: function () { let thresholds = this.vThresholds if (thresholds == null) { thresholds = [] } else { thresholds.forEach(function (v) { if (v.items == null) { v.items = [] } if (v.actions == null) { v.actions = [] } }) } return { editingIndex: -1, thresholds: thresholds, addingThreshold: { items: [], actions: [] }, isAdding: false, isAddingItem: false, isAddingAction: false, itemCode: "nodeAvgRequests", itemReportGroups: [], itemOperator: "lte", itemValue: "", itemDuration: "5", allItems: window.IP_ADDR_THRESHOLD_ITEMS, allOperators: [ { "name": "小于等于", "code": "lte" }, { "name": "大于", "code": "gt" }, { "name": "不等于", "code": "neq" }, { "name": "小于", "code": "lt" }, { "name": "大于等于", "code": "gte" } ], allActions: window.IP_ADDR_THRESHOLD_ACTIONS, actionCode: "up", actionBackupIPs: "", actionWebHookURL: "" } }, methods: { add: function () { this.isAdding = !this.isAdding }, cancel: function () { this.isAdding = false this.editingIndex = -1 this.addingThreshold = { items: [], actions: [] } }, confirm: function () { if (this.addingThreshold.items.length == 0) { teaweb.warn("需要至少添加一个阈值") return } if (this.addingThreshold.actions.length == 0) { teaweb.warn("需要至少添加一个动作") return } if (this.editingIndex >= 0) { this.thresholds[this.editingIndex].items = this.addingThreshold.items this.thresholds[this.editingIndex].actions = this.addingThreshold.actions } else { this.thresholds.push({ items: this.addingThreshold.items, actions: this.addingThreshold.actions }) } // 还原 this.cancel() }, remove: function (index) { this.cancel() this.thresholds.$remove(index) }, update: function (index) { this.editingIndex = index this.addingThreshold = { items: this.thresholds[index].items.$copy(), actions: this.thresholds[index].actions.$copy() } this.isAdding = true }, addItem: function () { this.isAddingItem = !this.isAddingItem let that = this setTimeout(function () { that.$refs.itemValue.focus() }, 100) }, cancelItem: function () { this.isAddingItem = false this.itemCode = "nodeAvgRequests" this.itmeOperator = "lte" this.itemValue = "" this.itemDuration = "5" this.itemReportGroups = [] }, confirmItem: function () { // 特殊阈值快速添加 if (["nodeHealthCheck"].$contains(this.itemCode)) { if (this.itemValue.toString().length == 0) { teaweb.warn("请选择检查结果") return } let value = parseInt(this.itemValue) if (isNaN(value)) { value = 0 } else if (value < 0) { value = 0 } else if (value > 1) { value = 1 } // 添加 this.addingThreshold.items.push({ item: this.itemCode, operator: this.itemOperator, value: value, duration: 0, durationUnit: "minute", options: {} }) this.cancelItem() return } if (this.itemDuration.length == 0) { let that = this teaweb.warn("请输入统计周期", function () { that.$refs.itemDuration.focus() }) return } let itemDuration = parseInt(this.itemDuration) if (isNaN(itemDuration) || itemDuration <= 0) { teaweb.warn("请输入正确的统计周期", function () { that.$refs.itemDuration.focus() }) return } if (this.itemValue.length == 0) { let that = this teaweb.warn("请输入对比值", function () { that.$refs.itemValue.focus() }) return } let itemValue = parseFloat(this.itemValue) if (isNaN(itemValue)) { teaweb.warn("请输入正确的对比值", function () { that.$refs.itemValue.focus() }) return } let options = {} switch (this.itemCode) { case "connectivity": // 连通性校验 if (itemValue > 100) { let that = this teaweb.warn("连通性对比值不能超过100", function () { that.$refs.itemValue.focus() }) return } options["groups"] = this.itemReportGroups break } // 添加 this.addingThreshold.items.push({ item: this.itemCode, operator: this.itemOperator, value: itemValue, duration: itemDuration, durationUnit: "minute", options: options }) // 还原 this.cancelItem() }, removeItem: function (index) { this.cancelItem() this.addingThreshold.items.$remove(index) }, changeReportGroups: function (groups) { this.itemReportGroups = groups }, itemName: function (item) { let result = "" this.allItems.forEach(function (v) { if (v.code == item) { result = v.name } }) return result }, itemUnitName: function (itemCode) { let result = "" this.allItems.forEach(function (v) { if (v.code == itemCode) { result = v.unit } }) return result }, itemDurationUnitName: function (unit) { switch (unit) { case "minute": return "分钟" case "second": return "秒" case "hour": return "小时" case "day": return "天" } return unit }, itemOperatorName: function (operator) { let result = "" this.allOperators.forEach(function (v) { if (v.code == operator) { result = v.name } }) return result }, addAction: function () { this.isAddingAction = !this.isAddingAction }, cancelAction: function () { this.isAddingAction = false this.actionCode = "up" this.actionBackupIPs = "" this.actionWebHookURL = "" }, confirmAction: function () { this.doConfirmAction(false) }, doConfirmAction: function (validated, options) { // 是否已存在 let exists = false let that = this this.addingThreshold.actions.forEach(function (v) { if (v.action == that.actionCode) { exists = true } }) if (exists) { teaweb.warn("此动作已经添加过了,无需重复添加") return } if (options == null) { options = {} } switch (this.actionCode) { case "switch": if (!validated) { Tea.action("/ui/validateIPs") .params({ "ips": this.actionBackupIPs }) .success(function (resp) { if (resp.data.ips.length == 0) { teaweb.warn("请输入备用IP", function () { that.$refs.actionBackupIPs.focus() }) return } options["ips"] = resp.data.ips that.doConfirmAction(true, options) }) .fail(function (resp) { teaweb.warn("输入的IP '" + resp.data.failIP + "' 格式不正确,请改正后提交", function () { that.$refs.actionBackupIPs.focus() }) }) .post() return } break case "webHook": if (this.actionWebHookURL.length == 0) { teaweb.warn("请输入WebHook URL", function () { that.$refs.webHookURL.focus() }) return } if (!this.actionWebHookURL.match(/^(http|https):\/\//i)) { teaweb.warn("URL开头必须是http://或者https://", function () { that.$refs.webHookURL.focus() }) return } options["url"] = this.actionWebHookURL } this.addingThreshold.actions.push({ action: this.actionCode, options: options }) // 还原 this.cancelAction() }, removeAction: function (index) { this.cancelAction() this.addingThreshold.actions.$remove(index) }, actionName: function (actionCode) { let result = "" this.allActions.forEach(function (v) { if (v.code == actionCode) { result = v.name } }) return result } }, template: `
[{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}] {{itemName(item.item)}} 成功 失败 [{{group.name}}   ] [{{itemOperatorName(item.operator)}}]  {{item.value}}{{itemUnitName(item.item)}}  AND   -> {{actionName(action.action)}} 到{{action.options.ips.join(", ")}} ({{action.options.url}})  AND    
阈值 动作
[{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}] {{itemName(item.item)}} 成功 失败 [{{group.name}}   ] [{{itemOperatorName(item.operator)}}] {{item.value}}{{itemUnitName(item.item)}}  
统计项目

{{item.description}}

统计周期
分钟
操作符
对比值
{{item.unit}}
检查结果

只有状态发生改变的时候才会触发。

终端分组
 
{{actionName(action.action)}}   到{{action.options.ips.join(", ")}} ({{action.options.url}})
动作类型

{{action.description}}

备用IP *

每行一个备用IP。

URL *

完整的URL,比如https://example.com/webhook/api,系统会在触发阈值的时候通过GET调用此URL。

 
 
` }) Vue.component("node-region-selector", { props: ["v-region"], data: function () { return { selectedRegion: this.vRegion } }, methods: { selectRegion: function () { let that = this teaweb.popup("/clusters/regions/selectPopup?clusterId=" + this.vClusterId, { callback: function (resp) { that.selectedRegion = resp.data.region } }) }, addRegion: function () { let that = this teaweb.popup("/clusters/regions/createPopup?clusterId=" + this.vClusterId, { callback: function (resp) { that.selectedRegion = resp.data.region } }) }, removeRegion: function () { this.selectedRegion = null } }, template: `
{{selectedRegion.name}}  
[选择区域]   [添加区域]
` }) Vue.component("node-combo-box", { props: ["v-cluster-id", "v-node-id"], data: function () { let that = this Tea.action("/clusters/nodeOptions") .params({ clusterId: this.vClusterId }) .post() .success(function (resp) { that.nodes = resp.data.nodes }) return { nodes: [] } }, template: `
` }) // 节点级别选择器 Vue.component("node-level-selector", { props: ["v-node-level"], data: function () { let levelCode = this.vNodeLevel if (levelCode == null || levelCode < 1) { levelCode = 1 } return { levels: [ { name: "边缘节点", code: 1, description: "普通的边缘节点。" }, { name: "L2节点", code: 2, description: "特殊的边缘节点,同时负责同组上一级节点的回源。" } ], levelCode: levelCode } }, watch: { levelCode: function (code) { this.$emit("change", code) } }, template: `

{{levels[levelCode - 1].description}}

` }) Vue.component("node-schedule-conds-viewer", { props: ["value", "v-params", "v-operators"], mounted: function () { this.formatConds(this.condsConfig.conds) this.$forceUpdate() }, data: function () { let paramMap = {} this.vParams.forEach(function (param) { paramMap[param.code] = param }) let operatorMap = {} this.vOperators.forEach(function (operator) { operatorMap[operator.code] = operator.name }) return { condsConfig: this.value, paramMap: paramMap, operatorMap: operatorMap } }, methods: { formatConds: function (conds) { let that = this conds.forEach(function (cond) { switch (that.paramMap[cond.param].valueType) { case "bandwidth": cond.valueFormat = cond.value.count + cond.value.unit[0].toUpperCase() + cond.value.unit.substring(1) + "ps" return case "traffic": cond.valueFormat = cond.value.count + cond.value.unit.toUpperCase() return case "cpu": cond.valueFormat = cond.value + "%" return case "memory": cond.valueFormat = cond.value + "%" return case "load": cond.valueFormat = cond.value return case "rate": cond.valueFormat = cond.value + "/秒" return } }) } }, template: `
{{paramMap[cond.param].name}} {{operatorMap[cond.operator]}} {{cond.valueFormat}}   
` }) Vue.component("dns-route-selector", { props: ["v-all-routes", "v-routes"], data: function () { let routes = this.vRoutes if (routes == null) { routes = [] } routes.$sort(function (v1, v2) { if (v1.domainId == v2.domainId) { return v1.code < v2.code } return (v1.domainId < v2.domainId) ? 1 : -1 }) return { routes: routes, routeCodes: routes.$map(function (k, v) { return v.code + "@" + v.domainId }), isAdding: false, routeCode: "", keyword: "", searchingRoutes: this.vAllRoutes.$copy() } }, methods: { add: function () { this.isAdding = true this.keyword = "" this.routeCode = "" let that = this setTimeout(function () { that.$refs.keywordRef.focus() }, 200) }, cancel: function () { this.isAdding = false }, confirm: function () { if (this.routeCode.length == 0) { return } if (this.routeCodes.$contains(this.routeCode)) { teaweb.warn("已经添加过此线路,不能重复添加") return } let that = this let route = this.vAllRoutes.$find(function (k, v) { return v.code + "@" + v.domainId == that.routeCode }) if (route == null) { return } this.routeCodes.push(this.routeCode) this.routes.push(route) this.routes.$sort(function (v1, v2) { if (v1.domainId == v2.domainId) { return v1.code < v2.code } return (v1.domainId < v2.domainId) ? 1 : -1 }) this.routeCode = "" this.isAdding = false }, remove: function (route) { this.routeCodes.$removeValue(route.code + "@" + route.domainId) this.routes.$removeIf(function (k, v) { return v.code + "@" + v.domainId == route.code + "@" + route.domainId }) }, clearKeyword: function () { this.keyword = "" } }, watch: { keyword: function (keyword) { if (keyword.length == 0) { this.searchingRoutes = this.vAllRoutes.$copy() this.routeCode = "" return } this.searchingRoutes = this.vAllRoutes.filter(function (route) { return teaweb.match(route.name, keyword) || teaweb.match(route.code, keyword) || teaweb.match(route.domainName, keyword) }) if (this.searchingRoutes.length > 0) { this.routeCode = this.searchingRoutes[0].code + "@" + this.searchingRoutes[0].domainId } else { this.routeCode = "" } } }, template: `
{{route.name}} ({{route.domainName}})
所有线路 没有和关键词“{{keyword}}”匹配的线路
搜索线路
 
` }) Vue.component("dns-domain-selector", { props: ["v-domain-id", "v-domain-name", "v-provider-name"], data: function () { let domainId = this.vDomainId if (domainId == null) { domainId = 0 } let domainName = this.vDomainName if (domainName == null) { domainName = "" } let providerName = this.vProviderName if (providerName == null) { providerName = "" } return { domainId: domainId, domainName: domainName, providerName: providerName } }, methods: { select: function () { let that = this teaweb.popup("/dns/domains/selectPopup", { callback: function (resp) { that.domainId = resp.data.domainId that.domainName = resp.data.domainName that.providerName = resp.data.providerName that.change() } }) }, remove: function() { this.domainId = 0 this.domainName = "" this.change() }, update: function () { let that = this teaweb.popup("/dns/domains/selectPopup?domainId=" + this.domainId, { callback: function (resp) { that.domainId = resp.data.domainId that.domainName = resp.data.domainName that.providerName = resp.data.providerName that.change() } }) }, change: function () { this.$emit("change", { id: this.domainId, name: this.domainName }) } }, template: `
{{providerName}} » {{domainName}}
[选择域名]
` }) Vue.component("dns-resolver-config-box", { props:["v-dns-resolver-config"], data: function () { let config = this.vDnsResolverConfig if (config == null) { config = { type: "default" } } return { config: config, types: [ { name: "默认", code: "default" }, { name: "CGO", code: "cgo" }, { name: "Go原生", code: "goNative" }, ] } }, template: `
使用的DNS解析库

边缘节点使用的DNS解析库。修改此项配置后,需要重启节点进程才会生效。

` }) Vue.component("dns-resolvers-config-box", { props: ["value", "name"], data: function () { let resolvers = this.value if (resolvers == null) { resolvers = [] } let name = this.name if (name == null || name.length == 0) { name = "dnsResolversJSON" } return { formName: name, resolvers: resolvers, host: "", isAdding: false } }, methods: { add: function () { this.isAdding = true let that = this setTimeout(function () { that.$refs.hostRef.focus() }) }, confirm: function () { let host = this.host.trim() if (host.length == 0) { let that = this setTimeout(function () { that.$refs.hostRef.focus() }) return } this.resolvers.push({ host: host, port: 0, // TODO protocol: "" // TODO }) this.cancel() }, cancel: function () { this.isAdding = false this.host = "" this.port = 0 this.protocol = "" }, remove: function (index) { this.resolvers.$remove(index) } }, template: `
{{resolver.protocol}}{{resolver.host}}:{{resolver.port}}  
 
` }) Vue.component("ad-instance-objects-box", { props: ["v-objects", "v-user-id"], mounted: function () { this.getUserServers(1) }, data: function () { let objects = this.vObjects if (objects == null) { objects = [] } let objectCodes = [] objects.forEach(function (v) { objectCodes.push(v.code) }) return { userId: this.vUserId, objects: objects, objectCodes: objectCodes, isAdding: true, servers: [], serversIsLoading: false } }, methods: { add: function () { this.isAdding = true }, cancel: function () { this.isAdding = false }, remove: function (index) { let that = this teaweb.confirm("确定要删除此防护对象吗?", function () { that.objects.$remove(index) that.notifyChange() }) }, removeObjectCode: function (objectCode) { let index = -1 this.objectCodes.forEach(function (v, k) { if (objectCode == v) { index = k } }) if (index >= 0) { this.objects.$remove(index) this.notifyChange() } }, getUserServers: function (page) { if (Tea.Vue == null) { let that = this setTimeout(function () { that.getUserServers(page) }, 100) return } let that = this this.serversIsLoading = true Tea.Vue.$post(".userServers") .params({ userId: this.userId, page: page, pageSize: 5 }) .success(function (resp) { that.servers = resp.data.servers that.$refs.serverPage.updateMax(resp.data.page.max) that.serversIsLoading = false }) .error(function () { that.serversIsLoading = false }) }, changeServerPage: function (page) { this.getUserServers(page) }, selectServerObject: function (server) { if (this.existObjectCode("server:" + server.id)) { return } this.objects.push({ "type": "server", "code": "server:" + server.id, "id": server.id, "name": server.name }) this.notifyChange() }, notifyChange: function () { let objectCodes = [] this.objects.forEach(function (v) { objectCodes.push(v.code) }) this.objectCodes = objectCodes }, existObjectCode: function (objectCode) { let found = false this.objects.forEach(function (v) { if (v.code == objectCode) { found = true } }) return found } }, template: `
暂时还没有设置任何防护对象。
已选中防护对象
网站:{{object.name}}  
对象类型 网站
网站列表 加载中...
暂时还没有可选的网站。
网站名称 操作
{{server.name}} 选中 取消
` }) Vue.component("grant-selector", { props: ["v-grant", "v-node-cluster-id", "v-ns-cluster-id"], data: function () { return { grantId: (this.vGrant == null) ? 0 : this.vGrant.id, grant: this.vGrant, nodeClusterId: (this.vNodeClusterId != null) ? this.vNodeClusterId : 0, nsClusterId: (this.vNsClusterId != null) ? this.vNsClusterId : 0 } }, methods: { // 选择授权 select: function () { let that = this; teaweb.popup("/clusters/grants/selectPopup?nodeClusterId=" + this.nodeClusterId + "&nsClusterId=" + this.nsClusterId, { callback: (resp) => { that.grantId = resp.data.grant.id; if (that.grantId > 0) { that.grant = resp.data.grant; } that.notifyUpdate() }, height: "26em" }) }, // 创建授权 create: function () { let that = this teaweb.popup("/clusters/grants/createPopup", { height: "26em", callback: (resp) => { that.grantId = resp.data.grant.id; if (that.grantId > 0) { that.grant = resp.data.grant; } that.notifyUpdate() } }) }, // 修改授权 update: function () { if (this.grant == null) { window.location.reload(); return; } let that = this teaweb.popup("/clusters/grants/updatePopup?grantId=" + this.grant.id, { height: "26em", callback: (resp) => { that.grant = resp.data.grant that.notifyUpdate() } }) }, // 删除已选择授权 remove: function () { this.grant = null this.grantId = 0 this.notifyUpdate() }, notifyUpdate: function () { this.$emit("change", this.grant) } }, template: `
{{grant.name}}({{grant.methodName}})({{grant.username}})
[选择已有认证]     [添加新认证]
` }) window.REQUEST_COND_COMPONENTS = [{"type":"url-extension","name":"文件扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-eq-index","name":"首页","description":"检查URL路径是为\"/\"","component":"http-cond-url-eq-index","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":false},{"type":"url-all","name":"全站","description":"全站所有URL","component":"http-cond-url-all","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":false},{"type":"url-prefix","name":"URL目录前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-prefix","paramsTitle":"URL目录前缀","isRequest":true,"caseInsensitive":true},{"type":"url-eq","name":"URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-regexp","name":"URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致","component":"http-cond-url-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"url-wildcard-match","name":"URL通配符","description":"使用通配符检查URL中的文件路径是否一致","component":"http-cond-url-wildcard-match","paramsTitle":"通配符","isRequest":true,"caseInsensitive":true},{"type":"user-agent-regexp","name":"User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识","component":"http-cond-user-agent-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"params","name":"参数匹配","description":"根据参数值进行匹配","component":"http-cond-params","paramsTitle":"参数配置","isRequest":true,"caseInsensitive":false},{"type":"url-not-extension","name":"排除:URL扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-not-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-not-prefix","name":"排除:URL前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-not-prefix","paramsTitle":"URL前缀","isRequest":true,"caseInsensitive":true},{"type":"url-not-eq","name":"排除:URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-not-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-not-regexp","name":"排除:URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致,如果一致,则不匹配","component":"http-cond-url-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"user-agent-not-regexp","name":"排除:User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识,如果含有,则不匹配","component":"http-cond-user-agent-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"mime-type","name":"内容MimeType","description":"根据服务器返回的内容的MimeType进行过滤。注意:当用于缓存条件时,此条件需要结合别的请求条件使用。","component":"http-cond-mime-type","paramsTitle":"MimeType列表","isRequest":false,"caseInsensitive":false}]; window.REQUEST_COND_OPERATORS = [{"description":"判断是否正则表达式匹配","name":"正则表达式匹配","op":"regexp"},{"description":"判断是否正则表达式不匹配","name":"正则表达式不匹配","op":"not regexp"},{"description":"判断是否和指定的通配符匹配","name":"通配符匹配","op":"wildcard match"},{"description":"判断是否和指定的通配符不匹配","name":"通配符不匹配","op":"wildcard not match"},{"description":"使用字符串对比参数值是否相等于某个值","name":"字符串等于","op":"eq"},{"description":"参数值包含某个前缀","name":"字符串前缀","op":"prefix"},{"description":"参数值包含某个后缀","name":"字符串后缀","op":"suffix"},{"description":"参数值包含另外一个字符串","name":"字符串包含","op":"contains"},{"description":"参数值不包含另外一个字符串","name":"字符串不包含","op":"not contains"},{"description":"使用字符串对比参数值是否不相等于某个值","name":"字符串不等于","op":"not"},{"description":"判断参数值在某个列表中","name":"在列表中","op":"in"},{"description":"判断参数值不在某个列表中","name":"不在列表中","op":"not in"},{"description":"判断小写的扩展名(不带点)在某个列表中","name":"扩展名","op":"file ext"},{"description":"判断MimeType在某个列表中,支持类似于image/*的语法","name":"MimeType","op":"mime type"},{"description":"判断版本号在某个范围内,格式为version1,version2","name":"版本号范围","op":"version range"},{"description":"将参数转换为整数数字后进行对比","name":"整数等于","op":"eq int"},{"description":"将参数转换为可以有小数的浮点数字进行对比","name":"浮点数等于","op":"eq float"},{"description":"将参数转换为数字进行对比","name":"数字大于","op":"gt"},{"description":"将参数转换为数字进行对比","name":"数字大于等于","op":"gte"},{"description":"将参数转换为数字进行对比","name":"数字小于","op":"lt"},{"description":"将参数转换为数字进行对比","name":"数字小于等于","op":"lte"},{"description":"对整数参数值取模,除数为10,对比值为余数","name":"整数取模10","op":"mod 10"},{"description":"对整数参数值取模,除数为100,对比值为余数","name":"整数取模100","op":"mod 100"},{"description":"对整数参数值取模,对比值格式为:除数,余数,比如10,1","name":"整数取模","op":"mod"},{"description":"将参数转换为IP进行对比","name":"IP等于","op":"eq ip"},{"description":"将参数转换为IP进行对比","name":"IP大于","op":"gt ip"},{"description":"将参数转换为IP进行对比","name":"IP大于等于","op":"gte ip"},{"description":"将参数转换为IP进行对比","name":"IP小于","op":"lt ip"},{"description":"将参数转换为IP进行对比","name":"IP小于等于","op":"lte ip"},{"description":"IP在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e,或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e","name":"IP范围","op":"ip range"},{"description":"对IP参数值取模,除数为10,对比值为余数","name":"IP取模10","op":"ip mod 10"},{"description":"对IP参数值取模,除数为100,对比值为余数","name":"IP取模100","op":"ip mod 100"},{"description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1","name":"IP取模","op":"ip mod"}]; window.REQUEST_VARIABLES = [{"code":"${edgeVersion}","description":"","name":"边缘节点版本"},{"code":"${remoteAddr}","description":"会依次根据X-Forwarded-For、X-Real-IP、RemoteAddr获取,适合前端有别的反向代理服务时使用,存在伪造的风险","name":"客户端地址(IP)"},{"code":"${rawRemoteAddr}","description":"返回直接连接服务的客户端原始IP地址","name":"客户端地址(IP)"},{"code":"${remotePort}","description":"","name":"客户端端口"},{"code":"${remoteUser}","description":"","name":"客户端用户名"},{"code":"${requestURI}","description":"比如/hello?name=lily","name":"请求URI"},{"code":"${requestPath}","description":"比如/hello","name":"请求路径(不包括参数)"},{"code":"${requestURL}","description":"比如https://example.com/hello?name=lily","name":"完整的请求URL"},{"code":"${requestLength}","description":"","name":"请求内容长度"},{"code":"${requestMethod}","description":"比如GET、POST","name":"请求方法"},{"code":"${requestFilename}","description":"","name":"请求文件路径"},{"code":"${requestPathExtension}","description":"请求路径中的文件扩展名,包括点符号,比如.html、.png","name":"请求文件扩展名"},{"code":"${requestPathLowerExtension}","description":"请求路径中的文件扩展名,其中大写字母会被自动转换为小写,包括点符号,比如.html、.png","name":"请求文件小写扩展名"},{"code":"${scheme}","description":"","name":"请求协议,http或https"},{"code":"${proto}","description:":"类似于HTTP/1.0","name":"包含版本的HTTP请求协议"},{"code":"${timeISO8601}","description":"比如2018-07-16T23:52:24.839+08:00","name":"ISO 8601格式的时间"},{"code":"${timeLocal}","description":"比如17/Jul/2018:09:52:24 +0800","name":"本地时间"},{"code":"${msec}","description":"比如1531756823.054","name":"带有毫秒的时间"},{"code":"${timestamp}","description":"","name":"unix时间戳,单位为秒"},{"code":"${host}","description":"","name":"主机名"},{"code":"${cname}","description":"比如38b48e4f.goedge.cn","name":"当前网站的CNAME"},{"code":"${serverName}","description":"","name":"接收请求的服务器名"},{"code":"${serverPort}","description":"","name":"接收请求的服务器端口"},{"code":"${referer}","description":"","name":"请求来源URL"},{"code":"${referer.host}","description":"","name":"请求来源URL域名"},{"code":"${userAgent}","description":"","name":"客户端信息"},{"code":"${contentType}","description":"","name":"请求头部的Content-Type"},{"code":"${cookies}","description":"","name":"所有cookie组合字符串"},{"code":"${cookie.NAME}","description":"","name":"单个cookie值"},{"code":"${isArgs}","description":"如果URL有参数,则值为`?`;否则,则值为空","name":"问号(?)标记"},{"code":"${args}","description":"","name":"所有参数组合字符串"},{"code":"${arg.NAME}","description":"","name":"单个参数值"},{"code":"${headers}","description":"","name":"所有Header信息组合字符串"},{"code":"${header.NAME}","description":"","name":"单个Header值"},{"code":"${geo.country.name}","description":"","name":"国家/地区名称"},{"code":"${geo.country.id}","description":"","name":"国家/地区ID"},{"code":"${geo.province.name}","description":"目前只包含中国省份","name":"省份名称"},{"code":"${geo.province.id}","description":"目前只包含中国省份","name":"省份ID"},{"code":"${geo.city.name}","description":"目前只包含中国城市","name":"城市名称"},{"code":"${geo.city.id}","description":"目前只包含中国城市","name":"城市名称"},{"code":"${isp.name}","description":"","name":"ISP服务商名称"},{"code":"${isp.id}","description":"","name":"ISP服务商ID"},{"code":"${browser.os.name}","description":"客户端所在操作系统名称","name":"操作系统名称"},{"code":"${browser.os.version}","description":"客户端所在操作系统版本","name":"操作系统版本"},{"code":"${browser.name}","description":"客户端浏览器名称","name":"浏览器名称"},{"code":"${browser.version}","description":"客户端浏览器版本","name":"浏览器版本"},{"code":"${browser.isMobile}","description":"如果客户端是手机,则值为1,否则为0","name":"手机标识"}]; window.METRIC_HTTP_KEYS = [{"name":"客户端地址(IP)","code":"${remoteAddr}","description":"会依次根据X-Forwarded-For、X-Real-IP、RemoteAddr获取,适用于前端可能有别的反向代理的情形,存在被伪造的可能","icon":""},{"name":"直接客户端地址(IP)","code":"${rawRemoteAddr}","description":"返回直接连接服务的客户端原始IP地址","icon":""},{"name":"客户端用户名","code":"${remoteUser}","description":"通过基本认证填入的用户名","icon":""},{"name":"请求URI","code":"${requestURI}","description":"包含参数,比如/hello?name=lily","icon":""},{"name":"请求路径","code":"${requestPath}","description":"不包含参数,比如/hello","icon":""},{"name":"完整URL","code":"${requestURL}","description":"比如https://example.com/hello?name=lily","icon":""},{"name":"请求方法","code":"${requestMethod}","description":"比如GET、POST等","icon":""},{"name":"请求协议Scheme","code":"${scheme}","description":"http或https","icon":""},{"name":"文件扩展名","code":"${requestPathExtension}","description":"请求路径中的文件扩展名,包括点符号,比如.html、.png","icon":""},{"name":"小写文件扩展名","code":"${requestPathLowerExtension}","description":"请求路径中的文件扩展名小写形式,包括点符号,比如.html、.png","icon":""},{"name":"主机名","code":"${host}","description":"通常是请求的域名","icon":""},{"name":"HTTP协议","code":"${proto}","description":"包含版本的HTTP请求协议,类似于HTTP/1.0","icon":""},{"name":"URL参数值","code":"${arg.NAME}","description":"单个URL参数值","icon":""},{"name":"请求来源URL","code":"${referer}","description":"请求来源Referer URL","icon":""},{"name":"请求来源URL域名","code":"${referer.host}","description":"请求来源Referer URL域名","icon":""},{"name":"Header值","code":"${header.NAME}","description":"单个Header值,比如${header.User-Agent}","icon":""},{"name":"Cookie值","code":"${cookie.NAME}","description":"单个cookie值,比如${cookie.sid}","icon":""},{"name":"状态码","code":"${status}","description":"","icon":""},{"name":"响应的Content-Type值","code":"${response.contentType}","description":"","icon":""}]; window.IP_ADDR_THRESHOLD_ITEMS = [{"code":"nodeAvgRequests","description":"当前节点在单位时间内接收到的平均请求数。","name":"节点平均请求数","unit":"个"},{"code":"nodeAvgTrafficOut","description":"当前节点在单位时间内发送的下行流量。","name":"节点平均下行流量","unit":"M"},{"code":"nodeAvgTrafficIn","description":"当前节点在单位时间内接收的上行流量。","name":"节点平均上行流量","unit":"M"},{"code":"nodeHealthCheck","description":"当前节点健康检查结果。","name":"节点健康检查结果","unit":""},{"code":"connectivity","description":"通过区域监控得到的当前IP地址的连通性数值,取值在0和100之间。","name":"IP连通性","unit":"%"},{"code":"groupAvgRequests","description":"当前节点所在分组在单位时间内接收到的平均请求数。","name":"分组平均请求数","unit":"个"},{"code":"groupAvgTrafficOut","description":"当前节点所在分组在单位时间内发送的下行流量。","name":"分组平均下行流量","unit":"M"},{"code":"groupAvgTrafficIn","description":"当前节点所在分组在单位时间内接收的上行流量。","name":"分组平均上行流量","unit":"M"},{"code":"clusterAvgRequests","description":"当前节点所在集群在单位时间内接收到的平均请求数。","name":"集群平均请求数","unit":"个"},{"code":"clusterAvgTrafficOut","description":"当前节点所在集群在单位时间内发送的下行流量。","name":"集群平均下行流量","unit":"M"},{"code":"clusterAvgTrafficIn","description":"当前节点所在集群在单位时间内接收的上行流量。","name":"集群平均上行流量","unit":"M"}]; window.IP_ADDR_THRESHOLD_ACTIONS = [{"code":"up","description":"上线当前IP。","name":"上线"},{"code":"down","description":"下线当前IP。","name":"下线"},{"code":"notify","description":"发送已达到阈值通知。","name":"通知"},{"code":"switch","description":"在DNS中记录中将IP切换到指定的备用IP。","name":"切换"},{"code":"webHook","description":"调用外部的WebHook。","name":"WebHook"}]; window.WAF_RULE_CHECKPOINTS = [{"description":"通用报头比如Cache-Control、Accept之类的长度限制,防止缓冲区溢出攻击。","name":"通用请求报头长度限制","prefix":"requestGeneralHeaderLength"},{"description":"通用报头比如Cache-Control、Date之类的长度限制,防止缓冲区溢出攻击。","name":"通用响应报头长度限制","prefix":"responseGeneralHeaderLength"},{"description":"试图通过分析X-Forwarded-For等报头获取的客户端地址,比如192.168.1.100,存在伪造的可能。","name":"客户端地址(IP)","prefix":"remoteAddr"},{"description":"直接连接的客户端地址,比如192.168.1.100。","name":"客户端源地址(IP)","prefix":"rawRemoteAddr"},{"description":"直接连接的客户端地址端口。","name":"客户端端口","prefix":"remotePort"},{"description":"通过BasicAuth登录的客户端用户名。","name":"客户端用户名","prefix":"remoteUser"},{"description":"包含URL参数的请求URI,类似于 /hello/world?lang=go,不包含域名部分。","name":"请求URI","prefix":"requestURI"},{"description":"不包含URL参数的请求路径,类似于 /hello/world,不包含域名部分。","name":"请求路径","prefix":"requestPath"},{"description":"完整的请求URL,包含协议、域名、请求路径、参数等,类似于 https://example.com/hello?name=lily 。","name":"请求完整URL","prefix":"requestURL"},{"description":"请求报头中的Content-Length。","name":"请求内容长度","prefix":"requestLength"},{"description":"通常在POST或者PUT等操作时会附带请求体,最大限制32M。","name":"请求体内容","prefix":"requestBody"},{"description":"${requestURI}和${requestBody}组合。","name":"请求URI和请求体组合","prefix":"requestAll"},{"description":"获取POST或者其他方法发送的表单参数,最大请求体限制32M。","name":"请求表单参数","prefix":"requestForm"},{"description":"获取POST上传的文件信息,最大请求体限制32M。","name":"上传文件","prefix":"requestUpload"},{"description":"获取POST或者其他方法发送的JSON,最大请求体限制32M,使用点(.)符号表示多级数据。","name":"请求JSON参数","prefix":"requestJSON"},{"description":"比如GET、POST。","name":"请求方法","prefix":"requestMethod"},{"description":"比如http或https。","name":"请求协议","prefix":"scheme"},{"description":"比如HTTP/1.1。","name":"HTTP协议版本","prefix":"proto"},{"description":"比如example.com。","name":"主机名","prefix":"host"},{"description":"当前网站服务CNAME,比如38b48e4f.example.com。","name":"CNAME","prefix":"cname"},{"description":"是否为CNAME,值为1(是)或0(否)。","name":"是否为CNAME","prefix":"isCNAME"},{"description":"请求报头中的Referer和Origin值。","name":"请求来源","prefix":"refererOrigin"},{"description":"请求报头中的Referer值。","name":"请求来源Referer","prefix":"referer"},{"description":"比如Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103。","name":"客户端信息","prefix":"userAgent"},{"description":"请求报头的Content-Type。","name":"内容类型","prefix":"contentType"},{"description":"比如sid=IxZVPFhE\u0026city=beijing\u0026uid=18237。","name":"所有cookie组合字符串","prefix":"cookies"},{"description":"单个cookie值。","name":"单个cookie值","prefix":"cookie"},{"description":"比如name=lu\u0026age=20。","name":"所有URL参数组合","prefix":"args"},{"description":"单个URL参数值。","name":"单个URL参数值","prefix":"arg"},{"description":"使用换行符(\\n)隔开的报头内容字符串,每行均为\"NAME: VALUE格式\"。","name":"所有请求报头内容","prefix":"headers"},{"description":"使用换行符(\\n)隔开的报头名称字符串,每行一个名称。","name":"所有请求报头名称","prefix":"headerNames"},{"description":"单个报头值。","name":"单个请求报头值","prefix":"header"},{"description":"当前客户端所处国家/地区名称。","name":"国家/地区名称","prefix":"geoCountryName"},{"description":"当前客户端所处中国省份名称。","name":"省份名称","prefix":"geoProvinceName"},{"description":"当前客户端所处中国城市名称。","name":"城市名称","prefix":"geoCityName"},{"description":"当前客户端所处ISP名称。","name":"ISP名称","prefix":"ispName"},{"description":"对统计对象进行统计。","name":"CC统计","prefix":"cc2"},{"description":"对统计对象进行统计。","name":"防盗链","prefix":"refererBlock"},{"description":"统计某段时间段内的请求信息(不推荐再使用,请使用新的CC2统计代替)。","name":"CC统计(旧)","prefix":"cc"},{"description":"响应状态码,比如200、404、500。","name":"响应状态码","prefix":"status"},{"description":"响应报头值。","name":"响应报头","prefix":"responseHeader"},{"description":"响应内容字符串。","name":"响应内容","prefix":"responseBody"},{"description":"响应内容长度,通过响应的报头Content-Length获取。","name":"响应内容长度","prefix":"bytesSent"}]; window.WAF_RULE_OPERATORS = [{"name":"正则匹配","code":"match","description":"使用正则表达式匹配,在头部使用(?i)表示不区分大小写,\u003ca href=\"https://goedge.cn/docs/Appendix/Regexp/Index.md\" target=\"_blank\"\u003e正则表达式语法 \u0026raquo;\u003c/a\u003e。","caseInsensitive":"yes","dataType":"regexp"},{"name":"正则不匹配","code":"not match","description":"使用正则表达式不匹配,在头部使用(?i)表示不区分大小写,\u003ca href=\"https://goedge.cn/docs/Appendix/Regexp/Index.md\" target=\"_blank\"\u003e正则表达式语法 \u0026raquo;\u003c/a\u003e。","caseInsensitive":"yes","dataType":"regexp"},{"name":"通配符匹配","code":"wildcard match","description":"判断是否和指定的通配符匹配,可以在对比值中使用星号通配符(*)表示任意字符。","caseInsensitive":"yes","dataType":"wildcard"},{"name":"通配符不匹配","code":"wildcard not match","description":"判断是否和指定的通配符不匹配,可以在对比值中使用星号通配符(*)表示任意字符。","caseInsensitive":"yes","dataType":"wildcard"},{"name":"字符串等于","code":"eq string","description":"使用字符串对比等于。","caseInsensitive":"no","dataType":"string"},{"name":"字符串不等于","code":"neq string","description":"使用字符串对比不等于。","caseInsensitive":"no","dataType":"string"},{"name":"包含字符串","code":"contains","description":"包含某个字符串,比如Hello World包含了World。","caseInsensitive":"no","dataType":"string"},{"name":"不包含字符串","code":"not contains","description":"不包含某个字符串,比如Hello字符串中不包含Hi。","caseInsensitive":"no","dataType":"string"},{"name":"包含任一字符串","code":"contains any","description":"包含字符串列表中的任意一个,比如/hello/world包含/hello和/hi中的/hello,对比值中每行一个字符串。","caseInsensitive":"no","dataType":"strings"},{"name":"包含所有字符串","code":"contains all","description":"包含字符串列表中的所有字符串,比如/hello/world必须包含/hello和/world,对比值中每行一个字符串。","caseInsensitive":"no","dataType":"strings"},{"name":"包含前缀","code":"prefix","description":"包含字符串前缀部分,比如/hello前缀会匹配/hello, /hello/world等。","caseInsensitive":"no","dataType":"string"},{"name":"包含后缀","code":"suffix","description":"包含字符串后缀部分,比如/hello后缀会匹配/hello, /hi/hello等。","caseInsensitive":"no","dataType":"string"},{"name":"包含任一单词","code":"contains any word","description":"包含某个独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"包含所有单词","code":"contains all words","description":"包含所有的独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"不包含任一单词","code":"not contains any word","description":"不包含某个独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"包含SQL注入","code":"contains sql injection","description":"检测字符串内容是否包含SQL注入。","caseInsensitive":"none","dataType":"none"},{"name":"包含SQL注入-严格模式","code":"contains sql injection strictly","description":"更加严格地检测字符串内容是否包含SQL注入,相对于非严格模式,有一定的误报几率。","caseInsensitive":"none","dataType":"none"},{"name":"包含XSS注入","code":"contains xss","description":"检测字符串内容是否包含XSS注入。","caseInsensitive":"none","dataType":"none"},{"name":"包含XSS注入-严格模式","code":"contains xss strictly","description":"更加严格地检测字符串内容是否包含XSS注入,相对于非严格模式,此时xml、audio、video等标签也会被匹配。","caseInsensitive":"none","dataType":"none"},{"name":"包含二进制数据","code":"contains binary","description":"包含一组二进制数据。","caseInsensitive":"no","dataType":"string"},{"name":"不包含二进制数据","code":"not contains binary","description":"不包含一组二进制数据。","caseInsensitive":"no","dataType":"string"},{"name":"数值大于","code":"gt","description":"使用数值对比大于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值大于等于","code":"gte","description":"使用数值对比大于等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值小于","code":"lt","description":"使用数值对比小于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值小于等于","code":"lte","description":"使用数值对比小于等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值等于","code":"eq","description":"使用数值对比等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值不等于","code":"neq","description":"使用数值对比不等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"包含索引","code":"has key","description":"对于一组数据拥有某个键值或者索引。","caseInsensitive":"no","dataType":"string|number"},{"name":"版本号大于","code":"version gt","description":"对比版本号大于。","caseInsensitive":"none","dataType":"version"},{"name":"版本号小于","code":"version lt","description":"对比版本号小于。","caseInsensitive":"none","dataType":"version"},{"name":"版本号范围","code":"version range","description":"判断版本号在某个范围内,格式为 起始version1,结束version2。","caseInsensitive":"none","dataType":"versionRange"},{"name":"IP等于","code":"eq ip","description":"将参数转换为IP进行对比,只能对比单个IP。","caseInsensitive":"none","dataType":"ip"},{"name":"在一组IP中","code":"in ip list","description":"判断参数IP在一组IP内,对比值中每行一个IP。","caseInsensitive":"none","dataType":"ips"},{"name":"IP大于","code":"gt ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP大于等于","code":"gte ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP小于","code":"lt ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP小于等于","code":"lte ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP范围","code":"ip range","description":"IP在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e;或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e;或者单个IP。可以填写多行,每行一个IP范围。","caseInsensitive":"none","dataType":"ips"},{"name":"不在IP范围","code":"not ip range","description":"IP不在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e;或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e;或者单个IP。可以填写多行,每行一个IP范围。","caseInsensitive":"none","dataType":"ips"},{"name":"IP取模10","code":"ip mod 10","description":"对IP参数值取模,除数为10,对比值为余数。","caseInsensitive":"none","dataType":"number"},{"name":"IP取模100","code":"ip mod 100","description":"对IP参数值取模,除数为100,对比值为余数。","caseInsensitive":"none","dataType":"number"},{"name":"IP取模","code":"ip mod","description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1。","caseInsensitive":"none","dataType":"number"}]; window.WAF_CAPTCHA_TYPES = [{"name":"验证码","code":"default","description":"通过输入验证码来验证人机。","icon":""},{"name":"点击验证","code":"oneClick","description":"通过点击界面元素来验证人机。","icon":""},{"name":"滑动解锁","code":"slide","description":"通过滑动方块解锁来验证人机。","icon":""},{"name":"极验-行为验","code":"geetest","description":"使用极验-行为验提供的人机验证方式。","icon":""}];