Vue.component("traffic-map-box", { props: ["v-stats", "v-is-attack"], mounted: function () { this.render() }, data: function () { let maxPercent = 0 let isAttack = this.vIsAttack this.vStats.forEach(function (v) { let percent = parseFloat(v.percent) if (percent > maxPercent) { maxPercent = percent } v.formattedCountRequests = teaweb.formatCount(v.countRequests) + "次" v.formattedCountAttackRequests = teaweb.formatCount(v.countAttackRequests) + "次" }) if (maxPercent < 100) { maxPercent *= 1.2 // 不要让某一项100% } let screenIsNarrow = window.innerWidth < 512 return { isAttack: isAttack, stats: this.vStats, chart: null, minOpacity: 0.2, maxPercent: maxPercent, selectedCountryName: "", screenIsNarrow: screenIsNarrow } }, methods: { render: function () { this.chart = teaweb.initChart(document.getElementById("traffic-map-box")); let that = this this.chart.setOption({ backgroundColor: "white", grid: { top: 0, bottom: 0, left: 0, right: 0 }, roam: false, tooltip: { trigger: "item" }, series: [{ type: "map", map: "world", zoom: 1.3, selectedMode: false, itemStyle: { areaColor: "#E9F0F9", borderColor: "#DDD" }, label: { show: false, fontSize: "10px", color: "#fff", backgroundColor: "#8B9BD3", padding: [2, 2, 2, 2] }, emphasis: { itemStyle: { areaColor: "#8B9BD3", opacity: 1.0 }, label: { show: true, fontSize: "10px", color: "#fff", backgroundColor: "#8B9BD3", padding: [2, 2, 2, 2] } }, //select: {itemStyle:{ areaColor: "#8B9BD3", opacity: 0.8 }}, tooltip: { formatter: function (args) { let name = args.name let stat = null that.stats.forEach(function (v) { if (v.name == name) { stat = v } }) if (stat != null) { return name + "
流量:" + 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设置

启用
单节点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 }) }, 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: [], 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.$emit("add") }, cancel: function () { this.isAdding = false this.$emit("cancel") }, confirm: function () { if (this.routeCode.length == 0) { return } let that = this this.routes.forEach(function (v) { if (v.code == that.routeCode) { that.selectedRoutes.push(v) } }) 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-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" } }, 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.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", 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}} {{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

排除

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

 
 
已添加
{{region.name}}
添加新国家/地区省份城市ISP *
       
排除

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

 
 
` }) 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 } } else if (accessLog.nsRecordId == null || accessLog.nsRecordId == 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-price-view", { props: ["v-plan"], data: function () { return { plan: this.vPlan } }, template: `
按时间周期计费
月度:¥{{plan.monthlyPrice}}元
季度:¥{{plan.seasonallyPrice}}元
年度:¥{{plan.yearlyPrice}}元
按流量计费
基础价格:¥{{plan.trafficPrice.base}}元/GB
按{{plan.bandwidthPrice.percentile}}th带宽计费
{{range.minMB}} - {{range.maxMB}}MB{{range.totalPrice}}元{{range.pricePerMB}}元/MB
` }) Vue.component("plan-bandwidth-ranges", { props: ["v-ranges"], data: function () { let ranges = this.vRanges if (ranges == null) { ranges = [] } return { ranges: ranges, isAdding: false, minMB: "", maxMB: "", 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 () { 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) } }, watch: { minMB: function (v) { let minMB = parseInt(v.toString()) if (isNaN(minMB) || minMB < 0) { minMB = 0 } this.addingRange.minMB = minMB }, maxMB: function (v) { let maxMB = parseInt(v.toString()) if (isNaN(maxMB) || maxMB < 0) { maxMB = 0 } this.addingRange.maxMB = 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: `
{{range.minMB}}MB - {{range.maxMB}}MB   价格:{{range.totalPrice}}元{{range.pricePerMB}}元/MB  
带宽下限 *
MB
带宽上限 *
MB

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

总价格
元/MB

和单位价格二选一。

单位价格
元/MB

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

 
` }) // 套餐价格配置 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: `
 按带宽    按流量    按时间周期
月度价格
季度价格
年度价格
基础流量费用 *
元/GB
带宽百分位 *
th
带宽价格
` }) 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-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"], 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, 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 if (hsts == null) { hsts = { isOn: false, maxAge: 31536000, includeSubDomains: false, preload: false, domains: [] } } return { policy: policy, // hsts hsts: hsts, hstsOptionsVisible: false, hstsDomainAdding: false, 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()) }) } teaweb.popup("/servers/certs/selectPopup?selectedCertIds=" + selectedCertIds, { width: "50em", height: "30em", callback: function (resp) { that.policy.certRefs.push(resp.data.certRef) that.policy.certs.push(resp.data.cert) } }) }, // 上传证书 uploadCert: function () { let that = this teaweb.popup("/servers/certs/uploadPopup", { height: "28em", callback: function (resp) { teaweb.success("上传成功", function () { that.policy.certRefs.push(resp.data.certRef) that.policy.certs.push(resp.data.cert) }) } }) }, // 申请证书 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 = this.hsts.maxAge if (isNaN(v)) { this.hsts.days = "-" return } this.hsts.days = parseInt(v / 86400) if (isNaN(this.hsts.days)) { this.hsts.days = "-" } else if (this.hsts.days < 0) { this.hsts.days = "-" } }, // 设置HSTS有效期 setHSTSMaxAge: function (maxAge) { this.hsts.maxAge = maxAge 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) { that.policy.clientCARefs.push(resp.data.certRef) that.policy.clientCACerts.push(resp.data.cert) } }) }, // 上传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
选择证书
{{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()}})
` }) Vue.component("http-request-scripts-config-box", { props: ["vRequestScriptsConfig", "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: `

请求初始化

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

准备发送请求

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

` }) // 显示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) + "") }, }, template: `
{{rule.name}}[{{rule.param}}] {{rule.checkpointOptions.period}}秒/{{rule.checkpointOptions.threshold}}请求 {{rule.checkpointOptions.allowDomains}} | {{paramFilter.code}} {{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(", ")}} 区间缓存 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" // 描述文字 ], 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 = "50em" let height = "30em" let viewSize = this.vViewSize if (viewSize == null) { viewSize = "normal" } if (viewSize == "mini") { width = "35em" height = "20em" } teaweb.popup("/servers/certs/selectPopup?viewSize=" + viewSize, { width: width, height: height, callback: function (resp) { that.certs.push(resp.data.cert) } }) }, // 上传证书 uploadCert: function () { let that = this teaweb.popup("/servers/certs/uploadPopup", { height: "28em", callback: function (resp) { teaweb.success("上传成功", function () { that.certs.push(resp.data.cert) }) } }) }, // 格式化时间 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: "30em", 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: "30em", 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跳转规则。

跳转前URL 跳转后URL 匹配模式 HTTP状态码 状态 操作
{{redirect.beforeURL}}
匹配条件
-> {{redirect.afterURL}} 匹配前缀 正则匹配 精准匹配 {{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: 2, unit: "hour"}, status: [200], maxSize: {count: 32, 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, enableIfNoneMatch: false, enableIfModifiedSince: 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 = "40em" break case "complex": let width = window.parent.innerWidth if (width > 1024) { width = 1024 } dialog.style.width = width + "px" 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}} * 不区分大小写

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

匹配条件分组 *

« 切换到简单条件

缓存有效期 * 缓存Key *

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

忽略URI参数

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

请求方法限制

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

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

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

可缓存的最小内容尺寸

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

支持分片内容

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

支持缓存区间内容

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

状态码列表

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

跳过的Cache-Control值

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

跳过Set-Cookie

选中后,当响应的Header中有Set-Cookie时不缓存响应内容。

支持请求no-cache刷新

选中后,当请求的Header中含有Pragma: no-cache或Cache-Control: no-cache时,会跳过缓存直接读取源内容。

允许If-None-Match回源

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

允许If-Modified-Since回源

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

` }) // 请求限制 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 } } return { firewall: firewall } }, template: `
WAF策略
{{vFirewallPolicy.name}}   [{{vFirewallPolicy.modeInfo.name}}] 

使用当前服务所在集群的设置。

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

启用WAF之后,各项WAF设置才会生效。

` }) // 指标图表 Vue.component("metric-chart", { props: ["v-chart", "v-stats", "v-item"], 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: "20em" } ] }) }, 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: "20em" } ] }) 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 = [] } return { cacheConfig: cacheConfig, moreOptionsVisible: false, enablePolicyRefs: !cacheConfig.disablePolicyRefs } }, watch: { enablePolicyRefs: function (v) { this.cacheConfig.disablePolicyRefs = !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 } }, template: `
缓存策略
{{vCachePolicy.name}}

使用当前服务所在集群的设置。

当前集群没有设置缓存策略,当前配置无法生效。
启用缓存
收起选项更多选项
使用默认缓存条件

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

添加X-Cache Header

选中后自动在响应Header中增加X-Cache: BYPASS|MISS|HIT|PURGE

添加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 = false 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 } let that = this setTimeout(function () { that.change() }, 100) return { keys: keys, period: period, threshold: threshold, ignoreCommonFiles: ignoreCommonFiles, options: {}, value: threshold } }, watch: { period: function () { this.change() }, threshold: function () { this.change() }, ignoreCommonFiles: 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 } this.vCheckpoint.options = [ { code: "keys", value: this.keys }, { code: "period", value: period, }, { code: "threshold", value: threshold }, { code: "ignoreCommonFiles", value: ignoreCommonFiles } ] }, thresholdTooLow: function () { let threshold = parseInt(this.threshold.toString()) if (isNaN(threshold) || threshold <= 0) { threshold = 1000 } return threshold > 0 && threshold < 5 } }, template: `
统计对象组合 *
统计周期 *
阈值 *

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

忽略常见文件

忽略js、css、jpg等常见在网页里被引用的文件名。

` }) // 防盗链 Vue.component("http-firewall-checkpoint-referer-block", { props: ["v-checkpoint"], data: function () { let allowEmpty = true let allowSameDomain = true let allowDomains = [] 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 } let that = this setTimeout(function () { that.change() }, 100) return { allowEmpty: allowEmpty, allowSameDomain: allowSameDomain, allowDomains: allowDomains, options: {}, value: 0 } }, watch: { allowEmpty: function () { this.change() }, allowSameDomain: function () { this.change() } }, methods: { changeAllowDomains: function (values) { this.allowDomains = values this.change() }, change: function () { this.vCheckpoint.options = [ { code: "allowEmpty", value: this.allowEmpty }, { code: "allowSameDomain", value: this.allowSameDomain, }, { code: "allowDomains", value: this.allowDomains } ] } }, template: `
来源域名允许为空

允许不带来源的访问。

来源域名允许一致

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

允许的来源域名

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

` }) 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"], 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 id = 0 refs.forEach(function (ref) { id++ ref.id = id }) 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) } that.change() } }) }, updateRef: function (index, cacheRef) { window.UPDATING_CACHE_REF = 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 () { 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("保存成功") } }) .post() } } }, 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(", ")}} 区间缓存 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, { height: "27em", callback: function (resp) { teaweb.success("保存成功", function () { window.location.reload() }) } }) }, createBackupOrigin: function () { teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, { 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, { height: "27em", callback: function (resp) { teaweb.success("保存成功", function () { window.location.reload() }) } }) }, deleteOrigin: function (originId, originType) { let that = this teaweb.confirm("确定要删除此源站吗?", function () { Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType) .post() .success(function () { teaweb.success("删除成功", function () { window.location.reload() }) }) }) } }, template: `

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

暂时还没有主要源站。

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

暂时还没有备用源站。

` }) Vue.component("origin-list-table", { props: ["v-origins", "v-origin-type"], data: function () { return {} }, methods: { deleteOrigin: function (originId) { this.$emit("deleteOrigin", originId, this.vOriginType) }, updateOrigin: function (originId) { this.$emit("updateOrigin", originId, this.vOriginType) } }, template: `
源站地址 权重 状态 操作
{{origin.addr}}  
{{origin.name}} 证书 主机名: {{origin.host}} 端口跟随 匹配: {{domain}}
{{origin.weight}} 修改   删除
` }) 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", { 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-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: `
启用配置
允许所有来源域(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 = 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 = "" } }, }, 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}}个域名
|
` }) 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"], data: function () { let domains = this.vDomains if (domains == null) { domains = [] } let realName = "domainsJSON" if (this.name != null && typeof this.name == "string") { realName = this.name } return { domains: domains, isAdding: false, addingDomain: "", realName: realName } }, methods: { add: function () { this.isAdding = true let that = this setTimeout(function () { that.$refs.addingDomain.focus() }, 100) }, confirm: function () { 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.addingDomain[0] == "~") { let expr = this.addingDomain.substring(1) try { new RegExp(expr) } catch (e) { teaweb.warn("正则表达式错误:" + e.message, function () { that.$refs.addingDomain.focus() }) return } } this.domains.push(this.addingDomain) this.cancel() }, remove: function (index) { this.domains.$remove(index) }, cancel: function () { this.isAdding = false this.addingDomain = "" } }, template: `
[正则] [后缀] [泛域名] {{domain}}  
 

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

` }) 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: [] } } if (config.allowDomains == null) { config.allowDomains = [] } return { config: config } }, methods: { isOn: function () { return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn }, changeAllowDomains: function (domains) { } }, template: `
启用

选中后表示开启防盗链。

允许直接访问网站

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

来源域名允许一致

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

允许的来源域名

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

` }) 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"], 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 = [] } let id = 0 let configs = [] if (this.vActionConfigs != null) { configs = this.vActionConfigs configs.forEach(function (v) { v.id = (id++) }) } var defaultPageBody = ` 403 Forbidden

403 Forbidden

Request ID: \${requestId}.
` return { id: id, actions: this.vActions, configs: configs, isAdding: false, editingIndex: -1, action: null, actionCode: "", actionOptions: {}, // IPList相关 ipListLevels: [], // 动作参数 blockTimeout: "", blockScope: "global", captchaLife: "", captchaMaxFails: "", captchaFailBlockTimeout: "", get302Life: "", post307Life: "", recordIPType: "black", recordIPLevel: "critical", recordIPTimeout: "", recordIPListId: 0, recordIPListName: "", tagTags: [], pageStatus: 403, pageBody: defaultPageBody, defaultPageBody: defaultPageBody, goGroupName: "", goGroupId: 0, goGroup: null, goSetId: 0, goSetName: "", jsCookieLife: "", jsCookieMaxFails: "", jsCookieFailBlockTimeout: "" } }, watch: { actionCode: function (code) { this.action = this.actions.$find(function (k, v) { return v.code == code }) this.actionOptions = {} }, blockTimeout: function (v) { v = parseInt(v) if (isNaN(v)) { this.actionOptions["timeout"] = 0 } else { this.actionOptions["timeout"] = 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) { this.goGroupName = "" } 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 = "block" this.isAdding = true this.actionOptions = {} // 动作参数 this.blockTimeout = "" 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.pageStatus = 403 this.pageBody = this.defaultPageBody 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 switch (config.code) { case "block": this.blockTimeout = "" if (config.options.timeout != null || config.options.timeout > 0) { this.blockTimeout = config.options.timeout.toString() } if (config.options.scope != null && config.options.scope.length > 0) { this.blockScope = config.options.scope } else { this.blockScope = "global" // 兼容先前版本遗留的默认值 } break case "allow": 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.pageStatus = 403 this.pageBody = this.defaultPageBody if (config.options.status != null) { this.pageStatus = config.options.status } if (config.options.body != null) { this.pageBody = config.options.body } 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 } 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 = { status: pageStatus, body: this.pageBody } } 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.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.groupName}} :{{config.options.groupName}} / {{config.options.setName}}   [所有服务] [当前服务]      
动作类型 *

{{action.description}}

封锁时间
封锁范围

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

封锁用户对所有网站服务的访问。

有效时间

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

最多失败次数

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

失败拦截时间

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

有效时间

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

最多失败次数

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

失败拦截时间

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

有效时间

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

有效时间

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

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

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

级别
超时时间

0表示不超时。

标签 *
状态码 *
网页内容
下一个分组 *
下一个分组 *
下一个规则集 *
 

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

` }) // 认证设置 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) } } }, template: `
` }) // 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 } } return { config: config } }, template: `
启用5秒盾

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

` }) 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 requestPolicy = this.vRequestHeaderPolicy if (requestPolicy != null) { if (requestPolicy.setHeaders != null) { requestSettingHeaders = requestPolicy.setHeaders } if (requestPolicy.deleteHeaders != null) { requestDeletingHeaders = requestPolicy.deleteHeaders } } // 响应相关 let responseSettingHeaders = [] let responseDeletingHeaders = [] let responsePolicy = this.vResponseHeaderPolicy if (responsePolicy != null) { if (responsePolicy.setHeaders != null) { responseSettingHeaders = responsePolicy.setHeaders } if (responsePolicy.deleteHeaders != null) { responseDeletingHeaders = responsePolicy.deleteHeaders } } return { type: type, typeName: (type == "request") ? "请求" : "响应", requestHeaderRef: requestHeaderRef, responseHeaderRef: responseHeaderRef, requestSettingHeaders: requestSettingHeaders, requestDeletingHeaders: requestDeletingHeaders, responseSettingHeaders: responseSettingHeaders, responseDeletingHeaders: responseDeletingHeaders } }, 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, { 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("保存成功") } }) }, updateSettingPopup: function (policyId, headerId) { teaweb.popup("/servers/server/settings/headers/updateSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&headerId=" + headerId+ "&type=" + this.type, { 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() }) }, deleteHeader: function (policyId, type, headerId) { teaweb.confirm("确定要删除此Header吗?", function () { this.$post("/servers/server/settings/headers/delete") .params({ headerPolicyId: policyId, type: type, headerId: headerId }) .refresh() } ) } }, template: `
由于已经在当前服务分组中进行了对应的配置,在这里的配置将不会生效。

设置请求Header [添加新Header]

暂时还没有Header。

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

删除请求Header

这里可以设置需要从请求中删除的Header。

需要删除的Header
{{headerName}}
由于已经在当前服务分组中进行了对应的配置,在这里的配置将不会生效。

设置响应Header [添加新Header]

将会覆盖已有的同名Header。

暂时还没有Header。

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

删除响应Header

这里可以设置需要从响应中删除的Header。

需要删除的Header
{{headerName}}
` }) // 通用设置 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-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: "url", url: "", body: "", status: 0 } if (this.vShutdownConfig != null) { if (this.vShutdownConfig.body == null) { this.vShutdownConfig.body = "" } if (this.vShutdownConfig.bodyType == null) { this.vShutdownConfig.bodyType = "url" } shutdownConfig = this.vShutdownConfig } let shutdownStatus = "" if (shutdownConfig.status > 0) { shutdownStatus = shutdownConfig.status.toString() } return { pages: pages, shutdownConfig: shutdownConfig, shutdownStatus: shutdownStatus } }, 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: "26em", callback: function (resp) { that.pages.push(resp.data.page) } }) }, 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) } }) }, removePage: function (pageIndex) { let that = this teaweb.confirm("确定要移除此页面吗?", function () { that.pages.$remove(pageIndex) }) }, addShutdownHTMLTemplate: function () { this.shutdownConfig.body = ` \t升级中 \t

网站升级中

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

Request ID: \${requestId}.
` } }, template: `
自定义页面
{{page.status}} -> {{page.url}}[HTML内容]

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

临时关闭页面
开启
内容类型 *
页面URL *

页面文件是相对于节点安装目录的页面文件比如pages/40x.html,或者一个完整的URL。

HTML *

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

状态码

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

` }) // 压缩配置 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: 5, decompressData: false, gzipRef: null, deflateRef: null, brotliRef: null, minLength: {count: 0, "unit": "kb"}, maxLength: {count: 0, "unit": "kb"}, mimeTypes: ["text/*", "application/*", "font/*"], extensions: [".js", ".json", ".html", ".htm", ".xml", ".css", ".woff2", ".txt"], conds: null } } 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 } }, watch: { "config.level": function (v) { let level = parseInt(v) if (isNaN(level)) { level = 1 } else if (level < 1) { level = 1 } else if (level > 10) { level = 10 } this.config.level = level } }, 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 }, 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将会被压缩,不区分大小写。

支持的MimeType

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

压缩算法

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

支持已压缩内容

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

内容最小长度

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

内容最大长度

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

匹配条件
` }) 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 } } 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}}秒
` }) 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"将会显著消耗更多的系统资源,建议仅在调试时启用,最大记录尺寸为2MB。

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

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

WAF相关

只记录WAF相关日志

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

` }) // 显示流量限制说明 Vue.component("traffic-limit-view", { props: ["v-traffic-limit"], data: function () { return { config: this.vTrafficLimit } }, template: `
日流量限制:{{config.dailySize.count}}{{config.dailySize.unit.toUpperCase()}}
月流量限制:{{config.monthlySize.count}}{{config.monthlySize.unit.toUpperCase()}}
没有限制。
` }) // 基本认证用户配置 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 文档根目录 源站 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"], data: function () { let config = this.vScriptConfig if (config == null) { config = { isPrior: false, isOn: false, code: "" } } if (config.code.length == 0) { config.code = "\n\n\n\n" } return { config: config } }, watch: { "config.isOn": function () { this.change() } }, methods: { change: function () { this.$emit("change", this.config) }, changeCode: function (code) { this.config.code = code this.change() } }, template: `
是否启用
脚本代码 {{config.code}}

{{comment}}

` }) 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: "" } }, methods: { updateSummary: function () { let summaryList = [] if (this.options.life > 0) { summaryList.push("有效时间" + this.options.life + "秒") } if (this.options.maxFails > 0) { summaryList.push("最多失败" + this.options.maxFails + "次") } if (this.options.failBlockTimeout > 0) { summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") } if (this.options.failBlockScopeAll) { summaryList.push("全局封禁") } if (this.options.uiIsOn) { summaryList.push("定制UI") } if (summaryList.length == 0) { this.summary = "默认配置" } else { this.summary = summaryList.join(" / ") } } }, template: `
{{summary}}
` }) Vue.component("reverse-proxy-box", { props: ["v-reverse-proxy-ref", "v-reverse-proxy-config", "v-is-location", "v-is-group", "v-family"], data: function () { 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 } } 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
{{header.name}}

选中后,会自动向源站请求添加这些Header。

请求URI(RequestURI)

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

去除URL前缀(StripPrefix)

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

是否自动刷新缓存区(AutoFlush)

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

源站连接失败超时时间

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

源站读取超时时间

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

源站最大并发连接数

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

源站最大空闲连接数

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

源站最大空闲超时时间

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

PROXY Protocol

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

PROXY Protocol版本

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

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

` }) 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}", isCustomized: false } } let optionValue = "" if (!config.isCustomized && (config.value == "${remoteAddr}" || config.value == "${rawRemoteAddr}")) { optionValue = config.value } return { config: config, options: [ { name: "直接获取", description: "用户直接访问边缘节点,即 \"用户 --> 边缘节点\" 模式,这时候可以直接从连接中读取到真实的IP地址。", value: "${rawRemoteAddr}" }, { name: "从上级代理中获取", description: "用户和边缘节点之间有别的代理服务转发,即 \"用户 --> [第三方代理服务] --> 边缘节点\",这时候只能从上级代理中获取传递的IP地址。", value: "${remoteAddr}" }, { name: "[自定义]", description: "通过自定义变量来获取客户端真实的IP地址。", value: "" } ], optionValue: optionValue } }, methods: { isOn: function () { return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn }, changeOptionValue: function () { if (this.optionValue.length > 0) { this.config.value = this.optionValue this.config.isCustomized = false } else { this.config.isCustomized = true } } }, template: `
是否启用

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

获取IP方式 *

{{option.description}}

读取IP变量值 *

通过此变量获取用户的IP地址。具体可用的请求变量列表可参考官方网站文档。

` }) // 访问日志搜索框 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("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 rootConfig = this.vRootConfig if (rootConfig == null) { rootConfig = { isPrior: false, isOn: true, dir: "", indexes: [], stripPrefix: "", decodePath: false, isBreak: false } } if (rootConfig.indexes == null) { rootConfig.indexes = [] } return { rootConfig: rootConfig, 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.rootConfig.indexes.push(resp.data.index) } }) }, removeIndex: function (i) { this.rootConfig.indexes.$remove(i) }, isOn: function () { return ((!this.vIsLocation && !this.vIsGroup) || this.rootConfig.isPrior) && this.rootConfig.isOn } }, template: `
是否开启静态资源分发
静态资源根目录

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

首页文件
{{index}}

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

去除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, quality: 50, minLength: {count: 0, "unit": "kb"}, maxLength: {count: 0, "unit": "kb"}, mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico", "image/gif"], 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, quality: config.quality } }, watch: { quality: function (v) { let quality = parseInt(v) if (isNaN(quality)) { quality = 90 } else if (quality < 1) { quality = 1 } else if (quality > 100) { quality = 100 } this.config.quality = quality } }, 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压缩;只有满足缓存条件的图片内容才会被转换

图片质量
%

取值在0到100之间,数值越大生成的图像越清晰,同时文件尺寸也会越大。

支持的扩展名

含有这些扩展名的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 { blockOptions: this.vBlockOptions, statusCode: this.vBlockOptions.statusCode, timeout: this.vBlockOptions.timeout, isEditing: false } }, watch: { statusCode: function (v) { let statusCode = parseInt(v) if (isNaN(statusCode)) { this.blockOptions.statusCode = 403 } else { this.blockOptions.statusCode = statusCode } }, timeout: function (v) { let timeout = parseInt(v) if (isNaN(timeout)) { this.blockOptions.timeout = 0 } else { this.blockOptions.timeout = timeout } } }, methods: { edit: function () { this.isEditing = !this.isEditing } }, template: `
状态码:{{statusCode}} / 提示内容:[{{blockOptions.body.length}}字符][无] / 超时时间:{{timeout}}秒
状态码
提示内容
超时时间

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

` }) 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-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, { callback: function (resp) { that.rules.push(resp.data.rule) } }) }, updateRule: function (index, rule) { window.UPDATING_RULE = rule let that = this teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { callback: function (resp) { Vue.set(that.rules, index, resp.data.rule) } }) }, removeRule: function (index) { let that = this teaweb.confirm("确定要删除此规则吗?", function () { that.rules.$remove(index) }) } }, template: `
{{rule.name}}[{{rule.param}}] {{rule.checkpointOptions.period}}秒/{{rule.checkpointOptions.threshold}}请求 {{rule.checkpointOptions.allowDomains}} | {{paramFilter.code}} {{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: "${requestPathExtension}", 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 } 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-not-extension", { props: ["v-cond"], data: function () { let cond = { isRequest: true, param: "${requestPathExtension}", operator: "not in", value: "[]" } if (this.vCond != null && this.vCond.param == cond.param) { cond.value = this.vCond.value } let extensions = [] try { extensions = JSON.parse(cond.value) } catch (e) { } return { cond: cond, extensions: extensions, // TODO 可以拖动排序 isAdding: false, addingExt: "" } }, watch: { extensions: function () { this.cond.value = JSON.stringify(this.extensions) } }, methods: { addExt: function () { this.isAdding = !this.isAdding if (this.isAdding) { let that = this setTimeout(function () { that.$refs.addingExt.focus() }, 100) } }, cancelAdding: function () { this.isAdding = false this.addingExt = "" }, confirmAdding: function () { // TODO 做更详细的校验 // TODO 如果有重复的则提示之 if (this.addingExt.length == 0) { return } if (this.addingExt[0] != ".") { this.addingExt = "." + this.addingExt } this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase() this.extensions.push(this.addingExt) // 清除状态 this.cancelAdding() }, removeExt: function (index) { this.extensions.$remove(index) } }, template: `
{{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路径是为/,不需要带域名。

` }) // 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$,不需要带域名。

` }) // 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},也可以是多个变量的组合。

操作符

{{operatorDescription}}

对比值

要匹配的正则表达式,比如^/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-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 = { countLetters: 0, life: 0, maxFails: 0, failBlockTimeout: 0, failBlockScopeAll: false, uiIsOn: false, uiTitle: "", uiPrompt: "", uiButtonTitle: "", uiShowRequestId: true, uiCss: "", uiFooter: "", uiBody: "", cookieId: "", lang: "" } } if (options.countLetters <= 0) { options.countLetters = 6 } return { options: options, isEditing: false, summary: "" } }, watch: { "options.countLetters": function (v) { let i = parseInt(v, 10) if (isNaN(i)) { i = 0 } else if (i < 0) { i = 0 } else if (i > 10) { i = 10 } this.options.countLetters = i }, "options.life": function (v) { let i = parseInt(v, 10) if (isNaN(i)) { i = 0 } this.options.life = i this.updateSummary() }, "options.maxFails": function (v) { let i = parseInt(v, 10) if (isNaN(i)) { i = 0 } this.options.maxFails = i this.updateSummary() }, "options.failBlockTimeout": function (v) { let i = parseInt(v, 10) if (isNaN(i)) { i = 0 } this.options.failBlockTimeout = i this.updateSummary() }, "options.failBlockScopeAll": function (v) { this.updateSummary() }, "options.uiIsOn": function (v) { this.updateSummary() } }, methods: { edit: function () { this.isEditing = !this.isEditing }, updateSummary: function () { let summaryList = [] if (this.options.life > 0) { summaryList.push("有效时间" + this.options.life + "秒") } if (this.options.maxFails > 0) { summaryList.push("最多失败" + this.options.maxFails + "次") } if (this.options.failBlockTimeout > 0) { summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") } if (this.options.failBlockScopeAll) { summaryList.push("全局封禁") } if (this.options.uiIsOn) { summaryList.push("定制UI") } if (summaryList.length == 0) { this.summary = "默认配置" } else { this.summary = summaryList.join(" / ") } }, confirm: function () { this.isEditing = false } }, template: `
{{summary}}
有效时间

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

最多失败次数

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

失败拦截时间

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

失败全局封禁

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

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

类似于提交验证

显示请求ID

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

CSS样式
页头提示

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

页尾提示

支持HTML。

页面模板

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

` }) Vue.component("firewall-syn-flood-config-box", { props: ["v-syn-flood-config"], data: function () { 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攻击。此功能需要节点已安装并启用Firewalld。

空连接次数
次/分钟

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

封禁时间
忽略局域网访问
` }) // 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"], data: function () { return { items: this.vItems, keyword: (this.vKeyword != null) ? this.vKeyword : "", selectedAll: false, hasSelectedItems: false } }, 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) }) }, 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) + "天" } }, template: `
[批量删除]
IP 类型 级别 过期时间 备注 操作
{{item.ipFrom}}  New   - {{item.ipTo}} *
{{item.region}} | {{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.ipFrom}} - {{vItem.ipTo}} {{vItem.ipFrom}}   级别:{{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("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("/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("/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 } }, watch: { "vServerType": function () { this.addresses = [] }, "vAddresses": function () { if (this.vAddresses != null) { this.addresses = this.vAddresses } } }, methods: { addAddr: function () { 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") } }, template: `
{{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 = "" 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"], data: function () { return { vTitle: (this.title == null) ? "打开链接" : this.title } }, 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) } }, 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("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) { teaweb.warn("找不到要下载的内容") 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"], 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) { 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 () { let date = new Date() // 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]) date.setFullYear(year) let month = parseInt(pieces[1]) if (month < 1 || month > 12) { this.hasDayError = true return } date.setMonth(month - 1) let day = parseInt(pieces[2]) if (day < 1 || day > 32) { this.hasDayError = true return } date.setDate(day) 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 date.setHours(hour) // 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 date.setMinutes(minute) // 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 date.setSeconds(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 }, 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天 

` }) // 启用状态标签 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("first-menu", { props: [], template: ' \
\ \
\
' }); /** * 更多选项 */ Vue.component("more-options-indicator", { 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) } }, 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("more-options-angle", { data: function () { return { isVisible: false } }, methods: { show: function () { this.isVisible = !this.isVisible this.$emit("change", this.isVisible) } }, 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("health-check-config-box", { props: ["v-health-check-config", "v-check-domain-url"], 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}指的是域名。

检测时间间隔

两次检查之间的间隔。

自动下线

选中后系统会根据健康检查的结果自动标记节点的上线/下线状态,并可能自动同步DNS设置。

连续上线次数

连续{{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: `` }) Vue.component("dot", { template: '' }) Vue.component("time-duration-box", { props: ["v-name", "v-value", "v-count", "v-unit"], 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 } return { duration: v, countString: (v.count >= 0) ? v.count.toString() : "" } }, 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("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 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("size-capacity-view", { props:["v-default-text", "v-value"], template: `
{{vValue.count}}{{vValue.unit.toUpperCase()}} {{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("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: ["v-name", "v-value", "v-bottom-left"], 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 = "day" } let day = this.vValue if (day == null) { day = "" } return { name: name, day: day } }, methods: { change: function () { 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-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"], data: function () { return { ipAddresses: (this.vIpAddresses == null) ? [] : this.vIpAddresses, supportThresholds: this.role != "ns" } }, methods: { // 添加IP地址 addIPAddress: function () { window.UPDATING_NODE_IP_ADDRESS = null let that = this; teaweb.popup("/nodes/ipAddresses/createPopup?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 = address let that = this; teaweb.popup("/nodes/ipAddresses/updatePopup?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}}个阈值]  
` }) // 节点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("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 }) } }, 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.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}})
` }) 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("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":"URL扩展名","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-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":"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":"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在某个范围之内,范围格式可以是英文逗号分隔的ip1,ip2,或者CIDR格式的ip/bits","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"},{"description":"判断参数值解析后的文件是否存在","name":"文件存在","op":"file exist"},{"description":"判断参数值解析后的文件是否不存在","name":"文件不存在","op":"file not exist"}] 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":"${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":"${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":"${host}","description":"通常是请求的域名","icon":""},{"name":"请求协议Proto","code":"${proto}","description":"包含版本的HTTP请求协议,类似于HTTP/1.0","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"}]