diff --git a/EdgeAPI/internal/clickhouse/logs_ingest_store.go b/EdgeAPI/internal/clickhouse/logs_ingest_store.go index 047bd1d..7c2359a 100644 --- a/EdgeAPI/internal/clickhouse/logs_ingest_store.go +++ b/EdgeAPI/internal/clickhouse/logs_ingest_store.go @@ -126,6 +126,9 @@ func (s *LogsIngestStore) List(ctx context.Context, f ListFilter) (rows []*LogsI if f.HasFirewallPolicy { conditions = append(conditions, "firewall_policy_id > 0") } + if f.HasError { + conditions = append(conditions, "status >= 400") + } if f.FirewallPolicyId > 0 { conditions = append(conditions, "firewall_policy_id = "+strconv.FormatInt(f.FirewallPolicyId, 10)) } diff --git a/EdgeAPI/internal/rpc/services/service_http_access_log.go b/EdgeAPI/internal/rpc/services/service_http_access_log.go index 7c5af2b..ce26818 100644 --- a/EdgeAPI/internal/rpc/services/service_http_access_log.go +++ b/EdgeAPI/internal/rpc/services/service_http_access_log.go @@ -67,14 +67,33 @@ func (this *HTTPAccessLogService) ListHTTPAccessLogs(ctx context.Context, req *p } store := clickhouse.NewLogsIngestStore() - if store.Client().IsConfigured() && req.Day != "" { + canReadFromClickHouse := this.shouldReadAccessLogsFromClickHouse() && store.Client().IsConfigured() && req.Day != "" + canReadFromMySQL := this.shouldReadAccessLogsFromMySQL() + if canReadFromClickHouse { resp, listErr := this.listHTTPAccessLogsFromClickHouse(ctx, tx, store, req, userId) - if listErr != nil { - return nil, listErr - } - if resp != nil { + if listErr == nil && resp != nil { return resp, nil } + if !canReadFromMySQL { + if listErr != nil { + return nil, listErr + } + return &pb.ListHTTPAccessLogsResponse{ + HttpAccessLogs: []*pb.HTTPAccessLog{}, + AccessLogs: []*pb.HTTPAccessLog{}, + HasMore: false, + RequestId: "", + }, nil + } + } + + if !canReadFromMySQL { + return &pb.ListHTTPAccessLogsResponse{ + HttpAccessLogs: []*pb.HTTPAccessLog{}, + AccessLogs: []*pb.HTTPAccessLog{}, + HasMore: false, + RequestId: "", + }, nil } accessLogs, requestId, hasMore, err := models.SharedHTTPAccessLogDAO.ListAccessLogs(tx, req.Partition, req.RequestId, req.Size, req.Day, req.HourFrom, req.HourTo, req.NodeClusterId, req.NodeId, req.ServerId, req.Reverse, req.HasError, req.FirewallPolicyId, req.FirewallRuleGroupId, req.FirewallRuleSetId, req.HasFirewallPolicy, req.UserId, req.Keyword, req.Ip, req.Domain) @@ -241,12 +260,15 @@ func (this *HTTPAccessLogService) FindHTTPAccessLog(ctx context.Context, req *pb // 优先从 ClickHouse 查询 store := clickhouse.NewLogsIngestStore() - if store.Client().IsConfigured() { + canReadFromClickHouse := this.shouldReadAccessLogsFromClickHouse() && store.Client().IsConfigured() + canReadFromMySQL := this.shouldReadAccessLogsFromMySQL() + if canReadFromClickHouse { row, err := store.FindByTraceId(ctx, req.RequestId) if err != nil { - return nil, err - } - if row != nil { + if !canReadFromMySQL { + return nil, err + } + } else if row != nil { // 检查权限 if userId > 0 { var tx = this.NullTx() @@ -260,6 +282,10 @@ func (this *HTTPAccessLogService) FindHTTPAccessLog(ctx context.Context, req *pb } } + if !canReadFromMySQL { + return &pb.FindHTTPAccessLogResponse{HttpAccessLog: nil}, nil + } + // 如果 ClickHouse 未配置或未找到,则回退到 MySQL var tx = this.NullTx() diff --git a/EdgeAPI/internal/rpc/services/service_http_access_log_ext.go b/EdgeAPI/internal/rpc/services/service_http_access_log_ext.go index b89ca10..55347d0 100644 --- a/EdgeAPI/internal/rpc/services/service_http_access_log_ext.go +++ b/EdgeAPI/internal/rpc/services/service_http_access_log_ext.go @@ -9,6 +9,14 @@ func (this *HTTPAccessLogService) canWriteAccessLogsToDB() bool { return true } +func (this *HTTPAccessLogService) shouldReadAccessLogsFromClickHouse() bool { + return true +} + +func (this *HTTPAccessLogService) shouldReadAccessLogsFromMySQL() bool { + return true +} + func (this *HTTPAccessLogService) writeAccessLogsToPolicy(pbAccessLogs []*pb.HTTPAccessLog) error { return nil } diff --git a/EdgeAPI/internal/rpc/services/service_http_access_log_ext_plus.go b/EdgeAPI/internal/rpc/services/service_http_access_log_ext_plus.go index 1f6ade6..5ef73ac 100644 --- a/EdgeAPI/internal/rpc/services/service_http_access_log_ext_plus.go +++ b/EdgeAPI/internal/rpc/services/service_http_access_log_ext_plus.go @@ -11,7 +11,15 @@ import ( ) func (this *HTTPAccessLogService) canWriteAccessLogsToDB() bool { - return false + return accesslogs.SharedStorageManager.WriteMySQL() +} + +func (this *HTTPAccessLogService) shouldReadAccessLogsFromClickHouse() bool { + return accesslogs.SharedStorageManager.WriteClickHouse() +} + +func (this *HTTPAccessLogService) shouldReadAccessLogsFromMySQL() bool { + return accesslogs.SharedStorageManager.WriteMySQL() } func (this *HTTPAccessLogService) writeAccessLogsToPolicy(pbAccessLogs []*pb.HTTPAccessLog) error { diff --git a/EdgeAdmin/web/public/js/components.js b/EdgeAdmin/web/public/js/components.js index d0e0799..99daa3f 100644 --- a/EdgeAdmin/web/public/js/components.js +++ b/EdgeAdmin/web/public/js/components.js @@ -1,3 +1,4691 @@ +// 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: `
+ +
` +}) + +Vue.component("ad-instance-objects-box", { + props: ["v-objects", "v-user-id"], + mounted: function () { + this.getUserServers(1) + }, + data: function () { + let objects = this.vObjects + if (objects == null) { + objects = [] + } + + let objectCodes = [] + objects.forEach(function (v) { + objectCodes.push(v.code) + }) + + return { + userId: this.vUserId, + objects: objects, + objectCodes: objectCodes, + isAdding: true, + + servers: [], + serversIsLoading: false + } + }, + methods: { + add: function () { + this.isAdding = true + }, + cancel: function () { + this.isAdding = false + }, + remove: function (index) { + let that = this + teaweb.confirm("确定要删除此防护对象吗?", function () { + that.objects.$remove(index) + that.notifyChange() + }) + }, + removeObjectCode: function (objectCode) { + let index = -1 + this.objectCodes.forEach(function (v, k) { + if (objectCode == v) { + index = k + } + }) + if (index >= 0) { + this.objects.$remove(index) + this.notifyChange() + } + }, + getUserServers: function (page) { + if (Tea.Vue == null) { + let that = this + setTimeout(function () { + that.getUserServers(page) + }, 100) + return + } + + let that = this + this.serversIsLoading = true + Tea.Vue.$post(".userServers") + .params({ + userId: this.userId, + page: page, + pageSize: 5 + }) + .success(function (resp) { + that.servers = resp.data.servers + + that.$refs.serverPage.updateMax(resp.data.page.max) + that.serversIsLoading = false + }) + .error(function () { + that.serversIsLoading = false + }) + }, + changeServerPage: function (page) { + this.getUserServers(page) + }, + selectServerObject: function (server) { + if (this.existObjectCode("server:" + server.id)) { + return + } + + this.objects.push({ + "type": "server", + "code": "server:" + server.id, + + "id": server.id, + "name": server.name + }) + this.notifyChange() + }, + notifyChange: function () { + let objectCodes = [] + this.objects.forEach(function (v) { + objectCodes.push(v.code) + }) + this.objectCodes = objectCodes + }, + existObjectCode: function (objectCode) { + let found = false + this.objects.forEach(function (v) { + if (v.code == objectCode) { + found = true + } + }) + return found + } + }, + template: `
+ + + +
+
暂时还没有设置任何防护对象。
+
+ + + + + +
已选中防护对象 +
+ 网站:{{object.name}} +   +
+
+
+
+
+ + +
+ + + + + + + + + + +
对象类型网站
网站列表 + 加载中... +
暂时还没有可选的网站。
+ + + + + + + + + + + +
网站名称操作
{{server.name}} + 选中 + 取消 +
+ + +
+
+ + +
+ +
+
` +}) + +Vue.component("api-node-addresses-box", { + props: ["v-addrs", "v-name"], + data: function () { + let addrs = this.vAddrs + if (addrs == null) { + addrs = [] + } + return { + addrs: addrs + } + }, + methods: { + // 添加IP地址 + addAddr: function () { + let that = this; + teaweb.popup("/settings/api/node/createAddrPopup", { + height: "16em", + callback: function (resp) { + that.addrs.push(resp.data.addr); + } + }) + }, + + // 修改地址 + updateAddr: function (index, addr) { + let that = this; + window.UPDATING_ADDR = addr + teaweb.popup("/settings/api/node/updateAddrPopup?addressId=", { + callback: function (resp) { + Vue.set(that.addrs, index, resp.data.addr); + } + }) + }, + + // 删除IP地址 + removeAddr: function (index) { + this.addrs.$remove(index); + } + }, + template: `
+ +
+
+
+ {{addr.protocol}}://{{addr.host.quoteIP()}}:{{addr.portRange}} + + +
+
+
+
+
+ +
+
` +}) + +Vue.component("api-node-selector", { + props: [], + data: function () { + return {} + }, + template: `
+ 暂未实现 +
` +}) + +// 单个集群选择 +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("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("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-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-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("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("node-ddos-protection-config-box", { + props: ["v-ddos-protection-config", "v-default-configs", "v-is-node", "v-cluster-is-on"], + data: function () { + let config = this.vDdosProtectionConfig + if (config == null) { + config = { + tcp: { + isPrior: false, + isOn: false, + maxConnections: 0, + maxConnectionsPerIP: 0, + newConnectionsRate: 0, + newConnectionsRateBlockTimeout: 0, + newConnectionsSecondlyRate: 0, + newConnectionSecondlyRateBlockTimeout: 0, + allowIPList: [], + ports: [] + } + } + } + + // initialize + if (config.tcp == null) { + config.tcp = { + isPrior: false, + isOn: false, + maxConnections: 0, + maxConnectionsPerIP: 0, + newConnectionsRate: 0, + newConnectionsRateBlockTimeout: 0, + newConnectionsSecondlyRate: 0, + newConnectionSecondlyRateBlockTimeout: 0, + allowIPList: [], + ports: [] + } + } + + + return { + config: config, + defaultConfigs: this.vDefaultConfigs, + isNode: this.vIsNode, + + isAddingPort: false + } + }, + methods: { + changeTCPPorts: function (ports) { + this.config.tcp.ports = ports + }, + changeTCPAllowIPList: function (ipList) { + this.config.tcp.allowIPList = ipList + } + }, + template: `
+ + +

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

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

TCP设置

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

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

+
单IP TCP最大连接数 + +

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

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

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

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

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

+
TCP端口列表 + +

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

+
IP白名单 + +

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

+
+
+
` +}) + +Vue.component("bandwidth-size-capacity-box", { + props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"], + data: function () { + let v = this.vValue + if (v == null) { + v = { + count: this.vCount, + unit: this.vUnit + } + } + if (v.unit == null || v.unit.length == 0) { + v.unit = "mb" + } + + if (typeof (v["count"]) != "number") { + v["count"] = -1 + } + + let vSize = this.size + if (vSize == null) { + vSize = 6 + } + + let vMaxlength = this.maxlength + if (vMaxlength == null) { + vMaxlength = 10 + } + + let supportedUnits = this.vSupportedUnits + if (supportedUnits == null) { + supportedUnits = [] + } + + return { + capacity: v, + countString: (v.count >= 0) ? v.count.toString() : "", + vSize: vSize, + vMaxlength: vMaxlength, + supportedUnits: supportedUnits + } + }, + watch: { + "countString": function (newValue) { + let value = newValue.trim() + if (value.length == 0) { + this.capacity.count = -1 + this.change() + return + } + let count = parseInt(value) + if (!isNaN(count)) { + this.capacity.count = count + } + this.change() + } + }, + methods: { + change: function () { + this.$emit("change", this.capacity) + } + }, + template: `
+ +
+ +
+
+ +
+
` +}) + +Vue.component("bandwidth-size-capacity-view", { + props: ["v-value"], + data: function () { + let capacity = this.vValue + if (capacity != null && capacity.count > 0 && typeof capacity.unit === "string") { + capacity.unit = capacity.unit[0].toUpperCase() + capacity.unit.substring(1) + "ps" + } + return { + capacity: capacity + } + }, + template: ` + {{capacity.count}}{{capacity.unit}} +` +}) + +Vue.component("bits-var", { + props: ["v-bits"], + data: function () { + let bits = this.vBits + if (typeof bits != "number") { + bits = 0 + } + let format = teaweb.splitFormat(teaweb.formatBits(bits)) + return { + format: format + } + }, + template:` + {{format[0]}}{{format[1]}} +` +}) + +Vue.component("bytes-var", { + props: ["v-bytes"], + data: function () { + let bytes = this.vBytes + if (typeof bytes != "number") { + bytes = 0 + } + let format = teaweb.splitFormat(teaweb.formatBytes(bytes)) + return { + format: format + } + }, + template:` + {{format[0]}}{{format[1]}} +` +}) + +Vue.component("chart-columns-grid", { + props: [], + mounted: function () { + this.columns = this.calculateColumns() + + let that = this + window.addEventListener("resize", function () { + that.columns = that.calculateColumns() + }) + }, + updated: function () { + let totalElements = this.$el.getElementsByClassName("column").length + if (totalElements == this.totalElements) { + return + } + this.totalElements = totalElements + this.calculateColumns() + }, + data: function () { + return { + columns: "four", + totalElements: 0 + } + }, + methods: { + calculateColumns: function () { + let w = window.innerWidth + let columns = Math.floor(w / 500) + if (columns == 0) { + columns = 1 + } + + let columnElements = this.$el.getElementsByClassName("column") + if (columnElements.length == 0) { + return "one" + } + let maxColumns = columnElements.length + if (columns > maxColumns) { + columns = maxColumns + } + + // 添加右侧边框 + for (let index = 0; index < columnElements.length; index++) { + let el = columnElements[index] + el.className = el.className.replace("with-border", "") + if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) { + el.className += " with-border" + } + } + + switch (columns) { + case 1: + return "one" + case 2: + return "two" + case 3: + return "three" + case 4: + return "four" + case 5: + return "five" + case 6: + return "six" + case 7: + return "seven" + case 8: + return "eight" + case 9: + return "nine" + case 10: + return "ten" + default: + return "ten" + } + } + }, + template: `
+ +
` +}) + +let checkboxId = 0 +Vue.component("checkbox", { + props: ["name", "value", "v-value", "id", "checked"], + data: function () { + checkboxId++ + let elementId = this.id + if (elementId == null) { + elementId = "checkbox" + checkboxId + } + + let elementValue = this.vValue + if (elementValue == null) { + elementValue = "1" + } + + let checkedValue = this.value + if (checkedValue == null && this.checked == "checked") { + checkedValue = elementValue + } + + return { + elementId: elementId, + elementValue: elementValue, + newValue: checkedValue + } + }, + methods: { + change: function () { + this.$emit("input", this.newValue) + }, + check: function () { + this.newValue = this.elementValue + }, + uncheck: function () { + this.newValue = "" + }, + isChecked: function () { + return (typeof (this.newValue) == "boolean" && this.newValue) || this.newValue == this.elementValue + } + }, + watch: { + value: function (v) { + if (typeof v == "boolean") { + this.newValue = v + } + } + }, + template: `
+ + +
` +}) + +Vue.component("columns-grid", { + props: [], + mounted: function () { + this.columns = this.calculateColumns() + + let that = this + window.addEventListener("resize", function () { + that.columns = that.calculateColumns() + }) + }, + data: function () { + return { + columns: "four" + } + }, + methods: { + calculateColumns: function () { + let w = window.innerWidth + let columns = Math.floor(w / 250) + if (columns == 0) { + columns = 1 + } + + let columnElements = this.$el.getElementsByClassName("column") + if (columnElements.length == 0) { + return + } + let maxColumns = columnElements.length + if (columns > maxColumns) { + columns = maxColumns + } + + // 添加右侧边框 + for (let index = 0; index < columnElements.length; index++) { + let el = columnElements[index] + el.className = el.className.replace("with-border", "") + if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) { + el.className += " with-border" + } + } + + switch (columns) { + case 1: + return "one" + case 2: + return "two" + case 3: + return "three" + case 4: + return "four" + case 5: + return "five" + case 6: + return "six" + case 7: + return "seven" + case 8: + return "eight" + case 9: + return "nine" + case 10: + return "ten" + default: + return "ten" + } + } + }, + template: `
+ +
` +}) + +Vue.component("combo-box", { + // data-url 和 data-key 成对出现 + props: [ + "name", "title", "placeholder", "size", "v-items", "v-value", + "data-url", // 数据源URL + "data-key", // 数据源中数据的键名 + "data-search", // 是否启用动态搜索,如果值为on或true,则表示启用 + "width" + ], + mounted: function () { + if (this.dataURL.length > 0) { + this.search("") + } + + // 设定菜单宽度 + let searchBox = this.$refs.searchBox + if (searchBox != null) { + let inputWidth = searchBox.offsetWidth + if (inputWidth != null && inputWidth > 0) { + this.$refs.menu.style.width = inputWidth + "px" + } else if (this.styleWidth.length > 0) { + this.$refs.menu.style.width = this.styleWidth + } + } + }, + data: function () { + let items = this.vItems + if (items == null || !(items instanceof Array)) { + items = [] + } + items = this.formatItems(items) + + // 当前选中项 + let selectedItem = null + if (this.vValue != null) { + let that = this + items.forEach(function (v) { + if (v.value == that.vValue) { + selectedItem = v + } + }) + } + + let width = this.width + if (width == null || width.length == 0) { + width = "11em" + } else { + if (/\d+$/.test(width)) { + width += "em" + } + } + + // data url + let dataURL = "" + if (typeof this.dataUrl == "string" && this.dataUrl.length > 0) { + dataURL = this.dataUrl + } + + return { + allItems: items, // 原始的所有的items + items: items.$copy(), // 候选的items + selectedItem: selectedItem, // 选中的item + keyword: "", + visible: false, + hideTimer: null, + hoverIndex: 0, + styleWidth: width, + + isInitial: true, + dataURL: dataURL, + urlRequestId: 0 // 记录URL请求ID,防止并行冲突 + } + }, + methods: { + search: function (keyword) { + // 从URL中获取选项数据 + let dataUrl = this.dataURL + let dataKey = this.dataKey + let that = this + + let requestId = Math.random() + this.urlRequestId = requestId + + Tea.action(dataUrl) + .params({ + keyword: (keyword == null) ? "" : keyword + }) + .post() + .success(function (resp) { + if (requestId != that.urlRequestId) { + return + } + + if (resp.data != null) { + if (typeof (resp.data[dataKey]) == "object") { + let items = that.formatItems(resp.data[dataKey]) + that.allItems = items + that.items = items.$copy() + + if (that.isInitial) { + that.isInitial = false + if (that.vValue != null) { + items.forEach(function (v) { + if (v.value == that.vValue) { + that.selectedItem = v + } + }) + } + } + } + } + }) + }, + formatItems: function (items) { + items.forEach(function (v) { + if (v.value == null) { + v.value = v.id + } + }) + return items + }, + reset: function () { + this.selectedItem = null + this.change() + this.hoverIndex = 0 + + let that = this + setTimeout(function () { + if (that.$refs.searchBox) { + that.$refs.searchBox.focus() + } + }) + }, + clear: function () { + this.selectedItem = null + this.change() + this.hoverIndex = 0 + }, + changeKeyword: function () { + let shouldSearch = this.dataURL.length > 0 && (this.dataSearch == "on" || this.dataSearch == "true") + + this.hoverIndex = 0 + let keyword = this.keyword + if (keyword.length == 0) { + if (shouldSearch) { + this.search(keyword) + } else { + this.items = this.allItems.$copy() + } + return + } + + + if (shouldSearch) { + this.search(keyword) + } else { + this.items = this.allItems.$copy().filter(function (v) { + if (v.fullname != null && v.fullname.length > 0 && teaweb.match(v.fullname, keyword)) { + return true + } + return teaweb.match(v.name, keyword) + }) + } + }, + selectItem: function (item) { + this.selectedItem = item + this.change() + this.hoverIndex = 0 + this.keyword = "" + this.changeKeyword() + }, + confirm: function () { + if (this.items.length > this.hoverIndex) { + this.selectItem(this.items[this.hoverIndex]) + } + }, + show: function () { + this.visible = true + + // 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换 + }, + hide: function () { + let that = this + this.hideTimer = setTimeout(function () { + that.visible = false + }, 500) + }, + downItem: function () { + this.hoverIndex++ + if (this.hoverIndex > this.items.length - 1) { + this.hoverIndex = 0 + } + this.focusItem() + }, + upItem: function () { + this.hoverIndex-- + if (this.hoverIndex < 0) { + this.hoverIndex = 0 + } + this.focusItem() + }, + focusItem: function () { + if (this.hoverIndex < this.items.length) { + this.$refs.itemRef[this.hoverIndex].focus() + let that = this + setTimeout(function () { + that.$refs.searchBox.focus() + if (that.hideTimer != null) { + clearTimeout(that.hideTimer) + that.hideTimer = null + } + }) + } + }, + change: function () { + this.$emit("change", this.selectedItem) + + let that = this + setTimeout(function () { + if (that.$refs.selectedLabel != null) { + that.$refs.selectedLabel.focus() + } + }) + }, + submitForm: function (event) { + if (event.target.tagName != "A") { + return + } + let parentBox = this.$refs.selectedLabel.parentNode + while (true) { + parentBox = parentBox.parentNode + if (parentBox == null || parentBox.tagName == "BODY") { + return + } + if (parentBox.tagName == "FORM") { + parentBox.submit() + break + } + } + }, + + setDataURL: function (dataURL) { + this.dataURL = dataURL + }, + reloadData: function () { + this.search("") + } + }, + template: `
+ +
+ +
+ + +
+ + {{title}}:{{selectedItem.name}} + + +
+ + +
+ +
+
` +}) + +Vue.component("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("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("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("datepicker", { + props: ["value", "v-name", "name", "v-value", "v-bottom-left", "placeholder"], + mounted: function () { + let that = this + teaweb.datepicker(this.$refs.dayInput, function (v) { + that.day = v + that.change() + }, !!this.vBottomLeft) + }, + data: function () { + let name = this.vName + if (name == null) { + name = this.name + } + if (name == null) { + name = "day" + } + + let day = this.vValue + if (day == null) { + day = this.value + if (day == null) { + day = "" + } + } + + let placeholder = "YYYY-MM-DD" + if (this.placeholder != null) { + placeholder = this.placeholder + } + + return { + realName: name, + realPlaceholder: placeholder, + day: day + } + }, + watch: { + value: function (v) { + this.day = v + + let picker = this.$refs.dayInput.picker + if (picker != null) { + if (v != null && /^\d+-\d+-\d+$/.test(v)) { + picker.setDate(v) + } + } + } + }, + methods: { + change: function () { + this.$emit("input", this.day) // support v-model,事件触发需要在 change 之前 + this.$emit("change", this.day) + } + }, + template: `
+ +
` +}) + +Vue.component("datetime-input", { + props: ["v-name", "v-timestamp"], + mounted: function () { + let that = this + teaweb.datepicker(this.$refs.dayInput, function (v) { + that.day = v + that.hour = "23" + that.minute = "59" + that.second = "59" + that.change() + }) + }, + data: function () { + let timestamp = this.vTimestamp + if (timestamp != null) { + timestamp = parseInt(timestamp) + if (isNaN(timestamp)) { + timestamp = 0 + } + } else { + timestamp = 0 + } + + let day = "" + let hour = "" + let minute = "" + let second = "" + + if (timestamp > 0) { + let date = new Date() + date.setTime(timestamp * 1000) + + let year = date.getFullYear().toString() + let month = this.leadingZero((date.getMonth() + 1).toString(), 2) + day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2) + + hour = this.leadingZero(date.getHours().toString(), 2) + minute = this.leadingZero(date.getMinutes().toString(), 2) + second = this.leadingZero(date.getSeconds().toString(), 2) + } + + return { + timestamp: timestamp, + day: day, + hour: hour, + minute: minute, + second: second, + + hasDayError: false, + hasHourError: false, + hasMinuteError: false, + hasSecondError: false + } + }, + methods: { + change: function () { + // day + if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) { + this.hasDayError = true + return + } + let pieces = this.day.split("-") + let year = parseInt(pieces[0]) + + let month = parseInt(pieces[1]) + if (month < 1 || month > 12) { + this.hasDayError = true + return + } + + let day = parseInt(pieces[2]) + if (day < 1 || day > 32) { + this.hasDayError = true + return + } + + this.hasDayError = false + + // hour + if (!/^\d+$/.test(this.hour)) { + this.hasHourError = true + return + } + let hour = parseInt(this.hour) + if (isNaN(hour)) { + this.hasHourError = true + return + } + if (hour < 0 || hour >= 24) { + this.hasHourError = true + return + } + this.hasHourError = false + + // minute + if (!/^\d+$/.test(this.minute)) { + this.hasMinuteError = true + return + } + let minute = parseInt(this.minute) + if (isNaN(minute)) { + this.hasMinuteError = true + return + } + if (minute < 0 || minute >= 60) { + this.hasMinuteError = true + return + } + this.hasMinuteError = false + + // second + if (!/^\d+$/.test(this.second)) { + this.hasSecondError = true + return + } + let second = parseInt(this.second) + if (isNaN(second)) { + this.hasSecondError = true + return + } + if (second < 0 || second >= 60) { + this.hasSecondError = true + return + } + this.hasSecondError = false + + let date = new Date(year, month - 1, day, hour, minute, second) + this.timestamp = Math.floor(date.getTime() / 1000) + }, + leadingZero: function (s, l) { + s = s.toString() + if (l <= s.length) { + return s + } + for (let i = 0; i < l - s.length; i++) { + s = "0" + s + } + return s + }, + resultTimestamp: function () { + return this.timestamp + }, + nextYear: function () { + let date = new Date() + date.setFullYear(date.getFullYear()+1) + this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) + this.hour = this.leadingZero(date.getHours(), 2) + this.minute = this.leadingZero(date.getMinutes(), 2) + this.second = this.leadingZero(date.getSeconds(), 2) + this.change() + }, + nextDays: function (days) { + let date = new Date() + date.setTime(date.getTime() + days * 86400 * 1000) + this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) + this.hour = this.leadingZero(date.getHours(), 2) + this.minute = this.leadingZero(date.getMinutes(), 2) + this.second = this.leadingZero(date.getSeconds(), 2) + this.change() + }, + nextHours: function (hours) { + let date = new Date() + date.setTime(date.getTime() + hours * 3600 * 1000) + this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) + this.hour = this.leadingZero(date.getHours(), 2) + this.minute = this.leadingZero(date.getMinutes(), 2) + this.second = this.leadingZero(date.getSeconds(), 2) + this.change() + } + }, + template: `
+ +
+
+ +
+
+
:
+
+
:
+
+
+

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

+
` +}) + +Vue.component("dot", { + template: '' +}) + +Vue.component("download-link", { + props: ["v-element", "v-file", "v-value"], + created: function () { + let that = this + setTimeout(function () { + that.url = that.composeURL() + }, 1000) + }, + data: function () { + let filename = this.vFile + if (filename == null || filename.length == 0) { + filename = "unknown-file" + } + return { + file: filename, + url: this.composeURL() + } + }, + methods: { + composeURL: function () { + let text = "" + if (this.vValue != null) { + text = this.vValue + } else { + let e = document.getElementById(this.vElement) + if (e == null) { + // 不提示错误,因为此时可能页面未加载完整 + return + } + text = e.innerText + if (text == null) { + text = e.textContent + } + } + return Tea.url("/ui/download", { + file: this.file, + text: text + }) + } + }, + template: ``, +}) + +Vue.component("file-textarea", { + props: ["value"], + data: function () { + let value = this.value + if (typeof value != "string") { + value = "" + } + return { + realValue: value + } + }, + mounted: function () { + }, + methods: { + dragover: function () {}, + drop: function (e) { + let that = this + e.dataTransfer.items[0].getAsFile().text().then(function (data) { + that.setValue(data) + }) + }, + setValue: function (value) { + this.realValue = value + }, + focus: function () { + this.$refs.textarea.focus() + } + }, + template: `` +}) + +/** + * 一级菜单 + */ +Vue.component("first-menu", { + props: [], + template: ' \ +
\ + \ +
\ +
' +}); + +Vue.component("health-check-config-box", { + props: ["v-health-check-config", "v-check-domain-url", "v-is-plus"], + data: function () { + let healthCheckConfig = this.vHealthCheckConfig + let urlProtocol = "http" + let urlPort = "" + let urlRequestURI = "/" + let urlHost = "" + + if (healthCheckConfig == null) { + healthCheckConfig = { + isOn: false, + url: "", + interval: {count: 60, unit: "second"}, + statusCodes: [200], + timeout: {count: 10, unit: "second"}, + countTries: 3, + tryDelay: {count: 100, unit: "ms"}, + autoDown: true, + countUp: 1, + countDown: 3, + userAgent: "", + onlyBasicRequest: true, + accessLogIsOn: true + } + let that = this + setTimeout(function () { + that.changeURL() + }, 500) + } else { + try { + let url = new URL(healthCheckConfig.url) + urlProtocol = url.protocol.substring(0, url.protocol.length - 1) + + // 域名 + urlHost = url.host + if (urlHost == "%24%7Bhost%7D") { + urlHost = "${host}" + } + let colonIndex = urlHost.indexOf(":") + if (colonIndex > 0) { + urlHost = urlHost.substring(0, colonIndex) + } + + urlPort = url.port + urlRequestURI = url.pathname + if (url.search.length > 0) { + urlRequestURI += url.search + } + } catch (e) { + } + + if (healthCheckConfig.statusCodes == null) { + healthCheckConfig.statusCodes = [200] + } + if (healthCheckConfig.interval == null) { + healthCheckConfig.interval = {count: 60, unit: "second"} + } + if (healthCheckConfig.timeout == null) { + healthCheckConfig.timeout = {count: 10, unit: "second"} + } + if (healthCheckConfig.tryDelay == null) { + healthCheckConfig.tryDelay = {count: 100, unit: "ms"} + } + if (healthCheckConfig.countUp == null || healthCheckConfig.countUp < 1) { + healthCheckConfig.countUp = 1 + } + if (healthCheckConfig.countDown == null || healthCheckConfig.countDown < 1) { + healthCheckConfig.countDown = 3 + } + } + + return { + healthCheck: healthCheckConfig, + advancedVisible: false, + urlProtocol: urlProtocol, + urlHost: urlHost, + urlPort: urlPort, + urlRequestURI: urlRequestURI, + urlIsEditing: healthCheckConfig.url.length == 0, + + hostErr: "" + } + }, + watch: { + urlRequestURI: function () { + if (this.urlRequestURI.length > 0 && this.urlRequestURI[0] != "/") { + this.urlRequestURI = "/" + this.urlRequestURI + } + this.changeURL() + }, + urlPort: function (v) { + let port = parseInt(v) + if (!isNaN(port)) { + this.urlPort = port.toString() + } else { + this.urlPort = "" + } + this.changeURL() + }, + urlProtocol: function () { + this.changeURL() + }, + urlHost: function () { + this.changeURL() + this.hostErr = "" + }, + "healthCheck.countTries": function (v) { + let count = parseInt(v) + if (!isNaN(count)) { + this.healthCheck.countTries = count + } else { + this.healthCheck.countTries = 0 + } + }, + "healthCheck.countUp": function (v) { + let count = parseInt(v) + if (!isNaN(count)) { + this.healthCheck.countUp = count + } else { + this.healthCheck.countUp = 0 + } + }, + "healthCheck.countDown": function (v) { + let count = parseInt(v) + if (!isNaN(count)) { + this.healthCheck.countDown = count + } else { + this.healthCheck.countDown = 0 + } + } + }, + methods: { + showAdvanced: function () { + this.advancedVisible = !this.advancedVisible + }, + changeURL: function () { + let urlHost = this.urlHost + if (urlHost.length == 0) { + urlHost = "${host}" + } + this.healthCheck.url = this.urlProtocol + "://" + urlHost + ((this.urlPort.length > 0) ? ":" + this.urlPort : "") + this.urlRequestURI + }, + changeStatus: function (values) { + this.healthCheck.statusCodes = values.$map(function (k, v) { + let status = parseInt(v) + if (isNaN(status)) { + return 0 + } else { + return status + } + }) + }, + onChangeURLHost: function () { + let checkDomainURL = this.vCheckDomainUrl + if (checkDomainURL == null || checkDomainURL.length == 0) { + return + } + + let that = this + Tea.action(checkDomainURL) + .params({host: this.urlHost}) + .success(function (resp) { + if (!resp.data.isOk) { + that.hostErr = "在当前集群中找不到此域名,可能会影响健康检查结果。" + } else { + that.hostErr = "" + } + }) + .post() + }, + editURL: function () { + this.urlIsEditing = !this.urlIsEditing + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用健康检查 +
+ + +
+

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

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

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

+
端口 + +

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

+
RequestURI +

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

+
+
+

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

+
+
检测时间间隔 + +

两次检查之间的间隔。

+
自动上/下线IP +
+ + +
+

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

+
连续上线次数 + +

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

+
连续下线次数 + +

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

+
允许的状态码 + +

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

+
超时时间 + +

读取检测URL超时时间。

+
连续尝试次数 + +

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

+
每次尝试间隔 + +

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

+
终端信息(User-Agent) + +

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

+
只基础请求 + +

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

+
记录访问日志 + +

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

+
+
+
` +}) + +/** + * 菜单项 + */ +Vue.component("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("inner-menu", { + 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("js-page", { + props: ["v-max"], + data: function () { + let max = this.vMax + if (max == null) { + max = 0 + } + return { + max: max, + page: 1 + } + }, + methods: { + updateMax: function (max) { + this.max = max + }, + selectPage: function(page) { + this.page = page + this.$emit("change", page) + } + }, + template:`
+
+ {{i}} +
+
` +}) + +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("labeled-input", { + props: ["name", "size", "maxlength", "label", "value"], + template: '
\ + \ + {{label}}\ +
' +}); + +// 启用状态标签 +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: `注意:通常不需要修改;如要修改,请在专家指导下进行。` +}) + + +// 使用Icon的链接方式 +Vue.component("link-icon", { + props: ["href", "title", "target", "size"], + data: function () { + let realSize = this.size + if (realSize == null || realSize.length == 0) { + realSize = "small" + } + + return { + vTitle: (this.title == null) ? "打开链接" : this.title, + realSize: realSize + } + }, + template: ` ` +}) + +// 带有下划虚线的连接 +Vue.component("link-red", { + props: ["href", "title"], + data: function () { + let href = this.href + if (href == null) { + href = "" + } + return { + vHref: href + } + }, + methods: { + clickPrevent: function () { + emitClick(this, arguments) + + if (this.vHref.length > 0) { + window.location = this.vHref + } + } + }, + template: `` +}) + +// 会弹出窗口的链接 +Vue.component("link-popup", { + props: ["title"], + methods: { + clickPrevent: function () { + emitClick(this, arguments) + } + }, + template: `` +}) + +Vue.component("popup-icon", { + props: ["title", "href", "height"], + methods: { + clickPrevent: function () { + if (this.href != null && this.href.length > 0) { + teaweb.popup(this.href, { + height: this.height + }) + } + } + }, + template: ` ` +}) + +// 小提示 +Vue.component("tip-icon", { + props: ["content"], + methods: { + showTip: function () { + teaweb.popupTip(this.content) + } + }, + template: `` +}) + +// 提交点击事件 +function emitClick(obj, arguments) { + let event = "click" + let newArgs = [event] + for (let i = 0; i < arguments.length; i++) { + newArgs.push(arguments[i]) + } + obj.$emit.apply(obj, newArgs) +} + +Vue.component("mask-warning", { + 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: '\ + \ + ' +}); + +Vue.component("loading-message", { + 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("more-options-angle", { + data: function () { + return { + isVisible: false + } + }, + methods: { + show: function () { + this.isVisible = !this.isVisible + this.$emit("change", this.isVisible) + } + }, + template: `更多选项收起选项` +}) + +/** + * 更多选项 + */ +Vue.component("more-options-indicator", { + props:[], + data: function () { + return { + visible: false + } + }, + methods: { + changeVisible: function () { + this.visible = !this.visible + if (Tea.Vue != null) { + Tea.Vue.moreOptionsVisible = this.visible + } + this.$emit("change", this.visible) + this.$emit("input", this.visible) + } + }, + template: '更多选项收起选项 ' +}); + +Vue.component("more-options-tbody", { + data: function () { + return { + isVisible: false + } + }, + methods: { + show: function () { + this.isVisible = !this.isVisible + this.$emit("change", this.isVisible) + } + }, + template: ` + + 更多选项收起选项 + +` +}) + +Vue.component("network-addresses-box", { + props: ["v-server-type", "v-addresses", "v-protocol", "v-name", "v-from", "v-support-range", "v-url"], + data: function () { + let addresses = this.vAddresses + if (addresses == null) { + addresses = [] + } + let protocol = this.vProtocol + if (protocol == null) { + protocol = "" + } + + let name = this.vName + if (name == null) { + name = "addresses" + } + + let from = this.vFrom + if (from == null) { + from = "" + } + + return { + addresses: addresses, + protocol: protocol, + name: name, + from: from, + isEditing: false + } + }, + watch: { + "vServerType": function () { + this.addresses = [] + }, + "vAddresses": function () { + if (this.vAddresses != null) { + this.addresses = this.vAddresses + } + } + }, + methods: { + addAddr: function () { + this.isEditing = true + + let that = this + window.UPDATING_ADDR = null + + let url = this.vUrl + if (url == null) { + url = "/servers/addPortPopup" + } + + teaweb.popup(url + "?serverType=" + this.vServerType + "&protocol=" + this.protocol + "&from=" + this.from + "&supportRange=" + (this.supportRange() ? 1 : 0), { + height: "18em", + callback: function (resp) { + var addr = resp.data.address + if (that.addresses.$find(function (k, v) { + return addr.host == v.host && addr.portRange == v.portRange && addr.protocol == v.protocol + }) != null) { + teaweb.warn("要添加的网络地址已经存在") + return + } + that.addresses.push(addr) + if (["https", "https4", "https6"].$contains(addr.protocol)) { + this.tlsProtocolName = "HTTPS" + } else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) { + this.tlsProtocolName = "TLS" + } + + // 发送事件 + that.$emit("change", that.addresses) + } + }) + }, + removeAddr: function (index) { + this.addresses.$remove(index); + + // 发送事件 + this.$emit("change", this.addresses) + }, + updateAddr: function (index, addr) { + let that = this + window.UPDATING_ADDR = addr + + let url = this.vUrl + if (url == null) { + url = "/servers/addPortPopup" + } + + teaweb.popup(url + "?serverType=" + this.vServerType + "&protocol=" + this.protocol + "&from=" + this.from + "&supportRange=" + (this.supportRange() ? 1 : 0), { + height: "18em", + callback: function (resp) { + var addr = resp.data.address + Vue.set(that.addresses, index, addr) + + if (["https", "https4", "https6"].$contains(addr.protocol)) { + this.tlsProtocolName = "HTTPS" + } else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) { + this.tlsProtocolName = "TLS" + } + + // 发送事件 + that.$emit("change", that.addresses) + } + }) + + // 发送事件 + this.$emit("change", this.addresses) + }, + supportRange: function () { + return this.vSupportRange || (this.vServerType == "tcpProxy" || this.vServerType == "udpProxy") + }, + edit: function () { + this.isEditing = true + } + }, + template: `
+ +
+
+
+ {{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}}{{addr.portRange}} +
+     [修改] +
+
+
+
+
+ {{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}}{{addr.portRange}} + + +
+
+
+ [添加端口绑定] +
+
` +}) + +Vue.component("network-addresses-view", { + props: ["v-addresses"], + template: `
+
+ {{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}} +
+
` +}) + +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("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}}` +}) + +Vue.component("not-found-box", { + props: ["message"], + template: `
+
+

{{message}}

+
` +}) + +Vue.component("page-box", { + data: function () { + return { + page: "" + } + }, + created: function () { + let that = this; + setTimeout(function () { + that.page = Tea.Vue.page; + }) + }, + 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("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}}
+
+
+
+ +
+
` +}) + +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("raquo-item", { + template: `»` +}) + +// 将变量转换为中文 +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("search-box", { + props: ["placeholder", "width"], + data: function () { + let width = this.width + if (width == null) { + width = "10em" + } + return { + realWidth: width, + realValue: "" + } + }, + methods: { + onInput: function () { + this.$emit("input", { value: this.realValue}) + this.$emit("change", { value: this.realValue}) + }, + clearValue: function () { + this.realValue = "" + this.focus() + this.onInput() + }, + focus: function () { + this.$refs.valueRef.focus() + } + }, + template: `
+
+ + +
+
` +}) + +/** + * 二级菜单 + */ +Vue.component("second-menu", { + 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("size-capacity-view", { + props:["v-default-text", "v-value"], + methods: { + composeCapacity: function (capacity) { + return teaweb.convertSizeCapacityToString(capacity) + } + }, + template: `
+ {{composeCapacity(vValue)}} + {{vDefaultText}} +
` +}) + +// 排序使用的箭头 +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: `  ` +}) + +// 给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) +} + + +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("submit-btn", { + template: '' +}); + +Vue.component("time-duration-box", { + props: ["v-name", "v-value", "v-count", "v-unit", "placeholder", "v-min-unit", "maxlength"], + mounted: function () { + this.change() + }, + data: function () { + let v = this.vValue + if (v == null) { + v = { + count: this.vCount, + unit: this.vUnit + } + } + if (typeof (v["count"]) != "number") { + v["count"] = -1 + } + + let minUnit = this.vMinUnit + let units = [ + { + code: "ms", + name: "毫秒" + }, + { + code: "second", + name: "秒" + }, + { + code: "minute", + name: "分钟" + }, + { + code: "hour", + name: "小时" + }, + { + code: "day", + name: "天" + } + ] + let minUnitIndex = -1 + if (minUnit != null && typeof minUnit == "string" && minUnit.length > 0) { + for (let i = 0; i < units.length; i++) { + if (units[i].code == minUnit) { + minUnitIndex = i + break + } + } + } + if (minUnitIndex > -1) { + units = units.slice(minUnitIndex) + } + + let maxLength = parseInt(this.maxlength) + if (typeof maxLength != "number") { + maxLength = 10 + } + + return { + duration: v, + countString: (v.count >= 0) ? v.count.toString() : "", + units: units, + realMaxLength: maxLength + } + }, + watch: { + "countString": function (newValue) { + let value = newValue.trim() + if (value.length == 0) { + this.duration.count = -1 + return + } + let count = parseInt(value) + if (!isNaN(count)) { + this.duration.count = count + } + this.change() + } + }, + methods: { + change: function () { + this.$emit("change", this.duration) + } + }, + template: `
+ +
+ +
+
+ +
+
` +}) + +Vue.component("time-duration-text", { + props: ["v-value"], + methods: { + unitName: function (unit) { + switch (unit) { + case "ms": + return "毫秒" + case "second": + return "秒" + case "minute": + return "分钟" + case "hour": + return "小时" + case "day": + return "天" + } + } + }, + template: ` + {{vValue.count}} {{unitName(vValue.unit)}} +` +}) + +// 信息提示窗口 +Vue.component("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("url-patterns-box", { + props: ["value"], + data: function () { + let patterns = [] + if (this.value != null) { + patterns = this.value + } + + return { + patterns: patterns, + isAdding: false, + + addingPattern: {"type": "wildcard", "pattern": ""}, + editingIndex: -1, + + patternIsInvalid: false, + + windowIsSmall: window.innerWidth < 600 + } + }, + methods: { + add: function () { + this.isAdding = true + let that = this + setTimeout(function () { + that.$refs.patternInput.focus() + }) + }, + edit: function (index) { + this.isAdding = true + this.editingIndex = index + this.addingPattern = { + type: this.patterns[index].type, + pattern: this.patterns[index].pattern + } + }, + confirm: function () { + if (this.requireURL(this.addingPattern.type)) { + let pattern = this.addingPattern.pattern.trim() + if (pattern.length == 0) { + let that = this + teaweb.warn("请输入URL", function () { + that.$refs.patternInput.focus() + }) + return + } + } + if (this.editingIndex < 0) { + this.patterns.push({ + type: this.addingPattern.type, + pattern: this.addingPattern.pattern + }) + } else { + this.patterns[this.editingIndex].type = this.addingPattern.type + this.patterns[this.editingIndex].pattern = this.addingPattern.pattern + } + this.notifyChange() + this.cancel() + }, + remove: function (index) { + this.patterns.$remove(index) + this.cancel() + this.notifyChange() + }, + cancel: function () { + this.isAdding = false + this.addingPattern = {"type": "wildcard", "pattern": ""} + this.editingIndex = -1 + }, + patternTypeName: function (patternType) { + switch (patternType) { + case "wildcard": + return "通配符" + case "regexp": + return "正则" + case "images": + return "常见图片文件" + case "audios": + return "常见音频文件" + case "videos": + return "常见视频文件" + } + return "" + }, + notifyChange: function () { + this.$emit("input", this.patterns) + }, + changePattern: function () { + this.patternIsInvalid = false + let pattern = this.addingPattern.pattern + switch (this.addingPattern.type) { + case "wildcard": + if (pattern.indexOf("?") >= 0) { + this.patternIsInvalid = true + } + break + case "regexp": + if (pattern.indexOf("?") >= 0) { + let pieces = pattern.split("?") + for (let i = 0; i < pieces.length - 1; i++) { + if (pieces[i].length == 0 || pieces[i][pieces[i].length - 1] != "\\") { + this.patternIsInvalid = true + } + } + } + break + } + }, + requireURL: function (patternType) { + return patternType == "wildcard" || patternType == "regexp" + } + }, + template: `
+
+
+ [{{patternTypeName(pattern.type)}}] {{pattern.pattern}}   + + +
+
+
+
+
+ +
+
+ +

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

+
+
+ + +
+
+ +
+
+
+
+ +
+
` +}) + +Vue.component("values-box", { + props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"], + data: function () { + let values = this.values; + if (values == null) { + values = []; + } + + if (this.vValues != null && typeof this.vValues == "object") { + values = this.vValues + } + + return { + "realValues": values, + "isUpdating": false, + "isAdding": false, + "index": 0, + "value": "", + isEditing: false + } + }, + methods: { + create: function () { + this.isAdding = true; + var that = this; + setTimeout(function () { + that.$refs.value.focus(); + }, 200); + }, + update: function (index) { + this.cancel() + this.isUpdating = true; + this.index = index; + this.value = this.realValues[index]; + var that = this; + setTimeout(function () { + that.$refs.value.focus(); + }, 200); + }, + confirm: function () { + if (this.value.length == 0) { + if (typeof(this.vAllowEmpty) != "boolean" || !this.vAllowEmpty) { + return + } + } + + // validate + if (typeof(this.validator) == "function") { + let resp = this.validator.call(this, this.value) + if (typeof resp == "object") { + if (typeof resp.isOk == "boolean" && !resp.isOk) { + if (typeof resp.message == "string") { + let that = this + teaweb.warn(resp.message, function () { + that.$refs.value.focus(); + }) + } + return + } + } + } + + if (this.isUpdating) { + Vue.set(this.realValues, this.index, this.value); + } else { + this.realValues.push(this.value); + } + this.cancel() + this.$emit("change", this.realValues) + }, + remove: function (index) { + this.realValues.$remove(index) + this.$emit("change", this.realValues) + }, + cancel: function () { + this.isUpdating = false; + this.isAdding = false; + this.value = ""; + }, + updateAll: function (values) { + this.realValues = values + }, + addValue: function (v) { + this.realValues.push(v) + }, + + startEditing: function () { + this.isEditing = !this.isEditing + }, + allValues: function () { + return this.realValues + } + }, + template: `
+
+
+ {{value}} + [空] +
+ [修改] +
+
+
+
+ {{value}} + [空] + +   + +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
` +}); + +// 警告消息 +Vue.component("warning-message", { + template: `
` +}) + +Vue.component("dns-domain-selector", { + props: ["v-domain-id", "v-domain-name", "v-provider-name"], + data: function () { + let domainId = this.vDomainId + if (domainId == null) { + domainId = 0 + } + let domainName = this.vDomainName + if (domainName == null) { + domainName = "" + } + + let providerName = this.vProviderName + if (providerName == null) { + providerName = "" + } + + return { + domainId: domainId, + domainName: domainName, + providerName: providerName + } + }, + methods: { + select: function () { + let that = this + teaweb.popup("/dns/domains/selectPopup", { + callback: function (resp) { + that.domainId = resp.data.domainId + that.domainName = resp.data.domainName + that.providerName = resp.data.providerName + that.change() + } + }) + }, + remove: function() { + this.domainId = 0 + this.domainName = "" + this.change() + }, + update: function () { + let that = this + teaweb.popup("/dns/domains/selectPopup?domainId=" + this.domainId, { + callback: function (resp) { + that.domainId = resp.data.domainId + that.domainName = resp.data.domainName + that.providerName = resp.data.providerName + that.change() + } + }) + }, + change: function () { + this.$emit("change", { + id: this.domainId, + name: this.domainName + }) + } + }, + template: `
+ +
+ + {{providerName}} » {{domainName}} + + + +
+
+ [选择域名] +
+
` +}) + +Vue.component("dns-resolver-config-box", { + props:["v-dns-resolver-config"], + data: function () { + let config = this.vDnsResolverConfig + if (config == null) { + config = { + type: "default" + } + } + return { + config: config, + types: [ + { + name: "默认", + code: "default" + }, + { + name: "CGO", + code: "cgo" + }, + { + name: "Go原生", + code: "goNative" + }, + ] + } + }, + template: `
+ + + + + + +
使用的DNS解析库 + +

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

+
+
+
` +}) + +Vue.component("dns-resolvers-config-box", { + props: ["value", "name"], + data: function () { + let resolvers = this.value + if (resolvers == null) { + resolvers = [] + } + + let name = this.name + if (name == null || name.length == 0) { + name = "dnsResolversJSON" + } + + return { + formName: name, + resolvers: resolvers, + + host: "", + + isAdding: false + } + }, + methods: { + add: function () { + this.isAdding = true + let that = this + setTimeout(function () { + that.$refs.hostRef.focus() + }) + }, + confirm: function () { + let host = this.host.trim() + if (host.length == 0) { + let that = this + setTimeout(function () { + that.$refs.hostRef.focus() + }) + return + } + this.resolvers.push({ + host: host, + port: 0, // TODO + protocol: "" // TODO + }) + this.cancel() + }, + cancel: function () { + this.isAdding = false + this.host = "" + this.port = 0 + this.protocol = "" + }, + remove: function (index) { + this.resolvers.$remove(index) + } + }, + template: `
+ +
+
+ {{resolver.protocol}}{{resolver.host}}:{{resolver.port}} +   + +
+
+ +
+
+
+ +
+
+ +   +
+
+
+ +
+ +
+
` +}) + +Vue.component("dns-route-selector", { + props: ["v-all-routes", "v-routes"], + data: function () { + let routes = this.vRoutes + if (routes == null) { + routes = [] + } + routes.$sort(function (v1, v2) { + if (v1.domainId == v2.domainId) { + return v1.code < v2.code + } + return (v1.domainId < v2.domainId) ? 1 : -1 + }) + return { + routes: routes, + routeCodes: routes.$map(function (k, v) { + return v.code + "@" + v.domainId + }), + isAdding: false, + routeCode: "", + keyword: "", + searchingRoutes: this.vAllRoutes.$copy() + } + }, + methods: { + add: function () { + this.isAdding = true + this.keyword = "" + this.routeCode = "" + + let that = this + setTimeout(function () { + that.$refs.keywordRef.focus() + }, 200) + }, + cancel: function () { + this.isAdding = false + }, + confirm: function () { + if (this.routeCode.length == 0) { + return + } + if (this.routeCodes.$contains(this.routeCode)) { + teaweb.warn("已经添加过此线路,不能重复添加") + return + } + let that = this + let route = this.vAllRoutes.$find(function (k, v) { + return v.code + "@" + v.domainId == that.routeCode + }) + if (route == null) { + return + } + + this.routeCodes.push(this.routeCode) + this.routes.push(route) + + this.routes.$sort(function (v1, v2) { + if (v1.domainId == v2.domainId) { + return v1.code < v2.code + } + return (v1.domainId < v2.domainId) ? 1 : -1 + }) + + this.routeCode = "" + this.isAdding = false + }, + remove: function (route) { + this.routeCodes.$removeValue(route.code + "@" + route.domainId) + this.routes.$removeIf(function (k, v) { + return v.code + "@" + v.domainId == route.code + "@" + route.domainId + }) + }, + clearKeyword: function () { + this.keyword = "" + } + }, + watch: { + keyword: function (keyword) { + if (keyword.length == 0) { + this.searchingRoutes = this.vAllRoutes.$copy() + this.routeCode = "" + return + } + this.searchingRoutes = this.vAllRoutes.filter(function (route) { + return teaweb.match(route.name, keyword) || teaweb.match(route.code, keyword) || teaweb.match(route.domainName, keyword) + }) + if (this.searchingRoutes.length > 0) { + this.routeCode = this.searchingRoutes[0].code + "@" + this.searchingRoutes[0].domainId + } else { + this.routeCode = "" + } + } + }, + template: `
+ +
+ + {{route.name}} ({{route.domainName}}) + +
+
+ +
+ + + + + + + + + +
所有线路 + 没有和关键词“{{keyword}}”匹配的线路 + + + +
搜索线路 +
+ + +
+
+ +   +
+
` +}) + +Vue.component("finance-user-selector", { + props: ["v-user-id"], + data: function () { + return {} + }, + methods: { + change: function (userId) { + this.$emit("change", userId) + } + }, + template: `
+ +
` +}) + +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}})
+
+ [选择已有认证]     [添加新认证] +
+
` +}) + +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("ip-item-text", { + props: ["v-item"], + template: ` + * + + {{vItem.value}} + + {{vItem.ipFrom}} + - {{vItem.ipTo}} + + +   级别:{{vItem.eventLevelName}} +` +}) + +// 绑定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: `
+ 绑定+   已绑定: +
+ {{list.name}} + +
+
` +}) + +Vue.component("ip-list-table", { + props: ["v-items", "v-keyword", "v-show-search-button", "v-total"/** total items >= items length **/], + data: function () { + let maxDeletes = 10000 + if (this.vTotal != null && this.vTotal > 0 && this.vTotal < maxDeletes) { + maxDeletes = this.vTotal + } + + return { + items: this.vItems, + keyword: (this.vKeyword != null) ? this.vKeyword : "", + selectedAll: false, + hasSelectedItems: false, + + MaxDeletes: maxDeletes + } + }, + methods: { + updateItem: function (itemId) { + this.$emit("update-item", itemId) + }, + deleteItem: function (itemId) { + this.$emit("delete-item", itemId) + }, + viewLogs: function (itemId) { + teaweb.popup("/servers/iplists/accessLogsPopup?itemId=" + itemId, { + width: "50em", + height: "30em" + }) + }, + changeSelectedAll: function () { + let boxes = this.$refs.itemCheckBox + if (boxes == null) { + return + } + + let that = this + boxes.forEach(function (box) { + box.checked = that.selectedAll + }) + + this.hasSelectedItems = this.selectedAll + }, + changeSelected: function (e) { + let that = this + that.hasSelectedItems = false + let boxes = that.$refs.itemCheckBox + if (boxes == null) { + return + } + boxes.forEach(function (box) { + if (box.checked) { + that.hasSelectedItems = true + } + }) + }, + deleteAll: function () { + let boxes = this.$refs.itemCheckBox + if (boxes == null) { + return + } + let itemIds = [] + boxes.forEach(function (box) { + if (box.checked) { + itemIds.push(box.value) + } + }) + if (itemIds.length == 0) { + return + } + + Tea.action("/servers/iplists/deleteItems") + .post() + .params({ + itemIds: itemIds + }) + .success(function () { + teaweb.successToast("批量删除成功", 1200, teaweb.reload) + }) + }, + deleteCount: function () { + let that = this + teaweb.confirm("确定要批量删除当前列表中的" + this.MaxDeletes + "个IP吗?", function () { + let query = window.location.search + if (query.startsWith("?")) { + query = query.substring(1) + } + Tea.action("/servers/iplists/deleteCount?" + query) + .post() + .params({count: that.MaxDeletes}) + .success(function () { + teaweb.successToast("批量删除成功", 1200, teaweb.reload) + }) + }) + }, + formatSeconds: function (seconds) { + if (seconds < 60) { + return seconds + "秒" + } + if (seconds < 3600) { + return Math.ceil(seconds / 60) + "分钟" + } + if (seconds < 86400) { + return Math.ceil(seconds / 3600) + "小时" + } + return Math.ceil(seconds / 86400) + "天" + }, + cancelChecked: function () { + this.hasSelectedItems = false + this.selectedAll = false + + let boxes = this.$refs.itemCheckBox + if (boxes == null) { + return + } + boxes.forEach(function (box) { + box.checked = false + }) + } + }, + template: `
+
+
+ +     + + +     + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
IP类型级别过期时间备注操作
+
+ + +
+
+ + {{item.value}} + + {{item.ipFrom}}  New   + - {{item.ipTo}} + + + * + +
+ {{item.region}} + | {{item.isp}} +
+
{{item.isp}}
+ + +
+ IPv4 + IPv4 + IPv6 + 所有IP + + {{item.eventLevelName}} + - + +
+ {{item.expiredTime}} +
+ 已过期 +
+
+ {{formatSeconds(item.lifeSeconds)}} + 已过期 +
+
+ 不过期 +
+ {{item.reason}} + - + + + + + + 日志   + 修改   + 删除 +
+
` +}) + Vue.component("traffic-map-box", { props: ["v-stats", "v-is-attack"], mounted: function () { @@ -267,575 +4955,62 @@ Vue.component("traffic-map-box-table", { ` }) -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 - } +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 - let exists = false - this.ports.forEach(function (v) { - if (v.port == port) { - exists = true - } - }) - if (exists) { - this.warn("端口号已经存在") - return - } - - this.ports.push({ - port: port, - description: this.addingPort.description - }) - this.notifyChange() - this.cancel() - }, - cancel: function () { - this.isAdding = false - this.addingPort = { - port: "", - description: "" - } - }, - remove: function (index) { - this.ports.$remove(index) - this.notifyChange() - }, - warn: function (message) { - let that = this - teaweb.warn(message, function () { - that.$refs.addingPortInput.focus() - }) - }, - notifyChange: function () { - this.$emit("change", this.ports) - } - }, - template: `
-
-
- {{portConfig.port}} ({{portConfig.description}}) -
-
-
-
-
-
-
- 端口 - -
-
-
-
- 备注 - -
-
-
- -  取消 -
-
-
-
- -
-
` -}) - -// 显示节点的多个集群 -Vue.component("node-clusters-labels", { - props: ["v-primary-cluster", "v-secondary-clusters", "size"], - data: function () { - let cluster = this.vPrimaryCluster - let secondaryClusters = this.vSecondaryClusters - if (secondaryClusters == null) { - secondaryClusters = [] - } - - let labelSize = this.size - if (labelSize == null) { - labelSize = "small" - } - return { - cluster: cluster, - secondaryClusters: secondaryClusters, - labelSize: labelSize - } - }, - template: `
- - {{cluster.name}} - {{cluster.name}} - - - {{c.name}} - {{c.name}} - -
` -}) - -// 单个集群选择 -Vue.component("cluster-selector", { - props: ["v-cluster-id"], - mounted: function () { - let that = this - - Tea.action("/clusters/options") - .post() - .success(function (resp) { - that.clusters = resp.data.clusters - }) - }, - data: function () { - let clusterId = this.vClusterId - if (clusterId == null) { - clusterId = 0 - } - return { - clusters: [], - clusterId: clusterId - } - }, - template: `
- -
` -}) - -Vue.component("node-ddos-protection-config-box", { - props: ["v-ddos-protection-config", "v-default-configs", "v-is-node", "v-cluster-is-on"], - data: function () { - let config = this.vDdosProtectionConfig - if (config == null) { - config = { - tcp: { - isPrior: false, - isOn: false, - maxConnections: 0, - maxConnectionsPerIP: 0, - newConnectionsRate: 0, - newConnectionsRateBlockTimeout: 0, - newConnectionsSecondlyRate: 0, - newConnectionSecondlyRateBlockTimeout: 0, - allowIPList: [], - ports: [] - } - } - } - - // initialize - if (config.tcp == null) { - config.tcp = { - isPrior: false, - isOn: false, - maxConnections: 0, - maxConnectionsPerIP: 0, - newConnectionsRate: 0, - newConnectionsRateBlockTimeout: 0, - newConnectionsSecondlyRate: 0, - newConnectionSecondlyRateBlockTimeout: 0, - allowIPList: [], - ports: [] - } - } - - - return { - config: config, - defaultConfigs: this.vDefaultConfigs, - isNode: this.vIsNode, - - isAddingPort: false - } - }, - methods: { - changeTCPPorts: function (ports) { - this.config.tcp.ports = ports - }, - changeTCPAllowIPList: function (ipList) { - this.config.tcp.allowIPList = ipList - } - }, - template: `
- - -

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

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

TCP设置

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

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

-
单IP TCP最大连接数 - -

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

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

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

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

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

-
TCP端口列表 - -

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

-
IP白名单 - -

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

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

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

-
从集群 -
-
{{cluster.name}}  
-
-
- -
-
+ // 初始化简介 + 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: `
+ +

` }) @@ -1008,65 +5183,6 @@ Vue.component("message-recipient-group-selector", {
` }) -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 () { @@ -1181,6 +5297,1839 @@ Vue.component("message-row", { ` }) +Vue.component("node-cache-disk-dirs-box", { + props: ["value", "name"], + data: function () { + let dirs = this.value + if (dirs == null) { + dirs = [] + } + return { + dirs: dirs, + + isEditing: false, + isAdding: false, + + addingPath: "" + } + }, + methods: { + add: function () { + this.isAdding = true + let that = this + setTimeout(function () { + that.$refs.addingPath.focus() + }, 100) + }, + confirm: function () { + let addingPath = this.addingPath.trim() + if (addingPath.length == 0) { + let that = this + teaweb.warn("请输入要添加的缓存目录", function () { + that.$refs.addingPath.focus() + }) + return + } + if (addingPath[0] != "/") { + addingPath = "/" + addingPath + } + this.dirs.push({ + path: addingPath + }) + this.cancel() + }, + cancel: function () { + this.addingPath = "" + this.isAdding = false + this.isEditing = false + }, + remove: function (index) { + let that = this + teaweb.confirm("确定要删除此目录吗?", function () { + that.dirs.$remove(index) + }) + } + }, + template: `
+ +
+ + {{dir.path}}   + +
+ + +
+
+
+ +
+
+ +   +
+
+
+ +
+ +
+
` +}) + +Vue.component("node-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-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}}   +
+
+ [选择分组]   [添加分组] +
+
` +}) + +Vue.component("node-ip-address-clusters-selector", { + props: ["vClusters"], + mounted: function () { + this.checkClusters() + }, + data: function () { + let clusters = this.vClusters + if (clusters == null) { + clusters = [] + } + return { + clusters: clusters, + hasCheckedCluster: false, + clustersVisible: false + } + }, + methods: { + checkClusters: function () { + let that = this + + let b = false + this.clusters.forEach(function (cluster) { + if (cluster.isChecked) { + b = true + } + }) + + this.hasCheckedCluster = b + + return b + }, + changeCluster: function (cluster) { + cluster.isChecked = !cluster.isChecked + this.checkClusters() + }, + showClusters: function () { + this.clustersVisible = !this.clustersVisible + } + }, + template: `
+ 默认用于所有集群   修改 +
+ {{cluster.name}}   修改 +

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

+
+
+
+ + {{cluster.name}} + +
+
` +}) + +// 节点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。

+
+
+   + +
+
+ +
+ +
+
+ + +
+   + +
+
+ +
+ +
+
` +}) + +// 节点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-addresses-box", { + props: ["v-ip-addresses", "role", "v-node-id"], + data: function () { + let nodeId = this.vNodeId + if (nodeId == null) { + nodeId = 0 + } + + return { + ipAddresses: (this.vIpAddresses == null) ? [] : this.vIpAddresses, + supportThresholds: this.role != "ns", + nodeId: nodeId + } + }, + methods: { + // 添加IP地址 + addIPAddress: function () { + window.UPDATING_NODE_IP_ADDRESS = null + + let that = this; + teaweb.popup("/nodes/ipAddresses/createPopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), { + callback: function (resp) { + that.ipAddresses.push(resp.data.ipAddress); + }, + height: "24em", + width: "44em" + }) + }, + + // 修改地址 + updateIPAddress: function (index, address) { + window.UPDATING_NODE_IP_ADDRESS = teaweb.clone(address) + + let that = this; + teaweb.popup("/nodes/ipAddresses/updatePopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), { + callback: function (resp) { + Vue.set(that.ipAddresses, index, resp.data.ipAddress); + }, + height: "24em", + width: "44em" + }) + }, + + // 删除IP地址 + removeIPAddress: function (index) { + this.ipAddresses.$remove(index); + }, + + // 判断是否为IPv6 + isIPv6: function (ip) { + return ip.indexOf(":") > -1 + } + }, + template: `
+ +
+
+
+ [IPv6] {{address.ip}} + (备注:{{address.name}},不公开访问 + (不公开访问) + [off] + [down] + [{{address.thresholds.length}}个阈值] +   + +   专属集群:[{{cluster.name}}] +   + + + + +
+
+
+
+
+ +
+
` +}) + +// 节点级别选择器 +Vue.component("node-level-selector", { + props: ["v-node-level"], + data: function () { + let levelCode = this.vNodeLevel + if (levelCode == null || levelCode < 1) { + levelCode = 1 + } + return { + levels: [ + { + name: "边缘节点", + code: 1, + description: "普通的边缘节点。" + }, + { + name: "L2节点", + code: 2, + description: "特殊的边缘节点,同时负责同组上一级节点的回源。" + } + ], + levelCode: levelCode + } + }, + watch: { + levelCode: function (code) { + this.$emit("change", code) + } + }, + template: `
+ +

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

+
` +}) + +// 节点登录推荐端口 +Vue.component("node-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-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-schedule-action-box", { + props: ["value", "v-actions"], + data: function () { + let actionConfig = this.value + if (actionConfig == null) { + actionConfig = { + code: "", + params: {} + } + } + + return { + actions: this.vActions, + currentAction: null, + actionConfig: actionConfig + } + }, + watch: { + "actionConfig.code": function (actionCode) { + if (actionCode.length == 0) { + this.currentAction = null + } else { + this.currentAction = this.actions.$find(function (k, v) { + return v.code == actionCode + }) + } + this.actionConfig.params = {} + } + }, + template: `
+ +
+
+ +
+

{{currentAction.description}}

+ +
+ +

接收通知的URL。

+
+
+
` +}) + +Vue.component("node-schedule-conds-box", { + props: ["value", "v-params", "v-operators"], + mounted: function () { + this.formatConds(this.condsConfig.conds) + this.$forceUpdate() + }, + data: function () { + let condsConfig = this.value + if (condsConfig == null) { + condsConfig = { + conds: [], + connector: "and" + } + } + if (condsConfig.conds == null) { + condsConfig.conds = [] + } + + let paramMap = {} + this.vParams.forEach(function (param) { + paramMap[param.code] = param + }) + + let operatorMap = {} + this.vOperators.forEach(function (operator) { + operatorMap[operator.code] = operator.name + }) + + return { + condsConfig: condsConfig, + params: this.vParams, + paramMap: paramMap, + operatorMap: operatorMap, + operator: "", + + isAdding: false, + + paramCode: "", + param: null, + + valueBandwidth: { + count: 100, + unit: "mb" + }, + valueTraffic: { + count: 1, + unit: "gb" + }, + valueCPU: 80, + valueMemory: 90, + valueLoad: 20, + valueRate: 0 + } + }, + watch: { + paramCode: function (code) { + if (code.length == 0) { + this.param = null + } else { + this.param = this.params.$find(function (k, v) { + return v.code == code + }) + } + this.$emit("changeparam", this.param) + } + }, + methods: { + add: function () { + this.isAdding = true + }, + confirm: function () { + if (this.param == null) { + teaweb.warn("请选择参数") + return + } + if (this.param.operators != null && this.param.operators.length > 0 && this.operator.length == 0) { + teaweb.warn("请选择操作符") + return + } + if (this.param.operators == null || this.param.operators.length == 0) { + this.operator = "" + } + + let value = null + switch (this.param.valueType) { + case "bandwidth": { + if (this.valueBandwidth.unit.length == 0) { + teaweb.warn("请选择带宽单位") + return + } + let count = parseInt(this.valueBandwidth.count.toString()) + if (isNaN(count)) { + count = 0 + } + if (count < 0) { + count = 0 + } + value = { + count: count, + unit: this.valueBandwidth.unit + } + } + break + case "traffic": { + if (this.valueTraffic.unit.length == 0) { + teaweb.warn("请选择带宽单位") + return + } + let count = parseInt(this.valueTraffic.count.toString()) + if (isNaN(count)) { + count = 0 + } + if (count < 0) { + count = 0 + } + value = { + count: count, + unit: this.valueTraffic.unit + } + } + break + case "cpu": + let cpu = parseInt(this.valueCPU.toString()) + if (isNaN(cpu)) { + cpu = 0 + } + if (cpu < 0) { + cpu = 0 + } + if (cpu > 100) { + cpu = 100 + } + value = cpu + break + case "memory": + let memory = parseInt(this.valueMemory.toString()) + if (isNaN(memory)) { + memory = 0 + } + if (memory < 0) { + memory = 0 + } + if (memory > 100) { + memory = 100 + } + value = memory + break + case "load": + let load = parseInt(this.valueLoad.toString()) + if (isNaN(load)) { + load = 0 + } + if (load < 0) { + load = 0 + } + value = load + break + case "rate": + let rate = parseInt(this.valueRate.toString()) + if (isNaN(rate)) { + rate = 0 + } + if (rate < 0) { + rate = 0 + } + value = rate + break + } + + this.condsConfig.conds.push({ + param: this.param.code, + operator: this.operator, + value: value + }) + this.formatConds(this.condsConfig.conds) + + this.cancel() + }, + cancel: function () { + this.isAdding = false + this.paramCode = "" + this.param = null + }, + remove: function (index) { + this.condsConfig.conds.$remove(index) + }, + formatConds: function (conds) { + let that = this + conds.forEach(function (cond) { + switch (that.paramMap[cond.param].valueType) { + case "bandwidth": + cond.valueFormat = cond.value.count + cond.value.unit[0].toUpperCase() + cond.value.unit.substring(1) + "ps" + return + case "traffic": + cond.valueFormat = cond.value.count + cond.value.unit.toUpperCase() + return + case "cpu": + cond.valueFormat = cond.value + "%" + return + case "memory": + cond.valueFormat = cond.value + "%" + return + case "load": + cond.valueFormat = cond.value + return + case "rate": + cond.valueFormat = cond.value + "/秒" + return + } + }) + } + }, + template: `
+ + + +
+ + + {{paramMap[cond.param].name}} + {{operatorMap[cond.operator]}} {{cond.valueFormat}} +   + + +    + +
+ +
+ + + + + + + + + + + + + + + +
参数 + +

{{param.description}}

+
操作符 + +
{{param.valueName}} + +
+
+
+ +
+
+ +
+
+
+ + +
+
+
+ +
+
+ +
+
+
+ + +
+
+ + % +
+
+ + +
+
+ + % +
+
+ + +
+ +
+ + +
+
+ + /秒 +
+
+
+   取消 +
+ +
+ +
+
` +}) + +Vue.component("node-schedule-conds-viewer", { + props: ["value", "v-params", "v-operators"], + mounted: function () { + this.formatConds(this.condsConfig.conds) + this.$forceUpdate() + }, + data: function () { + let paramMap = {} + this.vParams.forEach(function (param) { + paramMap[param.code] = param + }) + + let operatorMap = {} + this.vOperators.forEach(function (operator) { + operatorMap[operator.code] = operator.name + }) + + return { + condsConfig: this.value, + paramMap: paramMap, + operatorMap: operatorMap + } + }, + methods: { + formatConds: function (conds) { + let that = this + conds.forEach(function (cond) { + switch (that.paramMap[cond.param].valueType) { + case "bandwidth": + cond.valueFormat = cond.value.count + cond.value.unit[0].toUpperCase() + cond.value.unit.substring(1) + "ps" + return + case "traffic": + cond.valueFormat = cond.value.count + cond.value.unit.toUpperCase() + return + case "cpu": + cond.valueFormat = cond.value + "%" + return + case "memory": + cond.valueFormat = cond.value + "%" + return + case "load": + cond.valueFormat = cond.value + return + case "rate": + cond.valueFormat = cond.value + "/秒" + return + } + }) + } + }, + template: `
+ + + {{paramMap[cond.param].name}} + {{operatorMap[cond.operator]}} {{cond.valueFormat}} + + +    + +
` +}) + +Vue.component("ns-access-log-box", { + props: ["v-access-log", "v-keyword"], + data: function () { + let accessLog = this.vAccessLog + let isFailure = false + + if (accessLog.isRecursive) { + if (accessLog.recordValue == null || accessLog.recordValue.length == 0) { + isFailure = true + } + } else { + if (accessLog.recordType == "SOA" || accessLog.recordType == "NS") { + if (accessLog.recordValue == null || accessLog.recordValue.length == 0) { + isFailure = true + } + } + + // 没有找到记录的不需要高亮显示,防止管理员看到红色就心理恐慌 + } + + return { + accessLog: accessLog, + isFailure: isFailure + } + }, + methods: { + showLog: function () { + let that = this + let requestId = this.accessLog.requestId + this.$parent.$children.forEach(function (v) { + if (v.deselect != null) { + v.deselect() + } + }) + this.select() + + teaweb.popup("/ns/clusters/accessLogs/viewPopup?requestId=" + requestId, { + width: "50em", + height: "24em", + onClose: function () { + that.deselect() + } + }) + }, + select: function () { + this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)" + }, + deselect: function () { + this.$refs.box.parentNode.style.cssText = "" + } + }, + template: `
+ [{{accessLog.region}}] {{accessLog.remoteAddr}} [{{accessLog.timeLocal}}] [{{accessLog.networking}}] {{accessLog.questionType}} {{accessLog.questionName}} -> + + {{accessLog.recordType}} {{accessLog.recordValue}} +  [没有记录] + + +
+ 线路: {{route.name}} + 递归DNS +
+
+ 错误:[{{accessLog.error}}] +
+
` +}) + +Vue.component("ns-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-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("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-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-domain-group-selector", { props: ["v-domain-group-id"], data: function () { @@ -1221,169 +7170,423 @@ Vue.component("ns-domain-group-selector", { ` }) -// 选择多个线路 -Vue.component("ns-routes-selector", { - props: ["v-routes", "name"], - mounted: function () { - let that = this - Tea.action("/ns/routes/options") - .post() - .success(function (resp) { - that.routes = resp.data.routes - - // provinces - let provinces = {} - if (resp.data.provinces != null && resp.data.provinces.length > 0) { - for (const province of resp.data.provinces) { - let countryCode = province.countryCode - if (typeof provinces[countryCode] == "undefined") { - provinces[countryCode] = [] - } - provinces[countryCode].push({ - name: province.name, - code: province.code - }) - } - } - that.provinces = provinces - }) - }, +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 selectedRoutes = this.vRoutes - if (selectedRoutes == null) { - selectedRoutes = [] + 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: [] + } + } } - let inputName = this.name - if (typeof inputName != "string" || inputName.length == 0) { - inputName = "routeCodes" + // 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 { - routeCode: "default", - inputName: inputName, - routes: [], + config: config, + defaultConfigs: this.vDefaultConfigs, + isNode: this.vIsNode, - provinces: {}, // country code => [ province1, province2, ... ] - provinceRouteCode: "", - - isAdding: false, - routeType: "default", - selectedRoutes: selectedRoutes, - } - }, - watch: { - routeType: function (v) { - this.routeCode = "" - let that = this - this.routes.forEach(function (route) { - if (route.type == v && that.routeCode.length == 0) { - that.routeCode = route.code - } - }) + isAddingPort: false } }, methods: { - add: function () { - this.isAdding = true - this.routeType = "default" - this.routeCode = "default" - this.provinceRouteCode = "" - this.$emit("add") + changeTCPPorts: function (ports) { + this.config.tcp.ports = ports }, - cancel: function () { - this.isAdding = false - this.$emit("cancel") - }, - confirm: function () { - if (this.routeCode.length == 0) { - return - } - - let that = this - - // route - let selectedRoute = null - for (const route of this.routes) { - if (route.code == this.routeCode) { - selectedRoute = route - break - } - } - - if (selectedRoute != null) { - // province route - if (this.provinceRouteCode.length > 0 && this.provinces[this.routeCode] != null) { - for (const province of this.provinces[this.routeCode]) { - if (province.code == this.provinceRouteCode) { - selectedRoute = { - name: selectedRoute.name + "-" + province.name, - code: province.code - } - break - } - } - } - - that.selectedRoutes.push(selectedRoute) - } - - this.$emit("change", this.selectedRoutes) - this.cancel() - }, - remove: function (index) { - this.selectedRoutes.$remove(index) - this.$emit("change", this.selectedRoutes) + changeTCPAllowIPList: function (ipList) { + this.config.tcp.allowIPList = ipList } - } - , + }, template: `
-
-
- - {{route.name}}   -
-
-
-
- + + +

功能说明:此功能为试验性质,目前仅能防御简单的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-record-health-check-config-box", { + props:["value", "v-parent-config"], + data: function () { + let config = this.value + if (config == null) { + config = { + isOn: false, + port: 0, + timeoutSeconds: 0, + countUp: 0, + countDown: 0 + } + } + + let parentConfig = this.vParentConfig + + return { + config: config, + portString: config.port.toString(), + timeoutSecondsString: config.timeoutSeconds.toString(), + countUpString: config.countUp.toString(), + countDownString: config.countDown.toString(), + + portIsEditing: config.port > 0, + timeoutSecondsIsEditing: config.timeoutSeconds > 0, + countUpIsEditing: config.countUp > 0, + countDownIsEditing: config.countDown > 0, + + parentConfig: parentConfig + } + }, + watch: { + portString: function (value) { + let port = parseInt(value.toString()) + if (isNaN(port) || port > 65535 || port < 1) { + this.config.port = 0 + } else { + this.config.port = port + } + }, + timeoutSecondsString: function (value) { + let timeoutSeconds = parseInt(value.toString()) + if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) { + this.config.timeoutSeconds = 0 + } else { + this.config.timeoutSeconds = timeoutSeconds + } + }, + countUpString: function (value) { + let countUp = parseInt(value.toString()) + if (isNaN(countUp) || countUp > 1000 || countUp < 1) { + this.config.countUp = 0 + } else { + this.config.countUp = countUp + } + }, + countDownString: function (value) { + let countDown = parseInt(value.toString()) + if (isNaN(countDown) || countDown > 1000 || countDown < 1) { + this.config.countDown = 0 + } else { + this.config.countDown = countDown + } + } + }, + template: `
+ + + - + + + + + + + - + - - + + -
选择类型 *启用当前记录健康检查 - + +
检测端口 + + 默认{{parentConfig.port}} +   [修改] + +
+ + +

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

+
选择线路 *超时时间 - + + 默认{{parentConfig.timeoutSeconds}}秒 +   [修改] + +
+ +
+ + +
+
选择省/州
默认连续上线次数 - + + 默认{{parentConfig.countUp}}次 +   [修改] + +
+ +
+ + +
+

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

+
-
- -   取消 -
-
- + + 默认连续下线次数 + + + 默认{{parentConfig.countDown}}次 +   [修改] + +
+
+ [使用默认] +
+
+ + +
+

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

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

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

+
默认检测端口 + +

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

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

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

+
默认连续下线次数 +
+ + +
+

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

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

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

-
包含未添加的域名 - -

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

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

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

-
默认检测端口 - -

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

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

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

-
默认连续下线次数 -
- - -
-

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

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

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

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

TCP设置

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用 - -
单节点TCP最大连接数 - -

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

-
单IP TCP最大连接数 - -

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

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

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

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

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

-
TCP端口列表 - -

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

-
IP白名单 - -

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

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

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

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

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

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

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

-
-
-
-
` -}) - -Vue.component("ns-create-records-table", { - props: ["v-types"], - data: function () { - let types = this.vTypes - if (types == null) { - types = [] - } - return { - types: types, - records: [ - { - name: "", - type: "A", - value: "", - routeCodes: [], - ttl: 600, - index: 0 - } - ], - lastIndex: 0, - isAddingRoutes: false // 是否正在添加线路 - } - }, - methods: { - add: function () { - this.records.push({ - name: "", - type: "A", - value: "", - routeCodes: [], - ttl: 600, - index: ++this.lastIndex - }) - let that = this - setTimeout(function () { - that.$refs.nameInputs.$last().focus() - }, 100) - }, - remove: function (index) { - this.records.$remove(index) - }, - addRoutes: function () { - this.isAddingRoutes = true - }, - cancelRoutes: function () { - let that = this - setTimeout(function () { - that.isAddingRoutes = false - }, 1000) - }, - changeRoutes: function (record, routes) { - if (routes == null) { - record.routeCodes = [] - } else { - record.routeCodes = routes.map(function (route) { - return route.code - }) - } - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - -
记录名记录类型线路记录值TTL操作
- - - - - - - - -
- - -
-
- -
- -
`, -}) - // 选择单一线路 Vue.component("ns-route-selector", { props: ["v-route-code"], @@ -2742,6 +8373,172 @@ Vue.component("ns-route-selector", { ` }) +// 选择多个线路 +Vue.component("ns-routes-selector", { + props: ["v-routes", "name"], + mounted: function () { + let that = this + Tea.action("/ns/routes/options") + .post() + .success(function (resp) { + that.routes = resp.data.routes + + // provinces + let provinces = {} + if (resp.data.provinces != null && resp.data.provinces.length > 0) { + for (const province of resp.data.provinces) { + let countryCode = province.countryCode + if (typeof provinces[countryCode] == "undefined") { + provinces[countryCode] = [] + } + provinces[countryCode].push({ + name: province.name, + code: province.code + }) + } + } + that.provinces = provinces + }) + }, + data: function () { + let selectedRoutes = this.vRoutes + if (selectedRoutes == null) { + selectedRoutes = [] + } + + let inputName = this.name + if (typeof inputName != "string" || inputName.length == 0) { + inputName = "routeCodes" + } + + return { + routeCode: "default", + inputName: inputName, + routes: [], + + provinces: {}, // country code => [ province1, province2, ... ] + provinceRouteCode: "", + + isAdding: false, + routeType: "default", + selectedRoutes: selectedRoutes, + } + }, + watch: { + routeType: function (v) { + this.routeCode = "" + let that = this + this.routes.forEach(function (route) { + if (route.type == v && that.routeCode.length == 0) { + that.routeCode = route.code + } + }) + } + }, + methods: { + add: function () { + this.isAdding = true + this.routeType = "default" + this.routeCode = "default" + this.provinceRouteCode = "" + this.$emit("add") + }, + cancel: function () { + this.isAdding = false + this.$emit("cancel") + }, + confirm: function () { + if (this.routeCode.length == 0) { + return + } + + let that = this + + // route + let selectedRoute = null + for (const route of this.routes) { + if (route.code == this.routeCode) { + selectedRoute = route + break + } + } + + if (selectedRoute != null) { + // province route + if (this.provinceRouteCode.length > 0 && this.provinces[this.routeCode] != null) { + for (const province of this.provinces[this.routeCode]) { + if (province.code == this.provinceRouteCode) { + selectedRoute = { + name: selectedRoute.name + "-" + province.name, + code: province.code + } + break + } + } + } + + that.selectedRoutes.push(selectedRoute) + } + + this.$emit("change", this.selectedRoutes) + this.cancel() + }, + remove: function (index) { + this.selectedRoutes.$remove(index) + this.$emit("change", this.selectedRoutes) + } + } + , + template: `
+
+
+ + {{route.name}}   +
+
+
+
+ + + + + + + + + + + + + +
选择类型 * + +
选择线路 * + +
选择省/州 + +
+
+ +   取消 +
+
+ +
` +}) + Vue.component("ns-user-selector", { props: ["v-user-id"], data: function () { @@ -2757,150 +8554,237 @@ Vue.component("ns-user-selector", { ` }) -Vue.component("ns-access-log-box", { - props: ["v-access-log", "v-keyword"], +Vue.component("plan-bandwidth-limit-view", { + props: ["value"], + template: `
+ 带宽限制: +
` +}) + +Vue.component("plan-bandwidth-ranges", { + props: ["value"], 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 - } - } - - // 没有找到记录的不需要高亮显示,防止管理员看到红色就心理恐慌 + let ranges = this.value + if (ranges == null) { + ranges = [] } - return { - accessLog: accessLog, - isFailure: isFailure + ranges: ranges, + isAdding: false, + + minMB: "", + minMBUnit: "mb", + + maxMB: "", + maxMBUnit: "mb", + + pricePerMB: "", + totalPrice: "", + addingRange: { + minMB: 0, + maxMB: 0, + pricePerMB: 0, + totalPrice: 0 + } } }, methods: { - showLog: function () { + add: function () { + this.isAdding = !this.isAdding 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() - } + setTimeout(function () { + that.$refs.minMB.focus() }) }, - select: function () { - this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)" + cancelAdding: function () { + this.isAdding = false }, - 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) + confirm: function () { + if (this.addingRange.minMB < 0) { + teaweb.warn("带宽下限需要大于0") + return } + if (this.addingRange.maxMB < 0) { + teaweb.warn("带宽上限需要大于0") + return + } + if (this.addingRange.pricePerMB <= 0) { + teaweb.warn("请设置单位价格或者总价格") + return + } + + this.isAdding = false + this.minMB = "" + this.maxMB = "" + this.pricePerMB = "" + this.totalPrice = "" + this.ranges.push(this.addingRange) + this.ranges.$sort(function (v1, v2) { + if (v1.minMB < v2.minMB) { + return -1 + } + if (v1.minMB == v2.minMB) { + if (v2.maxMB == 0 || v1.maxMB < v2.maxMB) { + return -1 + } + return 0 + } + return 1 + }) + this.change() + this.addingRange = { + minMB: 0, + maxMB: 0, + pricePerMB: 0, + totalPrice: 0 + } + }, + remove: function (index) { + this.ranges.$remove(index) + this.change() + }, + change: function () { + this.$emit("change", this.ranges) + }, + formatMB: function (mb) { + return teaweb.formatBits(mb * 1024 * 1024) + }, + changeMinMB: function (v) { + let minMB = parseFloat(v.toString()) + if (isNaN(minMB) || minMB < 0) { + minMB = 0 + } + switch (this.minMBUnit) { + case "gb": + minMB *= 1024 + break + case "tb": + minMB *= 1024 * 1024 + break + } + this.addingRange.minMB = minMB + }, + changeMaxMB: function (v) { + let maxMB = parseFloat(v.toString()) + if (isNaN(maxMB) || maxMB < 0) { + maxMB = 0 + } + switch (this.maxMBUnit) { + case "gb": + maxMB *= 1024 + break + case "tb": + maxMB *= 1024 * 1024 + break + } + this.addingRange.maxMB = maxMB } }, - template: `
- -
` -}) - -Vue.component("plan-user-selector", { - props: ["v-user-id"], - data: function () { - return {} - }, - methods: { - change: function (userId) { - this.$emit("change", userId) + watch: { + minMB: function (v) { + this.changeMinMB(v) + }, + minMBUnit: function () { + this.changeMinMB(this.minMB) + }, + maxMB: function (v) { + this.changeMaxMB(v) + }, + maxMBUnit: function () { + this.changeMaxMB(this.maxMB) + }, + pricePerMB: function (v) { + let pricePerMB = parseFloat(v.toString()) + if (isNaN(pricePerMB) || pricePerMB < 0) { + pricePerMB = 0 + } + this.addingRange.pricePerMB = pricePerMB + }, + totalPrice: function (v) { + let totalPrice = parseFloat(v.toString()) + if (isNaN(totalPrice) || totalPrice < 0) { + totalPrice = 0 + } + this.addingRange.totalPrice = totalPrice } }, template: `
- + +
+
+ {{formatMB(range.minMB)}} - {{formatMB(range.maxMB)}}   价格:{{range.totalPrice}}元{{range.pricePerMB}}元/Mbps +   +
+
+
+ + +
+ + + + + + + + + + + + + + + + + +
带宽下限 * +
+
+ +
+
+ +
+
+
带宽上限 * +
+
+ +
+
+ +
+
+

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

+
单位价格 +
+ + 元/Mbps +
+

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

+
总价格 +
+ + +
+

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

+
+   + +
+ + +
+ +
` }) @@ -2946,38 +8830,112 @@ Vue.component("plan-limit-view", { ` }) -Vue.component("plan-price-view", { - props: ["v-plan"], +Vue.component("plan-price-bandwidth-config-box", { + props: ["v-plan-price-bandwidth-config"], data: function () { + let config = this.vPlanPriceBandwidthConfig + if (config == null) { + config = { + percentile: 95, + base: 0, + ranges: [], + supportRegions: false + } + } + + if (config.ranges == null) { + config.ranges = [] + } + return { - plan: this.vPlan + config: config, + bandwidthPercentile: config.percentile, + priceBase: config.base, + isEditing: false + } + }, + watch: { + priceBase: function (v) { + let f = parseFloat(v) + if (isNaN(f) || f < 0) { + this.config.base = 0 + } else { + this.config.base = f + } + }, + bandwidthPercentile: function (v) { + let i = parseInt(v) + if (isNaN(i) || i < 0) { + this.config.percentile = 0 + } else { + this.config.percentile = i + } + } + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing } }, template: `
- - 按时间周期计费 -
- - 月度:¥{{plan.monthlyPrice}}元
- 季度:¥{{plan.seasonallyPrice}}元
- 年度:¥{{plan.yearlyPrice}}元 -
-
-
- - 按流量计费 -
- 基础价格:¥{{plan.trafficPrice.base}}元/GiB -
-
-
- 按{{plan.bandwidthPrice.percentile}}th带宽计费 -
-
- {{range.minMB}} - {{range.maxMB}}MiB{{range.totalPrice}}元{{range.pricePerMB}}元/MiB -
-
-
+ +
+ 带宽百分位:{{config.percentile}}th没有设置   |   + 基础带宽价格:{{config.base}}元/Mbps没有设置   |   + 阶梯价格:{{config.ranges.length}}段没有设置   |  支持区域带宽计费 +  |  使用平均带宽算法 +
+ 修改 +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
带宽百分位 * +
+ + th +
+

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

+
基础带宽费用 +
+ + 元/Mbps +
+

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

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

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

+
带宽算法 + +

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

+

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

+
+
` }) @@ -3281,346 +9239,38 @@ Vue.component("plan-price-traffic-config-box", { ` }) -Vue.component("plan-bandwidth-limit-view", { - props: ["value"], - template: `
- 带宽限制: -
` -}) - -Vue.component("plan-bandwidth-ranges", { - props: ["value"], +Vue.component("plan-price-view", { + props: ["v-plan"], data: function () { - let ranges = this.value - if (ranges == null) { - ranges = [] - } return { - ranges: ranges, - isAdding: false, - - minMB: "", - minMBUnit: "mb", - - maxMB: "", - maxMBUnit: "mb", - - pricePerMB: "", - totalPrice: "", - addingRange: { - minMB: 0, - maxMB: 0, - pricePerMB: 0, - totalPrice: 0 - } - } - }, - methods: { - add: function () { - this.isAdding = !this.isAdding - let that = this - setTimeout(function () { - that.$refs.minMB.focus() - }) - }, - cancelAdding: function () { - this.isAdding = false - }, - confirm: function () { - if (this.addingRange.minMB < 0) { - teaweb.warn("带宽下限需要大于0") - return - } - if (this.addingRange.maxMB < 0) { - teaweb.warn("带宽上限需要大于0") - return - } - if (this.addingRange.pricePerMB <= 0) { - teaweb.warn("请设置单位价格或者总价格") - return - } - - this.isAdding = false - this.minMB = "" - this.maxMB = "" - this.pricePerMB = "" - this.totalPrice = "" - this.ranges.push(this.addingRange) - this.ranges.$sort(function (v1, v2) { - if (v1.minMB < v2.minMB) { - return -1 - } - if (v1.minMB == v2.minMB) { - if (v2.maxMB == 0 || v1.maxMB < v2.maxMB) { - return -1 - } - return 0 - } - return 1 - }) - this.change() - this.addingRange = { - minMB: 0, - maxMB: 0, - pricePerMB: 0, - totalPrice: 0 - } - }, - remove: function (index) { - this.ranges.$remove(index) - this.change() - }, - change: function () { - this.$emit("change", this.ranges) - }, - formatMB: function (mb) { - return teaweb.formatBits(mb * 1024 * 1024) - }, - changeMinMB: function (v) { - let minMB = parseFloat(v.toString()) - if (isNaN(minMB) || minMB < 0) { - minMB = 0 - } - switch (this.minMBUnit) { - case "gb": - minMB *= 1024 - break - case "tb": - minMB *= 1024 * 1024 - break - } - this.addingRange.minMB = minMB - }, - changeMaxMB: function (v) { - let maxMB = parseFloat(v.toString()) - if (isNaN(maxMB) || maxMB < 0) { - maxMB = 0 - } - switch (this.maxMBUnit) { - case "gb": - maxMB *= 1024 - break - case "tb": - maxMB *= 1024 * 1024 - break - } - this.addingRange.maxMB = maxMB - } - }, - watch: { - minMB: function (v) { - this.changeMinMB(v) - }, - minMBUnit: function () { - this.changeMinMB(this.minMB) - }, - maxMB: function (v) { - this.changeMaxMB(v) - }, - maxMBUnit: function () { - this.changeMaxMB(this.maxMB) - }, - pricePerMB: function (v) { - let pricePerMB = parseFloat(v.toString()) - if (isNaN(pricePerMB) || pricePerMB < 0) { - pricePerMB = 0 - } - this.addingRange.pricePerMB = pricePerMB - }, - totalPrice: function (v) { - let totalPrice = parseFloat(v.toString()) - if (isNaN(totalPrice) || totalPrice < 0) { - totalPrice = 0 - } - this.addingRange.totalPrice = totalPrice + plan: this.vPlan } }, template: `
- -
-
- {{formatMB(range.minMB)}} - {{formatMB(range.maxMB)}}   价格:{{range.totalPrice}}元{{range.pricePerMB}}元/Mbps -   + + 按时间周期计费 +
+ + 月度:¥{{plan.monthlyPrice}}元
+ 季度:¥{{plan.seasonallyPrice}}元
+ 年度:¥{{plan.yearlyPrice}}元 +
+
+
+ + 按流量计费 +
+ 基础价格:¥{{plan.trafficPrice.base}}元/GiB +
+
+
+ 按{{plan.bandwidthPrice.percentile}}th带宽计费 +
+
+ {{range.minMB}} - {{range.maxMB}}MiB{{range.totalPrice}}元{{range.pricePerMB}}元/MiB +
-
- - -
- - - - - - - - - - - - - - - - - -
带宽下限 * -
-
- -
-
- -
-
-
带宽上限 * -
-
- -
-
- -
-
-

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

-
单位价格 -
- - 元/Mbps -
-

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

-
总价格 -
- - -
-

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

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

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

-
基础带宽费用 -
- - 元/Mbps -
-

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

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

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

-
带宽算法 - -

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

-

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

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

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

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

SSL/TLS相关配置

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

点击可选套件添加。

-
-
开启HSTS -
- - -
-

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

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

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

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

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

-
OCSP Stapling -

选中表示启用OCSP Stapling。

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

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

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

暂时还没有缓存条件。

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

暂时还没有URL跳转规则。

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

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

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

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

+
SMTP端口 * + +

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

+
用户名 * + +

通常为发件人邮箱地址。

+
密码 * + +

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

+
发件人Email * + +

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

+
发件人名称 + +

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

+
发送测试[点此测试]
+
+
` +}) + +Vue.component("sms-sender", { + props: ["value", "name"], + mounted: function () { + this.initType(this.config.type) + }, + data: function () { + let config = this.value + if (config == null) { + config = { + isOn: false, + type: "webHook", + webHookParams: { + url: "", + method: "POST" + }, + aliyunSMSParams: { + sign: "", + templateCode: "", + codeVarName: "code", + accessKeyId: "", + accessKeySecret: "" + }, + tencentSMSParams: { + sdkAppId: "", + sign: "", + templateId: "", + accessKeyId: "", + accessKeySecret: "" + } + } + } + + if (config.aliyunSMSParams == null) { + Vue.set(config, "aliyunSMSParams", { + sign: "", + templateCode: "", + codeVarName: "code", + accessKeyId: "", + accessKeySecret: "" + }) + } + if (config.tencentSMSParams == null) { + Vue.set(config, "tencentSMSParams", { + sdkAppId: "", + sign: "", + templateId: "", + accessKeyId: "", + accessKeySecret: "" + }) + } + + return { + config: config + } + }, + watch: { + "config.type": function (v) { + this.initType(v) + } + }, + methods: { + initType: function (v) { + // initialize params + switch (v) { + case "webHook": + if (this.config.webHookParams == null) { + this.config.webHookParams = { + url: "", + method: "POST" + } + } + break + } + }, + test: function () { + window.TESTING_SMS_CONFIG = this.config + teaweb.popup("/users/setting/smsTest", { + height: "22em" + }) + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用
发送渠道 + +

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

+

通过阿里云短信服务发送短信接口;目前仅支持发送验证码

+

通过腾讯云短信服务发送短信接口;目前仅支持发送验证码

+
HTTP接口的URL地址 * + +

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

+
HTTP接口的请求方法 + +

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

+

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

+
签名名称 * +

在阿里云短信服务 “签名管理” 中添加并通过审核后才能使用。

+
模板CODE * + +

在阿里云短信服务 “模板管理” 中添加并通过审核后才能使用。

+
模板中验证码变量名称 * + +

默认为code,不需要带\${}等符号,即表示在模板中使用\${{{ config.aliyunSMSParams.codeVarName }}}代表要发送的验证码。

+
AccessKey ID * + +

在阿里云 -- RAM访问控制 -- AccessKey中可以创建和获取。

+
AccessKey Secret * + +

和表单中的AccessKey ID对应,在阿里云 -- RAM访问控制 -- AccessKey中可以创建和获取。

+
SDK应用ID * + +

在腾讯云 -- 短信 -- 应用管理 -- 应用列表中可以查看。

+
签名内容 * + +

比如“腾讯云”,在腾讯云 -- 短信 -- 签名管理中可以查看。

+
正文模板ID * + +

在腾讯云 -- 短信 -- 正文模板管理中可以查看。

+
密钥SecretId * + +

同SecretKey一同在腾讯云 -- 访问管理 -- API密钥管理中获取。

+
密钥SecretKey * + +

同SecretId一同在腾讯云 -- 访问管理 -- API密钥管理中获取。

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

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

+

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

+
+
+
+ +
+
` +}) + +Vue.component("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("firewall-syn-flood-config-box", { + props: ["v-syn-flood-config"], + data: function () { + let config = this.vSynFloodConfig + if (config == null) { + config = { + isOn: false, + minAttempts: 10, + timeoutSeconds: 600, + ignoreLocal: true + } + } + return { + config: config, + isEditing: false, + minAttempts: config.minAttempts, + timeoutSeconds: config.timeoutSeconds + } + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + } + }, + watch: { + minAttempts: function (v) { + let count = parseInt(v) + if (isNaN(count)) { + count = 10 + } + if (count < 5) { + count = 5 + } + this.config.minAttempts = count + }, + timeoutSeconds: function (v) { + let seconds = parseInt(v) + if (isNaN(seconds)) { + seconds = 10 + } + if (seconds < 60) { + seconds = 60 + } + this.config.timeoutSeconds = seconds + } + }, + template: `
+ + + + 已启用 / 空连接次数:{{config.minAttempts}}次/分钟 / 封禁时长:{{config.timeoutSeconds}}秒 / 忽略局域网访问 + + 未启用 + + + + + + + + + + + + + + + + + + + + +
启用 + +

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

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

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

+
封禁时长 +
+ + +
+
忽略局域网访问 + +
+
` +}) + +Vue.component("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("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-access-log-config-box", { + props: ["v-access-log-config", "v-fields", "v-default-field-codes", "v-is-location", "v-is-group"], + data: function () { + let that = this + + // 初始化 + setTimeout(function () { + that.changeFields() + }, 100) + + let accessLog = { + isPrior: false, + isOn: false, + fields: [1, 2, 6, 7], + status1: true, + status2: true, + status3: true, + status4: true, + status5: true, + + firewallOnly: false, + enableClientClosed: false + } + if (this.vAccessLogConfig != null) { + accessLog = this.vAccessLogConfig + } + + this.vFields.forEach(function (v) { + if (that.vAccessLogConfig == null) { // 初始化默认值 + v.isChecked = that.vDefaultFieldCodes.$contains(v.code) + } else { + v.isChecked = accessLog.fields.$contains(v.code) + } + }) + + return { + accessLog: accessLog, + hasRequestBodyField: this.vFields.$contains(8), + showAdvancedOptions: false + } + }, + methods: { + changeFields: function () { + this.accessLog.fields = this.vFields.filter(function (v) { + return v.isChecked + }).map(function (v) { + return v.code + }) + this.hasRequestBodyField = this.accessLog.fields.$contains(8) + }, + changeAdvanced: function (v) { + this.showAdvancedOptions = v + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用访问日志 +
+ + +
+
基础信息

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

高级信息 +
+ + +
+

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

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

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

+
+ +
+

WAF相关

+ + + + + +
只记录WAF相关日志 + +

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

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

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

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

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

+
主域名 * +
+
+ +
+
+ +
+
+

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

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

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

+
添加X-Cache报头 + +

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

+
添加Age Header + +

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

+
支持源站控制有效时间 + +

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

+
允许PURGE + +

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

+
PURGE Key * + +

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

+
+ +
+

过时缓存策略

+ +
+ +
+ +
+
+ +
+

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

+
` }) +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-cache-ref-box", { props: ["v-cache-ref", "v-is-reverse"], @@ -5543,1522 +12017,81 @@ Vue.component("http-cache-ref-box", { ` }) -// 请求限制 -Vue.component("http-request-limit-config-box", { - props: ["v-request-limit-config", "v-is-group", "v-is-location"], +// 缓存条件列表 +Vue.component("http-cache-refs-box", { + props: ["v-cache-refs"], 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" - } - } + let refs = this.vCacheRefs + if (refs == null) { + refs = [] } 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 - } + refs: refs } }, methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用请求限制 - -
最大并发连接数 - -

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

-
单IP最大并发连接数 - -

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

-
单连接带宽限制 - -

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

-
单请求最大尺寸 - -

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

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

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

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

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

-
人机识别验证方式 - -

使用系统默认的设置。你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

-

{{captchaType.description}}你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

-
启用系统全局规则 - -

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

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

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

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

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

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

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

-
主域名 * -
-
- -
-
- -
-
-

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

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

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

-
添加X-Cache报头 - -

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

-
添加Age Header - -

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

-
支持源站控制有效时间 - -

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

-
允许PURGE - -

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

-
PURGE Key * - -

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

-
- -
-

过时缓存策略

- -
- -
- -
-
- -
-

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

- +

暂时还没有缓存条件。

+
+ + + + + + + + + + + +
缓存条件缓存时间
+ + + + + 忽略URI参数 + + {{cacheRef.minSize.count}}{{cacheRef.minSize.unit}} + - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} + + 0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} + {{cacheRef.methods.join(", ")}} + Expires + 状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}} + 分片缓存 + Range回源 + If-None-Match + If-Modified-Since + 支持异步 + + {{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}} + 不缓存 +
` }) -// 通用Header长度 -let defaultGeneralHeaders = ["Cache-Control", "Connection", "Date", "Pragma", "Trailer", "Transfer-Encoding", "Upgrade", "Via", "Warning"] -Vue.component("http-cond-general-header-length", { - props: ["v-checkpoint"], - data: function () { - let headers = null - let length = null - - if (window.parent.UPDATING_RULE != null) { - let options = window.parent.UPDATING_RULE.checkpointOptions - if (options.headers != null && Array.$isArray(options.headers)) { - headers = options.headers - } - if (options.length != null) { - length = options.length - } - } - - - if (headers == null) { - headers = defaultGeneralHeaders - } - - if (length == null) { - length = 128 - } - - let that = this - setTimeout(function () { - that.change() - }, 100) - - return { - headers: headers, - length: length - } - }, - watch: { - length: function (v) { - let len = parseInt(v) - if (isNaN(len)) { - len = 0 - } - if (len < 0) { - len = 0 - } - this.length = len - this.change() - } - }, - methods: { - change: function () { - this.vCheckpoint.options = [ - { - code: "headers", - value: this.headers - }, - { - code: "length", - value: this.length - } - ] - } - }, - template: `
- - - - - - - - - -
通用Header列表 - -

需要检查的Header列表。

-
Header值超出长度 -
- - 字节 -
-

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

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

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

-
检查请求来源指纹 - -

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

-
忽略常用文件 - -

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

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

允许不带来源的访问。

-
来源域名允许一致 - -

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

-
允许的来源域名 - -

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

-
禁止的来源域名 - -

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

-
同时检查Origin - -

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

-
-
` -}) - -Vue.component("http-access-log-partitions-box", { - props: ["v-partition", "v-day", "v-query"], - mounted: function () { - let that = this - Tea.action("/servers/logs/partitionData") - .params({ - day: this.vDay - }) - .success(function (resp) { - that.partitions = [] - resp.data.partitions.reverse().forEach(function (v) { - that.partitions.push({ - code: v, - isDisabled: false, - hasLogs: false - }) - }) - if (that.partitions.length > 0) { - if (that.vPartition == null || that.vPartition < 0) { - that.selectedPartition = that.partitions[0].code - } - - if (that.partitions.length > 1) { - that.checkLogs() - } - } - }) - .post() - }, - data: function () { - return { - partitions: [], - selectedPartition: this.vPartition, - checkingPartition: 0 - } - }, - methods: { - url: function (p) { - let u = window.location.toString() - u = u.replace(/\?partition=-?\d+/, "?") - u = u.replace(/\?requestId=-?\d+/, "?") - u = u.replace(/&partition=-?\d+/, "") - u = u.replace(/&requestId=-?\d+/, "") - if (u.indexOf("?") > 0) { - u += "&partition=" + p - } else { - u += "?partition=" + p - } - return u - }, - disable: function (partition) { - this.partitions.forEach(function (p) { - if (p.code == partition) { - p.isDisabled = true - } - }) - }, - checkLogs: function () { - let that = this - let index = this.checkingPartition - let params = { - partition: index - } - let query = this.vQuery - if (query == null || query.length == 0) { - return - } - query.split("&").forEach(function (v) { - let param = v.split("=") - params[param[0]] = decodeURIComponent(param[1]) - }) - Tea.action("/servers/logs/hasLogs") - .params(params) - .post() - .success(function (response) { - if (response.data.hasLogs) { - // 因为是倒序,所以这里需要使用总长度减去index - that.partitions[that.partitions.length - 1 - index].hasLogs = true - } - - index++ - if (index >= that.partitions.length) { - return - } - that.checkingPartition = index - that.checkLogs() - }) - } - }, - template: `
-
- -
-
` -}) - Vue.component("http-cache-refs-config-box", { props: ["v-cache-refs", "v-cache-config", "v-cache-policy-id", "v-web-id", "v-max-bytes"], mounted: function () { @@ -7337,161 +12370,1554 @@ Vue.component("http-cache-refs-config-box", {
` }) -Vue.component("origin-list-box", { - props: ["v-primary-origins", "v-backup-origins", "v-server-type", "v-params"], +Vue.component("http-cache-stale-config", { + props: ["v-cache-stale-config"], data: function () { - return { - primaryOrigins: this.vPrimaryOrigins, - backupOrigins: this.vBackupOrigins - } - }, - methods: { - createPrimaryOrigin: function () { - teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, { - width: "45em", - height: "27em", - callback: function (resp) { - teaweb.success("保存成功", function () { - window.location.reload() - }) + let config = this.vCacheStaleConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + status: [], + supportStaleIfErrorHeader: true, + life: { + count: 1, + unit: "day" } - }) - }, - createBackupOrigin: function () { - teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, { - width: "45em", - height: "27em", - callback: function (resp) { - teaweb.success("保存成功", function () { - window.location.reload() - }) - } - }) - }, - updateOrigin: function (originId, originType) { - teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, { - width: "45em", - height: "27em", - callback: function (resp) { - teaweb.success("保存成功", function () { - window.location.reload() - }) - } - }) - }, - deleteOrigin: function (originId, originAddr, originType) { - let that = this - teaweb.confirm("确定要删除此源站(" + originAddr + ")吗?", function () { - Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType) - .post() - .success(function () { - teaweb.success("删除成功", function () { - window.location.reload() - }) - }) - }) - }, - updateOriginIsOn: function (originId, originAddr, isOn) { - let message - let resultMessage - if (isOn) { - message = "确定要启用此源站(" + originAddr + ")吗?" - resultMessage = "启用成功" - } else { - message = "确定要停用此源站(" + originAddr + ")吗?" - resultMessage = "停用成功" } - let that = this - teaweb.confirm(message, function () { - Tea.action("/servers/server/settings/origins/updateIsOn?" + that.vParams + "&originId=" + originId + "&isOn=" + (isOn ? 1 : 0)) - .post() - .success(function () { - teaweb.success(resultMessage, function () { - window.location.reload() - }) - }) - }) } - }, - template: `
-

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

-

暂时还没有主要源站。

- - -

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

-

暂时还没有备用源站。

- -
` -}) - -Vue.component("origin-list-table", { - props: ["v-origins", "v-origin-type"], - data: function () { - let hasMatchedDomains = false - let origins = this.vOrigins - if (origins != null && origins.length > 0) { - origins.forEach(function (origin) { - if (origin.domains != null && origin.domains.length > 0) { - hasMatchedDomains = true - } - }) - } - return { - hasMatchedDomains: hasMatchedDomains + config: config } }, - methods: { - deleteOrigin: function (originId, originAddr) { - this.$emit("deleteOrigin", originId, originAddr, this.vOriginType) - }, - updateOrigin: function (originId) { - this.$emit("updateOrigin", originId, this.vOriginType) - }, - updateOriginIsOn: function (originId, originAddr, isOn) { - this.$emit("updateOriginIsOn", originId, originAddr, isOn) + watch: { + config: { + deep: true, + handler: function () { + this.$emit("change", this.config) + } } }, - template: ` - - - - - - - - - + methods: {}, + template: `
源站地址权重状态操作
- - - + + + + + + + + + + + + +
- {{origin.addr}}   -
- 对象存储 - {{origin.name}} - 证书 - 主机名: {{origin.host}} - 端口跟随 - HTTP/2 - - 匹配: {{domain}} - 匹配: 所有域名 -
-
{{origin.weight}}
启用过时缓存 - + +

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

有效期 - 修改   - 停用启用   - 删除 + +

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

+
状态码 +

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

+
支持stale-if-error + +

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

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

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

+
例外URL + +

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

+
限制URL + +

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

+
忽略常用文件 + +

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

+
检查请求来源指纹 + +

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

+
启用GET302校验 + +

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

+
单IP最低QPS +
+ + 请求数/秒 +
+

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

+
使用自定义拦截阈值 + +
自定义拦截阈值设置 +
+
+ 单IP每5秒最多 + + 请求 +
+
+ +
+
+ 单IP每60秒 + + 请求 +
+
+
+
+ 单IP每300秒 + + 请求 +
+
+
+
+
` +}) + +Vue.component("http-charsets-box", { + props: ["v-usual-charsets", "v-all-charsets", "v-charset-config", "v-is-location", "v-is-group"], + data: function () { + let charsetConfig = this.vCharsetConfig + if (charsetConfig == null) { + charsetConfig = { + isPrior: false, + isOn: false, + charset: "", + isUpper: false, + force: false + } + } + return { + charsetConfig: charsetConfig, + advancedVisible: false + } + }, + methods: { + changeAdvancedVisible: function (v) { + this.advancedVisible = v + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
启用字符编码 +
+ + +
+
选择字符编码 +
强制替换 + +

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

+
字符编码大写 +
+ + +
+

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

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

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

+
例外扩展名 + +

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

+
支持的MimeType + +

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

+
压缩算法 +
+ + + +
+
+
+
+
+ + +
+
+
+ +

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

+
支持已压缩内容 + +

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

+
内容最小长度 + +

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

+
内容最大长度 + +

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

+
支持Partial
Content
+ +

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

+
例外URL + +

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

+
限制URL + +

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

+
匹配条件 + +
+
+
` +}) + +// URL扩展名条件 +Vue.component("http-cond-url-extension", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPathLowerExtension}", + operator: "in", + value: "[]" + } + if (this.vCond != null && this.vCond.param == cond.param) { + cond.value = this.vCond.value + } + + let extensions = [] + try { + extensions = JSON.parse(cond.value) + } catch (e) { + + } + + return { + cond: cond, + extensions: extensions, // TODO 可以拖动排序 + + isAdding: false, + addingExt: "" + } + }, + watch: { + extensions: function () { + this.cond.value = JSON.stringify(this.extensions) + } + }, + methods: { + addExt: function () { + this.isAdding = !this.isAdding + + if (this.isAdding) { + let that = this + setTimeout(function () { + that.$refs.addingExt.focus() + }, 100) + } + }, + cancelAdding: function () { + this.isAdding = false + this.addingExt = "" + }, + confirmAdding: function () { + // TODO 做更详细的校验 + // TODO 如果有重复的则提示之 + + if (this.addingExt.length == 0) { + return + } + + let that = this + this.addingExt.split(/[,;,;|]/).forEach(function (ext) { + ext = ext.trim() + if (ext.length > 0) { + if (ext[0] != ".") { + ext = "." + ext + } + ext = ext.replace(/\s+/g, "").toLowerCase() + that.extensions.push(ext) + } + }) + + // 清除状态 + this.cancelAdding() + }, + removeExt: function (index) { + this.extensions.$remove(index) + } + }, + template: `
+ +
+
{{ext}}
+
+
+
+
+ +
+
+ + +
+
+
+ +
+

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

+
` +}) + +// 排除URL扩展名条件 +Vue.component("http-cond-url-not-extension", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPathLowerExtension}", + operator: "not in", + value: "[]" + } + if (this.vCond != null && this.vCond.param == cond.param) { + cond.value = this.vCond.value + } + + let extensions = [] + try { + extensions = JSON.parse(cond.value) + } catch (e) { + + } + + return { + cond: cond, + extensions: extensions, // TODO 可以拖动排序 + + isAdding: false, + addingExt: "" + } + }, + watch: { + extensions: function () { + this.cond.value = JSON.stringify(this.extensions) + } + }, + methods: { + addExt: function () { + this.isAdding = !this.isAdding + + if (this.isAdding) { + let that = this + setTimeout(function () { + that.$refs.addingExt.focus() + }, 100) + } + }, + cancelAdding: function () { + this.isAdding = false + this.addingExt = "" + }, + confirmAdding: function () { + // TODO 做更详细的校验 + // TODO 如果有重复的则提示之 + + if (this.addingExt.length == 0) { + return + } + if (this.addingExt[0] != ".") { + this.addingExt = "." + this.addingExt + } + this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase() + this.extensions.push(this.addingExt) + + // 清除状态 + this.cancelAdding() + }, + removeExt: function (index) { + this.extensions.$remove(index) + } + }, + template: `
+ +
+
{{ext}}
+
+
+
+
+ +
+
+ + +
+
+
+ +
+

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

+
` +}) + +// 根据URL前缀 +Vue.component("http-cond-url-prefix", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "prefix", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof (this.vCond.value) == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +Vue.component("http-cond-url-not-prefix", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "prefix", + value: "", + isReverse: true, + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// 首页 +Vue.component("http-cond-url-eq-index", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "eq", + value: "/", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// 全站 +Vue.component("http-cond-url-all", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "prefix", + value: "/", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

支持全站所有URL。

+
` +}) + +// URL精准匹配 +Vue.component("http-cond-url-eq", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "eq", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +Vue.component("http-cond-url-not-eq", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "eq", + value: "", + isReverse: true, + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// URL正则匹配 +Vue.component("http-cond-url-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// 排除URL正则匹配 +Vue.component("http-cond-url-not-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "not regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// URL通配符 +Vue.component("http-cond-url-wildcard-match", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "wildcard match", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// User-Agent正则匹配 +Vue.component("http-cond-user-agent-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${userAgent}", + operator: "regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// User-Agent正则不匹配 +Vue.component("http-cond-user-agent-not-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${userAgent}", + operator: "not regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

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

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

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

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

+ + + + 操作符 + +
+ +

+
+ + + + 对比值 + + +
+ +

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

+
+ + +
+ +

要对比的数字。

+
+ + +
+ +

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

+
+
+ +

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

+
+
+
+
除:
+
+ +
+
余:
+
+ +
+
+
+ + +
+ +

和参数值一致的字符串。

+

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

+

参数值的前缀。

+

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

+

参数值包含此字符串。

+

参数值不包含此字符串。

+
+
+ +

添加参数值列表。

+

添加参数值列表。

+

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

+

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

+
+
+
+
+
-
+
+
+
+ + +
+ +

要对比的IP。

+
+
+ +

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

+
+
+ +

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

+
+ + + + 不区分大小写 + +
+ + +
+

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

+ + + +` +}) + Vue.component("http-cors-header-config-box", { props: ["value"], data: function () { @@ -7614,68 +14040,19 @@ Vue.component("http-cors-header-config-box", { ` }) -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 - }) - }, +// 页面动态加密配置 +Vue.component("http-encryption-config-box", { + props: ["v-encryption-config", "v-is-location", "v-is-group"], data: function () { - let firewallPolicy = this.vHttpFirewallPolicy - return { - count: 0, - firewallPolicy: firewallPolicy - } - }, - methods: { - remove: function () { - this.firewallPolicy = null - }, - select: function () { - let that = this - teaweb.popup("/servers/components/waf/selectPopup", { - height: "26em", - callback: function (resp) { - that.firewallPolicy = resp.data.firewallPolicy - } - }) - }, - create: function () { - let that = this - teaweb.popup("/servers/components/waf/createPopup", { - height: "26em", - callback: function (resp) { - that.firewallPolicy = resp.data.firewallPolicy - } - }) - } - }, - template: `
-
- - {{firewallPolicy.name}}     -
-
- [选择已有策略]     [创建新策略] -
-
` -}) - -// 压缩配置 -Vue.component("http-optimization-config-box", { - props: ["v-optimization-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vOptimizationConfig + let config = this.vEncryptionConfig return { config: config, htmlMoreOptions: false, javascriptMoreOptions: false, - cssMoreOptions: false + keyPolicyMoreOptions: false, + cacheMoreOptions: false, + encryptionMoreOptions: false } }, methods: { @@ -7684,7 +14061,7 @@ Vue.component("http-optimization-config-box", { } }, template: `
- +
@@ -7694,1329 +14071,350 @@ Vue.component("http-optimization-config-box", { - + - - + + - + - + - - - - - -
HTML优化启用页面动态加密
- +
-

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

+

启用后,将对 HTML 页面中的 JavaScript 进行动态加密,有效抵御批量爬虫和脚本工具。

HTML例外URL排除 URL - -

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

+ +

这些 URL 将跳过加密处理,支持正则表达式。

HTML限制URL - -

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

-
- - - - - - - - - - - - - - - - - - - - - -
Javascript优化 -
- - -
-

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

-
Javascript例外URL - -

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

-
Javascript限制URL - -

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

-
- - - - - - - - - - - - - - - - - - - -
CSS优化 -
- - -
-

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

-
CSS例外URL - -

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

-
CSS限制URL - -

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

-
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
HTML 加密 +
+ + +
+

加密 HTML 页面中的 JavaScript 脚本。

+
加密内联脚本 +
+ + +
+

加密 HTML 中的内联 <script> 标签内容。

+
加密外部脚本 +
+ + +
+

加密通过 src 属性引入的外部 JavaScript 文件。

+
URL 匹配规则 + +

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

+
+ +
+ + + + + + + + + + + + + + + + +
JavaScript 文件加密 +
+ + +
+

加密独立的 JavaScript 文件(.js 文件)。

+
URL 匹配规则 + +

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

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
服务器端密钥 + +

用于生成加密密钥的密码,建议使用复杂的随机字符串。默认密钥仅用于测试,生产环境请务必修改!

+
时间分片(秒) + +

加密密钥每隔多少秒更换一次。时间越短越安全,但可能影响性能。建议 60-120 秒,默认 60 秒。

+
IP CIDR 前缀长度 + +

将用户 IP 地址的前多少位作为识别依据。例如设置为 24 时,192.168.1.1 和 192.168.1.2 会被视为同一用户。默认 24。

+
简化 User-Agent +
+ + +
+

开启后,只识别浏览器类型(如 Chrome、Firefox),忽略版本号等细节,避免因浏览器自动更新导致解密失败。

+
+ +
+ + + + + + + + + + + + + + + + + + + + +
启用缓存 +
+ + +
+

开启后,相同内容的加密结果会被缓存,减少重复计算,提升响应速度。

+
缓存 TTL(秒) + +

缓存的有效期,超过这个时间后缓存会自动失效。建议与上面的"时间分片"保持一致。默认 60 秒。

+
最大缓存条目数 + +

最多缓存多少个加密结果。数量越大占用内存越多,建议根据服务器内存情况调整。默认 1000。

+
+
` }) -Vue.component("http-websocket-box", { - props: ["v-websocket-ref", "v-websocket-config", "v-is-location", "v-is-group"], + + +Vue.component("http-expires-time-config-box", { + props: ["v-expires-time"], data: function () { - let websocketRef = this.vWebsocketRef - if (websocketRef == null) { - websocketRef = { + let expiresTime = this.vExpiresTime + if (expiresTime == null) { + expiresTime = { isPrior: false, isOn: false, - websocketId: 0 + 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字段。

+
自动计算时间 +

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

+
强制缓存时间 + +

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

+
+
` +}) - let websocketConfig = this.vWebsocketConfig - if (websocketConfig == null) { - websocketConfig = { - id: 0, +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, - handshakeTimeout: { - count: 30, - unit: "second" - }, - allowAllOrigins: true, - allowedOrigins: [], - requestSameOrigin: true, - requestOrigin: "" + fastcgiIds: [] } + } + let fastcgiConfigs = this.vFastcgiConfigs + if (fastcgiConfigs == null) { + fastcgiConfigs = [] } else { - if (websocketConfig.handshakeTimeout == null) { - websocketConfig.handshakeTimeout = { - count: 30, - unit: "second", - } - } - if (websocketConfig.allowedOrigins == null) { - websocketConfig.allowedOrigins = [] - } + fastcgiRef.fastcgiIds = fastcgiConfigs.map(function (v) { + return v.id + }) } return { - websocketRef: websocketRef, - websocketConfig: websocketConfig, - handshakeTimeoutCountString: websocketConfig.handshakeTimeout.count.toString(), + fastcgiRef: fastcgiRef, + fastcgiConfigs: fastcgiConfigs, 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 + return (!this.vIsLocation || this.fastcgiRef.isPrior) && this.fastcgiRef.isOn }, - changeAdvancedVisible: function (v) { - this.advancedVisible = v - }, - createOrigin: function () { + createFastcgi: function () { let that = this - teaweb.popup("/servers/server/settings/websocket/createOrigin", { - height: "12.5em", + teaweb.popup("/servers/server/settings/fastcgi/createPopup", { + height: "26em", callback: function (resp) { - that.websocketConfig.allowedOrigins.push(resp.data.origin) + teaweb.success("添加成功", function () { + that.fastcgiConfigs.push(resp.data.fastcgi) + that.fastcgiRef.fastcgiIds.push(resp.data.fastcgi.id) + }) } }) }, - removeOrigin: function (index) { - this.websocketConfig.allowedOrigins.$remove(index) + 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: `
- - + - - + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用Websocket启用配置
- +
允许所有来源域(Origin)Fastcgi服务 -
- - -
-

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

-
允许的来源域列表(Origin) -
-
- {{origin}} +
+
+ {{fastcgi.address}}    
-
+
- -

只允许在列表中的来源域名访问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) - setTimeout(that.submitForm, 100) - } - }); - }, - - removeServerName: function (index) { - this.serverNames.$remove(index) - }, - - updateServerName: function (index, serverName) { - window.UPDATING_SERVER_NAME = teaweb.clone(serverName) - let that = this - teaweb.popup("/servers/addServerNamePopup", { - callback: function (resp) { - var serverName = resp.data.serverName - Vue.set(that.serverNames, index, serverName) - setTimeout(that.submitForm, 100) - } - }); - }, - showSearchBox: function () { - this.isSearching = !this.isSearching - if (this.isSearching) { - let that = this - setTimeout(function () { - that.$refs.keywordRef.focus() - }, 200) - } else { - this.keyword = "" - } - }, - allServerNames: function () { - if (this.serverNames == null) { - return [] - } - let result = [] - this.serverNames.forEach(function (serverName) { - if (serverName.subNames != null && serverName.subNames.length > 0) { - serverName.subNames.forEach(function (subName) { - if (subName != null && subName.length > 0) { - if (!result.$contains(subName)) { - result.push(subName) - } - } - }) - } else if (serverName.name != null && serverName.name.length > 0) { - if (!result.$contains(serverName.name)) { - result.push(serverName.name) - } - } - }) - return result - }, - submitForm: function () { - Tea.runActionOn(this.$refs.serverNamesRef.form) - } - }, - watch: { - keyword: function (v) { - this.serverNames.forEach(function (serverName) { - if (v.length == 0) { - serverName.isShowing = true - return - } - if (serverName.subNames == null || serverName.subNames.length == 0) { - if (!teaweb.match(serverName.name, v)) { - serverName.isShowing = false - } - } else { - let found = false - serverName.subNames.forEach(function (subName) { - if (teaweb.match(subName, v)) { - found = true - } - }) - serverName.isShowing = found - } - }) - } - }, - template: `
- -
-
- {{serverName.type}} - {{serverName.name}} - {{serverName.subNames[0]}}等{{serverName.subNames.length}}个域名 - -
-
-
-
- -
|
-
- - -
-
- -
-
-
` -}) - -// UAM模式配置 -Vue.component("uam-config-box", { - props: ["v-uam-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vUamConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - addToWhiteList: true, - onlyURLPatterns: [], - exceptURLPatterns: [], - minQPSPerIP: 0, - keyLife: 0 - } - } - if (config.onlyURLPatterns == null) { - config.onlyURLPatterns = [] - } - if (config.exceptURLPatterns == null) { - config.exceptURLPatterns = [] - } - return { - config: config, - moreOptionsVisible: false, - minQPSPerIP: config.minQPSPerIP, - keyLife: config.keyLife - } - }, - watch: { - minQPSPerIP: function (v) { - let qps = parseInt(v.toString()) - if (isNaN(qps) || qps < 0) { - qps = 0 - } - this.config.minQPSPerIP = qps - }, - keyLife: function (v) { - let keyLife = parseInt(v) - if (isNaN(keyLife) || keyLife <= 0) { - keyLife = 0 - } - this.config.keyLife = keyLife - } - }, - methods: { - showMoreOptions: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - }, - changeConds: function (conds) { - this.config.conds = conds - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用5秒盾 - -

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

-
验证有效期 -
- - -
-

单个客户端验证通过后,在这个有效期内不再重复验证;如果为0则表示系统默认。

-
单IP最低QPS -
- - 请求数/秒 -
-

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

-
加入IP白名单 - -

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

-
例外URL - -

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

-
限制URL - -

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

-
匹配条件 - -
-
-
` -}) - -Vue.component("http-cache-stale-config", { - props: ["v-cache-stale-config"], - data: function () { - let config = this.vCacheStaleConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - status: [], - supportStaleIfErrorHeader: true, - life: { - count: 1, - unit: "day" - } - } - } - return { - config: config - } - }, - watch: { - config: { - deep: true, - handler: function () { - this.$emit("change", this.config) - } - } - }, - methods: {}, - template: ` - - - - - - - - - - - - - - - - - - -
启用过时缓存 - -

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

-
有效期 - -

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

-
状态码 -

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

-
支持stale-if-error - -

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

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

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

-

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

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

选中后表示开启防盗链。

-
允许直接访问网站 - -

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

-
来源域名允许一致 - -

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

-
允许的来源域名 - > -

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

-
禁止的来源域名 - -

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

-
同时检查Origin - -

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

-
例外URL - -

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

-
限制URL - -

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

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

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

- - - - - - - - - - - - - - - -
状态码 - -
域名或IP地址 - -

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

-
端口 - -

默认端口为443。

-
-
- - -
-
- - -
-

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

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

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

-
端口 - -

默认端口为443。

-
允许的域名 - -

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

-
排除的域名 - -

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

-
-
+
` }) @@ -10035,180 +15433,1884 @@ Vue.component("http-firewall-actions-box", { ` }) -// 认证设置 -Vue.component("http-auth-config-box", { - props: ["v-auth-config", "v-is-location"], +// Action列表 +Vue.component("http-firewall-actions-view", { + props: ["v-actions"], + template: `
+
+ {{action.name}} ({{action.code.toUpperCase()}}) +
+ [{{action.options.status}}] + + [分组] + [网站] + [网站和策略] + + + 黑名单 + 白名单 + 灰名单 + +
+
+
+
` +}) + +Vue.component("http-firewall-block-options-viewer", { + props: ["v-block-options"], data: function () { - let authConfig = this.vAuthConfig - if (authConfig == null) { - authConfig = { - isPrior: false, - isOn: false + return { + options: this.vBlockOptions + } + }, + template: `
+ 默认设置 +
+ 状态码:{{options.statusCode}} / 提示内容:[{{options.body.length}}字符][无] / 超时时间:{{options.timeout}}秒 / 最大封禁时长:{{options.timeoutMax}}秒 + / 尝试全局封禁 +
+
+` +}) + +Vue.component("http-firewall-block-options", { + props: ["v-block-options"], + data: function () { + return { + options: this.vBlockOptions, + statusCode: this.vBlockOptions.statusCode, + timeout: this.vBlockOptions.timeout, + timeoutMax: this.vBlockOptions.timeoutMax, + isEditing: false + } + }, + watch: { + statusCode: function (v) { + let statusCode = parseInt(v) + if (isNaN(statusCode)) { + this.options.statusCode = 403 + } else { + this.options.statusCode = statusCode + } + }, + timeout: function (v) { + let timeout = parseInt(v) + if (isNaN(timeout)) { + this.options.timeout = 0 + } else { + this.options.timeout = timeout + } + }, + timeoutMax: function (v) { + let timeoutMax = parseInt(v) + if (isNaN(timeoutMax)) { + this.options.timeoutMax = 0 + } else { + this.options.timeoutMax = timeoutMax } } - if (authConfig.policyRefs == null) { - authConfig.policyRefs = [] + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + } + }, + template: `
+ + 状态码:{{statusCode}} / 提示内容:[{{options.body.length}}字符][无] / 封禁时长:{{timeout}}秒 + / 最大封禁时长:{{timeoutMax}}秒 + / 尝试全局封禁 + + + + + + + + + + + + + + + + + + + + + + +
状态码 + +
提示内容 + +
封禁时长 +
+ + +
+

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

+
最大封禁时长 +
+ + +
+

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

+
失败全局封禁 + +

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

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

{{captchaDef.description}}

+
有效时间 +
+ + +
+

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

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

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

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

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

+
失败全局封禁 + +

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

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

类似于提交验证

+
显示请求ID + +

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

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

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

+
页尾提示 + +

支持HTML。

+
页面模板 + +

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

+
+ + + + + + + + + + + + + + + + +
允许用户使用极验 +

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

+
极验-验证ID * + +

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

+
极验-验证Key * + +

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

+
+
+
+` +}) + +Vue.component("http-firewall-config-box", { + props: ["v-firewall-config", "v-is-location", "v-is-group", "v-firewall-policy"], + data: function () { + let firewall = this.vFirewallConfig + if (firewall == null) { + firewall = { + isPrior: false, + isOn: false, + firewallPolicyId: 0, + ignoreGlobalRules: false, + defaultCaptchaType: "none" + } + } + + if (firewall.defaultCaptchaType == null || firewall.defaultCaptchaType.length == 0) { + firewall.defaultCaptchaType = "none" + } + + let allCaptchaTypes = window.WAF_CAPTCHA_TYPES.$copy() + + // geetest + let geeTestIsOn = false + if (this.vFirewallPolicy != null && this.vFirewallPolicy.captchaAction != null && this.vFirewallPolicy.captchaAction.geeTestConfig != null) { + geeTestIsOn = this.vFirewallPolicy.captchaAction.geeTestConfig.isOn + } + + // 如果没有启用geetest,则还原 + if (!geeTestIsOn && firewall.defaultCaptchaType == "geetest") { + firewall.defaultCaptchaType = "none" + } + + return { + firewall: firewall, + moreOptionsVisible: false, + execGlobalRules: !firewall.ignoreGlobalRules, + captchaTypes: allCaptchaTypes, + geeTestIsOn: geeTestIsOn + } + }, + watch: { + execGlobalRules: function (v) { + this.firewall.ignoreGlobalRules = !v + } + }, + methods: { + changeOptionsVisible: function (v) { + this.moreOptionsVisible = v + } + }, + template: `
+ + + + + + + +
全局WAF策略 +
{{vFirewallPolicy.name}}   [{{vFirewallPolicy.modeInfo.name}}]  +

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

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

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

+
人机识别验证方式 + +

使用系统默认的设置。你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

+

{{captchaType.description}}你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

+
启用系统全局规则 + +

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

+
+
+
` +}) + +Vue.component("http-firewall-js-cookie-options-viewer", { + props: ["v-js-cookie-options"], + mounted: function () { + this.updateSummary() + }, + data: function () { + let options = this.vJsCookieOptions + if (options == null) { + options = { + life: 0, + maxFails: 0, + failBlockTimeout: 0, + failBlockScopeAll: false, + scope: "" + } + } + return { + options: options, + summary: "" + } + }, + methods: { + updateSummary: function () { + let summaryList = [] + if (this.options.life > 0) { + summaryList.push("有效时间" + this.options.life + "秒") + } + if (this.options.maxFails > 0) { + summaryList.push("最多失败" + this.options.maxFails + "次") + } + if (this.options.failBlockTimeout > 0) { + summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") + } + if (this.options.failBlockScopeAll) { + summaryList.push("尝试全局封禁") + } + + if (summaryList.length == 0) { + this.summary = "默认配置" + } else { + this.summary = summaryList.join(" / ") + } + } + }, + template: `
{{summary}}
+` +}) + +Vue.component("http-firewall-js-cookie-options", { + props: ["v-js-cookie-options"], + mounted: function () { + this.updateSummary() + }, + data: function () { + let options = this.vJsCookieOptions + if (options == null) { + options = { + life: 0, + maxFails: 0, + failBlockTimeout: 0, + failBlockScopeAll: false, + scope: "service" + } + } + + return { + options: options, + isEditing: false, + summary: "" + } + }, + watch: { + "options.life": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.life = i + this.updateSummary() + }, + "options.maxFails": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.maxFails = i + this.updateSummary() + }, + "options.failBlockTimeout": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.failBlockTimeout = i + this.updateSummary() + }, + "options.failBlockScopeAll": function (v) { + this.updateSummary() + } + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + }, + updateSummary: function () { + let summaryList = [] + if (this.options.life > 0) { + summaryList.push("有效时间" + this.options.life + "秒") + } + if (this.options.maxFails > 0) { + summaryList.push("最多失败" + this.options.maxFails + "次") + } + if (this.options.failBlockTimeout > 0) { + summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") + } + if (this.options.failBlockScopeAll) { + summaryList.push("尝试全局封禁") + } + + if (summaryList.length == 0) { + this.summary = "默认配置" + } else { + this.summary = summaryList.join(" / ") + } + }, + confirm: function () { + this.isEditing = false + } + }, + template: `
+ + {{summary}} +
+ + + + + + + + + + + + + + + + + + + +
有效时间 +
+ + +
+

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

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

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

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

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

+
失败全局封禁 + +

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

+
+
+
+` +}) + +Vue.component("http-firewall-page-options-viewer", { + props: ["v-page-options"], + data: function () { + return { + options: this.vPageOptions + } + }, + template: `
+ 默认设置 +
+ 状态码:{{options.status}} / 提示内容:[{{options.body.length}}字符] +
+
+` +}) + +Vue.component("http-firewall-page-options", { + props: ["v-page-options"], + data: function () { + var defaultPageBody = ` + + + 403 Forbidden + + + +

403 Forbidden By WAF

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

[使用模板]

+
+
+` +}) + +Vue.component("http-firewall-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-firewall-policy-selector", { + props: ["v-http-firewall-policy"], + mounted: function () { + let that = this + Tea.action("/servers/components/waf/count") + .post() + .success(function (resp) { + that.count = resp.data.count + }) + }, + data: function () { + let firewallPolicy = this.vHttpFirewallPolicy + return { + count: 0, + firewallPolicy: firewallPolicy + } + }, + methods: { + remove: function () { + this.firewallPolicy = null + }, + select: function () { + let that = this + teaweb.popup("/servers/components/waf/selectPopup", { + height: "26em", + callback: function (resp) { + that.firewallPolicy = resp.data.firewallPolicy + } + }) + }, + create: function () { + let that = this + teaweb.popup("/servers/components/waf/createPopup", { + height: "26em", + callback: function (resp) { + that.firewallPolicy = resp.data.firewallPolicy + } + }) + } + }, + template: `
+
+ + {{firewallPolicy.name}}     +
+
+ [选择已有策略]     [创建新策略] +
+
` +}) + +Vue.component("http-firewall-province-selector", { + props: ["v-type", "v-provinces"], + data: function () { + let provinces = this.vProvinces + if (provinces == null) { + provinces = [] + } + + return { + listType: this.vType, + provinces: provinces + } + }, + methods: { + addProvince: function () { + let selectedProvinceIds = this.provinces.map(function (province) { + return province.id + }) + let that = this + teaweb.popup("/servers/server/settings/waf/ipadmin/selectProvincesPopup?type=" + this.listType + "&selectedProvinceIds=" + selectedProvinceIds.join(","), { + width: "50em", + height: "26em", + callback: function (resp) { + that.provinces = resp.data.selectedProvinces + that.$forceUpdate() + that.notifyChange() + } + }) + }, + removeProvince: function (index) { + this.provinces.$remove(index) + this.notifyChange() + }, + resetProvinces: function () { + this.provinces = [] + this.notifyChange() + }, + notifyChange: function () { + this.$emit("change", { + "provinces": this.provinces + }) + } + }, + template: `
+ 暂时没有选择允许封禁的省份。 +
+
+ + {{province.name}} +
+
+
+   +
` +}) + +Vue.component("http-firewall-region-selector", { + props: ["v-type", "v-countries"], + data: function () { + let countries = this.vCountries + if (countries == null) { + countries = [] + } + + return { + listType: this.vType, + countries: countries + } + }, + methods: { + addCountry: function () { + let selectedCountryIds = this.countries.map(function (country) { + return country.id + }) + let that = this + teaweb.popup("/servers/server/settings/waf/ipadmin/selectCountriesPopup?type=" + this.listType + "&selectedCountryIds=" + selectedCountryIds.join(","), { + width: "52em", + height: "30em", + callback: function (resp) { + that.countries = resp.data.selectedCountries + that.$forceUpdate() + that.notifyChange() + } + }) + }, + removeCountry: function (index) { + this.countries.$remove(index) + this.notifyChange() + }, + resetCountries: function () { + this.countries = [] + this.notifyChange() + }, + notifyChange: function () { + this.$emit("change", { + "countries": this.countries + }) + } + }, + template: `
+ 暂时没有选择允许封禁的区域。 +
+
+ + ({{country.letter}}){{country.name}} +
+
+
+   +
` +}) + +// 显示WAF规则的标签 +Vue.component("http-firewall-rule-label", { + props: ["v-rule"], + data: function () { + return { + rule: this.vRule + } + }, + methods: { + showErr: function (err) { + teaweb.popupTip("规则校验错误,请修正:" + teaweb.encodeHTML(err) + "") + }, + calculateParamName: function (param) { + let paramName = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + } + }) + } + return paramName + }, + calculateParamDescription: function (param) { + let paramName = "" + let paramDescription = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + paramDescription = checkpoint.description + } + }) + } + return paramName + ": " + paramDescription + }, + operatorName: function (operatorCode) { + let operatorName = operatorCode + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + } + }) + } + + return operatorName + }, + operatorDescription: function (operatorCode) { + let operatorName = operatorCode + let operatorDescription = "" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + operatorDescription = v.description + } + }) + } + + return operatorName + ": " + operatorDescription + }, + operatorDataType: function (operatorCode) { + let operatorDataType = "none" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorDataType = v.dataType + } + }) + } + + return operatorDataType + }, + isEmptyString: function (v) { + return typeof v == "string" && v.length == 0 + } + }, + template: `
+
+ {{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} + + + + {{rule.checkpointOptions.period}}秒内请求数 + + + + + 允许{{rule.checkpointOptions.allowDomains}} + 禁止{{rule.checkpointOptions.denyDomains}} + + + + | {{paramFilter.code}} + <{{operatorName(rule.operator)}}> + {{rule.value}} + [空] + + + + ({{rule.description}}) + + 规则错误 +
+
` +}) + +Vue.component("http-firewall-rules-box", { + props: ["v-rules", "v-type"], + data: function () { + let rules = this.vRules + if (rules == null) { + rules = [] + } + return { + rules: rules + } + }, + methods: { + addRule: function () { + window.UPDATING_RULE = null + let that = this + teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { + height: "30em", + callback: function (resp) { + that.rules.push(resp.data.rule) + } + }) + }, + updateRule: function (index, rule) { + window.UPDATING_RULE = teaweb.clone(rule) + let that = this + teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { + height: "30em", + callback: function (resp) { + Vue.set(that.rules, index, resp.data.rule) + } + }) + }, + removeRule: function (index) { + let that = this + teaweb.confirm("确定要删除此规则吗?", function () { + that.rules.$remove(index) + }) + }, + operatorName: function (operatorCode) { + let operatorName = operatorCode + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + } + }) + } + + return operatorName + }, + operatorDescription: function (operatorCode) { + let operatorName = operatorCode + let operatorDescription = "" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + operatorDescription = v.description + } + }) + } + + return operatorName + ": " + operatorDescription + }, + operatorDataType: function (operatorCode) { + let operatorDataType = "none" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorDataType = v.dataType + } + }) + } + + return operatorDataType + }, + calculateParamName: function (param) { + let paramName = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + } + }) + } + return paramName + }, + calculateParamDescription: function (param) { + let paramName = "" + let paramDescription = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + paramDescription = checkpoint.description + } + }) + } + return paramName + ": " + paramDescription + }, + isEmptyString: function (v) { + return typeof v == "string" && v.length == 0 + } + }, + template: `
+ +
+
+ {{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} + + + + {{rule.checkpointOptions.period}}秒内请求数 + + + + + 允许{{rule.checkpointOptions.allowDomains}} + 禁止{{rule.checkpointOptions.denyDomains}} + + + + | {{paramFilter.code}} <{{operatorName(rule.operator)}}> + {{rule.value}} + [空] + + + + ({{rule.description}}) + + + +
+
+
+ +
` +}) + +// 通用Header长度 +let defaultGeneralHeaders = ["Cache-Control", "Connection", "Date", "Pragma", "Trailer", "Transfer-Encoding", "Upgrade", "Via", "Warning"] +Vue.component("http-cond-general-header-length", { + props: ["v-checkpoint"], + data: function () { + let headers = null + let length = null + + if (window.parent.UPDATING_RULE != null) { + let options = window.parent.UPDATING_RULE.checkpointOptions + if (options.headers != null && Array.$isArray(options.headers)) { + headers = options.headers + } + if (options.length != null) { + length = options.length + } + } + + + if (headers == null) { + headers = defaultGeneralHeaders + } + + if (length == null) { + length = 128 + } + + let that = this + setTimeout(function () { + that.change() + }, 100) + + return { + headers: headers, + length: length + } + }, + watch: { + length: function (v) { + let len = parseInt(v) + if (isNaN(len)) { + len = 0 + } + if (len < 0) { + len = 0 + } + this.length = len + this.change() + } + }, + methods: { + change: function () { + this.vCheckpoint.options = [ + { + code: "headers", + value: this.headers + }, + { + code: "length", + value: this.length + } + ] + } + }, + template: `
+ + + + + + + + + +
通用Header列表 + +

需要检查的Header列表。

+
Header值超出长度 +
+ + 字节 +
+

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

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

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

+
检查请求来源指纹 + +

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

+
忽略常用文件 + +

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

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

允许不带来源的访问。

+
来源域名允许一致 + +

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

+
允许的来源域名 + +

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

+
禁止的来源域名 + +

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

+
同时检查Origin + +

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

+
+
` +}) + +Vue.component("http-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.authConfig.isPrior) && this.authConfig.isOn + return (!this.vIsLocation || this.vGzipRef.isPrior) && this.vGzipRef.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) + changeAdvancedVisible: function (v) { + this.advancedVisible = v } }, template: `
- + - - + + - + + + + + + + + + + + + + + + + + + + + + +
启用鉴权启用Gzip压缩
- +
压缩级别 + +

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

+
Gzip内容最小长度 + +

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

+
Gzip内容最大长度 + +

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

+
匹配条件 + +
-
- -
-

鉴权方式

- - - - - - - - - - - - - - - - - - - -
名称鉴权方法参数状态操作
{{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"], +Vue.component("http-header-assistant", { + props: ["v-type", "v-value"], + mounted: function () { + let that = this + Tea.action("/servers/headers/options?type=" + this.vType) + .post() + .success(function (resp) { + that.allHeaders = resp.data.headers + }) + }, data: function () { - 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 + allHeaders: [], + matchedHeaders: [], + + selectedHeaderName: "" + } + }, + watch: { + vValue: function (v) { + if (v != this.selectedHeaderName) { + this.selectedHeaderName = "" + } + + if (v.length == 0) { + this.matchedHeaders = [] + return + } + this.matchedHeaders = this.allHeaders.filter(function (header) { + return teaweb.match(header, v) + }).slice(0, 10) } }, methods: { - change: function(item) { - if (item != null) { - this.$emit("change", item.id) - } else { - this.$emit("change", 0) - } - }, - clear: function () { - this.$refs.comboBox.clear() + select: function (header) { + this.$emit("select", header) + this.selectedHeaderName = header } }, - template: `
- -
` + template: ` + {{header}} +     +` }) Vue.component("http-header-policy-box", { @@ -10549,89 +17651,719 @@ Vue.component("http-header-policy-box", { ` }) -// 通用设置 -Vue.component("http-common-config-box", { - props: ["v-common-config"], +Vue.component("http-header-replace-values", { + props: ["v-replace-values"], data: function () { - let config = this.vCommonConfig - if (config == null) { - config = { - mergeSlashes: false - } + let values = this.vReplaceValues + if (values == null) { + values = [] } return { - config: config + 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}}[空] + +
+
+
+
合并重复的路径分隔符 -
- - -
-

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

-
+ + + + + + + + + + + + +
替换前内容 *
替换后内容
是否忽略大小写 + +
+ +
+   + +
+
+
+ +
+` +}) + +Vue.component("http-hls-config-box", { + props: ["value", "v-is-location", "v-is-group"], + data: function () { + let config = this.value + if (config == null) { + config = { + isPrior: false + } + } + + let encryptingConfig = config.encrypting + if (encryptingConfig == null) { + encryptingConfig = { + isOn: false, + onlyURLPatterns: [], + exceptURLPatterns: [] + } + config.encrypting = encryptingConfig + } + + return { + config: config, + + encryptingConfig: encryptingConfig, + encryptingMoreOptionsVisible: false + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) + }, + + showEncryptingMoreOptions: function () { + this.encryptingMoreOptionsVisible = !this.encryptingMoreOptionsVisible + } + }, + template: `
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + +
启用HLS加密 + +

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

+
例外URL + +

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

+
限制URL + +

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

+
` }) -Vue.component("http-cache-policy-selector", { - props: ["v-cache-policy"], +Vue.component("http-host-redirect-box", { + props: ["v-redirects"], mounted: function () { let that = this - Tea.action("/servers/components/cache/count") - .post() - .success(function (resp) { - that.count = resp.data.count + 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 cachePolicy = this.vCachePolicy + let redirects = this.vRedirects + if (redirects == null) { + redirects = [] + } + + let id = 0 + redirects.forEach(function (v) { + id++ + v.id = id + }) + return { - count: 0, - cachePolicy: cachePolicy + 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: { - remove: function () { - this.cachePolicy = null - }, - select: function () { + add: function () { let that = this - teaweb.popup("/servers/components/cache/selectPopup", { - width: "42em", - height: "26em", + window.UPDATING_REDIRECT = null + + teaweb.popup("/servers/server/settings/redirects/createPopup", { + width: "50em", + height: "36em", callback: function (resp) { - that.cachePolicy = resp.data.cachePolicy + that.id++ + resp.data.redirect.id = that.id + that.redirects.push(resp.data.redirect) + that.change() } }) }, - create: function () { + update: function (index, redirect) { let that = this - teaweb.popup("/servers/components/cache/createPopup", { - height: "26em", + window.UPDATING_REDIRECT = redirect + + teaweb.popup("/servers/server/settings/redirects/createPopup", { + width: "50em", + height: "36em", callback: function (resp) { - that.cachePolicy = resp.data.cachePolicy + 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: `
-
- - {{cachePolicy.name}}     + + + + [创建] + +
+ +

暂时还没有URL跳转规则。

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

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

-
- [选择已有策略]     [创建新策略] +
+
` +}) + +Vue.component("http-location-labels", { + props: ["v-location-config", "v-server-id"], + data: function () { + return { + location: this.vLocationConfig + } + }, + methods: { + // 判断是否已启用某配置 + configIsOn: function (config) { + return config != null && config.isPrior && config.isOn + }, + + refIsOn: function (ref, config) { + return this.configIsOn(ref) && config != null && config.isOn + }, + + len: function (arr) { + return (arr == null) ? 0 : arr.length + }, + url: function (path) { + return "/servers/server/settings/locations" + path + "?serverId=" + this.vServerId + "&locationId=" + this.location.id + } + }, + template: `
+ + {{location.name}} + + +
+ {{domain}} +
+ + + BREAK + + + 自动跳转HTTPS + + + 文档根目录 + + + 源站 + + + 5秒盾 + + + CC防护 + + + + + + CACHE + + + {{location.web.charset.charset}} + + + + + + + + + Gzip:{{location.web.gzip.level}} + + + 请求Header + 响应Header + + + Websocket + + + 请求脚本 + + + 访客IP地址 + + + 请求限制 + + +
+
PAGE [状态码{{page.status[0]}}] -> {{page.url}}
+
+
+ 临时关闭 +
+ + +
+
+ REWRITE {{rewriteRule.pattern}} -> {{rewriteRule.replace}} +
` }) +Vue.component("http-location-labels-label", { + props: ["v-class", "v-href"], + template: `` +}) + +// 请求方法列表 +Vue.component("http-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等。

+
+
+
+ +
+
` +}) + +// 压缩配置 +Vue.component("http-optimization-config-box", { + props: ["v-optimization-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vOptimizationConfig + + return { + config: config, + htmlMoreOptions: false, + javascriptMoreOptions: false, + cssMoreOptions: false + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + } + }, + template: `
+ + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
HTML优化 +
+ + +
+

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

+
HTML例外URL + +

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

+
HTML限制URL + +

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

+
+ + + + + + + + + + + + + + + + + + + + + +
Javascript优化 +
+ + +
+

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

+
Javascript例外URL + +

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

+
Javascript限制URL + +

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

+
+ + + + + + + + + + + + + + + + + + + + + +
CSS优化 +
+ + +
+

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

+
CSS例外URL + +

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

+
CSS限制URL + +

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

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

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

+ + + + {{name}}名称 * + + +

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

+ + + + {{name}}参数名称 * + + +

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

+ + +` +}) + Vue.component("http-pages-and-shutdown-box", { props: ["v-enable-global-pages", "v-pages", "v-shutdown-config", "v-is-location"], data: function () { @@ -10895,19 +18627,869 @@ Vue.component("http-pages-and-shutdown-box", {
` }) -// 页面动态加密配置 -Vue.component("http-encryption-config-box", { - props: ["v-encryption-config", "v-is-location", "v-is-group"], +Vue.component("http-pages-box", { + props: ["v-pages"], data: function () { - let config = this.vEncryptionConfig + let pages = [] + if (this.vPages != null) { + pages = this.vPages + } + + return { + pages: pages + } + }, + methods: { + addPage: function () { + let that = this + teaweb.popup("/servers/server/settings/pages/createPopup", { + height: "26em", + callback: function (resp) { + that.pages.push(resp.data.page) + that.notifyChange() + } + }) + }, + updatePage: function (pageIndex, pageId) { + let that = this + teaweb.popup("/servers/server/settings/pages/updatePopup?pageId=" + pageId, { + height: "26em", + callback: function (resp) { + Vue.set(that.pages, pageIndex, resp.data.page) + that.notifyChange() + } + }) + }, + removePage: function (pageIndex) { + let that = this + teaweb.confirm("确定要移除此页面吗?", function () { + that.pages.$remove(pageIndex) + that.notifyChange() + }) + }, + notifyChange: function () { + let parent = this.$el.parentNode + while (true) { + if (parent == null) { + break + } + if (parent.tagName == "FORM") { + break + } + parent = parent.parentNode + } + if (parent != null) { + setTimeout(function () { + Tea.runActionOn(parent) + }, 100) + } + } + }, + template: `
+ + +
+ + + + + + + + + + + + + + + + + + + +
响应状态码页面类型新状态码例外URL限制URL操作
+ + {{page.status[0]}} + {{page.status}} + + + + +
+ {{page.url}} +
+ 读取URL +
+
+
+ {{page.url}} +
+ 跳转URL + {{page.newStatus}} +
+
+
+ [HTML内容] +
+ {{page.newStatus}} +
+
+
+ {{page.newStatus}} + 保持 + +
+ {{urlPattern.pattern}} +
+ - +
+
+ {{urlPattern.pattern}} +
+ - +
+ 修改   + 删除 +
+
+
+ +
+
+
` +}) + +Vue.component("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-referers-config-box", { + props: ["v-referers-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vReferersConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + allowEmpty: true, + allowSameDomain: true, + allowDomains: [], + denyDomains: [], + checkOrigin: true + } + } + if (config.allowDomains == null) { + config.allowDomains = [] + } + if (config.denyDomains == null) { + config.denyDomains = [] + } + return { + config: config, + moreOptionsVisible: false + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + }, + changeAllowDomains: function (domains) { + if (typeof (domains) == "object") { + this.config.allowDomains = domains + this.$forceUpdate() + } + }, + changeDenyDomains: function (domains) { + if (typeof (domains) == "object") { + this.config.denyDomains = domains + this.$forceUpdate() + } + }, + showMoreOptions: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用防盗链 +
+ + +
+

选中后表示开启防盗链。

+
允许直接访问网站 + +

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

+
来源域名允许一致 + +

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

+
允许的来源域名 + > +

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

+
禁止的来源域名 + +

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

+
同时检查Origin + +

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

+
例外URL + +

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

+
限制URL + +

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

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

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

+
获取IP方式 * + +

{{option.description}}

+
请求报头 * + +

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

+
读取IP变量值 * + +

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

+
+
+
` +}) + +Vue.component("http-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-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("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-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: { @@ -10916,438 +19498,482 @@ Vue.component("http-encryption-config-box", { } }, template: `
- - - + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
启用请求限制 + +
最大并发连接数 + +

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

+
单IP最大并发连接数 + +

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

+
单连接带宽限制 + +

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

+
单请求最大尺寸 + +

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

+
- -
-
- - - - - - - - - - - - - - - - -
启用页面动态加密 -
- - -
-

启用后,将对 HTML 页面中的 JavaScript 进行动态加密,有效抵御批量爬虫和脚本工具。

-
排除 URL - -

这些 URL 将跳过加密处理,支持正则表达式。

-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - -
HTML 加密 -
- - -
-

加密 HTML 页面中的 JavaScript 脚本。

-
加密内联脚本 -
- - -
-

加密 HTML 中的内联 <script> 标签内容。

-
加密外部脚本 -
- - -
-

加密通过 src 属性引入的外部 JavaScript 文件。

-
URL 匹配规则 - -

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

-
- -
- - - - - - - - - - - - - - - - -
JavaScript 文件加密 -
- - -
-

加密独立的 JavaScript 文件(.js 文件)。

-
URL 匹配规则 - -

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

-
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
服务器端密钥 - -

用于生成加密密钥的密码,建议使用复杂的随机字符串。默认密钥仅用于测试,生产环境请务必修改!

-
时间分片(秒) - -

加密密钥每隔多少秒更换一次。时间越短越安全,但可能影响性能。建议 60-120 秒,默认 60 秒。

-
IP CIDR 前缀长度 - -

将用户 IP 地址的前多少位作为识别依据。例如设置为 24 时,192.168.1.1 和 192.168.1.2 会被视为同一用户。默认 24。

-
简化 User-Agent -
- - -
-

开启后,只识别浏览器类型(如 Chrome、Firefox),忽略版本号等细节,避免因浏览器自动更新导致解密失败。

-
- -
- - - - - - - - - - - - - - - - - - - - -
启用缓存 -
- - -
-

开启后,相同内容的加密结果会被缓存,减少重复计算,提升响应速度。

-
缓存 TTL(秒) - -

缓存的有效期,超过这个时间后缓存会自动失效。建议与上面的"时间分片"保持一致。默认 60 秒。

-
最大缓存条目数 - -

最多缓存多少个加密结果。数量越大占用内存越多,建议根据服务器内存情况调整。默认 1000。

-
-
-
-
` }) - - -Vue.component("http-firewall-page-options", { - props: ["v-page-options"], +Vue.component("http-request-scripts-config-box", { + props: ["vRequestScriptsConfig", "v-auditing-status", "v-is-location"], data: function () { - var defaultPageBody = ` - - - 403 Forbidden - - - -

403 Forbidden By WAF

-
Connection: \${remoteAddr} (Client) -> \${serverAddr} (Server)
-
Request ID: \${requestId}
- -` + let config = this.vRequestScriptsConfig + if (config == null) { + config = {} + } return { - pageOptions: this.vPageOptions, - status: this.vPageOptions.status, - body: this.vPageOptions.body, - defaultPageBody: defaultPageBody, - isEditing: false + config: config } }, - watch: { - status: function (v) { - if (typeof v === "string" && v.length != 3) { + methods: { + changeInitGroup: function (group) { + this.config.initGroup = group + this.$forceUpdate() + }, + changeRequestGroup: function (group) { + this.config.requestGroup = group + this.$forceUpdate() + } + }, + template: `
+ +
+

请求初始化

+

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

+
+ +
+

准备发送请求

+

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

+
+ +
+
+
` +}) + +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 statusCode = parseInt(v) - if (isNaN(statusCode)) { - this.pageOptions.status = 403 - } else { - this.pageOptions.status = statusCode - } - }, - body: function (v) { - this.pageOptions.body = v - } - }, - methods: { - edit: function () { - this.isEditing = !this.isEditing + 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: `
- - 状态码:{{status}} / 提示内容:[{{pageOptions.body.length}}字符][无] - - - - - - - - - - +
+

暂时还没有重写规则。

+
状态码 *
网页内容 - -

[使用模板]

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

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

+ +` }) -Vue.component("http-firewall-js-cookie-options", { - props: ["v-js-cookie-options"], - mounted: function () { - this.updateSummary() - }, - data: function () { - let options = this.vJsCookieOptions - if (options == null) { - options = { - life: 0, - maxFails: 0, - failBlockTimeout: 0, - failBlockScopeAll: false, - scope: "service" - } - } +Vue.component("http-rewrite-labels-label", { + props: ["v-class"], + template: `` +}) +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 { - options: options, - isEditing: false, - summary: "" + stat: stat } }, - watch: { - "options.life": function (v) { - let i = parseInt(v, 10) - if (isNaN(i)) { - i = 0 - } - this.options.life = i - this.updateSummary() - }, - "options.maxFails": function (v) { - let i = parseInt(v, 10) - if (isNaN(i)) { - i = 0 - } - this.options.maxFails = i - this.updateSummary() - }, - "options.failBlockTimeout": function (v) { - let i = parseInt(v, 10) - if (isNaN(i)) { - i = 0 - } - this.options.failBlockTimeout = i - this.updateSummary() - }, - "options.failBlockScopeAll": function (v) { - this.updateSummary() + template: `
+ + + + + + + + + +
启用统计 +
+ + +
+
+
+
` +}) + +// 请求方法列表 +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: { - edit: function () { - this.isEditing = !this.isEditing - }, - updateSummary: function () { - let summaryList = [] - if (this.options.life > 0) { - summaryList.push("有效时间" + this.options.life + "秒") - } - if (this.options.maxFails > 0) { - summaryList.push("最多失败" + this.options.maxFails + "次") - } - if (this.options.failBlockTimeout > 0) { - summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") - } - if (this.options.failBlockScopeAll) { - summaryList.push("尝试全局封禁") - } - - if (summaryList.length == 0) { - this.summary = "默认配置" - } else { - this.summary = summaryList.join(" / ") - } + add: function () { + this.isAdding = true + let that = this + setTimeout(function () { + that.$refs.addingStatus.focus() + }, 100) }, confirm: function () { - this.isEditing = false + 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: `
- - {{summary}} -
- - - - - - - - - - - - - - - - - - - -
有效时间 -
- - -
-

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

-
最多失败次数 -
- - -
-

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

-
失败拦截时间 -
- - -
-

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

-
失败全局封禁 - -

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

-
+ +
+ + {{status}} +   + +
-
-` +
+
+
+ +
+
+ +   +
+
+

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

+
+
+
+ +
+
` }) -// 压缩配置 -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() - }) - }, +Vue.component("http-web-root-box", { + props: ["v-root-config", "v-is-location", "v-is-group"], data: function () { - let config = this.vCompressionConfig + let config = this.vRootConfig if (config == null) { config = { isPrior: false, isOn: false, - useDefaultTypes: true, - types: ["brotli", "gzip", "zstd", "deflate"], - level: 0, - decompressData: false, - gzipRef: null, - deflateRef: null, - brotliRef: null, - minLength: {count: 1, "unit": "kb"}, - maxLength: {count: 32, "unit": "mb"}, - mimeTypes: ["text/*", "application/javascript", "application/json", "application/atom+xml", "application/rss+xml", "application/xhtml+xml", "font/*", "image/svg+xml"], - extensions: [".js", ".json", ".html", ".htm", ".xml", ".css", ".woff2", ".txt"], - exceptExtensions: [".apk", ".ipa"], - conds: null, - enablePartialContent: false, + dir: "", + indexes: [], + stripPrefix: "", + decodePath: false, + isBreak: false, + exceptHiddenFiles: true, onlyURLPatterns: [], exceptURLPatterns: [] } } - - if (config.types == null) { - config.types = [] + if (config.indexes == null) { + config.indexes = [] } + + if (config.onlyURLPatterns == null) { + config.onlyURLPatterns = [] + } + if (config.exceptURLPatterns == null) { + config.exceptURLPatterns = [] + } + + return { + config: config, + advancedVisible: false + } + }, + methods: { + changeAdvancedVisible: function (v) { + this.advancedVisible = v + }, + addIndex: function () { + let that = this + teaweb.popup("/servers/server/settings/web/createIndex", { + height: "10em", + callback: function (resp) { + that.config.indexes.push(resp.data.index) + } + }) + }, + removeIndex: function (i) { + this.config.indexes.$remove(i) + }, + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用静态资源分发 +
+ + +
+
静态资源根目录 + +

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

+
首页文件 + +
+
+ {{index}} +
+
+
+ +

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

+
例外URL + +

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

+
限制URL + +

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

+
排除隐藏文件 + +

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

+
去除URL前缀 + +

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

+
路径解码 +
+ + +
+

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

+
终止请求 +
+ + +
+

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

+
+
+
` +}) + +Vue.component("http-webp-config-box", { + props: ["v-webp-config", "v-is-location", "v-is-group", "v-require-cache"], + data: function () { + let config = this.vWebpConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + minLength: {count: 0, "unit": "kb"}, + maxLength: {count: 0, "unit": "kb"}, + mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico"], + extensions: [".png", ".jpeg", ".jpg", ".bmp", ".ico"], + conds: null + } + } + if (config.mimeTypes == null) { config.mimeTypes = [] } @@ -11355,49 +19981,9 @@ Vue.component("http-compression-config-box", { 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 + moreOptionsVisible: false } }, methods: { @@ -11412,14 +19998,6 @@ Vue.component("http-compression-config-box", { }) this.config.extensions = values }, - changeExceptExtensions: function (values) { - values.forEach(function (v, k) { - if (v.length > 0 && v[0] != ".") { - values[k] = "." + v - } - }) - this.config.exceptExtensions = values - }, changeMimeTypes: function (values) { this.config.mimeTypes = values }, @@ -11428,103 +20006,38 @@ Vue.component("http-compression-config-box", { }, 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: `
- + - + - - - - - - - - - - - - - - - + - + @@ -11541,31 +20054,172 @@ Vue.component("http-compression-config-box", {

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

- - - - - - - - - - - - + + +
启用内容压缩启用WebP压缩
-
支持的扩展名 - -

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

-
例外扩展名 - -

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

-
支持的MimeType - -

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

+

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

压缩算法支持的扩展名 -
- - - -
-
-
-
-
- - -
-
-
- -

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

+ +

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

支持已压缩内容支持的MimeType - -

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

+ +

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

支持Partial
Content
- -

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

-
例外URL - -

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

-
限制URL - -

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

-
匹配条件 +
+
+
` +}) + +Vue.component("http-websocket-box", { + props: ["v-websocket-ref", "v-websocket-config", "v-is-location", "v-is-group"], + data: function () { + let websocketRef = this.vWebsocketRef + if (websocketRef == null) { + websocketRef = { + isPrior: false, + isOn: false, + websocketId: 0 + } + } + + let websocketConfig = this.vWebsocketConfig + if (websocketConfig == null) { + websocketConfig = { + id: 0, + isOn: false, + handshakeTimeout: { + count: 30, + unit: "second" + }, + allowAllOrigins: true, + allowedOrigins: [], + requestSameOrigin: true, + requestOrigin: "" + } + } else { + if (websocketConfig.handshakeTimeout == null) { + websocketConfig.handshakeTimeout = { + count: 30, + unit: "second", + } + } + if (websocketConfig.allowedOrigins == null) { + websocketConfig.allowedOrigins = [] + } + } + + return { + websocketRef: websocketRef, + websocketConfig: websocketConfig, + handshakeTimeoutCountString: websocketConfig.handshakeTimeout.count.toString(), + advancedVisible: false + } + }, + watch: { + handshakeTimeoutCountString: function (v) { + let count = parseInt(v) + if (!isNaN(count) && count >= 0) { + this.websocketConfig.handshakeTimeout.count = count + } else { + this.websocketConfig.handshakeTimeout.count = 0 + } + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.websocketRef.isPrior) && this.websocketRef.isOn + }, + changeAdvancedVisible: function (v) { + this.advancedVisible = v + }, + createOrigin: function () { + let that = this + teaweb.popup("/servers/server/settings/websocket/createOrigin", { + height: "12.5em", + callback: function (resp) { + that.websocketConfig.allowedOrigins.push(resp.data.origin) + } + }) + }, + removeOrigin: function (index) { + this.websocketConfig.allowedOrigins.$remove(index) + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -11574,257 +20228,868 @@ Vue.component("http-compression-config-box", { ` }) -// HTTP CC防护配置 -Vue.component("http-cc-config-box", { - props: ["v-cc-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vCcConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - enableFingerprint: true, - enableGET302: true, - onlyURLPatterns: [], - exceptURLPatterns: [], - useDefaultThresholds: true, - ignoreCommonFiles: true - } - } - - if (config.thresholds == null || config.thresholds.length == 0) { - config.thresholds = [ - { - maxRequests: 0 - }, - { - maxRequests: 0 - }, - { - maxRequests: 0 - } - ] - } - - if (typeof config.enableFingerprint != "boolean") { - config.enableFingerprint = true - } - if (typeof config.enableGET302 != "boolean") { - config.enableGET302 = true - } - - if (config.onlyURLPatterns == null) { - config.onlyURLPatterns = [] - } - if (config.exceptURLPatterns == null) { - config.exceptURLPatterns = [] - } - return { - config: config, - moreOptionsVisible: false, - minQPSPerIP: config.minQPSPerIP, - useCustomThresholds: !config.useDefaultThresholds, - - thresholdMaxRequests0: this.maxRequestsStringAtThresholdIndex(config, 0), - thresholdMaxRequests1: this.maxRequestsStringAtThresholdIndex(config, 1), - thresholdMaxRequests2: this.maxRequestsStringAtThresholdIndex(config, 2) - } +// 指标图表 +Vue.component("metric-chart", { + props: ["v-chart", "v-stats", "v-item", "v-column" /** in column? **/], + mounted: function () { + this.load() }, - watch: { - minQPSPerIP: function (v) { - let qps = parseInt(v.toString()) - if (isNaN(qps) || qps < 0) { - qps = 0 + 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 + }) + } } - this.config.minQPSPerIP = qps - }, - thresholdMaxRequests0: function (v) { - this.setThresholdMaxRequests(0, v) - }, - thresholdMaxRequests1: function (v) { - this.setThresholdMaxRequests(1, v) - }, - thresholdMaxRequests2: function (v) { - this.setThresholdMaxRequests(2, v) - }, - useCustomThresholds: function (b) { - this.config.useDefaultThresholds = !b + } + 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: { - maxRequestsStringAtThresholdIndex: function (config, index) { - if (config.thresholds == null) { + load: function () { + var el = document.getElementById(this.chartId) + if (el == null || el.offsetWidth == 0 || el.offsetHeight == 0) { + setTimeout(this.load, 100) + } else { + this.render(el) + } + }, + render: function (el) { + let chart = echarts.init(el) + window.addEventListener("resize", function () { + chart.resize() + }) + switch (this.chart.type) { + case "pie": + this.renderPie(chart) + break + case "bar": + this.renderBar(chart) + break + case "timeBar": + this.renderTimeBar(chart) + break + case "timeLine": + this.renderTimeLine(chart) + break + case "table": + this.renderTable(chart) + break + } + }, + renderPie: function (chart) { + let values = this.stats.map(function (v) { + return { + name: v.keys[0], + value: v.value + } + }) + let that = this + chart.setOption({ + tooltip: { + show: true, + trigger: "item", + formatter: function (data) { + let stat = that.stats[data.dataIndex] + let percent = 0 + if (stat.total > 0) { + percent = Math.round((stat.value * 100 / stat.total) * 100) / 100 + } + let value = stat.value + switch (that.item.valueType) { + case "byte": + value = teaweb.formatBytes(value) + break + case "count": + value = teaweb.formatNumber(value) + break + } + return stat.keys[0] + "
" + that.valueTypeName + ": " + value + "
占比:" + percent + "%" + } + }, + series: [ + { + name: name, + type: "pie", + data: values, + areaStyle: {}, + color: ["#9DD3E8", "#B2DB9E", "#F39494", "#FBD88A", "#879BD7"] + } + ] + }) + }, + renderTimeBar: function (chart) { + this.stats.$sort(function (v1, v2) { + return (v1.time < v2.time) ? -1 : 1 + }) + let values = this.stats.map(function (v) { + return v.value + }) + + let axis = {unit: "", divider: 1} + switch (this.item.valueType) { + case "count": + axis = teaweb.countAxis(values, function (v) { + return v + }) + break + case "byte": + axis = teaweb.bytesAxis(values, function (v) { + return v + }) + break + } + + let that = this + chart.setOption({ + xAxis: { + data: this.stats.map(function (v) { + return that.formatTime(v.time) + }) + }, + yAxis: { + axisLabel: { + formatter: function (value) { + return value + axis.unit + } + } + }, + tooltip: { + show: true, + trigger: "item", + formatter: function (data) { + let stat = that.stats[data.dataIndex] + let value = stat.value + switch (that.item.valueType) { + case "byte": + value = teaweb.formatBytes(value) + break + } + return that.formatTime(stat.time) + ": " + value + } + }, + grid: { + left: 50, + top: 10, + right: 20, + bottom: 25 + }, + series: [ + { + name: name, + type: "bar", + data: values.map(function (v) { + return v / axis.divider + }), + itemStyle: { + color: teaweb.DefaultChartColor + }, + areaStyle: {}, + barWidth: "10em" + } + ] + }) + }, + renderTimeLine: function (chart) { + this.stats.$sort(function (v1, v2) { + return (v1.time < v2.time) ? -1 : 1 + }) + let values = this.stats.map(function (v) { + return v.value + }) + + let axis = {unit: "", divider: 1} + switch (this.item.valueType) { + case "count": + axis = teaweb.countAxis(values, function (v) { + return v + }) + break + case "byte": + axis = teaweb.bytesAxis(values, function (v) { + return v + }) + break + } + + let that = this + chart.setOption({ + xAxis: { + data: this.stats.map(function (v) { + return that.formatTime(v.time) + }) + }, + yAxis: { + axisLabel: { + formatter: function (value) { + return value + axis.unit + } + } + }, + tooltip: { + show: true, + trigger: "item", + formatter: function (data) { + let stat = that.stats[data.dataIndex] + let value = stat.value + switch (that.item.valueType) { + case "byte": + value = teaweb.formatBytes(value) + break + } + return that.formatTime(stat.time) + ": " + value + } + }, + grid: { + left: 50, + top: 10, + right: 20, + bottom: 25 + }, + series: [ + { + name: name, + type: "line", + data: values.map(function (v) { + return v / axis.divider + }), + itemStyle: { + color: teaweb.DefaultChartColor + }, + areaStyle: {} + } + ] + }) + }, + renderBar: function (chart) { + let values = this.stats.map(function (v) { + return v.value + }) + let axis = {unit: "", divider: 1} + switch (this.item.valueType) { + case "count": + axis = teaweb.countAxis(values, function (v) { + return v + }) + break + case "byte": + axis = teaweb.bytesAxis(values, function (v) { + return v + }) + break + } + let bottom = 24 + let rotate = 0 + let result = teaweb.xRotation(chart, this.stats.map(function (v) { + return v.keys[0] + })) + if (result != null) { + bottom = result[0] + rotate = result[1] + } + let that = this + chart.setOption({ + xAxis: { + data: this.stats.map(function (v) { + return v.keys[0] + }), + axisLabel: { + interval: 0, + rotate: rotate + } + }, + tooltip: { + show: true, + trigger: "item", + formatter: function (data) { + let stat = that.stats[data.dataIndex] + let percent = 0 + if (stat.total > 0) { + percent = Math.round((stat.value * 100 / stat.total) * 100) / 100 + } + let value = stat.value + switch (that.item.valueType) { + case "byte": + value = teaweb.formatBytes(value) + break + case "count": + value = teaweb.formatNumber(value) + break + } + return stat.keys[0] + "
" + that.valueTypeName + ":" + value + "
占比:" + percent + "%" + } + }, + yAxis: { + axisLabel: { + formatter: function (value) { + return value + axis.unit + } + } + }, + grid: { + left: 40, + top: 10, + right: 20, + bottom: bottom + }, + series: [ + { + name: name, + type: "bar", + data: values.map(function (v) { + return v / axis.divider + }), + itemStyle: { + color: teaweb.DefaultChartColor + }, + areaStyle: {}, + barWidth: "10em" + } + ] + }) + + if (this.item.keys != null) { + // IP相关操作 + if (this.item.keys.$contains("${remoteAddr}")) { + let that = this + chart.on("click", function (args) { + let index = that.item.keys.$indexesOf("${remoteAddr}")[0] + let value = that.stats[args.dataIndex].keys[index] + teaweb.popup("/servers/ipbox?ip=" + value, { + width: "50em", + height: "30em" + }) + }) + } + } + }, + renderTable: function (chart) { + let table = `
启用Websocket +
+ + +
+
允许所有来源域(Origin) +
+ + +
+

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

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

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

+
传递请求来源域 +
+ + +
+

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

+
指定传递的来源域 + +

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

+
握手超时时间(Handshake) +
+
+ +
+
+ 秒 +
+
+

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

+ + + + + + + ` + 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 "" } - if (index < config.thresholds.length) { - let s = config.thresholds[index].maxRequests.toString() - if (s == "0") { - s = "" - } - return s + 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 "" - }, - setThresholdMaxRequests: function (index, v) { - let maxRequests = parseInt(v) - if (isNaN(maxRequests) || maxRequests < 0) { - maxRequests = 0 - } - if (index < this.config.thresholds.length) { - this.config.thresholds[index].maxRequests = maxRequests - } - }, - showMoreOptions: function () { - this.moreOptionsVisible = !this.moreOptionsVisible + return time } }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用CC无感防护 - -

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

-
例外URL - -

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

-
限制URL - -

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

-
忽略常用文件 - -

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

-
检查请求来源指纹 - -

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

-
启用GET302校验 - -

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

-
单IP最低QPS -
- - 请求数/秒 -
-

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

-
使用自定义拦截阈值 - -
自定义拦截阈值设置 -
-
- 单IP每5秒最多 - - 请求 -
-
- -
-
- 单IP每60秒 - - 请求 -
-
-
-
- 单IP每300秒 - - 请求 -
-
-
-
+ template: `
+

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

+
+
` }) -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 = "" // 不要给默认值,因为黑白名单等默认值均有不同 - } +Vue.component("metric-board", { + template: `
` +}) - return { - levels: [], - description: "", - level: value - } - }, - methods: { - change: function () { - this.$emit("change") +// 显示指标对象名 +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)}} +
` +}) - 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("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("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("origin-list-box", { + props: ["v-primary-origins", "v-backup-origins", "v-server-type", "v-params"], + data: function () { + return { + primaryOrigins: this.vPrimaryOrigins, + backupOrigins: this.vBackupOrigins + } + }, + methods: { + createPrimaryOrigin: function () { + teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, { + width: "45em", + height: "27em", + callback: function (resp) { + teaweb.success("保存成功", function () { + window.location.reload() + }) + } + }) + }, + createBackupOrigin: function () { + teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, { + width: "45em", + height: "27em", + callback: function (resp) { + teaweb.success("保存成功", function () { + window.location.reload() + }) + } + }) + }, + updateOrigin: function (originId, originType) { + teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, { + width: "45em", + height: "27em", + callback: function (resp) { + teaweb.success("保存成功", function () { + window.location.reload() + }) + } + }) + }, + deleteOrigin: function (originId, originAddr, originType) { + let that = this + teaweb.confirm("确定要删除此源站(" + originAddr + ")吗?", function () { + Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType) + .post() + .success(function () { + teaweb.success("删除成功", function () { + window.location.reload() + }) + }) + }) + }, + updateOriginIsOn: function (originId, originAddr, isOn) { + let message + let resultMessage + if (isOn) { + message = "确定要启用此源站(" + originAddr + ")吗?" + resultMessage = "启用成功" + } else { + message = "确定要停用此源站(" + originAddr + ")吗?" + resultMessage = "停用成功" + } + let that = this + teaweb.confirm(message, function () { + Tea.action("/servers/server/settings/origins/updateIsOn?" + that.vParams + "&originId=" + originId + "&isOn=" + (isOn ? 1 : 0)) + .post() + .success(function () { + teaweb.success(resultMessage, function () { + window.location.reload() + }) + }) + }) + } + }, + template: `
+

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

+

暂时还没有主要源站。

+ + +

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

+

暂时还没有备用源站。

+ +
` +}) + +Vue.component("origin-list-table", { + props: ["v-origins", "v-origin-type"], + data: function () { + let hasMatchedDomains = false + let origins = this.vOrigins + if (origins != null && origins.length > 0) { + origins.forEach(function (origin) { + if (origin.domains != null && origin.domains.length > 0) { + hasMatchedDomains = true + } + }) + } + + return { + hasMatchedDomains: hasMatchedDomains + } + }, + methods: { + deleteOrigin: function (originId, originAddr) { + this.$emit("deleteOrigin", originId, originAddr, this.vOriginType) + }, + updateOrigin: function (originId) { + this.$emit("updateOrigin", originId, this.vOriginType) + }, + updateOriginIsOn: function (originId, originAddr, isOn) { + this.$emit("updateOriginIsOn", originId, originAddr, isOn) + } + }, + template: ` + + + + + + + + + + + + + + + + + +
源站地址权重状态操作
+ {{origin.addr}}   +
+ 对象存储 + {{origin.name}} + 证书 + 主机名: {{origin.host}} + 端口跟随 + HTTP/2 + + 匹配: {{domain}} + 匹配: 所有域名 +
+
{{origin.weight}} + + + 修改   + 停用启用   + 删除 +
` +}) + +Vue.component("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}}

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

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

-
字符编码大写 -
- - -
-

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

-
-
-
` -}) - -Vue.component("http-expires-time-config-box", { - props: ["v-expires-time"], - data: function () { - let expiresTime = this.vExpiresTime - if (expiresTime == null) { - expiresTime = { - isPrior: false, - isOn: false, - overwrite: true, - autoCalculate: true, - duration: {count: -1, "unit": "hour"} - } - } - return { - expiresTime: expiresTime - } - }, - watch: { - "expiresTime.isPrior": function () { - this.notifyChange() - }, - "expiresTime.isOn": function () { - this.notifyChange() - }, - "expiresTime.overwrite": function () { - this.notifyChange() - }, - "expiresTime.autoCalculate": function () { - this.notifyChange() - } - }, - methods: { - notifyChange: function () { - this.$emit("change", this.expiresTime) - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - -
启用 -

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

-
覆盖源站设置 - -

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

-
自动计算时间 -

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

-
强制缓存时间 - -

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

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

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

高级信息 -
- - -
-

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

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

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

-
- -
-

WAF相关

- - - - - -
只记录WAF相关日志 - -

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

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

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

-
Gzip内容最小长度 - -

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

-
Gzip内容最大长度 - -

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

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

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

-

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

- {{config.code}} -

{{comment}}

-
-
` -}) - -Vue.component("http-firewall-js-cookie-options-viewer", { - props: ["v-js-cookie-options"], - mounted: function () { - this.updateSummary() - }, - data: function () { - let options = this.vJsCookieOptions - if (options == null) { - options = { - life: 0, - maxFails: 0, - failBlockTimeout: 0, - failBlockScopeAll: false, - scope: "" - } - } - return { - options: options, - summary: "" - } - }, - methods: { - updateSummary: function () { - let summaryList = [] - if (this.options.life > 0) { - summaryList.push("有效时间" + this.options.life + "秒") - } - if (this.options.maxFails > 0) { - summaryList.push("最多失败" + this.options.maxFails + "次") - } - if (this.options.failBlockTimeout > 0) { - summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") - } - if (this.options.failBlockScopeAll) { - summaryList.push("尝试全局封禁") - } - - if (summaryList.length == 0) { - this.summary = "默认配置" - } else { - this.summary = summaryList.join(" / ") - } - } - }, - template: `
{{summary}}
-` -}) - -Vue.component("ssl-certs-view", { - props: ["v-certs"], - data: function () { - let certs = this.vCerts - if (certs == null) { - certs = [] - } - return { - certs: certs - } - }, - methods: { - // 格式化时间 - formatTime: function (timestamp) { - return new Date(timestamp * 1000).format("Y-m-d") - }, - - // 查看详情 - viewCert: function (certId) { - teaweb.popup("/servers/certs/certPopup?certId=" + certId, { - height: "28em", - width: "48em" - }) - } - }, - template: `
-
-
- {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   -
-
-
` -}) - -Vue.component("http-firewall-captcha-options-viewer", { - props: ["v-captcha-options"], - mounted: function () { - this.updateSummary() - }, - data: function () { - let options = this.vCaptchaOptions - if (options == null) { - options = { - life: 0, - maxFails: 0, - failBlockTimeout: 0, - failBlockScopeAll: false, - uiIsOn: false, - uiTitle: "", - uiPrompt: "", - uiButtonTitle: "", - uiShowRequestId: false, - uiCss: "", - uiFooter: "", - uiBody: "", - cookieId: "", - lang: "" - } - } - return { - options: options, - summary: "", - captchaTypes: window.WAF_CAPTCHA_TYPES - } - }, - methods: { - updateSummary: function () { - let summaryList = [] - if (this.options.life > 0) { - summaryList.push("有效时间" + this.options.life + "秒") - } - if (this.options.maxFails > 0) { - summaryList.push("最多失败" + this.options.maxFails + "次") - } - if (this.options.failBlockTimeout > 0) { - summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") - } - if (this.options.failBlockScopeAll) { - summaryList.push("全局封禁") - } - let that = this - let typeDef = this.captchaTypes.$find(function (k, v) { - return v.code == that.options.captchaType - }) - if (typeDef != null) { - summaryList.push("默认验证方式:" + typeDef.name) - } - - if (this.options.captchaType == "default") { - if (this.options.uiIsOn) { - summaryList.push("定制UI") - } - } - - if (this.options.geeTestConfig != null && this.options.geeTestConfig.isOn) { - summaryList.push("已配置极验") - } - - if (summaryList.length == 0) { - this.summary = "默认配置" - } else { - this.summary = summaryList.join(" / ") - } - } - }, - template: `
{{summary}}
-` -}) - Vue.component("reverse-proxy-box", { props: ["v-reverse-proxy-ref", "v-reverse-proxy-config", "v-is-location", "v-is-group", "v-family"], data: function () { @@ -13490,347 +21465,137 @@ Vue.component("reverse-proxy-box", {
` }) -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: "" - } +Vue.component("script-config-box", { + props: ["id", "v-script-config", "comment", "v-auditing-status"], + mounted: function () { + let that = this + setTimeout(function () { + that.$forceUpdate() + }, 100) }, - 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 + let config = this.vScriptConfig if (config == null) { config = { isPrior: false, isOn: false, - value: "${rawRemoteAddr}", - type: "default", - - requestHeaderName: "" + code: "", + auditingCode: "" } } - // type - if (config.type == null || config.type.length == 0) { - config.type = "default" - switch (config.value) { - case "${rawRemoteAddr}": - config.type = "default" - break - case "${remoteAddrValue}": - config.type = "default" - break - case "${remoteAddr}": - config.type = "proxy" - break - default: - if (config.value != null && config.value.length > 0) { - config.type = "variable" + let auditingStatus = null + if (config.auditingCodeMD5 != null && config.auditingCodeMD5.length > 0 && config.auditingCode != null && config.auditingCode.length > 0) { + config.code = config.auditingCode + + if (this.vAuditingStatus != null) { + for (let i = 0; i < this.vAuditingStatus.length; i++) { + let status = this.vAuditingStatus[i] + if (status.md5 == config.auditingCodeMD5) { + auditingStatus = status + break } + } } } - // value - if (config.value == null || config.value.length == 0) { - config.value = "${rawRemoteAddr}" + if (config.code.length == 0) { + config.code = "\n\n\n\n" } return { config: config, - options: [ - { - name: "直接获取", - description: "用户直接访问边缘节点,即 \"用户 --> 边缘节点\" 模式,这时候系统会试图从直接的连接中读取到客户端IP地址。", - value: "${rawRemoteAddr}", - type: "default" - }, - { - name: "从上级代理中获取", - description: "用户和边缘节点之间有别的代理服务转发,即 \"用户 --> [第三方代理服务] --> 边缘节点\",这时候只能从上级代理中获取传递的IP地址;上级代理传递的请求报头中必须包含 X-Forwarded-For 或 X-Real-IP 信息。", - value: "${remoteAddr}", - type: "proxy" - }, - { - name: "从请求报头中读取", - description: "从自定义请求报头读取客户端IP。", - value: "", - type: "requestHeader" - }, - { - name: "[自定义变量]", - description: "通过自定义变量来获取客户端真实的IP地址。", - value: "", - type: "variable" - } - ] + auditingStatus: auditingStatus } }, watch: { - "config.requestHeaderName": function (value) { - if (this.config.type == "requestHeader"){ - this.config.value = "${header." + value.trim() + "}" - } + "config.isOn": function () { + this.change() } }, methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + change: function () { + this.$emit("change", this.config) }, - changeOptionType: function () { - let that = this - - switch(this.config.type) { - case "default": - this.config.value = "${rawRemoteAddr}" - break - case "proxy": - this.config.value = "${remoteAddr}" - break - case "requestHeader": - this.config.value = "" - if (this.requestHeaderName != null && this.requestHeaderName.length > 0) { - this.config.value = "${header." + this.requestHeaderName + "}" - } - - setTimeout(function () { - that.$refs.requestHeaderInput.focus() - }) - break - case "variable": - this.config.value = "${rawRemoteAddr}" - - setTimeout(function () { - that.$refs.variableInput.focus() - }) - - break + changeCode: function (code) { + this.config.code = code + this.change() + }, + isPlus: function () { + if (Tea == null || Tea.Vue == null) { + return false } + return Tea.Vue.teaIsPlus } }, template: `
- - - + - - + + - - - + + + - - - - - - - - - - - -
启用访客IP设置 -
- - -
-

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

-
启用脚本设置
获取IP方式 *
脚本代码 - -

{{option.description}}

-
请求报头 * - -

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

-
读取IP变量值 * - -

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

+

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

+

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

+ {{config.code}} +

{{comment}}

-
` }) -// 访问日志搜索框 -Vue.component("http-access-log-search-box", { - props: ["v-ip", "v-domain", "v-keyword", "v-cluster-id", "v-node-id"], +Vue.component("script-group-config-box", { + props: ["v-group", "v-auditing-status", "v-is-location"], data: function () { - let ip = this.vIp - if (ip == null) { - ip = "" + let group = this.vGroup + if (group == null) { + group = { + isPrior: false, + isOn: true, + scripts: [] + } + } + if (group.scripts == null) { + group.scripts = [] } - let domain = this.vDomain - if (domain == null) { - domain = "" - } - - let keyword = this.vKeyword - if (keyword == null) { - keyword = "" + let script = null + if (group.scripts.length > 0) { + script = group.scripts[group.scripts.length - 1] } return { - ip: ip, - domain: domain, - keyword: keyword, - clusterId: this.vClusterId + group: group, + script: script } }, methods: { - cleanIP: function () { - this.ip = "" - this.submit() + changeScript: function (script) { + this.group.scripts = [script] // 目前只支持单个脚本 + this.change() }, - 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 + change: function () { + this.$emit("change", this.group) } }, - template: `
-
-
-
-
- IP - - -
+ template: `
+ + +
+
+
-
-
- 域名 - - -
-
-
-
- 关键词 - - -
-
-
-
-
-
- -
-
- -
- -
- -
-
` }) @@ -13855,2160 +21620,6 @@ Vue.component("server-config-copy-link", { template: `批量 ` }) -// 显示指标对象名 -Vue.component("metric-key-label", { - props: ["v-key"], - data: function () { - return { - keyDefs: window.METRIC_HTTP_KEYS - } - }, - methods: { - keyName: function (key) { - let that = this - let subKey = "" - let def = this.keyDefs.$find(function (k, v) { - if (v.code == key) { - return true - } - if (key.startsWith("${arg.") && v.code.startsWith("${arg.")) { - subKey = that.getSubKey("arg.", key) - return true - } - if (key.startsWith("${header.") && v.code.startsWith("${header.")) { - subKey = that.getSubKey("header.", key) - return true - } - if (key.startsWith("${cookie.") && v.code.startsWith("${cookie.")) { - subKey = that.getSubKey("cookie.", key) - return true - } - return false - }) - if (def != null) { - if (subKey.length > 0) { - return def.name + ": " + subKey - } - return def.name - } - return key - }, - getSubKey: function (prefix, key) { - prefix = "${" + prefix - let index = key.indexOf(prefix) - if (index >= 0) { - key = key.substring(index + prefix.length) - key = key.substring(0, key.length - 1) - return key - } - return "" - } - }, - template: `
- {{keyName(this.vKey)}} -
` -}) - -// 指标对象 -Vue.component("metric-keys-config-box", { - props: ["v-keys"], - data: function () { - let keys = this.vKeys - if (keys == null) { - keys = [] - } - return { - keys: keys, - isAdding: false, - key: "", - subKey: "", - keyDescription: "", - - keyDefs: window.METRIC_HTTP_KEYS - } - }, - watch: { - keys: function () { - this.$emit("change", this.keys) - } - }, - methods: { - cancel: function () { - this.key = "" - this.subKey = "" - this.keyDescription = "" - this.isAdding = false - }, - confirm: function () { - if (this.key.length == 0) { - return - } - - if (this.key.indexOf(".NAME") > 0) { - if (this.subKey.length == 0) { - teaweb.warn("请输入参数值") - return - } - this.key = this.key.replace(".NAME", "." + this.subKey) - } - this.keys.push(this.key) - this.cancel() - }, - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - if (that.$refs.key != null) { - that.$refs.key.focus() - } - }, 100) - }, - remove: function (index) { - this.keys.$remove(index) - }, - changeKey: function () { - if (this.key.length == 0) { - return - } - let that = this - let def = this.keyDefs.$find(function (k, v) { - return v.code == that.key - }) - if (def != null) { - this.keyDescription = def.description - } - }, - keyName: function (key) { - let that = this - let subKey = "" - let def = this.keyDefs.$find(function (k, v) { - if (v.code == key) { - return true - } - if (key.startsWith("${arg.") && v.code.startsWith("${arg.")) { - subKey = that.getSubKey("arg.", key) - return true - } - if (key.startsWith("${header.") && v.code.startsWith("${header.")) { - subKey = that.getSubKey("header.", key) - return true - } - if (key.startsWith("${cookie.") && v.code.startsWith("${cookie.")) { - subKey = that.getSubKey("cookie.", key) - return true - } - return false - }) - if (def != null) { - if (subKey.length > 0) { - return def.name + ": " + subKey - } - return def.name - } - return key - }, - getSubKey: function (prefix, key) { - prefix = "${" + prefix - let index = key.indexOf(prefix) - if (index >= 0) { - key = key.substring(index + prefix.length) - key = key.substring(0, key.length - 1) - return key - } - return "" - } - }, - template: `
- -
-
- {{keyName(key)}}   -
-
-
-
-
- -
-
- -
-
- -
-
- -
-
- - -
-
-

{{keyDescription}}

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

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

-
首页文件 - -
-
- {{index}} -
-
-
- -

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

-
例外URL - -

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

-
限制URL - -

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

-
排除隐藏文件 - -

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

-
去除URL前缀 - -

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

-
路径解码 -
- - -
-

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

-
终止请求 -
- - -
-

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

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

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

-
支持的扩展名 - -

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

-
支持的MimeType - -

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

-
内容最小长度 - -

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

-
内容最大长度 - -

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

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

{{scheduling.description}}

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

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

-
最大封禁时长 -
- - -
-

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

-
失败全局封禁 - -

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

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

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

-
例外URL - -

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

-
限制URL - -

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

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

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

- - - - {{name}}名称 * - - -

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

- - - - {{name}}参数名称 * - - -

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

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

请求初始化

-

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

-
- -
-

准备发送请求

-

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

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

格式为大写,比如GETPOST等。

-
-
-
- -
-
` -}) - -// URL扩展名条件 -Vue.component("http-cond-url-extension", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPathLowerExtension}", - operator: "in", - value: "[]" - } - if (this.vCond != null && this.vCond.param == cond.param) { - cond.value = this.vCond.value - } - - let extensions = [] - try { - extensions = JSON.parse(cond.value) - } catch (e) { - - } - - return { - cond: cond, - extensions: extensions, // TODO 可以拖动排序 - - isAdding: false, - addingExt: "" - } - }, - watch: { - extensions: function () { - this.cond.value = JSON.stringify(this.extensions) - } - }, - methods: { - addExt: function () { - this.isAdding = !this.isAdding - - if (this.isAdding) { - let that = this - setTimeout(function () { - that.$refs.addingExt.focus() - }, 100) - } - }, - cancelAdding: function () { - this.isAdding = false - this.addingExt = "" - }, - confirmAdding: function () { - // TODO 做更详细的校验 - // TODO 如果有重复的则提示之 - - if (this.addingExt.length == 0) { - return - } - - let that = this - this.addingExt.split(/[,;,;|]/).forEach(function (ext) { - ext = ext.trim() - if (ext.length > 0) { - if (ext[0] != ".") { - ext = "." + ext - } - ext = ext.replace(/\s+/g, "").toLowerCase() - that.extensions.push(ext) - } - }) - - // 清除状态 - this.cancelAdding() - }, - removeExt: function (index) { - this.extensions.$remove(index) - } - }, - template: `
- -
-
{{ext}}
-
-
-
-
- -
-
- - -
-
-
- -
-

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

-
` -}) - -// 排除URL扩展名条件 -Vue.component("http-cond-url-not-extension", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPathLowerExtension}", - operator: "not in", - value: "[]" - } - if (this.vCond != null && this.vCond.param == cond.param) { - cond.value = this.vCond.value - } - - let extensions = [] - try { - extensions = JSON.parse(cond.value) - } catch (e) { - - } - - return { - cond: cond, - extensions: extensions, // TODO 可以拖动排序 - - isAdding: false, - addingExt: "" - } - }, - watch: { - extensions: function () { - this.cond.value = JSON.stringify(this.extensions) - } - }, - methods: { - addExt: function () { - this.isAdding = !this.isAdding - - if (this.isAdding) { - let that = this - setTimeout(function () { - that.$refs.addingExt.focus() - }, 100) - } - }, - cancelAdding: function () { - this.isAdding = false - this.addingExt = "" - }, - confirmAdding: function () { - // TODO 做更详细的校验 - // TODO 如果有重复的则提示之 - - if (this.addingExt.length == 0) { - return - } - if (this.addingExt[0] != ".") { - this.addingExt = "." + this.addingExt - } - this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase() - this.extensions.push(this.addingExt) - - // 清除状态 - this.cancelAdding() - }, - removeExt: function (index) { - this.extensions.$remove(index) - } - }, - template: `
- -
-
{{ext}}
-
-
-
-
- -
-
- - -
-
-
- -
-

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

-
` -}) - -// 根据URL前缀 -Vue.component("http-cond-url-prefix", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "prefix", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof (this.vCond.value) == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

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

-
` -}) - -Vue.component("http-cond-url-not-prefix", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "prefix", - value: "", - isReverse: true, - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

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

-
` -}) - -// 首页 -Vue.component("http-cond-url-eq-index", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "eq", - value: "/", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

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

-
` -}) - -// 全站 -Vue.component("http-cond-url-all", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "prefix", - value: "/", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

支持全站所有URL。

-
` -}) - -// URL精准匹配 -Vue.component("http-cond-url-eq", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "eq", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

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

-
` -}) - -Vue.component("http-cond-url-not-eq", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "eq", - value: "", - isReverse: true, - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

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

-
` -}) - -// URL正则匹配 -Vue.component("http-cond-url-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

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

-
` -}) - -// 排除URL正则匹配 -Vue.component("http-cond-url-not-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "not regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

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

-
` -}) - -// URL通配符 -Vue.component("http-cond-url-wildcard-match", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "wildcard match", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

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

-
` -}) - -// User-Agent正则匹配 -Vue.component("http-cond-user-agent-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${userAgent}", - operator: "regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

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

-
` -}) - -// User-Agent正则不匹配 -Vue.component("http-cond-user-agent-not-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${userAgent}", - operator: "not regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

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

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

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

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

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

- - - - 操作符 - -
- -

-
- - - - 对比值 - - -
- -

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

-
- - -
- -

要对比的数字。

-
- - -
- -

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

-
-
- -

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

-
-
-
-
除:
-
- -
-
余:
-
- -
-
-
- - -
- -

和参数值一致的字符串。

-

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

-

参数值的前缀。

-

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

-

参数值包含此字符串。

-

参数值不包含此字符串。

-
-
- -

添加参数值列表。

-

添加参数值列表。

-

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

-

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

-
-
-
-
-
-
-
-
-
- - -
- -

要对比的IP。

-
-
- -

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

-
-
- -

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

-
- - - - 不区分大小写 - -
- - -
-

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

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

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

-
-
-
- -
-
` -}) - Vue.component("server-group-selector", { props: ["v-groups"], data: function () { @@ -16063,95 +21674,972 @@ Vue.component("server-group-selector", {
` }) -Vue.component("script-group-config-box", { - props: ["v-group", "v-auditing-status", "v-is-location"], +Vue.component("server-name-box", { + props: ["v-server-names"], data: function () { - let group = this.vGroup - if (group == null) { - group = { - isPrior: false, - isOn: true, - scripts: [] - } + let serverNames = this.vServerNames; + if (serverNames == null) { + serverNames = [] } - 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 + serverNames: serverNames, + isSearching: false, + keyword: "" } }, methods: { - changeScript: function (script) { - this.group.scripts = [script] // 目前只支持单个脚本 - this.change() + 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) + setTimeout(that.submitForm, 100) + } + }); }, - 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 + removeServerName: function (index) { + this.serverNames.$remove(index) + }, + + updateServerName: function (index, serverName) { + window.UPDATING_SERVER_NAME = teaweb.clone(serverName) + let that = this + teaweb.popup("/servers/addServerNamePopup", { + callback: function (resp) { + var serverName = resp.data.serverName + Vue.set(that.serverNames, index, serverName) + setTimeout(that.submitForm, 100) + } + }); + }, + showSearchBox: function () { + this.isSearching = !this.isSearching + if (this.isSearching) { + let that = this + setTimeout(function () { + that.$refs.keywordRef.focus() + }, 200) + } else { + this.keyword = "" } + }, + allServerNames: function () { + if (this.serverNames == null) { + return [] + } + let result = [] + this.serverNames.forEach(function (serverName) { + if (serverName.subNames != null && serverName.subNames.length > 0) { + serverName.subNames.forEach(function (subName) { + if (subName != null && subName.length > 0) { + if (!result.$contains(subName)) { + result.push(subName) + } + } + }) + } else if (serverName.name != null && serverName.name.length > 0) { + if (!result.$contains(serverName.name)) { + result.push(serverName.name) + } + } + }) + return result + }, + submitForm: function () { + Tea.runActionOn(this.$refs.serverNamesRef.form) } }, watch: { - "periodConfig.period": function (v) { - v = parseInt(v) - if (isNaN(v) || v <= 0) { - v = 1 - } - this.periodConfig.period = v + 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("server-traffic-limit-status-viewer", { + props: ["value"], + data: function () { + let targetTypeName = "流量" + if (this.value != null) { + targetTypeName = this.targetTypeToName(this.value.targetType) + } + + return { + status: this.value, + targetTypeName: targetTypeName + } + }, + methods: { + targetTypeToName: function (targetType) { + switch (targetType) { + case "traffic": + return "流量" + case "request": + return "请求数" + case "websocketConnections": + return "Websocket连接数" + } + return "流量" + } + }, + template: ` + 已达到套餐当日{{targetTypeName}}限制 + 已达到套餐当月{{targetTypeName}}限制 + 已达到套餐总体{{targetTypeName}}限制 +` +}) + +Vue.component("ssl-certs-box", { + props: [ + "v-certs", // 证书列表 + "v-cert", // 单个证书 + "v-protocol", // 协议:https|tls + "v-view-size", // 弹窗尺寸:normal, mini + "v-single-mode", // 单证书模式 + "v-description", // 描述文字 + "v-domains", // 搜索的域名列表或者函数 + "v-user-id" // 用户ID + ], + data: function () { + let certs = this.vCerts + if (certs == null) { + certs = [] + } + if (this.vCert != null) { + certs.push(this.vCert) + } + + let description = this.vDescription + if (description == null || typeof (description) != "string") { + description = "" + } + + return { + certs: certs, + description: description + } + }, + methods: { + certIds: function () { + return this.certs.map(function (v) { + return v.id + }) + }, + // 删除证书 + removeCert: function (index) { + let that = this + teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { + that.certs.$remove(index) + }) + }, + + // 选择证书 + selectCert: function () { + let that = this + let width = "54em" + let height = "32em" + let viewSize = this.vViewSize + if (viewSize == null) { + viewSize = "normal" + } + if (viewSize == "mini") { + width = "35em" + height = "20em" + } + + let searchingDomains = [] + if (this.vDomains != null) { + if (typeof this.vDomains == "function") { + let resultDomains = this.vDomains() + if (resultDomains != null && typeof resultDomains == "object" && (resultDomains instanceof Array)) { + searchingDomains = resultDomains + } + } else if (typeof this.vDomains == "object" && (this.vDomains instanceof Array)) { + searchingDomains = this.vDomains + } + if (searchingDomains.length > 10000) { + searchingDomains = searchingDomains.slice(0, 10000) + } + } + + let selectedCertIds = this.certs.map(function (cert) { + return cert.id + }) + let userId = this.vUserId + if (userId == null) { + userId = 0 + } + + teaweb.popup("/servers/certs/selectPopup?viewSize=" + viewSize + "&searchingDomains=" + window.encodeURIComponent(searchingDomains.join(",")) + "&selectedCertIds=" + selectedCertIds.join(",") + "&userId=" + userId, { + width: width, + height: height, + callback: function (resp) { + if (resp.data.cert != null) { + that.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 上传证书 + uploadCert: function () { + let that = this + let userId = this.vUserId + if (typeof userId != "number" && typeof userId != "string") { + userId = 0 + } + teaweb.popup("/servers/certs/uploadPopup?userId=" + userId, { + height: "28em", + callback: function (resp) { + teaweb.success("上传成功", function () { + if (resp.data.cert != null) { + that.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + }) + } + }) + }, + + // 批量上传 + uploadBatch: function () { + let that = this + let userId = this.vUserId + if (typeof userId != "number" && typeof userId != "string") { + userId = 0 + } + teaweb.popup("/servers/certs/uploadBatchPopup?userId=" + userId, { + callback: function (resp) { + if (resp.data.cert != null) { + that.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 格式化时间 + formatTime: function (timestamp) { + return new Date(timestamp * 1000).format("Y-m-d") + }, + + // 判断是否显示选择|上传按钮 + buttonsVisible: function () { + return this.vSingleMode == null || !this.vSingleMode || this.certs == null || this.certs.length == 0 + } + }, + template: `
+ +
+
+ {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   +
+
+
+
+ 选择或上传证书后HTTPSTLS服务才能生效。 + {{description}} +
+
+
+   + |   +   +   +
+
` +}) + +Vue.component("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("ssl-config-box", { + props: [ + "v-ssl-policy", + "v-protocol", + "v-server-id", + "v-support-http3" + ], + created: function () { + let that = this + setTimeout(function () { + that.sortableCipherSuites() + }, 100) + }, + data: function () { + let policy = this.vSslPolicy + if (policy == null) { + policy = { + id: 0, + isOn: true, + certRefs: [], + certs: [], + clientCARefs: [], + clientCACerts: [], + clientAuthType: 0, + minVersion: "TLS 1.1", + hsts: null, + cipherSuitesIsOn: false, + cipherSuites: [], + http2Enabled: true, + http3Enabled: false, + ocspIsOn: false + } + } else { + if (policy.certRefs == null) { + policy.certRefs = [] + } + if (policy.certs == null) { + policy.certs = [] + } + if (policy.clientCARefs == null) { + policy.clientCARefs = [] + } + if (policy.clientCACerts == null) { + policy.clientCACerts = [] + } + if (policy.cipherSuites == null) { + policy.cipherSuites = [] + } + } + + let hsts = policy.hsts + let hstsMaxAgeString = "31536000" + if (hsts == null) { + hsts = { + isOn: false, + maxAge: 31536000, + includeSubDomains: false, + preload: false, + domains: [] + } + } + if (hsts.maxAge != null) { + hstsMaxAgeString = hsts.maxAge.toString() + } + + return { + policy: policy, + + // hsts + hsts: hsts, + hstsOptionsVisible: false, + hstsDomainAdding: false, + hstsMaxAgeString: hstsMaxAgeString, + addingHstsDomain: "", + hstsDomainEditingIndex: -1, + + // 相关数据 + allVersions: window.SSL_ALL_VERSIONS, + allCipherSuites: window.SSL_ALL_CIPHER_SUITES.$copy(), + modernCipherSuites: window.SSL_MODERN_CIPHER_SUITES, + intermediateCipherSuites: window.SSL_INTERMEDIATE_CIPHER_SUITES, + allClientAuthTypes: window.SSL_ALL_CLIENT_AUTH_TYPES, + cipherSuitesVisible: false, + + // 高级选项 + moreOptionsVisible: false + } + }, + watch: { + hsts: { + deep: true, + handler: function () { + this.policy.hsts = this.hsts + } + } + }, + methods: { + // 删除证书 + removeCert: function (index) { + let that = this + teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { + that.policy.certRefs.$remove(index) + that.policy.certs.$remove(index) + }) + }, + + // 选择证书 + selectCert: function () { + let that = this + let selectedCertIds = [] + if (this.policy != null && this.policy.certs.length > 0) { + this.policy.certs.forEach(function (cert) { + selectedCertIds.push(cert.id.toString()) + }) + } + let serverId = this.vServerId + if (serverId == null) { + serverId = 0 + } + teaweb.popup("/servers/certs/selectPopup?selectedCertIds=" + selectedCertIds + "&serverId=" + serverId, { + width: "50em", + height: "30em", + callback: function (resp) { + if (resp.data.cert != null && resp.data.certRef != null) { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + } + if (resp.data.certs != null && resp.data.certRefs != null) { + that.policy.certRefs.$pushAll(resp.data.certRefs) + that.policy.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 上传证书 + uploadCert: function () { + let that = this + let serverId = this.vServerId + if (typeof serverId != "number" && typeof serverId != "string") { + serverId = 0 + } + teaweb.popup("/servers/certs/uploadPopup?serverId=" + serverId, { + height: "30em", + callback: function (resp) { + teaweb.success("上传成功", function () { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + }) + } + }) + }, + + // 批量上传 + uploadBatch: function () { + let that = this + let serverId = this.vServerId + if (typeof serverId != "number" && typeof serverId != "string") { + serverId = 0 + } + teaweb.popup("/servers/certs/uploadBatchPopup?serverId=" + serverId, { + callback: function (resp) { + if (resp.data.cert != null) { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.policy.certRefs.$pushAll(resp.data.certRefs) + that.policy.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 申请证书 + requestCert: function () { + // 已经在证书中的域名 + let excludeServerNames = [] + if (this.policy != null && this.policy.certs.length > 0) { + this.policy.certs.forEach(function (cert) { + excludeServerNames.$pushAll(cert.dnsNames) + }) + } + + let that = this + teaweb.popup("/servers/server/settings/https/requestCertPopup?serverId=" + this.vServerId + "&excludeServerNames=" + excludeServerNames.join(","), { + callback: function () { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + } + }) + }, + + // 更多选项 + changeOptionsVisible: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + }, + + // 格式化时间 + formatTime: function (timestamp) { + return new Date(timestamp * 1000).format("Y-m-d") + }, + + // 格式化加密套件 + formatCipherSuite: function (cipherSuite) { + return cipherSuite.replace(/(AES|3DES)/, "$1") + }, + + // 添加单个套件 + addCipherSuite: function (cipherSuite) { + if (!this.policy.cipherSuites.$contains(cipherSuite)) { + this.policy.cipherSuites.push(cipherSuite) + } + this.allCipherSuites.$removeValue(cipherSuite) + }, + + // 删除单个套件 + removeCipherSuite: function (cipherSuite) { + let that = this + teaweb.confirm("确定要删除此套件吗?", function () { + that.policy.cipherSuites.$removeValue(cipherSuite) + that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$findAll(function (k, v) { + return !that.policy.cipherSuites.$contains(v) + }) + }) + }, + + // 清除所选套件 + clearCipherSuites: function () { + let that = this + teaweb.confirm("确定要清除所有已选套件吗?", function () { + that.policy.cipherSuites = [] + that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$copy() + }) + }, + + // 批量添加套件 + addBatchCipherSuites: function (suites) { + var that = this + teaweb.confirm("确定要批量添加套件?", function () { + suites.$each(function (k, v) { + if (that.policy.cipherSuites.$contains(v)) { + return + } + that.policy.cipherSuites.push(v) + }) + }) + }, + + /** + * 套件拖动排序 + */ + sortableCipherSuites: function () { + var box = document.querySelector(".cipher-suites-box") + Sortable.create(box, { + draggable: ".label", + handle: ".icon.handle", + onStart: function () { + + }, + onUpdate: function (event) { + + } + }) + }, + + // 显示所有套件 + showAllCipherSuites: function () { + this.cipherSuitesVisible = !this.cipherSuitesVisible + }, + + // 显示HSTS更多选项 + showMoreHSTS: function () { + this.hstsOptionsVisible = !this.hstsOptionsVisible; + if (this.hstsOptionsVisible) { + this.changeHSTSMaxAge() + } + }, + + // 监控HSTS有效期修改 + changeHSTSMaxAge: function () { + var v = parseInt(this.hstsMaxAgeString) + if (isNaN(v) || v < 0) { + this.hsts.maxAge = 0 + this.hsts.days = "-" + return + } + this.hsts.maxAge = v + this.hsts.days = v / 86400 + if (this.hsts.days == 0) { + this.hsts.days = "-" + } + }, + + // 设置HSTS有效期 + setHSTSMaxAge: function (maxAge) { + this.hstsMaxAgeString = maxAge.toString() + this.changeHSTSMaxAge() + }, + + // 添加HSTS域名 + addHstsDomain: function () { + this.hstsDomainAdding = true + this.hstsDomainEditingIndex = -1 + let that = this + setTimeout(function () { + that.$refs.addingHstsDomain.focus() + }, 100) + }, + + // 修改HSTS域名 + editHstsDomain: function (index) { + this.hstsDomainEditingIndex = index + this.addingHstsDomain = this.hsts.domains[index] + this.hstsDomainAdding = true + let that = this + setTimeout(function () { + that.$refs.addingHstsDomain.focus() + }, 100) + }, + + // 确认HSTS域名添加 + confirmAddHstsDomain: function () { + this.addingHstsDomain = this.addingHstsDomain.trim() + if (this.addingHstsDomain.length == 0) { + return; + } + if (this.hstsDomainEditingIndex > -1) { + this.hsts.domains[this.hstsDomainEditingIndex] = this.addingHstsDomain + } else { + this.hsts.domains.push(this.addingHstsDomain) + } + this.cancelHstsDomainAdding() + }, + + // 取消HSTS域名添加 + cancelHstsDomainAdding: function () { + this.hstsDomainAdding = false + this.addingHstsDomain = "" + this.hstsDomainEditingIndex = -1 + }, + + // 删除HSTS域名 + removeHstsDomain: function (index) { + this.cancelHstsDomainAdding() + this.hsts.domains.$remove(index) + }, + + // 选择客户端CA证书 + selectClientCACert: function () { + let that = this + teaweb.popup("/servers/certs/selectPopup?isCA=1", { + width: "50em", + height: "30em", + callback: function (resp) { + if (resp.data.cert != null && resp.data.certRef != null) { + that.policy.clientCARefs.push(resp.data.certRef) + that.policy.clientCACerts.push(resp.data.cert) + } + if (resp.data.certs != null && resp.data.certRefs != null) { + that.policy.clientCARefs.$pushAll(resp.data.certRefs) + that.policy.clientCACerts.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 上传CA证书 + uploadClientCACert: function () { + let that = this + teaweb.popup("/servers/certs/uploadPopup?isCA=1", { + height: "28em", + callback: function (resp) { + teaweb.success("上传成功", function () { + that.policy.clientCARefs.push(resp.data.certRef) + that.policy.clientCACerts.push(resp.data.cert) + }) + } + }) + }, + + // 删除客户端CA证书 + removeClientCACert: function (index) { + let that = this + teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { + that.policy.clientCARefs.$remove(index) + that.policy.clientCACerts.$remove(index) + }) + } + }, + template: `
+

SSL/TLS相关配置

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

点击可选套件添加。

+
+
开启HSTS +
+ + +
+

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

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

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

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

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

+
OCSP Stapling +

选中表示启用OCSP Stapling。

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

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

+
+
` }) @@ -16260,302 +22748,129 @@ Vue.component("traffic-limit-config-box", {
` }) -Vue.component("http-firewall-captcha-options", { - props: ["v-captcha-options"], - mounted: function () { - this.updateSummary() - }, +// UAM模式配置 +Vue.component("uam-config-box", { + props: ["v-uam-config", "v-is-location", "v-is-group"], data: function () { - let options = this.vCaptchaOptions - if (options == null) { - options = { - captchaType: "default", - countLetters: 0, - life: 0, - maxFails: 0, - failBlockTimeout: 0, - failBlockScopeAll: false, - uiIsOn: false, - uiTitle: "", - uiPrompt: "", - uiButtonTitle: "", - uiShowRequestId: true, - uiCss: "", - uiFooter: "", - uiBody: "", - cookieId: "", - lang: "", - geeTestConfig: { - isOn: false, - captchaId: "", - captchaKey: "" - } + let config = this.vUamConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + addToWhiteList: true, + onlyURLPatterns: [], + exceptURLPatterns: [], + minQPSPerIP: 0, + keyLife: 0 } } - if (options.countLetters <= 0) { - options.countLetters = 6 + if (config.onlyURLPatterns == null) { + config.onlyURLPatterns = [] } - - if (options.captchaType == null || options.captchaType.length == 0) { - options.captchaType = "default" + if (config.exceptURLPatterns == null) { + config.exceptURLPatterns = [] } - - return { - options: options, - isEditing: false, - summary: "", - uiBodyWarning: "", - captchaTypes: window.WAF_CAPTCHA_TYPES + config: config, + moreOptionsVisible: false, + minQPSPerIP: config.minQPSPerIP, + keyLife: config.keyLife } }, 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 + minQPSPerIP: function (v) { + let qps = parseInt(v.toString()) + if (isNaN(qps) || qps < 0) { + qps = 0 } - this.options.countLetters = i + this.config.minQPSPerIP = qps }, - "options.life": function (v) { - let i = parseInt(v, 10) - if (isNaN(i)) { - i = 0 + keyLife: function (v) { + let keyLife = parseInt(v) + if (isNaN(keyLife) || keyLife <= 0) { + keyLife = 0 } - this.options.life = i - this.updateSummary() - }, - "options.maxFails": function (v) { - let i = parseInt(v, 10) - if (isNaN(i)) { - i = 0 - } - this.options.maxFails = i - this.updateSummary() - }, - "options.failBlockTimeout": function (v) { - let i = parseInt(v, 10) - if (isNaN(i)) { - i = 0 - } - this.options.failBlockTimeout = i - this.updateSummary() - }, - "options.failBlockScopeAll": function (v) { - this.updateSummary() - }, - "options.captchaType": function (v) { - this.updateSummary() - }, - "options.uiIsOn": function (v) { - this.updateSummary() - }, - "options.uiBody": function (v) { - if (/|\s).+\$\{body}.*<\/form>/s.test(v)) { - this.uiBodyWarning = "页面模板中不能使用
标签包裹\${body}变量,否则将导致验证码表单无法提交。" - } else { - this.uiBodyWarning = "" - } - }, - "options.geeTestConfig.isOn": function (v) { - this.updateSummary() + this.config.keyLife = keyLife } }, methods: { - edit: function () { - this.isEditing = !this.isEditing + showMoreOptions: function () { + this.moreOptionsVisible = !this.moreOptionsVisible }, - updateSummary: function () { - let summaryList = [] - if (this.options.life > 0) { - summaryList.push("有效时间" + this.options.life + "秒") - } - if (this.options.maxFails > 0) { - summaryList.push("最多失败" + this.options.maxFails + "次") - } - if (this.options.failBlockTimeout > 0) { - summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") - } - if (this.options.failBlockScopeAll) { - summaryList.push("尝试全局封禁") - } - - let that = this - let typeDef = this.captchaTypes.$find(function (k, v) { - return v.code == that.options.captchaType - }) - if (typeDef != null) { - summaryList.push("默认验证方式:" + typeDef.name) - } - - if (this.options.captchaType == "default") { - if (this.options.uiIsOn) { - summaryList.push("定制UI") - } - } - - if (this.options.geeTestConfig != null && this.options.geeTestConfig.isOn) { - summaryList.push("已配置极验") - } - - if (summaryList.length == 0) { - this.summary = "默认配置" - } else { - this.summary = summaryList.join(" / ") - } - }, - confirm: function () { - this.isEditing = false + changeConds: function (conds) { + this.config.conds = conds } }, template: `
- - {{summary}} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
默认验证方式 - -

{{captchaDef.description}}

-
有效时间 -
- - -
-

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

-
最多失败次数 -
- - -
-

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

-
失败拦截时间 -
- - -
-

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

-
失败全局封禁 - -

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

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

类似于提交验证

-
显示请求ID - -

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

-
CSS样式 - -
页头提示 - -

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

-
页尾提示 - -

支持HTML。

-
页面模板 - -

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

-
- - - - - - - - - - - - - - - - -
允许用户使用极验 -

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

-
极验-验证ID * - -

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

-
极验-验证Key * - -

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

-
-
-
-` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用5秒盾 + +

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

+
验证有效期 +
+ + +
+

单个客户端验证通过后,在这个有效期内不再重复验证;如果为0则表示系统默认。

+
单IP最低QPS +
+ + 请求数/秒 +
+

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

+
加入IP白名单 + +

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

+
例外URL + +

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

+
限制URL + +

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

+
匹配条件 + +
+
+
` }) Vue.component("user-agent-config-box", { @@ -16738,4113 +23053,42 @@ Vue.component("user-agent-config-box", {
` }) -Vue.component("http-pages-box", { - props: ["v-pages"], +Vue.component("user-selector", { + props: ["v-user-id", "data-url"], data: function () { - let pages = [] - if (this.vPages != null) { - pages = this.vPages + let userId = this.vUserId + if (userId == null) { + userId = 0 + } + + let dataURL = this.dataUrl + if (dataURL == null || dataURL.length == 0) { + dataURL = "/servers/users/options" } return { - pages: pages + users: [], + userId: userId, + dataURL: dataURL } }, methods: { - addPage: function () { - let that = this - teaweb.popup("/servers/server/settings/pages/createPopup", { - height: "26em", - callback: function (resp) { - that.pages.push(resp.data.page) - that.notifyChange() - } - }) - }, - updatePage: function (pageIndex, pageId) { - let that = this - teaweb.popup("/servers/server/settings/pages/updatePopup?pageId=" + pageId, { - height: "26em", - callback: function (resp) { - Vue.set(that.pages, pageIndex, resp.data.page) - that.notifyChange() - } - }) - }, - removePage: function (pageIndex) { - let that = this - teaweb.confirm("确定要移除此页面吗?", function () { - that.pages.$remove(pageIndex) - that.notifyChange() - }) - }, - notifyChange: function () { - let parent = this.$el.parentNode - while (true) { - if (parent == null) { - break - } - if (parent.tagName == "FORM") { - break - } - parent = parent.parentNode - } - if (parent != null) { - setTimeout(function () { - Tea.runActionOn(parent) - }, 100) - } - } - }, - template: `
- - -
- - - - - - - - - - - - - - - - - - - -
响应状态码页面类型新状态码例外URL限制URL操作
- - {{page.status[0]}} - {{page.status}} - - - - -
- {{page.url}} -
- 读取URL -
-
-
- {{page.url}} -
- 跳转URL - {{page.newStatus}} -
-
-
- [HTML内容] -
- {{page.newStatus}} -
-
-
- {{page.newStatus}} - 保持 - -
- {{urlPattern.pattern}} -
- - -
-
- {{urlPattern.pattern}} -
- - -
- 修改   - 删除 -
-
-
- -
-
-
` -}) - -Vue.component("firewall-syn-flood-config-box", { - props: ["v-syn-flood-config"], - data: function () { - let config = this.vSynFloodConfig - if (config == null) { - config = { - isOn: false, - minAttempts: 10, - timeoutSeconds: 600, - ignoreLocal: true - } - } - return { - config: config, - isEditing: false, - minAttempts: config.minAttempts, - timeoutSeconds: config.timeoutSeconds - } - }, - methods: { - edit: function () { - this.isEditing = !this.isEditing - } - }, - watch: { - minAttempts: function (v) { - let count = parseInt(v) - if (isNaN(count)) { - count = 10 - } - if (count < 5) { - count = 5 - } - this.config.minAttempts = count - }, - timeoutSeconds: function (v) { - let seconds = parseInt(v) - if (isNaN(seconds)) { - seconds = 10 - } - if (seconds < 60) { - seconds = 60 - } - this.config.timeoutSeconds = seconds - } - }, - template: `
- - - - 已启用 / 空连接次数:{{config.minAttempts}}次/分钟 / 封禁时长:{{config.timeoutSeconds}}秒 / 忽略局域网访问 - - 未启用 - - - - - - - - - - - - - - - - - - - - -
启用 - -

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

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

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

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

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

-

通过阿里云短信服务发送短信接口;目前仅支持发送验证码

-

通过腾讯云短信服务发送短信接口;目前仅支持发送验证码

-
HTTP接口的URL地址 * - -

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

-
HTTP接口的请求方法 - -

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

-

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

-
签名名称 * -

在阿里云短信服务 “签名管理” 中添加并通过审核后才能使用。

-
模板CODE * - -

在阿里云短信服务 “模板管理” 中添加并通过审核后才能使用。

-
模板中验证码变量名称 * - -

默认为code,不需要带\${}等符号,即表示在模板中使用\${{{ config.aliyunSMSParams.codeVarName }}}代表要发送的验证码。

-
AccessKey ID * - -

在阿里云 -- RAM访问控制 -- AccessKey中可以创建和获取。

-
AccessKey Secret * - -

和表单中的AccessKey ID对应,在阿里云 -- RAM访问控制 -- AccessKey中可以创建和获取。

-
SDK应用ID * - -

在腾讯云 -- 短信 -- 应用管理 -- 应用列表中可以查看。

-
签名内容 * - -

比如“腾讯云”,在腾讯云 -- 短信 -- 签名管理中可以查看。

-
正文模板ID * - -

在腾讯云 -- 短信 -- 正文模板管理中可以查看。

-
密钥SecretId * - -

同SecretKey一同在腾讯云 -- 访问管理 -- API密钥管理中获取。

-
密钥SecretKey * - -

同SecretId一同在腾讯云 -- 访问管理 -- API密钥管理中获取。

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

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

-
SMTP端口 * - -

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

-
用户名 * - -

通常为发件人邮箱地址。

-
密码 * - -

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

-
发件人Email * - -

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

-
发件人名称 - -

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

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

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

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

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

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

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

-
端口 - -

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

-
RequestURI -

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

-
-
-

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

-
-
检测时间间隔 - -

两次检查之间的间隔。

-
自动上/下线IP -
- - -
-

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

-
连续上线次数 - -

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

-
连续下线次数 - -

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

-
允许的状态码 - -

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

-
超时时间 - -

读取检测URL超时时间。

-
连续尝试次数 - -

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

-
每次尝试间隔 - -

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

-
终端信息(User-Agent) - -

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

-
只基础请求 - -

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

-
记录访问日志 - -

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

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

{{message}}

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

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

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

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

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

{{param.description}}

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

{{currentAction.description}}

- -
- -

接收通知的URL。

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

{{item.description}}

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

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

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

{{action.description}}

-
备用IP * - -

每行一个备用IP。

-
URL * - -

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

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

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

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

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

-
-
-
` -}) - -Vue.component("dns-resolvers-config-box", { - props: ["value", "name"], - data: function () { - let resolvers = this.value - if (resolvers == null) { - resolvers = [] - } - - let name = this.name - if (name == null || name.length == 0) { - name = "dnsResolversJSON" - } - - return { - formName: name, - resolvers: resolvers, - - host: "", - - isAdding: false - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.hostRef.focus() - }) - }, - confirm: function () { - let host = this.host.trim() - if (host.length == 0) { - let that = this - setTimeout(function () { - that.$refs.hostRef.focus() - }) - return - } - this.resolvers.push({ - host: host, - port: 0, // TODO - protocol: "" // TODO - }) - this.cancel() - }, - cancel: function () { - this.isAdding = false - this.host = "" - this.port = 0 - this.protocol = "" - }, - remove: function (index) { - this.resolvers.$remove(index) - } - }, - template: `
- -
-
- {{resolver.protocol}}{{resolver.host}}:{{resolver.port}} -   - -
-
- -
-
-
- -
-
- -   -
-
-
- -
- -
-
` -}) - -Vue.component("ad-instance-objects-box", { - props: ["v-objects", "v-user-id"], - mounted: function () { - this.getUserServers(1) - }, - data: function () { - let objects = this.vObjects - if (objects == null) { - objects = [] - } - - let objectCodes = [] - objects.forEach(function (v) { - objectCodes.push(v.code) - }) - - return { - userId: this.vUserId, - objects: objects, - objectCodes: objectCodes, - isAdding: true, - - servers: [], - serversIsLoading: false - } - }, - methods: { - add: function () { - this.isAdding = true - }, - cancel: function () { - this.isAdding = false - }, - remove: function (index) { - let that = this - teaweb.confirm("确定要删除此防护对象吗?", function () { - that.objects.$remove(index) - that.notifyChange() - }) - }, - removeObjectCode: function (objectCode) { - let index = -1 - this.objectCodes.forEach(function (v, k) { - if (objectCode == v) { - index = k - } - }) - if (index >= 0) { - this.objects.$remove(index) - this.notifyChange() - } - }, - getUserServers: function (page) { - if (Tea.Vue == null) { - let that = this - setTimeout(function () { - that.getUserServers(page) - }, 100) - return - } - - let that = this - this.serversIsLoading = true - Tea.Vue.$post(".userServers") - .params({ - userId: this.userId, - page: page, - pageSize: 5 - }) - .success(function (resp) { - that.servers = resp.data.servers - - that.$refs.serverPage.updateMax(resp.data.page.max) - that.serversIsLoading = false - }) - .error(function () { - that.serversIsLoading = false - }) - }, - changeServerPage: function (page) { - this.getUserServers(page) - }, - selectServerObject: function (server) { - if (this.existObjectCode("server:" + server.id)) { - return - } - - this.objects.push({ - "type": "server", - "code": "server:" + server.id, - - "id": server.id, - "name": server.name - }) - this.notifyChange() - }, - notifyChange: function () { - let objectCodes = [] - this.objects.forEach(function (v) { - objectCodes.push(v.code) - }) - this.objectCodes = objectCodes - }, - existObjectCode: function (objectCode) { - let found = false - this.objects.forEach(function (v) { - if (v.code == objectCode) { - found = true - } - }) - return found - } - }, - template: `
- - - -
-
暂时还没有设置任何防护对象。
-
- - - - - -
已选中防护对象 -
- 网站:{{object.name}} -   -
-
-
-
-
- - -
- - - - - - - - - - -
对象类型网站
网站列表 - 加载中... -
暂时还没有可选的网站。
- - - - - - - - - - - -
网站名称操作
{{server.name}} - 选中 - 取消 -
- - -
-
- - -
- -
-
` -}) - -Vue.component("grant-selector", { - props: ["v-grant", "v-node-cluster-id", "v-ns-cluster-id"], - data: function () { - return { - grantId: (this.vGrant == null) ? 0 : this.vGrant.id, - grant: this.vGrant, - nodeClusterId: (this.vNodeClusterId != null) ? this.vNodeClusterId : 0, - nsClusterId: (this.vNsClusterId != null) ? this.vNsClusterId : 0 - } - }, - methods: { - // 选择授权 - select: function () { - let that = this; - teaweb.popup("/clusters/grants/selectPopup?nodeClusterId=" + this.nodeClusterId + "&nsClusterId=" + this.nsClusterId, { - callback: (resp) => { - that.grantId = resp.data.grant.id; - if (that.grantId > 0) { - that.grant = resp.data.grant; - } - that.notifyUpdate() - }, - height: "26em" - }) - }, - - // 创建授权 - create: function () { - let that = this - teaweb.popup("/clusters/grants/createPopup", { - height: "26em", - callback: (resp) => { - that.grantId = resp.data.grant.id; - if (that.grantId > 0) { - that.grant = resp.data.grant; - } - that.notifyUpdate() - } - }) - }, - - // 修改授权 - update: function () { - if (this.grant == null) { - window.location.reload(); - return; - } - let that = this - teaweb.popup("/clusters/grants/updatePopup?grantId=" + this.grant.id, { - height: "26em", - callback: (resp) => { - that.grant = resp.data.grant - that.notifyUpdate() - } - }) - }, - - // 删除已选择授权 - remove: function () { - this.grant = null - this.grantId = 0 - this.notifyUpdate() - }, - notifyUpdate: function () { - this.$emit("change", this.grant) - } - }, - template: `
- -
{{grant.name}}({{grant.methodName}})({{grant.username}})
-
- [选择已有认证]     [添加新认证] -
-
` -}) - window.REQUEST_COND_COMPONENTS = [{"type":"url-extension","name":"文件扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-eq-index","name":"首页","description":"检查URL路径是为\"/\"","component":"http-cond-url-eq-index","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":false},{"type":"url-all","name":"全站","description":"全站所有URL","component":"http-cond-url-all","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":false},{"type":"url-prefix","name":"URL目录前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-prefix","paramsTitle":"URL目录前缀","isRequest":true,"caseInsensitive":true},{"type":"url-eq","name":"URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-regexp","name":"URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致","component":"http-cond-url-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"url-wildcard-match","name":"URL通配符","description":"使用通配符检查URL中的文件路径是否一致","component":"http-cond-url-wildcard-match","paramsTitle":"通配符","isRequest":true,"caseInsensitive":true},{"type":"user-agent-regexp","name":"User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识","component":"http-cond-user-agent-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"params","name":"参数匹配","description":"根据参数值进行匹配","component":"http-cond-params","paramsTitle":"参数配置","isRequest":true,"caseInsensitive":false},{"type":"url-not-extension","name":"排除:URL扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-not-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-not-prefix","name":"排除:URL前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-not-prefix","paramsTitle":"URL前缀","isRequest":true,"caseInsensitive":true},{"type":"url-not-eq","name":"排除:URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-not-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-not-regexp","name":"排除:URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致,如果一致,则不匹配","component":"http-cond-url-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"user-agent-not-regexp","name":"排除:User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识,如果含有,则不匹配","component":"http-cond-user-agent-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"mime-type","name":"内容MimeType","description":"根据服务器返回的内容的MimeType进行过滤。注意:当用于缓存条件时,此条件需要结合别的请求条件使用。","component":"http-cond-mime-type","paramsTitle":"MimeType列表","isRequest":false,"caseInsensitive":false}]; window.REQUEST_COND_OPERATORS = [{"description":"判断是否正则表达式匹配","name":"正则表达式匹配","op":"regexp"},{"description":"判断是否正则表达式不匹配","name":"正则表达式不匹配","op":"not regexp"},{"description":"判断是否和指定的通配符匹配","name":"通配符匹配","op":"wildcard match"},{"description":"判断是否和指定的通配符不匹配","name":"通配符不匹配","op":"wildcard not match"},{"description":"使用字符串对比参数值是否相等于某个值","name":"字符串等于","op":"eq"},{"description":"参数值包含某个前缀","name":"字符串前缀","op":"prefix"},{"description":"参数值包含某个后缀","name":"字符串后缀","op":"suffix"},{"description":"参数值包含另外一个字符串","name":"字符串包含","op":"contains"},{"description":"参数值不包含另外一个字符串","name":"字符串不包含","op":"not contains"},{"description":"使用字符串对比参数值是否不相等于某个值","name":"字符串不等于","op":"not"},{"description":"判断参数值在某个列表中","name":"在列表中","op":"in"},{"description":"判断参数值不在某个列表中","name":"不在列表中","op":"not in"},{"description":"判断小写的扩展名(不带点)在某个列表中","name":"扩展名","op":"file ext"},{"description":"判断MimeType在某个列表中,支持类似于image/*的语法","name":"MimeType","op":"mime type"},{"description":"判断版本号在某个范围内,格式为version1,version2","name":"版本号范围","op":"version range"},{"description":"将参数转换为整数数字后进行对比","name":"整数等于","op":"eq int"},{"description":"将参数转换为可以有小数的浮点数字进行对比","name":"浮点数等于","op":"eq float"},{"description":"将参数转换为数字进行对比","name":"数字大于","op":"gt"},{"description":"将参数转换为数字进行对比","name":"数字大于等于","op":"gte"},{"description":"将参数转换为数字进行对比","name":"数字小于","op":"lt"},{"description":"将参数转换为数字进行对比","name":"数字小于等于","op":"lte"},{"description":"对整数参数值取模,除数为10,对比值为余数","name":"整数取模10","op":"mod 10"},{"description":"对整数参数值取模,除数为100,对比值为余数","name":"整数取模100","op":"mod 100"},{"description":"对整数参数值取模,对比值格式为:除数,余数,比如10,1","name":"整数取模","op":"mod"},{"description":"将参数转换为IP进行对比","name":"IP等于","op":"eq ip"},{"description":"将参数转换为IP进行对比","name":"IP大于","op":"gt ip"},{"description":"将参数转换为IP进行对比","name":"IP大于等于","op":"gte ip"},{"description":"将参数转换为IP进行对比","name":"IP小于","op":"lt ip"},{"description":"将参数转换为IP进行对比","name":"IP小于等于","op":"lte ip"},{"description":"IP在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e,或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e","name":"IP范围","op":"ip range"},{"description":"对IP参数值取模,除数为10,对比值为余数","name":"IP取模10","op":"ip mod 10"},{"description":"对IP参数值取模,除数为100,对比值为余数","name":"IP取模100","op":"ip mod 100"},{"description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1","name":"IP取模","op":"ip mod"}]; @@ -23118,7 +23118,7 @@ window.IP_ADDR_THRESHOLD_ITEMS = [{"code":"nodeAvgRequests","description":"当 window.IP_ADDR_THRESHOLD_ACTIONS = [{"code":"up","description":"上线当前IP。","name":"上线"},{"code":"down","description":"下线当前IP。","name":"下线"},{"code":"notify","description":"发送已达到阈值通知。","name":"通知"},{"code":"switch","description":"在DNS中记录中将IP切换到指定的备用IP。","name":"切换"},{"code":"webHook","description":"调用外部的WebHook。","name":"WebHook"}]; -window.WAF_RULE_CHECKPOINTS = [{"description":"通用报头比如Cache-Control、Accept之类的长度限制,防止缓冲区溢出攻击。","name":"通用请求报头长度限制","prefix":"requestGeneralHeaderLength"},{"description":"通用报头比如Cache-Control、Date之类的长度限制,防止缓冲区溢出攻击。","name":"通用响应报头长度限制","prefix":"responseGeneralHeaderLength"},{"description":"试图通过分析X-Forwarded-For等报头获取的客户端地址,比如192.168.1.100,存在伪造的可能。","name":"客户端地址(IP)","prefix":"remoteAddr"},{"description":"直接连接的客户端地址,比如192.168.1.100。","name":"客户端源地址(IP)","prefix":"rawRemoteAddr"},{"description":"直接连接的客户端地址端口。","name":"客户端端口","prefix":"remotePort"},{"description":"通过BasicAuth登录的客户端用户名。","name":"客户端用户名","prefix":"remoteUser"},{"description":"包含URL参数的请求URI,类似于 /hello/world?lang=go,不包含域名部分。","name":"请求URI","prefix":"requestURI"},{"description":"不包含URL参数的请求路径,类似于 /hello/world,不包含域名部分。","name":"请求路径","prefix":"requestPath"},{"description":"完整的请求URL,包含协议、域名、请求路径、参数等,类似于 https://example.com/hello?name=lily 。","name":"请求完整URL","prefix":"requestURL"},{"description":"请求报头中的Content-Length。","name":"请求内容长度","prefix":"requestLength"},{"description":"通常在POST或者PUT等操作时会附带请求体,最大限制32M。","name":"请求体内容","prefix":"requestBody"},{"description":"${requestURI}和${requestBody}组合。","name":"请求URI和请求体组合","prefix":"requestAll"},{"description":"获取POST或者其他方法发送的表单参数,最大请求体限制32M。","name":"请求表单参数","prefix":"requestForm"},{"description":"获取POST上传的文件信息,最大请求体限制32M。","name":"上传文件","prefix":"requestUpload"},{"description":"获取POST或者其他方法发送的JSON,最大请求体限制32M,使用点(.)符号表示多级数据。","name":"请求JSON参数","prefix":"requestJSON"},{"description":"比如GET、POST。","name":"请求方法","prefix":"requestMethod"},{"description":"比如http或https。","name":"请求协议","prefix":"scheme"},{"description":"比如HTTP/1.1。","name":"HTTP协议版本","prefix":"proto"},{"description":"比如example.com。","name":"主机名","prefix":"host"},{"description":"当前网站服务CNAME,比如38b48e4f.example.com。","name":"CNAME","prefix":"cname"},{"description":"是否为CNAME,值为1(是)或0(否)。","name":"是否为CNAME","prefix":"isCNAME"},{"description":"请求报头中的Referer和Origin值。","name":"请求来源","prefix":"refererOrigin"},{"description":"请求报头中的Referer值。","name":"请求来源Referer","prefix":"referer"},{"description":"比如Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103。","name":"客户端信息","prefix":"userAgent"},{"description":"请求报头的Content-Type。","name":"内容类型","prefix":"contentType"},{"description":"比如sid=IxZVPFhE\u0026city=beijing\u0026uid=18237。","name":"所有cookie组合字符串","prefix":"cookies"},{"description":"单个cookie值。","name":"单个cookie值","prefix":"cookie"},{"description":"比如name=lu\u0026age=20。","name":"所有URL参数组合","prefix":"args"},{"description":"单个URL参数值。","name":"单个URL参数值","prefix":"arg"},{"description":"使用换行符(\\n)隔开的报头内容字符串,每行均为\"NAME: VALUE格式\"。","name":"所有请求报头内容","prefix":"headers"},{"description":"使用换行符(\\n)隔开的报头名称字符串,每行一个名称。","name":"所有请求报头名称","prefix":"headerNames"},{"description":"单个报头值。","name":"单个请求报头值","prefix":"header"},{"description":"最长的请求报头的长度。","name":"请求报头最大长度","prefix":"headerMaxLength"},{"description":"当前客户端所处国家/地区名称。","name":"国家/地区名称","prefix":"geoCountryName"},{"description":"当前客户端所处中国省份名称。","name":"省份名称","prefix":"geoProvinceName"},{"description":"当前客户端所处中国城市名称。","name":"城市名称","prefix":"geoCityName"},{"description":"当前客户端所处ISP名称。","name":"ISP名称","prefix":"ispName"},{"description":"对统计对象进行统计。","name":"CC统计","prefix":"cc2"},{"description":"对统计对象进行统计。","name":"防盗链","prefix":"refererBlock"},{"description":"统计某段时间段内的请求信息(不推荐再使用,请使用新的CC2统计代替)。","name":"CC统计(旧)","prefix":"cc"},{"description":"响应状态码,比如200、404、500。","name":"响应状态码","prefix":"status"},{"description":"响应报头值。","name":"响应报头","prefix":"responseHeader"},{"description":"响应内容字符串。","name":"响应内容","prefix":"responseBody"},{"description":"响应内容长度,通过响应的报头Content-Length获取。","name":"响应内容长度","prefix":"bytesSent"}]; +window.WAF_RULE_CHECKPOINTS = [{"description":"通用报头比如Cache-Control、Accept之类的长度限制,防止缓冲区溢出攻击。","name":"通用请求报头长度限制","prefix":"requestGeneralHeaderLength"},{"description":"通用报头比如Cache-Control、Date之类的长度限制,防止缓冲区溢出攻击。","name":"通用响应报头长度限制","prefix":"responseGeneralHeaderLength"},{"description":"试图通过分析X-Forwarded-For等报头获取的客户端地址,比如192.168.1.100,存在伪造的可能。","name":"客户端地址(IP)","prefix":"remoteAddr"},{"description":"直接连接的客户端地址,比如192.168.1.100。","name":"客户端源地址(IP)","prefix":"rawRemoteAddr"},{"description":"直接连接的客户端地址端口。","name":"客户端端口","prefix":"remotePort"},{"description":"通过BasicAuth登录的客户端用户名。","name":"客户端用户名","prefix":"remoteUser"},{"description":"包含URL参数的请求URI,类似于 /hello/world?lang=go,不包含域名部分。","name":"请求URI","prefix":"requestURI"},{"description":"不包含URL参数的请求路径,类似于 /hello/world,不包含域名部分。","name":"请求路径","prefix":"requestPath"},{"description":"完整的请求URL,包含协议、域名、请求路径、参数等,类似于 https://example.com/hello?name=lily 。","name":"请求完整URL","prefix":"requestURL"},{"description":"请求报头中的Content-Length。","name":"请求内容长度","prefix":"requestLength"},{"description":"通常在POST或者PUT等操作时会附带请求体,最大限制32M。","name":"请求体内容","prefix":"requestBody"},{"description":"${requestURI}和${requestBody}组合。","name":"请求URI和请求体组合","prefix":"requestAll"},{"description":"获取POST或者其他方法发送的表单参数,最大请求体限制32M。","name":"请求表单参数","prefix":"requestForm"},{"description":"获取POST上传的文件信息,最大请求体限制32M。","name":"上传文件","prefix":"requestUpload"},{"description":"获取POST或者其他方法发送的JSON,最大请求体限制32M,使用点(.)符号表示多级数据。","name":"请求JSON参数","prefix":"requestJSON"},{"description":"比如GET、POST。","name":"请求方法","prefix":"requestMethod"},{"description":"比如http或https。","name":"请求协议","prefix":"scheme"},{"description":"比如HTTP/1.1。","name":"HTTP协议版本","prefix":"proto"},{"description":"比如example.com。","name":"主机名","prefix":"host"},{"description":"当前网站服务CNAME,比如38b48e4f.example.com。","name":"CNAME","prefix":"cname"},{"description":"是否为CNAME,值为1(是)或0(否)。","name":"是否为CNAME","prefix":"isCNAME"},{"description":"请求报头中的Referer和Origin值。","name":"请求来源","prefix":"refererOrigin"},{"description":"请求报头中的Referer值。","name":"请求来源Referer","prefix":"referer"},{"description":"比如Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103。","name":"客户端信息","prefix":"userAgent"},{"description":"请求报头的Content-Type。","name":"内容类型","prefix":"contentType"},{"description":"比如sid=IxZVPFhE\u0026city=beijing\u0026uid=18237。","name":"所有cookie组合字符串","prefix":"cookies"},{"description":"单个cookie值。","name":"单个cookie值","prefix":"cookie"},{"description":"比如name=lu\u0026age=20。","name":"所有URL参数组合","prefix":"args"},{"description":"单个URL参数值。","name":"单个URL参数值","prefix":"arg"},{"description":"使用换行符(\\n)隔开的报头内容字符串,每行均为\"NAME: VALUE格式\"。","name":"所有请求报头内容","prefix":"headers"},{"description":"使用换行符(\\n)隔开的报头名称字符串,每行一个名称。","name":"所有请求报头名称","prefix":"headerNames"},{"description":"单个报头值。","name":"单个请求报头值","prefix":"header"},{"description":"最长的请求报头的长度。","name":"请求报头最大长度","prefix":"headerMaxLength"},{"description":"当前客户端所处国家/地区名称。","name":"国家/地区名称","prefix":"geoCountryName"},{"description":"当前客户端所处中国省份名称。","name":"省份名称","prefix":"geoProvinceName"},{"description":"当前客户端所处中国城市名称。","name":"城市名称","prefix":"geoCityName"},{"description":"当前客户端所处ISP名称。","name":"ISP名称","prefix":"ispName"},{"description":"对统计对象进行统计。","name":"CC统计","prefix":"cc2"},{"description":"对统计对象进行统计。","name":"防盗链","prefix":"refererBlock"},{"description":"响应状态码,比如200、404、500。","name":"响应状态码","prefix":"status"},{"description":"响应报头值。","name":"响应报头","prefix":"responseHeader"},{"description":"响应内容字符串。","name":"响应内容","prefix":"responseBody"},{"description":"响应内容长度,通过响应的报头Content-Length获取。","name":"响应内容长度","prefix":"bytesSent"}]; window.WAF_RULE_OPERATORS = [{"name":"正则匹配","code":"match","description":"使用正则表达式匹配,在头部使用(?i)表示不区分大小写,\u003ca href=\"https://goedge.cn/docs/Appendix/Regexp/Index.md\" target=\"_blank\"\u003e正则表达式语法 \u0026raquo;\u003c/a\u003e。","caseInsensitive":"yes","dataType":"regexp"},{"name":"正则不匹配","code":"not match","description":"使用正则表达式不匹配,在头部使用(?i)表示不区分大小写,\u003ca href=\"https://goedge.cn/docs/Appendix/Regexp/Index.md\" target=\"_blank\"\u003e正则表达式语法 \u0026raquo;\u003c/a\u003e。","caseInsensitive":"yes","dataType":"regexp"},{"name":"通配符匹配","code":"wildcard match","description":"判断是否和指定的通配符匹配,可以在对比值中使用星号通配符(*)表示任意字符。","caseInsensitive":"yes","dataType":"wildcard"},{"name":"通配符不匹配","code":"wildcard not match","description":"判断是否和指定的通配符不匹配,可以在对比值中使用星号通配符(*)表示任意字符。","caseInsensitive":"yes","dataType":"wildcard"},{"name":"字符串等于","code":"eq string","description":"使用字符串对比等于。","caseInsensitive":"no","dataType":"string"},{"name":"字符串不等于","code":"neq string","description":"使用字符串对比不等于。","caseInsensitive":"no","dataType":"string"},{"name":"包含字符串","code":"contains","description":"包含某个字符串,比如Hello World包含了World。","caseInsensitive":"no","dataType":"string"},{"name":"不包含字符串","code":"not contains","description":"不包含某个字符串,比如Hello字符串中不包含Hi。","caseInsensitive":"no","dataType":"string"},{"name":"包含任一字符串","code":"contains any","description":"包含字符串列表中的任意一个,比如/hello/world包含/hello和/hi中的/hello,对比值中每行一个字符串。","caseInsensitive":"no","dataType":"strings"},{"name":"包含所有字符串","code":"contains all","description":"包含字符串列表中的所有字符串,比如/hello/world必须包含/hello和/world,对比值中每行一个字符串。","caseInsensitive":"no","dataType":"strings"},{"name":"包含前缀","code":"prefix","description":"包含字符串前缀部分,比如/hello前缀会匹配/hello, /hello/world等。","caseInsensitive":"no","dataType":"string"},{"name":"包含后缀","code":"suffix","description":"包含字符串后缀部分,比如/hello后缀会匹配/hello, /hi/hello等。","caseInsensitive":"no","dataType":"string"},{"name":"包含任一单词","code":"contains any word","description":"包含某个独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"包含所有单词","code":"contains all words","description":"包含所有的独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"不包含任一单词","code":"not contains any word","description":"不包含某个独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"包含SQL注入","code":"contains sql injection","description":"检测字符串内容是否包含SQL注入。","caseInsensitive":"none","dataType":"none"},{"name":"包含SQL注入-严格模式","code":"contains sql injection strictly","description":"更加严格地检测字符串内容是否包含SQL注入,相对于非严格模式,有一定的误报几率。","caseInsensitive":"none","dataType":"none"},{"name":"包含XSS注入","code":"contains xss","description":"检测字符串内容是否包含XSS注入。","caseInsensitive":"none","dataType":"none"},{"name":"包含XSS注入-严格模式","code":"contains xss strictly","description":"更加严格地检测字符串内容是否包含XSS注入,相对于非严格模式,此时xml、audio、video等标签也会被匹配。","caseInsensitive":"none","dataType":"none"},{"name":"包含二进制数据","code":"contains binary","description":"包含一组二进制数据。","caseInsensitive":"no","dataType":"string"},{"name":"不包含二进制数据","code":"not contains binary","description":"不包含一组二进制数据。","caseInsensitive":"no","dataType":"string"},{"name":"数值大于","code":"gt","description":"使用数值对比大于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值大于等于","code":"gte","description":"使用数值对比大于等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值小于","code":"lt","description":"使用数值对比小于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值小于等于","code":"lte","description":"使用数值对比小于等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值等于","code":"eq","description":"使用数值对比等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值不等于","code":"neq","description":"使用数值对比不等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"包含索引","code":"has key","description":"对于一组数据拥有某个键值或者索引。","caseInsensitive":"no","dataType":"string|number"},{"name":"版本号大于","code":"version gt","description":"对比版本号大于。","caseInsensitive":"none","dataType":"version"},{"name":"版本号小于","code":"version lt","description":"对比版本号小于。","caseInsensitive":"none","dataType":"version"},{"name":"版本号范围","code":"version range","description":"判断版本号在某个范围内,格式为 起始version1,结束version2。","caseInsensitive":"none","dataType":"versionRange"},{"name":"IP等于","code":"eq ip","description":"将参数转换为IP进行对比,只能对比单个IP。","caseInsensitive":"none","dataType":"ip"},{"name":"在一组IP中","code":"in ip list","description":"判断参数IP在一组IP内,对比值中每行一个IP。","caseInsensitive":"none","dataType":"ips"},{"name":"IP大于","code":"gt ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP大于等于","code":"gte ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP小于","code":"lt ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP小于等于","code":"lte ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP范围","code":"ip range","description":"IP在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e;或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e;或者单个IP。可以填写多行,每行一个IP范围。","caseInsensitive":"none","dataType":"ips"},{"name":"不在IP范围","code":"not ip range","description":"IP不在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e;或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e;或者单个IP。可以填写多行,每行一个IP范围。","caseInsensitive":"none","dataType":"ips"},{"name":"IP取模10","code":"ip mod 10","description":"对IP参数值取模,除数为10,对比值为余数。","caseInsensitive":"none","dataType":"number"},{"name":"IP取模100","code":"ip mod 100","description":"对IP参数值取模,除数为100,对比值为余数。","caseInsensitive":"none","dataType":"number"},{"name":"IP取模","code":"ip mod","description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1。","caseInsensitive":"none","dataType":"number"}]; diff --git a/EdgeAdmin/web/public/js/components.src.js b/EdgeAdmin/web/public/js/components.src.js index d0e0799..99daa3f 100644 --- a/EdgeAdmin/web/public/js/components.src.js +++ b/EdgeAdmin/web/public/js/components.src.js @@ -1,3 +1,4691 @@ +// 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: `
+ +
` +}) + +Vue.component("ad-instance-objects-box", { + props: ["v-objects", "v-user-id"], + mounted: function () { + this.getUserServers(1) + }, + data: function () { + let objects = this.vObjects + if (objects == null) { + objects = [] + } + + let objectCodes = [] + objects.forEach(function (v) { + objectCodes.push(v.code) + }) + + return { + userId: this.vUserId, + objects: objects, + objectCodes: objectCodes, + isAdding: true, + + servers: [], + serversIsLoading: false + } + }, + methods: { + add: function () { + this.isAdding = true + }, + cancel: function () { + this.isAdding = false + }, + remove: function (index) { + let that = this + teaweb.confirm("确定要删除此防护对象吗?", function () { + that.objects.$remove(index) + that.notifyChange() + }) + }, + removeObjectCode: function (objectCode) { + let index = -1 + this.objectCodes.forEach(function (v, k) { + if (objectCode == v) { + index = k + } + }) + if (index >= 0) { + this.objects.$remove(index) + this.notifyChange() + } + }, + getUserServers: function (page) { + if (Tea.Vue == null) { + let that = this + setTimeout(function () { + that.getUserServers(page) + }, 100) + return + } + + let that = this + this.serversIsLoading = true + Tea.Vue.$post(".userServers") + .params({ + userId: this.userId, + page: page, + pageSize: 5 + }) + .success(function (resp) { + that.servers = resp.data.servers + + that.$refs.serverPage.updateMax(resp.data.page.max) + that.serversIsLoading = false + }) + .error(function () { + that.serversIsLoading = false + }) + }, + changeServerPage: function (page) { + this.getUserServers(page) + }, + selectServerObject: function (server) { + if (this.existObjectCode("server:" + server.id)) { + return + } + + this.objects.push({ + "type": "server", + "code": "server:" + server.id, + + "id": server.id, + "name": server.name + }) + this.notifyChange() + }, + notifyChange: function () { + let objectCodes = [] + this.objects.forEach(function (v) { + objectCodes.push(v.code) + }) + this.objectCodes = objectCodes + }, + existObjectCode: function (objectCode) { + let found = false + this.objects.forEach(function (v) { + if (v.code == objectCode) { + found = true + } + }) + return found + } + }, + template: `
+ + + +
+
暂时还没有设置任何防护对象。
+
+ + + + + +
已选中防护对象 +
+ 网站:{{object.name}} +   +
+
+
+
+
+ + +
+ + + + + + + + + + +
对象类型网站
网站列表 + 加载中... +
暂时还没有可选的网站。
+ + + + + + + + + + + +
网站名称操作
{{server.name}} + 选中 + 取消 +
+ + +
+
+ + +
+ +
+
` +}) + +Vue.component("api-node-addresses-box", { + props: ["v-addrs", "v-name"], + data: function () { + let addrs = this.vAddrs + if (addrs == null) { + addrs = [] + } + return { + addrs: addrs + } + }, + methods: { + // 添加IP地址 + addAddr: function () { + let that = this; + teaweb.popup("/settings/api/node/createAddrPopup", { + height: "16em", + callback: function (resp) { + that.addrs.push(resp.data.addr); + } + }) + }, + + // 修改地址 + updateAddr: function (index, addr) { + let that = this; + window.UPDATING_ADDR = addr + teaweb.popup("/settings/api/node/updateAddrPopup?addressId=", { + callback: function (resp) { + Vue.set(that.addrs, index, resp.data.addr); + } + }) + }, + + // 删除IP地址 + removeAddr: function (index) { + this.addrs.$remove(index); + } + }, + template: `
+ +
+
+
+ {{addr.protocol}}://{{addr.host.quoteIP()}}:{{addr.portRange}} + + +
+
+
+
+
+ +
+
` +}) + +Vue.component("api-node-selector", { + props: [], + data: function () { + return {} + }, + template: `
+ 暂未实现 +
` +}) + +// 单个集群选择 +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("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("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-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-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("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("node-ddos-protection-config-box", { + props: ["v-ddos-protection-config", "v-default-configs", "v-is-node", "v-cluster-is-on"], + data: function () { + let config = this.vDdosProtectionConfig + if (config == null) { + config = { + tcp: { + isPrior: false, + isOn: false, + maxConnections: 0, + maxConnectionsPerIP: 0, + newConnectionsRate: 0, + newConnectionsRateBlockTimeout: 0, + newConnectionsSecondlyRate: 0, + newConnectionSecondlyRateBlockTimeout: 0, + allowIPList: [], + ports: [] + } + } + } + + // initialize + if (config.tcp == null) { + config.tcp = { + isPrior: false, + isOn: false, + maxConnections: 0, + maxConnectionsPerIP: 0, + newConnectionsRate: 0, + newConnectionsRateBlockTimeout: 0, + newConnectionsSecondlyRate: 0, + newConnectionSecondlyRateBlockTimeout: 0, + allowIPList: [], + ports: [] + } + } + + + return { + config: config, + defaultConfigs: this.vDefaultConfigs, + isNode: this.vIsNode, + + isAddingPort: false + } + }, + methods: { + changeTCPPorts: function (ports) { + this.config.tcp.ports = ports + }, + changeTCPAllowIPList: function (ipList) { + this.config.tcp.allowIPList = ipList + } + }, + template: `
+ + +

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

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

TCP设置

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

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

+
单IP TCP最大连接数 + +

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

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

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

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

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

+
TCP端口列表 + +

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

+
IP白名单 + +

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

+
+
+
` +}) + +Vue.component("bandwidth-size-capacity-box", { + props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"], + data: function () { + let v = this.vValue + if (v == null) { + v = { + count: this.vCount, + unit: this.vUnit + } + } + if (v.unit == null || v.unit.length == 0) { + v.unit = "mb" + } + + if (typeof (v["count"]) != "number") { + v["count"] = -1 + } + + let vSize = this.size + if (vSize == null) { + vSize = 6 + } + + let vMaxlength = this.maxlength + if (vMaxlength == null) { + vMaxlength = 10 + } + + let supportedUnits = this.vSupportedUnits + if (supportedUnits == null) { + supportedUnits = [] + } + + return { + capacity: v, + countString: (v.count >= 0) ? v.count.toString() : "", + vSize: vSize, + vMaxlength: vMaxlength, + supportedUnits: supportedUnits + } + }, + watch: { + "countString": function (newValue) { + let value = newValue.trim() + if (value.length == 0) { + this.capacity.count = -1 + this.change() + return + } + let count = parseInt(value) + if (!isNaN(count)) { + this.capacity.count = count + } + this.change() + } + }, + methods: { + change: function () { + this.$emit("change", this.capacity) + } + }, + template: `
+ +
+ +
+
+ +
+
` +}) + +Vue.component("bandwidth-size-capacity-view", { + props: ["v-value"], + data: function () { + let capacity = this.vValue + if (capacity != null && capacity.count > 0 && typeof capacity.unit === "string") { + capacity.unit = capacity.unit[0].toUpperCase() + capacity.unit.substring(1) + "ps" + } + return { + capacity: capacity + } + }, + template: ` + {{capacity.count}}{{capacity.unit}} +` +}) + +Vue.component("bits-var", { + props: ["v-bits"], + data: function () { + let bits = this.vBits + if (typeof bits != "number") { + bits = 0 + } + let format = teaweb.splitFormat(teaweb.formatBits(bits)) + return { + format: format + } + }, + template:` + {{format[0]}}{{format[1]}} +` +}) + +Vue.component("bytes-var", { + props: ["v-bytes"], + data: function () { + let bytes = this.vBytes + if (typeof bytes != "number") { + bytes = 0 + } + let format = teaweb.splitFormat(teaweb.formatBytes(bytes)) + return { + format: format + } + }, + template:` + {{format[0]}}{{format[1]}} +` +}) + +Vue.component("chart-columns-grid", { + props: [], + mounted: function () { + this.columns = this.calculateColumns() + + let that = this + window.addEventListener("resize", function () { + that.columns = that.calculateColumns() + }) + }, + updated: function () { + let totalElements = this.$el.getElementsByClassName("column").length + if (totalElements == this.totalElements) { + return + } + this.totalElements = totalElements + this.calculateColumns() + }, + data: function () { + return { + columns: "four", + totalElements: 0 + } + }, + methods: { + calculateColumns: function () { + let w = window.innerWidth + let columns = Math.floor(w / 500) + if (columns == 0) { + columns = 1 + } + + let columnElements = this.$el.getElementsByClassName("column") + if (columnElements.length == 0) { + return "one" + } + let maxColumns = columnElements.length + if (columns > maxColumns) { + columns = maxColumns + } + + // 添加右侧边框 + for (let index = 0; index < columnElements.length; index++) { + let el = columnElements[index] + el.className = el.className.replace("with-border", "") + if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) { + el.className += " with-border" + } + } + + switch (columns) { + case 1: + return "one" + case 2: + return "two" + case 3: + return "three" + case 4: + return "four" + case 5: + return "five" + case 6: + return "six" + case 7: + return "seven" + case 8: + return "eight" + case 9: + return "nine" + case 10: + return "ten" + default: + return "ten" + } + } + }, + template: `
+ +
` +}) + +let checkboxId = 0 +Vue.component("checkbox", { + props: ["name", "value", "v-value", "id", "checked"], + data: function () { + checkboxId++ + let elementId = this.id + if (elementId == null) { + elementId = "checkbox" + checkboxId + } + + let elementValue = this.vValue + if (elementValue == null) { + elementValue = "1" + } + + let checkedValue = this.value + if (checkedValue == null && this.checked == "checked") { + checkedValue = elementValue + } + + return { + elementId: elementId, + elementValue: elementValue, + newValue: checkedValue + } + }, + methods: { + change: function () { + this.$emit("input", this.newValue) + }, + check: function () { + this.newValue = this.elementValue + }, + uncheck: function () { + this.newValue = "" + }, + isChecked: function () { + return (typeof (this.newValue) == "boolean" && this.newValue) || this.newValue == this.elementValue + } + }, + watch: { + value: function (v) { + if (typeof v == "boolean") { + this.newValue = v + } + } + }, + template: `
+ + +
` +}) + +Vue.component("columns-grid", { + props: [], + mounted: function () { + this.columns = this.calculateColumns() + + let that = this + window.addEventListener("resize", function () { + that.columns = that.calculateColumns() + }) + }, + data: function () { + return { + columns: "four" + } + }, + methods: { + calculateColumns: function () { + let w = window.innerWidth + let columns = Math.floor(w / 250) + if (columns == 0) { + columns = 1 + } + + let columnElements = this.$el.getElementsByClassName("column") + if (columnElements.length == 0) { + return + } + let maxColumns = columnElements.length + if (columns > maxColumns) { + columns = maxColumns + } + + // 添加右侧边框 + for (let index = 0; index < columnElements.length; index++) { + let el = columnElements[index] + el.className = el.className.replace("with-border", "") + if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) { + el.className += " with-border" + } + } + + switch (columns) { + case 1: + return "one" + case 2: + return "two" + case 3: + return "three" + case 4: + return "four" + case 5: + return "five" + case 6: + return "six" + case 7: + return "seven" + case 8: + return "eight" + case 9: + return "nine" + case 10: + return "ten" + default: + return "ten" + } + } + }, + template: `
+ +
` +}) + +Vue.component("combo-box", { + // data-url 和 data-key 成对出现 + props: [ + "name", "title", "placeholder", "size", "v-items", "v-value", + "data-url", // 数据源URL + "data-key", // 数据源中数据的键名 + "data-search", // 是否启用动态搜索,如果值为on或true,则表示启用 + "width" + ], + mounted: function () { + if (this.dataURL.length > 0) { + this.search("") + } + + // 设定菜单宽度 + let searchBox = this.$refs.searchBox + if (searchBox != null) { + let inputWidth = searchBox.offsetWidth + if (inputWidth != null && inputWidth > 0) { + this.$refs.menu.style.width = inputWidth + "px" + } else if (this.styleWidth.length > 0) { + this.$refs.menu.style.width = this.styleWidth + } + } + }, + data: function () { + let items = this.vItems + if (items == null || !(items instanceof Array)) { + items = [] + } + items = this.formatItems(items) + + // 当前选中项 + let selectedItem = null + if (this.vValue != null) { + let that = this + items.forEach(function (v) { + if (v.value == that.vValue) { + selectedItem = v + } + }) + } + + let width = this.width + if (width == null || width.length == 0) { + width = "11em" + } else { + if (/\d+$/.test(width)) { + width += "em" + } + } + + // data url + let dataURL = "" + if (typeof this.dataUrl == "string" && this.dataUrl.length > 0) { + dataURL = this.dataUrl + } + + return { + allItems: items, // 原始的所有的items + items: items.$copy(), // 候选的items + selectedItem: selectedItem, // 选中的item + keyword: "", + visible: false, + hideTimer: null, + hoverIndex: 0, + styleWidth: width, + + isInitial: true, + dataURL: dataURL, + urlRequestId: 0 // 记录URL请求ID,防止并行冲突 + } + }, + methods: { + search: function (keyword) { + // 从URL中获取选项数据 + let dataUrl = this.dataURL + let dataKey = this.dataKey + let that = this + + let requestId = Math.random() + this.urlRequestId = requestId + + Tea.action(dataUrl) + .params({ + keyword: (keyword == null) ? "" : keyword + }) + .post() + .success(function (resp) { + if (requestId != that.urlRequestId) { + return + } + + if (resp.data != null) { + if (typeof (resp.data[dataKey]) == "object") { + let items = that.formatItems(resp.data[dataKey]) + that.allItems = items + that.items = items.$copy() + + if (that.isInitial) { + that.isInitial = false + if (that.vValue != null) { + items.forEach(function (v) { + if (v.value == that.vValue) { + that.selectedItem = v + } + }) + } + } + } + } + }) + }, + formatItems: function (items) { + items.forEach(function (v) { + if (v.value == null) { + v.value = v.id + } + }) + return items + }, + reset: function () { + this.selectedItem = null + this.change() + this.hoverIndex = 0 + + let that = this + setTimeout(function () { + if (that.$refs.searchBox) { + that.$refs.searchBox.focus() + } + }) + }, + clear: function () { + this.selectedItem = null + this.change() + this.hoverIndex = 0 + }, + changeKeyword: function () { + let shouldSearch = this.dataURL.length > 0 && (this.dataSearch == "on" || this.dataSearch == "true") + + this.hoverIndex = 0 + let keyword = this.keyword + if (keyword.length == 0) { + if (shouldSearch) { + this.search(keyword) + } else { + this.items = this.allItems.$copy() + } + return + } + + + if (shouldSearch) { + this.search(keyword) + } else { + this.items = this.allItems.$copy().filter(function (v) { + if (v.fullname != null && v.fullname.length > 0 && teaweb.match(v.fullname, keyword)) { + return true + } + return teaweb.match(v.name, keyword) + }) + } + }, + selectItem: function (item) { + this.selectedItem = item + this.change() + this.hoverIndex = 0 + this.keyword = "" + this.changeKeyword() + }, + confirm: function () { + if (this.items.length > this.hoverIndex) { + this.selectItem(this.items[this.hoverIndex]) + } + }, + show: function () { + this.visible = true + + // 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换 + }, + hide: function () { + let that = this + this.hideTimer = setTimeout(function () { + that.visible = false + }, 500) + }, + downItem: function () { + this.hoverIndex++ + if (this.hoverIndex > this.items.length - 1) { + this.hoverIndex = 0 + } + this.focusItem() + }, + upItem: function () { + this.hoverIndex-- + if (this.hoverIndex < 0) { + this.hoverIndex = 0 + } + this.focusItem() + }, + focusItem: function () { + if (this.hoverIndex < this.items.length) { + this.$refs.itemRef[this.hoverIndex].focus() + let that = this + setTimeout(function () { + that.$refs.searchBox.focus() + if (that.hideTimer != null) { + clearTimeout(that.hideTimer) + that.hideTimer = null + } + }) + } + }, + change: function () { + this.$emit("change", this.selectedItem) + + let that = this + setTimeout(function () { + if (that.$refs.selectedLabel != null) { + that.$refs.selectedLabel.focus() + } + }) + }, + submitForm: function (event) { + if (event.target.tagName != "A") { + return + } + let parentBox = this.$refs.selectedLabel.parentNode + while (true) { + parentBox = parentBox.parentNode + if (parentBox == null || parentBox.tagName == "BODY") { + return + } + if (parentBox.tagName == "FORM") { + parentBox.submit() + break + } + } + }, + + setDataURL: function (dataURL) { + this.dataURL = dataURL + }, + reloadData: function () { + this.search("") + } + }, + template: `
+ +
+ +
+ + +
+ + {{title}}:{{selectedItem.name}} + + +
+ + +
+ +
+
` +}) + +Vue.component("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("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("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("datepicker", { + props: ["value", "v-name", "name", "v-value", "v-bottom-left", "placeholder"], + mounted: function () { + let that = this + teaweb.datepicker(this.$refs.dayInput, function (v) { + that.day = v + that.change() + }, !!this.vBottomLeft) + }, + data: function () { + let name = this.vName + if (name == null) { + name = this.name + } + if (name == null) { + name = "day" + } + + let day = this.vValue + if (day == null) { + day = this.value + if (day == null) { + day = "" + } + } + + let placeholder = "YYYY-MM-DD" + if (this.placeholder != null) { + placeholder = this.placeholder + } + + return { + realName: name, + realPlaceholder: placeholder, + day: day + } + }, + watch: { + value: function (v) { + this.day = v + + let picker = this.$refs.dayInput.picker + if (picker != null) { + if (v != null && /^\d+-\d+-\d+$/.test(v)) { + picker.setDate(v) + } + } + } + }, + methods: { + change: function () { + this.$emit("input", this.day) // support v-model,事件触发需要在 change 之前 + this.$emit("change", this.day) + } + }, + template: `
+ +
` +}) + +Vue.component("datetime-input", { + props: ["v-name", "v-timestamp"], + mounted: function () { + let that = this + teaweb.datepicker(this.$refs.dayInput, function (v) { + that.day = v + that.hour = "23" + that.minute = "59" + that.second = "59" + that.change() + }) + }, + data: function () { + let timestamp = this.vTimestamp + if (timestamp != null) { + timestamp = parseInt(timestamp) + if (isNaN(timestamp)) { + timestamp = 0 + } + } else { + timestamp = 0 + } + + let day = "" + let hour = "" + let minute = "" + let second = "" + + if (timestamp > 0) { + let date = new Date() + date.setTime(timestamp * 1000) + + let year = date.getFullYear().toString() + let month = this.leadingZero((date.getMonth() + 1).toString(), 2) + day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2) + + hour = this.leadingZero(date.getHours().toString(), 2) + minute = this.leadingZero(date.getMinutes().toString(), 2) + second = this.leadingZero(date.getSeconds().toString(), 2) + } + + return { + timestamp: timestamp, + day: day, + hour: hour, + minute: minute, + second: second, + + hasDayError: false, + hasHourError: false, + hasMinuteError: false, + hasSecondError: false + } + }, + methods: { + change: function () { + // day + if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) { + this.hasDayError = true + return + } + let pieces = this.day.split("-") + let year = parseInt(pieces[0]) + + let month = parseInt(pieces[1]) + if (month < 1 || month > 12) { + this.hasDayError = true + return + } + + let day = parseInt(pieces[2]) + if (day < 1 || day > 32) { + this.hasDayError = true + return + } + + this.hasDayError = false + + // hour + if (!/^\d+$/.test(this.hour)) { + this.hasHourError = true + return + } + let hour = parseInt(this.hour) + if (isNaN(hour)) { + this.hasHourError = true + return + } + if (hour < 0 || hour >= 24) { + this.hasHourError = true + return + } + this.hasHourError = false + + // minute + if (!/^\d+$/.test(this.minute)) { + this.hasMinuteError = true + return + } + let minute = parseInt(this.minute) + if (isNaN(minute)) { + this.hasMinuteError = true + return + } + if (minute < 0 || minute >= 60) { + this.hasMinuteError = true + return + } + this.hasMinuteError = false + + // second + if (!/^\d+$/.test(this.second)) { + this.hasSecondError = true + return + } + let second = parseInt(this.second) + if (isNaN(second)) { + this.hasSecondError = true + return + } + if (second < 0 || second >= 60) { + this.hasSecondError = true + return + } + this.hasSecondError = false + + let date = new Date(year, month - 1, day, hour, minute, second) + this.timestamp = Math.floor(date.getTime() / 1000) + }, + leadingZero: function (s, l) { + s = s.toString() + if (l <= s.length) { + return s + } + for (let i = 0; i < l - s.length; i++) { + s = "0" + s + } + return s + }, + resultTimestamp: function () { + return this.timestamp + }, + nextYear: function () { + let date = new Date() + date.setFullYear(date.getFullYear()+1) + this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) + this.hour = this.leadingZero(date.getHours(), 2) + this.minute = this.leadingZero(date.getMinutes(), 2) + this.second = this.leadingZero(date.getSeconds(), 2) + this.change() + }, + nextDays: function (days) { + let date = new Date() + date.setTime(date.getTime() + days * 86400 * 1000) + this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) + this.hour = this.leadingZero(date.getHours(), 2) + this.minute = this.leadingZero(date.getMinutes(), 2) + this.second = this.leadingZero(date.getSeconds(), 2) + this.change() + }, + nextHours: function (hours) { + let date = new Date() + date.setTime(date.getTime() + hours * 3600 * 1000) + this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) + this.hour = this.leadingZero(date.getHours(), 2) + this.minute = this.leadingZero(date.getMinutes(), 2) + this.second = this.leadingZero(date.getSeconds(), 2) + this.change() + } + }, + template: `
+ +
+
+ +
+
+
:
+
+
:
+
+
+

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

+
` +}) + +Vue.component("dot", { + template: '' +}) + +Vue.component("download-link", { + props: ["v-element", "v-file", "v-value"], + created: function () { + let that = this + setTimeout(function () { + that.url = that.composeURL() + }, 1000) + }, + data: function () { + let filename = this.vFile + if (filename == null || filename.length == 0) { + filename = "unknown-file" + } + return { + file: filename, + url: this.composeURL() + } + }, + methods: { + composeURL: function () { + let text = "" + if (this.vValue != null) { + text = this.vValue + } else { + let e = document.getElementById(this.vElement) + if (e == null) { + // 不提示错误,因为此时可能页面未加载完整 + return + } + text = e.innerText + if (text == null) { + text = e.textContent + } + } + return Tea.url("/ui/download", { + file: this.file, + text: text + }) + } + }, + template: ``, +}) + +Vue.component("file-textarea", { + props: ["value"], + data: function () { + let value = this.value + if (typeof value != "string") { + value = "" + } + return { + realValue: value + } + }, + mounted: function () { + }, + methods: { + dragover: function () {}, + drop: function (e) { + let that = this + e.dataTransfer.items[0].getAsFile().text().then(function (data) { + that.setValue(data) + }) + }, + setValue: function (value) { + this.realValue = value + }, + focus: function () { + this.$refs.textarea.focus() + } + }, + template: `` +}) + +/** + * 一级菜单 + */ +Vue.component("first-menu", { + props: [], + template: ' \ +
\ + \ +
\ +
' +}); + +Vue.component("health-check-config-box", { + props: ["v-health-check-config", "v-check-domain-url", "v-is-plus"], + data: function () { + let healthCheckConfig = this.vHealthCheckConfig + let urlProtocol = "http" + let urlPort = "" + let urlRequestURI = "/" + let urlHost = "" + + if (healthCheckConfig == null) { + healthCheckConfig = { + isOn: false, + url: "", + interval: {count: 60, unit: "second"}, + statusCodes: [200], + timeout: {count: 10, unit: "second"}, + countTries: 3, + tryDelay: {count: 100, unit: "ms"}, + autoDown: true, + countUp: 1, + countDown: 3, + userAgent: "", + onlyBasicRequest: true, + accessLogIsOn: true + } + let that = this + setTimeout(function () { + that.changeURL() + }, 500) + } else { + try { + let url = new URL(healthCheckConfig.url) + urlProtocol = url.protocol.substring(0, url.protocol.length - 1) + + // 域名 + urlHost = url.host + if (urlHost == "%24%7Bhost%7D") { + urlHost = "${host}" + } + let colonIndex = urlHost.indexOf(":") + if (colonIndex > 0) { + urlHost = urlHost.substring(0, colonIndex) + } + + urlPort = url.port + urlRequestURI = url.pathname + if (url.search.length > 0) { + urlRequestURI += url.search + } + } catch (e) { + } + + if (healthCheckConfig.statusCodes == null) { + healthCheckConfig.statusCodes = [200] + } + if (healthCheckConfig.interval == null) { + healthCheckConfig.interval = {count: 60, unit: "second"} + } + if (healthCheckConfig.timeout == null) { + healthCheckConfig.timeout = {count: 10, unit: "second"} + } + if (healthCheckConfig.tryDelay == null) { + healthCheckConfig.tryDelay = {count: 100, unit: "ms"} + } + if (healthCheckConfig.countUp == null || healthCheckConfig.countUp < 1) { + healthCheckConfig.countUp = 1 + } + if (healthCheckConfig.countDown == null || healthCheckConfig.countDown < 1) { + healthCheckConfig.countDown = 3 + } + } + + return { + healthCheck: healthCheckConfig, + advancedVisible: false, + urlProtocol: urlProtocol, + urlHost: urlHost, + urlPort: urlPort, + urlRequestURI: urlRequestURI, + urlIsEditing: healthCheckConfig.url.length == 0, + + hostErr: "" + } + }, + watch: { + urlRequestURI: function () { + if (this.urlRequestURI.length > 0 && this.urlRequestURI[0] != "/") { + this.urlRequestURI = "/" + this.urlRequestURI + } + this.changeURL() + }, + urlPort: function (v) { + let port = parseInt(v) + if (!isNaN(port)) { + this.urlPort = port.toString() + } else { + this.urlPort = "" + } + this.changeURL() + }, + urlProtocol: function () { + this.changeURL() + }, + urlHost: function () { + this.changeURL() + this.hostErr = "" + }, + "healthCheck.countTries": function (v) { + let count = parseInt(v) + if (!isNaN(count)) { + this.healthCheck.countTries = count + } else { + this.healthCheck.countTries = 0 + } + }, + "healthCheck.countUp": function (v) { + let count = parseInt(v) + if (!isNaN(count)) { + this.healthCheck.countUp = count + } else { + this.healthCheck.countUp = 0 + } + }, + "healthCheck.countDown": function (v) { + let count = parseInt(v) + if (!isNaN(count)) { + this.healthCheck.countDown = count + } else { + this.healthCheck.countDown = 0 + } + } + }, + methods: { + showAdvanced: function () { + this.advancedVisible = !this.advancedVisible + }, + changeURL: function () { + let urlHost = this.urlHost + if (urlHost.length == 0) { + urlHost = "${host}" + } + this.healthCheck.url = this.urlProtocol + "://" + urlHost + ((this.urlPort.length > 0) ? ":" + this.urlPort : "") + this.urlRequestURI + }, + changeStatus: function (values) { + this.healthCheck.statusCodes = values.$map(function (k, v) { + let status = parseInt(v) + if (isNaN(status)) { + return 0 + } else { + return status + } + }) + }, + onChangeURLHost: function () { + let checkDomainURL = this.vCheckDomainUrl + if (checkDomainURL == null || checkDomainURL.length == 0) { + return + } + + let that = this + Tea.action(checkDomainURL) + .params({host: this.urlHost}) + .success(function (resp) { + if (!resp.data.isOk) { + that.hostErr = "在当前集群中找不到此域名,可能会影响健康检查结果。" + } else { + that.hostErr = "" + } + }) + .post() + }, + editURL: function () { + this.urlIsEditing = !this.urlIsEditing + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用健康检查 +
+ + +
+

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

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

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

+
端口 + +

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

+
RequestURI +

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

+
+
+

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

+
+
检测时间间隔 + +

两次检查之间的间隔。

+
自动上/下线IP +
+ + +
+

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

+
连续上线次数 + +

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

+
连续下线次数 + +

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

+
允许的状态码 + +

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

+
超时时间 + +

读取检测URL超时时间。

+
连续尝试次数 + +

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

+
每次尝试间隔 + +

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

+
终端信息(User-Agent) + +

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

+
只基础请求 + +

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

+
记录访问日志 + +

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

+
+
+
` +}) + +/** + * 菜单项 + */ +Vue.component("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("inner-menu", { + 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("js-page", { + props: ["v-max"], + data: function () { + let max = this.vMax + if (max == null) { + max = 0 + } + return { + max: max, + page: 1 + } + }, + methods: { + updateMax: function (max) { + this.max = max + }, + selectPage: function(page) { + this.page = page + this.$emit("change", page) + } + }, + template:`
+
+ {{i}} +
+
` +}) + +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("labeled-input", { + props: ["name", "size", "maxlength", "label", "value"], + template: '
\ + \ + {{label}}\ +
' +}); + +// 启用状态标签 +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: `注意:通常不需要修改;如要修改,请在专家指导下进行。` +}) + + +// 使用Icon的链接方式 +Vue.component("link-icon", { + props: ["href", "title", "target", "size"], + data: function () { + let realSize = this.size + if (realSize == null || realSize.length == 0) { + realSize = "small" + } + + return { + vTitle: (this.title == null) ? "打开链接" : this.title, + realSize: realSize + } + }, + template: ` ` +}) + +// 带有下划虚线的连接 +Vue.component("link-red", { + props: ["href", "title"], + data: function () { + let href = this.href + if (href == null) { + href = "" + } + return { + vHref: href + } + }, + methods: { + clickPrevent: function () { + emitClick(this, arguments) + + if (this.vHref.length > 0) { + window.location = this.vHref + } + } + }, + template: `` +}) + +// 会弹出窗口的链接 +Vue.component("link-popup", { + props: ["title"], + methods: { + clickPrevent: function () { + emitClick(this, arguments) + } + }, + template: `` +}) + +Vue.component("popup-icon", { + props: ["title", "href", "height"], + methods: { + clickPrevent: function () { + if (this.href != null && this.href.length > 0) { + teaweb.popup(this.href, { + height: this.height + }) + } + } + }, + template: ` ` +}) + +// 小提示 +Vue.component("tip-icon", { + props: ["content"], + methods: { + showTip: function () { + teaweb.popupTip(this.content) + } + }, + template: `` +}) + +// 提交点击事件 +function emitClick(obj, arguments) { + let event = "click" + let newArgs = [event] + for (let i = 0; i < arguments.length; i++) { + newArgs.push(arguments[i]) + } + obj.$emit.apply(obj, newArgs) +} + +Vue.component("mask-warning", { + 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: '\ + \ + ' +}); + +Vue.component("loading-message", { + template: `
+
  +
` +}) + +// 可以展示更多条目的角图表 +Vue.component("more-items-angle", { + props: ["v-data-url", "v-url"], + data: function () { + return { + visible: false + } + }, + methods: { + show: function () { + this.visible = !this.visible + if (this.visible) { + this.showBox() + } else { + this.hideBox() + } + }, + showBox: function () { + let that = this + + this.visible = true + + Tea.action(this.vDataUrl) + .params({ + url: this.vUrl + }) + .post() + .success(function (resp) { + let groups = resp.data.groups + + let boxLeft = that.$el.offsetLeft + 120; + let boxTop = that.$el.offsetTop + 70; + + let box = document.createElement("div") + box.setAttribute("id", "more-items-box") + box.style.cssText = "z-index: 100; position: absolute; left: " + boxLeft + "px; top: " + boxTop + "px; max-height: 30em; overflow: auto; border-bottom: 1px solid rgba(34,36,38,.15)" + document.body.append(box) + + let menuHTML = "
    " + groups.forEach(function (group) { + menuHTML += "
    " + teaweb.encodeHTML(group.name) + "
    " + group.items.forEach(function (item) { + menuHTML += "" + teaweb.encodeHTML(item.name) + "" + }) + }) + menuHTML += "
" + box.innerHTML = menuHTML + + let listener = function (e) { + if (e.target.tagName == "I") { + return + } + + if (!that.isInBox(box, e.target)) { + document.removeEventListener("click", listener) + that.hideBox() + } + } + document.addEventListener("click", listener) + }) + }, + hideBox: function () { + let box = document.getElementById("more-items-box") + if (box != null) { + box.parentNode.removeChild(box) + } + this.visible = false + }, + isInBox: function (parent, child) { + while (true) { + if (child == null) { + break + } + if (child.parentNode == parent) { + return true + } + child = child.parentNode + } + return false + } + }, + template: `切换` +}) + +Vue.component("more-options-angle", { + data: function () { + return { + isVisible: false + } + }, + methods: { + show: function () { + this.isVisible = !this.isVisible + this.$emit("change", this.isVisible) + } + }, + template: `更多选项收起选项` +}) + +/** + * 更多选项 + */ +Vue.component("more-options-indicator", { + props:[], + data: function () { + return { + visible: false + } + }, + methods: { + changeVisible: function () { + this.visible = !this.visible + if (Tea.Vue != null) { + Tea.Vue.moreOptionsVisible = this.visible + } + this.$emit("change", this.visible) + this.$emit("input", this.visible) + } + }, + template: '更多选项收起选项 ' +}); + +Vue.component("more-options-tbody", { + data: function () { + return { + isVisible: false + } + }, + methods: { + show: function () { + this.isVisible = !this.isVisible + this.$emit("change", this.isVisible) + } + }, + template: ` + + 更多选项收起选项 + +` +}) + +Vue.component("network-addresses-box", { + props: ["v-server-type", "v-addresses", "v-protocol", "v-name", "v-from", "v-support-range", "v-url"], + data: function () { + let addresses = this.vAddresses + if (addresses == null) { + addresses = [] + } + let protocol = this.vProtocol + if (protocol == null) { + protocol = "" + } + + let name = this.vName + if (name == null) { + name = "addresses" + } + + let from = this.vFrom + if (from == null) { + from = "" + } + + return { + addresses: addresses, + protocol: protocol, + name: name, + from: from, + isEditing: false + } + }, + watch: { + "vServerType": function () { + this.addresses = [] + }, + "vAddresses": function () { + if (this.vAddresses != null) { + this.addresses = this.vAddresses + } + } + }, + methods: { + addAddr: function () { + this.isEditing = true + + let that = this + window.UPDATING_ADDR = null + + let url = this.vUrl + if (url == null) { + url = "/servers/addPortPopup" + } + + teaweb.popup(url + "?serverType=" + this.vServerType + "&protocol=" + this.protocol + "&from=" + this.from + "&supportRange=" + (this.supportRange() ? 1 : 0), { + height: "18em", + callback: function (resp) { + var addr = resp.data.address + if (that.addresses.$find(function (k, v) { + return addr.host == v.host && addr.portRange == v.portRange && addr.protocol == v.protocol + }) != null) { + teaweb.warn("要添加的网络地址已经存在") + return + } + that.addresses.push(addr) + if (["https", "https4", "https6"].$contains(addr.protocol)) { + this.tlsProtocolName = "HTTPS" + } else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) { + this.tlsProtocolName = "TLS" + } + + // 发送事件 + that.$emit("change", that.addresses) + } + }) + }, + removeAddr: function (index) { + this.addresses.$remove(index); + + // 发送事件 + this.$emit("change", this.addresses) + }, + updateAddr: function (index, addr) { + let that = this + window.UPDATING_ADDR = addr + + let url = this.vUrl + if (url == null) { + url = "/servers/addPortPopup" + } + + teaweb.popup(url + "?serverType=" + this.vServerType + "&protocol=" + this.protocol + "&from=" + this.from + "&supportRange=" + (this.supportRange() ? 1 : 0), { + height: "18em", + callback: function (resp) { + var addr = resp.data.address + Vue.set(that.addresses, index, addr) + + if (["https", "https4", "https6"].$contains(addr.protocol)) { + this.tlsProtocolName = "HTTPS" + } else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) { + this.tlsProtocolName = "TLS" + } + + // 发送事件 + that.$emit("change", that.addresses) + } + }) + + // 发送事件 + this.$emit("change", this.addresses) + }, + supportRange: function () { + return this.vSupportRange || (this.vServerType == "tcpProxy" || this.vServerType == "udpProxy") + }, + edit: function () { + this.isEditing = true + } + }, + template: `
+ +
+
+
+ {{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}}{{addr.portRange}} +
+     [修改] +
+
+
+
+
+ {{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}}{{addr.portRange}} + + +
+
+
+ [添加端口绑定] +
+
` +}) + +Vue.component("network-addresses-view", { + props: ["v-addresses"], + template: `
+
+ {{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}} +
+
` +}) + +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("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}}` +}) + +Vue.component("not-found-box", { + props: ["message"], + template: `
+
+

{{message}}

+
` +}) + +Vue.component("page-box", { + data: function () { + return { + page: "" + } + }, + created: function () { + let that = this; + setTimeout(function () { + that.page = Tea.Vue.page; + }) + }, + 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("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}}
+
+
+
+ +
+
` +}) + +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("raquo-item", { + template: `»` +}) + +// 将变量转换为中文 +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("search-box", { + props: ["placeholder", "width"], + data: function () { + let width = this.width + if (width == null) { + width = "10em" + } + return { + realWidth: width, + realValue: "" + } + }, + methods: { + onInput: function () { + this.$emit("input", { value: this.realValue}) + this.$emit("change", { value: this.realValue}) + }, + clearValue: function () { + this.realValue = "" + this.focus() + this.onInput() + }, + focus: function () { + this.$refs.valueRef.focus() + } + }, + template: `
+
+ + +
+
` +}) + +/** + * 二级菜单 + */ +Vue.component("second-menu", { + 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("size-capacity-view", { + props:["v-default-text", "v-value"], + methods: { + composeCapacity: function (capacity) { + return teaweb.convertSizeCapacityToString(capacity) + } + }, + template: `
+ {{composeCapacity(vValue)}} + {{vDefaultText}} +
` +}) + +// 排序使用的箭头 +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: `  ` +}) + +// 给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) +} + + +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("submit-btn", { + template: '' +}); + +Vue.component("time-duration-box", { + props: ["v-name", "v-value", "v-count", "v-unit", "placeholder", "v-min-unit", "maxlength"], + mounted: function () { + this.change() + }, + data: function () { + let v = this.vValue + if (v == null) { + v = { + count: this.vCount, + unit: this.vUnit + } + } + if (typeof (v["count"]) != "number") { + v["count"] = -1 + } + + let minUnit = this.vMinUnit + let units = [ + { + code: "ms", + name: "毫秒" + }, + { + code: "second", + name: "秒" + }, + { + code: "minute", + name: "分钟" + }, + { + code: "hour", + name: "小时" + }, + { + code: "day", + name: "天" + } + ] + let minUnitIndex = -1 + if (minUnit != null && typeof minUnit == "string" && minUnit.length > 0) { + for (let i = 0; i < units.length; i++) { + if (units[i].code == minUnit) { + minUnitIndex = i + break + } + } + } + if (minUnitIndex > -1) { + units = units.slice(minUnitIndex) + } + + let maxLength = parseInt(this.maxlength) + if (typeof maxLength != "number") { + maxLength = 10 + } + + return { + duration: v, + countString: (v.count >= 0) ? v.count.toString() : "", + units: units, + realMaxLength: maxLength + } + }, + watch: { + "countString": function (newValue) { + let value = newValue.trim() + if (value.length == 0) { + this.duration.count = -1 + return + } + let count = parseInt(value) + if (!isNaN(count)) { + this.duration.count = count + } + this.change() + } + }, + methods: { + change: function () { + this.$emit("change", this.duration) + } + }, + template: `
+ +
+ +
+
+ +
+
` +}) + +Vue.component("time-duration-text", { + props: ["v-value"], + methods: { + unitName: function (unit) { + switch (unit) { + case "ms": + return "毫秒" + case "second": + return "秒" + case "minute": + return "分钟" + case "hour": + return "小时" + case "day": + return "天" + } + } + }, + template: ` + {{vValue.count}} {{unitName(vValue.unit)}} +` +}) + +// 信息提示窗口 +Vue.component("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("url-patterns-box", { + props: ["value"], + data: function () { + let patterns = [] + if (this.value != null) { + patterns = this.value + } + + return { + patterns: patterns, + isAdding: false, + + addingPattern: {"type": "wildcard", "pattern": ""}, + editingIndex: -1, + + patternIsInvalid: false, + + windowIsSmall: window.innerWidth < 600 + } + }, + methods: { + add: function () { + this.isAdding = true + let that = this + setTimeout(function () { + that.$refs.patternInput.focus() + }) + }, + edit: function (index) { + this.isAdding = true + this.editingIndex = index + this.addingPattern = { + type: this.patterns[index].type, + pattern: this.patterns[index].pattern + } + }, + confirm: function () { + if (this.requireURL(this.addingPattern.type)) { + let pattern = this.addingPattern.pattern.trim() + if (pattern.length == 0) { + let that = this + teaweb.warn("请输入URL", function () { + that.$refs.patternInput.focus() + }) + return + } + } + if (this.editingIndex < 0) { + this.patterns.push({ + type: this.addingPattern.type, + pattern: this.addingPattern.pattern + }) + } else { + this.patterns[this.editingIndex].type = this.addingPattern.type + this.patterns[this.editingIndex].pattern = this.addingPattern.pattern + } + this.notifyChange() + this.cancel() + }, + remove: function (index) { + this.patterns.$remove(index) + this.cancel() + this.notifyChange() + }, + cancel: function () { + this.isAdding = false + this.addingPattern = {"type": "wildcard", "pattern": ""} + this.editingIndex = -1 + }, + patternTypeName: function (patternType) { + switch (patternType) { + case "wildcard": + return "通配符" + case "regexp": + return "正则" + case "images": + return "常见图片文件" + case "audios": + return "常见音频文件" + case "videos": + return "常见视频文件" + } + return "" + }, + notifyChange: function () { + this.$emit("input", this.patterns) + }, + changePattern: function () { + this.patternIsInvalid = false + let pattern = this.addingPattern.pattern + switch (this.addingPattern.type) { + case "wildcard": + if (pattern.indexOf("?") >= 0) { + this.patternIsInvalid = true + } + break + case "regexp": + if (pattern.indexOf("?") >= 0) { + let pieces = pattern.split("?") + for (let i = 0; i < pieces.length - 1; i++) { + if (pieces[i].length == 0 || pieces[i][pieces[i].length - 1] != "\\") { + this.patternIsInvalid = true + } + } + } + break + } + }, + requireURL: function (patternType) { + return patternType == "wildcard" || patternType == "regexp" + } + }, + template: `
+
+
+ [{{patternTypeName(pattern.type)}}] {{pattern.pattern}}   + + +
+
+
+
+
+ +
+
+ +

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

+
+
+ + +
+
+ +
+
+
+
+ +
+
` +}) + +Vue.component("values-box", { + props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"], + data: function () { + let values = this.values; + if (values == null) { + values = []; + } + + if (this.vValues != null && typeof this.vValues == "object") { + values = this.vValues + } + + return { + "realValues": values, + "isUpdating": false, + "isAdding": false, + "index": 0, + "value": "", + isEditing: false + } + }, + methods: { + create: function () { + this.isAdding = true; + var that = this; + setTimeout(function () { + that.$refs.value.focus(); + }, 200); + }, + update: function (index) { + this.cancel() + this.isUpdating = true; + this.index = index; + this.value = this.realValues[index]; + var that = this; + setTimeout(function () { + that.$refs.value.focus(); + }, 200); + }, + confirm: function () { + if (this.value.length == 0) { + if (typeof(this.vAllowEmpty) != "boolean" || !this.vAllowEmpty) { + return + } + } + + // validate + if (typeof(this.validator) == "function") { + let resp = this.validator.call(this, this.value) + if (typeof resp == "object") { + if (typeof resp.isOk == "boolean" && !resp.isOk) { + if (typeof resp.message == "string") { + let that = this + teaweb.warn(resp.message, function () { + that.$refs.value.focus(); + }) + } + return + } + } + } + + if (this.isUpdating) { + Vue.set(this.realValues, this.index, this.value); + } else { + this.realValues.push(this.value); + } + this.cancel() + this.$emit("change", this.realValues) + }, + remove: function (index) { + this.realValues.$remove(index) + this.$emit("change", this.realValues) + }, + cancel: function () { + this.isUpdating = false; + this.isAdding = false; + this.value = ""; + }, + updateAll: function (values) { + this.realValues = values + }, + addValue: function (v) { + this.realValues.push(v) + }, + + startEditing: function () { + this.isEditing = !this.isEditing + }, + allValues: function () { + return this.realValues + } + }, + template: `
+
+
+ {{value}} + [空] +
+ [修改] +
+
+
+
+ {{value}} + [空] + +   + +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
` +}); + +// 警告消息 +Vue.component("warning-message", { + template: `
` +}) + +Vue.component("dns-domain-selector", { + props: ["v-domain-id", "v-domain-name", "v-provider-name"], + data: function () { + let domainId = this.vDomainId + if (domainId == null) { + domainId = 0 + } + let domainName = this.vDomainName + if (domainName == null) { + domainName = "" + } + + let providerName = this.vProviderName + if (providerName == null) { + providerName = "" + } + + return { + domainId: domainId, + domainName: domainName, + providerName: providerName + } + }, + methods: { + select: function () { + let that = this + teaweb.popup("/dns/domains/selectPopup", { + callback: function (resp) { + that.domainId = resp.data.domainId + that.domainName = resp.data.domainName + that.providerName = resp.data.providerName + that.change() + } + }) + }, + remove: function() { + this.domainId = 0 + this.domainName = "" + this.change() + }, + update: function () { + let that = this + teaweb.popup("/dns/domains/selectPopup?domainId=" + this.domainId, { + callback: function (resp) { + that.domainId = resp.data.domainId + that.domainName = resp.data.domainName + that.providerName = resp.data.providerName + that.change() + } + }) + }, + change: function () { + this.$emit("change", { + id: this.domainId, + name: this.domainName + }) + } + }, + template: `
+ +
+ + {{providerName}} » {{domainName}} + + + +
+
+ [选择域名] +
+
` +}) + +Vue.component("dns-resolver-config-box", { + props:["v-dns-resolver-config"], + data: function () { + let config = this.vDnsResolverConfig + if (config == null) { + config = { + type: "default" + } + } + return { + config: config, + types: [ + { + name: "默认", + code: "default" + }, + { + name: "CGO", + code: "cgo" + }, + { + name: "Go原生", + code: "goNative" + }, + ] + } + }, + template: `
+ + + + + + +
使用的DNS解析库 + +

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

+
+
+
` +}) + +Vue.component("dns-resolvers-config-box", { + props: ["value", "name"], + data: function () { + let resolvers = this.value + if (resolvers == null) { + resolvers = [] + } + + let name = this.name + if (name == null || name.length == 0) { + name = "dnsResolversJSON" + } + + return { + formName: name, + resolvers: resolvers, + + host: "", + + isAdding: false + } + }, + methods: { + add: function () { + this.isAdding = true + let that = this + setTimeout(function () { + that.$refs.hostRef.focus() + }) + }, + confirm: function () { + let host = this.host.trim() + if (host.length == 0) { + let that = this + setTimeout(function () { + that.$refs.hostRef.focus() + }) + return + } + this.resolvers.push({ + host: host, + port: 0, // TODO + protocol: "" // TODO + }) + this.cancel() + }, + cancel: function () { + this.isAdding = false + this.host = "" + this.port = 0 + this.protocol = "" + }, + remove: function (index) { + this.resolvers.$remove(index) + } + }, + template: `
+ +
+
+ {{resolver.protocol}}{{resolver.host}}:{{resolver.port}} +   + +
+
+ +
+
+
+ +
+
+ +   +
+
+
+ +
+ +
+
` +}) + +Vue.component("dns-route-selector", { + props: ["v-all-routes", "v-routes"], + data: function () { + let routes = this.vRoutes + if (routes == null) { + routes = [] + } + routes.$sort(function (v1, v2) { + if (v1.domainId == v2.domainId) { + return v1.code < v2.code + } + return (v1.domainId < v2.domainId) ? 1 : -1 + }) + return { + routes: routes, + routeCodes: routes.$map(function (k, v) { + return v.code + "@" + v.domainId + }), + isAdding: false, + routeCode: "", + keyword: "", + searchingRoutes: this.vAllRoutes.$copy() + } + }, + methods: { + add: function () { + this.isAdding = true + this.keyword = "" + this.routeCode = "" + + let that = this + setTimeout(function () { + that.$refs.keywordRef.focus() + }, 200) + }, + cancel: function () { + this.isAdding = false + }, + confirm: function () { + if (this.routeCode.length == 0) { + return + } + if (this.routeCodes.$contains(this.routeCode)) { + teaweb.warn("已经添加过此线路,不能重复添加") + return + } + let that = this + let route = this.vAllRoutes.$find(function (k, v) { + return v.code + "@" + v.domainId == that.routeCode + }) + if (route == null) { + return + } + + this.routeCodes.push(this.routeCode) + this.routes.push(route) + + this.routes.$sort(function (v1, v2) { + if (v1.domainId == v2.domainId) { + return v1.code < v2.code + } + return (v1.domainId < v2.domainId) ? 1 : -1 + }) + + this.routeCode = "" + this.isAdding = false + }, + remove: function (route) { + this.routeCodes.$removeValue(route.code + "@" + route.domainId) + this.routes.$removeIf(function (k, v) { + return v.code + "@" + v.domainId == route.code + "@" + route.domainId + }) + }, + clearKeyword: function () { + this.keyword = "" + } + }, + watch: { + keyword: function (keyword) { + if (keyword.length == 0) { + this.searchingRoutes = this.vAllRoutes.$copy() + this.routeCode = "" + return + } + this.searchingRoutes = this.vAllRoutes.filter(function (route) { + return teaweb.match(route.name, keyword) || teaweb.match(route.code, keyword) || teaweb.match(route.domainName, keyword) + }) + if (this.searchingRoutes.length > 0) { + this.routeCode = this.searchingRoutes[0].code + "@" + this.searchingRoutes[0].domainId + } else { + this.routeCode = "" + } + } + }, + template: `
+ +
+ + {{route.name}} ({{route.domainName}}) + +
+
+ +
+ + + + + + + + + +
所有线路 + 没有和关键词“{{keyword}}”匹配的线路 + + + +
搜索线路 +
+ + +
+
+ +   +
+
` +}) + +Vue.component("finance-user-selector", { + props: ["v-user-id"], + data: function () { + return {} + }, + methods: { + change: function (userId) { + this.$emit("change", userId) + } + }, + template: `
+ +
` +}) + +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}})
+
+ [选择已有认证]     [添加新认证] +
+
` +}) + +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("ip-item-text", { + props: ["v-item"], + template: ` + * + + {{vItem.value}} + + {{vItem.ipFrom}} + - {{vItem.ipTo}} + + +   级别:{{vItem.eventLevelName}} +` +}) + +// 绑定IP列表 +Vue.component("ip-list-bind-box", { + props: ["v-http-firewall-policy-id", "v-type"], + mounted: function () { + this.refresh() + }, + data: function () { + return { + policyId: this.vHttpFirewallPolicyId, + type: this.vType, + lists: [] + } + }, + methods: { + bind: function () { + let that = this + teaweb.popup("/servers/iplists/bindHTTPFirewallPopup?httpFirewallPolicyId=" + this.policyId + "&type=" + this.type, { + width: "50em", + height: "34em", + callback: function () { + + }, + onClose: function () { + that.refresh() + } + }) + }, + remove: function (index, listId) { + let that = this + teaweb.confirm("确定要删除这个绑定的IP名单吗?", function () { + Tea.action("/servers/iplists/unbindHTTPFirewall") + .params({ + httpFirewallPolicyId: that.policyId, + listId: listId + }) + .post() + .success(function (resp) { + that.lists.$remove(index) + }) + }) + }, + refresh: function () { + let that = this + Tea.action("/servers/iplists/httpFirewall") + .params({ + httpFirewallPolicyId: this.policyId, + type: this.vType + }) + .post() + .success(function (resp) { + that.lists = resp.data.lists + }) + } + }, + template: `
+ 绑定+   已绑定: + +
` +}) + +Vue.component("ip-list-table", { + props: ["v-items", "v-keyword", "v-show-search-button", "v-total"/** total items >= items length **/], + data: function () { + let maxDeletes = 10000 + if (this.vTotal != null && this.vTotal > 0 && this.vTotal < maxDeletes) { + maxDeletes = this.vTotal + } + + return { + items: this.vItems, + keyword: (this.vKeyword != null) ? this.vKeyword : "", + selectedAll: false, + hasSelectedItems: false, + + MaxDeletes: maxDeletes + } + }, + methods: { + updateItem: function (itemId) { + this.$emit("update-item", itemId) + }, + deleteItem: function (itemId) { + this.$emit("delete-item", itemId) + }, + viewLogs: function (itemId) { + teaweb.popup("/servers/iplists/accessLogsPopup?itemId=" + itemId, { + width: "50em", + height: "30em" + }) + }, + changeSelectedAll: function () { + let boxes = this.$refs.itemCheckBox + if (boxes == null) { + return + } + + let that = this + boxes.forEach(function (box) { + box.checked = that.selectedAll + }) + + this.hasSelectedItems = this.selectedAll + }, + changeSelected: function (e) { + let that = this + that.hasSelectedItems = false + let boxes = that.$refs.itemCheckBox + if (boxes == null) { + return + } + boxes.forEach(function (box) { + if (box.checked) { + that.hasSelectedItems = true + } + }) + }, + deleteAll: function () { + let boxes = this.$refs.itemCheckBox + if (boxes == null) { + return + } + let itemIds = [] + boxes.forEach(function (box) { + if (box.checked) { + itemIds.push(box.value) + } + }) + if (itemIds.length == 0) { + return + } + + Tea.action("/servers/iplists/deleteItems") + .post() + .params({ + itemIds: itemIds + }) + .success(function () { + teaweb.successToast("批量删除成功", 1200, teaweb.reload) + }) + }, + deleteCount: function () { + let that = this + teaweb.confirm("确定要批量删除当前列表中的" + this.MaxDeletes + "个IP吗?", function () { + let query = window.location.search + if (query.startsWith("?")) { + query = query.substring(1) + } + Tea.action("/servers/iplists/deleteCount?" + query) + .post() + .params({count: that.MaxDeletes}) + .success(function () { + teaweb.successToast("批量删除成功", 1200, teaweb.reload) + }) + }) + }, + formatSeconds: function (seconds) { + if (seconds < 60) { + return seconds + "秒" + } + if (seconds < 3600) { + return Math.ceil(seconds / 60) + "分钟" + } + if (seconds < 86400) { + return Math.ceil(seconds / 3600) + "小时" + } + return Math.ceil(seconds / 86400) + "天" + }, + cancelChecked: function () { + this.hasSelectedItems = false + this.selectedAll = false + + let boxes = this.$refs.itemCheckBox + if (boxes == null) { + return + } + boxes.forEach(function (box) { + box.checked = false + }) + } + }, + template: `
+
+
+ +     + + +     + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
IP类型级别过期时间备注操作
+
+ + +
+
+ + {{item.value}} + + {{item.ipFrom}}  New   + - {{item.ipTo}} + + + * + +
+ {{item.region}} + | {{item.isp}} +
+
{{item.isp}}
+ + +
+ IPv4 + IPv4 + IPv6 + 所有IP + + {{item.eventLevelName}} + - + +
+ {{item.expiredTime}} +
+ 已过期 +
+
+ {{formatSeconds(item.lifeSeconds)}} + 已过期 +
+
+ 不过期 +
+ {{item.reason}} + - + + + + + + 日志   + 修改   + 删除 +
+
` +}) + Vue.component("traffic-map-box", { props: ["v-stats", "v-is-attack"], mounted: function () { @@ -267,575 +4955,62 @@ Vue.component("traffic-map-box-table", {
` }) -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 - } +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 - let exists = false - this.ports.forEach(function (v) { - if (v.port == port) { - exists = true - } - }) - if (exists) { - this.warn("端口号已经存在") - return - } - - this.ports.push({ - port: port, - description: this.addingPort.description - }) - this.notifyChange() - this.cancel() - }, - cancel: function () { - this.isAdding = false - this.addingPort = { - port: "", - description: "" - } - }, - remove: function (index) { - this.ports.$remove(index) - this.notifyChange() - }, - warn: function (message) { - let that = this - teaweb.warn(message, function () { - that.$refs.addingPortInput.focus() - }) - }, - notifyChange: function () { - this.$emit("change", this.ports) - } - }, - template: `
-
-
- {{portConfig.port}} ({{portConfig.description}}) -
-
-
-
-
-
-
- 端口 - -
-
-
-
- 备注 - -
-
-
- -  取消 -
-
-
-
- -
-
` -}) - -// 显示节点的多个集群 -Vue.component("node-clusters-labels", { - props: ["v-primary-cluster", "v-secondary-clusters", "size"], - data: function () { - let cluster = this.vPrimaryCluster - let secondaryClusters = this.vSecondaryClusters - if (secondaryClusters == null) { - secondaryClusters = [] - } - - let labelSize = this.size - if (labelSize == null) { - labelSize = "small" - } - return { - cluster: cluster, - secondaryClusters: secondaryClusters, - labelSize: labelSize - } - }, - template: `
- - {{cluster.name}} - {{cluster.name}} - - - {{c.name}} - {{c.name}} - -
` -}) - -// 单个集群选择 -Vue.component("cluster-selector", { - props: ["v-cluster-id"], - mounted: function () { - let that = this - - Tea.action("/clusters/options") - .post() - .success(function (resp) { - that.clusters = resp.data.clusters - }) - }, - data: function () { - let clusterId = this.vClusterId - if (clusterId == null) { - clusterId = 0 - } - return { - clusters: [], - clusterId: clusterId - } - }, - template: `
- -
` -}) - -Vue.component("node-ddos-protection-config-box", { - props: ["v-ddos-protection-config", "v-default-configs", "v-is-node", "v-cluster-is-on"], - data: function () { - let config = this.vDdosProtectionConfig - if (config == null) { - config = { - tcp: { - isPrior: false, - isOn: false, - maxConnections: 0, - maxConnectionsPerIP: 0, - newConnectionsRate: 0, - newConnectionsRateBlockTimeout: 0, - newConnectionsSecondlyRate: 0, - newConnectionSecondlyRateBlockTimeout: 0, - allowIPList: [], - ports: [] - } - } - } - - // initialize - if (config.tcp == null) { - config.tcp = { - isPrior: false, - isOn: false, - maxConnections: 0, - maxConnectionsPerIP: 0, - newConnectionsRate: 0, - newConnectionsRateBlockTimeout: 0, - newConnectionsSecondlyRate: 0, - newConnectionSecondlyRateBlockTimeout: 0, - allowIPList: [], - ports: [] - } - } - - - return { - config: config, - defaultConfigs: this.vDefaultConfigs, - isNode: this.vIsNode, - - isAddingPort: false - } - }, - methods: { - changeTCPPorts: function (ports) { - this.config.tcp.ports = ports - }, - changeTCPAllowIPList: function (ipList) { - this.config.tcp.allowIPList = ipList - } - }, - template: `
- - -

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

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

TCP设置

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

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

-
单IP TCP最大连接数 - -

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

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

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

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

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

-
TCP端口列表 - -

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

-
IP白名单 - -

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

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

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

-
从集群 -
-
{{cluster.name}}  
-
-
- -
-
+ // 初始化简介 + 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: `
+ +

` }) @@ -1008,65 +5183,6 @@ Vue.component("message-recipient-group-selector", {
` }) -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 () { @@ -1181,6 +5297,1839 @@ Vue.component("message-row", {
` }) +Vue.component("node-cache-disk-dirs-box", { + props: ["value", "name"], + data: function () { + let dirs = this.value + if (dirs == null) { + dirs = [] + } + return { + dirs: dirs, + + isEditing: false, + isAdding: false, + + addingPath: "" + } + }, + methods: { + add: function () { + this.isAdding = true + let that = this + setTimeout(function () { + that.$refs.addingPath.focus() + }, 100) + }, + confirm: function () { + let addingPath = this.addingPath.trim() + if (addingPath.length == 0) { + let that = this + teaweb.warn("请输入要添加的缓存目录", function () { + that.$refs.addingPath.focus() + }) + return + } + if (addingPath[0] != "/") { + addingPath = "/" + addingPath + } + this.dirs.push({ + path: addingPath + }) + this.cancel() + }, + cancel: function () { + this.addingPath = "" + this.isAdding = false + this.isEditing = false + }, + remove: function (index) { + let that = this + teaweb.confirm("确定要删除此目录吗?", function () { + that.dirs.$remove(index) + }) + } + }, + template: `
+ +
+ + {{dir.path}}   + +
+ + +
+
+
+ +
+
+ +   +
+
+
+ +
+ +
+
` +}) + +Vue.component("node-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-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}}   +
+
+ [选择分组]   [添加分组] +
+
` +}) + +Vue.component("node-ip-address-clusters-selector", { + props: ["vClusters"], + mounted: function () { + this.checkClusters() + }, + data: function () { + let clusters = this.vClusters + if (clusters == null) { + clusters = [] + } + return { + clusters: clusters, + hasCheckedCluster: false, + clustersVisible: false + } + }, + methods: { + checkClusters: function () { + let that = this + + let b = false + this.clusters.forEach(function (cluster) { + if (cluster.isChecked) { + b = true + } + }) + + this.hasCheckedCluster = b + + return b + }, + changeCluster: function (cluster) { + cluster.isChecked = !cluster.isChecked + this.checkClusters() + }, + showClusters: function () { + this.clustersVisible = !this.clustersVisible + } + }, + template: `
+ 默认用于所有集群   修改 +
+ {{cluster.name}}   修改 +

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

+
+
+
+ + {{cluster.name}} + +
+
` +}) + +// 节点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。

+
+
+   + +
+
+ +
+ +
+
+ + +
+   + +
+
+ +
+ +
+
` +}) + +// 节点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-addresses-box", { + props: ["v-ip-addresses", "role", "v-node-id"], + data: function () { + let nodeId = this.vNodeId + if (nodeId == null) { + nodeId = 0 + } + + return { + ipAddresses: (this.vIpAddresses == null) ? [] : this.vIpAddresses, + supportThresholds: this.role != "ns", + nodeId: nodeId + } + }, + methods: { + // 添加IP地址 + addIPAddress: function () { + window.UPDATING_NODE_IP_ADDRESS = null + + let that = this; + teaweb.popup("/nodes/ipAddresses/createPopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), { + callback: function (resp) { + that.ipAddresses.push(resp.data.ipAddress); + }, + height: "24em", + width: "44em" + }) + }, + + // 修改地址 + updateIPAddress: function (index, address) { + window.UPDATING_NODE_IP_ADDRESS = teaweb.clone(address) + + let that = this; + teaweb.popup("/nodes/ipAddresses/updatePopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), { + callback: function (resp) { + Vue.set(that.ipAddresses, index, resp.data.ipAddress); + }, + height: "24em", + width: "44em" + }) + }, + + // 删除IP地址 + removeIPAddress: function (index) { + this.ipAddresses.$remove(index); + }, + + // 判断是否为IPv6 + isIPv6: function (ip) { + return ip.indexOf(":") > -1 + } + }, + template: `
+ +
+
+
+ [IPv6] {{address.ip}} + (备注:{{address.name}},不公开访问 + (不公开访问) + [off] + [down] + [{{address.thresholds.length}}个阈值] +   + +   专属集群:[{{cluster.name}}] +   + + + + +
+
+
+
+
+ +
+
` +}) + +// 节点级别选择器 +Vue.component("node-level-selector", { + props: ["v-node-level"], + data: function () { + let levelCode = this.vNodeLevel + if (levelCode == null || levelCode < 1) { + levelCode = 1 + } + return { + levels: [ + { + name: "边缘节点", + code: 1, + description: "普通的边缘节点。" + }, + { + name: "L2节点", + code: 2, + description: "特殊的边缘节点,同时负责同组上一级节点的回源。" + } + ], + levelCode: levelCode + } + }, + watch: { + levelCode: function (code) { + this.$emit("change", code) + } + }, + template: `
+ +

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

+
` +}) + +// 节点登录推荐端口 +Vue.component("node-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-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-schedule-action-box", { + props: ["value", "v-actions"], + data: function () { + let actionConfig = this.value + if (actionConfig == null) { + actionConfig = { + code: "", + params: {} + } + } + + return { + actions: this.vActions, + currentAction: null, + actionConfig: actionConfig + } + }, + watch: { + "actionConfig.code": function (actionCode) { + if (actionCode.length == 0) { + this.currentAction = null + } else { + this.currentAction = this.actions.$find(function (k, v) { + return v.code == actionCode + }) + } + this.actionConfig.params = {} + } + }, + template: `
+ +
+
+ +
+

{{currentAction.description}}

+ +
+ +

接收通知的URL。

+
+
+
` +}) + +Vue.component("node-schedule-conds-box", { + props: ["value", "v-params", "v-operators"], + mounted: function () { + this.formatConds(this.condsConfig.conds) + this.$forceUpdate() + }, + data: function () { + let condsConfig = this.value + if (condsConfig == null) { + condsConfig = { + conds: [], + connector: "and" + } + } + if (condsConfig.conds == null) { + condsConfig.conds = [] + } + + let paramMap = {} + this.vParams.forEach(function (param) { + paramMap[param.code] = param + }) + + let operatorMap = {} + this.vOperators.forEach(function (operator) { + operatorMap[operator.code] = operator.name + }) + + return { + condsConfig: condsConfig, + params: this.vParams, + paramMap: paramMap, + operatorMap: operatorMap, + operator: "", + + isAdding: false, + + paramCode: "", + param: null, + + valueBandwidth: { + count: 100, + unit: "mb" + }, + valueTraffic: { + count: 1, + unit: "gb" + }, + valueCPU: 80, + valueMemory: 90, + valueLoad: 20, + valueRate: 0 + } + }, + watch: { + paramCode: function (code) { + if (code.length == 0) { + this.param = null + } else { + this.param = this.params.$find(function (k, v) { + return v.code == code + }) + } + this.$emit("changeparam", this.param) + } + }, + methods: { + add: function () { + this.isAdding = true + }, + confirm: function () { + if (this.param == null) { + teaweb.warn("请选择参数") + return + } + if (this.param.operators != null && this.param.operators.length > 0 && this.operator.length == 0) { + teaweb.warn("请选择操作符") + return + } + if (this.param.operators == null || this.param.operators.length == 0) { + this.operator = "" + } + + let value = null + switch (this.param.valueType) { + case "bandwidth": { + if (this.valueBandwidth.unit.length == 0) { + teaweb.warn("请选择带宽单位") + return + } + let count = parseInt(this.valueBandwidth.count.toString()) + if (isNaN(count)) { + count = 0 + } + if (count < 0) { + count = 0 + } + value = { + count: count, + unit: this.valueBandwidth.unit + } + } + break + case "traffic": { + if (this.valueTraffic.unit.length == 0) { + teaweb.warn("请选择带宽单位") + return + } + let count = parseInt(this.valueTraffic.count.toString()) + if (isNaN(count)) { + count = 0 + } + if (count < 0) { + count = 0 + } + value = { + count: count, + unit: this.valueTraffic.unit + } + } + break + case "cpu": + let cpu = parseInt(this.valueCPU.toString()) + if (isNaN(cpu)) { + cpu = 0 + } + if (cpu < 0) { + cpu = 0 + } + if (cpu > 100) { + cpu = 100 + } + value = cpu + break + case "memory": + let memory = parseInt(this.valueMemory.toString()) + if (isNaN(memory)) { + memory = 0 + } + if (memory < 0) { + memory = 0 + } + if (memory > 100) { + memory = 100 + } + value = memory + break + case "load": + let load = parseInt(this.valueLoad.toString()) + if (isNaN(load)) { + load = 0 + } + if (load < 0) { + load = 0 + } + value = load + break + case "rate": + let rate = parseInt(this.valueRate.toString()) + if (isNaN(rate)) { + rate = 0 + } + if (rate < 0) { + rate = 0 + } + value = rate + break + } + + this.condsConfig.conds.push({ + param: this.param.code, + operator: this.operator, + value: value + }) + this.formatConds(this.condsConfig.conds) + + this.cancel() + }, + cancel: function () { + this.isAdding = false + this.paramCode = "" + this.param = null + }, + remove: function (index) { + this.condsConfig.conds.$remove(index) + }, + formatConds: function (conds) { + let that = this + conds.forEach(function (cond) { + switch (that.paramMap[cond.param].valueType) { + case "bandwidth": + cond.valueFormat = cond.value.count + cond.value.unit[0].toUpperCase() + cond.value.unit.substring(1) + "ps" + return + case "traffic": + cond.valueFormat = cond.value.count + cond.value.unit.toUpperCase() + return + case "cpu": + cond.valueFormat = cond.value + "%" + return + case "memory": + cond.valueFormat = cond.value + "%" + return + case "load": + cond.valueFormat = cond.value + return + case "rate": + cond.valueFormat = cond.value + "/秒" + return + } + }) + } + }, + template: `
+ + + +
+ + + {{paramMap[cond.param].name}} + {{operatorMap[cond.operator]}} {{cond.valueFormat}} +   + + +    + +
+ +
+ + + + + + + + + + + + + + + +
参数 + +

{{param.description}}

+
操作符 + +
{{param.valueName}} + +
+
+
+ +
+
+ +
+
+
+ + +
+
+
+ +
+
+ +
+
+
+ + +
+
+ + % +
+
+ + +
+
+ + % +
+
+ + +
+ +
+ + +
+
+ + /秒 +
+
+
+   取消 +
+ +
+ +
+
` +}) + +Vue.component("node-schedule-conds-viewer", { + props: ["value", "v-params", "v-operators"], + mounted: function () { + this.formatConds(this.condsConfig.conds) + this.$forceUpdate() + }, + data: function () { + let paramMap = {} + this.vParams.forEach(function (param) { + paramMap[param.code] = param + }) + + let operatorMap = {} + this.vOperators.forEach(function (operator) { + operatorMap[operator.code] = operator.name + }) + + return { + condsConfig: this.value, + paramMap: paramMap, + operatorMap: operatorMap + } + }, + methods: { + formatConds: function (conds) { + let that = this + conds.forEach(function (cond) { + switch (that.paramMap[cond.param].valueType) { + case "bandwidth": + cond.valueFormat = cond.value.count + cond.value.unit[0].toUpperCase() + cond.value.unit.substring(1) + "ps" + return + case "traffic": + cond.valueFormat = cond.value.count + cond.value.unit.toUpperCase() + return + case "cpu": + cond.valueFormat = cond.value + "%" + return + case "memory": + cond.valueFormat = cond.value + "%" + return + case "load": + cond.valueFormat = cond.value + return + case "rate": + cond.valueFormat = cond.value + "/秒" + return + } + }) + } + }, + template: `
+ + + {{paramMap[cond.param].name}} + {{operatorMap[cond.operator]}} {{cond.valueFormat}} + + +    + +
` +}) + +Vue.component("ns-access-log-box", { + props: ["v-access-log", "v-keyword"], + data: function () { + let accessLog = this.vAccessLog + let isFailure = false + + if (accessLog.isRecursive) { + if (accessLog.recordValue == null || accessLog.recordValue.length == 0) { + isFailure = true + } + } else { + if (accessLog.recordType == "SOA" || accessLog.recordType == "NS") { + if (accessLog.recordValue == null || accessLog.recordValue.length == 0) { + isFailure = true + } + } + + // 没有找到记录的不需要高亮显示,防止管理员看到红色就心理恐慌 + } + + return { + accessLog: accessLog, + isFailure: isFailure + } + }, + methods: { + showLog: function () { + let that = this + let requestId = this.accessLog.requestId + this.$parent.$children.forEach(function (v) { + if (v.deselect != null) { + v.deselect() + } + }) + this.select() + + teaweb.popup("/ns/clusters/accessLogs/viewPopup?requestId=" + requestId, { + width: "50em", + height: "24em", + onClose: function () { + that.deselect() + } + }) + }, + select: function () { + this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)" + }, + deselect: function () { + this.$refs.box.parentNode.style.cssText = "" + } + }, + template: `
+ [{{accessLog.region}}] {{accessLog.remoteAddr}} [{{accessLog.timeLocal}}] [{{accessLog.networking}}] {{accessLog.questionType}} {{accessLog.questionName}} -> + + {{accessLog.recordType}} {{accessLog.recordValue}} +  [没有记录] + + +
+ 线路: {{route.name}} + 递归DNS +
+
+ 错误:[{{accessLog.error}}] +
+
` +}) + +Vue.component("ns-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-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("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-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-domain-group-selector", { props: ["v-domain-group-id"], data: function () { @@ -1221,169 +7170,423 @@ Vue.component("ns-domain-group-selector", { ` }) -// 选择多个线路 -Vue.component("ns-routes-selector", { - props: ["v-routes", "name"], - mounted: function () { - let that = this - Tea.action("/ns/routes/options") - .post() - .success(function (resp) { - that.routes = resp.data.routes - - // provinces - let provinces = {} - if (resp.data.provinces != null && resp.data.provinces.length > 0) { - for (const province of resp.data.provinces) { - let countryCode = province.countryCode - if (typeof provinces[countryCode] == "undefined") { - provinces[countryCode] = [] - } - provinces[countryCode].push({ - name: province.name, - code: province.code - }) - } - } - that.provinces = provinces - }) - }, +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 selectedRoutes = this.vRoutes - if (selectedRoutes == null) { - selectedRoutes = [] + 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: [] + } + } } - let inputName = this.name - if (typeof inputName != "string" || inputName.length == 0) { - inputName = "routeCodes" + // 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 { - routeCode: "default", - inputName: inputName, - routes: [], + config: config, + defaultConfigs: this.vDefaultConfigs, + isNode: this.vIsNode, - provinces: {}, // country code => [ province1, province2, ... ] - provinceRouteCode: "", - - isAdding: false, - routeType: "default", - selectedRoutes: selectedRoutes, - } - }, - watch: { - routeType: function (v) { - this.routeCode = "" - let that = this - this.routes.forEach(function (route) { - if (route.type == v && that.routeCode.length == 0) { - that.routeCode = route.code - } - }) + isAddingPort: false } }, methods: { - add: function () { - this.isAdding = true - this.routeType = "default" - this.routeCode = "default" - this.provinceRouteCode = "" - this.$emit("add") + changeTCPPorts: function (ports) { + this.config.tcp.ports = ports }, - cancel: function () { - this.isAdding = false - this.$emit("cancel") - }, - confirm: function () { - if (this.routeCode.length == 0) { - return - } - - let that = this - - // route - let selectedRoute = null - for (const route of this.routes) { - if (route.code == this.routeCode) { - selectedRoute = route - break - } - } - - if (selectedRoute != null) { - // province route - if (this.provinceRouteCode.length > 0 && this.provinces[this.routeCode] != null) { - for (const province of this.provinces[this.routeCode]) { - if (province.code == this.provinceRouteCode) { - selectedRoute = { - name: selectedRoute.name + "-" + province.name, - code: province.code - } - break - } - } - } - - that.selectedRoutes.push(selectedRoute) - } - - this.$emit("change", this.selectedRoutes) - this.cancel() - }, - remove: function (index) { - this.selectedRoutes.$remove(index) - this.$emit("change", this.selectedRoutes) + changeTCPAllowIPList: function (ipList) { + this.config.tcp.allowIPList = ipList } - } - , + }, template: `
-
-
- - {{route.name}}   -
-
-
-
- + + +

功能说明:此功能为试验性质,目前仅能防御简单的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-record-health-check-config-box", { + props:["value", "v-parent-config"], + data: function () { + let config = this.value + if (config == null) { + config = { + isOn: false, + port: 0, + timeoutSeconds: 0, + countUp: 0, + countDown: 0 + } + } + + let parentConfig = this.vParentConfig + + return { + config: config, + portString: config.port.toString(), + timeoutSecondsString: config.timeoutSeconds.toString(), + countUpString: config.countUp.toString(), + countDownString: config.countDown.toString(), + + portIsEditing: config.port > 0, + timeoutSecondsIsEditing: config.timeoutSeconds > 0, + countUpIsEditing: config.countUp > 0, + countDownIsEditing: config.countDown > 0, + + parentConfig: parentConfig + } + }, + watch: { + portString: function (value) { + let port = parseInt(value.toString()) + if (isNaN(port) || port > 65535 || port < 1) { + this.config.port = 0 + } else { + this.config.port = port + } + }, + timeoutSecondsString: function (value) { + let timeoutSeconds = parseInt(value.toString()) + if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) { + this.config.timeoutSeconds = 0 + } else { + this.config.timeoutSeconds = timeoutSeconds + } + }, + countUpString: function (value) { + let countUp = parseInt(value.toString()) + if (isNaN(countUp) || countUp > 1000 || countUp < 1) { + this.config.countUp = 0 + } else { + this.config.countUp = countUp + } + }, + countDownString: function (value) { + let countDown = parseInt(value.toString()) + if (isNaN(countDown) || countDown > 1000 || countDown < 1) { + this.config.countDown = 0 + } else { + this.config.countDown = countDown + } + } + }, + template: `
+ + + - + + + + + + + - + - - + + -
选择类型 *启用当前记录健康检查 - + +
检测端口 + + 默认{{parentConfig.port}} +   [修改] + +
+ + +

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

+
选择线路 *超时时间 - + + 默认{{parentConfig.timeoutSeconds}}秒 +   [修改] + +
+ +
+ + +
+
选择省/州
默认连续上线次数 - + + 默认{{parentConfig.countUp}}次 +   [修改] + +
+ +
+ + +
+

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

+
-
- -   取消 -
-
- + + 默认连续下线次数 + + + 默认{{parentConfig.countDown}}次 +   [修改] + +
+
+ [使用默认] +
+
+ + +
+

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

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

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

+
默认检测端口 + +

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

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

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

+
默认连续下线次数 +
+ + +
+

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

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

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

-
包含未添加的域名 - -

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

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

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

-
默认检测端口 - -

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

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

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

-
默认连续下线次数 -
- - -
-

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

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

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

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

TCP设置

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用 - -
单节点TCP最大连接数 - -

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

-
单IP TCP最大连接数 - -

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

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

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

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

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

-
TCP端口列表 - -

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

-
IP白名单 - -

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

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

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

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

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

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

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

-
-
-
-
` -}) - -Vue.component("ns-create-records-table", { - props: ["v-types"], - data: function () { - let types = this.vTypes - if (types == null) { - types = [] - } - return { - types: types, - records: [ - { - name: "", - type: "A", - value: "", - routeCodes: [], - ttl: 600, - index: 0 - } - ], - lastIndex: 0, - isAddingRoutes: false // 是否正在添加线路 - } - }, - methods: { - add: function () { - this.records.push({ - name: "", - type: "A", - value: "", - routeCodes: [], - ttl: 600, - index: ++this.lastIndex - }) - let that = this - setTimeout(function () { - that.$refs.nameInputs.$last().focus() - }, 100) - }, - remove: function (index) { - this.records.$remove(index) - }, - addRoutes: function () { - this.isAddingRoutes = true - }, - cancelRoutes: function () { - let that = this - setTimeout(function () { - that.isAddingRoutes = false - }, 1000) - }, - changeRoutes: function (record, routes) { - if (routes == null) { - record.routeCodes = [] - } else { - record.routeCodes = routes.map(function (route) { - return route.code - }) - } - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - -
记录名记录类型线路记录值TTL操作
- - - - - - - - -
- - -
-
- -
- -
`, -}) - // 选择单一线路 Vue.component("ns-route-selector", { props: ["v-route-code"], @@ -2742,6 +8373,172 @@ Vue.component("ns-route-selector", { ` }) +// 选择多个线路 +Vue.component("ns-routes-selector", { + props: ["v-routes", "name"], + mounted: function () { + let that = this + Tea.action("/ns/routes/options") + .post() + .success(function (resp) { + that.routes = resp.data.routes + + // provinces + let provinces = {} + if (resp.data.provinces != null && resp.data.provinces.length > 0) { + for (const province of resp.data.provinces) { + let countryCode = province.countryCode + if (typeof provinces[countryCode] == "undefined") { + provinces[countryCode] = [] + } + provinces[countryCode].push({ + name: province.name, + code: province.code + }) + } + } + that.provinces = provinces + }) + }, + data: function () { + let selectedRoutes = this.vRoutes + if (selectedRoutes == null) { + selectedRoutes = [] + } + + let inputName = this.name + if (typeof inputName != "string" || inputName.length == 0) { + inputName = "routeCodes" + } + + return { + routeCode: "default", + inputName: inputName, + routes: [], + + provinces: {}, // country code => [ province1, province2, ... ] + provinceRouteCode: "", + + isAdding: false, + routeType: "default", + selectedRoutes: selectedRoutes, + } + }, + watch: { + routeType: function (v) { + this.routeCode = "" + let that = this + this.routes.forEach(function (route) { + if (route.type == v && that.routeCode.length == 0) { + that.routeCode = route.code + } + }) + } + }, + methods: { + add: function () { + this.isAdding = true + this.routeType = "default" + this.routeCode = "default" + this.provinceRouteCode = "" + this.$emit("add") + }, + cancel: function () { + this.isAdding = false + this.$emit("cancel") + }, + confirm: function () { + if (this.routeCode.length == 0) { + return + } + + let that = this + + // route + let selectedRoute = null + for (const route of this.routes) { + if (route.code == this.routeCode) { + selectedRoute = route + break + } + } + + if (selectedRoute != null) { + // province route + if (this.provinceRouteCode.length > 0 && this.provinces[this.routeCode] != null) { + for (const province of this.provinces[this.routeCode]) { + if (province.code == this.provinceRouteCode) { + selectedRoute = { + name: selectedRoute.name + "-" + province.name, + code: province.code + } + break + } + } + } + + that.selectedRoutes.push(selectedRoute) + } + + this.$emit("change", this.selectedRoutes) + this.cancel() + }, + remove: function (index) { + this.selectedRoutes.$remove(index) + this.$emit("change", this.selectedRoutes) + } + } + , + template: `
+
+
+ + {{route.name}}   +
+
+
+
+ + + + + + + + + + + + + +
选择类型 * + +
选择线路 * + +
选择省/州 + +
+
+ +   取消 +
+
+ +
` +}) + Vue.component("ns-user-selector", { props: ["v-user-id"], data: function () { @@ -2757,150 +8554,237 @@ Vue.component("ns-user-selector", { ` }) -Vue.component("ns-access-log-box", { - props: ["v-access-log", "v-keyword"], +Vue.component("plan-bandwidth-limit-view", { + props: ["value"], + template: `
+ 带宽限制: +
` +}) + +Vue.component("plan-bandwidth-ranges", { + props: ["value"], 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 - } - } - - // 没有找到记录的不需要高亮显示,防止管理员看到红色就心理恐慌 + let ranges = this.value + if (ranges == null) { + ranges = [] } - return { - accessLog: accessLog, - isFailure: isFailure + ranges: ranges, + isAdding: false, + + minMB: "", + minMBUnit: "mb", + + maxMB: "", + maxMBUnit: "mb", + + pricePerMB: "", + totalPrice: "", + addingRange: { + minMB: 0, + maxMB: 0, + pricePerMB: 0, + totalPrice: 0 + } } }, methods: { - showLog: function () { + add: function () { + this.isAdding = !this.isAdding 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() - } + setTimeout(function () { + that.$refs.minMB.focus() }) }, - select: function () { - this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)" + cancelAdding: function () { + this.isAdding = false }, - 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) + confirm: function () { + if (this.addingRange.minMB < 0) { + teaweb.warn("带宽下限需要大于0") + return } + if (this.addingRange.maxMB < 0) { + teaweb.warn("带宽上限需要大于0") + return + } + if (this.addingRange.pricePerMB <= 0) { + teaweb.warn("请设置单位价格或者总价格") + return + } + + this.isAdding = false + this.minMB = "" + this.maxMB = "" + this.pricePerMB = "" + this.totalPrice = "" + this.ranges.push(this.addingRange) + this.ranges.$sort(function (v1, v2) { + if (v1.minMB < v2.minMB) { + return -1 + } + if (v1.minMB == v2.minMB) { + if (v2.maxMB == 0 || v1.maxMB < v2.maxMB) { + return -1 + } + return 0 + } + return 1 + }) + this.change() + this.addingRange = { + minMB: 0, + maxMB: 0, + pricePerMB: 0, + totalPrice: 0 + } + }, + remove: function (index) { + this.ranges.$remove(index) + this.change() + }, + change: function () { + this.$emit("change", this.ranges) + }, + formatMB: function (mb) { + return teaweb.formatBits(mb * 1024 * 1024) + }, + changeMinMB: function (v) { + let minMB = parseFloat(v.toString()) + if (isNaN(minMB) || minMB < 0) { + minMB = 0 + } + switch (this.minMBUnit) { + case "gb": + minMB *= 1024 + break + case "tb": + minMB *= 1024 * 1024 + break + } + this.addingRange.minMB = minMB + }, + changeMaxMB: function (v) { + let maxMB = parseFloat(v.toString()) + if (isNaN(maxMB) || maxMB < 0) { + maxMB = 0 + } + switch (this.maxMBUnit) { + case "gb": + maxMB *= 1024 + break + case "tb": + maxMB *= 1024 * 1024 + break + } + this.addingRange.maxMB = maxMB } }, - template: `
- -
` -}) - -Vue.component("plan-user-selector", { - props: ["v-user-id"], - data: function () { - return {} - }, - methods: { - change: function (userId) { - this.$emit("change", userId) + watch: { + minMB: function (v) { + this.changeMinMB(v) + }, + minMBUnit: function () { + this.changeMinMB(this.minMB) + }, + maxMB: function (v) { + this.changeMaxMB(v) + }, + maxMBUnit: function () { + this.changeMaxMB(this.maxMB) + }, + pricePerMB: function (v) { + let pricePerMB = parseFloat(v.toString()) + if (isNaN(pricePerMB) || pricePerMB < 0) { + pricePerMB = 0 + } + this.addingRange.pricePerMB = pricePerMB + }, + totalPrice: function (v) { + let totalPrice = parseFloat(v.toString()) + if (isNaN(totalPrice) || totalPrice < 0) { + totalPrice = 0 + } + this.addingRange.totalPrice = totalPrice } }, template: `
- + +
+
+ {{formatMB(range.minMB)}} - {{formatMB(range.maxMB)}}   价格:{{range.totalPrice}}元{{range.pricePerMB}}元/Mbps +   +
+
+
+ + +
+ + + + + + + + + + + + + + + + + +
带宽下限 * +
+
+ +
+
+ +
+
+
带宽上限 * +
+
+ +
+
+ +
+
+

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

+
单位价格 +
+ + 元/Mbps +
+

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

+
总价格 +
+ + +
+

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

+
+   + +
+ + +
+ +
` }) @@ -2946,38 +8830,112 @@ Vue.component("plan-limit-view", { ` }) -Vue.component("plan-price-view", { - props: ["v-plan"], +Vue.component("plan-price-bandwidth-config-box", { + props: ["v-plan-price-bandwidth-config"], data: function () { + let config = this.vPlanPriceBandwidthConfig + if (config == null) { + config = { + percentile: 95, + base: 0, + ranges: [], + supportRegions: false + } + } + + if (config.ranges == null) { + config.ranges = [] + } + return { - plan: this.vPlan + config: config, + bandwidthPercentile: config.percentile, + priceBase: config.base, + isEditing: false + } + }, + watch: { + priceBase: function (v) { + let f = parseFloat(v) + if (isNaN(f) || f < 0) { + this.config.base = 0 + } else { + this.config.base = f + } + }, + bandwidthPercentile: function (v) { + let i = parseInt(v) + if (isNaN(i) || i < 0) { + this.config.percentile = 0 + } else { + this.config.percentile = i + } + } + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing } }, template: `
- - 按时间周期计费 -
- - 月度:¥{{plan.monthlyPrice}}元
- 季度:¥{{plan.seasonallyPrice}}元
- 年度:¥{{plan.yearlyPrice}}元 -
-
-
- - 按流量计费 -
- 基础价格:¥{{plan.trafficPrice.base}}元/GiB -
-
-
- 按{{plan.bandwidthPrice.percentile}}th带宽计费 -
-
- {{range.minMB}} - {{range.maxMB}}MiB{{range.totalPrice}}元{{range.pricePerMB}}元/MiB -
-
-
+ +
+ 带宽百分位:{{config.percentile}}th没有设置   |   + 基础带宽价格:{{config.base}}元/Mbps没有设置   |   + 阶梯价格:{{config.ranges.length}}段没有设置   |  支持区域带宽计费 +  |  使用平均带宽算法 +
+ 修改 +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
带宽百分位 * +
+ + th +
+

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

+
基础带宽费用 +
+ + 元/Mbps +
+

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

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

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

+
带宽算法 + +

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

+

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

+
+
` }) @@ -3281,346 +9239,38 @@ Vue.component("plan-price-traffic-config-box", { ` }) -Vue.component("plan-bandwidth-limit-view", { - props: ["value"], - template: `
- 带宽限制: -
` -}) - -Vue.component("plan-bandwidth-ranges", { - props: ["value"], +Vue.component("plan-price-view", { + props: ["v-plan"], data: function () { - let ranges = this.value - if (ranges == null) { - ranges = [] - } return { - ranges: ranges, - isAdding: false, - - minMB: "", - minMBUnit: "mb", - - maxMB: "", - maxMBUnit: "mb", - - pricePerMB: "", - totalPrice: "", - addingRange: { - minMB: 0, - maxMB: 0, - pricePerMB: 0, - totalPrice: 0 - } - } - }, - methods: { - add: function () { - this.isAdding = !this.isAdding - let that = this - setTimeout(function () { - that.$refs.minMB.focus() - }) - }, - cancelAdding: function () { - this.isAdding = false - }, - confirm: function () { - if (this.addingRange.minMB < 0) { - teaweb.warn("带宽下限需要大于0") - return - } - if (this.addingRange.maxMB < 0) { - teaweb.warn("带宽上限需要大于0") - return - } - if (this.addingRange.pricePerMB <= 0) { - teaweb.warn("请设置单位价格或者总价格") - return - } - - this.isAdding = false - this.minMB = "" - this.maxMB = "" - this.pricePerMB = "" - this.totalPrice = "" - this.ranges.push(this.addingRange) - this.ranges.$sort(function (v1, v2) { - if (v1.minMB < v2.minMB) { - return -1 - } - if (v1.minMB == v2.minMB) { - if (v2.maxMB == 0 || v1.maxMB < v2.maxMB) { - return -1 - } - return 0 - } - return 1 - }) - this.change() - this.addingRange = { - minMB: 0, - maxMB: 0, - pricePerMB: 0, - totalPrice: 0 - } - }, - remove: function (index) { - this.ranges.$remove(index) - this.change() - }, - change: function () { - this.$emit("change", this.ranges) - }, - formatMB: function (mb) { - return teaweb.formatBits(mb * 1024 * 1024) - }, - changeMinMB: function (v) { - let minMB = parseFloat(v.toString()) - if (isNaN(minMB) || minMB < 0) { - minMB = 0 - } - switch (this.minMBUnit) { - case "gb": - minMB *= 1024 - break - case "tb": - minMB *= 1024 * 1024 - break - } - this.addingRange.minMB = minMB - }, - changeMaxMB: function (v) { - let maxMB = parseFloat(v.toString()) - if (isNaN(maxMB) || maxMB < 0) { - maxMB = 0 - } - switch (this.maxMBUnit) { - case "gb": - maxMB *= 1024 - break - case "tb": - maxMB *= 1024 * 1024 - break - } - this.addingRange.maxMB = maxMB - } - }, - watch: { - minMB: function (v) { - this.changeMinMB(v) - }, - minMBUnit: function () { - this.changeMinMB(this.minMB) - }, - maxMB: function (v) { - this.changeMaxMB(v) - }, - maxMBUnit: function () { - this.changeMaxMB(this.maxMB) - }, - pricePerMB: function (v) { - let pricePerMB = parseFloat(v.toString()) - if (isNaN(pricePerMB) || pricePerMB < 0) { - pricePerMB = 0 - } - this.addingRange.pricePerMB = pricePerMB - }, - totalPrice: function (v) { - let totalPrice = parseFloat(v.toString()) - if (isNaN(totalPrice) || totalPrice < 0) { - totalPrice = 0 - } - this.addingRange.totalPrice = totalPrice + plan: this.vPlan } }, template: `
- -
-
- {{formatMB(range.minMB)}} - {{formatMB(range.maxMB)}}   价格:{{range.totalPrice}}元{{range.pricePerMB}}元/Mbps -   + + 按时间周期计费 +
+ + 月度:¥{{plan.monthlyPrice}}元
+ 季度:¥{{plan.seasonallyPrice}}元
+ 年度:¥{{plan.yearlyPrice}}元 +
+
+
+ + 按流量计费 +
+ 基础价格:¥{{plan.trafficPrice.base}}元/GiB +
+
+
+ 按{{plan.bandwidthPrice.percentile}}th带宽计费 +
+
+ {{range.minMB}} - {{range.maxMB}}MiB{{range.totalPrice}}元{{range.pricePerMB}}元/MiB +
-
- - -
- - - - - - - - - - - - - - - - - -
带宽下限 * -
-
- -
-
- -
-
-
带宽上限 * -
-
- -
-
- -
-
-

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

-
单位价格 -
- - 元/Mbps -
-

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

-
总价格 -
- - -
-

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

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

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

-
基础带宽费用 -
- - 元/Mbps -
-

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

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

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

-
带宽算法 - -

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

-

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

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

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

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

SSL/TLS相关配置

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

点击可选套件添加。

-
-
开启HSTS -
- - -
-

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

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

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

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

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

-
OCSP Stapling -

选中表示启用OCSP Stapling。

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

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

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

暂时还没有缓存条件。

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

暂时还没有URL跳转规则。

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

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

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

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

+
SMTP端口 * + +

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

+
用户名 * + +

通常为发件人邮箱地址。

+
密码 * + +

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

+
发件人Email * + +

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

+
发件人名称 + +

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

+
发送测试[点此测试]
+
+
` +}) + +Vue.component("sms-sender", { + props: ["value", "name"], + mounted: function () { + this.initType(this.config.type) + }, + data: function () { + let config = this.value + if (config == null) { + config = { + isOn: false, + type: "webHook", + webHookParams: { + url: "", + method: "POST" + }, + aliyunSMSParams: { + sign: "", + templateCode: "", + codeVarName: "code", + accessKeyId: "", + accessKeySecret: "" + }, + tencentSMSParams: { + sdkAppId: "", + sign: "", + templateId: "", + accessKeyId: "", + accessKeySecret: "" + } + } + } + + if (config.aliyunSMSParams == null) { + Vue.set(config, "aliyunSMSParams", { + sign: "", + templateCode: "", + codeVarName: "code", + accessKeyId: "", + accessKeySecret: "" + }) + } + if (config.tencentSMSParams == null) { + Vue.set(config, "tencentSMSParams", { + sdkAppId: "", + sign: "", + templateId: "", + accessKeyId: "", + accessKeySecret: "" + }) + } + + return { + config: config + } + }, + watch: { + "config.type": function (v) { + this.initType(v) + } + }, + methods: { + initType: function (v) { + // initialize params + switch (v) { + case "webHook": + if (this.config.webHookParams == null) { + this.config.webHookParams = { + url: "", + method: "POST" + } + } + break + } + }, + test: function () { + window.TESTING_SMS_CONFIG = this.config + teaweb.popup("/users/setting/smsTest", { + height: "22em" + }) + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用
发送渠道 + +

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

+

通过阿里云短信服务发送短信接口;目前仅支持发送验证码

+

通过腾讯云短信服务发送短信接口;目前仅支持发送验证码

+
HTTP接口的URL地址 * + +

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

+
HTTP接口的请求方法 + +

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

+

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

+
签名名称 * +

在阿里云短信服务 “签名管理” 中添加并通过审核后才能使用。

+
模板CODE * + +

在阿里云短信服务 “模板管理” 中添加并通过审核后才能使用。

+
模板中验证码变量名称 * + +

默认为code,不需要带\${}等符号,即表示在模板中使用\${{{ config.aliyunSMSParams.codeVarName }}}代表要发送的验证码。

+
AccessKey ID * + +

在阿里云 -- RAM访问控制 -- AccessKey中可以创建和获取。

+
AccessKey Secret * + +

和表单中的AccessKey ID对应,在阿里云 -- RAM访问控制 -- AccessKey中可以创建和获取。

+
SDK应用ID * + +

在腾讯云 -- 短信 -- 应用管理 -- 应用列表中可以查看。

+
签名内容 * + +

比如“腾讯云”,在腾讯云 -- 短信 -- 签名管理中可以查看。

+
正文模板ID * + +

在腾讯云 -- 短信 -- 正文模板管理中可以查看。

+
密钥SecretId * + +

同SecretKey一同在腾讯云 -- 访问管理 -- API密钥管理中获取。

+
密钥SecretKey * + +

同SecretId一同在腾讯云 -- 访问管理 -- API密钥管理中获取。

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

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

+

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

+
+
+
+ +
+
` +}) + +Vue.component("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("firewall-syn-flood-config-box", { + props: ["v-syn-flood-config"], + data: function () { + let config = this.vSynFloodConfig + if (config == null) { + config = { + isOn: false, + minAttempts: 10, + timeoutSeconds: 600, + ignoreLocal: true + } + } + return { + config: config, + isEditing: false, + minAttempts: config.minAttempts, + timeoutSeconds: config.timeoutSeconds + } + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + } + }, + watch: { + minAttempts: function (v) { + let count = parseInt(v) + if (isNaN(count)) { + count = 10 + } + if (count < 5) { + count = 5 + } + this.config.minAttempts = count + }, + timeoutSeconds: function (v) { + let seconds = parseInt(v) + if (isNaN(seconds)) { + seconds = 10 + } + if (seconds < 60) { + seconds = 60 + } + this.config.timeoutSeconds = seconds + } + }, + template: `
+ + + + 已启用 / 空连接次数:{{config.minAttempts}}次/分钟 / 封禁时长:{{config.timeoutSeconds}}秒 / 忽略局域网访问 + + 未启用 + + + + + + + + + + + + + + + + + + + + +
启用 + +

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

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

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

+
封禁时长 +
+ + +
+
忽略局域网访问 + +
+
` +}) + +Vue.component("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("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-access-log-config-box", { + props: ["v-access-log-config", "v-fields", "v-default-field-codes", "v-is-location", "v-is-group"], + data: function () { + let that = this + + // 初始化 + setTimeout(function () { + that.changeFields() + }, 100) + + let accessLog = { + isPrior: false, + isOn: false, + fields: [1, 2, 6, 7], + status1: true, + status2: true, + status3: true, + status4: true, + status5: true, + + firewallOnly: false, + enableClientClosed: false + } + if (this.vAccessLogConfig != null) { + accessLog = this.vAccessLogConfig + } + + this.vFields.forEach(function (v) { + if (that.vAccessLogConfig == null) { // 初始化默认值 + v.isChecked = that.vDefaultFieldCodes.$contains(v.code) + } else { + v.isChecked = accessLog.fields.$contains(v.code) + } + }) + + return { + accessLog: accessLog, + hasRequestBodyField: this.vFields.$contains(8), + showAdvancedOptions: false + } + }, + methods: { + changeFields: function () { + this.accessLog.fields = this.vFields.filter(function (v) { + return v.isChecked + }).map(function (v) { + return v.code + }) + this.hasRequestBodyField = this.accessLog.fields.$contains(8) + }, + changeAdvanced: function (v) { + this.showAdvancedOptions = v + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用访问日志 +
+ + +
+
基础信息

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

高级信息 +
+ + +
+

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

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

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

+
+ +
+

WAF相关

+ + + + + +
只记录WAF相关日志 + +

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

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

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

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

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

+
主域名 * +
+
+ +
+
+ +
+
+

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

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

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

+
添加X-Cache报头 + +

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

+
添加Age Header + +

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

+
支持源站控制有效时间 + +

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

+
允许PURGE + +

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

+
PURGE Key * + +

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

+
+ +
+

过时缓存策略

+ +
+ +
+ +
+
+ +
+

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

+
` }) +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-cache-ref-box", { props: ["v-cache-ref", "v-is-reverse"], @@ -5543,1522 +12017,81 @@ Vue.component("http-cache-ref-box", { ` }) -// 请求限制 -Vue.component("http-request-limit-config-box", { - props: ["v-request-limit-config", "v-is-group", "v-is-location"], +// 缓存条件列表 +Vue.component("http-cache-refs-box", { + props: ["v-cache-refs"], 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" - } - } + let refs = this.vCacheRefs + if (refs == null) { + refs = [] } 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 - } + refs: refs } }, methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用请求限制 - -
最大并发连接数 - -

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

-
单IP最大并发连接数 - -

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

-
单连接带宽限制 - -

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

-
单请求最大尺寸 - -

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

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

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

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

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

-
人机识别验证方式 - -

使用系统默认的设置。你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

-

{{captchaType.description}}你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

-
启用系统全局规则 - -

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

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

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

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

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

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

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

-
主域名 * -
-
- -
-
- -
-
-

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

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

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

-
添加X-Cache报头 - -

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

-
添加Age Header - -

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

-
支持源站控制有效时间 - -

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

-
允许PURGE - -

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

-
PURGE Key * - -

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

-
- -
-

过时缓存策略

- -
- -
- -
-
- -
-

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

- +

暂时还没有缓存条件。

+
+ + + + + + + + + + + +
缓存条件缓存时间
+ + + + + 忽略URI参数 + + {{cacheRef.minSize.count}}{{cacheRef.minSize.unit}} + - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} + + 0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} + {{cacheRef.methods.join(", ")}} + Expires + 状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}} + 分片缓存 + Range回源 + If-None-Match + If-Modified-Since + 支持异步 + + {{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}} + 不缓存 +
` }) -// 通用Header长度 -let defaultGeneralHeaders = ["Cache-Control", "Connection", "Date", "Pragma", "Trailer", "Transfer-Encoding", "Upgrade", "Via", "Warning"] -Vue.component("http-cond-general-header-length", { - props: ["v-checkpoint"], - data: function () { - let headers = null - let length = null - - if (window.parent.UPDATING_RULE != null) { - let options = window.parent.UPDATING_RULE.checkpointOptions - if (options.headers != null && Array.$isArray(options.headers)) { - headers = options.headers - } - if (options.length != null) { - length = options.length - } - } - - - if (headers == null) { - headers = defaultGeneralHeaders - } - - if (length == null) { - length = 128 - } - - let that = this - setTimeout(function () { - that.change() - }, 100) - - return { - headers: headers, - length: length - } - }, - watch: { - length: function (v) { - let len = parseInt(v) - if (isNaN(len)) { - len = 0 - } - if (len < 0) { - len = 0 - } - this.length = len - this.change() - } - }, - methods: { - change: function () { - this.vCheckpoint.options = [ - { - code: "headers", - value: this.headers - }, - { - code: "length", - value: this.length - } - ] - } - }, - template: `
- - - - - - - - - -
通用Header列表 - -

需要检查的Header列表。

-
Header值超出长度 -
- - 字节 -
-

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

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

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

-
检查请求来源指纹 - -

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

-
忽略常用文件 - -

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

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

允许不带来源的访问。

-
来源域名允许一致 - -

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

-
允许的来源域名 - -

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

-
禁止的来源域名 - -

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

-
同时检查Origin - -

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

-
-
` -}) - -Vue.component("http-access-log-partitions-box", { - props: ["v-partition", "v-day", "v-query"], - mounted: function () { - let that = this - Tea.action("/servers/logs/partitionData") - .params({ - day: this.vDay - }) - .success(function (resp) { - that.partitions = [] - resp.data.partitions.reverse().forEach(function (v) { - that.partitions.push({ - code: v, - isDisabled: false, - hasLogs: false - }) - }) - if (that.partitions.length > 0) { - if (that.vPartition == null || that.vPartition < 0) { - that.selectedPartition = that.partitions[0].code - } - - if (that.partitions.length > 1) { - that.checkLogs() - } - } - }) - .post() - }, - data: function () { - return { - partitions: [], - selectedPartition: this.vPartition, - checkingPartition: 0 - } - }, - methods: { - url: function (p) { - let u = window.location.toString() - u = u.replace(/\?partition=-?\d+/, "?") - u = u.replace(/\?requestId=-?\d+/, "?") - u = u.replace(/&partition=-?\d+/, "") - u = u.replace(/&requestId=-?\d+/, "") - if (u.indexOf("?") > 0) { - u += "&partition=" + p - } else { - u += "?partition=" + p - } - return u - }, - disable: function (partition) { - this.partitions.forEach(function (p) { - if (p.code == partition) { - p.isDisabled = true - } - }) - }, - checkLogs: function () { - let that = this - let index = this.checkingPartition - let params = { - partition: index - } - let query = this.vQuery - if (query == null || query.length == 0) { - return - } - query.split("&").forEach(function (v) { - let param = v.split("=") - params[param[0]] = decodeURIComponent(param[1]) - }) - Tea.action("/servers/logs/hasLogs") - .params(params) - .post() - .success(function (response) { - if (response.data.hasLogs) { - // 因为是倒序,所以这里需要使用总长度减去index - that.partitions[that.partitions.length - 1 - index].hasLogs = true - } - - index++ - if (index >= that.partitions.length) { - return - } - that.checkingPartition = index - that.checkLogs() - }) - } - }, - template: `
-
- -
-
` -}) - Vue.component("http-cache-refs-config-box", { props: ["v-cache-refs", "v-cache-config", "v-cache-policy-id", "v-web-id", "v-max-bytes"], mounted: function () { @@ -7337,161 +12370,1554 @@ Vue.component("http-cache-refs-config-box", {
` }) -Vue.component("origin-list-box", { - props: ["v-primary-origins", "v-backup-origins", "v-server-type", "v-params"], +Vue.component("http-cache-stale-config", { + props: ["v-cache-stale-config"], data: function () { - return { - primaryOrigins: this.vPrimaryOrigins, - backupOrigins: this.vBackupOrigins - } - }, - methods: { - createPrimaryOrigin: function () { - teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, { - width: "45em", - height: "27em", - callback: function (resp) { - teaweb.success("保存成功", function () { - window.location.reload() - }) + let config = this.vCacheStaleConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + status: [], + supportStaleIfErrorHeader: true, + life: { + count: 1, + unit: "day" } - }) - }, - createBackupOrigin: function () { - teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, { - width: "45em", - height: "27em", - callback: function (resp) { - teaweb.success("保存成功", function () { - window.location.reload() - }) - } - }) - }, - updateOrigin: function (originId, originType) { - teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, { - width: "45em", - height: "27em", - callback: function (resp) { - teaweb.success("保存成功", function () { - window.location.reload() - }) - } - }) - }, - deleteOrigin: function (originId, originAddr, originType) { - let that = this - teaweb.confirm("确定要删除此源站(" + originAddr + ")吗?", function () { - Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType) - .post() - .success(function () { - teaweb.success("删除成功", function () { - window.location.reload() - }) - }) - }) - }, - updateOriginIsOn: function (originId, originAddr, isOn) { - let message - let resultMessage - if (isOn) { - message = "确定要启用此源站(" + originAddr + ")吗?" - resultMessage = "启用成功" - } else { - message = "确定要停用此源站(" + originAddr + ")吗?" - resultMessage = "停用成功" } - let that = this - teaweb.confirm(message, function () { - Tea.action("/servers/server/settings/origins/updateIsOn?" + that.vParams + "&originId=" + originId + "&isOn=" + (isOn ? 1 : 0)) - .post() - .success(function () { - teaweb.success(resultMessage, function () { - window.location.reload() - }) - }) - }) } - }, - template: `
-

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

-

暂时还没有主要源站。

- - -

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

-

暂时还没有备用源站。

- -
` -}) - -Vue.component("origin-list-table", { - props: ["v-origins", "v-origin-type"], - data: function () { - let hasMatchedDomains = false - let origins = this.vOrigins - if (origins != null && origins.length > 0) { - origins.forEach(function (origin) { - if (origin.domains != null && origin.domains.length > 0) { - hasMatchedDomains = true - } - }) - } - return { - hasMatchedDomains: hasMatchedDomains + config: config } }, - methods: { - deleteOrigin: function (originId, originAddr) { - this.$emit("deleteOrigin", originId, originAddr, this.vOriginType) - }, - updateOrigin: function (originId) { - this.$emit("updateOrigin", originId, this.vOriginType) - }, - updateOriginIsOn: function (originId, originAddr, isOn) { - this.$emit("updateOriginIsOn", originId, originAddr, isOn) + watch: { + config: { + deep: true, + handler: function () { + this.$emit("change", this.config) + } } }, - template: ` - - - - - - - - - + methods: {}, + template: `
源站地址权重状态操作
- - - + + + + + + + + + + + + +
- {{origin.addr}}   -
- 对象存储 - {{origin.name}} - 证书 - 主机名: {{origin.host}} - 端口跟随 - HTTP/2 - - 匹配: {{domain}} - 匹配: 所有域名 -
-
{{origin.weight}}
启用过时缓存 - + +

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

有效期 - 修改   - 停用启用   - 删除 + +

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

+
状态码 +

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

+
支持stale-if-error + +

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

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

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

+
例外URL + +

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

+
限制URL + +

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

+
忽略常用文件 + +

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

+
检查请求来源指纹 + +

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

+
启用GET302校验 + +

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

+
单IP最低QPS +
+ + 请求数/秒 +
+

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

+
使用自定义拦截阈值 + +
自定义拦截阈值设置 +
+
+ 单IP每5秒最多 + + 请求 +
+
+ +
+
+ 单IP每60秒 + + 请求 +
+
+
+
+ 单IP每300秒 + + 请求 +
+
+
+
+
` +}) + +Vue.component("http-charsets-box", { + props: ["v-usual-charsets", "v-all-charsets", "v-charset-config", "v-is-location", "v-is-group"], + data: function () { + let charsetConfig = this.vCharsetConfig + if (charsetConfig == null) { + charsetConfig = { + isPrior: false, + isOn: false, + charset: "", + isUpper: false, + force: false + } + } + return { + charsetConfig: charsetConfig, + advancedVisible: false + } + }, + methods: { + changeAdvancedVisible: function (v) { + this.advancedVisible = v + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
启用字符编码 +
+ + +
+
选择字符编码 +
强制替换 + +

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

+
字符编码大写 +
+ + +
+

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

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

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

+
例外扩展名 + +

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

+
支持的MimeType + +

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

+
压缩算法 +
+ + + +
+
+
+
+
+ + +
+
+
+ +

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

+
支持已压缩内容 + +

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

+
内容最小长度 + +

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

+
内容最大长度 + +

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

+
支持Partial
Content
+ +

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

+
例外URL + +

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

+
限制URL + +

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

+
匹配条件 + +
+
+
` +}) + +// URL扩展名条件 +Vue.component("http-cond-url-extension", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPathLowerExtension}", + operator: "in", + value: "[]" + } + if (this.vCond != null && this.vCond.param == cond.param) { + cond.value = this.vCond.value + } + + let extensions = [] + try { + extensions = JSON.parse(cond.value) + } catch (e) { + + } + + return { + cond: cond, + extensions: extensions, // TODO 可以拖动排序 + + isAdding: false, + addingExt: "" + } + }, + watch: { + extensions: function () { + this.cond.value = JSON.stringify(this.extensions) + } + }, + methods: { + addExt: function () { + this.isAdding = !this.isAdding + + if (this.isAdding) { + let that = this + setTimeout(function () { + that.$refs.addingExt.focus() + }, 100) + } + }, + cancelAdding: function () { + this.isAdding = false + this.addingExt = "" + }, + confirmAdding: function () { + // TODO 做更详细的校验 + // TODO 如果有重复的则提示之 + + if (this.addingExt.length == 0) { + return + } + + let that = this + this.addingExt.split(/[,;,;|]/).forEach(function (ext) { + ext = ext.trim() + if (ext.length > 0) { + if (ext[0] != ".") { + ext = "." + ext + } + ext = ext.replace(/\s+/g, "").toLowerCase() + that.extensions.push(ext) + } + }) + + // 清除状态 + this.cancelAdding() + }, + removeExt: function (index) { + this.extensions.$remove(index) + } + }, + template: `
+ +
+
{{ext}}
+
+
+
+
+ +
+
+ + +
+
+
+ +
+

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

+
` +}) + +// 排除URL扩展名条件 +Vue.component("http-cond-url-not-extension", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPathLowerExtension}", + operator: "not in", + value: "[]" + } + if (this.vCond != null && this.vCond.param == cond.param) { + cond.value = this.vCond.value + } + + let extensions = [] + try { + extensions = JSON.parse(cond.value) + } catch (e) { + + } + + return { + cond: cond, + extensions: extensions, // TODO 可以拖动排序 + + isAdding: false, + addingExt: "" + } + }, + watch: { + extensions: function () { + this.cond.value = JSON.stringify(this.extensions) + } + }, + methods: { + addExt: function () { + this.isAdding = !this.isAdding + + if (this.isAdding) { + let that = this + setTimeout(function () { + that.$refs.addingExt.focus() + }, 100) + } + }, + cancelAdding: function () { + this.isAdding = false + this.addingExt = "" + }, + confirmAdding: function () { + // TODO 做更详细的校验 + // TODO 如果有重复的则提示之 + + if (this.addingExt.length == 0) { + return + } + if (this.addingExt[0] != ".") { + this.addingExt = "." + this.addingExt + } + this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase() + this.extensions.push(this.addingExt) + + // 清除状态 + this.cancelAdding() + }, + removeExt: function (index) { + this.extensions.$remove(index) + } + }, + template: `
+ +
+
{{ext}}
+
+
+
+
+ +
+
+ + +
+
+
+ +
+

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

+
` +}) + +// 根据URL前缀 +Vue.component("http-cond-url-prefix", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "prefix", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof (this.vCond.value) == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +Vue.component("http-cond-url-not-prefix", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "prefix", + value: "", + isReverse: true, + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// 首页 +Vue.component("http-cond-url-eq-index", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "eq", + value: "/", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// 全站 +Vue.component("http-cond-url-all", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "prefix", + value: "/", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

支持全站所有URL。

+
` +}) + +// URL精准匹配 +Vue.component("http-cond-url-eq", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "eq", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +Vue.component("http-cond-url-not-eq", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "eq", + value: "", + isReverse: true, + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// URL正则匹配 +Vue.component("http-cond-url-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// 排除URL正则匹配 +Vue.component("http-cond-url-not-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "not regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// URL通配符 +Vue.component("http-cond-url-wildcard-match", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "wildcard match", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// User-Agent正则匹配 +Vue.component("http-cond-user-agent-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${userAgent}", + operator: "regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

+
` +}) + +// User-Agent正则不匹配 +Vue.component("http-cond-user-agent-not-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${userAgent}", + operator: "not regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

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

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

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

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

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

+ + + + 操作符 + +
+ +

+
+ + + + 对比值 + + +
+ +

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

+
+ + +
+ +

要对比的数字。

+
+ + +
+ +

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

+
+
+ +

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

+
+
+
+
除:
+
+ +
+
余:
+
+ +
+
+
+ + +
+ +

和参数值一致的字符串。

+

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

+

参数值的前缀。

+

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

+

参数值包含此字符串。

+

参数值不包含此字符串。

+
+
+ +

添加参数值列表。

+

添加参数值列表。

+

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

+

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

+
+
+
+
+
-
+
+
+
+ + +
+ +

要对比的IP。

+
+
+ +

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

+
+
+ +

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

+
+ + + + 不区分大小写 + +
+ + +
+

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

+ + + +` +}) + Vue.component("http-cors-header-config-box", { props: ["value"], data: function () { @@ -7614,68 +14040,19 @@ Vue.component("http-cors-header-config-box", { ` }) -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 - }) - }, +// 页面动态加密配置 +Vue.component("http-encryption-config-box", { + props: ["v-encryption-config", "v-is-location", "v-is-group"], data: function () { - let firewallPolicy = this.vHttpFirewallPolicy - return { - count: 0, - firewallPolicy: firewallPolicy - } - }, - methods: { - remove: function () { - this.firewallPolicy = null - }, - select: function () { - let that = this - teaweb.popup("/servers/components/waf/selectPopup", { - height: "26em", - callback: function (resp) { - that.firewallPolicy = resp.data.firewallPolicy - } - }) - }, - create: function () { - let that = this - teaweb.popup("/servers/components/waf/createPopup", { - height: "26em", - callback: function (resp) { - that.firewallPolicy = resp.data.firewallPolicy - } - }) - } - }, - template: `
-
- - {{firewallPolicy.name}}     -
-
- [选择已有策略]     [创建新策略] -
-
` -}) - -// 压缩配置 -Vue.component("http-optimization-config-box", { - props: ["v-optimization-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vOptimizationConfig + let config = this.vEncryptionConfig return { config: config, htmlMoreOptions: false, javascriptMoreOptions: false, - cssMoreOptions: false + keyPolicyMoreOptions: false, + cacheMoreOptions: false, + encryptionMoreOptions: false } }, methods: { @@ -7684,7 +14061,7 @@ Vue.component("http-optimization-config-box", { } }, template: `
- +
@@ -7694,1329 +14071,350 @@ Vue.component("http-optimization-config-box", { - + - - + + - + - + - - - - - -
HTML优化启用页面动态加密
- +
-

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

+

启用后,将对 HTML 页面中的 JavaScript 进行动态加密,有效抵御批量爬虫和脚本工具。

HTML例外URL排除 URL - -

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

+ +

这些 URL 将跳过加密处理,支持正则表达式。

HTML限制URL - -

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

-
- - - - - - - - - - - - - - - - - - - - - -
Javascript优化 -
- - -
-

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

-
Javascript例外URL - -

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

-
Javascript限制URL - -

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

-
- - - - - - - - - - - - - - - - - - - -
CSS优化 -
- - -
-

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

-
CSS例外URL - -

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

-
CSS限制URL - -

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

-
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
HTML 加密 +
+ + +
+

加密 HTML 页面中的 JavaScript 脚本。

+
加密内联脚本 +
+ + +
+

加密 HTML 中的内联 <script> 标签内容。

+
加密外部脚本 +
+ + +
+

加密通过 src 属性引入的外部 JavaScript 文件。

+
URL 匹配规则 + +

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

+
+ +
+ + + + + + + + + + + + + + + + +
JavaScript 文件加密 +
+ + +
+

加密独立的 JavaScript 文件(.js 文件)。

+
URL 匹配规则 + +

如果填写了匹配规则,表示只对这些 URL 进行加密处理;如果不填则表示支持所有的 URL。

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
服务器端密钥 + +

用于生成加密密钥的密码,建议使用复杂的随机字符串。默认密钥仅用于测试,生产环境请务必修改!

+
时间分片(秒) + +

加密密钥每隔多少秒更换一次。时间越短越安全,但可能影响性能。建议 60-120 秒,默认 60 秒。

+
IP CIDR 前缀长度 + +

将用户 IP 地址的前多少位作为识别依据。例如设置为 24 时,192.168.1.1 和 192.168.1.2 会被视为同一用户。默认 24。

+
简化 User-Agent +
+ + +
+

开启后,只识别浏览器类型(如 Chrome、Firefox),忽略版本号等细节,避免因浏览器自动更新导致解密失败。

+
+ +
+ + + + + + + + + + + + + + + + + + + + +
启用缓存 +
+ + +
+

开启后,相同内容的加密结果会被缓存,减少重复计算,提升响应速度。

+
缓存 TTL(秒) + +

缓存的有效期,超过这个时间后缓存会自动失效。建议与上面的"时间分片"保持一致。默认 60 秒。

+
最大缓存条目数 + +

最多缓存多少个加密结果。数量越大占用内存越多,建议根据服务器内存情况调整。默认 1000。

+
+
` }) -Vue.component("http-websocket-box", { - props: ["v-websocket-ref", "v-websocket-config", "v-is-location", "v-is-group"], + + +Vue.component("http-expires-time-config-box", { + props: ["v-expires-time"], data: function () { - let websocketRef = this.vWebsocketRef - if (websocketRef == null) { - websocketRef = { + let expiresTime = this.vExpiresTime + if (expiresTime == null) { + expiresTime = { isPrior: false, isOn: false, - websocketId: 0 + 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字段。

+
自动计算时间 +

根据已设置的缓存有效期进行计算。

+
强制缓存时间 + +

从客户端访问的时间开始要缓存的时长。

+
+
` +}) - let websocketConfig = this.vWebsocketConfig - if (websocketConfig == null) { - websocketConfig = { - id: 0, +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, - handshakeTimeout: { - count: 30, - unit: "second" - }, - allowAllOrigins: true, - allowedOrigins: [], - requestSameOrigin: true, - requestOrigin: "" + fastcgiIds: [] } + } + let fastcgiConfigs = this.vFastcgiConfigs + if (fastcgiConfigs == null) { + fastcgiConfigs = [] } else { - if (websocketConfig.handshakeTimeout == null) { - websocketConfig.handshakeTimeout = { - count: 30, - unit: "second", - } - } - if (websocketConfig.allowedOrigins == null) { - websocketConfig.allowedOrigins = [] - } + fastcgiRef.fastcgiIds = fastcgiConfigs.map(function (v) { + return v.id + }) } return { - websocketRef: websocketRef, - websocketConfig: websocketConfig, - handshakeTimeoutCountString: websocketConfig.handshakeTimeout.count.toString(), + fastcgiRef: fastcgiRef, + fastcgiConfigs: fastcgiConfigs, 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 + return (!this.vIsLocation || this.fastcgiRef.isPrior) && this.fastcgiRef.isOn }, - changeAdvancedVisible: function (v) { - this.advancedVisible = v - }, - createOrigin: function () { + createFastcgi: function () { let that = this - teaweb.popup("/servers/server/settings/websocket/createOrigin", { - height: "12.5em", + teaweb.popup("/servers/server/settings/fastcgi/createPopup", { + height: "26em", callback: function (resp) { - that.websocketConfig.allowedOrigins.push(resp.data.origin) + teaweb.success("添加成功", function () { + that.fastcgiConfigs.push(resp.data.fastcgi) + that.fastcgiRef.fastcgiIds.push(resp.data.fastcgi.id) + }) } }) }, - removeOrigin: function (index) { - this.websocketConfig.allowedOrigins.$remove(index) + 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: `
- - + - - + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用Websocket启用配置
- +
允许所有来源域(Origin)Fastcgi服务 -
- - -
-

选中表示允许所有的来源域。

-
允许的来源域列表(Origin) -
-
- {{origin}} +
+
+ {{fastcgi.address}}    
-
+
- -

只允许在列表中的来源域名访问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) - setTimeout(that.submitForm, 100) - } - }); - }, - - removeServerName: function (index) { - this.serverNames.$remove(index) - }, - - updateServerName: function (index, serverName) { - window.UPDATING_SERVER_NAME = teaweb.clone(serverName) - let that = this - teaweb.popup("/servers/addServerNamePopup", { - callback: function (resp) { - var serverName = resp.data.serverName - Vue.set(that.serverNames, index, serverName) - setTimeout(that.submitForm, 100) - } - }); - }, - showSearchBox: function () { - this.isSearching = !this.isSearching - if (this.isSearching) { - let that = this - setTimeout(function () { - that.$refs.keywordRef.focus() - }, 200) - } else { - this.keyword = "" - } - }, - allServerNames: function () { - if (this.serverNames == null) { - return [] - } - let result = [] - this.serverNames.forEach(function (serverName) { - if (serverName.subNames != null && serverName.subNames.length > 0) { - serverName.subNames.forEach(function (subName) { - if (subName != null && subName.length > 0) { - if (!result.$contains(subName)) { - result.push(subName) - } - } - }) - } else if (serverName.name != null && serverName.name.length > 0) { - if (!result.$contains(serverName.name)) { - result.push(serverName.name) - } - } - }) - return result - }, - submitForm: function () { - Tea.runActionOn(this.$refs.serverNamesRef.form) - } - }, - watch: { - keyword: function (v) { - this.serverNames.forEach(function (serverName) { - if (v.length == 0) { - serverName.isShowing = true - return - } - if (serverName.subNames == null || serverName.subNames.length == 0) { - if (!teaweb.match(serverName.name, v)) { - serverName.isShowing = false - } - } else { - let found = false - serverName.subNames.forEach(function (subName) { - if (teaweb.match(subName, v)) { - found = true - } - }) - serverName.isShowing = found - } - }) - } - }, - template: `
- -
-
- {{serverName.type}} - {{serverName.name}} - {{serverName.subNames[0]}}等{{serverName.subNames.length}}个域名 - -
-
-
-
- -
|
-
- - -
-
- -
-
-
` -}) - -// UAM模式配置 -Vue.component("uam-config-box", { - props: ["v-uam-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vUamConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - addToWhiteList: true, - onlyURLPatterns: [], - exceptURLPatterns: [], - minQPSPerIP: 0, - keyLife: 0 - } - } - if (config.onlyURLPatterns == null) { - config.onlyURLPatterns = [] - } - if (config.exceptURLPatterns == null) { - config.exceptURLPatterns = [] - } - return { - config: config, - moreOptionsVisible: false, - minQPSPerIP: config.minQPSPerIP, - keyLife: config.keyLife - } - }, - watch: { - minQPSPerIP: function (v) { - let qps = parseInt(v.toString()) - if (isNaN(qps) || qps < 0) { - qps = 0 - } - this.config.minQPSPerIP = qps - }, - keyLife: function (v) { - let keyLife = parseInt(v) - if (isNaN(keyLife) || keyLife <= 0) { - keyLife = 0 - } - this.config.keyLife = keyLife - } - }, - methods: { - showMoreOptions: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - }, - changeConds: function (conds) { - this.config.conds = conds - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用5秒盾 - -

启用后,访问网站时,自动检查浏览器环境,阻止非正常访问。

-
验证有效期 -
- - -
-

单个客户端验证通过后,在这个有效期内不再重复验证;如果为0则表示系统默认。

-
单IP最低QPS -
- - 请求数/秒 -
-

当某个IP在1分钟内平均QPS达到此值时,才会触发5秒盾;如果设置为0,表示任何访问都会触发。

-
加入IP白名单 - -

选中后,表示验证通过后,将访问者IP加入到临时白名单中,此IP下次访问时不再校验5秒盾;此白名单只对5秒盾有效,不影响其他规则。此选项主要用于可能无法正常使用Cookie的网站。

-
例外URL - -

如果填写了例外URL,表示这些URL跳过5秒盾不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行5秒盾处理;如果不填则表示支持所有的URL。

-
匹配条件 - -
-
-
` -}) - -Vue.component("http-cache-stale-config", { - props: ["v-cache-stale-config"], - data: function () { - let config = this.vCacheStaleConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - status: [], - supportStaleIfErrorHeader: true, - life: { - count: 1, - unit: "day" - } - } - } - return { - config: config - } - }, - watch: { - config: { - deep: true, - handler: function () { - this.$emit("change", this.config) - } - } - }, - methods: {}, - template: ` - - - - - - - - - - - - - - - - - - -
启用过时缓存 - -

选中后,在更新缓存失败后会尝试读取过时的缓存。

-
有效期 - -

缓存在过期之后,仍然保留的时间。

-
状态码 -

在这些状态码出现时使用过时缓存,默认支持50x状态码。

-
支持stale-if-error - -

选中后,支持在Cache-Control中通过stale-if-error指定过时缓存有效期。

-
` -}) - -Vue.component("firewall-syn-flood-config-viewer", { - props: ["v-syn-flood-config"], - data: function () { - let config = this.vSynFloodConfig - if (config == null) { - config = { - isOn: false, - minAttempts: 10, - timeoutSeconds: 600, - ignoreLocal: true - } - } - return { - config: config - } - }, - template: `
- - 已启用 / 空连接次数:{{config.minAttempts}}次/分钟 / 封禁时长:{{config.timeoutSeconds}}秒 / 忽略局域网访问 - - 未启用 -
` -}) - -// 域名列表 -Vue.component("domains-box", { - props: ["v-domains", "name", "v-support-wildcard"], - data: function () { - let domains = this.vDomains - if (domains == null) { - domains = [] - } - - let realName = "domainsJSON" - if (this.name != null && typeof this.name == "string") { - realName = this.name - } - - let supportWildcard = true - if (typeof this.vSupportWildcard == "boolean") { - supportWildcard = this.vSupportWildcard - } - - return { - domains: domains, - - mode: "single", // single | batch - batchDomains: "", - - isAdding: false, - addingDomain: "", - - isEditing: false, - editingIndex: -1, - - realName: realName, - supportWildcard: supportWildcard - } - }, - watch: { - vSupportWildcard: function (v) { - if (typeof v == "boolean") { - this.supportWildcard = v - } - }, - mode: function (mode) { - let that = this - setTimeout(function () { - if (mode == "single") { - if (that.$refs.addingDomain != null) { - that.$refs.addingDomain.focus() - } - } else if (mode == "batch") { - if (that.$refs.batchDomains != null) { - that.$refs.batchDomains.focus() - } - } - }, 100) - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.addingDomain.focus() - }, 100) - }, - confirm: function () { - if (this.mode == "batch") { - this.confirmBatch() - return - } - - let that = this - - // 删除其中的空格 - this.addingDomain = this.addingDomain.replace(/\s/g, "") - - if (this.addingDomain.length == 0) { - teaweb.warn("请输入要添加的域名", function () { - that.$refs.addingDomain.focus() - }) - return - } - - // 基本校验 - if (this.supportWildcard) { - if (this.addingDomain[0] == "~") { - let expr = this.addingDomain.substring(1) - try { - new RegExp(expr) - } catch (e) { - teaweb.warn("正则表达式错误:" + e.message, function () { - that.$refs.addingDomain.focus() - }) - return - } - } - } else { - if (/[*~^]/.test(this.addingDomain)) { - teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () { - that.$refs.addingDomain.focus() - }) - return - } - } - - if (this.isEditing && this.editingIndex >= 0) { - this.domains[this.editingIndex] = this.addingDomain - } else { - // 分割逗号(,)、顿号(、) - if (this.addingDomain.match("[,、,;]")) { - let domainList = this.addingDomain.split(new RegExp("[,、,;]")) - domainList.forEach(function (v) { - if (v.length > 0) { - that.domains.push(v) - } - }) - } else { - this.domains.push(this.addingDomain) - } - } - this.cancel() - this.change() - }, - confirmBatch: function () { - let domains = this.batchDomains.split("\n") - let realDomains = [] - let that = this - let hasProblems = false - domains.forEach(function (domain) { - if (hasProblems) { - return - } - if (domain.length == 0) { - return - } - if (that.supportWildcard) { - if (domain == "~") { - let expr = domain.substring(1) - try { - new RegExp(expr) - } catch (e) { - hasProblems = true - teaweb.warn("正则表达式错误:" + e.message, function () { - that.$refs.batchDomains.focus() - }) - return - } - } - } else { - if (/[*~^]/.test(domain)) { - hasProblems = true - teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () { - that.$refs.batchDomains.focus() - }) - return - } - } - realDomains.push(domain) - }) - if (hasProblems) { - return - } - if (realDomains.length == 0) { - teaweb.warn("请输入要添加的域名", function () { - that.$refs.batchDomains.focus() - }) - return - } - - realDomains.forEach(function (domain) { - that.domains.push(domain) - }) - this.cancel() - this.change() - }, - edit: function (index) { - this.addingDomain = this.domains[index] - this.isEditing = true - this.editingIndex = index - let that = this - setTimeout(function () { - that.$refs.addingDomain.focus() - }, 50) - }, - remove: function (index) { - this.domains.$remove(index) - this.change() - }, - cancel: function () { - this.isAdding = false - this.mode = "single" - this.batchDomains = "" - this.isEditing = false - this.editingIndex = -1 - this.addingDomain = "" - }, - change: function () { - this.$emit("change", this.domains) - } - }, - template: `
- -
- - [正则] - [后缀] - [泛域名] - {{domain}} - -   -   - - -   -   - - -
-
-
-
-
- -
-
-
- -
-
- -
-
-
- -   -
-
-

支持普通域名(example.com)、泛域名(*.example.com、域名后缀(以点号开头,如.example.com)和正则表达式(以波浪号开头,如~.*.example.com;如果域名后有端口,请加上端口号。

-

只支持普通域名(example.comwww.example.com)。

-
-
-
- -
-
` -}) - -Vue.component("http-firewall-province-selector", { - props: ["v-type", "v-provinces"], - data: function () { - let provinces = this.vProvinces - if (provinces == null) { - provinces = [] - } - - return { - listType: this.vType, - provinces: provinces - } - }, - methods: { - addProvince: function () { - let selectedProvinceIds = this.provinces.map(function (province) { - return province.id - }) - let that = this - teaweb.popup("/servers/server/settings/waf/ipadmin/selectProvincesPopup?type=" + this.listType + "&selectedProvinceIds=" + selectedProvinceIds.join(","), { - width: "50em", - height: "26em", - callback: function (resp) { - that.provinces = resp.data.selectedProvinces - that.$forceUpdate() - that.notifyChange() - } - }) - }, - removeProvince: function (index) { - this.provinces.$remove(index) - this.notifyChange() - }, - resetProvinces: function () { - this.provinces = [] - this.notifyChange() - }, - notifyChange: function () { - this.$emit("change", { - "provinces": this.provinces - }) - } - }, - template: `
- 暂时没有选择允许封禁的省份。 -
-
- - {{province.name}} -
-
-
-   -
` -}) - -Vue.component("http-referers-config-box", { - props: ["v-referers-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vReferersConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - allowEmpty: true, - allowSameDomain: true, - allowDomains: [], - denyDomains: [], - checkOrigin: true - } - } - if (config.allowDomains == null) { - config.allowDomains = [] - } - if (config.denyDomains == null) { - config.denyDomains = [] - } - return { - config: config, - moreOptionsVisible: false - } - }, - methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn - }, - changeAllowDomains: function (domains) { - if (typeof (domains) == "object") { - this.config.allowDomains = domains - this.$forceUpdate() - } - }, - changeDenyDomains: function (domains) { - if (typeof (domains) == "object") { - this.config.denyDomains = domains - this.$forceUpdate() - } - }, - showMoreOptions: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用防盗链 -
- - -
-

选中后表示开启防盗链。

-
允许直接访问网站 - -

允许用户直接访问网站,用户第一次访问网站时来源域名通常为空。

-
来源域名允许一致 - -

允许来源域名和当前访问的域名一致,相当于在站内访问。

-
允许的来源域名 - > -

允许的其他来源域名列表,比如example.com*.example.com。单个星号*表示允许所有域名。

-
禁止的来源域名 - -

禁止的来源域名列表,比如example.org*.example.org;除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。

-
同时检查Origin - -

如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。

-
例外URL - -

如果填写了例外URL,表示这些URL跳过不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行处理;如果不填则表示支持所有的URL。

-
-
-
` -}) - -Vue.component("server-traffic-limit-status-viewer", { - props: ["value"], - data: function () { - let targetTypeName = "流量" - if (this.value != null) { - targetTypeName = this.targetTypeToName(this.value.targetType) - } - - return { - status: this.value, - targetTypeName: targetTypeName - } - }, - methods: { - targetTypeToName: function (targetType) { - switch (targetType) { - case "traffic": - return "流量" - case "request": - return "请求数" - case "websocketConnections": - return "Websocket连接数" - } - return "流量" - } - }, - template: ` - 已达到套餐当日{{targetTypeName}}限制 - 已达到套餐当月{{targetTypeName}}限制 - 已达到套餐总体{{targetTypeName}}限制 -` -}) - -Vue.component("http-redirect-to-https-box", { - props: ["v-redirect-to-https-config", "v-is-location"], - data: function () { - let redirectToHttpsConfig = this.vRedirectToHttpsConfig - if (redirectToHttpsConfig == null) { - redirectToHttpsConfig = { - isPrior: false, - isOn: false, - host: "", - port: 0, - status: 0, - onlyDomains: [], - exceptDomains: [] - } - } else { - if (redirectToHttpsConfig.onlyDomains == null) { - redirectToHttpsConfig.onlyDomains = [] - } - if (redirectToHttpsConfig.exceptDomains == null) { - redirectToHttpsConfig.exceptDomains = [] - } - } - return { - redirectToHttpsConfig: redirectToHttpsConfig, - portString: (redirectToHttpsConfig.port > 0) ? redirectToHttpsConfig.port.toString() : "", - moreOptionsVisible: false, - statusOptions: [ - {"code": 301, "text": "Moved Permanently"}, - {"code": 308, "text": "Permanent Redirect"}, - {"code": 302, "text": "Found"}, - {"code": 303, "text": "See Other"}, - {"code": 307, "text": "Temporary Redirect"} - ] - } - }, - watch: { - "redirectToHttpsConfig.status": function () { - this.redirectToHttpsConfig.status = parseInt(this.redirectToHttpsConfig.status) - }, - portString: function (v) { - let port = parseInt(v) - if (!isNaN(port)) { - this.redirectToHttpsConfig.port = port - } else { - this.redirectToHttpsConfig.port = 0 - } - } - }, - methods: { - changeMoreOptions: function (isVisible) { - this.moreOptionsVisible = isVisible - }, - changeOnlyDomains: function (values) { - this.redirectToHttpsConfig.onlyDomains = values - this.$forceUpdate() - }, - changeExceptDomains: function (values) { - this.redirectToHttpsConfig.exceptDomains = values - this.$forceUpdate() - } - }, - template: `
- - - - - - - - - - - -
自动跳转到HTTPS -
- - -
-

开启后,所有HTTP的请求都会自动跳转到对应的HTTPS URL上,

- - - - - - - - - - - - - - - -
状态码 - -
域名或IP地址 - -

默认和用户正在访问的域名或IP地址一致。

-
端口 - -

默认端口为443。

-
-
- - -
-
- - -
-

开启后,所有HTTP的请求都会自动跳转到对应的HTTPS URL上,

- - - - - - - - - - - - - - - - - - - - - - - -
状态码 - -
跳转后域名或IP地址 - -

默认和用户正在访问的域名或IP地址一致,不填写就表示使用当前的域名。

-
端口 - -

默认端口为443。

-
允许的域名 - -

如果填写了允许的域名,那么只有这些域名可以自动跳转。

-
排除的域名 - -

如果填写了排除的域名,那么这些域名将不跳转。

-
-
+
` }) @@ -10035,180 +15433,1884 @@ Vue.component("http-firewall-actions-box", { ` }) -// 认证设置 -Vue.component("http-auth-config-box", { - props: ["v-auth-config", "v-is-location"], +// Action列表 +Vue.component("http-firewall-actions-view", { + props: ["v-actions"], + template: `
+
+ {{action.name}} ({{action.code.toUpperCase()}}) +
+ [{{action.options.status}}] + + [分组] + [网站] + [网站和策略] + + + 黑名单 + 白名单 + 灰名单 + +
+
+
+
` +}) + +Vue.component("http-firewall-block-options-viewer", { + props: ["v-block-options"], data: function () { - let authConfig = this.vAuthConfig - if (authConfig == null) { - authConfig = { - isPrior: false, - isOn: false + return { + options: this.vBlockOptions + } + }, + template: `
+ 默认设置 +
+ 状态码:{{options.statusCode}} / 提示内容:[{{options.body.length}}字符][无] / 超时时间:{{options.timeout}}秒 / 最大封禁时长:{{options.timeoutMax}}秒 + / 尝试全局封禁 +
+
+` +}) + +Vue.component("http-firewall-block-options", { + props: ["v-block-options"], + data: function () { + return { + options: this.vBlockOptions, + statusCode: this.vBlockOptions.statusCode, + timeout: this.vBlockOptions.timeout, + timeoutMax: this.vBlockOptions.timeoutMax, + isEditing: false + } + }, + watch: { + statusCode: function (v) { + let statusCode = parseInt(v) + if (isNaN(statusCode)) { + this.options.statusCode = 403 + } else { + this.options.statusCode = statusCode + } + }, + timeout: function (v) { + let timeout = parseInt(v) + if (isNaN(timeout)) { + this.options.timeout = 0 + } else { + this.options.timeout = timeout + } + }, + timeoutMax: function (v) { + let timeoutMax = parseInt(v) + if (isNaN(timeoutMax)) { + this.options.timeoutMax = 0 + } else { + this.options.timeoutMax = timeoutMax } } - if (authConfig.policyRefs == null) { - authConfig.policyRefs = [] + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + } + }, + template: `
+ + 状态码:{{statusCode}} / 提示内容:[{{options.body.length}}字符][无] / 封禁时长:{{timeout}}秒 + / 最大封禁时长:{{timeoutMax}}秒 + / 尝试全局封禁 + + + + + + + + + + + + + + + + + + + + + + +
状态码 + +
提示内容 + +
封禁时长 +
+ + +
+

触发阻止动作时,封禁客户端IP的时间。

+
最大封禁时长 +
+ + +
+

如果最大封禁时长大于封禁时长({{timeout}}秒),那么表示每次封禁的时候,将会在这两个时长数字之间随机选取一个数字作为最终的封禁时长。

+
失败全局封禁 + +

选中后,表示允许系统尝试全局封禁某个IP,以提升封禁性能。

+
+
+` +}) + +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 { - authConfig: authConfig + options: options, + summary: "", + captchaTypes: window.WAF_CAPTCHA_TYPES + } + }, + methods: { + updateSummary: function () { + let summaryList = [] + if (this.options.life > 0) { + summaryList.push("有效时间" + this.options.life + "秒") + } + if (this.options.maxFails > 0) { + summaryList.push("最多失败" + this.options.maxFails + "次") + } + if (this.options.failBlockTimeout > 0) { + summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") + } + if (this.options.failBlockScopeAll) { + summaryList.push("全局封禁") + } + let that = this + let typeDef = this.captchaTypes.$find(function (k, v) { + return v.code == that.options.captchaType + }) + if (typeDef != null) { + summaryList.push("默认验证方式:" + typeDef.name) + } + + if (this.options.captchaType == "default") { + if (this.options.uiIsOn) { + summaryList.push("定制UI") + } + } + + if (this.options.geeTestConfig != null && this.options.geeTestConfig.isOn) { + summaryList.push("已配置极验") + } + + if (summaryList.length == 0) { + this.summary = "默认配置" + } else { + this.summary = summaryList.join(" / ") + } + } + }, + template: `
{{summary}}
+` +}) + +Vue.component("http-firewall-captcha-options", { + props: ["v-captcha-options"], + mounted: function () { + this.updateSummary() + }, + data: function () { + let options = this.vCaptchaOptions + if (options == null) { + options = { + captchaType: "default", + countLetters: 0, + life: 0, + maxFails: 0, + failBlockTimeout: 0, + failBlockScopeAll: false, + uiIsOn: false, + uiTitle: "", + uiPrompt: "", + uiButtonTitle: "", + uiShowRequestId: true, + uiCss: "", + uiFooter: "", + uiBody: "", + cookieId: "", + lang: "", + geeTestConfig: { + isOn: false, + captchaId: "", + captchaKey: "" + } + } + } + if (options.countLetters <= 0) { + options.countLetters = 6 + } + + if (options.captchaType == null || options.captchaType.length == 0) { + options.captchaType = "default" + } + + + return { + options: options, + isEditing: false, + summary: "", + uiBodyWarning: "", + captchaTypes: window.WAF_CAPTCHA_TYPES + } + }, + watch: { + "options.countLetters": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } else if (i < 0) { + i = 0 + } else if (i > 10) { + i = 10 + } + this.options.countLetters = i + }, + "options.life": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.life = i + this.updateSummary() + }, + "options.maxFails": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.maxFails = i + this.updateSummary() + }, + "options.failBlockTimeout": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.failBlockTimeout = i + this.updateSummary() + }, + "options.failBlockScopeAll": function (v) { + this.updateSummary() + }, + "options.captchaType": function (v) { + this.updateSummary() + }, + "options.uiIsOn": function (v) { + this.updateSummary() + }, + "options.uiBody": function (v) { + if (/|\s).+\$\{body}.*<\/form>/s.test(v)) { + this.uiBodyWarning = "页面模板中不能使用
标签包裹\${body}变量,否则将导致验证码表单无法提交。" + } else { + this.uiBodyWarning = "" + } + }, + "options.geeTestConfig.isOn": function (v) { + this.updateSummary() + } + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + }, + updateSummary: function () { + let summaryList = [] + if (this.options.life > 0) { + summaryList.push("有效时间" + this.options.life + "秒") + } + if (this.options.maxFails > 0) { + summaryList.push("最多失败" + this.options.maxFails + "次") + } + if (this.options.failBlockTimeout > 0) { + summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") + } + if (this.options.failBlockScopeAll) { + summaryList.push("尝试全局封禁") + } + + let that = this + let typeDef = this.captchaTypes.$find(function (k, v) { + return v.code == that.options.captchaType + }) + if (typeDef != null) { + summaryList.push("默认验证方式:" + typeDef.name) + } + + if (this.options.captchaType == "default") { + if (this.options.uiIsOn) { + summaryList.push("定制UI") + } + } + + if (this.options.geeTestConfig != null && this.options.geeTestConfig.isOn) { + summaryList.push("已配置极验") + } + + if (summaryList.length == 0) { + this.summary = "默认配置" + } else { + this.summary = summaryList.join(" / ") + } + }, + confirm: function () { + this.isEditing = false + } + }, + template: `
+ + {{summary}} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
默认验证方式 + +

{{captchaDef.description}}

+
有效时间 +
+ + +
+

验证通过后在这个时间内不再验证,默认600秒。

+
最多失败次数 +
+ + +
+

建议填入一个不小于5的数字,以减少误判几率。允许用户失败尝试的最多次数,超过这个次数将被自动加入黑名单。如果为空或者为0,表示不限制。

+
失败拦截时间 +
+ + +
+

在达到最多失败次数(大于0)时,自动拦截的时长;如果为0表示不自动拦截。

+
失败全局封禁 + +

选中后,表示允许系统尝试全局封禁某个IP,以提升封禁性能。

+
验证码中数字个数 + +
定制UI
页面标题 + +
按钮标题 + +

类似于提交验证

+
显示请求ID + +

在界面上显示请求ID,方便用户报告问题。

+
CSS样式 + +
页头提示 + +

类似于请输入上面的验证码,支持HTML。

+
页尾提示 + +

支持HTML。

+
页面模板 + +

警告:{{uiBodyWarning}}模板中必须包含\${body}表示验证码表单!整个页面的模板,支持HTML,其中必须使用\${body}变量代表验证码表单,否则将无法正常显示验证码。

+
+ + + + + + + + + + + + + + + + +
允许用户使用极验 +

选中后,表示允许用户在WAF设置中选择极验。

+
极验-验证ID * + +

在极验控制台--业务管理中获取。

+
极验-验证Key * + +

在极验控制台--业务管理中获取。

+
+
+
+` +}) + +Vue.component("http-firewall-config-box", { + props: ["v-firewall-config", "v-is-location", "v-is-group", "v-firewall-policy"], + data: function () { + let firewall = this.vFirewallConfig + if (firewall == null) { + firewall = { + isPrior: false, + isOn: false, + firewallPolicyId: 0, + ignoreGlobalRules: false, + defaultCaptchaType: "none" + } + } + + if (firewall.defaultCaptchaType == null || firewall.defaultCaptchaType.length == 0) { + firewall.defaultCaptchaType = "none" + } + + let allCaptchaTypes = window.WAF_CAPTCHA_TYPES.$copy() + + // geetest + let geeTestIsOn = false + if (this.vFirewallPolicy != null && this.vFirewallPolicy.captchaAction != null && this.vFirewallPolicy.captchaAction.geeTestConfig != null) { + geeTestIsOn = this.vFirewallPolicy.captchaAction.geeTestConfig.isOn + } + + // 如果没有启用geetest,则还原 + if (!geeTestIsOn && firewall.defaultCaptchaType == "geetest") { + firewall.defaultCaptchaType = "none" + } + + return { + firewall: firewall, + moreOptionsVisible: false, + execGlobalRules: !firewall.ignoreGlobalRules, + captchaTypes: allCaptchaTypes, + geeTestIsOn: geeTestIsOn + } + }, + watch: { + execGlobalRules: function (v) { + this.firewall.ignoreGlobalRules = !v + } + }, + methods: { + changeOptionsVisible: function (v) { + this.moreOptionsVisible = v + } + }, + template: `
+ + + + + + + +
全局WAF策略 +
{{vFirewallPolicy.name}}   [{{vFirewallPolicy.modeInfo.name}}]  +

当前网站所在集群的设置。

+
+ 当前集群没有设置WAF策略,当前配置无法生效。 +
+ + + + + + + + + + + + + + + + + + + + +
启用Web防火墙 + +

选中后,表示启用当前网站的WAF功能。

+
人机识别验证方式 + +

使用系统默认的设置。你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

+

{{captchaType.description}}你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

+
启用系统全局规则 + +

选中后,表示使用系统全局WAF策略中定义的规则。

+
+
+
` +}) + +Vue.component("http-firewall-js-cookie-options-viewer", { + props: ["v-js-cookie-options"], + mounted: function () { + this.updateSummary() + }, + data: function () { + let options = this.vJsCookieOptions + if (options == null) { + options = { + life: 0, + maxFails: 0, + failBlockTimeout: 0, + failBlockScopeAll: false, + scope: "" + } + } + return { + options: options, + summary: "" + } + }, + methods: { + updateSummary: function () { + let summaryList = [] + if (this.options.life > 0) { + summaryList.push("有效时间" + this.options.life + "秒") + } + if (this.options.maxFails > 0) { + summaryList.push("最多失败" + this.options.maxFails + "次") + } + if (this.options.failBlockTimeout > 0) { + summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") + } + if (this.options.failBlockScopeAll) { + summaryList.push("尝试全局封禁") + } + + if (summaryList.length == 0) { + this.summary = "默认配置" + } else { + this.summary = summaryList.join(" / ") + } + } + }, + template: `
{{summary}}
+` +}) + +Vue.component("http-firewall-js-cookie-options", { + props: ["v-js-cookie-options"], + mounted: function () { + this.updateSummary() + }, + data: function () { + let options = this.vJsCookieOptions + if (options == null) { + options = { + life: 0, + maxFails: 0, + failBlockTimeout: 0, + failBlockScopeAll: false, + scope: "service" + } + } + + return { + options: options, + isEditing: false, + summary: "" + } + }, + watch: { + "options.life": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.life = i + this.updateSummary() + }, + "options.maxFails": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.maxFails = i + this.updateSummary() + }, + "options.failBlockTimeout": function (v) { + let i = parseInt(v, 10) + if (isNaN(i)) { + i = 0 + } + this.options.failBlockTimeout = i + this.updateSummary() + }, + "options.failBlockScopeAll": function (v) { + this.updateSummary() + } + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + }, + updateSummary: function () { + let summaryList = [] + if (this.options.life > 0) { + summaryList.push("有效时间" + this.options.life + "秒") + } + if (this.options.maxFails > 0) { + summaryList.push("最多失败" + this.options.maxFails + "次") + } + if (this.options.failBlockTimeout > 0) { + summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") + } + if (this.options.failBlockScopeAll) { + summaryList.push("尝试全局封禁") + } + + if (summaryList.length == 0) { + this.summary = "默认配置" + } else { + this.summary = summaryList.join(" / ") + } + }, + confirm: function () { + this.isEditing = false + } + }, + template: `
+ + {{summary}} +
+ + + + + + + + + + + + + + + + + + + +
有效时间 +
+ + +
+

验证通过后在这个时间内不再验证,默认3600秒。

+
最多失败次数 +
+ + +
+

建议填入一个不小于5的数字,以减少误判几率。允许用户失败尝试的最多次数,超过这个次数将被自动加入黑名单。如果为空或者为0,表示不限制。

+
失败拦截时间 +
+ + +
+

在达到最多失败次数(大于0)时,自动拦截的时长;如果为0表示不自动拦截。

+
失败全局封禁 + +

选中后,表示允许系统尝试全局封禁某个IP,以提升封禁性能。

+
+
+
+` +}) + +Vue.component("http-firewall-page-options-viewer", { + props: ["v-page-options"], + data: function () { + return { + options: this.vPageOptions + } + }, + template: `
+ 默认设置 +
+ 状态码:{{options.status}} / 提示内容:[{{options.body.length}}字符] +
+
+` +}) + +Vue.component("http-firewall-page-options", { + props: ["v-page-options"], + data: function () { + var defaultPageBody = ` + + + 403 Forbidden + + + +

403 Forbidden By WAF

+
Connection: \${remoteAddr} (Client) -> \${serverAddr} (Server)
+
Request ID: \${requestId}
+ +` + + return { + pageOptions: this.vPageOptions, + status: this.vPageOptions.status, + body: this.vPageOptions.body, + defaultPageBody: defaultPageBody, + isEditing: false + } + }, + watch: { + status: function (v) { + if (typeof v === "string" && v.length != 3) { + return + } + let statusCode = parseInt(v) + if (isNaN(statusCode)) { + this.pageOptions.status = 403 + } else { + this.pageOptions.status = statusCode + } + }, + body: function (v) { + this.pageOptions.body = v + } + }, + methods: { + edit: function () { + this.isEditing = !this.isEditing + } + }, + template: `
+ + 状态码:{{status}} / 提示内容:[{{pageOptions.body.length}}字符][无] + + + + + + + + + + +
状态码 *
网页内容 + +

[使用模板]

+
+
+` +}) + +Vue.component("http-firewall-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-firewall-policy-selector", { + props: ["v-http-firewall-policy"], + mounted: function () { + let that = this + Tea.action("/servers/components/waf/count") + .post() + .success(function (resp) { + that.count = resp.data.count + }) + }, + data: function () { + let firewallPolicy = this.vHttpFirewallPolicy + return { + count: 0, + firewallPolicy: firewallPolicy + } + }, + methods: { + remove: function () { + this.firewallPolicy = null + }, + select: function () { + let that = this + teaweb.popup("/servers/components/waf/selectPopup", { + height: "26em", + callback: function (resp) { + that.firewallPolicy = resp.data.firewallPolicy + } + }) + }, + create: function () { + let that = this + teaweb.popup("/servers/components/waf/createPopup", { + height: "26em", + callback: function (resp) { + that.firewallPolicy = resp.data.firewallPolicy + } + }) + } + }, + template: `
+
+ + {{firewallPolicy.name}}     +
+
+ [选择已有策略]     [创建新策略] +
+
` +}) + +Vue.component("http-firewall-province-selector", { + props: ["v-type", "v-provinces"], + data: function () { + let provinces = this.vProvinces + if (provinces == null) { + provinces = [] + } + + return { + listType: this.vType, + provinces: provinces + } + }, + methods: { + addProvince: function () { + let selectedProvinceIds = this.provinces.map(function (province) { + return province.id + }) + let that = this + teaweb.popup("/servers/server/settings/waf/ipadmin/selectProvincesPopup?type=" + this.listType + "&selectedProvinceIds=" + selectedProvinceIds.join(","), { + width: "50em", + height: "26em", + callback: function (resp) { + that.provinces = resp.data.selectedProvinces + that.$forceUpdate() + that.notifyChange() + } + }) + }, + removeProvince: function (index) { + this.provinces.$remove(index) + this.notifyChange() + }, + resetProvinces: function () { + this.provinces = [] + this.notifyChange() + }, + notifyChange: function () { + this.$emit("change", { + "provinces": this.provinces + }) + } + }, + template: `
+ 暂时没有选择允许封禁的省份。 +
+
+ + {{province.name}} +
+
+
+   +
` +}) + +Vue.component("http-firewall-region-selector", { + props: ["v-type", "v-countries"], + data: function () { + let countries = this.vCountries + if (countries == null) { + countries = [] + } + + return { + listType: this.vType, + countries: countries + } + }, + methods: { + addCountry: function () { + let selectedCountryIds = this.countries.map(function (country) { + return country.id + }) + let that = this + teaweb.popup("/servers/server/settings/waf/ipadmin/selectCountriesPopup?type=" + this.listType + "&selectedCountryIds=" + selectedCountryIds.join(","), { + width: "52em", + height: "30em", + callback: function (resp) { + that.countries = resp.data.selectedCountries + that.$forceUpdate() + that.notifyChange() + } + }) + }, + removeCountry: function (index) { + this.countries.$remove(index) + this.notifyChange() + }, + resetCountries: function () { + this.countries = [] + this.notifyChange() + }, + notifyChange: function () { + this.$emit("change", { + "countries": this.countries + }) + } + }, + template: `
+ 暂时没有选择允许封禁的区域。 +
+
+ + ({{country.letter}}){{country.name}} +
+
+
+   +
` +}) + +// 显示WAF规则的标签 +Vue.component("http-firewall-rule-label", { + props: ["v-rule"], + data: function () { + return { + rule: this.vRule + } + }, + methods: { + showErr: function (err) { + teaweb.popupTip("规则校验错误,请修正:" + teaweb.encodeHTML(err) + "") + }, + calculateParamName: function (param) { + let paramName = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + } + }) + } + return paramName + }, + calculateParamDescription: function (param) { + let paramName = "" + let paramDescription = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + paramDescription = checkpoint.description + } + }) + } + return paramName + ": " + paramDescription + }, + operatorName: function (operatorCode) { + let operatorName = operatorCode + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + } + }) + } + + return operatorName + }, + operatorDescription: function (operatorCode) { + let operatorName = operatorCode + let operatorDescription = "" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + operatorDescription = v.description + } + }) + } + + return operatorName + ": " + operatorDescription + }, + operatorDataType: function (operatorCode) { + let operatorDataType = "none" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorDataType = v.dataType + } + }) + } + + return operatorDataType + }, + isEmptyString: function (v) { + return typeof v == "string" && v.length == 0 + } + }, + template: `
+
+ {{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} + + + + {{rule.checkpointOptions.period}}秒内请求数 + + + + + 允许{{rule.checkpointOptions.allowDomains}} + 禁止{{rule.checkpointOptions.denyDomains}} + + + + | {{paramFilter.code}} + <{{operatorName(rule.operator)}}> + {{rule.value}} + [空] + + + + ({{rule.description}}) + + 规则错误 +
+
` +}) + +Vue.component("http-firewall-rules-box", { + props: ["v-rules", "v-type"], + data: function () { + let rules = this.vRules + if (rules == null) { + rules = [] + } + return { + rules: rules + } + }, + methods: { + addRule: function () { + window.UPDATING_RULE = null + let that = this + teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { + height: "30em", + callback: function (resp) { + that.rules.push(resp.data.rule) + } + }) + }, + updateRule: function (index, rule) { + window.UPDATING_RULE = teaweb.clone(rule) + let that = this + teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { + height: "30em", + callback: function (resp) { + Vue.set(that.rules, index, resp.data.rule) + } + }) + }, + removeRule: function (index) { + let that = this + teaweb.confirm("确定要删除此规则吗?", function () { + that.rules.$remove(index) + }) + }, + operatorName: function (operatorCode) { + let operatorName = operatorCode + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + } + }) + } + + return operatorName + }, + operatorDescription: function (operatorCode) { + let operatorName = operatorCode + let operatorDescription = "" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + operatorDescription = v.description + } + }) + } + + return operatorName + ": " + operatorDescription + }, + operatorDataType: function (operatorCode) { + let operatorDataType = "none" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorDataType = v.dataType + } + }) + } + + return operatorDataType + }, + calculateParamName: function (param) { + let paramName = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + } + }) + } + return paramName + }, + calculateParamDescription: function (param) { + let paramName = "" + let paramDescription = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + paramDescription = checkpoint.description + } + }) + } + return paramName + ": " + paramDescription + }, + isEmptyString: function (v) { + return typeof v == "string" && v.length == 0 + } + }, + template: `
+ +
+
+ {{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} + + + + {{rule.checkpointOptions.period}}秒内请求数 + + + + + 允许{{rule.checkpointOptions.allowDomains}} + 禁止{{rule.checkpointOptions.denyDomains}} + + + + | {{paramFilter.code}} <{{operatorName(rule.operator)}}> + {{rule.value}} + [空] + + + + ({{rule.description}}) + + + +
+
+
+ +
` +}) + +// 通用Header长度 +let defaultGeneralHeaders = ["Cache-Control", "Connection", "Date", "Pragma", "Trailer", "Transfer-Encoding", "Upgrade", "Via", "Warning"] +Vue.component("http-cond-general-header-length", { + props: ["v-checkpoint"], + data: function () { + let headers = null + let length = null + + if (window.parent.UPDATING_RULE != null) { + let options = window.parent.UPDATING_RULE.checkpointOptions + if (options.headers != null && Array.$isArray(options.headers)) { + headers = options.headers + } + if (options.length != null) { + length = options.length + } + } + + + if (headers == null) { + headers = defaultGeneralHeaders + } + + if (length == null) { + length = 128 + } + + let that = this + setTimeout(function () { + that.change() + }, 100) + + return { + headers: headers, + length: length + } + }, + watch: { + length: function (v) { + let len = parseInt(v) + if (isNaN(len)) { + len = 0 + } + if (len < 0) { + len = 0 + } + this.length = len + this.change() + } + }, + methods: { + change: function () { + this.vCheckpoint.options = [ + { + code: "headers", + value: this.headers + }, + { + code: "length", + value: this.length + } + ] + } + }, + template: `
+ + + + + + + + + +
通用Header列表 + +

需要检查的Header列表。

+
Header值超出长度 +
+ + 字节 +
+

超出此长度认为匹配成功,0表示不限制。

+
+
` +}) + +// CC +Vue.component("http-firewall-checkpoint-cc", { + props: ["v-checkpoint"], + data: function () { + let keys = [] + let period = 60 + let threshold = 1000 + let ignoreCommonFiles = true + let enableFingerprint = true + + let options = {} + if (window.parent.UPDATING_RULE != null) { + options = window.parent.UPDATING_RULE.checkpointOptions + } + + if (options == null) { + options = {} + } + if (options.keys != null) { + keys = options.keys + } + if (keys.length == 0) { + keys = ["${remoteAddr}", "${requestPath}"] + } + if (options.period != null) { + period = options.period + } + if (options.threshold != null) { + threshold = options.threshold + } + if (options.ignoreCommonFiles != null && typeof (options.ignoreCommonFiles) == "boolean") { + ignoreCommonFiles = options.ignoreCommonFiles + } + if (options.enableFingerprint != null && typeof (options.enableFingerprint) == "boolean") { + enableFingerprint = options.enableFingerprint + } + + let that = this + setTimeout(function () { + that.change() + }, 100) + + return { + keys: keys, + period: period, + threshold: threshold, + ignoreCommonFiles: ignoreCommonFiles, + enableFingerprint: enableFingerprint, + options: {}, + value: threshold + } + }, + watch: { + period: function () { + this.change() + }, + threshold: function () { + this.change() + }, + ignoreCommonFiles: function () { + this.change() + }, + enableFingerprint: function () { + this.change() + } + }, + methods: { + changeKeys: function (keys) { + this.keys = keys + this.change() + }, + change: function () { + let period = parseInt(this.period.toString()) + if (isNaN(period) || period <= 0) { + period = 60 + } + + let threshold = parseInt(this.threshold.toString()) + if (isNaN(threshold) || threshold <= 0) { + threshold = 1000 + } + this.value = threshold + + let ignoreCommonFiles = this.ignoreCommonFiles + if (typeof ignoreCommonFiles != "boolean") { + ignoreCommonFiles = false + } + + let enableFingerprint = this.enableFingerprint + if (typeof enableFingerprint != "boolean") { + enableFingerprint = true + } + + this.vCheckpoint.options = [ + { + code: "keys", + value: this.keys + }, + { + code: "period", + value: period, + }, + { + code: "threshold", + value: threshold + }, + { + code: "ignoreCommonFiles", + value: ignoreCommonFiles + }, + { + code: "enableFingerprint", + value: enableFingerprint + } + ] + }, + thresholdTooLow: function () { + let threshold = parseInt(this.threshold.toString()) + if (isNaN(threshold) || threshold <= 0) { + threshold = 1000 + } + return threshold > 0 && threshold < 5 + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + +
统计对象组合 * + +
统计周期 * +
+ + +
+
阈值 * + +

对于网站类应用来说,当前阈值设置的太低,有可能会影响用户正常访问。

+
检查请求来源指纹 + +

在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。

+
忽略常用文件 + +

忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。

+
+
` +}) + +// 防盗链 +Vue.component("http-firewall-checkpoint-referer-block", { + props: ["v-checkpoint"], + data: function () { + let allowEmpty = true + let allowSameDomain = true + let allowDomains = [] + let denyDomains = [] + let checkOrigin = true + + let options = {} + if (window.parent.UPDATING_RULE != null) { + options = window.parent.UPDATING_RULE.checkpointOptions + } + + if (options == null) { + options = {} + } + if (typeof (options.allowEmpty) == "boolean") { + allowEmpty = options.allowEmpty + } + if (typeof (options.allowSameDomain) == "boolean") { + allowSameDomain = options.allowSameDomain + } + if (options.allowDomains != null && typeof (options.allowDomains) == "object") { + allowDomains = options.allowDomains + } + if (options.denyDomains != null && typeof (options.denyDomains) == "object") { + denyDomains = options.denyDomains + } + if (typeof options.checkOrigin == "boolean") { + checkOrigin = options.checkOrigin + } + + let that = this + setTimeout(function () { + that.change() + }, 100) + + return { + allowEmpty: allowEmpty, + allowSameDomain: allowSameDomain, + allowDomains: allowDomains, + denyDomains: denyDomains, + checkOrigin: checkOrigin, + options: {}, + value: 0 + } + }, + watch: { + allowEmpty: function () { + this.change() + }, + allowSameDomain: function () { + this.change() + }, + checkOrigin: function () { + this.change() + } + }, + methods: { + changeAllowDomains: function (values) { + this.allowDomains = values + this.change() + }, + changeDenyDomains: function (values) { + this.denyDomains = values + this.change() + }, + change: function () { + this.vCheckpoint.options = [ + { + code: "allowEmpty", + value: this.allowEmpty + }, + { + code: "allowSameDomain", + value: this.allowSameDomain, + }, + { + code: "allowDomains", + value: this.allowDomains + }, + { + code: "denyDomains", + value: this.denyDomains + }, + { + code: "checkOrigin", + value: this.checkOrigin + } + ] + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + +
来源域名允许为空 + +

允许不带来源的访问。

+
来源域名允许一致 + +

允许来源域名和当前访问的域名一致,相当于在站内访问。

+
允许的来源域名 + +

允许的来源域名列表,比如example.com(顶级域名)、*.example.com(example.com的所有二级域名)。单个星号*表示允许所有域名。

+
禁止的来源域名 + +

禁止的来源域名列表,比如example.org(顶级域名)、*.example.org(example.org的所有二级域名);除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。

+
同时检查Origin + +

如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。

+
+
` +}) + +Vue.component("http-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.authConfig.isPrior) && this.authConfig.isOn + return (!this.vIsLocation || this.vGzipRef.isPrior) && this.vGzipRef.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) + changeAdvancedVisible: function (v) { + this.advancedVisible = v } }, template: `
- + - - + + - + + + + + + + + + + + + + + + + + + + + + +
启用鉴权启用Gzip压缩
- +
压缩级别 + +

级别越高,压缩比例越大。

+
Gzip内容最小长度 + +

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

+
Gzip内容最大长度 + +

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

+
匹配条件 + +
-
- -
-

鉴权方式

- - - - - - - - - - - - - - - - - - - -
名称鉴权方法参数状态操作
{{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"], +Vue.component("http-header-assistant", { + props: ["v-type", "v-value"], + mounted: function () { + let that = this + Tea.action("/servers/headers/options?type=" + this.vType) + .post() + .success(function (resp) { + that.allHeaders = resp.data.headers + }) + }, data: function () { - 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 + allHeaders: [], + matchedHeaders: [], + + selectedHeaderName: "" + } + }, + watch: { + vValue: function (v) { + if (v != this.selectedHeaderName) { + this.selectedHeaderName = "" + } + + if (v.length == 0) { + this.matchedHeaders = [] + return + } + this.matchedHeaders = this.allHeaders.filter(function (header) { + return teaweb.match(header, v) + }).slice(0, 10) } }, methods: { - change: function(item) { - if (item != null) { - this.$emit("change", item.id) - } else { - this.$emit("change", 0) - } - }, - clear: function () { - this.$refs.comboBox.clear() + select: function (header) { + this.$emit("select", header) + this.selectedHeaderName = header } }, - template: `
- -
` + template: ` + {{header}} +     +` }) Vue.component("http-header-policy-box", { @@ -10549,89 +17651,719 @@ Vue.component("http-header-policy-box", { ` }) -// 通用设置 -Vue.component("http-common-config-box", { - props: ["v-common-config"], +Vue.component("http-header-replace-values", { + props: ["v-replace-values"], data: function () { - let config = this.vCommonConfig - if (config == null) { - config = { - mergeSlashes: false - } + let values = this.vReplaceValues + if (values == null) { + values = [] } return { - config: config + 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}}[空] + +
+
+
+
合并重复的路径分隔符 -
- - -
-

合并URL中重复的路径分隔符为一个,比如//hello/world中的//

-
+ + + + + + + + + + + + +
替换前内容 *
替换后内容
是否忽略大小写 + +
+ +
+   + +
+
+
+ +
+` +}) + +Vue.component("http-hls-config-box", { + props: ["value", "v-is-location", "v-is-group"], + data: function () { + let config = this.value + if (config == null) { + config = { + isPrior: false + } + } + + let encryptingConfig = config.encrypting + if (encryptingConfig == null) { + encryptingConfig = { + isOn: false, + onlyURLPatterns: [], + exceptURLPatterns: [] + } + config.encrypting = encryptingConfig + } + + return { + config: config, + + encryptingConfig: encryptingConfig, + encryptingMoreOptionsVisible: false + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) + }, + + showEncryptingMoreOptions: function () { + this.encryptingMoreOptionsVisible = !this.encryptingMoreOptionsVisible + } + }, + template: `
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + +
启用HLS加密 + +

启用后,系统会自动在.m3u8文件中加入#EXT-X-KEY:METHOD=AES-128...,并将其中的.ts文件内容进行加密。

+
例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
限制URL + +

如果填写了限制URL,表示只对这些URL进行加密处理;如果不填则表示支持所有的URL。

+
` }) -Vue.component("http-cache-policy-selector", { - props: ["v-cache-policy"], +Vue.component("http-host-redirect-box", { + props: ["v-redirects"], mounted: function () { let that = this - Tea.action("/servers/components/cache/count") - .post() - .success(function (resp) { - that.count = resp.data.count + 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 cachePolicy = this.vCachePolicy + let redirects = this.vRedirects + if (redirects == null) { + redirects = [] + } + + let id = 0 + redirects.forEach(function (v) { + id++ + v.id = id + }) + return { - count: 0, - cachePolicy: cachePolicy + 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: { - remove: function () { - this.cachePolicy = null - }, - select: function () { + add: function () { let that = this - teaweb.popup("/servers/components/cache/selectPopup", { - width: "42em", - height: "26em", + window.UPDATING_REDIRECT = null + + teaweb.popup("/servers/server/settings/redirects/createPopup", { + width: "50em", + height: "36em", callback: function (resp) { - that.cachePolicy = resp.data.cachePolicy + that.id++ + resp.data.redirect.id = that.id + that.redirects.push(resp.data.redirect) + that.change() } }) }, - create: function () { + update: function (index, redirect) { let that = this - teaweb.popup("/servers/components/cache/createPopup", { - height: "26em", + window.UPDATING_REDIRECT = redirect + + teaweb.popup("/servers/server/settings/redirects/createPopup", { + width: "50em", + height: "36em", callback: function (resp) { - that.cachePolicy = resp.data.cachePolicy + 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: `
-
- - {{cachePolicy.name}}     + + + + [创建] + +
+ +

暂时还没有URL跳转规则。

+
+ + + + + + + + + + + + + + + + + + + + + + + +
跳转前跳转后HTTP状态码状态操作
+
+ {{redirect.beforeURL}} +
+ URL跳转 + 匹配前缀 + 正则匹配 + 精准匹配 + 排除:{{domain}} + 仅限:{{domain}} +
+
+
+ 所有域名 + + {{redirect.domainsBefore[0]}} + {{redirect.domainsBefore[0]}}等{{redirect.domainsBefore.length}}个域名 + +
+ 域名跳转 + {{redirect.domainAfterScheme}} + 忽略端口 +
+
+
+ 所有端口 + + {{redirect.portsBefore.join(", ")}} + {{redirect.portsBefore.slice(0, 5).join(", ")}}等{{redirect.portsBefore.length}}个端口 + +
+ 端口跳转 + {{redirect.portAfterScheme}} +
+
+ +
+ 匹配条件 +
+
-> + {{redirect.afterURL}} + {{redirect.domainAfter}} + {{redirect.portAfter}} + + {{redirect.status}} + 默认 + + 修改   + 删除 +
+

所有规则匹配顺序为从上到下,可以拖动左侧的排序。

-
- [选择已有策略]     [创建新策略] +
+
` +}) + +Vue.component("http-location-labels", { + props: ["v-location-config", "v-server-id"], + data: function () { + return { + location: this.vLocationConfig + } + }, + methods: { + // 判断是否已启用某配置 + configIsOn: function (config) { + return config != null && config.isPrior && config.isOn + }, + + refIsOn: function (ref, config) { + return this.configIsOn(ref) && config != null && config.isOn + }, + + len: function (arr) { + return (arr == null) ? 0 : arr.length + }, + url: function (path) { + return "/servers/server/settings/locations" + path + "?serverId=" + this.vServerId + "&locationId=" + this.location.id + } + }, + template: `
+ + {{location.name}} + + +
+ {{domain}} +
+ + + BREAK + + + 自动跳转HTTPS + + + 文档根目录 + + + 源站 + + + 5秒盾 + + + CC防护 + + + + + + CACHE + + + {{location.web.charset.charset}} + + + + + + + + + Gzip:{{location.web.gzip.level}} + + + 请求Header + 响应Header + + + Websocket + + + 请求脚本 + + + 访客IP地址 + + + 请求限制 + + +
+
PAGE [状态码{{page.status[0]}}] -> {{page.url}}
+
+
+ 临时关闭 +
+ + +
+
+ REWRITE {{rewriteRule.pattern}} -> {{rewriteRule.replace}} +
` }) +Vue.component("http-location-labels-label", { + props: ["v-class", "v-href"], + template: `` +}) + +// 请求方法列表 +Vue.component("http-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等。

+
+
+
+ +
+
` +}) + +// 压缩配置 +Vue.component("http-optimization-config-box", { + props: ["v-optimization-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vOptimizationConfig + + return { + config: config, + htmlMoreOptions: false, + javascriptMoreOptions: false, + cssMoreOptions: false + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + } + }, + template: `
+ + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
HTML优化 +
+ + +
+

可以自动优化HTML中包含的空白、注释、空标签等。只有文件可以缓存时才会被优化。

+
HTML例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
HTML限制URL + +

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

+
+ + + + + + + + + + + + + + + + + + + + + +
Javascript优化 +
+ + +
+

可以自动缩短Javascript中变量、函数名称等。只有文件可以缓存时才会被优化。

+
Javascript例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
Javascript限制URL + +

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

+
+ + + + + + + + + + + + + + + + + + + + + +
CSS优化 +
+ + +
+

可以自动去除CSS中包含的空白。只有文件可以缓存时才会被优化。

+
CSS例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
CSS限制URL + +

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

+
+
+ +
+
` +}) + +Vue.component("http-oss-bucket-params", { + props: ["v-oss-config", "v-params", "name"], + data: function () { + let params = this.vParams + if (params == null) { + params = [] + } + + let ossConfig = this.vOssConfig + if (ossConfig == null) { + ossConfig = { + bucketParam: "input", + bucketName: "", + bucketArgName: "" + } + } else { + // 兼容以往 + if (ossConfig.bucketParam != null && ossConfig.bucketParam.length == 0) { + ossConfig.bucketParam = "input" + } + if (ossConfig.options != null && ossConfig.options.bucketName != null && ossConfig.options.bucketName.length > 0) { + ossConfig.bucketName = ossConfig.options.bucketName + } + } + + return { + params: params, + ossConfig: ossConfig + } + }, + template: ` + + {{name}}名称获取方式 * + + +

{{param.description.replace("\${optionName}", name)}}

+ + + + {{name}}名称 * + + +

{{name}}名称,类似于bucket-12345678

+ + + + {{name}}参数名称 * + + +

{{name}}参数名称,比如?myBucketName=BUCKET-NAME中的myBucketName

+ + +` +}) + Vue.component("http-pages-and-shutdown-box", { props: ["v-enable-global-pages", "v-pages", "v-shutdown-config", "v-is-location"], data: function () { @@ -10895,19 +18627,869 @@ Vue.component("http-pages-and-shutdown-box", {
` }) -// 页面动态加密配置 -Vue.component("http-encryption-config-box", { - props: ["v-encryption-config", "v-is-location", "v-is-group"], +Vue.component("http-pages-box", { + props: ["v-pages"], data: function () { - let config = this.vEncryptionConfig + let pages = [] + if (this.vPages != null) { + pages = this.vPages + } + + return { + pages: pages + } + }, + methods: { + addPage: function () { + let that = this + teaweb.popup("/servers/server/settings/pages/createPopup", { + height: "26em", + callback: function (resp) { + that.pages.push(resp.data.page) + that.notifyChange() + } + }) + }, + updatePage: function (pageIndex, pageId) { + let that = this + teaweb.popup("/servers/server/settings/pages/updatePopup?pageId=" + pageId, { + height: "26em", + callback: function (resp) { + Vue.set(that.pages, pageIndex, resp.data.page) + that.notifyChange() + } + }) + }, + removePage: function (pageIndex) { + let that = this + teaweb.confirm("确定要移除此页面吗?", function () { + that.pages.$remove(pageIndex) + that.notifyChange() + }) + }, + notifyChange: function () { + let parent = this.$el.parentNode + while (true) { + if (parent == null) { + break + } + if (parent.tagName == "FORM") { + break + } + parent = parent.parentNode + } + if (parent != null) { + setTimeout(function () { + Tea.runActionOn(parent) + }, 100) + } + } + }, + template: `
+ + +
+ + + + + + + + + + + + + + + + + + + +
响应状态码页面类型新状态码例外URL限制URL操作
+ + {{page.status[0]}} + {{page.status}} + + + + +
+ {{page.url}} +
+ 读取URL +
+
+
+ {{page.url}} +
+ 跳转URL + {{page.newStatus}} +
+
+
+ [HTML内容] +
+ {{page.newStatus}} +
+
+
+ {{page.newStatus}} + 保持 + +
+ {{urlPattern.pattern}} +
+ - +
+
+ {{urlPattern.pattern}} +
+ - +
+ 修改   + 删除 +
+
+
+ +
+
+
` +}) + +Vue.component("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-referers-config-box", { + props: ["v-referers-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vReferersConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + allowEmpty: true, + allowSameDomain: true, + allowDomains: [], + denyDomains: [], + checkOrigin: true + } + } + if (config.allowDomains == null) { + config.allowDomains = [] + } + if (config.denyDomains == null) { + config.denyDomains = [] + } + return { + config: config, + moreOptionsVisible: false + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + }, + changeAllowDomains: function (domains) { + if (typeof (domains) == "object") { + this.config.allowDomains = domains + this.$forceUpdate() + } + }, + changeDenyDomains: function (domains) { + if (typeof (domains) == "object") { + this.config.denyDomains = domains + this.$forceUpdate() + } + }, + showMoreOptions: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用防盗链 +
+ + +
+

选中后表示开启防盗链。

+
允许直接访问网站 + +

允许用户直接访问网站,用户第一次访问网站时来源域名通常为空。

+
来源域名允许一致 + +

允许来源域名和当前访问的域名一致,相当于在站内访问。

+
允许的来源域名 + > +

允许的其他来源域名列表,比如example.com*.example.com。单个星号*表示允许所有域名。

+
禁止的来源域名 + +

禁止的来源域名列表,比如example.org*.example.org;除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。

+
同时检查Origin + +

如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。

+
例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
限制URL + +

如果填写了限制URL,表示只对这些URL进行处理;如果不填则表示支持所有的URL。

+
+
+
` +}) + +Vue.component("http-remote-addr-config-box", { + props: ["v-remote-addr-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vRemoteAddrConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + value: "${rawRemoteAddr}", + type: "default", + + requestHeaderName: "" + } + } + + // type + if (config.type == null || config.type.length == 0) { + config.type = "default" + switch (config.value) { + case "${rawRemoteAddr}": + config.type = "default" + break + case "${remoteAddrValue}": + config.type = "default" + break + case "${remoteAddr}": + config.type = "proxy" + break + default: + if (config.value != null && config.value.length > 0) { + config.type = "variable" + } + } + } + + // value + if (config.value == null || config.value.length == 0) { + config.value = "${rawRemoteAddr}" + } return { config: config, - htmlMoreOptions: false, - javascriptMoreOptions: false, - keyPolicyMoreOptions: false, - cacheMoreOptions: false, - encryptionMoreOptions: false + options: [ + { + name: "直接获取", + description: "用户直接访问边缘节点,即 \"用户 --> 边缘节点\" 模式,这时候系统会试图从直接的连接中读取到客户端IP地址。", + value: "${rawRemoteAddr}", + type: "default" + }, + { + name: "从上级代理中获取", + description: "用户和边缘节点之间有别的代理服务转发,即 \"用户 --> [第三方代理服务] --> 边缘节点\",这时候只能从上级代理中获取传递的IP地址;上级代理传递的请求报头中必须包含 X-Forwarded-For 或 X-Real-IP 信息。", + value: "${remoteAddr}", + type: "proxy" + }, + { + name: "从请求报头中读取", + description: "从自定义请求报头读取客户端IP。", + value: "", + type: "requestHeader" + }, + { + name: "[自定义变量]", + description: "通过自定义变量来获取客户端真实的IP地址。", + value: "", + type: "variable" + } + ] + } + }, + watch: { + "config.requestHeaderName": function (value) { + if (this.config.type == "requestHeader"){ + this.config.value = "${header." + value.trim() + "}" + } + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + }, + changeOptionType: function () { + let that = this + + switch(this.config.type) { + case "default": + this.config.value = "${rawRemoteAddr}" + break + case "proxy": + this.config.value = "${remoteAddr}" + break + case "requestHeader": + this.config.value = "" + if (this.requestHeaderName != null && this.requestHeaderName.length > 0) { + this.config.value = "${header." + this.requestHeaderName + "}" + } + + setTimeout(function () { + that.$refs.requestHeaderInput.focus() + }) + break + case "variable": + this.config.value = "${rawRemoteAddr}" + + setTimeout(function () { + that.$refs.variableInput.focus() + }) + + break + } + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用访客IP设置 +
+ + +
+

选中后,表示使用自定义的请求变量获取客户端IP。

+
获取IP方式 * + +

{{option.description}}

+
请求报头 * + +

请输入包含有客户端IP的请求报头,需要注意大小写,常见的有X-Forwarded-ForX-Real-IPX-Client-IP等。

+
读取IP变量值 * + +

通过此变量获取用户的IP地址。具体可用的请求变量列表可参考官方网站文档;比如通过报头传递IP的情形,可以使用\${header.你的自定义报头}(类似于\${header.X-Forwarded-For},需要注意大小写规范)。

+
+
+
` +}) + +Vue.component("http-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-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("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-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: { @@ -10916,438 +19498,482 @@ Vue.component("http-encryption-config-box", { } }, template: `
- - - + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
启用请求限制 + +
最大并发连接数 + +

当前网站最大并发连接数,超出此限制则响应用户429代码。为0表示不限制。

+
单IP最大并发连接数 + +

单IP最大连接数,统计单个IP总连接数时不区分网站,超出此限制则响应用户429代码。为0表示不限制。当前设置的并发连接数过低,可能会影响正常用户访问,建议不小于3。

+
单连接带宽限制 + +

客户端单个请求每秒可以读取的下行流量。

+
单请求最大尺寸 + +

单个请求能发送的最大内容尺寸。

+
- -
-
- - - - - - - - - - - - - - - - -
启用页面动态加密 -
- - -
-

启用后,将对 HTML 页面中的 JavaScript 进行动态加密,有效抵御批量爬虫和脚本工具。

-
排除 URL - -

这些 URL 将跳过加密处理,支持正则表达式。

-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - -
HTML 加密 -
- - -
-

加密 HTML 页面中的 JavaScript 脚本。

-
加密内联脚本 -
- - -
-

加密 HTML 中的内联 <script> 标签内容。

-
加密外部脚本 -
- - -
-

加密通过 src 属性引入的外部 JavaScript 文件。

-
URL 匹配规则 - -

如果填写了匹配规则,表示只对这些 URL 进行加密处理;如果不填则表示支持所有的 URL。

-
- -
- - - - - - - - - - - - - - - - -
JavaScript 文件加密 -
- - -
-

加密独立的 JavaScript 文件(.js 文件)。

-
URL 匹配规则 - -

如果填写了匹配规则,表示只对这些 URL 进行加密处理;如果不填则表示支持所有的 URL。

-
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
服务器端密钥 - -

用于生成加密密钥的密码,建议使用复杂的随机字符串。默认密钥仅用于测试,生产环境请务必修改!

-
时间分片(秒) - -

加密密钥每隔多少秒更换一次。时间越短越安全,但可能影响性能。建议 60-120 秒,默认 60 秒。

-
IP CIDR 前缀长度 - -

将用户 IP 地址的前多少位作为识别依据。例如设置为 24 时,192.168.1.1 和 192.168.1.2 会被视为同一用户。默认 24。

-
简化 User-Agent -
- - -
-

开启后,只识别浏览器类型(如 Chrome、Firefox),忽略版本号等细节,避免因浏览器自动更新导致解密失败。

-
- -
- - - - - - - - - - - - - - - - - - - - -
启用缓存 -
- - -
-

开启后,相同内容的加密结果会被缓存,减少重复计算,提升响应速度。

-
缓存 TTL(秒) - -

缓存的有效期,超过这个时间后缓存会自动失效。建议与上面的"时间分片"保持一致。默认 60 秒。

-
最大缓存条目数 - -

最多缓存多少个加密结果。数量越大占用内存越多,建议根据服务器内存情况调整。默认 1000。

-
-
-
-
` }) - - -Vue.component("http-firewall-page-options", { - props: ["v-page-options"], +Vue.component("http-request-scripts-config-box", { + props: ["vRequestScriptsConfig", "v-auditing-status", "v-is-location"], data: function () { - var defaultPageBody = ` - - - 403 Forbidden - - - -

403 Forbidden By WAF

-
Connection: \${remoteAddr} (Client) -> \${serverAddr} (Server)
-
Request ID: \${requestId}
- -` + let config = this.vRequestScriptsConfig + if (config == null) { + config = {} + } return { - pageOptions: this.vPageOptions, - status: this.vPageOptions.status, - body: this.vPageOptions.body, - defaultPageBody: defaultPageBody, - isEditing: false + config: config } }, - watch: { - status: function (v) { - if (typeof v === "string" && v.length != 3) { + methods: { + changeInitGroup: function (group) { + this.config.initGroup = group + this.$forceUpdate() + }, + changeRequestGroup: function (group) { + this.config.requestGroup = group + this.$forceUpdate() + } + }, + template: `
+ +
+

请求初始化

+

在请求刚初始化时调用,此时自定义报头等尚未生效。

+
+ +
+

准备发送请求

+

在准备执行请求或者转发请求之前调用,此时自定义报头、源站等已准备好。

+
+ +
+
+
` +}) + +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 statusCode = parseInt(v) - if (isNaN(statusCode)) { - this.pageOptions.status = 403 - } else { - this.pageOptions.status = statusCode - } - }, - body: function (v) { - this.pageOptions.body = v - } - }, - methods: { - edit: function () { - this.isEditing = !this.isEditing + 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: `
- - 状态码:{{status}} / 提示内容:[{{pageOptions.body.length}}字符][无] - - - - - - - - - - +
+

暂时还没有重写规则。

+
状态码 *
网页内容 - -

[使用模板]

-
+ + + + + + + + + + + + + + + + + + + +
匹配规则转发目标转发方式状态操作
{{rule.pattern}} +
+ BREAK + {{rule.redirectStatus}} + Host: {{rule.proxyHost}} +
{{rule.replace}} + 隐式 + 显示 + + + + 修改   + 删除 +
-
-` +

拖动左侧的图标可以对重写规则进行排序。

+ +` }) -Vue.component("http-firewall-js-cookie-options", { - props: ["v-js-cookie-options"], - mounted: function () { - this.updateSummary() - }, - data: function () { - let options = this.vJsCookieOptions - if (options == null) { - options = { - life: 0, - maxFails: 0, - failBlockTimeout: 0, - failBlockScopeAll: false, - scope: "service" - } - } +Vue.component("http-rewrite-labels-label", { + props: ["v-class"], + template: `` +}) +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 { - options: options, - isEditing: false, - summary: "" + stat: stat } }, - watch: { - "options.life": function (v) { - let i = parseInt(v, 10) - if (isNaN(i)) { - i = 0 - } - this.options.life = i - this.updateSummary() - }, - "options.maxFails": function (v) { - let i = parseInt(v, 10) - if (isNaN(i)) { - i = 0 - } - this.options.maxFails = i - this.updateSummary() - }, - "options.failBlockTimeout": function (v) { - let i = parseInt(v, 10) - if (isNaN(i)) { - i = 0 - } - this.options.failBlockTimeout = i - this.updateSummary() - }, - "options.failBlockScopeAll": function (v) { - this.updateSummary() + template: `
+ + + + + + + + + +
启用统计 +
+ + +
+
+
+
` +}) + +// 请求方法列表 +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: { - edit: function () { - this.isEditing = !this.isEditing - }, - updateSummary: function () { - let summaryList = [] - if (this.options.life > 0) { - summaryList.push("有效时间" + this.options.life + "秒") - } - if (this.options.maxFails > 0) { - summaryList.push("最多失败" + this.options.maxFails + "次") - } - if (this.options.failBlockTimeout > 0) { - summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") - } - if (this.options.failBlockScopeAll) { - summaryList.push("尝试全局封禁") - } - - if (summaryList.length == 0) { - this.summary = "默认配置" - } else { - this.summary = summaryList.join(" / ") - } + add: function () { + this.isAdding = true + let that = this + setTimeout(function () { + that.$refs.addingStatus.focus() + }, 100) }, confirm: function () { - this.isEditing = false + 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: `
- - {{summary}} -
- - - - - - - - - - - - - - - - - - - -
有效时间 -
- - -
-

验证通过后在这个时间内不再验证,默认3600秒。

-
最多失败次数 -
- - -
-

建议填入一个不小于5的数字,以减少误判几率。允许用户失败尝试的最多次数,超过这个次数将被自动加入黑名单。如果为空或者为0,表示不限制。

-
失败拦截时间 -
- - -
-

在达到最多失败次数(大于0)时,自动拦截的时长;如果为0表示不自动拦截。

-
失败全局封禁 - -

选中后,表示允许系统尝试全局封禁某个IP,以提升封禁性能。

-
+ +
+ + {{status}} +   + +
-
-` +
+
+
+ +
+
+ +   +
+
+

格式为三位数字,比如200404等。

+
+
+
+ +
+
` }) -// 压缩配置 -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() - }) - }, +Vue.component("http-web-root-box", { + props: ["v-root-config", "v-is-location", "v-is-group"], data: function () { - let config = this.vCompressionConfig + let config = this.vRootConfig if (config == null) { config = { isPrior: false, isOn: false, - useDefaultTypes: true, - types: ["brotli", "gzip", "zstd", "deflate"], - level: 0, - decompressData: false, - gzipRef: null, - deflateRef: null, - brotliRef: null, - minLength: {count: 1, "unit": "kb"}, - maxLength: {count: 32, "unit": "mb"}, - mimeTypes: ["text/*", "application/javascript", "application/json", "application/atom+xml", "application/rss+xml", "application/xhtml+xml", "font/*", "image/svg+xml"], - extensions: [".js", ".json", ".html", ".htm", ".xml", ".css", ".woff2", ".txt"], - exceptExtensions: [".apk", ".ipa"], - conds: null, - enablePartialContent: false, + dir: "", + indexes: [], + stripPrefix: "", + decodePath: false, + isBreak: false, + exceptHiddenFiles: true, onlyURLPatterns: [], exceptURLPatterns: [] } } - - if (config.types == null) { - config.types = [] + if (config.indexes == null) { + config.indexes = [] } + + if (config.onlyURLPatterns == null) { + config.onlyURLPatterns = [] + } + if (config.exceptURLPatterns == null) { + config.exceptURLPatterns = [] + } + + return { + config: config, + advancedVisible: false + } + }, + methods: { + changeAdvancedVisible: function (v) { + this.advancedVisible = v + }, + addIndex: function () { + let that = this + teaweb.popup("/servers/server/settings/web/createIndex", { + height: "10em", + callback: function (resp) { + that.config.indexes.push(resp.data.index) + } + }) + }, + removeIndex: function (i) { + this.config.indexes.$remove(i) + }, + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用静态资源分发 +
+ + +
+
静态资源根目录 + +

可以访问此根目录下的静态资源。

+
首页文件 + +
+
+ {{index}} +
+
+
+ +

在URL中只有目录没有文件名时默认查找的首页文件。

+
例外URL + +

如果填写了例外URL,表示不支持通过这些URL访问。

+
限制URL + +

如果填写了限制URL,表示仅支持通过这些URL访问。

+
排除隐藏文件 + +

排除以点(.)符号开头的隐藏目录或文件,比如/.git/logs/HEAD

+
去除URL前缀 + +

可以把请求的路径部分前缀去除后再查找文件,比如把 /web/app/index.html 去除前缀 /web 后就变成 /app/index.html

+
路径解码 +
+ + +
+

是否对请求路径进行URL解码,比如把 /Web+App+Browser.html 解码成 /Web App Browser.html 再查找文件。

+
终止请求 +
+ + +
+

在找不到要访问的文件的情况下是否终止请求并返回404,如果选择终止请求,则不再尝试反向代理等设置。

+
+
+
` +}) + +Vue.component("http-webp-config-box", { + props: ["v-webp-config", "v-is-location", "v-is-group", "v-require-cache"], + data: function () { + let config = this.vWebpConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + minLength: {count: 0, "unit": "kb"}, + maxLength: {count: 0, "unit": "kb"}, + mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico"], + extensions: [".png", ".jpeg", ".jpg", ".bmp", ".ico"], + conds: null + } + } + if (config.mimeTypes == null) { config.mimeTypes = [] } @@ -11355,49 +19981,9 @@ Vue.component("http-compression-config-box", { 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 + moreOptionsVisible: false } }, methods: { @@ -11412,14 +19998,6 @@ Vue.component("http-compression-config-box", { }) this.config.extensions = values }, - changeExceptExtensions: function (values) { - values.forEach(function (v, k) { - if (v.length > 0 && v[0] != ".") { - values[k] = "." + v - } - }) - this.config.exceptExtensions = values - }, changeMimeTypes: function (values) { this.config.mimeTypes = values }, @@ -11428,103 +20006,38 @@ Vue.component("http-compression-config-box", { }, 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: `
- + - + - - - - - - - - - - - - - - - + - + @@ -11541,31 +20054,172 @@ Vue.component("http-compression-config-box", {

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

- - - - - - - - - - - - + + +
启用内容压缩启用WebP压缩
-
支持的扩展名 - -

含有这些扩展名的URL将会被压缩,不区分大小写。

-
例外扩展名 - -

含有这些扩展名的URL将不会被压缩,不区分大小写。

-
支持的MimeType - -

响应的Content-Type里包含这些MimeType的内容将会被压缩。

+

选中后表示开启自动WebP压缩;图片的宽和高均不能超过16383像素;只有满足缓存条件的图片内容才会被转换

压缩算法支持的扩展名 -
- - - -
-
-
-
-
- - -
-
-
- -

选择支持的压缩算法和优先顺序,拖动图表排序。

+ +

含有这些扩展名的URL将会被转成WebP,不区分大小写。

支持已压缩内容支持的MimeType - -

支持对已压缩内容尝试重新使用新的算法压缩;不选中表示保留当前的压缩格式。

+ +

响应的Content-Type里包含这些MimeType的内容将会被转成WebP。

支持Partial
Content
- -

支持对分片内容(PartialContent)的压缩;除非客户端有特殊要求,一般不需要启用。

-
例外URL - -

如果填写了例外URL,表示这些URL跳过不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行压缩处理;如果不填则表示支持所有的URL。

-
匹配条件 +
+
+
` +}) + +Vue.component("http-websocket-box", { + props: ["v-websocket-ref", "v-websocket-config", "v-is-location", "v-is-group"], + data: function () { + let websocketRef = this.vWebsocketRef + if (websocketRef == null) { + websocketRef = { + isPrior: false, + isOn: false, + websocketId: 0 + } + } + + let websocketConfig = this.vWebsocketConfig + if (websocketConfig == null) { + websocketConfig = { + id: 0, + isOn: false, + handshakeTimeout: { + count: 30, + unit: "second" + }, + allowAllOrigins: true, + allowedOrigins: [], + requestSameOrigin: true, + requestOrigin: "" + } + } else { + if (websocketConfig.handshakeTimeout == null) { + websocketConfig.handshakeTimeout = { + count: 30, + unit: "second", + } + } + if (websocketConfig.allowedOrigins == null) { + websocketConfig.allowedOrigins = [] + } + } + + return { + websocketRef: websocketRef, + websocketConfig: websocketConfig, + handshakeTimeoutCountString: websocketConfig.handshakeTimeout.count.toString(), + advancedVisible: false + } + }, + watch: { + handshakeTimeoutCountString: function (v) { + let count = parseInt(v) + if (!isNaN(count) && count >= 0) { + this.websocketConfig.handshakeTimeout.count = count + } else { + this.websocketConfig.handshakeTimeout.count = 0 + } + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.websocketRef.isPrior) && this.websocketRef.isOn + }, + changeAdvancedVisible: function (v) { + this.advancedVisible = v + }, + createOrigin: function () { + let that = this + teaweb.popup("/servers/server/settings/websocket/createOrigin", { + height: "12.5em", + callback: function (resp) { + that.websocketConfig.allowedOrigins.push(resp.data.origin) + } + }) + }, + removeOrigin: function (index) { + this.websocketConfig.allowedOrigins.$remove(index) + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -11574,257 +20228,868 @@ Vue.component("http-compression-config-box", { ` }) -// HTTP CC防护配置 -Vue.component("http-cc-config-box", { - props: ["v-cc-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vCcConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - enableFingerprint: true, - enableGET302: true, - onlyURLPatterns: [], - exceptURLPatterns: [], - useDefaultThresholds: true, - ignoreCommonFiles: true - } - } - - if (config.thresholds == null || config.thresholds.length == 0) { - config.thresholds = [ - { - maxRequests: 0 - }, - { - maxRequests: 0 - }, - { - maxRequests: 0 - } - ] - } - - if (typeof config.enableFingerprint != "boolean") { - config.enableFingerprint = true - } - if (typeof config.enableGET302 != "boolean") { - config.enableGET302 = true - } - - if (config.onlyURLPatterns == null) { - config.onlyURLPatterns = [] - } - if (config.exceptURLPatterns == null) { - config.exceptURLPatterns = [] - } - return { - config: config, - moreOptionsVisible: false, - minQPSPerIP: config.minQPSPerIP, - useCustomThresholds: !config.useDefaultThresholds, - - thresholdMaxRequests0: this.maxRequestsStringAtThresholdIndex(config, 0), - thresholdMaxRequests1: this.maxRequestsStringAtThresholdIndex(config, 1), - thresholdMaxRequests2: this.maxRequestsStringAtThresholdIndex(config, 2) - } +// 指标图表 +Vue.component("metric-chart", { + props: ["v-chart", "v-stats", "v-item", "v-column" /** in column? **/], + mounted: function () { + this.load() }, - watch: { - minQPSPerIP: function (v) { - let qps = parseInt(v.toString()) - if (isNaN(qps) || qps < 0) { - qps = 0 + 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 + }) + } } - this.config.minQPSPerIP = qps - }, - thresholdMaxRequests0: function (v) { - this.setThresholdMaxRequests(0, v) - }, - thresholdMaxRequests1: function (v) { - this.setThresholdMaxRequests(1, v) - }, - thresholdMaxRequests2: function (v) { - this.setThresholdMaxRequests(2, v) - }, - useCustomThresholds: function (b) { - this.config.useDefaultThresholds = !b + } + 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: { - maxRequestsStringAtThresholdIndex: function (config, index) { - if (config.thresholds == null) { + load: function () { + var el = document.getElementById(this.chartId) + if (el == null || el.offsetWidth == 0 || el.offsetHeight == 0) { + setTimeout(this.load, 100) + } else { + this.render(el) + } + }, + render: function (el) { + let chart = echarts.init(el) + window.addEventListener("resize", function () { + chart.resize() + }) + switch (this.chart.type) { + case "pie": + this.renderPie(chart) + break + case "bar": + this.renderBar(chart) + break + case "timeBar": + this.renderTimeBar(chart) + break + case "timeLine": + this.renderTimeLine(chart) + break + case "table": + this.renderTable(chart) + break + } + }, + renderPie: function (chart) { + let values = this.stats.map(function (v) { + return { + name: v.keys[0], + value: v.value + } + }) + let that = this + chart.setOption({ + tooltip: { + show: true, + trigger: "item", + formatter: function (data) { + let stat = that.stats[data.dataIndex] + let percent = 0 + if (stat.total > 0) { + percent = Math.round((stat.value * 100 / stat.total) * 100) / 100 + } + let value = stat.value + switch (that.item.valueType) { + case "byte": + value = teaweb.formatBytes(value) + break + case "count": + value = teaweb.formatNumber(value) + break + } + return stat.keys[0] + "
" + that.valueTypeName + ": " + value + "
占比:" + percent + "%" + } + }, + series: [ + { + name: name, + type: "pie", + data: values, + areaStyle: {}, + color: ["#9DD3E8", "#B2DB9E", "#F39494", "#FBD88A", "#879BD7"] + } + ] + }) + }, + renderTimeBar: function (chart) { + this.stats.$sort(function (v1, v2) { + return (v1.time < v2.time) ? -1 : 1 + }) + let values = this.stats.map(function (v) { + return v.value + }) + + let axis = {unit: "", divider: 1} + switch (this.item.valueType) { + case "count": + axis = teaweb.countAxis(values, function (v) { + return v + }) + break + case "byte": + axis = teaweb.bytesAxis(values, function (v) { + return v + }) + break + } + + let that = this + chart.setOption({ + xAxis: { + data: this.stats.map(function (v) { + return that.formatTime(v.time) + }) + }, + yAxis: { + axisLabel: { + formatter: function (value) { + return value + axis.unit + } + } + }, + tooltip: { + show: true, + trigger: "item", + formatter: function (data) { + let stat = that.stats[data.dataIndex] + let value = stat.value + switch (that.item.valueType) { + case "byte": + value = teaweb.formatBytes(value) + break + } + return that.formatTime(stat.time) + ": " + value + } + }, + grid: { + left: 50, + top: 10, + right: 20, + bottom: 25 + }, + series: [ + { + name: name, + type: "bar", + data: values.map(function (v) { + return v / axis.divider + }), + itemStyle: { + color: teaweb.DefaultChartColor + }, + areaStyle: {}, + barWidth: "10em" + } + ] + }) + }, + renderTimeLine: function (chart) { + this.stats.$sort(function (v1, v2) { + return (v1.time < v2.time) ? -1 : 1 + }) + let values = this.stats.map(function (v) { + return v.value + }) + + let axis = {unit: "", divider: 1} + switch (this.item.valueType) { + case "count": + axis = teaweb.countAxis(values, function (v) { + return v + }) + break + case "byte": + axis = teaweb.bytesAxis(values, function (v) { + return v + }) + break + } + + let that = this + chart.setOption({ + xAxis: { + data: this.stats.map(function (v) { + return that.formatTime(v.time) + }) + }, + yAxis: { + axisLabel: { + formatter: function (value) { + return value + axis.unit + } + } + }, + tooltip: { + show: true, + trigger: "item", + formatter: function (data) { + let stat = that.stats[data.dataIndex] + let value = stat.value + switch (that.item.valueType) { + case "byte": + value = teaweb.formatBytes(value) + break + } + return that.formatTime(stat.time) + ": " + value + } + }, + grid: { + left: 50, + top: 10, + right: 20, + bottom: 25 + }, + series: [ + { + name: name, + type: "line", + data: values.map(function (v) { + return v / axis.divider + }), + itemStyle: { + color: teaweb.DefaultChartColor + }, + areaStyle: {} + } + ] + }) + }, + renderBar: function (chart) { + let values = this.stats.map(function (v) { + return v.value + }) + let axis = {unit: "", divider: 1} + switch (this.item.valueType) { + case "count": + axis = teaweb.countAxis(values, function (v) { + return v + }) + break + case "byte": + axis = teaweb.bytesAxis(values, function (v) { + return v + }) + break + } + let bottom = 24 + let rotate = 0 + let result = teaweb.xRotation(chart, this.stats.map(function (v) { + return v.keys[0] + })) + if (result != null) { + bottom = result[0] + rotate = result[1] + } + let that = this + chart.setOption({ + xAxis: { + data: this.stats.map(function (v) { + return v.keys[0] + }), + axisLabel: { + interval: 0, + rotate: rotate + } + }, + tooltip: { + show: true, + trigger: "item", + formatter: function (data) { + let stat = that.stats[data.dataIndex] + let percent = 0 + if (stat.total > 0) { + percent = Math.round((stat.value * 100 / stat.total) * 100) / 100 + } + let value = stat.value + switch (that.item.valueType) { + case "byte": + value = teaweb.formatBytes(value) + break + case "count": + value = teaweb.formatNumber(value) + break + } + return stat.keys[0] + "
" + that.valueTypeName + ":" + value + "
占比:" + percent + "%" + } + }, + yAxis: { + axisLabel: { + formatter: function (value) { + return value + axis.unit + } + } + }, + grid: { + left: 40, + top: 10, + right: 20, + bottom: bottom + }, + series: [ + { + name: name, + type: "bar", + data: values.map(function (v) { + return v / axis.divider + }), + itemStyle: { + color: teaweb.DefaultChartColor + }, + areaStyle: {}, + barWidth: "10em" + } + ] + }) + + if (this.item.keys != null) { + // IP相关操作 + if (this.item.keys.$contains("${remoteAddr}")) { + let that = this + chart.on("click", function (args) { + let index = that.item.keys.$indexesOf("${remoteAddr}")[0] + let value = that.stats[args.dataIndex].keys[index] + teaweb.popup("/servers/ipbox?ip=" + value, { + width: "50em", + height: "30em" + }) + }) + } + } + }, + renderTable: function (chart) { + let table = `
启用Websocket +
+ + +
+
允许所有来源域(Origin) +
+ + +
+

选中表示允许所有的来源域。

+
允许的来源域列表(Origin) +
+
+ {{origin}} +
+
+
+ +

只允许在列表中的来源域名访问Websocket服务。

+
传递请求来源域 +
+ + +
+

选中后,表示把接收到的请求中的Origin字段传递到源站。

+
指定传递的来源域 + +

指定向源站传递的Origin字段值。

+
握手超时时间(Handshake) +
+
+ +
+
+ 秒 +
+
+

0表示使用默认的时间设置。

+ + + + + + + ` + 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 "" } - if (index < config.thresholds.length) { - let s = config.thresholds[index].maxRequests.toString() - if (s == "0") { - s = "" - } - return s + 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 "" - }, - setThresholdMaxRequests: function (index, v) { - let maxRequests = parseInt(v) - if (isNaN(maxRequests) || maxRequests < 0) { - maxRequests = 0 - } - if (index < this.config.thresholds.length) { - this.config.thresholds[index].maxRequests = maxRequests - } - }, - showMoreOptions: function () { - this.moreOptionsVisible = !this.moreOptionsVisible + return time } }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用CC无感防护 - -

启用后,自动检测并拦截CC攻击。

-
例外URL - -

如果填写了例外URL,表示这些URL跳过CC防护不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行CC防护处理;如果不填则表示支持所有的URL。

-
忽略常用文件 - -

忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。

-
检查请求来源指纹 - -

在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。

-
启用GET302校验 - -

选中后,表示自动通过GET302方法来校验客户端。

-
单IP最低QPS -
- - 请求数/秒 -
-

当某个IP在1分钟内平均QPS达到此值时,才会开始检测;如果设置为0,表示任何访问都会检测。(注意这里设置的是检测开启阈值,不是拦截阈值,拦截阈值在当前表单下方可以设置)

-
使用自定义拦截阈值 - -
自定义拦截阈值设置 -
-
- 单IP每5秒最多 - - 请求 -
-
- -
-
- 单IP每60秒 - - 请求 -
-
-
-
- 单IP每300秒 - - 请求 -
-
-
-
+ template: `
+

{{chart.name}} ({{valueTypeName}})

+
+
` }) -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 = "" // 不要给默认值,因为黑白名单等默认值均有不同 - } +Vue.component("metric-board", { + template: `
` +}) - return { - levels: [], - description: "", - level: value - } - }, - methods: { - change: function () { - this.$emit("change") +// 显示指标对象名 +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)}} +
` +}) - 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("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("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("origin-list-box", { + props: ["v-primary-origins", "v-backup-origins", "v-server-type", "v-params"], + data: function () { + return { + primaryOrigins: this.vPrimaryOrigins, + backupOrigins: this.vBackupOrigins + } + }, + methods: { + createPrimaryOrigin: function () { + teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, { + width: "45em", + height: "27em", + callback: function (resp) { + teaweb.success("保存成功", function () { + window.location.reload() + }) + } + }) + }, + createBackupOrigin: function () { + teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, { + width: "45em", + height: "27em", + callback: function (resp) { + teaweb.success("保存成功", function () { + window.location.reload() + }) + } + }) + }, + updateOrigin: function (originId, originType) { + teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, { + width: "45em", + height: "27em", + callback: function (resp) { + teaweb.success("保存成功", function () { + window.location.reload() + }) + } + }) + }, + deleteOrigin: function (originId, originAddr, originType) { + let that = this + teaweb.confirm("确定要删除此源站(" + originAddr + ")吗?", function () { + Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType) + .post() + .success(function () { + teaweb.success("删除成功", function () { + window.location.reload() + }) + }) + }) + }, + updateOriginIsOn: function (originId, originAddr, isOn) { + let message + let resultMessage + if (isOn) { + message = "确定要启用此源站(" + originAddr + ")吗?" + resultMessage = "启用成功" + } else { + message = "确定要停用此源站(" + originAddr + ")吗?" + resultMessage = "停用成功" + } + let that = this + teaweb.confirm(message, function () { + Tea.action("/servers/server/settings/origins/updateIsOn?" + that.vParams + "&originId=" + originId + "&isOn=" + (isOn ? 1 : 0)) + .post() + .success(function () { + teaweb.success(resultMessage, function () { + window.location.reload() + }) + }) + }) + } + }, + template: `
+

主要源站 [添加主要源站]

+

暂时还没有主要源站。

+ + +

备用源站 [添加备用源站]

+

暂时还没有备用源站。

+ +
` +}) + +Vue.component("origin-list-table", { + props: ["v-origins", "v-origin-type"], + data: function () { + let hasMatchedDomains = false + let origins = this.vOrigins + if (origins != null && origins.length > 0) { + origins.forEach(function (origin) { + if (origin.domains != null && origin.domains.length > 0) { + hasMatchedDomains = true + } + }) + } + + return { + hasMatchedDomains: hasMatchedDomains + } + }, + methods: { + deleteOrigin: function (originId, originAddr) { + this.$emit("deleteOrigin", originId, originAddr, this.vOriginType) + }, + updateOrigin: function (originId) { + this.$emit("updateOrigin", originId, this.vOriginType) + }, + updateOriginIsOn: function (originId, originAddr, isOn) { + this.$emit("updateOriginIsOn", originId, originAddr, isOn) + } + }, + template: ` + + + + + + + + + + + + + + + + + +
源站地址权重状态操作
+ {{origin.addr}}   +
+ 对象存储 + {{origin.name}} + 证书 + 主机名: {{origin.host}} + 端口跟随 + HTTP/2 + + 匹配: {{domain}} + 匹配: 所有域名 +
+
{{origin.weight}} + + + 修改   + 停用启用   + 删除 +
` +}) + +Vue.component("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}}

+
` }) @@ -11859,1296 +21124,6 @@ Vue.component("prior-checkbox", { ` }) -Vue.component("http-charsets-box", { - props: ["v-usual-charsets", "v-all-charsets", "v-charset-config", "v-is-location", "v-is-group"], - data: function () { - let charsetConfig = this.vCharsetConfig - if (charsetConfig == null) { - charsetConfig = { - isPrior: false, - isOn: false, - charset: "", - isUpper: false, - force: false - } - } - return { - charsetConfig: charsetConfig, - advancedVisible: false - } - }, - methods: { - changeAdvancedVisible: function (v) { - this.advancedVisible = v - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - -
启用字符编码 -
- - -
-
选择字符编码 -
强制替换 - -

选中后,表示强制覆盖已经设置的字符集;不选中,表示如果源站已经设置了字符集,则保留不修改。

-
字符编码大写 -
- - -
-

选中后将指定的字符编码转换为大写,比如默认为utf-8,选中后将改为UTF-8

-
-
-
` -}) - -Vue.component("http-expires-time-config-box", { - props: ["v-expires-time"], - data: function () { - let expiresTime = this.vExpiresTime - if (expiresTime == null) { - expiresTime = { - isPrior: false, - isOn: false, - overwrite: true, - autoCalculate: true, - duration: {count: -1, "unit": "hour"} - } - } - return { - expiresTime: expiresTime - } - }, - watch: { - "expiresTime.isPrior": function () { - this.notifyChange() - }, - "expiresTime.isOn": function () { - this.notifyChange() - }, - "expiresTime.overwrite": function () { - this.notifyChange() - }, - "expiresTime.autoCalculate": function () { - this.notifyChange() - } - }, - methods: { - notifyChange: function () { - this.$emit("change", this.expiresTime) - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - -
启用 -

启用后,将会在响应的Header中添加Expires字段,浏览器据此会将内容缓存在客户端;同时,在管理后台执行清理缓存时,也将无法清理客户端已有的缓存。

-
覆盖源站设置 - -

选中后,会覆盖源站Header中已有的Expires字段。

-
自动计算时间 -

根据已设置的缓存有效期进行计算。

-
强制缓存时间 - -

从客户端访问的时间开始要缓存的时长。

-
-
` -}) - -Vue.component("http-access-log-box", { - props: ["v-access-log", "v-keyword", "v-show-server-link"], - data: function () { - let accessLog = this.vAccessLog - if (accessLog.header != null && accessLog.header.Upgrade != null && accessLog.header.Upgrade.values != null && accessLog.header.Upgrade.values.$contains("websocket")) { - if (accessLog.scheme == "http") { - accessLog.scheme = "ws" - } else if (accessLog.scheme == "https") { - accessLog.scheme = "wss" - } - } - - // 对TAG去重 - if (accessLog.tags != null && accessLog.tags.length > 0) { - let tagMap = {} - accessLog.tags = accessLog.tags.$filter(function (k, tag) { - let b = (typeof (tagMap[tag]) == "undefined") - tagMap[tag] = true - return b - }) - } - - // 域名 - accessLog.unicodeHost = "" - if (accessLog.host != null && accessLog.host.startsWith("xn--")) { - // port - let portIndex = accessLog.host.indexOf(":") - if (portIndex > 0) { - accessLog.unicodeHost = punycode.ToUnicode(accessLog.host.substring(0, portIndex)) - } else { - accessLog.unicodeHost = punycode.ToUnicode(accessLog.host) - } - } - - return { - accessLog: accessLog - } - }, - methods: { - formatCost: function (seconds) { - if (seconds == null) { - return "0" - } - let s = (seconds * 1000).toString(); - let pieces = s.split("."); - if (pieces.length < 2) { - return s; - } - - return pieces[0] + "." + pieces[1].substring(0, 3); - }, - showLog: function () { - let that = this - let requestId = this.accessLog.requestId - this.$parent.$children.forEach(function (v) { - if (v.deselect != null) { - v.deselect() - } - }) - this.select() - teaweb.popup("/servers/server/log/viewPopup?requestId=" + requestId, { - width: "50em", - height: "28em", - onClose: function () { - that.deselect() - } - }) - }, - select: function () { - this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)" - }, - deselect: function () { - this.$refs.box.parentNode.style.cssText = "" - }, - mismatch: function () { - teaweb.warn("当前访问没有匹配到任何网站") - } - }, - template: `
-
- [{{accessLog.node.name}}节点] - - - [网站] - [网站] - - [{{accessLog.region}}] - {{accessLog.remoteAddr}} [{{accessLog.timeLocal}}] "{{accessLog.requestMethod}} {{accessLog.scheme}}://{{accessLog.host}}{{accessLog.requestURI}} {{accessLog.proto}}" {{accessLog.status}} - - {{accessLog.unicodeHost}} - - - cache {{accessLog.attrs['cache.status'].toLowerCase()}} - - waf {{accessLog.firewallActions}} - - - - {{tag}} - - - - - - WAF - - {{accessLog.wafInfo.group.name}} - - {{accessLog.wafInfo.set.name}} - - - - - - - 耗时:{{formatCost(accessLog.requestTime)}} ms   ({{accessLog.humanTime}}) -   -
-
` -}) - -// Javascript Punycode converter derived from example in RFC3492. -// This implementation is created by some@domain.name and released into public domain -// 代码来自:https://stackoverflow.com/questions/183485/converting-punycode-with-dash-character-to-unicode -var punycode = new function Punycode() { - // This object converts to and from puny-code used in IDN - // - // punycode.ToASCII ( domain ) - // - // Returns a puny coded representation of "domain". - // It only converts the part of the domain name that - // has non ASCII characters. I.e. it dosent matter if - // you call it with a domain that already is in ASCII. - // - // punycode.ToUnicode (domain) - // - // Converts a puny-coded domain name to unicode. - // It only converts the puny-coded parts of the domain name. - // I.e. it dosent matter if you call it on a string - // that already has been converted to unicode. - // - // - this.utf16 = { - // The utf16-class is necessary to convert from javascripts internal character representation to unicode and back. - decode: function (input) { - var output = [], i = 0, len = input.length, value, extra; - while (i < len) { - value = input.charCodeAt(i++); - if ((value & 0xF800) === 0xD800) { - extra = input.charCodeAt(i++); - if (((value & 0xFC00) !== 0xD800) || ((extra & 0xFC00) !== 0xDC00)) { - throw new RangeError("UTF-16(decode): Illegal UTF-16 sequence"); - } - value = ((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000; - } - output.push(value); - } - return output; - }, - encode: function (input) { - var output = [], i = 0, len = input.length, value; - while (i < len) { - value = input[i++]; - if ((value & 0xF800) === 0xD800) { - throw new RangeError("UTF-16(encode): Illegal UTF-16 value"); - } - if (value > 0xFFFF) { - value -= 0x10000; - output.push(String.fromCharCode(((value >>> 10) & 0x3FF) | 0xD800)); - value = 0xDC00 | (value & 0x3FF); - } - output.push(String.fromCharCode(value)); - } - return output.join(""); - } - } - - //Default parameters - var initial_n = 0x80; - var initial_bias = 72; - var delimiter = "\x2D"; - var base = 36; - var damp = 700; - var tmin = 1; - var tmax = 26; - var skew = 38; - var maxint = 0x7FFFFFFF; - - // decode_digit(cp) returns the numeric value of a basic code - // point (for use in representing integers) in the range 0 to - // base-1, or base if cp is does not represent a value. - - function decode_digit(cp) { - return cp - 48 < 10 ? cp - 22 : cp - 65 < 26 ? cp - 65 : cp - 97 < 26 ? cp - 97 : base; - } - - // encode_digit(d,flag) returns the basic code point whose value - // (when used for representing integers) is d, which needs to be in - // the range 0 to base-1. The lowercase form is used unless flag is - // nonzero, in which case the uppercase form is used. The behavior - // is undefined if flag is nonzero and digit d has no uppercase form. - - function encode_digit(d, flag) { - return d + 22 + 75 * (d < 26) - ((flag != 0) << 5); - // 0..25 map to ASCII a..z or A..Z - // 26..35 map to ASCII 0..9 - } - - //** Bias adaptation function ** - function adapt(delta, numpoints, firsttime) { - var k; - delta = firsttime ? Math.floor(delta / damp) : (delta >> 1); - delta += Math.floor(delta / numpoints); - - for (k = 0; delta > (((base - tmin) * tmax) >> 1); k += base) { - delta = Math.floor(delta / (base - tmin)); - } - return Math.floor(k + (base - tmin + 1) * delta / (delta + skew)); - } - - // encode_basic(bcp,flag) forces a basic code point to lowercase if flag is zero, - // uppercase if flag is nonzero, and returns the resulting code point. - // The code point is unchanged if it is caseless. - // The behavior is undefined if bcp is not a basic code point. - - function encode_basic(bcp, flag) { - bcp -= (bcp - 97 < 26) << 5; - return bcp + ((!flag && (bcp - 65 < 26)) << 5); - } - - // Main decode - this.decode = function (input, preserveCase) { - // Dont use utf16 - var output = []; - var case_flags = []; - var input_length = input.length; - - var n, out, i, bias, basic, j, ic, oldi, w, k, digit, t, len; - - // Initialize the state: - - n = initial_n; - i = 0; - bias = initial_bias; - - // Handle the basic code points: Let basic be the number of input code - // points before the last delimiter, or 0 if there is none, then - // copy the first basic code points to the output. - - basic = input.lastIndexOf(delimiter); - if (basic < 0) basic = 0; - - for (j = 0; j < basic; ++j) { - if (preserveCase) case_flags[output.length] = (input.charCodeAt(j) - 65 < 26); - if (input.charCodeAt(j) >= 0x80) { - throw new RangeError("Illegal input >= 0x80"); - } - output.push(input.charCodeAt(j)); - } - - // Main decoding loop: Start just after the last delimiter if any - // basic code points were copied; start at the beginning otherwise. - - for (ic = basic > 0 ? basic + 1 : 0; ic < input_length;) { - - // ic is the index of the next character to be consumed, - - // Decode a generalized variable-length integer into delta, - // which gets added to i. The overflow checking is easier - // if we increase i as we go, then subtract off its starting - // value at the end to obtain delta. - for (oldi = i, w = 1, k = base; ; k += base) { - if (ic >= input_length) { - throw RangeError("punycode_bad_input(1)"); - } - digit = decode_digit(input.charCodeAt(ic++)); - - if (digit >= base) { - throw RangeError("punycode_bad_input(2)"); - } - if (digit > Math.floor((maxint - i) / w)) { - throw RangeError("punycode_overflow(1)"); - } - i += digit * w; - t = k <= bias ? tmin : k >= bias + tmax ? tmax : k - bias; - if (digit < t) { - break; - } - if (w > Math.floor(maxint / (base - t))) { - throw RangeError("punycode_overflow(2)"); - } - w *= (base - t); - } - - out = output.length + 1; - bias = adapt(i - oldi, out, oldi === 0); - - // i was supposed to wrap around from out to 0, - // incrementing n each time, so we'll fix that now: - if (Math.floor(i / out) > maxint - n) { - throw RangeError("punycode_overflow(3)"); - } - n += Math.floor(i / out); - i %= out; - - // Insert n at position i of the output: - // Case of last character determines uppercase flag: - if (preserveCase) { - case_flags.splice(i, 0, input.charCodeAt(ic - 1) - 65 < 26); - } - - output.splice(i, 0, n); - i++; - } - if (preserveCase) { - for (i = 0, len = output.length; i < len; i++) { - if (case_flags[i]) { - output[i] = (String.fromCharCode(output[i]).toUpperCase()).charCodeAt(0); - } - } - } - return this.utf16.encode(output); - }; - - //** Main encode function ** - - this.encode = function (input, preserveCase) { - //** Bias adaptation function ** - - var n, delta, h, b, bias, j, m, q, k, t, ijv, case_flags; - - if (preserveCase) { - // Preserve case, step1 of 2: Get a list of the unaltered string - case_flags = this.utf16.decode(input); - } - // Converts the input in UTF-16 to Unicode - input = this.utf16.decode(input.toLowerCase()); - - var input_length = input.length; // Cache the length - - if (preserveCase) { - // Preserve case, step2 of 2: Modify the list to true/false - for (j = 0; j < input_length; j++) { - case_flags[j] = input[j] != case_flags[j]; - } - } - - var output = []; - - - // Initialize the state: - n = initial_n; - delta = 0; - bias = initial_bias; - - // Handle the basic code points: - for (j = 0; j < input_length; ++j) { - if (input[j] < 0x80) { - output.push( - String.fromCharCode( - case_flags ? encode_basic(input[j], case_flags[j]) : input[j] - ) - ); - } - } - - h = b = output.length; - - // h is the number of code points that have been handled, b is the - // number of basic code points - - if (b > 0) output.push(delimiter); - - // Main encoding loop: - // - while (h < input_length) { - // All non-basic code points < n have been - // handled already. Find the next larger one: - - for (m = maxint, j = 0; j < input_length; ++j) { - ijv = input[j]; - if (ijv >= n && ijv < m) m = ijv; - } - - // Increase delta enough to advance the decoder's - // state to , but guard against overflow: - - if (m - n > Math.floor((maxint - delta) / (h + 1))) { - throw RangeError("punycode_overflow (1)"); - } - delta += (m - n) * (h + 1); - n = m; - - for (j = 0; j < input_length; ++j) { - ijv = input[j]; - - if (ijv < n) { - if (++delta > maxint) return Error("punycode_overflow(2)"); - } - - if (ijv == n) { - // Represent delta as a generalized variable-length integer: - for (q = delta, k = base; ; k += base) { - t = k <= bias ? tmin : k >= bias + tmax ? tmax : k - bias; - if (q < t) break; - output.push(String.fromCharCode(encode_digit(t + (q - t) % (base - t), 0))); - q = Math.floor((q - t) / (base - t)); - } - output.push(String.fromCharCode(encode_digit(q, preserveCase && case_flags[j] ? 1 : 0))); - bias = adapt(delta, h + 1, h == b); - delta = 0; - ++h; - } - } - - ++delta, ++n; - } - return output.join(""); - } - - this.ToASCII = function (domain) { - var domain_array = domain.split("."); - var out = []; - for (var i = 0; i < domain_array.length; ++i) { - var s = domain_array[i]; - out.push( - s.match(/[^A-Za-z0-9-]/) ? - "xn--" + punycode.encode(s) : - s - ); - } - return out.join("."); - } - this.ToUnicode = function (domain) { - var domain_array = domain.split("."); - var out = []; - for (var i = 0; i < domain_array.length; ++i) { - var s = domain_array[i]; - out.push( - s.match(/^xn--/) ? - punycode.decode(s.slice(4)) : - s - ); - } - return out.join("."); - } -}(); - -Vue.component("http-firewall-block-options-viewer", { - props: ["v-block-options"], - data: function () { - return { - options: this.vBlockOptions - } - }, - template: `
- 默认设置 -
- 状态码:{{options.statusCode}} / 提示内容:[{{options.body.length}}字符][无] / 超时时间:{{options.timeout}}秒 / 最大封禁时长:{{options.timeoutMax}}秒 - / 尝试全局封禁 -
-
-` -}) - -Vue.component("http-access-log-config-box", { - props: ["v-access-log-config", "v-fields", "v-default-field-codes", "v-is-location", "v-is-group"], - data: function () { - let that = this - - // 初始化 - setTimeout(function () { - that.changeFields() - }, 100) - - let accessLog = { - isPrior: false, - isOn: false, - fields: [1, 2, 6, 7], - status1: true, - status2: true, - status3: true, - status4: true, - status5: true, - - firewallOnly: false, - enableClientClosed: false - } - if (this.vAccessLogConfig != null) { - accessLog = this.vAccessLogConfig - } - - this.vFields.forEach(function (v) { - if (that.vAccessLogConfig == null) { // 初始化默认值 - v.isChecked = that.vDefaultFieldCodes.$contains(v.code) - } else { - v.isChecked = accessLog.fields.$contains(v.code) - } - }) - - return { - accessLog: accessLog, - hasRequestBodyField: this.vFields.$contains(8), - showAdvancedOptions: false - } - }, - methods: { - changeFields: function () { - this.accessLog.fields = this.vFields.filter(function (v) { - return v.isChecked - }).map(function (v) { - return v.code - }) - this.hasRequestBodyField = this.accessLog.fields.$contains(8) - }, - changeAdvanced: function (v) { - this.showAdvancedOptions = v - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用访问日志 -
- - -
-
基础信息

默认记录客户端IP、请求URL等基础信息。

高级信息 -
- - -
-

在基础信息之外要存储的信息。 - 记录"请求Body"将会显著消耗更多的系统资源,建议仅在调试时启用,最大记录尺寸为2MiB。 -

-
要存储的访问日志状态码 -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
记录客户端中断日志 -
- - -
-

499的状态码记录客户端主动中断日志。

-
- -
-

WAF相关

- - - - - -
只记录WAF相关日志 - -

选中后只记录WAF相关的日志。通过此选项可有效减少访问日志数量,降低网络带宽和存储压力。

-
-
-
-
` -}) - -// 基本认证用户配置 -Vue.component("http-auth-basic-auth-user-box", { - props: ["v-users"], - data: function () { - let users = this.vUsers - if (users == null) { - users = [] - } - return { - users: users, - isAdding: false, - updatingIndex: -1, - - username: "", - password: "" - } - }, - methods: { - add: function () { - this.isAdding = true - this.username = "" - this.password = "" - - let that = this - setTimeout(function () { - that.$refs.username.focus() - }, 100) - }, - cancel: function () { - this.isAdding = false - this.updatingIndex = -1 - }, - confirm: function () { - let that = this - if (this.username.length == 0) { - teaweb.warn("请输入用户名", function () { - that.$refs.username.focus() - }) - return - } - if (this.password.length == 0) { - teaweb.warn("请输入密码", function () { - that.$refs.password.focus() - }) - return - } - if (this.updatingIndex < 0) { - this.users.push({ - username: this.username, - password: this.password - }) - } else { - this.users[this.updatingIndex].username = this.username - this.users[this.updatingIndex].password = this.password - } - this.cancel() - }, - update: function (index, user) { - this.updatingIndex = index - - this.isAdding = true - this.username = user.username - this.password = user.password - - let that = this - setTimeout(function () { - that.$refs.username.focus() - }, 100) - }, - remove: function (index) { - this.users.$remove(index) - } - }, - template: `
- -
-
- {{user.username}} - -
-
-
-
-
-
- -
-
- -
-
-   - -
-
-
-
- -
-
` -}) - -Vue.component("http-location-labels", { - props: ["v-location-config", "v-server-id"], - data: function () { - return { - location: this.vLocationConfig - } - }, - methods: { - // 判断是否已启用某配置 - configIsOn: function (config) { - return config != null && config.isPrior && config.isOn - }, - - refIsOn: function (ref, config) { - return this.configIsOn(ref) && config != null && config.isOn - }, - - len: function (arr) { - return (arr == null) ? 0 : arr.length - }, - url: function (path) { - return "/servers/server/settings/locations" + path + "?serverId=" + this.vServerId + "&locationId=" + this.location.id - } - }, - template: `
- - {{location.name}} - - -
- {{domain}} -
- - - BREAK - - - 自动跳转HTTPS - - - 文档根目录 - - - 源站 - - - 5秒盾 - - - CC防护 - - - - - - CACHE - - - {{location.web.charset.charset}} - - - - - - - - - Gzip:{{location.web.gzip.level}} - - - 请求Header - 响应Header - - - Websocket - - - 请求脚本 - - - 访客IP地址 - - - 请求限制 - - -
-
PAGE [状态码{{page.status[0]}}] -> {{page.url}}
-
-
- 临时关闭 -
- - -
-
- REWRITE {{rewriteRule.pattern}} -> {{rewriteRule.replace}} -
-
-
` -}) - -Vue.component("http-location-labels-label", { - props: ["v-class", "v-href"], - template: `` -}) - -Vue.component("http-gzip-box", { - props: ["v-gzip-config", "v-gzip-ref", "v-is-location"], - data: function () { - let gzip = this.vGzipConfig - if (gzip == null) { - gzip = { - isOn: true, - level: 0, - minLength: null, - maxLength: null, - conds: null - } - } - - return { - gzip: gzip, - advancedVisible: false - } - }, - methods: { - isOn: function () { - return (!this.vIsLocation || this.vGzipRef.isPrior) && this.vGzipRef.isOn - }, - changeAdvancedVisible: function (v) { - this.advancedVisible = v - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用Gzip压缩 -
- - -
-
压缩级别 - -

级别越高,压缩比例越大。

-
Gzip内容最小长度 - -

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

-
Gzip内容最大长度 - -

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

-
匹配条件 - -
-
` -}) - -Vue.component("script-config-box", { - props: ["id", "v-script-config", "comment", "v-auditing-status"], - mounted: function () { - let that = this - setTimeout(function () { - that.$forceUpdate() - }, 100) - }, - data: function () { - let config = this.vScriptConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - code: "", - auditingCode: "" - } - } - - let auditingStatus = null - if (config.auditingCodeMD5 != null && config.auditingCodeMD5.length > 0 && config.auditingCode != null && config.auditingCode.length > 0) { - config.code = config.auditingCode - - if (this.vAuditingStatus != null) { - for (let i = 0; i < this.vAuditingStatus.length; i++) { - let status = this.vAuditingStatus[i] - if (status.md5 == config.auditingCodeMD5) { - auditingStatus = status - break - } - } - } - } - - if (config.code.length == 0) { - config.code = "\n\n\n\n" - } - - return { - config: config, - auditingStatus: auditingStatus - } - }, - watch: { - "config.isOn": function () { - this.change() - } - }, - methods: { - change: function () { - this.$emit("change", this.config) - }, - changeCode: function (code) { - this.config.code = code - this.change() - }, - isPlus: function () { - if (Tea == null || Tea.Vue == null) { - return false - } - return Tea.Vue.teaIsPlus - } - }, - template: `
- - - - - - - - - - - - - -
启用脚本设置
脚本代码 -

- 管理员审核结果:审核通过。 - 管理员审核结果:驳回     驳回理由:{{auditingStatus.rejectedReason}} - 当前脚本将在审核后生效,请耐心等待审核结果。 去审核 » -

-

管理员审核结果:审核通过。

- {{config.code}} -

{{comment}}

-
-
` -}) - -Vue.component("http-firewall-js-cookie-options-viewer", { - props: ["v-js-cookie-options"], - mounted: function () { - this.updateSummary() - }, - data: function () { - let options = this.vJsCookieOptions - if (options == null) { - options = { - life: 0, - maxFails: 0, - failBlockTimeout: 0, - failBlockScopeAll: false, - scope: "" - } - } - return { - options: options, - summary: "" - } - }, - methods: { - updateSummary: function () { - let summaryList = [] - if (this.options.life > 0) { - summaryList.push("有效时间" + this.options.life + "秒") - } - if (this.options.maxFails > 0) { - summaryList.push("最多失败" + this.options.maxFails + "次") - } - if (this.options.failBlockTimeout > 0) { - summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") - } - if (this.options.failBlockScopeAll) { - summaryList.push("尝试全局封禁") - } - - if (summaryList.length == 0) { - this.summary = "默认配置" - } else { - this.summary = summaryList.join(" / ") - } - } - }, - template: `
{{summary}}
-` -}) - -Vue.component("ssl-certs-view", { - props: ["v-certs"], - data: function () { - let certs = this.vCerts - if (certs == null) { - certs = [] - } - return { - certs: certs - } - }, - methods: { - // 格式化时间 - formatTime: function (timestamp) { - return new Date(timestamp * 1000).format("Y-m-d") - }, - - // 查看详情 - viewCert: function (certId) { - teaweb.popup("/servers/certs/certPopup?certId=" + certId, { - height: "28em", - width: "48em" - }) - } - }, - template: `
-
-
- {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   -
-
-
` -}) - -Vue.component("http-firewall-captcha-options-viewer", { - props: ["v-captcha-options"], - mounted: function () { - this.updateSummary() - }, - data: function () { - let options = this.vCaptchaOptions - if (options == null) { - options = { - life: 0, - maxFails: 0, - failBlockTimeout: 0, - failBlockScopeAll: false, - uiIsOn: false, - uiTitle: "", - uiPrompt: "", - uiButtonTitle: "", - uiShowRequestId: false, - uiCss: "", - uiFooter: "", - uiBody: "", - cookieId: "", - lang: "" - } - } - return { - options: options, - summary: "", - captchaTypes: window.WAF_CAPTCHA_TYPES - } - }, - methods: { - updateSummary: function () { - let summaryList = [] - if (this.options.life > 0) { - summaryList.push("有效时间" + this.options.life + "秒") - } - if (this.options.maxFails > 0) { - summaryList.push("最多失败" + this.options.maxFails + "次") - } - if (this.options.failBlockTimeout > 0) { - summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") - } - if (this.options.failBlockScopeAll) { - summaryList.push("全局封禁") - } - let that = this - let typeDef = this.captchaTypes.$find(function (k, v) { - return v.code == that.options.captchaType - }) - if (typeDef != null) { - summaryList.push("默认验证方式:" + typeDef.name) - } - - if (this.options.captchaType == "default") { - if (this.options.uiIsOn) { - summaryList.push("定制UI") - } - } - - if (this.options.geeTestConfig != null && this.options.geeTestConfig.isOn) { - summaryList.push("已配置极验") - } - - if (summaryList.length == 0) { - this.summary = "默认配置" - } else { - this.summary = summaryList.join(" / ") - } - } - }, - template: `
{{summary}}
-` -}) - Vue.component("reverse-proxy-box", { props: ["v-reverse-proxy-ref", "v-reverse-proxy-config", "v-is-location", "v-is-group", "v-family"], data: function () { @@ -13490,347 +21465,137 @@ Vue.component("reverse-proxy-box", {
` }) -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: "" - } +Vue.component("script-config-box", { + props: ["id", "v-script-config", "comment", "v-auditing-status"], + mounted: function () { + let that = this + setTimeout(function () { + that.$forceUpdate() + }, 100) }, - 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 + let config = this.vScriptConfig if (config == null) { config = { isPrior: false, isOn: false, - value: "${rawRemoteAddr}", - type: "default", - - requestHeaderName: "" + code: "", + auditingCode: "" } } - // type - if (config.type == null || config.type.length == 0) { - config.type = "default" - switch (config.value) { - case "${rawRemoteAddr}": - config.type = "default" - break - case "${remoteAddrValue}": - config.type = "default" - break - case "${remoteAddr}": - config.type = "proxy" - break - default: - if (config.value != null && config.value.length > 0) { - config.type = "variable" + let auditingStatus = null + if (config.auditingCodeMD5 != null && config.auditingCodeMD5.length > 0 && config.auditingCode != null && config.auditingCode.length > 0) { + config.code = config.auditingCode + + if (this.vAuditingStatus != null) { + for (let i = 0; i < this.vAuditingStatus.length; i++) { + let status = this.vAuditingStatus[i] + if (status.md5 == config.auditingCodeMD5) { + auditingStatus = status + break } + } } } - // value - if (config.value == null || config.value.length == 0) { - config.value = "${rawRemoteAddr}" + if (config.code.length == 0) { + config.code = "\n\n\n\n" } return { config: config, - options: [ - { - name: "直接获取", - description: "用户直接访问边缘节点,即 \"用户 --> 边缘节点\" 模式,这时候系统会试图从直接的连接中读取到客户端IP地址。", - value: "${rawRemoteAddr}", - type: "default" - }, - { - name: "从上级代理中获取", - description: "用户和边缘节点之间有别的代理服务转发,即 \"用户 --> [第三方代理服务] --> 边缘节点\",这时候只能从上级代理中获取传递的IP地址;上级代理传递的请求报头中必须包含 X-Forwarded-For 或 X-Real-IP 信息。", - value: "${remoteAddr}", - type: "proxy" - }, - { - name: "从请求报头中读取", - description: "从自定义请求报头读取客户端IP。", - value: "", - type: "requestHeader" - }, - { - name: "[自定义变量]", - description: "通过自定义变量来获取客户端真实的IP地址。", - value: "", - type: "variable" - } - ] + auditingStatus: auditingStatus } }, watch: { - "config.requestHeaderName": function (value) { - if (this.config.type == "requestHeader"){ - this.config.value = "${header." + value.trim() + "}" - } + "config.isOn": function () { + this.change() } }, methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + change: function () { + this.$emit("change", this.config) }, - changeOptionType: function () { - let that = this - - switch(this.config.type) { - case "default": - this.config.value = "${rawRemoteAddr}" - break - case "proxy": - this.config.value = "${remoteAddr}" - break - case "requestHeader": - this.config.value = "" - if (this.requestHeaderName != null && this.requestHeaderName.length > 0) { - this.config.value = "${header." + this.requestHeaderName + "}" - } - - setTimeout(function () { - that.$refs.requestHeaderInput.focus() - }) - break - case "variable": - this.config.value = "${rawRemoteAddr}" - - setTimeout(function () { - that.$refs.variableInput.focus() - }) - - break + changeCode: function (code) { + this.config.code = code + this.change() + }, + isPlus: function () { + if (Tea == null || Tea.Vue == null) { + return false } + return Tea.Vue.teaIsPlus } }, template: `
- - - + - - + + - - - + + + - - - - - - - - - - - -
启用访客IP设置 -
- - -
-

选中后,表示使用自定义的请求变量获取客户端IP。

-
启用脚本设置
获取IP方式 *
脚本代码 - -

{{option.description}}

-
请求报头 * - -

请输入包含有客户端IP的请求报头,需要注意大小写,常见的有X-Forwarded-ForX-Real-IPX-Client-IP等。

-
读取IP变量值 * - -

通过此变量获取用户的IP地址。具体可用的请求变量列表可参考官方网站文档;比如通过报头传递IP的情形,可以使用\${header.你的自定义报头}(类似于\${header.X-Forwarded-For},需要注意大小写规范)。

+

+ 管理员审核结果:审核通过。 + 管理员审核结果:驳回     驳回理由:{{auditingStatus.rejectedReason}} + 当前脚本将在审核后生效,请耐心等待审核结果。 去审核 » +

+

管理员审核结果:审核通过。

+ {{config.code}} +

{{comment}}

-
` }) -// 访问日志搜索框 -Vue.component("http-access-log-search-box", { - props: ["v-ip", "v-domain", "v-keyword", "v-cluster-id", "v-node-id"], +Vue.component("script-group-config-box", { + props: ["v-group", "v-auditing-status", "v-is-location"], data: function () { - let ip = this.vIp - if (ip == null) { - ip = "" + let group = this.vGroup + if (group == null) { + group = { + isPrior: false, + isOn: true, + scripts: [] + } + } + if (group.scripts == null) { + group.scripts = [] } - let domain = this.vDomain - if (domain == null) { - domain = "" - } - - let keyword = this.vKeyword - if (keyword == null) { - keyword = "" + let script = null + if (group.scripts.length > 0) { + script = group.scripts[group.scripts.length - 1] } return { - ip: ip, - domain: domain, - keyword: keyword, - clusterId: this.vClusterId + group: group, + script: script } }, methods: { - cleanIP: function () { - this.ip = "" - this.submit() + changeScript: function (script) { + this.group.scripts = [script] // 目前只支持单个脚本 + this.change() }, - 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 + change: function () { + this.$emit("change", this.group) } }, - template: `
-
-
-
-
- IP - - -
+ template: `
+ + +
+
+
-
-
- 域名 - - -
-
-
-
- 关键词 - - -
-
-
-
-
-
- -
-
- -
- -
- -
-
` }) @@ -13855,2160 +21620,6 @@ Vue.component("server-config-copy-link", { template: `批量 ` }) -// 显示指标对象名 -Vue.component("metric-key-label", { - props: ["v-key"], - data: function () { - return { - keyDefs: window.METRIC_HTTP_KEYS - } - }, - methods: { - keyName: function (key) { - let that = this - let subKey = "" - let def = this.keyDefs.$find(function (k, v) { - if (v.code == key) { - return true - } - if (key.startsWith("${arg.") && v.code.startsWith("${arg.")) { - subKey = that.getSubKey("arg.", key) - return true - } - if (key.startsWith("${header.") && v.code.startsWith("${header.")) { - subKey = that.getSubKey("header.", key) - return true - } - if (key.startsWith("${cookie.") && v.code.startsWith("${cookie.")) { - subKey = that.getSubKey("cookie.", key) - return true - } - return false - }) - if (def != null) { - if (subKey.length > 0) { - return def.name + ": " + subKey - } - return def.name - } - return key - }, - getSubKey: function (prefix, key) { - prefix = "${" + prefix - let index = key.indexOf(prefix) - if (index >= 0) { - key = key.substring(index + prefix.length) - key = key.substring(0, key.length - 1) - return key - } - return "" - } - }, - template: `
- {{keyName(this.vKey)}} -
` -}) - -// 指标对象 -Vue.component("metric-keys-config-box", { - props: ["v-keys"], - data: function () { - let keys = this.vKeys - if (keys == null) { - keys = [] - } - return { - keys: keys, - isAdding: false, - key: "", - subKey: "", - keyDescription: "", - - keyDefs: window.METRIC_HTTP_KEYS - } - }, - watch: { - keys: function () { - this.$emit("change", this.keys) - } - }, - methods: { - cancel: function () { - this.key = "" - this.subKey = "" - this.keyDescription = "" - this.isAdding = false - }, - confirm: function () { - if (this.key.length == 0) { - return - } - - if (this.key.indexOf(".NAME") > 0) { - if (this.subKey.length == 0) { - teaweb.warn("请输入参数值") - return - } - this.key = this.key.replace(".NAME", "." + this.subKey) - } - this.keys.push(this.key) - this.cancel() - }, - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - if (that.$refs.key != null) { - that.$refs.key.focus() - } - }, 100) - }, - remove: function (index) { - this.keys.$remove(index) - }, - changeKey: function () { - if (this.key.length == 0) { - return - } - let that = this - let def = this.keyDefs.$find(function (k, v) { - return v.code == that.key - }) - if (def != null) { - this.keyDescription = def.description - } - }, - keyName: function (key) { - let that = this - let subKey = "" - let def = this.keyDefs.$find(function (k, v) { - if (v.code == key) { - return true - } - if (key.startsWith("${arg.") && v.code.startsWith("${arg.")) { - subKey = that.getSubKey("arg.", key) - return true - } - if (key.startsWith("${header.") && v.code.startsWith("${header.")) { - subKey = that.getSubKey("header.", key) - return true - } - if (key.startsWith("${cookie.") && v.code.startsWith("${cookie.")) { - subKey = that.getSubKey("cookie.", key) - return true - } - return false - }) - if (def != null) { - if (subKey.length > 0) { - return def.name + ": " + subKey - } - return def.name - } - return key - }, - getSubKey: function (prefix, key) { - prefix = "${" + prefix - let index = key.indexOf(prefix) - if (index >= 0) { - key = key.substring(index + prefix.length) - key = key.substring(0, key.length - 1) - return key - } - return "" - } - }, - template: `
- -
-
- {{keyName(key)}}   -
-
-
-
-
- -
-
- -
-
- -
-
- -
-
- - -
-
-

{{keyDescription}}

-
-
- -
-
` -}) - -Vue.component("http-web-root-box", { - props: ["v-root-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vRootConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - dir: "", - indexes: [], - stripPrefix: "", - decodePath: false, - isBreak: false, - exceptHiddenFiles: true, - onlyURLPatterns: [], - exceptURLPatterns: [] - } - } - if (config.indexes == null) { - config.indexes = [] - } - - if (config.onlyURLPatterns == null) { - config.onlyURLPatterns = [] - } - if (config.exceptURLPatterns == null) { - config.exceptURLPatterns = [] - } - - return { - config: config, - advancedVisible: false - } - }, - methods: { - changeAdvancedVisible: function (v) { - this.advancedVisible = v - }, - addIndex: function () { - let that = this - teaweb.popup("/servers/server/settings/web/createIndex", { - height: "10em", - callback: function (resp) { - that.config.indexes.push(resp.data.index) - } - }) - }, - removeIndex: function (i) { - this.config.indexes.$remove(i) - }, - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用静态资源分发 -
- - -
-
静态资源根目录 - -

可以访问此根目录下的静态资源。

-
首页文件 - -
-
- {{index}} -
-
-
- -

在URL中只有目录没有文件名时默认查找的首页文件。

-
例外URL - -

如果填写了例外URL,表示不支持通过这些URL访问。

-
限制URL - -

如果填写了限制URL,表示仅支持通过这些URL访问。

-
排除隐藏文件 - -

排除以点(.)符号开头的隐藏目录或文件,比如/.git/logs/HEAD

-
去除URL前缀 - -

可以把请求的路径部分前缀去除后再查找文件,比如把 /web/app/index.html 去除前缀 /web 后就变成 /app/index.html

-
路径解码 -
- - -
-

是否对请求路径进行URL解码,比如把 /Web+App+Browser.html 解码成 /Web App Browser.html 再查找文件。

-
终止请求 -
- - -
-

在找不到要访问的文件的情况下是否终止请求并返回404,如果选择终止请求,则不再尝试反向代理等设置。

-
-
-
` -}) - -Vue.component("http-webp-config-box", { - props: ["v-webp-config", "v-is-location", "v-is-group", "v-require-cache"], - data: function () { - let config = this.vWebpConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - minLength: {count: 0, "unit": "kb"}, - maxLength: {count: 0, "unit": "kb"}, - mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico"], - extensions: [".png", ".jpeg", ".jpg", ".bmp", ".ico"], - conds: null - } - } - - if (config.mimeTypes == null) { - config.mimeTypes = [] - } - if (config.extensions == null) { - config.extensions = [] - } - - return { - config: config, - moreOptionsVisible: false - } - }, - methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn - }, - changeExtensions: function (values) { - values.forEach(function (v, k) { - if (v.length > 0 && v[0] != ".") { - values[k] = "." + v - } - }) - this.config.extensions = values - }, - changeMimeTypes: function (values) { - this.config.mimeTypes = values - }, - changeAdvancedVisible: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - }, - changeConds: function (conds) { - this.config.conds = conds - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用WebP压缩 -
- - -
-

选中后表示开启自动WebP压缩;图片的宽和高均不能超过16383像素;只有满足缓存条件的图片内容才会被转换

-
支持的扩展名 - -

含有这些扩展名的URL将会被转成WebP,不区分大小写。

-
支持的MimeType - -

响应的Content-Type里包含这些MimeType的内容将会被转成WebP。

-
内容最小长度 - -

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

-
内容最大长度 - -

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

-
匹配条件 - -
-
-
` -}) - -Vue.component("origin-scheduling-view-box", { - props: ["v-scheduling", "v-params"], - data: function () { - let scheduling = this.vScheduling - if (scheduling == null) { - scheduling = {} - } - return { - scheduling: scheduling - } - }, - methods: { - update: function () { - teaweb.popup("/servers/server/settings/reverseProxy/updateSchedulingPopup?" + this.vParams, { - height: "21em", - callback: function () { - window.location.reload() - }, - }) - } - }, - template: `
-
- - - - - -
当前正在使用的算法 - {{scheduling.name}}   [修改] -

{{scheduling.description}}

-
-
` -}) - -Vue.component("http-firewall-block-options", { - props: ["v-block-options"], - data: function () { - return { - options: this.vBlockOptions, - statusCode: this.vBlockOptions.statusCode, - timeout: this.vBlockOptions.timeout, - timeoutMax: this.vBlockOptions.timeoutMax, - isEditing: false - } - }, - watch: { - statusCode: function (v) { - let statusCode = parseInt(v) - if (isNaN(statusCode)) { - this.options.statusCode = 403 - } else { - this.options.statusCode = statusCode - } - }, - timeout: function (v) { - let timeout = parseInt(v) - if (isNaN(timeout)) { - this.options.timeout = 0 - } else { - this.options.timeout = timeout - } - }, - timeoutMax: function (v) { - let timeoutMax = parseInt(v) - if (isNaN(timeoutMax)) { - this.options.timeoutMax = 0 - } else { - this.options.timeoutMax = timeoutMax - } - } - }, - methods: { - edit: function () { - this.isEditing = !this.isEditing - } - }, - template: `
- - 状态码:{{statusCode}} / 提示内容:[{{options.body.length}}字符][无] / 封禁时长:{{timeout}}秒 - / 最大封禁时长:{{timeoutMax}}秒 - / 尝试全局封禁 - - - - - - - - - - - - - - - - - - - - - - -
状态码 - -
提示内容 - -
封禁时长 -
- - -
-

触发阻止动作时,封禁客户端IP的时间。

-
最大封禁时长 -
- - -
-

如果最大封禁时长大于封禁时长({{timeout}}秒),那么表示每次封禁的时候,将会在这两个时长数字之间随机选取一个数字作为最终的封禁时长。

-
失败全局封禁 - -

选中后,表示允许系统尝试全局封禁某个IP,以提升封禁性能。

-
-
-` -}) - -Vue.component("http-hls-config-box", { - props: ["value", "v-is-location", "v-is-group"], - data: function () { - let config = this.value - if (config == null) { - config = { - isPrior: false - } - } - - let encryptingConfig = config.encrypting - if (encryptingConfig == null) { - encryptingConfig = { - isOn: false, - onlyURLPatterns: [], - exceptURLPatterns: [] - } - config.encrypting = encryptingConfig - } - - return { - config: config, - - encryptingConfig: encryptingConfig, - encryptingMoreOptionsVisible: false - } - }, - methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) - }, - - showEncryptingMoreOptions: function () { - this.encryptingMoreOptionsVisible = !this.encryptingMoreOptionsVisible - } - }, - template: `
- - - -
- - - - - - - - - - - - - - - - - - - - - - - -
启用HLS加密 - -

启用后,系统会自动在.m3u8文件中加入#EXT-X-KEY:METHOD=AES-128...,并将其中的.ts文件内容进行加密。

-
例外URL - -

如果填写了例外URL,表示这些URL跳过不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行加密处理;如果不填则表示支持所有的URL。

-
-
-
` -}) - -Vue.component("http-oss-bucket-params", { - props: ["v-oss-config", "v-params", "name"], - data: function () { - let params = this.vParams - if (params == null) { - params = [] - } - - let ossConfig = this.vOssConfig - if (ossConfig == null) { - ossConfig = { - bucketParam: "input", - bucketName: "", - bucketArgName: "" - } - } else { - // 兼容以往 - if (ossConfig.bucketParam != null && ossConfig.bucketParam.length == 0) { - ossConfig.bucketParam = "input" - } - if (ossConfig.options != null && ossConfig.options.bucketName != null && ossConfig.options.bucketName.length > 0) { - ossConfig.bucketName = ossConfig.options.bucketName - } - } - - return { - params: params, - ossConfig: ossConfig - } - }, - template: ` - - {{name}}名称获取方式 * - - -

{{param.description.replace("\${optionName}", name)}}

- - - - {{name}}名称 * - - -

{{name}}名称,类似于bucket-12345678

- - - - {{name}}参数名称 * - - -

{{name}}参数名称,比如?myBucketName=BUCKET-NAME中的myBucketName

- - -` -}) - -Vue.component("http-request-scripts-config-box", { - props: ["vRequestScriptsConfig", "v-auditing-status", "v-is-location"], - data: function () { - let config = this.vRequestScriptsConfig - if (config == null) { - config = {} - } - - return { - config: config - } - }, - methods: { - changeInitGroup: function (group) { - this.config.initGroup = group - this.$forceUpdate() - }, - changeRequestGroup: function (group) { - this.config.requestGroup = group - this.$forceUpdate() - } - }, - template: `
- -
-

请求初始化

-

在请求刚初始化时调用,此时自定义报头等尚未生效。

-
- -
-

准备发送请求

-

在准备执行请求或者转发请求之前调用,此时自定义报头、源站等已准备好。

-
- -
-
-
` -}) - -Vue.component("http-request-cond-view", { - props: ["v-cond"], - data: function () { - return { - cond: this.vCond, - components: window.REQUEST_COND_COMPONENTS - } - }, - methods: { - typeName: function (cond) { - let c = this.components.$find(function (k, v) { - return v.type == cond.type - }) - if (c != null) { - return c.name; - } - return cond.param + " " + cond.operator - }, - updateConds: function (conds, simpleCond) { - for (let k in simpleCond) { - if (simpleCond.hasOwnProperty(k)) { - this.cond[k] = simpleCond[k] - } - } - }, - notifyChange: function () { - - } - }, - template: `
- - {{cond.param}} {{cond.operator}} - {{typeName(cond)}}: - {{cond.value}} - - -
` -}) - -Vue.component("http-header-assistant", { - props: ["v-type", "v-value"], - mounted: function () { - let that = this - Tea.action("/servers/headers/options?type=" + this.vType) - .post() - .success(function (resp) { - that.allHeaders = resp.data.headers - }) - }, - data: function () { - return { - allHeaders: [], - matchedHeaders: [], - - selectedHeaderName: "" - } - }, - watch: { - vValue: function (v) { - if (v != this.selectedHeaderName) { - this.selectedHeaderName = "" - } - - if (v.length == 0) { - this.matchedHeaders = [] - return - } - this.matchedHeaders = this.allHeaders.filter(function (header) { - return teaweb.match(header, v) - }).slice(0, 10) - } - }, - methods: { - select: function (header) { - this.$emit("select", header) - this.selectedHeaderName = header - } - }, - template: ` - {{header}} -     -` -}) - -Vue.component("http-firewall-rules-box", { - props: ["v-rules", "v-type"], - data: function () { - let rules = this.vRules - if (rules == null) { - rules = [] - } - return { - rules: rules - } - }, - methods: { - addRule: function () { - window.UPDATING_RULE = null - let that = this - teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { - height: "30em", - callback: function (resp) { - that.rules.push(resp.data.rule) - } - }) - }, - updateRule: function (index, rule) { - window.UPDATING_RULE = teaweb.clone(rule) - let that = this - teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { - height: "30em", - callback: function (resp) { - Vue.set(that.rules, index, resp.data.rule) - } - }) - }, - removeRule: function (index) { - let that = this - teaweb.confirm("确定要删除此规则吗?", function () { - that.rules.$remove(index) - }) - }, - operatorName: function (operatorCode) { - let operatorName = operatorCode - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorName = v.name - } - }) - } - - return operatorName - }, - operatorDescription: function (operatorCode) { - let operatorName = operatorCode - let operatorDescription = "" - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorName = v.name - operatorDescription = v.description - } - }) - } - - return operatorName + ": " + operatorDescription - }, - operatorDataType: function (operatorCode) { - let operatorDataType = "none" - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorDataType = v.dataType - } - }) - } - - return operatorDataType - }, - calculateParamName: function (param) { - let paramName = "" - if (param != null) { - window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { - if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { - paramName = checkpoint.name - } - }) - } - return paramName - }, - calculateParamDescription: function (param) { - let paramName = "" - let paramDescription = "" - if (param != null) { - window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { - if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { - paramName = checkpoint.name - paramDescription = checkpoint.description - } - }) - } - return paramName + ": " + paramDescription - }, - isEmptyString: function (v) { - return typeof v == "string" && v.length == 0 - } - }, - template: `
- -
-
- {{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} - - - - {{rule.checkpointOptions.period}}秒内请求数 - - - - - 允许{{rule.checkpointOptions.allowDomains}} - 禁止{{rule.checkpointOptions.denyDomains}} - - - - | {{paramFilter.code}} <{{operatorName(rule.operator)}}> - {{rule.value}} - [空] - - - - ({{rule.description}}) - - - -
-
-
- -
` -}) - -Vue.component("http-fastcgi-box", { - props: ["v-fastcgi-ref", "v-fastcgi-configs", "v-is-location"], - data: function () { - let fastcgiRef = this.vFastcgiRef - if (fastcgiRef == null) { - fastcgiRef = { - isPrior: false, - isOn: false, - fastcgiIds: [] - } - } - let fastcgiConfigs = this.vFastcgiConfigs - if (fastcgiConfigs == null) { - fastcgiConfigs = [] - } else { - fastcgiRef.fastcgiIds = fastcgiConfigs.map(function (v) { - return v.id - }) - } - - return { - fastcgiRef: fastcgiRef, - fastcgiConfigs: fastcgiConfigs, - advancedVisible: false - } - }, - methods: { - isOn: function () { - return (!this.vIsLocation || this.fastcgiRef.isPrior) && this.fastcgiRef.isOn - }, - createFastcgi: function () { - let that = this - teaweb.popup("/servers/server/settings/fastcgi/createPopup", { - height: "26em", - callback: function (resp) { - teaweb.success("添加成功", function () { - that.fastcgiConfigs.push(resp.data.fastcgi) - that.fastcgiRef.fastcgiIds.push(resp.data.fastcgi.id) - }) - } - }) - }, - updateFastcgi: function (fastcgiId, index) { - let that = this - teaweb.popup("/servers/server/settings/fastcgi/updatePopup?fastcgiId=" + fastcgiId, { - callback: function (resp) { - teaweb.success("修改成功", function () { - Vue.set(that.fastcgiConfigs, index, resp.data.fastcgi) - }) - } - }) - }, - removeFastcgi: function (index) { - this.fastcgiRef.fastcgiIds.$remove(index) - this.fastcgiConfigs.$remove(index) - } - }, - template: `
- - - - - - - - - - - - - - - -
启用配置 -
- - -
-
Fastcgi服务 -
-
- {{fastcgi.address}}     -
-
-
- -
-
-
` -}) - -// 请求方法列表 -Vue.component("http-methods-box", { - props: ["v-methods"], - data: function () { - let methods = this.vMethods - if (methods == null) { - methods = [] - } - return { - methods: methods, - isAdding: false, - addingMethod: "" - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.addingMethod.focus() - }, 100) - }, - confirm: function () { - let that = this - - // 删除其中的空格 - this.addingMethod = this.addingMethod.replace(/\s/g, "").toUpperCase() - - if (this.addingMethod.length == 0) { - teaweb.warn("请输入要添加的请求方法", function () { - that.$refs.addingMethod.focus() - }) - return - } - - // 是否已经存在 - if (this.methods.$contains(this.addingMethod)) { - teaweb.warn("此请求方法已经存在,无需重复添加", function () { - that.$refs.addingMethod.focus() - }) - return - } - - this.methods.push(this.addingMethod) - this.cancel() - }, - remove: function (index) { - this.methods.$remove(index) - }, - cancel: function () { - this.isAdding = false - this.addingMethod = "" - } - }, - template: `
- -
- - {{method}} -   - -
-
-
-
-
- -
-
- -   -
-
-

格式为大写,比如GETPOST等。

-
-
-
- -
-
` -}) - -// URL扩展名条件 -Vue.component("http-cond-url-extension", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPathLowerExtension}", - operator: "in", - value: "[]" - } - if (this.vCond != null && this.vCond.param == cond.param) { - cond.value = this.vCond.value - } - - let extensions = [] - try { - extensions = JSON.parse(cond.value) - } catch (e) { - - } - - return { - cond: cond, - extensions: extensions, // TODO 可以拖动排序 - - isAdding: false, - addingExt: "" - } - }, - watch: { - extensions: function () { - this.cond.value = JSON.stringify(this.extensions) - } - }, - methods: { - addExt: function () { - this.isAdding = !this.isAdding - - if (this.isAdding) { - let that = this - setTimeout(function () { - that.$refs.addingExt.focus() - }, 100) - } - }, - cancelAdding: function () { - this.isAdding = false - this.addingExt = "" - }, - confirmAdding: function () { - // TODO 做更详细的校验 - // TODO 如果有重复的则提示之 - - if (this.addingExt.length == 0) { - return - } - - let that = this - this.addingExt.split(/[,;,;|]/).forEach(function (ext) { - ext = ext.trim() - if (ext.length > 0) { - if (ext[0] != ".") { - ext = "." + ext - } - ext = ext.replace(/\s+/g, "").toLowerCase() - that.extensions.push(ext) - } - }) - - // 清除状态 - this.cancelAdding() - }, - removeExt: function (index) { - this.extensions.$remove(index) - } - }, - template: `
- -
-
{{ext}}
-
-
-
-
- -
-
- - -
-
-
- -
-

扩展名需要包含点(.)符号,例如.jpg.png之类;多个扩展名用逗号分割。

-
` -}) - -// 排除URL扩展名条件 -Vue.component("http-cond-url-not-extension", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPathLowerExtension}", - operator: "not in", - value: "[]" - } - if (this.vCond != null && this.vCond.param == cond.param) { - cond.value = this.vCond.value - } - - let extensions = [] - try { - extensions = JSON.parse(cond.value) - } catch (e) { - - } - - return { - cond: cond, - extensions: extensions, // TODO 可以拖动排序 - - isAdding: false, - addingExt: "" - } - }, - watch: { - extensions: function () { - this.cond.value = JSON.stringify(this.extensions) - } - }, - methods: { - addExt: function () { - this.isAdding = !this.isAdding - - if (this.isAdding) { - let that = this - setTimeout(function () { - that.$refs.addingExt.focus() - }, 100) - } - }, - cancelAdding: function () { - this.isAdding = false - this.addingExt = "" - }, - confirmAdding: function () { - // TODO 做更详细的校验 - // TODO 如果有重复的则提示之 - - if (this.addingExt.length == 0) { - return - } - if (this.addingExt[0] != ".") { - this.addingExt = "." + this.addingExt - } - this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase() - this.extensions.push(this.addingExt) - - // 清除状态 - this.cancelAdding() - }, - removeExt: function (index) { - this.extensions.$remove(index) - } - }, - template: `
- -
-
{{ext}}
-
-
-
-
- -
-
- - -
-
-
- -
-

扩展名需要包含点(.)符号,例如.jpg.png之类。

-
` -}) - -// 根据URL前缀 -Vue.component("http-cond-url-prefix", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "prefix", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof (this.vCond.value) == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static/images,不需要带域名。

-
` -}) - -Vue.component("http-cond-url-not-prefix", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "prefix", - value: "", - isReverse: true, - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

要排除的URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static/images,不需要带域名。

-
` -}) - -// 首页 -Vue.component("http-cond-url-eq-index", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "eq", - value: "/", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

检查URL路径是为/,不需要带域名。

-
` -}) - -// 全站 -Vue.component("http-cond-url-all", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "prefix", - value: "/", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

支持全站所有URL。

-
` -}) - -// URL精准匹配 -Vue.component("http-cond-url-eq", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "eq", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

完整的URL路径,通常以/开头,比如/static/ui.js,不需要带域名。

-
` -}) - -Vue.component("http-cond-url-not-eq", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "eq", - value: "", - isReverse: true, - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

要排除的完整的URL路径,通常以/开头,比如/static/ui.js,不需要带域名。

-
` -}) - -// URL正则匹配 -Vue.component("http-cond-url-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

匹配URL的正则表达式,比如^/static/(.*).js$,不需要带域名。

-
` -}) - -// 排除URL正则匹配 -Vue.component("http-cond-url-not-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "not regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

不要匹配URL的正则表达式,意即只要匹配成功则排除此条件,比如^/static/(.*).js$,不需要带域名。

-
` -}) - -// URL通配符 -Vue.component("http-cond-url-wildcard-match", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "wildcard match", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

匹配URL的通配符,用星号(*)表示任意字符,比如(/images/*.png/static/*,不需要带域名。

-
` -}) - -// User-Agent正则匹配 -Vue.component("http-cond-user-agent-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${userAgent}", - operator: "regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

匹配User-Agent的正则表达式,比如Android|iPhone

-
` -}) - -// User-Agent正则不匹配 -Vue.component("http-cond-user-agent-not-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${userAgent}", - operator: "not regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

匹配User-Agent的正则表达式,比如Android|iPhone,如果匹配,则排除此条件。

-
` -}) - -// 根据MimeType -Vue.component("http-cond-mime-type", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: false, - param: "${response.contentType}", - operator: "mime type", - value: "[]" - } - if (this.vCond != null && this.vCond.param == cond.param) { - cond.value = this.vCond.value - } - return { - cond: cond, - mimeTypes: JSON.parse(cond.value), // TODO 可以拖动排序 - - isAdding: false, - addingMimeType: "" - } - }, - watch: { - mimeTypes: function () { - this.cond.value = JSON.stringify(this.mimeTypes) - } - }, - methods: { - addMimeType: function () { - this.isAdding = !this.isAdding - - if (this.isAdding) { - let that = this - setTimeout(function () { - that.$refs.addingMimeType.focus() - }, 100) - } - }, - cancelAdding: function () { - this.isAdding = false - this.addingMimeType = "" - }, - confirmAdding: function () { - // TODO 做更详细的校验 - // TODO 如果有重复的则提示之 - - if (this.addingMimeType.length == 0) { - return - } - this.addingMimeType = this.addingMimeType.replace(/\s+/g, "") - this.mimeTypes.push(this.addingMimeType) - - // 清除状态 - this.cancelAdding() - }, - removeMimeType: function (index) { - this.mimeTypes.$remove(index) - } - }, - template: `
- -
-
{{mimeType}}
-
-
-
-
- -
-
- - -
-
-
- -
-

服务器返回的内容的MimeType,比如text/htmlimage/*等。

-
` -}) - -// 参数匹配 -Vue.component("http-cond-params", { - props: ["v-cond"], - mounted: function () { - let cond = this.vCond - if (cond == null) { - return - } - this.operator = cond.operator - - // stringValue - if (["regexp", "not regexp", "eq", "not", "prefix", "suffix", "contains", "not contains", "eq ip", "gt ip", "gte ip", "lt ip", "lte ip", "ip range"].$contains(cond.operator)) { - this.stringValue = cond.value - return - } - - // numberValue - if (["eq int", "eq float", "gt", "gte", "lt", "lte", "mod 10", "ip mod 10", "mod 100", "ip mod 100"].$contains(cond.operator)) { - this.numberValue = cond.value - return - } - - // modValue - if (["mod", "ip mod"].$contains(cond.operator)) { - let pieces = cond.value.split(",") - this.modDivValue = pieces[0] - if (pieces.length > 1) { - this.modRemValue = pieces[1] - } - return - } - - // stringValues - let that = this - if (["in", "not in", "file ext", "mime type"].$contains(cond.operator)) { - try { - let arr = JSON.parse(cond.value) - if (arr != null && (arr instanceof Array)) { - arr.forEach(function (v) { - that.stringValues.push(v) - }) - } - } catch (e) { - - } - return - } - - // versionValue - if (["version range"].$contains(cond.operator)) { - let pieces = cond.value.split(",") - this.versionRangeMinValue = pieces[0] - if (pieces.length > 1) { - this.versionRangeMaxValue = pieces[1] - } - return - } - }, - data: function () { - let cond = { - isRequest: true, - param: "", - operator: window.REQUEST_COND_OPERATORS[0].op, - value: "", - isCaseInsensitive: false - } - if (this.vCond != null) { - cond = this.vCond - } - return { - cond: cond, - operators: window.REQUEST_COND_OPERATORS, - operator: window.REQUEST_COND_OPERATORS[0].op, - operatorDescription: window.REQUEST_COND_OPERATORS[0].description, - variables: window.REQUEST_VARIABLES, - variable: "", - - // 各种类型的值 - stringValue: "", - numberValue: "", - - modDivValue: "", - modRemValue: "", - - stringValues: [], - - versionRangeMinValue: "", - versionRangeMaxValue: "" - } - }, - methods: { - changeVariable: function () { - let v = this.cond.param - if (v == null) { - v = "" - } - this.cond.param = v + this.variable - }, - changeOperator: function () { - let that = this - this.operators.forEach(function (v) { - if (v.op == that.operator) { - that.operatorDescription = v.description - } - }) - - this.cond.operator = this.operator - - // 移动光标 - let box = document.getElementById("variables-value-box") - if (box != null) { - setTimeout(function () { - let input = box.getElementsByTagName("INPUT") - if (input.length > 0) { - input[0].focus() - } - }, 100) - } - }, - changeStringValues: function (v) { - this.stringValues = v - this.cond.value = JSON.stringify(v) - } - }, - watch: { - stringValue: function (v) { - this.cond.value = v - }, - numberValue: function (v) { - // TODO 校验数字 - this.cond.value = v - }, - modDivValue: function (v) { - if (v.length == 0) { - return - } - let div = parseInt(v) - if (isNaN(div)) { - div = 1 - } - this.modDivValue = div - this.cond.value = div + "," + this.modRemValue - }, - modRemValue: function (v) { - if (v.length == 0) { - return - } - let rem = parseInt(v) - if (isNaN(rem)) { - rem = 0 - } - this.modRemValue = rem - this.cond.value = this.modDivValue + "," + rem - }, - versionRangeMinValue: function (v) { - this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue - }, - versionRangeMaxValue: function (v) { - this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue - } - }, - template: ` - - 参数值 - - -
-
- -
-
- -
-
-

其中可以使用变量,类似于\${requestPath},也可以是多个变量的组合。

- - - - 操作符 - -
- -

-
- - - - 对比值 - - -
- -

要匹配的正则表达式,比如^/static/(.+).js

-
- - -
- -

要对比的数字。

-
- - -
- -

参数值除以10的余数,在0-9之间。

-
-
- -

参数值除以100的余数,在0-99之间。

-
-
-
-
除:
-
- -
-
余:
-
- -
-
-
- - -
- -

和参数值一致的字符串。

-

和参数值不一致的字符串。

-

参数值的前缀。

-

参数值的后缀为此字符串。

-

参数值包含此字符串。

-

参数值不包含此字符串。

-
-
- -

添加参数值列表。

-

添加参数值列表。

-

添加扩展名列表,比如pnghtml,不包括点。

-

添加MimeType列表,类似于text/htmlimage/*

-
-
-
-
-
-
-
-
-
- - -
- -

要对比的IP。

-
-
- -

参数中IP转换成整数后除以10的余数,在0-9之间。

-
-
- -

参数中IP转换成整数后除以100的余数,在0-99之间。

-
- - - - 不区分大小写 - -
- - -
-

选中后表示对比时忽略参数值的大小写。

- - - -` -}) - -// 请求方法列表 -Vue.component("http-status-box", { - props: ["v-status-list"], - data: function () { - let statusList = this.vStatusList - if (statusList == null) { - statusList = [] - } - return { - statusList: statusList, - isAdding: false, - addingStatus: "" - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.addingStatus.focus() - }, 100) - }, - confirm: function () { - let that = this - - // 删除其中的空格 - this.addingStatus = this.addingStatus.replace(/\s/g, "").toUpperCase() - - if (this.addingStatus.length == 0) { - teaweb.warn("请输入要添加的状态码", function () { - that.$refs.addingStatus.focus() - }) - return - } - - // 是否已经存在 - if (this.statusList.$contains(this.addingStatus)) { - teaweb.warn("此状态码已经存在,无需重复添加", function () { - that.$refs.addingStatus.focus() - }) - return - } - - // 格式 - if (!this.addingStatus.match(/^\d{3}$/)) { - teaweb.warn("请输入正确的状态码", function () { - that.$refs.addingStatus.focus() - }) - return - } - - this.statusList.push(parseInt(this.addingStatus, 10)) - this.cancel() - }, - remove: function (index) { - this.statusList.$remove(index) - }, - cancel: function () { - this.isAdding = false - this.addingStatus = "" - } - }, - template: `
- -
- - {{status}} -   - -
-
-
-
-
- -
-
- -   -
-
-

格式为三位数字,比如200404等。

-
-
-
- -
-
` -}) - Vue.component("server-group-selector", { props: ["v-groups"], data: function () { @@ -16063,95 +21674,972 @@ Vue.component("server-group-selector", {
` }) -Vue.component("script-group-config-box", { - props: ["v-group", "v-auditing-status", "v-is-location"], +Vue.component("server-name-box", { + props: ["v-server-names"], data: function () { - let group = this.vGroup - if (group == null) { - group = { - isPrior: false, - isOn: true, - scripts: [] - } + let serverNames = this.vServerNames; + if (serverNames == null) { + serverNames = [] } - 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 + serverNames: serverNames, + isSearching: false, + keyword: "" } }, methods: { - changeScript: function (script) { - this.group.scripts = [script] // 目前只支持单个脚本 - this.change() + 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) + setTimeout(that.submitForm, 100) + } + }); }, - 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 + removeServerName: function (index) { + this.serverNames.$remove(index) + }, + + updateServerName: function (index, serverName) { + window.UPDATING_SERVER_NAME = teaweb.clone(serverName) + let that = this + teaweb.popup("/servers/addServerNamePopup", { + callback: function (resp) { + var serverName = resp.data.serverName + Vue.set(that.serverNames, index, serverName) + setTimeout(that.submitForm, 100) + } + }); + }, + showSearchBox: function () { + this.isSearching = !this.isSearching + if (this.isSearching) { + let that = this + setTimeout(function () { + that.$refs.keywordRef.focus() + }, 200) + } else { + this.keyword = "" } + }, + allServerNames: function () { + if (this.serverNames == null) { + return [] + } + let result = [] + this.serverNames.forEach(function (serverName) { + if (serverName.subNames != null && serverName.subNames.length > 0) { + serverName.subNames.forEach(function (subName) { + if (subName != null && subName.length > 0) { + if (!result.$contains(subName)) { + result.push(subName) + } + } + }) + } else if (serverName.name != null && serverName.name.length > 0) { + if (!result.$contains(serverName.name)) { + result.push(serverName.name) + } + } + }) + return result + }, + submitForm: function () { + Tea.runActionOn(this.$refs.serverNamesRef.form) } }, watch: { - "periodConfig.period": function (v) { - v = parseInt(v) - if (isNaN(v) || v <= 0) { - v = 1 - } - this.periodConfig.period = v + 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("server-traffic-limit-status-viewer", { + props: ["value"], + data: function () { + let targetTypeName = "流量" + if (this.value != null) { + targetTypeName = this.targetTypeToName(this.value.targetType) + } + + return { + status: this.value, + targetTypeName: targetTypeName + } + }, + methods: { + targetTypeToName: function (targetType) { + switch (targetType) { + case "traffic": + return "流量" + case "request": + return "请求数" + case "websocketConnections": + return "Websocket连接数" + } + return "流量" + } + }, + template: ` + 已达到套餐当日{{targetTypeName}}限制 + 已达到套餐当月{{targetTypeName}}限制 + 已达到套餐总体{{targetTypeName}}限制 +` +}) + +Vue.component("ssl-certs-box", { + props: [ + "v-certs", // 证书列表 + "v-cert", // 单个证书 + "v-protocol", // 协议:https|tls + "v-view-size", // 弹窗尺寸:normal, mini + "v-single-mode", // 单证书模式 + "v-description", // 描述文字 + "v-domains", // 搜索的域名列表或者函数 + "v-user-id" // 用户ID + ], + data: function () { + let certs = this.vCerts + if (certs == null) { + certs = [] + } + if (this.vCert != null) { + certs.push(this.vCert) + } + + let description = this.vDescription + if (description == null || typeof (description) != "string") { + description = "" + } + + return { + certs: certs, + description: description + } + }, + methods: { + certIds: function () { + return this.certs.map(function (v) { + return v.id + }) + }, + // 删除证书 + removeCert: function (index) { + let that = this + teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { + that.certs.$remove(index) + }) + }, + + // 选择证书 + selectCert: function () { + let that = this + let width = "54em" + let height = "32em" + let viewSize = this.vViewSize + if (viewSize == null) { + viewSize = "normal" + } + if (viewSize == "mini") { + width = "35em" + height = "20em" + } + + let searchingDomains = [] + if (this.vDomains != null) { + if (typeof this.vDomains == "function") { + let resultDomains = this.vDomains() + if (resultDomains != null && typeof resultDomains == "object" && (resultDomains instanceof Array)) { + searchingDomains = resultDomains + } + } else if (typeof this.vDomains == "object" && (this.vDomains instanceof Array)) { + searchingDomains = this.vDomains + } + if (searchingDomains.length > 10000) { + searchingDomains = searchingDomains.slice(0, 10000) + } + } + + let selectedCertIds = this.certs.map(function (cert) { + return cert.id + }) + let userId = this.vUserId + if (userId == null) { + userId = 0 + } + + teaweb.popup("/servers/certs/selectPopup?viewSize=" + viewSize + "&searchingDomains=" + window.encodeURIComponent(searchingDomains.join(",")) + "&selectedCertIds=" + selectedCertIds.join(",") + "&userId=" + userId, { + width: width, + height: height, + callback: function (resp) { + if (resp.data.cert != null) { + that.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 上传证书 + uploadCert: function () { + let that = this + let userId = this.vUserId + if (typeof userId != "number" && typeof userId != "string") { + userId = 0 + } + teaweb.popup("/servers/certs/uploadPopup?userId=" + userId, { + height: "28em", + callback: function (resp) { + teaweb.success("上传成功", function () { + if (resp.data.cert != null) { + that.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + }) + } + }) + }, + + // 批量上传 + uploadBatch: function () { + let that = this + let userId = this.vUserId + if (typeof userId != "number" && typeof userId != "string") { + userId = 0 + } + teaweb.popup("/servers/certs/uploadBatchPopup?userId=" + userId, { + callback: function (resp) { + if (resp.data.cert != null) { + that.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 格式化时间 + formatTime: function (timestamp) { + return new Date(timestamp * 1000).format("Y-m-d") + }, + + // 判断是否显示选择|上传按钮 + buttonsVisible: function () { + return this.vSingleMode == null || !this.vSingleMode || this.certs == null || this.certs.length == 0 + } + }, + template: `
+ +
+
+ {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   +
+
+
+
+ 选择或上传证书后HTTPSTLS服务才能生效。 + {{description}} +
+
+
+   + |   +   +   +
+
` +}) + +Vue.component("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("ssl-config-box", { + props: [ + "v-ssl-policy", + "v-protocol", + "v-server-id", + "v-support-http3" + ], + created: function () { + let that = this + setTimeout(function () { + that.sortableCipherSuites() + }, 100) + }, + data: function () { + let policy = this.vSslPolicy + if (policy == null) { + policy = { + id: 0, + isOn: true, + certRefs: [], + certs: [], + clientCARefs: [], + clientCACerts: [], + clientAuthType: 0, + minVersion: "TLS 1.1", + hsts: null, + cipherSuitesIsOn: false, + cipherSuites: [], + http2Enabled: true, + http3Enabled: false, + ocspIsOn: false + } + } else { + if (policy.certRefs == null) { + policy.certRefs = [] + } + if (policy.certs == null) { + policy.certs = [] + } + if (policy.clientCARefs == null) { + policy.clientCARefs = [] + } + if (policy.clientCACerts == null) { + policy.clientCACerts = [] + } + if (policy.cipherSuites == null) { + policy.cipherSuites = [] + } + } + + let hsts = policy.hsts + let hstsMaxAgeString = "31536000" + if (hsts == null) { + hsts = { + isOn: false, + maxAge: 31536000, + includeSubDomains: false, + preload: false, + domains: [] + } + } + if (hsts.maxAge != null) { + hstsMaxAgeString = hsts.maxAge.toString() + } + + return { + policy: policy, + + // hsts + hsts: hsts, + hstsOptionsVisible: false, + hstsDomainAdding: false, + hstsMaxAgeString: hstsMaxAgeString, + addingHstsDomain: "", + hstsDomainEditingIndex: -1, + + // 相关数据 + allVersions: window.SSL_ALL_VERSIONS, + allCipherSuites: window.SSL_ALL_CIPHER_SUITES.$copy(), + modernCipherSuites: window.SSL_MODERN_CIPHER_SUITES, + intermediateCipherSuites: window.SSL_INTERMEDIATE_CIPHER_SUITES, + allClientAuthTypes: window.SSL_ALL_CLIENT_AUTH_TYPES, + cipherSuitesVisible: false, + + // 高级选项 + moreOptionsVisible: false + } + }, + watch: { + hsts: { + deep: true, + handler: function () { + this.policy.hsts = this.hsts + } + } + }, + methods: { + // 删除证书 + removeCert: function (index) { + let that = this + teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { + that.policy.certRefs.$remove(index) + that.policy.certs.$remove(index) + }) + }, + + // 选择证书 + selectCert: function () { + let that = this + let selectedCertIds = [] + if (this.policy != null && this.policy.certs.length > 0) { + this.policy.certs.forEach(function (cert) { + selectedCertIds.push(cert.id.toString()) + }) + } + let serverId = this.vServerId + if (serverId == null) { + serverId = 0 + } + teaweb.popup("/servers/certs/selectPopup?selectedCertIds=" + selectedCertIds + "&serverId=" + serverId, { + width: "50em", + height: "30em", + callback: function (resp) { + if (resp.data.cert != null && resp.data.certRef != null) { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + } + if (resp.data.certs != null && resp.data.certRefs != null) { + that.policy.certRefs.$pushAll(resp.data.certRefs) + that.policy.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 上传证书 + uploadCert: function () { + let that = this + let serverId = this.vServerId + if (typeof serverId != "number" && typeof serverId != "string") { + serverId = 0 + } + teaweb.popup("/servers/certs/uploadPopup?serverId=" + serverId, { + height: "30em", + callback: function (resp) { + teaweb.success("上传成功", function () { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + }) + } + }) + }, + + // 批量上传 + uploadBatch: function () { + let that = this + let serverId = this.vServerId + if (typeof serverId != "number" && typeof serverId != "string") { + serverId = 0 + } + teaweb.popup("/servers/certs/uploadBatchPopup?serverId=" + serverId, { + callback: function (resp) { + if (resp.data.cert != null) { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.policy.certRefs.$pushAll(resp.data.certRefs) + that.policy.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 申请证书 + requestCert: function () { + // 已经在证书中的域名 + let excludeServerNames = [] + if (this.policy != null && this.policy.certs.length > 0) { + this.policy.certs.forEach(function (cert) { + excludeServerNames.$pushAll(cert.dnsNames) + }) + } + + let that = this + teaweb.popup("/servers/server/settings/https/requestCertPopup?serverId=" + this.vServerId + "&excludeServerNames=" + excludeServerNames.join(","), { + callback: function () { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + } + }) + }, + + // 更多选项 + changeOptionsVisible: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + }, + + // 格式化时间 + formatTime: function (timestamp) { + return new Date(timestamp * 1000).format("Y-m-d") + }, + + // 格式化加密套件 + formatCipherSuite: function (cipherSuite) { + return cipherSuite.replace(/(AES|3DES)/, "$1") + }, + + // 添加单个套件 + addCipherSuite: function (cipherSuite) { + if (!this.policy.cipherSuites.$contains(cipherSuite)) { + this.policy.cipherSuites.push(cipherSuite) + } + this.allCipherSuites.$removeValue(cipherSuite) + }, + + // 删除单个套件 + removeCipherSuite: function (cipherSuite) { + let that = this + teaweb.confirm("确定要删除此套件吗?", function () { + that.policy.cipherSuites.$removeValue(cipherSuite) + that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$findAll(function (k, v) { + return !that.policy.cipherSuites.$contains(v) + }) + }) + }, + + // 清除所选套件 + clearCipherSuites: function () { + let that = this + teaweb.confirm("确定要清除所有已选套件吗?", function () { + that.policy.cipherSuites = [] + that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$copy() + }) + }, + + // 批量添加套件 + addBatchCipherSuites: function (suites) { + var that = this + teaweb.confirm("确定要批量添加套件?", function () { + suites.$each(function (k, v) { + if (that.policy.cipherSuites.$contains(v)) { + return + } + that.policy.cipherSuites.push(v) + }) + }) + }, + + /** + * 套件拖动排序 + */ + sortableCipherSuites: function () { + var box = document.querySelector(".cipher-suites-box") + Sortable.create(box, { + draggable: ".label", + handle: ".icon.handle", + onStart: function () { + + }, + onUpdate: function (event) { + + } + }) + }, + + // 显示所有套件 + showAllCipherSuites: function () { + this.cipherSuitesVisible = !this.cipherSuitesVisible + }, + + // 显示HSTS更多选项 + showMoreHSTS: function () { + this.hstsOptionsVisible = !this.hstsOptionsVisible; + if (this.hstsOptionsVisible) { + this.changeHSTSMaxAge() + } + }, + + // 监控HSTS有效期修改 + changeHSTSMaxAge: function () { + var v = parseInt(this.hstsMaxAgeString) + if (isNaN(v) || v < 0) { + this.hsts.maxAge = 0 + this.hsts.days = "-" + return + } + this.hsts.maxAge = v + this.hsts.days = v / 86400 + if (this.hsts.days == 0) { + this.hsts.days = "-" + } + }, + + // 设置HSTS有效期 + setHSTSMaxAge: function (maxAge) { + this.hstsMaxAgeString = maxAge.toString() + this.changeHSTSMaxAge() + }, + + // 添加HSTS域名 + addHstsDomain: function () { + this.hstsDomainAdding = true + this.hstsDomainEditingIndex = -1 + let that = this + setTimeout(function () { + that.$refs.addingHstsDomain.focus() + }, 100) + }, + + // 修改HSTS域名 + editHstsDomain: function (index) { + this.hstsDomainEditingIndex = index + this.addingHstsDomain = this.hsts.domains[index] + this.hstsDomainAdding = true + let that = this + setTimeout(function () { + that.$refs.addingHstsDomain.focus() + }, 100) + }, + + // 确认HSTS域名添加 + confirmAddHstsDomain: function () { + this.addingHstsDomain = this.addingHstsDomain.trim() + if (this.addingHstsDomain.length == 0) { + return; + } + if (this.hstsDomainEditingIndex > -1) { + this.hsts.domains[this.hstsDomainEditingIndex] = this.addingHstsDomain + } else { + this.hsts.domains.push(this.addingHstsDomain) + } + this.cancelHstsDomainAdding() + }, + + // 取消HSTS域名添加 + cancelHstsDomainAdding: function () { + this.hstsDomainAdding = false + this.addingHstsDomain = "" + this.hstsDomainEditingIndex = -1 + }, + + // 删除HSTS域名 + removeHstsDomain: function (index) { + this.cancelHstsDomainAdding() + this.hsts.domains.$remove(index) + }, + + // 选择客户端CA证书 + selectClientCACert: function () { + let that = this + teaweb.popup("/servers/certs/selectPopup?isCA=1", { + width: "50em", + height: "30em", + callback: function (resp) { + if (resp.data.cert != null && resp.data.certRef != null) { + that.policy.clientCARefs.push(resp.data.certRef) + that.policy.clientCACerts.push(resp.data.cert) + } + if (resp.data.certs != null && resp.data.certRefs != null) { + that.policy.clientCARefs.$pushAll(resp.data.certRefs) + that.policy.clientCACerts.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 上传CA证书 + uploadClientCACert: function () { + let that = this + teaweb.popup("/servers/certs/uploadPopup?isCA=1", { + height: "28em", + callback: function (resp) { + teaweb.success("上传成功", function () { + that.policy.clientCARefs.push(resp.data.certRef) + that.policy.clientCACerts.push(resp.data.cert) + }) + } + }) + }, + + // 删除客户端CA证书 + removeClientCACert: function (index) { + let that = this + teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { + that.policy.clientCARefs.$remove(index) + that.policy.clientCACerts.$remove(index) + }) + } + }, + template: `
+

SSL/TLS相关配置

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用HTTP/2 +
+ + +
+
启用HTTP/3 +
+ + +
+
设置证书 +
+
+ {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   +
+
+
+
+ 选择或上传证书后HTTPSTLS服务才能生效。 +
+
+   + |   +   +   + |   + +
TLS最低版本 + +
加密算法套件(CipherSuites) +
+ + +
+
+
+
+ 已添加套件({{policy.cipherSuites.length}}): +
+ +   + +
+
+ + + +

点击可选套件添加。

+
+
开启HSTS +
+ + +
+

+ 开启后,会自动在响应Header中加入 + Strict-Transport-Security: + ... + max-age={{hsts.maxAge}} + ; includeSubDomains + ; preload + + + 修改 + +

+
HSTS有效时间(max-age) +
+
+ +
+
+ 秒 +
+
{{hsts.days}}天
+
+

+ [1年/365天]     + [6个月/182.5天]     + [1个月/30天] +

+
HSTS包含子域名(includeSubDomains) +
+ + +
+
HSTS预加载(preload) +
+ + +
+
HSTS生效的域名 +
+ {{domain}} +   + + + +
+
+
+ +
+
+ +   取消 +
+
+
+ +
+

如果没有设置域名的话,则默认支持所有的域名。

+
OCSP Stapling +

选中表示启用OCSP Stapling。

+
客户端认证方式 + +
客户端认证CA证书 +
+
+ {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   +
+
+
+   + +

用来校验客户端证书以增强安全性,通常不需要设置。

+
+
` }) @@ -16260,302 +22748,129 @@ Vue.component("traffic-limit-config-box", {
` }) -Vue.component("http-firewall-captcha-options", { - props: ["v-captcha-options"], - mounted: function () { - this.updateSummary() - }, +// UAM模式配置 +Vue.component("uam-config-box", { + props: ["v-uam-config", "v-is-location", "v-is-group"], data: function () { - let options = this.vCaptchaOptions - if (options == null) { - options = { - captchaType: "default", - countLetters: 0, - life: 0, - maxFails: 0, - failBlockTimeout: 0, - failBlockScopeAll: false, - uiIsOn: false, - uiTitle: "", - uiPrompt: "", - uiButtonTitle: "", - uiShowRequestId: true, - uiCss: "", - uiFooter: "", - uiBody: "", - cookieId: "", - lang: "", - geeTestConfig: { - isOn: false, - captchaId: "", - captchaKey: "" - } + let config = this.vUamConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + addToWhiteList: true, + onlyURLPatterns: [], + exceptURLPatterns: [], + minQPSPerIP: 0, + keyLife: 0 } } - if (options.countLetters <= 0) { - options.countLetters = 6 + if (config.onlyURLPatterns == null) { + config.onlyURLPatterns = [] } - - if (options.captchaType == null || options.captchaType.length == 0) { - options.captchaType = "default" + if (config.exceptURLPatterns == null) { + config.exceptURLPatterns = [] } - - return { - options: options, - isEditing: false, - summary: "", - uiBodyWarning: "", - captchaTypes: window.WAF_CAPTCHA_TYPES + config: config, + moreOptionsVisible: false, + minQPSPerIP: config.minQPSPerIP, + keyLife: config.keyLife } }, 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 + minQPSPerIP: function (v) { + let qps = parseInt(v.toString()) + if (isNaN(qps) || qps < 0) { + qps = 0 } - this.options.countLetters = i + this.config.minQPSPerIP = qps }, - "options.life": function (v) { - let i = parseInt(v, 10) - if (isNaN(i)) { - i = 0 + keyLife: function (v) { + let keyLife = parseInt(v) + if (isNaN(keyLife) || keyLife <= 0) { + keyLife = 0 } - this.options.life = i - this.updateSummary() - }, - "options.maxFails": function (v) { - let i = parseInt(v, 10) - if (isNaN(i)) { - i = 0 - } - this.options.maxFails = i - this.updateSummary() - }, - "options.failBlockTimeout": function (v) { - let i = parseInt(v, 10) - if (isNaN(i)) { - i = 0 - } - this.options.failBlockTimeout = i - this.updateSummary() - }, - "options.failBlockScopeAll": function (v) { - this.updateSummary() - }, - "options.captchaType": function (v) { - this.updateSummary() - }, - "options.uiIsOn": function (v) { - this.updateSummary() - }, - "options.uiBody": function (v) { - if (/|\s).+\$\{body}.*<\/form>/s.test(v)) { - this.uiBodyWarning = "页面模板中不能使用
标签包裹\${body}变量,否则将导致验证码表单无法提交。" - } else { - this.uiBodyWarning = "" - } - }, - "options.geeTestConfig.isOn": function (v) { - this.updateSummary() + this.config.keyLife = keyLife } }, methods: { - edit: function () { - this.isEditing = !this.isEditing + showMoreOptions: function () { + this.moreOptionsVisible = !this.moreOptionsVisible }, - updateSummary: function () { - let summaryList = [] - if (this.options.life > 0) { - summaryList.push("有效时间" + this.options.life + "秒") - } - if (this.options.maxFails > 0) { - summaryList.push("最多失败" + this.options.maxFails + "次") - } - if (this.options.failBlockTimeout > 0) { - summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒") - } - if (this.options.failBlockScopeAll) { - summaryList.push("尝试全局封禁") - } - - let that = this - let typeDef = this.captchaTypes.$find(function (k, v) { - return v.code == that.options.captchaType - }) - if (typeDef != null) { - summaryList.push("默认验证方式:" + typeDef.name) - } - - if (this.options.captchaType == "default") { - if (this.options.uiIsOn) { - summaryList.push("定制UI") - } - } - - if (this.options.geeTestConfig != null && this.options.geeTestConfig.isOn) { - summaryList.push("已配置极验") - } - - if (summaryList.length == 0) { - this.summary = "默认配置" - } else { - this.summary = summaryList.join(" / ") - } - }, - confirm: function () { - this.isEditing = false + changeConds: function (conds) { + this.config.conds = conds } }, template: `
- - {{summary}} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
默认验证方式 - -

{{captchaDef.description}}

-
有效时间 -
- - -
-

验证通过后在这个时间内不再验证,默认600秒。

-
最多失败次数 -
- - -
-

建议填入一个不小于5的数字,以减少误判几率。允许用户失败尝试的最多次数,超过这个次数将被自动加入黑名单。如果为空或者为0,表示不限制。

-
失败拦截时间 -
- - -
-

在达到最多失败次数(大于0)时,自动拦截的时长;如果为0表示不自动拦截。

-
失败全局封禁 - -

选中后,表示允许系统尝试全局封禁某个IP,以提升封禁性能。

-
验证码中数字个数 - -
定制UI
页面标题 - -
按钮标题 - -

类似于提交验证

-
显示请求ID - -

在界面上显示请求ID,方便用户报告问题。

-
CSS样式 - -
页头提示 - -

类似于请输入上面的验证码,支持HTML。

-
页尾提示 - -

支持HTML。

-
页面模板 - -

警告:{{uiBodyWarning}}模板中必须包含\${body}表示验证码表单!整个页面的模板,支持HTML,其中必须使用\${body}变量代表验证码表单,否则将无法正常显示验证码。

-
- - - - - - - - - - - - - - - - -
允许用户使用极验 -

选中后,表示允许用户在WAF设置中选择极验。

-
极验-验证ID * - -

在极验控制台--业务管理中获取。

-
极验-验证Key * - -

在极验控制台--业务管理中获取。

-
-
-
-` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用5秒盾 + +

启用后,访问网站时,自动检查浏览器环境,阻止非正常访问。

+
验证有效期 +
+ + +
+

单个客户端验证通过后,在这个有效期内不再重复验证;如果为0则表示系统默认。

+
单IP最低QPS +
+ + 请求数/秒 +
+

当某个IP在1分钟内平均QPS达到此值时,才会触发5秒盾;如果设置为0,表示任何访问都会触发。

+
加入IP白名单 + +

选中后,表示验证通过后,将访问者IP加入到临时白名单中,此IP下次访问时不再校验5秒盾;此白名单只对5秒盾有效,不影响其他规则。此选项主要用于可能无法正常使用Cookie的网站。

+
例外URL + +

如果填写了例外URL,表示这些URL跳过5秒盾不做处理。

+
限制URL + +

如果填写了限制URL,表示只对这些URL进行5秒盾处理;如果不填则表示支持所有的URL。

+
匹配条件 + +
+
+
` }) Vue.component("user-agent-config-box", { @@ -16738,4113 +23053,42 @@ Vue.component("user-agent-config-box", {
` }) -Vue.component("http-pages-box", { - props: ["v-pages"], +Vue.component("user-selector", { + props: ["v-user-id", "data-url"], data: function () { - let pages = [] - if (this.vPages != null) { - pages = this.vPages + let userId = this.vUserId + if (userId == null) { + userId = 0 + } + + let dataURL = this.dataUrl + if (dataURL == null || dataURL.length == 0) { + dataURL = "/servers/users/options" } return { - pages: pages + users: [], + userId: userId, + dataURL: dataURL } }, methods: { - addPage: function () { - let that = this - teaweb.popup("/servers/server/settings/pages/createPopup", { - height: "26em", - callback: function (resp) { - that.pages.push(resp.data.page) - that.notifyChange() - } - }) - }, - updatePage: function (pageIndex, pageId) { - let that = this - teaweb.popup("/servers/server/settings/pages/updatePopup?pageId=" + pageId, { - height: "26em", - callback: function (resp) { - Vue.set(that.pages, pageIndex, resp.data.page) - that.notifyChange() - } - }) - }, - removePage: function (pageIndex) { - let that = this - teaweb.confirm("确定要移除此页面吗?", function () { - that.pages.$remove(pageIndex) - that.notifyChange() - }) - }, - notifyChange: function () { - let parent = this.$el.parentNode - while (true) { - if (parent == null) { - break - } - if (parent.tagName == "FORM") { - break - } - parent = parent.parentNode - } - if (parent != null) { - setTimeout(function () { - Tea.runActionOn(parent) - }, 100) - } - } - }, - template: `
- - -
- - - - - - - - - - - - - - - - - - - -
响应状态码页面类型新状态码例外URL限制URL操作
- - {{page.status[0]}} - {{page.status}} - - - - -
- {{page.url}} -
- 读取URL -
-
-
- {{page.url}} -
- 跳转URL - {{page.newStatus}} -
-
-
- [HTML内容] -
- {{page.newStatus}} -
-
-
- {{page.newStatus}} - 保持 - -
- {{urlPattern.pattern}} -
- - -
-
- {{urlPattern.pattern}} -
- - -
- 修改   - 删除 -
-
-
- -
-
-
` -}) - -Vue.component("firewall-syn-flood-config-box", { - props: ["v-syn-flood-config"], - data: function () { - let config = this.vSynFloodConfig - if (config == null) { - config = { - isOn: false, - minAttempts: 10, - timeoutSeconds: 600, - ignoreLocal: true - } - } - return { - config: config, - isEditing: false, - minAttempts: config.minAttempts, - timeoutSeconds: config.timeoutSeconds - } - }, - methods: { - edit: function () { - this.isEditing = !this.isEditing - } - }, - watch: { - minAttempts: function (v) { - let count = parseInt(v) - if (isNaN(count)) { - count = 10 - } - if (count < 5) { - count = 5 - } - this.config.minAttempts = count - }, - timeoutSeconds: function (v) { - let seconds = parseInt(v) - if (isNaN(seconds)) { - seconds = 10 - } - if (seconds < 60) { - seconds = 60 - } - this.config.timeoutSeconds = seconds - } - }, - template: `
- - - - 已启用 / 空连接次数:{{config.minAttempts}}次/分钟 / 封禁时长:{{config.timeoutSeconds}}秒 / 忽略局域网访问 - - 未启用 - - - - - - - - - - - - - - - - - - - - -
启用 - -

启用后,WAF将会尝试自动检测并阻止SYN Flood攻击。此功能需要节点已安装并启用nftables或Firewalld。

-
空连接次数 -
- - 次/分钟 -
-

超过此数字的"空连接"将被视为SYN Flood攻击,为了防止误判,此数值默认不小于5。

-
封禁时长 -
- - -
-
忽略局域网访问 - -
-
` -}) - -Vue.component("http-firewall-region-selector", { - props: ["v-type", "v-countries"], - data: function () { - let countries = this.vCountries - if (countries == null) { - countries = [] - } - - return { - listType: this.vType, - countries: countries - } - }, - methods: { - addCountry: function () { - let selectedCountryIds = this.countries.map(function (country) { - return country.id - }) - let that = this - teaweb.popup("/servers/server/settings/waf/ipadmin/selectCountriesPopup?type=" + this.listType + "&selectedCountryIds=" + selectedCountryIds.join(","), { - width: "52em", - height: "30em", - callback: function (resp) { - that.countries = resp.data.selectedCountries - that.$forceUpdate() - that.notifyChange() - } - }) - }, - removeCountry: function (index) { - this.countries.$remove(index) - this.notifyChange() - }, - resetCountries: function () { - this.countries = [] - this.notifyChange() - }, - notifyChange: function () { - this.$emit("change", { - "countries": this.countries - }) - } - }, - template: `
- 暂时没有选择允许封禁的区域。 -
-
- - ({{country.letter}}){{country.name}} -
-
-
-   -
` -}) - -// TODO 支持关键词搜索 -// TODO 改成弹窗选择 -Vue.component("admin-selector", { - props: ["v-admin-id"], - mounted: function () { - let that = this - Tea.action("/admins/options") - .post() - .success(function (resp) { - that.admins = resp.data.admins - }) - }, - data: function () { - let adminId = this.vAdminId - if (adminId == null) { - adminId = 0 - } - return { - admins: [], - adminId: adminId - } - }, - template: `
- -
` -}) - -// 绑定IP列表 -Vue.component("ip-list-bind-box", { - props: ["v-http-firewall-policy-id", "v-type"], - mounted: function () { - this.refresh() - }, - data: function () { - return { - policyId: this.vHttpFirewallPolicyId, - type: this.vType, - lists: [] - } - }, - methods: { - bind: function () { - let that = this - teaweb.popup("/servers/iplists/bindHTTPFirewallPopup?httpFirewallPolicyId=" + this.policyId + "&type=" + this.type, { - width: "50em", - height: "34em", - callback: function () { - - }, - onClose: function () { - that.refresh() - } - }) - }, - remove: function (index, listId) { - let that = this - teaweb.confirm("确定要删除这个绑定的IP名单吗?", function () { - Tea.action("/servers/iplists/unbindHTTPFirewall") - .params({ - httpFirewallPolicyId: that.policyId, - listId: listId - }) - .post() - .success(function (resp) { - that.lists.$remove(index) - }) - }) - }, - refresh: function () { - let that = this - Tea.action("/servers/iplists/httpFirewall") - .params({ - httpFirewallPolicyId: this.policyId, - type: this.vType - }) - .post() - .success(function (resp) { - that.lists = resp.data.lists - }) - } - }, - template: `
- 绑定+   已绑定: - -
` -}) - -Vue.component("ip-list-table", { - props: ["v-items", "v-keyword", "v-show-search-button", "v-total"/** total items >= items length **/], - data: function () { - let maxDeletes = 10000 - if (this.vTotal != null && this.vTotal > 0 && this.vTotal < maxDeletes) { - maxDeletes = this.vTotal - } - - return { - items: this.vItems, - keyword: (this.vKeyword != null) ? this.vKeyword : "", - selectedAll: false, - hasSelectedItems: false, - - MaxDeletes: maxDeletes - } - }, - methods: { - updateItem: function (itemId) { - this.$emit("update-item", itemId) - }, - deleteItem: function (itemId) { - this.$emit("delete-item", itemId) - }, - viewLogs: function (itemId) { - teaweb.popup("/servers/iplists/accessLogsPopup?itemId=" + itemId, { - width: "50em", - height: "30em" - }) - }, - changeSelectedAll: function () { - let boxes = this.$refs.itemCheckBox - if (boxes == null) { - return - } - - let that = this - boxes.forEach(function (box) { - box.checked = that.selectedAll - }) - - this.hasSelectedItems = this.selectedAll - }, - changeSelected: function (e) { - let that = this - that.hasSelectedItems = false - let boxes = that.$refs.itemCheckBox - if (boxes == null) { - return - } - boxes.forEach(function (box) { - if (box.checked) { - that.hasSelectedItems = true - } - }) - }, - deleteAll: function () { - let boxes = this.$refs.itemCheckBox - if (boxes == null) { - return - } - let itemIds = [] - boxes.forEach(function (box) { - if (box.checked) { - itemIds.push(box.value) - } - }) - if (itemIds.length == 0) { - return - } - - Tea.action("/servers/iplists/deleteItems") - .post() - .params({ - itemIds: itemIds - }) - .success(function () { - teaweb.successToast("批量删除成功", 1200, teaweb.reload) - }) - }, - deleteCount: function () { - let that = this - teaweb.confirm("确定要批量删除当前列表中的" + this.MaxDeletes + "个IP吗?", function () { - let query = window.location.search - if (query.startsWith("?")) { - query = query.substring(1) - } - Tea.action("/servers/iplists/deleteCount?" + query) - .post() - .params({count: that.MaxDeletes}) - .success(function () { - teaweb.successToast("批量删除成功", 1200, teaweb.reload) - }) - }) - }, - formatSeconds: function (seconds) { - if (seconds < 60) { - return seconds + "秒" - } - if (seconds < 3600) { - return Math.ceil(seconds / 60) + "分钟" - } - if (seconds < 86400) { - return Math.ceil(seconds / 3600) + "小时" - } - return Math.ceil(seconds / 86400) + "天" - }, - cancelChecked: function () { - this.hasSelectedItems = false - this.selectedAll = false - - let boxes = this.$refs.itemCheckBox - if (boxes == null) { - return - } - boxes.forEach(function (box) { - box.checked = false - }) - } - }, - template: `
-
-
- -     - - -     - -
- - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
IP类型级别过期时间备注操作
-
- - -
-
- - {{item.value}} - - {{item.ipFrom}}  New   - - {{item.ipTo}} - - - * - -
- {{item.region}} - | {{item.isp}} -
-
{{item.isp}}
- - -
- IPv4 - IPv4 - IPv6 - 所有IP - - {{item.eventLevelName}} - - - -
- {{item.expiredTime}} -
- 已过期 -
-
- {{formatSeconds(item.lifeSeconds)}} - 已过期 -
-
- 不过期 -
- {{item.reason}} - - - - - - - - 日志   - 修改   - 删除 -
-
` -}) - -Vue.component("ip-item-text", { - props: ["v-item"], - template: ` - * - - {{vItem.value}} - - {{vItem.ipFrom}} - - {{vItem.ipTo}} - - -   级别:{{vItem.eventLevelName}} -` -}) - -Vue.component("ip-box", { - props: ["v-ip"], - methods: { - popup: function () { - let ip = this.vIp - if (ip == null || ip.length == 0) { - let e = this.$refs.container - ip = e.innerText - if (ip == null) { - ip = e.textContent - } - } - - teaweb.popup("/servers/ipbox?ip=" + ip, { - width: "50em", - height: "30em" - }) - } - }, - template: `` -}) - -Vue.component("sms-sender", { - props: ["value", "name"], - mounted: function () { - this.initType(this.config.type) - }, - data: function () { - let config = this.value - if (config == null) { - config = { - isOn: false, - type: "webHook", - webHookParams: { - url: "", - method: "POST" - }, - aliyunSMSParams: { - sign: "", - templateCode: "", - codeVarName: "code", - accessKeyId: "", - accessKeySecret: "" - }, - tencentSMSParams: { - sdkAppId: "", - sign: "", - templateId: "", - accessKeyId: "", - accessKeySecret: "" - } - } - } - - if (config.aliyunSMSParams == null) { - Vue.set(config, "aliyunSMSParams", { - sign: "", - templateCode: "", - codeVarName: "code", - accessKeyId: "", - accessKeySecret: "" - }) - } - if (config.tencentSMSParams == null) { - Vue.set(config, "tencentSMSParams", { - sdkAppId: "", - sign: "", - templateId: "", - accessKeyId: "", - accessKeySecret: "" - }) - } - - return { - config: config - } - }, - watch: { - "config.type": function (v) { - this.initType(v) - } - }, - methods: { - initType: function (v) { - // initialize params - switch (v) { - case "webHook": - if (this.config.webHookParams == null) { - this.config.webHookParams = { - url: "", - method: "POST" - } - } - break - } - }, - test: function () { - window.TESTING_SMS_CONFIG = this.config - teaweb.popup("/users/setting/smsTest", { - height: "22em" - }) - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用
发送渠道 - -

通过HTTP接口的方式调用你的自定义发送短信接口。

-

通过阿里云短信服务发送短信接口;目前仅支持发送验证码

-

通过腾讯云短信服务发送短信接口;目前仅支持发送验证码

-
HTTP接口的URL地址 * - -

接收发送短信请求的URL,必须以http://https://开头。

-
HTTP接口的请求方法 - -

以在URL参数中加入mobile、body和code三个参数(YOUR_API_URL?mobile=手机号&body=短信内容&code=验证码)的方式调用你的HTTP接口的URL地址;状态码返回200表示成功。

-

通过POST表单发送mobile、body和code三个参数(mobile=手机号&body=短信内容&code=验证码)的方式调用你的HTTP接口URL地址;状态码返回200表示成功。

-
签名名称 * -

在阿里云短信服务 “签名管理” 中添加并通过审核后才能使用。

-
模板CODE * - -

在阿里云短信服务 “模板管理” 中添加并通过审核后才能使用。

-
模板中验证码变量名称 * - -

默认为code,不需要带\${}等符号,即表示在模板中使用\${{{ config.aliyunSMSParams.codeVarName }}}代表要发送的验证码。

-
AccessKey ID * - -

在阿里云 -- RAM访问控制 -- AccessKey中可以创建和获取。

-
AccessKey Secret * - -

和表单中的AccessKey ID对应,在阿里云 -- RAM访问控制 -- AccessKey中可以创建和获取。

-
SDK应用ID * - -

在腾讯云 -- 短信 -- 应用管理 -- 应用列表中可以查看。

-
签名内容 * - -

比如“腾讯云”,在腾讯云 -- 短信 -- 签名管理中可以查看。

-
正文模板ID * - -

在腾讯云 -- 短信 -- 正文模板管理中可以查看。

-
密钥SecretId * - -

同SecretKey一同在腾讯云 -- 访问管理 -- API密钥管理中获取。

-
密钥SecretKey * - -

同SecretId一同在腾讯云 -- 访问管理 -- API密钥管理中获取。

-
发送测试[点此测试]
-
-
` -}) - -Vue.component("email-sender", { - props: ["value", "name"], - data: function () { - let value = this.value - if (value == null) { - value = { - isOn: false, - smtpHost: "", - smtpPort: 0, - username: "", - password: "", - fromEmail: "", - fromName: "" - } - } - let smtpPortString = value.smtpPort.toString() - if (smtpPortString == "0") { - smtpPortString = "" - } - - return { - config: value, - smtpPortString: smtpPortString - } - }, - watch: { - smtpPortString: function (v) { - let port = parseInt(v) - if (!isNaN(port)) { - this.config.smtpPort = port - } - } - }, - methods: { - test: function () { - window.TESTING_EMAIL_CONFIG = this.config - teaweb.popup("/users/setting/emailTest", { - height: "36em" - }) - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用
SMTP地址 * - -

SMTP主机地址,比如smtp.qq.com,目前仅支持TLS协议,如不清楚,请查询对应邮件服务商文档。

-
SMTP端口 * - -

SMTP主机端口,比如587465,如不清楚,请查询对应邮件服务商文档。

-
用户名 * - -

通常为发件人邮箱地址。

-
密码 * - -

邮箱登录密码或授权码,如不清楚,请查询对应邮件服务商文档。。

-
发件人Email * - -

使用的发件人邮箱地址,通常和发件用户名一致。

-
发件人名称 - -

使用的发件人名称,默认使用系统设置的产品名称

-
发送测试[点此测试]
-
-
` -}) - -Vue.component("api-node-selector", { - props: [], - data: function () { - return {} - }, - template: `
- 暂未实现 -
` -}) - -Vue.component("api-node-addresses-box", { - props: ["v-addrs", "v-name"], - data: function () { - let addrs = this.vAddrs - if (addrs == null) { - addrs = [] - } - return { - addrs: addrs - } - }, - methods: { - // 添加IP地址 - addAddr: function () { - let that = this; - teaweb.popup("/settings/api/node/createAddrPopup", { - height: "16em", - callback: function (resp) { - that.addrs.push(resp.data.addr); - } - }) - }, - - // 修改地址 - updateAddr: function (index, addr) { - let that = this; - window.UPDATING_ADDR = addr - teaweb.popup("/settings/api/node/updateAddrPopup?addressId=", { - callback: function (resp) { - Vue.set(that.addrs, index, resp.data.addr); - } - }) - }, - - // 删除IP地址 - removeAddr: function (index) { - this.addrs.$remove(index); - } - }, - template: `
- -
-
-
- {{addr.protocol}}://{{addr.host.quoteIP()}}:{{addr.portRange}} - - -
-
-
-
-
- -
-
` -}) - -// 给Table增加排序功能 -function sortTable(callback) { - // 引入js - let jsFile = document.createElement("script") - jsFile.setAttribute("src", "/js/sortable.min.js") - jsFile.addEventListener("load", function () { - // 初始化 - let box = document.querySelector("#sortable-table") - if (box == null) { - return - } - Sortable.create(box, { - draggable: "tbody", - handle: ".icon.handle", - onStart: function () { - }, - onUpdate: function (event) { - let rows = box.querySelectorAll("tbody") - let rowIds = [] - rows.forEach(function (row) { - rowIds.push(parseInt(row.getAttribute("v-id"))) - }) - callback(rowIds) - } - }) - }) - document.head.appendChild(jsFile) -} - -function sortLoad(callback) { - let jsFile = document.createElement("script") - jsFile.setAttribute("src", "/js/sortable.min.js") - jsFile.addEventListener("load", function () { - if (typeof (callback) == "function") { - callback() - } - }) - document.head.appendChild(jsFile) -} - - -Vue.component("page-box", { - data: function () { - return { - page: "" - } - }, - created: function () { - let that = this; - setTimeout(function () { - that.page = Tea.Vue.page; - }) - }, - template: `
-
-
` -}) - -Vue.component("network-addresses-box", { - props: ["v-server-type", "v-addresses", "v-protocol", "v-name", "v-from", "v-support-range", "v-url"], - data: function () { - let addresses = this.vAddresses - if (addresses == null) { - addresses = [] - } - let protocol = this.vProtocol - if (protocol == null) { - protocol = "" - } - - let name = this.vName - if (name == null) { - name = "addresses" - } - - let from = this.vFrom - if (from == null) { - from = "" - } - - return { - addresses: addresses, - protocol: protocol, - name: name, - from: from, - isEditing: false - } - }, - watch: { - "vServerType": function () { - this.addresses = [] - }, - "vAddresses": function () { - if (this.vAddresses != null) { - this.addresses = this.vAddresses - } - } - }, - methods: { - addAddr: function () { - this.isEditing = true - - let that = this - window.UPDATING_ADDR = null - - let url = this.vUrl - if (url == null) { - url = "/servers/addPortPopup" - } - - teaweb.popup(url + "?serverType=" + this.vServerType + "&protocol=" + this.protocol + "&from=" + this.from + "&supportRange=" + (this.supportRange() ? 1 : 0), { - height: "18em", - callback: function (resp) { - var addr = resp.data.address - if (that.addresses.$find(function (k, v) { - return addr.host == v.host && addr.portRange == v.portRange && addr.protocol == v.protocol - }) != null) { - teaweb.warn("要添加的网络地址已经存在") - return - } - that.addresses.push(addr) - if (["https", "https4", "https6"].$contains(addr.protocol)) { - this.tlsProtocolName = "HTTPS" - } else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) { - this.tlsProtocolName = "TLS" - } - - // 发送事件 - that.$emit("change", that.addresses) - } - }) - }, - removeAddr: function (index) { - this.addresses.$remove(index); - - // 发送事件 - this.$emit("change", this.addresses) - }, - updateAddr: function (index, addr) { - let that = this - window.UPDATING_ADDR = addr - - let url = this.vUrl - if (url == null) { - url = "/servers/addPortPopup" - } - - teaweb.popup(url + "?serverType=" + this.vServerType + "&protocol=" + this.protocol + "&from=" + this.from + "&supportRange=" + (this.supportRange() ? 1 : 0), { - height: "18em", - callback: function (resp) { - var addr = resp.data.address - Vue.set(that.addresses, index, addr) - - if (["https", "https4", "https6"].$contains(addr.protocol)) { - this.tlsProtocolName = "HTTPS" - } else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) { - this.tlsProtocolName = "TLS" - } - - // 发送事件 - that.$emit("change", that.addresses) - } - }) - - // 发送事件 - this.$emit("change", this.addresses) - }, - supportRange: function () { - return this.vSupportRange || (this.vServerType == "tcpProxy" || this.vServerType == "udpProxy") - }, - edit: function () { - this.isEditing = true - } - }, - template: `
- -
-
-
- {{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}}{{addr.portRange}} -
-     [修改] -
-
-
-
-
- {{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}}{{addr.portRange}} - - -
-
-
- [添加端口绑定] -
-
` -}) - -/** - * 保存按钮 - */ -Vue.component("submit-btn", { - template: '' -}); - -// 可以展示更多条目的角图表 -Vue.component("more-items-angle", { - props: ["v-data-url", "v-url"], - data: function () { - return { - visible: false - } - }, - methods: { - show: function () { - this.visible = !this.visible - if (this.visible) { - this.showBox() + change: function(item) { + if (item != null) { + this.$emit("change", item.id) } else { - this.hideBox() + this.$emit("change", 0) } }, - showBox: function () { - let that = this - - this.visible = true - - Tea.action(this.vDataUrl) - .params({ - url: this.vUrl - }) - .post() - .success(function (resp) { - let groups = resp.data.groups - - let boxLeft = that.$el.offsetLeft + 120; - let boxTop = that.$el.offsetTop + 70; - - let box = document.createElement("div") - box.setAttribute("id", "more-items-box") - box.style.cssText = "z-index: 100; position: absolute; left: " + boxLeft + "px; top: " + boxTop + "px; max-height: 30em; overflow: auto; border-bottom: 1px solid rgba(34,36,38,.15)" - document.body.append(box) - - let menuHTML = "
    " - groups.forEach(function (group) { - menuHTML += "
    " + teaweb.encodeHTML(group.name) + "
    " - group.items.forEach(function (item) { - menuHTML += "" + teaweb.encodeHTML(item.name) + "" - }) - }) - menuHTML += "
" - box.innerHTML = menuHTML - - let listener = function (e) { - if (e.target.tagName == "I") { - return - } - - if (!that.isInBox(box, e.target)) { - document.removeEventListener("click", listener) - that.hideBox() - } - } - document.addEventListener("click", listener) - }) - }, - hideBox: function () { - let box = document.getElementById("more-items-box") - if (box != null) { - box.parentNode.removeChild(box) - } - this.visible = false - }, - isInBox: function (parent, child) { - while (true) { - if (child == null) { - break - } - if (child.parentNode == parent) { - return true - } - child = child.parentNode - } - return false - } - }, - template: `切换` -}) - -/** - * 菜单项 - */ -Vue.component("menu-item", { - props: ["href", "active", "code"], - data: function () { - let active = this.active - if (typeof (active) == "undefined") { - var itemCode = "" - if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") { - itemCode = window.TEA.ACTION.data.firstMenuItem - } - if (itemCode != null && itemCode.length > 0 && this.code != null && this.code.length > 0) { - if (itemCode.indexOf(",") > 0) { - active = itemCode.split(",").$contains(this.code) - } else { - active = (itemCode == this.code) - } - } - } - - let href = (this.href == null) ? "" : this.href - if (typeof (href) == "string" && href.length > 0 && href.startsWith(".")) { - let qIndex = href.indexOf("?") - if (qIndex >= 0) { - href = Tea.url(href.substring(0, qIndex)) + href.substring(qIndex) - } else { - href = Tea.url(href) - } - } - - return { - vHref: href, - vActive: active - } - }, - methods: { - click: function (e) { - this.$emit("click", e) - } - }, - template: '\ - \ - ' -}); - -// 使用Icon的链接方式 -Vue.component("link-icon", { - props: ["href", "title", "target", "size"], - data: function () { - let realSize = this.size - if (realSize == null || realSize.length == 0) { - realSize = "small" - } - - return { - vTitle: (this.title == null) ? "打开链接" : this.title, - realSize: realSize - } - }, - template: ` ` -}) - -// 带有下划虚线的连接 -Vue.component("link-red", { - props: ["href", "title"], - data: function () { - let href = this.href - if (href == null) { - href = "" - } - return { - vHref: href - } - }, - methods: { - clickPrevent: function () { - emitClick(this, arguments) - - if (this.vHref.length > 0) { - window.location = this.vHref - } - } - }, - template: `` -}) - -// 会弹出窗口的链接 -Vue.component("link-popup", { - props: ["title"], - methods: { - clickPrevent: function () { - emitClick(this, arguments) - } - }, - template: `` -}) - -Vue.component("popup-icon", { - props: ["title", "href", "height"], - methods: { - clickPrevent: function () { - if (this.href != null && this.href.length > 0) { - teaweb.popup(this.href, { - height: this.height - }) - } - } - }, - template: ` ` -}) - -// 小提示 -Vue.component("tip-icon", { - props: ["content"], - methods: { - showTip: function () { - teaweb.popupTip(this.content) - } - }, - template: `` -}) - -// 提交点击事件 -function emitClick(obj, arguments) { - let event = "click" - let newArgs = [event] - for (let i = 0; i < arguments.length; i++) { - newArgs.push(arguments[i]) - } - obj.$emit.apply(obj, newArgs) -} - -Vue.component("countries-selector", { - props: ["v-countries"], - data: function () { - let countries = this.vCountries - if (countries == null) { - countries = [] - } - let countryIds = countries.$map(function (k, v) { - return v.id - }) - return { - countries: countries, - countryIds: countryIds - } - }, - methods: { - add: function () { - let countryStringIds = this.countryIds.map(function (v) { - return v.toString() - }) - let that = this - teaweb.popup("/ui/selectCountriesPopup?countryIds=" + countryStringIds.join(","), { - width: "48em", - height: "23em", - callback: function (resp) { - that.countries = resp.data.countries - that.change() - } - }) - }, - remove: function (index) { - this.countries.$remove(index) - this.change() - }, - change: function () { - this.countryIds = this.countries.$map(function (k, v) { - return v.id - }) - } - }, - template: `
- -
-
{{country.name}}
-
-
-
- -
-
` -}) - -Vue.component("raquo-item", { - template: `»` -}) - -Vue.component("bandwidth-size-capacity-view", { - props: ["v-value"], - data: function () { - let capacity = this.vValue - if (capacity != null && capacity.count > 0 && typeof capacity.unit === "string") { - capacity.unit = capacity.unit[0].toUpperCase() + capacity.unit.substring(1) + "ps" - } - return { - capacity: capacity - } - }, - template: ` - {{capacity.count}}{{capacity.unit}} -` -}) - -Vue.component("more-options-tbody", { - data: function () { - return { - isVisible: false - } - }, - methods: { - show: function () { - this.isVisible = !this.isVisible - this.$emit("change", this.isVisible) - } - }, - template: ` - - 更多选项收起选项 - -` -}) - -Vue.component("download-link", { - props: ["v-element", "v-file", "v-value"], - created: function () { - let that = this - setTimeout(function () { - that.url = that.composeURL() - }, 1000) - }, - data: function () { - let filename = this.vFile - if (filename == null || filename.length == 0) { - filename = "unknown-file" - } - return { - file: filename, - url: this.composeURL() - } - }, - methods: { - composeURL: function () { - let text = "" - if (this.vValue != null) { - text = this.vValue - } else { - let e = document.getElementById(this.vElement) - if (e == null) { - // 不提示错误,因为此时可能页面未加载完整 - return - } - text = e.innerText - if (text == null) { - text = e.textContent - } - } - return Tea.url("/ui/download", { - file: this.file, - text: text - }) - } - }, - template: ``, -}) - -Vue.component("values-box", { - props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"], - data: function () { - let values = this.values; - if (values == null) { - values = []; - } - - if (this.vValues != null && typeof this.vValues == "object") { - values = this.vValues - } - - return { - "realValues": values, - "isUpdating": false, - "isAdding": false, - "index": 0, - "value": "", - isEditing: false - } - }, - methods: { - create: function () { - this.isAdding = true; - var that = this; - setTimeout(function () { - that.$refs.value.focus(); - }, 200); - }, - update: function (index) { - this.cancel() - this.isUpdating = true; - this.index = index; - this.value = this.realValues[index]; - var that = this; - setTimeout(function () { - that.$refs.value.focus(); - }, 200); - }, - confirm: function () { - if (this.value.length == 0) { - if (typeof(this.vAllowEmpty) != "boolean" || !this.vAllowEmpty) { - return - } - } - - // validate - if (typeof(this.validator) == "function") { - let resp = this.validator.call(this, this.value) - if (typeof resp == "object") { - if (typeof resp.isOk == "boolean" && !resp.isOk) { - if (typeof resp.message == "string") { - let that = this - teaweb.warn(resp.message, function () { - that.$refs.value.focus(); - }) - } - return - } - } - } - - if (this.isUpdating) { - Vue.set(this.realValues, this.index, this.value); - } else { - this.realValues.push(this.value); - } - this.cancel() - this.$emit("change", this.realValues) - }, - remove: function (index) { - this.realValues.$remove(index) - this.$emit("change", this.realValues) - }, - cancel: function () { - this.isUpdating = false; - this.isAdding = false; - this.value = ""; - }, - updateAll: function (values) { - this.realValues = values - }, - addValue: function (v) { - this.realValues.push(v) - }, - - startEditing: function () { - this.isEditing = !this.isEditing - }, - allValues: function () { - return this.realValues - } - }, - template: `
-
-
- {{value}} - [空] -
- [修改] -
-
-
-
- {{value}} - [空] - -   - -
-
-
- -
-
-
- -
-
- -
-
- -
-
-
-
- -
-
-
` -}); - -Vue.component("datetime-input", { - props: ["v-name", "v-timestamp"], - mounted: function () { - let that = this - teaweb.datepicker(this.$refs.dayInput, function (v) { - that.day = v - that.hour = "23" - that.minute = "59" - that.second = "59" - that.change() - }) - }, - data: function () { - let timestamp = this.vTimestamp - if (timestamp != null) { - timestamp = parseInt(timestamp) - if (isNaN(timestamp)) { - timestamp = 0 - } - } else { - timestamp = 0 - } - - let day = "" - let hour = "" - let minute = "" - let second = "" - - if (timestamp > 0) { - let date = new Date() - date.setTime(timestamp * 1000) - - let year = date.getFullYear().toString() - let month = this.leadingZero((date.getMonth() + 1).toString(), 2) - day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2) - - hour = this.leadingZero(date.getHours().toString(), 2) - minute = this.leadingZero(date.getMinutes().toString(), 2) - second = this.leadingZero(date.getSeconds().toString(), 2) - } - - return { - timestamp: timestamp, - day: day, - hour: hour, - minute: minute, - second: second, - - hasDayError: false, - hasHourError: false, - hasMinuteError: false, - hasSecondError: false - } - }, - methods: { - change: function () { - // day - if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) { - this.hasDayError = true - return - } - let pieces = this.day.split("-") - let year = parseInt(pieces[0]) - - let month = parseInt(pieces[1]) - if (month < 1 || month > 12) { - this.hasDayError = true - return - } - - let day = parseInt(pieces[2]) - if (day < 1 || day > 32) { - this.hasDayError = true - return - } - - this.hasDayError = false - - // hour - if (!/^\d+$/.test(this.hour)) { - this.hasHourError = true - return - } - let hour = parseInt(this.hour) - if (isNaN(hour)) { - this.hasHourError = true - return - } - if (hour < 0 || hour >= 24) { - this.hasHourError = true - return - } - this.hasHourError = false - - // minute - if (!/^\d+$/.test(this.minute)) { - this.hasMinuteError = true - return - } - let minute = parseInt(this.minute) - if (isNaN(minute)) { - this.hasMinuteError = true - return - } - if (minute < 0 || minute >= 60) { - this.hasMinuteError = true - return - } - this.hasMinuteError = false - - // second - if (!/^\d+$/.test(this.second)) { - this.hasSecondError = true - return - } - let second = parseInt(this.second) - if (isNaN(second)) { - this.hasSecondError = true - return - } - if (second < 0 || second >= 60) { - this.hasSecondError = true - return - } - this.hasSecondError = false - - let date = new Date(year, month - 1, day, hour, minute, second) - this.timestamp = Math.floor(date.getTime() / 1000) - }, - leadingZero: function (s, l) { - s = s.toString() - if (l <= s.length) { - return s - } - for (let i = 0; i < l - s.length; i++) { - s = "0" + s - } - return s - }, - resultTimestamp: function () { - return this.timestamp - }, - nextYear: function () { - let date = new Date() - date.setFullYear(date.getFullYear()+1) - this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) - this.hour = this.leadingZero(date.getHours(), 2) - this.minute = this.leadingZero(date.getMinutes(), 2) - this.second = this.leadingZero(date.getSeconds(), 2) - this.change() - }, - nextDays: function (days) { - let date = new Date() - date.setTime(date.getTime() + days * 86400 * 1000) - this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) - this.hour = this.leadingZero(date.getHours(), 2) - this.minute = this.leadingZero(date.getMinutes(), 2) - this.second = this.leadingZero(date.getSeconds(), 2) - this.change() - }, - nextHours: function (hours) { - let date = new Date() - date.setTime(date.getTime() + hours * 3600 * 1000) - this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) - this.hour = this.leadingZero(date.getHours(), 2) - this.minute = this.leadingZero(date.getMinutes(), 2) - this.second = this.leadingZero(date.getSeconds(), 2) - this.change() - } - }, - template: `
- -
-
- -
-
-
:
-
-
:
-
-
-

常用时间:  1小时  |  1天  |  3天  |  1周  |  30天  |  1年 

-
` -}) - -// 启用状态标签 -Vue.component("label-on", { - props: ["v-is-on"], - template: '
已启用已停用
' -}) - -// 文字代码标签 -Vue.component("code-label", { - methods: { - click: function (args) { - this.$emit("click", args) - } - }, - template: `` -}) - -Vue.component("code-label-plain", { - template: `` -}) - - -// tiny标签 -Vue.component("tiny-label", { - template: `` -}) - -Vue.component("tiny-basic-label", { - template: `` -}) - -// 更小的标签 -Vue.component("micro-basic-label", { - template: `` -}) - - -// 灰色的Label -Vue.component("grey-label", { - props: ["color"], - data: function () { - let color = "grey" - if (this.color != null && this.color.length > 0) { - color = "red" - } - return { - labelColor: color - } - }, - template: `` -}) - -// 可选标签 -Vue.component("optional-label", { - template: `(可选)` -}) - -// Plus专属 -Vue.component("plus-label", { - template: `Plus专属功能。` -}) - -// 提醒设置项为专业设置 -Vue.component("pro-warning-label", { - template: `注意:通常不需要修改;如要修改,请在专家指导下进行。` -}) - - -Vue.component("js-page", { - props: ["v-max"], - data: function () { - let max = this.vMax - if (max == null) { - max = 0 - } - return { - max: max, - page: 1 - } - }, - methods: { - updateMax: function (max) { - this.max = max - }, - selectPage: function(page) { - this.page = page - this.$emit("change", page) - } - }, - template:`
-
- {{i}} -
-
` -}) - -/** - * 一级菜单 - */ -Vue.component("first-menu", { - props: [], - template: ' \ -
\ - \ -
\ -
' -}); - -/** - * 更多选项 - */ -Vue.component("more-options-indicator", { - props:[], - data: function () { - return { - visible: false - } - }, - methods: { - changeVisible: function () { - this.visible = !this.visible - if (Tea.Vue != null) { - Tea.Vue.moreOptionsVisible = this.visible - } - this.$emit("change", this.visible) - this.$emit("input", this.visible) - } - }, - template: '更多选项收起选项 ' -}); - -Vue.component("page-size-selector", { - data: function () { - let query = window.location.search - let pageSize = 10 - if (query.length > 0) { - query = query.substr(1) - let params = query.split("&") - params.forEach(function (v) { - let pieces = v.split("=") - if (pieces.length == 2 && pieces[0] == "pageSize") { - let pageSizeString = pieces[1] - if (pageSizeString.match(/^\d+$/)) { - pageSize = parseInt(pageSizeString, 10) - if (isNaN(pageSize) || pageSize < 1) { - pageSize = 10 - } - } - } - }) - } - return { - pageSize: pageSize - } - }, - watch: { - pageSize: function () { - window.ChangePageSize(this.pageSize) - } - }, - template: `` -}) - -/** - * 二级菜单 - */ -Vue.component("second-menu", { - template: ' \ -
\ - \ -
\ -
' -}); - -Vue.component("loading-message", { - template: `
-
  -
` -}) - -Vue.component("file-textarea", { - props: ["value"], - data: function () { - let value = this.value - if (typeof value != "string") { - value = "" - } - return { - realValue: value - } - }, - mounted: function () { - }, - methods: { - dragover: function () {}, - drop: function (e) { - let that = this - e.dataTransfer.items[0].getAsFile().text().then(function (data) { - that.setValue(data) - }) - }, - setValue: function (value) { - this.realValue = value - }, - focus: function () { - this.$refs.textarea.focus() - } - }, - template: `` -}) - -Vue.component("more-options-angle", { - data: function () { - return { - isVisible: false - } - }, - methods: { - show: function () { - this.isVisible = !this.isVisible - this.$emit("change", this.isVisible) - } - }, - template: `更多选项收起选项` -}) - -Vue.component("columns-grid", { - props: [], - mounted: function () { - this.columns = this.calculateColumns() - - let that = this - window.addEventListener("resize", function () { - that.columns = that.calculateColumns() - }) - }, - data: function () { - return { - columns: "four" - } - }, - methods: { - calculateColumns: function () { - let w = window.innerWidth - let columns = Math.floor(w / 250) - if (columns == 0) { - columns = 1 - } - - let columnElements = this.$el.getElementsByClassName("column") - if (columnElements.length == 0) { - return - } - let maxColumns = columnElements.length - if (columns > maxColumns) { - columns = maxColumns - } - - // 添加右侧边框 - for (let index = 0; index < columnElements.length; index++) { - let el = columnElements[index] - el.className = el.className.replace("with-border", "") - if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) { - el.className += " with-border" - } - } - - switch (columns) { - case 1: - return "one" - case 2: - return "two" - case 3: - return "three" - case 4: - return "four" - case 5: - return "five" - case 6: - return "six" - case 7: - return "seven" - case 8: - return "eight" - case 9: - return "nine" - case 10: - return "ten" - default: - return "ten" - } - } - }, - template: `
- -
` -}) - -/** - * 菜单项 - */ -Vue.component("inner-menu-item", { - props: ["href", "active", "code"], - data: function () { - var active = this.active; - if (typeof(active) =="undefined") { - var itemCode = ""; - if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") { - itemCode = window.TEA.ACTION.data.firstMenuItem; - } - active = (itemCode == this.code); - } - return { - vHref: (this.href == null) ? "" : this.href, - vActive: active - }; - }, - template: '\ - [] \ - ' -}); - -Vue.component("bandwidth-size-capacity-box", { - props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"], - data: function () { - let v = this.vValue - if (v == null) { - v = { - count: this.vCount, - unit: this.vUnit - } - } - if (v.unit == null || v.unit.length == 0) { - v.unit = "mb" - } - - if (typeof (v["count"]) != "number") { - v["count"] = -1 - } - - let vSize = this.size - if (vSize == null) { - vSize = 6 - } - - let vMaxlength = this.maxlength - if (vMaxlength == null) { - vMaxlength = 10 - } - - let supportedUnits = this.vSupportedUnits - if (supportedUnits == null) { - supportedUnits = [] - } - - return { - capacity: v, - countString: (v.count >= 0) ? v.count.toString() : "", - vSize: vSize, - vMaxlength: vMaxlength, - supportedUnits: supportedUnits - } - }, - watch: { - "countString": function (newValue) { - let value = newValue.trim() - if (value.length == 0) { - this.capacity.count = -1 - this.change() - return - } - let count = parseInt(value) - if (!isNaN(count)) { - this.capacity.count = count - } - this.change() - } - }, - methods: { - change: function () { - this.$emit("change", this.capacity) - } - }, - template: `
- -
- -
-
- -
-
` -}) - -Vue.component("health-check-config-box", { - props: ["v-health-check-config", "v-check-domain-url", "v-is-plus"], - data: function () { - let healthCheckConfig = this.vHealthCheckConfig - let urlProtocol = "http" - let urlPort = "" - let urlRequestURI = "/" - let urlHost = "" - - if (healthCheckConfig == null) { - healthCheckConfig = { - isOn: false, - url: "", - interval: {count: 60, unit: "second"}, - statusCodes: [200], - timeout: {count: 10, unit: "second"}, - countTries: 3, - tryDelay: {count: 100, unit: "ms"}, - autoDown: true, - countUp: 1, - countDown: 3, - userAgent: "", - onlyBasicRequest: true, - accessLogIsOn: true - } - let that = this - setTimeout(function () { - that.changeURL() - }, 500) - } else { - try { - let url = new URL(healthCheckConfig.url) - urlProtocol = url.protocol.substring(0, url.protocol.length - 1) - - // 域名 - urlHost = url.host - if (urlHost == "%24%7Bhost%7D") { - urlHost = "${host}" - } - let colonIndex = urlHost.indexOf(":") - if (colonIndex > 0) { - urlHost = urlHost.substring(0, colonIndex) - } - - urlPort = url.port - urlRequestURI = url.pathname - if (url.search.length > 0) { - urlRequestURI += url.search - } - } catch (e) { - } - - if (healthCheckConfig.statusCodes == null) { - healthCheckConfig.statusCodes = [200] - } - if (healthCheckConfig.interval == null) { - healthCheckConfig.interval = {count: 60, unit: "second"} - } - if (healthCheckConfig.timeout == null) { - healthCheckConfig.timeout = {count: 10, unit: "second"} - } - if (healthCheckConfig.tryDelay == null) { - healthCheckConfig.tryDelay = {count: 100, unit: "ms"} - } - if (healthCheckConfig.countUp == null || healthCheckConfig.countUp < 1) { - healthCheckConfig.countUp = 1 - } - if (healthCheckConfig.countDown == null || healthCheckConfig.countDown < 1) { - healthCheckConfig.countDown = 3 - } - } - - return { - healthCheck: healthCheckConfig, - advancedVisible: false, - urlProtocol: urlProtocol, - urlHost: urlHost, - urlPort: urlPort, - urlRequestURI: urlRequestURI, - urlIsEditing: healthCheckConfig.url.length == 0, - - hostErr: "" - } - }, - watch: { - urlRequestURI: function () { - if (this.urlRequestURI.length > 0 && this.urlRequestURI[0] != "/") { - this.urlRequestURI = "/" + this.urlRequestURI - } - this.changeURL() - }, - urlPort: function (v) { - let port = parseInt(v) - if (!isNaN(port)) { - this.urlPort = port.toString() - } else { - this.urlPort = "" - } - this.changeURL() - }, - urlProtocol: function () { - this.changeURL() - }, - urlHost: function () { - this.changeURL() - this.hostErr = "" - }, - "healthCheck.countTries": function (v) { - let count = parseInt(v) - if (!isNaN(count)) { - this.healthCheck.countTries = count - } else { - this.healthCheck.countTries = 0 - } - }, - "healthCheck.countUp": function (v) { - let count = parseInt(v) - if (!isNaN(count)) { - this.healthCheck.countUp = count - } else { - this.healthCheck.countUp = 0 - } - }, - "healthCheck.countDown": function (v) { - let count = parseInt(v) - if (!isNaN(count)) { - this.healthCheck.countDown = count - } else { - this.healthCheck.countDown = 0 - } - } - }, - methods: { - showAdvanced: function () { - this.advancedVisible = !this.advancedVisible - }, - changeURL: function () { - let urlHost = this.urlHost - if (urlHost.length == 0) { - urlHost = "${host}" - } - this.healthCheck.url = this.urlProtocol + "://" + urlHost + ((this.urlPort.length > 0) ? ":" + this.urlPort : "") + this.urlRequestURI - }, - changeStatus: function (values) { - this.healthCheck.statusCodes = values.$map(function (k, v) { - let status = parseInt(v) - if (isNaN(status)) { - return 0 - } else { - return status - } - }) - }, - onChangeURLHost: function () { - let checkDomainURL = this.vCheckDomainUrl - if (checkDomainURL == null || checkDomainURL.length == 0) { - return - } - - let that = this - Tea.action(checkDomainURL) - .params({host: this.urlHost}) - .success(function (resp) { - if (!resp.data.isOk) { - that.hostErr = "在当前集群中找不到此域名,可能会影响健康检查结果。" - } else { - that.hostErr = "" - } - }) - .post() - }, - editURL: function () { - this.urlIsEditing = !this.urlIsEditing - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用健康检查 -
- - -
-

通过访问节点上的网站URL来确定节点是否健康。

-
检测URL * -
{{healthCheck.url}}   修改
-
- - - - - - - - - - - - - - - - - -
协议 - -
域名 - -

{{hostErr}}已经部署到当前集群的一个域名;如果为空则使用节点IP作为域名。如果协议是https,这里必须填写一个已经设置了SSL证书的域名。

-
端口 - -

域名或者IP的端口,可选项,默认为80/443。

-
RequestURI -

请求的路径,可以带参数,可选项。

-
-
-

拼接后的检测URL:{{healthCheck.url}},其中\${host}指的是域名。

-
-
检测时间间隔 - -

两次检查之间的间隔。

-
自动上/下线IP -
- - -
-

选中后系统会根据健康检查的结果自动标记节点IP节点的上线/下线状态,并可能自动同步DNS设置。注意:免费版的只能整体上下线整个节点,商业版的可以下线单个IP。

-
连续上线次数 - -

连续{{healthCheck.countUp}}次检查成功后自动恢复上线。

-
连续下线次数 - -

连续{{healthCheck.countDown}}次检查失败后自动下线。

-
允许的状态码 - -

允许检测URL返回的状态码列表。

-
超时时间 - -

读取检测URL超时时间。

-
连续尝试次数 - -

如果读取检测URL失败后需要再次尝试的次数。

-
每次尝试间隔 - -

如果读取检测URL失败后再次尝试时的间隔时间。

-
终端信息(User-Agent) - -

发送到服务器的User-Agent值,不填写表示使用默认值。

-
只基础请求 - -

只做基础的请求,不处理反向代理(不检查源站)、WAF等。

-
记录访问日志 - -

记录健康检查的访问日志。

-
-
-
` -}) - -// 将变量转换为中文 -Vue.component("request-variables-describer", { - data: function () { - return { - vars:[] - } - }, - methods: { - update: function (variablesString) { - this.vars = [] - let that = this - variablesString.replace(/\${.+?}/g, function (v) { - let def = that.findVar(v) - if (def == null) { - return v - } - that.vars.push(def) - }) - }, - findVar: function (name) { - let def = null - window.REQUEST_VARIABLES.forEach(function (v) { - if (v.code == name) { - def = v - } - }) - return def - } - }, - template: ` - {{v.code}} - {{v.name}} -` -}) - - -Vue.component("combo-box", { - // data-url 和 data-key 成对出现 - props: [ - "name", "title", "placeholder", "size", "v-items", "v-value", - "data-url", // 数据源URL - "data-key", // 数据源中数据的键名 - "data-search", // 是否启用动态搜索,如果值为on或true,则表示启用 - "width" - ], - mounted: function () { - if (this.dataURL.length > 0) { - this.search("") - } - - // 设定菜单宽度 - let searchBox = this.$refs.searchBox - if (searchBox != null) { - let inputWidth = searchBox.offsetWidth - if (inputWidth != null && inputWidth > 0) { - this.$refs.menu.style.width = inputWidth + "px" - } else if (this.styleWidth.length > 0) { - this.$refs.menu.style.width = this.styleWidth - } - } - }, - data: function () { - let items = this.vItems - if (items == null || !(items instanceof Array)) { - items = [] - } - items = this.formatItems(items) - - // 当前选中项 - let selectedItem = null - if (this.vValue != null) { - let that = this - items.forEach(function (v) { - if (v.value == that.vValue) { - selectedItem = v - } - }) - } - - let width = this.width - if (width == null || width.length == 0) { - width = "11em" - } else { - if (/\d+$/.test(width)) { - width += "em" - } - } - - // data url - let dataURL = "" - if (typeof this.dataUrl == "string" && this.dataUrl.length > 0) { - dataURL = this.dataUrl - } - - return { - allItems: items, // 原始的所有的items - items: items.$copy(), // 候选的items - selectedItem: selectedItem, // 选中的item - keyword: "", - visible: false, - hideTimer: null, - hoverIndex: 0, - styleWidth: width, - - isInitial: true, - dataURL: dataURL, - urlRequestId: 0 // 记录URL请求ID,防止并行冲突 - } - }, - methods: { - search: function (keyword) { - // 从URL中获取选项数据 - let dataUrl = this.dataURL - let dataKey = this.dataKey - let that = this - - let requestId = Math.random() - this.urlRequestId = requestId - - Tea.action(dataUrl) - .params({ - keyword: (keyword == null) ? "" : keyword - }) - .post() - .success(function (resp) { - if (requestId != that.urlRequestId) { - return - } - - if (resp.data != null) { - if (typeof (resp.data[dataKey]) == "object") { - let items = that.formatItems(resp.data[dataKey]) - that.allItems = items - that.items = items.$copy() - - if (that.isInitial) { - that.isInitial = false - if (that.vValue != null) { - items.forEach(function (v) { - if (v.value == that.vValue) { - that.selectedItem = v - } - }) - } - } - } - } - }) - }, - formatItems: function (items) { - items.forEach(function (v) { - if (v.value == null) { - v.value = v.id - } - }) - return items - }, - reset: function () { - this.selectedItem = null - this.change() - this.hoverIndex = 0 - - let that = this - setTimeout(function () { - if (that.$refs.searchBox) { - that.$refs.searchBox.focus() - } - }) - }, clear: function () { - this.selectedItem = null - this.change() - this.hoverIndex = 0 - }, - changeKeyword: function () { - let shouldSearch = this.dataURL.length > 0 && (this.dataSearch == "on" || this.dataSearch == "true") - - this.hoverIndex = 0 - let keyword = this.keyword - if (keyword.length == 0) { - if (shouldSearch) { - this.search(keyword) - } else { - this.items = this.allItems.$copy() - } - return - } - - - if (shouldSearch) { - this.search(keyword) - } else { - this.items = this.allItems.$copy().filter(function (v) { - if (v.fullname != null && v.fullname.length > 0 && teaweb.match(v.fullname, keyword)) { - return true - } - return teaweb.match(v.name, keyword) - }) - } - }, - selectItem: function (item) { - this.selectedItem = item - this.change() - this.hoverIndex = 0 - this.keyword = "" - this.changeKeyword() - }, - confirm: function () { - if (this.items.length > this.hoverIndex) { - this.selectItem(this.items[this.hoverIndex]) - } - }, - show: function () { - this.visible = true - - // 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换 - }, - hide: function () { - let that = this - this.hideTimer = setTimeout(function () { - that.visible = false - }, 500) - }, - downItem: function () { - this.hoverIndex++ - if (this.hoverIndex > this.items.length - 1) { - this.hoverIndex = 0 - } - this.focusItem() - }, - upItem: function () { - this.hoverIndex-- - if (this.hoverIndex < 0) { - this.hoverIndex = 0 - } - this.focusItem() - }, - focusItem: function () { - if (this.hoverIndex < this.items.length) { - this.$refs.itemRef[this.hoverIndex].focus() - let that = this - setTimeout(function () { - that.$refs.searchBox.focus() - if (that.hideTimer != null) { - clearTimeout(that.hideTimer) - that.hideTimer = null - } - }) - } - }, - change: function () { - this.$emit("change", this.selectedItem) - - let that = this - setTimeout(function () { - if (that.$refs.selectedLabel != null) { - that.$refs.selectedLabel.focus() - } - }) - }, - submitForm: function (event) { - if (event.target.tagName != "A") { - return - } - let parentBox = this.$refs.selectedLabel.parentNode - while (true) { - parentBox = parentBox.parentNode - if (parentBox == null || parentBox.tagName == "BODY") { - return - } - if (parentBox.tagName == "FORM") { - parentBox.submit() - break - } - } - }, - - setDataURL: function (dataURL) { - this.dataURL = dataURL - }, - reloadData: function () { - this.search("") - } - }, - template: `
- -
- -
- - -
- - {{title}}:{{selectedItem.name}} - - -
- - - -
` -}) - -Vue.component("search-box", { - props: ["placeholder", "width"], - data: function () { - let width = this.width - if (width == null) { - width = "10em" - } - return { - realWidth: width, - realValue: "" - } - }, - methods: { - onInput: function () { - this.$emit("input", { value: this.realValue}) - this.$emit("change", { value: this.realValue}) - }, - clearValue: function () { - this.realValue = "" - this.focus() - this.onInput() - }, - focus: function () { - this.$refs.valueRef.focus() + this.$refs.comboBox.clear() } }, template: `
-
- - -
+
` }) -Vue.component("dot", { - template: '' -}) - -Vue.component("time-duration-box", { - props: ["v-name", "v-value", "v-count", "v-unit", "placeholder", "v-min-unit", "maxlength"], - mounted: function () { - this.change() - }, - data: function () { - let v = this.vValue - if (v == null) { - v = { - count: this.vCount, - unit: this.vUnit - } - } - if (typeof (v["count"]) != "number") { - v["count"] = -1 - } - - let minUnit = this.vMinUnit - let units = [ - { - code: "ms", - name: "毫秒" - }, - { - code: "second", - name: "秒" - }, - { - code: "minute", - name: "分钟" - }, - { - code: "hour", - name: "小时" - }, - { - code: "day", - name: "天" - } - ] - let minUnitIndex = -1 - if (minUnit != null && typeof minUnit == "string" && minUnit.length > 0) { - for (let i = 0; i < units.length; i++) { - if (units[i].code == minUnit) { - minUnitIndex = i - break - } - } - } - if (minUnitIndex > -1) { - units = units.slice(minUnitIndex) - } - - let maxLength = parseInt(this.maxlength) - if (typeof maxLength != "number") { - maxLength = 10 - } - - return { - duration: v, - countString: (v.count >= 0) ? v.count.toString() : "", - units: units, - realMaxLength: maxLength - } - }, - watch: { - "countString": function (newValue) { - let value = newValue.trim() - if (value.length == 0) { - this.duration.count = -1 - return - } - let count = parseInt(value) - if (!isNaN(count)) { - this.duration.count = count - } - this.change() - } - }, - methods: { - change: function () { - this.$emit("change", this.duration) - } - }, - template: `
- -
- -
-
- -
-
` -}) - -Vue.component("time-duration-text", { - props: ["v-value"], - methods: { - unitName: function (unit) { - switch (unit) { - case "ms": - return "毫秒" - case "second": - return "秒" - case "minute": - return "分钟" - case "hour": - return "小时" - case "day": - return "天" - } - } - }, - template: ` - {{vValue.count}} {{unitName(vValue.unit)}} -` -}) - -Vue.component("not-found-box", { - props: ["message"], - template: `
-
-

{{message}}

-
` -}) - -// 警告消息 -Vue.component("warning-message", { - template: `
` -}) - -let checkboxId = 0 -Vue.component("checkbox", { - props: ["name", "value", "v-value", "id", "checked"], - data: function () { - checkboxId++ - let elementId = this.id - if (elementId == null) { - elementId = "checkbox" + checkboxId - } - - let elementValue = this.vValue - if (elementValue == null) { - elementValue = "1" - } - - let checkedValue = this.value - if (checkedValue == null && this.checked == "checked") { - checkedValue = elementValue - } - - return { - elementId: elementId, - elementValue: elementValue, - newValue: checkedValue - } - }, - methods: { - change: function () { - this.$emit("input", this.newValue) - }, - check: function () { - this.newValue = this.elementValue - }, - uncheck: function () { - this.newValue = "" - }, - isChecked: function () { - return (typeof (this.newValue) == "boolean" && this.newValue) || this.newValue == this.elementValue - } - }, - watch: { - value: function (v) { - if (typeof v == "boolean") { - this.newValue = v - } - } - }, - template: `
- - -
` -}) - -Vue.component("network-addresses-view", { - props: ["v-addresses"], - template: `
-
- {{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}} -
-
` -}) - -Vue.component("url-patterns-box", { - props: ["value"], - data: function () { - let patterns = [] - if (this.value != null) { - patterns = this.value - } - - return { - patterns: patterns, - isAdding: false, - - addingPattern: {"type": "wildcard", "pattern": ""}, - editingIndex: -1, - - patternIsInvalid: false, - - windowIsSmall: window.innerWidth < 600 - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.patternInput.focus() - }) - }, - edit: function (index) { - this.isAdding = true - this.editingIndex = index - this.addingPattern = { - type: this.patterns[index].type, - pattern: this.patterns[index].pattern - } - }, - confirm: function () { - if (this.requireURL(this.addingPattern.type)) { - let pattern = this.addingPattern.pattern.trim() - if (pattern.length == 0) { - let that = this - teaweb.warn("请输入URL", function () { - that.$refs.patternInput.focus() - }) - return - } - } - if (this.editingIndex < 0) { - this.patterns.push({ - type: this.addingPattern.type, - pattern: this.addingPattern.pattern - }) - } else { - this.patterns[this.editingIndex].type = this.addingPattern.type - this.patterns[this.editingIndex].pattern = this.addingPattern.pattern - } - this.notifyChange() - this.cancel() - }, - remove: function (index) { - this.patterns.$remove(index) - this.cancel() - this.notifyChange() - }, - cancel: function () { - this.isAdding = false - this.addingPattern = {"type": "wildcard", "pattern": ""} - this.editingIndex = -1 - }, - patternTypeName: function (patternType) { - switch (patternType) { - case "wildcard": - return "通配符" - case "regexp": - return "正则" - case "images": - return "常见图片文件" - case "audios": - return "常见音频文件" - case "videos": - return "常见视频文件" - } - return "" - }, - notifyChange: function () { - this.$emit("input", this.patterns) - }, - changePattern: function () { - this.patternIsInvalid = false - let pattern = this.addingPattern.pattern - switch (this.addingPattern.type) { - case "wildcard": - if (pattern.indexOf("?") >= 0) { - this.patternIsInvalid = true - } - break - case "regexp": - if (pattern.indexOf("?") >= 0) { - let pieces = pattern.split("?") - for (let i = 0; i < pieces.length - 1; i++) { - if (pieces[i].length == 0 || pieces[i][pieces[i].length - 1] != "\\") { - this.patternIsInvalid = true - } - } - } - break - } - }, - requireURL: function (patternType) { - return patternType == "wildcard" || patternType == "regexp" - } - }, - template: `
-
-
- [{{patternTypeName(pattern.type)}}] {{pattern.pattern}}   - - -
-
-
-
-
- -
-
- -

通配符正则表达式中不能包含问号(?)及问号以后的内容。

-
-
- - -
-
- -
-
-
-
- -
-
` -}) - -Vue.component("size-capacity-view", { - props:["v-default-text", "v-value"], - methods: { - composeCapacity: function (capacity) { - return teaweb.convertSizeCapacityToString(capacity) - } - }, - template: `
- {{composeCapacity(vValue)}} - {{vDefaultText}} -
` -}) - -// 信息提示窗口 -Vue.component("tip-message-box", { - props: ["code"], - mounted: function () { - let that = this - Tea.action("/ui/showTip") - .params({ - code: this.code - }) - .success(function (resp) { - that.visible = resp.data.visible - }) - .post() - }, - data: function () { - return { - visible: false - } - }, - methods: { - close: function () { - this.visible = false - Tea.action("/ui/hideTip") - .params({ - code: this.code - }) - .post() - } - }, - template: `
- - -
- -
-
` -}) - -Vue.component("digit-input", { - props: ["value", "maxlength", "size", "min", "max", "required", "placeholder"], - mounted: function () { - let that = this - setTimeout(function () { - that.check() - }) - }, - data: function () { - let realMaxLength = this.maxlength - if (realMaxLength == null) { - realMaxLength = 20 - } - - let realSize = this.size - if (realSize == null) { - realSize = 6 - } - - return { - realValue: this.value, - realMaxLength: realMaxLength, - realSize: realSize, - isValid: true - } - }, - watch: { - realValue: function (v) { - this.notifyChange() - } - }, - methods: { - notifyChange: function () { - let v = parseInt(this.realValue.toString(), 10) - if (isNaN(v)) { - v = 0 - } - this.check() - this.$emit("input", v) - }, - check: function () { - if (this.realValue == null) { - return - } - let s = this.realValue.toString() - if (!/^\d+$/.test(s)) { - this.isValid = false - return - } - let v = parseInt(s, 10) - if (isNaN(v)) { - this.isValid = false - } else { - if (this.required) { - this.isValid = (this.min == null || this.min <= v) && (this.max == null || this.max >= v) - } else { - this.isValid = (v == 0 || (this.min == null || this.min <= v) && (this.max == null || this.max >= v)) - } - } - } - }, - template: `` -}) - -Vue.component("keyword", { - props: ["v-word"], - data: function () { - let word = this.vWord - if (word == null) { - word = "" - } else { - word = word.replace(/\)/g, "\\)") - word = word.replace(/\(/g, "\\(") - word = word.replace(/\+/g, "\\+") - word = word.replace(/\^/g, "\\^") - word = word.replace(/\$/g, "\\$") - word = word.replace(/\?/g, "\\?") - word = word.replace(/\*/g, "\\*") - word = word.replace(/\[/g, "\\[") - word = word.replace(/{/g, "\\{") - word = word.replace(/\./g, "\\.") - } - - let slot = this.$slots["default"][0] - let text = slot.text - if (word.length > 0) { - let that = this - let m = [] // replacement => tmp - let tmpIndex = 0 - text = text.replaceAll(new RegExp("(" + word + ")", "ig"), function (replacement) { - tmpIndex++ - let s = "" + that.encodeHTML(replacement) + "" - let tmpKey = "$TMP__KEY__" + tmpIndex.toString() + "$" - m.push([tmpKey, s]) - return tmpKey - }) - text = this.encodeHTML(text) - - m.forEach(function (r) { - text = text.replace(r[0], r[1]) - }) - - } else { - text = this.encodeHTML(text) - } - - return { - word: word, - text: text - } - }, - methods: { - encodeHTML: function (s) { - s = s.replace(/&/g, "&") - s = s.replace(//g, ">") - s = s.replace(/"/g, """) - return s - } - }, - template: `` -}) - -Vue.component("bits-var", { - props: ["v-bits"], - data: function () { - let bits = this.vBits - if (typeof bits != "number") { - bits = 0 - } - let format = teaweb.splitFormat(teaweb.formatBits(bits)) - return { - format: format - } - }, - template:` - {{format[0]}}{{format[1]}} -` -}) - -Vue.component("mask-warning", { - template: `为了安全起见,此项数据保存后将不允许在界面查看完整明文,为避免忘记,请自行记录原始数据。` -}) - -Vue.component("chart-columns-grid", { - props: [], - mounted: function () { - this.columns = this.calculateColumns() - - let that = this - window.addEventListener("resize", function () { - that.columns = that.calculateColumns() - }) - }, - updated: function () { - let totalElements = this.$el.getElementsByClassName("column").length - if (totalElements == this.totalElements) { - return - } - this.totalElements = totalElements - this.calculateColumns() - }, - data: function () { - return { - columns: "four", - totalElements: 0 - } - }, - methods: { - calculateColumns: function () { - let w = window.innerWidth - let columns = Math.floor(w / 500) - if (columns == 0) { - columns = 1 - } - - let columnElements = this.$el.getElementsByClassName("column") - if (columnElements.length == 0) { - return "one" - } - let maxColumns = columnElements.length - if (columns > maxColumns) { - columns = maxColumns - } - - // 添加右侧边框 - for (let index = 0; index < columnElements.length; index++) { - let el = columnElements[index] - el.className = el.className.replace("with-border", "") - if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) { - el.className += " with-border" - } - } - - switch (columns) { - case 1: - return "one" - case 2: - return "two" - case 3: - return "three" - case 4: - return "four" - case 5: - return "five" - case 6: - return "six" - case 7: - return "seven" - case 8: - return "eight" - case 9: - return "nine" - case 10: - return "ten" - default: - return "ten" - } - } - }, - template: `
- -
` -}) - -Vue.component("bytes-var", { - props: ["v-bytes"], - data: function () { - let bytes = this.vBytes - if (typeof bytes != "number") { - bytes = 0 - } - let format = teaweb.splitFormat(teaweb.formatBytes(bytes)) - return { - format: format - } - }, - template:` - {{format[0]}}{{format[1]}} -` -}) - -Vue.component("node-log-row", { - props: ["v-log", "v-keyword"], - data: function () { - return { - log: this.vLog, - keyword: this.vKeyword - } - }, - template: `
-
[{{log.createdTime}}][{{log.createdTime}}][{{log.tag}}]{{log.description}}   共{{log.count}}条 {{log.server.name}}
-
` -}) - -Vue.component("provinces-selector", { - props: ["v-provinces"], - data: function () { - let provinces = this.vProvinces - if (provinces == null) { - provinces = [] - } - let provinceIds = provinces.$map(function (k, v) { - return v.id - }) - return { - provinces: provinces, - provinceIds: provinceIds - } - }, - methods: { - add: function () { - let provinceStringIds = this.provinceIds.map(function (v) { - return v.toString() - }) - let that = this - teaweb.popup("/ui/selectProvincesPopup?provinceIds=" + provinceStringIds.join(","), { - width: "48em", - height: "23em", - callback: function (resp) { - that.provinces = resp.data.provinces - that.change() - } - }) - }, - remove: function (index) { - this.provinces.$remove(index) - this.change() - }, - change: function () { - this.provinceIds = this.provinces.$map(function (k, v) { - return v.id - }) - } - }, - template: `
- -
-
{{province.name}}
-
-
-
- -
-
` -}) - -Vue.component("csrf-token", { - created: function () { - this.refreshToken() - }, - mounted: function () { - let that = this - this.$refs.token.form.addEventListener("submit", function () { - that.refreshToken() - }) - - // 自动刷新 - setInterval(function () { - that.refreshToken() - }, 10 * 60 * 1000) - }, - data: function () { - return { - token: "" - } - }, - methods: { - refreshToken: function () { - let that = this - Tea.action("/csrf/token") - .get() - .success(function (resp) { - that.token = resp.data.token - }) - } - }, - template: `` -}) - - -Vue.component("labeled-input", { - props: ["name", "size", "maxlength", "label", "value"], - template: '
\ - \ - {{label}}\ -
' -}); - -let radioId = 0 -Vue.component("radio", { - props: ["name", "value", "v-value", "id"], - data: function () { - radioId++ - let elementId = this.id - if (elementId == null) { - elementId = "radio" + radioId - } - return { - "elementId": elementId - } - }, - methods: { - change: function () { - this.$emit("input", this.vValue) - } - }, - template: `
- - -
` -}) - -Vue.component("copy-to-clipboard", { - props: ["v-target"], - created: function () { - if (typeof ClipboardJS == "undefined") { - let jsFile = document.createElement("script") - jsFile.setAttribute("src", "/js/clipboard.min.js") - document.head.appendChild(jsFile) - } - }, - methods: { - copy: function () { - new ClipboardJS('[data-clipboard-target]'); - teaweb.successToast("已复制到剪切板") - } - }, - template: `` -}) - -// 节点角色名称 -Vue.component("node-role-name", { - props: ["v-role"], - data: function () { - let roleName = "" - switch (this.vRole) { - case "node": - roleName = "边缘节点" - break - case "monitor": - roleName = "监控节点" - break - case "api": - roleName = "API节点" - break - case "user": - roleName = "用户平台" - break - case "admin": - roleName = "管理平台" - break - case "database": - roleName = "数据库节点" - break - case "dns": - roleName = "DNS节点" - break - case "report": - roleName = "区域监控终端" - break - } - return { - roleName: roleName - } - }, - template: `{{roleName}}` -}) - -let sourceCodeBoxIndex = 0 - -Vue.component("source-code-box", { - props: ["name", "type", "id", "read-only", "width", "height", "focus"], - mounted: function () { - let readOnly = this.readOnly - if (typeof readOnly != "boolean") { - readOnly = true - } - let box = document.getElementById("source-code-box-" + this.index) - let valueBox = document.getElementById(this.valueBoxId) - let value = "" - if (valueBox.textContent != null) { - value = valueBox.textContent - } else if (valueBox.innerText != null) { - value = valueBox.innerText - } - - this.createEditor(box, value, readOnly) - }, - data: function () { - let index = sourceCodeBoxIndex++ - - let valueBoxId = 'source-code-box-value-' + sourceCodeBoxIndex - if (this.id != null) { - valueBoxId = this.id - } - - return { - index: index, - valueBoxId: valueBoxId - } - }, - methods: { - createEditor: function (box, value, readOnly) { - let boxEditor = CodeMirror.fromTextArea(box, { - theme: "idea", - lineNumbers: true, - value: "", - readOnly: readOnly, - showCursorWhenSelecting: true, - height: "auto", - //scrollbarStyle: null, - viewportMargin: Infinity, - lineWrapping: true, - highlightFormatting: false, - indentUnit: 4, - indentWithTabs: true, - }) - let that = this - boxEditor.on("change", function () { - that.change(boxEditor.getValue()) - }) - boxEditor.setValue(value) - - if (this.focus) { - boxEditor.focus() - } - - let width = this.width - let height = this.height - if (width != null && height != null) { - width = parseInt(width) - height = parseInt(height) - if (!isNaN(width) && !isNaN(height)) { - if (width <= 0) { - width = box.parentNode.offsetWidth - } - boxEditor.setSize(width, height) - } - } else if (height != null) { - height = parseInt(height) - if (!isNaN(height)) { - boxEditor.setSize("100%", height) - } - } - - let info = CodeMirror.findModeByMIME(this.type) - if (info != null) { - boxEditor.setOption("mode", info.mode) - CodeMirror.modeURL = "/codemirror/mode/%N/%N.js" - CodeMirror.autoLoadMode(boxEditor, info.mode) - } - }, - change: function (code) { - this.$emit("change", code) - } - }, - template: `
-
- -
` -}) - -Vue.component("size-capacity-box", { - props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"], - data: function () { - let v = this.vValue - if (v == null) { - v = { - count: this.vCount, - unit: this.vUnit - } - } - if (typeof (v["count"]) != "number") { - v["count"] = -1 - } - - let vSize = this.size - if (vSize == null) { - vSize = 6 - } - - let vMaxlength = this.maxlength - if (vMaxlength == null) { - vMaxlength = 10 - } - - let supportedUnits = this.vSupportedUnits - if (supportedUnits == null) { - supportedUnits = [] - } - - return { - capacity: v, - countString: (v.count >= 0) ? v.count.toString() : "", - vSize: vSize, - vMaxlength: vMaxlength, - supportedUnits: supportedUnits - } - }, - watch: { - "countString": function (newValue) { - let value = newValue.trim() - if (value.length == 0) { - this.capacity.count = -1 - this.change() - return - } - let count = parseInt(value) - if (!isNaN(count)) { - this.capacity.count = count - } - this.change() - } - }, - methods: { - change: function () { - this.$emit("change", this.capacity) - } - }, - template: `
- -
- -
-
- -
-
` -}) - -/** - * 二级菜单 - */ -Vue.component("inner-menu", { - template: ` -
- -
` -}); - -Vue.component("datepicker", { - props: ["value", "v-name", "name", "v-value", "v-bottom-left", "placeholder"], - mounted: function () { - let that = this - teaweb.datepicker(this.$refs.dayInput, function (v) { - that.day = v - that.change() - }, !!this.vBottomLeft) - }, - data: function () { - let name = this.vName - if (name == null) { - name = this.name - } - if (name == null) { - name = "day" - } - - let day = this.vValue - if (day == null) { - day = this.value - if (day == null) { - day = "" - } - } - - let placeholder = "YYYY-MM-DD" - if (this.placeholder != null) { - placeholder = this.placeholder - } - - return { - realName: name, - realPlaceholder: placeholder, - day: day - } - }, - watch: { - value: function (v) { - this.day = v - - let picker = this.$refs.dayInput.picker - if (picker != null) { - if (v != null && /^\d+-\d+-\d+$/.test(v)) { - picker.setDate(v) - } - } - } - }, - methods: { - change: function () { - this.$emit("input", this.day) // support v-model,事件触发需要在 change 之前 - this.$emit("change", this.day) - } - }, - template: `
- -
` -}) - -// 排序使用的箭头 -Vue.component("sort-arrow", { - props: ["name"], - data: function () { - let url = window.location.toString() - let order = "" - let iconTitle = "" - let newArgs = [] - if (window.location.search != null && window.location.search.length > 0) { - let queryString = window.location.search.substring(1) - let pieces = queryString.split("&") - let that = this - pieces.forEach(function (v) { - let eqIndex = v.indexOf("=") - if (eqIndex > 0) { - let argName = v.substring(0, eqIndex) - let argValue = v.substring(eqIndex + 1) - if (argName == that.name) { - order = argValue - } else if (argName != "page" && argValue != "asc" && argValue != "desc") { - newArgs.push(v) - } - } else { - newArgs.push(v) - } - }) - } - if (order == "asc") { - newArgs.push(this.name + "=desc") - iconTitle = "当前正序排列" - } else if (order == "desc") { - newArgs.push(this.name + "=asc") - iconTitle = "当前倒序排列" - } else { - newArgs.push(this.name + "=desc") - iconTitle = "当前正序排列" - } - - let qIndex = url.indexOf("?") - if (qIndex > 0) { - url = url.substring(0, qIndex) + "?" + newArgs.join("&") - } else { - url = url + "?" + newArgs.join("&") - } - - return { - order: order, - url: url, - iconTitle: iconTitle - } - }, - template: `  ` -}) - Vue.component("user-link", { props: ["v-user", "v-keyword"], data: function () { @@ -20862,2250 +23106,6 @@ Vue.component("user-link", {
` }) -// 监控节点分组选择 -Vue.component("report-node-groups-selector", { - props: ["v-group-ids"], - mounted: function () { - let that = this - Tea.action("/clusters/monitors/groups/options") - .post() - .success(function (resp) { - that.groups = resp.data.groups.map(function (group) { - group.isChecked = that.groupIds.$contains(group.id) - return group - }) - that.isLoaded = true - }) - }, - data: function () { - var groupIds = this.vGroupIds - if (groupIds == null) { - groupIds = [] - } - - return { - groups: [], - groupIds: groupIds, - isLoaded: false, - allGroups: groupIds.length == 0 - } - }, - methods: { - check: function (group) { - group.isChecked = !group.isChecked - this.groupIds = [] - let that = this - this.groups.forEach(function (v) { - if (v.isChecked) { - that.groupIds.push(v.id) - } - }) - this.change() - }, - change: function () { - let that = this - let groups = [] - this.groupIds.forEach(function (groupId) { - let group = that.groups.$find(function (k, v) { - return v.id == groupId - }) - if (group == null) { - return - } - groups.push({ - id: group.id, - name: group.name - }) - }) - this.$emit("change", groups) - } - }, - watch: { - allGroups: function (b) { - if (b) { - this.groupIds = [] - this.groups.forEach(function (v) { - v.isChecked = false - }) - } - - this.change() - } - }, - template: `
- - 还没有分组。 -
-
-
- - -
-
-
-
-
-
- - -
-
-
-
-
` -}) - -Vue.component("finance-user-selector", { - props: ["v-user-id"], - data: function () { - return {} - }, - methods: { - change: function (userId) { - this.$emit("change", userId) - } - }, - template: `
- -
` -}) - -Vue.component("node-cache-disk-dirs-box", { - props: ["value", "name"], - data: function () { - let dirs = this.value - if (dirs == null) { - dirs = [] - } - return { - dirs: dirs, - - isEditing: false, - isAdding: false, - - addingPath: "" - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.addingPath.focus() - }, 100) - }, - confirm: function () { - let addingPath = this.addingPath.trim() - if (addingPath.length == 0) { - let that = this - teaweb.warn("请输入要添加的缓存目录", function () { - that.$refs.addingPath.focus() - }) - return - } - if (addingPath[0] != "/") { - addingPath = "/" + addingPath - } - this.dirs.push({ - path: addingPath - }) - this.cancel() - }, - cancel: function () { - this.addingPath = "" - this.isAdding = false - this.isEditing = false - }, - remove: function (index) { - let that = this - teaweb.confirm("确定要删除此目录吗?", function () { - that.dirs.$remove(index) - }) - } - }, - template: `
- -
- - {{dir.path}}   - -
- - -
-
-
- -
-
- -   -
-
-
- -
- -
-
` -}) - -Vue.component("node-ip-address-clusters-selector", { - props: ["vClusters"], - mounted: function () { - this.checkClusters() - }, - data: function () { - let clusters = this.vClusters - if (clusters == null) { - clusters = [] - } - return { - clusters: clusters, - hasCheckedCluster: false, - clustersVisible: false - } - }, - methods: { - checkClusters: function () { - let that = this - - let b = false - this.clusters.forEach(function (cluster) { - if (cluster.isChecked) { - b = true - } - }) - - this.hasCheckedCluster = b - - return b - }, - changeCluster: function (cluster) { - cluster.isChecked = !cluster.isChecked - this.checkClusters() - }, - showClusters: function () { - this.clustersVisible = !this.clustersVisible - } - }, - template: `
- 默认用于所有集群   修改 -
- {{cluster.name}}   修改 -

当前IP仅在所选集群中有效。

-
-
-
- - {{cluster.name}} - -
-
` -}) - -// 节点登录推荐端口 -Vue.component("node-login-suggest-ports", { - data: function () { - return { - ports: [], - availablePorts: [], - autoSelected: false, - isLoading: false - } - }, - methods: { - reload: function (host) { - let that = this - this.autoSelected = false - this.isLoading = true - Tea.action("/clusters/cluster/suggestLoginPorts") - .params({ - host: host - }) - .success(function (resp) { - if (resp.data.availablePorts != null) { - that.availablePorts = resp.data.availablePorts - if (that.availablePorts.length > 0) { - that.autoSelectPort(that.availablePorts[0]) - that.autoSelected = true - } - } - if (resp.data.ports != null) { - that.ports = resp.data.ports - if (that.ports.length > 0 && !that.autoSelected) { - that.autoSelectPort(that.ports[0]) - that.autoSelected = true - } - } - }) - .done(function () { - that.isLoading = false - }) - .post() - }, - selectPort: function (port) { - this.$emit("select", port) - }, - autoSelectPort: function (port) { - this.$emit("auto-select", port) - } - }, - template: ` - 正在检查端口... - - 可能端口:{{port}} -     - - - 常用端口:{{port}} - - 常用端口有22等。 - (可以点击要使用的端口) -` -}) - -Vue.component("node-group-selector", { - props: ["v-cluster-id", "v-group"], - data: function () { - return { - selectedGroup: this.vGroup - } - }, - methods: { - selectGroup: function () { - let that = this - teaweb.popup("/clusters/cluster/groups/selectPopup?clusterId=" + this.vClusterId, { - callback: function (resp) { - that.selectedGroup = resp.data.group - } - }) - }, - addGroup: function () { - let that = this - teaweb.popup("/clusters/cluster/groups/createPopup?clusterId=" + this.vClusterId, { - callback: function (resp) { - that.selectedGroup = resp.data.group - } - }) - }, - removeGroup: function () { - this.selectedGroup = null - } - }, - template: `
-
- - {{selectedGroup.name}}   -
- -
` -}) - -// 节点IP地址管理(标签形式) -Vue.component("node-ip-addresses-box", { - props: ["v-ip-addresses", "role", "v-node-id"], - data: function () { - let nodeId = this.vNodeId - if (nodeId == null) { - nodeId = 0 - } - - return { - ipAddresses: (this.vIpAddresses == null) ? [] : this.vIpAddresses, - supportThresholds: this.role != "ns", - nodeId: nodeId - } - }, - methods: { - // 添加IP地址 - addIPAddress: function () { - window.UPDATING_NODE_IP_ADDRESS = null - - let that = this; - teaweb.popup("/nodes/ipAddresses/createPopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), { - callback: function (resp) { - that.ipAddresses.push(resp.data.ipAddress); - }, - height: "24em", - width: "44em" - }) - }, - - // 修改地址 - updateIPAddress: function (index, address) { - window.UPDATING_NODE_IP_ADDRESS = teaweb.clone(address) - - let that = this; - teaweb.popup("/nodes/ipAddresses/updatePopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), { - callback: function (resp) { - Vue.set(that.ipAddresses, index, resp.data.ipAddress); - }, - height: "24em", - width: "44em" - }) - }, - - // 删除IP地址 - removeIPAddress: function (index) { - this.ipAddresses.$remove(index); - }, - - // 判断是否为IPv6 - isIPv6: function (ip) { - return ip.indexOf(":") > -1 - } - }, - template: `
- -
-
-
- [IPv6] {{address.ip}} - (备注:{{address.name}},不公开访问 - (不公开访问) - [off] - [down] - [{{address.thresholds.length}}个阈值] -   - -   专属集群:[{{cluster.name}}] -   - - - - -
-
-
-
-
- -
-
` -}) - -Vue.component("node-schedule-conds-box", { - props: ["value", "v-params", "v-operators"], - mounted: function () { - this.formatConds(this.condsConfig.conds) - this.$forceUpdate() - }, - data: function () { - let condsConfig = this.value - if (condsConfig == null) { - condsConfig = { - conds: [], - connector: "and" - } - } - if (condsConfig.conds == null) { - condsConfig.conds = [] - } - - let paramMap = {} - this.vParams.forEach(function (param) { - paramMap[param.code] = param - }) - - let operatorMap = {} - this.vOperators.forEach(function (operator) { - operatorMap[operator.code] = operator.name - }) - - return { - condsConfig: condsConfig, - params: this.vParams, - paramMap: paramMap, - operatorMap: operatorMap, - operator: "", - - isAdding: false, - - paramCode: "", - param: null, - - valueBandwidth: { - count: 100, - unit: "mb" - }, - valueTraffic: { - count: 1, - unit: "gb" - }, - valueCPU: 80, - valueMemory: 90, - valueLoad: 20, - valueRate: 0 - } - }, - watch: { - paramCode: function (code) { - if (code.length == 0) { - this.param = null - } else { - this.param = this.params.$find(function (k, v) { - return v.code == code - }) - } - this.$emit("changeparam", this.param) - } - }, - methods: { - add: function () { - this.isAdding = true - }, - confirm: function () { - if (this.param == null) { - teaweb.warn("请选择参数") - return - } - if (this.param.operators != null && this.param.operators.length > 0 && this.operator.length == 0) { - teaweb.warn("请选择操作符") - return - } - if (this.param.operators == null || this.param.operators.length == 0) { - this.operator = "" - } - - let value = null - switch (this.param.valueType) { - case "bandwidth": { - if (this.valueBandwidth.unit.length == 0) { - teaweb.warn("请选择带宽单位") - return - } - let count = parseInt(this.valueBandwidth.count.toString()) - if (isNaN(count)) { - count = 0 - } - if (count < 0) { - count = 0 - } - value = { - count: count, - unit: this.valueBandwidth.unit - } - } - break - case "traffic": { - if (this.valueTraffic.unit.length == 0) { - teaweb.warn("请选择带宽单位") - return - } - let count = parseInt(this.valueTraffic.count.toString()) - if (isNaN(count)) { - count = 0 - } - if (count < 0) { - count = 0 - } - value = { - count: count, - unit: this.valueTraffic.unit - } - } - break - case "cpu": - let cpu = parseInt(this.valueCPU.toString()) - if (isNaN(cpu)) { - cpu = 0 - } - if (cpu < 0) { - cpu = 0 - } - if (cpu > 100) { - cpu = 100 - } - value = cpu - break - case "memory": - let memory = parseInt(this.valueMemory.toString()) - if (isNaN(memory)) { - memory = 0 - } - if (memory < 0) { - memory = 0 - } - if (memory > 100) { - memory = 100 - } - value = memory - break - case "load": - let load = parseInt(this.valueLoad.toString()) - if (isNaN(load)) { - load = 0 - } - if (load < 0) { - load = 0 - } - value = load - break - case "rate": - let rate = parseInt(this.valueRate.toString()) - if (isNaN(rate)) { - rate = 0 - } - if (rate < 0) { - rate = 0 - } - value = rate - break - } - - this.condsConfig.conds.push({ - param: this.param.code, - operator: this.operator, - value: value - }) - this.formatConds(this.condsConfig.conds) - - this.cancel() - }, - cancel: function () { - this.isAdding = false - this.paramCode = "" - this.param = null - }, - remove: function (index) { - this.condsConfig.conds.$remove(index) - }, - formatConds: function (conds) { - let that = this - conds.forEach(function (cond) { - switch (that.paramMap[cond.param].valueType) { - case "bandwidth": - cond.valueFormat = cond.value.count + cond.value.unit[0].toUpperCase() + cond.value.unit.substring(1) + "ps" - return - case "traffic": - cond.valueFormat = cond.value.count + cond.value.unit.toUpperCase() - return - case "cpu": - cond.valueFormat = cond.value + "%" - return - case "memory": - cond.valueFormat = cond.value + "%" - return - case "load": - cond.valueFormat = cond.value - return - case "rate": - cond.valueFormat = cond.value + "/秒" - return - } - }) - } - }, - template: `
- - - -
- - - {{paramMap[cond.param].name}} - {{operatorMap[cond.operator]}} {{cond.valueFormat}} -   - - -    - -
- -
- - - - - - - - - - - - - - - -
参数 - -

{{param.description}}

-
操作符 - -
{{param.valueName}} - -
-
-
- -
-
- -
-
-
- - -
-
-
- -
-
- -
-
-
- - -
-
- - % -
-
- - -
-
- - % -
-
- - -
- -
- - -
-
- - /秒 -
-
-
-   取消 -
- -
- -
-
` -}) - -Vue.component("node-schedule-action-box", { - props: ["value", "v-actions"], - data: function () { - let actionConfig = this.value - if (actionConfig == null) { - actionConfig = { - code: "", - params: {} - } - } - - return { - actions: this.vActions, - currentAction: null, - actionConfig: actionConfig - } - }, - watch: { - "actionConfig.code": function (actionCode) { - if (actionCode.length == 0) { - this.currentAction = null - } else { - this.currentAction = this.actions.$find(function (k, v) { - return v.code == actionCode - }) - } - this.actionConfig.params = {} - } - }, - template: `
- -
-
- -
-

{{currentAction.description}}

- -
- -

接收通知的URL。

-
-
-
` -}) - -// 节点IP阈值 -Vue.component("node-ip-address-thresholds-view", { - props: ["v-thresholds"], - data: function () { - let thresholds = this.vThresholds - if (thresholds == null) { - thresholds = [] - } else { - thresholds.forEach(function (v) { - if (v.items == null) { - v.items = [] - } - if (v.actions == null) { - v.actions = [] - } - }) - } - - return { - thresholds: thresholds, - allItems: window.IP_ADDR_THRESHOLD_ITEMS, - allOperators: [ - { - "name": "小于等于", - "code": "lte" - }, - { - "name": "大于", - "code": "gt" - }, - { - "name": "不等于", - "code": "neq" - }, - { - "name": "小于", - "code": "lt" - }, - { - "name": "大于等于", - "code": "gte" - } - ], - allActions: window.IP_ADDR_THRESHOLD_ACTIONS - } - }, - methods: { - itemName: function (item) { - let result = "" - this.allItems.forEach(function (v) { - if (v.code == item) { - result = v.name - } - }) - return result - }, - itemUnitName: function (itemCode) { - let result = "" - this.allItems.forEach(function (v) { - if (v.code == itemCode) { - result = v.unit - } - }) - return result - }, - itemDurationUnitName: function (unit) { - switch (unit) { - case "minute": - return "分钟" - case "second": - return "秒" - case "hour": - return "小时" - case "day": - return "天" - } - return unit - }, - itemOperatorName: function (operator) { - let result = "" - this.allOperators.forEach(function (v) { - if (v.code == operator) { - result = v.name - } - }) - return result - }, - actionName: function (actionCode) { - let result = "" - this.allActions.forEach(function (v) { - if (v.code == actionCode) { - result = v.name - } - }) - return result - } - }, - template: `
- -
-
- - - - [{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}] - - {{itemName(item.item)}} - - - - 成功 - 失败 - - - - [{{group.name}}   ] - - [{{itemOperatorName(item.operator)}}] {{item.value}}{{itemUnitName(item.item)}}   - - - AND   - -> - {{actionName(action.action)}} - 到{{action.options.ips.join(", ")}} - ({{action.options.url}}) -   - AND   - -
-
-
` -}) - -// 节点IP阈值 -Vue.component("node-ip-address-thresholds-box", { - props: ["v-thresholds"], - data: function () { - let thresholds = this.vThresholds - if (thresholds == null) { - thresholds = [] - } else { - thresholds.forEach(function (v) { - if (v.items == null) { - v.items = [] - } - if (v.actions == null) { - v.actions = [] - } - }) - } - - return { - editingIndex: -1, - thresholds: thresholds, - addingThreshold: { - items: [], - actions: [] - }, - isAdding: false, - isAddingItem: false, - isAddingAction: false, - - itemCode: "nodeAvgRequests", - itemReportGroups: [], - itemOperator: "lte", - itemValue: "", - itemDuration: "5", - allItems: window.IP_ADDR_THRESHOLD_ITEMS, - allOperators: [ - { - "name": "小于等于", - "code": "lte" - }, - { - "name": "大于", - "code": "gt" - }, - { - "name": "不等于", - "code": "neq" - }, - { - "name": "小于", - "code": "lt" - }, - { - "name": "大于等于", - "code": "gte" - } - ], - allActions: window.IP_ADDR_THRESHOLD_ACTIONS, - - actionCode: "up", - actionBackupIPs: "", - actionWebHookURL: "" - } - }, - methods: { - add: function () { - this.isAdding = !this.isAdding - }, - cancel: function () { - this.isAdding = false - this.editingIndex = -1 - this.addingThreshold = { - items: [], - actions: [] - } - }, - confirm: function () { - if (this.addingThreshold.items.length == 0) { - teaweb.warn("需要至少添加一个阈值") - return - } - if (this.addingThreshold.actions.length == 0) { - teaweb.warn("需要至少添加一个动作") - return - } - - if (this.editingIndex >= 0) { - this.thresholds[this.editingIndex].items = this.addingThreshold.items - this.thresholds[this.editingIndex].actions = this.addingThreshold.actions - } else { - this.thresholds.push({ - items: this.addingThreshold.items, - actions: this.addingThreshold.actions - }) - } - - // 还原 - this.cancel() - }, - remove: function (index) { - this.cancel() - this.thresholds.$remove(index) - }, - update: function (index) { - this.editingIndex = index - this.addingThreshold = { - items: this.thresholds[index].items.$copy(), - actions: this.thresholds[index].actions.$copy() - } - this.isAdding = true - }, - - addItem: function () { - this.isAddingItem = !this.isAddingItem - let that = this - setTimeout(function () { - that.$refs.itemValue.focus() - }, 100) - }, - cancelItem: function () { - this.isAddingItem = false - - this.itemCode = "nodeAvgRequests" - this.itmeOperator = "lte" - this.itemValue = "" - this.itemDuration = "5" - this.itemReportGroups = [] - }, - confirmItem: function () { - // 特殊阈值快速添加 - if (["nodeHealthCheck"].$contains(this.itemCode)) { - if (this.itemValue.toString().length == 0) { - teaweb.warn("请选择检查结果") - return - } - - let value = parseInt(this.itemValue) - if (isNaN(value)) { - value = 0 - } else if (value < 0) { - value = 0 - } else if (value > 1) { - value = 1 - } - - // 添加 - this.addingThreshold.items.push({ - item: this.itemCode, - operator: this.itemOperator, - value: value, - duration: 0, - durationUnit: "minute", - options: {} - }) - this.cancelItem() - return - } - - if (this.itemDuration.length == 0) { - let that = this - teaweb.warn("请输入统计周期", function () { - that.$refs.itemDuration.focus() - }) - return - } - let itemDuration = parseInt(this.itemDuration) - if (isNaN(itemDuration) || itemDuration <= 0) { - teaweb.warn("请输入正确的统计周期", function () { - that.$refs.itemDuration.focus() - }) - return - } - - if (this.itemValue.length == 0) { - let that = this - teaweb.warn("请输入对比值", function () { - that.$refs.itemValue.focus() - }) - return - } - let itemValue = parseFloat(this.itemValue) - if (isNaN(itemValue)) { - teaweb.warn("请输入正确的对比值", function () { - that.$refs.itemValue.focus() - }) - return - } - - - let options = {} - - switch (this.itemCode) { - case "connectivity": // 连通性校验 - if (itemValue > 100) { - let that = this - teaweb.warn("连通性对比值不能超过100", function () { - that.$refs.itemValue.focus() - }) - return - } - - options["groups"] = this.itemReportGroups - break - } - - // 添加 - this.addingThreshold.items.push({ - item: this.itemCode, - operator: this.itemOperator, - value: itemValue, - duration: itemDuration, - durationUnit: "minute", - options: options - }) - - // 还原 - this.cancelItem() - }, - removeItem: function (index) { - this.cancelItem() - this.addingThreshold.items.$remove(index) - }, - changeReportGroups: function (groups) { - this.itemReportGroups = groups - }, - itemName: function (item) { - let result = "" - this.allItems.forEach(function (v) { - if (v.code == item) { - result = v.name - } - }) - return result - }, - itemUnitName: function (itemCode) { - let result = "" - this.allItems.forEach(function (v) { - if (v.code == itemCode) { - result = v.unit - } - }) - return result - }, - itemDurationUnitName: function (unit) { - switch (unit) { - case "minute": - return "分钟" - case "second": - return "秒" - case "hour": - return "小时" - case "day": - return "天" - } - return unit - }, - itemOperatorName: function (operator) { - let result = "" - this.allOperators.forEach(function (v) { - if (v.code == operator) { - result = v.name - } - }) - return result - }, - - addAction: function () { - this.isAddingAction = !this.isAddingAction - }, - cancelAction: function () { - this.isAddingAction = false - this.actionCode = "up" - this.actionBackupIPs = "" - this.actionWebHookURL = "" - }, - confirmAction: function () { - this.doConfirmAction(false) - }, - doConfirmAction: function (validated, options) { - // 是否已存在 - let exists = false - let that = this - this.addingThreshold.actions.forEach(function (v) { - if (v.action == that.actionCode) { - exists = true - } - }) - if (exists) { - teaweb.warn("此动作已经添加过了,无需重复添加") - return - } - - if (options == null) { - options = {} - } - - switch (this.actionCode) { - case "switch": - if (!validated) { - Tea.action("/ui/validateIPs") - .params({ - "ips": this.actionBackupIPs - }) - .success(function (resp) { - if (resp.data.ips.length == 0) { - teaweb.warn("请输入备用IP", function () { - that.$refs.actionBackupIPs.focus() - }) - return - } - options["ips"] = resp.data.ips - that.doConfirmAction(true, options) - }) - .fail(function (resp) { - teaweb.warn("输入的IP '" + resp.data.failIP + "' 格式不正确,请改正后提交", function () { - that.$refs.actionBackupIPs.focus() - }) - }) - .post() - return - } - break - case "webHook": - if (this.actionWebHookURL.length == 0) { - teaweb.warn("请输入WebHook URL", function () { - that.$refs.webHookURL.focus() - }) - return - } - if (!this.actionWebHookURL.match(/^(http|https):\/\//i)) { - teaweb.warn("URL开头必须是http://或者https://", function () { - that.$refs.webHookURL.focus() - }) - return - } - options["url"] = this.actionWebHookURL - } - - this.addingThreshold.actions.push({ - action: this.actionCode, - options: options - }) - - // 还原 - this.cancelAction() - }, - removeAction: function (index) { - this.cancelAction() - this.addingThreshold.actions.$remove(index) - }, - actionName: function (actionCode) { - let result = "" - this.allActions.forEach(function (v) { - if (v.code == actionCode) { - result = v.name - } - }) - return result - } - }, - template: `
- - - -
-
- - - [{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}] - - {{itemName(item.item)}} - - - - 成功 - 失败 - - - - [{{group.name}}   ] - - [{{itemOperatorName(item.operator)}}]  {{item.value}}{{itemUnitName(item.item)}} - -  AND   - - -> - {{actionName(action.action)}} - 到{{action.options.ips.join(", ")}} - ({{action.options.url}}) -  AND   -   - - -
-
- - -
- - - - - - - - - - - -
阈值动作
- -
-
- - [{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}] - - {{itemName(item.item)}} - - - - 成功 - 失败 - - - - [{{group.name}}   ] - [{{itemOperatorName(item.operator)}}] {{item.value}}{{itemUnitName(item.item)}} - -   - -
-
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
统计项目 - -

{{item.description}}

-
统计周期 -
- - 分钟 -
-
操作符 - -
对比值 -
- - {{item.unit}} -
-
检查结果 - -

只有状态发生改变的时候才会触发。

-
终端分组 -
-
-
-   - -
-
-
- -
-
- -
-
- {{actionName(action.action)}}   - 到{{action.options.ips.join(", ")}} - ({{action.options.url}}) - -
-
- - -
- - - - - - - - - - - - - - - - - -
动作类型 - -

{{action.description}}

-
备用IP * - -

每行一个备用IP。

-
URL * - -

完整的URL,比如https://example.com/webhook/api,系统会在触发阈值的时候通过GET调用此URL。

-
-
-   - -
-
- -
- -
-
- - -
-   - -
-
- -
- -
-
` -}) - -Vue.component("node-region-selector", { - props: ["v-region"], - data: function () { - return { - selectedRegion: this.vRegion - } - }, - methods: { - selectRegion: function () { - let that = this - teaweb.popup("/clusters/regions/selectPopup?clusterId=" + this.vClusterId, { - callback: function (resp) { - that.selectedRegion = resp.data.region - } - }) - }, - addRegion: function () { - let that = this - teaweb.popup("/clusters/regions/createPopup?clusterId=" + this.vClusterId, { - callback: function (resp) { - that.selectedRegion = resp.data.region - } - }) - }, - removeRegion: function () { - this.selectedRegion = null - } - }, - template: `
-
- - {{selectedRegion.name}}   -
- -
` -}) - -Vue.component("node-combo-box", { - props: ["v-cluster-id", "v-node-id"], - data: function () { - let that = this - Tea.action("/clusters/nodeOptions") - .params({ - clusterId: this.vClusterId - }) - .post() - .success(function (resp) { - that.nodes = resp.data.nodes - }) - return { - nodes: [] - } - }, - template: `
- -
` -}) - -// 节点级别选择器 -Vue.component("node-level-selector", { - props: ["v-node-level"], - data: function () { - let levelCode = this.vNodeLevel - if (levelCode == null || levelCode < 1) { - levelCode = 1 - } - return { - levels: [ - { - name: "边缘节点", - code: 1, - description: "普通的边缘节点。" - }, - { - name: "L2节点", - code: 2, - description: "特殊的边缘节点,同时负责同组上一级节点的回源。" - } - ], - levelCode: levelCode - } - }, - watch: { - levelCode: function (code) { - this.$emit("change", code) - } - }, - template: `
- -

{{levels[levelCode - 1].description}}

-
` -}) - -Vue.component("node-schedule-conds-viewer", { - props: ["value", "v-params", "v-operators"], - mounted: function () { - this.formatConds(this.condsConfig.conds) - this.$forceUpdate() - }, - data: function () { - let paramMap = {} - this.vParams.forEach(function (param) { - paramMap[param.code] = param - }) - - let operatorMap = {} - this.vOperators.forEach(function (operator) { - operatorMap[operator.code] = operator.name - }) - - return { - condsConfig: this.value, - paramMap: paramMap, - operatorMap: operatorMap - } - }, - methods: { - formatConds: function (conds) { - let that = this - conds.forEach(function (cond) { - switch (that.paramMap[cond.param].valueType) { - case "bandwidth": - cond.valueFormat = cond.value.count + cond.value.unit[0].toUpperCase() + cond.value.unit.substring(1) + "ps" - return - case "traffic": - cond.valueFormat = cond.value.count + cond.value.unit.toUpperCase() - return - case "cpu": - cond.valueFormat = cond.value + "%" - return - case "memory": - cond.valueFormat = cond.value + "%" - return - case "load": - cond.valueFormat = cond.value - return - case "rate": - cond.valueFormat = cond.value + "/秒" - return - } - }) - } - }, - template: `
- - - {{paramMap[cond.param].name}} - {{operatorMap[cond.operator]}} {{cond.valueFormat}} - - -    - -
` -}) - -Vue.component("dns-route-selector", { - props: ["v-all-routes", "v-routes"], - data: function () { - let routes = this.vRoutes - if (routes == null) { - routes = [] - } - routes.$sort(function (v1, v2) { - if (v1.domainId == v2.domainId) { - return v1.code < v2.code - } - return (v1.domainId < v2.domainId) ? 1 : -1 - }) - return { - routes: routes, - routeCodes: routes.$map(function (k, v) { - return v.code + "@" + v.domainId - }), - isAdding: false, - routeCode: "", - keyword: "", - searchingRoutes: this.vAllRoutes.$copy() - } - }, - methods: { - add: function () { - this.isAdding = true - this.keyword = "" - this.routeCode = "" - - let that = this - setTimeout(function () { - that.$refs.keywordRef.focus() - }, 200) - }, - cancel: function () { - this.isAdding = false - }, - confirm: function () { - if (this.routeCode.length == 0) { - return - } - if (this.routeCodes.$contains(this.routeCode)) { - teaweb.warn("已经添加过此线路,不能重复添加") - return - } - let that = this - let route = this.vAllRoutes.$find(function (k, v) { - return v.code + "@" + v.domainId == that.routeCode - }) - if (route == null) { - return - } - - this.routeCodes.push(this.routeCode) - this.routes.push(route) - - this.routes.$sort(function (v1, v2) { - if (v1.domainId == v2.domainId) { - return v1.code < v2.code - } - return (v1.domainId < v2.domainId) ? 1 : -1 - }) - - this.routeCode = "" - this.isAdding = false - }, - remove: function (route) { - this.routeCodes.$removeValue(route.code + "@" + route.domainId) - this.routes.$removeIf(function (k, v) { - return v.code + "@" + v.domainId == route.code + "@" + route.domainId - }) - }, - clearKeyword: function () { - this.keyword = "" - } - }, - watch: { - keyword: function (keyword) { - if (keyword.length == 0) { - this.searchingRoutes = this.vAllRoutes.$copy() - this.routeCode = "" - return - } - this.searchingRoutes = this.vAllRoutes.filter(function (route) { - return teaweb.match(route.name, keyword) || teaweb.match(route.code, keyword) || teaweb.match(route.domainName, keyword) - }) - if (this.searchingRoutes.length > 0) { - this.routeCode = this.searchingRoutes[0].code + "@" + this.searchingRoutes[0].domainId - } else { - this.routeCode = "" - } - } - }, - template: `
- -
- - {{route.name}} ({{route.domainName}}) - -
-
- -
- - - - - - - - - -
所有线路 - 没有和关键词“{{keyword}}”匹配的线路 - - - -
搜索线路 -
- - -
-
- -   -
-
` -}) - -Vue.component("dns-domain-selector", { - props: ["v-domain-id", "v-domain-name", "v-provider-name"], - data: function () { - let domainId = this.vDomainId - if (domainId == null) { - domainId = 0 - } - let domainName = this.vDomainName - if (domainName == null) { - domainName = "" - } - - let providerName = this.vProviderName - if (providerName == null) { - providerName = "" - } - - return { - domainId: domainId, - domainName: domainName, - providerName: providerName - } - }, - methods: { - select: function () { - let that = this - teaweb.popup("/dns/domains/selectPopup", { - callback: function (resp) { - that.domainId = resp.data.domainId - that.domainName = resp.data.domainName - that.providerName = resp.data.providerName - that.change() - } - }) - }, - remove: function() { - this.domainId = 0 - this.domainName = "" - this.change() - }, - update: function () { - let that = this - teaweb.popup("/dns/domains/selectPopup?domainId=" + this.domainId, { - callback: function (resp) { - that.domainId = resp.data.domainId - that.domainName = resp.data.domainName - that.providerName = resp.data.providerName - that.change() - } - }) - }, - change: function () { - this.$emit("change", { - id: this.domainId, - name: this.domainName - }) - } - }, - template: `
- -
- - {{providerName}} » {{domainName}} - - - -
- -
` -}) - -Vue.component("dns-resolver-config-box", { - props:["v-dns-resolver-config"], - data: function () { - let config = this.vDnsResolverConfig - if (config == null) { - config = { - type: "default" - } - } - return { - config: config, - types: [ - { - name: "默认", - code: "default" - }, - { - name: "CGO", - code: "cgo" - }, - { - name: "Go原生", - code: "goNative" - }, - ] - } - }, - template: `
- - - - - - -
使用的DNS解析库 - -

边缘节点使用的DNS解析库。修改此项配置后,需要重启节点进程才会生效。

-
-
-
` -}) - -Vue.component("dns-resolvers-config-box", { - props: ["value", "name"], - data: function () { - let resolvers = this.value - if (resolvers == null) { - resolvers = [] - } - - let name = this.name - if (name == null || name.length == 0) { - name = "dnsResolversJSON" - } - - return { - formName: name, - resolvers: resolvers, - - host: "", - - isAdding: false - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.hostRef.focus() - }) - }, - confirm: function () { - let host = this.host.trim() - if (host.length == 0) { - let that = this - setTimeout(function () { - that.$refs.hostRef.focus() - }) - return - } - this.resolvers.push({ - host: host, - port: 0, // TODO - protocol: "" // TODO - }) - this.cancel() - }, - cancel: function () { - this.isAdding = false - this.host = "" - this.port = 0 - this.protocol = "" - }, - remove: function (index) { - this.resolvers.$remove(index) - } - }, - template: `
- -
-
- {{resolver.protocol}}{{resolver.host}}:{{resolver.port}} -   - -
-
- -
-
-
- -
-
- -   -
-
-
- -
- -
-
` -}) - -Vue.component("ad-instance-objects-box", { - props: ["v-objects", "v-user-id"], - mounted: function () { - this.getUserServers(1) - }, - data: function () { - let objects = this.vObjects - if (objects == null) { - objects = [] - } - - let objectCodes = [] - objects.forEach(function (v) { - objectCodes.push(v.code) - }) - - return { - userId: this.vUserId, - objects: objects, - objectCodes: objectCodes, - isAdding: true, - - servers: [], - serversIsLoading: false - } - }, - methods: { - add: function () { - this.isAdding = true - }, - cancel: function () { - this.isAdding = false - }, - remove: function (index) { - let that = this - teaweb.confirm("确定要删除此防护对象吗?", function () { - that.objects.$remove(index) - that.notifyChange() - }) - }, - removeObjectCode: function (objectCode) { - let index = -1 - this.objectCodes.forEach(function (v, k) { - if (objectCode == v) { - index = k - } - }) - if (index >= 0) { - this.objects.$remove(index) - this.notifyChange() - } - }, - getUserServers: function (page) { - if (Tea.Vue == null) { - let that = this - setTimeout(function () { - that.getUserServers(page) - }, 100) - return - } - - let that = this - this.serversIsLoading = true - Tea.Vue.$post(".userServers") - .params({ - userId: this.userId, - page: page, - pageSize: 5 - }) - .success(function (resp) { - that.servers = resp.data.servers - - that.$refs.serverPage.updateMax(resp.data.page.max) - that.serversIsLoading = false - }) - .error(function () { - that.serversIsLoading = false - }) - }, - changeServerPage: function (page) { - this.getUserServers(page) - }, - selectServerObject: function (server) { - if (this.existObjectCode("server:" + server.id)) { - return - } - - this.objects.push({ - "type": "server", - "code": "server:" + server.id, - - "id": server.id, - "name": server.name - }) - this.notifyChange() - }, - notifyChange: function () { - let objectCodes = [] - this.objects.forEach(function (v) { - objectCodes.push(v.code) - }) - this.objectCodes = objectCodes - }, - existObjectCode: function (objectCode) { - let found = false - this.objects.forEach(function (v) { - if (v.code == objectCode) { - found = true - } - }) - return found - } - }, - template: `
- - - -
-
暂时还没有设置任何防护对象。
-
- - - - - -
已选中防护对象 -
- 网站:{{object.name}} -   -
-
-
-
-
- - -
- - - - - - - - - - -
对象类型网站
网站列表 - 加载中... -
暂时还没有可选的网站。
- - - - - - - - - - - -
网站名称操作
{{server.name}} - 选中 - 取消 -
- - -
-
- - -
- -
-
` -}) - -Vue.component("grant-selector", { - props: ["v-grant", "v-node-cluster-id", "v-ns-cluster-id"], - data: function () { - return { - grantId: (this.vGrant == null) ? 0 : this.vGrant.id, - grant: this.vGrant, - nodeClusterId: (this.vNodeClusterId != null) ? this.vNodeClusterId : 0, - nsClusterId: (this.vNsClusterId != null) ? this.vNsClusterId : 0 - } - }, - methods: { - // 选择授权 - select: function () { - let that = this; - teaweb.popup("/clusters/grants/selectPopup?nodeClusterId=" + this.nodeClusterId + "&nsClusterId=" + this.nsClusterId, { - callback: (resp) => { - that.grantId = resp.data.grant.id; - if (that.grantId > 0) { - that.grant = resp.data.grant; - } - that.notifyUpdate() - }, - height: "26em" - }) - }, - - // 创建授权 - create: function () { - let that = this - teaweb.popup("/clusters/grants/createPopup", { - height: "26em", - callback: (resp) => { - that.grantId = resp.data.grant.id; - if (that.grantId > 0) { - that.grant = resp.data.grant; - } - that.notifyUpdate() - } - }) - }, - - // 修改授权 - update: function () { - if (this.grant == null) { - window.location.reload(); - return; - } - let that = this - teaweb.popup("/clusters/grants/updatePopup?grantId=" + this.grant.id, { - height: "26em", - callback: (resp) => { - that.grant = resp.data.grant - that.notifyUpdate() - } - }) - }, - - // 删除已选择授权 - remove: function () { - this.grant = null - this.grantId = 0 - this.notifyUpdate() - }, - notifyUpdate: function () { - this.$emit("change", this.grant) - } - }, - template: `
- -
{{grant.name}}({{grant.methodName}})({{grant.username}})
- -
` -}) - window.REQUEST_COND_COMPONENTS = [{"type":"url-extension","name":"文件扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-eq-index","name":"首页","description":"检查URL路径是为\"/\"","component":"http-cond-url-eq-index","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":false},{"type":"url-all","name":"全站","description":"全站所有URL","component":"http-cond-url-all","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":false},{"type":"url-prefix","name":"URL目录前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-prefix","paramsTitle":"URL目录前缀","isRequest":true,"caseInsensitive":true},{"type":"url-eq","name":"URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-regexp","name":"URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致","component":"http-cond-url-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"url-wildcard-match","name":"URL通配符","description":"使用通配符检查URL中的文件路径是否一致","component":"http-cond-url-wildcard-match","paramsTitle":"通配符","isRequest":true,"caseInsensitive":true},{"type":"user-agent-regexp","name":"User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识","component":"http-cond-user-agent-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"params","name":"参数匹配","description":"根据参数值进行匹配","component":"http-cond-params","paramsTitle":"参数配置","isRequest":true,"caseInsensitive":false},{"type":"url-not-extension","name":"排除:URL扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-not-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-not-prefix","name":"排除:URL前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-not-prefix","paramsTitle":"URL前缀","isRequest":true,"caseInsensitive":true},{"type":"url-not-eq","name":"排除:URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-not-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-not-regexp","name":"排除:URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致,如果一致,则不匹配","component":"http-cond-url-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"user-agent-not-regexp","name":"排除:User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识,如果含有,则不匹配","component":"http-cond-user-agent-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"mime-type","name":"内容MimeType","description":"根据服务器返回的内容的MimeType进行过滤。注意:当用于缓存条件时,此条件需要结合别的请求条件使用。","component":"http-cond-mime-type","paramsTitle":"MimeType列表","isRequest":false,"caseInsensitive":false}]; window.REQUEST_COND_OPERATORS = [{"description":"判断是否正则表达式匹配","name":"正则表达式匹配","op":"regexp"},{"description":"判断是否正则表达式不匹配","name":"正则表达式不匹配","op":"not regexp"},{"description":"判断是否和指定的通配符匹配","name":"通配符匹配","op":"wildcard match"},{"description":"判断是否和指定的通配符不匹配","name":"通配符不匹配","op":"wildcard not match"},{"description":"使用字符串对比参数值是否相等于某个值","name":"字符串等于","op":"eq"},{"description":"参数值包含某个前缀","name":"字符串前缀","op":"prefix"},{"description":"参数值包含某个后缀","name":"字符串后缀","op":"suffix"},{"description":"参数值包含另外一个字符串","name":"字符串包含","op":"contains"},{"description":"参数值不包含另外一个字符串","name":"字符串不包含","op":"not contains"},{"description":"使用字符串对比参数值是否不相等于某个值","name":"字符串不等于","op":"not"},{"description":"判断参数值在某个列表中","name":"在列表中","op":"in"},{"description":"判断参数值不在某个列表中","name":"不在列表中","op":"not in"},{"description":"判断小写的扩展名(不带点)在某个列表中","name":"扩展名","op":"file ext"},{"description":"判断MimeType在某个列表中,支持类似于image/*的语法","name":"MimeType","op":"mime type"},{"description":"判断版本号在某个范围内,格式为version1,version2","name":"版本号范围","op":"version range"},{"description":"将参数转换为整数数字后进行对比","name":"整数等于","op":"eq int"},{"description":"将参数转换为可以有小数的浮点数字进行对比","name":"浮点数等于","op":"eq float"},{"description":"将参数转换为数字进行对比","name":"数字大于","op":"gt"},{"description":"将参数转换为数字进行对比","name":"数字大于等于","op":"gte"},{"description":"将参数转换为数字进行对比","name":"数字小于","op":"lt"},{"description":"将参数转换为数字进行对比","name":"数字小于等于","op":"lte"},{"description":"对整数参数值取模,除数为10,对比值为余数","name":"整数取模10","op":"mod 10"},{"description":"对整数参数值取模,除数为100,对比值为余数","name":"整数取模100","op":"mod 100"},{"description":"对整数参数值取模,对比值格式为:除数,余数,比如10,1","name":"整数取模","op":"mod"},{"description":"将参数转换为IP进行对比","name":"IP等于","op":"eq ip"},{"description":"将参数转换为IP进行对比","name":"IP大于","op":"gt ip"},{"description":"将参数转换为IP进行对比","name":"IP大于等于","op":"gte ip"},{"description":"将参数转换为IP进行对比","name":"IP小于","op":"lt ip"},{"description":"将参数转换为IP进行对比","name":"IP小于等于","op":"lte ip"},{"description":"IP在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e,或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e","name":"IP范围","op":"ip range"},{"description":"对IP参数值取模,除数为10,对比值为余数","name":"IP取模10","op":"ip mod 10"},{"description":"对IP参数值取模,除数为100,对比值为余数","name":"IP取模100","op":"ip mod 100"},{"description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1","name":"IP取模","op":"ip mod"}]; @@ -23118,7 +23118,7 @@ window.IP_ADDR_THRESHOLD_ITEMS = [{"code":"nodeAvgRequests","description":"当 window.IP_ADDR_THRESHOLD_ACTIONS = [{"code":"up","description":"上线当前IP。","name":"上线"},{"code":"down","description":"下线当前IP。","name":"下线"},{"code":"notify","description":"发送已达到阈值通知。","name":"通知"},{"code":"switch","description":"在DNS中记录中将IP切换到指定的备用IP。","name":"切换"},{"code":"webHook","description":"调用外部的WebHook。","name":"WebHook"}]; -window.WAF_RULE_CHECKPOINTS = [{"description":"通用报头比如Cache-Control、Accept之类的长度限制,防止缓冲区溢出攻击。","name":"通用请求报头长度限制","prefix":"requestGeneralHeaderLength"},{"description":"通用报头比如Cache-Control、Date之类的长度限制,防止缓冲区溢出攻击。","name":"通用响应报头长度限制","prefix":"responseGeneralHeaderLength"},{"description":"试图通过分析X-Forwarded-For等报头获取的客户端地址,比如192.168.1.100,存在伪造的可能。","name":"客户端地址(IP)","prefix":"remoteAddr"},{"description":"直接连接的客户端地址,比如192.168.1.100。","name":"客户端源地址(IP)","prefix":"rawRemoteAddr"},{"description":"直接连接的客户端地址端口。","name":"客户端端口","prefix":"remotePort"},{"description":"通过BasicAuth登录的客户端用户名。","name":"客户端用户名","prefix":"remoteUser"},{"description":"包含URL参数的请求URI,类似于 /hello/world?lang=go,不包含域名部分。","name":"请求URI","prefix":"requestURI"},{"description":"不包含URL参数的请求路径,类似于 /hello/world,不包含域名部分。","name":"请求路径","prefix":"requestPath"},{"description":"完整的请求URL,包含协议、域名、请求路径、参数等,类似于 https://example.com/hello?name=lily 。","name":"请求完整URL","prefix":"requestURL"},{"description":"请求报头中的Content-Length。","name":"请求内容长度","prefix":"requestLength"},{"description":"通常在POST或者PUT等操作时会附带请求体,最大限制32M。","name":"请求体内容","prefix":"requestBody"},{"description":"${requestURI}和${requestBody}组合。","name":"请求URI和请求体组合","prefix":"requestAll"},{"description":"获取POST或者其他方法发送的表单参数,最大请求体限制32M。","name":"请求表单参数","prefix":"requestForm"},{"description":"获取POST上传的文件信息,最大请求体限制32M。","name":"上传文件","prefix":"requestUpload"},{"description":"获取POST或者其他方法发送的JSON,最大请求体限制32M,使用点(.)符号表示多级数据。","name":"请求JSON参数","prefix":"requestJSON"},{"description":"比如GET、POST。","name":"请求方法","prefix":"requestMethod"},{"description":"比如http或https。","name":"请求协议","prefix":"scheme"},{"description":"比如HTTP/1.1。","name":"HTTP协议版本","prefix":"proto"},{"description":"比如example.com。","name":"主机名","prefix":"host"},{"description":"当前网站服务CNAME,比如38b48e4f.example.com。","name":"CNAME","prefix":"cname"},{"description":"是否为CNAME,值为1(是)或0(否)。","name":"是否为CNAME","prefix":"isCNAME"},{"description":"请求报头中的Referer和Origin值。","name":"请求来源","prefix":"refererOrigin"},{"description":"请求报头中的Referer值。","name":"请求来源Referer","prefix":"referer"},{"description":"比如Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103。","name":"客户端信息","prefix":"userAgent"},{"description":"请求报头的Content-Type。","name":"内容类型","prefix":"contentType"},{"description":"比如sid=IxZVPFhE\u0026city=beijing\u0026uid=18237。","name":"所有cookie组合字符串","prefix":"cookies"},{"description":"单个cookie值。","name":"单个cookie值","prefix":"cookie"},{"description":"比如name=lu\u0026age=20。","name":"所有URL参数组合","prefix":"args"},{"description":"单个URL参数值。","name":"单个URL参数值","prefix":"arg"},{"description":"使用换行符(\\n)隔开的报头内容字符串,每行均为\"NAME: VALUE格式\"。","name":"所有请求报头内容","prefix":"headers"},{"description":"使用换行符(\\n)隔开的报头名称字符串,每行一个名称。","name":"所有请求报头名称","prefix":"headerNames"},{"description":"单个报头值。","name":"单个请求报头值","prefix":"header"},{"description":"最长的请求报头的长度。","name":"请求报头最大长度","prefix":"headerMaxLength"},{"description":"当前客户端所处国家/地区名称。","name":"国家/地区名称","prefix":"geoCountryName"},{"description":"当前客户端所处中国省份名称。","name":"省份名称","prefix":"geoProvinceName"},{"description":"当前客户端所处中国城市名称。","name":"城市名称","prefix":"geoCityName"},{"description":"当前客户端所处ISP名称。","name":"ISP名称","prefix":"ispName"},{"description":"对统计对象进行统计。","name":"CC统计","prefix":"cc2"},{"description":"对统计对象进行统计。","name":"防盗链","prefix":"refererBlock"},{"description":"统计某段时间段内的请求信息(不推荐再使用,请使用新的CC2统计代替)。","name":"CC统计(旧)","prefix":"cc"},{"description":"响应状态码,比如200、404、500。","name":"响应状态码","prefix":"status"},{"description":"响应报头值。","name":"响应报头","prefix":"responseHeader"},{"description":"响应内容字符串。","name":"响应内容","prefix":"responseBody"},{"description":"响应内容长度,通过响应的报头Content-Length获取。","name":"响应内容长度","prefix":"bytesSent"}]; +window.WAF_RULE_CHECKPOINTS = [{"description":"通用报头比如Cache-Control、Accept之类的长度限制,防止缓冲区溢出攻击。","name":"通用请求报头长度限制","prefix":"requestGeneralHeaderLength"},{"description":"通用报头比如Cache-Control、Date之类的长度限制,防止缓冲区溢出攻击。","name":"通用响应报头长度限制","prefix":"responseGeneralHeaderLength"},{"description":"试图通过分析X-Forwarded-For等报头获取的客户端地址,比如192.168.1.100,存在伪造的可能。","name":"客户端地址(IP)","prefix":"remoteAddr"},{"description":"直接连接的客户端地址,比如192.168.1.100。","name":"客户端源地址(IP)","prefix":"rawRemoteAddr"},{"description":"直接连接的客户端地址端口。","name":"客户端端口","prefix":"remotePort"},{"description":"通过BasicAuth登录的客户端用户名。","name":"客户端用户名","prefix":"remoteUser"},{"description":"包含URL参数的请求URI,类似于 /hello/world?lang=go,不包含域名部分。","name":"请求URI","prefix":"requestURI"},{"description":"不包含URL参数的请求路径,类似于 /hello/world,不包含域名部分。","name":"请求路径","prefix":"requestPath"},{"description":"完整的请求URL,包含协议、域名、请求路径、参数等,类似于 https://example.com/hello?name=lily 。","name":"请求完整URL","prefix":"requestURL"},{"description":"请求报头中的Content-Length。","name":"请求内容长度","prefix":"requestLength"},{"description":"通常在POST或者PUT等操作时会附带请求体,最大限制32M。","name":"请求体内容","prefix":"requestBody"},{"description":"${requestURI}和${requestBody}组合。","name":"请求URI和请求体组合","prefix":"requestAll"},{"description":"获取POST或者其他方法发送的表单参数,最大请求体限制32M。","name":"请求表单参数","prefix":"requestForm"},{"description":"获取POST上传的文件信息,最大请求体限制32M。","name":"上传文件","prefix":"requestUpload"},{"description":"获取POST或者其他方法发送的JSON,最大请求体限制32M,使用点(.)符号表示多级数据。","name":"请求JSON参数","prefix":"requestJSON"},{"description":"比如GET、POST。","name":"请求方法","prefix":"requestMethod"},{"description":"比如http或https。","name":"请求协议","prefix":"scheme"},{"description":"比如HTTP/1.1。","name":"HTTP协议版本","prefix":"proto"},{"description":"比如example.com。","name":"主机名","prefix":"host"},{"description":"当前网站服务CNAME,比如38b48e4f.example.com。","name":"CNAME","prefix":"cname"},{"description":"是否为CNAME,值为1(是)或0(否)。","name":"是否为CNAME","prefix":"isCNAME"},{"description":"请求报头中的Referer和Origin值。","name":"请求来源","prefix":"refererOrigin"},{"description":"请求报头中的Referer值。","name":"请求来源Referer","prefix":"referer"},{"description":"比如Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103。","name":"客户端信息","prefix":"userAgent"},{"description":"请求报头的Content-Type。","name":"内容类型","prefix":"contentType"},{"description":"比如sid=IxZVPFhE\u0026city=beijing\u0026uid=18237。","name":"所有cookie组合字符串","prefix":"cookies"},{"description":"单个cookie值。","name":"单个cookie值","prefix":"cookie"},{"description":"比如name=lu\u0026age=20。","name":"所有URL参数组合","prefix":"args"},{"description":"单个URL参数值。","name":"单个URL参数值","prefix":"arg"},{"description":"使用换行符(\\n)隔开的报头内容字符串,每行均为\"NAME: VALUE格式\"。","name":"所有请求报头内容","prefix":"headers"},{"description":"使用换行符(\\n)隔开的报头名称字符串,每行一个名称。","name":"所有请求报头名称","prefix":"headerNames"},{"description":"单个报头值。","name":"单个请求报头值","prefix":"header"},{"description":"最长的请求报头的长度。","name":"请求报头最大长度","prefix":"headerMaxLength"},{"description":"当前客户端所处国家/地区名称。","name":"国家/地区名称","prefix":"geoCountryName"},{"description":"当前客户端所处中国省份名称。","name":"省份名称","prefix":"geoProvinceName"},{"description":"当前客户端所处中国城市名称。","name":"城市名称","prefix":"geoCityName"},{"description":"当前客户端所处ISP名称。","name":"ISP名称","prefix":"ispName"},{"description":"对统计对象进行统计。","name":"CC统计","prefix":"cc2"},{"description":"对统计对象进行统计。","name":"防盗链","prefix":"refererBlock"},{"description":"响应状态码,比如200、404、500。","name":"响应状态码","prefix":"status"},{"description":"响应报头值。","name":"响应报头","prefix":"responseHeader"},{"description":"响应内容字符串。","name":"响应内容","prefix":"responseBody"},{"description":"响应内容长度,通过响应的报头Content-Length获取。","name":"响应内容长度","prefix":"bytesSent"}]; window.WAF_RULE_OPERATORS = [{"name":"正则匹配","code":"match","description":"使用正则表达式匹配,在头部使用(?i)表示不区分大小写,\u003ca href=\"https://goedge.cn/docs/Appendix/Regexp/Index.md\" target=\"_blank\"\u003e正则表达式语法 \u0026raquo;\u003c/a\u003e。","caseInsensitive":"yes","dataType":"regexp"},{"name":"正则不匹配","code":"not match","description":"使用正则表达式不匹配,在头部使用(?i)表示不区分大小写,\u003ca href=\"https://goedge.cn/docs/Appendix/Regexp/Index.md\" target=\"_blank\"\u003e正则表达式语法 \u0026raquo;\u003c/a\u003e。","caseInsensitive":"yes","dataType":"regexp"},{"name":"通配符匹配","code":"wildcard match","description":"判断是否和指定的通配符匹配,可以在对比值中使用星号通配符(*)表示任意字符。","caseInsensitive":"yes","dataType":"wildcard"},{"name":"通配符不匹配","code":"wildcard not match","description":"判断是否和指定的通配符不匹配,可以在对比值中使用星号通配符(*)表示任意字符。","caseInsensitive":"yes","dataType":"wildcard"},{"name":"字符串等于","code":"eq string","description":"使用字符串对比等于。","caseInsensitive":"no","dataType":"string"},{"name":"字符串不等于","code":"neq string","description":"使用字符串对比不等于。","caseInsensitive":"no","dataType":"string"},{"name":"包含字符串","code":"contains","description":"包含某个字符串,比如Hello World包含了World。","caseInsensitive":"no","dataType":"string"},{"name":"不包含字符串","code":"not contains","description":"不包含某个字符串,比如Hello字符串中不包含Hi。","caseInsensitive":"no","dataType":"string"},{"name":"包含任一字符串","code":"contains any","description":"包含字符串列表中的任意一个,比如/hello/world包含/hello和/hi中的/hello,对比值中每行一个字符串。","caseInsensitive":"no","dataType":"strings"},{"name":"包含所有字符串","code":"contains all","description":"包含字符串列表中的所有字符串,比如/hello/world必须包含/hello和/world,对比值中每行一个字符串。","caseInsensitive":"no","dataType":"strings"},{"name":"包含前缀","code":"prefix","description":"包含字符串前缀部分,比如/hello前缀会匹配/hello, /hello/world等。","caseInsensitive":"no","dataType":"string"},{"name":"包含后缀","code":"suffix","description":"包含字符串后缀部分,比如/hello后缀会匹配/hello, /hi/hello等。","caseInsensitive":"no","dataType":"string"},{"name":"包含任一单词","code":"contains any word","description":"包含某个独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"包含所有单词","code":"contains all words","description":"包含所有的独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"不包含任一单词","code":"not contains any word","description":"不包含某个独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"包含SQL注入","code":"contains sql injection","description":"检测字符串内容是否包含SQL注入。","caseInsensitive":"none","dataType":"none"},{"name":"包含SQL注入-严格模式","code":"contains sql injection strictly","description":"更加严格地检测字符串内容是否包含SQL注入,相对于非严格模式,有一定的误报几率。","caseInsensitive":"none","dataType":"none"},{"name":"包含XSS注入","code":"contains xss","description":"检测字符串内容是否包含XSS注入。","caseInsensitive":"none","dataType":"none"},{"name":"包含XSS注入-严格模式","code":"contains xss strictly","description":"更加严格地检测字符串内容是否包含XSS注入,相对于非严格模式,此时xml、audio、video等标签也会被匹配。","caseInsensitive":"none","dataType":"none"},{"name":"包含二进制数据","code":"contains binary","description":"包含一组二进制数据。","caseInsensitive":"no","dataType":"string"},{"name":"不包含二进制数据","code":"not contains binary","description":"不包含一组二进制数据。","caseInsensitive":"no","dataType":"string"},{"name":"数值大于","code":"gt","description":"使用数值对比大于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值大于等于","code":"gte","description":"使用数值对比大于等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值小于","code":"lt","description":"使用数值对比小于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值小于等于","code":"lte","description":"使用数值对比小于等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值等于","code":"eq","description":"使用数值对比等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值不等于","code":"neq","description":"使用数值对比不等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"包含索引","code":"has key","description":"对于一组数据拥有某个键值或者索引。","caseInsensitive":"no","dataType":"string|number"},{"name":"版本号大于","code":"version gt","description":"对比版本号大于。","caseInsensitive":"none","dataType":"version"},{"name":"版本号小于","code":"version lt","description":"对比版本号小于。","caseInsensitive":"none","dataType":"version"},{"name":"版本号范围","code":"version range","description":"判断版本号在某个范围内,格式为 起始version1,结束version2。","caseInsensitive":"none","dataType":"versionRange"},{"name":"IP等于","code":"eq ip","description":"将参数转换为IP进行对比,只能对比单个IP。","caseInsensitive":"none","dataType":"ip"},{"name":"在一组IP中","code":"in ip list","description":"判断参数IP在一组IP内,对比值中每行一个IP。","caseInsensitive":"none","dataType":"ips"},{"name":"IP大于","code":"gt ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP大于等于","code":"gte ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP小于","code":"lt ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP小于等于","code":"lte ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP范围","code":"ip range","description":"IP在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e;或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e;或者单个IP。可以填写多行,每行一个IP范围。","caseInsensitive":"none","dataType":"ips"},{"name":"不在IP范围","code":"not ip range","description":"IP不在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e;或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e;或者单个IP。可以填写多行,每行一个IP范围。","caseInsensitive":"none","dataType":"ips"},{"name":"IP取模10","code":"ip mod 10","description":"对IP参数值取模,除数为10,对比值为余数。","caseInsensitive":"none","dataType":"number"},{"name":"IP取模100","code":"ip mod 100","description":"对IP参数值取模,除数为100,对比值为余数。","caseInsensitive":"none","dataType":"number"},{"name":"IP取模","code":"ip mod","description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1。","caseInsensitive":"none","dataType":"number"}]; diff --git a/EdgeNode/go.tar.gz b/EdgeNode/go.tar.gz new file mode 100644 index 0000000..4a4ae60 Binary files /dev/null and b/EdgeNode/go.tar.gz differ diff --git a/EdgeNode/internal/utils/net_unix.go b/EdgeNode/internal/utils/net_unix.go index 16dd53b..74816f7 100644 --- a/EdgeNode/internal/utils/net_unix.go +++ b/EdgeNode/internal/utils/net_unix.go @@ -15,7 +15,7 @@ func ListenReuseAddr(network string, addr string) (net.Listener, error) { config := &net.ListenConfig{ Control: func(network, address string, c syscall.RawConn) error { return c.Control(func(fd uintptr) { - err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1) + err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, 15, 1) // 15 = SO_REUSEPORT on Linux if err != nil { logs.Println("[LISTEN]" + err.Error()) } diff --git a/EdgeNode/internal/waf/injectionutils/utils_sqli.go b/EdgeNode/internal/waf/injectionutils/utils_sqli.go index ebacd4e..8d7e63b 100644 --- a/EdgeNode/internal/waf/injectionutils/utils_sqli.go +++ b/EdgeNode/internal/waf/injectionutils/utils_sqli.go @@ -1,3 +1,6 @@ +//go:build cgo +// +build cgo + // Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . package injectionutils @@ -8,8 +11,6 @@ package injectionutils #include #include */ -//go:build cgo - import "C" import ( "github.com/TeaOSLab/EdgeNode/internal/utils/fasttime" diff --git a/EdgeNode/internal/waf/injectionutils/utils_xss.go b/EdgeNode/internal/waf/injectionutils/utils_xss.go index 3d3d0c2..6206a20 100644 --- a/EdgeNode/internal/waf/injectionutils/utils_xss.go +++ b/EdgeNode/internal/waf/injectionutils/utils_xss.go @@ -1,3 +1,6 @@ +//go:build cgo +// +build cgo + // Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . package injectionutils @@ -8,8 +11,6 @@ package injectionutils #include #include */ -//go:build cgo - import "C" import ( "github.com/TeaOSLab/EdgeNode/internal/utils/fasttime" diff --git a/EdgeUser/web/public/js/components.js b/EdgeUser/web/public/js/components.js index 41047f1..78a3b17 100644 --- a/EdgeUser/web/public/js/components.js +++ b/EdgeUser/web/public/js/components.js @@ -1,3 +1,3241 @@ +Vue.component("ad-instance-objects-box", { + props: ["v-objects", "v-user-id"], + mounted: function () { + this.getUserServers(1) + }, + data: function () { + let objects = this.vObjects + if (objects == null) { + objects = [] + } + + let objectCodes = [] + objects.forEach(function (v) { + objectCodes.push(v.code) + }) + + return { + userId: this.vUserId, + objects: objects, + objectCodes: objectCodes, + isAdding: true, + + servers: [], + serversIsLoading: false + } + }, + methods: { + add: function () { + this.isAdding = true + }, + cancel: function () { + this.isAdding = false + }, + remove: function (index) { + let that = this + teaweb.confirm("确定要删除此防护对象吗?", function () { + that.objects.$remove(index) + that.notifyChange() + }) + }, + removeObjectCode: function (objectCode) { + let index = -1 + this.objectCodes.forEach(function (v, k) { + if (objectCode == v) { + index = k + } + }) + if (index >= 0) { + this.objects.$remove(index) + this.notifyChange() + } + }, + getUserServers: function (page) { + if (Tea.Vue == null) { + let that = this + setTimeout(function () { + that.getUserServers(page) + }, 100) + return + } + + let that = this + this.serversIsLoading = true + Tea.Vue.$post(".userServers") + .params({ + userId: this.userId, + page: page, + pageSize: 5 + }) + .success(function (resp) { + that.servers = resp.data.servers + + that.$refs.serverPage.updateMax(resp.data.page.max) + that.serversIsLoading = false + }) + .error(function () { + that.serversIsLoading = false + }) + }, + changeServerPage: function (page) { + this.getUserServers(page) + }, + selectServerObject: function (server) { + if (this.existObjectCode("server:" + server.id)) { + return + } + + this.objects.push({ + "type": "server", + "code": "server:" + server.id, + + "id": server.id, + "name": server.name + }) + this.notifyChange() + }, + notifyChange: function () { + let objectCodes = [] + this.objects.forEach(function (v) { + objectCodes.push(v.code) + }) + this.objectCodes = objectCodes + }, + existObjectCode: function (objectCode) { + let found = false + this.objects.forEach(function (v) { + if (v.code == objectCode) { + found = true + } + }) + return found + } + }, + template: `
+ + + +
+
暂时还没有设置任何防护对象。
+
+ + + + + +
已选中防护对象 +
+ 网站:{{object.name}} +   +
+
+
+
+
+ + +
+ + + + + + + + + + +
对象类型网站
网站列表 + 加载中... +
暂时还没有可选的网站。
+ + + + + + + + + + + +
网站名称操作
{{server.name}} + 选中 + 取消 +
+ + +
+
+ + +
+ +
+
` +}) + +Vue.component("bandwidth-size-capacity-box", { + props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"], + data: function () { + let v = this.vValue + if (v == null) { + v = { + count: this.vCount, + unit: this.vUnit + } + } + if (v.unit == null || v.unit.length == 0) { + v.unit = "mb" + } + + if (typeof (v["count"]) != "number") { + v["count"] = -1 + } + + let vSize = this.size + if (vSize == null) { + vSize = 6 + } + + let vMaxlength = this.maxlength + if (vMaxlength == null) { + vMaxlength = 10 + } + + let supportedUnits = this.vSupportedUnits + if (supportedUnits == null) { + supportedUnits = [] + } + + return { + capacity: v, + countString: (v.count >= 0) ? v.count.toString() : "", + vSize: vSize, + vMaxlength: vMaxlength, + supportedUnits: supportedUnits + } + }, + watch: { + "countString": function (newValue) { + let value = newValue.trim() + if (value.length == 0) { + this.capacity.count = -1 + this.change() + return + } + let count = parseInt(value) + if (!isNaN(count)) { + this.capacity.count = count + } + this.change() + } + }, + methods: { + change: function () { + this.$emit("change", this.capacity) + } + }, + template: `
+ +
+ +
+
+ +
+
` +}) + +Vue.component("bandwidth-size-capacity-view", { + props: ["v-value"], + data: function () { + let capacity = this.vValue + if (capacity != null && capacity.count > 0 && typeof capacity.unit === "string") { + capacity.unit = capacity.unit[0].toUpperCase() + capacity.unit.substring(1) + "ps" + } + return { + capacity: capacity + } + }, + template: ` + {{capacity.count}}{{capacity.unit}} +` +}) + +Vue.component("bits-var", { + props: ["v-bits"], + data: function () { + let bits = this.vBits + if (typeof bits != "number") { + bits = 0 + } + let format = teaweb.splitFormat(teaweb.formatBits(bits)) + return { + format: format + } + }, + template:` + {{format[0]}}{{format[1]}} +` +}) + +Vue.component("bytes-var", { + props: ["v-bytes"], + data: function () { + let bytes = this.vBytes + if (typeof bytes != "number") { + bytes = 0 + } + let format = teaweb.splitFormat(teaweb.formatBytes(bytes)) + return { + format: format + } + }, + template:` + {{format[0]}}{{format[1]}} +` +}) + +let checkboxId = 0 +Vue.component("checkbox", { + props: ["name", "value", "v-value", "id", "checked"], + data: function () { + checkboxId++ + let elementId = this.id + if (elementId == null) { + elementId = "checkbox" + checkboxId + } + + let elementValue = this.vValue + if (elementValue == null) { + elementValue = "1" + } + + let checkedValue = this.value + if (checkedValue == null && this.checked == "checked") { + checkedValue = elementValue + } + + return { + elementId: elementId, + elementValue: elementValue, + newValue: checkedValue + } + }, + methods: { + change: function () { + this.$emit("input", this.newValue) + }, + check: function () { + this.newValue = this.elementValue + }, + uncheck: function () { + this.newValue = "" + }, + isChecked: function () { + return (typeof (this.newValue) == "boolean" && this.newValue) || this.newValue == this.elementValue + } + }, + watch: { + value: function (v) { + if (typeof v == "boolean") { + this.newValue = v + } + } + }, + template: `
+ + +
` +}) + +Vue.component("columns-grid", { + props: [], + mounted: function () { + this.columns = this.calculateColumns() + + let that = this + window.addEventListener("resize", function () { + that.columns = that.calculateColumns() + }) + }, + data: function () { + return { + columns: "four" + } + }, + methods: { + calculateColumns: function () { + let w = window.innerWidth + let columns = Math.floor(w / 250) + if (columns == 0) { + columns = 1 + } + + let columnElements = this.$el.getElementsByClassName("column") + if (columnElements.length == 0) { + return + } + let maxColumns = columnElements.length + if (columns > maxColumns) { + columns = maxColumns + } + + // 添加右侧边框 + for (let index = 0; index < columnElements.length; index++) { + let el = columnElements[index] + el.className = el.className.replace("with-border", "") + if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) { + el.className += " with-border" + } + } + + switch (columns) { + case 1: + return "one" + case 2: + return "two" + case 3: + return "three" + case 4: + return "four" + case 5: + return "five" + case 6: + return "six" + case 7: + return "seven" + case 8: + return "eight" + case 9: + return "nine" + case 10: + return "ten" + default: + return "ten" + } + } + }, + template: `
+ +
` +}) + +Vue.component("combo-box", { + // data-url 和 data-key 成对出现 + props: [ + "name", "title", "placeholder", "size", "v-items", "v-value", + "data-url", // 数据源URL + "data-key", // 数据源中数据的键名 + "data-search", // 是否启用动态搜索,如果值为on或true,则表示启用 + "width" + ], + mounted: function () { + if (this.dataURL.length > 0) { + this.search("") + } + + // 设定菜单宽度 + let searchBox = this.$refs.searchBox + if (searchBox != null) { + let inputWidth = searchBox.offsetWidth + if (inputWidth != null && inputWidth > 0) { + this.$refs.menu.style.width = inputWidth + "px" + } else if (this.styleWidth.length > 0) { + this.$refs.menu.style.width = this.styleWidth + } + } + }, + data: function () { + let items = this.vItems + if (items == null || !(items instanceof Array)) { + items = [] + } + items = this.formatItems(items) + + // 当前选中项 + let selectedItem = null + if (this.vValue != null) { + let that = this + items.forEach(function (v) { + if (v.value == that.vValue) { + selectedItem = v + } + }) + } + + let width = this.width + if (width == null || width.length == 0) { + width = "11em" + } else { + if (/\d+$/.test(width)) { + width += "em" + } + } + + // data url + let dataURL = "" + if (typeof this.dataUrl == "string" && this.dataUrl.length > 0) { + dataURL = this.dataUrl + } + + return { + allItems: items, // 原始的所有的items + items: items.$copy(), // 候选的items + selectedItem: selectedItem, // 选中的item + keyword: "", + visible: false, + hideTimer: null, + hoverIndex: 0, + styleWidth: width, + + isInitial: true, + dataURL: dataURL, + urlRequestId: 0 // 记录URL请求ID,防止并行冲突 + } + }, + methods: { + search: function (keyword) { + // 从URL中获取选项数据 + let dataUrl = this.dataURL + let dataKey = this.dataKey + let that = this + + let requestId = Math.random() + this.urlRequestId = requestId + + Tea.action(dataUrl) + .params({ + keyword: (keyword == null) ? "" : keyword + }) + .post() + .success(function (resp) { + if (requestId != that.urlRequestId) { + return + } + + if (resp.data != null) { + if (typeof (resp.data[dataKey]) == "object") { + let items = that.formatItems(resp.data[dataKey]) + that.allItems = items + that.items = items.$copy() + + if (that.isInitial) { + that.isInitial = false + if (that.vValue != null) { + items.forEach(function (v) { + if (v.value == that.vValue) { + that.selectedItem = v + } + }) + } + } + } + } + }) + }, + formatItems: function (items) { + items.forEach(function (v) { + if (v.value == null) { + v.value = v.id + } + }) + return items + }, + reset: function () { + this.selectedItem = null + this.change() + this.hoverIndex = 0 + + let that = this + setTimeout(function () { + if (that.$refs.searchBox) { + that.$refs.searchBox.focus() + } + }) + }, + clear: function () { + this.selectedItem = null + this.change() + this.hoverIndex = 0 + }, + changeKeyword: function () { + let shouldSearch = this.dataURL.length > 0 && (this.dataSearch == "on" || this.dataSearch == "true") + + this.hoverIndex = 0 + let keyword = this.keyword + if (keyword.length == 0) { + if (shouldSearch) { + this.search(keyword) + } else { + this.items = this.allItems.$copy() + } + return + } + + + if (shouldSearch) { + this.search(keyword) + } else { + this.items = this.allItems.$copy().filter(function (v) { + if (v.fullname != null && v.fullname.length > 0 && teaweb.match(v.fullname, keyword)) { + return true + } + return teaweb.match(v.name, keyword) + }) + } + }, + selectItem: function (item) { + this.selectedItem = item + this.change() + this.hoverIndex = 0 + this.keyword = "" + this.changeKeyword() + }, + confirm: function () { + if (this.items.length > this.hoverIndex) { + this.selectItem(this.items[this.hoverIndex]) + } + }, + show: function () { + this.visible = true + + // 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换 + }, + hide: function () { + let that = this + this.hideTimer = setTimeout(function () { + that.visible = false + }, 500) + }, + downItem: function () { + this.hoverIndex++ + if (this.hoverIndex > this.items.length - 1) { + this.hoverIndex = 0 + } + this.focusItem() + }, + upItem: function () { + this.hoverIndex-- + if (this.hoverIndex < 0) { + this.hoverIndex = 0 + } + this.focusItem() + }, + focusItem: function () { + if (this.hoverIndex < this.items.length) { + this.$refs.itemRef[this.hoverIndex].focus() + let that = this + setTimeout(function () { + that.$refs.searchBox.focus() + if (that.hideTimer != null) { + clearTimeout(that.hideTimer) + that.hideTimer = null + } + }) + } + }, + change: function () { + this.$emit("change", this.selectedItem) + + let that = this + setTimeout(function () { + if (that.$refs.selectedLabel != null) { + that.$refs.selectedLabel.focus() + } + }) + }, + submitForm: function (event) { + if (event.target.tagName != "A") { + return + } + let parentBox = this.$refs.selectedLabel.parentNode + while (true) { + parentBox = parentBox.parentNode + if (parentBox == null || parentBox.tagName == "BODY") { + return + } + if (parentBox.tagName == "FORM") { + parentBox.submit() + break + } + } + }, + + setDataURL: function (dataURL) { + this.dataURL = dataURL + }, + reloadData: function () { + this.search("") + } + }, + template: `
+ +
+ +
+ + +
+ + {{title}}:{{selectedItem.name}} + + +
+ + + +
` +}) + +Vue.component("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.success("已复制到剪切板") + } + }, + template: `` +}) + +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("csrf-token", { + created: function () { + this.refreshToken() + }, + mounted: function () { + let that = this + var form = this.$refs.token.form + + // 监听表单提交,在提交前刷新token并确保更新到 DOM + form.addEventListener("submit", function (e) { + // 如果正在刷新,等待刷新完成 + if (that.refreshing) { + e.preventDefault() + e.stopPropagation() + return false + } + + // 阻止默认提交,先刷新 token + e.preventDefault() + e.stopPropagation() + that.refreshing = true + + // 刷新 token + that.refreshToken(function () { + // 确保 DOM 中的 token 值是最新的 + that.$forceUpdate() + that.$nextTick(function () { + var tokenInput = form.querySelector('input[name="csrfToken"]') + if (tokenInput) { + tokenInput.value = that.token + } + if (that.$refs.token) { + that.$refs.token.value = that.token + } + + // 确保 DOM 已更新后,再触发表单提交 + setTimeout(function () { + that.refreshing = false + // 重新触发表单提交 + Tea.runActionOn(form) + }, 50) + }) + }) + + return false + }) + + // 自动刷新 + setInterval(function () { + that.refreshToken() + }, 10 * 60 * 1000) + + // 监听表单提交失败,如果是 CSRF token 错误,自动刷新 token 并重试 + this.setupAutoRetry(form) + }, + data: function () { + return { + token: "", + retrying: false, + refreshing: false + } + }, + methods: { + refreshToken: function (callback) { + let that = this + Tea.action("/csrf/token") + .get() + .success(function (resp) { + that.token = resp.data.token + if (callback) { + callback() + } + }) + .fail(function () { + if (callback) { + callback() + } + }) + }, + setupAutoRetry: function (form) { + let that = this + var originalFail = form.getAttribute("data-tea-fail") + + // 确保 Tea.Vue 存在 + if (typeof Tea === "undefined" || Tea.Vue == null) { + if (typeof Tea === "undefined") { + window.Tea = {} + } + if (Tea.Vue == null) { + Tea.Vue = {} + } + } + + // 创建一个包装的 fail 函数 + var wrappedFailName = "csrfAutoRetryFail_" + Math.random().toString(36).substr(2, 9) + form.setAttribute("data-tea-fail", wrappedFailName) + + Tea.Vue[wrappedFailName] = function (resp) { + // 检查是否是 CSRF token 错误 + var isCSRFError = false + if (resp && resp.message) { + // 检查消息是否包含 "表单已失效" 或 "001" + if (resp.message.indexOf("表单已失效") >= 0 || resp.message.indexOf("(001)") >= 0) { + isCSRFError = true + } + } + // 检查 HTTP 状态码是否为 403 或 400 + if (!isCSRFError && resp && (resp.statusCode === 403 || resp.status === 403 || resp.statusCode === 400 || resp.status === 400)) { + isCSRFError = true + } + + if (isCSRFError) { + // 如果不是正在重试,则立即刷新 token 并自动重试 + if (!that.retrying) { + that.retrying = true + // 立即刷新 token + that.refreshToken(function () { + // 强制更新 Vue,确保响应式数据已更新 + that.$forceUpdate() + + // 使用 $nextTick 等待 Vue 完成 DOM 更新 + that.$nextTick(function () { + // 直接查找并更新 DOM 中的 input 元素(通过 name 属性) + var tokenInput = form.querySelector('input[name="csrfToken"]') + if (tokenInput) { + tokenInput.value = that.token + } + + // 如果 ref 存在,也更新它 + if (that.$refs.token) { + that.$refs.token.value = that.token + } + + // 使用 setTimeout 确保 DOM 已完全更新 + setTimeout(function () { + // 再次确认 token 值已更新 + var finalTokenInput = form.querySelector('input[name="csrfToken"]') + if (finalTokenInput && finalTokenInput.value !== that.token) { + finalTokenInput.value = that.token + } + + that.retrying = false + // 重新触发表单提交 + Tea.runActionOn(form) + }, 150) + }) + }) + return // 不调用原始 fail 函数 + } else { + // 如果正在重试,说明已经刷新过 token,直接调用原始 fail 函数 + if (originalFail && typeof Tea.Vue[originalFail] === "function") { + return Tea.Vue[originalFail].call(Tea.Vue, resp) + } else { + Tea.failResponse(resp) + } + } + } + + // 不是 CSRF 错误,调用原始 fail 函数或默认处理 + if (originalFail && typeof Tea.Vue[originalFail] === "function") { + return Tea.Vue[originalFail].call(Tea.Vue, resp) + } else { + Tea.failResponse(resp) + } + } + } + }, + template: `` +}) + + +Vue.component("datepicker", { + props: ["value", "v-name", "name", "v-value", "v-bottom-left", "placeholder"], + mounted: function () { + let that = this + teaweb.datepicker(this.$refs.dayInput, function (v) { + that.day = v + that.change() + }, !!this.vBottomLeft) + }, + data: function () { + let name = this.vName + if (name == null) { + name = this.name + } + if (name == null) { + name = "day" + } + + let day = this.vValue + if (day == null) { + day = this.value + if (day == null) { + day = "" + } + } + + let placeholder = "YYYY-MM-DD" + if (this.placeholder != null) { + placeholder = this.placeholder + } + + return { + realName: name, + realPlaceholder: placeholder, + day: day + } + }, + watch: { + value: function (v) { + this.day = v + + let picker = this.$refs.dayInput.picker + if (picker != null) { + if (v != null && /^\d+-\d+-\d+$/.test(v)) { + picker.setDate(v) + } + } + } + }, + methods: { + change: function () { + this.$emit("input", this.day) // support v-model,事件触发需要在 change 之前 + this.$emit("change", this.day) + } + }, + template: `
+ +
` +}) + +Vue.component("datetime-input", { + props: ["v-name", "v-timestamp"], + mounted: function () { + let that = this + teaweb.datepicker(this.$refs.dayInput, function (v) { + that.day = v + that.hour = "23" + that.minute = "59" + that.second = "59" + that.change() + }) + }, + data: function () { + let timestamp = this.vTimestamp + if (timestamp != null) { + timestamp = parseInt(timestamp) + if (isNaN(timestamp)) { + timestamp = 0 + } + } else { + timestamp = 0 + } + + let day = "" + let hour = "" + let minute = "" + let second = "" + + if (timestamp > 0) { + let date = new Date() + date.setTime(timestamp * 1000) + + let year = date.getFullYear().toString() + let month = this.leadingZero((date.getMonth() + 1).toString(), 2) + day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2) + + hour = this.leadingZero(date.getHours().toString(), 2) + minute = this.leadingZero(date.getMinutes().toString(), 2) + second = this.leadingZero(date.getSeconds().toString(), 2) + } + + return { + timestamp: timestamp, + day: day, + hour: hour, + minute: minute, + second: second, + + hasDayError: false, + hasHourError: false, + hasMinuteError: false, + hasSecondError: false + } + }, + methods: { + change: function () { + // day + if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) { + this.hasDayError = true + return + } + let pieces = this.day.split("-") + let year = parseInt(pieces[0]) + + let month = parseInt(pieces[1]) + if (month < 1 || month > 12) { + this.hasDayError = true + return + } + + let day = parseInt(pieces[2]) + if (day < 1 || day > 32) { + this.hasDayError = true + return + } + + this.hasDayError = false + + // hour + if (!/^\d+$/.test(this.hour)) { + this.hasHourError = true + return + } + let hour = parseInt(this.hour) + if (isNaN(hour)) { + this.hasHourError = true + return + } + if (hour < 0 || hour >= 24) { + this.hasHourError = true + return + } + this.hasHourError = false + + // minute + if (!/^\d+$/.test(this.minute)) { + this.hasMinuteError = true + return + } + let minute = parseInt(this.minute) + if (isNaN(minute)) { + this.hasMinuteError = true + return + } + if (minute < 0 || minute >= 60) { + this.hasMinuteError = true + return + } + this.hasMinuteError = false + + // second + if (!/^\d+$/.test(this.second)) { + this.hasSecondError = true + return + } + let second = parseInt(this.second) + if (isNaN(second)) { + this.hasSecondError = true + return + } + if (second < 0 || second >= 60) { + this.hasSecondError = true + return + } + this.hasSecondError = false + + let date = new Date(year, month - 1, day, hour, minute, second) + this.timestamp = Math.floor(date.getTime() / 1000) + }, + leadingZero: function (s, l) { + s = s.toString() + if (l <= s.length) { + return s + } + for (let i = 0; i < l - s.length; i++) { + s = "0" + s + } + return s + }, + resultTimestamp: function () { + return this.timestamp + }, + nextYear: function () { + let date = new Date() + date.setFullYear(date.getFullYear()+1) + this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) + this.hour = this.leadingZero(date.getHours(), 2) + this.minute = this.leadingZero(date.getMinutes(), 2) + this.second = this.leadingZero(date.getSeconds(), 2) + this.change() + }, + nextDays: function (days) { + let date = new Date() + date.setTime(date.getTime() + days * 86400 * 1000) + this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) + this.hour = this.leadingZero(date.getHours(), 2) + this.minute = this.leadingZero(date.getMinutes(), 2) + this.second = this.leadingZero(date.getSeconds(), 2) + this.change() + }, + nextHours: function (hours) { + let date = new Date() + date.setTime(date.getTime() + hours * 3600 * 1000) + this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) + this.hour = this.leadingZero(date.getHours(), 2) + this.minute = this.leadingZero(date.getMinutes(), 2) + this.second = this.leadingZero(date.getSeconds(), 2) + this.change() + } + }, + template: `
+ +
+
+ +
+
+
:
+
+
:
+
+
+

常用时间:  1小时  |  1天  |  3天  |  1周  |  30天  |  1年 

+
` +}) + +Vue.component("download-link", { + props: ["v-element", "v-file"], + 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 e = document.getElementById(this.vElement) + if (e == null) { + teaweb.warn("找不到要下载的内容") + return + } + let text = e.innerText + if (text == null) { + text = e.textContent + } + return Tea.url("/ui/download", { + file: this.file, + text: text + }) + } + }, + template: ``, +}) + +Vue.component("file-textarea", { + props: ["value"], + data: function () { + let value = this.value + if (typeof value != "string") { + value = "" + } + return { + realValue: value + } + }, + mounted: function () { + }, + methods: { + dragover: function () {}, + drop: function (e) { + let that = this + e.dataTransfer.items[0].getAsFile().text().then(function (data) { + that.setValue(data) + }) + }, + setValue: function (value) { + this.realValue = value + }, + focus: function () { + this.$refs.textarea.focus() + } + }, + template: `` +}) + +/** + * 一级菜单 + */ +Vue.component("first-menu", { + props: [], + 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("inner-menu", { + template: ` +
+ +
` +}); + +Vue.component("js-page", { + props: ["v-max"], + data: function () { + let max = this.vMax + if (max == null) { + max = 0 + } + return { + max: max, + page: 1 + } + }, + methods: { + updateMax: function (max) { + this.max = max + }, + selectPage: function(page) { + this.page = page + this.$emit("change", page) + } + }, + template:`
+
+ {{i}} +
+
` +}) + +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("labeled-input", { + props: ["name", "size", "maxlength", "label", "value"], + template: '
\ + \ + {{label}}\ +
' +}); + +// 启用状态标签 +Vue.component("label-on", { + props: ["v-is-on"], + template: '
已启用已停用
' +}) + +// 文字代码标签 +Vue.component("code-label", { + methods: { + click: function (args) { + this.$emit("click", args) + } + }, + template: `` +}) + +// tiny标签 +Vue.component("tiny-label", { + template: `` +}) + +Vue.component("tiny-basic-label", { + template: `` +}) + +// 更小的标签 +Vue.component("micro-basic-label", { + template: `` +}) + + +// 灰色的Label +Vue.component("grey-label", { + template: `` +}) + +// Plus专属 +Vue.component("plus-label", { + template: `` +}) + + +// 使用Icon的链接方式 +Vue.component("link-icon", { + props: ["href", "title"], + 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 () { + 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("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: '\ + \ + ' +}); + +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("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) + this.$emit("input", this.visible) + } + }, + 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("network-addresses-box", { + props: ["v-server-type", "v-addresses", "v-protocol", "v-name"], + 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" + } + + return { + addresses: addresses, + protocol: protocol, + name: name + } + }, + watch: { + "vServerType": function () { + this.addresses = [] + } + }, + methods: { + addAddr: function () { + let that = this + window.UPDATING_ADDR = null + teaweb.popup("/servers/addPortPopup?serverType=" + this.vServerType + "&protocol=" + this.protocol, { + height: "16em", + callback: function (resp) { + var addr = resp.data.address + 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 + teaweb.popup("/servers/addPortPopup?serverType=" + this.vServerType + "&protocol=" + this.protocol, { + height: "16em", + 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) + } + }, + template: `
+ +
+
+ {{addr.protocol}}://{{addr.host}}*:{{addr.portRange}} + +
+
+
+ [添加端口绑定] +
` +}) + +Vue.component("network-addresses-view", { + props: ["v-addresses"], + template: `
+
+ {{addr.protocol}}://{{addr.host}}:{{addr.portRange}} +
+
` +}) + +Vue.component("not-found-box", { + props: ["message"], + template: `
+
+

{{message}}

+
` +}) + +Vue.component("page-box", { + data: function () { + return { + page: "" + } + }, + created: function () { + let that = this; + setTimeout(function () { + if (Tea && Tea.Vue && Tea.Vue.page) { + that.page = Tea.Vue.page; + } + }) + }, + template: `
+
+
` +}) + +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}}
+
+
+
+ +
+
` +}) + +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("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("search-box", { + props: ["placeholder", "width"], + data: function () { + let width = this.width + if (width == null) { + width = "10em" + } + return { + realWidth: width, + realValue: "" + } + }, + methods: { + onInput: function () { + this.$emit("input", { value: this.realValue}) + this.$emit("change", { value: this.realValue}) + }, + clearValue: function () { + this.realValue = "" + this.focus() + this.onInput() + }, + focus: function () { + this.$refs.valueRef.focus() + } + }, + template: `
+
+ + +
+
` +}) + +/** + * 二级菜单 + */ +Vue.component("second-menu", { + template: ' \ +
\ + \ +
\ +
' +}); + +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("size-capacity-box", { + props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength"], + 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 + } + + return { + capacity: v, + countString: (v.count >= 0) ? v.count.toString() : "", + vSize: vSize, + vMaxlength: vMaxlength + } + }, + 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("size-capacity-view", { + props:["v-default-text", "v-value"], + methods: { + composeCapacity: function (capacity) { + return teaweb.convertSizeCapacityToString(capacity) + } + }, + template: `
+ {{composeCapacity(vValue)}} + {{vDefaultText}} +
` +}) + +// 排序使用的箭头 +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: `  ` +}) + +// 给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) +} + + +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("submit-btn", { + template: '' +}); + +Vue.component("theme-color-picker", { + props: ["v-model"], + data: function () { + return { + showPicker: false, + r: 20, + g: 83, + b: 196, + hex: "#14539A", + clickHandler: null + } + }, + mounted: function () { + console.log("Theme color picker component mounted, element:", this.$el) + // 从localStorage加载保存的颜色 + let savedColor = localStorage.getItem("themeColor") + if (savedColor) { + this.setColor(savedColor) + } else { + // 从当前页面获取默认颜色 + let defaultColor = this.getCurrentThemeColor() + if (defaultColor) { + this.setColor(defaultColor) + } + } + + // 应用颜色 + this.applyColor() + }, + beforeDestroy: function() { + // 移除点击监听器 + if (this.clickHandler) { + document.removeEventListener("click", this.clickHandler) + this.clickHandler = null + } + }, + methods: { + // 打开/关闭颜色选择器 + togglePicker: function (e) { + if (e) { + e.preventDefault() + e.stopPropagation() + } + console.log("Toggle picker clicked, current showPicker:", this.showPicker) + this.showPicker = !this.showPicker + console.log("Toggle picker clicked, new showPicker:", this.showPicker) + console.log("Popup should be visible:", this.showPicker) + + // 管理外部点击监听器 + if (this.showPicker) { + // 打开时,延迟添加监听器 + let that = this + setTimeout(function() { + if (!that.clickHandler) { + that.clickHandler = function(e) { + if (that.showPicker && !that.$el.contains(e.target)) { + that.showPicker = false + } + } + document.addEventListener("click", that.clickHandler) + } + }, 100) + } else { + // 关闭时,移除监听器 + if (this.clickHandler) { + document.removeEventListener("click", this.clickHandler) + this.clickHandler = null + } + } + }, + + // RGB转16进制 + rgbToHex: function (r, g, b) { + return "#" + [r, g, b].map(function (x) { + x = parseInt(x) + const hex = x.toString(16) + return hex.length === 1 ? "0" + hex : hex + }).join("").toUpperCase() + }, + + // 16进制转RGB + hexToRgb: function (hex) { + hex = hex.replace("#", "") + const r = parseInt(hex.substring(0, 2), 16) + const g = parseInt(hex.substring(2, 4), 16) + const b = parseInt(hex.substring(4, 6), 16) + return { r: r, g: g, b: b } + }, + + // 设置颜色(支持hex或rgb对象) + setColor: function (color) { + if (typeof color === "string") { + if (color.startsWith("#")) { + let rgb = this.hexToRgb(color) + this.r = rgb.r + this.g = rgb.g + this.b = rgb.b + this.hex = color.toUpperCase() + } else { + // 假设是16进制不带#号 + this.setColor("#" + color) + } + } else if (typeof color === "object" && color.r !== undefined) { + this.r = color.r + this.g = color.g + this.b = color.b + this.hex = this.rgbToHex(color.r, color.g, color.b) + } + }, + + // RGB值变化时更新hex + updateHex: function () { + this.hex = this.rgbToHex(this.r, this.g, this.b) + this.applyColor() + }, + + // Hex值变化时更新RGB + updateRgb: function () { + if (this.hex.match(/^#[0-9A-Fa-f]{6}$/)) { + let rgb = this.hexToRgb(this.hex) + this.r = rgb.r + this.g = rgb.g + this.b = rgb.b + this.applyColor() + } + }, + + // 应用颜色到整个网站 + applyColor: function () { + let color = this.rgbToHex(this.r, this.g, this.b) + + // 保存到localStorage + localStorage.setItem("themeColor", color) + + // 创建或更新样式 + let styleId = "theme-color-custom" + let styleEl = document.getElementById(styleId) + if (!styleEl) { + styleEl = document.createElement("style") + styleEl.id = styleId + document.head.appendChild(styleEl) + } + + // 应用颜色到主题元素 + styleEl.textContent = ` + .top-nav, .main-menu, .main-menu .menu { + background: ${color} !important; + } + .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless { + background: ${color} !important; + } + .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .item { + background: ${color} !important; + } + .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .sub-items { + background: ${color} !important; + } + .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .sub-items .item { + background: ${color} !important; + } + .main-menu .ui.menu .sub-items { + background: ${color} !important; + } + .main-menu .ui.menu .sub-items .item { + background: ${color} !important; + } + .main-menu .ui.labeled.menu .sub-items { + background: ${color} !important; + } + .main-menu .ui.labeled.menu .sub-items .item { + background: ${color} !important; + } + .main-menu .sub-items { + background: ${color} !important; + } + .main-menu .sub-items .item { + background: ${color} !important; + } + .ui.menu.blue.inverted { + background: ${color} !important; + } + .ui.menu.blue.inverted .item { + background: ${color} !important; + } + .ui.menu.vertical.blue.inverted { + background: ${color} !important; + } + .ui.menu.vertical.blue.inverted .item { + background: ${color} !important; + } + .ui.labeled.menu.vertical.blue.inverted { + background: ${color} !important; + } + .ui.labeled.menu.vertical.blue.inverted .item { + background: ${color} !important; + } + ` + + // 触发事件通知其他组件 + if (window.Tea && window.Tea.Vue) { + window.Tea.Vue.$emit("theme-color-changed", color) + } + + // 更新v-model(如果使用) + this.$emit("input", color.replace("#", "")) + }, + + // 获取当前主题颜色 + getCurrentThemeColor: function () { + let topNav = document.querySelector(".top-nav") + if (topNav) { + let bgColor = window.getComputedStyle(topNav).backgroundColor + if (bgColor && bgColor !== "rgba(0, 0, 0, 0)" && bgColor !== "transparent") { + // 转换rgb/rgba为hex + let match = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/) + if (match) { + return this.rgbToHex(parseInt(match[1]), parseInt(match[2]), parseInt(match[3])) + } + } + } + return null + }, + + // 重置为默认颜色 + resetColor: function () { + this.setColor("#14539A") // 默认蓝色 + this.applyColor() + } + }, + template: ` +
+ + 主题色 + + +
+
+ RGB颜色调节 +
+ + +
+
+
+ {{ hex }} | RGB({{ r }}, {{ g }}, {{ b }}) +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+ + +
+
+
+ ` +}) + + +Vue.component("time-duration-box", { + props: ["v-name", "v-value", "v-count", "v-unit", "placeholder", "v-min-unit", "maxlength"], + mounted: function () { + this.change() + }, + data: function () { + let v = this.vValue + if (v == null) { + v = { + count: this.vCount, + unit: this.vUnit + } + } + if (typeof (v["count"]) != "number") { + v["count"] = -1 + } + + let minUnit = this.vMinUnit + let units = [ + { + code: "ms", + name: "毫秒" + }, + { + code: "second", + name: "秒" + }, + { + code: "minute", + name: "分钟" + }, + { + code: "hour", + name: "小时" + }, + { + code: "day", + name: "天" + } + ] + let minUnitIndex = -1 + if (minUnit != null && typeof minUnit == "string" && minUnit.length > 0) { + for (let i = 0; i < units.length; i++) { + if (units[i].code == minUnit) { + minUnitIndex = i + break + } + } + } + if (minUnitIndex > -1) { + units = units.slice(minUnitIndex) + } + + let maxLength = parseInt(this.maxlength) + if (typeof maxLength != "number") { + maxLength = 10 + } + + return { + duration: v, + countString: (v.count >= 0) ? v.count.toString() : "", + units: units, + realMaxLength: maxLength + } + }, + watch: { + "countString": function (newValue) { + let value = newValue.trim() + if (value.length == 0) { + this.duration.count = -1 + return + } + let count = parseInt(value) + if (!isNaN(count)) { + this.duration.count = count + } + this.change() + } + }, + methods: { + change: function () { + this.$emit("change", this.duration) + } + }, + template: `
+ +
+ +
+
+ +
+
` +}) + +Vue.component("time-duration-text", { + props: ["v-value"], + methods: { + unitName: function (unit) { + switch (unit) { + case "ms": + return "毫秒" + case "second": + return "秒" + case "minute": + return "分钟" + case "hour": + return "小时" + case "day": + return "天" + } + } + }, + template: ` + {{vValue.count}} {{unitName(vValue.unit)}} +` +}) + +Vue.component("url-patterns-box", { + props: ["value"], + data: function () { + let patterns = [] + if (this.value != null) { + patterns = this.value + } + + return { + patterns: patterns, + isAdding: false, + + addingPattern: {"type": "wildcard", "pattern": ""}, + editingIndex: -1, + + patternIsInvalid: false, + + windowIsSmall: window.innerWidth < 600 + } + }, + methods: { + add: function () { + this.isAdding = true + let that = this + setTimeout(function () { + that.$refs.patternInput.focus() + }) + }, + edit: function (index) { + this.isAdding = true + this.editingIndex = index + this.addingPattern = { + type: this.patterns[index].type, + pattern: this.patterns[index].pattern + } + }, + confirm: function () { + if (this.requireURL(this.addingPattern.type)) { + let pattern = this.addingPattern.pattern.trim() + if (pattern.length == 0) { + let that = this + teaweb.warn("请输入URL", function () { + that.$refs.patternInput.focus() + }) + return + } + } + if (this.editingIndex < 0) { + this.patterns.push({ + type: this.addingPattern.type, + pattern: this.addingPattern.pattern + }) + } else { + this.patterns[this.editingIndex].type = this.addingPattern.type + this.patterns[this.editingIndex].pattern = this.addingPattern.pattern + } + this.notifyChange() + this.cancel() + }, + remove: function (index) { + this.patterns.$remove(index) + this.cancel() + this.notifyChange() + }, + cancel: function () { + this.isAdding = false + this.addingPattern = {"type": "wildcard", "pattern": ""} + this.editingIndex = -1 + }, + patternTypeName: function (patternType) { + switch (patternType) { + case "wildcard": + return "通配符" + case "regexp": + return "正则" + case "images": + return "常见图片文件" + case "audios": + return "常见音频文件" + case "videos": + return "常见视频文件" + } + return "" + }, + notifyChange: function () { + this.$emit("input", this.patterns) + }, + changePattern: function () { + this.patternIsInvalid = false + let pattern = this.addingPattern.pattern + switch (this.addingPattern.type) { + case "wildcard": + if (pattern.indexOf("?") >= 0) { + this.patternIsInvalid = true + } + break + case "regexp": + if (pattern.indexOf("?") >= 0) { + let pieces = pattern.split("?") + for (let i = 0; i < pieces.length - 1; i++) { + if (pieces[i].length == 0 || pieces[i][pieces[i].length - 1] != "\\") { + this.patternIsInvalid = true + } + } + } + break + } + }, + requireURL: function (patternType) { + return patternType == "wildcard" || patternType == "regexp" + } + }, + template: `
+
+
+ [{{patternTypeName(pattern.type)}}] {{pattern.pattern}}   + + +
+
+
+
+
+ +
+
+ +

通配符正则表达式中不能包含问号(?)及问号以后的内容。

+
+
+ + +
+
+ +
+
+
+
+ +
+
` +}) + +Vue.component("values-box", { + props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"], + data: function () { + let values = this.values; + if (values == null) { + values = []; + } + + if (this.vValues != null && typeof this.vValues == "object") { + values = this.vValues + } + + return { + "realValues": values, + "isUpdating": false, + "isAdding": false, + "index": 0, + "value": "", + isEditing: false + } + }, + methods: { + create: function () { + this.isAdding = true; + var that = this; + setTimeout(function () { + that.$refs.value.focus(); + }, 200); + }, + update: function (index) { + this.cancel() + this.isUpdating = true; + this.index = index; + this.value = this.realValues[index]; + var that = this; + setTimeout(function () { + that.$refs.value.focus(); + }, 200); + }, + confirm: function () { + if (this.value.length == 0) { + if (typeof(this.vAllowEmpty) != "boolean" || !this.vAllowEmpty) { + return + } + } + + // validate + if (typeof(this.validator) == "function") { + let resp = this.validator.call(this, this.value) + if (typeof resp == "object") { + if (typeof resp.isOk == "boolean" && !resp.isOk) { + if (typeof resp.message == "string") { + let that = this + teaweb.warn(resp.message, function () { + that.$refs.value.focus(); + }) + } + return + } + } + } + + if (this.isUpdating) { + Vue.set(this.realValues, this.index, this.value); + } else { + this.realValues.push(this.value); + } + this.cancel() + this.$emit("change", this.realValues) + }, + remove: function (index) { + this.realValues.$remove(index) + this.$emit("change", this.realValues) + }, + cancel: function () { + this.isUpdating = false; + this.isAdding = false; + this.value = ""; + }, + updateAll: function (values) { + this.realValues = values + }, + addValue: function (v) { + this.realValues.push(v) + }, + + startEditing: function () { + this.isEditing = !this.isEditing + }, + allValues: function () { + return this.realValues + } + }, + template: `
+
+
+ {{value}} + [空] +
+ [修改] +
+
+
+
+ {{value}} + [空] + +   + +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
` +}); + +// 警告消息 +Vue.component("warning-message", { + template: `
` +}) + +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("ip-item-text", { + props: ["v-item"], + template: ` + * + + {{vItem.value}} + + {{vItem.ipFrom}} + - {{vItem.ipTo}} + + +   级别:{{vItem.eventLevelName}} +` +}) + +// 绑定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("/waf/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("/waf/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.value}} + + {{item.ipFrom}}  New   + - {{item.ipTo}} + + + +
+ {{item.region}} + | {{item.isp}} +
+
{{item.isp}}
+ +
+ 添加于 {{item.createdTime}} + + @ + + [名单:{{item.list.name}}] + [名单:{{item.list.name}} + + + + [网站:{{item.policy.server.name}}] + [网站:{{item.policy.server.name}}] + [网站:{{item.policy.server.name}}] + + + + +
+
+ IPv4 + IPv4 + IPv6 + 所有IP + + {{item.eventLevelName}} + - + +
+ {{item.expiredTime}} +
+ 已过期 +
+
+ {{formatSeconds(item.lifeSeconds)}} + 已过期 +
+
+ 不过期 +
+ {{item.reason}} + - + + + + + + 修改   + 删除 +
+
` +}) + Vue.component("message-row", { props: ["v-message"], data: function () { @@ -87,6 +3325,212 @@ Vue.component("message-row", {
` }) +Vue.component("ns-access-log-box", { + props: ["v-access-log", "v-keyword"], + data: function () { + let accessLog = this.vAccessLog + return { + accessLog: accessLog + } + }, + 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-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-create-records-table", { + props: ["v-types", "v-default-ttl"], + data: function () { + let types = this.vTypes + if (types == null) { + types = [] + } + + let defaultTTL = this.vDefaultTtl + if (defaultTTL != null) { + defaultTTL = parseInt(defaultTTL.toString()) + } + if (defaultTTL <= 0) { + defaultTTL = 600 + } + + return { + types: types, + defaultTTL: defaultTTL, + records: [ + { + name: "", + type: "A", + value: "", + routeCodes: [], + ttl: defaultTTL, + index: 0 + } + ], + lastIndex: 0, + isAddingRoutes: false // 是否正在添加线路 + } + }, + methods: { + add: function () { + this.records.push({ + name: "", + type: "A", + value: "", + routeCodes: [], + ttl: this.defaultTTL, + 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-domain-group-selector", { props: ["v-domain-group-id"], data: function () { @@ -127,188 +3571,268 @@ Vue.component("ns-domain-group-selector", {
` }) -// 选择多个线路 -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 - that.supportChinaProvinceRoutes = resp.data.supportChinaProvinceRoutes - that.supportISPRoutes = resp.data.supportISPRoutes - that.supportWorldRegionRoutes = resp.data.supportWorldRegionRoutes - that.supportAgentRoutes = resp.data.supportAgentRoutes - that.publicCategories = resp.data.publicCategories - that.supportPublicRoutes = resp.data.supportPublicRoutes - - // provinces - let provinces = {} - if (resp.data.provinces != null && resp.data.provinces.length > 0) { - for (const province of resp.data.provinces) { - let countryCode = province.countryCode - if (typeof provinces[countryCode] == "undefined") { - provinces[countryCode] = [] - } - provinces[countryCode].push({ - name: province.name, - code: province.code - }) - } - } - that.provinces = provinces - }) - }, +Vue.component("ns-record-health-check-config-box", { + props:["value", "v-parent-config"], data: function () { - let selectedRoutes = this.vRoutes - if (selectedRoutes == null) { - selectedRoutes = [] + let config = this.value + if (config == null) { + config = { + isOn: false, + port: 0, + timeoutSeconds: 0, + countUp: 0, + countDown: 0 + } } - let inputName = this.name - if (typeof inputName != "string" || inputName.length == 0) { - inputName = "routeCodes" - } + let parentConfig = this.vParentConfig return { - routeCode: "default", - inputName: inputName, - routes: [], + config: config, + portString: config.port.toString(), + timeoutSecondsString: config.timeoutSeconds.toString(), + countUpString: config.countUp.toString(), + countDownString: config.countDown.toString(), + portIsEditing: config.port > 0, + timeoutSecondsIsEditing: config.timeoutSeconds > 0, + countUpIsEditing: config.countUp > 0, + countDownIsEditing: config.countDown > 0, - provinces: {}, // country code => [ province1, province2, ... ] - provinceRouteCode: "", - - isAdding: false, - routeType: "default", - selectedRoutes: selectedRoutes, - - supportChinaProvinceRoutes: false, - supportISPRoutes: false, - supportWorldRegionRoutes: false, - supportAgentRoutes: false, - supportPublicRoutes: false, - publicCategories: [] + parentConfig: parentConfig } }, 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 - } - }) + portString: function (value) { + let port = parseInt(value.toString()) + if (isNaN(port) || port > 65535 || port < 1) { + this.config.port = 0 + } else { + this.config.port = port + } + }, + timeoutSecondsString: function (value) { + let timeoutSeconds = parseInt(value.toString()) + if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) { + this.config.timeoutSeconds = 0 + } else { + this.config.timeoutSeconds = timeoutSeconds + } + }, + countUpString: function (value) { + let countUp = parseInt(value.toString()) + if (isNaN(countUp) || countUp > 1000 || countUp < 1) { + this.config.countUp = 0 + } else { + this.config.countUp = countUp + } + }, + countDownString: function (value) { + let countDown = parseInt(value.toString()) + if (isNaN(countDown) || countDown > 1000 || countDown < 1) { + this.config.countDown = 0 + } else { + this.config.countDown = countDown + } } }, - methods: { - add: function () { - this.isAdding = true - this.routeType = "default" - this.routeCode = "default" - this.provinceRouteCode = "" - this.$emit("add") - }, - cancel: function () { - this.isAdding = false - this.$emit("cancel") - }, - confirm: function () { - if (this.routeCode.length == 0) { - return - } - - let that = this - - - // route - let selectedRoute = null - for (const route of this.routes) { - if (route.code == this.routeCode) { - selectedRoute = route - break - } - } - - if (selectedRoute != null) { - // province route - if (this.provinceRouteCode.length > 0 && this.provinces[this.routeCode] != null) { - for (const province of this.provinces[this.routeCode]) { - if (province.code == this.provinceRouteCode) { - selectedRoute = { - name: selectedRoute.name + "-" + province.name, - code: province.code - } - break - } - } - } - - that.selectedRoutes.push(selectedRoute) - } - - this.$emit("change", this.selectedRoutes) - this.cancel() - }, - remove: function (index) { - this.selectedRoutes.$remove(index) - this.$emit("change", this.selectedRoutes) - } - } - , template: `
-
-
- - {{route.name}}   -
-
-
-
- + +
+ - + + + + + + + - + - - + + -
选择类型 *启用当前记录健康检查 - + +
检测端口 + + 默认{{parentConfig.port}} +   [修改] + +
+ + +

通过尝试连接A/AAAA记录中的IP加此端口来确定当前记录是否健康。

+
选择线路 *超时时间 - + + 默认{{parentConfig.timeoutSeconds}}秒 +   [修改] + +
+ +
+ + +
+
选择省/州
默认连续上线次数 - + + 默认{{parentConfig.countUp}}次 +   [修改] + +
+ +
+ + +
+

连续检测{{config.countUp}}N次成功后,认为当前记录是在线的。

+
-
- -   取消 -
-
- -

由于套餐限制,当前用户只能使用部分线路。

+ + 默认连续下线次数 + + + 默认{{parentConfig.countDown}}次 +   [修改] + +
+
+ [使用默认] +
+
+ + +
+

连续检测{{config.countDown}}N次失败后,认为当前记录是离线的。

+
+ + + + +
+
` +}) + +Vue.component("ns-records-health-check-config-box", { + props:["value"], + data: function () { + let config = this.value + if (config == null) { + config = { + isOn: false, + port: 80, + timeoutSeconds: 5, + countUp: 1, + countDown: 3 + } + } + return { + config: config, + portString: config.port.toString(), + timeoutSecondsString: config.timeoutSeconds.toString(), + countUpString: config.countUp.toString(), + countDownString: config.countDown.toString() + } + }, + watch: { + portString: function (value) { + let port = parseInt(value.toString()) + if (isNaN(port) || port > 65535 || port < 1) { + this.config.port = 80 + } else { + this.config.port = port + } + }, + timeoutSecondsString: function (value) { + let timeoutSeconds = parseInt(value.toString()) + if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) { + this.config.timeoutSeconds = 5 + } else { + this.config.timeoutSeconds = timeoutSeconds + } + }, + countUpString: function (value) { + let countUp = parseInt(value.toString()) + if (isNaN(countUp) || countUp > 1000 || countUp < 1) { + this.config.countUp = 1 + } else { + this.config.countUp = countUp + } + }, + countDownString: function (value) { + let countDown = parseInt(value.toString()) + if (isNaN(countDown) || countDown > 1000 || countDown < 1) { + this.config.countDown = 3 + } else { + this.config.countDown = countDown + } + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
启用健康检查 + +

选中后,表示启用当前域名下A/AAAA记录的健康检查;启用此设置后,你仍需设置单个A/AAAA记录的健康检查。

+
默认检测端口 + +

通过尝试连接A/AAAA记录中的IP加此端口来确定当前记录是否健康。

+
默认超时时间 +
+ + +
+
默认连续上线次数 +
+ + +
+

连续检测{{config.countUp}}N次成功后,认为当前记录是在线的。

+
默认连续下线次数 +
+ + +
+

连续检测{{config.countDown}}N次失败后,认为当前记录是离线的。

+
+
` }) @@ -478,158 +4002,6 @@ Vue.component("ns-recursion-config-box", { ` }) -Vue.component("ns-access-log-ref-box", { - props: ["v-access-log-ref", "v-is-parent"], - data: function () { - let config = this.vAccessLogRef - if (config == null) { - config = { - isOn: false, - isPrior: false, - logMissingDomains: false - } - } - if (typeof (config.logMissingDomains) == "undefined") { - config.logMissingDomains = false - } - return { - config: config - } - }, - template: `
- - - - - - - - - - - - - -
启用 - -
记录所有访问 - -

包括对没有在系统里创建的域名访问。

-
-
-
` -}) - -Vue.component("ns-records-health-check-config-box", { - props:["value"], - data: function () { - let config = this.value - if (config == null) { - config = { - isOn: false, - port: 80, - timeoutSeconds: 5, - countUp: 1, - countDown: 3 - } - } - return { - config: config, - portString: config.port.toString(), - timeoutSecondsString: config.timeoutSeconds.toString(), - countUpString: config.countUp.toString(), - countDownString: config.countDown.toString() - } - }, - watch: { - portString: function (value) { - let port = parseInt(value.toString()) - if (isNaN(port) || port > 65535 || port < 1) { - this.config.port = 80 - } else { - this.config.port = port - } - }, - timeoutSecondsString: function (value) { - let timeoutSeconds = parseInt(value.toString()) - if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) { - this.config.timeoutSeconds = 5 - } else { - this.config.timeoutSeconds = timeoutSeconds - } - }, - countUpString: function (value) { - let countUp = parseInt(value.toString()) - if (isNaN(countUp) || countUp > 1000 || countUp < 1) { - this.config.countUp = 1 - } else { - this.config.countUp = countUp - } - }, - countDownString: function (value) { - let countDown = parseInt(value.toString()) - if (isNaN(countDown) || countDown > 1000 || countDown < 1) { - this.config.countDown = 3 - } else { - this.config.countDown = countDown - } - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - -
启用健康检查 - -

选中后,表示启用当前域名下A/AAAA记录的健康检查;启用此设置后,你仍需设置单个A/AAAA记录的健康检查。

-
默认检测端口 - -

通过尝试连接A/AAAA记录中的IP加此端口来确定当前记录是否健康。

-
默认超时时间 -
- - -
-
默认连续上线次数 -
- - -
-

连续检测{{config.countUp}}N次成功后,认为当前记录是在线的。

-
默认连续下线次数 -
- - -
-

连续检测{{config.countDown}}N次失败后,认为当前记录是离线的。

-
-
-
` -}) - Vue.component("ns-route-ranges-box", { props: ["v-ranges"], data: function () { @@ -1216,274 +4588,6 @@ Vue.component("ns-route-ranges-box", { ` }) -Vue.component("ns-record-health-check-config-box", { - props:["value", "v-parent-config"], - data: function () { - let config = this.value - if (config == null) { - config = { - isOn: false, - port: 0, - timeoutSeconds: 0, - countUp: 0, - countDown: 0 - } - } - - let parentConfig = this.vParentConfig - - return { - config: config, - portString: config.port.toString(), - timeoutSecondsString: config.timeoutSeconds.toString(), - countUpString: config.countUp.toString(), - countDownString: config.countDown.toString(), - - portIsEditing: config.port > 0, - timeoutSecondsIsEditing: config.timeoutSeconds > 0, - countUpIsEditing: config.countUp > 0, - countDownIsEditing: config.countDown > 0, - - parentConfig: parentConfig - } - }, - watch: { - portString: function (value) { - let port = parseInt(value.toString()) - if (isNaN(port) || port > 65535 || port < 1) { - this.config.port = 0 - } else { - this.config.port = port - } - }, - timeoutSecondsString: function (value) { - let timeoutSeconds = parseInt(value.toString()) - if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) { - this.config.timeoutSeconds = 0 - } else { - this.config.timeoutSeconds = timeoutSeconds - } - }, - countUpString: function (value) { - let countUp = parseInt(value.toString()) - if (isNaN(countUp) || countUp > 1000 || countUp < 1) { - this.config.countUp = 0 - } else { - this.config.countUp = countUp - } - }, - countDownString: function (value) { - let countDown = parseInt(value.toString()) - if (isNaN(countDown) || countDown > 1000 || countDown < 1) { - this.config.countDown = 0 - } else { - this.config.countDown = countDown - } - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - -
启用当前记录健康检查 - -
检测端口 - - 默认{{parentConfig.port}} -   [修改] - -
- - -

通过尝试连接A/AAAA记录中的IP加此端口来确定当前记录是否健康。

-
-
超时时间 - - 默认{{parentConfig.timeoutSeconds}}秒 -   [修改] - -
- -
- - -
-
-
默认连续上线次数 - - 默认{{parentConfig.countUp}}次 -   [修改] - -
- -
- - -
-

连续检测{{config.countUp}}N次成功后,认为当前记录是在线的。

-
-
默认连续下线次数 - - 默认{{parentConfig.countDown}}次 -   [修改] - -
- -
- - -
-

连续检测{{config.countDown}}N次失败后,认为当前记录是离线的。

-
-
-
-
` -}) - -Vue.component("ns-create-records-table", { - props: ["v-types", "v-default-ttl"], - data: function () { - let types = this.vTypes - if (types == null) { - types = [] - } - - let defaultTTL = this.vDefaultTtl - if (defaultTTL != null) { - defaultTTL = parseInt(defaultTTL.toString()) - } - if (defaultTTL <= 0) { - defaultTTL = 600 - } - - return { - types: types, - defaultTTL: defaultTTL, - records: [ - { - name: "", - type: "A", - value: "", - routeCodes: [], - ttl: defaultTTL, - index: 0 - } - ], - lastIndex: 0, - isAddingRoutes: false // 是否正在添加线路 - } - }, - methods: { - add: function () { - this.records.push({ - name: "", - type: "A", - value: "", - routeCodes: [], - ttl: this.defaultTTL, - 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"], @@ -1515,54 +4619,243 @@ Vue.component("ns-route-selector", { ` }) -Vue.component("ns-access-log-box", { - props: ["v-access-log", "v-keyword"], +// 选择多个线路 +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 + that.supportChinaProvinceRoutes = resp.data.supportChinaProvinceRoutes + that.supportISPRoutes = resp.data.supportISPRoutes + that.supportWorldRegionRoutes = resp.data.supportWorldRegionRoutes + that.supportAgentRoutes = resp.data.supportAgentRoutes + that.publicCategories = resp.data.publicCategories + that.supportPublicRoutes = resp.data.supportPublicRoutes + + // provinces + let provinces = {} + if (resp.data.provinces != null && resp.data.provinces.length > 0) { + for (const province of resp.data.provinces) { + let countryCode = province.countryCode + if (typeof provinces[countryCode] == "undefined") { + provinces[countryCode] = [] + } + provinces[countryCode].push({ + name: province.name, + code: province.code + }) + } + } + that.provinces = provinces + }) + }, data: function () { - let accessLog = this.vAccessLog + let selectedRoutes = this.vRoutes + if (selectedRoutes == null) { + selectedRoutes = [] + } + + let inputName = this.name + if (typeof inputName != "string" || inputName.length == 0) { + inputName = "routeCodes" + } + return { - accessLog: accessLog + routeCode: "default", + inputName: inputName, + routes: [], + + + provinces: {}, // country code => [ province1, province2, ... ] + provinceRouteCode: "", + + isAdding: false, + routeType: "default", + selectedRoutes: selectedRoutes, + + supportChinaProvinceRoutes: false, + supportISPRoutes: false, + supportWorldRegionRoutes: false, + supportAgentRoutes: false, + supportPublicRoutes: false, + publicCategories: [] + } + }, + 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: { - showLog: function () { - let that = this - let requestId = this.accessLog.requestId - this.$parent.$children.forEach(function (v) { - if (v.deselect != null) { - v.deselect() - } - }) - this.select() + add: function () { + this.isAdding = true + this.routeType = "default" + this.routeCode = "default" + this.provinceRouteCode = "" + this.$emit("add") + }, + cancel: function () { + this.isAdding = false + this.$emit("cancel") + }, + confirm: function () { + if (this.routeCode.length == 0) { + return + } - teaweb.popup("/ns/clusters/accessLogs/viewPopup?requestId=" + requestId, { - width: "50em", - height: "24em", - onClose: function () { - that.deselect() + let that = this + + + // route + let selectedRoute = null + for (const route of this.routes) { + if (route.code == this.routeCode) { + selectedRoute = route + break } + } + + if (selectedRoute != null) { + // province route + if (this.provinceRouteCode.length > 0 && this.provinces[this.routeCode] != null) { + for (const province of this.provinces[this.routeCode]) { + if (province.code == this.provinceRouteCode) { + selectedRoute = { + name: selectedRoute.name + "-" + province.name, + code: province.code + } + break + } + } + } + + that.selectedRoutes.push(selectedRoute) + } + + this.$emit("change", this.selectedRoutes) + this.cancel() + }, + remove: function (index) { + this.selectedRoutes.$remove(index) + this.$emit("change", this.selectedRoutes) + } + } + , + template: `
+
+
+ + {{route.name}}   +
+
+
+
+ + + + + + + + + + + + + +
选择类型 * + +
选择线路 * + +
选择省/州 + +
+
+ +   取消 +
+
+ +

由于套餐限制,当前用户只能使用部分线路。

+
` +}) + +Vue.component("pay-method-selector", { + mounted: function () { + this.$emit("change", this.currentMethodCode) + + let that = this + Tea.action("/finance/methodOptions") + .success(function (resp) { + that.isLoading = false + that.balance = resp.data.balance + that.methods = resp.data.methods + that.canPay = resp.data.enablePay }) - }, - select: function () { - this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)" - }, - deselect: function () { - this.$refs.box.parentNode.style.cssText = "" + .post() + }, + data: function () { + return { + isLoading: true, + canPay: true, + balance: 0, + methods: [], + currentMethodCode: "@balance" } }, - template: `
- [{{accessLog.region}}] {{accessLog.remoteAddr}} [{{accessLog.timeLocal}}] [{{accessLog.networking}}] {{accessLog.questionType}} {{accessLog.questionName}} -> - - {{accessLog.recordType}} {{accessLog.recordValue}} -  [没有记录] - - -
- 线路: {{route.name}} - 递归DNS + methods: { + selectMethod: function (method) { + this.currentMethodCode = method.code + this.$emit("change", method.code) + } + }, + template: `
+
+
+ 余额支付 ({{balance}}元) +

使用余额支付

-
- 错误:[{{accessLog.error}}] +
+ {{method.name}} +

{{method.description}}

+
+ +
+ 暂时不支持线上支付,请联系客服购买。 +
+ +
` +}) + +Vue.component("plan-bandwidth-limit-view", { + props: ["value"], + template: `
+ 带宽限制:
` }) @@ -1643,1363 +4936,1161 @@ Vue.component("plan-price-view", {
` }) -Vue.component("plan-bandwidth-limit-view", { - props: ["value"], - template: `
- 带宽限制: -
` -}) - -Vue.component("pay-method-selector", { - mounted: function () { - this.$emit("change", this.currentMethodCode) - - let that = this - Tea.action("/finance/methodOptions") - .success(function (resp) { - that.isLoading = false - that.balance = resp.data.balance - that.methods = resp.data.methods - that.canPay = resp.data.enablePay - }) - .post() - }, +Vue.component("cache-cond-box", { data: function () { return { - isLoading: true, - canPay: true, - balance: 0, - methods: [], - currentMethodCode: "@balance" + conds: [], + addingExt: false, + addingPath: false, + + extDuration: null, + pathDuration: null } }, methods: { - selectMethod: function (method) { - this.currentMethodCode = method.code - this.$emit("change", method.code) - } - }, - template: `
-
-
- 余额支付 ({{balance}}元) -

使用余额支付

-
-
- {{method.name}} -

{{method.description}}

-
-
+ addExt: function () { + this.addingExt = !this.addingExt + this.addingPath = false -
- 暂时不支持线上支付,请联系客服购买。 -
- -
` -}) - -Vue.component("http-stat-config-box", { - props: ["v-stat-config", "v-is-location"], - data: function () { - let stat = this.vStatConfig - if (stat == null) { - stat = { - isPrior: false, - isOn: false + if (this.addingExt) { + let that = this + setTimeout(function () { + if (that.$refs.extInput != null) { + that.$refs.extInput.focus() + } + }) } - } - 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 + changeExtDuration: function (duration) { + this.extDuration = duration + }, + confirmExt: function () { + let value = this.$refs.extInput.value + if (value.length == 0) { + return + } - let that = this - teaweb.popup("/servers/server/settings/conds/addGroupPopup", { - height: "30em", - callback: function (resp) { - that.conds.groups.push(resp.data.group) - that.change() + let exts = [] + let pieces = value.split(/[,,]/) + pieces.forEach(function (v) { + v = v.trim() + v = v.replace(/\s+/, "") + if (v.length > 0) { + if (v[0] != ".") { + v = "." + v + } + exts.push(v) } }) - }, - 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() - } + + this.conds.push({ + type: "url-extension", + value: JSON.stringify(exts), + duration: this.extDuration }) + this.$refs.extInput.value = "" + this.cancel() }, - 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; + addPath: function () { + this.addingExt = false + this.addingPath = !this.addingPath + + if (this.addingPath) { + let that = this + setTimeout(function () { + if (that.$refs.pathInput != null) { + that.$refs.pathInput.focus() + } + }) } - return cond.param + " " + cond.operator + }, + changePathDuration: function (duration) { + this.pathDuration = duration + }, + confirmPath: function () { + let value = this.$refs.pathInput.value + if (value.length == 0) { + return + } + + if (value[0] != "/") { + value = "/" + value + } + + this.conds.push({ + type: "url-prefix", + value: value, + duration: this.pathDuration + }) + this.$refs.pathInput.value = "" + this.cancel() + }, + remove: function (index) { + this.conds.$remove(index) + }, + cancel: function () { + this.addingExt = false + this.addingPath = false } }, template: `
- -
- - - - - - -
分组{{groupIndex+1}} - - - {{cond.param}} {{cond.operator}} - {{typeName(cond)}}: - {{cond.value}} - - - - {{group.connector}}   - - - -
-
+ +
+
+ 扩展名 + 路径:{{cond.value}}   ()   +
- - - - - - - -
分组之间关系 - -

- 只要满足其中一个条件分组即可。 - 需要满足所有条件分组。 -

-
- -
- +
+
+ + +
+
+
+ +
+
+ +
+
+ 确定   +
-
+
+ + +
+
+
+ +
+
+ +
+
+ 确定   +
+
+
+ +
+   + +
` }) -Vue.component("ssl-config-box", { - props: [ - "v-ssl-policy", - "v-protocol", - "v-server-id", - "v-support-http3" - ], - created: function () { - let that = this - setTimeout(function () { - that.sortableCipherSuites() - }, 100) - }, +// 域名列表 +Vue.component("domains-box", { + props: ["v-domains", "name", "v-support-wildcard"], data: function () { - let policy = this.vSslPolicy - if (policy == null) { - policy = { - id: 0, - isOn: true, - certRefs: [], - certs: [], - clientCARefs: [], - clientCACerts: [], - clientAuthType: 0, - minVersion: "TLS 1.1", - hsts: null, - cipherSuitesIsOn: false, - cipherSuites: [], - http2Enabled: true, - http3Enabled: false, - ocspIsOn: false - } - } else { - if (policy.certRefs == null) { - policy.certRefs = [] - } - if (policy.certs == null) { - policy.certs = [] - } - if (policy.clientCARefs == null) { - policy.clientCARefs = [] - } - if (policy.clientCACerts == null) { - policy.clientCACerts = [] - } - if (policy.cipherSuites == null) { - policy.cipherSuites = [] - } + let domains = this.vDomains + if (domains == null) { + domains = [] } - let hsts = policy.hsts - let hstsMaxAgeString = "31536000" - if (hsts == null) { - hsts = { - isOn: false, - maxAge: 31536000, - includeSubDomains: false, - preload: false, - domains: [] - } + let realName = "domainsJSON" + if (this.name != null && typeof this.name == "string") { + realName = this.name } - if (hsts.maxAge != null) { - hstsMaxAgeString = hsts.maxAge.toString() + + let supportWildcard = true + if (typeof this.vSupportWildcard == "boolean") { + supportWildcard = this.vSupportWildcard } return { - policy: policy, + domains: domains, - // hsts - hsts: hsts, - hstsOptionsVisible: false, - hstsDomainAdding: false, - hstsMaxAgeString: hstsMaxAgeString, - addingHstsDomain: "", - hstsDomainEditingIndex: -1, + mode: "single", // single | batch + batchDomains: "", - // 相关数据 - 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, + isAdding: false, + addingDomain: "", - // 高级选项 - moreOptionsVisible: false + isEditing: false, + editingIndex: -1, + + realName: realName, + supportWildcard: supportWildcard } }, watch: { - hsts: { - deep: true, - handler: function () { - this.policy.hsts = this.hsts - } - } - }, - methods: { - // 删除证书 - removeCert: function (index) { - let that = this - teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { - that.policy.certRefs.$remove(index) - that.policy.certs.$remove(index) - }) - }, - - // 选择证书 - selectCert: function () { - let that = this - let selectedCertIds = [] - if (this.policy != null && this.policy.certs.length > 0) { - this.policy.certs.forEach(function (cert) { - selectedCertIds.push(cert.id.toString()) - }) - } - let serverId = this.vServerId - if (serverId == null) { - serverId = 0 - } - teaweb.popup("/servers/certs/selectPopup?selectedCertIds=" + selectedCertIds + "&serverId=" + serverId, { - width: "50em", - height: "30em", - callback: function (resp) { - if (resp.data.cert != null && resp.data.certRef != null) { - that.policy.certRefs.push(resp.data.certRef) - that.policy.certs.push(resp.data.cert) - } - if (resp.data.certs != null && resp.data.certRefs != null) { - that.policy.certRefs.$pushAll(resp.data.certRefs) - that.policy.certs.$pushAll(resp.data.certs) - } - that.$forceUpdate() - } - }) - }, - - // 上传证书 - uploadCert: function () { - let that = this - let serverId = this.vServerId - if (typeof serverId != "number" && typeof serverId != "string") { - serverId = 0 - } - teaweb.popup("/servers/certs/uploadPopup?serverId=" + serverId, { - height: "30em", - callback: function (resp) { - teaweb.success("上传成功", function () { - that.policy.certRefs.push(resp.data.certRef) - that.policy.certs.push(resp.data.cert) - }) - } - }) - }, - - // 批量上传 - uploadBatch: function () { - let that = this - let serverId = this.vServerId - if (typeof serverId != "number" && typeof serverId != "string") { - serverId = 0 - } - teaweb.popup("/servers/certs/uploadBatchPopup?serverId=" + serverId, { - callback: function (resp) { - if (resp.data.cert != null) { - that.policy.certRefs.push(resp.data.certRef) - that.policy.certs.push(resp.data.cert) - } - if (resp.data.certs != null) { - that.policy.certRefs.$pushAll(resp.data.certRefs) - that.policy.certs.$pushAll(resp.data.certs) - } - that.$forceUpdate() - } - }) - }, - - // 申请证书 - requestCert: function () { - // 已经在证书中的域名 - let excludeServerNames = [] - if (this.policy != null && this.policy.certs.length > 0) { - this.policy.certs.forEach(function (cert) { - excludeServerNames.$pushAll(cert.dnsNames) - }) - } - - let that = this - teaweb.popup("/servers/server/settings/https/requestCertPopup?serverId=" + this.vServerId + "&excludeServerNames=" + excludeServerNames.join(","), { - callback: function () { - that.policy.certRefs.push(resp.data.certRef) - that.policy.certs.push(resp.data.cert) - } - }) - }, - - // 更多选项 - changeOptionsVisible: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - }, - - // 格式化时间 - formatTime: function (timestamp) { - return new Date(timestamp * 1000).format("Y-m-d") - }, - - // 格式化加密套件 - formatCipherSuite: function (cipherSuite) { - return cipherSuite.replace(/(AES|3DES)/, "$1") - }, - - // 添加单个套件 - addCipherSuite: function (cipherSuite) { - if (!this.policy.cipherSuites.$contains(cipherSuite)) { - this.policy.cipherSuites.push(cipherSuite) - } - this.allCipherSuites.$removeValue(cipherSuite) - }, - - // 删除单个套件 - removeCipherSuite: function (cipherSuite) { - let that = this - teaweb.confirm("确定要删除此套件吗?", function () { - that.policy.cipherSuites.$removeValue(cipherSuite) - that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$findAll(function (k, v) { - return !that.policy.cipherSuites.$contains(v) - }) - }) - }, - - // 清除所选套件 - clearCipherSuites: function () { - let that = this - teaweb.confirm("确定要清除所有已选套件吗?", function () { - that.policy.cipherSuites = [] - that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$copy() - }) - }, - - // 批量添加套件 - addBatchCipherSuites: function (suites) { - var that = this - teaweb.confirm("确定要批量添加套件?", function () { - suites.$each(function (k, v) { - if (that.policy.cipherSuites.$contains(v)) { - return - } - that.policy.cipherSuites.push(v) - }) - }) - }, - - /** - * 套件拖动排序 - */ - sortableCipherSuites: function () { - var box = document.querySelector(".cipher-suites-box") - Sortable.create(box, { - draggable: ".label", - handle: ".icon.handle", - onStart: function () { - - }, - onUpdate: function (event) { - - } - }) - }, - - // 显示所有套件 - showAllCipherSuites: function () { - this.cipherSuitesVisible = !this.cipherSuitesVisible - }, - - // 显示HSTS更多选项 - showMoreHSTS: function () { - this.hstsOptionsVisible = !this.hstsOptionsVisible; - if (this.hstsOptionsVisible) { - this.changeHSTSMaxAge() + vSupportWildcard: function (v) { + if (typeof v == "boolean") { + this.supportWildcard = v } }, - - // 监控HSTS有效期修改 - changeHSTSMaxAge: function () { - var v = parseInt(this.hstsMaxAgeString) - if (isNaN(v) || v < 0) { - this.hsts.maxAge = 0 - this.hsts.days = "-" - return - } - this.hsts.maxAge = v - this.hsts.days = v / 86400 - if (this.hsts.days == 0) { - this.hsts.days = "-" - } - }, - - // 设置HSTS有效期 - setHSTSMaxAge: function (maxAge) { - this.hstsMaxAgeString = maxAge.toString() - this.changeHSTSMaxAge() - }, - - // 添加HSTS域名 - addHstsDomain: function () { - this.hstsDomainAdding = true - this.hstsDomainEditingIndex = -1 + mode: function (mode) { let that = this setTimeout(function () { - that.$refs.addingHstsDomain.focus() + if (mode == "single") { + if (that.$refs.addingDomain != null) { + that.$refs.addingDomain.focus() + } + } else if (mode == "batch") { + if (that.$refs.batchDomains != null) { + that.$refs.batchDomains.focus() + } + } }, 100) - }, - - // 修改HSTS域名 - editHstsDomain: function (index) { - this.hstsDomainEditingIndex = index - this.addingHstsDomain = this.hsts.domains[index] - this.hstsDomainAdding = true - let that = this - setTimeout(function () { - that.$refs.addingHstsDomain.focus() - }, 100) - }, - - // 确认HSTS域名添加 - confirmAddHstsDomain: function () { - this.addingHstsDomain = this.addingHstsDomain.trim() - if (this.addingHstsDomain.length == 0) { - return; - } - if (this.hstsDomainEditingIndex > -1) { - this.hsts.domains[this.hstsDomainEditingIndex] = this.addingHstsDomain - } else { - this.hsts.domains.push(this.addingHstsDomain) - } - this.cancelHstsDomainAdding() - }, - - // 取消HSTS域名添加 - cancelHstsDomainAdding: function () { - this.hstsDomainAdding = false - this.addingHstsDomain = "" - this.hstsDomainEditingIndex = -1 - }, - - // 删除HSTS域名 - removeHstsDomain: function (index) { - this.cancelHstsDomainAdding() - this.hsts.domains.$remove(index) - }, - - // 选择客户端CA证书 - selectClientCACert: function () { - let that = this - teaweb.popup("/servers/certs/selectPopup?isCA=1", { - width: "50em", - height: "30em", - callback: function (resp) { - if (resp.data.cert != null && resp.data.certRef != null) { - that.policy.clientCARefs.push(resp.data.certRef) - that.policy.clientCACerts.push(resp.data.cert) - } - if (resp.data.certs != null && resp.data.certRefs != null) { - that.policy.clientCARefs.$pushAll(resp.data.certRefs) - that.policy.clientCACerts.$pushAll(resp.data.certs) - } - that.$forceUpdate() - } - }) - }, - - // 上传CA证书 - uploadClientCACert: function () { - let that = this - teaweb.popup("/servers/certs/uploadPopup?isCA=1", { - height: "28em", - callback: function (resp) { - teaweb.success("上传成功", function () { - that.policy.clientCARefs.push(resp.data.certRef) - that.policy.clientCACerts.push(resp.data.cert) - }) - } - }) - }, - - // 删除客户端CA证书 - removeClientCACert: function (index) { - let that = this - teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { - that.policy.clientCARefs.$remove(index) - that.policy.clientCACerts.$remove(index) - }) - } - }, - template: `
-

SSL/TLS相关配置

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用HTTP/2 -
- - -
-
启用HTTP/3 -
- - -
-
设置证书 -
-
- {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   -
-
-
-
- 选择或上传证书后HTTPSTLS服务才能生效。 -
-
-   - |   -   -   - |   - -
TLS最低版本 - -
加密算法套件(CipherSuites) -
- - -
-
-
-
- 已添加套件({{policy.cipherSuites.length}}): -
- -   - -
-
- - - -

点击可选套件添加。

-
-
开启HSTS -
- - -
-

- 开启后,会自动在响应Header中加入 - Strict-Transport-Security: - ... - max-age={{hsts.maxAge}} - ; includeSubDomains - ; preload - - - 修改 - -

-
HSTS有效时间(max-age) -
-
- -
-
- 秒 -
-
{{hsts.days}}天
-
-

- [1年/365天]     - [6个月/182.5天]     - [1个月/30天] -

-
HSTS包含子域名(includeSubDomains) -
- - -
-
HSTS预加载(preload) -
- - -
-
HSTS生效的域名 -
- {{domain}} -   - - - -
-
-
- -
-
- -   取消 -
-
-
- -
-

如果没有设置域名的话,则默认支持所有的域名。

-
OCSP Stapling -

选中表示启用OCSP Stapling。

-
客户端认证方式 - -
客户端认证CA证书 -
-
- {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   -
-
-
-   - -

用来校验客户端证书以增强安全性,通常不需要设置。

-
-
-
` -}) - -// Action列表 -Vue.component("http-firewall-actions-view", { - props: ["v-actions"], - template: `
-
- {{action.name}} ({{action.code.toUpperCase()}}) -
- [{{action.options.status}}] - - [分组] - [网站] - [网站和策略] - - - 黑名单 - 白名单 - 灰名单 - -
-
-
-
` -}) - -// 显示WAF规则的标签 -Vue.component("http-firewall-rule-label", { - props: ["v-rule"], - data: function () { - return { - rule: this.vRule - } - }, - methods: { - showErr: function (err) { - teaweb.popupTip("规则校验错误,请修正:" + teaweb.encodeHTML(err) + "") - }, - calculateParamName: function (param) { - let paramName = "" - if (param != null) { - window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { - if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { - paramName = checkpoint.name - } - }) - } - return paramName - }, - calculateParamDescription: function (param) { - let paramName = "" - let paramDescription = "" - if (param != null) { - window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { - if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { - paramName = checkpoint.name - paramDescription = checkpoint.description - } - }) - } - return paramName + ": " + paramDescription - }, - operatorName: function (operatorCode) { - let operatorName = operatorCode - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorName = v.name - } - }) - } - - return operatorName - }, - operatorDescription: function (operatorCode) { - let operatorName = operatorCode - let operatorDescription = "" - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorName = v.name - operatorDescription = v.description - } - }) - } - - return operatorName + ": " + operatorDescription - }, - operatorDataType: function (operatorCode) { - let operatorDataType = "none" - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorDataType = v.dataType - } - }) - } - - return operatorDataType - }, - isEmptyString: function (v) { - return typeof v == "string" && v.length == 0 - } - }, - template: `
-
- {{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} - - - - {{rule.checkpointOptions.period}}秒内请求数 - - - - - 允许{{rule.checkpointOptions.allowDomains}} - 禁止{{rule.checkpointOptions.denyDomains}} - - - - | {{paramFilter.code}} - <{{operatorName(rule.operator)}}> - {{rule.value}} - [空] - - - - ({{rule.description}}) - - 规则错误 -
-
` -}) - -// 缓存条件列表 -Vue.component("http-cache-refs-box", { - props: ["v-cache-refs"], - data: function () { - let refs = this.vCacheRefs - if (refs == null) { - refs = [] - } - return { - refs: refs - } - }, - methods: { - timeUnitName: function (unit) { - switch (unit) { - case "ms": - return "毫秒" - case "second": - return "秒" - case "minute": - return "分钟" - case "hour": - return "小时" - case "day": - return "天" - case "week": - return "周 " - } - return unit - } - }, - template: `
- - -

暂时还没有缓存条件。

-
- - - - - - - - - - - -
缓存条件缓存时间
- - - - - 忽略URI参数 - - {{cacheRef.minSize.count}}{{cacheRef.minSize.unit}} - - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} - - 0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} - {{cacheRef.methods.join(", ")}} - Expires - 状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}} - 分片缓存 - Range回源 - If-None-Match - If-Modified-Since - 支持异步 - - {{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}} - 不缓存 -
-
-
-
` -}) - -Vue.component("ssl-certs-box", { - props: [ - "v-certs", // 证书列表 - "v-cert", // 单个证书 - "v-protocol", // 协议:https|tls - "v-view-size", // 弹窗尺寸:normal, mini - "v-single-mode", // 单证书模式 - "v-description", // 描述文字 - "v-domains", // 搜索的域名列表或者函数 - "v-user-id" // 用户ID - ], - data: function () { - let certs = this.vCerts - if (certs == null) { - certs = [] - } - if (this.vCert != null) { - certs.push(this.vCert) - } - - let description = this.vDescription - if (description == null || typeof (description) != "string") { - description = "" - } - - return { - certs: certs, - description: description - } - }, - methods: { - certIds: function () { - return this.certs.map(function (v) { - return v.id - }) - }, - // 删除证书 - removeCert: function (index) { - let that = this - teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { - that.certs.$remove(index) - }) - }, - - // 选择证书 - selectCert: function () { - let that = this - let width = "54em" - let height = "32em" - let viewSize = this.vViewSize - if (viewSize == null) { - viewSize = "normal" - } - if (viewSize == "mini") { - width = "35em" - height = "20em" - } - - let searchingDomains = [] - if (this.vDomains != null) { - if (typeof this.vDomains == "function") { - let resultDomains = this.vDomains() - if (resultDomains != null && typeof resultDomains == "object" && (resultDomains instanceof Array)) { - searchingDomains = resultDomains - } - } else if (typeof this.vDomains == "object" && (this.vDomains instanceof Array)) { - searchingDomains = this.vDomains - } - if (searchingDomains.length > 10000) { - searchingDomains = searchingDomains.slice(0, 10000) - } - } - - let selectedCertIds = this.certs.map(function (cert) { - return cert.id - }) - let userId = this.vUserId - if (userId == null) { - userId = 0 - } - - teaweb.popup("/servers/certs/selectPopup?viewSize=" + viewSize + "&searchingDomains=" + window.encodeURIComponent(searchingDomains.join(",")) + "&selectedCertIds=" + selectedCertIds.join(",") + "&userId=" + userId, { - width: width, - height: height, - callback: function (resp) { - if (resp.data.cert != null) { - that.certs.push(resp.data.cert) - } - if (resp.data.certs != null) { - that.certs.$pushAll(resp.data.certs) - } - that.$forceUpdate() - } - }) - }, - - // 上传证书 - uploadCert: function () { - let that = this - let userId = this.vUserId - if (typeof userId != "number" && typeof userId != "string") { - userId = 0 - } - teaweb.popup("/servers/certs/uploadPopup?userId=" + userId, { - height: "28em", - callback: function (resp) { - teaweb.success("上传成功", function () { - if (resp.data.cert != null) { - that.certs.push(resp.data.cert) - } - if (resp.data.certs != null) { - that.certs.$pushAll(resp.data.certs) - } - that.$forceUpdate() - }) - } - }) - }, - - // 批量上传 - uploadBatch: function () { - let that = this - let userId = this.vUserId - if (typeof userId != "number" && typeof userId != "string") { - userId = 0 - } - teaweb.popup("/servers/certs/uploadBatchPopup?userId=" + userId, { - callback: function (resp) { - if (resp.data.cert != null) { - that.certs.push(resp.data.cert) - } - if (resp.data.certs != null) { - that.certs.$pushAll(resp.data.certs) - } - that.$forceUpdate() - } - }) - }, - - // 格式化时间 - formatTime: function (timestamp) { - return new Date(timestamp * 1000).format("Y-m-d") - }, - - // 判断是否显示选择|上传按钮 - buttonsVisible: function () { - return this.vSingleMode == null || !this.vSingleMode || this.certs == null || this.certs.length == 0 - } - }, - template: `
- -
-
- {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   -
-
-
-
- 选择或上传证书后HTTPSTLS服务才能生效。 - {{description}} -
-
-
-   - |   -   -   -
-
` -}) - -Vue.component("http-host-redirect-box", { - props: ["v-redirects"], - mounted: function () { - let that = this - sortTable(function (ids) { - let newRedirects = [] - ids.forEach(function (id) { - that.redirects.forEach(function (redirect) { - if (redirect.id == id) { - newRedirects.push(redirect) - } - }) - }) - that.updateRedirects(newRedirects) - }) - }, - data: function () { - let redirects = this.vRedirects - if (redirects == null) { - redirects = [] - } - - let id = 0 - redirects.forEach(function (v) { - id++ - v.id = id - }) - - return { - redirects: redirects, - statusOptions: [ - {"code": 301, "text": "Moved Permanently"}, - {"code": 308, "text": "Permanent Redirect"}, - {"code": 302, "text": "Found"}, - {"code": 303, "text": "See Other"}, - {"code": 307, "text": "Temporary Redirect"} - ], - id: id } }, methods: { add: function () { + this.isAdding = true let that = this - window.UPDATING_REDIRECT = null - - teaweb.popup("/servers/server/settings/redirects/createPopup", { - width: "50em", - height: "36em", - callback: function (resp) { - that.id++ - resp.data.redirect.id = that.id - that.redirects.push(resp.data.redirect) - that.change() - } - }) - }, - update: function (index, redirect) { - let that = this - window.UPDATING_REDIRECT = redirect - - teaweb.popup("/servers/server/settings/redirects/createPopup", { - width: "50em", - height: "36em", - callback: function (resp) { - resp.data.redirect.id = redirect.id - Vue.set(that.redirects, index, resp.data.redirect) - that.change() - } - }) - }, - remove: function (index) { - let that = this - teaweb.confirm("确定要删除这条跳转规则吗?", function () { - that.redirects.$remove(index) - that.change() - }) - }, - change: function () { - let that = this - setTimeout(function (){ - that.$emit("change", that.redirects) + setTimeout(function () { + that.$refs.addingDomain.focus() }, 100) }, - updateRedirects: function (newRedirects) { - this.redirects = newRedirects + confirm: function () { + if (this.mode == "batch") { + this.confirmBatch() + return + } + + let that = this + + // 删除其中的空格 + this.addingDomain = this.addingDomain.replace(/\s/g, "") + + if (this.addingDomain.length == 0) { + teaweb.warn("请输入要添加的域名", function () { + that.$refs.addingDomain.focus() + }) + return + } + + // 基本校验 + if (this.supportWildcard) { + if (this.addingDomain[0] == "~") { + let expr = this.addingDomain.substring(1) + try { + new RegExp(expr) + } catch (e) { + teaweb.warn("正则表达式错误:" + e.message, function () { + that.$refs.addingDomain.focus() + }) + return + } + } + } else { + if (/[*~^]/.test(this.addingDomain)) { + teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () { + that.$refs.addingDomain.focus() + }) + return + } + } + + if (this.isEditing && this.editingIndex >= 0) { + this.domains[this.editingIndex] = this.addingDomain + } else { + // 分割逗号(,)、顿号(、) + if (this.addingDomain.match("[,、,;]")) { + let domainList = this.addingDomain.split(new RegExp("[,、,;]")) + domainList.forEach(function (v) { + if (v.length > 0) { + that.domains.push(v) + } + }) + } else { + this.domains.push(this.addingDomain) + } + } + this.cancel() this.change() + }, + confirmBatch: function () { + let domains = this.batchDomains.split("\n") + let realDomains = [] + let that = this + let hasProblems = false + domains.forEach(function (domain) { + if (hasProblems) { + return + } + if (domain.length == 0) { + return + } + if (that.supportWildcard) { + if (domain == "~") { + let expr = domain.substring(1) + try { + new RegExp(expr) + } catch (e) { + hasProblems = true + teaweb.warn("正则表达式错误:" + e.message, function () { + that.$refs.batchDomains.focus() + }) + return + } + } + } else { + if (/[*~^]/.test(domain)) { + hasProblems = true + teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () { + that.$refs.batchDomains.focus() + }) + return + } + } + realDomains.push(domain) + }) + if (hasProblems) { + return + } + if (realDomains.length == 0) { + teaweb.warn("请输入要添加的域名", function () { + that.$refs.batchDomains.focus() + }) + return + } + + realDomains.forEach(function (domain) { + that.domains.push(domain) + }) + this.cancel() + this.change() + }, + edit: function (index) { + this.addingDomain = this.domains[index] + this.isEditing = true + this.editingIndex = index + let that = this + setTimeout(function () { + that.$refs.addingDomain.focus() + }, 50) + }, + remove: function (index) { + this.domains.$remove(index) + this.change() + }, + cancel: function () { + this.isAdding = false + this.mode = "single" + this.batchDomains = "" + this.isEditing = false + this.editingIndex = -1 + this.addingDomain = "" + }, + change: function () { + this.$emit("change", this.domains) } }, template: `
- - - - [创建] - -
+ +
+ + [正则] + [后缀] + [泛域名] + {{domain}} + +   +   + + +   +   + + +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+
+ +   +
+
+

支持普通域名(example.com)、泛域名(*.example.com、域名后缀(以点号开头,如.example.com)和正则表达式(以波浪号开头,如~.*.example.com;如果域名后有端口,请加上端口号。

+

只支持普通域名(example.comwww.example.com)。

+
+
+
+ +
+
` +}) -

暂时还没有URL跳转规则。

-
- - - - - - - - - - - - - - - - + + + + + + + + + + +
跳转前跳转后HTTP状态码状态操作
-
- {{redirect.beforeURL}} -
- URL跳转 - 匹配前缀 - 正则匹配 - 精准匹配 - 排除:{{domain}} - 仅限:{{domain}} -
+Vue.component("http-access-log-box", { + props: ["v-access-log"], + 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" + } + } + 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: "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.requestMethod}} {{accessLog.scheme}}://{{accessLog.host}}{{accessLog.requestURI}} {{accessLog.proto}}" {{accessLog.status}} [cached] waf {{accessLog.firewallActions}} - {{tag}} - 耗时:{{formatCost(accessLog.requestTime)}} ms +   +
` +}) + +Vue.component("http-access-log-config-box", { + props: ["v-access-log-config", "v-fields", "v-default-field-codes", "v-access-log-policies", "v-is-location", "v-is-group"], + data: function () { + let that = this + + // 初始化 + setTimeout(function () { + that.changeFields() + that.changePolicy() + }, 100) + + let accessLog = { + isPrior: false, + isOn: false, + fields: [], + status1: true, + status2: true, + status3: true, + status4: true, + status5: true, + + storageOnly: false, + storagePolicies: [], + + firewallOnly: 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) + } + }) + this.vAccessLogPolicies.forEach(function (v) { + v.isChecked = accessLog.storagePolicies.$contains(v.id) + }) + + return { + accessLog: accessLog, + showAdvancedOptions: false + } + }, + methods: { + changeFields: function () { + this.accessLog.fields = this.vFields.filter(function (v) { + return v.isChecked + }).map(function (v) { + return v.code + }) + }, + changePolicy: function () { + this.accessLog.storagePolicies = this.vAccessLogPolicies.filter(function (v) { + return v.isChecked + }).map(function (v) { + return v.id + }) + }, + changeAdvanced: function (v) { + this.showAdvancedOptions = v + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - -
启用访问日志 +
+ + +
+
要存储的访问日志字段 +
+ + +
+
要存储的访问日志状态码 +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
选择输出的日志策略 + 暂时还没有缓存策略。 +
+
+ +
-
- 所有域名 - - {{redirect.domainsBefore[0]}} - {{redirect.domainsBefore[0]}}等{{redirect.domainsBefore.length}}个域名 - -
- 域名跳转 - {{redirect.domainAfterScheme}} - 忽略端口 -
-
-
- 所有端口 - - {{redirect.portsBefore.join(", ")}} - {{redirect.portsBefore.slice(0, 5).join(", ")}}等{{redirect.portsBefore.length}}个端口 - -
- 端口跳转 - {{redirect.portAfterScheme}} -
-
- -
- 匹配条件 -
-
-> - {{redirect.afterURL}} - {{redirect.domainAfter}} - {{redirect.portAfter}} - - {{redirect.status}} - 默认 - - 修改   - 删除 -
-

所有规则匹配顺序为从上到下,可以拖动左侧的排序。

+
+
是否只输出到日志策略 +
+ + +
+

选中表示只输出日志到日志策略,而停止默认的日志存储。

+
只记录WAF相关日志 + +

选中后只记录WAF相关的日志。

+
+
+
` +}) + +// 访问日志搜索框 +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("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-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("http-cache-config-box", { + props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy", "v-web-id"], + data: function () { + let cacheConfig = this.vCacheConfig + if (cacheConfig == null) { + cacheConfig = { + isPrior: false, + isOn: false, + addStatusHeader: true, + addAgeHeader: false, + enableCacheControlMaxAge: false, + cacheRefs: [], + purgeIsOn: false, + purgeKey: "", + disablePolicyRefs: false + } + } + if (cacheConfig.cacheRefs == null) { + cacheConfig.cacheRefs = [] + } + + let maxBytes = null + if (this.vCachePolicy != null && this.vCachePolicy.maxBytes != null) { + maxBytes = this.vCachePolicy.maxBytes + } + + // key + if (cacheConfig.key == null) { + // use Vue.set to activate vue events + Vue.set(cacheConfig, "key", { + isOn: false, + scheme: "https", + host: "" + }) + } + + return { + cacheConfig: cacheConfig, + moreOptionsVisible: false, + enablePolicyRefs: !cacheConfig.disablePolicyRefs, + maxBytes: maxBytes, + + searchBoxVisible: false, + searchKeyword: "", + + keyOptionsVisible: false + } + }, + watch: { + enablePolicyRefs: function (v) { + this.cacheConfig.disablePolicyRefs = !v + }, + searchKeyword: function (v) { + this.$refs.cacheRefsConfigBoxRef.search(v) + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.cacheConfig.isPrior) && this.cacheConfig.isOn + }, + isPlus: function () { + return true + }, + generatePurgeKey: function () { + let r = Math.random().toString() + Math.random().toString() + let s = r.replace(/0\./g, "") + .replace(/\./g, "") + let result = "" + for (let i = 0; i < s.length; i++) { + result += String.fromCharCode(parseInt(s.substring(i, i + 1)) + ((Math.random() < 0.5) ? "a" : "A").charCodeAt(0)) + } + this.cacheConfig.purgeKey = result + }, + showMoreOptions: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + }, + changeStale: function (stale) { + this.cacheConfig.stale = stale + }, + showSearchBox: function () { + this.searchBoxVisible = !this.searchBoxVisible + if (this.searchBoxVisible) { + let that = this + setTimeout(function () { + that.$refs.searchBox.focus() + }) + } else { + this.searchKeyword = "" + } + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用缓存 +
+ + +
+
缓存主域名 +
默认   [修改]
+
使用主域名:{{cacheConfig.key.scheme}}://{{cacheConfig.key.host}}   [修改]
+
+
+ + + + + + + + + +
启用主域名 +

启用主域名后,所有缓存键值中的协议和域名部分都会修改为主域名,用来实现缓存不区分域名。

+
主域名 * +
+
+ +
+
+ +
+
+

此域名必须是当前网站已绑定域名,在刷新缓存时也需要使用此域名。

+
+ +
+
+ 收起选项更多选项 +
使用默认缓存条件 + +

选中后使用系统中已经定义的默认缓存条件。

+
添加X-Cache报头 + +

选中后自动在响应报头中增加X-Cache: BYPASS|MISS|HIT|PURGE;在浏览器端查看X-Cache值时请先禁用浏览器缓存,避免影响观察。

+
添加Age Header + +

选中后自动在响应Header中增加Age: [存活时间秒数]

+
支持源站控制有效时间 + +

选中后表示支持源站在Header中设置的Cache-Control: max-age=[有效时间秒数]

+
允许PURGE + +

允许使用PURGE方法清除某个URL缓存。

+
PURGE Key * + +

[随机生成]。需要在PURGE方法调用时加入Edge-Purge-Key: {{cacheConfig.purgeKey}} Header。只能包含字符、数字、下划线。

+
+ + +
+ +
+
+ +
+

缓存条件   [添加]   [搜索] +
+ + +
+

+
` @@ -3366,1115 +6457,81 @@ Vue.component("http-cache-ref-box", { ` }) -// 请求限制 -Vue.component("http-request-limit-config-box", { - props: ["v-request-limit-config", "v-is-group", "v-is-location"], +// 缓存条件列表 +Vue.component("http-cache-refs-box", { + props: ["v-cache-refs"], 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" - } - } + let refs = this.vCacheRefs + if (refs == null) { + refs = [] } 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 - } + refs: refs } }, 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 + timeUnitName: function (unit) { + switch (unit) { + case "ms": + return "毫秒" + case "second": + return "秒" + case "minute": + return "分钟" + case "hour": + return "小时" + case "day": + return "天" + case "week": + return "周 " } - - this.values.push(this.addingValue) - this.cancel() - }, - cancel: function () { - this.isAdding = false - this.addingValue = {"pattern": "", "replacement": "", "isCaseInsensitive": false, "isRegexp": false} + return unit } }, template: `
- -
-
- {{value.pattern}} => {{value.replacement}}[空] - -
-
-
- - - - - - - - - - - - - + + +

暂时还没有缓存条件。

+
+
替换前内容 *
替换后内容
是否忽略大小写 - -
+ + + + + + + + + +
缓存条件缓存时间
+ + + + + 忽略URI参数 + + {{cacheRef.minSize.count}}{{cacheRef.minSize.unit}} + - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} + + 0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} + {{cacheRef.methods.join(", ")}} + Expires + 状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}} + 分片缓存 + Range回源 + If-None-Match + If-Modified-Since + 支持异步 + + {{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}} + 不缓存 +
- -
-   - -
-
-
- -
-
` -}) - -// 浏览条件列表 -Vue.component("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("cache-cond-box", { - data: function () { - return { - conds: [], - addingExt: false, - addingPath: false, - - extDuration: null, - pathDuration: null - } - }, - methods: { - addExt: function () { - this.addingExt = !this.addingExt - this.addingPath = false - - if (this.addingExt) { - let that = this - setTimeout(function () { - if (that.$refs.extInput != null) { - that.$refs.extInput.focus() - } - }) - } - }, - changeExtDuration: function (duration) { - this.extDuration = duration - }, - confirmExt: function () { - let value = this.$refs.extInput.value - if (value.length == 0) { - return - } - - let exts = [] - let pieces = value.split(/[,,]/) - pieces.forEach(function (v) { - v = v.trim() - v = v.replace(/\s+/, "") - if (v.length > 0) { - if (v[0] != ".") { - v = "." + v - } - exts.push(v) - } - }) - - this.conds.push({ - type: "url-extension", - value: JSON.stringify(exts), - duration: this.extDuration - }) - this.$refs.extInput.value = "" - this.cancel() - }, - addPath: function () { - this.addingExt = false - this.addingPath = !this.addingPath - - if (this.addingPath) { - let that = this - setTimeout(function () { - if (that.$refs.pathInput != null) { - that.$refs.pathInput.focus() - } - }) - } - }, - changePathDuration: function (duration) { - this.pathDuration = duration - }, - confirmPath: function () { - let value = this.$refs.pathInput.value - if (value.length == 0) { - return - } - - if (value[0] != "/") { - value = "/" + value - } - - this.conds.push({ - type: "url-prefix", - value: value, - duration: this.pathDuration - }) - this.$refs.pathInput.value = "" - this.cancel() - }, - remove: function (index) { - this.conds.$remove(index) - }, - cancel: function () { - this.addingExt = false - this.addingPath = false - } - }, - template: `
- -
-
- 扩展名 - 路径:{{cond.value}}   ()   - -
-
-
- - -
-
-
- -
-
- -
-
- 确定   -
-
-
- - -
-
-
- -
-
- -
-
- 确定   -
-
-
- -
-   - -
-
` -}) - -Vue.component("http-firewall-config-box", { - props: ["v-firewall-config", "v-is-location", "v-firewall-policy"], - data: function () { - let firewall = this.vFirewallConfig - if (firewall == null) { - firewall = { - isPrior: false, - isOn: false, - firewallPolicyId: 0, - ignoreGlobalRules: false, - defaultCaptchaType: "none" - } - } - - if (firewall.defaultCaptchaType == null || firewall.defaultCaptchaType.length == 0) { - firewall.defaultCaptchaType = "none" - } - - let allCaptchaTypes = window.WAF_CAPTCHA_TYPES.$copy() - - // geetest - let geeTestIsOn = false - if (this.vFirewallPolicy != null && this.vFirewallPolicy.captchaAction != null && this.vFirewallPolicy.captchaAction.geeTestConfig != null) { - geeTestIsOn = this.vFirewallPolicy.captchaAction.geeTestConfig.isOn - } - - // 如果没有启用geetest,则还原 - if (!geeTestIsOn && firewall.defaultCaptchaType == "geetest") { - firewall.defaultCaptchaType = "none" - } - - return { - firewall: firewall, - moreOptionsVisible: false, - execGlobalRules: !firewall.ignoreGlobalRules, - captchaTypes: allCaptchaTypes, - geeTestIsOn: geeTestIsOn - } - }, - watch: { - execGlobalRules: function (v) { - this.firewall.ignoreGlobalRules = !v - } - }, - methods: { - changeOptionsVisible: function (v) { - this.moreOptionsVisible = v - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - -
启用Web防火墙 -
- - -
-
人机识别验证方式 - -

使用系统默认的设置。你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

-

{{captchaType.description}}你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

-
启用系统全局规则 - -

选中后,表示使用系统全局WAF策略中定义的规则。

-
-
-
` -}) - -Vue.component("http-cache-config-box", { - props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy", "v-web-id"], - data: function () { - let cacheConfig = this.vCacheConfig - if (cacheConfig == null) { - cacheConfig = { - isPrior: false, - isOn: false, - addStatusHeader: true, - addAgeHeader: false, - enableCacheControlMaxAge: false, - cacheRefs: [], - purgeIsOn: false, - purgeKey: "", - disablePolicyRefs: false - } - } - if (cacheConfig.cacheRefs == null) { - cacheConfig.cacheRefs = [] - } - - let maxBytes = null - if (this.vCachePolicy != null && this.vCachePolicy.maxBytes != null) { - maxBytes = this.vCachePolicy.maxBytes - } - - // key - if (cacheConfig.key == null) { - // use Vue.set to activate vue events - Vue.set(cacheConfig, "key", { - isOn: false, - scheme: "https", - host: "" - }) - } - - return { - cacheConfig: cacheConfig, - moreOptionsVisible: false, - enablePolicyRefs: !cacheConfig.disablePolicyRefs, - maxBytes: maxBytes, - - searchBoxVisible: false, - searchKeyword: "", - - keyOptionsVisible: false - } - }, - watch: { - enablePolicyRefs: function (v) { - this.cacheConfig.disablePolicyRefs = !v - }, - searchKeyword: function (v) { - this.$refs.cacheRefsConfigBoxRef.search(v) - } - }, - methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.cacheConfig.isPrior) && this.cacheConfig.isOn - }, - isPlus: function () { - return true - }, - generatePurgeKey: function () { - let r = Math.random().toString() + Math.random().toString() - let s = r.replace(/0\./g, "") - .replace(/\./g, "") - let result = "" - for (let i = 0; i < s.length; i++) { - result += String.fromCharCode(parseInt(s.substring(i, i + 1)) + ((Math.random() < 0.5) ? "a" : "A").charCodeAt(0)) - } - this.cacheConfig.purgeKey = result - }, - showMoreOptions: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - }, - changeStale: function (stale) { - this.cacheConfig.stale = stale - }, - showSearchBox: function () { - this.searchBoxVisible = !this.searchBoxVisible - if (this.searchBoxVisible) { - let that = this - setTimeout(function () { - that.$refs.searchBox.focus() - }) - } else { - this.searchKeyword = "" - } - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用缓存 -
- - -
-
缓存主域名 -
默认   [修改]
-
使用主域名:{{cacheConfig.key.scheme}}://{{cacheConfig.key.host}}   [修改]
-
-
- - - - - - - - - -
启用主域名 -

启用主域名后,所有缓存键值中的协议和域名部分都会修改为主域名,用来实现缓存不区分域名。

-
主域名 * -
-
- -
-
- -
-
-

此域名必须是当前网站已绑定域名,在刷新缓存时也需要使用此域名。

-
- -
-
- 收起选项更多选项 -
使用默认缓存条件 - -

选中后使用系统中已经定义的默认缓存条件。

-
添加X-Cache报头 - -

选中后自动在响应报头中增加X-Cache: BYPASS|MISS|HIT|PURGE;在浏览器端查看X-Cache值时请先禁用浏览器缓存,避免影响观察。

-
添加Age Header - -

选中后自动在响应Header中增加Age: [存活时间秒数]

-
支持源站控制有效时间 - -

选中后表示支持源站在Header中设置的Cache-Control: max-age=[有效时间秒数]

-
允许PURGE - -

允许使用PURGE方法清除某个URL缓存。

-
PURGE Key * - -

[随机生成]。需要在PURGE方法调用时加入Edge-Purge-Key: {{cacheConfig.purgeKey}} Header。只能包含字符、数字、下划线。

-
- - -
- -
-
- -
-

缓存条件   [添加]   [搜索] -
- - -
-

-
` }) -// 通用Header长度 -let defaultGeneralHeaders = ["Cache-Control", "Connection", "Date", "Pragma", "Trailer", "Transfer-Encoding", "Upgrade", "Via", "Warning"] -Vue.component("http-cond-general-header-length", { - props: ["v-checkpoint"], - data: function () { - let headers = null - let length = null - - if (window.parent.UPDATING_RULE != null) { - let options = window.parent.UPDATING_RULE.checkpointOptions - if (options.headers != null && Array.$isArray(options.headers)) { - headers = options.headers - } - if (options.length != null) { - length = options.length - } - } - - - if (headers == null) { - headers = defaultGeneralHeaders - } - - if (length == null) { - length = 128 - } - - let that = this - setTimeout(function () { - that.change() - }, 100) - - return { - headers: headers, - length: length - } - }, - watch: { - length: function (v) { - let len = parseInt(v) - if (isNaN(len)) { - len = 0 - } - if (len < 0) { - len = 0 - } - this.length = len - this.change() - } - }, - methods: { - change: function () { - this.vCheckpoint.options = [ - { - code: "headers", - value: this.headers - }, - { - code: "length", - value: this.length - } - ] - } - }, - template: `
- - - - - - - - - -
通用Header列表 - -

需要检查的Header列表。

-
Header值超出长度 -
- - 字节 -
-

超出此长度认为匹配成功,0表示不限制。

-
-
` -}) - -// CC -Vue.component("http-firewall-checkpoint-cc", { - props: ["v-checkpoint"], - data: function () { - let keys = [] - let period = 60 - let threshold = 1000 - let ignoreCommonFiles = true - let enableFingerprint = true - - let options = {} - if (window.parent.UPDATING_RULE != null) { - options = window.parent.UPDATING_RULE.checkpointOptions - } - - if (options == null) { - options = {} - } - if (options.keys != null) { - keys = options.keys - } - if (keys.length == 0) { - keys = ["${remoteAddr}", "${requestPath}"] - } - if (options.period != null) { - period = options.period - } - if (options.threshold != null) { - threshold = options.threshold - } - if (options.ignoreCommonFiles != null && typeof (options.ignoreCommonFiles) == "boolean") { - ignoreCommonFiles = options.ignoreCommonFiles - } - if (options.enableFingerprint != null && typeof (options.enableFingerprint) == "boolean") { - enableFingerprint = options.enableFingerprint - } - - let that = this - setTimeout(function () { - that.change() - }, 100) - - return { - keys: keys, - period: period, - threshold: threshold, - ignoreCommonFiles: ignoreCommonFiles, - enableFingerprint: enableFingerprint, - options: {}, - value: threshold - } - }, - watch: { - period: function () { - this.change() - }, - threshold: function () { - this.change() - }, - ignoreCommonFiles: function () { - this.change() - }, - enableFingerprint: function () { - this.change() - } - }, - methods: { - changeKeys: function (keys) { - this.keys = keys - this.change() - }, - change: function () { - let period = parseInt(this.period.toString()) - if (isNaN(period) || period <= 0) { - period = 60 - } - - let threshold = parseInt(this.threshold.toString()) - if (isNaN(threshold) || threshold <= 0) { - threshold = 1000 - } - this.value = threshold - - let ignoreCommonFiles = this.ignoreCommonFiles - if (typeof ignoreCommonFiles != "boolean") { - ignoreCommonFiles = false - } - - let enableFingerprint = this.enableFingerprint - if (typeof enableFingerprint != "boolean") { - enableFingerprint = true - } - - this.vCheckpoint.options = [ - { - code: "keys", - value: this.keys - }, - { - code: "period", - value: period, - }, - { - code: "threshold", - value: threshold - }, - { - code: "ignoreCommonFiles", - value: ignoreCommonFiles - }, - { - code: "enableFingerprint", - value: enableFingerprint - } - ] - }, - thresholdTooLow: function () { - let threshold = parseInt(this.threshold.toString()) - if (isNaN(threshold) || threshold <= 0) { - threshold = 1000 - } - return threshold > 0 && threshold < 5 - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - -
统计对象组合 * - -
统计周期 * -
- - -
-
阈值 * - -

对于网站类应用来说,当前阈值设置的太低,有可能会影响用户正常访问。

-
检查请求来源指纹 - -

在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。

-
忽略常用文件 - -

忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。

-
-
` -}) - -// 防盗链 -Vue.component("http-firewall-checkpoint-referer-block", { - props: ["v-checkpoint"], - data: function () { - let allowEmpty = true - let allowSameDomain = true - let allowDomains = [] - let denyDomains = [] - let checkOrigin = true - - let options = {} - if (window.parent.UPDATING_RULE != null) { - options = window.parent.UPDATING_RULE.checkpointOptions - } - - if (options == null) { - options = {} - } - if (typeof (options.allowEmpty) == "boolean") { - allowEmpty = options.allowEmpty - } - if (typeof (options.allowSameDomain) == "boolean") { - allowSameDomain = options.allowSameDomain - } - if (options.allowDomains != null && typeof (options.allowDomains) == "object") { - allowDomains = options.allowDomains - } - if (options.denyDomains != null && typeof (options.denyDomains) == "object") { - denyDomains = options.denyDomains - } - if (typeof options.checkOrigin == "boolean") { - checkOrigin = options.checkOrigin - } - - let that = this - setTimeout(function () { - that.change() - }, 100) - - return { - allowEmpty: allowEmpty, - allowSameDomain: allowSameDomain, - allowDomains: allowDomains, - denyDomains: denyDomains, - checkOrigin: checkOrigin, - options: {}, - value: 0 - } - }, - watch: { - allowEmpty: function () { - this.change() - }, - allowSameDomain: function () { - this.change() - }, - checkOrigin: function () { - this.change() - } - }, - methods: { - changeAllowDomains: function (values) { - this.allowDomains = values - this.change() - }, - changeDenyDomains: function (values) { - this.denyDomains = values - this.change() - }, - change: function () { - this.vCheckpoint.options = [ - { - code: "allowEmpty", - value: this.allowEmpty - }, - { - code: "allowSameDomain", - value: this.allowSameDomain, - }, - { - code: "allowDomains", - value: this.allowDomains - }, - { - code: "denyDomains", - value: this.denyDomains - }, - { - code: "checkOrigin", - value: this.checkOrigin - } - ] - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - -
来源域名允许为空 - -

允许不带来源的访问。

-
来源域名允许一致 - -

允许来源域名和当前访问的域名一致,相当于在站内访问。

-
允许的来源域名 - -

允许的来源域名列表,比如example.com(顶级域名)、*.example.com(example.com的所有二级域名)。单个星号*表示允许所有域名。

-
禁止的来源域名 - -

禁止的来源域名列表,比如example.org(顶级域名)、*.example.org(example.org的所有二级域名);除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。

-
同时检查Origin - -

如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。

-
-
` -}) - Vue.component("http-cache-refs-config-box", { props: ["v-cache-refs", "v-cache-config", "v-cache-policy-id", "v-web-id", "v-max-bytes"], mounted: function () { @@ -4751,161 +6808,1523 @@ Vue.component("http-cache-refs-config-box", {
` }) -Vue.component("origin-list-box", { - props: ["v-primary-origins", "v-backup-origins", "v-server-type", "v-params"], +Vue.component("http-cache-stale-config", { + props: ["v-cache-stale-config"], data: function () { - return { - primaryOrigins: this.vPrimaryOrigins, - backupOrigins: this.vBackupOrigins - } - }, - methods: { - createPrimaryOrigin: function () { - teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, { - width: "45em", - height: "27em", - callback: function (resp) { - teaweb.success("保存成功", function () { - window.location.reload() - }) + let config = this.vCacheStaleConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + status: [], + supportStaleIfErrorHeader: true, + life: { + count: 1, + unit: "day" } - }) - }, - createBackupOrigin: function () { - teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, { - width: "45em", - height: "27em", - callback: function (resp) { - teaweb.success("保存成功", function () { - window.location.reload() - }) - } - }) - }, - updateOrigin: function (originId, originType) { - teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, { - width: "45em", - height: "27em", - callback: function (resp) { - teaweb.success("保存成功", function () { - window.location.reload() - }) - } - }) - }, - deleteOrigin: function (originId, originAddr, originType) { - let that = this - teaweb.confirm("确定要删除此源站(" + originAddr + ")吗?", function () { - Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType) - .post() - .success(function () { - teaweb.success("删除成功", function () { - window.location.reload() - }) - }) - }) - }, - updateOriginIsOn: function (originId, originAddr, isOn) { - let message - let resultMessage - if (isOn) { - message = "确定要启用此源站(" + originAddr + ")吗?" - resultMessage = "启用成功" - } else { - message = "确定要停用此源站(" + originAddr + ")吗?" - resultMessage = "停用成功" } - let that = this - teaweb.confirm(message, function () { - Tea.action("/servers/server/settings/origins/updateIsOn?" + that.vParams + "&originId=" + originId + "&isOn=" + (isOn ? 1 : 0)) - .post() - .success(function () { - teaweb.success(resultMessage, function () { - window.location.reload() - }) - }) - }) } - }, - template: `
-

主要源站 [添加主要源站]

-

暂时还没有主要源站。

- - -

备用源站 [添加备用源站]

-

暂时还没有备用源站。

- -
` -}) - -Vue.component("origin-list-table", { - props: ["v-origins", "v-origin-type"], - data: function () { - let hasMatchedDomains = false - let origins = this.vOrigins - if (origins != null && origins.length > 0) { - origins.forEach(function (origin) { - if (origin.domains != null && origin.domains.length > 0) { - hasMatchedDomains = true - } - }) - } - return { - hasMatchedDomains: hasMatchedDomains + config: config } }, - methods: { - deleteOrigin: function (originId, originAddr) { - this.$emit("deleteOrigin", originId, originAddr, this.vOriginType) - }, - updateOrigin: function (originId) { - this.$emit("updateOrigin", originId, this.vOriginType) - }, - updateOriginIsOn: function (originId, originAddr, isOn) { - this.$emit("updateOriginIsOn", originId, originAddr, isOn) + watch: { + config: { + deep: true, + handler: function () { + this.$emit("change", this.config) + } } }, - template: ` - - - - - - - - - + methods: {}, + template: `
源站地址权重状态操作
- - - + + + + + + + + + + + + +
- {{origin.addr}}   -
- 对象存储 - {{origin.name}} - 证书 - 主机名: {{origin.host}} - 端口跟随 - HTTP/2 - - 匹配: {{domain}} - 匹配: 所有域名 -
-
{{origin.weight}}
启用过时缓存 - + +

选中后,在更新缓存失败后会尝试读取过时的缓存。

有效期 - 修改   - 停用启用   - 删除 + +

缓存在过期之后,仍然保留的时间。

+
状态码 +

在这些状态码出现时使用过时缓存,默认支持50x状态码。

+
支持stale-if-error + +

选中后,支持在Cache-Control中通过stale-if-error指定过时缓存有效期。

` }) +// HTTP CC防护配置 +Vue.component("http-cc-config-box", { + props: ["v-cc-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vCcConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + enableFingerprint: true, + enableGET302: true, + onlyURLPatterns: [], + exceptURLPatterns: [], + useDefaultThresholds: true, + ignoreCommonFiles: true + } + } + + if (config.thresholds == null || config.thresholds.length == 0) { + config.thresholds = [ + { + maxRequests: 0 + }, + { + maxRequests: 0 + }, + { + maxRequests: 0 + } + ] + } + + if (typeof config.enableFingerprint != "boolean") { + config.enableFingerprint = true + } + if (typeof config.enableGET302 != "boolean") { + config.enableGET302 = true + } + + if (config.onlyURLPatterns == null) { + config.onlyURLPatterns = [] + } + if (config.exceptURLPatterns == null) { + config.exceptURLPatterns = [] + } + return { + config: config, + moreOptionsVisible: false, + minQPSPerIP: config.minQPSPerIP, + useCustomThresholds: !config.useDefaultThresholds, + + thresholdMaxRequests0: this.maxRequestsStringAtThresholdIndex(config, 0), + thresholdMaxRequests1: this.maxRequestsStringAtThresholdIndex(config, 1), + thresholdMaxRequests2: this.maxRequestsStringAtThresholdIndex(config, 2) + } + }, + watch: { + minQPSPerIP: function (v) { + let qps = parseInt(v.toString()) + if (isNaN(qps) || qps < 0) { + qps = 0 + } + this.config.minQPSPerIP = qps + }, + thresholdMaxRequests0: function (v) { + this.setThresholdMaxRequests(0, v) + }, + thresholdMaxRequests1: function (v) { + this.setThresholdMaxRequests(1, v) + }, + thresholdMaxRequests2: function (v) { + this.setThresholdMaxRequests(2, v) + }, + useCustomThresholds: function (b) { + this.config.useDefaultThresholds = !b + } + }, + methods: { + maxRequestsStringAtThresholdIndex: function (config, index) { + if (config.thresholds == null) { + return "" + } + if (index < config.thresholds.length) { + let s = config.thresholds[index].maxRequests.toString() + if (s == "0") { + s = "" + } + return s + } + return "" + }, + setThresholdMaxRequests: function (index, v) { + let maxRequests = parseInt(v) + if (isNaN(maxRequests) || maxRequests < 0) { + maxRequests = 0 + } + if (index < this.config.thresholds.length) { + this.config.thresholds[index].maxRequests = maxRequests + } + }, + showMoreOptions: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用CC无感防护 + +

启用后,自动检测并拦截CC攻击。

+
例外URL + +

如果填写了例外URL,表示这些URL跳过CC防护不做处理。

+
限制URL + +

如果填写了限制URL,表示只对这些URL进行CC防护处理;如果不填则表示支持所有的URL。

+
忽略常用文件 + +

忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。

+
检查请求来源指纹 + +

在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。

+
启用GET302校验 + +

选中后,表示自动通过GET302方法来校验客户端。

+
单IP最低QPS +
+ + 请求数/秒 +
+

当某个IP在1分钟内平均QPS达到此值时,才会开始检测;如果设置为0,表示任何访问都会检测。(注意这里设置的是检测开启阈值,不是拦截阈值,拦截阈值在当前表单下方可以设置)

+
使用自定义拦截阈值 + +
自定义拦截阈值设置 +
+
+ 单IP每5秒最多 + + 请求 +
+
+ +
+
+ 单IP每60秒 + + 请求 +
+
+
+
+ 单IP每300秒 + + 请求 +
+
+
+
+
` +}) + +Vue.component("http-charsets-box", { + props: ["v-usual-charsets", "v-all-charsets", "v-charset-config", "v-is-location", "v-is-group"], + data: function () { + let charsetConfig = this.vCharsetConfig + if (charsetConfig == null) { + charsetConfig = { + isPrior: false, + isOn: false, + charset: "", + isUpper: false, + force: false + } + } + return { + charsetConfig: charsetConfig, + advancedVisible: false + } + }, + methods: { + changeAdvancedVisible: function (v) { + this.advancedVisible = v + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
启用字符编码 +
+ + +
+
选择字符编码 +
强制替换 + +

选中后,表示强制覆盖已经设置的字符集;不选中,表示如果源站已经设置了字符集,则保留不修改。

+
字符编码大写 +
+ + +
+

选中后将指定的字符编码转换为大写,比如默认为utf-8,选中后将改为UTF-8

+
+
+
` +}) + +// 压缩配置 +Vue.component("http-compression-config-box", { + props: ["v-compression-config", "v-is-location", "v-is-group"], + mounted: function () { + let that = this + sortLoad(function () { + that.initSortableTypes() + }) + }, + data: function () { + let config = this.vCompressionConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + useDefaultTypes: true, + types: ["brotli", "gzip", "zstd", "deflate"], + level: 0, + decompressData: false, + gzipRef: null, + deflateRef: null, + brotliRef: null, + minLength: {count: 1, "unit": "kb"}, + maxLength: {count: 32, "unit": "mb"}, + mimeTypes: ["text/*", "application/javascript", "application/json", "application/atom+xml", "application/rss+xml", "application/xhtml+xml", "font/*", "image/svg+xml"], + extensions: [".js", ".json", ".html", ".htm", ".xml", ".css", ".woff2", ".txt"], + exceptExtensions: [".apk", ".ipa"], + conds: null, + enablePartialContent: false, + onlyURLPatterns: [], + exceptURLPatterns: [] + } + } + + if (config.types == null) { + config.types = [] + } + if (config.mimeTypes == null) { + config.mimeTypes = [] + } + if (config.extensions == null) { + config.extensions = [] + } + + let allTypes = [ + { + name: "Gzip", + code: "gzip", + isOn: true + }, + { + name: "Deflate", + code: "deflate", + isOn: true + }, + { + name: "Brotli", + code: "brotli", + isOn: true + }, + { + name: "ZSTD", + code: "zstd", + isOn: true + } + ] + + let configTypes = [] + config.types.forEach(function (typeCode) { + allTypes.forEach(function (t) { + if (typeCode == t.code) { + t.isOn = true + configTypes.push(t) + } + }) + }) + allTypes.forEach(function (t) { + if (!config.types.$contains(t.code)) { + t.isOn = false + configTypes.push(t) + } + }) + + return { + config: config, + moreOptionsVisible: false, + allTypes: configTypes + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + }, + changeExtensions: function (values) { + values.forEach(function (v, k) { + if (v.length > 0 && v[0] != ".") { + values[k] = "." + v + } + }) + this.config.extensions = values + }, + changeExceptExtensions: function (values) { + values.forEach(function (v, k) { + if (v.length > 0 && v[0] != ".") { + values[k] = "." + v + } + }) + this.config.exceptExtensions = values + }, + changeMimeTypes: function (values) { + this.config.mimeTypes = values + }, + changeAdvancedVisible: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + }, + changeConds: function (conds) { + this.config.conds = conds + }, + changeType: function () { + this.config.types = [] + let that = this + this.allTypes.forEach(function (v) { + if (v.isOn) { + that.config.types.push(v.code) + } + }) + }, + initSortableTypes: function () { + let box = document.querySelector("#compression-types-box") + let that = this + Sortable.create(box, { + draggable: ".checkbox", + handle: ".icon.handle", + onStart: function () { + + }, + onUpdate: function (event) { + let checkboxes = box.querySelectorAll(".checkbox") + let codes = [] + checkboxes.forEach(function (checkbox) { + let code = checkbox.getAttribute("data-code") + codes.push(code) + }) + that.config.types = codes + } + }) + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用内容压缩 +
+ + +
+
支持的扩展名 + +

含有这些扩展名的URL将会被压缩,不区分大小写。

+
例外扩展名 + +

含有这些扩展名的URL将不会被压缩,不区分大小写。

+
支持的MimeType + +

响应的Content-Type里包含这些MimeType的内容将会被压缩。

+
压缩算法 +
+ + + +
+
+
+
+
+ + +
+
+
+ +

选择支持的压缩算法和优先顺序,拖动图表排序。

+
支持已压缩内容 + +

支持对已压缩内容尝试重新使用新的算法压缩;不选中表示保留当前的压缩格式。

+
内容最小长度 + +

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

+
内容最大长度 + +

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

+
支持Partial
Content
+ +

支持对分片内容(PartialContent)的压缩;除非客户端有特殊要求,一般不需要启用。

+
例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
限制URL + +

如果填写了限制URL,表示只对这些URL进行压缩处理;如果不填则表示支持所有的URL。

+
匹配条件 + +
+
+
` +}) + +// URL扩展名条件 +Vue.component("http-cond-url-extension", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPathLowerExtension}", + operator: "in", + value: "[]" + } + if (this.vCond != null && this.vCond.param == cond.param) { + cond.value = this.vCond.value + } + + let extensions = [] + try { + extensions = JSON.parse(cond.value) + } catch (e) { + + } + + return { + cond: cond, + extensions: extensions, // TODO 可以拖动排序 + + isAdding: false, + addingExt: "" + } + }, + watch: { + extensions: function () { + this.cond.value = JSON.stringify(this.extensions) + } + }, + methods: { + addExt: function () { + this.isAdding = !this.isAdding + + if (this.isAdding) { + let that = this + setTimeout(function () { + that.$refs.addingExt.focus() + }, 100) + } + }, + cancelAdding: function () { + this.isAdding = false + this.addingExt = "" + }, + confirmAdding: function () { + // TODO 做更详细的校验 + // TODO 如果有重复的则提示之 + + if (this.addingExt.length == 0) { + return + } + + let that = this + this.addingExt.split(/[,;,;|]/).forEach(function (ext) { + ext = ext.trim() + if (ext.length > 0) { + if (ext[0] != ".") { + ext = "." + ext + } + ext = ext.replace(/\s+/g, "").toLowerCase() + that.extensions.push(ext) + } + }) + + // 清除状态 + this.cancelAdding() + }, + removeExt: function (index) { + this.extensions.$remove(index) + } + }, + template: `
+ +
+
{{ext}}
+
+
+
+
+ +
+
+ + +
+
+
+ +
+

扩展名需要包含点(.)符号,例如.jpg.png之类;多个扩展名用逗号分割。

+
` +}) + +// 排除URL扩展名条件 +Vue.component("http-cond-url-not-extension", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPathLowerExtension}", + operator: "not in", + value: "[]" + } + if (this.vCond != null && this.vCond.param == cond.param) { + cond.value = this.vCond.value + } + + let extensions = [] + try { + extensions = JSON.parse(cond.value) + } catch (e) { + + } + + return { + cond: cond, + extensions: extensions, // TODO 可以拖动排序 + + isAdding: false, + addingExt: "" + } + }, + watch: { + extensions: function () { + this.cond.value = JSON.stringify(this.extensions) + } + }, + methods: { + addExt: function () { + this.isAdding = !this.isAdding + + if (this.isAdding) { + let that = this + setTimeout(function () { + that.$refs.addingExt.focus() + }, 100) + } + }, + cancelAdding: function () { + this.isAdding = false + this.addingExt = "" + }, + confirmAdding: function () { + // TODO 做更详细的校验 + // TODO 如果有重复的则提示之 + + if (this.addingExt.length == 0) { + return + } + if (this.addingExt[0] != ".") { + this.addingExt = "." + this.addingExt + } + this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase() + this.extensions.push(this.addingExt) + + // 清除状态 + this.cancelAdding() + }, + removeExt: function (index) { + this.extensions.$remove(index) + } + }, + template: `
+ +
+
{{ext}}
+
+
+
+
+ +
+
+ + +
+
+
+ +
+

扩展名需要包含点(.)符号,例如.jpg.png之类。

+
` +}) + +// 根据URL前缀 +Vue.component("http-cond-url-prefix", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "prefix", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof (this.vCond.value) == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static/images,不需要带域名。

+
` +}) + +Vue.component("http-cond-url-not-prefix", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "prefix", + value: "", + isReverse: true, + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

要排除的URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static/images,不需要带域名。

+
` +}) + +// 首页 +Vue.component("http-cond-url-eq-index", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "eq", + value: "/", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

检查URL路径是为/,不需要带域名。

+
` +}) + +// 全站 +Vue.component("http-cond-url-all", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "prefix", + value: "/", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

支持全站所有URL。

+
` +}) + +// URL精准匹配 +Vue.component("http-cond-url-eq", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "eq", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

完整的URL路径,通常以/开头,比如/static/ui.js,不需要带域名。

+
` +}) + +Vue.component("http-cond-url-not-eq", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "eq", + value: "", + isReverse: true, + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

要排除的完整的URL路径,通常以/开头,比如/static/ui.js,不需要带域名。

+
` +}) + +// URL正则匹配 +Vue.component("http-cond-url-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

匹配URL的正则表达式,比如^/static/(.*).js$,不需要带域名。

+
` +}) + +// 排除URL正则匹配 +Vue.component("http-cond-url-not-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "not regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

不要匹配URL的正则表达式,意即只要匹配成功则排除此条件,比如^/static/(.*).js$,不需要带域名。

+
` +}) + +// URL通配符 +Vue.component("http-cond-url-wildcard-match", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "wildcard match", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

匹配URL的通配符,用星号(*)表示任意字符,比如(/images/*.png/static/*,不需要带域名。

+
` +}) + +// User-Agent正则匹配 +Vue.component("http-cond-user-agent-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${userAgent}", + operator: "regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

匹配User-Agent的正则表达式,比如Android|iPhone

+
` +}) + +// User-Agent正则不匹配 +Vue.component("http-cond-user-agent-not-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${userAgent}", + operator: "not regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

匹配User-Agent的正则表达式,比如Android|iPhone,如果匹配,则排除此条件。

+
` +}) + +// 根据MimeType +Vue.component("http-cond-mime-type", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: false, + param: "${response.contentType}", + operator: "mime type", + value: "[]" + } + if (this.vCond != null && this.vCond.param == cond.param) { + cond.value = this.vCond.value + } + return { + cond: cond, + mimeTypes: JSON.parse(cond.value), // TODO 可以拖动排序 + + isAdding: false, + addingMimeType: "" + } + }, + watch: { + mimeTypes: function () { + this.cond.value = JSON.stringify(this.mimeTypes) + } + }, + methods: { + addMimeType: function () { + this.isAdding = !this.isAdding + + if (this.isAdding) { + let that = this + setTimeout(function () { + that.$refs.addingMimeType.focus() + }, 100) + } + }, + cancelAdding: function () { + this.isAdding = false + this.addingMimeType = "" + }, + confirmAdding: function () { + // TODO 做更详细的校验 + // TODO 如果有重复的则提示之 + + if (this.addingMimeType.length == 0) { + return + } + this.addingMimeType = this.addingMimeType.replace(/\s+/g, "") + this.mimeTypes.push(this.addingMimeType) + + // 清除状态 + this.cancelAdding() + }, + removeMimeType: function (index) { + this.mimeTypes.$remove(index) + } + }, + template: `
+ +
+
{{mimeType}}
+
+
+
+
+ +
+
+ + +
+
+
+ +
+

服务器返回的内容的MimeType,比如text/htmlimage/*等。

+
` +}) + +// 参数匹配 +Vue.component("http-cond-params", { + props: ["v-cond"], + mounted: function () { + let cond = this.vCond + if (cond == null) { + return + } + this.operator = cond.operator + + // stringValue + if (["regexp", "not regexp", "eq", "not", "prefix", "suffix", "contains", "not contains", "eq ip", "gt ip", "gte ip", "lt ip", "lte ip", "ip range"].$contains(cond.operator)) { + this.stringValue = cond.value + return + } + + // numberValue + if (["eq int", "eq float", "gt", "gte", "lt", "lte", "mod 10", "ip mod 10", "mod 100", "ip mod 100"].$contains(cond.operator)) { + this.numberValue = cond.value + return + } + + // modValue + if (["mod", "ip mod"].$contains(cond.operator)) { + let pieces = cond.value.split(",") + this.modDivValue = pieces[0] + if (pieces.length > 1) { + this.modRemValue = pieces[1] + } + return + } + + // stringValues + let that = this + if (["in", "not in", "file ext", "mime type"].$contains(cond.operator)) { + try { + let arr = JSON.parse(cond.value) + if (arr != null && (arr instanceof Array)) { + arr.forEach(function (v) { + that.stringValues.push(v) + }) + } + } catch (e) { + + } + return + } + + // versionValue + if (["version range"].$contains(cond.operator)) { + let pieces = cond.value.split(",") + this.versionRangeMinValue = pieces[0] + if (pieces.length > 1) { + this.versionRangeMaxValue = pieces[1] + } + return + } + }, + data: function () { + let cond = { + isRequest: true, + param: "", + operator: window.REQUEST_COND_OPERATORS[0].op, + value: "", + isCaseInsensitive: false + } + if (this.vCond != null) { + cond = this.vCond + } + return { + cond: cond, + operators: window.REQUEST_COND_OPERATORS, + operator: window.REQUEST_COND_OPERATORS[0].op, + operatorDescription: window.REQUEST_COND_OPERATORS[0].description, + variables: window.REQUEST_VARIABLES, + variable: "", + + // 各种类型的值 + stringValue: "", + numberValue: "", + + modDivValue: "", + modRemValue: "", + + stringValues: [], + + versionRangeMinValue: "", + versionRangeMaxValue: "" + } + }, + methods: { + changeVariable: function () { + let v = this.cond.param + if (v == null) { + v = "" + } + this.cond.param = v + this.variable + }, + changeOperator: function () { + let that = this + this.operators.forEach(function (v) { + if (v.op == that.operator) { + that.operatorDescription = v.description + } + }) + + this.cond.operator = this.operator + + // 移动光标 + let box = document.getElementById("variables-value-box") + if (box != null) { + setTimeout(function () { + let input = box.getElementsByTagName("INPUT") + if (input.length > 0) { + input[0].focus() + } + }, 100) + } + }, + changeStringValues: function (v) { + this.stringValues = v + this.cond.value = JSON.stringify(v) + } + }, + watch: { + stringValue: function (v) { + this.cond.value = v + }, + numberValue: function (v) { + // TODO 校验数字 + this.cond.value = v + }, + modDivValue: function (v) { + if (v.length == 0) { + return + } + let div = parseInt(v) + if (isNaN(div)) { + div = 1 + } + this.modDivValue = div + this.cond.value = div + "," + this.modRemValue + }, + modRemValue: function (v) { + if (v.length == 0) { + return + } + let rem = parseInt(v) + if (isNaN(rem)) { + rem = 0 + } + this.modRemValue = rem + this.cond.value = this.modDivValue + "," + rem + }, + versionRangeMinValue: function (v) { + this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue + }, + versionRangeMaxValue: function (v) { + this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue + } + }, + template: ` + + 参数值 + + +
+
+ +
+
+ +
+
+

其中可以使用变量,类似于\${requestPath},也可以是多个变量的组合。

+ + + + 操作符 + +
+ +

+
+ + + + 对比值 + + +
+ +

要匹配的正则表达式,比如^/static/(.+).js

+
+ + +
+ +

要对比的数字。

+
+ + +
+ +

参数值除以10的余数,在0-9之间。

+
+
+ +

参数值除以100的余数,在0-99之间。

+
+
+
+
除:
+
+ +
+
余:
+
+ +
+
+
+ + +
+ +

和参数值一致的字符串。

+

和参数值不一致的字符串。

+

参数值的前缀。

+

参数值的后缀为此字符串。

+

参数值包含此字符串。

+

参数值不包含此字符串。

+
+
+ +

添加参数值列表。

+

添加参数值列表。

+

添加扩展名列表,比如pnghtml,不包括点。

+

添加MimeType列表,类似于text/htmlimage/*

+
+
+
+
+
-
+
+
+
+ + +
+ +

要对比的IP。

+
+
+ +

参数中IP转换成整数后除以10的余数,在0-9之间。

+
+
+ +

参数中IP转换成整数后除以100的余数,在0-99之间。

+
+ + + + 不区分大小写 + +
+ + +
+

选中后表示对比时忽略参数值的大小写。

+ + + +` +}) + Vue.component("http-cors-header-config-box", { props: ["value"], data: function () { @@ -5028,67 +8447,19 @@ Vue.component("http-cors-header-config-box", {
` }) -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 - }) - }, +// 页面动态加密配置 +Vue.component("http-encryption-config-box", { + props: ["v-encryption-config", "v-is-location", "v-is-group"], 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-optimization-config-box", { - props: ["v-optimization-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vOptimizationConfig + let config = this.vEncryptionConfig return { config: config, htmlMoreOptions: false, javascriptMoreOptions: false, - cssMoreOptions: false + keyPolicyMoreOptions: false, + cacheMoreOptions: false, + encryptionMoreOptions: false } }, methods: { @@ -5097,1316 +8468,270 @@ Vue.component("http-optimization-config-box", { } }, template: `
- +
-
+
- + - - + + - + - + - - - - - -
HTML优化启用页面动态加密
- +
-

可以自动优化HTML中包含的空白、注释、空标签等。只有文件可以缓存时才会被优化。

+

启用后,将对 HTML 页面中的 JavaScript 进行动态加密,有效抵御批量爬虫和脚本工具。

HTML例外URL排除 URL - -

如果填写了例外URL,表示这些URL跳过不做处理。

+ +

这些 URL 将跳过加密处理,支持正则表达式。

HTML限制URL - -

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

-
- - - - - - - - - - - - - - - - - - - - - -
Javascript优化 -
- - -
-

可以自动缩短Javascript中变量、函数名称等。只有文件可以缓存时才会被优化。

-
Javascript例外URL - -

如果填写了例外URL,表示这些URL跳过不做处理。

-
Javascript限制URL - -

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

-
- - - - - - - - - - - - - - - - - - - -
CSS优化 -
- - -
-

可以自动去除CSS中包含的空白。只有文件可以缓存时才会被优化。

-
CSS例外URL - -

如果填写了例外URL,表示这些URL跳过不做处理。

-
CSS限制URL - -

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

-
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
HTML 加密 +
+ + +
+

加密 HTML 页面中的 JavaScript 脚本。

+
加密内联脚本 +
+ + +
+

加密 HTML 中的内联 <script> 标签内容。

+
加密外部脚本 +
+ + +
+

加密通过 src 属性引入的外部 JavaScript 文件。

+
URL 匹配规则 + +

如果填写了匹配规则,表示只对这些 URL 进行加密处理;如果不填则表示支持所有的 URL。

+
+ +
+ + + + + + + + + + + + + + + + +
JavaScript 文件加密 +
+ + +
+

加密独立的 JavaScript 文件(.js 文件)。

+
URL 匹配规则 + +

如果填写了匹配规则,表示只对这些 URL 进行加密处理;如果不填则表示支持所有的 URL。

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
服务器端密钥 + +

用于生成加密密钥的密码,建议使用复杂的随机字符串。默认密钥仅用于测试,生产环境请务必修改!

+
时间分片(秒) + +

加密密钥每隔多少秒更换一次。时间越短越安全,但可能影响性能。建议 60-120 秒,默认 60 秒。

+
IP CIDR 前缀长度 + +

将用户 IP 地址的前多少位作为识别依据。例如设置为 24 时,192.168.1.1 和 192.168.1.2 会被视为同一用户。默认 24。

+
简化 User-Agent +
+ + +
+

开启后,只识别浏览器类型(如 Chrome、Firefox),忽略版本号等细节,避免因浏览器自动更新导致解密失败。

+
+ +
+ + + + + + + + + + + + + + + + + + + + +
启用缓存 +
+ + +
+

开启后,相同内容的加密结果会被缓存,减少重复计算,提升响应速度。

+
缓存 TTL(秒) + +

缓存的有效期,超过这个时间后缓存会自动失效。建议与上面的"时间分片"保持一致。默认 60 秒。

+
最大缓存条目数 + +

最多缓存多少个加密结果。数量越大占用内存越多,建议根据服务器内存情况调整。默认 1000。

+
+
` }) -Vue.component("http-websocket-box", { - props: ["v-websocket-ref", "v-websocket-config", "v-is-location"], + + +Vue.component("http-expires-time-config-box", { + props: ["v-expires-time"], data: function () { - let websocketRef = this.vWebsocketRef - if (websocketRef == null) { - websocketRef = { + let expiresTime = this.vExpiresTime + if (expiresTime == null) { + expiresTime = { isPrior: false, isOn: false, - websocketId: 0 + overwrite: true, + autoCalculate: true, + duration: {count: -1, "unit": "hour"} } } - - 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 + expiresTime: expiresTime } }, 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.websocketRef.isPrior) && this.websocketRef.isOn - }, - changeAdvancedVisible: function (v) { - this.advancedVisible = v - }, - createOrigin: function () { - let that = this - teaweb.popup("/servers/server/settings/websocket/createOrigin", { - height: "12.5em", - callback: function (resp) { - that.websocketConfig.allowedOrigins.push(resp.data.origin) - } - }) - }, - removeOrigin: function (index) { - this.websocketConfig.allowedOrigins.$remove(index) - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用Websocket -
- - -
-
允许所有来源域(Origin) -
- - -
-

选中表示允许所有的来源域。

-
允许的来源域列表(Origin) -
-
- {{origin}} -
-
-
- -

只允许在列表中的来源域名访问Websocket服务。

-
传递请求来源域 -
- - -
-

选中后,表示把接收到的请求中的Origin字段传递到源站。

-
指定传递的来源域 - -

指定向源站传递的Origin字段值。

-
握手超时时间(Handshake) -
-
- -
-
- 秒 -
-
-

0表示使用默认的时间设置。

-
-
-
` -}) - -Vue.component("http-rewrite-rule-list", { - props: ["v-web-id", "v-rewrite-rules"], - mounted: function () { - setTimeout(this.sort, 1000) - }, - data: function () { - let rewriteRules = this.vRewriteRules - if (rewriteRules == null) { - rewriteRules = [] - } - return { - rewriteRules: rewriteRules - } - }, - methods: { - updateRewriteRule: function (rewriteRuleId) { - teaweb.popup("/servers/server/settings/rewrite/updatePopup?webId=" + this.vWebId + "&rewriteRuleId=" + rewriteRuleId, { - height: "26em", - callback: function () { - window.location.reload() - } - }) - }, - deleteRewriteRule: function (rewriteRuleId) { - let that = this - teaweb.confirm("确定要删除此重写规则吗?", function () { - Tea.action("/servers/server/settings/rewrite/delete") - .params({ - webId: that.vWebId, - rewriteRuleId: rewriteRuleId - }) - .post() - .refresh() - }) - }, - // 排序 - sort: function () { - if (this.rewriteRules.length == 0) { - return - } - let that = this - sortTable(function (rowIds) { - Tea.action("/servers/server/settings/rewrite/sort") - .post() - .params({ - webId: that.vWebId, - rewriteRuleIds: rowIds - }) - .success(function () { - teaweb.success("保存成功") - }) - }) - } - }, - template: `
-
-

暂时还没有重写规则。

- - - - - - - - - - - - - - - - - - - - - -
匹配规则转发目标转发方式状态操作
{{rule.pattern}} -
- BREAK - {{rule.redirectStatus}} - Host: {{rule.proxyHost}} -
{{rule.replace}} - 隐式 - 显示 - - - - 修改   - 删除 -
-

拖动左侧的图标可以对重写规则进行排序。

- -
` -}) - -Vue.component("http-rewrite-labels-label", { - props: ["v-class"], - template: `` -}) - -Vue.component("server-name-box", { - props: ["v-server-names"], - data: function () { - let serverNames = this.vServerNames; - if (serverNames == null) { - serverNames = [] - } - return { - serverNames: serverNames, - isSearching: false, - keyword: "" - } - }, - methods: { - addServerName: function () { - window.UPDATING_SERVER_NAME = null - let that = this - teaweb.popup("/servers/addServerNamePopup", { - callback: function (resp) { - var serverName = resp.data.serverName - that.serverNames.push(serverName) - setTimeout(that.submitForm, 100) - } - }); - }, - - removeServerName: function (index) { - this.serverNames.$remove(index) - }, - - updateServerName: function (index, serverName) { - window.UPDATING_SERVER_NAME = teaweb.clone(serverName) - let that = this - teaweb.popup("/servers/addServerNamePopup", { - callback: function (resp) { - var serverName = resp.data.serverName - Vue.set(that.serverNames, index, serverName) - setTimeout(that.submitForm, 100) - } - }); - }, - showSearchBox: function () { - this.isSearching = !this.isSearching - if (this.isSearching) { - let that = this - setTimeout(function () { - that.$refs.keywordRef.focus() - }, 200) - } else { - this.keyword = "" - } - }, - allServerNames: function () { - if (this.serverNames == null) { - return [] - } - let result = [] - this.serverNames.forEach(function (serverName) { - if (serverName.subNames != null && serverName.subNames.length > 0) { - serverName.subNames.forEach(function (subName) { - if (subName != null && subName.length > 0) { - if (!result.$contains(subName)) { - result.push(subName) - } - } - }) - } else if (serverName.name != null && serverName.name.length > 0) { - if (!result.$contains(serverName.name)) { - result.push(serverName.name) - } - } - }) - return result - }, - submitForm: function () { - Tea.runActionOn(this.$refs.serverNamesRef.form) - } - }, - watch: { - keyword: function (v) { - this.serverNames.forEach(function (serverName) { - if (v.length == 0) { - serverName.isShowing = true - return - } - if (serverName.subNames == null || serverName.subNames.length == 0) { - if (!teaweb.match(serverName.name, v)) { - serverName.isShowing = false - } - } else { - let found = false - serverName.subNames.forEach(function (subName) { - if (teaweb.match(subName, v)) { - found = true - } - }) - serverName.isShowing = found - } - }) - } - }, - template: `
- -
-
- {{serverName.type}} - {{serverName.name}} - {{serverName.subNames[0]}}等{{serverName.subNames.length}}个域名 - -
-
-
-
- -
|
-
- - -
-
- -
-
-
` -}) - -// UAM模式配置 -Vue.component("uam-config-box", { - props: ["v-uam-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vUamConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - addToWhiteList: true, - onlyURLPatterns: [], - exceptURLPatterns: [], - minQPSPerIP: 0, - keyLife: 0 - } - } - if (config.onlyURLPatterns == null) { - config.onlyURLPatterns = [] - } - if (config.exceptURLPatterns == null) { - config.exceptURLPatterns = [] - } - return { - config: config, - moreOptionsVisible: false, - minQPSPerIP: config.minQPSPerIP, - keyLife: config.keyLife - } - }, - watch: { - minQPSPerIP: function (v) { - let qps = parseInt(v.toString()) - if (isNaN(qps) || qps < 0) { - qps = 0 - } - this.config.minQPSPerIP = qps - }, - keyLife: function (v) { - let keyLife = parseInt(v) - if (isNaN(keyLife) || keyLife <= 0) { - keyLife = 0 - } - this.config.keyLife = keyLife - } - }, - methods: { - showMoreOptions: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - }, - changeConds: function (conds) { - this.config.conds = conds - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用5秒盾 - -

启用后,访问网站时,自动检查浏览器环境,阻止非正常访问。

-
验证有效期 -
- - -
-

单个客户端验证通过后,在这个有效期内不再重复验证;如果为0则表示系统默认。

-
单IP最低QPS -
- - 请求数/秒 -
-

当某个IP在1分钟内平均QPS达到此值时,才会触发5秒盾;如果设置为0,表示任何访问都会触发。

-
加入IP白名单 - -

选中后,表示验证通过后,将访问者IP加入到临时白名单中,此IP下次访问时不再校验5秒盾;此白名单只对5秒盾有效,不影响其他规则。此选项主要用于可能无法正常使用Cookie的网站。

-
例外URL - -

如果填写了例外URL,表示这些URL跳过5秒盾不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行5秒盾处理;如果不填则表示支持所有的URL。

-
匹配条件 - -
-
-
` -}) - -Vue.component("http-cache-stale-config", { - props: ["v-cache-stale-config"], - data: function () { - let config = this.vCacheStaleConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - status: [], - supportStaleIfErrorHeader: true, - life: { - count: 1, - unit: "day" - } - } - } - return { - config: config - } - }, - watch: { - config: { - deep: true, - handler: function () { - this.$emit("change", this.config) - } - } - }, - methods: {}, - template: ` - - - - - - - - - - - - - - - - - - -
启用过时缓存 - -

选中后,在更新缓存失败后会尝试读取过时的缓存。

-
有效期 - -

缓存在过期之后,仍然保留的时间。

-
状态码 -

在这些状态码出现时使用过时缓存,默认支持50x状态码。

-
支持stale-if-error - -

选中后,支持在Cache-Control中通过stale-if-error指定过时缓存有效期。

-
` -}) - -// 域名列表 -Vue.component("domains-box", { - props: ["v-domains", "name", "v-support-wildcard"], - data: function () { - let domains = this.vDomains - if (domains == null) { - domains = [] - } - - let realName = "domainsJSON" - if (this.name != null && typeof this.name == "string") { - realName = this.name - } - - let supportWildcard = true - if (typeof this.vSupportWildcard == "boolean") { - supportWildcard = this.vSupportWildcard - } - - return { - domains: domains, - - mode: "single", // single | batch - batchDomains: "", - - isAdding: false, - addingDomain: "", - - isEditing: false, - editingIndex: -1, - - realName: realName, - supportWildcard: supportWildcard - } - }, - watch: { - vSupportWildcard: function (v) { - if (typeof v == "boolean") { - this.supportWildcard = v - } - }, - mode: function (mode) { - let that = this - setTimeout(function () { - if (mode == "single") { - if (that.$refs.addingDomain != null) { - that.$refs.addingDomain.focus() - } - } else if (mode == "batch") { - if (that.$refs.batchDomains != null) { - that.$refs.batchDomains.focus() - } - } - }, 100) - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.addingDomain.focus() - }, 100) - }, - confirm: function () { - if (this.mode == "batch") { - this.confirmBatch() - return - } - - let that = this - - // 删除其中的空格 - this.addingDomain = this.addingDomain.replace(/\s/g, "") - - if (this.addingDomain.length == 0) { - teaweb.warn("请输入要添加的域名", function () { - that.$refs.addingDomain.focus() - }) - return - } - - // 基本校验 - if (this.supportWildcard) { - if (this.addingDomain[0] == "~") { - let expr = this.addingDomain.substring(1) - try { - new RegExp(expr) - } catch (e) { - teaweb.warn("正则表达式错误:" + e.message, function () { - that.$refs.addingDomain.focus() - }) - return - } - } - } else { - if (/[*~^]/.test(this.addingDomain)) { - teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () { - that.$refs.addingDomain.focus() - }) - return - } - } - - if (this.isEditing && this.editingIndex >= 0) { - this.domains[this.editingIndex] = this.addingDomain - } else { - // 分割逗号(,)、顿号(、) - if (this.addingDomain.match("[,、,;]")) { - let domainList = this.addingDomain.split(new RegExp("[,、,;]")) - domainList.forEach(function (v) { - if (v.length > 0) { - that.domains.push(v) - } - }) - } else { - this.domains.push(this.addingDomain) - } - } - this.cancel() - this.change() - }, - confirmBatch: function () { - let domains = this.batchDomains.split("\n") - let realDomains = [] - let that = this - let hasProblems = false - domains.forEach(function (domain) { - if (hasProblems) { - return - } - if (domain.length == 0) { - return - } - if (that.supportWildcard) { - if (domain == "~") { - let expr = domain.substring(1) - try { - new RegExp(expr) - } catch (e) { - hasProblems = true - teaweb.warn("正则表达式错误:" + e.message, function () { - that.$refs.batchDomains.focus() - }) - return - } - } - } else { - if (/[*~^]/.test(domain)) { - hasProblems = true - teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () { - that.$refs.batchDomains.focus() - }) - return - } - } - realDomains.push(domain) - }) - if (hasProblems) { - return - } - if (realDomains.length == 0) { - teaweb.warn("请输入要添加的域名", function () { - that.$refs.batchDomains.focus() - }) - return - } - - realDomains.forEach(function (domain) { - that.domains.push(domain) - }) - this.cancel() - this.change() - }, - edit: function (index) { - this.addingDomain = this.domains[index] - this.isEditing = true - this.editingIndex = index - let that = this - setTimeout(function () { - that.$refs.addingDomain.focus() - }, 50) - }, - remove: function (index) { - this.domains.$remove(index) - this.change() - }, - cancel: function () { - this.isAdding = false - this.mode = "single" - this.batchDomains = "" - this.isEditing = false - this.editingIndex = -1 - this.addingDomain = "" - }, - change: function () { - this.$emit("change", this.domains) - } - }, - template: `
- -
- - [正则] - [后缀] - [泛域名] - {{domain}} - -   -   - - -   -   - - -
-
-
-
-
- -
-
-
- -
-
- -
-
-
- -   -
-
-

支持普通域名(example.com)、泛域名(*.example.com、域名后缀(以点号开头,如.example.com)和正则表达式(以波浪号开头,如~.*.example.com;如果域名后有端口,请加上端口号。

-

只支持普通域名(example.comwww.example.com)。

-
-
-
- -
-
` -}) - -Vue.component("http-firewall-province-selector", { - props: ["v-type", "v-provinces"], - data: function () { - let provinces = this.vProvinces - if (provinces == null) { - provinces = [] - } - - return { - listType: this.vType, - provinces: provinces - } - }, - methods: { - addProvince: function () { - let selectedProvinceIds = this.provinces.map(function (province) { - return province.id - }) - let that = this - teaweb.popup("/servers/server/settings/waf/ipadmin/selectProvincesPopup?type=" + this.listType + "&selectedProvinceIds=" + selectedProvinceIds.join(","), { - width: "50em", - height: "26em", - callback: function (resp) { - that.provinces = resp.data.selectedProvinces - that.$forceUpdate() - that.notifyChange() - } - }) - }, - removeProvince: function (index) { - this.provinces.$remove(index) + "expiresTime.isPrior": function () { this.notifyChange() }, - resetProvinces: function () { - this.provinces = [] + "expiresTime.isOn": function () { this.notifyChange() }, + "expiresTime.overwrite": function () { + this.notifyChange() + }, + "expiresTime.autoCalculate": function () { + this.notifyChange() + } + }, + methods: { notifyChange: function () { - this.$emit("change", { - "provinces": this.provinces - }) + this.$emit("change", this.expiresTime) } }, template: `
- 暂时没有选择允许封禁的省份。 -
-
- - {{province.name}} -
-
-
-   -
` -}) - -Vue.component("http-referers-config-box", { - props: ["v-referers-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vReferersConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - allowEmpty: true, - allowSameDomain: true, - allowDomains: [], - denyDomains: [], - checkOrigin: true - } - } - if (config.allowDomains == null) { - config.allowDomains = [] - } - if (config.denyDomains == null) { - config.denyDomains = [] - } - return { - config: config, - moreOptionsVisible: false - } - }, - methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn - }, - changeAllowDomains: function (domains) { - if (typeof (domains) == "object") { - this.config.allowDomains = domains - this.$forceUpdate() - } - }, - changeDenyDomains: function (domains) { - if (typeof (domains) == "object") { - this.config.denyDomains = domains - this.$forceUpdate() - } - }, - showMoreOptions: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用防盗链 -
- - -
-

选中后表示开启防盗链。

-
允许直接访问网站 - -

允许用户直接访问网站,用户第一次访问网站时来源域名通常为空。

-
来源域名允许一致 - -

允许来源域名和当前访问的域名一致,相当于在站内访问。

-
允许的来源域名 - > -

允许的其他来源域名列表,比如example.com*.example.com。单个星号*表示允许所有域名。

-
禁止的来源域名 - -

禁止的来源域名列表,比如example.org*.example.org;除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。

-
同时检查Origin - -

如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。

-
例外URL - -

如果填写了例外URL,表示这些URL不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行处理;如果不填则表示支持所有的URL。

-
-
-
` -}) - -Vue.component("server-traffic-limit-status-viewer", { - props: ["value"], - data: function () { - let targetTypeName = "流量" - if (this.value != null) { - targetTypeName = this.targetTypeToName(this.value.targetType) - } - - return { - status: this.value, - targetTypeName: targetTypeName - } - }, - methods: { - targetTypeToName: function (targetType) { - switch (targetType) { - case "traffic": - return "流量" - case "request": - return "请求数" - case "websocketConnections": - return "Websocket连接数" - } - return "流量" - } - }, - template: ` - 已达到套餐当日{{targetTypeName}}限制 - 已达到套餐当月{{targetTypeName}}限制 - 已达到套餐总体{{targetTypeName}}限制 -` -}) - -Vue.component("http-redirect-to-https-box", { - props: ["v-redirect-to-https-config", "v-is-location"], - data: function () { - let redirectToHttpsConfig = this.vRedirectToHttpsConfig - if (redirectToHttpsConfig == null) { - redirectToHttpsConfig = { - isPrior: false, - isOn: false, - host: "", - port: 0, - status: 0, - onlyDomains: [], - exceptDomains: [] - } - } else { - if (redirectToHttpsConfig.onlyDomains == null) { - redirectToHttpsConfig.onlyDomains = [] - } - if (redirectToHttpsConfig.exceptDomains == null) { - redirectToHttpsConfig.exceptDomains = [] - } - } - return { - redirectToHttpsConfig: redirectToHttpsConfig, - portString: (redirectToHttpsConfig.port > 0) ? redirectToHttpsConfig.port.toString() : "", - moreOptionsVisible: false, - statusOptions: [ - {"code": 301, "text": "Moved Permanently"}, - {"code": 308, "text": "Permanent Redirect"}, - {"code": 302, "text": "Found"}, - {"code": 303, "text": "See Other"}, - {"code": 307, "text": "Temporary Redirect"} - ] - } - }, - watch: { - "redirectToHttpsConfig.status": function () { - this.redirectToHttpsConfig.status = parseInt(this.redirectToHttpsConfig.status) - }, - portString: function (v) { - let port = parseInt(v) - if (!isNaN(port)) { - this.redirectToHttpsConfig.port = port - } else { - this.redirectToHttpsConfig.port = 0 - } - } - }, - methods: { - changeMoreOptions: function (isVisible) { - this.moreOptionsVisible = isVisible - }, - changeOnlyDomains: function (values) { - this.redirectToHttpsConfig.onlyDomains = values - this.$forceUpdate() - }, - changeExceptDomains: function (values) { - this.redirectToHttpsConfig.exceptDomains = values - this.$forceUpdate() - } - }, - template: `
- - - - - - +
+ + - - + - + + + + + + + + + + + + +
自动跳转到HTTPS -
- - -
-

开启后,所有HTTP的请求都会自动跳转到对应的HTTPS URL上,

- - - - - - - - - - - - - - - -
状态码 - -
域名或IP地址 - -

默认和用户正在访问的域名或IP地址一致。

-
端口 - -

默认端口为443。

-
+
启用 +

启用后,将会在响应的Header中添加Expires字段,浏览器据此会将内容缓存在客户端;同时,在管理后台执行清理缓存时,也将无法清理客户端已有的缓存。

覆盖源站设置 + +

选中后,会覆盖源站Header中已有的Expires字段。

+
自动计算时间 +

根据已设置的缓存有效期进行计算。

+
强制缓存时间 + +

从客户端访问的时间开始要缓存的时长。

+
- - -
-
- - -
-

开启后,所有HTTP的请求都会自动跳转到对应的HTTPS URL上,

- - - - - - - - - - - - - - - - - - - - - - - -
状态码 - -
跳转后域名或IP地址 - -

默认和用户正在访问的域名或IP地址一致,不填写就表示使用当前的域名。

-
端口 - -

默认端口为443。

-
允许的域名 - -

如果填写了允许的域名,那么只有这些域名可以自动跳转。

-
排除的域名 - -

如果填写了排除的域名,那么这些域名将不跳转。

-
-
-
` }) @@ -7420,146 +9745,1047 @@ Vue.component("http-firewall-actions-box", {
` }) -// 认证设置 -Vue.component("http-auth-config-box", { - props: ["v-auth-config", "v-is-location"], +// Action列表 +Vue.component("http-firewall-actions-view", { + props: ["v-actions"], + template: `
+
+ {{action.name}} ({{action.code.toUpperCase()}}) +
+ [{{action.options.status}}] + + [分组] + [网站] + [网站和策略] + + + 黑名单 + 白名单 + 灰名单 + +
+
+
+
` +}) + +Vue.component("http-firewall-config-box", { + props: ["v-firewall-config", "v-is-location", "v-firewall-policy"], data: function () { - let authConfig = this.vAuthConfig - if (authConfig == null) { - authConfig = { + let firewall = this.vFirewallConfig + if (firewall == null) { + firewall = { isPrior: false, - isOn: false + isOn: false, + firewallPolicyId: 0, + ignoreGlobalRules: false, + defaultCaptchaType: "none" } } - if (authConfig.policyRefs == null) { - authConfig.policyRefs = [] + + if (firewall.defaultCaptchaType == null || firewall.defaultCaptchaType.length == 0) { + firewall.defaultCaptchaType = "none" } + + let allCaptchaTypes = window.WAF_CAPTCHA_TYPES.$copy() + + // geetest + let geeTestIsOn = false + if (this.vFirewallPolicy != null && this.vFirewallPolicy.captchaAction != null && this.vFirewallPolicy.captchaAction.geeTestConfig != null) { + geeTestIsOn = this.vFirewallPolicy.captchaAction.geeTestConfig.isOn + } + + // 如果没有启用geetest,则还原 + if (!geeTestIsOn && firewall.defaultCaptchaType == "geetest") { + firewall.defaultCaptchaType = "none" + } + return { - authConfig: authConfig + firewall: firewall, + moreOptionsVisible: false, + execGlobalRules: !firewall.ignoreGlobalRules, + captchaTypes: allCaptchaTypes, + geeTestIsOn: geeTestIsOn + } + }, + watch: { + execGlobalRules: function (v) { + this.firewall.ignoreGlobalRules = !v } }, 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) + changeOptionsVisible: function (v) { + this.moreOptionsVisible = v } }, template: `
- - - - - - - - - -
启用鉴权 -
- - -
-
-
- -
-

鉴权方式

- - + +
+ + - - - - - - - - - - + - + + + + + + + + +
名称鉴权方法参数状态操作
{{ref.authPolicy.name}}启用Web防火墙 - {{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}} +
+ +
人机识别验证方式 - + +

使用系统默认的设置。你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

+

{{captchaType.description}}你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

启用系统全局规则 - 修改   - 删除 + +

选中后,表示使用系统全局WAF策略中定义的规则。

- -
-
+
` }) +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-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-firewall-province-selector", { + props: ["v-type", "v-provinces"], + data: function () { + let provinces = this.vProvinces + if (provinces == null) { + provinces = [] + } + + return { + listType: this.vType, + provinces: provinces + } + }, + methods: { + addProvince: function () { + let selectedProvinceIds = this.provinces.map(function (province) { + return province.id + }) + let that = this + teaweb.popup("/servers/server/settings/waf/ipadmin/selectProvincesPopup?type=" + this.listType + "&selectedProvinceIds=" + selectedProvinceIds.join(","), { + width: "50em", + height: "26em", + callback: function (resp) { + that.provinces = resp.data.selectedProvinces + that.$forceUpdate() + that.notifyChange() + } + }) + }, + removeProvince: function (index) { + this.provinces.$remove(index) + this.notifyChange() + }, + resetProvinces: function () { + this.provinces = [] + this.notifyChange() + }, + notifyChange: function () { + this.$emit("change", { + "provinces": this.provinces + }) + } + }, + template: `
+ 暂时没有选择允许封禁的省份。 +
+
+ + {{province.name}} +
+
+
+   +
` +}) + +Vue.component("http-firewall-region-selector", { + props: ["v-type", "v-countries"], + data: function () { + let countries = this.vCountries + if (countries == null) { + countries = [] + } + + return { + listType: this.vType, + countries: countries + } + }, + methods: { + addCountry: function () { + let selectedCountryIds = this.countries.map(function (country) { + return country.id + }) + let that = this + teaweb.popup("/servers/server/settings/waf/ipadmin/selectCountriesPopup?type=" + this.listType + "&selectedCountryIds=" + selectedCountryIds.join(","), { + width: "52em", + height: "30em", + callback: function (resp) { + that.countries = resp.data.selectedCountries + that.$forceUpdate() + that.notifyChange() + } + }) + }, + removeCountry: function (index) { + this.countries.$remove(index) + this.notifyChange() + }, + resetCountries: function () { + this.countries = [] + this.notifyChange() + }, + notifyChange: function () { + this.$emit("change", { + "countries": this.countries + }) + } + }, + template: `
+ 暂时没有选择允许封禁的区域。 +
+
+ + ({{country.letter}}){{country.name}} +
+
+
+   +
` +}) + +// 显示WAF规则的标签 +Vue.component("http-firewall-rule-label", { + props: ["v-rule"], + data: function () { + return { + rule: this.vRule + } + }, + methods: { + showErr: function (err) { + teaweb.popupTip("规则校验错误,请修正:" + teaweb.encodeHTML(err) + "") + }, + calculateParamName: function (param) { + let paramName = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + } + }) + } + return paramName + }, + calculateParamDescription: function (param) { + let paramName = "" + let paramDescription = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + paramDescription = checkpoint.description + } + }) + } + return paramName + ": " + paramDescription + }, + operatorName: function (operatorCode) { + let operatorName = operatorCode + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + } + }) + } + + return operatorName + }, + operatorDescription: function (operatorCode) { + let operatorName = operatorCode + let operatorDescription = "" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + operatorDescription = v.description + } + }) + } + + return operatorName + ": " + operatorDescription + }, + operatorDataType: function (operatorCode) { + let operatorDataType = "none" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorDataType = v.dataType + } + }) + } + + return operatorDataType + }, + isEmptyString: function (v) { + return typeof v == "string" && v.length == 0 + } + }, + template: `
+
+ {{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} + + + + {{rule.checkpointOptions.period}}秒内请求数 + + + + + 允许{{rule.checkpointOptions.allowDomains}} + 禁止{{rule.checkpointOptions.denyDomains}} + + + + | {{paramFilter.code}} + <{{operatorName(rule.operator)}}> + {{rule.value}} + [空] + + + + ({{rule.description}}) + + 规则错误 +
+
` +}) + +Vue.component("http-firewall-rules-box", { + props: ["v-rules", "v-type"], + data: function () { + let rules = this.vRules + if (rules == null) { + rules = [] + } + return { + rules: rules + } + }, + methods: { + addRule: function () { + window.UPDATING_RULE = null + let that = this + teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { + height: "30em", + callback: function (resp) { + that.rules.push(resp.data.rule) + } + }) + }, + updateRule: function (index, rule) { + window.UPDATING_RULE = teaweb.clone(rule) + let that = this + teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { + height: "30em", + callback: function (resp) { + Vue.set(that.rules, index, resp.data.rule) + } + }) + }, + removeRule: function (index) { + let that = this + teaweb.confirm("确定要删除此规则吗?", function () { + that.rules.$remove(index) + }) + }, + operatorName: function (operatorCode) { + let operatorName = operatorCode + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + } + }) + } + + return operatorName + }, + operatorDescription: function (operatorCode) { + let operatorName = operatorCode + let operatorDescription = "" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + operatorDescription = v.description + } + }) + } + + return operatorName + ": " + operatorDescription + }, + operatorDataType: function (operatorCode) { + let operatorDataType = "none" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorDataType = v.dataType + } + }) + } + + return operatorDataType + }, + calculateParamName: function (param) { + let paramName = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + } + }) + } + return paramName + }, + calculateParamDescription: function (param) { + let paramName = "" + let paramDescription = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + paramDescription = checkpoint.description + } + }) + } + return paramName + ": " + paramDescription + }, + isEmptyString: function (v) { + return typeof v == "string" && v.length == 0 + } + }, + template: `
+ +
+
+ {{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} + + + + {{rule.checkpointOptions.period}}秒内请求数 + + + + + 允许{{rule.checkpointOptions.allowDomains}} + 禁止{{rule.checkpointOptions.denyDomains}} + + + + | {{paramFilter.code}} <{{operatorName(rule.operator)}}> + {{rule.value}} + [空] + + + + ({{rule.description}}) + + + +
+
+
+ +
` +}) + +// 通用Header长度 +let defaultGeneralHeaders = ["Cache-Control", "Connection", "Date", "Pragma", "Trailer", "Transfer-Encoding", "Upgrade", "Via", "Warning"] +Vue.component("http-cond-general-header-length", { + props: ["v-checkpoint"], + data: function () { + let headers = null + let length = null + + if (window.parent.UPDATING_RULE != null) { + let options = window.parent.UPDATING_RULE.checkpointOptions + if (options.headers != null && Array.$isArray(options.headers)) { + headers = options.headers + } + if (options.length != null) { + length = options.length + } + } + + + if (headers == null) { + headers = defaultGeneralHeaders + } + + if (length == null) { + length = 128 + } + + let that = this + setTimeout(function () { + that.change() + }, 100) + + return { + headers: headers, + length: length + } + }, + watch: { + length: function (v) { + let len = parseInt(v) + if (isNaN(len)) { + len = 0 + } + if (len < 0) { + len = 0 + } + this.length = len + this.change() + } + }, + methods: { + change: function () { + this.vCheckpoint.options = [ + { + code: "headers", + value: this.headers + }, + { + code: "length", + value: this.length + } + ] + } + }, + template: `
+ + + + + + + + + +
通用Header列表 + +

需要检查的Header列表。

+
Header值超出长度 +
+ + 字节 +
+

超出此长度认为匹配成功,0表示不限制。

+
+
` +}) + +// CC +Vue.component("http-firewall-checkpoint-cc", { + props: ["v-checkpoint"], + data: function () { + let keys = [] + let period = 60 + let threshold = 1000 + let ignoreCommonFiles = true + let enableFingerprint = true + + let options = {} + if (window.parent.UPDATING_RULE != null) { + options = window.parent.UPDATING_RULE.checkpointOptions + } + + if (options == null) { + options = {} + } + if (options.keys != null) { + keys = options.keys + } + if (keys.length == 0) { + keys = ["${remoteAddr}", "${requestPath}"] + } + if (options.period != null) { + period = options.period + } + if (options.threshold != null) { + threshold = options.threshold + } + if (options.ignoreCommonFiles != null && typeof (options.ignoreCommonFiles) == "boolean") { + ignoreCommonFiles = options.ignoreCommonFiles + } + if (options.enableFingerprint != null && typeof (options.enableFingerprint) == "boolean") { + enableFingerprint = options.enableFingerprint + } + + let that = this + setTimeout(function () { + that.change() + }, 100) + + return { + keys: keys, + period: period, + threshold: threshold, + ignoreCommonFiles: ignoreCommonFiles, + enableFingerprint: enableFingerprint, + options: {}, + value: threshold + } + }, + watch: { + period: function () { + this.change() + }, + threshold: function () { + this.change() + }, + ignoreCommonFiles: function () { + this.change() + }, + enableFingerprint: function () { + this.change() + } + }, + methods: { + changeKeys: function (keys) { + this.keys = keys + this.change() + }, + change: function () { + let period = parseInt(this.period.toString()) + if (isNaN(period) || period <= 0) { + period = 60 + } + + let threshold = parseInt(this.threshold.toString()) + if (isNaN(threshold) || threshold <= 0) { + threshold = 1000 + } + this.value = threshold + + let ignoreCommonFiles = this.ignoreCommonFiles + if (typeof ignoreCommonFiles != "boolean") { + ignoreCommonFiles = false + } + + let enableFingerprint = this.enableFingerprint + if (typeof enableFingerprint != "boolean") { + enableFingerprint = true + } + + this.vCheckpoint.options = [ + { + code: "keys", + value: this.keys + }, + { + code: "period", + value: period, + }, + { + code: "threshold", + value: threshold + }, + { + code: "ignoreCommonFiles", + value: ignoreCommonFiles + }, + { + code: "enableFingerprint", + value: enableFingerprint + } + ] + }, + thresholdTooLow: function () { + let threshold = parseInt(this.threshold.toString()) + if (isNaN(threshold) || threshold <= 0) { + threshold = 1000 + } + return threshold > 0 && threshold < 5 + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + +
统计对象组合 * + +
统计周期 * +
+ + +
+
阈值 * + +

对于网站类应用来说,当前阈值设置的太低,有可能会影响用户正常访问。

+
检查请求来源指纹 + +

在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。

+
忽略常用文件 + +

忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。

+
+
` +}) + +// 防盗链 +Vue.component("http-firewall-checkpoint-referer-block", { + props: ["v-checkpoint"], + data: function () { + let allowEmpty = true + let allowSameDomain = true + let allowDomains = [] + let denyDomains = [] + let checkOrigin = true + + let options = {} + if (window.parent.UPDATING_RULE != null) { + options = window.parent.UPDATING_RULE.checkpointOptions + } + + if (options == null) { + options = {} + } + if (typeof (options.allowEmpty) == "boolean") { + allowEmpty = options.allowEmpty + } + if (typeof (options.allowSameDomain) == "boolean") { + allowSameDomain = options.allowSameDomain + } + if (options.allowDomains != null && typeof (options.allowDomains) == "object") { + allowDomains = options.allowDomains + } + if (options.denyDomains != null && typeof (options.denyDomains) == "object") { + denyDomains = options.denyDomains + } + if (typeof options.checkOrigin == "boolean") { + checkOrigin = options.checkOrigin + } + + let that = this + setTimeout(function () { + that.change() + }, 100) + + return { + allowEmpty: allowEmpty, + allowSameDomain: allowSameDomain, + allowDomains: allowDomains, + denyDomains: denyDomains, + checkOrigin: checkOrigin, + options: {}, + value: 0 + } + }, + watch: { + allowEmpty: function () { + this.change() + }, + allowSameDomain: function () { + this.change() + }, + checkOrigin: function () { + this.change() + } + }, + methods: { + changeAllowDomains: function (values) { + this.allowDomains = values + this.change() + }, + changeDenyDomains: function (values) { + this.denyDomains = values + this.change() + }, + change: function () { + this.vCheckpoint.options = [ + { + code: "allowEmpty", + value: this.allowEmpty + }, + { + code: "allowSameDomain", + value: this.allowSameDomain, + }, + { + code: "allowDomains", + value: this.allowDomains + }, + { + code: "denyDomains", + value: this.denyDomains + }, + { + code: "checkOrigin", + value: this.checkOrigin + } + ] + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + +
来源域名允许为空 + +

允许不带来源的访问。

+
来源域名允许一致 + +

允许来源域名和当前访问的域名一致,相当于在站内访问。

+
允许的来源域名 + +

允许的来源域名列表,比如example.com(顶级域名)、*.example.com(example.com的所有二级域名)。单个星号*表示允许所有域名。

+
禁止的来源域名 + +

禁止的来源域名列表,比如example.org(顶级域名)、*.example.org(example.org的所有二级域名);除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。

+
同时检查Origin + +

如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。

+
+
` +}) + +Vue.component("http-header-assistant", { + props: ["v-type", "v-value"], + mounted: function () { + let that = this + Tea.action("/servers/headers/options?type=" + this.vType) + .post() + .success(function (resp) { + that.allHeaders = resp.data.headers + }) + }, + data: function () { + return { + allHeaders: [], + matchedHeaders: [], + + selectedHeaderName: "" + } + }, + watch: { + vValue: function (v) { + if (v != this.selectedHeaderName) { + this.selectedHeaderName = "" + } + + if (v.length == 0) { + this.matchedHeaders = [] + return + } + this.matchedHeaders = this.allHeaders.filter(function (header) { + return teaweb.match(header, v) + }).slice(0, 10) + } + }, + methods: { + select: function (header) { + this.$emit("select", header) + this.selectedHeaderName = header + } + }, + template: ` + {{header}} +     +` +}) + Vue.component("http-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 () { @@ -7898,8 +11124,610 @@ Vue.component("http-header-policy-box", { ` }) -Vue.component("server-feature-required", { - template: `当前网站绑定的套餐或当前用户未开通此功能。` +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-hls-config-box", { + props: ["value", "v-is-location", "v-is-group"], + data: function () { + let config = this.value + if (config == null) { + config = { + isPrior: false + } + } + + let encryptingConfig = config.encrypting + if (encryptingConfig == null) { + encryptingConfig = { + isOn: false, + onlyURLPatterns: [], + exceptURLPatterns: [] + } + config.encrypting = encryptingConfig + } + + return { + config: config, + + encryptingConfig: encryptingConfig, + encryptingMoreOptionsVisible: false + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) + }, + + showEncryptingMoreOptions: function () { + this.encryptingMoreOptionsVisible = !this.encryptingMoreOptionsVisible + } + }, + template: `
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
启用HLS加密 + +

启用后,系统会自动在.m3u8文件中加入#EXT-X-KEY:METHOD=AES-128...,并将其中的.ts文件内容进行加密。

+
例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
限制URL + +

如果填写了限制URL,表示只对这些URL进行加密处理;如果不填则表示支持所有的URL。

+
+
+
` +}) + +Vue.component("http-host-redirect-box", { + props: ["v-redirects"], + mounted: function () { + let that = this + sortTable(function (ids) { + let newRedirects = [] + ids.forEach(function (id) { + that.redirects.forEach(function (redirect) { + if (redirect.id == id) { + newRedirects.push(redirect) + } + }) + }) + that.updateRedirects(newRedirects) + }) + }, + data: function () { + let redirects = this.vRedirects + if (redirects == null) { + redirects = [] + } + + let id = 0 + redirects.forEach(function (v) { + id++ + v.id = id + }) + + return { + redirects: redirects, + statusOptions: [ + {"code": 301, "text": "Moved Permanently"}, + {"code": 308, "text": "Permanent Redirect"}, + {"code": 302, "text": "Found"}, + {"code": 303, "text": "See Other"}, + {"code": 307, "text": "Temporary Redirect"} + ], + id: id + } + }, + methods: { + add: function () { + let that = this + window.UPDATING_REDIRECT = null + + teaweb.popup("/servers/server/settings/redirects/createPopup", { + width: "50em", + height: "36em", + callback: function (resp) { + that.id++ + resp.data.redirect.id = that.id + that.redirects.push(resp.data.redirect) + that.change() + } + }) + }, + update: function (index, redirect) { + let that = this + window.UPDATING_REDIRECT = redirect + + teaweb.popup("/servers/server/settings/redirects/createPopup", { + width: "50em", + height: "36em", + callback: function (resp) { + resp.data.redirect.id = redirect.id + Vue.set(that.redirects, index, resp.data.redirect) + that.change() + } + }) + }, + remove: function (index) { + let that = this + teaweb.confirm("确定要删除这条跳转规则吗?", function () { + that.redirects.$remove(index) + that.change() + }) + }, + change: function () { + let that = this + setTimeout(function (){ + that.$emit("change", that.redirects) + }, 100) + }, + updateRedirects: function (newRedirects) { + this.redirects = newRedirects + this.change() + } + }, + template: `
+ + + + [创建] + +
+ +

暂时还没有URL跳转规则。

+
+ + + + + + + + + + + + + + + + + + + + + + + +
跳转前跳转后HTTP状态码状态操作
+
+ {{redirect.beforeURL}} +
+ URL跳转 + 匹配前缀 + 正则匹配 + 精准匹配 + 排除:{{domain}} + 仅限:{{domain}} +
+
+
+ 所有域名 + + {{redirect.domainsBefore[0]}} + {{redirect.domainsBefore[0]}}等{{redirect.domainsBefore.length}}个域名 + +
+ 域名跳转 + {{redirect.domainAfterScheme}} + 忽略端口 +
+
+
+ 所有端口 + + {{redirect.portsBefore.join(", ")}} + {{redirect.portsBefore.slice(0, 5).join(", ")}}等{{redirect.portsBefore.length}}个端口 + +
+ 端口跳转 + {{redirect.portAfterScheme}} +
+
+ +
+ 匹配条件 +
+
-> + {{redirect.afterURL}} + {{redirect.domainAfter}} + {{redirect.portAfter}} + + {{redirect.status}} + 默认 + + 修改   + 删除 +
+

所有规则匹配顺序为从上到下,可以拖动左侧的排序。

+
+
+
` +}) + +// 请求方法列表 +Vue.component("http-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等。

+
+
+
+ +
+
` +}) + +// 压缩配置 +Vue.component("http-optimization-config-box", { + props: ["v-optimization-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vOptimizationConfig + + return { + config: config, + htmlMoreOptions: false, + javascriptMoreOptions: false, + cssMoreOptions: false + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + } + }, + template: `
+ + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
HTML优化 +
+ + +
+

可以自动优化HTML中包含的空白、注释、空标签等。只有文件可以缓存时才会被优化。

+
HTML例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
HTML限制URL + +

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

+
+ + + + + + + + + + + + + + + + + + + + + +
Javascript优化 +
+ + +
+

可以自动缩短Javascript中变量、函数名称等。只有文件可以缓存时才会被优化。

+
Javascript例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
Javascript限制URL + +

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

+
+ + + + + + + + + + + + + + + + + + + + + +
CSS优化 +
+ + +
+

可以自动去除CSS中包含的空白。只有文件可以缓存时才会被优化。

+
CSS例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
CSS限制URL + +

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

+
+
+ +
+
` +}) + +Vue.component("http-oss-bucket-params", { + props: ["v-oss-config", "v-params", "name"], + data: function () { + let params = this.vParams + if (params == null) { + params = [] + } + + let ossConfig = this.vOssConfig + if (ossConfig == null) { + ossConfig = { + bucketParam: "input", + bucketName: "", + bucketArgName: "" + } + } else { + // 兼容以往 + if (ossConfig.bucketParam != null && ossConfig.bucketParam.length == 0) { + ossConfig.bucketParam = "input" + } + if (ossConfig.options != null && ossConfig.options.bucketName != null && ossConfig.options.bucketName.length > 0) { + ossConfig.bucketName = ossConfig.options.bucketName + } + } + + return { + params: params, + ossConfig: ossConfig + } + }, + template: ` + + {{name}}名称获取方式 * + + +

{{param.description.replace("\${optionName}", name)}}

+ + + + {{name}}名称 * + + +

{{name}}名称,类似于bucket-12345678

+ + + + {{name}}参数名称 * + + +

{{name}}参数名称,比如?myBucketName=BUCKET-NAME中的myBucketName

+ + +` }) Vue.component("http-pages-and-shutdown-box", { @@ -8165,19 +11993,736 @@ Vue.component("http-pages-and-shutdown-box", { ` }) -// 页面动态加密配置 -Vue.component("http-encryption-config-box", { - props: ["v-encryption-config", "v-is-location", "v-is-group"], +Vue.component("http-redirect-to-https-box", { + props: ["v-redirect-to-https-config", "v-is-location"], data: function () { - let config = this.vEncryptionConfig + 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-referers-config-box", { + props: ["v-referers-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vReferersConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + allowEmpty: true, + allowSameDomain: true, + allowDomains: [], + denyDomains: [], + checkOrigin: true + } + } + if (config.allowDomains == null) { + config.allowDomains = [] + } + if (config.denyDomains == null) { + config.denyDomains = [] + } + return { + config: config, + moreOptionsVisible: false + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + }, + changeAllowDomains: function (domains) { + if (typeof (domains) == "object") { + this.config.allowDomains = domains + this.$forceUpdate() + } + }, + changeDenyDomains: function (domains) { + if (typeof (domains) == "object") { + this.config.denyDomains = domains + this.$forceUpdate() + } + }, + showMoreOptions: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用防盗链 +
+ + +
+

选中后表示开启防盗链。

+
允许直接访问网站 + +

允许用户直接访问网站,用户第一次访问网站时来源域名通常为空。

+
来源域名允许一致 + +

允许来源域名和当前访问的域名一致,相当于在站内访问。

+
允许的来源域名 + > +

允许的其他来源域名列表,比如example.com*.example.com。单个星号*表示允许所有域名。

+
禁止的来源域名 + +

禁止的来源域名列表,比如example.org*.example.org;除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。

+
同时检查Origin + +

如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。

+
例外URL + +

如果填写了例外URL,表示这些URL不做处理。

+
限制URL + +

如果填写了限制URL,表示只对这些URL进行处理;如果不填则表示支持所有的URL。

+
+
+
` +}) + +Vue.component("http-remote-addr-config-box", { + props: ["v-remote-addr-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vRemoteAddrConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + value: "${rawRemoteAddr}", + type: "default", + + requestHeaderName: "" + } + } + + // type + if (config.type == null || config.type.length == 0) { + config.type = "default" + switch (config.value) { + case "${rawRemoteAddr}": + config.type = "default" + break + case "${remoteAddrValue}": + config.type = "default" + break + case "${remoteAddr}": + config.type = "proxy" + break + default: + if (config.value != null && config.value.length > 0) { + config.type = "variable" + } + } + } + + // value + if (config.value == null || config.value.length == 0) { + config.value = "${rawRemoteAddr}" + } return { config: config, - htmlMoreOptions: false, - javascriptMoreOptions: false, - keyPolicyMoreOptions: false, - cacheMoreOptions: false, - encryptionMoreOptions: false + options: [ + { + name: "直接获取", + description: "用户直接访问边缘节点,即 \"用户 --> 边缘节点\" 模式,这时候系统会试图从直接的连接中读取到客户端IP地址。", + value: "${rawRemoteAddr}", + type: "default" + }, + { + name: "从上级代理中获取", + description: "用户和边缘节点之间有别的代理服务转发,即 \"用户 --> [第三方代理服务] --> 边缘节点\",这时候只能从上级代理中获取传递的IP地址;上级代理传递的请求报头中必须包含 X-Forwarded-For 或 X-Real-IP 信息。", + value: "${remoteAddr}", + type: "proxy" + }, + { + name: "从请求报头中读取", + description: "从自定义请求报头读取客户端IP。", + value: "", + type: "requestHeader" + }, + { + name: "[自定义变量]", + description: "通过自定义变量来获取客户端真实的IP地址。", + value: "", + type: "variable" + } + ] + } + }, + watch: { + "config.requestHeaderName": function (value) { + if (this.config.type == "requestHeader"){ + this.config.value = "${header." + value.trim() + "}" + } + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + }, + changeOptionType: function () { + let that = this + + switch(this.config.type) { + case "default": + this.config.value = "${rawRemoteAddr}" + break + case "proxy": + this.config.value = "${remoteAddr}" + break + case "requestHeader": + this.config.value = "" + if (this.requestHeaderName != null && this.requestHeaderName.length > 0) { + this.config.value = "${header." + this.requestHeaderName + "}" + } + + setTimeout(function () { + that.$refs.requestHeaderInput.focus() + }) + break + case "variable": + this.config.value = "${rawRemoteAddr}" + + setTimeout(function () { + that.$refs.variableInput.focus() + }) + + break + } + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用访客IP设置 +
+ + +
+

选中后,表示使用自定义的请求变量获取客户端IP。

+
获取IP方式 * + +

{{option.description}}

+
请求报头 * + +

请输入包含有客户端IP的请求报头,需要注意大小写,常见的有X-Forwarded-ForX-Real-IPX-Client-IP等。

+
读取IP变量值 * + +

通过此变量获取用户的IP地址。具体可用的请求变量列表可参考官方网站文档;比如通过报头传递IP的情形,可以使用\${header.你的自定义报头}(类似于\${header.X-Forwarded-For},需要注意大小写规范)。

+
+
+
` +}) + +Vue.component("http-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-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("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-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: { @@ -8186,239 +12731,333 @@ Vue.component("http-encryption-config-box", { } }, template: `
- - - + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
启用请求限制 + +
最大并发连接数 + +

当前网站最大并发连接数,超出此限制则响应用户429代码。为0表示不限制。

+
单IP最大并发连接数 + +

单IP最大连接数,统计单个IP总连接数时不区分服务,超出此限制则响应用户429代码。为0表示不限制。当前设置的并发连接数过低,可能会影响正常用户访问,建议不小于3。

+
单连接带宽限制 + +

客户端单个请求每秒可以读取的下行流量。

+
单请求最大尺寸 + +

单个请求能发送的最大内容尺寸。

+
- -
-
- - - - - - - - - - - - - - - - -
启用页面动态加密 -
- - -
-

启用后,将对 HTML 页面中的 JavaScript 进行动态加密,有效抵御批量爬虫和脚本工具。

-
排除 URL - -

这些 URL 将跳过加密处理,支持正则表达式。

-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - -
HTML 加密 -
- - -
-

加密 HTML 页面中的 JavaScript 脚本。

-
加密内联脚本 -
- - -
-

加密 HTML 中的内联 <script> 标签内容。

-
加密外部脚本 -
- - -
-

加密通过 src 属性引入的外部 JavaScript 文件。

-
URL 匹配规则 - -

如果填写了匹配规则,表示只对这些 URL 进行加密处理;如果不填则表示支持所有的 URL。

-
- -
- - - - - - - - - - - - - - - - -
JavaScript 文件加密 -
- - -
-

加密独立的 JavaScript 文件(.js 文件)。

-
URL 匹配规则 - -

如果填写了匹配规则,表示只对这些 URL 进行加密处理;如果不填则表示支持所有的 URL。

-
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
服务器端密钥 - -

用于生成加密密钥的密码,建议使用复杂的随机字符串。默认密钥仅用于测试,生产环境请务必修改!

-
时间分片(秒) - -

加密密钥每隔多少秒更换一次。时间越短越安全,但可能影响性能。建议 60-120 秒,默认 60 秒。

-
IP CIDR 前缀长度 - -

将用户 IP 地址的前多少位作为识别依据。例如设置为 24 时,192.168.1.1 和 192.168.1.2 会被视为同一用户。默认 24。

-
简化 User-Agent -
- - -
-

开启后,只识别浏览器类型(如 Chrome、Firefox),忽略版本号等细节,避免因浏览器自动更新导致解密失败。

-
- -
- - - - - - - - - - - - - - - - - - - - -
启用缓存 -
- - -
-

开启后,相同内容的加密结果会被缓存,减少重复计算,提升响应速度。

-
缓存 TTL(秒) - -

缓存的有效期,超过这个时间后缓存会自动失效。建议与上面的"时间分片"保持一致。默认 60 秒。

-
最大缓存条目数 - -

最多缓存多少个加密结果。数量越大占用内存越多,建议根据服务器内存情况调整。默认 1000。

-
-
-
-
` }) +Vue.component("http-request-scripts-config-box", { + props: ["vRequestScriptsConfig", "v-auditing-status", "v-is-location"], + data: function () { + let config = this.vRequestScriptsConfig + if (config == null) { + config = {} + } + return { + config: config + } + }, + methods: { + changeInitGroup: function (group) { + this.config.initGroup = group + this.$forceUpdate() + }, + changeRequestGroup: function (group) { + this.config.requestGroup = group + this.$forceUpdate() + } + }, + template: `
+ +
+

请求初始化

+

在请求刚初始化时调用,此时自定义报头等尚未生效。

+
+ +
+

准备发送请求

+

在准备执行请求或者转发请求之前调用,此时自定义报头、源站等已准备好。

+
+ +
+
+
` +}) -// 压缩配置 -Vue.component("http-compression-config-box", { - props: ["v-compression-config", "v-is-location", "v-is-group"], +Vue.component("http-rewrite-rule-list", { + props: ["v-web-id", "v-rewrite-rules"], mounted: function () { - let that = this - sortLoad(function () { - that.initSortableTypes() - }) + setTimeout(this.sort, 1000) }, data: function () { - let config = this.vCompressionConfig + 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("http-stat-config-box", { + props: ["v-stat-config", "v-is-location"], + data: function () { + let stat = this.vStatConfig + if (stat == null) { + stat = { + isPrior: false, + isOn: false + } + } + return { + stat: stat + } + }, + template: `
+ + + + + + + + + +
启用统计 +
+ + +
+
+
+
` +}) + +// 请求方法列表 +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("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, - useDefaultTypes: true, - types: ["brotli", "gzip", "zstd", "deflate"], - level: 0, - decompressData: false, - gzipRef: null, - deflateRef: null, - brotliRef: null, - minLength: {count: 1, "unit": "kb"}, - maxLength: {count: 32, "unit": "mb"}, - mimeTypes: ["text/*", "application/javascript", "application/json", "application/atom+xml", "application/rss+xml", "application/xhtml+xml", "font/*", "image/svg+xml"], - extensions: [".js", ".json", ".html", ".htm", ".xml", ".css", ".woff2", ".txt"], - exceptExtensions: [".apk", ".ipa"], - conds: null, - enablePartialContent: false, - onlyURLPatterns: [], - exceptURLPatterns: [] + minLength: {count: 0, "unit": "kb"}, + maxLength: {count: 0, "unit": "kb"}, + mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico"], + extensions: [".png", ".jpeg", ".jpg", ".bmp", ".ico"], + conds: null } } - if (config.types == null) { - config.types = [] - } if (config.mimeTypes == null) { config.mimeTypes = [] } @@ -8426,49 +13065,9 @@ Vue.component("http-compression-config-box", { 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 + moreOptionsVisible: false } }, methods: { @@ -8483,14 +13082,6 @@ Vue.component("http-compression-config-box", { }) this.config.extensions = values }, - changeExceptExtensions: function (values) { - values.forEach(function (v, k) { - if (v.length > 0 && v[0] != ".") { - values[k] = "." + v - } - }) - this.config.exceptExtensions = values - }, changeMimeTypes: function (values) { this.config.mimeTypes = values }, @@ -8499,103 +13090,38 @@ Vue.component("http-compression-config-box", { }, 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: `
- + - + - - - - - - - - - - - - - - - + - + @@ -8612,341 +13138,172 @@ Vue.component("http-compression-config-box", {

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

- - - - - - - - - - - - + -
启用内容压缩启用WebP压缩
-
支持的扩展名 - -

含有这些扩展名的URL将会被压缩,不区分大小写。

-
例外扩展名 - -

含有这些扩展名的URL将不会被压缩,不区分大小写。

-
支持的MimeType - -

响应的Content-Type里包含这些MimeType的内容将会被压缩。

+

选中后表示开启自动WebP压缩;图片的宽和高均不能超过16383像素;只有满足缓存条件的图片内容才会被转换

压缩算法支持的扩展名 -
- - - -
-
-
-
-
- - -
-
-
- -

选择支持的压缩算法和优先顺序,拖动图表排序。

+ +

含有这些扩展名的URL将会被转成WebP,不区分大小写。

支持已压缩内容支持的MimeType - -

支持对已压缩内容尝试重新使用新的算法压缩;不选中表示保留当前的压缩格式。

+ +

响应的Content-Type里包含这些MimeType的内容将会被转成WebP。

支持Partial
Content
- -

支持对分片内容(PartialContent)的压缩;除非客户端有特殊要求,一般不需要启用。

-
例外URL - -

如果填写了例外URL,表示这些URL跳过不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行压缩处理;如果不填则表示支持所有的URL。

-
匹配条件 -
-
+ +
` }) -// HTTP CC防护配置 -Vue.component("http-cc-config-box", { - props: ["v-cc-config", "v-is-location", "v-is-group"], +Vue.component("http-websocket-box", { + props: ["v-websocket-ref", "v-websocket-config", "v-is-location"], data: function () { - let config = this.vCcConfig - if (config == null) { - config = { + let websocketRef = this.vWebsocketRef + if (websocketRef == null) { + websocketRef = { isPrior: false, isOn: false, - enableFingerprint: true, - enableGET302: true, - onlyURLPatterns: [], - exceptURLPatterns: [], - useDefaultThresholds: true, - ignoreCommonFiles: true + websocketId: 0 } } - if (config.thresholds == null || config.thresholds.length == 0) { - config.thresholds = [ - { - maxRequests: 0 - }, - { - maxRequests: 0 - }, - { - maxRequests: 0 - } - ] - } - - if (typeof config.enableFingerprint != "boolean") { - config.enableFingerprint = true - } - if (typeof config.enableGET302 != "boolean") { - config.enableGET302 = true - } - - if (config.onlyURLPatterns == null) { - config.onlyURLPatterns = [] - } - if (config.exceptURLPatterns == null) { - config.exceptURLPatterns = [] - } - return { - config: config, - moreOptionsVisible: false, - minQPSPerIP: config.minQPSPerIP, - useCustomThresholds: !config.useDefaultThresholds, - - thresholdMaxRequests0: this.maxRequestsStringAtThresholdIndex(config, 0), - thresholdMaxRequests1: this.maxRequestsStringAtThresholdIndex(config, 1), - thresholdMaxRequests2: this.maxRequestsStringAtThresholdIndex(config, 2) - } - }, - watch: { - minQPSPerIP: function (v) { - let qps = parseInt(v.toString()) - if (isNaN(qps) || qps < 0) { - qps = 0 - } - this.config.minQPSPerIP = qps - }, - thresholdMaxRequests0: function (v) { - this.setThresholdMaxRequests(0, v) - }, - thresholdMaxRequests1: function (v) { - this.setThresholdMaxRequests(1, v) - }, - thresholdMaxRequests2: function (v) { - this.setThresholdMaxRequests(2, v) - }, - useCustomThresholds: function (b) { - this.config.useDefaultThresholds = !b - } - }, - methods: { - maxRequestsStringAtThresholdIndex: function (config, index) { - if (config.thresholds == null) { - return "" - } - if (index < config.thresholds.length) { - let s = config.thresholds[index].maxRequests.toString() - if (s == "0") { - s = "" - } - return s - } - return "" - }, - setThresholdMaxRequests: function (index, v) { - let maxRequests = parseInt(v) - if (isNaN(maxRequests) || maxRequests < 0) { - maxRequests = 0 - } - if (index < this.config.thresholds.length) { - this.config.thresholds[index].maxRequests = maxRequests - } - }, - showMoreOptions: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用CC无感防护 - -

启用后,自动检测并拦截CC攻击。

-
例外URL - -

如果填写了例外URL,表示这些URL跳过CC防护不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行CC防护处理;如果不填则表示支持所有的URL。

-
忽略常用文件 - -

忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。

-
检查请求来源指纹 - -

在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。

-
启用GET302校验 - -

选中后,表示自动通过GET302方法来校验客户端。

-
单IP最低QPS -
- - 请求数/秒 -
-

当某个IP在1分钟内平均QPS达到此值时,才会开始检测;如果设置为0,表示任何访问都会检测。(注意这里设置的是检测开启阈值,不是拦截阈值,拦截阈值在当前表单下方可以设置)

-
使用自定义拦截阈值 - -
自定义拦截阈值设置 -
-
- 单IP每5秒最多 - - 请求 -
-
- -
-
- 单IP每60秒 - - 请求 -
-
-
-
- 单IP每300秒 - - 请求 -
-
-
-
-
` -}) - -Vue.component("prior-checkbox", { - props: ["v-config"], - data: function () { - return { - isPrior: this.vConfig.isPrior - } - }, - watch: { - isPrior: function (v) { - this.vConfig.isPrior = v - } - }, - template: ` - - 打开独立配置 - -
- - -
-

[已打开] 打开后可以覆盖父级或子级配置。

- - -` -}) - -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, + let websocketConfig = this.vWebsocketConfig + if (websocketConfig == null) { + websocketConfig = { + id: 0, isOn: false, - charset: "", - isUpper: false, - force: 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 { - charsetConfig: charsetConfig, + 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.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: `
- + + - - + + - + - + - - + - - + - + + + + - + + + + + + + + + + + + + + @@ -8955,287 +13312,149 @@ Vue.component("http-charsets-box", { ` }) -Vue.component("http-expires-time-config-box", { - props: ["v-expires-time"], +// 指标对象 +Vue.component("metric-keys-config-box", { + props: ["v-keys"], data: function () { - let expiresTime = this.vExpiresTime - if (expiresTime == null) { - expiresTime = { - isPrior: false, - isOn: false, - overwrite: true, - autoCalculate: true, - duration: {count: -1, "unit": "hour"} - } + let keys = this.vKeys + if (keys == null) { + keys = [] } return { - expiresTime: expiresTime + keys: keys, + isAdding: false, + key: "", + subKey: "", + keyDescription: "", + + keyDefs: window.METRIC_HTTP_KEYS } }, watch: { - "expiresTime.isPrior": function () { - this.notifyChange() - }, - "expiresTime.isOn": function () { - this.notifyChange() - }, - "expiresTime.overwrite": function () { - this.notifyChange() - }, - "expiresTime.autoCalculate": function () { - this.notifyChange() + keys: function () { + this.$emit("change", this.keys) } }, methods: { - notifyChange: function () { - this.$emit("change", this.expiresTime) - } - }, - template: `
-
启用字符编码启用Websocket
- +
选择字符编码 + 允许所有来源域(Origin) +
+ + +
+

选中表示允许所有的来源域。

强制替换允许的来源域列表(Origin) - -

选中后,表示强制覆盖已经设置的字符集;不选中,表示如果源站已经设置了字符集,则保留不修改。

+
+
+ {{origin}} +
+
+
+ +

只允许在列表中的来源域名访问Websocket服务。

字符编码大写传递请求来源域
- +
-

选中后将指定的字符编码转换为大写,比如默认为utf-8,选中后将改为UTF-8

+

选中后,表示把接收到的请求中的Origin字段传递到源站。

+
指定传递的来源域 + +

指定向源站传递的Origin字段值。

+
握手超时时间(Handshake) +
+
+ +
+
+ 秒 +
+
+

0表示使用默认的时间设置。

- - - - - - - - - - - - - - - - - - - -
启用 -

启用后,将会在响应的Header中添加Expires字段,浏览器据此会将内容缓存在客户端;同时,在管理后台执行清理缓存时,也将无法清理客户端已有的缓存。

-
覆盖源站设置 - -

选中后,会覆盖源站Header中已有的Expires字段。

-
自动计算时间 -

根据已设置的缓存有效期进行计算。

-
强制缓存时间 - -

从客户端访问的时间开始要缓存的时长。

-
-
` -}) - -Vue.component("http-access-log-box", { - props: ["v-access-log"], - 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" - } - } - 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); + cancel: function () { + this.key = "" + this.subKey = "" + this.keyDescription = "" + this.isAdding = false }, - showLog: function () { + 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 - let requestId = this.accessLog.requestId - this.$parent.$children.forEach(function (v) { - if (v.deselect != null) { - v.deselect() + setTimeout(function () { + if (that.$refs.key != null) { + that.$refs.key.focus() } - }) - this.select() - teaweb.popup("/servers/server/log/viewPopup?requestId=" + requestId, { - width: "50em", - height: "24em", - onClose: function () { - that.deselect() - } - }) + }, 100) }, - select: function () { - this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)" + remove: function (index) { + this.keys.$remove(index) }, - deselect: function () { - this.$refs.box.parentNode.style.cssText = "" - } - }, - template: `
- [{{accessLog.region}}] {{accessLog.remoteAddr}} [{{accessLog.timeLocal}}] "{{accessLog.requestMethod}} {{accessLog.scheme}}://{{accessLog.host}}{{accessLog.requestURI}} {{accessLog.proto}}" {{accessLog.status}} [cached] waf {{accessLog.firewallActions}} - {{tag}} - 耗时:{{formatCost(accessLog.requestTime)}} ms -   -
` -}) - -Vue.component("http-access-log-config-box", { - props: ["v-access-log-config", "v-fields", "v-default-field-codes", "v-access-log-policies", "v-is-location", "v-is-group"], - data: function () { - let that = this - - // 初始化 - setTimeout(function () { - that.changeFields() - that.changePolicy() - }, 100) - - let accessLog = { - isPrior: false, - isOn: false, - fields: [], - status1: true, - status2: true, - status3: true, - status4: true, - status5: true, - - storageOnly: false, - storagePolicies: [], - - firewallOnly: 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) + changeKey: function () { + if (this.key.length == 0) { + return } - }) - this.vAccessLogPolicies.forEach(function (v) { - v.isChecked = accessLog.storagePolicies.$contains(v.id) - }) - - return { - accessLog: accessLog, - showAdvancedOptions: false - } - }, - methods: { - changeFields: function () { - this.accessLog.fields = this.vFields.filter(function (v) { - return v.isChecked - }).map(function (v) { - return v.code + let that = this + let def = this.keyDefs.$find(function (k, v) { + return v.code == that.key }) + if (def != null) { + this.keyDescription = def.description + } }, - changePolicy: function () { - this.accessLog.storagePolicies = this.vAccessLogPolicies.filter(function (v) { - return v.isChecked - }).map(function (v) { - return v.id + 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 }, - changeAdvanced: function (v) { - this.showAdvancedOptions = v + 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: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用访问日志 -
- - -
-
要存储的访问日志字段 -
- - -
-
要存储的访问日志状态码 -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
选择输出的日志策略 - 暂时还没有缓存策略。 -
-
- - -
-
-
是否只输出到日志策略 -
- - -
-

选中表示只输出日志到日志策略,而停止默认的日志存储。

-
只记录WAF相关日志 - -

选中后只记录WAF相关的日志。

-
-
+ +
+
+ {{keyName(key)}}   +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+

{{keyDescription}}

+
+
+ +
` }) @@ -9374,220 +13593,222 @@ Vue.component("origin-input-box", { ` }) -// 基本认证用户配置 -Vue.component("http-auth-basic-auth-user-box", { - props: ["v-users"], +Vue.component("origin-list-box", { + props: ["v-primary-origins", "v-backup-origins", "v-server-type", "v-params"], data: function () { - let users = this.vUsers - if (users == null) { - users = [] - } return { - users: users, - isAdding: false, - updatingIndex: -1, - - username: "", - password: "" + primaryOrigins: this.vPrimaryOrigins, + backupOrigins: this.vBackupOrigins } }, 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("script-config-box", { - props: ["id", "v-script-config", "comment", "v-auditing-status"], - data: function () { - let config = this.vScriptConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - code: "", - auditingCode: "" - } - } - - let auditingStatus = null - if (config.auditingCodeMD5 != null && config.auditingCodeMD5.length > 0 && config.auditingCode != null && config.auditingCode.length > 0) { - config.code = config.auditingCode - - if (this.vAuditingStatus != null) { - for (let i = 0; i < this.vAuditingStatus.length; i ++ ){ - let status = this.vAuditingStatus[i] - if (status.md5 == config.auditingCodeMD5) { - auditingStatus = status - break - } + createPrimaryOrigin: function () { + teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, { + width: "45em", + height: "27em", + callback: function (resp) { + teaweb.success("保存成功", function () { + window.location.reload() + }) } + }) + }, + createBackupOrigin: function () { + teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, { + width: "45em", + height: "27em", + callback: function (resp) { + teaweb.success("保存成功", function () { + window.location.reload() + }) + } + }) + }, + updateOrigin: function (originId, originType) { + teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, { + width: "45em", + height: "27em", + callback: function (resp) { + teaweb.success("保存成功", function () { + window.location.reload() + }) + } + }) + }, + deleteOrigin: function (originId, originAddr, originType) { + let that = this + teaweb.confirm("确定要删除此源站(" + originAddr + ")吗?", function () { + Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType) + .post() + .success(function () { + teaweb.success("删除成功", function () { + window.location.reload() + }) + }) + }) + }, + updateOriginIsOn: function (originId, originAddr, isOn) { + let message + let resultMessage + if (isOn) { + message = "确定要启用此源站(" + originAddr + ")吗?" + resultMessage = "启用成功" + } else { + message = "确定要停用此源站(" + originAddr + ")吗?" + resultMessage = "停用成功" } - } - - if (config.code.length == 0) { - config.code = "\n\n\n\n" - } - - return { - config: config, - auditingStatus: auditingStatus - } - }, - watch: { - "config.isOn": function () { - this.change() - } - }, - methods: { - change: function () { - this.$emit("change", this.config) - }, - changeCode: function (code) { - this.config.code = code - this.change() - } - }, - template: `
- - - - - - - - - - - - - -
启用脚本设置
脚本代码 -

- 管理员审核结果:审核通过。 - 管理员审核结果:驳回     驳回理由:{{auditingStatus.rejectedReason}} - 当前脚本将在审核后生效,请耐心等待审核结果。 -

-

管理员审核结果:审核通过。

- {{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" + let that = this + teaweb.confirm(message, function () { + Tea.action("/servers/server/settings/origins/updateIsOn?" + that.vParams + "&originId=" + originId + "&isOn=" + (isOn ? 1 : 0)) + .post() + .success(function () { + teaweb.success(resultMessage, function () { + window.location.reload() + }) + }) }) } }, template: `
-
-
- {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   -
-
+

主要源站 [添加主要源站]

+

暂时还没有主要源站。

+ + +

备用源站 [添加备用源站]

+

暂时还没有备用源站。

+
` }) +Vue.component("origin-list-table", { + props: ["v-origins", "v-origin-type"], + data: function () { + let hasMatchedDomains = false + let origins = this.vOrigins + if (origins != null && origins.length > 0) { + origins.forEach(function (origin) { + if (origin.domains != null && origin.domains.length > 0) { + hasMatchedDomains = true + } + }) + } + + return { + hasMatchedDomains: hasMatchedDomains + } + }, + methods: { + deleteOrigin: function (originId, originAddr) { + this.$emit("deleteOrigin", originId, originAddr, this.vOriginType) + }, + updateOrigin: function (originId) { + this.$emit("updateOrigin", originId, this.vOriginType) + }, + updateOriginIsOn: function (originId, originAddr, isOn) { + this.$emit("updateOriginIsOn", originId, originAddr, isOn) + } + }, + template: ` + + + + + + + + + + + + + + + + + +
源站地址权重状态操作
+ {{origin.addr}}   +
+ 对象存储 + {{origin.name}} + 证书 + 主机名: {{origin.host}} + 端口跟随 + HTTP/2 + + 匹配: {{domain}} + 匹配: 所有域名 +
+
{{origin.weight}} + + + 修改   + 停用启用   + 删除 +
` +}) + +Vue.component("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("prior-checkbox", { + props: ["v-config"], + data: function () { + return { + isPrior: this.vConfig.isPrior + } + }, + watch: { + isPrior: function (v) { + this.vConfig.isPrior = v + } + }, + template: ` + + 打开独立配置 + +
+ + +
+

[已打开] 打开后可以覆盖父级或子级配置。

+ + +` +}) + Vue.component("reverse-proxy-box", { props: ["v-reverse-proxy-ref", "v-reverse-proxy-config", "v-is-location", "v-family"], data: function () { @@ -9802,339 +14023,125 @@ Vue.component("reverse-proxy-box", { ` }) -Vue.component("http-firewall-param-filters-box", { - props: ["v-filters"], +Vue.component("script-config-box", { + props: ["id", "v-script-config", "comment", "v-auditing-status"], 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 + let config = this.vScriptConfig if (config == null) { config = { isPrior: false, isOn: false, - value: "${rawRemoteAddr}", - type: "default", - - requestHeaderName: "" + code: "", + auditingCode: "" } } - // type - if (config.type == null || config.type.length == 0) { - config.type = "default" - switch (config.value) { - case "${rawRemoteAddr}": - config.type = "default" - break - case "${remoteAddrValue}": - config.type = "default" - break - case "${remoteAddr}": - config.type = "proxy" - break - default: - if (config.value != null && config.value.length > 0) { - config.type = "variable" + let auditingStatus = null + if (config.auditingCodeMD5 != null && config.auditingCodeMD5.length > 0 && config.auditingCode != null && config.auditingCode.length > 0) { + config.code = config.auditingCode + + if (this.vAuditingStatus != null) { + for (let i = 0; i < this.vAuditingStatus.length; i ++ ){ + let status = this.vAuditingStatus[i] + if (status.md5 == config.auditingCodeMD5) { + auditingStatus = status + break } + } } } - // value - if (config.value == null || config.value.length == 0) { - config.value = "${rawRemoteAddr}" + if (config.code.length == 0) { + config.code = "\n\n\n\n" } return { config: config, - options: [ - { - name: "直接获取", - description: "用户直接访问边缘节点,即 \"用户 --> 边缘节点\" 模式,这时候系统会试图从直接的连接中读取到客户端IP地址。", - value: "${rawRemoteAddr}", - type: "default" - }, - { - name: "从上级代理中获取", - description: "用户和边缘节点之间有别的代理服务转发,即 \"用户 --> [第三方代理服务] --> 边缘节点\",这时候只能从上级代理中获取传递的IP地址;上级代理传递的请求报头中必须包含 X-Forwarded-For 或 X-Real-IP 信息。", - value: "${remoteAddr}", - type: "proxy" - }, - { - name: "从请求报头中读取", - description: "从自定义请求报头读取客户端IP。", - value: "", - type: "requestHeader" - }, - { - name: "[自定义变量]", - description: "通过自定义变量来获取客户端真实的IP地址。", - value: "", - type: "variable" - } - ] + auditingStatus: auditingStatus } }, watch: { - "config.requestHeaderName": function (value) { - if (this.config.type == "requestHeader"){ - this.config.value = "${header." + value.trim() + "}" - } + "config.isOn": function () { + this.change() } }, methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + change: function () { + this.$emit("change", this.config) }, - changeOptionType: function () { - let that = this - - switch(this.config.type) { - case "default": - this.config.value = "${rawRemoteAddr}" - break - case "proxy": - this.config.value = "${remoteAddr}" - break - case "requestHeader": - this.config.value = "" - if (this.requestHeaderName != null && this.requestHeaderName.length > 0) { - this.config.value = "${header." + this.requestHeaderName + "}" - } - - setTimeout(function () { - that.$refs.requestHeaderInput.focus() - }) - break - case "variable": - this.config.value = "${rawRemoteAddr}" - - setTimeout(function () { - that.$refs.variableInput.focus() - }) - - break - } + changeCode: function (code) { + this.config.code = code + this.change() } }, template: `
- - - + - - + + - - - + + + - - - - - - - - - - - -
启用访客IP设置 -
- - -
-

选中后,表示使用自定义的请求变量获取客户端IP。

-
启用脚本设置
获取IP方式 *
脚本代码 - -

{{option.description}}

-
请求报头 * - -

请输入包含有客户端IP的请求报头,需要注意大小写,常见的有X-Forwarded-ForX-Real-IPX-Client-IP等。

-
读取IP变量值 * - -

通过此变量获取用户的IP地址。具体可用的请求变量列表可参考官方网站文档;比如通过报头传递IP的情形,可以使用\${header.你的自定义报头}(类似于\${header.X-Forwarded-For},需要注意大小写规范)。

+

+ 管理员审核结果:审核通过。 + 管理员审核结果:驳回     驳回理由:{{auditingStatus.rejectedReason}} + 当前脚本将在审核后生效,请耐心等待审核结果。 +

+

管理员审核结果:审核通过。

+ {{config.code}} +

{{comment}}

-
` }) -// 访问日志搜索框 -Vue.component("http-access-log-search-box", { - props: ["v-ip", "v-domain", "v-keyword", "v-cluster-id", "v-node-id"], +Vue.component("script-group-config-box", { + props: ["v-group", "v-auditing-status", "v-is-location"], data: function () { - let ip = this.vIp - if (ip == null) { - ip = "" + let group = this.vGroup + if (group == null) { + group = { + isPrior: false, + isOn: true, + scripts: [] + } + } + if (group.scripts == null) { + group.scripts = [] } - let domain = this.vDomain - if (domain == null) { - domain = "" - } - - let keyword = this.vKeyword - if (keyword == null) { - keyword = "" + let script = null + if (group.scripts.length > 0) { + script = group.scripts[group.scripts.length - 1] } return { - ip: ip, - domain: domain, - keyword: keyword, - clusterId: this.vClusterId + group: group, + script: script } }, methods: { - cleanIP: function () { - this.ip = "" - this.submit() + changeScript: function (script) { + this.group.scripts = [script] // 目前只支持单个脚本 + this.change() }, - 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 + change: function () { + this.$emit("change", this.group) } }, - template: `
-
-
-
-
- IP - - -
+ template: `
+ + +
+
+
-
-
- 域名 - - -
-
-
-
- 关键词 - - -
-
-
-
- -
-
-
` }) @@ -10159,1771 +14166,8 @@ Vue.component("server-config-copy-link", { template: `批量 ` }) -// 指标对象 -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-webp-config-box", { - props: ["v-webp-config", "v-is-location", "v-is-group", "v-require-cache"], - data: function () { - let config = this.vWebpConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - minLength: {count: 0, "unit": "kb"}, - maxLength: {count: 0, "unit": "kb"}, - mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico"], - extensions: [".png", ".jpeg", ".jpg", ".bmp", ".ico"], - conds: null - } - } - - if (config.mimeTypes == null) { - config.mimeTypes = [] - } - if (config.extensions == null) { - config.extensions = [] - } - - return { - config: config, - moreOptionsVisible: false - } - }, - methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn - }, - changeExtensions: function (values) { - values.forEach(function (v, k) { - if (v.length > 0 && v[0] != ".") { - values[k] = "." + v - } - }) - this.config.extensions = values - }, - changeMimeTypes: function (values) { - this.config.mimeTypes = values - }, - changeAdvancedVisible: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - }, - changeConds: function (conds) { - this.config.conds = conds - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用WebP压缩 -
- - -
-

选中后表示开启自动WebP压缩;图片的宽和高均不能超过16383像素;只有满足缓存条件的图片内容才会被转换

-
支持的扩展名 - -

含有这些扩展名的URL将会被转成WebP,不区分大小写。

-
支持的MimeType - -

响应的Content-Type里包含这些MimeType的内容将会被转成WebP。

-
内容最小长度 - -

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

-
内容最大长度 - -

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

-
匹配条件 - -
-
-
` -}) - -Vue.component("origin-scheduling-view-box", { - props: ["v-scheduling", "v-params"], - data: function () { - let scheduling = this.vScheduling - if (scheduling == null) { - scheduling = {} - } - return { - scheduling: scheduling - } - }, - methods: { - update: function () { - teaweb.popup("/servers/server/settings/reverseProxy/updateSchedulingPopup?" + this.vParams, { - height: "21em", - callback: function () { - window.location.reload() - }, - }) - } - }, - template: `
-
- - - - - -
当前正在使用的算法 - {{scheduling.name}}   [修改] -

{{scheduling.description}}

-
-
` -}) - -Vue.component("http-hls-config-box", { - props: ["value", "v-is-location", "v-is-group"], - data: function () { - let config = this.value - if (config == null) { - config = { - isPrior: false - } - } - - let encryptingConfig = config.encrypting - if (encryptingConfig == null) { - encryptingConfig = { - isOn: false, - onlyURLPatterns: [], - exceptURLPatterns: [] - } - config.encrypting = encryptingConfig - } - - return { - config: config, - - encryptingConfig: encryptingConfig, - encryptingMoreOptionsVisible: false - } - }, - methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) - }, - - showEncryptingMoreOptions: function () { - this.encryptingMoreOptionsVisible = !this.encryptingMoreOptionsVisible - } - }, - template: `
- - - -
- - - - - - - - - - - - - - - - - - - - - - - -
启用HLS加密 - -

启用后,系统会自动在.m3u8文件中加入#EXT-X-KEY:METHOD=AES-128...,并将其中的.ts文件内容进行加密。

-
例外URL - -

如果填写了例外URL,表示这些URL跳过不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行加密处理;如果不填则表示支持所有的URL。

-
-
-
` -}) - -Vue.component("http-oss-bucket-params", { - props: ["v-oss-config", "v-params", "name"], - data: function () { - let params = this.vParams - if (params == null) { - params = [] - } - - let ossConfig = this.vOssConfig - if (ossConfig == null) { - ossConfig = { - bucketParam: "input", - bucketName: "", - bucketArgName: "" - } - } else { - // 兼容以往 - if (ossConfig.bucketParam != null && ossConfig.bucketParam.length == 0) { - ossConfig.bucketParam = "input" - } - if (ossConfig.options != null && ossConfig.options.bucketName != null && ossConfig.options.bucketName.length > 0) { - ossConfig.bucketName = ossConfig.options.bucketName - } - } - - return { - params: params, - ossConfig: ossConfig - } - }, - template: ` - - {{name}}名称获取方式 * - - -

{{param.description.replace("\${optionName}", name)}}

- - - - {{name}}名称 * - - -

{{name}}名称,类似于bucket-12345678

- - - - {{name}}参数名称 * - - -

{{name}}参数名称,比如?myBucketName=BUCKET-NAME中的myBucketName

- - -` -}) - -Vue.component("http-request-scripts-config-box", { - props: ["vRequestScriptsConfig", "v-auditing-status", "v-is-location"], - data: function () { - let config = this.vRequestScriptsConfig - if (config == null) { - config = {} - } - - return { - config: config - } - }, - methods: { - changeInitGroup: function (group) { - this.config.initGroup = group - this.$forceUpdate() - }, - changeRequestGroup: function (group) { - this.config.requestGroup = group - this.$forceUpdate() - } - }, - template: `
- -
-

请求初始化

-

在请求刚初始化时调用,此时自定义报头等尚未生效。

-
- -
-

准备发送请求

-

在准备执行请求或者转发请求之前调用,此时自定义报头、源站等已准备好。

-
- -
-
-
` -}) - -Vue.component("http-request-cond-view", { - props: ["v-cond"], - data: function () { - return { - cond: this.vCond, - components: window.REQUEST_COND_COMPONENTS - } - }, - methods: { - typeName: function (cond) { - let c = this.components.$find(function (k, v) { - return v.type == cond.type - }) - if (c != null) { - return c.name; - } - return cond.param + " " + cond.operator - }, - updateConds: function (conds, simpleCond) { - for (let k in simpleCond) { - if (simpleCond.hasOwnProperty(k)) { - this.cond[k] = simpleCond[k] - } - } - }, - notifyChange: function () { - - } - }, - template: `
- - {{cond.param}} {{cond.operator}} - {{typeName(cond)}}: - {{cond.value}} - - -
` -}) - -Vue.component("http-header-assistant", { - props: ["v-type", "v-value"], - mounted: function () { - let that = this - Tea.action("/servers/headers/options?type=" + this.vType) - .post() - .success(function (resp) { - that.allHeaders = resp.data.headers - }) - }, - data: function () { - return { - allHeaders: [], - matchedHeaders: [], - - selectedHeaderName: "" - } - }, - watch: { - vValue: function (v) { - if (v != this.selectedHeaderName) { - this.selectedHeaderName = "" - } - - if (v.length == 0) { - this.matchedHeaders = [] - return - } - this.matchedHeaders = this.allHeaders.filter(function (header) { - return teaweb.match(header, v) - }).slice(0, 10) - } - }, - methods: { - select: function (header) { - this.$emit("select", header) - this.selectedHeaderName = header - } - }, - template: ` - {{header}} -     -` -}) - -Vue.component("http-firewall-rules-box", { - props: ["v-rules", "v-type"], - data: function () { - let rules = this.vRules - if (rules == null) { - rules = [] - } - return { - rules: rules - } - }, - methods: { - addRule: function () { - window.UPDATING_RULE = null - let that = this - teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { - height: "30em", - callback: function (resp) { - that.rules.push(resp.data.rule) - } - }) - }, - updateRule: function (index, rule) { - window.UPDATING_RULE = teaweb.clone(rule) - let that = this - teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { - height: "30em", - callback: function (resp) { - Vue.set(that.rules, index, resp.data.rule) - } - }) - }, - removeRule: function (index) { - let that = this - teaweb.confirm("确定要删除此规则吗?", function () { - that.rules.$remove(index) - }) - }, - operatorName: function (operatorCode) { - let operatorName = operatorCode - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorName = v.name - } - }) - } - - return operatorName - }, - operatorDescription: function (operatorCode) { - let operatorName = operatorCode - let operatorDescription = "" - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorName = v.name - operatorDescription = v.description - } - }) - } - - return operatorName + ": " + operatorDescription - }, - operatorDataType: function (operatorCode) { - let operatorDataType = "none" - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorDataType = v.dataType - } - }) - } - - return operatorDataType - }, - calculateParamName: function (param) { - let paramName = "" - if (param != null) { - window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { - if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { - paramName = checkpoint.name - } - }) - } - return paramName - }, - calculateParamDescription: function (param) { - let paramName = "" - let paramDescription = "" - if (param != null) { - window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { - if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { - paramName = checkpoint.name - paramDescription = checkpoint.description - } - }) - } - return paramName + ": " + paramDescription - }, - isEmptyString: function (v) { - return typeof v == "string" && v.length == 0 - } - }, - template: `
- -
-
- {{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} - - - - {{rule.checkpointOptions.period}}秒内请求数 - - - - - 允许{{rule.checkpointOptions.allowDomains}} - 禁止{{rule.checkpointOptions.denyDomains}} - - - - | {{paramFilter.code}} <{{operatorName(rule.operator)}}> - {{rule.value}} - [空] - - - - ({{rule.description}}) - - - -
-
-
- -
` -}) - -// 请求方法列表 -Vue.component("http-methods-box", { - props: ["v-methods"], - data: function () { - let methods = this.vMethods - if (methods == null) { - methods = [] - } - return { - methods: methods, - isAdding: false, - addingMethod: "" - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.addingMethod.focus() - }, 100) - }, - confirm: function () { - let that = this - - // 删除其中的空格 - this.addingMethod = this.addingMethod.replace(/\s/g, "").toUpperCase() - - if (this.addingMethod.length == 0) { - teaweb.warn("请输入要添加的请求方法", function () { - that.$refs.addingMethod.focus() - }) - return - } - - // 是否已经存在 - if (this.methods.$contains(this.addingMethod)) { - teaweb.warn("此请求方法已经存在,无需重复添加", function () { - that.$refs.addingMethod.focus() - }) - return - } - - this.methods.push(this.addingMethod) - this.cancel() - }, - remove: function (index) { - this.methods.$remove(index) - }, - cancel: function () { - this.isAdding = false - this.addingMethod = "" - } - }, - template: `
- -
- - {{method}} -   - -
-
-
-
-
- -
-
- -   -
-
-

格式为大写,比如GETPOST等。

-
-
-
- -
-
` -}) - -// URL扩展名条件 -Vue.component("http-cond-url-extension", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPathLowerExtension}", - operator: "in", - value: "[]" - } - if (this.vCond != null && this.vCond.param == cond.param) { - cond.value = this.vCond.value - } - - let extensions = [] - try { - extensions = JSON.parse(cond.value) - } catch (e) { - - } - - return { - cond: cond, - extensions: extensions, // TODO 可以拖动排序 - - isAdding: false, - addingExt: "" - } - }, - watch: { - extensions: function () { - this.cond.value = JSON.stringify(this.extensions) - } - }, - methods: { - addExt: function () { - this.isAdding = !this.isAdding - - if (this.isAdding) { - let that = this - setTimeout(function () { - that.$refs.addingExt.focus() - }, 100) - } - }, - cancelAdding: function () { - this.isAdding = false - this.addingExt = "" - }, - confirmAdding: function () { - // TODO 做更详细的校验 - // TODO 如果有重复的则提示之 - - if (this.addingExt.length == 0) { - return - } - - let that = this - this.addingExt.split(/[,;,;|]/).forEach(function (ext) { - ext = ext.trim() - if (ext.length > 0) { - if (ext[0] != ".") { - ext = "." + ext - } - ext = ext.replace(/\s+/g, "").toLowerCase() - that.extensions.push(ext) - } - }) - - // 清除状态 - this.cancelAdding() - }, - removeExt: function (index) { - this.extensions.$remove(index) - } - }, - template: `
- -
-
{{ext}}
-
-
-
-
- -
-
- - -
-
-
- -
-

扩展名需要包含点(.)符号,例如.jpg.png之类;多个扩展名用逗号分割。

-
` -}) - -// 排除URL扩展名条件 -Vue.component("http-cond-url-not-extension", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPathLowerExtension}", - operator: "not in", - value: "[]" - } - if (this.vCond != null && this.vCond.param == cond.param) { - cond.value = this.vCond.value - } - - let extensions = [] - try { - extensions = JSON.parse(cond.value) - } catch (e) { - - } - - return { - cond: cond, - extensions: extensions, // TODO 可以拖动排序 - - isAdding: false, - addingExt: "" - } - }, - watch: { - extensions: function () { - this.cond.value = JSON.stringify(this.extensions) - } - }, - methods: { - addExt: function () { - this.isAdding = !this.isAdding - - if (this.isAdding) { - let that = this - setTimeout(function () { - that.$refs.addingExt.focus() - }, 100) - } - }, - cancelAdding: function () { - this.isAdding = false - this.addingExt = "" - }, - confirmAdding: function () { - // TODO 做更详细的校验 - // TODO 如果有重复的则提示之 - - if (this.addingExt.length == 0) { - return - } - if (this.addingExt[0] != ".") { - this.addingExt = "." + this.addingExt - } - this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase() - this.extensions.push(this.addingExt) - - // 清除状态 - this.cancelAdding() - }, - removeExt: function (index) { - this.extensions.$remove(index) - } - }, - template: `
- -
-
{{ext}}
-
-
-
-
- -
-
- - -
-
-
- -
-

扩展名需要包含点(.)符号,例如.jpg.png之类。

-
` -}) - -// 根据URL前缀 -Vue.component("http-cond-url-prefix", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "prefix", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof (this.vCond.value) == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static/images,不需要带域名。

-
` -}) - -Vue.component("http-cond-url-not-prefix", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "prefix", - value: "", - isReverse: true, - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

要排除的URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static/images,不需要带域名。

-
` -}) - -// 首页 -Vue.component("http-cond-url-eq-index", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "eq", - value: "/", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

检查URL路径是为/,不需要带域名。

-
` -}) - -// 全站 -Vue.component("http-cond-url-all", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "prefix", - value: "/", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

支持全站所有URL。

-
` -}) - -// URL精准匹配 -Vue.component("http-cond-url-eq", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "eq", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

完整的URL路径,通常以/开头,比如/static/ui.js,不需要带域名。

-
` -}) - -Vue.component("http-cond-url-not-eq", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "eq", - value: "", - isReverse: true, - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

要排除的完整的URL路径,通常以/开头,比如/static/ui.js,不需要带域名。

-
` -}) - -// URL正则匹配 -Vue.component("http-cond-url-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

匹配URL的正则表达式,比如^/static/(.*).js$,不需要带域名。

-
` -}) - -// 排除URL正则匹配 -Vue.component("http-cond-url-not-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "not regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

不要匹配URL的正则表达式,意即只要匹配成功则排除此条件,比如^/static/(.*).js$,不需要带域名。

-
` -}) - -// URL通配符 -Vue.component("http-cond-url-wildcard-match", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "wildcard match", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

匹配URL的通配符,用星号(*)表示任意字符,比如(/images/*.png/static/*,不需要带域名。

-
` -}) - -// User-Agent正则匹配 -Vue.component("http-cond-user-agent-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${userAgent}", - operator: "regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

匹配User-Agent的正则表达式,比如Android|iPhone

-
` -}) - -// User-Agent正则不匹配 -Vue.component("http-cond-user-agent-not-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${userAgent}", - operator: "not regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

匹配User-Agent的正则表达式,比如Android|iPhone,如果匹配,则排除此条件。

-
` -}) - -// 根据MimeType -Vue.component("http-cond-mime-type", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: false, - param: "${response.contentType}", - operator: "mime type", - value: "[]" - } - if (this.vCond != null && this.vCond.param == cond.param) { - cond.value = this.vCond.value - } - return { - cond: cond, - mimeTypes: JSON.parse(cond.value), // TODO 可以拖动排序 - - isAdding: false, - addingMimeType: "" - } - }, - watch: { - mimeTypes: function () { - this.cond.value = JSON.stringify(this.mimeTypes) - } - }, - methods: { - addMimeType: function () { - this.isAdding = !this.isAdding - - if (this.isAdding) { - let that = this - setTimeout(function () { - that.$refs.addingMimeType.focus() - }, 100) - } - }, - cancelAdding: function () { - this.isAdding = false - this.addingMimeType = "" - }, - confirmAdding: function () { - // TODO 做更详细的校验 - // TODO 如果有重复的则提示之 - - if (this.addingMimeType.length == 0) { - return - } - this.addingMimeType = this.addingMimeType.replace(/\s+/g, "") - this.mimeTypes.push(this.addingMimeType) - - // 清除状态 - this.cancelAdding() - }, - removeMimeType: function (index) { - this.mimeTypes.$remove(index) - } - }, - template: `
- -
-
{{mimeType}}
-
-
-
-
- -
-
- - -
-
-
- -
-

服务器返回的内容的MimeType,比如text/htmlimage/*等。

-
` -}) - -// 参数匹配 -Vue.component("http-cond-params", { - props: ["v-cond"], - mounted: function () { - let cond = this.vCond - if (cond == null) { - return - } - this.operator = cond.operator - - // stringValue - if (["regexp", "not regexp", "eq", "not", "prefix", "suffix", "contains", "not contains", "eq ip", "gt ip", "gte ip", "lt ip", "lte ip", "ip range"].$contains(cond.operator)) { - this.stringValue = cond.value - return - } - - // numberValue - if (["eq int", "eq float", "gt", "gte", "lt", "lte", "mod 10", "ip mod 10", "mod 100", "ip mod 100"].$contains(cond.operator)) { - this.numberValue = cond.value - return - } - - // modValue - if (["mod", "ip mod"].$contains(cond.operator)) { - let pieces = cond.value.split(",") - this.modDivValue = pieces[0] - if (pieces.length > 1) { - this.modRemValue = pieces[1] - } - return - } - - // stringValues - let that = this - if (["in", "not in", "file ext", "mime type"].$contains(cond.operator)) { - try { - let arr = JSON.parse(cond.value) - if (arr != null && (arr instanceof Array)) { - arr.forEach(function (v) { - that.stringValues.push(v) - }) - } - } catch (e) { - - } - return - } - - // versionValue - if (["version range"].$contains(cond.operator)) { - let pieces = cond.value.split(",") - this.versionRangeMinValue = pieces[0] - if (pieces.length > 1) { - this.versionRangeMaxValue = pieces[1] - } - return - } - }, - data: function () { - let cond = { - isRequest: true, - param: "", - operator: window.REQUEST_COND_OPERATORS[0].op, - value: "", - isCaseInsensitive: false - } - if (this.vCond != null) { - cond = this.vCond - } - return { - cond: cond, - operators: window.REQUEST_COND_OPERATORS, - operator: window.REQUEST_COND_OPERATORS[0].op, - operatorDescription: window.REQUEST_COND_OPERATORS[0].description, - variables: window.REQUEST_VARIABLES, - variable: "", - - // 各种类型的值 - stringValue: "", - numberValue: "", - - modDivValue: "", - modRemValue: "", - - stringValues: [], - - versionRangeMinValue: "", - versionRangeMaxValue: "" - } - }, - methods: { - changeVariable: function () { - let v = this.cond.param - if (v == null) { - v = "" - } - this.cond.param = v + this.variable - }, - changeOperator: function () { - let that = this - this.operators.forEach(function (v) { - if (v.op == that.operator) { - that.operatorDescription = v.description - } - }) - - this.cond.operator = this.operator - - // 移动光标 - let box = document.getElementById("variables-value-box") - if (box != null) { - setTimeout(function () { - let input = box.getElementsByTagName("INPUT") - if (input.length > 0) { - input[0].focus() - } - }, 100) - } - }, - changeStringValues: function (v) { - this.stringValues = v - this.cond.value = JSON.stringify(v) - } - }, - watch: { - stringValue: function (v) { - this.cond.value = v - }, - numberValue: function (v) { - // TODO 校验数字 - this.cond.value = v - }, - modDivValue: function (v) { - if (v.length == 0) { - return - } - let div = parseInt(v) - if (isNaN(div)) { - div = 1 - } - this.modDivValue = div - this.cond.value = div + "," + this.modRemValue - }, - modRemValue: function (v) { - if (v.length == 0) { - return - } - let rem = parseInt(v) - if (isNaN(rem)) { - rem = 0 - } - this.modRemValue = rem - this.cond.value = this.modDivValue + "," + rem - }, - versionRangeMinValue: function (v) { - this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue - }, - versionRangeMaxValue: function (v) { - this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue - } - }, - template: ` - - 参数值 - - -
-
- -
-
- -
-
-

其中可以使用变量,类似于\${requestPath},也可以是多个变量的组合。

- - - - 操作符 - -
- -

-
- - - - 对比值 - - -
- -

要匹配的正则表达式,比如^/static/(.+).js

-
- - -
- -

要对比的数字。

-
- - -
- -

参数值除以10的余数,在0-9之间。

-
-
- -

参数值除以100的余数,在0-99之间。

-
-
-
-
除:
-
- -
-
余:
-
- -
-
-
- - -
- -

和参数值一致的字符串。

-

和参数值不一致的字符串。

-

参数值的前缀。

-

参数值的后缀为此字符串。

-

参数值包含此字符串。

-

参数值不包含此字符串。

-
-
- -

添加参数值列表。

-

添加参数值列表。

-

添加扩展名列表,比如pnghtml,不包括点。

-

添加MimeType列表,类似于text/htmlimage/*

-
-
-
-
-
-
-
-
-
- - -
- -

要对比的IP。

-
-
- -

参数中IP转换成整数后除以10的余数,在0-9之间。

-
-
- -

参数中IP转换成整数后除以100的余数,在0-99之间。

-
- - - - 不区分大小写 - -
- - -
-

选中后表示对比时忽略参数值的大小写。

- - - -` -}) - -// 请求方法列表 -Vue.component("http-status-box", { - props: ["v-status-list"], - data: function () { - let statusList = this.vStatusList - if (statusList == null) { - statusList = [] - } - return { - statusList: statusList, - isAdding: false, - addingStatus: "" - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.addingStatus.focus() - }, 100) - }, - confirm: function () { - let that = this - - // 删除其中的空格 - this.addingStatus = this.addingStatus.replace(/\s/g, "").toUpperCase() - - if (this.addingStatus.length == 0) { - teaweb.warn("请输入要添加的状态码", function () { - that.$refs.addingStatus.focus() - }) - return - } - - // 是否已经存在 - if (this.statusList.$contains(this.addingStatus)) { - teaweb.warn("此状态码已经存在,无需重复添加", function () { - that.$refs.addingStatus.focus() - }) - return - } - - // 格式 - if (!this.addingStatus.match(/^\d{3}$/)) { - teaweb.warn("请输入正确的状态码", function () { - that.$refs.addingStatus.focus() - }) - return - } - - this.statusList.push(parseInt(this.addingStatus, 10)) - this.cancel() - }, - remove: function (index) { - this.statusList.$remove(index) - }, - cancel: function () { - this.isAdding = false - this.addingStatus = "" - } - }, - template: `
- -
- - {{status}} -   - -
-
-
-
-
- -
-
- -   -
-
-

格式为三位数字,比如200404等。

-
-
-
- -
-
` +Vue.component("server-feature-required", { + template: `当前网站绑定的套餐或当前用户未开通此功能。` }) Vue.component("server-group-selector", { @@ -11980,47 +14224,1097 @@ Vue.component("server-group-selector", {
` }) -Vue.component("script-group-config-box", { - props: ["v-group", "v-auditing-status", "v-is-location"], +Vue.component("server-name-box", { + props: ["v-server-names"], data: function () { - let group = this.vGroup - if (group == null) { - group = { - isPrior: false, - isOn: true, - scripts: [] - } + let serverNames = this.vServerNames; + if (serverNames == null) { + serverNames = [] } - 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 + serverNames: serverNames, + isSearching: false, + keyword: "" } }, methods: { - changeScript: function (script) { - this.group.scripts = [script] // 目前只支持单个脚本 - this.change() + 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) + setTimeout(that.submitForm, 100) + } + }); }, - change: function () { - this.$emit("change", this.group) + + removeServerName: function (index) { + this.serverNames.$remove(index) + }, + + updateServerName: function (index, serverName) { + window.UPDATING_SERVER_NAME = teaweb.clone(serverName) + let that = this + teaweb.popup("/servers/addServerNamePopup", { + callback: function (resp) { + var serverName = resp.data.serverName + Vue.set(that.serverNames, index, serverName) + setTimeout(that.submitForm, 100) + } + }); + }, + showSearchBox: function () { + this.isSearching = !this.isSearching + if (this.isSearching) { + let that = this + setTimeout(function () { + that.$refs.keywordRef.focus() + }, 200) + } else { + this.keyword = "" + } + }, + allServerNames: function () { + if (this.serverNames == null) { + return [] + } + let result = [] + this.serverNames.forEach(function (serverName) { + if (serverName.subNames != null && serverName.subNames.length > 0) { + serverName.subNames.forEach(function (subName) { + if (subName != null && subName.length > 0) { + if (!result.$contains(subName)) { + result.push(subName) + } + } + }) + } else if (serverName.name != null && serverName.name.length > 0) { + if (!result.$contains(serverName.name)) { + result.push(serverName.name) + } + } + }) + return result + }, + submitForm: function () { + Tea.runActionOn(this.$refs.serverNamesRef.form) + } + }, + 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("server-traffic-limit-status-viewer", { + props: ["value"], + data: function () { + let targetTypeName = "流量" + if (this.value != null) { + targetTypeName = this.targetTypeToName(this.value.targetType) + } + + return { + status: this.value, + targetTypeName: targetTypeName + } + }, + methods: { + targetTypeToName: function (targetType) { + switch (targetType) { + case "traffic": + return "流量" + case "request": + return "请求数" + case "websocketConnections": + return "Websocket连接数" + } + return "流量" + } + }, + template: ` + 已达到套餐当日{{targetTypeName}}限制 + 已达到套餐当月{{targetTypeName}}限制 + 已达到套餐总体{{targetTypeName}}限制 +` +}) + +Vue.component("ssl-certs-box", { + props: [ + "v-certs", // 证书列表 + "v-cert", // 单个证书 + "v-protocol", // 协议:https|tls + "v-view-size", // 弹窗尺寸:normal, mini + "v-single-mode", // 单证书模式 + "v-description", // 描述文字 + "v-domains", // 搜索的域名列表或者函数 + "v-user-id" // 用户ID + ], + data: function () { + let certs = this.vCerts + if (certs == null) { + certs = [] + } + if (this.vCert != null) { + certs.push(this.vCert) + } + + let description = this.vDescription + if (description == null || typeof (description) != "string") { + description = "" + } + + return { + certs: certs, + description: description + } + }, + methods: { + certIds: function () { + return this.certs.map(function (v) { + return v.id + }) + }, + // 删除证书 + removeCert: function (index) { + let that = this + teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { + that.certs.$remove(index) + }) + }, + + // 选择证书 + selectCert: function () { + let that = this + let width = "54em" + let height = "32em" + let viewSize = this.vViewSize + if (viewSize == null) { + viewSize = "normal" + } + if (viewSize == "mini") { + width = "35em" + height = "20em" + } + + let searchingDomains = [] + if (this.vDomains != null) { + if (typeof this.vDomains == "function") { + let resultDomains = this.vDomains() + if (resultDomains != null && typeof resultDomains == "object" && (resultDomains instanceof Array)) { + searchingDomains = resultDomains + } + } else if (typeof this.vDomains == "object" && (this.vDomains instanceof Array)) { + searchingDomains = this.vDomains + } + if (searchingDomains.length > 10000) { + searchingDomains = searchingDomains.slice(0, 10000) + } + } + + let selectedCertIds = this.certs.map(function (cert) { + return cert.id + }) + let userId = this.vUserId + if (userId == null) { + userId = 0 + } + + teaweb.popup("/servers/certs/selectPopup?viewSize=" + viewSize + "&searchingDomains=" + window.encodeURIComponent(searchingDomains.join(",")) + "&selectedCertIds=" + selectedCertIds.join(",") + "&userId=" + userId, { + width: width, + height: height, + callback: function (resp) { + if (resp.data.cert != null) { + that.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 上传证书 + uploadCert: function () { + let that = this + let userId = this.vUserId + if (typeof userId != "number" && typeof userId != "string") { + userId = 0 + } + teaweb.popup("/servers/certs/uploadPopup?userId=" + userId, { + height: "28em", + callback: function (resp) { + teaweb.success("上传成功", function () { + if (resp.data.cert != null) { + that.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + }) + } + }) + }, + + // 批量上传 + uploadBatch: function () { + let that = this + let userId = this.vUserId + if (typeof userId != "number" && typeof userId != "string") { + userId = 0 + } + teaweb.popup("/servers/certs/uploadBatchPopup?userId=" + userId, { + callback: function (resp) { + if (resp.data.cert != null) { + that.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 格式化时间 + formatTime: function (timestamp) { + return new Date(timestamp * 1000).format("Y-m-d") + }, + + // 判断是否显示选择|上传按钮 + buttonsVisible: function () { + return this.vSingleMode == null || !this.vSingleMode || this.certs == null || this.certs.length == 0 + } + }, + template: `
+ +
+
+ {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   +
+
+
+
+ 选择或上传证书后HTTPSTLS服务才能生效。 + {{description}} +
+
+
+   + |   +   +   +
+
` +}) + +Vue.component("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("ssl-config-box", { + props: [ + "v-ssl-policy", + "v-protocol", + "v-server-id", + "v-support-http3" + ], + created: function () { + let that = this + setTimeout(function () { + that.sortableCipherSuites() + }, 100) + }, + data: function () { + let policy = this.vSslPolicy + if (policy == null) { + policy = { + id: 0, + isOn: true, + certRefs: [], + certs: [], + clientCARefs: [], + clientCACerts: [], + clientAuthType: 0, + minVersion: "TLS 1.1", + hsts: null, + cipherSuitesIsOn: false, + cipherSuites: [], + http2Enabled: true, + http3Enabled: false, + ocspIsOn: false + } + } else { + if (policy.certRefs == null) { + policy.certRefs = [] + } + if (policy.certs == null) { + policy.certs = [] + } + if (policy.clientCARefs == null) { + policy.clientCARefs = [] + } + if (policy.clientCACerts == null) { + policy.clientCACerts = [] + } + if (policy.cipherSuites == null) { + policy.cipherSuites = [] + } + } + + let hsts = policy.hsts + let hstsMaxAgeString = "31536000" + if (hsts == null) { + hsts = { + isOn: false, + maxAge: 31536000, + includeSubDomains: false, + preload: false, + domains: [] + } + } + if (hsts.maxAge != null) { + hstsMaxAgeString = hsts.maxAge.toString() + } + + return { + policy: policy, + + // hsts + hsts: hsts, + hstsOptionsVisible: false, + hstsDomainAdding: false, + hstsMaxAgeString: hstsMaxAgeString, + addingHstsDomain: "", + hstsDomainEditingIndex: -1, + + // 相关数据 + allVersions: window.SSL_ALL_VERSIONS, + allCipherSuites: window.SSL_ALL_CIPHER_SUITES.$copy(), + modernCipherSuites: window.SSL_MODERN_CIPHER_SUITES, + intermediateCipherSuites: window.SSL_INTERMEDIATE_CIPHER_SUITES, + allClientAuthTypes: window.SSL_ALL_CLIENT_AUTH_TYPES, + cipherSuitesVisible: false, + + // 高级选项 + moreOptionsVisible: false + } + }, + watch: { + hsts: { + deep: true, + handler: function () { + this.policy.hsts = this.hsts + } + } + }, + methods: { + // 删除证书 + removeCert: function (index) { + let that = this + teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { + that.policy.certRefs.$remove(index) + that.policy.certs.$remove(index) + }) + }, + + // 选择证书 + selectCert: function () { + let that = this + let selectedCertIds = [] + if (this.policy != null && this.policy.certs.length > 0) { + this.policy.certs.forEach(function (cert) { + selectedCertIds.push(cert.id.toString()) + }) + } + let serverId = this.vServerId + if (serverId == null) { + serverId = 0 + } + teaweb.popup("/servers/certs/selectPopup?selectedCertIds=" + selectedCertIds + "&serverId=" + serverId, { + width: "50em", + height: "30em", + callback: function (resp) { + if (resp.data.cert != null && resp.data.certRef != null) { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + } + if (resp.data.certs != null && resp.data.certRefs != null) { + that.policy.certRefs.$pushAll(resp.data.certRefs) + that.policy.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 上传证书 + uploadCert: function () { + let that = this + let serverId = this.vServerId + if (typeof serverId != "number" && typeof serverId != "string") { + serverId = 0 + } + teaweb.popup("/servers/certs/uploadPopup?serverId=" + serverId, { + height: "30em", + callback: function (resp) { + teaweb.success("上传成功", function () { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + }) + } + }) + }, + + // 批量上传 + uploadBatch: function () { + let that = this + let serverId = this.vServerId + if (typeof serverId != "number" && typeof serverId != "string") { + serverId = 0 + } + teaweb.popup("/servers/certs/uploadBatchPopup?serverId=" + serverId, { + callback: function (resp) { + if (resp.data.cert != null) { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.policy.certRefs.$pushAll(resp.data.certRefs) + that.policy.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 申请证书 + requestCert: function () { + // 已经在证书中的域名 + let excludeServerNames = [] + if (this.policy != null && this.policy.certs.length > 0) { + this.policy.certs.forEach(function (cert) { + excludeServerNames.$pushAll(cert.dnsNames) + }) + } + + let that = this + teaweb.popup("/servers/server/settings/https/requestCertPopup?serverId=" + this.vServerId + "&excludeServerNames=" + excludeServerNames.join(","), { + callback: function () { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + } + }) + }, + + // 更多选项 + changeOptionsVisible: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + }, + + // 格式化时间 + formatTime: function (timestamp) { + return new Date(timestamp * 1000).format("Y-m-d") + }, + + // 格式化加密套件 + formatCipherSuite: function (cipherSuite) { + return cipherSuite.replace(/(AES|3DES)/, "$1") + }, + + // 添加单个套件 + addCipherSuite: function (cipherSuite) { + if (!this.policy.cipherSuites.$contains(cipherSuite)) { + this.policy.cipherSuites.push(cipherSuite) + } + this.allCipherSuites.$removeValue(cipherSuite) + }, + + // 删除单个套件 + removeCipherSuite: function (cipherSuite) { + let that = this + teaweb.confirm("确定要删除此套件吗?", function () { + that.policy.cipherSuites.$removeValue(cipherSuite) + that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$findAll(function (k, v) { + return !that.policy.cipherSuites.$contains(v) + }) + }) + }, + + // 清除所选套件 + clearCipherSuites: function () { + let that = this + teaweb.confirm("确定要清除所有已选套件吗?", function () { + that.policy.cipherSuites = [] + that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$copy() + }) + }, + + // 批量添加套件 + addBatchCipherSuites: function (suites) { + var that = this + teaweb.confirm("确定要批量添加套件?", function () { + suites.$each(function (k, v) { + if (that.policy.cipherSuites.$contains(v)) { + return + } + that.policy.cipherSuites.push(v) + }) + }) + }, + + /** + * 套件拖动排序 + */ + sortableCipherSuites: function () { + var box = document.querySelector(".cipher-suites-box") + Sortable.create(box, { + draggable: ".label", + handle: ".icon.handle", + onStart: function () { + + }, + onUpdate: function (event) { + + } + }) + }, + + // 显示所有套件 + showAllCipherSuites: function () { + this.cipherSuitesVisible = !this.cipherSuitesVisible + }, + + // 显示HSTS更多选项 + showMoreHSTS: function () { + this.hstsOptionsVisible = !this.hstsOptionsVisible; + if (this.hstsOptionsVisible) { + this.changeHSTSMaxAge() + } + }, + + // 监控HSTS有效期修改 + changeHSTSMaxAge: function () { + var v = parseInt(this.hstsMaxAgeString) + if (isNaN(v) || v < 0) { + this.hsts.maxAge = 0 + this.hsts.days = "-" + return + } + this.hsts.maxAge = v + this.hsts.days = v / 86400 + if (this.hsts.days == 0) { + this.hsts.days = "-" + } + }, + + // 设置HSTS有效期 + setHSTSMaxAge: function (maxAge) { + this.hstsMaxAgeString = maxAge.toString() + this.changeHSTSMaxAge() + }, + + // 添加HSTS域名 + addHstsDomain: function () { + this.hstsDomainAdding = true + this.hstsDomainEditingIndex = -1 + let that = this + setTimeout(function () { + that.$refs.addingHstsDomain.focus() + }, 100) + }, + + // 修改HSTS域名 + editHstsDomain: function (index) { + this.hstsDomainEditingIndex = index + this.addingHstsDomain = this.hsts.domains[index] + this.hstsDomainAdding = true + let that = this + setTimeout(function () { + that.$refs.addingHstsDomain.focus() + }, 100) + }, + + // 确认HSTS域名添加 + confirmAddHstsDomain: function () { + this.addingHstsDomain = this.addingHstsDomain.trim() + if (this.addingHstsDomain.length == 0) { + return; + } + if (this.hstsDomainEditingIndex > -1) { + this.hsts.domains[this.hstsDomainEditingIndex] = this.addingHstsDomain + } else { + this.hsts.domains.push(this.addingHstsDomain) + } + this.cancelHstsDomainAdding() + }, + + // 取消HSTS域名添加 + cancelHstsDomainAdding: function () { + this.hstsDomainAdding = false + this.addingHstsDomain = "" + this.hstsDomainEditingIndex = -1 + }, + + // 删除HSTS域名 + removeHstsDomain: function (index) { + this.cancelHstsDomainAdding() + this.hsts.domains.$remove(index) + }, + + // 选择客户端CA证书 + selectClientCACert: function () { + let that = this + teaweb.popup("/servers/certs/selectPopup?isCA=1", { + width: "50em", + height: "30em", + callback: function (resp) { + if (resp.data.cert != null && resp.data.certRef != null) { + that.policy.clientCARefs.push(resp.data.certRef) + that.policy.clientCACerts.push(resp.data.cert) + } + if (resp.data.certs != null && resp.data.certRefs != null) { + that.policy.clientCARefs.$pushAll(resp.data.certRefs) + that.policy.clientCACerts.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 上传CA证书 + uploadClientCACert: function () { + let that = this + teaweb.popup("/servers/certs/uploadPopup?isCA=1", { + height: "28em", + callback: function (resp) { + teaweb.success("上传成功", function () { + that.policy.clientCARefs.push(resp.data.certRef) + that.policy.clientCACerts.push(resp.data.cert) + }) + } + }) + }, + + // 删除客户端CA证书 + removeClientCACert: function (index) { + let that = this + teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { + that.policy.clientCARefs.$remove(index) + that.policy.clientCACerts.$remove(index) + }) + } + }, + template: `
+

SSL/TLS相关配置

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用HTTP/2 +
+ + +
+
启用HTTP/3 +
+ + +
+
设置证书 +
+
+ {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   +
+
+
+
+ 选择或上传证书后HTTPSTLS服务才能生效。 +
+
+   + |   +   +   + |   + +
TLS最低版本 + +
加密算法套件(CipherSuites) +
+ + +
+
+
+
+ 已添加套件({{policy.cipherSuites.length}}): +
+ +   + +
+
+ + + +

点击可选套件添加。

+
+
开启HSTS +
+ + +
+

+ 开启后,会自动在响应Header中加入 + Strict-Transport-Security: + ... + max-age={{hsts.maxAge}} + ; includeSubDomains + ; preload + + + 修改 + +

+
HSTS有效时间(max-age) +
+
+ +
+
+ 秒 +
+
{{hsts.days}}天
+
+

+ [1年/365天]     + [6个月/182.5天]     + [1个月/30天] +

+
HSTS包含子域名(includeSubDomains) +
+ + +
+
HSTS预加载(preload) +
+ + +
+
HSTS生效的域名 +
+ {{domain}} +   + + + +
+
+
+ +
+
+ +   取消 +
+
+
+ +
+

如果没有设置域名的话,则默认支持所有的域名。

+
OCSP Stapling +

选中表示启用OCSP Stapling。

+
客户端认证方式 + +
客户端认证CA证书 +
+
+ {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   +
+
+
+   + +

用来校验客户端证书以增强安全性,通常不需要设置。

+
+
+
` +}) + +// UAM模式配置 +Vue.component("uam-config-box", { + props: ["v-uam-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vUamConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + addToWhiteList: true, + onlyURLPatterns: [], + exceptURLPatterns: [], + minQPSPerIP: 0, + keyLife: 0 + } + } + if (config.onlyURLPatterns == null) { + config.onlyURLPatterns = [] + } + if (config.exceptURLPatterns == null) { + config.exceptURLPatterns = [] + } + return { + config: config, + moreOptionsVisible: false, + minQPSPerIP: config.minQPSPerIP, + keyLife: config.keyLife + } + }, + watch: { + minQPSPerIP: function (v) { + let qps = parseInt(v.toString()) + if (isNaN(qps) || qps < 0) { + qps = 0 + } + this.config.minQPSPerIP = qps + }, + keyLife: function (v) { + let keyLife = parseInt(v) + if (isNaN(keyLife) || keyLife <= 0) { + keyLife = 0 + } + this.config.keyLife = keyLife + } + }, + methods: { + showMoreOptions: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + }, + changeConds: function (conds) { + this.config.conds = conds + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用5秒盾 + +

启用后,访问网站时,自动检查浏览器环境,阻止非正常访问。

+
验证有效期 +
+ + +
+

单个客户端验证通过后,在这个有效期内不再重复验证;如果为0则表示系统默认。

+
单IP最低QPS +
+ + 请求数/秒 +
+

当某个IP在1分钟内平均QPS达到此值时,才会触发5秒盾;如果设置为0,表示任何访问都会触发。

+
加入IP白名单 + +

选中后,表示验证通过后,将访问者IP加入到临时白名单中,此IP下次访问时不再校验5秒盾;此白名单只对5秒盾有效,不影响其他规则。此选项主要用于可能无法正常使用Cookie的网站。

+
例外URL + +

如果填写了例外URL,表示这些URL跳过5秒盾不做处理。

+
限制URL + +

如果填写了限制URL,表示只对这些URL进行5秒盾处理;如果不填则表示支持所有的URL。

+
匹配条件 + +
+
` }) @@ -12204,3300 +15498,6 @@ Vue.component("user-agent-config-box", {
` }) -Vue.component("http-firewall-region-selector", { - props: ["v-type", "v-countries"], - data: function () { - let countries = this.vCountries - if (countries == null) { - countries = [] - } - - return { - listType: this.vType, - countries: countries - } - }, - methods: { - addCountry: function () { - let selectedCountryIds = this.countries.map(function (country) { - return country.id - }) - let that = this - teaweb.popup("/servers/server/settings/waf/ipadmin/selectCountriesPopup?type=" + this.listType + "&selectedCountryIds=" + selectedCountryIds.join(","), { - width: "52em", - height: "30em", - callback: function (resp) { - that.countries = resp.data.selectedCountries - that.$forceUpdate() - that.notifyChange() - } - }) - }, - removeCountry: function (index) { - this.countries.$remove(index) - this.notifyChange() - }, - resetCountries: function () { - this.countries = [] - this.notifyChange() - }, - notifyChange: function () { - this.$emit("change", { - "countries": this.countries - }) - } - }, - template: `
- 暂时没有选择允许封禁的区域。 -
-
- - ({{country.letter}}){{country.name}} -
-
-
-   -
` -}) - -// 绑定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("/waf/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("/waf/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.value}} - - {{item.ipFrom}}  New   - - {{item.ipTo}} - - - -
- {{item.region}} - | {{item.isp}} -
-
{{item.isp}}
- -
- 添加于 {{item.createdTime}} - - @ - - [名单:{{item.list.name}}] - [名单:{{item.list.name}} - - - - [网站:{{item.policy.server.name}}] - [网站:{{item.policy.server.name}}] - [网站:{{item.policy.server.name}}] - - - - -
-
- IPv4 - IPv4 - IPv6 - 所有IP - - {{item.eventLevelName}} - - - -
- {{item.expiredTime}} -
- 已过期 -
-
- {{formatSeconds(item.lifeSeconds)}} - 已过期 -
-
- 不过期 -
- {{item.reason}} - - - - - - - - 修改   - 删除 -
-
` -}) - -Vue.component("ip-item-text", { - props: ["v-item"], - template: ` - * - - {{vItem.value}} - - {{vItem.ipFrom}} - - {{vItem.ipTo}} - - -   级别:{{vItem.eventLevelName}} -` -}) - -Vue.component("ip-box", { - props: ["v-ip"], - methods: { - popup: function () { - let ip = this.vIp - if (ip == null || ip.length == 0) { - let e = this.$refs.container - ip = e.innerText - if (ip == null) { - ip = e.textContent - } - } - - teaweb.popup("/servers/ipbox?ip=" + ip, { - width: "50em", - height: "30em" - }) - } - }, - template: `` -}) - -// 给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 () { - if (Tea && Tea.Vue && Tea.Vue.page) { - that.page = Tea.Vue.page; - } - }) - }, - template: `
-
-
` -}) - -Vue.component("network-addresses-box", { - props: ["v-server-type", "v-addresses", "v-protocol", "v-name"], - 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" - } - - return { - addresses: addresses, - protocol: protocol, - name: name - } - }, - watch: { - "vServerType": function () { - this.addresses = [] - } - }, - methods: { - addAddr: function () { - let that = this - window.UPDATING_ADDR = null - teaweb.popup("/servers/addPortPopup?serverType=" + this.vServerType + "&protocol=" + this.protocol, { - height: "16em", - callback: function (resp) { - var addr = resp.data.address - 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 - teaweb.popup("/servers/addPortPopup?serverType=" + this.vServerType + "&protocol=" + this.protocol, { - height: "16em", - 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) - } - }, - template: `
- -
-
- {{addr.protocol}}://{{addr.host}}*:{{addr.portRange}} - -
-
-
- [添加端口绑定] -
` -}) - -/** - * 保存按钮 - */ -Vue.component("submit-btn", { - 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"], - 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 () { - 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("bandwidth-size-capacity-view", { - props: ["v-value"], - data: function () { - let capacity = this.vValue - if (capacity != null && capacity.count > 0 && typeof capacity.unit === "string") { - capacity.unit = capacity.unit[0].toUpperCase() + capacity.unit.substring(1) + "ps" - } - return { - capacity: capacity - } - }, - template: ` - {{capacity.count}}{{capacity.unit}} -` -}) - -Vue.component("more-options-tbody", { - data: function () { - return { - isVisible: false - } - }, - methods: { - show: function () { - this.isVisible = !this.isVisible - this.$emit("change", this.isVisible) - } - }, - template: ` - - 更多选项收起选项 - -` -}) - -Vue.component("download-link", { - props: ["v-element", "v-file"], - 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 e = document.getElementById(this.vElement) - if (e == null) { - teaweb.warn("找不到要下载的内容") - return - } - let text = e.innerText - if (text == null) { - text = e.textContent - } - return Tea.url("/ui/download", { - file: this.file, - text: text - }) - } - }, - template: ``, -}) - -Vue.component("values-box", { - props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"], - data: function () { - let values = this.values; - if (values == null) { - values = []; - } - - if (this.vValues != null && typeof this.vValues == "object") { - values = this.vValues - } - - return { - "realValues": values, - "isUpdating": false, - "isAdding": false, - "index": 0, - "value": "", - isEditing: false - } - }, - methods: { - create: function () { - this.isAdding = true; - var that = this; - setTimeout(function () { - that.$refs.value.focus(); - }, 200); - }, - update: function (index) { - this.cancel() - this.isUpdating = true; - this.index = index; - this.value = this.realValues[index]; - var that = this; - setTimeout(function () { - that.$refs.value.focus(); - }, 200); - }, - confirm: function () { - if (this.value.length == 0) { - if (typeof(this.vAllowEmpty) != "boolean" || !this.vAllowEmpty) { - return - } - } - - // validate - if (typeof(this.validator) == "function") { - let resp = this.validator.call(this, this.value) - if (typeof resp == "object") { - if (typeof resp.isOk == "boolean" && !resp.isOk) { - if (typeof resp.message == "string") { - let that = this - teaweb.warn(resp.message, function () { - that.$refs.value.focus(); - }) - } - return - } - } - } - - if (this.isUpdating) { - Vue.set(this.realValues, this.index, this.value); - } else { - this.realValues.push(this.value); - } - this.cancel() - this.$emit("change", this.realValues) - }, - remove: function (index) { - this.realValues.$remove(index) - this.$emit("change", this.realValues) - }, - cancel: function () { - this.isUpdating = false; - this.isAdding = false; - this.value = ""; - }, - updateAll: function (values) { - this.realValues = values - }, - addValue: function (v) { - this.realValues.push(v) - }, - - startEditing: function () { - this.isEditing = !this.isEditing - }, - allValues: function () { - return this.realValues - } - }, - template: `
-
-
- {{value}} - [空] -
- [修改] -
-
-
-
- {{value}} - [空] - -   - -
-
-
- -
-
-
- -
-
- -
-
- -
-
-
-
- -
-
-
` -}); - -Vue.component("datetime-input", { - props: ["v-name", "v-timestamp"], - mounted: function () { - let that = this - teaweb.datepicker(this.$refs.dayInput, function (v) { - that.day = v - that.hour = "23" - that.minute = "59" - that.second = "59" - that.change() - }) - }, - data: function () { - let timestamp = this.vTimestamp - if (timestamp != null) { - timestamp = parseInt(timestamp) - if (isNaN(timestamp)) { - timestamp = 0 - } - } else { - timestamp = 0 - } - - let day = "" - let hour = "" - let minute = "" - let second = "" - - if (timestamp > 0) { - let date = new Date() - date.setTime(timestamp * 1000) - - let year = date.getFullYear().toString() - let month = this.leadingZero((date.getMonth() + 1).toString(), 2) - day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2) - - hour = this.leadingZero(date.getHours().toString(), 2) - minute = this.leadingZero(date.getMinutes().toString(), 2) - second = this.leadingZero(date.getSeconds().toString(), 2) - } - - return { - timestamp: timestamp, - day: day, - hour: hour, - minute: minute, - second: second, - - hasDayError: false, - hasHourError: false, - hasMinuteError: false, - hasSecondError: false - } - }, - methods: { - change: function () { - // day - if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) { - this.hasDayError = true - return - } - let pieces = this.day.split("-") - let year = parseInt(pieces[0]) - - let month = parseInt(pieces[1]) - if (month < 1 || month > 12) { - this.hasDayError = true - return - } - - let day = parseInt(pieces[2]) - if (day < 1 || day > 32) { - this.hasDayError = true - return - } - - this.hasDayError = false - - // hour - if (!/^\d+$/.test(this.hour)) { - this.hasHourError = true - return - } - let hour = parseInt(this.hour) - if (isNaN(hour)) { - this.hasHourError = true - return - } - if (hour < 0 || hour >= 24) { - this.hasHourError = true - return - } - this.hasHourError = false - - // minute - if (!/^\d+$/.test(this.minute)) { - this.hasMinuteError = true - return - } - let minute = parseInt(this.minute) - if (isNaN(minute)) { - this.hasMinuteError = true - return - } - if (minute < 0 || minute >= 60) { - this.hasMinuteError = true - return - } - this.hasMinuteError = false - - // second - if (!/^\d+$/.test(this.second)) { - this.hasSecondError = true - return - } - let second = parseInt(this.second) - if (isNaN(second)) { - this.hasSecondError = true - return - } - if (second < 0 || second >= 60) { - this.hasSecondError = true - return - } - this.hasSecondError = false - - let date = new Date(year, month - 1, day, hour, minute, second) - this.timestamp = Math.floor(date.getTime() / 1000) - }, - leadingZero: function (s, l) { - s = s.toString() - if (l <= s.length) { - return s - } - for (let i = 0; i < l - s.length; i++) { - s = "0" + s - } - return s - }, - resultTimestamp: function () { - return this.timestamp - }, - nextYear: function () { - let date = new Date() - date.setFullYear(date.getFullYear()+1) - this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) - this.hour = this.leadingZero(date.getHours(), 2) - this.minute = this.leadingZero(date.getMinutes(), 2) - this.second = this.leadingZero(date.getSeconds(), 2) - this.change() - }, - nextDays: function (days) { - let date = new Date() - date.setTime(date.getTime() + days * 86400 * 1000) - this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) - this.hour = this.leadingZero(date.getHours(), 2) - this.minute = this.leadingZero(date.getMinutes(), 2) - this.second = this.leadingZero(date.getSeconds(), 2) - this.change() - }, - nextHours: function (hours) { - let date = new Date() - date.setTime(date.getTime() + hours * 3600 * 1000) - this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) - this.hour = this.leadingZero(date.getHours(), 2) - this.minute = this.leadingZero(date.getMinutes(), 2) - this.second = this.leadingZero(date.getSeconds(), 2) - this.change() - } - }, - template: `
- -
-
- -
-
-
:
-
-
:
-
-
-

常用时间:  1小时  |  1天  |  3天  |  1周  |  30天  |  1年 

-
` -}) - -// 启用状态标签 -Vue.component("label-on", { - props: ["v-is-on"], - template: '
已启用已停用
' -}) - -// 文字代码标签 -Vue.component("code-label", { - methods: { - click: function (args) { - this.$emit("click", args) - } - }, - template: `` -}) - -// tiny标签 -Vue.component("tiny-label", { - template: `` -}) - -Vue.component("tiny-basic-label", { - template: `` -}) - -// 更小的标签 -Vue.component("micro-basic-label", { - template: `` -}) - - -// 灰色的Label -Vue.component("grey-label", { - template: `` -}) - -// Plus专属 -Vue.component("plus-label", { - template: `` -}) - - -Vue.component("js-page", { - props: ["v-max"], - data: function () { - let max = this.vMax - if (max == null) { - max = 0 - } - return { - max: max, - page: 1 - } - }, - methods: { - updateMax: function (max) { - this.max = max - }, - selectPage: function(page) { - this.page = page - this.$emit("change", page) - } - }, - template:`
-
- {{i}} -
-
` -}) - -/** - * 一级菜单 - */ -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) - this.$emit("input", this.visible) - } - }, - template: '更多选项收起选项 ' -}); - -/** - * 二级菜单 - */ -Vue.component("second-menu", { - template: ' \ -
\ - \ -
\ -
' -}); - -Vue.component("file-textarea", { - props: ["value"], - data: function () { - let value = this.value - if (typeof value != "string") { - value = "" - } - return { - realValue: value - } - }, - mounted: function () { - }, - methods: { - dragover: function () {}, - drop: function (e) { - let that = this - e.dataTransfer.items[0].getAsFile().text().then(function (data) { - that.setValue(data) - }) - }, - setValue: function (value) { - this.realValue = value - }, - focus: function () { - this.$refs.textarea.focus() - } - }, - template: `` -}) - -Vue.component("more-options-angle", { - data: function () { - return { - isVisible: false - } - }, - methods: { - show: function () { - this.isVisible = !this.isVisible - this.$emit("change", this.isVisible) - } - }, - template: `更多选项收起选项` -}) - -Vue.component("columns-grid", { - props: [], - mounted: function () { - this.columns = this.calculateColumns() - - let that = this - window.addEventListener("resize", function () { - that.columns = that.calculateColumns() - }) - }, - data: function () { - return { - columns: "four" - } - }, - methods: { - calculateColumns: function () { - let w = window.innerWidth - let columns = Math.floor(w / 250) - if (columns == 0) { - columns = 1 - } - - let columnElements = this.$el.getElementsByClassName("column") - if (columnElements.length == 0) { - return - } - let maxColumns = columnElements.length - if (columns > maxColumns) { - columns = maxColumns - } - - // 添加右侧边框 - for (let index = 0; index < columnElements.length; index++) { - let el = columnElements[index] - el.className = el.className.replace("with-border", "") - if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) { - el.className += " with-border" - } - } - - switch (columns) { - case 1: - return "one" - case 2: - return "two" - case 3: - return "three" - case 4: - return "four" - case 5: - return "five" - case 6: - return "six" - case 7: - return "seven" - case 8: - return "eight" - case 9: - return "nine" - case 10: - return "ten" - default: - return "ten" - } - } - }, - template: `
- -
` -}) - -/** - * 菜单项 - */ -Vue.component("inner-menu-item", { - props: ["href", "active", "code"], - data: function () { - var active = this.active; - if (typeof(active) =="undefined") { - var itemCode = ""; - if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") { - itemCode = window.TEA.ACTION.data.firstMenuItem; - } - active = (itemCode == this.code); - } - return { - vHref: (this.href == null) ? "" : this.href, - vActive: active - }; - }, - template: '\ - [] \ - ' -}); - -Vue.component("bandwidth-size-capacity-box", { - props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"], - data: function () { - let v = this.vValue - if (v == null) { - v = { - count: this.vCount, - unit: this.vUnit - } - } - if (v.unit == null || v.unit.length == 0) { - v.unit = "mb" - } - - if (typeof (v["count"]) != "number") { - v["count"] = -1 - } - - let vSize = this.size - if (vSize == null) { - vSize = 6 - } - - let vMaxlength = this.maxlength - if (vMaxlength == null) { - vMaxlength = 10 - } - - let supportedUnits = this.vSupportedUnits - if (supportedUnits == null) { - supportedUnits = [] - } - - return { - capacity: v, - countString: (v.count >= 0) ? v.count.toString() : "", - vSize: vSize, - vMaxlength: vMaxlength, - supportedUnits: supportedUnits - } - }, - watch: { - "countString": function (newValue) { - let value = newValue.trim() - if (value.length == 0) { - this.capacity.count = -1 - this.change() - return - } - let count = parseInt(value) - if (!isNaN(count)) { - this.capacity.count = count - } - this.change() - } - }, - methods: { - change: function () { - this.$emit("change", this.capacity) - } - }, - template: `
- -
- -
-
- -
-
` -}) - -// 将变量转换为中文 -Vue.component("request-variables-describer", { - data: function () { - return { - vars:[] - } - }, - methods: { - update: function (variablesString) { - this.vars = [] - let that = this - variablesString.replace(/\${.+?}/g, function (v) { - let def = that.findVar(v) - if (def == null) { - return v - } - that.vars.push(def) - }) - }, - findVar: function (name) { - let def = null - window.REQUEST_VARIABLES.forEach(function (v) { - if (v.code == name) { - def = v - } - }) - return def - } - }, - template: ` - {{v.code}} - {{v.name}} -` -}) - - -Vue.component("combo-box", { - // data-url 和 data-key 成对出现 - props: [ - "name", "title", "placeholder", "size", "v-items", "v-value", - "data-url", // 数据源URL - "data-key", // 数据源中数据的键名 - "data-search", // 是否启用动态搜索,如果值为on或true,则表示启用 - "width" - ], - mounted: function () { - if (this.dataURL.length > 0) { - this.search("") - } - - // 设定菜单宽度 - let searchBox = this.$refs.searchBox - if (searchBox != null) { - let inputWidth = searchBox.offsetWidth - if (inputWidth != null && inputWidth > 0) { - this.$refs.menu.style.width = inputWidth + "px" - } else if (this.styleWidth.length > 0) { - this.$refs.menu.style.width = this.styleWidth - } - } - }, - data: function () { - let items = this.vItems - if (items == null || !(items instanceof Array)) { - items = [] - } - items = this.formatItems(items) - - // 当前选中项 - let selectedItem = null - if (this.vValue != null) { - let that = this - items.forEach(function (v) { - if (v.value == that.vValue) { - selectedItem = v - } - }) - } - - let width = this.width - if (width == null || width.length == 0) { - width = "11em" - } else { - if (/\d+$/.test(width)) { - width += "em" - } - } - - // data url - let dataURL = "" - if (typeof this.dataUrl == "string" && this.dataUrl.length > 0) { - dataURL = this.dataUrl - } - - return { - allItems: items, // 原始的所有的items - items: items.$copy(), // 候选的items - selectedItem: selectedItem, // 选中的item - keyword: "", - visible: false, - hideTimer: null, - hoverIndex: 0, - styleWidth: width, - - isInitial: true, - dataURL: dataURL, - urlRequestId: 0 // 记录URL请求ID,防止并行冲突 - } - }, - methods: { - search: function (keyword) { - // 从URL中获取选项数据 - let dataUrl = this.dataURL - let dataKey = this.dataKey - let that = this - - let requestId = Math.random() - this.urlRequestId = requestId - - Tea.action(dataUrl) - .params({ - keyword: (keyword == null) ? "" : keyword - }) - .post() - .success(function (resp) { - if (requestId != that.urlRequestId) { - return - } - - if (resp.data != null) { - if (typeof (resp.data[dataKey]) == "object") { - let items = that.formatItems(resp.data[dataKey]) - that.allItems = items - that.items = items.$copy() - - if (that.isInitial) { - that.isInitial = false - if (that.vValue != null) { - items.forEach(function (v) { - if (v.value == that.vValue) { - that.selectedItem = v - } - }) - } - } - } - } - }) - }, - formatItems: function (items) { - items.forEach(function (v) { - if (v.value == null) { - v.value = v.id - } - }) - return items - }, - reset: function () { - this.selectedItem = null - this.change() - this.hoverIndex = 0 - - let that = this - setTimeout(function () { - if (that.$refs.searchBox) { - that.$refs.searchBox.focus() - } - }) - }, - clear: function () { - this.selectedItem = null - this.change() - this.hoverIndex = 0 - }, - changeKeyword: function () { - let shouldSearch = this.dataURL.length > 0 && (this.dataSearch == "on" || this.dataSearch == "true") - - this.hoverIndex = 0 - let keyword = this.keyword - if (keyword.length == 0) { - if (shouldSearch) { - this.search(keyword) - } else { - this.items = this.allItems.$copy() - } - return - } - - - if (shouldSearch) { - this.search(keyword) - } else { - this.items = this.allItems.$copy().filter(function (v) { - if (v.fullname != null && v.fullname.length > 0 && teaweb.match(v.fullname, keyword)) { - return true - } - return teaweb.match(v.name, keyword) - }) - } - }, - selectItem: function (item) { - this.selectedItem = item - this.change() - this.hoverIndex = 0 - this.keyword = "" - this.changeKeyword() - }, - confirm: function () { - if (this.items.length > this.hoverIndex) { - this.selectItem(this.items[this.hoverIndex]) - } - }, - show: function () { - this.visible = true - - // 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换 - }, - hide: function () { - let that = this - this.hideTimer = setTimeout(function () { - that.visible = false - }, 500) - }, - downItem: function () { - this.hoverIndex++ - if (this.hoverIndex > this.items.length - 1) { - this.hoverIndex = 0 - } - this.focusItem() - }, - upItem: function () { - this.hoverIndex-- - if (this.hoverIndex < 0) { - this.hoverIndex = 0 - } - this.focusItem() - }, - focusItem: function () { - if (this.hoverIndex < this.items.length) { - this.$refs.itemRef[this.hoverIndex].focus() - let that = this - setTimeout(function () { - that.$refs.searchBox.focus() - if (that.hideTimer != null) { - clearTimeout(that.hideTimer) - that.hideTimer = null - } - }) - } - }, - change: function () { - this.$emit("change", this.selectedItem) - - let that = this - setTimeout(function () { - if (that.$refs.selectedLabel != null) { - that.$refs.selectedLabel.focus() - } - }) - }, - submitForm: function (event) { - if (event.target.tagName != "A") { - return - } - let parentBox = this.$refs.selectedLabel.parentNode - while (true) { - parentBox = parentBox.parentNode - if (parentBox == null || parentBox.tagName == "BODY") { - return - } - if (parentBox.tagName == "FORM") { - parentBox.submit() - break - } - } - }, - - setDataURL: function (dataURL) { - this.dataURL = dataURL - }, - reloadData: function () { - this.search("") - } - }, - template: `
- -
- -
- - -
- - {{title}}:{{selectedItem.name}} - - -
- - -
- -
-
` -}) - -Vue.component("search-box", { - props: ["placeholder", "width"], - data: function () { - let width = this.width - if (width == null) { - width = "10em" - } - return { - realWidth: width, - realValue: "" - } - }, - methods: { - onInput: function () { - this.$emit("input", { value: this.realValue}) - this.$emit("change", { value: this.realValue}) - }, - clearValue: function () { - this.realValue = "" - this.focus() - this.onInput() - }, - focus: function () { - this.$refs.valueRef.focus() - } - }, - template: `
-
- - -
-
` -}) - -Vue.component("time-duration-box", { - props: ["v-name", "v-value", "v-count", "v-unit", "placeholder", "v-min-unit", "maxlength"], - mounted: function () { - this.change() - }, - data: function () { - let v = this.vValue - if (v == null) { - v = { - count: this.vCount, - unit: this.vUnit - } - } - if (typeof (v["count"]) != "number") { - v["count"] = -1 - } - - let minUnit = this.vMinUnit - let units = [ - { - code: "ms", - name: "毫秒" - }, - { - code: "second", - name: "秒" - }, - { - code: "minute", - name: "分钟" - }, - { - code: "hour", - name: "小时" - }, - { - code: "day", - name: "天" - } - ] - let minUnitIndex = -1 - if (minUnit != null && typeof minUnit == "string" && minUnit.length > 0) { - for (let i = 0; i < units.length; i++) { - if (units[i].code == minUnit) { - minUnitIndex = i - break - } - } - } - if (minUnitIndex > -1) { - units = units.slice(minUnitIndex) - } - - let maxLength = parseInt(this.maxlength) - if (typeof maxLength != "number") { - maxLength = 10 - } - - return { - duration: v, - countString: (v.count >= 0) ? v.count.toString() : "", - units: units, - realMaxLength: maxLength - } - }, - watch: { - "countString": function (newValue) { - let value = newValue.trim() - if (value.length == 0) { - this.duration.count = -1 - return - } - let count = parseInt(value) - if (!isNaN(count)) { - this.duration.count = count - } - this.change() - } - }, - methods: { - change: function () { - this.$emit("change", this.duration) - } - }, - template: `
- -
- -
-
- -
-
` -}) - -Vue.component("time-duration-text", { - props: ["v-value"], - methods: { - unitName: function (unit) { - switch (unit) { - case "ms": - return "毫秒" - case "second": - return "秒" - case "minute": - return "分钟" - case "hour": - return "小时" - case "day": - return "天" - } - } - }, - template: ` - {{vValue.count}} {{unitName(vValue.unit)}} -` -}) - -Vue.component("not-found-box", { - props: ["message"], - template: `
-
-

{{message}}

-
` -}) - -// 警告消息 -Vue.component("warning-message", { - template: `
` -}) - -let checkboxId = 0 -Vue.component("checkbox", { - props: ["name", "value", "v-value", "id", "checked"], - data: function () { - checkboxId++ - let elementId = this.id - if (elementId == null) { - elementId = "checkbox" + checkboxId - } - - let elementValue = this.vValue - if (elementValue == null) { - elementValue = "1" - } - - let checkedValue = this.value - if (checkedValue == null && this.checked == "checked") { - checkedValue = elementValue - } - - return { - elementId: elementId, - elementValue: elementValue, - newValue: checkedValue - } - }, - methods: { - change: function () { - this.$emit("input", this.newValue) - }, - check: function () { - this.newValue = this.elementValue - }, - uncheck: function () { - this.newValue = "" - }, - isChecked: function () { - return (typeof (this.newValue) == "boolean" && this.newValue) || this.newValue == this.elementValue - } - }, - watch: { - value: function (v) { - if (typeof v == "boolean") { - this.newValue = v - } - } - }, - template: `
- - -
` -}) - -Vue.component("network-addresses-view", { - props: ["v-addresses"], - template: `
-
- {{addr.protocol}}://{{addr.host}}:{{addr.portRange}} -
-
` -}) - -Vue.component("url-patterns-box", { - props: ["value"], - data: function () { - let patterns = [] - if (this.value != null) { - patterns = this.value - } - - return { - patterns: patterns, - isAdding: false, - - addingPattern: {"type": "wildcard", "pattern": ""}, - editingIndex: -1, - - patternIsInvalid: false, - - windowIsSmall: window.innerWidth < 600 - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.patternInput.focus() - }) - }, - edit: function (index) { - this.isAdding = true - this.editingIndex = index - this.addingPattern = { - type: this.patterns[index].type, - pattern: this.patterns[index].pattern - } - }, - confirm: function () { - if (this.requireURL(this.addingPattern.type)) { - let pattern = this.addingPattern.pattern.trim() - if (pattern.length == 0) { - let that = this - teaweb.warn("请输入URL", function () { - that.$refs.patternInput.focus() - }) - return - } - } - if (this.editingIndex < 0) { - this.patterns.push({ - type: this.addingPattern.type, - pattern: this.addingPattern.pattern - }) - } else { - this.patterns[this.editingIndex].type = this.addingPattern.type - this.patterns[this.editingIndex].pattern = this.addingPattern.pattern - } - this.notifyChange() - this.cancel() - }, - remove: function (index) { - this.patterns.$remove(index) - this.cancel() - this.notifyChange() - }, - cancel: function () { - this.isAdding = false - this.addingPattern = {"type": "wildcard", "pattern": ""} - this.editingIndex = -1 - }, - patternTypeName: function (patternType) { - switch (patternType) { - case "wildcard": - return "通配符" - case "regexp": - return "正则" - case "images": - return "常见图片文件" - case "audios": - return "常见音频文件" - case "videos": - return "常见视频文件" - } - return "" - }, - notifyChange: function () { - this.$emit("input", this.patterns) - }, - changePattern: function () { - this.patternIsInvalid = false - let pattern = this.addingPattern.pattern - switch (this.addingPattern.type) { - case "wildcard": - if (pattern.indexOf("?") >= 0) { - this.patternIsInvalid = true - } - break - case "regexp": - if (pattern.indexOf("?") >= 0) { - let pieces = pattern.split("?") - for (let i = 0; i < pieces.length - 1; i++) { - if (pieces[i].length == 0 || pieces[i][pieces[i].length - 1] != "\\") { - this.patternIsInvalid = true - } - } - } - break - } - }, - requireURL: function (patternType) { - return patternType == "wildcard" || patternType == "regexp" - } - }, - template: `
-
-
- [{{patternTypeName(pattern.type)}}] {{pattern.pattern}}   - - -
-
-
-
-
- -
-
- -

通配符正则表达式中不能包含问号(?)及问号以后的内容。

-
-
- - -
-
- -
-
-
-
- -
-
` -}) - -Vue.component("size-capacity-view", { - props:["v-default-text", "v-value"], - methods: { - composeCapacity: function (capacity) { - return teaweb.convertSizeCapacityToString(capacity) - } - }, - template: `
- {{composeCapacity(vValue)}} - {{vDefaultText}} -
` -}) - -Vue.component("keyword", { - props: ["v-word"], - data: function () { - let word = this.vWord - if (word == null) { - word = "" - } else { - word = word.replace(/\)/g, "\\)") - word = word.replace(/\(/g, "\\(") - word = word.replace(/\+/g, "\\+") - word = word.replace(/\^/g, "\\^") - word = word.replace(/\$/g, "\\$") - word = word.replace(/\?/g, "\\?") - word = word.replace(/\*/g, "\\*") - word = word.replace(/\[/g, "\\[") - word = word.replace(/{/g, "\\{") - word = word.replace(/\./g, "\\.") - } - - let slot = this.$slots["default"][0] - let text = slot.text - if (word.length > 0) { - let that = this - let m = [] // replacement => tmp - let tmpIndex = 0 - text = text.replaceAll(new RegExp("(" + word + ")", "ig"), function (replacement) { - tmpIndex++ - let s = "" + that.encodeHTML(replacement) + "" - let tmpKey = "$TMP__KEY__" + tmpIndex.toString() + "$" - m.push([tmpKey, s]) - return tmpKey - }) - text = this.encodeHTML(text) - - m.forEach(function (r) { - text = text.replace(r[0], r[1]) - }) - - } else { - text = this.encodeHTML(text) - } - - return { - word: word, - text: text - } - }, - methods: { - encodeHTML: function (s) { - s = s.replace(/&/g, "&") - s = s.replace(//g, ">") - s = s.replace(/"/g, """) - return s - } - }, - template: `` -}) - -Vue.component("bits-var", { - props: ["v-bits"], - data: function () { - let bits = this.vBits - if (typeof bits != "number") { - bits = 0 - } - let format = teaweb.splitFormat(teaweb.formatBits(bits)) - return { - format: format - } - }, - template:` - {{format[0]}}{{format[1]}} -` -}) - -Vue.component("theme-color-picker", { - props: ["v-model"], - data: function () { - return { - showPicker: false, - r: 20, - g: 83, - b: 196, - hex: "#14539A", - clickHandler: null - } - }, - mounted: function () { - console.log("Theme color picker component mounted, element:", this.$el) - // 从localStorage加载保存的颜色 - let savedColor = localStorage.getItem("themeColor") - if (savedColor) { - this.setColor(savedColor) - } else { - // 从当前页面获取默认颜色 - let defaultColor = this.getCurrentThemeColor() - if (defaultColor) { - this.setColor(defaultColor) - } - } - - // 应用颜色 - this.applyColor() - }, - beforeDestroy: function() { - // 移除点击监听器 - if (this.clickHandler) { - document.removeEventListener("click", this.clickHandler) - this.clickHandler = null - } - }, - methods: { - // 打开/关闭颜色选择器 - togglePicker: function (e) { - if (e) { - e.preventDefault() - e.stopPropagation() - } - console.log("Toggle picker clicked, current showPicker:", this.showPicker) - this.showPicker = !this.showPicker - console.log("Toggle picker clicked, new showPicker:", this.showPicker) - console.log("Popup should be visible:", this.showPicker) - - // 管理外部点击监听器 - if (this.showPicker) { - // 打开时,延迟添加监听器 - let that = this - setTimeout(function() { - if (!that.clickHandler) { - that.clickHandler = function(e) { - if (that.showPicker && !that.$el.contains(e.target)) { - that.showPicker = false - } - } - document.addEventListener("click", that.clickHandler) - } - }, 100) - } else { - // 关闭时,移除监听器 - if (this.clickHandler) { - document.removeEventListener("click", this.clickHandler) - this.clickHandler = null - } - } - }, - - // RGB转16进制 - rgbToHex: function (r, g, b) { - return "#" + [r, g, b].map(function (x) { - x = parseInt(x) - const hex = x.toString(16) - return hex.length === 1 ? "0" + hex : hex - }).join("").toUpperCase() - }, - - // 16进制转RGB - hexToRgb: function (hex) { - hex = hex.replace("#", "") - const r = parseInt(hex.substring(0, 2), 16) - const g = parseInt(hex.substring(2, 4), 16) - const b = parseInt(hex.substring(4, 6), 16) - return { r: r, g: g, b: b } - }, - - // 设置颜色(支持hex或rgb对象) - setColor: function (color) { - if (typeof color === "string") { - if (color.startsWith("#")) { - let rgb = this.hexToRgb(color) - this.r = rgb.r - this.g = rgb.g - this.b = rgb.b - this.hex = color.toUpperCase() - } else { - // 假设是16进制不带#号 - this.setColor("#" + color) - } - } else if (typeof color === "object" && color.r !== undefined) { - this.r = color.r - this.g = color.g - this.b = color.b - this.hex = this.rgbToHex(color.r, color.g, color.b) - } - }, - - // RGB值变化时更新hex - updateHex: function () { - this.hex = this.rgbToHex(this.r, this.g, this.b) - this.applyColor() - }, - - // Hex值变化时更新RGB - updateRgb: function () { - if (this.hex.match(/^#[0-9A-Fa-f]{6}$/)) { - let rgb = this.hexToRgb(this.hex) - this.r = rgb.r - this.g = rgb.g - this.b = rgb.b - this.applyColor() - } - }, - - // 应用颜色到整个网站 - applyColor: function () { - let color = this.rgbToHex(this.r, this.g, this.b) - - // 保存到localStorage - localStorage.setItem("themeColor", color) - - // 创建或更新样式 - let styleId = "theme-color-custom" - let styleEl = document.getElementById(styleId) - if (!styleEl) { - styleEl = document.createElement("style") - styleEl.id = styleId - document.head.appendChild(styleEl) - } - - // 应用颜色到主题元素 - styleEl.textContent = ` - .top-nav, .main-menu, .main-menu .menu { - background: ${color} !important; - } - .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless { - background: ${color} !important; - } - .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .item { - background: ${color} !important; - } - .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .sub-items { - background: ${color} !important; - } - .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .sub-items .item { - background: ${color} !important; - } - .main-menu .ui.menu .sub-items { - background: ${color} !important; - } - .main-menu .ui.menu .sub-items .item { - background: ${color} !important; - } - .main-menu .ui.labeled.menu .sub-items { - background: ${color} !important; - } - .main-menu .ui.labeled.menu .sub-items .item { - background: ${color} !important; - } - .main-menu .sub-items { - background: ${color} !important; - } - .main-menu .sub-items .item { - background: ${color} !important; - } - .ui.menu.blue.inverted { - background: ${color} !important; - } - .ui.menu.blue.inverted .item { - background: ${color} !important; - } - .ui.menu.vertical.blue.inverted { - background: ${color} !important; - } - .ui.menu.vertical.blue.inverted .item { - background: ${color} !important; - } - .ui.labeled.menu.vertical.blue.inverted { - background: ${color} !important; - } - .ui.labeled.menu.vertical.blue.inverted .item { - background: ${color} !important; - } - ` - - // 触发事件通知其他组件 - if (window.Tea && window.Tea.Vue) { - window.Tea.Vue.$emit("theme-color-changed", color) - } - - // 更新v-model(如果使用) - this.$emit("input", color.replace("#", "")) - }, - - // 获取当前主题颜色 - getCurrentThemeColor: function () { - let topNav = document.querySelector(".top-nav") - if (topNav) { - let bgColor = window.getComputedStyle(topNav).backgroundColor - if (bgColor && bgColor !== "rgba(0, 0, 0, 0)" && bgColor !== "transparent") { - // 转换rgb/rgba为hex - let match = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/) - if (match) { - return this.rgbToHex(parseInt(match[1]), parseInt(match[2]), parseInt(match[3])) - } - } - } - return null - }, - - // 重置为默认颜色 - resetColor: function () { - this.setColor("#14539A") // 默认蓝色 - this.applyColor() - } - }, - template: ` -
- - 主题色 - - -
-
- RGB颜色调节 -
- - -
-
-
- {{ hex }} | RGB({{ r }}, {{ g }}, {{ b }}) -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
- - -
- - -
-
-
- ` -}) - - -Vue.component("bytes-var", { - props: ["v-bytes"], - data: function () { - let bytes = this.vBytes - if (typeof bytes != "number") { - bytes = 0 - } - let format = teaweb.splitFormat(teaweb.formatBytes(bytes)) - return { - format: format - } - }, - template:` - {{format[0]}}{{format[1]}} -` -}) - -Vue.component("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 - var form = this.$refs.token.form - - // 监听表单提交,在提交前刷新token并确保更新到 DOM - form.addEventListener("submit", function (e) { - // 如果正在刷新,等待刷新完成 - if (that.refreshing) { - e.preventDefault() - e.stopPropagation() - return false - } - - // 阻止默认提交,先刷新 token - e.preventDefault() - e.stopPropagation() - that.refreshing = true - - // 刷新 token - that.refreshToken(function () { - // 确保 DOM 中的 token 值是最新的 - that.$forceUpdate() - that.$nextTick(function () { - var tokenInput = form.querySelector('input[name="csrfToken"]') - if (tokenInput) { - tokenInput.value = that.token - } - if (that.$refs.token) { - that.$refs.token.value = that.token - } - - // 确保 DOM 已更新后,再触发表单提交 - setTimeout(function () { - that.refreshing = false - // 重新触发表单提交 - Tea.runActionOn(form) - }, 50) - }) - }) - - return false - }) - - // 自动刷新 - setInterval(function () { - that.refreshToken() - }, 10 * 60 * 1000) - - // 监听表单提交失败,如果是 CSRF token 错误,自动刷新 token 并重试 - this.setupAutoRetry(form) - }, - data: function () { - return { - token: "", - retrying: false, - refreshing: false - } - }, - methods: { - refreshToken: function (callback) { - let that = this - Tea.action("/csrf/token") - .get() - .success(function (resp) { - that.token = resp.data.token - if (callback) { - callback() - } - }) - .fail(function () { - if (callback) { - callback() - } - }) - }, - setupAutoRetry: function (form) { - let that = this - var originalFail = form.getAttribute("data-tea-fail") - - // 确保 Tea.Vue 存在 - if (typeof Tea === "undefined" || Tea.Vue == null) { - if (typeof Tea === "undefined") { - window.Tea = {} - } - if (Tea.Vue == null) { - Tea.Vue = {} - } - } - - // 创建一个包装的 fail 函数 - var wrappedFailName = "csrfAutoRetryFail_" + Math.random().toString(36).substr(2, 9) - form.setAttribute("data-tea-fail", wrappedFailName) - - Tea.Vue[wrappedFailName] = function (resp) { - // 检查是否是 CSRF token 错误 - var isCSRFError = false - if (resp && resp.message) { - // 检查消息是否包含 "表单已失效" 或 "001" - if (resp.message.indexOf("表单已失效") >= 0 || resp.message.indexOf("(001)") >= 0) { - isCSRFError = true - } - } - // 检查 HTTP 状态码是否为 403 或 400 - if (!isCSRFError && resp && (resp.statusCode === 403 || resp.status === 403 || resp.statusCode === 400 || resp.status === 400)) { - isCSRFError = true - } - - if (isCSRFError) { - // 如果不是正在重试,则立即刷新 token 并自动重试 - if (!that.retrying) { - that.retrying = true - // 立即刷新 token - that.refreshToken(function () { - // 强制更新 Vue,确保响应式数据已更新 - that.$forceUpdate() - - // 使用 $nextTick 等待 Vue 完成 DOM 更新 - that.$nextTick(function () { - // 直接查找并更新 DOM 中的 input 元素(通过 name 属性) - var tokenInput = form.querySelector('input[name="csrfToken"]') - if (tokenInput) { - tokenInput.value = that.token - } - - // 如果 ref 存在,也更新它 - if (that.$refs.token) { - that.$refs.token.value = that.token - } - - // 使用 setTimeout 确保 DOM 已完全更新 - setTimeout(function () { - // 再次确认 token 值已更新 - var finalTokenInput = form.querySelector('input[name="csrfToken"]') - if (finalTokenInput && finalTokenInput.value !== that.token) { - finalTokenInput.value = that.token - } - - that.retrying = false - // 重新触发表单提交 - Tea.runActionOn(form) - }, 150) - }) - }) - return // 不调用原始 fail 函数 - } else { - // 如果正在重试,说明已经刷新过 token,直接调用原始 fail 函数 - if (originalFail && typeof Tea.Vue[originalFail] === "function") { - return Tea.Vue[originalFail].call(Tea.Vue, resp) - } else { - Tea.failResponse(resp) - } - } - } - - // 不是 CSRF 错误,调用原始 fail 函数或默认处理 - if (originalFail && typeof Tea.Vue[originalFail] === "function") { - return Tea.Vue[originalFail].call(Tea.Vue, resp) - } else { - Tea.failResponse(resp) - } - } - } - }, - 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.success("已复制到剪切板") - } - }, - template: `` -}) - -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}}   -
-
-
-
- [选择分组]   [添加分组] -
-
` -}) - -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"], - 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 - } - - return { - capacity: v, - countString: (v.count >= 0) ? v.count.toString() : "", - vSize: vSize, - vMaxlength: vMaxlength - } - }, - watch: { - "countString": function (newValue) { - let value = newValue.trim() - if (value.length == 0) { - this.capacity.count = -1 - this.change() - return - } - let count = parseInt(value) - if (!isNaN(count)) { - this.capacity.count = count - } - this.change() - } - }, - methods: { - change: function () { - this.$emit("change", this.capacity) - } - }, - template: `
- -
- -
-
- -
-
` -}) - -/** - * 二级菜单 - */ -Vue.component("inner-menu", { - template: ` -
- -
` -}); - -Vue.component("datepicker", { - props: ["value", "v-name", "name", "v-value", "v-bottom-left", "placeholder"], - mounted: function () { - let that = this - teaweb.datepicker(this.$refs.dayInput, function (v) { - that.day = v - that.change() - }, !!this.vBottomLeft) - }, - data: function () { - let name = this.vName - if (name == null) { - name = this.name - } - if (name == null) { - name = "day" - } - - let day = this.vValue - if (day == null) { - day = this.value - if (day == null) { - day = "" - } - } - - let placeholder = "YYYY-MM-DD" - if (this.placeholder != null) { - placeholder = this.placeholder - } - - return { - realName: name, - realPlaceholder: placeholder, - day: day - } - }, - watch: { - value: function (v) { - this.day = v - - let picker = this.$refs.dayInput.picker - if (picker != null) { - if (v != null && /^\d+-\d+-\d+$/.test(v)) { - picker.setDate(v) - } - } - } - }, - methods: { - change: function () { - this.$emit("input", this.day) // support v-model,事件触发需要在 change 之前 - this.$emit("change", this.day) - } - }, - template: `
- -
` -}) - -// 排序使用的箭头 -Vue.component("sort-arrow", { - props: ["name"], - data: function () { - let url = window.location.toString() - let order = "" - let iconTitle = "" - let newArgs = [] - if (window.location.search != null && window.location.search.length > 0) { - let queryString = window.location.search.substring(1) - let pieces = queryString.split("&") - let that = this - pieces.forEach(function (v) { - let eqIndex = v.indexOf("=") - if (eqIndex > 0) { - let argName = v.substring(0, eqIndex) - let argValue = v.substring(eqIndex + 1) - if (argName == that.name) { - order = argValue - } else if (argName != "page" && argValue != "asc" && argValue != "desc") { - newArgs.push(v) - } - } else { - newArgs.push(v) - } - }) - } - if (order == "asc") { - newArgs.push(this.name + "=desc") - iconTitle = "当前正序排列" - } else if (order == "desc") { - newArgs.push(this.name + "=asc") - iconTitle = "当前倒序排列" - } else { - newArgs.push(this.name + "=desc") - iconTitle = "当前正序排列" - } - - let qIndex = url.indexOf("?") - if (qIndex > 0) { - url = url.substring(0, qIndex) + "?" + newArgs.join("&") - } else { - url = url + "?" + newArgs.join("&") - } - - return { - order: order, - url: url, - iconTitle: iconTitle - } - }, - template: `  ` -}) - -Vue.component("ad-instance-objects-box", { - props: ["v-objects", "v-user-id"], - mounted: function () { - this.getUserServers(1) - }, - data: function () { - let objects = this.vObjects - if (objects == null) { - objects = [] - } - - let objectCodes = [] - objects.forEach(function (v) { - objectCodes.push(v.code) - }) - - return { - userId: this.vUserId, - objects: objects, - objectCodes: objectCodes, - isAdding: true, - - servers: [], - serversIsLoading: false - } - }, - methods: { - add: function () { - this.isAdding = true - }, - cancel: function () { - this.isAdding = false - }, - remove: function (index) { - let that = this - teaweb.confirm("确定要删除此防护对象吗?", function () { - that.objects.$remove(index) - that.notifyChange() - }) - }, - removeObjectCode: function (objectCode) { - let index = -1 - this.objectCodes.forEach(function (v, k) { - if (objectCode == v) { - index = k - } - }) - if (index >= 0) { - this.objects.$remove(index) - this.notifyChange() - } - }, - getUserServers: function (page) { - if (Tea.Vue == null) { - let that = this - setTimeout(function () { - that.getUserServers(page) - }, 100) - return - } - - let that = this - this.serversIsLoading = true - Tea.Vue.$post(".userServers") - .params({ - userId: this.userId, - page: page, - pageSize: 5 - }) - .success(function (resp) { - that.servers = resp.data.servers - - that.$refs.serverPage.updateMax(resp.data.page.max) - that.serversIsLoading = false - }) - .error(function () { - that.serversIsLoading = false - }) - }, - changeServerPage: function (page) { - this.getUserServers(page) - }, - selectServerObject: function (server) { - if (this.existObjectCode("server:" + server.id)) { - return - } - - this.objects.push({ - "type": "server", - "code": "server:" + server.id, - - "id": server.id, - "name": server.name - }) - this.notifyChange() - }, - notifyChange: function () { - let objectCodes = [] - this.objects.forEach(function (v) { - objectCodes.push(v.code) - }) - this.objectCodes = objectCodes - }, - existObjectCode: function (objectCode) { - let found = false - this.objects.forEach(function (v) { - if (v.code == objectCode) { - found = true - } - }) - return found - } - }, - template: `
- - - -
-
暂时还没有设置任何防护对象。
-
- - - - - -
已选中防护对象 -
- 网站:{{object.name}} -   -
-
-
-
-
- - -
- - - - - - - - - - -
对象类型网站
网站列表 - 加载中... -
暂时还没有可选的网站。
- - - - - - - - - - - -
网站名称操作
{{server.name}} - 选中 - 取消 -
- - -
-
- - -
- -
-
` -}) - window.REQUEST_COND_COMPONENTS = [{"type":"url-extension","name":"文件扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-eq-index","name":"首页","description":"检查URL路径是为\"/\"","component":"http-cond-url-eq-index","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":false},{"type":"url-all","name":"全站","description":"全站所有URL","component":"http-cond-url-all","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":false},{"type":"url-prefix","name":"URL目录前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-prefix","paramsTitle":"URL目录前缀","isRequest":true,"caseInsensitive":true},{"type":"url-eq","name":"URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-regexp","name":"URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致","component":"http-cond-url-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"url-wildcard-match","name":"URL通配符","description":"使用通配符检查URL中的文件路径是否一致","component":"http-cond-url-wildcard-match","paramsTitle":"通配符","isRequest":true,"caseInsensitive":true},{"type":"user-agent-regexp","name":"User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识","component":"http-cond-user-agent-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"params","name":"参数匹配","description":"根据参数值进行匹配","component":"http-cond-params","paramsTitle":"参数配置","isRequest":true,"caseInsensitive":false},{"type":"url-not-extension","name":"排除:URL扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-not-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-not-prefix","name":"排除:URL前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-not-prefix","paramsTitle":"URL前缀","isRequest":true,"caseInsensitive":true},{"type":"url-not-eq","name":"排除:URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-not-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-not-regexp","name":"排除:URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致,如果一致,则不匹配","component":"http-cond-url-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"user-agent-not-regexp","name":"排除:User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识,如果含有,则不匹配","component":"http-cond-user-agent-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"mime-type","name":"内容MimeType","description":"根据服务器返回的内容的MimeType进行过滤。注意:当用于缓存条件时,此条件需要结合别的请求条件使用。","component":"http-cond-mime-type","paramsTitle":"MimeType列表","isRequest":false,"caseInsensitive":false}]; window.REQUEST_COND_OPERATORS = [{"description":"判断是否正则表达式匹配","name":"正则表达式匹配","op":"regexp"},{"description":"判断是否正则表达式不匹配","name":"正则表达式不匹配","op":"not regexp"},{"description":"判断是否和指定的通配符匹配","name":"通配符匹配","op":"wildcard match"},{"description":"判断是否和指定的通配符不匹配","name":"通配符不匹配","op":"wildcard not match"},{"description":"使用字符串对比参数值是否相等于某个值","name":"字符串等于","op":"eq"},{"description":"参数值包含某个前缀","name":"字符串前缀","op":"prefix"},{"description":"参数值包含某个后缀","name":"字符串后缀","op":"suffix"},{"description":"参数值包含另外一个字符串","name":"字符串包含","op":"contains"},{"description":"参数值不包含另外一个字符串","name":"字符串不包含","op":"not contains"},{"description":"使用字符串对比参数值是否不相等于某个值","name":"字符串不等于","op":"not"},{"description":"判断参数值在某个列表中","name":"在列表中","op":"in"},{"description":"判断参数值不在某个列表中","name":"不在列表中","op":"not in"},{"description":"判断小写的扩展名(不带点)在某个列表中","name":"扩展名","op":"file ext"},{"description":"判断MimeType在某个列表中,支持类似于image/*的语法","name":"MimeType","op":"mime type"},{"description":"判断版本号在某个范围内,格式为version1,version2","name":"版本号范围","op":"version range"},{"description":"将参数转换为整数数字后进行对比","name":"整数等于","op":"eq int"},{"description":"将参数转换为可以有小数的浮点数字进行对比","name":"浮点数等于","op":"eq float"},{"description":"将参数转换为数字进行对比","name":"数字大于","op":"gt"},{"description":"将参数转换为数字进行对比","name":"数字大于等于","op":"gte"},{"description":"将参数转换为数字进行对比","name":"数字小于","op":"lt"},{"description":"将参数转换为数字进行对比","name":"数字小于等于","op":"lte"},{"description":"对整数参数值取模,除数为10,对比值为余数","name":"整数取模10","op":"mod 10"},{"description":"对整数参数值取模,除数为100,对比值为余数","name":"整数取模100","op":"mod 100"},{"description":"对整数参数值取模,对比值格式为:除数,余数,比如10,1","name":"整数取模","op":"mod"},{"description":"将参数转换为IP进行对比","name":"IP等于","op":"eq ip"},{"description":"将参数转换为IP进行对比","name":"IP大于","op":"gt ip"},{"description":"将参数转换为IP进行对比","name":"IP大于等于","op":"gte ip"},{"description":"将参数转换为IP进行对比","name":"IP小于","op":"lt ip"},{"description":"将参数转换为IP进行对比","name":"IP小于等于","op":"lte ip"},{"description":"IP在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e,或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e","name":"IP范围","op":"ip range"},{"description":"对IP参数值取模,除数为10,对比值为余数","name":"IP取模10","op":"ip mod 10"},{"description":"对IP参数值取模,除数为100,对比值为余数","name":"IP取模100","op":"ip mod 100"},{"description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1","name":"IP取模","op":"ip mod"}]; @@ -15506,7 +15506,7 @@ window.REQUEST_VARIABLES = [{"code":"${edgeVersion}","description":"","name":" window.METRIC_HTTP_KEYS = [{"name":"客户端地址(IP)","code":"${remoteAddr}","description":"会依次根据X-Forwarded-For、X-Real-IP、RemoteAddr获取,适用于前端可能有别的反向代理的情形,存在被伪造的可能","icon":""},{"name":"直接客户端地址(IP)","code":"${rawRemoteAddr}","description":"返回直接连接服务的客户端原始IP地址","icon":""},{"name":"客户端用户名","code":"${remoteUser}","description":"通过基本认证填入的用户名","icon":""},{"name":"请求URI","code":"${requestURI}","description":"包含参数,比如/hello?name=lily","icon":""},{"name":"请求路径","code":"${requestPath}","description":"不包含参数,比如/hello","icon":""},{"name":"完整URL","code":"${requestURL}","description":"比如https://example.com/hello?name=lily","icon":""},{"name":"请求方法","code":"${requestMethod}","description":"比如GET、POST等","icon":""},{"name":"请求协议Scheme","code":"${scheme}","description":"http或https","icon":""},{"name":"文件扩展名","code":"${requestPathExtension}","description":"请求路径中的文件扩展名,包括点符号,比如.html、.png","icon":""},{"name":"小写文件扩展名","code":"${requestPathLowerExtension}","description":"请求路径中的文件扩展名小写形式,包括点符号,比如.html、.png","icon":""},{"name":"主机名","code":"${host}","description":"通常是请求的域名","icon":""},{"name":"HTTP协议","code":"${proto}","description":"包含版本的HTTP请求协议,类似于HTTP/1.0","icon":""},{"name":"URL参数值","code":"${arg.NAME}","description":"单个URL参数值","icon":""},{"name":"请求来源URL","code":"${referer}","description":"请求来源Referer URL","icon":""},{"name":"请求来源URL域名","code":"${referer.host}","description":"请求来源Referer URL域名","icon":""},{"name":"Header值","code":"${header.NAME}","description":"单个Header值,比如${header.User-Agent}","icon":""},{"name":"Cookie值","code":"${cookie.NAME}","description":"单个cookie值,比如${cookie.sid}","icon":""},{"name":"状态码","code":"${status}","description":"","icon":""},{"name":"响应的Content-Type值","code":"${response.contentType}","description":"","icon":""}]; -window.WAF_RULE_CHECKPOINTS = [{"description":"通用报头比如Cache-Control、Accept之类的长度限制,防止缓冲区溢出攻击。","name":"通用请求报头长度限制","prefix":"requestGeneralHeaderLength"},{"description":"通用报头比如Cache-Control、Date之类的长度限制,防止缓冲区溢出攻击。","name":"通用响应报头长度限制","prefix":"responseGeneralHeaderLength"},{"description":"试图通过分析X-Forwarded-For等报头获取的客户端地址,比如192.168.1.100,存在伪造的可能。","name":"客户端地址(IP)","prefix":"remoteAddr"},{"description":"直接连接的客户端地址,比如192.168.1.100。","name":"客户端源地址(IP)","prefix":"rawRemoteAddr"},{"description":"直接连接的客户端地址端口。","name":"客户端端口","prefix":"remotePort"},{"description":"通过BasicAuth登录的客户端用户名。","name":"客户端用户名","prefix":"remoteUser"},{"description":"包含URL参数的请求URI,类似于 /hello/world?lang=go,不包含域名部分。","name":"请求URI","prefix":"requestURI"},{"description":"不包含URL参数的请求路径,类似于 /hello/world,不包含域名部分。","name":"请求路径","prefix":"requestPath"},{"description":"完整的请求URL,包含协议、域名、请求路径、参数等,类似于 https://example.com/hello?name=lily 。","name":"请求完整URL","prefix":"requestURL"},{"description":"请求报头中的Content-Length。","name":"请求内容长度","prefix":"requestLength"},{"description":"通常在POST或者PUT等操作时会附带请求体,最大限制32M。","name":"请求体内容","prefix":"requestBody"},{"description":"${requestURI}和${requestBody}组合。","name":"请求URI和请求体组合","prefix":"requestAll"},{"description":"获取POST或者其他方法发送的表单参数,最大请求体限制32M。","name":"请求表单参数","prefix":"requestForm"},{"description":"获取POST上传的文件信息,最大请求体限制32M。","name":"上传文件","prefix":"requestUpload"},{"description":"获取POST或者其他方法发送的JSON,最大请求体限制32M,使用点(.)符号表示多级数据。","name":"请求JSON参数","prefix":"requestJSON"},{"description":"比如GET、POST。","name":"请求方法","prefix":"requestMethod"},{"description":"比如http或https。","name":"请求协议","prefix":"scheme"},{"description":"比如HTTP/1.1。","name":"HTTP协议版本","prefix":"proto"},{"description":"比如example.com。","name":"主机名","prefix":"host"},{"description":"当前网站服务CNAME,比如38b48e4f.example.com。","name":"CNAME","prefix":"cname"},{"description":"是否为CNAME,值为1(是)或0(否)。","name":"是否为CNAME","prefix":"isCNAME"},{"description":"请求报头中的Referer和Origin值。","name":"请求来源","prefix":"refererOrigin"},{"description":"请求报头中的Referer值。","name":"请求来源Referer","prefix":"referer"},{"description":"比如Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103。","name":"客户端信息","prefix":"userAgent"},{"description":"请求报头的Content-Type。","name":"内容类型","prefix":"contentType"},{"description":"比如sid=IxZVPFhE\u0026city=beijing\u0026uid=18237。","name":"所有cookie组合字符串","prefix":"cookies"},{"description":"单个cookie值。","name":"单个cookie值","prefix":"cookie"},{"description":"比如name=lu\u0026age=20。","name":"所有URL参数组合","prefix":"args"},{"description":"单个URL参数值。","name":"单个URL参数值","prefix":"arg"},{"description":"使用换行符(\\n)隔开的报头内容字符串,每行均为\"NAME: VALUE格式\"。","name":"所有请求报头内容","prefix":"headers"},{"description":"使用换行符(\\n)隔开的报头名称字符串,每行一个名称。","name":"所有请求报头名称","prefix":"headerNames"},{"description":"单个报头值。","name":"单个请求报头值","prefix":"header"},{"description":"最长的请求报头的长度。","name":"请求报头最大长度","prefix":"headerMaxLength"},{"description":"当前客户端所处国家/地区名称。","name":"国家/地区名称","prefix":"geoCountryName"},{"description":"当前客户端所处中国省份名称。","name":"省份名称","prefix":"geoProvinceName"},{"description":"当前客户端所处中国城市名称。","name":"城市名称","prefix":"geoCityName"},{"description":"当前客户端所处ISP名称。","name":"ISP名称","prefix":"ispName"},{"description":"对统计对象进行统计。","name":"CC统计","prefix":"cc2"},{"description":"对统计对象进行统计。","name":"防盗链","prefix":"refererBlock"},{"description":"统计某段时间段内的请求信息(不推荐再使用,请使用新的CC2统计代替)。","name":"CC统计(旧)","prefix":"cc"},{"description":"响应状态码,比如200、404、500。","name":"响应状态码","prefix":"status"},{"description":"响应报头值。","name":"响应报头","prefix":"responseHeader"},{"description":"响应内容字符串。","name":"响应内容","prefix":"responseBody"},{"description":"响应内容长度,通过响应的报头Content-Length获取。","name":"响应内容长度","prefix":"bytesSent"}]; +window.WAF_RULE_CHECKPOINTS = [{"description":"通用报头比如Cache-Control、Accept之类的长度限制,防止缓冲区溢出攻击。","name":"通用请求报头长度限制","prefix":"requestGeneralHeaderLength"},{"description":"通用报头比如Cache-Control、Date之类的长度限制,防止缓冲区溢出攻击。","name":"通用响应报头长度限制","prefix":"responseGeneralHeaderLength"},{"description":"试图通过分析X-Forwarded-For等报头获取的客户端地址,比如192.168.1.100,存在伪造的可能。","name":"客户端地址(IP)","prefix":"remoteAddr"},{"description":"直接连接的客户端地址,比如192.168.1.100。","name":"客户端源地址(IP)","prefix":"rawRemoteAddr"},{"description":"直接连接的客户端地址端口。","name":"客户端端口","prefix":"remotePort"},{"description":"通过BasicAuth登录的客户端用户名。","name":"客户端用户名","prefix":"remoteUser"},{"description":"包含URL参数的请求URI,类似于 /hello/world?lang=go,不包含域名部分。","name":"请求URI","prefix":"requestURI"},{"description":"不包含URL参数的请求路径,类似于 /hello/world,不包含域名部分。","name":"请求路径","prefix":"requestPath"},{"description":"完整的请求URL,包含协议、域名、请求路径、参数等,类似于 https://example.com/hello?name=lily 。","name":"请求完整URL","prefix":"requestURL"},{"description":"请求报头中的Content-Length。","name":"请求内容长度","prefix":"requestLength"},{"description":"通常在POST或者PUT等操作时会附带请求体,最大限制32M。","name":"请求体内容","prefix":"requestBody"},{"description":"${requestURI}和${requestBody}组合。","name":"请求URI和请求体组合","prefix":"requestAll"},{"description":"获取POST或者其他方法发送的表单参数,最大请求体限制32M。","name":"请求表单参数","prefix":"requestForm"},{"description":"获取POST上传的文件信息,最大请求体限制32M。","name":"上传文件","prefix":"requestUpload"},{"description":"获取POST或者其他方法发送的JSON,最大请求体限制32M,使用点(.)符号表示多级数据。","name":"请求JSON参数","prefix":"requestJSON"},{"description":"比如GET、POST。","name":"请求方法","prefix":"requestMethod"},{"description":"比如http或https。","name":"请求协议","prefix":"scheme"},{"description":"比如HTTP/1.1。","name":"HTTP协议版本","prefix":"proto"},{"description":"比如example.com。","name":"主机名","prefix":"host"},{"description":"当前网站服务CNAME,比如38b48e4f.example.com。","name":"CNAME","prefix":"cname"},{"description":"是否为CNAME,值为1(是)或0(否)。","name":"是否为CNAME","prefix":"isCNAME"},{"description":"请求报头中的Referer和Origin值。","name":"请求来源","prefix":"refererOrigin"},{"description":"请求报头中的Referer值。","name":"请求来源Referer","prefix":"referer"},{"description":"比如Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103。","name":"客户端信息","prefix":"userAgent"},{"description":"请求报头的Content-Type。","name":"内容类型","prefix":"contentType"},{"description":"比如sid=IxZVPFhE\u0026city=beijing\u0026uid=18237。","name":"所有cookie组合字符串","prefix":"cookies"},{"description":"单个cookie值。","name":"单个cookie值","prefix":"cookie"},{"description":"比如name=lu\u0026age=20。","name":"所有URL参数组合","prefix":"args"},{"description":"单个URL参数值。","name":"单个URL参数值","prefix":"arg"},{"description":"使用换行符(\\n)隔开的报头内容字符串,每行均为\"NAME: VALUE格式\"。","name":"所有请求报头内容","prefix":"headers"},{"description":"使用换行符(\\n)隔开的报头名称字符串,每行一个名称。","name":"所有请求报头名称","prefix":"headerNames"},{"description":"单个报头值。","name":"单个请求报头值","prefix":"header"},{"description":"最长的请求报头的长度。","name":"请求报头最大长度","prefix":"headerMaxLength"},{"description":"当前客户端所处国家/地区名称。","name":"国家/地区名称","prefix":"geoCountryName"},{"description":"当前客户端所处中国省份名称。","name":"省份名称","prefix":"geoProvinceName"},{"description":"当前客户端所处中国城市名称。","name":"城市名称","prefix":"geoCityName"},{"description":"当前客户端所处ISP名称。","name":"ISP名称","prefix":"ispName"},{"description":"对统计对象进行统计。","name":"CC统计","prefix":"cc2"},{"description":"对统计对象进行统计。","name":"防盗链","prefix":"refererBlock"},{"description":"响应状态码,比如200、404、500。","name":"响应状态码","prefix":"status"},{"description":"响应报头值。","name":"响应报头","prefix":"responseHeader"},{"description":"响应内容字符串。","name":"响应内容","prefix":"responseBody"},{"description":"响应内容长度,通过响应的报头Content-Length获取。","name":"响应内容长度","prefix":"bytesSent"}]; window.WAF_RULE_OPERATORS = [{"name":"正则匹配","code":"match","description":"使用正则表达式匹配,在头部使用(?i)表示不区分大小写,\u003ca href=\"https://goedge.cn/docs/Appendix/Regexp/Index.md\" target=\"_blank\"\u003e正则表达式语法 \u0026raquo;\u003c/a\u003e。","caseInsensitive":"yes","dataType":"regexp"},{"name":"正则不匹配","code":"not match","description":"使用正则表达式不匹配,在头部使用(?i)表示不区分大小写,\u003ca href=\"https://goedge.cn/docs/Appendix/Regexp/Index.md\" target=\"_blank\"\u003e正则表达式语法 \u0026raquo;\u003c/a\u003e。","caseInsensitive":"yes","dataType":"regexp"},{"name":"通配符匹配","code":"wildcard match","description":"判断是否和指定的通配符匹配,可以在对比值中使用星号通配符(*)表示任意字符。","caseInsensitive":"yes","dataType":"wildcard"},{"name":"通配符不匹配","code":"wildcard not match","description":"判断是否和指定的通配符不匹配,可以在对比值中使用星号通配符(*)表示任意字符。","caseInsensitive":"yes","dataType":"wildcard"},{"name":"字符串等于","code":"eq string","description":"使用字符串对比等于。","caseInsensitive":"no","dataType":"string"},{"name":"字符串不等于","code":"neq string","description":"使用字符串对比不等于。","caseInsensitive":"no","dataType":"string"},{"name":"包含字符串","code":"contains","description":"包含某个字符串,比如Hello World包含了World。","caseInsensitive":"no","dataType":"string"},{"name":"不包含字符串","code":"not contains","description":"不包含某个字符串,比如Hello字符串中不包含Hi。","caseInsensitive":"no","dataType":"string"},{"name":"包含任一字符串","code":"contains any","description":"包含字符串列表中的任意一个,比如/hello/world包含/hello和/hi中的/hello,对比值中每行一个字符串。","caseInsensitive":"no","dataType":"strings"},{"name":"包含所有字符串","code":"contains all","description":"包含字符串列表中的所有字符串,比如/hello/world必须包含/hello和/world,对比值中每行一个字符串。","caseInsensitive":"no","dataType":"strings"},{"name":"包含前缀","code":"prefix","description":"包含字符串前缀部分,比如/hello前缀会匹配/hello, /hello/world等。","caseInsensitive":"no","dataType":"string"},{"name":"包含后缀","code":"suffix","description":"包含字符串后缀部分,比如/hello后缀会匹配/hello, /hi/hello等。","caseInsensitive":"no","dataType":"string"},{"name":"包含任一单词","code":"contains any word","description":"包含某个独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"包含所有单词","code":"contains all words","description":"包含所有的独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"不包含任一单词","code":"not contains any word","description":"不包含某个独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"包含SQL注入","code":"contains sql injection","description":"检测字符串内容是否包含SQL注入。","caseInsensitive":"none","dataType":"none"},{"name":"包含SQL注入-严格模式","code":"contains sql injection strictly","description":"更加严格地检测字符串内容是否包含SQL注入,相对于非严格模式,有一定的误报几率。","caseInsensitive":"none","dataType":"none"},{"name":"包含XSS注入","code":"contains xss","description":"检测字符串内容是否包含XSS注入。","caseInsensitive":"none","dataType":"none"},{"name":"包含XSS注入-严格模式","code":"contains xss strictly","description":"更加严格地检测字符串内容是否包含XSS注入,相对于非严格模式,此时xml、audio、video等标签也会被匹配。","caseInsensitive":"none","dataType":"none"},{"name":"包含二进制数据","code":"contains binary","description":"包含一组二进制数据。","caseInsensitive":"no","dataType":"string"},{"name":"不包含二进制数据","code":"not contains binary","description":"不包含一组二进制数据。","caseInsensitive":"no","dataType":"string"},{"name":"数值大于","code":"gt","description":"使用数值对比大于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值大于等于","code":"gte","description":"使用数值对比大于等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值小于","code":"lt","description":"使用数值对比小于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值小于等于","code":"lte","description":"使用数值对比小于等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值等于","code":"eq","description":"使用数值对比等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值不等于","code":"neq","description":"使用数值对比不等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"包含索引","code":"has key","description":"对于一组数据拥有某个键值或者索引。","caseInsensitive":"no","dataType":"string|number"},{"name":"版本号大于","code":"version gt","description":"对比版本号大于。","caseInsensitive":"none","dataType":"version"},{"name":"版本号小于","code":"version lt","description":"对比版本号小于。","caseInsensitive":"none","dataType":"version"},{"name":"版本号范围","code":"version range","description":"判断版本号在某个范围内,格式为 起始version1,结束version2。","caseInsensitive":"none","dataType":"versionRange"},{"name":"IP等于","code":"eq ip","description":"将参数转换为IP进行对比,只能对比单个IP。","caseInsensitive":"none","dataType":"ip"},{"name":"在一组IP中","code":"in ip list","description":"判断参数IP在一组IP内,对比值中每行一个IP。","caseInsensitive":"none","dataType":"ips"},{"name":"IP大于","code":"gt ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP大于等于","code":"gte ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP小于","code":"lt ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP小于等于","code":"lte ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP范围","code":"ip range","description":"IP在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e;或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e;或者单个IP。可以填写多行,每行一个IP范围。","caseInsensitive":"none","dataType":"ips"},{"name":"不在IP范围","code":"not ip range","description":"IP不在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e;或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e;或者单个IP。可以填写多行,每行一个IP范围。","caseInsensitive":"none","dataType":"ips"},{"name":"IP取模10","code":"ip mod 10","description":"对IP参数值取模,除数为10,对比值为余数。","caseInsensitive":"none","dataType":"number"},{"name":"IP取模100","code":"ip mod 100","description":"对IP参数值取模,除数为100,对比值为余数。","caseInsensitive":"none","dataType":"number"},{"name":"IP取模","code":"ip mod","description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1。","caseInsensitive":"none","dataType":"number"}]; diff --git a/EdgeUser/web/public/js/components.src.js b/EdgeUser/web/public/js/components.src.js index 41047f1..78a3b17 100644 --- a/EdgeUser/web/public/js/components.src.js +++ b/EdgeUser/web/public/js/components.src.js @@ -1,3 +1,3241 @@ +Vue.component("ad-instance-objects-box", { + props: ["v-objects", "v-user-id"], + mounted: function () { + this.getUserServers(1) + }, + data: function () { + let objects = this.vObjects + if (objects == null) { + objects = [] + } + + let objectCodes = [] + objects.forEach(function (v) { + objectCodes.push(v.code) + }) + + return { + userId: this.vUserId, + objects: objects, + objectCodes: objectCodes, + isAdding: true, + + servers: [], + serversIsLoading: false + } + }, + methods: { + add: function () { + this.isAdding = true + }, + cancel: function () { + this.isAdding = false + }, + remove: function (index) { + let that = this + teaweb.confirm("确定要删除此防护对象吗?", function () { + that.objects.$remove(index) + that.notifyChange() + }) + }, + removeObjectCode: function (objectCode) { + let index = -1 + this.objectCodes.forEach(function (v, k) { + if (objectCode == v) { + index = k + } + }) + if (index >= 0) { + this.objects.$remove(index) + this.notifyChange() + } + }, + getUserServers: function (page) { + if (Tea.Vue == null) { + let that = this + setTimeout(function () { + that.getUserServers(page) + }, 100) + return + } + + let that = this + this.serversIsLoading = true + Tea.Vue.$post(".userServers") + .params({ + userId: this.userId, + page: page, + pageSize: 5 + }) + .success(function (resp) { + that.servers = resp.data.servers + + that.$refs.serverPage.updateMax(resp.data.page.max) + that.serversIsLoading = false + }) + .error(function () { + that.serversIsLoading = false + }) + }, + changeServerPage: function (page) { + this.getUserServers(page) + }, + selectServerObject: function (server) { + if (this.existObjectCode("server:" + server.id)) { + return + } + + this.objects.push({ + "type": "server", + "code": "server:" + server.id, + + "id": server.id, + "name": server.name + }) + this.notifyChange() + }, + notifyChange: function () { + let objectCodes = [] + this.objects.forEach(function (v) { + objectCodes.push(v.code) + }) + this.objectCodes = objectCodes + }, + existObjectCode: function (objectCode) { + let found = false + this.objects.forEach(function (v) { + if (v.code == objectCode) { + found = true + } + }) + return found + } + }, + template: `
+ + + +
+
暂时还没有设置任何防护对象。
+
+ + + + + +
已选中防护对象 +
+ 网站:{{object.name}} +   +
+
+
+
+
+ + +
+ + + + + + + + + + +
对象类型网站
网站列表 + 加载中... +
暂时还没有可选的网站。
+ + + + + + + + + + + +
网站名称操作
{{server.name}} + 选中 + 取消 +
+ + +
+
+ + +
+ +
+
` +}) + +Vue.component("bandwidth-size-capacity-box", { + props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"], + data: function () { + let v = this.vValue + if (v == null) { + v = { + count: this.vCount, + unit: this.vUnit + } + } + if (v.unit == null || v.unit.length == 0) { + v.unit = "mb" + } + + if (typeof (v["count"]) != "number") { + v["count"] = -1 + } + + let vSize = this.size + if (vSize == null) { + vSize = 6 + } + + let vMaxlength = this.maxlength + if (vMaxlength == null) { + vMaxlength = 10 + } + + let supportedUnits = this.vSupportedUnits + if (supportedUnits == null) { + supportedUnits = [] + } + + return { + capacity: v, + countString: (v.count >= 0) ? v.count.toString() : "", + vSize: vSize, + vMaxlength: vMaxlength, + supportedUnits: supportedUnits + } + }, + watch: { + "countString": function (newValue) { + let value = newValue.trim() + if (value.length == 0) { + this.capacity.count = -1 + this.change() + return + } + let count = parseInt(value) + if (!isNaN(count)) { + this.capacity.count = count + } + this.change() + } + }, + methods: { + change: function () { + this.$emit("change", this.capacity) + } + }, + template: `
+ +
+ +
+
+ +
+
` +}) + +Vue.component("bandwidth-size-capacity-view", { + props: ["v-value"], + data: function () { + let capacity = this.vValue + if (capacity != null && capacity.count > 0 && typeof capacity.unit === "string") { + capacity.unit = capacity.unit[0].toUpperCase() + capacity.unit.substring(1) + "ps" + } + return { + capacity: capacity + } + }, + template: ` + {{capacity.count}}{{capacity.unit}} +` +}) + +Vue.component("bits-var", { + props: ["v-bits"], + data: function () { + let bits = this.vBits + if (typeof bits != "number") { + bits = 0 + } + let format = teaweb.splitFormat(teaweb.formatBits(bits)) + return { + format: format + } + }, + template:` + {{format[0]}}{{format[1]}} +` +}) + +Vue.component("bytes-var", { + props: ["v-bytes"], + data: function () { + let bytes = this.vBytes + if (typeof bytes != "number") { + bytes = 0 + } + let format = teaweb.splitFormat(teaweb.formatBytes(bytes)) + return { + format: format + } + }, + template:` + {{format[0]}}{{format[1]}} +` +}) + +let checkboxId = 0 +Vue.component("checkbox", { + props: ["name", "value", "v-value", "id", "checked"], + data: function () { + checkboxId++ + let elementId = this.id + if (elementId == null) { + elementId = "checkbox" + checkboxId + } + + let elementValue = this.vValue + if (elementValue == null) { + elementValue = "1" + } + + let checkedValue = this.value + if (checkedValue == null && this.checked == "checked") { + checkedValue = elementValue + } + + return { + elementId: elementId, + elementValue: elementValue, + newValue: checkedValue + } + }, + methods: { + change: function () { + this.$emit("input", this.newValue) + }, + check: function () { + this.newValue = this.elementValue + }, + uncheck: function () { + this.newValue = "" + }, + isChecked: function () { + return (typeof (this.newValue) == "boolean" && this.newValue) || this.newValue == this.elementValue + } + }, + watch: { + value: function (v) { + if (typeof v == "boolean") { + this.newValue = v + } + } + }, + template: `
+ + +
` +}) + +Vue.component("columns-grid", { + props: [], + mounted: function () { + this.columns = this.calculateColumns() + + let that = this + window.addEventListener("resize", function () { + that.columns = that.calculateColumns() + }) + }, + data: function () { + return { + columns: "four" + } + }, + methods: { + calculateColumns: function () { + let w = window.innerWidth + let columns = Math.floor(w / 250) + if (columns == 0) { + columns = 1 + } + + let columnElements = this.$el.getElementsByClassName("column") + if (columnElements.length == 0) { + return + } + let maxColumns = columnElements.length + if (columns > maxColumns) { + columns = maxColumns + } + + // 添加右侧边框 + for (let index = 0; index < columnElements.length; index++) { + let el = columnElements[index] + el.className = el.className.replace("with-border", "") + if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) { + el.className += " with-border" + } + } + + switch (columns) { + case 1: + return "one" + case 2: + return "two" + case 3: + return "three" + case 4: + return "four" + case 5: + return "five" + case 6: + return "six" + case 7: + return "seven" + case 8: + return "eight" + case 9: + return "nine" + case 10: + return "ten" + default: + return "ten" + } + } + }, + template: `
+ +
` +}) + +Vue.component("combo-box", { + // data-url 和 data-key 成对出现 + props: [ + "name", "title", "placeholder", "size", "v-items", "v-value", + "data-url", // 数据源URL + "data-key", // 数据源中数据的键名 + "data-search", // 是否启用动态搜索,如果值为on或true,则表示启用 + "width" + ], + mounted: function () { + if (this.dataURL.length > 0) { + this.search("") + } + + // 设定菜单宽度 + let searchBox = this.$refs.searchBox + if (searchBox != null) { + let inputWidth = searchBox.offsetWidth + if (inputWidth != null && inputWidth > 0) { + this.$refs.menu.style.width = inputWidth + "px" + } else if (this.styleWidth.length > 0) { + this.$refs.menu.style.width = this.styleWidth + } + } + }, + data: function () { + let items = this.vItems + if (items == null || !(items instanceof Array)) { + items = [] + } + items = this.formatItems(items) + + // 当前选中项 + let selectedItem = null + if (this.vValue != null) { + let that = this + items.forEach(function (v) { + if (v.value == that.vValue) { + selectedItem = v + } + }) + } + + let width = this.width + if (width == null || width.length == 0) { + width = "11em" + } else { + if (/\d+$/.test(width)) { + width += "em" + } + } + + // data url + let dataURL = "" + if (typeof this.dataUrl == "string" && this.dataUrl.length > 0) { + dataURL = this.dataUrl + } + + return { + allItems: items, // 原始的所有的items + items: items.$copy(), // 候选的items + selectedItem: selectedItem, // 选中的item + keyword: "", + visible: false, + hideTimer: null, + hoverIndex: 0, + styleWidth: width, + + isInitial: true, + dataURL: dataURL, + urlRequestId: 0 // 记录URL请求ID,防止并行冲突 + } + }, + methods: { + search: function (keyword) { + // 从URL中获取选项数据 + let dataUrl = this.dataURL + let dataKey = this.dataKey + let that = this + + let requestId = Math.random() + this.urlRequestId = requestId + + Tea.action(dataUrl) + .params({ + keyword: (keyword == null) ? "" : keyword + }) + .post() + .success(function (resp) { + if (requestId != that.urlRequestId) { + return + } + + if (resp.data != null) { + if (typeof (resp.data[dataKey]) == "object") { + let items = that.formatItems(resp.data[dataKey]) + that.allItems = items + that.items = items.$copy() + + if (that.isInitial) { + that.isInitial = false + if (that.vValue != null) { + items.forEach(function (v) { + if (v.value == that.vValue) { + that.selectedItem = v + } + }) + } + } + } + } + }) + }, + formatItems: function (items) { + items.forEach(function (v) { + if (v.value == null) { + v.value = v.id + } + }) + return items + }, + reset: function () { + this.selectedItem = null + this.change() + this.hoverIndex = 0 + + let that = this + setTimeout(function () { + if (that.$refs.searchBox) { + that.$refs.searchBox.focus() + } + }) + }, + clear: function () { + this.selectedItem = null + this.change() + this.hoverIndex = 0 + }, + changeKeyword: function () { + let shouldSearch = this.dataURL.length > 0 && (this.dataSearch == "on" || this.dataSearch == "true") + + this.hoverIndex = 0 + let keyword = this.keyword + if (keyword.length == 0) { + if (shouldSearch) { + this.search(keyword) + } else { + this.items = this.allItems.$copy() + } + return + } + + + if (shouldSearch) { + this.search(keyword) + } else { + this.items = this.allItems.$copy().filter(function (v) { + if (v.fullname != null && v.fullname.length > 0 && teaweb.match(v.fullname, keyword)) { + return true + } + return teaweb.match(v.name, keyword) + }) + } + }, + selectItem: function (item) { + this.selectedItem = item + this.change() + this.hoverIndex = 0 + this.keyword = "" + this.changeKeyword() + }, + confirm: function () { + if (this.items.length > this.hoverIndex) { + this.selectItem(this.items[this.hoverIndex]) + } + }, + show: function () { + this.visible = true + + // 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换 + }, + hide: function () { + let that = this + this.hideTimer = setTimeout(function () { + that.visible = false + }, 500) + }, + downItem: function () { + this.hoverIndex++ + if (this.hoverIndex > this.items.length - 1) { + this.hoverIndex = 0 + } + this.focusItem() + }, + upItem: function () { + this.hoverIndex-- + if (this.hoverIndex < 0) { + this.hoverIndex = 0 + } + this.focusItem() + }, + focusItem: function () { + if (this.hoverIndex < this.items.length) { + this.$refs.itemRef[this.hoverIndex].focus() + let that = this + setTimeout(function () { + that.$refs.searchBox.focus() + if (that.hideTimer != null) { + clearTimeout(that.hideTimer) + that.hideTimer = null + } + }) + } + }, + change: function () { + this.$emit("change", this.selectedItem) + + let that = this + setTimeout(function () { + if (that.$refs.selectedLabel != null) { + that.$refs.selectedLabel.focus() + } + }) + }, + submitForm: function (event) { + if (event.target.tagName != "A") { + return + } + let parentBox = this.$refs.selectedLabel.parentNode + while (true) { + parentBox = parentBox.parentNode + if (parentBox == null || parentBox.tagName == "BODY") { + return + } + if (parentBox.tagName == "FORM") { + parentBox.submit() + break + } + } + }, + + setDataURL: function (dataURL) { + this.dataURL = dataURL + }, + reloadData: function () { + this.search("") + } + }, + template: `
+ +
+ +
+ + +
+ + {{title}}:{{selectedItem.name}} + + +
+ + +
+ +
+
` +}) + +Vue.component("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.success("已复制到剪切板") + } + }, + template: `` +}) + +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("csrf-token", { + created: function () { + this.refreshToken() + }, + mounted: function () { + let that = this + var form = this.$refs.token.form + + // 监听表单提交,在提交前刷新token并确保更新到 DOM + form.addEventListener("submit", function (e) { + // 如果正在刷新,等待刷新完成 + if (that.refreshing) { + e.preventDefault() + e.stopPropagation() + return false + } + + // 阻止默认提交,先刷新 token + e.preventDefault() + e.stopPropagation() + that.refreshing = true + + // 刷新 token + that.refreshToken(function () { + // 确保 DOM 中的 token 值是最新的 + that.$forceUpdate() + that.$nextTick(function () { + var tokenInput = form.querySelector('input[name="csrfToken"]') + if (tokenInput) { + tokenInput.value = that.token + } + if (that.$refs.token) { + that.$refs.token.value = that.token + } + + // 确保 DOM 已更新后,再触发表单提交 + setTimeout(function () { + that.refreshing = false + // 重新触发表单提交 + Tea.runActionOn(form) + }, 50) + }) + }) + + return false + }) + + // 自动刷新 + setInterval(function () { + that.refreshToken() + }, 10 * 60 * 1000) + + // 监听表单提交失败,如果是 CSRF token 错误,自动刷新 token 并重试 + this.setupAutoRetry(form) + }, + data: function () { + return { + token: "", + retrying: false, + refreshing: false + } + }, + methods: { + refreshToken: function (callback) { + let that = this + Tea.action("/csrf/token") + .get() + .success(function (resp) { + that.token = resp.data.token + if (callback) { + callback() + } + }) + .fail(function () { + if (callback) { + callback() + } + }) + }, + setupAutoRetry: function (form) { + let that = this + var originalFail = form.getAttribute("data-tea-fail") + + // 确保 Tea.Vue 存在 + if (typeof Tea === "undefined" || Tea.Vue == null) { + if (typeof Tea === "undefined") { + window.Tea = {} + } + if (Tea.Vue == null) { + Tea.Vue = {} + } + } + + // 创建一个包装的 fail 函数 + var wrappedFailName = "csrfAutoRetryFail_" + Math.random().toString(36).substr(2, 9) + form.setAttribute("data-tea-fail", wrappedFailName) + + Tea.Vue[wrappedFailName] = function (resp) { + // 检查是否是 CSRF token 错误 + var isCSRFError = false + if (resp && resp.message) { + // 检查消息是否包含 "表单已失效" 或 "001" + if (resp.message.indexOf("表单已失效") >= 0 || resp.message.indexOf("(001)") >= 0) { + isCSRFError = true + } + } + // 检查 HTTP 状态码是否为 403 或 400 + if (!isCSRFError && resp && (resp.statusCode === 403 || resp.status === 403 || resp.statusCode === 400 || resp.status === 400)) { + isCSRFError = true + } + + if (isCSRFError) { + // 如果不是正在重试,则立即刷新 token 并自动重试 + if (!that.retrying) { + that.retrying = true + // 立即刷新 token + that.refreshToken(function () { + // 强制更新 Vue,确保响应式数据已更新 + that.$forceUpdate() + + // 使用 $nextTick 等待 Vue 完成 DOM 更新 + that.$nextTick(function () { + // 直接查找并更新 DOM 中的 input 元素(通过 name 属性) + var tokenInput = form.querySelector('input[name="csrfToken"]') + if (tokenInput) { + tokenInput.value = that.token + } + + // 如果 ref 存在,也更新它 + if (that.$refs.token) { + that.$refs.token.value = that.token + } + + // 使用 setTimeout 确保 DOM 已完全更新 + setTimeout(function () { + // 再次确认 token 值已更新 + var finalTokenInput = form.querySelector('input[name="csrfToken"]') + if (finalTokenInput && finalTokenInput.value !== that.token) { + finalTokenInput.value = that.token + } + + that.retrying = false + // 重新触发表单提交 + Tea.runActionOn(form) + }, 150) + }) + }) + return // 不调用原始 fail 函数 + } else { + // 如果正在重试,说明已经刷新过 token,直接调用原始 fail 函数 + if (originalFail && typeof Tea.Vue[originalFail] === "function") { + return Tea.Vue[originalFail].call(Tea.Vue, resp) + } else { + Tea.failResponse(resp) + } + } + } + + // 不是 CSRF 错误,调用原始 fail 函数或默认处理 + if (originalFail && typeof Tea.Vue[originalFail] === "function") { + return Tea.Vue[originalFail].call(Tea.Vue, resp) + } else { + Tea.failResponse(resp) + } + } + } + }, + template: `` +}) + + +Vue.component("datepicker", { + props: ["value", "v-name", "name", "v-value", "v-bottom-left", "placeholder"], + mounted: function () { + let that = this + teaweb.datepicker(this.$refs.dayInput, function (v) { + that.day = v + that.change() + }, !!this.vBottomLeft) + }, + data: function () { + let name = this.vName + if (name == null) { + name = this.name + } + if (name == null) { + name = "day" + } + + let day = this.vValue + if (day == null) { + day = this.value + if (day == null) { + day = "" + } + } + + let placeholder = "YYYY-MM-DD" + if (this.placeholder != null) { + placeholder = this.placeholder + } + + return { + realName: name, + realPlaceholder: placeholder, + day: day + } + }, + watch: { + value: function (v) { + this.day = v + + let picker = this.$refs.dayInput.picker + if (picker != null) { + if (v != null && /^\d+-\d+-\d+$/.test(v)) { + picker.setDate(v) + } + } + } + }, + methods: { + change: function () { + this.$emit("input", this.day) // support v-model,事件触发需要在 change 之前 + this.$emit("change", this.day) + } + }, + template: `
+ +
` +}) + +Vue.component("datetime-input", { + props: ["v-name", "v-timestamp"], + mounted: function () { + let that = this + teaweb.datepicker(this.$refs.dayInput, function (v) { + that.day = v + that.hour = "23" + that.minute = "59" + that.second = "59" + that.change() + }) + }, + data: function () { + let timestamp = this.vTimestamp + if (timestamp != null) { + timestamp = parseInt(timestamp) + if (isNaN(timestamp)) { + timestamp = 0 + } + } else { + timestamp = 0 + } + + let day = "" + let hour = "" + let minute = "" + let second = "" + + if (timestamp > 0) { + let date = new Date() + date.setTime(timestamp * 1000) + + let year = date.getFullYear().toString() + let month = this.leadingZero((date.getMonth() + 1).toString(), 2) + day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2) + + hour = this.leadingZero(date.getHours().toString(), 2) + minute = this.leadingZero(date.getMinutes().toString(), 2) + second = this.leadingZero(date.getSeconds().toString(), 2) + } + + return { + timestamp: timestamp, + day: day, + hour: hour, + minute: minute, + second: second, + + hasDayError: false, + hasHourError: false, + hasMinuteError: false, + hasSecondError: false + } + }, + methods: { + change: function () { + // day + if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) { + this.hasDayError = true + return + } + let pieces = this.day.split("-") + let year = parseInt(pieces[0]) + + let month = parseInt(pieces[1]) + if (month < 1 || month > 12) { + this.hasDayError = true + return + } + + let day = parseInt(pieces[2]) + if (day < 1 || day > 32) { + this.hasDayError = true + return + } + + this.hasDayError = false + + // hour + if (!/^\d+$/.test(this.hour)) { + this.hasHourError = true + return + } + let hour = parseInt(this.hour) + if (isNaN(hour)) { + this.hasHourError = true + return + } + if (hour < 0 || hour >= 24) { + this.hasHourError = true + return + } + this.hasHourError = false + + // minute + if (!/^\d+$/.test(this.minute)) { + this.hasMinuteError = true + return + } + let minute = parseInt(this.minute) + if (isNaN(minute)) { + this.hasMinuteError = true + return + } + if (minute < 0 || minute >= 60) { + this.hasMinuteError = true + return + } + this.hasMinuteError = false + + // second + if (!/^\d+$/.test(this.second)) { + this.hasSecondError = true + return + } + let second = parseInt(this.second) + if (isNaN(second)) { + this.hasSecondError = true + return + } + if (second < 0 || second >= 60) { + this.hasSecondError = true + return + } + this.hasSecondError = false + + let date = new Date(year, month - 1, day, hour, minute, second) + this.timestamp = Math.floor(date.getTime() / 1000) + }, + leadingZero: function (s, l) { + s = s.toString() + if (l <= s.length) { + return s + } + for (let i = 0; i < l - s.length; i++) { + s = "0" + s + } + return s + }, + resultTimestamp: function () { + return this.timestamp + }, + nextYear: function () { + let date = new Date() + date.setFullYear(date.getFullYear()+1) + this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) + this.hour = this.leadingZero(date.getHours(), 2) + this.minute = this.leadingZero(date.getMinutes(), 2) + this.second = this.leadingZero(date.getSeconds(), 2) + this.change() + }, + nextDays: function (days) { + let date = new Date() + date.setTime(date.getTime() + days * 86400 * 1000) + this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) + this.hour = this.leadingZero(date.getHours(), 2) + this.minute = this.leadingZero(date.getMinutes(), 2) + this.second = this.leadingZero(date.getSeconds(), 2) + this.change() + }, + nextHours: function (hours) { + let date = new Date() + date.setTime(date.getTime() + hours * 3600 * 1000) + this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) + this.hour = this.leadingZero(date.getHours(), 2) + this.minute = this.leadingZero(date.getMinutes(), 2) + this.second = this.leadingZero(date.getSeconds(), 2) + this.change() + } + }, + template: `
+ +
+
+ +
+
+
:
+
+
:
+
+
+

常用时间:  1小时  |  1天  |  3天  |  1周  |  30天  |  1年 

+
` +}) + +Vue.component("download-link", { + props: ["v-element", "v-file"], + 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 e = document.getElementById(this.vElement) + if (e == null) { + teaweb.warn("找不到要下载的内容") + return + } + let text = e.innerText + if (text == null) { + text = e.textContent + } + return Tea.url("/ui/download", { + file: this.file, + text: text + }) + } + }, + template: ``, +}) + +Vue.component("file-textarea", { + props: ["value"], + data: function () { + let value = this.value + if (typeof value != "string") { + value = "" + } + return { + realValue: value + } + }, + mounted: function () { + }, + methods: { + dragover: function () {}, + drop: function (e) { + let that = this + e.dataTransfer.items[0].getAsFile().text().then(function (data) { + that.setValue(data) + }) + }, + setValue: function (value) { + this.realValue = value + }, + focus: function () { + this.$refs.textarea.focus() + } + }, + template: `` +}) + +/** + * 一级菜单 + */ +Vue.component("first-menu", { + props: [], + 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("inner-menu", { + template: ` +
+ +
` +}); + +Vue.component("js-page", { + props: ["v-max"], + data: function () { + let max = this.vMax + if (max == null) { + max = 0 + } + return { + max: max, + page: 1 + } + }, + methods: { + updateMax: function (max) { + this.max = max + }, + selectPage: function(page) { + this.page = page + this.$emit("change", page) + } + }, + template:`
+
+ {{i}} +
+
` +}) + +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("labeled-input", { + props: ["name", "size", "maxlength", "label", "value"], + template: '
\ + \ + {{label}}\ +
' +}); + +// 启用状态标签 +Vue.component("label-on", { + props: ["v-is-on"], + template: '
已启用已停用
' +}) + +// 文字代码标签 +Vue.component("code-label", { + methods: { + click: function (args) { + this.$emit("click", args) + } + }, + template: `` +}) + +// tiny标签 +Vue.component("tiny-label", { + template: `` +}) + +Vue.component("tiny-basic-label", { + template: `` +}) + +// 更小的标签 +Vue.component("micro-basic-label", { + template: `` +}) + + +// 灰色的Label +Vue.component("grey-label", { + template: `` +}) + +// Plus专属 +Vue.component("plus-label", { + template: `` +}) + + +// 使用Icon的链接方式 +Vue.component("link-icon", { + props: ["href", "title"], + 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 () { + 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("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: '\ + \ + ' +}); + +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("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) + this.$emit("input", this.visible) + } + }, + 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("network-addresses-box", { + props: ["v-server-type", "v-addresses", "v-protocol", "v-name"], + 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" + } + + return { + addresses: addresses, + protocol: protocol, + name: name + } + }, + watch: { + "vServerType": function () { + this.addresses = [] + } + }, + methods: { + addAddr: function () { + let that = this + window.UPDATING_ADDR = null + teaweb.popup("/servers/addPortPopup?serverType=" + this.vServerType + "&protocol=" + this.protocol, { + height: "16em", + callback: function (resp) { + var addr = resp.data.address + 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 + teaweb.popup("/servers/addPortPopup?serverType=" + this.vServerType + "&protocol=" + this.protocol, { + height: "16em", + 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) + } + }, + template: `
+ +
+
+ {{addr.protocol}}://{{addr.host}}*:{{addr.portRange}} + +
+
+
+ [添加端口绑定] +
` +}) + +Vue.component("network-addresses-view", { + props: ["v-addresses"], + template: `
+
+ {{addr.protocol}}://{{addr.host}}:{{addr.portRange}} +
+
` +}) + +Vue.component("not-found-box", { + props: ["message"], + template: `
+
+

{{message}}

+
` +}) + +Vue.component("page-box", { + data: function () { + return { + page: "" + } + }, + created: function () { + let that = this; + setTimeout(function () { + if (Tea && Tea.Vue && Tea.Vue.page) { + that.page = Tea.Vue.page; + } + }) + }, + template: `
+
+
` +}) + +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}}
+
+
+
+ +
+
` +}) + +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("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("search-box", { + props: ["placeholder", "width"], + data: function () { + let width = this.width + if (width == null) { + width = "10em" + } + return { + realWidth: width, + realValue: "" + } + }, + methods: { + onInput: function () { + this.$emit("input", { value: this.realValue}) + this.$emit("change", { value: this.realValue}) + }, + clearValue: function () { + this.realValue = "" + this.focus() + this.onInput() + }, + focus: function () { + this.$refs.valueRef.focus() + } + }, + template: `
+
+ + +
+
` +}) + +/** + * 二级菜单 + */ +Vue.component("second-menu", { + template: ' \ +
\ + \ +
\ +
' +}); + +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("size-capacity-box", { + props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength"], + 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 + } + + return { + capacity: v, + countString: (v.count >= 0) ? v.count.toString() : "", + vSize: vSize, + vMaxlength: vMaxlength + } + }, + 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("size-capacity-view", { + props:["v-default-text", "v-value"], + methods: { + composeCapacity: function (capacity) { + return teaweb.convertSizeCapacityToString(capacity) + } + }, + template: `
+ {{composeCapacity(vValue)}} + {{vDefaultText}} +
` +}) + +// 排序使用的箭头 +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: `  ` +}) + +// 给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) +} + + +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("submit-btn", { + template: '' +}); + +Vue.component("theme-color-picker", { + props: ["v-model"], + data: function () { + return { + showPicker: false, + r: 20, + g: 83, + b: 196, + hex: "#14539A", + clickHandler: null + } + }, + mounted: function () { + console.log("Theme color picker component mounted, element:", this.$el) + // 从localStorage加载保存的颜色 + let savedColor = localStorage.getItem("themeColor") + if (savedColor) { + this.setColor(savedColor) + } else { + // 从当前页面获取默认颜色 + let defaultColor = this.getCurrentThemeColor() + if (defaultColor) { + this.setColor(defaultColor) + } + } + + // 应用颜色 + this.applyColor() + }, + beforeDestroy: function() { + // 移除点击监听器 + if (this.clickHandler) { + document.removeEventListener("click", this.clickHandler) + this.clickHandler = null + } + }, + methods: { + // 打开/关闭颜色选择器 + togglePicker: function (e) { + if (e) { + e.preventDefault() + e.stopPropagation() + } + console.log("Toggle picker clicked, current showPicker:", this.showPicker) + this.showPicker = !this.showPicker + console.log("Toggle picker clicked, new showPicker:", this.showPicker) + console.log("Popup should be visible:", this.showPicker) + + // 管理外部点击监听器 + if (this.showPicker) { + // 打开时,延迟添加监听器 + let that = this + setTimeout(function() { + if (!that.clickHandler) { + that.clickHandler = function(e) { + if (that.showPicker && !that.$el.contains(e.target)) { + that.showPicker = false + } + } + document.addEventListener("click", that.clickHandler) + } + }, 100) + } else { + // 关闭时,移除监听器 + if (this.clickHandler) { + document.removeEventListener("click", this.clickHandler) + this.clickHandler = null + } + } + }, + + // RGB转16进制 + rgbToHex: function (r, g, b) { + return "#" + [r, g, b].map(function (x) { + x = parseInt(x) + const hex = x.toString(16) + return hex.length === 1 ? "0" + hex : hex + }).join("").toUpperCase() + }, + + // 16进制转RGB + hexToRgb: function (hex) { + hex = hex.replace("#", "") + const r = parseInt(hex.substring(0, 2), 16) + const g = parseInt(hex.substring(2, 4), 16) + const b = parseInt(hex.substring(4, 6), 16) + return { r: r, g: g, b: b } + }, + + // 设置颜色(支持hex或rgb对象) + setColor: function (color) { + if (typeof color === "string") { + if (color.startsWith("#")) { + let rgb = this.hexToRgb(color) + this.r = rgb.r + this.g = rgb.g + this.b = rgb.b + this.hex = color.toUpperCase() + } else { + // 假设是16进制不带#号 + this.setColor("#" + color) + } + } else if (typeof color === "object" && color.r !== undefined) { + this.r = color.r + this.g = color.g + this.b = color.b + this.hex = this.rgbToHex(color.r, color.g, color.b) + } + }, + + // RGB值变化时更新hex + updateHex: function () { + this.hex = this.rgbToHex(this.r, this.g, this.b) + this.applyColor() + }, + + // Hex值变化时更新RGB + updateRgb: function () { + if (this.hex.match(/^#[0-9A-Fa-f]{6}$/)) { + let rgb = this.hexToRgb(this.hex) + this.r = rgb.r + this.g = rgb.g + this.b = rgb.b + this.applyColor() + } + }, + + // 应用颜色到整个网站 + applyColor: function () { + let color = this.rgbToHex(this.r, this.g, this.b) + + // 保存到localStorage + localStorage.setItem("themeColor", color) + + // 创建或更新样式 + let styleId = "theme-color-custom" + let styleEl = document.getElementById(styleId) + if (!styleEl) { + styleEl = document.createElement("style") + styleEl.id = styleId + document.head.appendChild(styleEl) + } + + // 应用颜色到主题元素 + styleEl.textContent = ` + .top-nav, .main-menu, .main-menu .menu { + background: ${color} !important; + } + .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless { + background: ${color} !important; + } + .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .item { + background: ${color} !important; + } + .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .sub-items { + background: ${color} !important; + } + .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .sub-items .item { + background: ${color} !important; + } + .main-menu .ui.menu .sub-items { + background: ${color} !important; + } + .main-menu .ui.menu .sub-items .item { + background: ${color} !important; + } + .main-menu .ui.labeled.menu .sub-items { + background: ${color} !important; + } + .main-menu .ui.labeled.menu .sub-items .item { + background: ${color} !important; + } + .main-menu .sub-items { + background: ${color} !important; + } + .main-menu .sub-items .item { + background: ${color} !important; + } + .ui.menu.blue.inverted { + background: ${color} !important; + } + .ui.menu.blue.inverted .item { + background: ${color} !important; + } + .ui.menu.vertical.blue.inverted { + background: ${color} !important; + } + .ui.menu.vertical.blue.inverted .item { + background: ${color} !important; + } + .ui.labeled.menu.vertical.blue.inverted { + background: ${color} !important; + } + .ui.labeled.menu.vertical.blue.inverted .item { + background: ${color} !important; + } + ` + + // 触发事件通知其他组件 + if (window.Tea && window.Tea.Vue) { + window.Tea.Vue.$emit("theme-color-changed", color) + } + + // 更新v-model(如果使用) + this.$emit("input", color.replace("#", "")) + }, + + // 获取当前主题颜色 + getCurrentThemeColor: function () { + let topNav = document.querySelector(".top-nav") + if (topNav) { + let bgColor = window.getComputedStyle(topNav).backgroundColor + if (bgColor && bgColor !== "rgba(0, 0, 0, 0)" && bgColor !== "transparent") { + // 转换rgb/rgba为hex + let match = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/) + if (match) { + return this.rgbToHex(parseInt(match[1]), parseInt(match[2]), parseInt(match[3])) + } + } + } + return null + }, + + // 重置为默认颜色 + resetColor: function () { + this.setColor("#14539A") // 默认蓝色 + this.applyColor() + } + }, + template: ` +
+ + 主题色 + + +
+
+ RGB颜色调节 +
+ + +
+
+
+ {{ hex }} | RGB({{ r }}, {{ g }}, {{ b }}) +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+ + +
+
+
+ ` +}) + + +Vue.component("time-duration-box", { + props: ["v-name", "v-value", "v-count", "v-unit", "placeholder", "v-min-unit", "maxlength"], + mounted: function () { + this.change() + }, + data: function () { + let v = this.vValue + if (v == null) { + v = { + count: this.vCount, + unit: this.vUnit + } + } + if (typeof (v["count"]) != "number") { + v["count"] = -1 + } + + let minUnit = this.vMinUnit + let units = [ + { + code: "ms", + name: "毫秒" + }, + { + code: "second", + name: "秒" + }, + { + code: "minute", + name: "分钟" + }, + { + code: "hour", + name: "小时" + }, + { + code: "day", + name: "天" + } + ] + let minUnitIndex = -1 + if (minUnit != null && typeof minUnit == "string" && minUnit.length > 0) { + for (let i = 0; i < units.length; i++) { + if (units[i].code == minUnit) { + minUnitIndex = i + break + } + } + } + if (minUnitIndex > -1) { + units = units.slice(minUnitIndex) + } + + let maxLength = parseInt(this.maxlength) + if (typeof maxLength != "number") { + maxLength = 10 + } + + return { + duration: v, + countString: (v.count >= 0) ? v.count.toString() : "", + units: units, + realMaxLength: maxLength + } + }, + watch: { + "countString": function (newValue) { + let value = newValue.trim() + if (value.length == 0) { + this.duration.count = -1 + return + } + let count = parseInt(value) + if (!isNaN(count)) { + this.duration.count = count + } + this.change() + } + }, + methods: { + change: function () { + this.$emit("change", this.duration) + } + }, + template: `
+ +
+ +
+
+ +
+
` +}) + +Vue.component("time-duration-text", { + props: ["v-value"], + methods: { + unitName: function (unit) { + switch (unit) { + case "ms": + return "毫秒" + case "second": + return "秒" + case "minute": + return "分钟" + case "hour": + return "小时" + case "day": + return "天" + } + } + }, + template: ` + {{vValue.count}} {{unitName(vValue.unit)}} +` +}) + +Vue.component("url-patterns-box", { + props: ["value"], + data: function () { + let patterns = [] + if (this.value != null) { + patterns = this.value + } + + return { + patterns: patterns, + isAdding: false, + + addingPattern: {"type": "wildcard", "pattern": ""}, + editingIndex: -1, + + patternIsInvalid: false, + + windowIsSmall: window.innerWidth < 600 + } + }, + methods: { + add: function () { + this.isAdding = true + let that = this + setTimeout(function () { + that.$refs.patternInput.focus() + }) + }, + edit: function (index) { + this.isAdding = true + this.editingIndex = index + this.addingPattern = { + type: this.patterns[index].type, + pattern: this.patterns[index].pattern + } + }, + confirm: function () { + if (this.requireURL(this.addingPattern.type)) { + let pattern = this.addingPattern.pattern.trim() + if (pattern.length == 0) { + let that = this + teaweb.warn("请输入URL", function () { + that.$refs.patternInput.focus() + }) + return + } + } + if (this.editingIndex < 0) { + this.patterns.push({ + type: this.addingPattern.type, + pattern: this.addingPattern.pattern + }) + } else { + this.patterns[this.editingIndex].type = this.addingPattern.type + this.patterns[this.editingIndex].pattern = this.addingPattern.pattern + } + this.notifyChange() + this.cancel() + }, + remove: function (index) { + this.patterns.$remove(index) + this.cancel() + this.notifyChange() + }, + cancel: function () { + this.isAdding = false + this.addingPattern = {"type": "wildcard", "pattern": ""} + this.editingIndex = -1 + }, + patternTypeName: function (patternType) { + switch (patternType) { + case "wildcard": + return "通配符" + case "regexp": + return "正则" + case "images": + return "常见图片文件" + case "audios": + return "常见音频文件" + case "videos": + return "常见视频文件" + } + return "" + }, + notifyChange: function () { + this.$emit("input", this.patterns) + }, + changePattern: function () { + this.patternIsInvalid = false + let pattern = this.addingPattern.pattern + switch (this.addingPattern.type) { + case "wildcard": + if (pattern.indexOf("?") >= 0) { + this.patternIsInvalid = true + } + break + case "regexp": + if (pattern.indexOf("?") >= 0) { + let pieces = pattern.split("?") + for (let i = 0; i < pieces.length - 1; i++) { + if (pieces[i].length == 0 || pieces[i][pieces[i].length - 1] != "\\") { + this.patternIsInvalid = true + } + } + } + break + } + }, + requireURL: function (patternType) { + return patternType == "wildcard" || patternType == "regexp" + } + }, + template: `
+
+
+ [{{patternTypeName(pattern.type)}}] {{pattern.pattern}}   + + +
+
+
+
+
+ +
+
+ +

通配符正则表达式中不能包含问号(?)及问号以后的内容。

+
+
+ + +
+
+ +
+
+
+
+ +
+
` +}) + +Vue.component("values-box", { + props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"], + data: function () { + let values = this.values; + if (values == null) { + values = []; + } + + if (this.vValues != null && typeof this.vValues == "object") { + values = this.vValues + } + + return { + "realValues": values, + "isUpdating": false, + "isAdding": false, + "index": 0, + "value": "", + isEditing: false + } + }, + methods: { + create: function () { + this.isAdding = true; + var that = this; + setTimeout(function () { + that.$refs.value.focus(); + }, 200); + }, + update: function (index) { + this.cancel() + this.isUpdating = true; + this.index = index; + this.value = this.realValues[index]; + var that = this; + setTimeout(function () { + that.$refs.value.focus(); + }, 200); + }, + confirm: function () { + if (this.value.length == 0) { + if (typeof(this.vAllowEmpty) != "boolean" || !this.vAllowEmpty) { + return + } + } + + // validate + if (typeof(this.validator) == "function") { + let resp = this.validator.call(this, this.value) + if (typeof resp == "object") { + if (typeof resp.isOk == "boolean" && !resp.isOk) { + if (typeof resp.message == "string") { + let that = this + teaweb.warn(resp.message, function () { + that.$refs.value.focus(); + }) + } + return + } + } + } + + if (this.isUpdating) { + Vue.set(this.realValues, this.index, this.value); + } else { + this.realValues.push(this.value); + } + this.cancel() + this.$emit("change", this.realValues) + }, + remove: function (index) { + this.realValues.$remove(index) + this.$emit("change", this.realValues) + }, + cancel: function () { + this.isUpdating = false; + this.isAdding = false; + this.value = ""; + }, + updateAll: function (values) { + this.realValues = values + }, + addValue: function (v) { + this.realValues.push(v) + }, + + startEditing: function () { + this.isEditing = !this.isEditing + }, + allValues: function () { + return this.realValues + } + }, + template: `
+
+
+ {{value}} + [空] +
+ [修改] +
+
+
+
+ {{value}} + [空] + +   + +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
` +}); + +// 警告消息 +Vue.component("warning-message", { + template: `
` +}) + +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("ip-item-text", { + props: ["v-item"], + template: ` + * + + {{vItem.value}} + + {{vItem.ipFrom}} + - {{vItem.ipTo}} + + +   级别:{{vItem.eventLevelName}} +` +}) + +// 绑定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("/waf/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("/waf/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.value}} + + {{item.ipFrom}}  New   + - {{item.ipTo}} + + + +
+ {{item.region}} + | {{item.isp}} +
+
{{item.isp}}
+ +
+ 添加于 {{item.createdTime}} + + @ + + [名单:{{item.list.name}}] + [名单:{{item.list.name}} + + + + [网站:{{item.policy.server.name}}] + [网站:{{item.policy.server.name}}] + [网站:{{item.policy.server.name}}] + + + + +
+
+ IPv4 + IPv4 + IPv6 + 所有IP + + {{item.eventLevelName}} + - + +
+ {{item.expiredTime}} +
+ 已过期 +
+
+ {{formatSeconds(item.lifeSeconds)}} + 已过期 +
+
+ 不过期 +
+ {{item.reason}} + - + + + + + + 修改   + 删除 +
+
` +}) + Vue.component("message-row", { props: ["v-message"], data: function () { @@ -87,6 +3325,212 @@ Vue.component("message-row", {
` }) +Vue.component("ns-access-log-box", { + props: ["v-access-log", "v-keyword"], + data: function () { + let accessLog = this.vAccessLog + return { + accessLog: accessLog + } + }, + 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-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-create-records-table", { + props: ["v-types", "v-default-ttl"], + data: function () { + let types = this.vTypes + if (types == null) { + types = [] + } + + let defaultTTL = this.vDefaultTtl + if (defaultTTL != null) { + defaultTTL = parseInt(defaultTTL.toString()) + } + if (defaultTTL <= 0) { + defaultTTL = 600 + } + + return { + types: types, + defaultTTL: defaultTTL, + records: [ + { + name: "", + type: "A", + value: "", + routeCodes: [], + ttl: defaultTTL, + index: 0 + } + ], + lastIndex: 0, + isAddingRoutes: false // 是否正在添加线路 + } + }, + methods: { + add: function () { + this.records.push({ + name: "", + type: "A", + value: "", + routeCodes: [], + ttl: this.defaultTTL, + 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-domain-group-selector", { props: ["v-domain-group-id"], data: function () { @@ -127,188 +3571,268 @@ Vue.component("ns-domain-group-selector", { ` }) -// 选择多个线路 -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 - that.supportChinaProvinceRoutes = resp.data.supportChinaProvinceRoutes - that.supportISPRoutes = resp.data.supportISPRoutes - that.supportWorldRegionRoutes = resp.data.supportWorldRegionRoutes - that.supportAgentRoutes = resp.data.supportAgentRoutes - that.publicCategories = resp.data.publicCategories - that.supportPublicRoutes = resp.data.supportPublicRoutes - - // provinces - let provinces = {} - if (resp.data.provinces != null && resp.data.provinces.length > 0) { - for (const province of resp.data.provinces) { - let countryCode = province.countryCode - if (typeof provinces[countryCode] == "undefined") { - provinces[countryCode] = [] - } - provinces[countryCode].push({ - name: province.name, - code: province.code - }) - } - } - that.provinces = provinces - }) - }, +Vue.component("ns-record-health-check-config-box", { + props:["value", "v-parent-config"], data: function () { - let selectedRoutes = this.vRoutes - if (selectedRoutes == null) { - selectedRoutes = [] + let config = this.value + if (config == null) { + config = { + isOn: false, + port: 0, + timeoutSeconds: 0, + countUp: 0, + countDown: 0 + } } - let inputName = this.name - if (typeof inputName != "string" || inputName.length == 0) { - inputName = "routeCodes" - } + let parentConfig = this.vParentConfig return { - routeCode: "default", - inputName: inputName, - routes: [], + config: config, + portString: config.port.toString(), + timeoutSecondsString: config.timeoutSeconds.toString(), + countUpString: config.countUp.toString(), + countDownString: config.countDown.toString(), + portIsEditing: config.port > 0, + timeoutSecondsIsEditing: config.timeoutSeconds > 0, + countUpIsEditing: config.countUp > 0, + countDownIsEditing: config.countDown > 0, - provinces: {}, // country code => [ province1, province2, ... ] - provinceRouteCode: "", - - isAdding: false, - routeType: "default", - selectedRoutes: selectedRoutes, - - supportChinaProvinceRoutes: false, - supportISPRoutes: false, - supportWorldRegionRoutes: false, - supportAgentRoutes: false, - supportPublicRoutes: false, - publicCategories: [] + parentConfig: parentConfig } }, 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 - } - }) + portString: function (value) { + let port = parseInt(value.toString()) + if (isNaN(port) || port > 65535 || port < 1) { + this.config.port = 0 + } else { + this.config.port = port + } + }, + timeoutSecondsString: function (value) { + let timeoutSeconds = parseInt(value.toString()) + if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) { + this.config.timeoutSeconds = 0 + } else { + this.config.timeoutSeconds = timeoutSeconds + } + }, + countUpString: function (value) { + let countUp = parseInt(value.toString()) + if (isNaN(countUp) || countUp > 1000 || countUp < 1) { + this.config.countUp = 0 + } else { + this.config.countUp = countUp + } + }, + countDownString: function (value) { + let countDown = parseInt(value.toString()) + if (isNaN(countDown) || countDown > 1000 || countDown < 1) { + this.config.countDown = 0 + } else { + this.config.countDown = countDown + } } }, - methods: { - add: function () { - this.isAdding = true - this.routeType = "default" - this.routeCode = "default" - this.provinceRouteCode = "" - this.$emit("add") - }, - cancel: function () { - this.isAdding = false - this.$emit("cancel") - }, - confirm: function () { - if (this.routeCode.length == 0) { - return - } - - let that = this - - - // route - let selectedRoute = null - for (const route of this.routes) { - if (route.code == this.routeCode) { - selectedRoute = route - break - } - } - - if (selectedRoute != null) { - // province route - if (this.provinceRouteCode.length > 0 && this.provinces[this.routeCode] != null) { - for (const province of this.provinces[this.routeCode]) { - if (province.code == this.provinceRouteCode) { - selectedRoute = { - name: selectedRoute.name + "-" + province.name, - code: province.code - } - break - } - } - } - - that.selectedRoutes.push(selectedRoute) - } - - this.$emit("change", this.selectedRoutes) - this.cancel() - }, - remove: function (index) { - this.selectedRoutes.$remove(index) - this.$emit("change", this.selectedRoutes) - } - } - , template: `
-
-
- - {{route.name}}   -
-
-
-
- + +
+ - + + + + + + + - + - - + + -
选择类型 *启用当前记录健康检查 - + +
检测端口 + + 默认{{parentConfig.port}} +   [修改] + +
+ + +

通过尝试连接A/AAAA记录中的IP加此端口来确定当前记录是否健康。

+
选择线路 *超时时间 - + + 默认{{parentConfig.timeoutSeconds}}秒 +   [修改] + +
+ +
+ + +
+
选择省/州
默认连续上线次数 - + + 默认{{parentConfig.countUp}}次 +   [修改] + +
+ +
+ + +
+

连续检测{{config.countUp}}N次成功后,认为当前记录是在线的。

+
-
- -   取消 -
-
- -

由于套餐限制,当前用户只能使用部分线路。

+ + 默认连续下线次数 + + + 默认{{parentConfig.countDown}}次 +   [修改] + +
+
+ [使用默认] +
+
+ + +
+

连续检测{{config.countDown}}N次失败后,认为当前记录是离线的。

+
+ + + + +
+
` +}) + +Vue.component("ns-records-health-check-config-box", { + props:["value"], + data: function () { + let config = this.value + if (config == null) { + config = { + isOn: false, + port: 80, + timeoutSeconds: 5, + countUp: 1, + countDown: 3 + } + } + return { + config: config, + portString: config.port.toString(), + timeoutSecondsString: config.timeoutSeconds.toString(), + countUpString: config.countUp.toString(), + countDownString: config.countDown.toString() + } + }, + watch: { + portString: function (value) { + let port = parseInt(value.toString()) + if (isNaN(port) || port > 65535 || port < 1) { + this.config.port = 80 + } else { + this.config.port = port + } + }, + timeoutSecondsString: function (value) { + let timeoutSeconds = parseInt(value.toString()) + if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) { + this.config.timeoutSeconds = 5 + } else { + this.config.timeoutSeconds = timeoutSeconds + } + }, + countUpString: function (value) { + let countUp = parseInt(value.toString()) + if (isNaN(countUp) || countUp > 1000 || countUp < 1) { + this.config.countUp = 1 + } else { + this.config.countUp = countUp + } + }, + countDownString: function (value) { + let countDown = parseInt(value.toString()) + if (isNaN(countDown) || countDown > 1000 || countDown < 1) { + this.config.countDown = 3 + } else { + this.config.countDown = countDown + } + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
启用健康检查 + +

选中后,表示启用当前域名下A/AAAA记录的健康检查;启用此设置后,你仍需设置单个A/AAAA记录的健康检查。

+
默认检测端口 + +

通过尝试连接A/AAAA记录中的IP加此端口来确定当前记录是否健康。

+
默认超时时间 +
+ + +
+
默认连续上线次数 +
+ + +
+

连续检测{{config.countUp}}N次成功后,认为当前记录是在线的。

+
默认连续下线次数 +
+ + +
+

连续检测{{config.countDown}}N次失败后,认为当前记录是离线的。

+
+
` }) @@ -478,158 +4002,6 @@ Vue.component("ns-recursion-config-box", { ` }) -Vue.component("ns-access-log-ref-box", { - props: ["v-access-log-ref", "v-is-parent"], - data: function () { - let config = this.vAccessLogRef - if (config == null) { - config = { - isOn: false, - isPrior: false, - logMissingDomains: false - } - } - if (typeof (config.logMissingDomains) == "undefined") { - config.logMissingDomains = false - } - return { - config: config - } - }, - template: `
- - - - - - - - - - - - - -
启用 - -
记录所有访问 - -

包括对没有在系统里创建的域名访问。

-
-
-
` -}) - -Vue.component("ns-records-health-check-config-box", { - props:["value"], - data: function () { - let config = this.value - if (config == null) { - config = { - isOn: false, - port: 80, - timeoutSeconds: 5, - countUp: 1, - countDown: 3 - } - } - return { - config: config, - portString: config.port.toString(), - timeoutSecondsString: config.timeoutSeconds.toString(), - countUpString: config.countUp.toString(), - countDownString: config.countDown.toString() - } - }, - watch: { - portString: function (value) { - let port = parseInt(value.toString()) - if (isNaN(port) || port > 65535 || port < 1) { - this.config.port = 80 - } else { - this.config.port = port - } - }, - timeoutSecondsString: function (value) { - let timeoutSeconds = parseInt(value.toString()) - if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) { - this.config.timeoutSeconds = 5 - } else { - this.config.timeoutSeconds = timeoutSeconds - } - }, - countUpString: function (value) { - let countUp = parseInt(value.toString()) - if (isNaN(countUp) || countUp > 1000 || countUp < 1) { - this.config.countUp = 1 - } else { - this.config.countUp = countUp - } - }, - countDownString: function (value) { - let countDown = parseInt(value.toString()) - if (isNaN(countDown) || countDown > 1000 || countDown < 1) { - this.config.countDown = 3 - } else { - this.config.countDown = countDown - } - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - -
启用健康检查 - -

选中后,表示启用当前域名下A/AAAA记录的健康检查;启用此设置后,你仍需设置单个A/AAAA记录的健康检查。

-
默认检测端口 - -

通过尝试连接A/AAAA记录中的IP加此端口来确定当前记录是否健康。

-
默认超时时间 -
- - -
-
默认连续上线次数 -
- - -
-

连续检测{{config.countUp}}N次成功后,认为当前记录是在线的。

-
默认连续下线次数 -
- - -
-

连续检测{{config.countDown}}N次失败后,认为当前记录是离线的。

-
-
-
` -}) - Vue.component("ns-route-ranges-box", { props: ["v-ranges"], data: function () { @@ -1216,274 +4588,6 @@ Vue.component("ns-route-ranges-box", { ` }) -Vue.component("ns-record-health-check-config-box", { - props:["value", "v-parent-config"], - data: function () { - let config = this.value - if (config == null) { - config = { - isOn: false, - port: 0, - timeoutSeconds: 0, - countUp: 0, - countDown: 0 - } - } - - let parentConfig = this.vParentConfig - - return { - config: config, - portString: config.port.toString(), - timeoutSecondsString: config.timeoutSeconds.toString(), - countUpString: config.countUp.toString(), - countDownString: config.countDown.toString(), - - portIsEditing: config.port > 0, - timeoutSecondsIsEditing: config.timeoutSeconds > 0, - countUpIsEditing: config.countUp > 0, - countDownIsEditing: config.countDown > 0, - - parentConfig: parentConfig - } - }, - watch: { - portString: function (value) { - let port = parseInt(value.toString()) - if (isNaN(port) || port > 65535 || port < 1) { - this.config.port = 0 - } else { - this.config.port = port - } - }, - timeoutSecondsString: function (value) { - let timeoutSeconds = parseInt(value.toString()) - if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) { - this.config.timeoutSeconds = 0 - } else { - this.config.timeoutSeconds = timeoutSeconds - } - }, - countUpString: function (value) { - let countUp = parseInt(value.toString()) - if (isNaN(countUp) || countUp > 1000 || countUp < 1) { - this.config.countUp = 0 - } else { - this.config.countUp = countUp - } - }, - countDownString: function (value) { - let countDown = parseInt(value.toString()) - if (isNaN(countDown) || countDown > 1000 || countDown < 1) { - this.config.countDown = 0 - } else { - this.config.countDown = countDown - } - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - -
启用当前记录健康检查 - -
检测端口 - - 默认{{parentConfig.port}} -   [修改] - -
- - -

通过尝试连接A/AAAA记录中的IP加此端口来确定当前记录是否健康。

-
-
超时时间 - - 默认{{parentConfig.timeoutSeconds}}秒 -   [修改] - -
- -
- - -
-
-
默认连续上线次数 - - 默认{{parentConfig.countUp}}次 -   [修改] - -
- -
- - -
-

连续检测{{config.countUp}}N次成功后,认为当前记录是在线的。

-
-
默认连续下线次数 - - 默认{{parentConfig.countDown}}次 -   [修改] - -
- -
- - -
-

连续检测{{config.countDown}}N次失败后,认为当前记录是离线的。

-
-
-
-
` -}) - -Vue.component("ns-create-records-table", { - props: ["v-types", "v-default-ttl"], - data: function () { - let types = this.vTypes - if (types == null) { - types = [] - } - - let defaultTTL = this.vDefaultTtl - if (defaultTTL != null) { - defaultTTL = parseInt(defaultTTL.toString()) - } - if (defaultTTL <= 0) { - defaultTTL = 600 - } - - return { - types: types, - defaultTTL: defaultTTL, - records: [ - { - name: "", - type: "A", - value: "", - routeCodes: [], - ttl: defaultTTL, - index: 0 - } - ], - lastIndex: 0, - isAddingRoutes: false // 是否正在添加线路 - } - }, - methods: { - add: function () { - this.records.push({ - name: "", - type: "A", - value: "", - routeCodes: [], - ttl: this.defaultTTL, - 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"], @@ -1515,54 +4619,243 @@ Vue.component("ns-route-selector", { ` }) -Vue.component("ns-access-log-box", { - props: ["v-access-log", "v-keyword"], +// 选择多个线路 +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 + that.supportChinaProvinceRoutes = resp.data.supportChinaProvinceRoutes + that.supportISPRoutes = resp.data.supportISPRoutes + that.supportWorldRegionRoutes = resp.data.supportWorldRegionRoutes + that.supportAgentRoutes = resp.data.supportAgentRoutes + that.publicCategories = resp.data.publicCategories + that.supportPublicRoutes = resp.data.supportPublicRoutes + + // provinces + let provinces = {} + if (resp.data.provinces != null && resp.data.provinces.length > 0) { + for (const province of resp.data.provinces) { + let countryCode = province.countryCode + if (typeof provinces[countryCode] == "undefined") { + provinces[countryCode] = [] + } + provinces[countryCode].push({ + name: province.name, + code: province.code + }) + } + } + that.provinces = provinces + }) + }, data: function () { - let accessLog = this.vAccessLog + let selectedRoutes = this.vRoutes + if (selectedRoutes == null) { + selectedRoutes = [] + } + + let inputName = this.name + if (typeof inputName != "string" || inputName.length == 0) { + inputName = "routeCodes" + } + return { - accessLog: accessLog + routeCode: "default", + inputName: inputName, + routes: [], + + + provinces: {}, // country code => [ province1, province2, ... ] + provinceRouteCode: "", + + isAdding: false, + routeType: "default", + selectedRoutes: selectedRoutes, + + supportChinaProvinceRoutes: false, + supportISPRoutes: false, + supportWorldRegionRoutes: false, + supportAgentRoutes: false, + supportPublicRoutes: false, + publicCategories: [] + } + }, + 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: { - showLog: function () { - let that = this - let requestId = this.accessLog.requestId - this.$parent.$children.forEach(function (v) { - if (v.deselect != null) { - v.deselect() - } - }) - this.select() + add: function () { + this.isAdding = true + this.routeType = "default" + this.routeCode = "default" + this.provinceRouteCode = "" + this.$emit("add") + }, + cancel: function () { + this.isAdding = false + this.$emit("cancel") + }, + confirm: function () { + if (this.routeCode.length == 0) { + return + } - teaweb.popup("/ns/clusters/accessLogs/viewPopup?requestId=" + requestId, { - width: "50em", - height: "24em", - onClose: function () { - that.deselect() + let that = this + + + // route + let selectedRoute = null + for (const route of this.routes) { + if (route.code == this.routeCode) { + selectedRoute = route + break } + } + + if (selectedRoute != null) { + // province route + if (this.provinceRouteCode.length > 0 && this.provinces[this.routeCode] != null) { + for (const province of this.provinces[this.routeCode]) { + if (province.code == this.provinceRouteCode) { + selectedRoute = { + name: selectedRoute.name + "-" + province.name, + code: province.code + } + break + } + } + } + + that.selectedRoutes.push(selectedRoute) + } + + this.$emit("change", this.selectedRoutes) + this.cancel() + }, + remove: function (index) { + this.selectedRoutes.$remove(index) + this.$emit("change", this.selectedRoutes) + } + } + , + template: `
+
+
+ + {{route.name}}   +
+
+
+
+ + + + + + + + + + + + + +
选择类型 * + +
选择线路 * + +
选择省/州 + +
+
+ +   取消 +
+
+ +

由于套餐限制,当前用户只能使用部分线路。

+
` +}) + +Vue.component("pay-method-selector", { + mounted: function () { + this.$emit("change", this.currentMethodCode) + + let that = this + Tea.action("/finance/methodOptions") + .success(function (resp) { + that.isLoading = false + that.balance = resp.data.balance + that.methods = resp.data.methods + that.canPay = resp.data.enablePay }) - }, - select: function () { - this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)" - }, - deselect: function () { - this.$refs.box.parentNode.style.cssText = "" + .post() + }, + data: function () { + return { + isLoading: true, + canPay: true, + balance: 0, + methods: [], + currentMethodCode: "@balance" } }, - template: `
- [{{accessLog.region}}] {{accessLog.remoteAddr}} [{{accessLog.timeLocal}}] [{{accessLog.networking}}] {{accessLog.questionType}} {{accessLog.questionName}} -> - - {{accessLog.recordType}} {{accessLog.recordValue}} -  [没有记录] - - -
- 线路: {{route.name}} - 递归DNS + methods: { + selectMethod: function (method) { + this.currentMethodCode = method.code + this.$emit("change", method.code) + } + }, + template: `
+
+
+ 余额支付 ({{balance}}元) +

使用余额支付

-
- 错误:[{{accessLog.error}}] +
+ {{method.name}} +

{{method.description}}

+
+ +
+ 暂时不支持线上支付,请联系客服购买。 +
+ +
` +}) + +Vue.component("plan-bandwidth-limit-view", { + props: ["value"], + template: `
+ 带宽限制:
` }) @@ -1643,1363 +4936,1161 @@ Vue.component("plan-price-view", {
` }) -Vue.component("plan-bandwidth-limit-view", { - props: ["value"], - template: `
- 带宽限制: -
` -}) - -Vue.component("pay-method-selector", { - mounted: function () { - this.$emit("change", this.currentMethodCode) - - let that = this - Tea.action("/finance/methodOptions") - .success(function (resp) { - that.isLoading = false - that.balance = resp.data.balance - that.methods = resp.data.methods - that.canPay = resp.data.enablePay - }) - .post() - }, +Vue.component("cache-cond-box", { data: function () { return { - isLoading: true, - canPay: true, - balance: 0, - methods: [], - currentMethodCode: "@balance" + conds: [], + addingExt: false, + addingPath: false, + + extDuration: null, + pathDuration: null } }, methods: { - selectMethod: function (method) { - this.currentMethodCode = method.code - this.$emit("change", method.code) - } - }, - template: `
-
-
- 余额支付 ({{balance}}元) -

使用余额支付

-
-
- {{method.name}} -

{{method.description}}

-
-
+ addExt: function () { + this.addingExt = !this.addingExt + this.addingPath = false -
- 暂时不支持线上支付,请联系客服购买。 -
- -
` -}) - -Vue.component("http-stat-config-box", { - props: ["v-stat-config", "v-is-location"], - data: function () { - let stat = this.vStatConfig - if (stat == null) { - stat = { - isPrior: false, - isOn: false + if (this.addingExt) { + let that = this + setTimeout(function () { + if (that.$refs.extInput != null) { + that.$refs.extInput.focus() + } + }) } - } - 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 + changeExtDuration: function (duration) { + this.extDuration = duration + }, + confirmExt: function () { + let value = this.$refs.extInput.value + if (value.length == 0) { + return + } - let that = this - teaweb.popup("/servers/server/settings/conds/addGroupPopup", { - height: "30em", - callback: function (resp) { - that.conds.groups.push(resp.data.group) - that.change() + let exts = [] + let pieces = value.split(/[,,]/) + pieces.forEach(function (v) { + v = v.trim() + v = v.replace(/\s+/, "") + if (v.length > 0) { + if (v[0] != ".") { + v = "." + v + } + exts.push(v) } }) - }, - 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() - } + + this.conds.push({ + type: "url-extension", + value: JSON.stringify(exts), + duration: this.extDuration }) + this.$refs.extInput.value = "" + this.cancel() }, - 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; + addPath: function () { + this.addingExt = false + this.addingPath = !this.addingPath + + if (this.addingPath) { + let that = this + setTimeout(function () { + if (that.$refs.pathInput != null) { + that.$refs.pathInput.focus() + } + }) } - return cond.param + " " + cond.operator + }, + changePathDuration: function (duration) { + this.pathDuration = duration + }, + confirmPath: function () { + let value = this.$refs.pathInput.value + if (value.length == 0) { + return + } + + if (value[0] != "/") { + value = "/" + value + } + + this.conds.push({ + type: "url-prefix", + value: value, + duration: this.pathDuration + }) + this.$refs.pathInput.value = "" + this.cancel() + }, + remove: function (index) { + this.conds.$remove(index) + }, + cancel: function () { + this.addingExt = false + this.addingPath = false } }, template: `
- -
- - - - - - -
分组{{groupIndex+1}} - - - {{cond.param}} {{cond.operator}} - {{typeName(cond)}}: - {{cond.value}} - - - - {{group.connector}}   - - - -
-
+ +
+
+ 扩展名 + 路径:{{cond.value}}   ()   +
- - - - - - - -
分组之间关系 - -

- 只要满足其中一个条件分组即可。 - 需要满足所有条件分组。 -

-
- -
- +
+
+ + +
+
+
+ +
+
+ +
+
+ 确定   +
-
+
+ + +
+
+
+ +
+
+ +
+
+ 确定   +
+
+
+ +
+   + +
` }) -Vue.component("ssl-config-box", { - props: [ - "v-ssl-policy", - "v-protocol", - "v-server-id", - "v-support-http3" - ], - created: function () { - let that = this - setTimeout(function () { - that.sortableCipherSuites() - }, 100) - }, +// 域名列表 +Vue.component("domains-box", { + props: ["v-domains", "name", "v-support-wildcard"], data: function () { - let policy = this.vSslPolicy - if (policy == null) { - policy = { - id: 0, - isOn: true, - certRefs: [], - certs: [], - clientCARefs: [], - clientCACerts: [], - clientAuthType: 0, - minVersion: "TLS 1.1", - hsts: null, - cipherSuitesIsOn: false, - cipherSuites: [], - http2Enabled: true, - http3Enabled: false, - ocspIsOn: false - } - } else { - if (policy.certRefs == null) { - policy.certRefs = [] - } - if (policy.certs == null) { - policy.certs = [] - } - if (policy.clientCARefs == null) { - policy.clientCARefs = [] - } - if (policy.clientCACerts == null) { - policy.clientCACerts = [] - } - if (policy.cipherSuites == null) { - policy.cipherSuites = [] - } + let domains = this.vDomains + if (domains == null) { + domains = [] } - let hsts = policy.hsts - let hstsMaxAgeString = "31536000" - if (hsts == null) { - hsts = { - isOn: false, - maxAge: 31536000, - includeSubDomains: false, - preload: false, - domains: [] - } + let realName = "domainsJSON" + if (this.name != null && typeof this.name == "string") { + realName = this.name } - if (hsts.maxAge != null) { - hstsMaxAgeString = hsts.maxAge.toString() + + let supportWildcard = true + if (typeof this.vSupportWildcard == "boolean") { + supportWildcard = this.vSupportWildcard } return { - policy: policy, + domains: domains, - // hsts - hsts: hsts, - hstsOptionsVisible: false, - hstsDomainAdding: false, - hstsMaxAgeString: hstsMaxAgeString, - addingHstsDomain: "", - hstsDomainEditingIndex: -1, + mode: "single", // single | batch + batchDomains: "", - // 相关数据 - 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, + isAdding: false, + addingDomain: "", - // 高级选项 - moreOptionsVisible: false + isEditing: false, + editingIndex: -1, + + realName: realName, + supportWildcard: supportWildcard } }, watch: { - hsts: { - deep: true, - handler: function () { - this.policy.hsts = this.hsts - } - } - }, - methods: { - // 删除证书 - removeCert: function (index) { - let that = this - teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { - that.policy.certRefs.$remove(index) - that.policy.certs.$remove(index) - }) - }, - - // 选择证书 - selectCert: function () { - let that = this - let selectedCertIds = [] - if (this.policy != null && this.policy.certs.length > 0) { - this.policy.certs.forEach(function (cert) { - selectedCertIds.push(cert.id.toString()) - }) - } - let serverId = this.vServerId - if (serverId == null) { - serverId = 0 - } - teaweb.popup("/servers/certs/selectPopup?selectedCertIds=" + selectedCertIds + "&serverId=" + serverId, { - width: "50em", - height: "30em", - callback: function (resp) { - if (resp.data.cert != null && resp.data.certRef != null) { - that.policy.certRefs.push(resp.data.certRef) - that.policy.certs.push(resp.data.cert) - } - if (resp.data.certs != null && resp.data.certRefs != null) { - that.policy.certRefs.$pushAll(resp.data.certRefs) - that.policy.certs.$pushAll(resp.data.certs) - } - that.$forceUpdate() - } - }) - }, - - // 上传证书 - uploadCert: function () { - let that = this - let serverId = this.vServerId - if (typeof serverId != "number" && typeof serverId != "string") { - serverId = 0 - } - teaweb.popup("/servers/certs/uploadPopup?serverId=" + serverId, { - height: "30em", - callback: function (resp) { - teaweb.success("上传成功", function () { - that.policy.certRefs.push(resp.data.certRef) - that.policy.certs.push(resp.data.cert) - }) - } - }) - }, - - // 批量上传 - uploadBatch: function () { - let that = this - let serverId = this.vServerId - if (typeof serverId != "number" && typeof serverId != "string") { - serverId = 0 - } - teaweb.popup("/servers/certs/uploadBatchPopup?serverId=" + serverId, { - callback: function (resp) { - if (resp.data.cert != null) { - that.policy.certRefs.push(resp.data.certRef) - that.policy.certs.push(resp.data.cert) - } - if (resp.data.certs != null) { - that.policy.certRefs.$pushAll(resp.data.certRefs) - that.policy.certs.$pushAll(resp.data.certs) - } - that.$forceUpdate() - } - }) - }, - - // 申请证书 - requestCert: function () { - // 已经在证书中的域名 - let excludeServerNames = [] - if (this.policy != null && this.policy.certs.length > 0) { - this.policy.certs.forEach(function (cert) { - excludeServerNames.$pushAll(cert.dnsNames) - }) - } - - let that = this - teaweb.popup("/servers/server/settings/https/requestCertPopup?serverId=" + this.vServerId + "&excludeServerNames=" + excludeServerNames.join(","), { - callback: function () { - that.policy.certRefs.push(resp.data.certRef) - that.policy.certs.push(resp.data.cert) - } - }) - }, - - // 更多选项 - changeOptionsVisible: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - }, - - // 格式化时间 - formatTime: function (timestamp) { - return new Date(timestamp * 1000).format("Y-m-d") - }, - - // 格式化加密套件 - formatCipherSuite: function (cipherSuite) { - return cipherSuite.replace(/(AES|3DES)/, "$1") - }, - - // 添加单个套件 - addCipherSuite: function (cipherSuite) { - if (!this.policy.cipherSuites.$contains(cipherSuite)) { - this.policy.cipherSuites.push(cipherSuite) - } - this.allCipherSuites.$removeValue(cipherSuite) - }, - - // 删除单个套件 - removeCipherSuite: function (cipherSuite) { - let that = this - teaweb.confirm("确定要删除此套件吗?", function () { - that.policy.cipherSuites.$removeValue(cipherSuite) - that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$findAll(function (k, v) { - return !that.policy.cipherSuites.$contains(v) - }) - }) - }, - - // 清除所选套件 - clearCipherSuites: function () { - let that = this - teaweb.confirm("确定要清除所有已选套件吗?", function () { - that.policy.cipherSuites = [] - that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$copy() - }) - }, - - // 批量添加套件 - addBatchCipherSuites: function (suites) { - var that = this - teaweb.confirm("确定要批量添加套件?", function () { - suites.$each(function (k, v) { - if (that.policy.cipherSuites.$contains(v)) { - return - } - that.policy.cipherSuites.push(v) - }) - }) - }, - - /** - * 套件拖动排序 - */ - sortableCipherSuites: function () { - var box = document.querySelector(".cipher-suites-box") - Sortable.create(box, { - draggable: ".label", - handle: ".icon.handle", - onStart: function () { - - }, - onUpdate: function (event) { - - } - }) - }, - - // 显示所有套件 - showAllCipherSuites: function () { - this.cipherSuitesVisible = !this.cipherSuitesVisible - }, - - // 显示HSTS更多选项 - showMoreHSTS: function () { - this.hstsOptionsVisible = !this.hstsOptionsVisible; - if (this.hstsOptionsVisible) { - this.changeHSTSMaxAge() + vSupportWildcard: function (v) { + if (typeof v == "boolean") { + this.supportWildcard = v } }, - - // 监控HSTS有效期修改 - changeHSTSMaxAge: function () { - var v = parseInt(this.hstsMaxAgeString) - if (isNaN(v) || v < 0) { - this.hsts.maxAge = 0 - this.hsts.days = "-" - return - } - this.hsts.maxAge = v - this.hsts.days = v / 86400 - if (this.hsts.days == 0) { - this.hsts.days = "-" - } - }, - - // 设置HSTS有效期 - setHSTSMaxAge: function (maxAge) { - this.hstsMaxAgeString = maxAge.toString() - this.changeHSTSMaxAge() - }, - - // 添加HSTS域名 - addHstsDomain: function () { - this.hstsDomainAdding = true - this.hstsDomainEditingIndex = -1 + mode: function (mode) { let that = this setTimeout(function () { - that.$refs.addingHstsDomain.focus() + if (mode == "single") { + if (that.$refs.addingDomain != null) { + that.$refs.addingDomain.focus() + } + } else if (mode == "batch") { + if (that.$refs.batchDomains != null) { + that.$refs.batchDomains.focus() + } + } }, 100) - }, - - // 修改HSTS域名 - editHstsDomain: function (index) { - this.hstsDomainEditingIndex = index - this.addingHstsDomain = this.hsts.domains[index] - this.hstsDomainAdding = true - let that = this - setTimeout(function () { - that.$refs.addingHstsDomain.focus() - }, 100) - }, - - // 确认HSTS域名添加 - confirmAddHstsDomain: function () { - this.addingHstsDomain = this.addingHstsDomain.trim() - if (this.addingHstsDomain.length == 0) { - return; - } - if (this.hstsDomainEditingIndex > -1) { - this.hsts.domains[this.hstsDomainEditingIndex] = this.addingHstsDomain - } else { - this.hsts.domains.push(this.addingHstsDomain) - } - this.cancelHstsDomainAdding() - }, - - // 取消HSTS域名添加 - cancelHstsDomainAdding: function () { - this.hstsDomainAdding = false - this.addingHstsDomain = "" - this.hstsDomainEditingIndex = -1 - }, - - // 删除HSTS域名 - removeHstsDomain: function (index) { - this.cancelHstsDomainAdding() - this.hsts.domains.$remove(index) - }, - - // 选择客户端CA证书 - selectClientCACert: function () { - let that = this - teaweb.popup("/servers/certs/selectPopup?isCA=1", { - width: "50em", - height: "30em", - callback: function (resp) { - if (resp.data.cert != null && resp.data.certRef != null) { - that.policy.clientCARefs.push(resp.data.certRef) - that.policy.clientCACerts.push(resp.data.cert) - } - if (resp.data.certs != null && resp.data.certRefs != null) { - that.policy.clientCARefs.$pushAll(resp.data.certRefs) - that.policy.clientCACerts.$pushAll(resp.data.certs) - } - that.$forceUpdate() - } - }) - }, - - // 上传CA证书 - uploadClientCACert: function () { - let that = this - teaweb.popup("/servers/certs/uploadPopup?isCA=1", { - height: "28em", - callback: function (resp) { - teaweb.success("上传成功", function () { - that.policy.clientCARefs.push(resp.data.certRef) - that.policy.clientCACerts.push(resp.data.cert) - }) - } - }) - }, - - // 删除客户端CA证书 - removeClientCACert: function (index) { - let that = this - teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { - that.policy.clientCARefs.$remove(index) - that.policy.clientCACerts.$remove(index) - }) - } - }, - template: `
-

SSL/TLS相关配置

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用HTTP/2 -
- - -
-
启用HTTP/3 -
- - -
-
设置证书 -
-
- {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   -
-
-
-
- 选择或上传证书后HTTPSTLS服务才能生效。 -
-
-   - |   -   -   - |   - -
TLS最低版本 - -
加密算法套件(CipherSuites) -
- - -
-
-
-
- 已添加套件({{policy.cipherSuites.length}}): -
- -   - -
-
- - - -

点击可选套件添加。

-
-
开启HSTS -
- - -
-

- 开启后,会自动在响应Header中加入 - Strict-Transport-Security: - ... - max-age={{hsts.maxAge}} - ; includeSubDomains - ; preload - - - 修改 - -

-
HSTS有效时间(max-age) -
-
- -
-
- 秒 -
-
{{hsts.days}}天
-
-

- [1年/365天]     - [6个月/182.5天]     - [1个月/30天] -

-
HSTS包含子域名(includeSubDomains) -
- - -
-
HSTS预加载(preload) -
- - -
-
HSTS生效的域名 -
- {{domain}} -   - - - -
-
-
- -
-
- -   取消 -
-
-
- -
-

如果没有设置域名的话,则默认支持所有的域名。

-
OCSP Stapling -

选中表示启用OCSP Stapling。

-
客户端认证方式 - -
客户端认证CA证书 -
-
- {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   -
-
-
-   - -

用来校验客户端证书以增强安全性,通常不需要设置。

-
-
-
` -}) - -// Action列表 -Vue.component("http-firewall-actions-view", { - props: ["v-actions"], - template: `
-
- {{action.name}} ({{action.code.toUpperCase()}}) -
- [{{action.options.status}}] - - [分组] - [网站] - [网站和策略] - - - 黑名单 - 白名单 - 灰名单 - -
-
-
-
` -}) - -// 显示WAF规则的标签 -Vue.component("http-firewall-rule-label", { - props: ["v-rule"], - data: function () { - return { - rule: this.vRule - } - }, - methods: { - showErr: function (err) { - teaweb.popupTip("规则校验错误,请修正:" + teaweb.encodeHTML(err) + "") - }, - calculateParamName: function (param) { - let paramName = "" - if (param != null) { - window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { - if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { - paramName = checkpoint.name - } - }) - } - return paramName - }, - calculateParamDescription: function (param) { - let paramName = "" - let paramDescription = "" - if (param != null) { - window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { - if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { - paramName = checkpoint.name - paramDescription = checkpoint.description - } - }) - } - return paramName + ": " + paramDescription - }, - operatorName: function (operatorCode) { - let operatorName = operatorCode - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorName = v.name - } - }) - } - - return operatorName - }, - operatorDescription: function (operatorCode) { - let operatorName = operatorCode - let operatorDescription = "" - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorName = v.name - operatorDescription = v.description - } - }) - } - - return operatorName + ": " + operatorDescription - }, - operatorDataType: function (operatorCode) { - let operatorDataType = "none" - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorDataType = v.dataType - } - }) - } - - return operatorDataType - }, - isEmptyString: function (v) { - return typeof v == "string" && v.length == 0 - } - }, - template: `
-
- {{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} - - - - {{rule.checkpointOptions.period}}秒内请求数 - - - - - 允许{{rule.checkpointOptions.allowDomains}} - 禁止{{rule.checkpointOptions.denyDomains}} - - - - | {{paramFilter.code}} - <{{operatorName(rule.operator)}}> - {{rule.value}} - [空] - - - - ({{rule.description}}) - - 规则错误 -
-
` -}) - -// 缓存条件列表 -Vue.component("http-cache-refs-box", { - props: ["v-cache-refs"], - data: function () { - let refs = this.vCacheRefs - if (refs == null) { - refs = [] - } - return { - refs: refs - } - }, - methods: { - timeUnitName: function (unit) { - switch (unit) { - case "ms": - return "毫秒" - case "second": - return "秒" - case "minute": - return "分钟" - case "hour": - return "小时" - case "day": - return "天" - case "week": - return "周 " - } - return unit - } - }, - template: `
- - -

暂时还没有缓存条件。

-
- - - - - - - - - - - -
缓存条件缓存时间
- - - - - 忽略URI参数 - - {{cacheRef.minSize.count}}{{cacheRef.minSize.unit}} - - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} - - 0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} - {{cacheRef.methods.join(", ")}} - Expires - 状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}} - 分片缓存 - Range回源 - If-None-Match - If-Modified-Since - 支持异步 - - {{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}} - 不缓存 -
-
-
-
` -}) - -Vue.component("ssl-certs-box", { - props: [ - "v-certs", // 证书列表 - "v-cert", // 单个证书 - "v-protocol", // 协议:https|tls - "v-view-size", // 弹窗尺寸:normal, mini - "v-single-mode", // 单证书模式 - "v-description", // 描述文字 - "v-domains", // 搜索的域名列表或者函数 - "v-user-id" // 用户ID - ], - data: function () { - let certs = this.vCerts - if (certs == null) { - certs = [] - } - if (this.vCert != null) { - certs.push(this.vCert) - } - - let description = this.vDescription - if (description == null || typeof (description) != "string") { - description = "" - } - - return { - certs: certs, - description: description - } - }, - methods: { - certIds: function () { - return this.certs.map(function (v) { - return v.id - }) - }, - // 删除证书 - removeCert: function (index) { - let that = this - teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { - that.certs.$remove(index) - }) - }, - - // 选择证书 - selectCert: function () { - let that = this - let width = "54em" - let height = "32em" - let viewSize = this.vViewSize - if (viewSize == null) { - viewSize = "normal" - } - if (viewSize == "mini") { - width = "35em" - height = "20em" - } - - let searchingDomains = [] - if (this.vDomains != null) { - if (typeof this.vDomains == "function") { - let resultDomains = this.vDomains() - if (resultDomains != null && typeof resultDomains == "object" && (resultDomains instanceof Array)) { - searchingDomains = resultDomains - } - } else if (typeof this.vDomains == "object" && (this.vDomains instanceof Array)) { - searchingDomains = this.vDomains - } - if (searchingDomains.length > 10000) { - searchingDomains = searchingDomains.slice(0, 10000) - } - } - - let selectedCertIds = this.certs.map(function (cert) { - return cert.id - }) - let userId = this.vUserId - if (userId == null) { - userId = 0 - } - - teaweb.popup("/servers/certs/selectPopup?viewSize=" + viewSize + "&searchingDomains=" + window.encodeURIComponent(searchingDomains.join(",")) + "&selectedCertIds=" + selectedCertIds.join(",") + "&userId=" + userId, { - width: width, - height: height, - callback: function (resp) { - if (resp.data.cert != null) { - that.certs.push(resp.data.cert) - } - if (resp.data.certs != null) { - that.certs.$pushAll(resp.data.certs) - } - that.$forceUpdate() - } - }) - }, - - // 上传证书 - uploadCert: function () { - let that = this - let userId = this.vUserId - if (typeof userId != "number" && typeof userId != "string") { - userId = 0 - } - teaweb.popup("/servers/certs/uploadPopup?userId=" + userId, { - height: "28em", - callback: function (resp) { - teaweb.success("上传成功", function () { - if (resp.data.cert != null) { - that.certs.push(resp.data.cert) - } - if (resp.data.certs != null) { - that.certs.$pushAll(resp.data.certs) - } - that.$forceUpdate() - }) - } - }) - }, - - // 批量上传 - uploadBatch: function () { - let that = this - let userId = this.vUserId - if (typeof userId != "number" && typeof userId != "string") { - userId = 0 - } - teaweb.popup("/servers/certs/uploadBatchPopup?userId=" + userId, { - callback: function (resp) { - if (resp.data.cert != null) { - that.certs.push(resp.data.cert) - } - if (resp.data.certs != null) { - that.certs.$pushAll(resp.data.certs) - } - that.$forceUpdate() - } - }) - }, - - // 格式化时间 - formatTime: function (timestamp) { - return new Date(timestamp * 1000).format("Y-m-d") - }, - - // 判断是否显示选择|上传按钮 - buttonsVisible: function () { - return this.vSingleMode == null || !this.vSingleMode || this.certs == null || this.certs.length == 0 - } - }, - template: `
- -
-
- {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   -
-
-
-
- 选择或上传证书后HTTPSTLS服务才能生效。 - {{description}} -
-
-
-   - |   -   -   -
-
` -}) - -Vue.component("http-host-redirect-box", { - props: ["v-redirects"], - mounted: function () { - let that = this - sortTable(function (ids) { - let newRedirects = [] - ids.forEach(function (id) { - that.redirects.forEach(function (redirect) { - if (redirect.id == id) { - newRedirects.push(redirect) - } - }) - }) - that.updateRedirects(newRedirects) - }) - }, - data: function () { - let redirects = this.vRedirects - if (redirects == null) { - redirects = [] - } - - let id = 0 - redirects.forEach(function (v) { - id++ - v.id = id - }) - - return { - redirects: redirects, - statusOptions: [ - {"code": 301, "text": "Moved Permanently"}, - {"code": 308, "text": "Permanent Redirect"}, - {"code": 302, "text": "Found"}, - {"code": 303, "text": "See Other"}, - {"code": 307, "text": "Temporary Redirect"} - ], - id: id } }, methods: { add: function () { + this.isAdding = true let that = this - window.UPDATING_REDIRECT = null - - teaweb.popup("/servers/server/settings/redirects/createPopup", { - width: "50em", - height: "36em", - callback: function (resp) { - that.id++ - resp.data.redirect.id = that.id - that.redirects.push(resp.data.redirect) - that.change() - } - }) - }, - update: function (index, redirect) { - let that = this - window.UPDATING_REDIRECT = redirect - - teaweb.popup("/servers/server/settings/redirects/createPopup", { - width: "50em", - height: "36em", - callback: function (resp) { - resp.data.redirect.id = redirect.id - Vue.set(that.redirects, index, resp.data.redirect) - that.change() - } - }) - }, - remove: function (index) { - let that = this - teaweb.confirm("确定要删除这条跳转规则吗?", function () { - that.redirects.$remove(index) - that.change() - }) - }, - change: function () { - let that = this - setTimeout(function (){ - that.$emit("change", that.redirects) + setTimeout(function () { + that.$refs.addingDomain.focus() }, 100) }, - updateRedirects: function (newRedirects) { - this.redirects = newRedirects + confirm: function () { + if (this.mode == "batch") { + this.confirmBatch() + return + } + + let that = this + + // 删除其中的空格 + this.addingDomain = this.addingDomain.replace(/\s/g, "") + + if (this.addingDomain.length == 0) { + teaweb.warn("请输入要添加的域名", function () { + that.$refs.addingDomain.focus() + }) + return + } + + // 基本校验 + if (this.supportWildcard) { + if (this.addingDomain[0] == "~") { + let expr = this.addingDomain.substring(1) + try { + new RegExp(expr) + } catch (e) { + teaweb.warn("正则表达式错误:" + e.message, function () { + that.$refs.addingDomain.focus() + }) + return + } + } + } else { + if (/[*~^]/.test(this.addingDomain)) { + teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () { + that.$refs.addingDomain.focus() + }) + return + } + } + + if (this.isEditing && this.editingIndex >= 0) { + this.domains[this.editingIndex] = this.addingDomain + } else { + // 分割逗号(,)、顿号(、) + if (this.addingDomain.match("[,、,;]")) { + let domainList = this.addingDomain.split(new RegExp("[,、,;]")) + domainList.forEach(function (v) { + if (v.length > 0) { + that.domains.push(v) + } + }) + } else { + this.domains.push(this.addingDomain) + } + } + this.cancel() this.change() + }, + confirmBatch: function () { + let domains = this.batchDomains.split("\n") + let realDomains = [] + let that = this + let hasProblems = false + domains.forEach(function (domain) { + if (hasProblems) { + return + } + if (domain.length == 0) { + return + } + if (that.supportWildcard) { + if (domain == "~") { + let expr = domain.substring(1) + try { + new RegExp(expr) + } catch (e) { + hasProblems = true + teaweb.warn("正则表达式错误:" + e.message, function () { + that.$refs.batchDomains.focus() + }) + return + } + } + } else { + if (/[*~^]/.test(domain)) { + hasProblems = true + teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () { + that.$refs.batchDomains.focus() + }) + return + } + } + realDomains.push(domain) + }) + if (hasProblems) { + return + } + if (realDomains.length == 0) { + teaweb.warn("请输入要添加的域名", function () { + that.$refs.batchDomains.focus() + }) + return + } + + realDomains.forEach(function (domain) { + that.domains.push(domain) + }) + this.cancel() + this.change() + }, + edit: function (index) { + this.addingDomain = this.domains[index] + this.isEditing = true + this.editingIndex = index + let that = this + setTimeout(function () { + that.$refs.addingDomain.focus() + }, 50) + }, + remove: function (index) { + this.domains.$remove(index) + this.change() + }, + cancel: function () { + this.isAdding = false + this.mode = "single" + this.batchDomains = "" + this.isEditing = false + this.editingIndex = -1 + this.addingDomain = "" + }, + change: function () { + this.$emit("change", this.domains) } }, template: `
- - - - [创建] - -
+ +
+ + [正则] + [后缀] + [泛域名] + {{domain}} + +   +   + + +   +   + + +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+
+ +   +
+
+

支持普通域名(example.com)、泛域名(*.example.com、域名后缀(以点号开头,如.example.com)和正则表达式(以波浪号开头,如~.*.example.com;如果域名后有端口,请加上端口号。

+

只支持普通域名(example.comwww.example.com)。

+
+
+
+ +
+
` +}) -

暂时还没有URL跳转规则。

-
- - - - - - - - - - - - - - - - + + + + + + + + + + +
跳转前跳转后HTTP状态码状态操作
-
- {{redirect.beforeURL}} -
- URL跳转 - 匹配前缀 - 正则匹配 - 精准匹配 - 排除:{{domain}} - 仅限:{{domain}} -
+Vue.component("http-access-log-box", { + props: ["v-access-log"], + 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" + } + } + 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: "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.requestMethod}} {{accessLog.scheme}}://{{accessLog.host}}{{accessLog.requestURI}} {{accessLog.proto}}" {{accessLog.status}} [cached] waf {{accessLog.firewallActions}} - {{tag}} - 耗时:{{formatCost(accessLog.requestTime)}} ms +   +
` +}) + +Vue.component("http-access-log-config-box", { + props: ["v-access-log-config", "v-fields", "v-default-field-codes", "v-access-log-policies", "v-is-location", "v-is-group"], + data: function () { + let that = this + + // 初始化 + setTimeout(function () { + that.changeFields() + that.changePolicy() + }, 100) + + let accessLog = { + isPrior: false, + isOn: false, + fields: [], + status1: true, + status2: true, + status3: true, + status4: true, + status5: true, + + storageOnly: false, + storagePolicies: [], + + firewallOnly: 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) + } + }) + this.vAccessLogPolicies.forEach(function (v) { + v.isChecked = accessLog.storagePolicies.$contains(v.id) + }) + + return { + accessLog: accessLog, + showAdvancedOptions: false + } + }, + methods: { + changeFields: function () { + this.accessLog.fields = this.vFields.filter(function (v) { + return v.isChecked + }).map(function (v) { + return v.code + }) + }, + changePolicy: function () { + this.accessLog.storagePolicies = this.vAccessLogPolicies.filter(function (v) { + return v.isChecked + }).map(function (v) { + return v.id + }) + }, + changeAdvanced: function (v) { + this.showAdvancedOptions = v + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - -
启用访问日志 +
+ + +
+
要存储的访问日志字段 +
+ + +
+
要存储的访问日志状态码 +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
选择输出的日志策略 + 暂时还没有缓存策略。 +
+
+ +
-
- 所有域名 - - {{redirect.domainsBefore[0]}} - {{redirect.domainsBefore[0]}}等{{redirect.domainsBefore.length}}个域名 - -
- 域名跳转 - {{redirect.domainAfterScheme}} - 忽略端口 -
-
-
- 所有端口 - - {{redirect.portsBefore.join(", ")}} - {{redirect.portsBefore.slice(0, 5).join(", ")}}等{{redirect.portsBefore.length}}个端口 - -
- 端口跳转 - {{redirect.portAfterScheme}} -
-
- -
- 匹配条件 -
-
-> - {{redirect.afterURL}} - {{redirect.domainAfter}} - {{redirect.portAfter}} - - {{redirect.status}} - 默认 - - 修改   - 删除 -
-

所有规则匹配顺序为从上到下,可以拖动左侧的排序。

+
+
是否只输出到日志策略 +
+ + +
+

选中表示只输出日志到日志策略,而停止默认的日志存储。

+
只记录WAF相关日志 + +

选中后只记录WAF相关的日志。

+
+
+
` +}) + +// 访问日志搜索框 +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("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-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("http-cache-config-box", { + props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy", "v-web-id"], + data: function () { + let cacheConfig = this.vCacheConfig + if (cacheConfig == null) { + cacheConfig = { + isPrior: false, + isOn: false, + addStatusHeader: true, + addAgeHeader: false, + enableCacheControlMaxAge: false, + cacheRefs: [], + purgeIsOn: false, + purgeKey: "", + disablePolicyRefs: false + } + } + if (cacheConfig.cacheRefs == null) { + cacheConfig.cacheRefs = [] + } + + let maxBytes = null + if (this.vCachePolicy != null && this.vCachePolicy.maxBytes != null) { + maxBytes = this.vCachePolicy.maxBytes + } + + // key + if (cacheConfig.key == null) { + // use Vue.set to activate vue events + Vue.set(cacheConfig, "key", { + isOn: false, + scheme: "https", + host: "" + }) + } + + return { + cacheConfig: cacheConfig, + moreOptionsVisible: false, + enablePolicyRefs: !cacheConfig.disablePolicyRefs, + maxBytes: maxBytes, + + searchBoxVisible: false, + searchKeyword: "", + + keyOptionsVisible: false + } + }, + watch: { + enablePolicyRefs: function (v) { + this.cacheConfig.disablePolicyRefs = !v + }, + searchKeyword: function (v) { + this.$refs.cacheRefsConfigBoxRef.search(v) + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.cacheConfig.isPrior) && this.cacheConfig.isOn + }, + isPlus: function () { + return true + }, + generatePurgeKey: function () { + let r = Math.random().toString() + Math.random().toString() + let s = r.replace(/0\./g, "") + .replace(/\./g, "") + let result = "" + for (let i = 0; i < s.length; i++) { + result += String.fromCharCode(parseInt(s.substring(i, i + 1)) + ((Math.random() < 0.5) ? "a" : "A").charCodeAt(0)) + } + this.cacheConfig.purgeKey = result + }, + showMoreOptions: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + }, + changeStale: function (stale) { + this.cacheConfig.stale = stale + }, + showSearchBox: function () { + this.searchBoxVisible = !this.searchBoxVisible + if (this.searchBoxVisible) { + let that = this + setTimeout(function () { + that.$refs.searchBox.focus() + }) + } else { + this.searchKeyword = "" + } + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用缓存 +
+ + +
+
缓存主域名 +
默认   [修改]
+
使用主域名:{{cacheConfig.key.scheme}}://{{cacheConfig.key.host}}   [修改]
+
+
+ + + + + + + + + +
启用主域名 +

启用主域名后,所有缓存键值中的协议和域名部分都会修改为主域名,用来实现缓存不区分域名。

+
主域名 * +
+
+ +
+
+ +
+
+

此域名必须是当前网站已绑定域名,在刷新缓存时也需要使用此域名。

+
+ +
+
+ 收起选项更多选项 +
使用默认缓存条件 + +

选中后使用系统中已经定义的默认缓存条件。

+
添加X-Cache报头 + +

选中后自动在响应报头中增加X-Cache: BYPASS|MISS|HIT|PURGE;在浏览器端查看X-Cache值时请先禁用浏览器缓存,避免影响观察。

+
添加Age Header + +

选中后自动在响应Header中增加Age: [存活时间秒数]

+
支持源站控制有效时间 + +

选中后表示支持源站在Header中设置的Cache-Control: max-age=[有效时间秒数]

+
允许PURGE + +

允许使用PURGE方法清除某个URL缓存。

+
PURGE Key * + +

[随机生成]。需要在PURGE方法调用时加入Edge-Purge-Key: {{cacheConfig.purgeKey}} Header。只能包含字符、数字、下划线。

+
+ + +
+ +
+
+ +
+

缓存条件   [添加]   [搜索] +
+ + +
+

+
` @@ -3366,1115 +6457,81 @@ Vue.component("http-cache-ref-box", { ` }) -// 请求限制 -Vue.component("http-request-limit-config-box", { - props: ["v-request-limit-config", "v-is-group", "v-is-location"], +// 缓存条件列表 +Vue.component("http-cache-refs-box", { + props: ["v-cache-refs"], 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" - } - } + let refs = this.vCacheRefs + if (refs == null) { + refs = [] } 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 - } + refs: refs } }, 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 + timeUnitName: function (unit) { + switch (unit) { + case "ms": + return "毫秒" + case "second": + return "秒" + case "minute": + return "分钟" + case "hour": + return "小时" + case "day": + return "天" + case "week": + return "周 " } - - this.values.push(this.addingValue) - this.cancel() - }, - cancel: function () { - this.isAdding = false - this.addingValue = {"pattern": "", "replacement": "", "isCaseInsensitive": false, "isRegexp": false} + return unit } }, template: `
- -
-
- {{value.pattern}} => {{value.replacement}}[空] - -
-
-
- - - - - - - - - - - - - + + +

暂时还没有缓存条件。

+
+
替换前内容 *
替换后内容
是否忽略大小写 - -
+ + + + + + + + + +
缓存条件缓存时间
+ + + + + 忽略URI参数 + + {{cacheRef.minSize.count}}{{cacheRef.minSize.unit}} + - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} + + 0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} + {{cacheRef.methods.join(", ")}} + Expires + 状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}} + 分片缓存 + Range回源 + If-None-Match + If-Modified-Since + 支持异步 + + {{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}} + 不缓存 +
- -
-   - -
-
-
- -
-
` -}) - -// 浏览条件列表 -Vue.component("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("cache-cond-box", { - data: function () { - return { - conds: [], - addingExt: false, - addingPath: false, - - extDuration: null, - pathDuration: null - } - }, - methods: { - addExt: function () { - this.addingExt = !this.addingExt - this.addingPath = false - - if (this.addingExt) { - let that = this - setTimeout(function () { - if (that.$refs.extInput != null) { - that.$refs.extInput.focus() - } - }) - } - }, - changeExtDuration: function (duration) { - this.extDuration = duration - }, - confirmExt: function () { - let value = this.$refs.extInput.value - if (value.length == 0) { - return - } - - let exts = [] - let pieces = value.split(/[,,]/) - pieces.forEach(function (v) { - v = v.trim() - v = v.replace(/\s+/, "") - if (v.length > 0) { - if (v[0] != ".") { - v = "." + v - } - exts.push(v) - } - }) - - this.conds.push({ - type: "url-extension", - value: JSON.stringify(exts), - duration: this.extDuration - }) - this.$refs.extInput.value = "" - this.cancel() - }, - addPath: function () { - this.addingExt = false - this.addingPath = !this.addingPath - - if (this.addingPath) { - let that = this - setTimeout(function () { - if (that.$refs.pathInput != null) { - that.$refs.pathInput.focus() - } - }) - } - }, - changePathDuration: function (duration) { - this.pathDuration = duration - }, - confirmPath: function () { - let value = this.$refs.pathInput.value - if (value.length == 0) { - return - } - - if (value[0] != "/") { - value = "/" + value - } - - this.conds.push({ - type: "url-prefix", - value: value, - duration: this.pathDuration - }) - this.$refs.pathInput.value = "" - this.cancel() - }, - remove: function (index) { - this.conds.$remove(index) - }, - cancel: function () { - this.addingExt = false - this.addingPath = false - } - }, - template: `
- -
-
- 扩展名 - 路径:{{cond.value}}   ()   - -
-
-
- - -
-
-
- -
-
- -
-
- 确定   -
-
-
- - -
-
-
- -
-
- -
-
- 确定   -
-
-
- -
-   - -
-
` -}) - -Vue.component("http-firewall-config-box", { - props: ["v-firewall-config", "v-is-location", "v-firewall-policy"], - data: function () { - let firewall = this.vFirewallConfig - if (firewall == null) { - firewall = { - isPrior: false, - isOn: false, - firewallPolicyId: 0, - ignoreGlobalRules: false, - defaultCaptchaType: "none" - } - } - - if (firewall.defaultCaptchaType == null || firewall.defaultCaptchaType.length == 0) { - firewall.defaultCaptchaType = "none" - } - - let allCaptchaTypes = window.WAF_CAPTCHA_TYPES.$copy() - - // geetest - let geeTestIsOn = false - if (this.vFirewallPolicy != null && this.vFirewallPolicy.captchaAction != null && this.vFirewallPolicy.captchaAction.geeTestConfig != null) { - geeTestIsOn = this.vFirewallPolicy.captchaAction.geeTestConfig.isOn - } - - // 如果没有启用geetest,则还原 - if (!geeTestIsOn && firewall.defaultCaptchaType == "geetest") { - firewall.defaultCaptchaType = "none" - } - - return { - firewall: firewall, - moreOptionsVisible: false, - execGlobalRules: !firewall.ignoreGlobalRules, - captchaTypes: allCaptchaTypes, - geeTestIsOn: geeTestIsOn - } - }, - watch: { - execGlobalRules: function (v) { - this.firewall.ignoreGlobalRules = !v - } - }, - methods: { - changeOptionsVisible: function (v) { - this.moreOptionsVisible = v - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - -
启用Web防火墙 -
- - -
-
人机识别验证方式 - -

使用系统默认的设置。你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

-

{{captchaType.description}}你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

-
启用系统全局规则 - -

选中后,表示使用系统全局WAF策略中定义的规则。

-
-
-
` -}) - -Vue.component("http-cache-config-box", { - props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy", "v-web-id"], - data: function () { - let cacheConfig = this.vCacheConfig - if (cacheConfig == null) { - cacheConfig = { - isPrior: false, - isOn: false, - addStatusHeader: true, - addAgeHeader: false, - enableCacheControlMaxAge: false, - cacheRefs: [], - purgeIsOn: false, - purgeKey: "", - disablePolicyRefs: false - } - } - if (cacheConfig.cacheRefs == null) { - cacheConfig.cacheRefs = [] - } - - let maxBytes = null - if (this.vCachePolicy != null && this.vCachePolicy.maxBytes != null) { - maxBytes = this.vCachePolicy.maxBytes - } - - // key - if (cacheConfig.key == null) { - // use Vue.set to activate vue events - Vue.set(cacheConfig, "key", { - isOn: false, - scheme: "https", - host: "" - }) - } - - return { - cacheConfig: cacheConfig, - moreOptionsVisible: false, - enablePolicyRefs: !cacheConfig.disablePolicyRefs, - maxBytes: maxBytes, - - searchBoxVisible: false, - searchKeyword: "", - - keyOptionsVisible: false - } - }, - watch: { - enablePolicyRefs: function (v) { - this.cacheConfig.disablePolicyRefs = !v - }, - searchKeyword: function (v) { - this.$refs.cacheRefsConfigBoxRef.search(v) - } - }, - methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.cacheConfig.isPrior) && this.cacheConfig.isOn - }, - isPlus: function () { - return true - }, - generatePurgeKey: function () { - let r = Math.random().toString() + Math.random().toString() - let s = r.replace(/0\./g, "") - .replace(/\./g, "") - let result = "" - for (let i = 0; i < s.length; i++) { - result += String.fromCharCode(parseInt(s.substring(i, i + 1)) + ((Math.random() < 0.5) ? "a" : "A").charCodeAt(0)) - } - this.cacheConfig.purgeKey = result - }, - showMoreOptions: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - }, - changeStale: function (stale) { - this.cacheConfig.stale = stale - }, - showSearchBox: function () { - this.searchBoxVisible = !this.searchBoxVisible - if (this.searchBoxVisible) { - let that = this - setTimeout(function () { - that.$refs.searchBox.focus() - }) - } else { - this.searchKeyword = "" - } - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用缓存 -
- - -
-
缓存主域名 -
默认   [修改]
-
使用主域名:{{cacheConfig.key.scheme}}://{{cacheConfig.key.host}}   [修改]
-
-
- - - - - - - - - -
启用主域名 -

启用主域名后,所有缓存键值中的协议和域名部分都会修改为主域名,用来实现缓存不区分域名。

-
主域名 * -
-
- -
-
- -
-
-

此域名必须是当前网站已绑定域名,在刷新缓存时也需要使用此域名。

-
- -
-
- 收起选项更多选项 -
使用默认缓存条件 - -

选中后使用系统中已经定义的默认缓存条件。

-
添加X-Cache报头 - -

选中后自动在响应报头中增加X-Cache: BYPASS|MISS|HIT|PURGE;在浏览器端查看X-Cache值时请先禁用浏览器缓存,避免影响观察。

-
添加Age Header - -

选中后自动在响应Header中增加Age: [存活时间秒数]

-
支持源站控制有效时间 - -

选中后表示支持源站在Header中设置的Cache-Control: max-age=[有效时间秒数]

-
允许PURGE - -

允许使用PURGE方法清除某个URL缓存。

-
PURGE Key * - -

[随机生成]。需要在PURGE方法调用时加入Edge-Purge-Key: {{cacheConfig.purgeKey}} Header。只能包含字符、数字、下划线。

-
- - -
- -
-
- -
-

缓存条件   [添加]   [搜索] -
- - -
-

-
` }) -// 通用Header长度 -let defaultGeneralHeaders = ["Cache-Control", "Connection", "Date", "Pragma", "Trailer", "Transfer-Encoding", "Upgrade", "Via", "Warning"] -Vue.component("http-cond-general-header-length", { - props: ["v-checkpoint"], - data: function () { - let headers = null - let length = null - - if (window.parent.UPDATING_RULE != null) { - let options = window.parent.UPDATING_RULE.checkpointOptions - if (options.headers != null && Array.$isArray(options.headers)) { - headers = options.headers - } - if (options.length != null) { - length = options.length - } - } - - - if (headers == null) { - headers = defaultGeneralHeaders - } - - if (length == null) { - length = 128 - } - - let that = this - setTimeout(function () { - that.change() - }, 100) - - return { - headers: headers, - length: length - } - }, - watch: { - length: function (v) { - let len = parseInt(v) - if (isNaN(len)) { - len = 0 - } - if (len < 0) { - len = 0 - } - this.length = len - this.change() - } - }, - methods: { - change: function () { - this.vCheckpoint.options = [ - { - code: "headers", - value: this.headers - }, - { - code: "length", - value: this.length - } - ] - } - }, - template: `
- - - - - - - - - -
通用Header列表 - -

需要检查的Header列表。

-
Header值超出长度 -
- - 字节 -
-

超出此长度认为匹配成功,0表示不限制。

-
-
` -}) - -// CC -Vue.component("http-firewall-checkpoint-cc", { - props: ["v-checkpoint"], - data: function () { - let keys = [] - let period = 60 - let threshold = 1000 - let ignoreCommonFiles = true - let enableFingerprint = true - - let options = {} - if (window.parent.UPDATING_RULE != null) { - options = window.parent.UPDATING_RULE.checkpointOptions - } - - if (options == null) { - options = {} - } - if (options.keys != null) { - keys = options.keys - } - if (keys.length == 0) { - keys = ["${remoteAddr}", "${requestPath}"] - } - if (options.period != null) { - period = options.period - } - if (options.threshold != null) { - threshold = options.threshold - } - if (options.ignoreCommonFiles != null && typeof (options.ignoreCommonFiles) == "boolean") { - ignoreCommonFiles = options.ignoreCommonFiles - } - if (options.enableFingerprint != null && typeof (options.enableFingerprint) == "boolean") { - enableFingerprint = options.enableFingerprint - } - - let that = this - setTimeout(function () { - that.change() - }, 100) - - return { - keys: keys, - period: period, - threshold: threshold, - ignoreCommonFiles: ignoreCommonFiles, - enableFingerprint: enableFingerprint, - options: {}, - value: threshold - } - }, - watch: { - period: function () { - this.change() - }, - threshold: function () { - this.change() - }, - ignoreCommonFiles: function () { - this.change() - }, - enableFingerprint: function () { - this.change() - } - }, - methods: { - changeKeys: function (keys) { - this.keys = keys - this.change() - }, - change: function () { - let period = parseInt(this.period.toString()) - if (isNaN(period) || period <= 0) { - period = 60 - } - - let threshold = parseInt(this.threshold.toString()) - if (isNaN(threshold) || threshold <= 0) { - threshold = 1000 - } - this.value = threshold - - let ignoreCommonFiles = this.ignoreCommonFiles - if (typeof ignoreCommonFiles != "boolean") { - ignoreCommonFiles = false - } - - let enableFingerprint = this.enableFingerprint - if (typeof enableFingerprint != "boolean") { - enableFingerprint = true - } - - this.vCheckpoint.options = [ - { - code: "keys", - value: this.keys - }, - { - code: "period", - value: period, - }, - { - code: "threshold", - value: threshold - }, - { - code: "ignoreCommonFiles", - value: ignoreCommonFiles - }, - { - code: "enableFingerprint", - value: enableFingerprint - } - ] - }, - thresholdTooLow: function () { - let threshold = parseInt(this.threshold.toString()) - if (isNaN(threshold) || threshold <= 0) { - threshold = 1000 - } - return threshold > 0 && threshold < 5 - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - -
统计对象组合 * - -
统计周期 * -
- - -
-
阈值 * - -

对于网站类应用来说,当前阈值设置的太低,有可能会影响用户正常访问。

-
检查请求来源指纹 - -

在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。

-
忽略常用文件 - -

忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。

-
-
` -}) - -// 防盗链 -Vue.component("http-firewall-checkpoint-referer-block", { - props: ["v-checkpoint"], - data: function () { - let allowEmpty = true - let allowSameDomain = true - let allowDomains = [] - let denyDomains = [] - let checkOrigin = true - - let options = {} - if (window.parent.UPDATING_RULE != null) { - options = window.parent.UPDATING_RULE.checkpointOptions - } - - if (options == null) { - options = {} - } - if (typeof (options.allowEmpty) == "boolean") { - allowEmpty = options.allowEmpty - } - if (typeof (options.allowSameDomain) == "boolean") { - allowSameDomain = options.allowSameDomain - } - if (options.allowDomains != null && typeof (options.allowDomains) == "object") { - allowDomains = options.allowDomains - } - if (options.denyDomains != null && typeof (options.denyDomains) == "object") { - denyDomains = options.denyDomains - } - if (typeof options.checkOrigin == "boolean") { - checkOrigin = options.checkOrigin - } - - let that = this - setTimeout(function () { - that.change() - }, 100) - - return { - allowEmpty: allowEmpty, - allowSameDomain: allowSameDomain, - allowDomains: allowDomains, - denyDomains: denyDomains, - checkOrigin: checkOrigin, - options: {}, - value: 0 - } - }, - watch: { - allowEmpty: function () { - this.change() - }, - allowSameDomain: function () { - this.change() - }, - checkOrigin: function () { - this.change() - } - }, - methods: { - changeAllowDomains: function (values) { - this.allowDomains = values - this.change() - }, - changeDenyDomains: function (values) { - this.denyDomains = values - this.change() - }, - change: function () { - this.vCheckpoint.options = [ - { - code: "allowEmpty", - value: this.allowEmpty - }, - { - code: "allowSameDomain", - value: this.allowSameDomain, - }, - { - code: "allowDomains", - value: this.allowDomains - }, - { - code: "denyDomains", - value: this.denyDomains - }, - { - code: "checkOrigin", - value: this.checkOrigin - } - ] - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - -
来源域名允许为空 - -

允许不带来源的访问。

-
来源域名允许一致 - -

允许来源域名和当前访问的域名一致,相当于在站内访问。

-
允许的来源域名 - -

允许的来源域名列表,比如example.com(顶级域名)、*.example.com(example.com的所有二级域名)。单个星号*表示允许所有域名。

-
禁止的来源域名 - -

禁止的来源域名列表,比如example.org(顶级域名)、*.example.org(example.org的所有二级域名);除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。

-
同时检查Origin - -

如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。

-
-
` -}) - Vue.component("http-cache-refs-config-box", { props: ["v-cache-refs", "v-cache-config", "v-cache-policy-id", "v-web-id", "v-max-bytes"], mounted: function () { @@ -4751,161 +6808,1523 @@ Vue.component("http-cache-refs-config-box", {
` }) -Vue.component("origin-list-box", { - props: ["v-primary-origins", "v-backup-origins", "v-server-type", "v-params"], +Vue.component("http-cache-stale-config", { + props: ["v-cache-stale-config"], data: function () { - return { - primaryOrigins: this.vPrimaryOrigins, - backupOrigins: this.vBackupOrigins - } - }, - methods: { - createPrimaryOrigin: function () { - teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, { - width: "45em", - height: "27em", - callback: function (resp) { - teaweb.success("保存成功", function () { - window.location.reload() - }) + let config = this.vCacheStaleConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + status: [], + supportStaleIfErrorHeader: true, + life: { + count: 1, + unit: "day" } - }) - }, - createBackupOrigin: function () { - teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, { - width: "45em", - height: "27em", - callback: function (resp) { - teaweb.success("保存成功", function () { - window.location.reload() - }) - } - }) - }, - updateOrigin: function (originId, originType) { - teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, { - width: "45em", - height: "27em", - callback: function (resp) { - teaweb.success("保存成功", function () { - window.location.reload() - }) - } - }) - }, - deleteOrigin: function (originId, originAddr, originType) { - let that = this - teaweb.confirm("确定要删除此源站(" + originAddr + ")吗?", function () { - Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType) - .post() - .success(function () { - teaweb.success("删除成功", function () { - window.location.reload() - }) - }) - }) - }, - updateOriginIsOn: function (originId, originAddr, isOn) { - let message - let resultMessage - if (isOn) { - message = "确定要启用此源站(" + originAddr + ")吗?" - resultMessage = "启用成功" - } else { - message = "确定要停用此源站(" + originAddr + ")吗?" - resultMessage = "停用成功" } - let that = this - teaweb.confirm(message, function () { - Tea.action("/servers/server/settings/origins/updateIsOn?" + that.vParams + "&originId=" + originId + "&isOn=" + (isOn ? 1 : 0)) - .post() - .success(function () { - teaweb.success(resultMessage, function () { - window.location.reload() - }) - }) - }) } - }, - template: `
-

主要源站 [添加主要源站]

-

暂时还没有主要源站。

- - -

备用源站 [添加备用源站]

-

暂时还没有备用源站。

- -
` -}) - -Vue.component("origin-list-table", { - props: ["v-origins", "v-origin-type"], - data: function () { - let hasMatchedDomains = false - let origins = this.vOrigins - if (origins != null && origins.length > 0) { - origins.forEach(function (origin) { - if (origin.domains != null && origin.domains.length > 0) { - hasMatchedDomains = true - } - }) - } - return { - hasMatchedDomains: hasMatchedDomains + config: config } }, - methods: { - deleteOrigin: function (originId, originAddr) { - this.$emit("deleteOrigin", originId, originAddr, this.vOriginType) - }, - updateOrigin: function (originId) { - this.$emit("updateOrigin", originId, this.vOriginType) - }, - updateOriginIsOn: function (originId, originAddr, isOn) { - this.$emit("updateOriginIsOn", originId, originAddr, isOn) + watch: { + config: { + deep: true, + handler: function () { + this.$emit("change", this.config) + } } }, - template: ` - - - - - - - - - + methods: {}, + template: `
源站地址权重状态操作
- - - + + + + + + + + + + + + +
- {{origin.addr}}   -
- 对象存储 - {{origin.name}} - 证书 - 主机名: {{origin.host}} - 端口跟随 - HTTP/2 - - 匹配: {{domain}} - 匹配: 所有域名 -
-
{{origin.weight}}
启用过时缓存 - + +

选中后,在更新缓存失败后会尝试读取过时的缓存。

有效期 - 修改   - 停用启用   - 删除 + +

缓存在过期之后,仍然保留的时间。

+
状态码 +

在这些状态码出现时使用过时缓存,默认支持50x状态码。

+
支持stale-if-error + +

选中后,支持在Cache-Control中通过stale-if-error指定过时缓存有效期。

` }) +// HTTP CC防护配置 +Vue.component("http-cc-config-box", { + props: ["v-cc-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vCcConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + enableFingerprint: true, + enableGET302: true, + onlyURLPatterns: [], + exceptURLPatterns: [], + useDefaultThresholds: true, + ignoreCommonFiles: true + } + } + + if (config.thresholds == null || config.thresholds.length == 0) { + config.thresholds = [ + { + maxRequests: 0 + }, + { + maxRequests: 0 + }, + { + maxRequests: 0 + } + ] + } + + if (typeof config.enableFingerprint != "boolean") { + config.enableFingerprint = true + } + if (typeof config.enableGET302 != "boolean") { + config.enableGET302 = true + } + + if (config.onlyURLPatterns == null) { + config.onlyURLPatterns = [] + } + if (config.exceptURLPatterns == null) { + config.exceptURLPatterns = [] + } + return { + config: config, + moreOptionsVisible: false, + minQPSPerIP: config.minQPSPerIP, + useCustomThresholds: !config.useDefaultThresholds, + + thresholdMaxRequests0: this.maxRequestsStringAtThresholdIndex(config, 0), + thresholdMaxRequests1: this.maxRequestsStringAtThresholdIndex(config, 1), + thresholdMaxRequests2: this.maxRequestsStringAtThresholdIndex(config, 2) + } + }, + watch: { + minQPSPerIP: function (v) { + let qps = parseInt(v.toString()) + if (isNaN(qps) || qps < 0) { + qps = 0 + } + this.config.minQPSPerIP = qps + }, + thresholdMaxRequests0: function (v) { + this.setThresholdMaxRequests(0, v) + }, + thresholdMaxRequests1: function (v) { + this.setThresholdMaxRequests(1, v) + }, + thresholdMaxRequests2: function (v) { + this.setThresholdMaxRequests(2, v) + }, + useCustomThresholds: function (b) { + this.config.useDefaultThresholds = !b + } + }, + methods: { + maxRequestsStringAtThresholdIndex: function (config, index) { + if (config.thresholds == null) { + return "" + } + if (index < config.thresholds.length) { + let s = config.thresholds[index].maxRequests.toString() + if (s == "0") { + s = "" + } + return s + } + return "" + }, + setThresholdMaxRequests: function (index, v) { + let maxRequests = parseInt(v) + if (isNaN(maxRequests) || maxRequests < 0) { + maxRequests = 0 + } + if (index < this.config.thresholds.length) { + this.config.thresholds[index].maxRequests = maxRequests + } + }, + showMoreOptions: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用CC无感防护 + +

启用后,自动检测并拦截CC攻击。

+
例外URL + +

如果填写了例外URL,表示这些URL跳过CC防护不做处理。

+
限制URL + +

如果填写了限制URL,表示只对这些URL进行CC防护处理;如果不填则表示支持所有的URL。

+
忽略常用文件 + +

忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。

+
检查请求来源指纹 + +

在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。

+
启用GET302校验 + +

选中后,表示自动通过GET302方法来校验客户端。

+
单IP最低QPS +
+ + 请求数/秒 +
+

当某个IP在1分钟内平均QPS达到此值时,才会开始检测;如果设置为0,表示任何访问都会检测。(注意这里设置的是检测开启阈值,不是拦截阈值,拦截阈值在当前表单下方可以设置)

+
使用自定义拦截阈值 + +
自定义拦截阈值设置 +
+
+ 单IP每5秒最多 + + 请求 +
+
+ +
+
+ 单IP每60秒 + + 请求 +
+
+
+
+ 单IP每300秒 + + 请求 +
+
+
+
+
` +}) + +Vue.component("http-charsets-box", { + props: ["v-usual-charsets", "v-all-charsets", "v-charset-config", "v-is-location", "v-is-group"], + data: function () { + let charsetConfig = this.vCharsetConfig + if (charsetConfig == null) { + charsetConfig = { + isPrior: false, + isOn: false, + charset: "", + isUpper: false, + force: false + } + } + return { + charsetConfig: charsetConfig, + advancedVisible: false + } + }, + methods: { + changeAdvancedVisible: function (v) { + this.advancedVisible = v + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
启用字符编码 +
+ + +
+
选择字符编码 +
强制替换 + +

选中后,表示强制覆盖已经设置的字符集;不选中,表示如果源站已经设置了字符集,则保留不修改。

+
字符编码大写 +
+ + +
+

选中后将指定的字符编码转换为大写,比如默认为utf-8,选中后将改为UTF-8

+
+
+
` +}) + +// 压缩配置 +Vue.component("http-compression-config-box", { + props: ["v-compression-config", "v-is-location", "v-is-group"], + mounted: function () { + let that = this + sortLoad(function () { + that.initSortableTypes() + }) + }, + data: function () { + let config = this.vCompressionConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + useDefaultTypes: true, + types: ["brotli", "gzip", "zstd", "deflate"], + level: 0, + decompressData: false, + gzipRef: null, + deflateRef: null, + brotliRef: null, + minLength: {count: 1, "unit": "kb"}, + maxLength: {count: 32, "unit": "mb"}, + mimeTypes: ["text/*", "application/javascript", "application/json", "application/atom+xml", "application/rss+xml", "application/xhtml+xml", "font/*", "image/svg+xml"], + extensions: [".js", ".json", ".html", ".htm", ".xml", ".css", ".woff2", ".txt"], + exceptExtensions: [".apk", ".ipa"], + conds: null, + enablePartialContent: false, + onlyURLPatterns: [], + exceptURLPatterns: [] + } + } + + if (config.types == null) { + config.types = [] + } + if (config.mimeTypes == null) { + config.mimeTypes = [] + } + if (config.extensions == null) { + config.extensions = [] + } + + let allTypes = [ + { + name: "Gzip", + code: "gzip", + isOn: true + }, + { + name: "Deflate", + code: "deflate", + isOn: true + }, + { + name: "Brotli", + code: "brotli", + isOn: true + }, + { + name: "ZSTD", + code: "zstd", + isOn: true + } + ] + + let configTypes = [] + config.types.forEach(function (typeCode) { + allTypes.forEach(function (t) { + if (typeCode == t.code) { + t.isOn = true + configTypes.push(t) + } + }) + }) + allTypes.forEach(function (t) { + if (!config.types.$contains(t.code)) { + t.isOn = false + configTypes.push(t) + } + }) + + return { + config: config, + moreOptionsVisible: false, + allTypes: configTypes + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + }, + changeExtensions: function (values) { + values.forEach(function (v, k) { + if (v.length > 0 && v[0] != ".") { + values[k] = "." + v + } + }) + this.config.extensions = values + }, + changeExceptExtensions: function (values) { + values.forEach(function (v, k) { + if (v.length > 0 && v[0] != ".") { + values[k] = "." + v + } + }) + this.config.exceptExtensions = values + }, + changeMimeTypes: function (values) { + this.config.mimeTypes = values + }, + changeAdvancedVisible: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + }, + changeConds: function (conds) { + this.config.conds = conds + }, + changeType: function () { + this.config.types = [] + let that = this + this.allTypes.forEach(function (v) { + if (v.isOn) { + that.config.types.push(v.code) + } + }) + }, + initSortableTypes: function () { + let box = document.querySelector("#compression-types-box") + let that = this + Sortable.create(box, { + draggable: ".checkbox", + handle: ".icon.handle", + onStart: function () { + + }, + onUpdate: function (event) { + let checkboxes = box.querySelectorAll(".checkbox") + let codes = [] + checkboxes.forEach(function (checkbox) { + let code = checkbox.getAttribute("data-code") + codes.push(code) + }) + that.config.types = codes + } + }) + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用内容压缩 +
+ + +
+
支持的扩展名 + +

含有这些扩展名的URL将会被压缩,不区分大小写。

+
例外扩展名 + +

含有这些扩展名的URL将不会被压缩,不区分大小写。

+
支持的MimeType + +

响应的Content-Type里包含这些MimeType的内容将会被压缩。

+
压缩算法 +
+ + + +
+
+
+
+
+ + +
+
+
+ +

选择支持的压缩算法和优先顺序,拖动图表排序。

+
支持已压缩内容 + +

支持对已压缩内容尝试重新使用新的算法压缩;不选中表示保留当前的压缩格式。

+
内容最小长度 + +

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

+
内容最大长度 + +

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

+
支持Partial
Content
+ +

支持对分片内容(PartialContent)的压缩;除非客户端有特殊要求,一般不需要启用。

+
例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
限制URL + +

如果填写了限制URL,表示只对这些URL进行压缩处理;如果不填则表示支持所有的URL。

+
匹配条件 + +
+
+
` +}) + +// URL扩展名条件 +Vue.component("http-cond-url-extension", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPathLowerExtension}", + operator: "in", + value: "[]" + } + if (this.vCond != null && this.vCond.param == cond.param) { + cond.value = this.vCond.value + } + + let extensions = [] + try { + extensions = JSON.parse(cond.value) + } catch (e) { + + } + + return { + cond: cond, + extensions: extensions, // TODO 可以拖动排序 + + isAdding: false, + addingExt: "" + } + }, + watch: { + extensions: function () { + this.cond.value = JSON.stringify(this.extensions) + } + }, + methods: { + addExt: function () { + this.isAdding = !this.isAdding + + if (this.isAdding) { + let that = this + setTimeout(function () { + that.$refs.addingExt.focus() + }, 100) + } + }, + cancelAdding: function () { + this.isAdding = false + this.addingExt = "" + }, + confirmAdding: function () { + // TODO 做更详细的校验 + // TODO 如果有重复的则提示之 + + if (this.addingExt.length == 0) { + return + } + + let that = this + this.addingExt.split(/[,;,;|]/).forEach(function (ext) { + ext = ext.trim() + if (ext.length > 0) { + if (ext[0] != ".") { + ext = "." + ext + } + ext = ext.replace(/\s+/g, "").toLowerCase() + that.extensions.push(ext) + } + }) + + // 清除状态 + this.cancelAdding() + }, + removeExt: function (index) { + this.extensions.$remove(index) + } + }, + template: `
+ +
+
{{ext}}
+
+
+
+
+ +
+
+ + +
+
+
+ +
+

扩展名需要包含点(.)符号,例如.jpg.png之类;多个扩展名用逗号分割。

+
` +}) + +// 排除URL扩展名条件 +Vue.component("http-cond-url-not-extension", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPathLowerExtension}", + operator: "not in", + value: "[]" + } + if (this.vCond != null && this.vCond.param == cond.param) { + cond.value = this.vCond.value + } + + let extensions = [] + try { + extensions = JSON.parse(cond.value) + } catch (e) { + + } + + return { + cond: cond, + extensions: extensions, // TODO 可以拖动排序 + + isAdding: false, + addingExt: "" + } + }, + watch: { + extensions: function () { + this.cond.value = JSON.stringify(this.extensions) + } + }, + methods: { + addExt: function () { + this.isAdding = !this.isAdding + + if (this.isAdding) { + let that = this + setTimeout(function () { + that.$refs.addingExt.focus() + }, 100) + } + }, + cancelAdding: function () { + this.isAdding = false + this.addingExt = "" + }, + confirmAdding: function () { + // TODO 做更详细的校验 + // TODO 如果有重复的则提示之 + + if (this.addingExt.length == 0) { + return + } + if (this.addingExt[0] != ".") { + this.addingExt = "." + this.addingExt + } + this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase() + this.extensions.push(this.addingExt) + + // 清除状态 + this.cancelAdding() + }, + removeExt: function (index) { + this.extensions.$remove(index) + } + }, + template: `
+ +
+
{{ext}}
+
+
+
+
+ +
+
+ + +
+
+
+ +
+

扩展名需要包含点(.)符号,例如.jpg.png之类。

+
` +}) + +// 根据URL前缀 +Vue.component("http-cond-url-prefix", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "prefix", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof (this.vCond.value) == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static/images,不需要带域名。

+
` +}) + +Vue.component("http-cond-url-not-prefix", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "prefix", + value: "", + isReverse: true, + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

要排除的URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static/images,不需要带域名。

+
` +}) + +// 首页 +Vue.component("http-cond-url-eq-index", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "eq", + value: "/", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

检查URL路径是为/,不需要带域名。

+
` +}) + +// 全站 +Vue.component("http-cond-url-all", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "prefix", + value: "/", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

支持全站所有URL。

+
` +}) + +// URL精准匹配 +Vue.component("http-cond-url-eq", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "eq", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

完整的URL路径,通常以/开头,比如/static/ui.js,不需要带域名。

+
` +}) + +Vue.component("http-cond-url-not-eq", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "eq", + value: "", + isReverse: true, + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

要排除的完整的URL路径,通常以/开头,比如/static/ui.js,不需要带域名。

+
` +}) + +// URL正则匹配 +Vue.component("http-cond-url-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

匹配URL的正则表达式,比如^/static/(.*).js$,不需要带域名。

+
` +}) + +// 排除URL正则匹配 +Vue.component("http-cond-url-not-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "not regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

不要匹配URL的正则表达式,意即只要匹配成功则排除此条件,比如^/static/(.*).js$,不需要带域名。

+
` +}) + +// URL通配符 +Vue.component("http-cond-url-wildcard-match", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${requestPath}", + operator: "wildcard match", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

匹配URL的通配符,用星号(*)表示任意字符,比如(/images/*.png/static/*,不需要带域名。

+
` +}) + +// User-Agent正则匹配 +Vue.component("http-cond-user-agent-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${userAgent}", + operator: "regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

匹配User-Agent的正则表达式,比如Android|iPhone

+
` +}) + +// User-Agent正则不匹配 +Vue.component("http-cond-user-agent-not-regexp", { + props: ["v-cond"], + mounted: function () { + this.$refs.valueInput.focus() + }, + data: function () { + let cond = { + isRequest: true, + param: "${userAgent}", + operator: "not regexp", + value: "", + isCaseInsensitive: false + } + if (this.vCond != null && typeof this.vCond.value == "string") { + cond.value = this.vCond.value + } + return { + cond: cond + } + }, + methods: { + changeCaseInsensitive: function (isCaseInsensitive) { + this.cond.isCaseInsensitive = isCaseInsensitive + } + }, + template: `
+ + +

匹配User-Agent的正则表达式,比如Android|iPhone,如果匹配,则排除此条件。

+
` +}) + +// 根据MimeType +Vue.component("http-cond-mime-type", { + props: ["v-cond"], + data: function () { + let cond = { + isRequest: false, + param: "${response.contentType}", + operator: "mime type", + value: "[]" + } + if (this.vCond != null && this.vCond.param == cond.param) { + cond.value = this.vCond.value + } + return { + cond: cond, + mimeTypes: JSON.parse(cond.value), // TODO 可以拖动排序 + + isAdding: false, + addingMimeType: "" + } + }, + watch: { + mimeTypes: function () { + this.cond.value = JSON.stringify(this.mimeTypes) + } + }, + methods: { + addMimeType: function () { + this.isAdding = !this.isAdding + + if (this.isAdding) { + let that = this + setTimeout(function () { + that.$refs.addingMimeType.focus() + }, 100) + } + }, + cancelAdding: function () { + this.isAdding = false + this.addingMimeType = "" + }, + confirmAdding: function () { + // TODO 做更详细的校验 + // TODO 如果有重复的则提示之 + + if (this.addingMimeType.length == 0) { + return + } + this.addingMimeType = this.addingMimeType.replace(/\s+/g, "") + this.mimeTypes.push(this.addingMimeType) + + // 清除状态 + this.cancelAdding() + }, + removeMimeType: function (index) { + this.mimeTypes.$remove(index) + } + }, + template: `
+ +
+
{{mimeType}}
+
+
+
+
+ +
+
+ + +
+
+
+ +
+

服务器返回的内容的MimeType,比如text/htmlimage/*等。

+
` +}) + +// 参数匹配 +Vue.component("http-cond-params", { + props: ["v-cond"], + mounted: function () { + let cond = this.vCond + if (cond == null) { + return + } + this.operator = cond.operator + + // stringValue + if (["regexp", "not regexp", "eq", "not", "prefix", "suffix", "contains", "not contains", "eq ip", "gt ip", "gte ip", "lt ip", "lte ip", "ip range"].$contains(cond.operator)) { + this.stringValue = cond.value + return + } + + // numberValue + if (["eq int", "eq float", "gt", "gte", "lt", "lte", "mod 10", "ip mod 10", "mod 100", "ip mod 100"].$contains(cond.operator)) { + this.numberValue = cond.value + return + } + + // modValue + if (["mod", "ip mod"].$contains(cond.operator)) { + let pieces = cond.value.split(",") + this.modDivValue = pieces[0] + if (pieces.length > 1) { + this.modRemValue = pieces[1] + } + return + } + + // stringValues + let that = this + if (["in", "not in", "file ext", "mime type"].$contains(cond.operator)) { + try { + let arr = JSON.parse(cond.value) + if (arr != null && (arr instanceof Array)) { + arr.forEach(function (v) { + that.stringValues.push(v) + }) + } + } catch (e) { + + } + return + } + + // versionValue + if (["version range"].$contains(cond.operator)) { + let pieces = cond.value.split(",") + this.versionRangeMinValue = pieces[0] + if (pieces.length > 1) { + this.versionRangeMaxValue = pieces[1] + } + return + } + }, + data: function () { + let cond = { + isRequest: true, + param: "", + operator: window.REQUEST_COND_OPERATORS[0].op, + value: "", + isCaseInsensitive: false + } + if (this.vCond != null) { + cond = this.vCond + } + return { + cond: cond, + operators: window.REQUEST_COND_OPERATORS, + operator: window.REQUEST_COND_OPERATORS[0].op, + operatorDescription: window.REQUEST_COND_OPERATORS[0].description, + variables: window.REQUEST_VARIABLES, + variable: "", + + // 各种类型的值 + stringValue: "", + numberValue: "", + + modDivValue: "", + modRemValue: "", + + stringValues: [], + + versionRangeMinValue: "", + versionRangeMaxValue: "" + } + }, + methods: { + changeVariable: function () { + let v = this.cond.param + if (v == null) { + v = "" + } + this.cond.param = v + this.variable + }, + changeOperator: function () { + let that = this + this.operators.forEach(function (v) { + if (v.op == that.operator) { + that.operatorDescription = v.description + } + }) + + this.cond.operator = this.operator + + // 移动光标 + let box = document.getElementById("variables-value-box") + if (box != null) { + setTimeout(function () { + let input = box.getElementsByTagName("INPUT") + if (input.length > 0) { + input[0].focus() + } + }, 100) + } + }, + changeStringValues: function (v) { + this.stringValues = v + this.cond.value = JSON.stringify(v) + } + }, + watch: { + stringValue: function (v) { + this.cond.value = v + }, + numberValue: function (v) { + // TODO 校验数字 + this.cond.value = v + }, + modDivValue: function (v) { + if (v.length == 0) { + return + } + let div = parseInt(v) + if (isNaN(div)) { + div = 1 + } + this.modDivValue = div + this.cond.value = div + "," + this.modRemValue + }, + modRemValue: function (v) { + if (v.length == 0) { + return + } + let rem = parseInt(v) + if (isNaN(rem)) { + rem = 0 + } + this.modRemValue = rem + this.cond.value = this.modDivValue + "," + rem + }, + versionRangeMinValue: function (v) { + this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue + }, + versionRangeMaxValue: function (v) { + this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue + } + }, + template: ` + + 参数值 + + +
+
+ +
+
+ +
+
+

其中可以使用变量,类似于\${requestPath},也可以是多个变量的组合。

+ + + + 操作符 + +
+ +

+
+ + + + 对比值 + + +
+ +

要匹配的正则表达式,比如^/static/(.+).js

+
+ + +
+ +

要对比的数字。

+
+ + +
+ +

参数值除以10的余数,在0-9之间。

+
+
+ +

参数值除以100的余数,在0-99之间。

+
+
+
+
除:
+
+ +
+
余:
+
+ +
+
+
+ + +
+ +

和参数值一致的字符串。

+

和参数值不一致的字符串。

+

参数值的前缀。

+

参数值的后缀为此字符串。

+

参数值包含此字符串。

+

参数值不包含此字符串。

+
+
+ +

添加参数值列表。

+

添加参数值列表。

+

添加扩展名列表,比如pnghtml,不包括点。

+

添加MimeType列表,类似于text/htmlimage/*

+
+
+
+
+
-
+
+
+
+ + +
+ +

要对比的IP。

+
+
+ +

参数中IP转换成整数后除以10的余数,在0-9之间。

+
+
+ +

参数中IP转换成整数后除以100的余数,在0-99之间。

+
+ + + + 不区分大小写 + +
+ + +
+

选中后表示对比时忽略参数值的大小写。

+ + + +` +}) + Vue.component("http-cors-header-config-box", { props: ["value"], data: function () { @@ -5028,67 +8447,19 @@ Vue.component("http-cors-header-config-box", {
` }) -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 - }) - }, +// 页面动态加密配置 +Vue.component("http-encryption-config-box", { + props: ["v-encryption-config", "v-is-location", "v-is-group"], 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-optimization-config-box", { - props: ["v-optimization-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vOptimizationConfig + let config = this.vEncryptionConfig return { config: config, htmlMoreOptions: false, javascriptMoreOptions: false, - cssMoreOptions: false + keyPolicyMoreOptions: false, + cacheMoreOptions: false, + encryptionMoreOptions: false } }, methods: { @@ -5097,1316 +8468,270 @@ Vue.component("http-optimization-config-box", { } }, template: `
- +
-
+
- + - - + + - + - + - - - - - -
HTML优化启用页面动态加密
- +
-

可以自动优化HTML中包含的空白、注释、空标签等。只有文件可以缓存时才会被优化。

+

启用后,将对 HTML 页面中的 JavaScript 进行动态加密,有效抵御批量爬虫和脚本工具。

HTML例外URL排除 URL - -

如果填写了例外URL,表示这些URL跳过不做处理。

+ +

这些 URL 将跳过加密处理,支持正则表达式。

HTML限制URL - -

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

-
- - - - - - - - - - - - - - - - - - - - - -
Javascript优化 -
- - -
-

可以自动缩短Javascript中变量、函数名称等。只有文件可以缓存时才会被优化。

-
Javascript例外URL - -

如果填写了例外URL,表示这些URL跳过不做处理。

-
Javascript限制URL - -

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

-
- - - - - - - - - - - - - - - - - - - -
CSS优化 -
- - -
-

可以自动去除CSS中包含的空白。只有文件可以缓存时才会被优化。

-
CSS例外URL - -

如果填写了例外URL,表示这些URL跳过不做处理。

-
CSS限制URL - -

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

-
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
HTML 加密 +
+ + +
+

加密 HTML 页面中的 JavaScript 脚本。

+
加密内联脚本 +
+ + +
+

加密 HTML 中的内联 <script> 标签内容。

+
加密外部脚本 +
+ + +
+

加密通过 src 属性引入的外部 JavaScript 文件。

+
URL 匹配规则 + +

如果填写了匹配规则,表示只对这些 URL 进行加密处理;如果不填则表示支持所有的 URL。

+
+ +
+ + + + + + + + + + + + + + + + +
JavaScript 文件加密 +
+ + +
+

加密独立的 JavaScript 文件(.js 文件)。

+
URL 匹配规则 + +

如果填写了匹配规则,表示只对这些 URL 进行加密处理;如果不填则表示支持所有的 URL。

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
服务器端密钥 + +

用于生成加密密钥的密码,建议使用复杂的随机字符串。默认密钥仅用于测试,生产环境请务必修改!

+
时间分片(秒) + +

加密密钥每隔多少秒更换一次。时间越短越安全,但可能影响性能。建议 60-120 秒,默认 60 秒。

+
IP CIDR 前缀长度 + +

将用户 IP 地址的前多少位作为识别依据。例如设置为 24 时,192.168.1.1 和 192.168.1.2 会被视为同一用户。默认 24。

+
简化 User-Agent +
+ + +
+

开启后,只识别浏览器类型(如 Chrome、Firefox),忽略版本号等细节,避免因浏览器自动更新导致解密失败。

+
+ +
+ + + + + + + + + + + + + + + + + + + + +
启用缓存 +
+ + +
+

开启后,相同内容的加密结果会被缓存,减少重复计算,提升响应速度。

+
缓存 TTL(秒) + +

缓存的有效期,超过这个时间后缓存会自动失效。建议与上面的"时间分片"保持一致。默认 60 秒。

+
最大缓存条目数 + +

最多缓存多少个加密结果。数量越大占用内存越多,建议根据服务器内存情况调整。默认 1000。

+
+
` }) -Vue.component("http-websocket-box", { - props: ["v-websocket-ref", "v-websocket-config", "v-is-location"], + + +Vue.component("http-expires-time-config-box", { + props: ["v-expires-time"], data: function () { - let websocketRef = this.vWebsocketRef - if (websocketRef == null) { - websocketRef = { + let expiresTime = this.vExpiresTime + if (expiresTime == null) { + expiresTime = { isPrior: false, isOn: false, - websocketId: 0 + overwrite: true, + autoCalculate: true, + duration: {count: -1, "unit": "hour"} } } - - 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 + expiresTime: expiresTime } }, 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.websocketRef.isPrior) && this.websocketRef.isOn - }, - changeAdvancedVisible: function (v) { - this.advancedVisible = v - }, - createOrigin: function () { - let that = this - teaweb.popup("/servers/server/settings/websocket/createOrigin", { - height: "12.5em", - callback: function (resp) { - that.websocketConfig.allowedOrigins.push(resp.data.origin) - } - }) - }, - removeOrigin: function (index) { - this.websocketConfig.allowedOrigins.$remove(index) - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用Websocket -
- - -
-
允许所有来源域(Origin) -
- - -
-

选中表示允许所有的来源域。

-
允许的来源域列表(Origin) -
-
- {{origin}} -
-
-
- -

只允许在列表中的来源域名访问Websocket服务。

-
传递请求来源域 -
- - -
-

选中后,表示把接收到的请求中的Origin字段传递到源站。

-
指定传递的来源域 - -

指定向源站传递的Origin字段值。

-
握手超时时间(Handshake) -
-
- -
-
- 秒 -
-
-

0表示使用默认的时间设置。

-
-
-
` -}) - -Vue.component("http-rewrite-rule-list", { - props: ["v-web-id", "v-rewrite-rules"], - mounted: function () { - setTimeout(this.sort, 1000) - }, - data: function () { - let rewriteRules = this.vRewriteRules - if (rewriteRules == null) { - rewriteRules = [] - } - return { - rewriteRules: rewriteRules - } - }, - methods: { - updateRewriteRule: function (rewriteRuleId) { - teaweb.popup("/servers/server/settings/rewrite/updatePopup?webId=" + this.vWebId + "&rewriteRuleId=" + rewriteRuleId, { - height: "26em", - callback: function () { - window.location.reload() - } - }) - }, - deleteRewriteRule: function (rewriteRuleId) { - let that = this - teaweb.confirm("确定要删除此重写规则吗?", function () { - Tea.action("/servers/server/settings/rewrite/delete") - .params({ - webId: that.vWebId, - rewriteRuleId: rewriteRuleId - }) - .post() - .refresh() - }) - }, - // 排序 - sort: function () { - if (this.rewriteRules.length == 0) { - return - } - let that = this - sortTable(function (rowIds) { - Tea.action("/servers/server/settings/rewrite/sort") - .post() - .params({ - webId: that.vWebId, - rewriteRuleIds: rowIds - }) - .success(function () { - teaweb.success("保存成功") - }) - }) - } - }, - template: `
-
-

暂时还没有重写规则。

- - - - - - - - - - - - - - - - - - - - - -
匹配规则转发目标转发方式状态操作
{{rule.pattern}} -
- BREAK - {{rule.redirectStatus}} - Host: {{rule.proxyHost}} -
{{rule.replace}} - 隐式 - 显示 - - - - 修改   - 删除 -
-

拖动左侧的图标可以对重写规则进行排序。

- -
` -}) - -Vue.component("http-rewrite-labels-label", { - props: ["v-class"], - template: `` -}) - -Vue.component("server-name-box", { - props: ["v-server-names"], - data: function () { - let serverNames = this.vServerNames; - if (serverNames == null) { - serverNames = [] - } - return { - serverNames: serverNames, - isSearching: false, - keyword: "" - } - }, - methods: { - addServerName: function () { - window.UPDATING_SERVER_NAME = null - let that = this - teaweb.popup("/servers/addServerNamePopup", { - callback: function (resp) { - var serverName = resp.data.serverName - that.serverNames.push(serverName) - setTimeout(that.submitForm, 100) - } - }); - }, - - removeServerName: function (index) { - this.serverNames.$remove(index) - }, - - updateServerName: function (index, serverName) { - window.UPDATING_SERVER_NAME = teaweb.clone(serverName) - let that = this - teaweb.popup("/servers/addServerNamePopup", { - callback: function (resp) { - var serverName = resp.data.serverName - Vue.set(that.serverNames, index, serverName) - setTimeout(that.submitForm, 100) - } - }); - }, - showSearchBox: function () { - this.isSearching = !this.isSearching - if (this.isSearching) { - let that = this - setTimeout(function () { - that.$refs.keywordRef.focus() - }, 200) - } else { - this.keyword = "" - } - }, - allServerNames: function () { - if (this.serverNames == null) { - return [] - } - let result = [] - this.serverNames.forEach(function (serverName) { - if (serverName.subNames != null && serverName.subNames.length > 0) { - serverName.subNames.forEach(function (subName) { - if (subName != null && subName.length > 0) { - if (!result.$contains(subName)) { - result.push(subName) - } - } - }) - } else if (serverName.name != null && serverName.name.length > 0) { - if (!result.$contains(serverName.name)) { - result.push(serverName.name) - } - } - }) - return result - }, - submitForm: function () { - Tea.runActionOn(this.$refs.serverNamesRef.form) - } - }, - watch: { - keyword: function (v) { - this.serverNames.forEach(function (serverName) { - if (v.length == 0) { - serverName.isShowing = true - return - } - if (serverName.subNames == null || serverName.subNames.length == 0) { - if (!teaweb.match(serverName.name, v)) { - serverName.isShowing = false - } - } else { - let found = false - serverName.subNames.forEach(function (subName) { - if (teaweb.match(subName, v)) { - found = true - } - }) - serverName.isShowing = found - } - }) - } - }, - template: `
- -
-
- {{serverName.type}} - {{serverName.name}} - {{serverName.subNames[0]}}等{{serverName.subNames.length}}个域名 - -
-
-
-
- -
|
-
- - -
-
- -
-
-
` -}) - -// UAM模式配置 -Vue.component("uam-config-box", { - props: ["v-uam-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vUamConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - addToWhiteList: true, - onlyURLPatterns: [], - exceptURLPatterns: [], - minQPSPerIP: 0, - keyLife: 0 - } - } - if (config.onlyURLPatterns == null) { - config.onlyURLPatterns = [] - } - if (config.exceptURLPatterns == null) { - config.exceptURLPatterns = [] - } - return { - config: config, - moreOptionsVisible: false, - minQPSPerIP: config.minQPSPerIP, - keyLife: config.keyLife - } - }, - watch: { - minQPSPerIP: function (v) { - let qps = parseInt(v.toString()) - if (isNaN(qps) || qps < 0) { - qps = 0 - } - this.config.minQPSPerIP = qps - }, - keyLife: function (v) { - let keyLife = parseInt(v) - if (isNaN(keyLife) || keyLife <= 0) { - keyLife = 0 - } - this.config.keyLife = keyLife - } - }, - methods: { - showMoreOptions: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - }, - changeConds: function (conds) { - this.config.conds = conds - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用5秒盾 - -

启用后,访问网站时,自动检查浏览器环境,阻止非正常访问。

-
验证有效期 -
- - -
-

单个客户端验证通过后,在这个有效期内不再重复验证;如果为0则表示系统默认。

-
单IP最低QPS -
- - 请求数/秒 -
-

当某个IP在1分钟内平均QPS达到此值时,才会触发5秒盾;如果设置为0,表示任何访问都会触发。

-
加入IP白名单 - -

选中后,表示验证通过后,将访问者IP加入到临时白名单中,此IP下次访问时不再校验5秒盾;此白名单只对5秒盾有效,不影响其他规则。此选项主要用于可能无法正常使用Cookie的网站。

-
例外URL - -

如果填写了例外URL,表示这些URL跳过5秒盾不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行5秒盾处理;如果不填则表示支持所有的URL。

-
匹配条件 - -
-
-
` -}) - -Vue.component("http-cache-stale-config", { - props: ["v-cache-stale-config"], - data: function () { - let config = this.vCacheStaleConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - status: [], - supportStaleIfErrorHeader: true, - life: { - count: 1, - unit: "day" - } - } - } - return { - config: config - } - }, - watch: { - config: { - deep: true, - handler: function () { - this.$emit("change", this.config) - } - } - }, - methods: {}, - template: ` - - - - - - - - - - - - - - - - - - -
启用过时缓存 - -

选中后,在更新缓存失败后会尝试读取过时的缓存。

-
有效期 - -

缓存在过期之后,仍然保留的时间。

-
状态码 -

在这些状态码出现时使用过时缓存,默认支持50x状态码。

-
支持stale-if-error - -

选中后,支持在Cache-Control中通过stale-if-error指定过时缓存有效期。

-
` -}) - -// 域名列表 -Vue.component("domains-box", { - props: ["v-domains", "name", "v-support-wildcard"], - data: function () { - let domains = this.vDomains - if (domains == null) { - domains = [] - } - - let realName = "domainsJSON" - if (this.name != null && typeof this.name == "string") { - realName = this.name - } - - let supportWildcard = true - if (typeof this.vSupportWildcard == "boolean") { - supportWildcard = this.vSupportWildcard - } - - return { - domains: domains, - - mode: "single", // single | batch - batchDomains: "", - - isAdding: false, - addingDomain: "", - - isEditing: false, - editingIndex: -1, - - realName: realName, - supportWildcard: supportWildcard - } - }, - watch: { - vSupportWildcard: function (v) { - if (typeof v == "boolean") { - this.supportWildcard = v - } - }, - mode: function (mode) { - let that = this - setTimeout(function () { - if (mode == "single") { - if (that.$refs.addingDomain != null) { - that.$refs.addingDomain.focus() - } - } else if (mode == "batch") { - if (that.$refs.batchDomains != null) { - that.$refs.batchDomains.focus() - } - } - }, 100) - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.addingDomain.focus() - }, 100) - }, - confirm: function () { - if (this.mode == "batch") { - this.confirmBatch() - return - } - - let that = this - - // 删除其中的空格 - this.addingDomain = this.addingDomain.replace(/\s/g, "") - - if (this.addingDomain.length == 0) { - teaweb.warn("请输入要添加的域名", function () { - that.$refs.addingDomain.focus() - }) - return - } - - // 基本校验 - if (this.supportWildcard) { - if (this.addingDomain[0] == "~") { - let expr = this.addingDomain.substring(1) - try { - new RegExp(expr) - } catch (e) { - teaweb.warn("正则表达式错误:" + e.message, function () { - that.$refs.addingDomain.focus() - }) - return - } - } - } else { - if (/[*~^]/.test(this.addingDomain)) { - teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () { - that.$refs.addingDomain.focus() - }) - return - } - } - - if (this.isEditing && this.editingIndex >= 0) { - this.domains[this.editingIndex] = this.addingDomain - } else { - // 分割逗号(,)、顿号(、) - if (this.addingDomain.match("[,、,;]")) { - let domainList = this.addingDomain.split(new RegExp("[,、,;]")) - domainList.forEach(function (v) { - if (v.length > 0) { - that.domains.push(v) - } - }) - } else { - this.domains.push(this.addingDomain) - } - } - this.cancel() - this.change() - }, - confirmBatch: function () { - let domains = this.batchDomains.split("\n") - let realDomains = [] - let that = this - let hasProblems = false - domains.forEach(function (domain) { - if (hasProblems) { - return - } - if (domain.length == 0) { - return - } - if (that.supportWildcard) { - if (domain == "~") { - let expr = domain.substring(1) - try { - new RegExp(expr) - } catch (e) { - hasProblems = true - teaweb.warn("正则表达式错误:" + e.message, function () { - that.$refs.batchDomains.focus() - }) - return - } - } - } else { - if (/[*~^]/.test(domain)) { - hasProblems = true - teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () { - that.$refs.batchDomains.focus() - }) - return - } - } - realDomains.push(domain) - }) - if (hasProblems) { - return - } - if (realDomains.length == 0) { - teaweb.warn("请输入要添加的域名", function () { - that.$refs.batchDomains.focus() - }) - return - } - - realDomains.forEach(function (domain) { - that.domains.push(domain) - }) - this.cancel() - this.change() - }, - edit: function (index) { - this.addingDomain = this.domains[index] - this.isEditing = true - this.editingIndex = index - let that = this - setTimeout(function () { - that.$refs.addingDomain.focus() - }, 50) - }, - remove: function (index) { - this.domains.$remove(index) - this.change() - }, - cancel: function () { - this.isAdding = false - this.mode = "single" - this.batchDomains = "" - this.isEditing = false - this.editingIndex = -1 - this.addingDomain = "" - }, - change: function () { - this.$emit("change", this.domains) - } - }, - template: `
- -
- - [正则] - [后缀] - [泛域名] - {{domain}} - -   -   - - -   -   - - -
-
-
-
-
- -
-
-
- -
-
- -
-
-
- -   -
-
-

支持普通域名(example.com)、泛域名(*.example.com、域名后缀(以点号开头,如.example.com)和正则表达式(以波浪号开头,如~.*.example.com;如果域名后有端口,请加上端口号。

-

只支持普通域名(example.comwww.example.com)。

-
-
-
- -
-
` -}) - -Vue.component("http-firewall-province-selector", { - props: ["v-type", "v-provinces"], - data: function () { - let provinces = this.vProvinces - if (provinces == null) { - provinces = [] - } - - return { - listType: this.vType, - provinces: provinces - } - }, - methods: { - addProvince: function () { - let selectedProvinceIds = this.provinces.map(function (province) { - return province.id - }) - let that = this - teaweb.popup("/servers/server/settings/waf/ipadmin/selectProvincesPopup?type=" + this.listType + "&selectedProvinceIds=" + selectedProvinceIds.join(","), { - width: "50em", - height: "26em", - callback: function (resp) { - that.provinces = resp.data.selectedProvinces - that.$forceUpdate() - that.notifyChange() - } - }) - }, - removeProvince: function (index) { - this.provinces.$remove(index) + "expiresTime.isPrior": function () { this.notifyChange() }, - resetProvinces: function () { - this.provinces = [] + "expiresTime.isOn": function () { this.notifyChange() }, + "expiresTime.overwrite": function () { + this.notifyChange() + }, + "expiresTime.autoCalculate": function () { + this.notifyChange() + } + }, + methods: { notifyChange: function () { - this.$emit("change", { - "provinces": this.provinces - }) + this.$emit("change", this.expiresTime) } }, template: `
- 暂时没有选择允许封禁的省份。 -
-
- - {{province.name}} -
-
-
-   -
` -}) - -Vue.component("http-referers-config-box", { - props: ["v-referers-config", "v-is-location", "v-is-group"], - data: function () { - let config = this.vReferersConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - allowEmpty: true, - allowSameDomain: true, - allowDomains: [], - denyDomains: [], - checkOrigin: true - } - } - if (config.allowDomains == null) { - config.allowDomains = [] - } - if (config.denyDomains == null) { - config.denyDomains = [] - } - return { - config: config, - moreOptionsVisible: false - } - }, - methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn - }, - changeAllowDomains: function (domains) { - if (typeof (domains) == "object") { - this.config.allowDomains = domains - this.$forceUpdate() - } - }, - changeDenyDomains: function (domains) { - if (typeof (domains) == "object") { - this.config.denyDomains = domains - this.$forceUpdate() - } - }, - showMoreOptions: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用防盗链 -
- - -
-

选中后表示开启防盗链。

-
允许直接访问网站 - -

允许用户直接访问网站,用户第一次访问网站时来源域名通常为空。

-
来源域名允许一致 - -

允许来源域名和当前访问的域名一致,相当于在站内访问。

-
允许的来源域名 - > -

允许的其他来源域名列表,比如example.com*.example.com。单个星号*表示允许所有域名。

-
禁止的来源域名 - -

禁止的来源域名列表,比如example.org*.example.org;除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。

-
同时检查Origin - -

如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。

-
例外URL - -

如果填写了例外URL,表示这些URL不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行处理;如果不填则表示支持所有的URL。

-
-
-
` -}) - -Vue.component("server-traffic-limit-status-viewer", { - props: ["value"], - data: function () { - let targetTypeName = "流量" - if (this.value != null) { - targetTypeName = this.targetTypeToName(this.value.targetType) - } - - return { - status: this.value, - targetTypeName: targetTypeName - } - }, - methods: { - targetTypeToName: function (targetType) { - switch (targetType) { - case "traffic": - return "流量" - case "request": - return "请求数" - case "websocketConnections": - return "Websocket连接数" - } - return "流量" - } - }, - template: ` - 已达到套餐当日{{targetTypeName}}限制 - 已达到套餐当月{{targetTypeName}}限制 - 已达到套餐总体{{targetTypeName}}限制 -` -}) - -Vue.component("http-redirect-to-https-box", { - props: ["v-redirect-to-https-config", "v-is-location"], - data: function () { - let redirectToHttpsConfig = this.vRedirectToHttpsConfig - if (redirectToHttpsConfig == null) { - redirectToHttpsConfig = { - isPrior: false, - isOn: false, - host: "", - port: 0, - status: 0, - onlyDomains: [], - exceptDomains: [] - } - } else { - if (redirectToHttpsConfig.onlyDomains == null) { - redirectToHttpsConfig.onlyDomains = [] - } - if (redirectToHttpsConfig.exceptDomains == null) { - redirectToHttpsConfig.exceptDomains = [] - } - } - return { - redirectToHttpsConfig: redirectToHttpsConfig, - portString: (redirectToHttpsConfig.port > 0) ? redirectToHttpsConfig.port.toString() : "", - moreOptionsVisible: false, - statusOptions: [ - {"code": 301, "text": "Moved Permanently"}, - {"code": 308, "text": "Permanent Redirect"}, - {"code": 302, "text": "Found"}, - {"code": 303, "text": "See Other"}, - {"code": 307, "text": "Temporary Redirect"} - ] - } - }, - watch: { - "redirectToHttpsConfig.status": function () { - this.redirectToHttpsConfig.status = parseInt(this.redirectToHttpsConfig.status) - }, - portString: function (v) { - let port = parseInt(v) - if (!isNaN(port)) { - this.redirectToHttpsConfig.port = port - } else { - this.redirectToHttpsConfig.port = 0 - } - } - }, - methods: { - changeMoreOptions: function (isVisible) { - this.moreOptionsVisible = isVisible - }, - changeOnlyDomains: function (values) { - this.redirectToHttpsConfig.onlyDomains = values - this.$forceUpdate() - }, - changeExceptDomains: function (values) { - this.redirectToHttpsConfig.exceptDomains = values - this.$forceUpdate() - } - }, - template: `
- - - - - - +
+ + - - + - + + + + + + + + + + + + +
自动跳转到HTTPS -
- - -
-

开启后,所有HTTP的请求都会自动跳转到对应的HTTPS URL上,

- - - - - - - - - - - - - - - -
状态码 - -
域名或IP地址 - -

默认和用户正在访问的域名或IP地址一致。

-
端口 - -

默认端口为443。

-
+
启用 +

启用后,将会在响应的Header中添加Expires字段,浏览器据此会将内容缓存在客户端;同时,在管理后台执行清理缓存时,也将无法清理客户端已有的缓存。

覆盖源站设置 + +

选中后,会覆盖源站Header中已有的Expires字段。

+
自动计算时间 +

根据已设置的缓存有效期进行计算。

+
强制缓存时间 + +

从客户端访问的时间开始要缓存的时长。

+
- - -
-
- - -
-

开启后,所有HTTP的请求都会自动跳转到对应的HTTPS URL上,

- - - - - - - - - - - - - - - - - - - - - - - -
状态码 - -
跳转后域名或IP地址 - -

默认和用户正在访问的域名或IP地址一致,不填写就表示使用当前的域名。

-
端口 - -

默认端口为443。

-
允许的域名 - -

如果填写了允许的域名,那么只有这些域名可以自动跳转。

-
排除的域名 - -

如果填写了排除的域名,那么这些域名将不跳转。

-
-
-
` }) @@ -7420,146 +9745,1047 @@ Vue.component("http-firewall-actions-box", {
` }) -// 认证设置 -Vue.component("http-auth-config-box", { - props: ["v-auth-config", "v-is-location"], +// Action列表 +Vue.component("http-firewall-actions-view", { + props: ["v-actions"], + template: `
+
+ {{action.name}} ({{action.code.toUpperCase()}}) +
+ [{{action.options.status}}] + + [分组] + [网站] + [网站和策略] + + + 黑名单 + 白名单 + 灰名单 + +
+
+
+
` +}) + +Vue.component("http-firewall-config-box", { + props: ["v-firewall-config", "v-is-location", "v-firewall-policy"], data: function () { - let authConfig = this.vAuthConfig - if (authConfig == null) { - authConfig = { + let firewall = this.vFirewallConfig + if (firewall == null) { + firewall = { isPrior: false, - isOn: false + isOn: false, + firewallPolicyId: 0, + ignoreGlobalRules: false, + defaultCaptchaType: "none" } } - if (authConfig.policyRefs == null) { - authConfig.policyRefs = [] + + if (firewall.defaultCaptchaType == null || firewall.defaultCaptchaType.length == 0) { + firewall.defaultCaptchaType = "none" } + + let allCaptchaTypes = window.WAF_CAPTCHA_TYPES.$copy() + + // geetest + let geeTestIsOn = false + if (this.vFirewallPolicy != null && this.vFirewallPolicy.captchaAction != null && this.vFirewallPolicy.captchaAction.geeTestConfig != null) { + geeTestIsOn = this.vFirewallPolicy.captchaAction.geeTestConfig.isOn + } + + // 如果没有启用geetest,则还原 + if (!geeTestIsOn && firewall.defaultCaptchaType == "geetest") { + firewall.defaultCaptchaType = "none" + } + return { - authConfig: authConfig + firewall: firewall, + moreOptionsVisible: false, + execGlobalRules: !firewall.ignoreGlobalRules, + captchaTypes: allCaptchaTypes, + geeTestIsOn: geeTestIsOn + } + }, + watch: { + execGlobalRules: function (v) { + this.firewall.ignoreGlobalRules = !v } }, 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) + changeOptionsVisible: function (v) { + this.moreOptionsVisible = v } }, template: `
- - - - - - - - - -
启用鉴权 -
- - -
-
-
- -
-

鉴权方式

- - + +
+ + - - - - - - - - - - + - + + + + + + + + +
名称鉴权方法参数状态操作
{{ref.authPolicy.name}}启用Web防火墙 - {{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}} +
+ +
人机识别验证方式 - + +

使用系统默认的设置。你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

+

{{captchaType.description}}你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。

启用系统全局规则 - 修改   - 删除 + +

选中后,表示使用系统全局WAF策略中定义的规则。

- -
-
+
` }) +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-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-firewall-province-selector", { + props: ["v-type", "v-provinces"], + data: function () { + let provinces = this.vProvinces + if (provinces == null) { + provinces = [] + } + + return { + listType: this.vType, + provinces: provinces + } + }, + methods: { + addProvince: function () { + let selectedProvinceIds = this.provinces.map(function (province) { + return province.id + }) + let that = this + teaweb.popup("/servers/server/settings/waf/ipadmin/selectProvincesPopup?type=" + this.listType + "&selectedProvinceIds=" + selectedProvinceIds.join(","), { + width: "50em", + height: "26em", + callback: function (resp) { + that.provinces = resp.data.selectedProvinces + that.$forceUpdate() + that.notifyChange() + } + }) + }, + removeProvince: function (index) { + this.provinces.$remove(index) + this.notifyChange() + }, + resetProvinces: function () { + this.provinces = [] + this.notifyChange() + }, + notifyChange: function () { + this.$emit("change", { + "provinces": this.provinces + }) + } + }, + template: `
+ 暂时没有选择允许封禁的省份。 +
+
+ + {{province.name}} +
+
+
+   +
` +}) + +Vue.component("http-firewall-region-selector", { + props: ["v-type", "v-countries"], + data: function () { + let countries = this.vCountries + if (countries == null) { + countries = [] + } + + return { + listType: this.vType, + countries: countries + } + }, + methods: { + addCountry: function () { + let selectedCountryIds = this.countries.map(function (country) { + return country.id + }) + let that = this + teaweb.popup("/servers/server/settings/waf/ipadmin/selectCountriesPopup?type=" + this.listType + "&selectedCountryIds=" + selectedCountryIds.join(","), { + width: "52em", + height: "30em", + callback: function (resp) { + that.countries = resp.data.selectedCountries + that.$forceUpdate() + that.notifyChange() + } + }) + }, + removeCountry: function (index) { + this.countries.$remove(index) + this.notifyChange() + }, + resetCountries: function () { + this.countries = [] + this.notifyChange() + }, + notifyChange: function () { + this.$emit("change", { + "countries": this.countries + }) + } + }, + template: `
+ 暂时没有选择允许封禁的区域。 +
+
+ + ({{country.letter}}){{country.name}} +
+
+
+   +
` +}) + +// 显示WAF规则的标签 +Vue.component("http-firewall-rule-label", { + props: ["v-rule"], + data: function () { + return { + rule: this.vRule + } + }, + methods: { + showErr: function (err) { + teaweb.popupTip("规则校验错误,请修正:" + teaweb.encodeHTML(err) + "") + }, + calculateParamName: function (param) { + let paramName = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + } + }) + } + return paramName + }, + calculateParamDescription: function (param) { + let paramName = "" + let paramDescription = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + paramDescription = checkpoint.description + } + }) + } + return paramName + ": " + paramDescription + }, + operatorName: function (operatorCode) { + let operatorName = operatorCode + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + } + }) + } + + return operatorName + }, + operatorDescription: function (operatorCode) { + let operatorName = operatorCode + let operatorDescription = "" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + operatorDescription = v.description + } + }) + } + + return operatorName + ": " + operatorDescription + }, + operatorDataType: function (operatorCode) { + let operatorDataType = "none" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorDataType = v.dataType + } + }) + } + + return operatorDataType + }, + isEmptyString: function (v) { + return typeof v == "string" && v.length == 0 + } + }, + template: `
+
+ {{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} + + + + {{rule.checkpointOptions.period}}秒内请求数 + + + + + 允许{{rule.checkpointOptions.allowDomains}} + 禁止{{rule.checkpointOptions.denyDomains}} + + + + | {{paramFilter.code}} + <{{operatorName(rule.operator)}}> + {{rule.value}} + [空] + + + + ({{rule.description}}) + + 规则错误 +
+
` +}) + +Vue.component("http-firewall-rules-box", { + props: ["v-rules", "v-type"], + data: function () { + let rules = this.vRules + if (rules == null) { + rules = [] + } + return { + rules: rules + } + }, + methods: { + addRule: function () { + window.UPDATING_RULE = null + let that = this + teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { + height: "30em", + callback: function (resp) { + that.rules.push(resp.data.rule) + } + }) + }, + updateRule: function (index, rule) { + window.UPDATING_RULE = teaweb.clone(rule) + let that = this + teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { + height: "30em", + callback: function (resp) { + Vue.set(that.rules, index, resp.data.rule) + } + }) + }, + removeRule: function (index) { + let that = this + teaweb.confirm("确定要删除此规则吗?", function () { + that.rules.$remove(index) + }) + }, + operatorName: function (operatorCode) { + let operatorName = operatorCode + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + } + }) + } + + return operatorName + }, + operatorDescription: function (operatorCode) { + let operatorName = operatorCode + let operatorDescription = "" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorName = v.name + operatorDescription = v.description + } + }) + } + + return operatorName + ": " + operatorDescription + }, + operatorDataType: function (operatorCode) { + let operatorDataType = "none" + if (typeof (window.WAF_RULE_OPERATORS) != null) { + window.WAF_RULE_OPERATORS.forEach(function (v) { + if (v.code == operatorCode) { + operatorDataType = v.dataType + } + }) + } + + return operatorDataType + }, + calculateParamName: function (param) { + let paramName = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + } + }) + } + return paramName + }, + calculateParamDescription: function (param) { + let paramName = "" + let paramDescription = "" + if (param != null) { + window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { + if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { + paramName = checkpoint.name + paramDescription = checkpoint.description + } + }) + } + return paramName + ": " + paramDescription + }, + isEmptyString: function (v) { + return typeof v == "string" && v.length == 0 + } + }, + template: `
+ +
+
+ {{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} + + + + {{rule.checkpointOptions.period}}秒内请求数 + + + + + 允许{{rule.checkpointOptions.allowDomains}} + 禁止{{rule.checkpointOptions.denyDomains}} + + + + | {{paramFilter.code}} <{{operatorName(rule.operator)}}> + {{rule.value}} + [空] + + + + ({{rule.description}}) + + + +
+
+
+ +
` +}) + +// 通用Header长度 +let defaultGeneralHeaders = ["Cache-Control", "Connection", "Date", "Pragma", "Trailer", "Transfer-Encoding", "Upgrade", "Via", "Warning"] +Vue.component("http-cond-general-header-length", { + props: ["v-checkpoint"], + data: function () { + let headers = null + let length = null + + if (window.parent.UPDATING_RULE != null) { + let options = window.parent.UPDATING_RULE.checkpointOptions + if (options.headers != null && Array.$isArray(options.headers)) { + headers = options.headers + } + if (options.length != null) { + length = options.length + } + } + + + if (headers == null) { + headers = defaultGeneralHeaders + } + + if (length == null) { + length = 128 + } + + let that = this + setTimeout(function () { + that.change() + }, 100) + + return { + headers: headers, + length: length + } + }, + watch: { + length: function (v) { + let len = parseInt(v) + if (isNaN(len)) { + len = 0 + } + if (len < 0) { + len = 0 + } + this.length = len + this.change() + } + }, + methods: { + change: function () { + this.vCheckpoint.options = [ + { + code: "headers", + value: this.headers + }, + { + code: "length", + value: this.length + } + ] + } + }, + template: `
+ + + + + + + + + +
通用Header列表 + +

需要检查的Header列表。

+
Header值超出长度 +
+ + 字节 +
+

超出此长度认为匹配成功,0表示不限制。

+
+
` +}) + +// CC +Vue.component("http-firewall-checkpoint-cc", { + props: ["v-checkpoint"], + data: function () { + let keys = [] + let period = 60 + let threshold = 1000 + let ignoreCommonFiles = true + let enableFingerprint = true + + let options = {} + if (window.parent.UPDATING_RULE != null) { + options = window.parent.UPDATING_RULE.checkpointOptions + } + + if (options == null) { + options = {} + } + if (options.keys != null) { + keys = options.keys + } + if (keys.length == 0) { + keys = ["${remoteAddr}", "${requestPath}"] + } + if (options.period != null) { + period = options.period + } + if (options.threshold != null) { + threshold = options.threshold + } + if (options.ignoreCommonFiles != null && typeof (options.ignoreCommonFiles) == "boolean") { + ignoreCommonFiles = options.ignoreCommonFiles + } + if (options.enableFingerprint != null && typeof (options.enableFingerprint) == "boolean") { + enableFingerprint = options.enableFingerprint + } + + let that = this + setTimeout(function () { + that.change() + }, 100) + + return { + keys: keys, + period: period, + threshold: threshold, + ignoreCommonFiles: ignoreCommonFiles, + enableFingerprint: enableFingerprint, + options: {}, + value: threshold + } + }, + watch: { + period: function () { + this.change() + }, + threshold: function () { + this.change() + }, + ignoreCommonFiles: function () { + this.change() + }, + enableFingerprint: function () { + this.change() + } + }, + methods: { + changeKeys: function (keys) { + this.keys = keys + this.change() + }, + change: function () { + let period = parseInt(this.period.toString()) + if (isNaN(period) || period <= 0) { + period = 60 + } + + let threshold = parseInt(this.threshold.toString()) + if (isNaN(threshold) || threshold <= 0) { + threshold = 1000 + } + this.value = threshold + + let ignoreCommonFiles = this.ignoreCommonFiles + if (typeof ignoreCommonFiles != "boolean") { + ignoreCommonFiles = false + } + + let enableFingerprint = this.enableFingerprint + if (typeof enableFingerprint != "boolean") { + enableFingerprint = true + } + + this.vCheckpoint.options = [ + { + code: "keys", + value: this.keys + }, + { + code: "period", + value: period, + }, + { + code: "threshold", + value: threshold + }, + { + code: "ignoreCommonFiles", + value: ignoreCommonFiles + }, + { + code: "enableFingerprint", + value: enableFingerprint + } + ] + }, + thresholdTooLow: function () { + let threshold = parseInt(this.threshold.toString()) + if (isNaN(threshold) || threshold <= 0) { + threshold = 1000 + } + return threshold > 0 && threshold < 5 + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + +
统计对象组合 * + +
统计周期 * +
+ + +
+
阈值 * + +

对于网站类应用来说,当前阈值设置的太低,有可能会影响用户正常访问。

+
检查请求来源指纹 + +

在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。

+
忽略常用文件 + +

忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。

+
+
` +}) + +// 防盗链 +Vue.component("http-firewall-checkpoint-referer-block", { + props: ["v-checkpoint"], + data: function () { + let allowEmpty = true + let allowSameDomain = true + let allowDomains = [] + let denyDomains = [] + let checkOrigin = true + + let options = {} + if (window.parent.UPDATING_RULE != null) { + options = window.parent.UPDATING_RULE.checkpointOptions + } + + if (options == null) { + options = {} + } + if (typeof (options.allowEmpty) == "boolean") { + allowEmpty = options.allowEmpty + } + if (typeof (options.allowSameDomain) == "boolean") { + allowSameDomain = options.allowSameDomain + } + if (options.allowDomains != null && typeof (options.allowDomains) == "object") { + allowDomains = options.allowDomains + } + if (options.denyDomains != null && typeof (options.denyDomains) == "object") { + denyDomains = options.denyDomains + } + if (typeof options.checkOrigin == "boolean") { + checkOrigin = options.checkOrigin + } + + let that = this + setTimeout(function () { + that.change() + }, 100) + + return { + allowEmpty: allowEmpty, + allowSameDomain: allowSameDomain, + allowDomains: allowDomains, + denyDomains: denyDomains, + checkOrigin: checkOrigin, + options: {}, + value: 0 + } + }, + watch: { + allowEmpty: function () { + this.change() + }, + allowSameDomain: function () { + this.change() + }, + checkOrigin: function () { + this.change() + } + }, + methods: { + changeAllowDomains: function (values) { + this.allowDomains = values + this.change() + }, + changeDenyDomains: function (values) { + this.denyDomains = values + this.change() + }, + change: function () { + this.vCheckpoint.options = [ + { + code: "allowEmpty", + value: this.allowEmpty + }, + { + code: "allowSameDomain", + value: this.allowSameDomain, + }, + { + code: "allowDomains", + value: this.allowDomains + }, + { + code: "denyDomains", + value: this.denyDomains + }, + { + code: "checkOrigin", + value: this.checkOrigin + } + ] + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + +
来源域名允许为空 + +

允许不带来源的访问。

+
来源域名允许一致 + +

允许来源域名和当前访问的域名一致,相当于在站内访问。

+
允许的来源域名 + +

允许的来源域名列表,比如example.com(顶级域名)、*.example.com(example.com的所有二级域名)。单个星号*表示允许所有域名。

+
禁止的来源域名 + +

禁止的来源域名列表,比如example.org(顶级域名)、*.example.org(example.org的所有二级域名);除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。

+
同时检查Origin + +

如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。

+
+
` +}) + +Vue.component("http-header-assistant", { + props: ["v-type", "v-value"], + mounted: function () { + let that = this + Tea.action("/servers/headers/options?type=" + this.vType) + .post() + .success(function (resp) { + that.allHeaders = resp.data.headers + }) + }, + data: function () { + return { + allHeaders: [], + matchedHeaders: [], + + selectedHeaderName: "" + } + }, + watch: { + vValue: function (v) { + if (v != this.selectedHeaderName) { + this.selectedHeaderName = "" + } + + if (v.length == 0) { + this.matchedHeaders = [] + return + } + this.matchedHeaders = this.allHeaders.filter(function (header) { + return teaweb.match(header, v) + }).slice(0, 10) + } + }, + methods: { + select: function (header) { + this.$emit("select", header) + this.selectedHeaderName = header + } + }, + template: ` + {{header}} +     +` +}) + Vue.component("http-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 () { @@ -7898,8 +11124,610 @@ Vue.component("http-header-policy-box", { ` }) -Vue.component("server-feature-required", { - template: `当前网站绑定的套餐或当前用户未开通此功能。` +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-hls-config-box", { + props: ["value", "v-is-location", "v-is-group"], + data: function () { + let config = this.value + if (config == null) { + config = { + isPrior: false + } + } + + let encryptingConfig = config.encrypting + if (encryptingConfig == null) { + encryptingConfig = { + isOn: false, + onlyURLPatterns: [], + exceptURLPatterns: [] + } + config.encrypting = encryptingConfig + } + + return { + config: config, + + encryptingConfig: encryptingConfig, + encryptingMoreOptionsVisible: false + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) + }, + + showEncryptingMoreOptions: function () { + this.encryptingMoreOptionsVisible = !this.encryptingMoreOptionsVisible + } + }, + template: `
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
启用HLS加密 + +

启用后,系统会自动在.m3u8文件中加入#EXT-X-KEY:METHOD=AES-128...,并将其中的.ts文件内容进行加密。

+
例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
限制URL + +

如果填写了限制URL,表示只对这些URL进行加密处理;如果不填则表示支持所有的URL。

+
+
+
` +}) + +Vue.component("http-host-redirect-box", { + props: ["v-redirects"], + mounted: function () { + let that = this + sortTable(function (ids) { + let newRedirects = [] + ids.forEach(function (id) { + that.redirects.forEach(function (redirect) { + if (redirect.id == id) { + newRedirects.push(redirect) + } + }) + }) + that.updateRedirects(newRedirects) + }) + }, + data: function () { + let redirects = this.vRedirects + if (redirects == null) { + redirects = [] + } + + let id = 0 + redirects.forEach(function (v) { + id++ + v.id = id + }) + + return { + redirects: redirects, + statusOptions: [ + {"code": 301, "text": "Moved Permanently"}, + {"code": 308, "text": "Permanent Redirect"}, + {"code": 302, "text": "Found"}, + {"code": 303, "text": "See Other"}, + {"code": 307, "text": "Temporary Redirect"} + ], + id: id + } + }, + methods: { + add: function () { + let that = this + window.UPDATING_REDIRECT = null + + teaweb.popup("/servers/server/settings/redirects/createPopup", { + width: "50em", + height: "36em", + callback: function (resp) { + that.id++ + resp.data.redirect.id = that.id + that.redirects.push(resp.data.redirect) + that.change() + } + }) + }, + update: function (index, redirect) { + let that = this + window.UPDATING_REDIRECT = redirect + + teaweb.popup("/servers/server/settings/redirects/createPopup", { + width: "50em", + height: "36em", + callback: function (resp) { + resp.data.redirect.id = redirect.id + Vue.set(that.redirects, index, resp.data.redirect) + that.change() + } + }) + }, + remove: function (index) { + let that = this + teaweb.confirm("确定要删除这条跳转规则吗?", function () { + that.redirects.$remove(index) + that.change() + }) + }, + change: function () { + let that = this + setTimeout(function (){ + that.$emit("change", that.redirects) + }, 100) + }, + updateRedirects: function (newRedirects) { + this.redirects = newRedirects + this.change() + } + }, + template: `
+ + + + [创建] + +
+ +

暂时还没有URL跳转规则。

+
+ + + + + + + + + + + + + + + + + + + + + + + +
跳转前跳转后HTTP状态码状态操作
+
+ {{redirect.beforeURL}} +
+ URL跳转 + 匹配前缀 + 正则匹配 + 精准匹配 + 排除:{{domain}} + 仅限:{{domain}} +
+
+
+ 所有域名 + + {{redirect.domainsBefore[0]}} + {{redirect.domainsBefore[0]}}等{{redirect.domainsBefore.length}}个域名 + +
+ 域名跳转 + {{redirect.domainAfterScheme}} + 忽略端口 +
+
+
+ 所有端口 + + {{redirect.portsBefore.join(", ")}} + {{redirect.portsBefore.slice(0, 5).join(", ")}}等{{redirect.portsBefore.length}}个端口 + +
+ 端口跳转 + {{redirect.portAfterScheme}} +
+
+ +
+ 匹配条件 +
+
-> + {{redirect.afterURL}} + {{redirect.domainAfter}} + {{redirect.portAfter}} + + {{redirect.status}} + 默认 + + 修改   + 删除 +
+

所有规则匹配顺序为从上到下,可以拖动左侧的排序。

+
+
+
` +}) + +// 请求方法列表 +Vue.component("http-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等。

+
+
+
+ +
+
` +}) + +// 压缩配置 +Vue.component("http-optimization-config-box", { + props: ["v-optimization-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vOptimizationConfig + + return { + config: config, + htmlMoreOptions: false, + javascriptMoreOptions: false, + cssMoreOptions: false + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + } + }, + template: `
+ + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
HTML优化 +
+ + +
+

可以自动优化HTML中包含的空白、注释、空标签等。只有文件可以缓存时才会被优化。

+
HTML例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
HTML限制URL + +

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

+
+ + + + + + + + + + + + + + + + + + + + + +
Javascript优化 +
+ + +
+

可以自动缩短Javascript中变量、函数名称等。只有文件可以缓存时才会被优化。

+
Javascript例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
Javascript限制URL + +

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

+
+ + + + + + + + + + + + + + + + + + + + + +
CSS优化 +
+ + +
+

可以自动去除CSS中包含的空白。只有文件可以缓存时才会被优化。

+
CSS例外URL + +

如果填写了例外URL,表示这些URL跳过不做处理。

+
CSS限制URL + +

如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。

+
+
+ +
+
` +}) + +Vue.component("http-oss-bucket-params", { + props: ["v-oss-config", "v-params", "name"], + data: function () { + let params = this.vParams + if (params == null) { + params = [] + } + + let ossConfig = this.vOssConfig + if (ossConfig == null) { + ossConfig = { + bucketParam: "input", + bucketName: "", + bucketArgName: "" + } + } else { + // 兼容以往 + if (ossConfig.bucketParam != null && ossConfig.bucketParam.length == 0) { + ossConfig.bucketParam = "input" + } + if (ossConfig.options != null && ossConfig.options.bucketName != null && ossConfig.options.bucketName.length > 0) { + ossConfig.bucketName = ossConfig.options.bucketName + } + } + + return { + params: params, + ossConfig: ossConfig + } + }, + template: ` + + {{name}}名称获取方式 * + + +

{{param.description.replace("\${optionName}", name)}}

+ + + + {{name}}名称 * + + +

{{name}}名称,类似于bucket-12345678

+ + + + {{name}}参数名称 * + + +

{{name}}参数名称,比如?myBucketName=BUCKET-NAME中的myBucketName

+ + +` }) Vue.component("http-pages-and-shutdown-box", { @@ -8165,19 +11993,736 @@ Vue.component("http-pages-and-shutdown-box", { ` }) -// 页面动态加密配置 -Vue.component("http-encryption-config-box", { - props: ["v-encryption-config", "v-is-location", "v-is-group"], +Vue.component("http-redirect-to-https-box", { + props: ["v-redirect-to-https-config", "v-is-location"], data: function () { - let config = this.vEncryptionConfig + 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-referers-config-box", { + props: ["v-referers-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vReferersConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + allowEmpty: true, + allowSameDomain: true, + allowDomains: [], + denyDomains: [], + checkOrigin: true + } + } + if (config.allowDomains == null) { + config.allowDomains = [] + } + if (config.denyDomains == null) { + config.denyDomains = [] + } + return { + config: config, + moreOptionsVisible: false + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + }, + changeAllowDomains: function (domains) { + if (typeof (domains) == "object") { + this.config.allowDomains = domains + this.$forceUpdate() + } + }, + changeDenyDomains: function (domains) { + if (typeof (domains) == "object") { + this.config.denyDomains = domains + this.$forceUpdate() + } + }, + showMoreOptions: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用防盗链 +
+ + +
+

选中后表示开启防盗链。

+
允许直接访问网站 + +

允许用户直接访问网站,用户第一次访问网站时来源域名通常为空。

+
来源域名允许一致 + +

允许来源域名和当前访问的域名一致,相当于在站内访问。

+
允许的来源域名 + > +

允许的其他来源域名列表,比如example.com*.example.com。单个星号*表示允许所有域名。

+
禁止的来源域名 + +

禁止的来源域名列表,比如example.org*.example.org;除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。

+
同时检查Origin + +

如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。

+
例外URL + +

如果填写了例外URL,表示这些URL不做处理。

+
限制URL + +

如果填写了限制URL,表示只对这些URL进行处理;如果不填则表示支持所有的URL。

+
+
+
` +}) + +Vue.component("http-remote-addr-config-box", { + props: ["v-remote-addr-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vRemoteAddrConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + value: "${rawRemoteAddr}", + type: "default", + + requestHeaderName: "" + } + } + + // type + if (config.type == null || config.type.length == 0) { + config.type = "default" + switch (config.value) { + case "${rawRemoteAddr}": + config.type = "default" + break + case "${remoteAddrValue}": + config.type = "default" + break + case "${remoteAddr}": + config.type = "proxy" + break + default: + if (config.value != null && config.value.length > 0) { + config.type = "variable" + } + } + } + + // value + if (config.value == null || config.value.length == 0) { + config.value = "${rawRemoteAddr}" + } return { config: config, - htmlMoreOptions: false, - javascriptMoreOptions: false, - keyPolicyMoreOptions: false, - cacheMoreOptions: false, - encryptionMoreOptions: false + options: [ + { + name: "直接获取", + description: "用户直接访问边缘节点,即 \"用户 --> 边缘节点\" 模式,这时候系统会试图从直接的连接中读取到客户端IP地址。", + value: "${rawRemoteAddr}", + type: "default" + }, + { + name: "从上级代理中获取", + description: "用户和边缘节点之间有别的代理服务转发,即 \"用户 --> [第三方代理服务] --> 边缘节点\",这时候只能从上级代理中获取传递的IP地址;上级代理传递的请求报头中必须包含 X-Forwarded-For 或 X-Real-IP 信息。", + value: "${remoteAddr}", + type: "proxy" + }, + { + name: "从请求报头中读取", + description: "从自定义请求报头读取客户端IP。", + value: "", + type: "requestHeader" + }, + { + name: "[自定义变量]", + description: "通过自定义变量来获取客户端真实的IP地址。", + value: "", + type: "variable" + } + ] + } + }, + watch: { + "config.requestHeaderName": function (value) { + if (this.config.type == "requestHeader"){ + this.config.value = "${header." + value.trim() + "}" + } + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + }, + changeOptionType: function () { + let that = this + + switch(this.config.type) { + case "default": + this.config.value = "${rawRemoteAddr}" + break + case "proxy": + this.config.value = "${remoteAddr}" + break + case "requestHeader": + this.config.value = "" + if (this.requestHeaderName != null && this.requestHeaderName.length > 0) { + this.config.value = "${header." + this.requestHeaderName + "}" + } + + setTimeout(function () { + that.$refs.requestHeaderInput.focus() + }) + break + case "variable": + this.config.value = "${rawRemoteAddr}" + + setTimeout(function () { + that.$refs.variableInput.focus() + }) + + break + } + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用访客IP设置 +
+ + +
+

选中后,表示使用自定义的请求变量获取客户端IP。

+
获取IP方式 * + +

{{option.description}}

+
请求报头 * + +

请输入包含有客户端IP的请求报头,需要注意大小写,常见的有X-Forwarded-ForX-Real-IPX-Client-IP等。

+
读取IP变量值 * + +

通过此变量获取用户的IP地址。具体可用的请求变量列表可参考官方网站文档;比如通过报头传递IP的情形,可以使用\${header.你的自定义报头}(类似于\${header.X-Forwarded-For},需要注意大小写规范)。

+
+
+
` +}) + +Vue.component("http-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-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("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-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: { @@ -8186,239 +12731,333 @@ Vue.component("http-encryption-config-box", { } }, template: `
- - - + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
启用请求限制 + +
最大并发连接数 + +

当前网站最大并发连接数,超出此限制则响应用户429代码。为0表示不限制。

+
单IP最大并发连接数 + +

单IP最大连接数,统计单个IP总连接数时不区分服务,超出此限制则响应用户429代码。为0表示不限制。当前设置的并发连接数过低,可能会影响正常用户访问,建议不小于3。

+
单连接带宽限制 + +

客户端单个请求每秒可以读取的下行流量。

+
单请求最大尺寸 + +

单个请求能发送的最大内容尺寸。

+
- -
-
- - - - - - - - - - - - - - - - -
启用页面动态加密 -
- - -
-

启用后,将对 HTML 页面中的 JavaScript 进行动态加密,有效抵御批量爬虫和脚本工具。

-
排除 URL - -

这些 URL 将跳过加密处理,支持正则表达式。

-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - -
HTML 加密 -
- - -
-

加密 HTML 页面中的 JavaScript 脚本。

-
加密内联脚本 -
- - -
-

加密 HTML 中的内联 <script> 标签内容。

-
加密外部脚本 -
- - -
-

加密通过 src 属性引入的外部 JavaScript 文件。

-
URL 匹配规则 - -

如果填写了匹配规则,表示只对这些 URL 进行加密处理;如果不填则表示支持所有的 URL。

-
- -
- - - - - - - - - - - - - - - - -
JavaScript 文件加密 -
- - -
-

加密独立的 JavaScript 文件(.js 文件)。

-
URL 匹配规则 - -

如果填写了匹配规则,表示只对这些 URL 进行加密处理;如果不填则表示支持所有的 URL。

-
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
服务器端密钥 - -

用于生成加密密钥的密码,建议使用复杂的随机字符串。默认密钥仅用于测试,生产环境请务必修改!

-
时间分片(秒) - -

加密密钥每隔多少秒更换一次。时间越短越安全,但可能影响性能。建议 60-120 秒,默认 60 秒。

-
IP CIDR 前缀长度 - -

将用户 IP 地址的前多少位作为识别依据。例如设置为 24 时,192.168.1.1 和 192.168.1.2 会被视为同一用户。默认 24。

-
简化 User-Agent -
- - -
-

开启后,只识别浏览器类型(如 Chrome、Firefox),忽略版本号等细节,避免因浏览器自动更新导致解密失败。

-
- -
- - - - - - - - - - - - - - - - - - - - -
启用缓存 -
- - -
-

开启后,相同内容的加密结果会被缓存,减少重复计算,提升响应速度。

-
缓存 TTL(秒) - -

缓存的有效期,超过这个时间后缓存会自动失效。建议与上面的"时间分片"保持一致。默认 60 秒。

-
最大缓存条目数 - -

最多缓存多少个加密结果。数量越大占用内存越多,建议根据服务器内存情况调整。默认 1000。

-
-
-
-
` }) +Vue.component("http-request-scripts-config-box", { + props: ["vRequestScriptsConfig", "v-auditing-status", "v-is-location"], + data: function () { + let config = this.vRequestScriptsConfig + if (config == null) { + config = {} + } + return { + config: config + } + }, + methods: { + changeInitGroup: function (group) { + this.config.initGroup = group + this.$forceUpdate() + }, + changeRequestGroup: function (group) { + this.config.requestGroup = group + this.$forceUpdate() + } + }, + template: `
+ +
+

请求初始化

+

在请求刚初始化时调用,此时自定义报头等尚未生效。

+
+ +
+

准备发送请求

+

在准备执行请求或者转发请求之前调用,此时自定义报头、源站等已准备好。

+
+ +
+
+
` +}) -// 压缩配置 -Vue.component("http-compression-config-box", { - props: ["v-compression-config", "v-is-location", "v-is-group"], +Vue.component("http-rewrite-rule-list", { + props: ["v-web-id", "v-rewrite-rules"], mounted: function () { - let that = this - sortLoad(function () { - that.initSortableTypes() - }) + setTimeout(this.sort, 1000) }, data: function () { - let config = this.vCompressionConfig + 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("http-stat-config-box", { + props: ["v-stat-config", "v-is-location"], + data: function () { + let stat = this.vStatConfig + if (stat == null) { + stat = { + isPrior: false, + isOn: false + } + } + return { + stat: stat + } + }, + template: `
+ + + + + + + + + +
启用统计 +
+ + +
+
+
+
` +}) + +// 请求方法列表 +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("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, - useDefaultTypes: true, - types: ["brotli", "gzip", "zstd", "deflate"], - level: 0, - decompressData: false, - gzipRef: null, - deflateRef: null, - brotliRef: null, - minLength: {count: 1, "unit": "kb"}, - maxLength: {count: 32, "unit": "mb"}, - mimeTypes: ["text/*", "application/javascript", "application/json", "application/atom+xml", "application/rss+xml", "application/xhtml+xml", "font/*", "image/svg+xml"], - extensions: [".js", ".json", ".html", ".htm", ".xml", ".css", ".woff2", ".txt"], - exceptExtensions: [".apk", ".ipa"], - conds: null, - enablePartialContent: false, - onlyURLPatterns: [], - exceptURLPatterns: [] + minLength: {count: 0, "unit": "kb"}, + maxLength: {count: 0, "unit": "kb"}, + mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico"], + extensions: [".png", ".jpeg", ".jpg", ".bmp", ".ico"], + conds: null } } - if (config.types == null) { - config.types = [] - } if (config.mimeTypes == null) { config.mimeTypes = [] } @@ -8426,49 +13065,9 @@ Vue.component("http-compression-config-box", { 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 + moreOptionsVisible: false } }, methods: { @@ -8483,14 +13082,6 @@ Vue.component("http-compression-config-box", { }) this.config.extensions = values }, - changeExceptExtensions: function (values) { - values.forEach(function (v, k) { - if (v.length > 0 && v[0] != ".") { - values[k] = "." + v - } - }) - this.config.exceptExtensions = values - }, changeMimeTypes: function (values) { this.config.mimeTypes = values }, @@ -8499,103 +13090,38 @@ Vue.component("http-compression-config-box", { }, 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: `
- + - + - - - - - - - - - - - - - - - + - + @@ -8612,341 +13138,172 @@ Vue.component("http-compression-config-box", {

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

- - - - - - - - - - - - + -
启用内容压缩启用WebP压缩
-
支持的扩展名 - -

含有这些扩展名的URL将会被压缩,不区分大小写。

-
例外扩展名 - -

含有这些扩展名的URL将不会被压缩,不区分大小写。

-
支持的MimeType - -

响应的Content-Type里包含这些MimeType的内容将会被压缩。

+

选中后表示开启自动WebP压缩;图片的宽和高均不能超过16383像素;只有满足缓存条件的图片内容才会被转换

压缩算法支持的扩展名 -
- - - -
-
-
-
-
- - -
-
-
- -

选择支持的压缩算法和优先顺序,拖动图表排序。

+ +

含有这些扩展名的URL将会被转成WebP,不区分大小写。

支持已压缩内容支持的MimeType - -

支持对已压缩内容尝试重新使用新的算法压缩;不选中表示保留当前的压缩格式。

+ +

响应的Content-Type里包含这些MimeType的内容将会被转成WebP。

支持Partial
Content
- -

支持对分片内容(PartialContent)的压缩;除非客户端有特殊要求,一般不需要启用。

-
例外URL - -

如果填写了例外URL,表示这些URL跳过不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行压缩处理;如果不填则表示支持所有的URL。

-
匹配条件 -
-
+ +
` }) -// HTTP CC防护配置 -Vue.component("http-cc-config-box", { - props: ["v-cc-config", "v-is-location", "v-is-group"], +Vue.component("http-websocket-box", { + props: ["v-websocket-ref", "v-websocket-config", "v-is-location"], data: function () { - let config = this.vCcConfig - if (config == null) { - config = { + let websocketRef = this.vWebsocketRef + if (websocketRef == null) { + websocketRef = { isPrior: false, isOn: false, - enableFingerprint: true, - enableGET302: true, - onlyURLPatterns: [], - exceptURLPatterns: [], - useDefaultThresholds: true, - ignoreCommonFiles: true + websocketId: 0 } } - if (config.thresholds == null || config.thresholds.length == 0) { - config.thresholds = [ - { - maxRequests: 0 - }, - { - maxRequests: 0 - }, - { - maxRequests: 0 - } - ] - } - - if (typeof config.enableFingerprint != "boolean") { - config.enableFingerprint = true - } - if (typeof config.enableGET302 != "boolean") { - config.enableGET302 = true - } - - if (config.onlyURLPatterns == null) { - config.onlyURLPatterns = [] - } - if (config.exceptURLPatterns == null) { - config.exceptURLPatterns = [] - } - return { - config: config, - moreOptionsVisible: false, - minQPSPerIP: config.minQPSPerIP, - useCustomThresholds: !config.useDefaultThresholds, - - thresholdMaxRequests0: this.maxRequestsStringAtThresholdIndex(config, 0), - thresholdMaxRequests1: this.maxRequestsStringAtThresholdIndex(config, 1), - thresholdMaxRequests2: this.maxRequestsStringAtThresholdIndex(config, 2) - } - }, - watch: { - minQPSPerIP: function (v) { - let qps = parseInt(v.toString()) - if (isNaN(qps) || qps < 0) { - qps = 0 - } - this.config.minQPSPerIP = qps - }, - thresholdMaxRequests0: function (v) { - this.setThresholdMaxRequests(0, v) - }, - thresholdMaxRequests1: function (v) { - this.setThresholdMaxRequests(1, v) - }, - thresholdMaxRequests2: function (v) { - this.setThresholdMaxRequests(2, v) - }, - useCustomThresholds: function (b) { - this.config.useDefaultThresholds = !b - } - }, - methods: { - maxRequestsStringAtThresholdIndex: function (config, index) { - if (config.thresholds == null) { - return "" - } - if (index < config.thresholds.length) { - let s = config.thresholds[index].maxRequests.toString() - if (s == "0") { - s = "" - } - return s - } - return "" - }, - setThresholdMaxRequests: function (index, v) { - let maxRequests = parseInt(v) - if (isNaN(maxRequests) || maxRequests < 0) { - maxRequests = 0 - } - if (index < this.config.thresholds.length) { - this.config.thresholds[index].maxRequests = maxRequests - } - }, - showMoreOptions: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用CC无感防护 - -

启用后,自动检测并拦截CC攻击。

-
例外URL - -

如果填写了例外URL,表示这些URL跳过CC防护不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行CC防护处理;如果不填则表示支持所有的URL。

-
忽略常用文件 - -

忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。

-
检查请求来源指纹 - -

在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。

-
启用GET302校验 - -

选中后,表示自动通过GET302方法来校验客户端。

-
单IP最低QPS -
- - 请求数/秒 -
-

当某个IP在1分钟内平均QPS达到此值时,才会开始检测;如果设置为0,表示任何访问都会检测。(注意这里设置的是检测开启阈值,不是拦截阈值,拦截阈值在当前表单下方可以设置)

-
使用自定义拦截阈值 - -
自定义拦截阈值设置 -
-
- 单IP每5秒最多 - - 请求 -
-
- -
-
- 单IP每60秒 - - 请求 -
-
-
-
- 单IP每300秒 - - 请求 -
-
-
-
-
` -}) - -Vue.component("prior-checkbox", { - props: ["v-config"], - data: function () { - return { - isPrior: this.vConfig.isPrior - } - }, - watch: { - isPrior: function (v) { - this.vConfig.isPrior = v - } - }, - template: ` - - 打开独立配置 - -
- - -
-

[已打开] 打开后可以覆盖父级或子级配置。

- - -` -}) - -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, + let websocketConfig = this.vWebsocketConfig + if (websocketConfig == null) { + websocketConfig = { + id: 0, isOn: false, - charset: "", - isUpper: false, - force: 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 { - charsetConfig: charsetConfig, + 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.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: `
- + + - - + + - + - + - - + - - + - + + + + - + + + + + + + + + + + + + + @@ -8955,287 +13312,149 @@ Vue.component("http-charsets-box", { ` }) -Vue.component("http-expires-time-config-box", { - props: ["v-expires-time"], +// 指标对象 +Vue.component("metric-keys-config-box", { + props: ["v-keys"], data: function () { - let expiresTime = this.vExpiresTime - if (expiresTime == null) { - expiresTime = { - isPrior: false, - isOn: false, - overwrite: true, - autoCalculate: true, - duration: {count: -1, "unit": "hour"} - } + let keys = this.vKeys + if (keys == null) { + keys = [] } return { - expiresTime: expiresTime + keys: keys, + isAdding: false, + key: "", + subKey: "", + keyDescription: "", + + keyDefs: window.METRIC_HTTP_KEYS } }, watch: { - "expiresTime.isPrior": function () { - this.notifyChange() - }, - "expiresTime.isOn": function () { - this.notifyChange() - }, - "expiresTime.overwrite": function () { - this.notifyChange() - }, - "expiresTime.autoCalculate": function () { - this.notifyChange() + keys: function () { + this.$emit("change", this.keys) } }, methods: { - notifyChange: function () { - this.$emit("change", this.expiresTime) - } - }, - template: `
-
启用字符编码启用Websocket
- +
选择字符编码 + 允许所有来源域(Origin) +
+ + +
+

选中表示允许所有的来源域。

强制替换允许的来源域列表(Origin) - -

选中后,表示强制覆盖已经设置的字符集;不选中,表示如果源站已经设置了字符集,则保留不修改。

+
+
+ {{origin}} +
+
+
+ +

只允许在列表中的来源域名访问Websocket服务。

字符编码大写传递请求来源域
- +
-

选中后将指定的字符编码转换为大写,比如默认为utf-8,选中后将改为UTF-8

+

选中后,表示把接收到的请求中的Origin字段传递到源站。

+
指定传递的来源域 + +

指定向源站传递的Origin字段值。

+
握手超时时间(Handshake) +
+
+ +
+
+ 秒 +
+
+

0表示使用默认的时间设置。

- - - - - - - - - - - - - - - - - - - -
启用 -

启用后,将会在响应的Header中添加Expires字段,浏览器据此会将内容缓存在客户端;同时,在管理后台执行清理缓存时,也将无法清理客户端已有的缓存。

-
覆盖源站设置 - -

选中后,会覆盖源站Header中已有的Expires字段。

-
自动计算时间 -

根据已设置的缓存有效期进行计算。

-
强制缓存时间 - -

从客户端访问的时间开始要缓存的时长。

-
-
` -}) - -Vue.component("http-access-log-box", { - props: ["v-access-log"], - 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" - } - } - 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); + cancel: function () { + this.key = "" + this.subKey = "" + this.keyDescription = "" + this.isAdding = false }, - showLog: function () { + 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 - let requestId = this.accessLog.requestId - this.$parent.$children.forEach(function (v) { - if (v.deselect != null) { - v.deselect() + setTimeout(function () { + if (that.$refs.key != null) { + that.$refs.key.focus() } - }) - this.select() - teaweb.popup("/servers/server/log/viewPopup?requestId=" + requestId, { - width: "50em", - height: "24em", - onClose: function () { - that.deselect() - } - }) + }, 100) }, - select: function () { - this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)" + remove: function (index) { + this.keys.$remove(index) }, - deselect: function () { - this.$refs.box.parentNode.style.cssText = "" - } - }, - template: `
- [{{accessLog.region}}] {{accessLog.remoteAddr}} [{{accessLog.timeLocal}}] "{{accessLog.requestMethod}} {{accessLog.scheme}}://{{accessLog.host}}{{accessLog.requestURI}} {{accessLog.proto}}" {{accessLog.status}} [cached] waf {{accessLog.firewallActions}} - {{tag}} - 耗时:{{formatCost(accessLog.requestTime)}} ms -   -
` -}) - -Vue.component("http-access-log-config-box", { - props: ["v-access-log-config", "v-fields", "v-default-field-codes", "v-access-log-policies", "v-is-location", "v-is-group"], - data: function () { - let that = this - - // 初始化 - setTimeout(function () { - that.changeFields() - that.changePolicy() - }, 100) - - let accessLog = { - isPrior: false, - isOn: false, - fields: [], - status1: true, - status2: true, - status3: true, - status4: true, - status5: true, - - storageOnly: false, - storagePolicies: [], - - firewallOnly: 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) + changeKey: function () { + if (this.key.length == 0) { + return } - }) - this.vAccessLogPolicies.forEach(function (v) { - v.isChecked = accessLog.storagePolicies.$contains(v.id) - }) - - return { - accessLog: accessLog, - showAdvancedOptions: false - } - }, - methods: { - changeFields: function () { - this.accessLog.fields = this.vFields.filter(function (v) { - return v.isChecked - }).map(function (v) { - return v.code + let that = this + let def = this.keyDefs.$find(function (k, v) { + return v.code == that.key }) + if (def != null) { + this.keyDescription = def.description + } }, - changePolicy: function () { - this.accessLog.storagePolicies = this.vAccessLogPolicies.filter(function (v) { - return v.isChecked - }).map(function (v) { - return v.id + 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 }, - changeAdvanced: function (v) { - this.showAdvancedOptions = v + 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: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用访问日志 -
- - -
-
要存储的访问日志字段 -
- - -
-
要存储的访问日志状态码 -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
选择输出的日志策略 - 暂时还没有缓存策略。 -
-
- - -
-
-
是否只输出到日志策略 -
- - -
-

选中表示只输出日志到日志策略,而停止默认的日志存储。

-
只记录WAF相关日志 - -

选中后只记录WAF相关的日志。

-
-
+ +
+
+ {{keyName(key)}}   +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+

{{keyDescription}}

+
+
+ +
` }) @@ -9374,220 +13593,222 @@ Vue.component("origin-input-box", { ` }) -// 基本认证用户配置 -Vue.component("http-auth-basic-auth-user-box", { - props: ["v-users"], +Vue.component("origin-list-box", { + props: ["v-primary-origins", "v-backup-origins", "v-server-type", "v-params"], data: function () { - let users = this.vUsers - if (users == null) { - users = [] - } return { - users: users, - isAdding: false, - updatingIndex: -1, - - username: "", - password: "" + primaryOrigins: this.vPrimaryOrigins, + backupOrigins: this.vBackupOrigins } }, 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("script-config-box", { - props: ["id", "v-script-config", "comment", "v-auditing-status"], - data: function () { - let config = this.vScriptConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - code: "", - auditingCode: "" - } - } - - let auditingStatus = null - if (config.auditingCodeMD5 != null && config.auditingCodeMD5.length > 0 && config.auditingCode != null && config.auditingCode.length > 0) { - config.code = config.auditingCode - - if (this.vAuditingStatus != null) { - for (let i = 0; i < this.vAuditingStatus.length; i ++ ){ - let status = this.vAuditingStatus[i] - if (status.md5 == config.auditingCodeMD5) { - auditingStatus = status - break - } + createPrimaryOrigin: function () { + teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, { + width: "45em", + height: "27em", + callback: function (resp) { + teaweb.success("保存成功", function () { + window.location.reload() + }) } + }) + }, + createBackupOrigin: function () { + teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, { + width: "45em", + height: "27em", + callback: function (resp) { + teaweb.success("保存成功", function () { + window.location.reload() + }) + } + }) + }, + updateOrigin: function (originId, originType) { + teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, { + width: "45em", + height: "27em", + callback: function (resp) { + teaweb.success("保存成功", function () { + window.location.reload() + }) + } + }) + }, + deleteOrigin: function (originId, originAddr, originType) { + let that = this + teaweb.confirm("确定要删除此源站(" + originAddr + ")吗?", function () { + Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType) + .post() + .success(function () { + teaweb.success("删除成功", function () { + window.location.reload() + }) + }) + }) + }, + updateOriginIsOn: function (originId, originAddr, isOn) { + let message + let resultMessage + if (isOn) { + message = "确定要启用此源站(" + originAddr + ")吗?" + resultMessage = "启用成功" + } else { + message = "确定要停用此源站(" + originAddr + ")吗?" + resultMessage = "停用成功" } - } - - if (config.code.length == 0) { - config.code = "\n\n\n\n" - } - - return { - config: config, - auditingStatus: auditingStatus - } - }, - watch: { - "config.isOn": function () { - this.change() - } - }, - methods: { - change: function () { - this.$emit("change", this.config) - }, - changeCode: function (code) { - this.config.code = code - this.change() - } - }, - template: `
- - - - - - - - - - - - - -
启用脚本设置
脚本代码 -

- 管理员审核结果:审核通过。 - 管理员审核结果:驳回     驳回理由:{{auditingStatus.rejectedReason}} - 当前脚本将在审核后生效,请耐心等待审核结果。 -

-

管理员审核结果:审核通过。

- {{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" + let that = this + teaweb.confirm(message, function () { + Tea.action("/servers/server/settings/origins/updateIsOn?" + that.vParams + "&originId=" + originId + "&isOn=" + (isOn ? 1 : 0)) + .post() + .success(function () { + teaweb.success(resultMessage, function () { + window.location.reload() + }) + }) }) } }, template: `
-
-
- {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   -
-
+

主要源站 [添加主要源站]

+

暂时还没有主要源站。

+ + +

备用源站 [添加备用源站]

+

暂时还没有备用源站。

+
` }) +Vue.component("origin-list-table", { + props: ["v-origins", "v-origin-type"], + data: function () { + let hasMatchedDomains = false + let origins = this.vOrigins + if (origins != null && origins.length > 0) { + origins.forEach(function (origin) { + if (origin.domains != null && origin.domains.length > 0) { + hasMatchedDomains = true + } + }) + } + + return { + hasMatchedDomains: hasMatchedDomains + } + }, + methods: { + deleteOrigin: function (originId, originAddr) { + this.$emit("deleteOrigin", originId, originAddr, this.vOriginType) + }, + updateOrigin: function (originId) { + this.$emit("updateOrigin", originId, this.vOriginType) + }, + updateOriginIsOn: function (originId, originAddr, isOn) { + this.$emit("updateOriginIsOn", originId, originAddr, isOn) + } + }, + template: ` + + + + + + + + + + + + + + + + + +
源站地址权重状态操作
+ {{origin.addr}}   +
+ 对象存储 + {{origin.name}} + 证书 + 主机名: {{origin.host}} + 端口跟随 + HTTP/2 + + 匹配: {{domain}} + 匹配: 所有域名 +
+
{{origin.weight}} + + + 修改   + 停用启用   + 删除 +
` +}) + +Vue.component("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("prior-checkbox", { + props: ["v-config"], + data: function () { + return { + isPrior: this.vConfig.isPrior + } + }, + watch: { + isPrior: function (v) { + this.vConfig.isPrior = v + } + }, + template: ` + + 打开独立配置 + +
+ + +
+

[已打开] 打开后可以覆盖父级或子级配置。

+ + +` +}) + Vue.component("reverse-proxy-box", { props: ["v-reverse-proxy-ref", "v-reverse-proxy-config", "v-is-location", "v-family"], data: function () { @@ -9802,339 +14023,125 @@ Vue.component("reverse-proxy-box", { ` }) -Vue.component("http-firewall-param-filters-box", { - props: ["v-filters"], +Vue.component("script-config-box", { + props: ["id", "v-script-config", "comment", "v-auditing-status"], 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 + let config = this.vScriptConfig if (config == null) { config = { isPrior: false, isOn: false, - value: "${rawRemoteAddr}", - type: "default", - - requestHeaderName: "" + code: "", + auditingCode: "" } } - // type - if (config.type == null || config.type.length == 0) { - config.type = "default" - switch (config.value) { - case "${rawRemoteAddr}": - config.type = "default" - break - case "${remoteAddrValue}": - config.type = "default" - break - case "${remoteAddr}": - config.type = "proxy" - break - default: - if (config.value != null && config.value.length > 0) { - config.type = "variable" + let auditingStatus = null + if (config.auditingCodeMD5 != null && config.auditingCodeMD5.length > 0 && config.auditingCode != null && config.auditingCode.length > 0) { + config.code = config.auditingCode + + if (this.vAuditingStatus != null) { + for (let i = 0; i < this.vAuditingStatus.length; i ++ ){ + let status = this.vAuditingStatus[i] + if (status.md5 == config.auditingCodeMD5) { + auditingStatus = status + break } + } } } - // value - if (config.value == null || config.value.length == 0) { - config.value = "${rawRemoteAddr}" + if (config.code.length == 0) { + config.code = "\n\n\n\n" } return { config: config, - options: [ - { - name: "直接获取", - description: "用户直接访问边缘节点,即 \"用户 --> 边缘节点\" 模式,这时候系统会试图从直接的连接中读取到客户端IP地址。", - value: "${rawRemoteAddr}", - type: "default" - }, - { - name: "从上级代理中获取", - description: "用户和边缘节点之间有别的代理服务转发,即 \"用户 --> [第三方代理服务] --> 边缘节点\",这时候只能从上级代理中获取传递的IP地址;上级代理传递的请求报头中必须包含 X-Forwarded-For 或 X-Real-IP 信息。", - value: "${remoteAddr}", - type: "proxy" - }, - { - name: "从请求报头中读取", - description: "从自定义请求报头读取客户端IP。", - value: "", - type: "requestHeader" - }, - { - name: "[自定义变量]", - description: "通过自定义变量来获取客户端真实的IP地址。", - value: "", - type: "variable" - } - ] + auditingStatus: auditingStatus } }, watch: { - "config.requestHeaderName": function (value) { - if (this.config.type == "requestHeader"){ - this.config.value = "${header." + value.trim() + "}" - } + "config.isOn": function () { + this.change() } }, methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + change: function () { + this.$emit("change", this.config) }, - changeOptionType: function () { - let that = this - - switch(this.config.type) { - case "default": - this.config.value = "${rawRemoteAddr}" - break - case "proxy": - this.config.value = "${remoteAddr}" - break - case "requestHeader": - this.config.value = "" - if (this.requestHeaderName != null && this.requestHeaderName.length > 0) { - this.config.value = "${header." + this.requestHeaderName + "}" - } - - setTimeout(function () { - that.$refs.requestHeaderInput.focus() - }) - break - case "variable": - this.config.value = "${rawRemoteAddr}" - - setTimeout(function () { - that.$refs.variableInput.focus() - }) - - break - } + changeCode: function (code) { + this.config.code = code + this.change() } }, template: `
- - - + - - + + - - - + + + - - - - - - - - - - - -
启用访客IP设置 -
- - -
-

选中后,表示使用自定义的请求变量获取客户端IP。

-
启用脚本设置
获取IP方式 *
脚本代码 - -

{{option.description}}

-
请求报头 * - -

请输入包含有客户端IP的请求报头,需要注意大小写,常见的有X-Forwarded-ForX-Real-IPX-Client-IP等。

-
读取IP变量值 * - -

通过此变量获取用户的IP地址。具体可用的请求变量列表可参考官方网站文档;比如通过报头传递IP的情形,可以使用\${header.你的自定义报头}(类似于\${header.X-Forwarded-For},需要注意大小写规范)。

+

+ 管理员审核结果:审核通过。 + 管理员审核结果:驳回     驳回理由:{{auditingStatus.rejectedReason}} + 当前脚本将在审核后生效,请耐心等待审核结果。 +

+

管理员审核结果:审核通过。

+ {{config.code}} +

{{comment}}

-
` }) -// 访问日志搜索框 -Vue.component("http-access-log-search-box", { - props: ["v-ip", "v-domain", "v-keyword", "v-cluster-id", "v-node-id"], +Vue.component("script-group-config-box", { + props: ["v-group", "v-auditing-status", "v-is-location"], data: function () { - let ip = this.vIp - if (ip == null) { - ip = "" + let group = this.vGroup + if (group == null) { + group = { + isPrior: false, + isOn: true, + scripts: [] + } + } + if (group.scripts == null) { + group.scripts = [] } - let domain = this.vDomain - if (domain == null) { - domain = "" - } - - let keyword = this.vKeyword - if (keyword == null) { - keyword = "" + let script = null + if (group.scripts.length > 0) { + script = group.scripts[group.scripts.length - 1] } return { - ip: ip, - domain: domain, - keyword: keyword, - clusterId: this.vClusterId + group: group, + script: script } }, methods: { - cleanIP: function () { - this.ip = "" - this.submit() + changeScript: function (script) { + this.group.scripts = [script] // 目前只支持单个脚本 + this.change() }, - 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 + change: function () { + this.$emit("change", this.group) } }, - template: `
-
-
-
-
- IP - - -
+ template: `
+ + +
+
+
-
-
- 域名 - - -
-
-
-
- 关键词 - - -
-
-
-
- -
-
-
` }) @@ -10159,1771 +14166,8 @@ Vue.component("server-config-copy-link", { template: `批量 ` }) -// 指标对象 -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-webp-config-box", { - props: ["v-webp-config", "v-is-location", "v-is-group", "v-require-cache"], - data: function () { - let config = this.vWebpConfig - if (config == null) { - config = { - isPrior: false, - isOn: false, - minLength: {count: 0, "unit": "kb"}, - maxLength: {count: 0, "unit": "kb"}, - mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico"], - extensions: [".png", ".jpeg", ".jpg", ".bmp", ".ico"], - conds: null - } - } - - if (config.mimeTypes == null) { - config.mimeTypes = [] - } - if (config.extensions == null) { - config.extensions = [] - } - - return { - config: config, - moreOptionsVisible: false - } - }, - methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn - }, - changeExtensions: function (values) { - values.forEach(function (v, k) { - if (v.length > 0 && v[0] != ".") { - values[k] = "." + v - } - }) - this.config.extensions = values - }, - changeMimeTypes: function (values) { - this.config.mimeTypes = values - }, - changeAdvancedVisible: function () { - this.moreOptionsVisible = !this.moreOptionsVisible - }, - changeConds: function (conds) { - this.config.conds = conds - } - }, - template: `
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
启用WebP压缩 -
- - -
-

选中后表示开启自动WebP压缩;图片的宽和高均不能超过16383像素;只有满足缓存条件的图片内容才会被转换

-
支持的扩展名 - -

含有这些扩展名的URL将会被转成WebP,不区分大小写。

-
支持的MimeType - -

响应的Content-Type里包含这些MimeType的内容将会被转成WebP。

-
内容最小长度 - -

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

-
内容最大长度 - -

0表示不限制,内容长度从文件尺寸或Content-Length中获取。

-
匹配条件 - -
-
-
` -}) - -Vue.component("origin-scheduling-view-box", { - props: ["v-scheduling", "v-params"], - data: function () { - let scheduling = this.vScheduling - if (scheduling == null) { - scheduling = {} - } - return { - scheduling: scheduling - } - }, - methods: { - update: function () { - teaweb.popup("/servers/server/settings/reverseProxy/updateSchedulingPopup?" + this.vParams, { - height: "21em", - callback: function () { - window.location.reload() - }, - }) - } - }, - template: `
-
- - - - - -
当前正在使用的算法 - {{scheduling.name}}   [修改] -

{{scheduling.description}}

-
-
` -}) - -Vue.component("http-hls-config-box", { - props: ["value", "v-is-location", "v-is-group"], - data: function () { - let config = this.value - if (config == null) { - config = { - isPrior: false - } - } - - let encryptingConfig = config.encrypting - if (encryptingConfig == null) { - encryptingConfig = { - isOn: false, - onlyURLPatterns: [], - exceptURLPatterns: [] - } - config.encrypting = encryptingConfig - } - - return { - config: config, - - encryptingConfig: encryptingConfig, - encryptingMoreOptionsVisible: false - } - }, - methods: { - isOn: function () { - return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) - }, - - showEncryptingMoreOptions: function () { - this.encryptingMoreOptionsVisible = !this.encryptingMoreOptionsVisible - } - }, - template: `
- - - -
- - - - - - - - - - - - - - - - - - - - - - - -
启用HLS加密 - -

启用后,系统会自动在.m3u8文件中加入#EXT-X-KEY:METHOD=AES-128...,并将其中的.ts文件内容进行加密。

-
例外URL - -

如果填写了例外URL,表示这些URL跳过不做处理。

-
限制URL - -

如果填写了限制URL,表示只对这些URL进行加密处理;如果不填则表示支持所有的URL。

-
-
-
` -}) - -Vue.component("http-oss-bucket-params", { - props: ["v-oss-config", "v-params", "name"], - data: function () { - let params = this.vParams - if (params == null) { - params = [] - } - - let ossConfig = this.vOssConfig - if (ossConfig == null) { - ossConfig = { - bucketParam: "input", - bucketName: "", - bucketArgName: "" - } - } else { - // 兼容以往 - if (ossConfig.bucketParam != null && ossConfig.bucketParam.length == 0) { - ossConfig.bucketParam = "input" - } - if (ossConfig.options != null && ossConfig.options.bucketName != null && ossConfig.options.bucketName.length > 0) { - ossConfig.bucketName = ossConfig.options.bucketName - } - } - - return { - params: params, - ossConfig: ossConfig - } - }, - template: ` - - {{name}}名称获取方式 * - - -

{{param.description.replace("\${optionName}", name)}}

- - - - {{name}}名称 * - - -

{{name}}名称,类似于bucket-12345678

- - - - {{name}}参数名称 * - - -

{{name}}参数名称,比如?myBucketName=BUCKET-NAME中的myBucketName

- - -` -}) - -Vue.component("http-request-scripts-config-box", { - props: ["vRequestScriptsConfig", "v-auditing-status", "v-is-location"], - data: function () { - let config = this.vRequestScriptsConfig - if (config == null) { - config = {} - } - - return { - config: config - } - }, - methods: { - changeInitGroup: function (group) { - this.config.initGroup = group - this.$forceUpdate() - }, - changeRequestGroup: function (group) { - this.config.requestGroup = group - this.$forceUpdate() - } - }, - template: `
- -
-

请求初始化

-

在请求刚初始化时调用,此时自定义报头等尚未生效。

-
- -
-

准备发送请求

-

在准备执行请求或者转发请求之前调用,此时自定义报头、源站等已准备好。

-
- -
-
-
` -}) - -Vue.component("http-request-cond-view", { - props: ["v-cond"], - data: function () { - return { - cond: this.vCond, - components: window.REQUEST_COND_COMPONENTS - } - }, - methods: { - typeName: function (cond) { - let c = this.components.$find(function (k, v) { - return v.type == cond.type - }) - if (c != null) { - return c.name; - } - return cond.param + " " + cond.operator - }, - updateConds: function (conds, simpleCond) { - for (let k in simpleCond) { - if (simpleCond.hasOwnProperty(k)) { - this.cond[k] = simpleCond[k] - } - } - }, - notifyChange: function () { - - } - }, - template: `
- - {{cond.param}} {{cond.operator}} - {{typeName(cond)}}: - {{cond.value}} - - -
` -}) - -Vue.component("http-header-assistant", { - props: ["v-type", "v-value"], - mounted: function () { - let that = this - Tea.action("/servers/headers/options?type=" + this.vType) - .post() - .success(function (resp) { - that.allHeaders = resp.data.headers - }) - }, - data: function () { - return { - allHeaders: [], - matchedHeaders: [], - - selectedHeaderName: "" - } - }, - watch: { - vValue: function (v) { - if (v != this.selectedHeaderName) { - this.selectedHeaderName = "" - } - - if (v.length == 0) { - this.matchedHeaders = [] - return - } - this.matchedHeaders = this.allHeaders.filter(function (header) { - return teaweb.match(header, v) - }).slice(0, 10) - } - }, - methods: { - select: function (header) { - this.$emit("select", header) - this.selectedHeaderName = header - } - }, - template: ` - {{header}} -     -` -}) - -Vue.component("http-firewall-rules-box", { - props: ["v-rules", "v-type"], - data: function () { - let rules = this.vRules - if (rules == null) { - rules = [] - } - return { - rules: rules - } - }, - methods: { - addRule: function () { - window.UPDATING_RULE = null - let that = this - teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { - height: "30em", - callback: function (resp) { - that.rules.push(resp.data.rule) - } - }) - }, - updateRule: function (index, rule) { - window.UPDATING_RULE = teaweb.clone(rule) - let that = this - teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, { - height: "30em", - callback: function (resp) { - Vue.set(that.rules, index, resp.data.rule) - } - }) - }, - removeRule: function (index) { - let that = this - teaweb.confirm("确定要删除此规则吗?", function () { - that.rules.$remove(index) - }) - }, - operatorName: function (operatorCode) { - let operatorName = operatorCode - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorName = v.name - } - }) - } - - return operatorName - }, - operatorDescription: function (operatorCode) { - let operatorName = operatorCode - let operatorDescription = "" - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorName = v.name - operatorDescription = v.description - } - }) - } - - return operatorName + ": " + operatorDescription - }, - operatorDataType: function (operatorCode) { - let operatorDataType = "none" - if (typeof (window.WAF_RULE_OPERATORS) != null) { - window.WAF_RULE_OPERATORS.forEach(function (v) { - if (v.code == operatorCode) { - operatorDataType = v.dataType - } - }) - } - - return operatorDataType - }, - calculateParamName: function (param) { - let paramName = "" - if (param != null) { - window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { - if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { - paramName = checkpoint.name - } - }) - } - return paramName - }, - calculateParamDescription: function (param) { - let paramName = "" - let paramDescription = "" - if (param != null) { - window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) { - if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) { - paramName = checkpoint.name - paramDescription = checkpoint.description - } - }) - } - return paramName + ": " + paramDescription - }, - isEmptyString: function (v) { - return typeof v == "string" && v.length == 0 - } - }, - template: `
- -
-
- {{rule.name}} {{calculateParamName(rule.param)}} {{rule.param}} - - - - {{rule.checkpointOptions.period}}秒内请求数 - - - - - 允许{{rule.checkpointOptions.allowDomains}} - 禁止{{rule.checkpointOptions.denyDomains}} - - - - | {{paramFilter.code}} <{{operatorName(rule.operator)}}> - {{rule.value}} - [空] - - - - ({{rule.description}}) - - - -
-
-
- -
` -}) - -// 请求方法列表 -Vue.component("http-methods-box", { - props: ["v-methods"], - data: function () { - let methods = this.vMethods - if (methods == null) { - methods = [] - } - return { - methods: methods, - isAdding: false, - addingMethod: "" - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.addingMethod.focus() - }, 100) - }, - confirm: function () { - let that = this - - // 删除其中的空格 - this.addingMethod = this.addingMethod.replace(/\s/g, "").toUpperCase() - - if (this.addingMethod.length == 0) { - teaweb.warn("请输入要添加的请求方法", function () { - that.$refs.addingMethod.focus() - }) - return - } - - // 是否已经存在 - if (this.methods.$contains(this.addingMethod)) { - teaweb.warn("此请求方法已经存在,无需重复添加", function () { - that.$refs.addingMethod.focus() - }) - return - } - - this.methods.push(this.addingMethod) - this.cancel() - }, - remove: function (index) { - this.methods.$remove(index) - }, - cancel: function () { - this.isAdding = false - this.addingMethod = "" - } - }, - template: `
- -
- - {{method}} -   - -
-
-
-
-
- -
-
- -   -
-
-

格式为大写,比如GETPOST等。

-
-
-
- -
-
` -}) - -// URL扩展名条件 -Vue.component("http-cond-url-extension", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPathLowerExtension}", - operator: "in", - value: "[]" - } - if (this.vCond != null && this.vCond.param == cond.param) { - cond.value = this.vCond.value - } - - let extensions = [] - try { - extensions = JSON.parse(cond.value) - } catch (e) { - - } - - return { - cond: cond, - extensions: extensions, // TODO 可以拖动排序 - - isAdding: false, - addingExt: "" - } - }, - watch: { - extensions: function () { - this.cond.value = JSON.stringify(this.extensions) - } - }, - methods: { - addExt: function () { - this.isAdding = !this.isAdding - - if (this.isAdding) { - let that = this - setTimeout(function () { - that.$refs.addingExt.focus() - }, 100) - } - }, - cancelAdding: function () { - this.isAdding = false - this.addingExt = "" - }, - confirmAdding: function () { - // TODO 做更详细的校验 - // TODO 如果有重复的则提示之 - - if (this.addingExt.length == 0) { - return - } - - let that = this - this.addingExt.split(/[,;,;|]/).forEach(function (ext) { - ext = ext.trim() - if (ext.length > 0) { - if (ext[0] != ".") { - ext = "." + ext - } - ext = ext.replace(/\s+/g, "").toLowerCase() - that.extensions.push(ext) - } - }) - - // 清除状态 - this.cancelAdding() - }, - removeExt: function (index) { - this.extensions.$remove(index) - } - }, - template: `
- -
-
{{ext}}
-
-
-
-
- -
-
- - -
-
-
- -
-

扩展名需要包含点(.)符号,例如.jpg.png之类;多个扩展名用逗号分割。

-
` -}) - -// 排除URL扩展名条件 -Vue.component("http-cond-url-not-extension", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPathLowerExtension}", - operator: "not in", - value: "[]" - } - if (this.vCond != null && this.vCond.param == cond.param) { - cond.value = this.vCond.value - } - - let extensions = [] - try { - extensions = JSON.parse(cond.value) - } catch (e) { - - } - - return { - cond: cond, - extensions: extensions, // TODO 可以拖动排序 - - isAdding: false, - addingExt: "" - } - }, - watch: { - extensions: function () { - this.cond.value = JSON.stringify(this.extensions) - } - }, - methods: { - addExt: function () { - this.isAdding = !this.isAdding - - if (this.isAdding) { - let that = this - setTimeout(function () { - that.$refs.addingExt.focus() - }, 100) - } - }, - cancelAdding: function () { - this.isAdding = false - this.addingExt = "" - }, - confirmAdding: function () { - // TODO 做更详细的校验 - // TODO 如果有重复的则提示之 - - if (this.addingExt.length == 0) { - return - } - if (this.addingExt[0] != ".") { - this.addingExt = "." + this.addingExt - } - this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase() - this.extensions.push(this.addingExt) - - // 清除状态 - this.cancelAdding() - }, - removeExt: function (index) { - this.extensions.$remove(index) - } - }, - template: `
- -
-
{{ext}}
-
-
-
-
- -
-
- - -
-
-
- -
-

扩展名需要包含点(.)符号,例如.jpg.png之类。

-
` -}) - -// 根据URL前缀 -Vue.component("http-cond-url-prefix", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "prefix", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof (this.vCond.value) == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static/images,不需要带域名。

-
` -}) - -Vue.component("http-cond-url-not-prefix", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "prefix", - value: "", - isReverse: true, - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

要排除的URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static/images,不需要带域名。

-
` -}) - -// 首页 -Vue.component("http-cond-url-eq-index", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "eq", - value: "/", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

检查URL路径是为/,不需要带域名。

-
` -}) - -// 全站 -Vue.component("http-cond-url-all", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "prefix", - value: "/", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

支持全站所有URL。

-
` -}) - -// URL精准匹配 -Vue.component("http-cond-url-eq", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "eq", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

完整的URL路径,通常以/开头,比如/static/ui.js,不需要带域名。

-
` -}) - -Vue.component("http-cond-url-not-eq", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "eq", - value: "", - isReverse: true, - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

要排除的完整的URL路径,通常以/开头,比如/static/ui.js,不需要带域名。

-
` -}) - -// URL正则匹配 -Vue.component("http-cond-url-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

匹配URL的正则表达式,比如^/static/(.*).js$,不需要带域名。

-
` -}) - -// 排除URL正则匹配 -Vue.component("http-cond-url-not-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "not regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

不要匹配URL的正则表达式,意即只要匹配成功则排除此条件,比如^/static/(.*).js$,不需要带域名。

-
` -}) - -// URL通配符 -Vue.component("http-cond-url-wildcard-match", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${requestPath}", - operator: "wildcard match", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

匹配URL的通配符,用星号(*)表示任意字符,比如(/images/*.png/static/*,不需要带域名。

-
` -}) - -// User-Agent正则匹配 -Vue.component("http-cond-user-agent-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${userAgent}", - operator: "regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

匹配User-Agent的正则表达式,比如Android|iPhone

-
` -}) - -// User-Agent正则不匹配 -Vue.component("http-cond-user-agent-not-regexp", { - props: ["v-cond"], - mounted: function () { - this.$refs.valueInput.focus() - }, - data: function () { - let cond = { - isRequest: true, - param: "${userAgent}", - operator: "not regexp", - value: "", - isCaseInsensitive: false - } - if (this.vCond != null && typeof this.vCond.value == "string") { - cond.value = this.vCond.value - } - return { - cond: cond - } - }, - methods: { - changeCaseInsensitive: function (isCaseInsensitive) { - this.cond.isCaseInsensitive = isCaseInsensitive - } - }, - template: `
- - -

匹配User-Agent的正则表达式,比如Android|iPhone,如果匹配,则排除此条件。

-
` -}) - -// 根据MimeType -Vue.component("http-cond-mime-type", { - props: ["v-cond"], - data: function () { - let cond = { - isRequest: false, - param: "${response.contentType}", - operator: "mime type", - value: "[]" - } - if (this.vCond != null && this.vCond.param == cond.param) { - cond.value = this.vCond.value - } - return { - cond: cond, - mimeTypes: JSON.parse(cond.value), // TODO 可以拖动排序 - - isAdding: false, - addingMimeType: "" - } - }, - watch: { - mimeTypes: function () { - this.cond.value = JSON.stringify(this.mimeTypes) - } - }, - methods: { - addMimeType: function () { - this.isAdding = !this.isAdding - - if (this.isAdding) { - let that = this - setTimeout(function () { - that.$refs.addingMimeType.focus() - }, 100) - } - }, - cancelAdding: function () { - this.isAdding = false - this.addingMimeType = "" - }, - confirmAdding: function () { - // TODO 做更详细的校验 - // TODO 如果有重复的则提示之 - - if (this.addingMimeType.length == 0) { - return - } - this.addingMimeType = this.addingMimeType.replace(/\s+/g, "") - this.mimeTypes.push(this.addingMimeType) - - // 清除状态 - this.cancelAdding() - }, - removeMimeType: function (index) { - this.mimeTypes.$remove(index) - } - }, - template: `
- -
-
{{mimeType}}
-
-
-
-
- -
-
- - -
-
-
- -
-

服务器返回的内容的MimeType,比如text/htmlimage/*等。

-
` -}) - -// 参数匹配 -Vue.component("http-cond-params", { - props: ["v-cond"], - mounted: function () { - let cond = this.vCond - if (cond == null) { - return - } - this.operator = cond.operator - - // stringValue - if (["regexp", "not regexp", "eq", "not", "prefix", "suffix", "contains", "not contains", "eq ip", "gt ip", "gte ip", "lt ip", "lte ip", "ip range"].$contains(cond.operator)) { - this.stringValue = cond.value - return - } - - // numberValue - if (["eq int", "eq float", "gt", "gte", "lt", "lte", "mod 10", "ip mod 10", "mod 100", "ip mod 100"].$contains(cond.operator)) { - this.numberValue = cond.value - return - } - - // modValue - if (["mod", "ip mod"].$contains(cond.operator)) { - let pieces = cond.value.split(",") - this.modDivValue = pieces[0] - if (pieces.length > 1) { - this.modRemValue = pieces[1] - } - return - } - - // stringValues - let that = this - if (["in", "not in", "file ext", "mime type"].$contains(cond.operator)) { - try { - let arr = JSON.parse(cond.value) - if (arr != null && (arr instanceof Array)) { - arr.forEach(function (v) { - that.stringValues.push(v) - }) - } - } catch (e) { - - } - return - } - - // versionValue - if (["version range"].$contains(cond.operator)) { - let pieces = cond.value.split(",") - this.versionRangeMinValue = pieces[0] - if (pieces.length > 1) { - this.versionRangeMaxValue = pieces[1] - } - return - } - }, - data: function () { - let cond = { - isRequest: true, - param: "", - operator: window.REQUEST_COND_OPERATORS[0].op, - value: "", - isCaseInsensitive: false - } - if (this.vCond != null) { - cond = this.vCond - } - return { - cond: cond, - operators: window.REQUEST_COND_OPERATORS, - operator: window.REQUEST_COND_OPERATORS[0].op, - operatorDescription: window.REQUEST_COND_OPERATORS[0].description, - variables: window.REQUEST_VARIABLES, - variable: "", - - // 各种类型的值 - stringValue: "", - numberValue: "", - - modDivValue: "", - modRemValue: "", - - stringValues: [], - - versionRangeMinValue: "", - versionRangeMaxValue: "" - } - }, - methods: { - changeVariable: function () { - let v = this.cond.param - if (v == null) { - v = "" - } - this.cond.param = v + this.variable - }, - changeOperator: function () { - let that = this - this.operators.forEach(function (v) { - if (v.op == that.operator) { - that.operatorDescription = v.description - } - }) - - this.cond.operator = this.operator - - // 移动光标 - let box = document.getElementById("variables-value-box") - if (box != null) { - setTimeout(function () { - let input = box.getElementsByTagName("INPUT") - if (input.length > 0) { - input[0].focus() - } - }, 100) - } - }, - changeStringValues: function (v) { - this.stringValues = v - this.cond.value = JSON.stringify(v) - } - }, - watch: { - stringValue: function (v) { - this.cond.value = v - }, - numberValue: function (v) { - // TODO 校验数字 - this.cond.value = v - }, - modDivValue: function (v) { - if (v.length == 0) { - return - } - let div = parseInt(v) - if (isNaN(div)) { - div = 1 - } - this.modDivValue = div - this.cond.value = div + "," + this.modRemValue - }, - modRemValue: function (v) { - if (v.length == 0) { - return - } - let rem = parseInt(v) - if (isNaN(rem)) { - rem = 0 - } - this.modRemValue = rem - this.cond.value = this.modDivValue + "," + rem - }, - versionRangeMinValue: function (v) { - this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue - }, - versionRangeMaxValue: function (v) { - this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue - } - }, - template: ` - - 参数值 - - -
-
- -
-
- -
-
-

其中可以使用变量,类似于\${requestPath},也可以是多个变量的组合。

- - - - 操作符 - -
- -

-
- - - - 对比值 - - -
- -

要匹配的正则表达式,比如^/static/(.+).js

-
- - -
- -

要对比的数字。

-
- - -
- -

参数值除以10的余数,在0-9之间。

-
-
- -

参数值除以100的余数,在0-99之间。

-
-
-
-
除:
-
- -
-
余:
-
- -
-
-
- - -
- -

和参数值一致的字符串。

-

和参数值不一致的字符串。

-

参数值的前缀。

-

参数值的后缀为此字符串。

-

参数值包含此字符串。

-

参数值不包含此字符串。

-
-
- -

添加参数值列表。

-

添加参数值列表。

-

添加扩展名列表,比如pnghtml,不包括点。

-

添加MimeType列表,类似于text/htmlimage/*

-
-
-
-
-
-
-
-
-
- - -
- -

要对比的IP。

-
-
- -

参数中IP转换成整数后除以10的余数,在0-9之间。

-
-
- -

参数中IP转换成整数后除以100的余数,在0-99之间。

-
- - - - 不区分大小写 - -
- - -
-

选中后表示对比时忽略参数值的大小写。

- - - -` -}) - -// 请求方法列表 -Vue.component("http-status-box", { - props: ["v-status-list"], - data: function () { - let statusList = this.vStatusList - if (statusList == null) { - statusList = [] - } - return { - statusList: statusList, - isAdding: false, - addingStatus: "" - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.addingStatus.focus() - }, 100) - }, - confirm: function () { - let that = this - - // 删除其中的空格 - this.addingStatus = this.addingStatus.replace(/\s/g, "").toUpperCase() - - if (this.addingStatus.length == 0) { - teaweb.warn("请输入要添加的状态码", function () { - that.$refs.addingStatus.focus() - }) - return - } - - // 是否已经存在 - if (this.statusList.$contains(this.addingStatus)) { - teaweb.warn("此状态码已经存在,无需重复添加", function () { - that.$refs.addingStatus.focus() - }) - return - } - - // 格式 - if (!this.addingStatus.match(/^\d{3}$/)) { - teaweb.warn("请输入正确的状态码", function () { - that.$refs.addingStatus.focus() - }) - return - } - - this.statusList.push(parseInt(this.addingStatus, 10)) - this.cancel() - }, - remove: function (index) { - this.statusList.$remove(index) - }, - cancel: function () { - this.isAdding = false - this.addingStatus = "" - } - }, - template: `
- -
- - {{status}} -   - -
-
-
-
-
- -
-
- -   -
-
-

格式为三位数字,比如200404等。

-
-
-
- -
-
` +Vue.component("server-feature-required", { + template: `当前网站绑定的套餐或当前用户未开通此功能。` }) Vue.component("server-group-selector", { @@ -11980,47 +14224,1097 @@ Vue.component("server-group-selector", {
` }) -Vue.component("script-group-config-box", { - props: ["v-group", "v-auditing-status", "v-is-location"], +Vue.component("server-name-box", { + props: ["v-server-names"], data: function () { - let group = this.vGroup - if (group == null) { - group = { - isPrior: false, - isOn: true, - scripts: [] - } + let serverNames = this.vServerNames; + if (serverNames == null) { + serverNames = [] } - 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 + serverNames: serverNames, + isSearching: false, + keyword: "" } }, methods: { - changeScript: function (script) { - this.group.scripts = [script] // 目前只支持单个脚本 - this.change() + 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) + setTimeout(that.submitForm, 100) + } + }); }, - change: function () { - this.$emit("change", this.group) + + removeServerName: function (index) { + this.serverNames.$remove(index) + }, + + updateServerName: function (index, serverName) { + window.UPDATING_SERVER_NAME = teaweb.clone(serverName) + let that = this + teaweb.popup("/servers/addServerNamePopup", { + callback: function (resp) { + var serverName = resp.data.serverName + Vue.set(that.serverNames, index, serverName) + setTimeout(that.submitForm, 100) + } + }); + }, + showSearchBox: function () { + this.isSearching = !this.isSearching + if (this.isSearching) { + let that = this + setTimeout(function () { + that.$refs.keywordRef.focus() + }, 200) + } else { + this.keyword = "" + } + }, + allServerNames: function () { + if (this.serverNames == null) { + return [] + } + let result = [] + this.serverNames.forEach(function (serverName) { + if (serverName.subNames != null && serverName.subNames.length > 0) { + serverName.subNames.forEach(function (subName) { + if (subName != null && subName.length > 0) { + if (!result.$contains(subName)) { + result.push(subName) + } + } + }) + } else if (serverName.name != null && serverName.name.length > 0) { + if (!result.$contains(serverName.name)) { + result.push(serverName.name) + } + } + }) + return result + }, + submitForm: function () { + Tea.runActionOn(this.$refs.serverNamesRef.form) + } + }, + 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("server-traffic-limit-status-viewer", { + props: ["value"], + data: function () { + let targetTypeName = "流量" + if (this.value != null) { + targetTypeName = this.targetTypeToName(this.value.targetType) + } + + return { + status: this.value, + targetTypeName: targetTypeName + } + }, + methods: { + targetTypeToName: function (targetType) { + switch (targetType) { + case "traffic": + return "流量" + case "request": + return "请求数" + case "websocketConnections": + return "Websocket连接数" + } + return "流量" + } + }, + template: ` + 已达到套餐当日{{targetTypeName}}限制 + 已达到套餐当月{{targetTypeName}}限制 + 已达到套餐总体{{targetTypeName}}限制 +` +}) + +Vue.component("ssl-certs-box", { + props: [ + "v-certs", // 证书列表 + "v-cert", // 单个证书 + "v-protocol", // 协议:https|tls + "v-view-size", // 弹窗尺寸:normal, mini + "v-single-mode", // 单证书模式 + "v-description", // 描述文字 + "v-domains", // 搜索的域名列表或者函数 + "v-user-id" // 用户ID + ], + data: function () { + let certs = this.vCerts + if (certs == null) { + certs = [] + } + if (this.vCert != null) { + certs.push(this.vCert) + } + + let description = this.vDescription + if (description == null || typeof (description) != "string") { + description = "" + } + + return { + certs: certs, + description: description + } + }, + methods: { + certIds: function () { + return this.certs.map(function (v) { + return v.id + }) + }, + // 删除证书 + removeCert: function (index) { + let that = this + teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { + that.certs.$remove(index) + }) + }, + + // 选择证书 + selectCert: function () { + let that = this + let width = "54em" + let height = "32em" + let viewSize = this.vViewSize + if (viewSize == null) { + viewSize = "normal" + } + if (viewSize == "mini") { + width = "35em" + height = "20em" + } + + let searchingDomains = [] + if (this.vDomains != null) { + if (typeof this.vDomains == "function") { + let resultDomains = this.vDomains() + if (resultDomains != null && typeof resultDomains == "object" && (resultDomains instanceof Array)) { + searchingDomains = resultDomains + } + } else if (typeof this.vDomains == "object" && (this.vDomains instanceof Array)) { + searchingDomains = this.vDomains + } + if (searchingDomains.length > 10000) { + searchingDomains = searchingDomains.slice(0, 10000) + } + } + + let selectedCertIds = this.certs.map(function (cert) { + return cert.id + }) + let userId = this.vUserId + if (userId == null) { + userId = 0 + } + + teaweb.popup("/servers/certs/selectPopup?viewSize=" + viewSize + "&searchingDomains=" + window.encodeURIComponent(searchingDomains.join(",")) + "&selectedCertIds=" + selectedCertIds.join(",") + "&userId=" + userId, { + width: width, + height: height, + callback: function (resp) { + if (resp.data.cert != null) { + that.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 上传证书 + uploadCert: function () { + let that = this + let userId = this.vUserId + if (typeof userId != "number" && typeof userId != "string") { + userId = 0 + } + teaweb.popup("/servers/certs/uploadPopup?userId=" + userId, { + height: "28em", + callback: function (resp) { + teaweb.success("上传成功", function () { + if (resp.data.cert != null) { + that.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + }) + } + }) + }, + + // 批量上传 + uploadBatch: function () { + let that = this + let userId = this.vUserId + if (typeof userId != "number" && typeof userId != "string") { + userId = 0 + } + teaweb.popup("/servers/certs/uploadBatchPopup?userId=" + userId, { + callback: function (resp) { + if (resp.data.cert != null) { + that.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 格式化时间 + formatTime: function (timestamp) { + return new Date(timestamp * 1000).format("Y-m-d") + }, + + // 判断是否显示选择|上传按钮 + buttonsVisible: function () { + return this.vSingleMode == null || !this.vSingleMode || this.certs == null || this.certs.length == 0 + } + }, + template: `
+ +
+
+ {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   +
+
+
+
+ 选择或上传证书后HTTPSTLS服务才能生效。 + {{description}} +
+
+
+   + |   +   +   +
+
` +}) + +Vue.component("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("ssl-config-box", { + props: [ + "v-ssl-policy", + "v-protocol", + "v-server-id", + "v-support-http3" + ], + created: function () { + let that = this + setTimeout(function () { + that.sortableCipherSuites() + }, 100) + }, + data: function () { + let policy = this.vSslPolicy + if (policy == null) { + policy = { + id: 0, + isOn: true, + certRefs: [], + certs: [], + clientCARefs: [], + clientCACerts: [], + clientAuthType: 0, + minVersion: "TLS 1.1", + hsts: null, + cipherSuitesIsOn: false, + cipherSuites: [], + http2Enabled: true, + http3Enabled: false, + ocspIsOn: false + } + } else { + if (policy.certRefs == null) { + policy.certRefs = [] + } + if (policy.certs == null) { + policy.certs = [] + } + if (policy.clientCARefs == null) { + policy.clientCARefs = [] + } + if (policy.clientCACerts == null) { + policy.clientCACerts = [] + } + if (policy.cipherSuites == null) { + policy.cipherSuites = [] + } + } + + let hsts = policy.hsts + let hstsMaxAgeString = "31536000" + if (hsts == null) { + hsts = { + isOn: false, + maxAge: 31536000, + includeSubDomains: false, + preload: false, + domains: [] + } + } + if (hsts.maxAge != null) { + hstsMaxAgeString = hsts.maxAge.toString() + } + + return { + policy: policy, + + // hsts + hsts: hsts, + hstsOptionsVisible: false, + hstsDomainAdding: false, + hstsMaxAgeString: hstsMaxAgeString, + addingHstsDomain: "", + hstsDomainEditingIndex: -1, + + // 相关数据 + allVersions: window.SSL_ALL_VERSIONS, + allCipherSuites: window.SSL_ALL_CIPHER_SUITES.$copy(), + modernCipherSuites: window.SSL_MODERN_CIPHER_SUITES, + intermediateCipherSuites: window.SSL_INTERMEDIATE_CIPHER_SUITES, + allClientAuthTypes: window.SSL_ALL_CLIENT_AUTH_TYPES, + cipherSuitesVisible: false, + + // 高级选项 + moreOptionsVisible: false + } + }, + watch: { + hsts: { + deep: true, + handler: function () { + this.policy.hsts = this.hsts + } + } + }, + methods: { + // 删除证书 + removeCert: function (index) { + let that = this + teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { + that.policy.certRefs.$remove(index) + that.policy.certs.$remove(index) + }) + }, + + // 选择证书 + selectCert: function () { + let that = this + let selectedCertIds = [] + if (this.policy != null && this.policy.certs.length > 0) { + this.policy.certs.forEach(function (cert) { + selectedCertIds.push(cert.id.toString()) + }) + } + let serverId = this.vServerId + if (serverId == null) { + serverId = 0 + } + teaweb.popup("/servers/certs/selectPopup?selectedCertIds=" + selectedCertIds + "&serverId=" + serverId, { + width: "50em", + height: "30em", + callback: function (resp) { + if (resp.data.cert != null && resp.data.certRef != null) { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + } + if (resp.data.certs != null && resp.data.certRefs != null) { + that.policy.certRefs.$pushAll(resp.data.certRefs) + that.policy.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 上传证书 + uploadCert: function () { + let that = this + let serverId = this.vServerId + if (typeof serverId != "number" && typeof serverId != "string") { + serverId = 0 + } + teaweb.popup("/servers/certs/uploadPopup?serverId=" + serverId, { + height: "30em", + callback: function (resp) { + teaweb.success("上传成功", function () { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + }) + } + }) + }, + + // 批量上传 + uploadBatch: function () { + let that = this + let serverId = this.vServerId + if (typeof serverId != "number" && typeof serverId != "string") { + serverId = 0 + } + teaweb.popup("/servers/certs/uploadBatchPopup?serverId=" + serverId, { + callback: function (resp) { + if (resp.data.cert != null) { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + } + if (resp.data.certs != null) { + that.policy.certRefs.$pushAll(resp.data.certRefs) + that.policy.certs.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 申请证书 + requestCert: function () { + // 已经在证书中的域名 + let excludeServerNames = [] + if (this.policy != null && this.policy.certs.length > 0) { + this.policy.certs.forEach(function (cert) { + excludeServerNames.$pushAll(cert.dnsNames) + }) + } + + let that = this + teaweb.popup("/servers/server/settings/https/requestCertPopup?serverId=" + this.vServerId + "&excludeServerNames=" + excludeServerNames.join(","), { + callback: function () { + that.policy.certRefs.push(resp.data.certRef) + that.policy.certs.push(resp.data.cert) + } + }) + }, + + // 更多选项 + changeOptionsVisible: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + }, + + // 格式化时间 + formatTime: function (timestamp) { + return new Date(timestamp * 1000).format("Y-m-d") + }, + + // 格式化加密套件 + formatCipherSuite: function (cipherSuite) { + return cipherSuite.replace(/(AES|3DES)/, "$1") + }, + + // 添加单个套件 + addCipherSuite: function (cipherSuite) { + if (!this.policy.cipherSuites.$contains(cipherSuite)) { + this.policy.cipherSuites.push(cipherSuite) + } + this.allCipherSuites.$removeValue(cipherSuite) + }, + + // 删除单个套件 + removeCipherSuite: function (cipherSuite) { + let that = this + teaweb.confirm("确定要删除此套件吗?", function () { + that.policy.cipherSuites.$removeValue(cipherSuite) + that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$findAll(function (k, v) { + return !that.policy.cipherSuites.$contains(v) + }) + }) + }, + + // 清除所选套件 + clearCipherSuites: function () { + let that = this + teaweb.confirm("确定要清除所有已选套件吗?", function () { + that.policy.cipherSuites = [] + that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$copy() + }) + }, + + // 批量添加套件 + addBatchCipherSuites: function (suites) { + var that = this + teaweb.confirm("确定要批量添加套件?", function () { + suites.$each(function (k, v) { + if (that.policy.cipherSuites.$contains(v)) { + return + } + that.policy.cipherSuites.push(v) + }) + }) + }, + + /** + * 套件拖动排序 + */ + sortableCipherSuites: function () { + var box = document.querySelector(".cipher-suites-box") + Sortable.create(box, { + draggable: ".label", + handle: ".icon.handle", + onStart: function () { + + }, + onUpdate: function (event) { + + } + }) + }, + + // 显示所有套件 + showAllCipherSuites: function () { + this.cipherSuitesVisible = !this.cipherSuitesVisible + }, + + // 显示HSTS更多选项 + showMoreHSTS: function () { + this.hstsOptionsVisible = !this.hstsOptionsVisible; + if (this.hstsOptionsVisible) { + this.changeHSTSMaxAge() + } + }, + + // 监控HSTS有效期修改 + changeHSTSMaxAge: function () { + var v = parseInt(this.hstsMaxAgeString) + if (isNaN(v) || v < 0) { + this.hsts.maxAge = 0 + this.hsts.days = "-" + return + } + this.hsts.maxAge = v + this.hsts.days = v / 86400 + if (this.hsts.days == 0) { + this.hsts.days = "-" + } + }, + + // 设置HSTS有效期 + setHSTSMaxAge: function (maxAge) { + this.hstsMaxAgeString = maxAge.toString() + this.changeHSTSMaxAge() + }, + + // 添加HSTS域名 + addHstsDomain: function () { + this.hstsDomainAdding = true + this.hstsDomainEditingIndex = -1 + let that = this + setTimeout(function () { + that.$refs.addingHstsDomain.focus() + }, 100) + }, + + // 修改HSTS域名 + editHstsDomain: function (index) { + this.hstsDomainEditingIndex = index + this.addingHstsDomain = this.hsts.domains[index] + this.hstsDomainAdding = true + let that = this + setTimeout(function () { + that.$refs.addingHstsDomain.focus() + }, 100) + }, + + // 确认HSTS域名添加 + confirmAddHstsDomain: function () { + this.addingHstsDomain = this.addingHstsDomain.trim() + if (this.addingHstsDomain.length == 0) { + return; + } + if (this.hstsDomainEditingIndex > -1) { + this.hsts.domains[this.hstsDomainEditingIndex] = this.addingHstsDomain + } else { + this.hsts.domains.push(this.addingHstsDomain) + } + this.cancelHstsDomainAdding() + }, + + // 取消HSTS域名添加 + cancelHstsDomainAdding: function () { + this.hstsDomainAdding = false + this.addingHstsDomain = "" + this.hstsDomainEditingIndex = -1 + }, + + // 删除HSTS域名 + removeHstsDomain: function (index) { + this.cancelHstsDomainAdding() + this.hsts.domains.$remove(index) + }, + + // 选择客户端CA证书 + selectClientCACert: function () { + let that = this + teaweb.popup("/servers/certs/selectPopup?isCA=1", { + width: "50em", + height: "30em", + callback: function (resp) { + if (resp.data.cert != null && resp.data.certRef != null) { + that.policy.clientCARefs.push(resp.data.certRef) + that.policy.clientCACerts.push(resp.data.cert) + } + if (resp.data.certs != null && resp.data.certRefs != null) { + that.policy.clientCARefs.$pushAll(resp.data.certRefs) + that.policy.clientCACerts.$pushAll(resp.data.certs) + } + that.$forceUpdate() + } + }) + }, + + // 上传CA证书 + uploadClientCACert: function () { + let that = this + teaweb.popup("/servers/certs/uploadPopup?isCA=1", { + height: "28em", + callback: function (resp) { + teaweb.success("上传成功", function () { + that.policy.clientCARefs.push(resp.data.certRef) + that.policy.clientCACerts.push(resp.data.cert) + }) + } + }) + }, + + // 删除客户端CA证书 + removeClientCACert: function (index) { + let that = this + teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () { + that.policy.clientCARefs.$remove(index) + that.policy.clientCACerts.$remove(index) + }) + } + }, + template: `
+

SSL/TLS相关配置

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用HTTP/2 +
+ + +
+
启用HTTP/3 +
+ + +
+
设置证书 +
+
+ {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   +
+
+
+
+ 选择或上传证书后HTTPSTLS服务才能生效。 +
+
+   + |   +   +   + |   + +
TLS最低版本 + +
加密算法套件(CipherSuites) +
+ + +
+
+
+
+ 已添加套件({{policy.cipherSuites.length}}): +
+ +   + +
+
+ + + +

点击可选套件添加。

+
+
开启HSTS +
+ + +
+

+ 开启后,会自动在响应Header中加入 + Strict-Transport-Security: + ... + max-age={{hsts.maxAge}} + ; includeSubDomains + ; preload + + + 修改 + +

+
HSTS有效时间(max-age) +
+
+ +
+
+ 秒 +
+
{{hsts.days}}天
+
+

+ [1年/365天]     + [6个月/182.5天]     + [1个月/30天] +

+
HSTS包含子域名(includeSubDomains) +
+ + +
+
HSTS预加载(preload) +
+ + +
+
HSTS生效的域名 +
+ {{domain}} +   + + + +
+
+
+ +
+
+ +   取消 +
+
+
+ +
+

如果没有设置域名的话,则默认支持所有的域名。

+
OCSP Stapling +

选中表示启用OCSP Stapling。

+
客户端认证方式 + +
客户端认证CA证书 +
+
+ {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   +
+
+
+   + +

用来校验客户端证书以增强安全性,通常不需要设置。

+
+
+
` +}) + +// UAM模式配置 +Vue.component("uam-config-box", { + props: ["v-uam-config", "v-is-location", "v-is-group"], + data: function () { + let config = this.vUamConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + addToWhiteList: true, + onlyURLPatterns: [], + exceptURLPatterns: [], + minQPSPerIP: 0, + keyLife: 0 + } + } + if (config.onlyURLPatterns == null) { + config.onlyURLPatterns = [] + } + if (config.exceptURLPatterns == null) { + config.exceptURLPatterns = [] + } + return { + config: config, + moreOptionsVisible: false, + minQPSPerIP: config.minQPSPerIP, + keyLife: config.keyLife + } + }, + watch: { + minQPSPerIP: function (v) { + let qps = parseInt(v.toString()) + if (isNaN(qps) || qps < 0) { + qps = 0 + } + this.config.minQPSPerIP = qps + }, + keyLife: function (v) { + let keyLife = parseInt(v) + if (isNaN(keyLife) || keyLife <= 0) { + keyLife = 0 + } + this.config.keyLife = keyLife + } + }, + methods: { + showMoreOptions: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + }, + changeConds: function (conds) { + this.config.conds = conds + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
启用5秒盾 + +

启用后,访问网站时,自动检查浏览器环境,阻止非正常访问。

+
验证有效期 +
+ + +
+

单个客户端验证通过后,在这个有效期内不再重复验证;如果为0则表示系统默认。

+
单IP最低QPS +
+ + 请求数/秒 +
+

当某个IP在1分钟内平均QPS达到此值时,才会触发5秒盾;如果设置为0,表示任何访问都会触发。

+
加入IP白名单 + +

选中后,表示验证通过后,将访问者IP加入到临时白名单中,此IP下次访问时不再校验5秒盾;此白名单只对5秒盾有效,不影响其他规则。此选项主要用于可能无法正常使用Cookie的网站。

+
例外URL + +

如果填写了例外URL,表示这些URL跳过5秒盾不做处理。

+
限制URL + +

如果填写了限制URL,表示只对这些URL进行5秒盾处理;如果不填则表示支持所有的URL。

+
匹配条件 + +
+
` }) @@ -12204,3300 +15498,6 @@ Vue.component("user-agent-config-box", {
` }) -Vue.component("http-firewall-region-selector", { - props: ["v-type", "v-countries"], - data: function () { - let countries = this.vCountries - if (countries == null) { - countries = [] - } - - return { - listType: this.vType, - countries: countries - } - }, - methods: { - addCountry: function () { - let selectedCountryIds = this.countries.map(function (country) { - return country.id - }) - let that = this - teaweb.popup("/servers/server/settings/waf/ipadmin/selectCountriesPopup?type=" + this.listType + "&selectedCountryIds=" + selectedCountryIds.join(","), { - width: "52em", - height: "30em", - callback: function (resp) { - that.countries = resp.data.selectedCountries - that.$forceUpdate() - that.notifyChange() - } - }) - }, - removeCountry: function (index) { - this.countries.$remove(index) - this.notifyChange() - }, - resetCountries: function () { - this.countries = [] - this.notifyChange() - }, - notifyChange: function () { - this.$emit("change", { - "countries": this.countries - }) - } - }, - template: `
- 暂时没有选择允许封禁的区域。 -
-
- - ({{country.letter}}){{country.name}} -
-
-
-   -
` -}) - -// 绑定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("/waf/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("/waf/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.value}} - - {{item.ipFrom}}  New   - - {{item.ipTo}} - - - -
- {{item.region}} - | {{item.isp}} -
-
{{item.isp}}
- -
- 添加于 {{item.createdTime}} - - @ - - [名单:{{item.list.name}}] - [名单:{{item.list.name}} - - - - [网站:{{item.policy.server.name}}] - [网站:{{item.policy.server.name}}] - [网站:{{item.policy.server.name}}] - - - - -
-
- IPv4 - IPv4 - IPv6 - 所有IP - - {{item.eventLevelName}} - - - -
- {{item.expiredTime}} -
- 已过期 -
-
- {{formatSeconds(item.lifeSeconds)}} - 已过期 -
-
- 不过期 -
- {{item.reason}} - - - - - - - - 修改   - 删除 -
-
` -}) - -Vue.component("ip-item-text", { - props: ["v-item"], - template: ` - * - - {{vItem.value}} - - {{vItem.ipFrom}} - - {{vItem.ipTo}} - - -   级别:{{vItem.eventLevelName}} -` -}) - -Vue.component("ip-box", { - props: ["v-ip"], - methods: { - popup: function () { - let ip = this.vIp - if (ip == null || ip.length == 0) { - let e = this.$refs.container - ip = e.innerText - if (ip == null) { - ip = e.textContent - } - } - - teaweb.popup("/servers/ipbox?ip=" + ip, { - width: "50em", - height: "30em" - }) - } - }, - template: `` -}) - -// 给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 () { - if (Tea && Tea.Vue && Tea.Vue.page) { - that.page = Tea.Vue.page; - } - }) - }, - template: `
-
-
` -}) - -Vue.component("network-addresses-box", { - props: ["v-server-type", "v-addresses", "v-protocol", "v-name"], - 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" - } - - return { - addresses: addresses, - protocol: protocol, - name: name - } - }, - watch: { - "vServerType": function () { - this.addresses = [] - } - }, - methods: { - addAddr: function () { - let that = this - window.UPDATING_ADDR = null - teaweb.popup("/servers/addPortPopup?serverType=" + this.vServerType + "&protocol=" + this.protocol, { - height: "16em", - callback: function (resp) { - var addr = resp.data.address - 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 - teaweb.popup("/servers/addPortPopup?serverType=" + this.vServerType + "&protocol=" + this.protocol, { - height: "16em", - 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) - } - }, - template: `
- -
-
- {{addr.protocol}}://{{addr.host}}*:{{addr.portRange}} - -
-
-
- [添加端口绑定] -
` -}) - -/** - * 保存按钮 - */ -Vue.component("submit-btn", { - 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"], - 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 () { - 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("bandwidth-size-capacity-view", { - props: ["v-value"], - data: function () { - let capacity = this.vValue - if (capacity != null && capacity.count > 0 && typeof capacity.unit === "string") { - capacity.unit = capacity.unit[0].toUpperCase() + capacity.unit.substring(1) + "ps" - } - return { - capacity: capacity - } - }, - template: ` - {{capacity.count}}{{capacity.unit}} -` -}) - -Vue.component("more-options-tbody", { - data: function () { - return { - isVisible: false - } - }, - methods: { - show: function () { - this.isVisible = !this.isVisible - this.$emit("change", this.isVisible) - } - }, - template: ` - - 更多选项收起选项 - -` -}) - -Vue.component("download-link", { - props: ["v-element", "v-file"], - 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 e = document.getElementById(this.vElement) - if (e == null) { - teaweb.warn("找不到要下载的内容") - return - } - let text = e.innerText - if (text == null) { - text = e.textContent - } - return Tea.url("/ui/download", { - file: this.file, - text: text - }) - } - }, - template: ``, -}) - -Vue.component("values-box", { - props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"], - data: function () { - let values = this.values; - if (values == null) { - values = []; - } - - if (this.vValues != null && typeof this.vValues == "object") { - values = this.vValues - } - - return { - "realValues": values, - "isUpdating": false, - "isAdding": false, - "index": 0, - "value": "", - isEditing: false - } - }, - methods: { - create: function () { - this.isAdding = true; - var that = this; - setTimeout(function () { - that.$refs.value.focus(); - }, 200); - }, - update: function (index) { - this.cancel() - this.isUpdating = true; - this.index = index; - this.value = this.realValues[index]; - var that = this; - setTimeout(function () { - that.$refs.value.focus(); - }, 200); - }, - confirm: function () { - if (this.value.length == 0) { - if (typeof(this.vAllowEmpty) != "boolean" || !this.vAllowEmpty) { - return - } - } - - // validate - if (typeof(this.validator) == "function") { - let resp = this.validator.call(this, this.value) - if (typeof resp == "object") { - if (typeof resp.isOk == "boolean" && !resp.isOk) { - if (typeof resp.message == "string") { - let that = this - teaweb.warn(resp.message, function () { - that.$refs.value.focus(); - }) - } - return - } - } - } - - if (this.isUpdating) { - Vue.set(this.realValues, this.index, this.value); - } else { - this.realValues.push(this.value); - } - this.cancel() - this.$emit("change", this.realValues) - }, - remove: function (index) { - this.realValues.$remove(index) - this.$emit("change", this.realValues) - }, - cancel: function () { - this.isUpdating = false; - this.isAdding = false; - this.value = ""; - }, - updateAll: function (values) { - this.realValues = values - }, - addValue: function (v) { - this.realValues.push(v) - }, - - startEditing: function () { - this.isEditing = !this.isEditing - }, - allValues: function () { - return this.realValues - } - }, - template: `
-
-
- {{value}} - [空] -
- [修改] -
-
-
-
- {{value}} - [空] - -   - -
-
-
- -
-
-
- -
-
- -
-
- -
-
-
-
- -
-
-
` -}); - -Vue.component("datetime-input", { - props: ["v-name", "v-timestamp"], - mounted: function () { - let that = this - teaweb.datepicker(this.$refs.dayInput, function (v) { - that.day = v - that.hour = "23" - that.minute = "59" - that.second = "59" - that.change() - }) - }, - data: function () { - let timestamp = this.vTimestamp - if (timestamp != null) { - timestamp = parseInt(timestamp) - if (isNaN(timestamp)) { - timestamp = 0 - } - } else { - timestamp = 0 - } - - let day = "" - let hour = "" - let minute = "" - let second = "" - - if (timestamp > 0) { - let date = new Date() - date.setTime(timestamp * 1000) - - let year = date.getFullYear().toString() - let month = this.leadingZero((date.getMonth() + 1).toString(), 2) - day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2) - - hour = this.leadingZero(date.getHours().toString(), 2) - minute = this.leadingZero(date.getMinutes().toString(), 2) - second = this.leadingZero(date.getSeconds().toString(), 2) - } - - return { - timestamp: timestamp, - day: day, - hour: hour, - minute: minute, - second: second, - - hasDayError: false, - hasHourError: false, - hasMinuteError: false, - hasSecondError: false - } - }, - methods: { - change: function () { - // day - if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) { - this.hasDayError = true - return - } - let pieces = this.day.split("-") - let year = parseInt(pieces[0]) - - let month = parseInt(pieces[1]) - if (month < 1 || month > 12) { - this.hasDayError = true - return - } - - let day = parseInt(pieces[2]) - if (day < 1 || day > 32) { - this.hasDayError = true - return - } - - this.hasDayError = false - - // hour - if (!/^\d+$/.test(this.hour)) { - this.hasHourError = true - return - } - let hour = parseInt(this.hour) - if (isNaN(hour)) { - this.hasHourError = true - return - } - if (hour < 0 || hour >= 24) { - this.hasHourError = true - return - } - this.hasHourError = false - - // minute - if (!/^\d+$/.test(this.minute)) { - this.hasMinuteError = true - return - } - let minute = parseInt(this.minute) - if (isNaN(minute)) { - this.hasMinuteError = true - return - } - if (minute < 0 || minute >= 60) { - this.hasMinuteError = true - return - } - this.hasMinuteError = false - - // second - if (!/^\d+$/.test(this.second)) { - this.hasSecondError = true - return - } - let second = parseInt(this.second) - if (isNaN(second)) { - this.hasSecondError = true - return - } - if (second < 0 || second >= 60) { - this.hasSecondError = true - return - } - this.hasSecondError = false - - let date = new Date(year, month - 1, day, hour, minute, second) - this.timestamp = Math.floor(date.getTime() / 1000) - }, - leadingZero: function (s, l) { - s = s.toString() - if (l <= s.length) { - return s - } - for (let i = 0; i < l - s.length; i++) { - s = "0" + s - } - return s - }, - resultTimestamp: function () { - return this.timestamp - }, - nextYear: function () { - let date = new Date() - date.setFullYear(date.getFullYear()+1) - this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) - this.hour = this.leadingZero(date.getHours(), 2) - this.minute = this.leadingZero(date.getMinutes(), 2) - this.second = this.leadingZero(date.getSeconds(), 2) - this.change() - }, - nextDays: function (days) { - let date = new Date() - date.setTime(date.getTime() + days * 86400 * 1000) - this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) - this.hour = this.leadingZero(date.getHours(), 2) - this.minute = this.leadingZero(date.getMinutes(), 2) - this.second = this.leadingZero(date.getSeconds(), 2) - this.change() - }, - nextHours: function (hours) { - let date = new Date() - date.setTime(date.getTime() + hours * 3600 * 1000) - this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2) - this.hour = this.leadingZero(date.getHours(), 2) - this.minute = this.leadingZero(date.getMinutes(), 2) - this.second = this.leadingZero(date.getSeconds(), 2) - this.change() - } - }, - template: `
- -
-
- -
-
-
:
-
-
:
-
-
-

常用时间:  1小时  |  1天  |  3天  |  1周  |  30天  |  1年 

-
` -}) - -// 启用状态标签 -Vue.component("label-on", { - props: ["v-is-on"], - template: '
已启用已停用
' -}) - -// 文字代码标签 -Vue.component("code-label", { - methods: { - click: function (args) { - this.$emit("click", args) - } - }, - template: `` -}) - -// tiny标签 -Vue.component("tiny-label", { - template: `` -}) - -Vue.component("tiny-basic-label", { - template: `` -}) - -// 更小的标签 -Vue.component("micro-basic-label", { - template: `` -}) - - -// 灰色的Label -Vue.component("grey-label", { - template: `` -}) - -// Plus专属 -Vue.component("plus-label", { - template: `` -}) - - -Vue.component("js-page", { - props: ["v-max"], - data: function () { - let max = this.vMax - if (max == null) { - max = 0 - } - return { - max: max, - page: 1 - } - }, - methods: { - updateMax: function (max) { - this.max = max - }, - selectPage: function(page) { - this.page = page - this.$emit("change", page) - } - }, - template:`
-
- {{i}} -
-
` -}) - -/** - * 一级菜单 - */ -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) - this.$emit("input", this.visible) - } - }, - template: '更多选项收起选项 ' -}); - -/** - * 二级菜单 - */ -Vue.component("second-menu", { - template: ' \ -
\ - \ -
\ -
' -}); - -Vue.component("file-textarea", { - props: ["value"], - data: function () { - let value = this.value - if (typeof value != "string") { - value = "" - } - return { - realValue: value - } - }, - mounted: function () { - }, - methods: { - dragover: function () {}, - drop: function (e) { - let that = this - e.dataTransfer.items[0].getAsFile().text().then(function (data) { - that.setValue(data) - }) - }, - setValue: function (value) { - this.realValue = value - }, - focus: function () { - this.$refs.textarea.focus() - } - }, - template: `` -}) - -Vue.component("more-options-angle", { - data: function () { - return { - isVisible: false - } - }, - methods: { - show: function () { - this.isVisible = !this.isVisible - this.$emit("change", this.isVisible) - } - }, - template: `更多选项收起选项` -}) - -Vue.component("columns-grid", { - props: [], - mounted: function () { - this.columns = this.calculateColumns() - - let that = this - window.addEventListener("resize", function () { - that.columns = that.calculateColumns() - }) - }, - data: function () { - return { - columns: "four" - } - }, - methods: { - calculateColumns: function () { - let w = window.innerWidth - let columns = Math.floor(w / 250) - if (columns == 0) { - columns = 1 - } - - let columnElements = this.$el.getElementsByClassName("column") - if (columnElements.length == 0) { - return - } - let maxColumns = columnElements.length - if (columns > maxColumns) { - columns = maxColumns - } - - // 添加右侧边框 - for (let index = 0; index < columnElements.length; index++) { - let el = columnElements[index] - el.className = el.className.replace("with-border", "") - if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) { - el.className += " with-border" - } - } - - switch (columns) { - case 1: - return "one" - case 2: - return "two" - case 3: - return "three" - case 4: - return "four" - case 5: - return "five" - case 6: - return "six" - case 7: - return "seven" - case 8: - return "eight" - case 9: - return "nine" - case 10: - return "ten" - default: - return "ten" - } - } - }, - template: `
- -
` -}) - -/** - * 菜单项 - */ -Vue.component("inner-menu-item", { - props: ["href", "active", "code"], - data: function () { - var active = this.active; - if (typeof(active) =="undefined") { - var itemCode = ""; - if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") { - itemCode = window.TEA.ACTION.data.firstMenuItem; - } - active = (itemCode == this.code); - } - return { - vHref: (this.href == null) ? "" : this.href, - vActive: active - }; - }, - template: '\ - [] \ - ' -}); - -Vue.component("bandwidth-size-capacity-box", { - props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"], - data: function () { - let v = this.vValue - if (v == null) { - v = { - count: this.vCount, - unit: this.vUnit - } - } - if (v.unit == null || v.unit.length == 0) { - v.unit = "mb" - } - - if (typeof (v["count"]) != "number") { - v["count"] = -1 - } - - let vSize = this.size - if (vSize == null) { - vSize = 6 - } - - let vMaxlength = this.maxlength - if (vMaxlength == null) { - vMaxlength = 10 - } - - let supportedUnits = this.vSupportedUnits - if (supportedUnits == null) { - supportedUnits = [] - } - - return { - capacity: v, - countString: (v.count >= 0) ? v.count.toString() : "", - vSize: vSize, - vMaxlength: vMaxlength, - supportedUnits: supportedUnits - } - }, - watch: { - "countString": function (newValue) { - let value = newValue.trim() - if (value.length == 0) { - this.capacity.count = -1 - this.change() - return - } - let count = parseInt(value) - if (!isNaN(count)) { - this.capacity.count = count - } - this.change() - } - }, - methods: { - change: function () { - this.$emit("change", this.capacity) - } - }, - template: `
- -
- -
-
- -
-
` -}) - -// 将变量转换为中文 -Vue.component("request-variables-describer", { - data: function () { - return { - vars:[] - } - }, - methods: { - update: function (variablesString) { - this.vars = [] - let that = this - variablesString.replace(/\${.+?}/g, function (v) { - let def = that.findVar(v) - if (def == null) { - return v - } - that.vars.push(def) - }) - }, - findVar: function (name) { - let def = null - window.REQUEST_VARIABLES.forEach(function (v) { - if (v.code == name) { - def = v - } - }) - return def - } - }, - template: ` - {{v.code}} - {{v.name}} -` -}) - - -Vue.component("combo-box", { - // data-url 和 data-key 成对出现 - props: [ - "name", "title", "placeholder", "size", "v-items", "v-value", - "data-url", // 数据源URL - "data-key", // 数据源中数据的键名 - "data-search", // 是否启用动态搜索,如果值为on或true,则表示启用 - "width" - ], - mounted: function () { - if (this.dataURL.length > 0) { - this.search("") - } - - // 设定菜单宽度 - let searchBox = this.$refs.searchBox - if (searchBox != null) { - let inputWidth = searchBox.offsetWidth - if (inputWidth != null && inputWidth > 0) { - this.$refs.menu.style.width = inputWidth + "px" - } else if (this.styleWidth.length > 0) { - this.$refs.menu.style.width = this.styleWidth - } - } - }, - data: function () { - let items = this.vItems - if (items == null || !(items instanceof Array)) { - items = [] - } - items = this.formatItems(items) - - // 当前选中项 - let selectedItem = null - if (this.vValue != null) { - let that = this - items.forEach(function (v) { - if (v.value == that.vValue) { - selectedItem = v - } - }) - } - - let width = this.width - if (width == null || width.length == 0) { - width = "11em" - } else { - if (/\d+$/.test(width)) { - width += "em" - } - } - - // data url - let dataURL = "" - if (typeof this.dataUrl == "string" && this.dataUrl.length > 0) { - dataURL = this.dataUrl - } - - return { - allItems: items, // 原始的所有的items - items: items.$copy(), // 候选的items - selectedItem: selectedItem, // 选中的item - keyword: "", - visible: false, - hideTimer: null, - hoverIndex: 0, - styleWidth: width, - - isInitial: true, - dataURL: dataURL, - urlRequestId: 0 // 记录URL请求ID,防止并行冲突 - } - }, - methods: { - search: function (keyword) { - // 从URL中获取选项数据 - let dataUrl = this.dataURL - let dataKey = this.dataKey - let that = this - - let requestId = Math.random() - this.urlRequestId = requestId - - Tea.action(dataUrl) - .params({ - keyword: (keyword == null) ? "" : keyword - }) - .post() - .success(function (resp) { - if (requestId != that.urlRequestId) { - return - } - - if (resp.data != null) { - if (typeof (resp.data[dataKey]) == "object") { - let items = that.formatItems(resp.data[dataKey]) - that.allItems = items - that.items = items.$copy() - - if (that.isInitial) { - that.isInitial = false - if (that.vValue != null) { - items.forEach(function (v) { - if (v.value == that.vValue) { - that.selectedItem = v - } - }) - } - } - } - } - }) - }, - formatItems: function (items) { - items.forEach(function (v) { - if (v.value == null) { - v.value = v.id - } - }) - return items - }, - reset: function () { - this.selectedItem = null - this.change() - this.hoverIndex = 0 - - let that = this - setTimeout(function () { - if (that.$refs.searchBox) { - that.$refs.searchBox.focus() - } - }) - }, - clear: function () { - this.selectedItem = null - this.change() - this.hoverIndex = 0 - }, - changeKeyword: function () { - let shouldSearch = this.dataURL.length > 0 && (this.dataSearch == "on" || this.dataSearch == "true") - - this.hoverIndex = 0 - let keyword = this.keyword - if (keyword.length == 0) { - if (shouldSearch) { - this.search(keyword) - } else { - this.items = this.allItems.$copy() - } - return - } - - - if (shouldSearch) { - this.search(keyword) - } else { - this.items = this.allItems.$copy().filter(function (v) { - if (v.fullname != null && v.fullname.length > 0 && teaweb.match(v.fullname, keyword)) { - return true - } - return teaweb.match(v.name, keyword) - }) - } - }, - selectItem: function (item) { - this.selectedItem = item - this.change() - this.hoverIndex = 0 - this.keyword = "" - this.changeKeyword() - }, - confirm: function () { - if (this.items.length > this.hoverIndex) { - this.selectItem(this.items[this.hoverIndex]) - } - }, - show: function () { - this.visible = true - - // 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换 - }, - hide: function () { - let that = this - this.hideTimer = setTimeout(function () { - that.visible = false - }, 500) - }, - downItem: function () { - this.hoverIndex++ - if (this.hoverIndex > this.items.length - 1) { - this.hoverIndex = 0 - } - this.focusItem() - }, - upItem: function () { - this.hoverIndex-- - if (this.hoverIndex < 0) { - this.hoverIndex = 0 - } - this.focusItem() - }, - focusItem: function () { - if (this.hoverIndex < this.items.length) { - this.$refs.itemRef[this.hoverIndex].focus() - let that = this - setTimeout(function () { - that.$refs.searchBox.focus() - if (that.hideTimer != null) { - clearTimeout(that.hideTimer) - that.hideTimer = null - } - }) - } - }, - change: function () { - this.$emit("change", this.selectedItem) - - let that = this - setTimeout(function () { - if (that.$refs.selectedLabel != null) { - that.$refs.selectedLabel.focus() - } - }) - }, - submitForm: function (event) { - if (event.target.tagName != "A") { - return - } - let parentBox = this.$refs.selectedLabel.parentNode - while (true) { - parentBox = parentBox.parentNode - if (parentBox == null || parentBox.tagName == "BODY") { - return - } - if (parentBox.tagName == "FORM") { - parentBox.submit() - break - } - } - }, - - setDataURL: function (dataURL) { - this.dataURL = dataURL - }, - reloadData: function () { - this.search("") - } - }, - template: `
- -
- -
- - -
- - {{title}}:{{selectedItem.name}} - - -
- - -
- -
-
` -}) - -Vue.component("search-box", { - props: ["placeholder", "width"], - data: function () { - let width = this.width - if (width == null) { - width = "10em" - } - return { - realWidth: width, - realValue: "" - } - }, - methods: { - onInput: function () { - this.$emit("input", { value: this.realValue}) - this.$emit("change", { value: this.realValue}) - }, - clearValue: function () { - this.realValue = "" - this.focus() - this.onInput() - }, - focus: function () { - this.$refs.valueRef.focus() - } - }, - template: `
-
- - -
-
` -}) - -Vue.component("time-duration-box", { - props: ["v-name", "v-value", "v-count", "v-unit", "placeholder", "v-min-unit", "maxlength"], - mounted: function () { - this.change() - }, - data: function () { - let v = this.vValue - if (v == null) { - v = { - count: this.vCount, - unit: this.vUnit - } - } - if (typeof (v["count"]) != "number") { - v["count"] = -1 - } - - let minUnit = this.vMinUnit - let units = [ - { - code: "ms", - name: "毫秒" - }, - { - code: "second", - name: "秒" - }, - { - code: "minute", - name: "分钟" - }, - { - code: "hour", - name: "小时" - }, - { - code: "day", - name: "天" - } - ] - let minUnitIndex = -1 - if (minUnit != null && typeof minUnit == "string" && minUnit.length > 0) { - for (let i = 0; i < units.length; i++) { - if (units[i].code == minUnit) { - minUnitIndex = i - break - } - } - } - if (minUnitIndex > -1) { - units = units.slice(minUnitIndex) - } - - let maxLength = parseInt(this.maxlength) - if (typeof maxLength != "number") { - maxLength = 10 - } - - return { - duration: v, - countString: (v.count >= 0) ? v.count.toString() : "", - units: units, - realMaxLength: maxLength - } - }, - watch: { - "countString": function (newValue) { - let value = newValue.trim() - if (value.length == 0) { - this.duration.count = -1 - return - } - let count = parseInt(value) - if (!isNaN(count)) { - this.duration.count = count - } - this.change() - } - }, - methods: { - change: function () { - this.$emit("change", this.duration) - } - }, - template: `
- -
- -
-
- -
-
` -}) - -Vue.component("time-duration-text", { - props: ["v-value"], - methods: { - unitName: function (unit) { - switch (unit) { - case "ms": - return "毫秒" - case "second": - return "秒" - case "minute": - return "分钟" - case "hour": - return "小时" - case "day": - return "天" - } - } - }, - template: ` - {{vValue.count}} {{unitName(vValue.unit)}} -` -}) - -Vue.component("not-found-box", { - props: ["message"], - template: `
-
-

{{message}}

-
` -}) - -// 警告消息 -Vue.component("warning-message", { - template: `
` -}) - -let checkboxId = 0 -Vue.component("checkbox", { - props: ["name", "value", "v-value", "id", "checked"], - data: function () { - checkboxId++ - let elementId = this.id - if (elementId == null) { - elementId = "checkbox" + checkboxId - } - - let elementValue = this.vValue - if (elementValue == null) { - elementValue = "1" - } - - let checkedValue = this.value - if (checkedValue == null && this.checked == "checked") { - checkedValue = elementValue - } - - return { - elementId: elementId, - elementValue: elementValue, - newValue: checkedValue - } - }, - methods: { - change: function () { - this.$emit("input", this.newValue) - }, - check: function () { - this.newValue = this.elementValue - }, - uncheck: function () { - this.newValue = "" - }, - isChecked: function () { - return (typeof (this.newValue) == "boolean" && this.newValue) || this.newValue == this.elementValue - } - }, - watch: { - value: function (v) { - if (typeof v == "boolean") { - this.newValue = v - } - } - }, - template: `
- - -
` -}) - -Vue.component("network-addresses-view", { - props: ["v-addresses"], - template: `
-
- {{addr.protocol}}://{{addr.host}}:{{addr.portRange}} -
-
` -}) - -Vue.component("url-patterns-box", { - props: ["value"], - data: function () { - let patterns = [] - if (this.value != null) { - patterns = this.value - } - - return { - patterns: patterns, - isAdding: false, - - addingPattern: {"type": "wildcard", "pattern": ""}, - editingIndex: -1, - - patternIsInvalid: false, - - windowIsSmall: window.innerWidth < 600 - } - }, - methods: { - add: function () { - this.isAdding = true - let that = this - setTimeout(function () { - that.$refs.patternInput.focus() - }) - }, - edit: function (index) { - this.isAdding = true - this.editingIndex = index - this.addingPattern = { - type: this.patterns[index].type, - pattern: this.patterns[index].pattern - } - }, - confirm: function () { - if (this.requireURL(this.addingPattern.type)) { - let pattern = this.addingPattern.pattern.trim() - if (pattern.length == 0) { - let that = this - teaweb.warn("请输入URL", function () { - that.$refs.patternInput.focus() - }) - return - } - } - if (this.editingIndex < 0) { - this.patterns.push({ - type: this.addingPattern.type, - pattern: this.addingPattern.pattern - }) - } else { - this.patterns[this.editingIndex].type = this.addingPattern.type - this.patterns[this.editingIndex].pattern = this.addingPattern.pattern - } - this.notifyChange() - this.cancel() - }, - remove: function (index) { - this.patterns.$remove(index) - this.cancel() - this.notifyChange() - }, - cancel: function () { - this.isAdding = false - this.addingPattern = {"type": "wildcard", "pattern": ""} - this.editingIndex = -1 - }, - patternTypeName: function (patternType) { - switch (patternType) { - case "wildcard": - return "通配符" - case "regexp": - return "正则" - case "images": - return "常见图片文件" - case "audios": - return "常见音频文件" - case "videos": - return "常见视频文件" - } - return "" - }, - notifyChange: function () { - this.$emit("input", this.patterns) - }, - changePattern: function () { - this.patternIsInvalid = false - let pattern = this.addingPattern.pattern - switch (this.addingPattern.type) { - case "wildcard": - if (pattern.indexOf("?") >= 0) { - this.patternIsInvalid = true - } - break - case "regexp": - if (pattern.indexOf("?") >= 0) { - let pieces = pattern.split("?") - for (let i = 0; i < pieces.length - 1; i++) { - if (pieces[i].length == 0 || pieces[i][pieces[i].length - 1] != "\\") { - this.patternIsInvalid = true - } - } - } - break - } - }, - requireURL: function (patternType) { - return patternType == "wildcard" || patternType == "regexp" - } - }, - template: `
-
-
- [{{patternTypeName(pattern.type)}}] {{pattern.pattern}}   - - -
-
-
-
-
- -
-
- -

通配符正则表达式中不能包含问号(?)及问号以后的内容。

-
-
- - -
-
- -
-
-
-
- -
-
` -}) - -Vue.component("size-capacity-view", { - props:["v-default-text", "v-value"], - methods: { - composeCapacity: function (capacity) { - return teaweb.convertSizeCapacityToString(capacity) - } - }, - template: `
- {{composeCapacity(vValue)}} - {{vDefaultText}} -
` -}) - -Vue.component("keyword", { - props: ["v-word"], - data: function () { - let word = this.vWord - if (word == null) { - word = "" - } else { - word = word.replace(/\)/g, "\\)") - word = word.replace(/\(/g, "\\(") - word = word.replace(/\+/g, "\\+") - word = word.replace(/\^/g, "\\^") - word = word.replace(/\$/g, "\\$") - word = word.replace(/\?/g, "\\?") - word = word.replace(/\*/g, "\\*") - word = word.replace(/\[/g, "\\[") - word = word.replace(/{/g, "\\{") - word = word.replace(/\./g, "\\.") - } - - let slot = this.$slots["default"][0] - let text = slot.text - if (word.length > 0) { - let that = this - let m = [] // replacement => tmp - let tmpIndex = 0 - text = text.replaceAll(new RegExp("(" + word + ")", "ig"), function (replacement) { - tmpIndex++ - let s = "" + that.encodeHTML(replacement) + "" - let tmpKey = "$TMP__KEY__" + tmpIndex.toString() + "$" - m.push([tmpKey, s]) - return tmpKey - }) - text = this.encodeHTML(text) - - m.forEach(function (r) { - text = text.replace(r[0], r[1]) - }) - - } else { - text = this.encodeHTML(text) - } - - return { - word: word, - text: text - } - }, - methods: { - encodeHTML: function (s) { - s = s.replace(/&/g, "&") - s = s.replace(//g, ">") - s = s.replace(/"/g, """) - return s - } - }, - template: `` -}) - -Vue.component("bits-var", { - props: ["v-bits"], - data: function () { - let bits = this.vBits - if (typeof bits != "number") { - bits = 0 - } - let format = teaweb.splitFormat(teaweb.formatBits(bits)) - return { - format: format - } - }, - template:` - {{format[0]}}{{format[1]}} -` -}) - -Vue.component("theme-color-picker", { - props: ["v-model"], - data: function () { - return { - showPicker: false, - r: 20, - g: 83, - b: 196, - hex: "#14539A", - clickHandler: null - } - }, - mounted: function () { - console.log("Theme color picker component mounted, element:", this.$el) - // 从localStorage加载保存的颜色 - let savedColor = localStorage.getItem("themeColor") - if (savedColor) { - this.setColor(savedColor) - } else { - // 从当前页面获取默认颜色 - let defaultColor = this.getCurrentThemeColor() - if (defaultColor) { - this.setColor(defaultColor) - } - } - - // 应用颜色 - this.applyColor() - }, - beforeDestroy: function() { - // 移除点击监听器 - if (this.clickHandler) { - document.removeEventListener("click", this.clickHandler) - this.clickHandler = null - } - }, - methods: { - // 打开/关闭颜色选择器 - togglePicker: function (e) { - if (e) { - e.preventDefault() - e.stopPropagation() - } - console.log("Toggle picker clicked, current showPicker:", this.showPicker) - this.showPicker = !this.showPicker - console.log("Toggle picker clicked, new showPicker:", this.showPicker) - console.log("Popup should be visible:", this.showPicker) - - // 管理外部点击监听器 - if (this.showPicker) { - // 打开时,延迟添加监听器 - let that = this - setTimeout(function() { - if (!that.clickHandler) { - that.clickHandler = function(e) { - if (that.showPicker && !that.$el.contains(e.target)) { - that.showPicker = false - } - } - document.addEventListener("click", that.clickHandler) - } - }, 100) - } else { - // 关闭时,移除监听器 - if (this.clickHandler) { - document.removeEventListener("click", this.clickHandler) - this.clickHandler = null - } - } - }, - - // RGB转16进制 - rgbToHex: function (r, g, b) { - return "#" + [r, g, b].map(function (x) { - x = parseInt(x) - const hex = x.toString(16) - return hex.length === 1 ? "0" + hex : hex - }).join("").toUpperCase() - }, - - // 16进制转RGB - hexToRgb: function (hex) { - hex = hex.replace("#", "") - const r = parseInt(hex.substring(0, 2), 16) - const g = parseInt(hex.substring(2, 4), 16) - const b = parseInt(hex.substring(4, 6), 16) - return { r: r, g: g, b: b } - }, - - // 设置颜色(支持hex或rgb对象) - setColor: function (color) { - if (typeof color === "string") { - if (color.startsWith("#")) { - let rgb = this.hexToRgb(color) - this.r = rgb.r - this.g = rgb.g - this.b = rgb.b - this.hex = color.toUpperCase() - } else { - // 假设是16进制不带#号 - this.setColor("#" + color) - } - } else if (typeof color === "object" && color.r !== undefined) { - this.r = color.r - this.g = color.g - this.b = color.b - this.hex = this.rgbToHex(color.r, color.g, color.b) - } - }, - - // RGB值变化时更新hex - updateHex: function () { - this.hex = this.rgbToHex(this.r, this.g, this.b) - this.applyColor() - }, - - // Hex值变化时更新RGB - updateRgb: function () { - if (this.hex.match(/^#[0-9A-Fa-f]{6}$/)) { - let rgb = this.hexToRgb(this.hex) - this.r = rgb.r - this.g = rgb.g - this.b = rgb.b - this.applyColor() - } - }, - - // 应用颜色到整个网站 - applyColor: function () { - let color = this.rgbToHex(this.r, this.g, this.b) - - // 保存到localStorage - localStorage.setItem("themeColor", color) - - // 创建或更新样式 - let styleId = "theme-color-custom" - let styleEl = document.getElementById(styleId) - if (!styleEl) { - styleEl = document.createElement("style") - styleEl.id = styleId - document.head.appendChild(styleEl) - } - - // 应用颜色到主题元素 - styleEl.textContent = ` - .top-nav, .main-menu, .main-menu .menu { - background: ${color} !important; - } - .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless { - background: ${color} !important; - } - .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .item { - background: ${color} !important; - } - .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .sub-items { - background: ${color} !important; - } - .main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .sub-items .item { - background: ${color} !important; - } - .main-menu .ui.menu .sub-items { - background: ${color} !important; - } - .main-menu .ui.menu .sub-items .item { - background: ${color} !important; - } - .main-menu .ui.labeled.menu .sub-items { - background: ${color} !important; - } - .main-menu .ui.labeled.menu .sub-items .item { - background: ${color} !important; - } - .main-menu .sub-items { - background: ${color} !important; - } - .main-menu .sub-items .item { - background: ${color} !important; - } - .ui.menu.blue.inverted { - background: ${color} !important; - } - .ui.menu.blue.inverted .item { - background: ${color} !important; - } - .ui.menu.vertical.blue.inverted { - background: ${color} !important; - } - .ui.menu.vertical.blue.inverted .item { - background: ${color} !important; - } - .ui.labeled.menu.vertical.blue.inverted { - background: ${color} !important; - } - .ui.labeled.menu.vertical.blue.inverted .item { - background: ${color} !important; - } - ` - - // 触发事件通知其他组件 - if (window.Tea && window.Tea.Vue) { - window.Tea.Vue.$emit("theme-color-changed", color) - } - - // 更新v-model(如果使用) - this.$emit("input", color.replace("#", "")) - }, - - // 获取当前主题颜色 - getCurrentThemeColor: function () { - let topNav = document.querySelector(".top-nav") - if (topNav) { - let bgColor = window.getComputedStyle(topNav).backgroundColor - if (bgColor && bgColor !== "rgba(0, 0, 0, 0)" && bgColor !== "transparent") { - // 转换rgb/rgba为hex - let match = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/) - if (match) { - return this.rgbToHex(parseInt(match[1]), parseInt(match[2]), parseInt(match[3])) - } - } - } - return null - }, - - // 重置为默认颜色 - resetColor: function () { - this.setColor("#14539A") // 默认蓝色 - this.applyColor() - } - }, - template: ` -
- - 主题色 - - -
-
- RGB颜色调节 -
- - -
-
-
- {{ hex }} | RGB({{ r }}, {{ g }}, {{ b }}) -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
- - -
- - -
-
-
- ` -}) - - -Vue.component("bytes-var", { - props: ["v-bytes"], - data: function () { - let bytes = this.vBytes - if (typeof bytes != "number") { - bytes = 0 - } - let format = teaweb.splitFormat(teaweb.formatBytes(bytes)) - return { - format: format - } - }, - template:` - {{format[0]}}{{format[1]}} -` -}) - -Vue.component("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 - var form = this.$refs.token.form - - // 监听表单提交,在提交前刷新token并确保更新到 DOM - form.addEventListener("submit", function (e) { - // 如果正在刷新,等待刷新完成 - if (that.refreshing) { - e.preventDefault() - e.stopPropagation() - return false - } - - // 阻止默认提交,先刷新 token - e.preventDefault() - e.stopPropagation() - that.refreshing = true - - // 刷新 token - that.refreshToken(function () { - // 确保 DOM 中的 token 值是最新的 - that.$forceUpdate() - that.$nextTick(function () { - var tokenInput = form.querySelector('input[name="csrfToken"]') - if (tokenInput) { - tokenInput.value = that.token - } - if (that.$refs.token) { - that.$refs.token.value = that.token - } - - // 确保 DOM 已更新后,再触发表单提交 - setTimeout(function () { - that.refreshing = false - // 重新触发表单提交 - Tea.runActionOn(form) - }, 50) - }) - }) - - return false - }) - - // 自动刷新 - setInterval(function () { - that.refreshToken() - }, 10 * 60 * 1000) - - // 监听表单提交失败,如果是 CSRF token 错误,自动刷新 token 并重试 - this.setupAutoRetry(form) - }, - data: function () { - return { - token: "", - retrying: false, - refreshing: false - } - }, - methods: { - refreshToken: function (callback) { - let that = this - Tea.action("/csrf/token") - .get() - .success(function (resp) { - that.token = resp.data.token - if (callback) { - callback() - } - }) - .fail(function () { - if (callback) { - callback() - } - }) - }, - setupAutoRetry: function (form) { - let that = this - var originalFail = form.getAttribute("data-tea-fail") - - // 确保 Tea.Vue 存在 - if (typeof Tea === "undefined" || Tea.Vue == null) { - if (typeof Tea === "undefined") { - window.Tea = {} - } - if (Tea.Vue == null) { - Tea.Vue = {} - } - } - - // 创建一个包装的 fail 函数 - var wrappedFailName = "csrfAutoRetryFail_" + Math.random().toString(36).substr(2, 9) - form.setAttribute("data-tea-fail", wrappedFailName) - - Tea.Vue[wrappedFailName] = function (resp) { - // 检查是否是 CSRF token 错误 - var isCSRFError = false - if (resp && resp.message) { - // 检查消息是否包含 "表单已失效" 或 "001" - if (resp.message.indexOf("表单已失效") >= 0 || resp.message.indexOf("(001)") >= 0) { - isCSRFError = true - } - } - // 检查 HTTP 状态码是否为 403 或 400 - if (!isCSRFError && resp && (resp.statusCode === 403 || resp.status === 403 || resp.statusCode === 400 || resp.status === 400)) { - isCSRFError = true - } - - if (isCSRFError) { - // 如果不是正在重试,则立即刷新 token 并自动重试 - if (!that.retrying) { - that.retrying = true - // 立即刷新 token - that.refreshToken(function () { - // 强制更新 Vue,确保响应式数据已更新 - that.$forceUpdate() - - // 使用 $nextTick 等待 Vue 完成 DOM 更新 - that.$nextTick(function () { - // 直接查找并更新 DOM 中的 input 元素(通过 name 属性) - var tokenInput = form.querySelector('input[name="csrfToken"]') - if (tokenInput) { - tokenInput.value = that.token - } - - // 如果 ref 存在,也更新它 - if (that.$refs.token) { - that.$refs.token.value = that.token - } - - // 使用 setTimeout 确保 DOM 已完全更新 - setTimeout(function () { - // 再次确认 token 值已更新 - var finalTokenInput = form.querySelector('input[name="csrfToken"]') - if (finalTokenInput && finalTokenInput.value !== that.token) { - finalTokenInput.value = that.token - } - - that.retrying = false - // 重新触发表单提交 - Tea.runActionOn(form) - }, 150) - }) - }) - return // 不调用原始 fail 函数 - } else { - // 如果正在重试,说明已经刷新过 token,直接调用原始 fail 函数 - if (originalFail && typeof Tea.Vue[originalFail] === "function") { - return Tea.Vue[originalFail].call(Tea.Vue, resp) - } else { - Tea.failResponse(resp) - } - } - } - - // 不是 CSRF 错误,调用原始 fail 函数或默认处理 - if (originalFail && typeof Tea.Vue[originalFail] === "function") { - return Tea.Vue[originalFail].call(Tea.Vue, resp) - } else { - Tea.failResponse(resp) - } - } - } - }, - 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.success("已复制到剪切板") - } - }, - template: `` -}) - -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}}   -
-
-
-
- [选择分组]   [添加分组] -
-
` -}) - -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"], - 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 - } - - return { - capacity: v, - countString: (v.count >= 0) ? v.count.toString() : "", - vSize: vSize, - vMaxlength: vMaxlength - } - }, - watch: { - "countString": function (newValue) { - let value = newValue.trim() - if (value.length == 0) { - this.capacity.count = -1 - this.change() - return - } - let count = parseInt(value) - if (!isNaN(count)) { - this.capacity.count = count - } - this.change() - } - }, - methods: { - change: function () { - this.$emit("change", this.capacity) - } - }, - template: `
- -
- -
-
- -
-
` -}) - -/** - * 二级菜单 - */ -Vue.component("inner-menu", { - template: ` -
- -
` -}); - -Vue.component("datepicker", { - props: ["value", "v-name", "name", "v-value", "v-bottom-left", "placeholder"], - mounted: function () { - let that = this - teaweb.datepicker(this.$refs.dayInput, function (v) { - that.day = v - that.change() - }, !!this.vBottomLeft) - }, - data: function () { - let name = this.vName - if (name == null) { - name = this.name - } - if (name == null) { - name = "day" - } - - let day = this.vValue - if (day == null) { - day = this.value - if (day == null) { - day = "" - } - } - - let placeholder = "YYYY-MM-DD" - if (this.placeholder != null) { - placeholder = this.placeholder - } - - return { - realName: name, - realPlaceholder: placeholder, - day: day - } - }, - watch: { - value: function (v) { - this.day = v - - let picker = this.$refs.dayInput.picker - if (picker != null) { - if (v != null && /^\d+-\d+-\d+$/.test(v)) { - picker.setDate(v) - } - } - } - }, - methods: { - change: function () { - this.$emit("input", this.day) // support v-model,事件触发需要在 change 之前 - this.$emit("change", this.day) - } - }, - template: `
- -
` -}) - -// 排序使用的箭头 -Vue.component("sort-arrow", { - props: ["name"], - data: function () { - let url = window.location.toString() - let order = "" - let iconTitle = "" - let newArgs = [] - if (window.location.search != null && window.location.search.length > 0) { - let queryString = window.location.search.substring(1) - let pieces = queryString.split("&") - let that = this - pieces.forEach(function (v) { - let eqIndex = v.indexOf("=") - if (eqIndex > 0) { - let argName = v.substring(0, eqIndex) - let argValue = v.substring(eqIndex + 1) - if (argName == that.name) { - order = argValue - } else if (argName != "page" && argValue != "asc" && argValue != "desc") { - newArgs.push(v) - } - } else { - newArgs.push(v) - } - }) - } - if (order == "asc") { - newArgs.push(this.name + "=desc") - iconTitle = "当前正序排列" - } else if (order == "desc") { - newArgs.push(this.name + "=asc") - iconTitle = "当前倒序排列" - } else { - newArgs.push(this.name + "=desc") - iconTitle = "当前正序排列" - } - - let qIndex = url.indexOf("?") - if (qIndex > 0) { - url = url.substring(0, qIndex) + "?" + newArgs.join("&") - } else { - url = url + "?" + newArgs.join("&") - } - - return { - order: order, - url: url, - iconTitle: iconTitle - } - }, - template: `  ` -}) - -Vue.component("ad-instance-objects-box", { - props: ["v-objects", "v-user-id"], - mounted: function () { - this.getUserServers(1) - }, - data: function () { - let objects = this.vObjects - if (objects == null) { - objects = [] - } - - let objectCodes = [] - objects.forEach(function (v) { - objectCodes.push(v.code) - }) - - return { - userId: this.vUserId, - objects: objects, - objectCodes: objectCodes, - isAdding: true, - - servers: [], - serversIsLoading: false - } - }, - methods: { - add: function () { - this.isAdding = true - }, - cancel: function () { - this.isAdding = false - }, - remove: function (index) { - let that = this - teaweb.confirm("确定要删除此防护对象吗?", function () { - that.objects.$remove(index) - that.notifyChange() - }) - }, - removeObjectCode: function (objectCode) { - let index = -1 - this.objectCodes.forEach(function (v, k) { - if (objectCode == v) { - index = k - } - }) - if (index >= 0) { - this.objects.$remove(index) - this.notifyChange() - } - }, - getUserServers: function (page) { - if (Tea.Vue == null) { - let that = this - setTimeout(function () { - that.getUserServers(page) - }, 100) - return - } - - let that = this - this.serversIsLoading = true - Tea.Vue.$post(".userServers") - .params({ - userId: this.userId, - page: page, - pageSize: 5 - }) - .success(function (resp) { - that.servers = resp.data.servers - - that.$refs.serverPage.updateMax(resp.data.page.max) - that.serversIsLoading = false - }) - .error(function () { - that.serversIsLoading = false - }) - }, - changeServerPage: function (page) { - this.getUserServers(page) - }, - selectServerObject: function (server) { - if (this.existObjectCode("server:" + server.id)) { - return - } - - this.objects.push({ - "type": "server", - "code": "server:" + server.id, - - "id": server.id, - "name": server.name - }) - this.notifyChange() - }, - notifyChange: function () { - let objectCodes = [] - this.objects.forEach(function (v) { - objectCodes.push(v.code) - }) - this.objectCodes = objectCodes - }, - existObjectCode: function (objectCode) { - let found = false - this.objects.forEach(function (v) { - if (v.code == objectCode) { - found = true - } - }) - return found - } - }, - template: `
- - - -
-
暂时还没有设置任何防护对象。
-
- - - - - -
已选中防护对象 -
- 网站:{{object.name}} -   -
-
-
-
-
- - -
- - - - - - - - - - -
对象类型网站
网站列表 - 加载中... -
暂时还没有可选的网站。
- - - - - - - - - - - -
网站名称操作
{{server.name}} - 选中 - 取消 -
- - -
-
- - -
- -
-
` -}) - window.REQUEST_COND_COMPONENTS = [{"type":"url-extension","name":"文件扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-eq-index","name":"首页","description":"检查URL路径是为\"/\"","component":"http-cond-url-eq-index","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":false},{"type":"url-all","name":"全站","description":"全站所有URL","component":"http-cond-url-all","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":false},{"type":"url-prefix","name":"URL目录前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-prefix","paramsTitle":"URL目录前缀","isRequest":true,"caseInsensitive":true},{"type":"url-eq","name":"URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-regexp","name":"URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致","component":"http-cond-url-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"url-wildcard-match","name":"URL通配符","description":"使用通配符检查URL中的文件路径是否一致","component":"http-cond-url-wildcard-match","paramsTitle":"通配符","isRequest":true,"caseInsensitive":true},{"type":"user-agent-regexp","name":"User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识","component":"http-cond-user-agent-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"params","name":"参数匹配","description":"根据参数值进行匹配","component":"http-cond-params","paramsTitle":"参数配置","isRequest":true,"caseInsensitive":false},{"type":"url-not-extension","name":"排除:URL扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-not-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-not-prefix","name":"排除:URL前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-not-prefix","paramsTitle":"URL前缀","isRequest":true,"caseInsensitive":true},{"type":"url-not-eq","name":"排除:URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-not-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-not-regexp","name":"排除:URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致,如果一致,则不匹配","component":"http-cond-url-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"user-agent-not-regexp","name":"排除:User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识,如果含有,则不匹配","component":"http-cond-user-agent-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"mime-type","name":"内容MimeType","description":"根据服务器返回的内容的MimeType进行过滤。注意:当用于缓存条件时,此条件需要结合别的请求条件使用。","component":"http-cond-mime-type","paramsTitle":"MimeType列表","isRequest":false,"caseInsensitive":false}]; window.REQUEST_COND_OPERATORS = [{"description":"判断是否正则表达式匹配","name":"正则表达式匹配","op":"regexp"},{"description":"判断是否正则表达式不匹配","name":"正则表达式不匹配","op":"not regexp"},{"description":"判断是否和指定的通配符匹配","name":"通配符匹配","op":"wildcard match"},{"description":"判断是否和指定的通配符不匹配","name":"通配符不匹配","op":"wildcard not match"},{"description":"使用字符串对比参数值是否相等于某个值","name":"字符串等于","op":"eq"},{"description":"参数值包含某个前缀","name":"字符串前缀","op":"prefix"},{"description":"参数值包含某个后缀","name":"字符串后缀","op":"suffix"},{"description":"参数值包含另外一个字符串","name":"字符串包含","op":"contains"},{"description":"参数值不包含另外一个字符串","name":"字符串不包含","op":"not contains"},{"description":"使用字符串对比参数值是否不相等于某个值","name":"字符串不等于","op":"not"},{"description":"判断参数值在某个列表中","name":"在列表中","op":"in"},{"description":"判断参数值不在某个列表中","name":"不在列表中","op":"not in"},{"description":"判断小写的扩展名(不带点)在某个列表中","name":"扩展名","op":"file ext"},{"description":"判断MimeType在某个列表中,支持类似于image/*的语法","name":"MimeType","op":"mime type"},{"description":"判断版本号在某个范围内,格式为version1,version2","name":"版本号范围","op":"version range"},{"description":"将参数转换为整数数字后进行对比","name":"整数等于","op":"eq int"},{"description":"将参数转换为可以有小数的浮点数字进行对比","name":"浮点数等于","op":"eq float"},{"description":"将参数转换为数字进行对比","name":"数字大于","op":"gt"},{"description":"将参数转换为数字进行对比","name":"数字大于等于","op":"gte"},{"description":"将参数转换为数字进行对比","name":"数字小于","op":"lt"},{"description":"将参数转换为数字进行对比","name":"数字小于等于","op":"lte"},{"description":"对整数参数值取模,除数为10,对比值为余数","name":"整数取模10","op":"mod 10"},{"description":"对整数参数值取模,除数为100,对比值为余数","name":"整数取模100","op":"mod 100"},{"description":"对整数参数值取模,对比值格式为:除数,余数,比如10,1","name":"整数取模","op":"mod"},{"description":"将参数转换为IP进行对比","name":"IP等于","op":"eq ip"},{"description":"将参数转换为IP进行对比","name":"IP大于","op":"gt ip"},{"description":"将参数转换为IP进行对比","name":"IP大于等于","op":"gte ip"},{"description":"将参数转换为IP进行对比","name":"IP小于","op":"lt ip"},{"description":"将参数转换为IP进行对比","name":"IP小于等于","op":"lte ip"},{"description":"IP在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e,或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e","name":"IP范围","op":"ip range"},{"description":"对IP参数值取模,除数为10,对比值为余数","name":"IP取模10","op":"ip mod 10"},{"description":"对IP参数值取模,除数为100,对比值为余数","name":"IP取模100","op":"ip mod 100"},{"description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1","name":"IP取模","op":"ip mod"}]; @@ -15506,7 +15506,7 @@ window.REQUEST_VARIABLES = [{"code":"${edgeVersion}","description":"","name":" window.METRIC_HTTP_KEYS = [{"name":"客户端地址(IP)","code":"${remoteAddr}","description":"会依次根据X-Forwarded-For、X-Real-IP、RemoteAddr获取,适用于前端可能有别的反向代理的情形,存在被伪造的可能","icon":""},{"name":"直接客户端地址(IP)","code":"${rawRemoteAddr}","description":"返回直接连接服务的客户端原始IP地址","icon":""},{"name":"客户端用户名","code":"${remoteUser}","description":"通过基本认证填入的用户名","icon":""},{"name":"请求URI","code":"${requestURI}","description":"包含参数,比如/hello?name=lily","icon":""},{"name":"请求路径","code":"${requestPath}","description":"不包含参数,比如/hello","icon":""},{"name":"完整URL","code":"${requestURL}","description":"比如https://example.com/hello?name=lily","icon":""},{"name":"请求方法","code":"${requestMethod}","description":"比如GET、POST等","icon":""},{"name":"请求协议Scheme","code":"${scheme}","description":"http或https","icon":""},{"name":"文件扩展名","code":"${requestPathExtension}","description":"请求路径中的文件扩展名,包括点符号,比如.html、.png","icon":""},{"name":"小写文件扩展名","code":"${requestPathLowerExtension}","description":"请求路径中的文件扩展名小写形式,包括点符号,比如.html、.png","icon":""},{"name":"主机名","code":"${host}","description":"通常是请求的域名","icon":""},{"name":"HTTP协议","code":"${proto}","description":"包含版本的HTTP请求协议,类似于HTTP/1.0","icon":""},{"name":"URL参数值","code":"${arg.NAME}","description":"单个URL参数值","icon":""},{"name":"请求来源URL","code":"${referer}","description":"请求来源Referer URL","icon":""},{"name":"请求来源URL域名","code":"${referer.host}","description":"请求来源Referer URL域名","icon":""},{"name":"Header值","code":"${header.NAME}","description":"单个Header值,比如${header.User-Agent}","icon":""},{"name":"Cookie值","code":"${cookie.NAME}","description":"单个cookie值,比如${cookie.sid}","icon":""},{"name":"状态码","code":"${status}","description":"","icon":""},{"name":"响应的Content-Type值","code":"${response.contentType}","description":"","icon":""}]; -window.WAF_RULE_CHECKPOINTS = [{"description":"通用报头比如Cache-Control、Accept之类的长度限制,防止缓冲区溢出攻击。","name":"通用请求报头长度限制","prefix":"requestGeneralHeaderLength"},{"description":"通用报头比如Cache-Control、Date之类的长度限制,防止缓冲区溢出攻击。","name":"通用响应报头长度限制","prefix":"responseGeneralHeaderLength"},{"description":"试图通过分析X-Forwarded-For等报头获取的客户端地址,比如192.168.1.100,存在伪造的可能。","name":"客户端地址(IP)","prefix":"remoteAddr"},{"description":"直接连接的客户端地址,比如192.168.1.100。","name":"客户端源地址(IP)","prefix":"rawRemoteAddr"},{"description":"直接连接的客户端地址端口。","name":"客户端端口","prefix":"remotePort"},{"description":"通过BasicAuth登录的客户端用户名。","name":"客户端用户名","prefix":"remoteUser"},{"description":"包含URL参数的请求URI,类似于 /hello/world?lang=go,不包含域名部分。","name":"请求URI","prefix":"requestURI"},{"description":"不包含URL参数的请求路径,类似于 /hello/world,不包含域名部分。","name":"请求路径","prefix":"requestPath"},{"description":"完整的请求URL,包含协议、域名、请求路径、参数等,类似于 https://example.com/hello?name=lily 。","name":"请求完整URL","prefix":"requestURL"},{"description":"请求报头中的Content-Length。","name":"请求内容长度","prefix":"requestLength"},{"description":"通常在POST或者PUT等操作时会附带请求体,最大限制32M。","name":"请求体内容","prefix":"requestBody"},{"description":"${requestURI}和${requestBody}组合。","name":"请求URI和请求体组合","prefix":"requestAll"},{"description":"获取POST或者其他方法发送的表单参数,最大请求体限制32M。","name":"请求表单参数","prefix":"requestForm"},{"description":"获取POST上传的文件信息,最大请求体限制32M。","name":"上传文件","prefix":"requestUpload"},{"description":"获取POST或者其他方法发送的JSON,最大请求体限制32M,使用点(.)符号表示多级数据。","name":"请求JSON参数","prefix":"requestJSON"},{"description":"比如GET、POST。","name":"请求方法","prefix":"requestMethod"},{"description":"比如http或https。","name":"请求协议","prefix":"scheme"},{"description":"比如HTTP/1.1。","name":"HTTP协议版本","prefix":"proto"},{"description":"比如example.com。","name":"主机名","prefix":"host"},{"description":"当前网站服务CNAME,比如38b48e4f.example.com。","name":"CNAME","prefix":"cname"},{"description":"是否为CNAME,值为1(是)或0(否)。","name":"是否为CNAME","prefix":"isCNAME"},{"description":"请求报头中的Referer和Origin值。","name":"请求来源","prefix":"refererOrigin"},{"description":"请求报头中的Referer值。","name":"请求来源Referer","prefix":"referer"},{"description":"比如Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103。","name":"客户端信息","prefix":"userAgent"},{"description":"请求报头的Content-Type。","name":"内容类型","prefix":"contentType"},{"description":"比如sid=IxZVPFhE\u0026city=beijing\u0026uid=18237。","name":"所有cookie组合字符串","prefix":"cookies"},{"description":"单个cookie值。","name":"单个cookie值","prefix":"cookie"},{"description":"比如name=lu\u0026age=20。","name":"所有URL参数组合","prefix":"args"},{"description":"单个URL参数值。","name":"单个URL参数值","prefix":"arg"},{"description":"使用换行符(\\n)隔开的报头内容字符串,每行均为\"NAME: VALUE格式\"。","name":"所有请求报头内容","prefix":"headers"},{"description":"使用换行符(\\n)隔开的报头名称字符串,每行一个名称。","name":"所有请求报头名称","prefix":"headerNames"},{"description":"单个报头值。","name":"单个请求报头值","prefix":"header"},{"description":"最长的请求报头的长度。","name":"请求报头最大长度","prefix":"headerMaxLength"},{"description":"当前客户端所处国家/地区名称。","name":"国家/地区名称","prefix":"geoCountryName"},{"description":"当前客户端所处中国省份名称。","name":"省份名称","prefix":"geoProvinceName"},{"description":"当前客户端所处中国城市名称。","name":"城市名称","prefix":"geoCityName"},{"description":"当前客户端所处ISP名称。","name":"ISP名称","prefix":"ispName"},{"description":"对统计对象进行统计。","name":"CC统计","prefix":"cc2"},{"description":"对统计对象进行统计。","name":"防盗链","prefix":"refererBlock"},{"description":"统计某段时间段内的请求信息(不推荐再使用,请使用新的CC2统计代替)。","name":"CC统计(旧)","prefix":"cc"},{"description":"响应状态码,比如200、404、500。","name":"响应状态码","prefix":"status"},{"description":"响应报头值。","name":"响应报头","prefix":"responseHeader"},{"description":"响应内容字符串。","name":"响应内容","prefix":"responseBody"},{"description":"响应内容长度,通过响应的报头Content-Length获取。","name":"响应内容长度","prefix":"bytesSent"}]; +window.WAF_RULE_CHECKPOINTS = [{"description":"通用报头比如Cache-Control、Accept之类的长度限制,防止缓冲区溢出攻击。","name":"通用请求报头长度限制","prefix":"requestGeneralHeaderLength"},{"description":"通用报头比如Cache-Control、Date之类的长度限制,防止缓冲区溢出攻击。","name":"通用响应报头长度限制","prefix":"responseGeneralHeaderLength"},{"description":"试图通过分析X-Forwarded-For等报头获取的客户端地址,比如192.168.1.100,存在伪造的可能。","name":"客户端地址(IP)","prefix":"remoteAddr"},{"description":"直接连接的客户端地址,比如192.168.1.100。","name":"客户端源地址(IP)","prefix":"rawRemoteAddr"},{"description":"直接连接的客户端地址端口。","name":"客户端端口","prefix":"remotePort"},{"description":"通过BasicAuth登录的客户端用户名。","name":"客户端用户名","prefix":"remoteUser"},{"description":"包含URL参数的请求URI,类似于 /hello/world?lang=go,不包含域名部分。","name":"请求URI","prefix":"requestURI"},{"description":"不包含URL参数的请求路径,类似于 /hello/world,不包含域名部分。","name":"请求路径","prefix":"requestPath"},{"description":"完整的请求URL,包含协议、域名、请求路径、参数等,类似于 https://example.com/hello?name=lily 。","name":"请求完整URL","prefix":"requestURL"},{"description":"请求报头中的Content-Length。","name":"请求内容长度","prefix":"requestLength"},{"description":"通常在POST或者PUT等操作时会附带请求体,最大限制32M。","name":"请求体内容","prefix":"requestBody"},{"description":"${requestURI}和${requestBody}组合。","name":"请求URI和请求体组合","prefix":"requestAll"},{"description":"获取POST或者其他方法发送的表单参数,最大请求体限制32M。","name":"请求表单参数","prefix":"requestForm"},{"description":"获取POST上传的文件信息,最大请求体限制32M。","name":"上传文件","prefix":"requestUpload"},{"description":"获取POST或者其他方法发送的JSON,最大请求体限制32M,使用点(.)符号表示多级数据。","name":"请求JSON参数","prefix":"requestJSON"},{"description":"比如GET、POST。","name":"请求方法","prefix":"requestMethod"},{"description":"比如http或https。","name":"请求协议","prefix":"scheme"},{"description":"比如HTTP/1.1。","name":"HTTP协议版本","prefix":"proto"},{"description":"比如example.com。","name":"主机名","prefix":"host"},{"description":"当前网站服务CNAME,比如38b48e4f.example.com。","name":"CNAME","prefix":"cname"},{"description":"是否为CNAME,值为1(是)或0(否)。","name":"是否为CNAME","prefix":"isCNAME"},{"description":"请求报头中的Referer和Origin值。","name":"请求来源","prefix":"refererOrigin"},{"description":"请求报头中的Referer值。","name":"请求来源Referer","prefix":"referer"},{"description":"比如Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103。","name":"客户端信息","prefix":"userAgent"},{"description":"请求报头的Content-Type。","name":"内容类型","prefix":"contentType"},{"description":"比如sid=IxZVPFhE\u0026city=beijing\u0026uid=18237。","name":"所有cookie组合字符串","prefix":"cookies"},{"description":"单个cookie值。","name":"单个cookie值","prefix":"cookie"},{"description":"比如name=lu\u0026age=20。","name":"所有URL参数组合","prefix":"args"},{"description":"单个URL参数值。","name":"单个URL参数值","prefix":"arg"},{"description":"使用换行符(\\n)隔开的报头内容字符串,每行均为\"NAME: VALUE格式\"。","name":"所有请求报头内容","prefix":"headers"},{"description":"使用换行符(\\n)隔开的报头名称字符串,每行一个名称。","name":"所有请求报头名称","prefix":"headerNames"},{"description":"单个报头值。","name":"单个请求报头值","prefix":"header"},{"description":"最长的请求报头的长度。","name":"请求报头最大长度","prefix":"headerMaxLength"},{"description":"当前客户端所处国家/地区名称。","name":"国家/地区名称","prefix":"geoCountryName"},{"description":"当前客户端所处中国省份名称。","name":"省份名称","prefix":"geoProvinceName"},{"description":"当前客户端所处中国城市名称。","name":"城市名称","prefix":"geoCityName"},{"description":"当前客户端所处ISP名称。","name":"ISP名称","prefix":"ispName"},{"description":"对统计对象进行统计。","name":"CC统计","prefix":"cc2"},{"description":"对统计对象进行统计。","name":"防盗链","prefix":"refererBlock"},{"description":"响应状态码,比如200、404、500。","name":"响应状态码","prefix":"status"},{"description":"响应报头值。","name":"响应报头","prefix":"responseHeader"},{"description":"响应内容字符串。","name":"响应内容","prefix":"responseBody"},{"description":"响应内容长度,通过响应的报头Content-Length获取。","name":"响应内容长度","prefix":"bytesSent"}]; window.WAF_RULE_OPERATORS = [{"name":"正则匹配","code":"match","description":"使用正则表达式匹配,在头部使用(?i)表示不区分大小写,\u003ca href=\"https://goedge.cn/docs/Appendix/Regexp/Index.md\" target=\"_blank\"\u003e正则表达式语法 \u0026raquo;\u003c/a\u003e。","caseInsensitive":"yes","dataType":"regexp"},{"name":"正则不匹配","code":"not match","description":"使用正则表达式不匹配,在头部使用(?i)表示不区分大小写,\u003ca href=\"https://goedge.cn/docs/Appendix/Regexp/Index.md\" target=\"_blank\"\u003e正则表达式语法 \u0026raquo;\u003c/a\u003e。","caseInsensitive":"yes","dataType":"regexp"},{"name":"通配符匹配","code":"wildcard match","description":"判断是否和指定的通配符匹配,可以在对比值中使用星号通配符(*)表示任意字符。","caseInsensitive":"yes","dataType":"wildcard"},{"name":"通配符不匹配","code":"wildcard not match","description":"判断是否和指定的通配符不匹配,可以在对比值中使用星号通配符(*)表示任意字符。","caseInsensitive":"yes","dataType":"wildcard"},{"name":"字符串等于","code":"eq string","description":"使用字符串对比等于。","caseInsensitive":"no","dataType":"string"},{"name":"字符串不等于","code":"neq string","description":"使用字符串对比不等于。","caseInsensitive":"no","dataType":"string"},{"name":"包含字符串","code":"contains","description":"包含某个字符串,比如Hello World包含了World。","caseInsensitive":"no","dataType":"string"},{"name":"不包含字符串","code":"not contains","description":"不包含某个字符串,比如Hello字符串中不包含Hi。","caseInsensitive":"no","dataType":"string"},{"name":"包含任一字符串","code":"contains any","description":"包含字符串列表中的任意一个,比如/hello/world包含/hello和/hi中的/hello,对比值中每行一个字符串。","caseInsensitive":"no","dataType":"strings"},{"name":"包含所有字符串","code":"contains all","description":"包含字符串列表中的所有字符串,比如/hello/world必须包含/hello和/world,对比值中每行一个字符串。","caseInsensitive":"no","dataType":"strings"},{"name":"包含前缀","code":"prefix","description":"包含字符串前缀部分,比如/hello前缀会匹配/hello, /hello/world等。","caseInsensitive":"no","dataType":"string"},{"name":"包含后缀","code":"suffix","description":"包含字符串后缀部分,比如/hello后缀会匹配/hello, /hi/hello等。","caseInsensitive":"no","dataType":"string"},{"name":"包含任一单词","code":"contains any word","description":"包含某个独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"包含所有单词","code":"contains all words","description":"包含所有的独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"不包含任一单词","code":"not contains any word","description":"不包含某个独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"包含SQL注入","code":"contains sql injection","description":"检测字符串内容是否包含SQL注入。","caseInsensitive":"none","dataType":"none"},{"name":"包含SQL注入-严格模式","code":"contains sql injection strictly","description":"更加严格地检测字符串内容是否包含SQL注入,相对于非严格模式,有一定的误报几率。","caseInsensitive":"none","dataType":"none"},{"name":"包含XSS注入","code":"contains xss","description":"检测字符串内容是否包含XSS注入。","caseInsensitive":"none","dataType":"none"},{"name":"包含XSS注入-严格模式","code":"contains xss strictly","description":"更加严格地检测字符串内容是否包含XSS注入,相对于非严格模式,此时xml、audio、video等标签也会被匹配。","caseInsensitive":"none","dataType":"none"},{"name":"包含二进制数据","code":"contains binary","description":"包含一组二进制数据。","caseInsensitive":"no","dataType":"string"},{"name":"不包含二进制数据","code":"not contains binary","description":"不包含一组二进制数据。","caseInsensitive":"no","dataType":"string"},{"name":"数值大于","code":"gt","description":"使用数值对比大于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值大于等于","code":"gte","description":"使用数值对比大于等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值小于","code":"lt","description":"使用数值对比小于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值小于等于","code":"lte","description":"使用数值对比小于等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值等于","code":"eq","description":"使用数值对比等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值不等于","code":"neq","description":"使用数值对比不等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"包含索引","code":"has key","description":"对于一组数据拥有某个键值或者索引。","caseInsensitive":"no","dataType":"string|number"},{"name":"版本号大于","code":"version gt","description":"对比版本号大于。","caseInsensitive":"none","dataType":"version"},{"name":"版本号小于","code":"version lt","description":"对比版本号小于。","caseInsensitive":"none","dataType":"version"},{"name":"版本号范围","code":"version range","description":"判断版本号在某个范围内,格式为 起始version1,结束version2。","caseInsensitive":"none","dataType":"versionRange"},{"name":"IP等于","code":"eq ip","description":"将参数转换为IP进行对比,只能对比单个IP。","caseInsensitive":"none","dataType":"ip"},{"name":"在一组IP中","code":"in ip list","description":"判断参数IP在一组IP内,对比值中每行一个IP。","caseInsensitive":"none","dataType":"ips"},{"name":"IP大于","code":"gt ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP大于等于","code":"gte ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP小于","code":"lt ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP小于等于","code":"lte ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP范围","code":"ip range","description":"IP在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e;或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e;或者单个IP。可以填写多行,每行一个IP范围。","caseInsensitive":"none","dataType":"ips"},{"name":"不在IP范围","code":"not ip range","description":"IP不在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e;或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e;或者单个IP。可以填写多行,每行一个IP范围。","caseInsensitive":"none","dataType":"ips"},{"name":"IP取模10","code":"ip mod 10","description":"对IP参数值取模,除数为10,对比值为余数。","caseInsensitive":"none","dataType":"number"},{"name":"IP取模100","code":"ip mod 100","description":"对IP参数值取模,除数为100,对比值为余数。","caseInsensitive":"none","dataType":"number"},{"name":"IP取模","code":"ip mod","description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1。","caseInsensitive":"none","dataType":"number"}]; diff --git a/EdgeUser/web/views/@default/@layout.html b/EdgeUser/web/views/@default/@layout.html index 23adbd6..9308a1c 100644 --- a/EdgeUser/web/views/@default/@layout.html +++ b/EdgeUser/web/views/@default/@layout.html @@ -1,134 +1,157 @@ + {$ htmlEncode .teaTitle} - + {$if eq .teaFaviconFileId 0} - + {$else} - + {$end} - + {$TEA.SEMANTIC} - + {$TEA.VUE} {$echo "header"} - - - - + + + + {$if not (eq .teaThemeBackgroundColor "")} {$end} + -
- - - -{$echo "footer"} + {$echo "footer"} + \ No newline at end of file diff --git a/访问日志策略配置手册.md b/访问日志策略配置手册.md new file mode 100644 index 0000000..ecf79ea --- /dev/null +++ b/访问日志策略配置手册.md @@ -0,0 +1,141 @@ +# 访问日志策略配置手册(默认安装 / 仅MySQL / 仅ClickHouse / 双写) + +## 1. 适用范围 +- 代码基线:`e:\AI_PRODUCT\waf-platform` +- 页面入口:`系统设置 -> 访问日志 -> 日志策略` +- 查询入口:`网站 -> 站点 -> 日志`(`/servers/server/log`) + +--- + +## 2. 默认安装后的行为(什么都不配) + +```mermaid +flowchart TD + A[EdgeNode 产生日志] --> B[写本地文件 /var/log/edge/edge-node/*.log] + A --> C[上报 EdgeAPI] + C --> D[写 MySQL 访问日志表] + E[日志查询页] --> D +``` + +- 默认即可写日志,不会因为没配 ClickHouse 就停写。 +- 查询默认走 MySQL。 +- 是否有“独立日志数据库节点”会影响写到哪个 MySQL: + - 有日志库节点:优先写日志库节点池。 + - 没有日志库节点:回退写默认数据库。 + +--- + +## 3. 必须设置项(上线最小集) + +### 3.1 基础必需(任何模式都建议) +1. `EdgeAPI` 数据库连接可用(`db.yaml` / `.db.yaml`)。 +2. `EdgeNode` 与 `EdgeAPI` 通信正常(节点在线,可上报日志)。 +3. 建议创建并启用一个**公用**访问日志策略(避免多环境行为不一致)。 + +### 3.2 仅 ClickHouse / MySQL+ClickHouse 额外必需 +1. `EdgeAPI` 配置 ClickHouse 读取: + - `EdgeAPI/configs/api.yaml`: + ```yaml + clickhouse: + host: 127.0.0.1 + port: 8123 + user: default + password: "xxxxxx" + database: default + ``` +2. Fluent Bit 已部署并运行,采集: + - `/var/log/edge/edge-node/*.log` +3. ClickHouse 已建表:`logs_ingest`(见 `deploy/fluent-bit/README.md`)。 + +--- + +## 4. 三种目标模式怎么配 + +## 4.1 只写入 MySQL + +在“日志策略”中: +1. 新建或修改策略,`存储类型` 选 **文件+MySQL**。 +2. 设为 **公用**,并确保 **启用**。 +3. `日志文件路径` 填一个 API 可写路径(必填校验项): + - 示例:`/var/log/edge/edge-api/http-access-${date}.log` + +结果: +- 写入:MySQL(主路径)+ Node 本地日志文件 +- 查询:MySQL +- 不依赖 ClickHouse + +--- + +## 4.2 只写入 ClickHouse + +在“日志策略”中: +1. `存储类型` 选 **文件+ClickHouse**。 +2. 设为 **公用**,并确保 **启用**。 +3. `日志文件路径` 仍需填写(策略校验要求): + - 示例:`/var/log/edge/edge-api/http-access-${date}.log` +4. 确保 Fluent Bit 正在采集 Node 目录并写入 ClickHouse。 +5. 确保 `EdgeAPI` 的 ClickHouse 连接已配置。 + +结果: +- 写入:Node 本地文件 -> Fluent Bit -> ClickHouse +- API 不写 MySQL +- 查询优先 ClickHouse(无 CH 时可能查不到数据) + +--- + +## 4.3 同时写入 MySQL + ClickHouse + +在“日志策略”中: +1. `存储类型` 选 **文件+MySQL+ClickHouse**。 +2. 设为 **公用**,并确保 **启用**。 +3. `日志文件路径` 填写有效路径(同上)。 +4. ClickHouse + Fluent Bit 同 4.2 要求。 + +结果: +- 写入:MySQL + ClickHouse(并行) +- 查询:优先 ClickHouse,失败可回退 MySQL + +--- + +## 5. 配置生效链路图 + +```mermaid +flowchart LR + P[公用日志策略 type/writeTargets] --> C[EdgeAPI 解析 writeTargets] + C --> N[下发到 EdgeNode GlobalServerConfig.HTTPAccessLog.WriteTargets] + N --> W1[NeedWriteFile] + N --> W2[NeedReportToAPI] + W1 --> F[Node本地日志文件] + F --> FB[Fluent Bit] + FB --> CH[(ClickHouse.logs_ingest)] + W2 --> API[CreateHTTPAccessLogs] + API --> MYSQL[(MySQL访问日志表)] +``` + +--- + +## 6. 验证清单(建议上线前逐项过) + +1. 打开 `/servers/server/log`,持续压测 1~2 分钟。 +2. 检查最新日志是否持续上顶(不是停在旧时间段)。 +3. 错误日志筛选是否只显示 `status>=400`。 +4. 仅 CH 模式下,停掉 Fluent Bit 后确认告警和查询表现符合预期。 +5. MySQL+CH 模式下,临时断 CH,确认页面可回退 MySQL。 + +--- + +## 7. 常见问题 + +### Q1:策略里的“日志文件路径”是干嘛的? +- 这是策略 `file` 配置的必填项(API 侧校验)。 +- 即使你用 ClickHouse 链路,当前实现仍要求该字段有值。 +- 真正给 Fluent Bit 采集的是 **Node 目录**:`/var/log/edge/edge-node/*.log`。 + +### Q2:不勾“停用默认数据库存储”,会不会同时写默认库和独立日志库? +- 正常不会双写同一条。 +- 有独立日志库节点时优先写节点池;节点池不可用时才回退默认库。 + +### Q3:修改策略后要不要重启? +- 通常 1 分钟内自动刷新生效。 +- 若要立即生效:重启 `edge-api`,并在需要时重启 `edge-node`、`fluent-bit`。 +