diff --git a/build/build.sh b/build/build.sh index 70a090b5..2547bbae 100755 --- a/build/build.sh +++ b/build/build.sh @@ -58,6 +58,10 @@ function build() { rm -f $(basename $EDGE_API_ZIP_FILE) cd - + # generate files + echo "generating files ..." + go run -tags $TAG $ROOT/../cmd/edge-admin/main.go generate + # build echo "building "${NAME}" ..." env GOOS=$OS GOARCH=$ARCH go build -tags $TAG -ldflags="-s -w" -o $DIST/bin/${NAME} $ROOT/../cmd/edge-admin/main.go diff --git a/cmd/edge-admin/main.go b/cmd/edge-admin/main.go index 6ec7eca2..df293f98 100644 --- a/cmd/edge-admin/main.go +++ b/cmd/edge-admin/main.go @@ -5,6 +5,7 @@ import ( "github.com/TeaOSLab/EdgeAdmin/internal/apps" "github.com/TeaOSLab/EdgeAdmin/internal/configs" teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" + "github.com/TeaOSLab/EdgeAdmin/internal/gen" "github.com/TeaOSLab/EdgeAdmin/internal/nodes" _ "github.com/TeaOSLab/EdgeAdmin/internal/web" _ "github.com/iwind/TeaGo/bootstrap" @@ -70,6 +71,13 @@ func main() { } fmt.Println("change demo mode successfully") }) + app.On("generate", func() { + err := gen.Generate() + if err != nil { + fmt.Println("generate failed: " + err.Error()) + return + } + }) app.Run(func() { adminNode := nodes.NewAdminNode() adminNode.Run() diff --git a/internal/gen/generate.go b/internal/gen/generate.go new file mode 100644 index 00000000..54a5476f --- /dev/null +++ b/internal/gen/generate.go @@ -0,0 +1,129 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package gen + +import ( + "bytes" + "encoding/json" + "errors" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/server/settings/conds/condutils" + "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared" + "github.com/iwind/TeaGo/Tea" + "github.com/iwind/TeaGo/files" + "github.com/iwind/TeaGo/logs" + "io" + "os" + "path/filepath" +) + +func Generate() error { + err := generateComponentsJSFile() + if err != nil { + return errors.New("generate 'components.js' failed: " + err.Error()) + } + + return nil +} + +// 生成Javascript文件 +func generateComponentsJSFile() error { + var buffer = bytes.NewBuffer([]byte{}) + + var webRoot string + if Tea.IsTesting() { + webRoot = Tea.Root + "/../web/public/js/components/" + } else { + webRoot = Tea.Root + "/web/public/js/components/" + } + f := files.NewFile(webRoot) + + f.Range(func(file *files.File) { + if !file.IsFile() { + return + } + if file.Ext() != ".js" { + return + } + data, err := file.ReadAll() + if err != nil { + logs.Error(err) + return + } + buffer.Write(data) + buffer.Write([]byte{'\n', '\n'}) + }) + + // 条件组件 + typesJSON, err := json.Marshal(condutils.ReadAllAvailableCondTypes()) + if err != nil { + logs.Println("ComponentsAction marshal request cond types failed: " + err.Error()) + } else { + buffer.WriteString("window.REQUEST_COND_COMPONENTS = ") + buffer.Write(typesJSON) + buffer.Write([]byte{'\n', '\n'}) + } + + // 条件操作符 + requestOperatorsJSON, err := json.Marshal(shared.AllRequestOperators()) + if err != nil { + logs.Println("ComponentsAction marshal request operators failed: " + err.Error()) + } else { + buffer.WriteString("window.REQUEST_COND_OPERATORS = ") + buffer.Write(requestOperatorsJSON) + buffer.Write([]byte{'\n', '\n'}) + } + + // 请求变量 + requestVariablesJSON, err := json.Marshal(shared.DefaultRequestVariables()) + if err != nil { + logs.Println("ComponentsAction marshal request variables failed: " + err.Error()) + } else { + buffer.WriteString("window.REQUEST_VARIABLES = ") + buffer.Write(requestVariablesJSON) + buffer.Write([]byte{'\n', '\n'}) + } + + // 指标 + metricHTTPKeysJSON, err := json.Marshal(serverconfigs.FindAllMetricKeyDefinitions(serverconfigs.MetricItemCategoryHTTP)) + if err != nil { + logs.Println("ComponentsAction marshal metric http keys failed: " + err.Error()) + } else { + buffer.WriteString("window.METRIC_HTTP_KEYS = ") + buffer.Write(metricHTTPKeysJSON) + buffer.Write([]byte{'\n', '\n'}) + } + + // IP地址阈值项目 + ipAddrThresholdItemsJSON, err := json.Marshal(nodeconfigs.FindAllIPAddressThresholdItems()) + if err != nil { + logs.Println("ComponentsAction marshal ip addr threshold items failed: " + err.Error()) + } else { + buffer.WriteString("window.IP_ADDR_THRESHOLD_ITEMS = ") + buffer.Write(ipAddrThresholdItemsJSON) + buffer.Write([]byte{'\n', '\n'}) + } + + // IP地址阈值动作 + ipAddrThresholdActionsJSON, err := json.Marshal(nodeconfigs.FindAllIPAddressThresholdActions()) + if err != nil { + logs.Println("ComponentsAction marshal ip addr threshold actions failed: " + err.Error()) + } else { + buffer.WriteString("window.IP_ADDR_THRESHOLD_ACTIONS = ") + buffer.Write(ipAddrThresholdActionsJSON) + buffer.Write([]byte{'\n', '\n'}) + } + + fp, err := os.OpenFile(filepath.Clean(Tea.PublicFile("/js/components.js")), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0777) + if err != nil { + return err + } + + _, err = io.Copy(fp, buffer) + if err != nil { + return err + } + + return nil +} diff --git a/internal/gen/generate_test.go b/internal/gen/generate_test.go new file mode 100644 index 00000000..8092964d --- /dev/null +++ b/internal/gen/generate_test.go @@ -0,0 +1,13 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package gen + +import "testing" + +func TestGenerate(t *testing.T) { + err := Generate() + if err != nil { + t.Fatal(err) + } + t.Log("ok") +} diff --git a/internal/web/actions/default/ui/init.go b/internal/web/actions/default/ui/init.go index fc0543f6..390e9f8b 100644 --- a/internal/web/actions/default/ui/init.go +++ b/internal/web/actions/default/ui/init.go @@ -1,11 +1,10 @@ package ui import ( - "compress/gzip" "github.com/TeaOSLab/EdgeAdmin/internal/configloaders" "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" "github.com/iwind/TeaGo" - "github.com/iwind/TeaGo/actions" + "github.com/iwind/TeaGo/Tea" ) func init() { @@ -16,11 +15,6 @@ func init() { // 公共可以访问的链接 Get("/image/:fileId", new(ImageAction)). - // 以下的需要压缩 - Helper(&actions.Gzip{Level: gzip.BestCompression}). - Get("/components.js", new(ComponentsAction)). - EndHelpers(). - // 以下需要登录 Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeCommon)). Get("/download", new(DownloadAction)). @@ -31,7 +25,13 @@ func init() { Post("/hideTip", new(HideTipAction)). Post("/theme", new(ThemeAction)). Post("/validateIPs", new(ValidateIPsAction)). - EndAll() + + // 开发环境下总是动态加载,以便于调试 + if Tea.IsTesting() { + server. + Get("/js/components.js", new(ComponentsAction)). + EndAll() + } }) } diff --git a/web/public/js/components.js b/web/public/js/components.js new file mode 100755 index 00000000..ad11774f --- /dev/null +++ b/web/public/js/components.js @@ -0,0 +1,12042 @@ +// 显示节点的多个集群 +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-clusters-selector", { + props: ["v-primary-cluster", "v-secondary-clusters"], + data: function () { + let primaryCluster = this.vPrimaryCluster + + let secondaryClusters = this.vSecondaryClusters + if (secondaryClusters == null) { + secondaryClusters = [] + } + + return { + primaryClusterId: (primaryCluster == null) ? 0 : primaryCluster.id, + secondaryClusterIds: secondaryClusters.map(function (v) { + return v.id + }), + + primaryCluster: primaryCluster, + secondaryClusters: secondaryClusters + } + }, + methods: { + addPrimary: function () { + let that = this + let selectedClusterIds = [this.primaryClusterId].concat(this.secondaryClusterIds) + teaweb.popup("/clusters/selectPopup?selectedClusterIds=" + selectedClusterIds.join(",") + "&mode=single", { + height: "30em", + width: "50em", + callback: function (resp) { + if (resp.data.cluster != null) { + that.primaryCluster = resp.data.cluster + that.primaryClusterId = that.primaryCluster.id + that.notifyChange() + } + } + }) + }, + removePrimary: function () { + this.primaryClusterId = 0 + this.primaryCluster = null + this.notifyChange() + }, + addSecondary: function () { + let that = this + let selectedClusterIds = [this.primaryClusterId].concat(this.secondaryClusterIds) + teaweb.popup("/clusters/selectPopup?selectedClusterIds=" + selectedClusterIds.join(",") + "&mode=multiple", { + height: "30em", + width: "50em", + callback: function (resp) { + if (resp.data.cluster != null) { + that.secondaryClusterIds.push(resp.data.cluster.id) + that.secondaryClusters.push(resp.data.cluster) + that.notifyChange() + } + } + }) + }, + removeSecondary: function (index) { + this.secondaryClusterIds.$remove(index) + this.secondaryClusters.$remove(index) + this.notifyChange() + }, + notifyChange: function () { + this.$emit("change", { + clusterId: this.primaryClusterId + }) + } + }, + template: `
+ + + + + + + + + + + +
主集群 +
+
{{primaryCluster.name}}  
+
+
+ +
+

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

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

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

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

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

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

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

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

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

+
不允许的域名 + +

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

+
+
+
` +}) + +Vue.component("ns-access-log-ref-box", { + props: ["v-access-log-ref", "v-is-parent"], + data: function () { + let config = this.vAccessLogRef + if (config == null) { + config = { + isOn: false, + isPrior: false, + logMissingDomains: false + } + } + if (typeof (config.logMissingDomains) == "undefined") { + config.logMissingDomains = false + } + return { + config: config + } + }, + template: `
+ + + + + + + + + + + + + +
是否启用 + +
记录所有访问 + +

包括对没有在系统里创建的域名访问。

+
+
+
` +}) + +Vue.component("ns-route-ranges-box", { + props: ["v-ranges"], + data: function () { + let ranges = this.vRanges + if (ranges == null) { + ranges = [] + } + return { + ranges: ranges, + isAdding: false, + + // IP范围 + ipRangeFrom: "", + ipRangeTo: "" + } + }, + methods: { + add: function () { + this.isAdding = true + let that = this + setTimeout(function () { + that.$refs.ipRangeFrom.focus() + }, 100) + }, + remove: function (index) { + this.ranges.$remove(index) + }, + cancelIPRange: function () { + this.isAdding = false + this.ipRangeFrom = "" + this.ipRangeTo = "" + }, + confirmIPRange: function () { + // 校验IP + let that = this + this.ipRangeFrom = this.ipRangeFrom.trim() + if (!this.validateIP(this.ipRangeFrom)) { + teaweb.warn("开始IP填写错误", function () { + that.$refs.ipRangeFrom.focus() + }) + return + } + + this.ipRangeTo = this.ipRangeTo.trim() + if (!this.validateIP(this.ipRangeTo)) { + teaweb.warn("结束IP填写错误", function () { + that.$refs.ipRangeTo.focus() + }) + return + } + + this.ranges.push({ + type: "ipRange", + params: { + ipFrom: this.ipRangeFrom, + ipTo: this.ipRangeTo + } + }) + this.cancelIPRange() + }, + validateIP: function (ip) { + if (!ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) { + return false + } + let pieces = ip.split(".") + let isOk = true + pieces.forEach(function (v) { + let v1 = parseInt(v) + if (v1 > 255) { + isOk = false + } + }) + return isOk + } + }, + template: `
+ +
+
+ IP范围: + {{range.params.ipFrom}} - {{range.params.ipTo}}   +
+
+
+ + +
+
+
+ +
+
-
+
+ +
+
+   + +
+
+
+ + +
` +}) + +// 选择单一线路 +Vue.component("ns-route-selector", { + props: ["v-route-code"], + mounted: function () { + let that = this + Tea.action("/ns/routes/options") + .post() + .success(function (resp) { + that.routes = resp.data.routes + }) + }, + data: function () { + let routeCode = this.vRouteCode + if (routeCode == null) { + routeCode = "" + } + return { + routeCode: routeCode, + routes: [] + } + }, + template: `
+
+ +
+
` +}) + +Vue.component("ns-user-selector", { + mounted: function () { + let that = this + + Tea.action("/ns/users/options") + .post() + .success(function (resp) { + that.users = resp.data.users + }) + }, + props: ["v-user-id"], + data: function () { + let userId = this.vUserId + if (userId == null) { + userId = 0 + } + return { + users: [], + userId: userId + } + }, + template: `
+ +
` +}) + +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-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("plan-user-selector", { + mounted: function () { + let that = this + + Tea.action("/plans/users/options") + .post() + .success(function (resp) { + that.users = resp.data.users + }) + }, + props: ["v-user-id"], + data: function () { + let userId = this.vUserId + if (userId == null) { + userId = 0 + } + return { + users: [], + userId: userId + } + }, + watch: { + userId: function (v) { + this.$emit("change", v) + } + }, + template: `
+ +
` +}) + +Vue.component("plan-price-view", { + props: ["v-plan"], + data: function () { + return { + plan: this.vPlan + } + }, + template: `
+ + 月度:¥{{plan.monthlyPrice}}元
+ 季度:¥{{plan.seasonallyPrice}}元
+ 年度:¥{{plan.yearlyPrice}}元 +
+ + 基础价格:¥{{plan.trafficPrice.base}}元/GB + +
` +}) + +// 套餐价格配置 +Vue.component("plan-price-config-box", { + props: ["v-price-type", "v-monthly-price", "v-seasonally-price", "v-yearly-price", "v-traffic-price"], + data: function () { + let priceType = this.vPriceType + if (priceType == null) { + priceType = "period" + } + + let monthlyPriceNumber = 0 + let monthlyPrice = this.vMonthlyPrice + if (monthlyPrice == null || monthlyPrice <= 0) { + monthlyPrice = "" + } else { + monthlyPrice = monthlyPrice.toString() + monthlyPriceNumber = parseFloat(monthlyPrice) + if (isNaN(monthlyPriceNumber)) { + monthlyPriceNumber = 0 + } + } + + let seasonallyPriceNumber = 0 + let seasonallyPrice = this.vSeasonallyPrice + if (seasonallyPrice == null || seasonallyPrice <= 0) { + seasonallyPrice = "" + } else { + seasonallyPrice = seasonallyPrice.toString() + seasonallyPriceNumber = parseFloat(seasonallyPrice) + if (isNaN(seasonallyPriceNumber)) { + seasonallyPriceNumber = 0 + } + } + + let yearlyPriceNumber = 0 + let yearlyPrice = this.vYearlyPrice + if (yearlyPrice == null || yearlyPrice <= 0) { + yearlyPrice = "" + } else { + yearlyPrice = yearlyPrice.toString() + yearlyPriceNumber = parseFloat(yearlyPrice) + if (isNaN(yearlyPriceNumber)) { + yearlyPriceNumber = 0 + } + } + + let trafficPrice = this.vTrafficPrice + let trafficPriceBaseNumber = 0 + if (trafficPrice != null) { + trafficPriceBaseNumber = trafficPrice.base + } else { + trafficPrice = { + base: 0 + } + } + let trafficPriceBase = "" + if (trafficPriceBaseNumber > 0) { + trafficPriceBase = trafficPriceBaseNumber.toString() + } + + return { + priceType: priceType, + monthlyPrice: monthlyPrice, + seasonallyPrice: seasonallyPrice, + yearlyPrice: yearlyPrice, + + monthlyPriceNumber: monthlyPriceNumber, + seasonallyPriceNumber: seasonallyPriceNumber, + yearlyPriceNumber: yearlyPriceNumber, + + trafficPriceBase: trafficPriceBase, + trafficPrice: trafficPrice + } + }, + watch: { + monthlyPrice: function (v) { + let price = parseFloat(v) + if (isNaN(price)) { + price = 0 + } + this.monthlyPriceNumber = price + }, + seasonallyPrice: function (v) { + let price = parseFloat(v) + if (isNaN(price)) { + price = 0 + } + this.seasonallyPriceNumber = price + }, + yearlyPrice: function (v) { + let price = parseFloat(v) + if (isNaN(price)) { + price = 0 + } + this.yearlyPriceNumber = price + }, + trafficPriceBase: function (v) { + let price = parseFloat(v) + if (isNaN(price)) { + price = 0 + } + this.trafficPrice.base = price + } + }, + template: `
+ + + + + + +
+  按时间周期     +  按流量 +
+ + +
+
+ + + + + + + + + + + + + +
月度价格 +
+ + +
+
季度价格 +
+ + +
+
年度价格 +
+ + +
+
+
+ + +
+
+ + + + + +
基础流量费用 +
+ + 元/GB +
+
+
+
` +}) + +Vue.component("http-stat-config-box", { + props: ["v-stat-config", "v-is-location", "v-is-group"], + data: function () { + let stat = this.vStatConfig + if (stat == null) { + stat = { + isPrior: false, + isOn: false + } + } + return { + stat: stat + } + }, + template: `
+ + + + + + + + + +
是否开启统计 +
+ + +
+
+
+
` +}) + +Vue.component("http-request-conds-box", { + props: ["v-conds"], + data: function () { + let conds = this.vConds + if (conds == null) { + conds = { + isOn: true, + connector: "or", + groups: [] + } + } + return { + conds: conds, + components: window.REQUEST_COND_COMPONENTS + } + }, + methods: { + change: function () { + this.$emit("change", this.conds) + }, + addGroup: function () { + window.UPDATING_COND_GROUP = null + + let that = this + teaweb.popup("/servers/server/settings/conds/addGroupPopup", { + height: "30em", + callback: function (resp) { + that.conds.groups.push(resp.data.group) + that.change() + } + }) + }, + updateGroup: function (groupIndex, group) { + window.UPDATING_COND_GROUP = group + let that = this + teaweb.popup("/servers/server/settings/conds/addGroupPopup", { + height: "30em", + callback: function (resp) { + Vue.set(that.conds.groups, groupIndex, resp.data.group) + that.change() + } + }) + }, + removeGroup: function (groupIndex) { + let that = this + teaweb.confirm("确定要删除这一组条件吗?", function () { + that.conds.groups.$remove(groupIndex) + that.change() + }) + }, + typeName: function (cond) { + let c = this.components.$find(function (k, v) { + return v.type == cond.type + }) + if (c != null) { + return c.name; + } + return cond.param + " " + cond.operator + } + }, + template: `
+ +
+ + + + + + +
分组{{groupIndex+1}} + + + {{cond.param}} {{cond.operator}} + {{typeName(cond)}}: + {{cond.value}} + + + {{group.connector}}   + + + +
+
+
+ + + + + + + +
分组之间关系 + +

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

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

SSL/TLS相关配置

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

点击可选套件添加。

+
+
是否开启HSTS +
+ + +
+

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

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

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

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

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

+
+
+
` +}) + +// Action列表 +Vue.component("http-firewall-actions-view", { + props: ["v-actions"], + template: `
+
+ {{action.name}} ({{action.code.toUpperCase()}}) +
+
` +}) + +// 显示WAF规则的标签 +Vue.component("http-firewall-rule-label", { + props: ["v-rule"], + data: function () { + return { + rule: this.vRule + } + }, + methods: { + showErr: function (err) { + + teaweb.popupTip("规则校验错误,请修正:" + teaweb.encodeHTML(err) + "") + }, + + }, + template: `
+
+ {{rule.name}}[{{rule.param}}] + + + + {{rule.checkpointOptions.period}}秒/{{rule.checkpointOptions.threshold}}请求 + + + + + {{rule.checkpointOptions.allowDomains}} + + + + | {{paramFilter.code}} + {{rule.operator}} + {{rule.value}} + + + 规则错误 +
+
` +}) + +// 缓存条件列表 +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: `
+ + +

暂时还没有缓存条件。

+
+ + + + + + + + + + + + + +
缓存条件分组关系缓存时间
+ + + {{cacheRef.minSize.count}}{{cacheRef.minSize.unit}} + - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} + + 0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} + 状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}} + + + + + {{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}} + 不缓存 +
+
+
+
` +}) + +Vue.component("ssl-certs-box", { + props: [ + "v-certs", // 证书列表 + "v-protocol", // 协议:https|tls + "v-view-size", // 弹窗尺寸 + "v-single-mode" // 单证书模式 + ], + data: function () { + let certs = this.vCerts + if (certs == null) { + certs = [] + } + + return { + certs: certs + } + }, + methods: { + certIds: function () { + return this.certs.map(function (v) { + return v.id + }) + }, + // 删除证书 + removeCert: function (index) { + let that = this + teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前服务不再使用此证书。", function () { + that.certs.$remove(index) + }) + }, + + // 选择证书 + selectCert: function () { + let that = this + let width = "50em" + let height = "30em" + let viewSize = this.vViewSize + if (viewSize == null) { + viewSize = "normal" + } + if (viewSize == "mini") { + width = "35em" + height = "20em" + } + teaweb.popup("/servers/certs/selectPopup?viewSize=" + viewSize, { + width: width, + height: height, + callback: function (resp) { + that.certs.push(resp.data.cert) + } + }) + }, + + // 上传证书 + uploadCert: function () { + let that = this + teaweb.popup("/servers/certs/uploadPopup", { + height: "28em", + callback: function (resp) { + teaweb.success("上传成功", function () { + that.certs.push(resp.data.cert) + }) + } + }) + }, + + // 格式化时间 + formatTime: function (timestamp) { + return new Date(timestamp * 1000).format("Y-m-d") + }, + + // 判断是否显示选择|上传按钮 + buttonsVisible: function () { + return this.vSingleMode == null || !this.vSingleMode || this.certs == null || this.certs.length == 0 + } + }, + template: `
+ +
+
+ {{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}   +
+
+
+
+ 选择或上传证书后HTTPSTLS服务才能生效。 +
+
+
+   +   +
+
` +}) + +Vue.component("http-host-redirect-box", { + props: ["v-redirects"], + mounted: function () { + let that = this + sortTable(function (ids) { + let newRedirects = [] + ids.forEach(function (id) { + that.redirects.forEach(function (redirect) { + if (redirect.id == id) { + newRedirects.push(redirect) + } + }) + }) + that.updateRedirects(newRedirects) + }) + }, + data: function () { + let redirects = this.vRedirects + if (redirects == null) { + redirects = [] + } + + let id = 0 + redirects.forEach(function (v) { + id++ + v.id = id + }) + + return { + redirects: redirects, + statusOptions: [ + {"code": 301, "text": "Moved Permanently"}, + {"code": 308, "text": "Permanent Redirect"}, + {"code": 302, "text": "Found"}, + {"code": 303, "text": "See Other"}, + {"code": 307, "text": "Temporary Redirect"} + ], + id: id + } + }, + methods: { + add: function () { + let that = this + window.UPDATING_REDIRECT = null + + teaweb.popup("/servers/server/settings/redirects/createPopup", { + width: "50em", + height: "30em", + callback: function (resp) { + that.id++ + resp.data.redirect.id = that.id + that.redirects.push(resp.data.redirect) + that.change() + } + }) + }, + update: function (index, redirect) { + let that = this + window.UPDATING_REDIRECT = redirect + + teaweb.popup("/servers/server/settings/redirects/createPopup", { + width: "50em", + height: "30em", + callback: function (resp) { + resp.data.redirect.id = redirect.id + Vue.set(that.redirects, index, resp.data.redirect) + that.change() + } + }) + }, + remove: function (index) { + let that = this + teaweb.confirm("确定要删除这条跳转规则吗?", function () { + that.redirects.$remove(index) + that.change() + }) + }, + change: function () { + let that = this + setTimeout(function (){ + that.$emit("change", that.redirects) + }, 100) + }, + updateRedirects: function (newRedirects) { + this.redirects = newRedirects + this.change() + } + }, + template: `
+ + + + [创建] + +
+ +

暂时还没有URL跳转规则。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
跳转前URL跳转后URL匹配模式HTTP状态码状态操作
+ {{redirect.beforeURL}} +
+ 匹配条件 +
+
->{{redirect.afterURL}} + 匹配前缀 + 正则匹配 + 精准匹配 + + {{redirect.status}} + 默认 + + 修改   + 删除 +
+

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

+
+
+
` +}) + +// 单个缓存条件设置 +Vue.component("http-cache-ref-box", { + props: ["v-cache-ref", "v-is-reverse"], + data: function () { + let ref = this.vCacheRef + if (ref == null) { + ref = { + isOn: true, + cachePolicyId: 0, + key: "${scheme}://${host}${requestURI}", + life: {count: 2, unit: "hour"}, + status: [200], + maxSize: {count: 32, unit: "mb"}, + minSize: {count: 0, unit: "kb"}, + skipCacheControlValues: ["private", "no-cache", "no-store"], + skipSetCookie: true, + enableRequestCachePragma: false, + conds: null, + allowChunkedEncoding: true, + isReverse: this.vIsReverse + } + } + if (ref.life == null) { + ref.life = {count: 2, unit: "hour"} + } + if (ref.maxSize == null) { + ref.maxSize = {count: 32, unit: "mb"} + } + if (ref.minSize == null) { + ref.minSize = {count: 0, unit: "kb"} + } + return { + ref: ref, + moreOptionsVisible: false + } + }, + methods: { + changeOptionsVisible: function (v) { + this.moreOptionsVisible = v + }, + changeLife: function (v) { + this.ref.life = v + }, + changeMaxSize: function (v) { + this.ref.maxSize = v + }, + changeMinSize: function (v) { + this.ref.minSize = v + }, + changeConds: function (v) { + this.ref.conds = v + }, + changeStatusList: function (list) { + let result = [] + list.forEach(function (status) { + let statusNumber = parseInt(status) + if (isNaN(statusNumber) || statusNumber < 100 || statusNumber > 999) { + return + } + result.push(statusNumber) + }) + this.ref.status = result + } + }, + template: ` + + 匹配条件分组 * + + + + + + + + 缓存有效期 * + + + + + + 缓存Key * + + +

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

+ + + + + + + 可缓存的最大内容尺寸 + + +

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

+ + + + 可缓存的最小内容尺寸 + + +

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

+ + + + 支持分片内容 + + +

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

+ + + + 状态码列表 + + +

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

+ + + + 跳过的Cache-Control值 + + +

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

+ + + + 跳过Set-Cookie + +
+ + +
+

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

+ + + + 支持请求no-cache刷新 + +
+ + +
+

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

+ + +` +}) + +// 浏览条件列表 +Vue.component("http-request-conds-view", { + props: ["v-conds"], + data: function () { + let conds = this.vConds + if (conds == null) { + conds = { + isOn: true, + connector: "or", + groups: [] + } + } + + let that = this + conds.groups.forEach(function (group) { + group.conds.forEach(function (cond) { + cond.typeName = that.typeName(cond) + }) + }) + + return { + initConds: conds, + version: 0 // 为了让组件能及时更新加入此变量 + } + }, + 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 + }, + notifyChange: function () { + this.version++ + let that = this + this.initConds.groups.forEach(function (group) { + group.conds.forEach(function (cond) { + cond.typeName = that.typeName(cond) + }) + }) + } + }, + template: `
+ {{version}} +
+
+ + + {{cond.param}} {{cond.operator}} + {{cond.typeName}}: + {{cond.value}} + + + {{group.connector}}   + +
+
+ {{group.description}} +
+
+
+
+` +}) + +Vue.component("http-firewall-config-box", { + props: ["v-firewall-config", "v-is-location", "v-is-group", "v-firewall-policy"], + data: function () { + let firewall = this.vFirewallConfig + if (firewall == null) { + firewall = { + isPrior: false, + isOn: false, + firewallPolicyId: 0 + } + } + + return { + firewall: firewall + } + }, + template: `
+ + + + + + + + + + + + + +
WAF策略 +
{{vFirewallPolicy.name}}   [{{vFirewallPolicy.modeInfo.name}}]  +

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

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

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

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

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

+
+
+
` +}) + +Vue.component("metric-board", { + template: `
` +}) + +Vue.component("http-cache-config-box", { + props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy"], + data: function () { + let cacheConfig = this.vCacheConfig + if (cacheConfig == null) { + cacheConfig = { + isPrior: false, + isOn: false, + addStatusHeader: true, + cacheRefs: [], + purgeIsOn: false, + purgeKey: "" + } + } + if (cacheConfig.cacheRefs == null) { + cacheConfig.cacheRefs = [] + } + return { + cacheConfig: cacheConfig, + moreOptionsVisible: false + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.cacheConfig.isPrior) && this.cacheConfig.isOn + }, + 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 + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
缓存策略 +
{{vCachePolicy.name}} +

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

+
+ 当前集群没有设置缓存策略,当前配置无法生效。 +
是否开启缓存 +
+ + +
+
+ 收起选项更多选项 +
自动添加X-Cache Header + +

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

+
允许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 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 + } + + let that = this + setTimeout(function () { + that.change() + }, 100) + + return { + keys: keys, + period: period, + threshold: threshold, + options: {}, + value: threshold + } + }, + watch: { + period: function () { + this.change() + }, + threshold: 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 + + this.vCheckpoint.options = [ + { + code: "keys", + value: this.keys + }, + { + code: "period", + value: period, + }, + { + code: "threshold", + value: threshold + } + ] + } + }, + template: `
+ + + + + + + + + + + + + + + +
统计对象组合 * + +
统计周期 * +
+ + +
+
阈值 * + +
+
` +}) + +// 防盗链 +Vue.component("http-firewall-checkpoint-referer-block", { + props: ["v-checkpoint"], + data: function () { + let allowEmpty = true + let allowSameDomain = true + let allowDomains = [] + + let options = {} + if (window.parent.UPDATING_RULE != null) { + options = window.parent.UPDATING_RULE.checkpointOptions + } + + if (options == null) { + options = {} + } + if (typeof (options.allowEmpty) == "boolean") { + allowEmpty = options.allowEmpty + } + if (typeof (options.allowSameDomain) == "boolean") { + allowSameDomain = options.allowSameDomain + } + if (options.allowDomains != null && typeof (options.allowDomains) == "object") { + allowDomains = options.allowDomains + } + + let that = this + setTimeout(function () { + that.change() + }, 100) + + return { + allowEmpty: allowEmpty, + allowSameDomain: allowSameDomain, + allowDomains: allowDomains, + options: {}, + value: 0 + } + }, + watch: { + allowEmpty: function () { + this.change() + }, + allowSameDomain: function () { + this.change() + } + }, + methods: { + changeAllowDomains: function (values) { + this.allowDomains = values + this.change() + }, + change: function () { + this.vCheckpoint.options = [ + { + code: "allowEmpty", + value: this.allowEmpty + }, + { + code: "allowSameDomain", + value: this.allowSameDomain, + }, + { + code: "allowDomains", + value: this.allowDomains + } + ] + } + }, + template: `
+ + + + + + + + + + + + + + + +
来源域名允许为空 + +

允许不带来源的访问。

+
来源域名允许一致 + +

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

+
允许的来源域名 + +

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

+
+
` +}) + +Vue.component("http-cache-refs-config-box", { + props: ["v-cache-refs", "v-cache-config", "v-cache-policy-id"], + mounted: function () { + let that = this + sortTable(function (ids) { + let newRefs = [] + ids.forEach(function (id) { + that.refs.forEach(function (ref) { + if (ref.id == id) { + newRefs.push(ref) + } + }) + }) + that.updateRefs(newRefs) + that.change() + }) + }, + data: function () { + let refs = this.vCacheRefs + if (refs == null) { + refs = [] + } + + let id = 0 + refs.forEach(function (ref) { + id++ + ref.id = id + }) + return { + refs: refs, + id: id // 用来对条件进行排序 + } + }, + methods: { + addRef: function (isReverse) { + window.UPDATING_CACHE_REF = null + + let width = window.innerWidth + if (width > 1024) { + width = 1024 + } + let height = window.innerHeight + if (height > 500) { + height = 500 + } + let that = this + teaweb.popup("/servers/server/settings/cache/createPopup?isReverse=" + (isReverse ? 1 : 0), { + width: width + "px", + height: height + "px", + callback: function (resp) { + let newRef = resp.data.cacheRef + if (newRef.conds == null) { + return + } + + that.id++ + newRef.id = that.id + + if (newRef.isReverse) { + let newRefs = [] + let isAdded = false + that.refs.forEach(function (v) { + if (!v.isReverse && !isAdded) { + newRefs.push(newRef) + isAdded = true + } + newRefs.push(v) + }) + if (!isAdded) { + newRefs.push(newRef) + } + + that.updateRefs(newRefs) + } else { + that.refs.push(newRef) + } + + that.change() + } + }) + }, + updateRef: function (index, cacheRef) { + window.UPDATING_CACHE_REF = cacheRef + + let width = window.innerWidth + if (width > 1024) { + width = 1024 + } + let height = window.innerHeight + if (height > 500) { + height = 500 + } + let that = this + teaweb.popup("/servers/server/settings/cache/createPopup", { + width: width + "px", + height: height + "px", + callback: function (resp) { + resp.data.cacheRef.id = that.refs[index].id + Vue.set(that.refs, index, resp.data.cacheRef) + + // 通知子组件更新 + that.$refs.cacheRef[index].notifyChange() + + that.change() + } + }) + }, + removeRef: function (index) { + let that = this + teaweb.confirm("确定要删除此缓存设置吗?", function () { + that.refs.$remove(index) + that.change() + }) + }, + updateRefs: function (newRefs) { + this.refs = newRefs + if (this.vCacheConfig != null) { + this.vCacheConfig.cacheRefs = newRefs + } + }, + timeUnitName: function (unit) { + switch (unit) { + case "ms": + return "毫秒" + case "second": + return "秒" + case "minute": + return "分钟" + case "hour": + return "小时" + case "day": + return "天" + case "week": + return "周 " + } + return unit + }, + change: function () { + // 自动保存 + if (this.vCachePolicyId != null && this.vCachePolicyId > 0) { + Tea.action("/servers/components/cache/updateRefs") + .params({ + cachePolicyId: this.vCachePolicyId, + refsJSON: JSON.stringify(this.refs) + }) + .post() + } + } + }, + template: `
+ + +
+

暂时还没有缓存条件。

+ + + + + + + + + + + + + + + + + + + +
缓存条件分组关系缓存时间操作
+ + + {{cacheRef.minSize.count}}{{cacheRef.minSize.unit}} + - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} + + 0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}} + 状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}} + + + + + {{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}} + 不缓存 + + 修改   + 删除 +
+

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

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

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

+

暂时还没有主要源站。

+ + +

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

+

暂时还没有备用源站。

+ +
` +}) + +Vue.component("origin-list-table", { + props: ["v-origins", "v-origin-type"], + data: function () { + return {} + }, + methods: { + deleteOrigin: function (originId) { + this.$emit("deleteOrigin", originId, this.vOriginType) + }, + updateOrigin: function (originId) { + this.$emit("updateOrigin", originId, this.vOriginType) + } + }, + template: ` + + + + + + + + + + + + + + + +
源站地址权重状态操作
{{origin.addr}}   +
+ {{origin.name}} +
+
+ {{domain}} +
+
{{origin.weight}} + + + 修改   + 删除 +
` +}) + +Vue.component("http-firewall-policy-selector", { + props: ["v-http-firewall-policy"], + mounted: function () { + let that = this + Tea.action("/servers/components/waf/count") + .post() + .success(function (resp) { + that.count = resp.data.count + }) + }, + data: function () { + let firewallPolicy = this.vHttpFirewallPolicy + return { + count: 0, + firewallPolicy: firewallPolicy + } + }, + methods: { + remove: function () { + this.firewallPolicy = null + }, + select: function () { + let that = this + teaweb.popup("/servers/components/waf/selectPopup", { + callback: function (resp) { + that.firewallPolicy = resp.data.firewallPolicy + } + }) + }, + create: function () { + let that = this + teaweb.popup("/servers/components/waf/createPopup", { + height: "26em", + callback: function (resp) { + that.firewallPolicy = resp.data.firewallPolicy + } + }) + } + }, + template: `
+
+ + {{firewallPolicy.name}}     +
+
+ [选择已有策略]     [创建新策略] +
+
` +}) + +Vue.component("http-websocket-box", { + props: ["v-websocket-ref", "v-websocket-config", "v-is-location", "v-is-group"], + data: function () { + let websocketRef = this.vWebsocketRef + if (websocketRef == null) { + websocketRef = { + isPrior: false, + isOn: false, + websocketId: 0 + } + } + + let websocketConfig = this.vWebsocketConfig + if (websocketConfig == null) { + websocketConfig = { + id: 0, + isOn: false, + handshakeTimeout: { + count: 30, + unit: "second" + }, + allowAllOrigins: true, + allowedOrigins: [], + requestSameOrigin: true, + requestOrigin: "" + } + } else { + if (websocketConfig.handshakeTimeout == null) { + websocketConfig.handshakeTimeout = { + count: 30, + unit: "second", + } + } + if (websocketConfig.allowedOrigins == null) { + websocketConfig.allowedOrigins = [] + } + } + + return { + websocketRef: websocketRef, + websocketConfig: websocketConfig, + handshakeTimeoutCountString: websocketConfig.handshakeTimeout.count.toString(), + advancedVisible: false + } + }, + watch: { + handshakeTimeoutCountString: function (v) { + let count = parseInt(v) + if (!isNaN(count) && count >= 0) { + this.websocketConfig.handshakeTimeout.count = count + } else { + this.websocketConfig.handshakeTimeout.count = 0 + } + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.websocketRef.isPrior) && this.websocketRef.isOn + }, + changeAdvancedVisible: function (v) { + this.advancedVisible = v + }, + createOrigin: function () { + let that = this + teaweb.popup("/servers/server/settings/websocket/createOrigin", { + height: "12.5em", + callback: function (resp) { + that.websocketConfig.allowedOrigins.push(resp.data.origin) + } + }) + }, + removeOrigin: function (index) { + this.websocketConfig.allowedOrigins.$remove(index) + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
是否启用配置 +
+ + +
+
允许所有来源域(Origin) +
+ + +
+

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

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

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

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

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

+
指定传递的来源域 + +

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

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

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

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

暂时还没有重写规则。

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

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

+ +
` +}) + +Vue.component("http-rewrite-labels-label", { + props: ["v-class"], + template: `` +}) + +Vue.component("server-name-box", { + props: ["v-server-names"], + data: function () { + let serverNames = this.vServerNames; + if (serverNames == null) { + serverNames = [] + } + return { + serverNames: serverNames, + isSearching: false, + keyword: "" + } + }, + methods: { + addServerName: function () { + window.UPDATING_SERVER_NAME = null + let that = this + teaweb.popup("/servers/addServerNamePopup", { + callback: function (resp) { + var serverName = resp.data.serverName + that.serverNames.push(serverName) + } + }); + }, + + removeServerName: function (index) { + this.serverNames.$remove(index) + }, + + updateServerName: function (index, serverName) { + window.UPDATING_SERVER_NAME = serverName + let that = this + teaweb.popup("/servers/addServerNamePopup", { + callback: function (resp) { + var serverName = resp.data.serverName + Vue.set(that.serverNames, index, serverName) + } + }); + }, + showSearchBox: function () { + this.isSearching = !this.isSearching + if (this.isSearching) { + let that = this + setTimeout(function () { + that.$refs.keywordRef.focus() + }, 200) + } else { + this.keyword = "" + } + }, + }, + watch: { + keyword: function (v) { + this.serverNames.forEach(function (serverName) { + if (v.length == 0) { + serverName.isShowing = true + return + } + if (serverName.subNames == null || serverName.subNames.length == 0) { + if (!teaweb.match(serverName.name, v)) { + serverName.isShowing = false + } + } else { + let found = false + serverName.subNames.forEach(function (subName) { + if (teaweb.match(subName, v)) { + found = true + } + }) + serverName.isShowing = found + } + }) + } + }, + template: `
+ +
+
+ {{serverName.type}} + {{serverName.name}} + {{serverName.subNames[0]}}等{{serverName.subNames.length}}个域名 + +
+
+
+
+ +
|
+
+ + +
+
+ +
+
+
` +}) + +// 域名列表 +Vue.component("domains-box", { + props: ["v-domains"], + data: function () { + let domains = this.vDomains + if (domains == null) { + domains = [] + } + return { + domains: domains, + isAdding: false, + addingDomain: "" + } + }, + methods: { + add: function () { + this.isAdding = true + let that = this + setTimeout(function () { + that.$refs.addingDomain.focus() + }, 100) + }, + confirm: function () { + let that = this + + // 删除其中的空格 + this.addingDomain = this.addingDomain.replace(/\s/g, "") + + if (this.addingDomain.length == 0) { + teaweb.warn("请输入要添加的域名", function () { + that.$refs.addingDomain.focus() + }) + return + } + + + // 基本校验 + if (this.addingDomain[0] == "~") { + let expr = this.addingDomain.substring(1) + try { + new RegExp(expr) + } catch (e) { + teaweb.warn("正则表达式错误:" + e.message, function () { + that.$refs.addingDomain.focus() + }) + return + } + } + + this.domains.push(this.addingDomain) + this.cancel() + }, + remove: function (index) { + this.domains.$remove(index) + }, + cancel: function () { + this.isAdding = false + this.addingDomain = "" + } + }, + template: `
+ +
+ + [正则] + [后缀] + [泛域名] + {{domain}} +   + +
+
+
+
+
+ +
+
+ +   +
+
+

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

+
+
+
+ +
+
` +}) + +Vue.component("http-redirect-to-https-box", { + props: ["v-redirect-to-https-config", "v-is-location"], + data: function () { + let redirectToHttpsConfig = this.vRedirectToHttpsConfig + if (redirectToHttpsConfig == null) { + redirectToHttpsConfig = { + isPrior: false, + isOn: false, + host: "", + port: 0, + status: 0, + onlyDomains: [], + exceptDomains: [] + } + } else { + if (redirectToHttpsConfig.onlyDomains == null) { + redirectToHttpsConfig.onlyDomains = [] + } + if (redirectToHttpsConfig.exceptDomains == null) { + redirectToHttpsConfig.exceptDomains = [] + } + } + return { + redirectToHttpsConfig: redirectToHttpsConfig, + portString: (redirectToHttpsConfig.port > 0) ? redirectToHttpsConfig.port.toString() : "", + moreOptionsVisible: false, + statusOptions: [ + {"code": 301, "text": "Moved Permanently"}, + {"code": 308, "text": "Permanent Redirect"}, + {"code": 302, "text": "Found"}, + {"code": 303, "text": "See Other"}, + {"code": 307, "text": "Temporary Redirect"} + ] + } + }, + watch: { + "redirectToHttpsConfig.status": function () { + this.redirectToHttpsConfig.status = parseInt(this.redirectToHttpsConfig.status) + }, + portString: function (v) { + let port = parseInt(v) + if (!isNaN(port)) { + this.redirectToHttpsConfig.port = port + } else { + this.redirectToHttpsConfig.port = 0 + } + } + }, + methods: { + changeMoreOptions: function (isVisible) { + this.moreOptionsVisible = isVisible + }, + changeOnlyDomains: function (values) { + this.redirectToHttpsConfig.onlyDomains = values + this.$forceUpdate() + }, + changeExceptDomains: function (values) { + this.redirectToHttpsConfig.exceptDomains = values + this.$forceUpdate() + } + }, + template: `
+ + + + + + + + + + + +
自动跳转到HTTPS +
+ + +
+

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

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

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

+
端口 + +

默认端口为443。

+
+
+ + +
+
+ + +
+

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

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

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

+
端口 + +

默认端口为443。

+
允许的域名 + +

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

+
排除的域名 + +

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

+
+
+
+
` +}) + +// 动作选择 +Vue.component("http-firewall-actions-box", { + props: ["v-actions", "v-firewall-policy", "v-action-configs"], + mounted: function () { + let that = this + Tea.action("/servers/iplists/levelOptions") + .success(function (resp) { + that.ipListLevels = resp.data.levels + }) + .post() + + this.loadJS(function () { + let box = document.getElementById("actions-box") + Sortable.create(box, { + draggable: ".label", + handle: ".icon.handle", + onStart: function () { + that.cancel() + }, + onUpdate: function (event) { + let labels = box.getElementsByClassName("label") + let newConfigs = [] + for (let i = 0; i < labels.length; i++) { + let index = parseInt(labels[i].getAttribute("data-index")) + newConfigs.push(that.configs[index]) + } + that.configs = newConfigs + } + }) + }) + }, + data: function () { + if (this.vFirewallPolicy.inbound == null) { + this.vFirewallPolicy.inbound = {} + } + if (this.vFirewallPolicy.inbound.groups == null) { + this.vFirewallPolicy.inbound.groups = [] + } + + let id = 0 + let configs = [] + if (this.vActionConfigs != null) { + configs = this.vActionConfigs + configs.forEach(function (v) { + v.id = (id++) + }) + } + + var defaultPageBody = ` + + +403 Forbidden + +` + + + return { + id: id, + + actions: this.vActions, + configs: configs, + isAdding: false, + editingIndex: -1, + + action: null, + actionCode: "", + actionOptions: {}, + + // IPList相关 + ipListLevels: [], + + // 动作参数 + blockTimeout: "", + blockScope: "global", + + captchaLife: "", + get302Life: "", + post307Life: "", + recordIPType: "black", + recordIPLevel: "critical", + recordIPTimeout: "", + recordIPListId: 0, + recordIPListName: "", + + tagTags: [], + + pageStatus: 403, + pageBody: defaultPageBody, + defaultPageBody: defaultPageBody, + + goGroupName: "", + goGroupId: 0, + goGroup: null, + + goSetId: 0, + goSetName: "" + } + }, + watch: { + actionCode: function (code) { + this.action = this.actions.$find(function (k, v) { + return v.code == code + }) + this.actionOptions = {} + }, + blockTimeout: function (v) { + v = parseInt(v) + if (isNaN(v)) { + this.actionOptions["timeout"] = 0 + } else { + this.actionOptions["timeout"] = v + } + }, + blockScope: function (v) { + this.actionOptions["scope"] = v + }, + captchaLife: function (v) { + v = parseInt(v) + if (isNaN(v)) { + this.actionOptions["life"] = 0 + } else { + this.actionOptions["life"] = v + } + }, + get302Life: function (v) { + v = parseInt(v) + if (isNaN(v)) { + this.actionOptions["life"] = 0 + } else { + this.actionOptions["life"] = v + } + }, + post307Life: function (v) { + v = parseInt(v) + if (isNaN(v)) { + this.actionOptions["life"] = 0 + } else { + this.actionOptions["life"] = v + } + }, + recordIPType: function (v) { + this.recordIPListId = 0 + }, + recordIPTimeout: function (v) { + v = parseInt(v) + if (isNaN(v)) { + this.actionOptions["timeout"] = 0 + } else { + this.actionOptions["timeout"] = v + } + }, + goGroupId: function (groupId) { + let group = this.vFirewallPolicy.inbound.groups.$find(function (k, v) { + return v.id == groupId + }) + this.goGroup = group + if (group == null) { + this.goGroupName = "" + } else { + this.goGroupName = group.name + } + this.goSetId = 0 + this.goSetName = "" + }, + goSetId: function (setId) { + if (this.goGroup == null) { + return + } + let set = this.goGroup.sets.$find(function (k, v) { + return v.id == setId + }) + if (set == null) { + this.goSetId = 0 + this.goSetName = "" + } else { + this.goSetName = set.name + } + } + }, + methods: { + add: function () { + this.action = null + this.actionCode = "block" + this.isAdding = true + this.actionOptions = {} + + // 动作参数 + this.blockTimeout = "" + this.blockScope = "global" + this.captchaLife = "" + this.get302Life = "" + this.post307Life = "" + + this.recordIPLevel = "critical" + this.recordIPType = "black" + this.recordIPTimeout = "" + this.recordIPListId = 0 + this.recordIPListName = "" + + this.tagTags = [] + + this.pageStatus = 403 + this.pageBody = this.defaultPageBody + + this.goGroupName = "" + this.goGroupId = 0 + this.goGroup = null + + this.goSetId = 0 + this.goSetName = "" + + let that = this + this.action = this.vActions.$find(function (k, v) { + return v.code == that.actionCode + }) + + // 滚到界面底部 + this.scroll() + }, + remove: function (index) { + this.isAdding = false + this.editingIndex = -1 + this.configs.$remove(index) + }, + update: function (index, config) { + if (this.isAdding && this.editingIndex == index) { + this.cancel() + return + } + + this.add() + + this.isAdding = true + this.editingIndex = index + + this.actionCode = config.code + + switch (config.code) { + case "block": + this.blockTimeout = "" + if (config.options.timeout != null || config.options.timeout > 0) { + this.blockTimeout = config.options.timeout.toString() + } + if (config.options.scope != null && config.options.scope.length > 0) { + this.blockScope = config.options.scope + } else { + this.blockScope = "global" // 兼容先前版本遗留的默认值 + } + break + case "allow": + break + case "log": + break + case "captcha": + this.captchaLife = "" + if (config.options.life != null || config.options.life > 0) { + this.captchaLife = config.options.life.toString() + } + break + case "notify": + break + case "get_302": + this.get302Life = "" + if (config.options.life != null || config.options.life > 0) { + this.get302Life = config.options.life.toString() + } + break + case "post_307": + this.post307Life = "" + if (config.options.life != null || config.options.life > 0) { + this.post307Life = config.options.life.toString() + } + break; + case "record_ip": + if (config.options != null) { + this.recordIPLevel = config.options.level + this.recordIPType = config.options.type + if (config.options.timeout > 0) { + this.recordIPTimeout = config.options.timeout.toString() + } + let that = this + + // VUE需要在函数执行完之后才会调用watch函数,这样会导致设置的值被覆盖,所以这里使用setTimeout + setTimeout(function () { + that.recordIPListId = config.options.ipListId + that.recordIPListName = config.options.ipListName + }) + } + break + case "tag": + this.tagTags = [] + if (config.options.tags != null) { + this.tagTags = config.options.tags + } + break + case "page": + this.pageStatus = 403 + this.pageBody = this.defaultPageBody + if (config.options.status != null) { + this.pageStatus = config.options.status + } + if (config.options.body != null) { + this.pageBody = config.options.body + } + + break + case "go_group": + if (config.options != null) { + this.goGroupName = config.options.groupName + this.goGroupId = config.options.groupId + this.goGroup = this.vFirewallPolicy.inbound.groups.$find(function (k, v) { + return v.id == config.options.groupId + }) + } + break + case "go_set": + if (config.options != null) { + this.goGroupName = config.options.groupName + this.goGroupId = config.options.groupId + this.goGroup = this.vFirewallPolicy.inbound.groups.$find(function (k, v) { + return v.id == config.options.groupId + }) + + // VUE需要在函数执行完之后才会调用watch函数,这样会导致设置的值被覆盖,所以这里使用setTimeout + let that = this + setTimeout(function () { + that.goSetId = config.options.setId + if (that.goGroup != null) { + let set = that.goGroup.sets.$find(function (k, v) { + return v.id == config.options.setId + }) + if (set != null) { + that.goSetName = set.name + } + } + }) + } + break + } + + // 滚到界面底部 + this.scroll() + }, + cancel: function () { + this.isAdding = false + this.editingIndex = -1 + }, + confirm: function () { + if (this.action == null) { + return + } + + if (this.actionOptions == null) { + this.actionOptions = {} + } + + // record_ip + if (this.actionCode == "record_ip") { + let timeout = parseInt(this.recordIPTimeout) + if (isNaN(timeout)) { + timeout = 0 + } + if (this.recordIPListId <= 0) { + return + } + this.actionOptions = { + type: this.recordIPType, + level: this.recordIPLevel, + timeout: timeout, + ipListId: this.recordIPListId, + ipListName: this.recordIPListName + } + } else if (this.actionCode == "tag") { // tag + if (this.tagTags == null || this.tagTags.length == 0) { + return + } + this.actionOptions = { + tags: this.tagTags + } + } else if (this.actionCode == "page") { + let pageStatus = this.pageStatus.toString() + if (!pageStatus.match(/^\d{3}$/)) { + pageStatus = 403 + } else { + pageStatus = parseInt(pageStatus) + } + + this.actionOptions = { + status: pageStatus, + body: this.pageBody + } + } else if (this.actionCode == "go_group") { // go_group + let groupId = this.goGroupId + if (typeof (groupId) == "string") { + groupId = parseInt(groupId) + if (isNaN(groupId)) { + groupId = 0 + } + } + if (groupId <= 0) { + return + } + this.actionOptions = { + groupId: groupId.toString(), + groupName: this.goGroupName + } + } else if (this.actionCode == "go_set") { // go_set + let groupId = this.goGroupId + if (typeof (groupId) == "string") { + groupId = parseInt(groupId) + if (isNaN(groupId)) { + groupId = 0 + } + } + + let setId = this.goSetId + if (typeof (setId) == "string") { + setId = parseInt(setId) + if (isNaN(setId)) { + setId = 0 + } + } + if (setId <= 0) { + return + } + this.actionOptions = { + groupId: groupId.toString(), + groupName: this.goGroupName, + setId: setId.toString(), + setName: this.goSetName + } + } + + let options = {} + for (let k in this.actionOptions) { + if (this.actionOptions.hasOwnProperty(k)) { + options[k] = this.actionOptions[k] + } + } + if (this.editingIndex > -1) { + this.configs[this.editingIndex] = { + id: this.configs[this.editingIndex].id, + code: this.actionCode, + name: this.action.name, + options: options + } + } else { + this.configs.push({ + id: (this.id++), + code: this.actionCode, + name: this.action.name, + options: options + }) + } + + this.cancel() + }, + removeRecordIPList: function () { + this.recordIPListId = 0 + }, + selectRecordIPList: function () { + let that = this + teaweb.popup("/servers/iplists/selectPopup?type=" + this.recordIPType, { + width: "50em", + height: "30em", + callback: function (resp) { + that.recordIPListId = resp.data.list.id + that.recordIPListName = resp.data.list.name + } + }) + }, + changeTags: function (tags) { + this.tagTags = tags + }, + loadJS: function (callback) { + if (typeof Sortable != "undefined") { + callback() + return + } + + // 引入js + let jsFile = document.createElement("script") + jsFile.setAttribute("src", "/js/sortable.min.js") + jsFile.addEventListener("load", function () { + callback() + }) + document.head.appendChild(jsFile) + }, + scroll: function () { + setTimeout(function () { + let mainDiv = document.getElementsByClassName("main") + if (mainDiv.length > 0) { + mainDiv[0].scrollTo(0, 1000) + } + }, 10) + } + }, + template: `
+ +
+
+ {{config.name}} ({{config.code.toUpperCase()}}) + + + :有效期{{config.options.timeout}}秒 + + + :有效期{{config.options.life}}秒 + + + :有效期{{config.options.life}}秒 + + + :有效期{{config.options.life}}秒 + + + :{{config.options.ipListName}} + + + :{{config.options.tags.join(", ")}} + + + :[{{config.options.status}}] + + + :{{config.options.groupName}} + + + :{{config.options.groupName}} / {{config.options.setName}} + + + +   + [所有服务] + [当前服务] + + + +       +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
动作类型 * + +

{{action.description}}

+
封锁时间 +
+ + +
+
封锁范围 + +
有效时间 +
+ + +
+

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

+
有效时间 +
+ + +
+

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

+
有效时间 +
+ + +
+

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

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

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

+
级别 + +
超时时间 +
+ + +
+

0表示不超时。

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

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

+
` +}) + +// 认证设置 +Vue.component("http-auth-config-box", { + props: ["v-auth-config", "v-is-location"], + data: function () { + let authConfig = this.vAuthConfig + if (authConfig == null) { + authConfig = { + isPrior: false, + isOn: false + } + } + if (authConfig.policyRefs == null) { + authConfig.policyRefs = [] + } + return { + authConfig: authConfig + } + }, + methods: { + isOn: function () { + return (!this.vIsLocation || this.authConfig.isPrior) && this.authConfig.isOn + }, + add: function () { + let that = this + teaweb.popup("/servers/server/settings/access/createPopup", { + callback: function (resp) { + that.authConfig.policyRefs.push(resp.data.policyRef) + }, + 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) + }, + methodName: function (methodType) { + switch (methodType) { + case "basicAuth": + return "BasicAuth" + case "subRequest": + return "子请求" + } + return "" + } + }, + template: `
+ + + + + + + + + +
启用认证 +
+ + +
+
+
+ +
+

认证方式

+ + + + + + + + + + + + + + + + + + + +
名称认证方法参数状态操作
{{ref.authPolicy.name}} + {{methodName(ref.authPolicy.type)}} + + {{ref.authPolicy.params.users.length}}个用户 + + [{{ref.authPolicy.params.method}}] + {{ref.authPolicy.params.url}} + + + + + 修改   + 删除 +
+ +
+
+
` +}) + +Vue.component("user-selector", { + mounted: function () { + let that = this + + Tea.action("/servers/users/options") + .post() + .success(function (resp) { + that.users = resp.data.users + }) + }, + props: ["v-user-id"], + data: function () { + let userId = this.vUserId + if (userId == null) { + userId = 0 + } + return { + users: [], + userId: userId + } + }, + watch: { + userId: function (v) { + this.$emit("change", v) + } + }, + template: `
+ +
` +}) + +Vue.component("http-header-policy-box", { + props: ["v-request-header-policy", "v-request-header-ref", "v-response-header-policy", "v-response-header-ref", "v-params", "v-is-location", "v-is-group", "v-has-group-request-config", "v-has-group-response-config", "v-group-setting-url"], + data: function () { + let type = "request" + let hash = window.location.hash + if (hash == "#response") { + type = "response" + } + + // ref + let requestHeaderRef = this.vRequestHeaderRef + if (requestHeaderRef == null) { + requestHeaderRef = { + isPrior: false, + isOn: true, + headerPolicyId: 0 + } + } + + let responseHeaderRef = this.vResponseHeaderRef + if (responseHeaderRef == null) { + responseHeaderRef = { + isPrior: false, + isOn: true, + headerPolicyId: 0 + } + } + + // 请求相关 + let requestSettingHeaders = [] + let requestDeletingHeaders = [] + + let requestPolicy = this.vRequestHeaderPolicy + if (requestPolicy != null) { + if (requestPolicy.setHeaders != null) { + requestSettingHeaders = requestPolicy.setHeaders + } + if (requestPolicy.deleteHeaders != null) { + requestDeletingHeaders = requestPolicy.deleteHeaders + } + } + + // 响应相关 + let responseSettingHeaders = [] + let responseDeletingHeaders = [] + + let responsePolicy = this.vResponseHeaderPolicy + if (responsePolicy != null) { + if (responsePolicy.setHeaders != null) { + responseSettingHeaders = responsePolicy.setHeaders + } + if (responsePolicy.deleteHeaders != null) { + responseDeletingHeaders = responsePolicy.deleteHeaders + } + } + + return { + type: type, + typeName: (type == "request") ? "请求" : "响应", + requestHeaderRef: requestHeaderRef, + responseHeaderRef: responseHeaderRef, + requestSettingHeaders: requestSettingHeaders, + requestDeletingHeaders: requestDeletingHeaders, + responseSettingHeaders: responseSettingHeaders, + responseDeletingHeaders: responseDeletingHeaders + } + }, + methods: { + selectType: function (type) { + this.type = type + window.location.hash = "#" + type + window.location.reload() + }, + addSettingHeader: function (policyId) { + teaweb.popup("/servers/server/settings/headers/createSetPopup?" + this.vParams + "&headerPolicyId=" + policyId, { + callback: function () { + window.location.reload() + } + }) + }, + addDeletingHeader: function (policyId, type) { + teaweb.popup("/servers/server/settings/headers/createDeletePopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + type, { + callback: function () { + window.location.reload() + } + }) + }, + updateSettingPopup: function (policyId, headerId) { + teaweb.popup("/servers/server/settings/headers/updateSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&headerId=" + headerId, { + callback: function () { + window.location.reload() + } + }) + }, + deleteDeletingHeader: function (policyId, headerName) { + teaweb.confirm("确定要删除'" + headerName + "'吗?", function () { + Tea.action("/servers/server/settings/headers/deleteDeletingHeader") + .params({ + headerPolicyId: policyId, + headerName: headerName + }) + .post() + .refresh() + }) + }, + deleteHeader: function (policyId, type, headerId) { + teaweb.confirm("确定要删除此Header吗?", function () { + this.$post("/servers/server/settings/headers/delete") + .params({ + headerPolicyId: policyId, + type: type, + headerId: headerId + }) + .refresh() + } + ) + } + }, + template: `
+ + +
+ + + + +
+ + + +
+ +
+ +
+
+
+ 由于已经在当前服务分组中进行了对应的配置,在这里的配置将不会生效。 +
+
+

设置请求Header [添加新Header]

+

暂时还没有Header。

+ + + + + + + + + + + + + +
名称操作
{{header.name}}{{header.value}}修改   删除
+ +

删除请求Header

+

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

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

设置响应Header [添加新Header]

+

将会覆盖已有的同名Header。

+

暂时还没有Header。

+ + + + + + + + + + + + + +
名称操作
{{header.name}}{{header.value}}修改   删除
+ +

删除响应Header

+

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

+ + + + +
需要删除的Header +
+
{{headerName}}
+
+
+ +
+
+
+
+
` +}) + +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", { + callback: function (resp) { + that.cachePolicy = resp.data.cachePolicy + } + }) + }, + create: function () { + let that = this + teaweb.popup("/servers/components/cache/createPopup", { + height: "26em", + callback: function (resp) { + that.cachePolicy = resp.data.cachePolicy + } + }) + } + }, + template: `
+
+ + {{cachePolicy.name}}     +
+
+ [选择已有策略]     [创建新策略] +
+
` +}) + +Vue.component("http-pages-and-shutdown-box", { + props: ["v-pages", "v-shutdown-config", "v-is-location"], + data: function () { + let pages = [] + if (this.vPages != null) { + pages = this.vPages + } + let shutdownConfig = { + isPrior: false, + isOn: false, + bodyType: "url", + url: "", + body: "", + status: 0 + } + if (this.vShutdownConfig != null) { + if (this.vShutdownConfig.body == null) { + this.vShutdownConfig.body = "" + } + if (this.vShutdownConfig.bodyType == null) { + this.vShutdownConfig.bodyType = "url" + } + shutdownConfig = this.vShutdownConfig + } + + let shutdownStatus = "" + if (shutdownConfig.status > 0) { + shutdownStatus = shutdownConfig.status.toString() + } + + return { + pages: pages, + shutdownConfig: shutdownConfig, + shutdownStatus: shutdownStatus + } + }, + watch: { + shutdownStatus: function (status) { + let statusInt = parseInt(status) + if (!isNaN(statusInt) && statusInt > 0 && statusInt < 1000) { + this.shutdownConfig.status = statusInt + } else { + this.shutdownConfig.status = 0 + } + } + }, + methods: { + addPage: function () { + let that = this + teaweb.popup("/servers/server/settings/pages/createPopup", { + height: "26em", + callback: function (resp) { + that.pages.push(resp.data.page) + } + }) + }, + updatePage: function (pageIndex, pageId) { + let that = this + teaweb.popup("/servers/server/settings/pages/updatePopup?pageId=" + pageId, { + height: "26em", + callback: function (resp) { + Vue.set(that.pages, pageIndex, resp.data.page) + } + }) + }, + removePage: function (pageIndex) { + let that = this + teaweb.confirm("确定要移除此页面吗?", function () { + that.pages.$remove(pageIndex) + }) + }, + addShutdownHTMLTemplate: function () { + this.shutdownConfig.body = ` + + +\t升级中 +\t + + + +

网站升级中

+

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

+ + + + +` + } + }, + template: `
+ + + + + + + + + + + +
特殊页面 +
+
+ {{page.status}} -> {{page.url}}[HTML内容] +
+
+
+
+ +
+

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

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

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

+
HTML * + +

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

+
状态码
+

开启临时关闭页面时,所有请求的响应都会显示此页面。可用于临时升级网站使用。

+
+
+
+
` +}) + +// 压缩配置 +Vue.component("http-compression-config-box", { + props: ["v-compression-config", "v-is-location", "v-is-group"], + mounted: function () { + let that = this + sortLoad(function () { + that.initSortableTypes() + }) + }, + data: function () { + let config = this.vCompressionConfig + if (config == null) { + config = { + isPrior: false, + isOn: false, + useDefaultTypes: true, + types: ["brotli", "gzip", "deflate"], + level: 5, + decompressData: false, + gzipRef: null, + deflateRef: null, + brotliRef: null, + minLength: {count: 0, "unit": "kb"}, + maxLength: {count: 0, "unit": "kb"}, + mimeTypes: ["text/*", "application/*", "font/*"], + extensions: [".js", ".json", ".html", ".htm", ".xml", ".css", ".woff2", ".txt"], + conds: null + } + } + + if (config.types == null) { + config.types = [] + } + if (config.mimeTypes == null) { + config.mimeTypes = [] + } + if (config.extensions == null) { + config.extensions = [] + } + + let allTypes = [ + { + name: "Gzip", + code: "gzip", + isOn: true + }, + { + name: "Deflate", + code: "deflate", + isOn: true + }, + { + name: "Brotli", + code: "brotli", + isOn: true + } + ] + + let configTypes = [] + config.types.forEach(function (typeCode) { + allTypes.forEach(function (t) { + if (typeCode == t.code) { + t.isOn = true + configTypes.push(t) + } + }) + }) + allTypes.forEach(function (t) { + if (!config.types.$contains(t.code)) { + t.isOn = false + configTypes.push(t) + } + }) + + return { + config: config, + moreOptionsVisible: false, + allTypes: configTypes + } + }, + watch: { + "config.level": function (v) { + let level = parseInt(v) + if (isNaN(level)) { + level = 1 + } else if (level < 1) { + level = 1 + } else if (level > 10) { + level = 10 + } + this.config.level = level + } + }, + methods: { + isOn: function () { + return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn + }, + changeExtensions: function (values) { + values.forEach(function (v, k) { + if (v.length > 0 && v[0] != ".") { + values[k] = "." + v + } + }) + this.config.extensions = values + }, + changeMimeTypes: function (values) { + this.config.mimeTypes = values + }, + changeAdvancedVisible: function () { + this.moreOptionsVisible = !this.moreOptionsVisible + }, + changeConds: function (conds) { + this.config.conds = conds + }, + changeType: function () { + this.config.types = [] + let that = this + this.allTypes.forEach(function (v) { + if (v.isOn) { + that.config.types.push(v.code) + } + }) + }, + initSortableTypes: function () { + let box = document.querySelector("#compression-types-box") + let that = this + Sortable.create(box, { + draggable: ".checkbox", + handle: ".icon.handle", + onStart: function () { + + }, + onUpdate: function (event) { + let checkboxes = box.querySelectorAll(".checkbox") + let codes = [] + checkboxes.forEach(function (checkbox) { + let code = checkbox.getAttribute("data-code") + codes.push(code) + }) + that.config.types = codes + } + }) + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
是否启用 +
+ + +
+
压缩级别 + +

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

+
支持的扩展名 + +

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

+
支持的MimeType + +

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

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

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

+
支持已压缩内容 + +

支持对已压缩内容尝试重新使用新的算法压缩。

+
内容最小长度 + +

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

+
内容最大长度 + +

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

+
匹配条件 + +
+
+
` +}) + +Vue.component("firewall-event-level-options", { + props: ["v-value"], + mounted: function () { + let that = this + Tea.action("/ui/eventLevelOptions") + .post() + .success(function (resp) { + that.levels = resp.data.eventLevels + that.change() + }) + }, + data: function () { + let value = this.vValue + if (value == null || value.length == 0) { + value = "" // 不要给默认值,因为黑白名单等默认值均有不同 + } + + return { + levels: [], + description: "", + level: value + } + }, + methods: { + change: function () { + this.$emit("change") + + let that = this + let l = this.levels.$find(function (k, v) { + return v.code == that.level + }) + if (l != null) { + this.description = l.description + } else { + this.description = "" + } + } + }, + template: `
+ +

{{description}}

+
` +}) + +Vue.component("prior-checkbox", { + props: ["v-config"], + 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, + isOn: false, + charset: "", + isUpper: false + } + } + return { + charsetConfig: charsetConfig, + advancedVisible: false + } + }, + methods: { + changeAdvancedVisible: function (v) { + this.advancedVisible = v + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + +
是否启用 +
+ + +
+
选择字符编码 +
字符编码是否大写 +
+ + +
+

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

+
+
+
` +}) + +Vue.component("http-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" + } + } + + return { + accessLog: accessLog + } + }, + methods: { + formatCost: function (seconds) { + var s = (seconds * 1000).toString(); + var pieces = s.split("."); + if (pieces.length < 2) { + return s; + } + + return pieces[0] + "." + pieces[1].substr(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 = "" + } + }, + template: `
+ [{{accessLog.node.name}}节点] + [服务] + [{{accessLog.region}}] {{accessLog.remoteAddr}} [{{accessLog.timeLocal}}] "{{accessLog.requestMethod}} {{accessLog.scheme}}://{{accessLog.host}}{{accessLog.requestURI}} {{accessLog.proto}}" {{accessLog.status}} cache hit waf {{accessLog.firewallActions}} - {{tag}} - 耗时:{{formatCost(accessLog.requestTime)}} ms   ({{accessLog.humanTime}}) +   +
` +}) + +Vue.component("http-access-log-config-box", { + props: ["v-access-log-config", "v-fields", "v-default-field-codes", "v-is-location", "v-is-group"], + data: function () { + let that = this + + // 初始化 + setTimeout(function () { + that.changeFields() + }, 100) + + let accessLog = { + isPrior: false, + isOn: false, + fields: [], + status1: true, + status2: true, + status3: true, + status4: true, + status5: true, + + 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) + } + }) + + return { + accessLog: accessLog + } + }, + methods: { + changeFields: function () { + this.accessLog.fields = this.vFields.filter(function (v) { + return v.isChecked + }).map(function (v) { + return v.code + }) + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + +
是否开启访问日志存储 +
+ + +
+

关闭访问日志,并不影响统计的运行。

+
要存储的访问日志字段 +
+ + +
+
要存储的访问日志状态码 +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+

WAF相关

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

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

+
+
+
+
` +}) + +// 显示流量限制说明 +Vue.component("traffic-limit-view", { + props: ["v-traffic-limit"], + data: function () { + return { + config: this.vTrafficLimit + } + }, + template: `
+
+ 日流量限制:{{config.dailySize.count}}{{config.dailySize.unit.toUpperCase()}}
+ 月流量限制:{{config.monthlySize.count}}{{config.monthlySize.unit.toUpperCase()}}
+
+ 没有限制。 +
` +}) + +// 基本认证用户配置 +Vue.component("http-auth-basic-auth-user-box", { + props: ["v-users"], + data: function () { + let users = this.vUsers + if (users == null) { + users = [] + } + return { + users: users, + isAdding: false, + updatingIndex: -1, + + username: "", + password: "" + } + }, + methods: { + add: function () { + this.isAdding = true + this.username = "" + this.password = "" + + let that = this + setTimeout(function () { + that.$refs.username.focus() + }, 100) + }, + cancel: function () { + this.isAdding = false + this.updatingIndex = -1 + }, + confirm: function () { + let that = this + if (this.username.length == 0) { + teaweb.warn("请输入用户名", function () { + that.$refs.username.focus() + }) + return + } + if (this.password.length == 0) { + teaweb.warn("请输入密码", function () { + that.$refs.password.focus() + }) + return + } + if (this.updatingIndex < 0) { + this.users.push({ + username: this.username, + password: this.password + }) + } else { + this.users[this.updatingIndex].username = this.username + this.users[this.updatingIndex].password = this.password + } + this.cancel() + }, + update: function (index, user) { + this.updatingIndex = index + + this.isAdding = true + this.username = user.username + this.password = user.password + + let that = this + setTimeout(function () { + that.$refs.username.focus() + }, 100) + }, + remove: function (index) { + this.users.$remove(index) + } + }, + template: `
+ +
+
+ {{user.username}} + +
+
+
+
+
+
+ +
+
+ +
+
+   + +
+
+
+
+ +
+
` +}) + +Vue.component("http-location-labels", { + props: ["v-location-config", "v-server-id"], + data: function () { + return { + location: this.vLocationConfig + } + }, + methods: { + // 判断是否已启用某配置 + configIsOn: function (config) { + return config != null && config.isPrior && config.isOn + }, + + refIsOn: function (ref, config) { + return this.configIsOn(ref) && config != null && config.isOn + }, + + len: function (arr) { + return (arr == null) ? 0 : arr.length + }, + url: function (path) { + return "/servers/server/settings/locations" + path + "?serverId=" + this.vServerId + "&locationId=" + this.location.id + } + }, + template: `
+ + {{location.name}} + BREAK + + + 自动跳转HTTPS + + + 文档根目录 + + + 反向代理 + + + + + + CACHE + + + {{location.web.charset.charset}} + + + + + + + + + Gzip:{{location.web.gzip.level}} + + + 请求Header + 响应Header + + + Websocket + + +
+
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("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("reverse-proxy-box", { + props: ["v-reverse-proxy-ref", "v-reverse-proxy-config", "v-is-location", "v-is-group", "v-family"], + data: function () { + let reverseProxyRef = this.vReverseProxyRef + if (reverseProxyRef == null) { + reverseProxyRef = { + isPrior: false, + isOn: false, + reverseProxyId: 0 + } + } + + let reverseProxyConfig = this.vReverseProxyConfig + if (reverseProxyConfig == null) { + reverseProxyConfig = { + requestPath: "", + stripPrefix: "", + requestURI: "", + requestHost: "", + requestHostType: 0, + addHeaders: [], + connTimeout: {count: 0, unit: "second"}, + readTimeout: {count: 0, unit: "second"}, + idleTimeout: {count: 0, unit: "second"}, + maxConns: 0, + maxIdleConns: 0 + } + } + if (reverseProxyConfig.addHeaders == null) { + reverseProxyConfig.addHeaders = [] + } + if (reverseProxyConfig.connTimeout == null) { + reverseProxyConfig.connTimeout = {count: 0, unit: "second"} + } + if (reverseProxyConfig.readTimeout == null) { + reverseProxyConfig.readTimeout = {count: 0, unit: "second"} + } + if (reverseProxyConfig.idleTimeout == null) { + reverseProxyConfig.idleTimeout = {count: 0, unit: "second"} + } + + if (reverseProxyConfig.proxyProtocol == null) { + // 如果直接赋值Vue将不会触发变更通知 + Vue.set(reverseProxyConfig, "proxyProtocol", { + isOn: false, + version: 1 + }) + } + + let forwardHeaders = [ + { + name: "X-Real-IP", + isChecked: false + }, + { + name: "X-Forwarded-For", + isChecked: false + }, + { + name: "X-Forwarded-By", + isChecked: false + }, + { + name: "X-Forwarded-Host", + isChecked: false + }, + { + name: "X-Forwarded-Proto", + isChecked: false + } + ] + forwardHeaders.forEach(function (v) { + v.isChecked = reverseProxyConfig.addHeaders.$contains(v.name) + }) + + return { + reverseProxyRef: reverseProxyRef, + reverseProxyConfig: reverseProxyConfig, + advancedVisible: false, + family: this.vFamily, + forwardHeaders: forwardHeaders + } + }, + watch: { + "reverseProxyConfig.requestHostType": function (v) { + let requestHostType = parseInt(v) + if (isNaN(requestHostType)) { + requestHostType = 0 + } + this.reverseProxyConfig.requestHostType = requestHostType + }, + "reverseProxyConfig.connTimeout.count": function (v) { + let count = parseInt(v) + if (isNaN(count) || count < 0) { + count = 0 + } + this.reverseProxyConfig.connTimeout.count = count + }, + "reverseProxyConfig.readTimeout.count": function (v) { + let count = parseInt(v) + if (isNaN(count) || count < 0) { + count = 0 + } + this.reverseProxyConfig.readTimeout.count = count + }, + "reverseProxyConfig.idleTimeout.count": function (v) { + let count = parseInt(v) + if (isNaN(count) || count < 0) { + count = 0 + } + this.reverseProxyConfig.idleTimeout.count = count + }, + "reverseProxyConfig.maxConns": function (v) { + let maxConns = parseInt(v) + if (isNaN(maxConns) || maxConns < 0) { + maxConns = 0 + } + this.reverseProxyConfig.maxConns = maxConns + }, + "reverseProxyConfig.maxIdleConns": function (v) { + let maxIdleConns = parseInt(v) + if (isNaN(maxIdleConns) || maxIdleConns < 0) { + maxIdleConns = 0 + } + this.reverseProxyConfig.maxIdleConns = maxIdleConns + }, + "reverseProxyConfig.proxyProtocol.version": function (v) { + let version = parseInt(v) + if (isNaN(version)) { + version = 1 + } + this.reverseProxyConfig.proxyProtocol.version = version + } + }, + methods: { + isOn: function () { + if (this.vIsLocation || this.vIsGroup) { + return this.reverseProxyRef.isPrior && this.reverseProxyRef.isOn + } + return this.reverseProxyRef.isOn + }, + changeAdvancedVisible: function (v) { + this.advancedVisible = v + }, + changeAddHeader: function () { + this.reverseProxyConfig.addHeaders = this.forwardHeaders.filter(function (v) { + return v.isChecked + }).map(function (v) { + return v.name + }) + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
是否启用反向代理 +
+ + +
+
回源主机名(Host) + 跟随代理服务   + 跟随源站   + 自定义 +
+ +
+

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

+
自动添加的Header +
+
+ {{header.name}} +
+
+
+

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

+
请求URI(RequestURI) + +

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

+
去除URL前缀(StripPrefix) + +

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

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

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

+
源站连接失败超时时间 +
+
+ +
+
+ 秒 +
+
+

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

+
源站读取超时时间 +
+
+ +
+
+ 秒 +
+
+

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

+
源站最大并发连接数 +
+
+ +
+
+

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

+
源站最大空闲连接数 +
+
+ +
+
+

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

+
源站最大空闲超时时间 +
+
+ +
+
+ 秒 +
+
+

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

+
PROXY Protocol + +

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

+
PROXY Protocol版本 + +

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

+

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

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

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

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

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

+
获取IP方式 * + +

{{option.description}}

+
读取IP变量值 * + +
+ +

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

+
+
+
+
` +}) + +// 访问日志搜索框 +Vue.component("http-access-log-search-box", { + props: ["v-ip", "v-domain", "v-keyword"], + 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 + } + }, + 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) + } + } + }, + template: `
+
+
+
+
+ IP + + +
+
+
+
+ 域名 + + +
+
+
+
+ 关键词 + + +
+
+ +
+ +
+
+
` +}) + +// 显示指标对象名 +Vue.component("metric-key-label", { + props: ["v-key"], + data: function () { + return { + keyDefs: window.METRIC_HTTP_KEYS + } + }, + methods: { + keyName: function (key) { + let that = this + let subKey = "" + let def = this.keyDefs.$find(function (k, v) { + if (v.code == key) { + return true + } + if (key.startsWith("${arg.") && v.code.startsWith("${arg.")) { + subKey = that.getSubKey("arg.", key) + return true + } + if (key.startsWith("${header.") && v.code.startsWith("${header.")) { + subKey = that.getSubKey("header.", key) + return true + } + if (key.startsWith("${cookie.") && v.code.startsWith("${cookie.")) { + subKey = that.getSubKey("cookie.", key) + return true + } + return false + }) + if (def != null) { + if (subKey.length > 0) { + return def.name + ": " + subKey + } + return def.name + } + return key + }, + getSubKey: function (prefix, key) { + prefix = "${" + prefix + let index = key.indexOf(prefix) + if (index >= 0) { + key = key.substring(index + prefix.length) + key = key.substring(0, key.length - 1) + return key + } + return "" + } + }, + template: `
+ {{keyName(this.vKey)}} +
` +}) + +// 指标对象 +Vue.component("metric-keys-config-box", { + props: ["v-keys"], + data: function () { + let keys = this.vKeys + if (keys == null) { + keys = [] + } + return { + keys: keys, + isAdding: false, + key: "", + subKey: "", + keyDescription: "", + + keyDefs: window.METRIC_HTTP_KEYS + } + }, + watch: { + keys: function () { + this.$emit("change", this.keys) + } + }, + methods: { + cancel: function () { + this.key = "" + this.subKey = "" + this.keyDescription = "" + this.isAdding = false + }, + confirm: function () { + if (this.key.length == 0) { + return + } + + if (this.key.indexOf(".NAME") > 0) { + if (this.subKey.length == 0) { + teaweb.warn("请输入参数值") + return + } + this.key = this.key.replace(".NAME", "." + this.subKey) + } + this.keys.push(this.key) + this.cancel() + }, + add: function () { + this.isAdding = true + let that = this + setTimeout(function () { + if (that.$refs.key != null) { + that.$refs.key.focus() + } + }, 100) + }, + remove: function (index) { + this.keys.$remove(index) + }, + changeKey: function () { + if (this.key.length == 0) { + return + } + let that = this + let def = this.keyDefs.$find(function (k, v) { + return v.code == that.key + }) + if (def != null) { + this.keyDescription = def.description + } + }, + keyName: function (key) { + let that = this + let subKey = "" + let def = this.keyDefs.$find(function (k, v) { + if (v.code == key) { + return true + } + if (key.startsWith("${arg.") && v.code.startsWith("${arg.")) { + subKey = that.getSubKey("arg.", key) + return true + } + if (key.startsWith("${header.") && v.code.startsWith("${header.")) { + subKey = that.getSubKey("header.", key) + return true + } + if (key.startsWith("${cookie.") && v.code.startsWith("${cookie.")) { + subKey = that.getSubKey("cookie.", key) + return true + } + return false + }) + if (def != null) { + if (subKey.length > 0) { + return def.name + ": " + subKey + } + return def.name + } + return key + }, + getSubKey: function (prefix, key) { + prefix = "${" + prefix + let index = key.indexOf(prefix) + if (index >= 0) { + key = key.substring(index + prefix.length) + key = key.substring(0, key.length - 1) + return key + } + return "" + } + }, + template: `
+ +
+
+ {{keyName(key)}}   +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+

{{keyDescription}}

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

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

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

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

+
去除URL前缀 + +

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

+
路径解码 +
+ + +
+

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

+
是否终止请求 +
+ + +
+

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

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

选中后表示开启自动WebP压缩。

+
图片质量 +
+ + % +
+

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

+
支持的扩展名 + +

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

+
支持的MimeType + +

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

+
内容最小长度 + +

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

+
内容最大长度 + +

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

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

{{scheduling.description}}

+
+
` +}) + +Vue.component("http-firewall-block-options", { + props: ["v-block-options"], + data: function () { + return { + blockOptions: this.vBlockOptions, + statusCode: this.vBlockOptions.statusCode, + timeout: this.vBlockOptions.timeout + } + }, + watch: { + statusCode: function (v) { + let statusCode = parseInt(v) + if (isNaN(statusCode)) { + this.blockOptions.statusCode = 403 + } else { + this.blockOptions.statusCode = statusCode + } + }, + timeout: function (v) { + let timeout = parseInt(v) + if (isNaN(timeout)) { + this.blockOptions.timeout = 0 + } else { + this.blockOptions.timeout = timeout + } + } + }, + template: `
+ + + + + + + + + + + + + + +
状态码 + +
提示内容 + +
超时时间 +
+ + +
+

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

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

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

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

URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static

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

要排除的URL前缀,有此前缀的URL都将会被匹配,通常以/开头,比如/static

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

完整的URL路径,通常以/开头,比如/static/ui.js

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

要排除的完整的URL路径,通常以/开头,比如/static/ui.js

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

匹配URL的正则表达式,比如^/static/(.*).js$

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

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

+
` +}) + +// 根据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: "" + } + if (this.vCond != null) { + cond = this.vCond + } + return { + cond: cond, + operators: window.REQUEST_COND_OPERATORS, + operator: window.REQUEST_COND_OPERATORS[0].op, + operatorDescription: window.REQUEST_COND_OPERATORS[0].description, + variables: window.REQUEST_VARIABLES, + variable: "", + + // 各种类型的值 + stringValue: "", + numberValue: "", + + modDivValue: "", + modRemValue: "", + + stringValues: [], + + versionRangeMinValue: "", + versionRangeMaxValue: "" + } + }, + methods: { + changeVariable: function () { + let v = this.cond.param + if (v == null) { + v = "" + } + this.cond.param = v + this.variable + }, + changeOperator: function () { + let that = this + this.operators.forEach(function (v) { + if (v.op == that.operator) { + that.operatorDescription = v.description + } + }) + + this.cond.operator = this.operator + + // 移动光标 + let box = document.getElementById("variables-value-box") + if (box != null) { + setTimeout(function () { + let input = box.getElementsByTagName("INPUT") + if (input.length > 0) { + input[0].focus() + } + }, 100) + } + }, + changeStringValues: function (v) { + this.stringValues = v + this.cond.value = JSON.stringify(v) + } + }, + watch: { + stringValue: function (v) { + this.cond.value = v + }, + numberValue: function (v) { + // TODO 校验数字 + this.cond.value = v + }, + modDivValue: function (v) { + if (v.length == 0) { + return + } + let div = parseInt(v) + if (isNaN(div)) { + div = 1 + } + this.modDivValue = div + this.cond.value = div + "," + this.modRemValue + }, + modRemValue: function (v) { + if (v.length == 0) { + return + } + let rem = parseInt(v) + if (isNaN(rem)) { + rem = 0 + } + this.modRemValue = rem + this.cond.value = this.modDivValue + "," + rem + }, + versionRangeMinValue: function (v) { + this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue + }, + versionRangeMaxValue: function (v) { + this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue + } + }, + template: ` + + 参数值 + + +
+
+
+ +
+
+ +
+
+
+

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

+ + + + 操作符 + +
+ +

{{operatorDescription}}

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

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

+
+ + +
+ +

要对比的数字。

+
+ + +
+ +

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

+
+
+ +

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

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

和参数值一致的字符串。

+

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

+

参数值的前缀。

+

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

+

参数值包含此字符串。

+

参数值不包含此字符串。

+
+
+ +

添加参数值列表。

+

添加参数值列表。

+

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

+

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

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

要对比的IP。

+
+
+ +

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

+
+
+ +

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

+
+ + +` +}) + +Vue.component("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("metric-period-config-box", { + props: ["v-period", "v-period-unit"], + data: function () { + let period = this.vPeriod + let periodUnit = this.vPeriodUnit + if (period == null || period.toString().length == 0) { + period = 1 + } + if (periodUnit == null || periodUnit.length == 0) { + periodUnit = "day" + } + return { + periodConfig: { + period: period, + unit: periodUnit + } + } + }, + watch: { + "periodConfig.period": function (v) { + v = parseInt(v) + if (isNaN(v) || v <= 0) { + v = 1 + } + this.periodConfig.period = v + } + }, + template: `
+ +
+
+ +
+
+ +
+
+

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

+
` +}) + +Vue.component("traffic-limit-config-box", { + props: ["v-traffic-limit"], + data: function () { + let config = this.vTrafficLimit + if (config == null) { + config = { + isOn: false, + dailySize: { + count: -1, + unit: "gb" + }, + monthlySize: { + count: -1, + unit: "gb" + }, + totalSize: { + count: -1, + unit: "gb" + }, + noticePageBody: "" + } + } + if (config.dailySize == null) { + config.dailySize = { + count: -1, + unit: "gb" + } + } + if (config.monthlySize == null) { + config.monthlySize = { + count: -1, + unit: "gb" + } + } + if (config.totalSize == null) { + config.totalSize = { + count: -1, + unit: "gb" + } + } + return { + config: config + } + }, + methods: { + showBodyTemplate: function () { + this.config.noticePageBody = ` + + +Traffic Limit Exceeded Warning + + +The site traffic has exceeded the limit. Please contact with the site administrator. + + +` + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + +
是否启用 + +

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

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

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

+
+
+
` +}) + +// 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: `
+ 绑定+   已绑定: +
+ {{list.name}} + +
+
` +}) + +Vue.component("ip-list-table", { + props: ["v-items", "v-keyword", "v-show-search-button"], + data: function () { + return { + items: this.vItems, + keyword: (this.vKeyword != null) ? this.vKeyword : "", + selectedAll: false, + hasSelectedItems: false + } + }, + methods: { + updateItem: function (itemId) { + this.$emit("update-item", itemId) + }, + deleteItem: function (itemId) { + this.$emit("delete-item", itemId) + }, + viewLogs: function (itemId) { + teaweb.popup("/servers/iplists/accessLogsPopup?itemId=" + itemId, { + width: "50em", + height: "30em" + }) + }, + changeSelectedAll: function () { + let boxes = this.$refs.itemCheckBox + if (boxes == null) { + return + } + + let that = this + boxes.forEach(function (box) { + box.checked = that.selectedAll + }) + + this.hasSelectedItems = this.selectedAll + }, + changeSelected: function (e) { + let that = this + that.hasSelectedItems = false + let boxes = that.$refs.itemCheckBox + if (boxes == null) { + return + } + boxes.forEach(function (box) { + if (box.checked) { + that.hasSelectedItems = true + } + }) + }, + deleteAll: function () { + let boxes = this.$refs.itemCheckBox + if (boxes == null) { + return + } + let itemIds = [] + boxes.forEach(function (box) { + if (box.checked) { + itemIds.push(box.value) + } + }) + if (itemIds.length == 0) { + return + } + + Tea.action("/servers/iplists/deleteItems") + .post() + .params({ + itemIds: itemIds + }) + .success(function () { + teaweb.successToast("批量删除成功", 1200, teaweb.reload) + }) + } + }, + template: `
+
+ [批量删除] +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
IP类型级别过期时间备注操作
+
+ + +
+
+ + {{item.ipFrom}}   + - {{item.ipTo}} + * +
+ 添加于 {{item.createdTime}} + + @ + + [名单:{{item.list.name}}] + [名单:{{item.list.name}} + + + + [服务:{{item.policy.server.name}}] + [服务:{{item.policy.server.name}}] + + + [策略:{{item.policy.name}}] + + + + +
+
+ IPv4 + IPv4 + IPv6 + 所有IP + + {{item.eventLevelName}} + - + +
+ {{item.expiredTime}} +
+ 已过期 +
+
+ 不过期 +
+ {{item.reason}} + - + + + + + 日志   + 修改   + 删除 +
+
` +}) + +Vue.component("ip-item-text", { + props: ["v-item"], + template: ` + * + + {{vItem.ipFrom}} + - {{vItem.ipTo}} + + {{vItem.ipFrom}} +   级别:{{vItem.eventLevelName}} +` +}) + +Vue.component("ip-box", { + props: [], + methods: { + popup: function () { + let e = this.$refs.container + let text = e.innerText + if (text == null) { + text = e.textContent + } + + teaweb.popup("/servers/ipbox?ip=" + text, { + width: "50em", + height: "30em" + }) + } + }, + template: `` +}) + +Vue.component("api-node-selector", { + props: [], + data: function () { + return {} + }, + template: `
+ 暂未实现 +
` +}) + +Vue.component("api-node-addresses-box", { + props: ["v-addrs", "v-name"], + data: function () { + let addrs = this.vAddrs + if (addrs == null) { + addrs = [] + } + return { + addrs: addrs + } + }, + methods: { + // 添加IP地址 + addAddr: function () { + let that = this; + teaweb.popup("/api/node/createAddrPopup", { + height: "16em", + callback: function (resp) { + that.addrs.push(resp.data.addr); + } + }) + }, + + // 修改地址 + updateAddr: function (index, addr) { + let that = this; + window.UPDATING_ADDR = addr + teaweb.popup("/api/node/updateAddrPopup?addressId=", { + callback: function (resp) { + Vue.set(that.addrs, index, resp.data.addr); + } + }) + }, + + // 删除IP地址 + removeAddr: function (index) { + this.addrs.$remove(index); + } + }, + template: `
+ +
+
+
+ {{addr.protocol}}://{{addr.host.quoteIP()}}:{{addr.portRange}} + + +
+
+
+
+
+ +
+
` +}) + +// 给Table增加排序功能 +function sortTable(callback) { + // 引入js + let jsFile = document.createElement("script") + jsFile.setAttribute("src", "/js/sortable.min.js") + jsFile.addEventListener("load", function () { + // 初始化 + let box = document.querySelector("#sortable-table") + if (box == null) { + return + } + Sortable.create(box, { + draggable: "tbody", + handle: ".icon.handle", + onStart: function () { + }, + onUpdate: function (event) { + let rows = box.querySelectorAll("tbody") + let rowIds = [] + rows.forEach(function (row) { + rowIds.push(parseInt(row.getAttribute("v-id"))) + }) + callback(rowIds) + } + }) + }) + document.head.appendChild(jsFile) +} + +function sortLoad(callback) { + let jsFile = document.createElement("script") + jsFile.setAttribute("src", "/js/sortable.min.js") + jsFile.addEventListener("load", function () { + if (typeof (callback) == "function") { + callback() + } + }) + document.head.appendChild(jsFile) +} + + +Vue.component("page-box", { + data: function () { + return { + page: "" + } + }, + created: function () { + let that = this; + setTimeout(function () { + that.page = Tea.Vue.page; + }) + }, + template: `
+
+
` +}) + +Vue.component("network-addresses-box", { + props: ["v-server-type", "v-addresses", "v-protocol", "v-name", "v-from", "v-support-range"], + data: function () { + let addresses = this.vAddresses + if (addresses == null) { + addresses = [] + } + let protocol = this.vProtocol + if (protocol == null) { + protocol = "" + } + + let name = this.vName + if (name == null) { + name = "addresses" + } + + let from = this.vFrom + if (from == null) { + from = "" + } + + return { + addresses: addresses, + protocol: protocol, + name: name, + from: from + } + }, + watch: { + "vServerType": function () { + this.addresses = [] + }, + "vAddresses": function () { + if (this.vAddresses != null) { + this.addresses = this.vAddresses + } + } + }, + methods: { + addAddr: function () { + let that = this + window.UPDATING_ADDR = null + teaweb.popup("/servers/addPortPopup?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 + teaweb.popup("/servers/addPortPopup?serverType=" + this.vServerType + "&protocol=" + this.protocol + "&from=" + this.from + "&supportRange=" + (this.supportRange() ? 1 : 0), { + height: "18em", + callback: function (resp) { + var addr = resp.data.address + Vue.set(that.addresses, index, addr) + + if (["https", "https4", "https6"].$contains(addr.protocol)) { + this.tlsProtocolName = "HTTPS" + } else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) { + this.tlsProtocolName = "TLS" + } + + // 发送事件 + that.$emit("change", that.addresses) + } + }) + + // 发送事件 + this.$emit("change", this.addresses) + }, + supportRange: function () { + return this.vSupportRange || (this.vServerType == "tcpProxy" || this.vServerType == "udpProxy") + } + }, + template: `
+ +
+
+ {{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}}{{addr.portRange}} + +
+
+
+ [添加端口绑定] +
` +}) + +/** + * 保存按钮 + */ +Vue.component("submit-btn", { + template: '' +}); + +// 可以展示更多条目的角图表 +Vue.component("more-items-angle", { + props: ["v-data-url", "v-url"], + data: function () { + return { + visible: false + } + }, + methods: { + show: function () { + this.visible = !this.visible + if (this.visible) { + this.showBox() + } else { + this.hideBox() + } + }, + showBox: function () { + let that = this + + this.visible = true + + Tea.action(this.vDataUrl) + .params({ + url: this.vUrl + }) + .post() + .success(function (resp) { + let groups = resp.data.groups + + let boxLeft = that.$el.offsetLeft + 120; + let boxTop = that.$el.offsetTop + 70; + + let box = document.createElement("div") + box.setAttribute("id", "more-items-box") + box.style.cssText = "z-index: 100; position: absolute; left: " + boxLeft + "px; top: " + boxTop + "px; max-height: 30em; overflow: auto; border-bottom: 1px solid rgba(34,36,38,.15)" + document.body.append(box) + + let menuHTML = "" + box.innerHTML = menuHTML + + let listener = function (e) { + if (e.target.tagName == "I") { + return + } + + if (!that.isInBox(box, e.target)) { + document.removeEventListener("click", listener) + that.hideBox() + } + } + document.addEventListener("click", listener) + }) + }, + hideBox: function () { + let box = document.getElementById("more-items-box") + if (box != null) { + box.parentNode.removeChild(box) + } + this.visible = false + }, + isInBox: function (parent, child) { + while (true) { + if (child == null) { + break + } + if (child.parentNode == parent) { + return true + } + child = child.parentNode + } + return false + } + }, + template: `` +}) + +/** + * 菜单项 + */ +Vue.component("menu-item", { + props: ["href", "active", "code"], + data: function () { + let active = this.active + if (typeof (active) == "undefined") { + var itemCode = "" + if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") { + itemCode = window.TEA.ACTION.data.firstMenuItem + } + if (itemCode != null && itemCode.length > 0 && this.code != null && this.code.length > 0) { + if (itemCode.indexOf(",") > 0) { + active = itemCode.split(",").$contains(this.code) + } else { + active = (itemCode == this.code) + } + } + } + + let href = (this.href == null) ? "" : this.href + if (typeof (href) == "string" && href.length > 0 && href.startsWith(".")) { + let qIndex = href.indexOf("?") + if (qIndex >= 0) { + href = Tea.url(href.substring(0, qIndex)) + href.substring(qIndex) + } else { + href = Tea.url(href) + } + } + + return { + vHref: href, + vActive: active + } + }, + methods: { + click: function (e) { + this.$emit("click", e) + } + }, + template: '\ + \ + ' +}); + +// 使用Icon的链接方式 +Vue.component("link-icon", { + props: ["href", "title", "target"], + data: function () { + return { + vTitle: (this.title == null) ? "打开链接" : this.title + } + }, + template: ` ` +}) + +// 带有下划虚线的连接 +Vue.component("link-red", { + props: ["href", "title"], + data: function () { + let href = this.href + if (href == null) { + href = "" + } + return { + vHref: href + } + }, + methods: { + clickPrevent: function () { + emitClick(this, arguments) + } + }, + template: `` +}) + +// 会弹出窗口的链接 +Vue.component("link-popup", { + props: ["title"], + methods: { + clickPrevent: function () { + emitClick(this, arguments) + } + }, + template: `` +}) + +Vue.component("popup-icon", { + props: ["title", "href", "height"], + methods: { + clickPrevent: function () { + if (this.href != null && this.href.length > 0) { + teaweb.popup(this.href, { + height: this.height + }) + } + } + }, + template: ` ` +}) + +// 小提示 +Vue.component("tip-icon", { + props: ["content"], + methods: { + showTip: function () { + teaweb.popupTip(this.content) + } + }, + template: `` +}) + +// 提交点击事件 +function emitClick(obj, arguments) { + let event = "click" + let newArgs = [event] + for (let i = 0; i < arguments.length; i++) { + newArgs.push(arguments[i]) + } + obj.$emit.apply(obj, newArgs) +} + +Vue.component("countries-selector", { + props: ["v-countries"], + data: function () { + let countries = this.vCountries + if (countries == null) { + countries = [] + } + let countryIds = countries.$map(function (k, v) { + return v.id + }) + return { + countries: countries, + countryIds: countryIds + } + }, + methods: { + add: function () { + let countryStringIds = this.countryIds.map(function (v) { + return v.toString() + }) + let that = this + teaweb.popup("/ui/selectCountriesPopup?countryIds=" + countryStringIds.join(","), { + width: "48em", + height: "23em", + callback: function (resp) { + that.countries = resp.data.countries + that.change() + } + }) + }, + remove: function (index) { + this.countries.$remove(index) + this.change() + }, + change: function () { + this.countryIds = this.countries.$map(function (k, v) { + return v.id + }) + } + }, + template: `
+ +
+
{{country.name}}
+
+
+
+ +
+
` +}) + +Vue.component("more-options-tbody", { + data: function () { + return { + isVisible: false + } + }, + methods: { + show: function () { + this.isVisible = !this.isVisible + this.$emit("change", this.isVisible) + } + }, + template: ` + + 更多选项收起选项 + +` +}) + +Vue.component("download-link", { + props: ["v-element", "v-file", "v-value"], + created: function () { + let that = this + setTimeout(function () { + that.url = that.composeURL() + }, 1000) + }, + data: function () { + let filename = this.vFile + if (filename == null || filename.length == 0) { + filename = "unknown-file" + } + return { + file: filename, + url: this.composeURL() + } + }, + methods: { + composeURL: function () { + let text = "" + if (this.vValue != null) { + text = this.vValue + } else { + let e = document.getElementById(this.vElement) + if (e == null) { + teaweb.warn("找不到要下载的内容") + return + } + text = e.innerText + if (text == null) { + text = e.textContent + } + } + return Tea.url("/ui/download", { + file: this.file, + text: text + }) + } + }, + template: ``, +}) + +Vue.component("values-box", { + props: ["values", "size", "maxlength", "name", "placeholder"], + data: function () { + let values = this.values; + if (values == null) { + values = []; + } + return { + "vValues": 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.vValues[index]; + var that = this; + setTimeout(function () { + that.$refs.value.focus(); + }, 200); + }, + confirm: function () { + if (this.value.length == 0) { + return + } + + if (this.isUpdating) { + Vue.set(this.vValues, this.index, this.value); + } else { + this.vValues.push(this.value); + } + this.cancel() + this.$emit("change", this.vValues) + }, + remove: function (index) { + this.vValues.$remove(index) + this.$emit("change", this.vValues) + }, + cancel: function () { + this.isUpdating = false; + this.isAdding = false; + this.value = ""; + }, + updateAll: function (values) { + this.vValeus = values + }, + addValue: function (v) { + this.vValues.push(v) + }, + + startEditing: function () { + this.isEditing = !this.isEditing + } + }, + template: `
+
+
{{value}}
+ [修改] +
+
+
+
{{value}} + +   + +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
` +}); + +Vue.component("datetime-input", { + props: ["v-name", "v-timestamp"], + mounted: function () { + let that = this + teaweb.datepicker(this.$refs.dayInput, function (v) { + that.day = v + that.hour = "23" + that.minute = "59" + that.second = "59" + that.change() + }) + }, + data: function () { + let timestamp = this.vTimestamp + if (timestamp != null) { + timestamp = parseInt(timestamp) + if (isNaN(timestamp)) { + timestamp = 0 + } + } else { + timestamp = 0 + } + + let day = "" + let hour = "" + let minute = "" + let second = "" + + if (timestamp > 0) { + let date = new Date() + date.setTime(timestamp * 1000) + + let year = date.getFullYear().toString() + let month = this.leadingZero((date.getMonth() + 1).toString(), 2) + day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2) + + hour = this.leadingZero(date.getHours().toString(), 2) + minute = this.leadingZero(date.getMinutes().toString(), 2) + second = this.leadingZero(date.getSeconds().toString(), 2) + } + + return { + timestamp: timestamp, + day: day, + hour: hour, + minute: minute, + second: second, + + hasDayError: false, + hasHourError: false, + hasMinuteError: false, + hasSecondError: false + } + }, + methods: { + change: function () { + let date = new Date() + + // day + if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) { + this.hasDayError = true + return + } + let pieces = this.day.split("-") + let year = parseInt(pieces[0]) + date.setFullYear(year) + + let month = parseInt(pieces[1]) + if (month < 1 || month > 12) { + this.hasDayError = true + return + } + date.setMonth(month - 1) + + let day = parseInt(pieces[2]) + if (day < 1 || day > 32) { + this.hasDayError = true + return + } + date.setDate(day) + + this.hasDayError = false + + // hour + if (!/^\d+$/.test(this.hour)) { + this.hasHourError = true + return + } + let hour = parseInt(this.hour) + if (isNaN(hour)) { + this.hasHourError = true + return + } + if (hour < 0 || hour >= 24) { + this.hasHourError = true + return + } + this.hasHourError = false + date.setHours(hour) + + // minute + if (!/^\d+$/.test(this.minute)) { + this.hasMinuteError = true + return + } + let minute = parseInt(this.minute) + if (isNaN(minute)) { + this.hasMinuteError = true + return + } + if (minute < 0 || minute >= 60) { + this.hasMinuteError = true + return + } + this.hasMinuteError = false + date.setMinutes(minute) + + // second + if (!/^\d+$/.test(this.second)) { + this.hasSecondError = true + return + } + let second = parseInt(this.second) + if (isNaN(second)) { + this.hasSecondError = true + return + } + if (second < 0 || second >= 60) { + this.hasSecondError = true + return + } + this.hasSecondError = false + date.setSeconds(second) + + this.timestamp = Math.floor(date.getTime() / 1000) + }, + leadingZero: function (s, l) { + if (l <= s.length) { + return s + } + for (let i = 0; i < l - s.length; i++) { + s = "0" + s + } + return s + } + }, + template: `
+ +
+
+ +
+
+
:
+
+
:
+
+
+
` +}) + +// 启用状态标签 +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: `` +}) + + +/** + * 一级菜单 + */ +Vue.component("first-menu", { + props: [], + template: ' \ +
\ + \ +
\ +
' +}); + +/** + * 更多选项 + */ +Vue.component("more-options-indicator", { + data: function () { + return { + visible: false + } + }, + methods: { + changeVisible: function () { + this.visible = !this.visible + if (Tea.Vue != null) { + Tea.Vue.moreOptionsVisible = this.visible + } + this.$emit("change", this.visible) + } + }, + template: '更多选项收起选项 ' +}); + +/** + * 二级菜单 + */ +Vue.component("second-menu", { + template: ' \ +
\ + \ +
\ +
' +}); + +Vue.component("more-options-angle", { + data: function () { + return { + isVisible: false + } + }, + methods: { + show: function () { + this.isVisible = !this.isVisible + this.$emit("change", this.isVisible) + } + }, + template: `更多选项收起选项` +}) + +/** + * 菜单项 + */ +Vue.component("inner-menu-item", { + props: ["href", "active", "code"], + data: function () { + var active = this.active; + if (typeof(active) =="undefined") { + var itemCode = ""; + if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") { + itemCode = window.TEA.ACTION.data.firstMenuItem; + } + active = (itemCode == this.code); + } + return { + vHref: (this.href == null) ? "" : this.href, + vActive: active + }; + }, + template: '\ + [] \ + ' +}); + +Vue.component("health-check-config-box", { + props: ["v-health-check-config"], + 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: false + } + 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 + } + }, + 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() + }, + "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 + } + }) + }, + editURL: function () { + this.urlIsEditing = !this.urlIsEditing + } + }, + template: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
是否启用 +
+ + +
+
URL * +
{{healthCheck.url}}   修改
+
+ + + + + + + + + + + + + + + + + +
协议 + +
域名 + +

在此集群上可以访问到的一个域名。

+
端口 + +
RequestURI
+
+

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

+
+
检测时间间隔 + +
是否自动下线 +
+ + +
+

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

+
连续上线次数 + +

连续N次检查成功后自动恢复上线。

+
连续下线次数 + +

连续N次检查失败后自动下线。

+
允许的状态码 + +
超时时间 + +
连续尝试次数 + +
每次尝试间隔 + +
终端信息(User-Agent) + +

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

+
只基础请求 + +

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

+
+
+
` +}) + +Vue.component("time-duration-box", { + props: ["v-name", "v-value", "v-count", "v-unit"], + mounted: function () { + this.change() + }, + data: function () { + let v = this.vValue + if (v == null) { + v = { + count: this.vCount, + unit: this.vUnit + } + } + if (typeof (v["count"]) != "number") { + v["count"] = -1 + } + return { + duration: v, + countString: (v.count >= 0) ? v.count.toString() : "" + } + }, + watch: { + "countString": function (newValue) { + let value = newValue.trim() + if (value.length == 0) { + this.duration.count = -1 + return + } + let count = parseInt(value) + if (!isNaN(count)) { + this.duration.count = count + } + this.change() + } + }, + methods: { + change: function () { + this.$emit("change", this.duration) + } + }, + template: `
+ +
+ +
+
+ +
+
` +}) + +Vue.component("not-found-box", { + props: ["message"], + template: `
+
+

{{message}}

+
` +}) + +// 警告消息 +Vue.component("warning-message", { + template: `
` +}) + +let checkboxId = 0 +Vue.component("checkbox", { + props: ["name", "value", "v-value", "id", "checked"], + data: function () { + checkboxId++ + let elementId = this.id + if (elementId == null) { + elementId = "checkbox" + checkboxId + } + + let elementValue = this.vValue + if (elementValue == null) { + elementValue = "1" + } + + let checkedValue = this.value + if (checkedValue == null && this.checked == "checked") { + checkedValue = elementValue + } + + return { + elementId: elementId, + elementValue: elementValue, + newValue: checkedValue + } + }, + methods: { + change: function () { + this.$emit("input", this.newValue) + } + }, + watch: { + value: function (v) { + if (typeof v == "boolean") { + this.newValue = v + } + } + }, + template: `
+ + +
` +}) + +Vue.component("network-addresses-view", { + props: ["v-addresses"], + template: `
+
+ {{addr.protocol}}://{{addr.host.quoteIP()}}*:{{addr.portRange}} +
+
` +}) + +Vue.component("size-capacity-view", { + props:["v-default-text", "v-value"], + template: `
+ {{vValue.count}}{{vValue.unit.toUpperCase()}} + {{vDefaultText}} +
` +}) + +// 信息提示窗口 +Vue.component("tip-message-box", { + props: ["code"], + mounted: function () { + let that = this + Tea.action("/ui/showTip") + .params({ + code: this.code + }) + .success(function (resp) { + that.visible = resp.data.visible + }) + .post() + }, + data: function () { + return { + visible: false + } + }, + methods: { + close: function () { + this.visible = false + Tea.action("/ui/hideTip") + .params({ + code: this.code + }) + .post() + } + }, + template: `
+ + +
+ +
+
` +}) + +Vue.component("keyword", { + props: ["v-word"], + data: function () { + let word = this.vWord + if (word == null) { + word = "" + } else { + word = word.replace(/\)/, "\\)") + word = word.replace(/\(/, "\\(") + word = word.replace(/\+/, "\\+") + word = word.replace(/\^/, "\\^") + word = word.replace(/\$/, "\\$") + } + + let slot = this.$slots["default"][0] + let text = this.encodeHTML(slot.text) + if (word.length > 0) { + text = text.replace(new RegExp("(" + word + ")", "ig"), "$1") + } + + return { + word: word, + text: text + } + }, + methods: { + encodeHTML: function (s) { + s = s.replace("&", "&") + s = s.replace("<", "<") + s = s.replace(">", ">") + return s + } + }, + template: `` +}) + +Vue.component("node-log-row", { + props: ["v-log", "v-keyword"], + data: function () { + return { + log: this.vLog, + keyword: this.vKeyword + } + }, + template: `
+
[{{log.createdTime}}][{{log.createdTime}}][{{log.tag}}]{{log.description}}   共{{log.count}}条 {{log.server.name}}
+
` +}) + +Vue.component("provinces-selector", { + props: ["v-provinces"], + data: function () { + let provinces = this.vProvinces + if (provinces == null) { + provinces = [] + } + let provinceIds = provinces.$map(function (k, v) { + return v.id + }) + return { + provinces: provinces, + provinceIds: provinceIds + } + }, + methods: { + add: function () { + let provinceStringIds = this.provinceIds.map(function (v) { + return v.toString() + }) + let that = this + teaweb.popup("/ui/selectProvincesPopup?provinceIds=" + provinceStringIds.join(","), { + width: "48em", + height: "23em", + callback: function (resp) { + that.provinces = resp.data.provinces + that.change() + } + }) + }, + remove: function (index) { + this.provinces.$remove(index) + this.change() + }, + change: function () { + this.provinceIds = this.provinces.$map(function (k, v) { + return v.id + }) + } + }, + template: `
+ +
+
{{province.name}}
+
+
+
+ +
+
` +}) + +Vue.component("csrf-token", { + created: function () { + this.refreshToken() + }, + mounted: function () { + let that = this + this.$refs.token.form.addEventListener("submit", function () { + that.refreshToken() + }) + + // 自动刷新 + setInterval(function () { + that.refreshToken() + }, 10 * 60 * 1000) + }, + data: function () { + return { + token: "" + } + }, + methods: { + refreshToken: function () { + let that = this + Tea.action("/csrf/token") + .get() + .success(function (resp) { + that.token = resp.data.token + }) + } + }, + template: `` +}) + + +Vue.component("labeled-input", { + props: ["name", "size", "maxlength", "label", "value"], + template: '
\ + \ + {{label}}\ +
' +}); + +let radioId = 0 +Vue.component("radio", { + props: ["name", "value", "v-value", "id"], + data: function () { + radioId++ + let elementId = this.id + if (elementId == null) { + elementId = "radio" + radioId + } + return { + "elementId": elementId + } + }, + methods: { + change: function () { + this.$emit("input", this.vValue) + } + }, + template: `
+ + +
` +}) + +Vue.component("copy-to-clipboard", { + props: ["v-target"], + created: function () { + if (typeof ClipboardJS == "undefined") { + let jsFile = document.createElement("script") + jsFile.setAttribute("src", "/js/clipboard.min.js") + document.head.appendChild(jsFile) + } + }, + methods: { + copy: function () { + new ClipboardJS('[data-clipboard-target]'); + teaweb.successToast("已复制到剪切板") + } + }, + template: `` +}) + +// 节点角色名称 +Vue.component("node-role-name", { + props: ["v-role"], + data: function () { + let roleName = "" + switch (this.vRole) { + case "node": + roleName = "边缘节点" + break + case "monitor": + roleName = "监控节点" + break + case "api": + roleName = "API节点" + break + case "user": + roleName = "用户平台" + break + case "admin": + roleName = "管理平台" + break + case "database": + roleName = "数据库节点" + break + case "dns": + roleName = "DNS节点" + break + case "report": + roleName = "区域监控终端" + break + } + return { + roleName: roleName + } + }, + template: `{{roleName}}` +}) + +let sourceCodeBoxIndex = 0 + +Vue.component("source-code-box", { + props: ["name", "type", "id", "read-only"], + 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 + } + 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 + }) + boxEditor.setValue(value) + + 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) + } + }, + data: function () { + let index = sourceCodeBoxIndex++ + + let valueBoxId = 'source-code-box-value-' + sourceCodeBoxIndex + if (this.id != null) { + valueBoxId = this.id + } + + return { + index: index, + valueBoxId: valueBoxId + } + }, + 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: ["v-name", "v-value", "v-bottom-left"], + mounted: function () { + let that = this + teaweb.datepicker(this.$refs.dayInput, function (v) { + that.day = v + that.change() + }, !!this.vBottomLeft) + }, + data: function () { + let name = this.vName + if (name == null) { + name = "day" + } + + let day = this.vValue + if (day == null) { + day = "" + } + + return { + name: name, + day: day + } + }, + methods: { + change: function () { + this.$emit("change", this.day) + } + }, + template: `
+ +
` +}) + +// 排序使用的箭头 +Vue.component("sort-arrow", { + props: ["name"], + data: function () { + let url = window.location.toString() + let order = "" + let 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 (argValue != "asc" && argValue != "desc") { + newArgs.push(v) + } + } else { + newArgs.push(v) + } + }) + } + if (order == "asc") { + newArgs.push(this.name + "=desc") + } else if (order == "desc") { + newArgs.push(this.name + "=asc") + } else { + newArgs.push(this.name + "=desc") + } + + let qIndex = url.indexOf("?") + if (qIndex > 0) { + url = url.substring(0, qIndex) + "?" + newArgs.join("&") + } else { + url = url + "?" + newArgs.join("&") + } + + return { + order: order, + url: url + } + }, + template: `  ` +}) + +Vue.component("user-link", { + props: ["v-user", "v-keyword"], + data: function () { + let user = this.vUser + if (user == null) { + user = {id: 0, "username": "", "fullname": ""} + } + return { + user: user + } + }, + template: `
+ {{user.fullname}}{{user.username}} + [已删除] +
` +}) + +// 监控节点分组选择 +Vue.component("report-node-groups-selector", { + props: ["v-group-ids"], + mounted: function () { + let that = this + Tea.action("/clusters/monitors/groups/options") + .post() + .success(function (resp) { + that.groups = resp.data.groups.map(function (group) { + group.isChecked = that.groupIds.$contains(group.id) + return group + }) + that.isLoaded = true + }) + }, + data: function () { + var groupIds = this.vGroupIds + if (groupIds == null) { + groupIds = [] + } + + return { + groups: [], + groupIds: groupIds, + isLoaded: false, + allGroups: groupIds.length == 0 + } + }, + methods: { + check: function (group) { + group.isChecked = !group.isChecked + this.groupIds = [] + let that = this + this.groups.forEach(function (v) { + if (v.isChecked) { + that.groupIds.push(v.id) + } + }) + this.change() + }, + change: function () { + let that = this + let groups = [] + this.groupIds.forEach(function (groupId) { + let group = that.groups.$find(function (k, v) { + return v.id == groupId + }) + if (group == null) { + return + } + groups.push({ + id: group.id, + name: group.name + }) + }) + this.$emit("change", groups) + } + }, + watch: { + allGroups: function (b) { + if (b) { + this.groupIds = [] + this.groups.forEach(function (v) { + v.isChecked = false + }) + } + + this.change() + } + }, + template: `
+ + 还没有分组。 +
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
` +}) + +Vue.component("finance-user-selector", { + mounted: function () { + let that = this + + Tea.action("/finance/users/options") + .post() + .success(function (resp) { + that.users = resp.data.users + }) + }, + props: ["v-user-id"], + data: function () { + let userId = this.vUserId + if (userId == null) { + userId = 0 + } + return { + users: [], + userId: userId + } + }, + watch: { + userId: function (v) { + this.$emit("change", v) + } + }, + template: `
+ +
` +}) + +// 节点登录推荐端口 +Vue.component("node-login-suggest-ports", { + data: function () { + return { + ports: [], + availablePorts: [], + autoSelected: false, + isLoading: false + } + }, + methods: { + reload: function (host) { + let that = this + this.autoSelected = false + this.isLoading = true + Tea.action("/clusters/cluster/suggestLoginPorts") + .params({ + host: host + }) + .success(function (resp) { + if (resp.data.availablePorts != null) { + that.availablePorts = resp.data.availablePorts + if (that.availablePorts.length > 0) { + that.autoSelectPort(that.availablePorts[0]) + that.autoSelected = true + } + } + if (resp.data.ports != null) { + that.ports = resp.data.ports + if (that.ports.length > 0 && !that.autoSelected) { + that.autoSelectPort(that.ports[0]) + that.autoSelected = true + } + } + }) + .done(function () { + that.isLoading = false + }) + .post() + }, + selectPort: function (port) { + this.$emit("select", port) + }, + autoSelectPort: function (port) { + this.$emit("auto-select", port) + } + }, + template: ` + 正在检查端口... + + 可能端口:{{port}} +     + + + 常用端口:{{port}} + + 常用端口有22等。 + (可以点击要使用的端口) +` +}) + +Vue.component("node-group-selector", { + props: ["v-cluster-id", "v-group"], + data: function () { + return { + selectedGroup: this.vGroup + } + }, + methods: { + selectGroup: function () { + let that = this + teaweb.popup("/clusters/cluster/groups/selectPopup?clusterId=" + this.vClusterId, { + callback: function (resp) { + that.selectedGroup = resp.data.group + } + }) + }, + addGroup: function () { + let that = this + teaweb.popup("/clusters/cluster/groups/createPopup?clusterId=" + this.vClusterId, { + callback: function (resp) { + that.selectedGroup = resp.data.group + } + }) + }, + removeGroup: function () { + this.selectedGroup = null + } + }, + template: `
+
+ + {{selectedGroup.name}}   +
+
+ [选择分组]   [添加分组] +
+
` +}) + +// 节点IP地址管理(标签形式) +Vue.component("node-ip-addresses-box", { + props: ["v-ip-addresses", "role"], + data: function () { + return { + ipAddresses: (this.vIpAddresses == null) ? [] : this.vIpAddresses, + supportThresholds: this.role != "ns" + } + }, + methods: { + // 添加IP地址 + addIPAddress: function () { + window.UPDATING_NODE_IP_ADDRESS = null + + let that = this; + teaweb.popup("/nodes/ipAddresses/createPopup?supportThresholds=" + (this.supportThresholds ? 1 : 0), { + callback: function (resp) { + that.ipAddresses.push(resp.data.ipAddress); + }, + height: "24em", + width: "44em" + }) + }, + + // 修改地址 + updateIPAddress: function (index, address) { + window.UPDATING_NODE_IP_ADDRESS = address + + let that = this; + teaweb.popup("/nodes/ipAddresses/updatePopup?supportThresholds=" + (this.supportThresholds ? 1 : 0), { + callback: function (resp) { + Vue.set(that.ipAddresses, index, resp.data.ipAddress); + }, + height: "24em", + width: "44em" + }) + }, + + // 删除IP地址 + removeIPAddress: function (index) { + this.ipAddresses.$remove(index); + }, + + // 判断是否为IPv6 + isIPv6: function (ip) { + return ip.indexOf(":") > -1 + } + }, + template: `
+ +
+
+
+ [IPv6] {{address.ip}} + ({{address.name}},不可访问 + (不可访问) + [off] + [down] + [{{address.thresholds.length}}个阈值] +   + + +
+
+
+
+
+ +
+
` +}) + +// 节点IP阈值 +Vue.component("node-ip-address-thresholds-view", { + props: ["v-thresholds"], + data: function () { + let thresholds = this.vThresholds + if (thresholds == null) { + thresholds = [] + } else { + thresholds.forEach(function (v) { + if (v.items == null) { + v.items = [] + } + if (v.actions == null) { + v.actions = [] + } + }) + } + + return { + thresholds: thresholds, + allItems: window.IP_ADDR_THRESHOLD_ITEMS, + allOperators: [ + { + "name": "小于等于", + "code": "lte" + }, + { + "name": "大于", + "code": "gt" + }, + { + "name": "不等于", + "code": "neq" + }, + { + "name": "小于", + "code": "lt" + }, + { + "name": "大于等于", + "code": "gte" + } + ], + allActions: window.IP_ADDR_THRESHOLD_ACTIONS + } + }, + methods: { + itemName: function (item) { + let result = "" + this.allItems.forEach(function (v) { + if (v.code == item) { + result = v.name + } + }) + return result + }, + itemUnitName: function (itemCode) { + let result = "" + this.allItems.forEach(function (v) { + if (v.code == itemCode) { + result = v.unit + } + }) + return result + }, + itemDurationUnitName: function (unit) { + switch (unit) { + case "minute": + return "分钟" + case "second": + return "秒" + case "hour": + return "小时" + case "day": + return "天" + } + return unit + }, + itemOperatorName: function (operator) { + let result = "" + this.allOperators.forEach(function (v) { + if (v.code == operator) { + result = v.name + } + }) + return result + }, + actionName: function (actionCode) { + let result = "" + this.allActions.forEach(function (v) { + if (v.code == actionCode) { + result = v.name + } + }) + return result + } + }, + template: `
+ +
+
+ + + + [{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}] + + {{itemName(item.item)}} + + + + 成功 + 失败 + + + + [{{group.name}}   ] + + [{{itemOperatorName(item.operator)}}] {{item.value}}{{itemUnitName(item.item)}}   + + + AND   + -> + {{actionName(action.action)}} + 到{{action.options.ips.join(", ")}} + ({{action.options.url}}) +   + AND   + +
+
+
` +}) + +// 节点IP阈值 +Vue.component("node-ip-address-thresholds-box", { + props: ["v-thresholds"], + data: function () { + let thresholds = this.vThresholds + if (thresholds == null) { + thresholds = [] + } else { + thresholds.forEach(function (v) { + if (v.items == null) { + v.items = [] + } + if (v.actions == null) { + v.actions = [] + } + }) + } + + return { + editingIndex: -1, + thresholds: thresholds, + addingThreshold: { + items: [], + actions: [] + }, + isAdding: false, + isAddingItem: false, + isAddingAction: false, + + itemCode: "nodeAvgRequests", + itemReportGroups: [], + itemOperator: "lte", + itemValue: "", + itemDuration: "5", + allItems: window.IP_ADDR_THRESHOLD_ITEMS, + allOperators: [ + { + "name": "小于等于", + "code": "lte" + }, + { + "name": "大于", + "code": "gt" + }, + { + "name": "不等于", + "code": "neq" + }, + { + "name": "小于", + "code": "lt" + }, + { + "name": "大于等于", + "code": "gte" + } + ], + allActions: window.IP_ADDR_THRESHOLD_ACTIONS, + + actionCode: "up", + actionBackupIPs: "", + actionWebHookURL: "" + } + }, + methods: { + add: function () { + this.isAdding = !this.isAdding + }, + cancel: function () { + this.isAdding = false + this.editingIndex = -1 + this.addingThreshold = { + items: [], + actions: [] + } + }, + confirm: function () { + if (this.addingThreshold.items.length == 0) { + teaweb.warn("需要至少添加一个阈值") + return + } + if (this.addingThreshold.actions.length == 0) { + teaweb.warn("需要至少添加一个动作") + return + } + + if (this.editingIndex >= 0) { + this.thresholds[this.editingIndex].items = this.addingThreshold.items + this.thresholds[this.editingIndex].actions = this.addingThreshold.actions + } else { + this.thresholds.push({ + items: this.addingThreshold.items, + actions: this.addingThreshold.actions + }) + } + + // 还原 + this.cancel() + }, + remove: function (index) { + this.cancel() + this.thresholds.$remove(index) + }, + update: function (index) { + this.editingIndex = index + this.addingThreshold = { + items: this.thresholds[index].items.$copy(), + actions: this.thresholds[index].actions.$copy() + } + this.isAdding = true + }, + + addItem: function () { + this.isAddingItem = !this.isAddingItem + let that = this + setTimeout(function () { + that.$refs.itemValue.focus() + }, 100) + }, + cancelItem: function () { + this.isAddingItem = false + + this.itemCode = "nodeAvgRequests" + this.itmeOperator = "lte" + this.itemValue = "" + this.itemDuration = "5" + this.itemReportGroups = [] + }, + confirmItem: function () { + // 特殊阈值快速添加 + if (["nodeHealthCheck"].$contains(this.itemCode)) { + if (this.itemValue.toString().length == 0) { + teaweb.warn("请选择检查结果") + return + } + + let value = parseInt(this.itemValue) + if (isNaN(value)) { + value = 0 + } else if (value < 0) { + value = 0 + } else if (value > 1) { + value = 1 + } + + // 添加 + this.addingThreshold.items.push({ + item: this.itemCode, + operator: this.itemOperator, + value: value, + duration: 0, + durationUnit: "minute", + options: {} + }) + this.cancelItem() + return + } + + if (this.itemDuration.length == 0) { + let that = this + teaweb.warn("请输入统计周期", function () { + that.$refs.itemDuration.focus() + }) + return + } + let itemDuration = parseInt(this.itemDuration) + if (isNaN(itemDuration) || itemDuration <= 0) { + teaweb.warn("请输入正确的统计周期", function () { + that.$refs.itemDuration.focus() + }) + return + } + + if (this.itemValue.length == 0) { + let that = this + teaweb.warn("请输入对比值", function () { + that.$refs.itemValue.focus() + }) + return + } + let itemValue = parseFloat(this.itemValue) + if (isNaN(itemValue)) { + teaweb.warn("请输入正确的对比值", function () { + that.$refs.itemValue.focus() + }) + return + } + + + let options = {} + + switch (this.itemCode) { + case "connectivity": // 连通性校验 + if (itemValue > 100) { + let that = this + teaweb.warn("连通性对比值不能超过100", function () { + that.$refs.itemValue.focus() + }) + return + } + + options["groups"] = this.itemReportGroups + break + } + + // 添加 + this.addingThreshold.items.push({ + item: this.itemCode, + operator: this.itemOperator, + value: itemValue, + duration: itemDuration, + durationUnit: "minute", + options: options + }) + + // 还原 + this.cancelItem() + }, + removeItem: function (index) { + this.cancelItem() + this.addingThreshold.items.$remove(index) + }, + changeReportGroups: function (groups) { + this.itemReportGroups = groups + }, + itemName: function (item) { + let result = "" + this.allItems.forEach(function (v) { + if (v.code == item) { + result = v.name + } + }) + return result + }, + itemUnitName: function (itemCode) { + let result = "" + this.allItems.forEach(function (v) { + if (v.code == itemCode) { + result = v.unit + } + }) + return result + }, + itemDurationUnitName: function (unit) { + switch (unit) { + case "minute": + return "分钟" + case "second": + return "秒" + case "hour": + return "小时" + case "day": + return "天" + } + return unit + }, + itemOperatorName: function (operator) { + let result = "" + this.allOperators.forEach(function (v) { + if (v.code == operator) { + result = v.name + } + }) + return result + }, + + addAction: function () { + this.isAddingAction = !this.isAddingAction + }, + cancelAction: function () { + this.isAddingAction = false + this.actionCode = "up" + this.actionBackupIPs = "" + this.actionWebHookURL = "" + }, + confirmAction: function () { + this.doConfirmAction(false) + }, + doConfirmAction: function (validated, options) { + // 是否已存在 + let exists = false + let that = this + this.addingThreshold.actions.forEach(function (v) { + if (v.action == that.actionCode) { + exists = true + } + }) + if (exists) { + teaweb.warn("此动作已经添加过了,无需重复添加") + return + } + + if (options == null) { + options = {} + } + + switch (this.actionCode) { + case "switch": + if (!validated) { + Tea.action("/ui/validateIPs") + .params({ + "ips": this.actionBackupIPs + }) + .success(function (resp) { + if (resp.data.ips.length == 0) { + teaweb.warn("请输入备用IP", function () { + that.$refs.actionBackupIPs.focus() + }) + return + } + options["ips"] = resp.data.ips + that.doConfirmAction(true, options) + }) + .fail(function (resp) { + teaweb.warn("输入的IP '" + resp.data.failIP + "' 格式不正确,请改正后提交", function () { + that.$refs.actionBackupIPs.focus() + }) + }) + .post() + return + } + break + case "webHook": + if (this.actionWebHookURL.length == 0) { + teaweb.warn("请输入WebHook URL", function () { + that.$refs.webHookURL.focus() + }) + return + } + if (!this.actionWebHookURL.match(/^(http|https):\/\//i)) { + teaweb.warn("URL开头必须是http://或者https://", function () { + that.$refs.webHookURL.focus() + }) + return + } + options["url"] = this.actionWebHookURL + } + + this.addingThreshold.actions.push({ + action: this.actionCode, + options: options + }) + + // 还原 + this.cancelAction() + }, + removeAction: function (index) { + this.cancelAction() + this.addingThreshold.actions.$remove(index) + }, + actionName: function (actionCode) { + let result = "" + this.allActions.forEach(function (v) { + if (v.code == actionCode) { + result = v.name + } + }) + return result + } + }, + template: `
+ + + +
+
+ + + [{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}] + + {{itemName(item.item)}} + + + + 成功 + 失败 + + + + [{{group.name}}   ] + + [{{itemOperatorName(item.operator)}}]  {{item.value}}{{itemUnitName(item.item)}} + +  AND   + + -> + {{actionName(action.action)}} + 到{{action.options.ips.join(", ")}} + ({{action.options.url}}) +  AND   +   + + +
+
+ + +
+ + + + + + + + + + + +
阈值动作
+ +
+
+ + [{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}] + + {{itemName(item.item)}} + + + + 成功 + 失败 + + + + [{{group.name}}   ] + [{{itemOperatorName(item.operator)}}] {{item.value}}{{itemUnitName(item.item)}} + +   + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
统计项目 + +

{{item.description}}

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

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

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

{{action.description}}

+
备用IP * + +

每行一个备用IP。

+
URL * + +

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

+
+
+   + +
+
+ +
+ +
+
+ + +
+   + +
+
+ +
+ +
+
` +}) + +Vue.component("node-region-selector", { + props: ["v-region"], + data: function () { + return { + selectedRegion: this.vRegion + } + }, + methods: { + selectRegion: function () { + let that = this + teaweb.popup("/clusters/regions/selectPopup?clusterId=" + this.vClusterId, { + callback: function (resp) { + that.selectedRegion = resp.data.region + } + }) + }, + addRegion: function () { + let that = this + teaweb.popup("/clusters/regions/createPopup?clusterId=" + this.vClusterId, { + callback: function (resp) { + that.selectedRegion = resp.data.region + } + }) + }, + removeRegion: function () { + this.selectedRegion = null + } + }, + template: `
+
+ + {{selectedRegion.name}}   +
+
+ [选择区域]   [添加区域] +
+
` +}) + +Vue.component("dns-route-selector", { + props: ["v-all-routes", "v-routes"], + data: function () { + let routes = this.vRoutes + if (routes == null) { + routes = [] + } + routes.$sort(function (v1, v2) { + if (v1.domainId == v2.domainId) { + return v1.code < v2.code + } + return (v1.domainId < v2.domainId) ? 1 : -1 + }) + return { + routes: routes, + routeCodes: routes.$map(function (k, v) { + return v.code + "@" + v.domainId + }), + isAdding: false, + routeCode: "", + keyword: "", + searchingRoutes: this.vAllRoutes.$copy() + } + }, + methods: { + add: function () { + this.isAdding = true + this.keyword = "" + this.routeCode = "" + + let that = this + setTimeout(function () { + that.$refs.keywordRef.focus() + }, 200) + }, + cancel: function () { + this.isAdding = false + }, + confirm: function () { + if (this.routeCode.length == 0) { + return + } + if (this.routeCodes.$contains(this.routeCode)) { + teaweb.warn("已经添加过此线路,不能重复添加") + return + } + let that = this + let route = this.vAllRoutes.$find(function (k, v) { + return v.code + "@" + v.domainId == that.routeCode + }) + if (route == null) { + return + } + + this.routeCodes.push(this.routeCode) + this.routes.push(route) + + this.routes.$sort(function (v1, v2) { + if (v1.domainId == v2.domainId) { + return v1.code < v2.code + } + return (v1.domainId < v2.domainId) ? 1 : -1 + }) + + this.routeCode = "" + this.isAdding = false + }, + remove: function (route) { + this.routeCodes.$removeValue(route.code + "@" + route.domainId) + this.routes.$removeIf(function (k, v) { + return v.code + "@" + v.domainId == route.code + "@" + route.domainId + }) + } + }, + watch: { + keyword: function (keyword) { + if (keyword.length == 0) { + this.searchingRoutes = this.vAllRoutes.$copy() + this.routeCode = "" + return + } + this.searchingRoutes = this.vAllRoutes.filter(function (route) { + return teaweb.match(route.name, keyword) || teaweb.match(route.domainName, keyword) + }) + if (this.searchingRoutes.length > 0) { + this.routeCode = this.searchingRoutes[0].code + "@" + this.searchingRoutes[0].domainId + } else { + this.routeCode = "" + } + } + }, + template: `
+ +
+ + {{route.name}} ({{route.domainName}}) + +
+
+ +
+
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+
` +}) + +Vue.component("dns-domain-selector", { + props: ["v-domain-id", "v-domain-name"], + data: function () { + let domainId = this.vDomainId + if (domainId == null) { + domainId = 0 + } + let domainName = this.vDomainName + if (domainName == null) { + domainName = "" + } + return { + domainId: domainId, + domainName: domainName + } + }, + 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.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.change() + } + }) + }, + change: function () { + this.$emit("change", { + id: this.domainId, + name: this.domainName + }) + } + }, + template: `
+ +
+ + {{domainName}} + + + +
+
+ [选择域名] +
+
` +}) + +Vue.component("grant-selector", { + props: ["v-grant", "v-node-cluster-id", "v-ns-cluster-id"], + data: function () { + return { + grantId: (this.vGrant == null) ? 0 : this.vGrant.id, + grant: this.vGrant, + nodeClusterId: (this.vNodeClusterId != null) ? this.vNodeClusterId : 0, + nsClusterId: (this.vNsClusterId != null) ? this.vNsClusterId : 0 + } + }, + methods: { + // 选择授权 + select: function () { + let that = this; + teaweb.popup("/clusters/grants/selectPopup?nodeClusterId=" + this.nodeClusterId + "&nsClusterId=" + this.nsClusterId, { + callback: (resp) => { + that.grantId = resp.data.grant.id; + if (that.grantId > 0) { + that.grant = resp.data.grant; + } + that.notifyUpdate() + }, + height: "26em" + }) + }, + + // 创建授权 + create: function () { + let that = this + teaweb.popup("/clusters/grants/createPopup", { + height: "26em", + callback: (resp) => { + that.grantId = resp.data.grant.id; + if (that.grantId > 0) { + that.grant = resp.data.grant; + } + that.notifyUpdate() + } + }) + }, + + // 修改授权 + update: function () { + if (this.grant == null) { + window.location.reload(); + return; + } + let that = this + teaweb.popup("/clusters/grants/updatePopup?grantId=" + this.grant.id, { + height: "26em", + callback: (resp) => { + that.grant = resp.data.grant + that.notifyUpdate() + } + }) + }, + + // 删除已选择授权 + remove: function () { + this.grant = null + this.grantId = 0 + this.notifyUpdate() + }, + notifyUpdate: function () { + this.$emit("change", this.grant) + } + }, + template: `
+ +
{{grant.name}}({{grant.methodName}})({{grant.username}})
+
+ [选择已有认证]     [添加新认证] +
+
` +}) + +window.REQUEST_COND_COMPONENTS = [{"type":"url-extension","name":"URL扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-extension","paramsTitle":"扩展名列表","isRequest":true},{"type":"url-prefix","name":"URL前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-prefix","paramsTitle":"URL前缀","isRequest":true},{"type":"url-eq","name":"URL精准匹配","description":"检查URL中的文件路径是否一致","component":"http-cond-url-eq","paramsTitle":"URL完整路径","isRequest":true},{"type":"url-regexp","name":"URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致","component":"http-cond-url-regexp","paramsTitle":"正则表达式","isRequest":true},{"type":"params","name":"参数匹配","description":"根据参数值进行匹配","component":"http-cond-params","paramsTitle":"参数配置","isRequest":true},{"type":"url-not-prefix","name":"排除:URL前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-not-prefix","paramsTitle":"URL前缀","isRequest":true},{"type":"url-not-eq","name":"排除:URL精准匹配","description":"检查URL中的文件路径是否一致","component":"http-cond-url-not-eq","paramsTitle":"URL完整路径","isRequest":true},{"type":"url-not-regexp","name":"排除:URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致,如果一致,则不匹配","component":"http-cond-url-not-regexp","paramsTitle":"正则表达式","isRequest":true},{"type":"mime-type","name":"内容MimeType","description":"根据服务器返回的内容的MimeType进行过滤。注意:当用于缓存条件时,此条件需要结合别的请求条件使用。","component":"http-cond-mime-type","paramsTitle":"MimeType列表","isRequest":false}] + +window.REQUEST_COND_OPERATORS = [{"description":"判断是否正则表达式匹配","name":"正则表达式匹配","op":"regexp"},{"description":"判断是否正则表达式不匹配","name":"正则表达式不匹配","op":"not regexp"},{"description":"使用字符串对比参数值是否相等于某个值","name":"字符串等于","op":"eq"},{"description":"参数值包含某个前缀","name":"字符串前缀","op":"prefix"},{"description":"参数值包含某个后缀","name":"字符串后缀","op":"suffix"},{"description":"参数值包含另外一个字符串","name":"字符串包含","op":"contains"},{"description":"参数值不包含另外一个字符串","name":"字符串不包含","op":"not contains"},{"description":"使用字符串对比参数值是否不相等于某个值","name":"字符串不等于","op":"not"},{"description":"判断参数值在某个列表中","name":"在列表中","op":"in"},{"description":"判断参数值不在某个列表中","name":"不在列表中","op":"not in"},{"description":"判断小写的扩展名(不带点)在某个列表中","name":"扩展名","op":"file ext"},{"description":"判断MimeType在某个列表中,支持类似于image/*的语法","name":"MimeType","op":"mime type"},{"description":"判断版本号在某个范围内,格式为version1,version2","name":"版本号范围","op":"version range"},{"description":"将参数转换为整数数字后进行对比","name":"整数等于","op":"eq int"},{"description":"将参数转换为可以有小数的浮点数字进行对比","name":"浮点数等于","op":"eq float"},{"description":"将参数转换为数字进行对比","name":"数字大于","op":"gt"},{"description":"将参数转换为数字进行对比","name":"数字大于等于","op":"gte"},{"description":"将参数转换为数字进行对比","name":"数字小于","op":"lt"},{"description":"将参数转换为数字进行对比","name":"数字小于等于","op":"lte"},{"description":"对整数参数值取模,除数为10,对比值为余数","name":"整数取模10","op":"mod 10"},{"description":"对整数参数值取模,除数为100,对比值为余数","name":"整数取模100","op":"mod 100"},{"description":"对整数参数值取模,对比值格式为:除数,余数,比如10,1","name":"整数取模","op":"mod"},{"description":"将参数转换为IP进行对比","name":"IP等于","op":"eq ip"},{"description":"将参数转换为IP进行对比","name":"IP大于","op":"gt ip"},{"description":"将参数转换为IP进行对比","name":"IP大于等于","op":"gte ip"},{"description":"将参数转换为IP进行对比","name":"IP小于","op":"lt ip"},{"description":"将参数转换为IP进行对比","name":"IP小于等于","op":"lte ip"},{"description":"IP在某个范围之内,范围格式可以是英文逗号分隔的ip1,ip2,或者CIDR格式的ip/bits","name":"IP范围","op":"ip range"},{"description":"对IP参数值取模,除数为10,对比值为余数","name":"IP取模10","op":"ip mod 10"},{"description":"对IP参数值取模,除数为100,对比值为余数","name":"IP取模100","op":"ip mod 100"},{"description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1","name":"IP取模","op":"ip mod"},{"description":"判断参数值解析后的文件是否存在","name":"文件存在","op":"file exist"},{"description":"判断参数值解析后的文件是否不存在","name":"文件不存在","op":"file not exist"}] + +window.REQUEST_VARIABLES = [{"code":"${edgeVersion}","description":"","name":"边缘节点版本"},{"code":"${remoteAddr}","description":"会依次根据X-Forwarded-For、X-Real-IP、RemoteAddr获取,适合前端有别的反向代理服务时使用,存在伪造的风险","name":"客户端地址(IP)"},{"code":"${rawRemoteAddr}","description":"返回直接连接服务的客户端原始IP地址","name":"客户端地址(IP)"},{"code":"${remotePort}","description":"","name":"客户端端口"},{"code":"${remoteUser}","description":"","name":"客户端用户名"},{"code":"${requestURI}","description":"比如/hello?name=lily","name":"请求URI"},{"code":"${requestPath}","description":"比如/hello","name":"请求路径(不包括参数)"},{"code":"${requestURL}","description":"比如https://example.com/hello?name=lily","name":"完整的请求URL"},{"code":"${requestLength}","description":"","name":"请求内容长度"},{"code":"${requestMethod}","description":"比如GET、POST","name":"请求方法"},{"code":"${requestFilename}","description":"","name":"请求文件路径"},{"code":"${scheme}","description":"","name":"请求协议,http或https"},{"code":"${proto}","description:":"类似于HTTP/1.0","name":"包含版本的HTTP请求协议"},{"code":"${timeISO8601}","description":"比如2018-07-16T23:52:24.839+08:00","name":"ISO 8601格式的时间"},{"code":"${timeLocal}","description":"比如17/Jul/2018:09:52:24 +0800","name":"本地时间"},{"code":"${msec}","description":"比如1531756823.054","name":"带有毫秒的时间"},{"code":"${timestamp}","description":"","name":"unix时间戳,单位为秒"},{"code":"${host}","description":"","name":"主机名"},{"code":"${serverName}","description":"","name":"接收请求的服务器名"},{"code":"${serverPort}","description":"","name":"接收请求的服务器端口"},{"code":"${referer}","description":"","name":"请求来源URL"},{"code":"${referer.host}","description":"","name":"请求来源URL域名"},{"code":"${userAgent}","description":"","name":"客户端信息"},{"code":"${contentType}","description":"","name":"请求头部的Content-Type"},{"code":"${cookies}","description":"","name":"所有cookie组合字符串"},{"code":"${cookie.NAME}","description":"","name":"单个cookie值"},{"code":"${args}","description":"","name":"所有参数组合字符串"},{"code":"${arg.NAME}","description":"","name":"单个参数值"},{"code":"${headers}","description":"","name":"所有Header信息组合字符串"},{"code":"${header.NAME}","description":"","name":"单个Header值"}] + +window.METRIC_HTTP_KEYS = [{"name":"客户端地址(IP)","code":"${remoteAddr}","description":"会依次根据X-Forwarded-For、X-Real-IP、RemoteAddr获取,适用于前端可能有别的反向代理的情形,存在被伪造的可能","icon":""},{"name":"直接客户端地址(IP)","code":"${rawRemoteAddr}","description":"返回直接连接服务的客户端原始IP地址","icon":""},{"name":"客户端用户名","code":"${remoteUser}","description":"通过基本认证填入的用户名","icon":""},{"name":"请求URI","code":"${requestURI}","description":"包含参数,比如/hello?name=lily","icon":""},{"name":"请求路径","code":"${requestPath}","description":"不包含参数,比如/hello","icon":""},{"name":"完整URL","code":"${requestURL}","description":"比如https://example.com/hello?name=lily","icon":""},{"name":"请求方法","code":"${requestMethod}","description":"比如GET、POST等","icon":""},{"name":"请求协议Scheme","code":"${scheme}","description":"http或https","icon":""},{"name":"文件扩展名","code":"${requestPathExtension}","description":"请求路径中的文件扩展名,包括点符号,比如.html、.png","icon":""},{"name":"主机名","code":"${host}","description":"通常是请求的域名","icon":""},{"name":"请求协议Proto","code":"${proto}","description":"包含版本的HTTP请求协议,类似于HTTP/1.0","icon":""},{"name":"HTTP协议","code":"${proto}","description":"包含版本的HTTP请求协议,类似于HTTP/1.0","icon":""},{"name":"URL参数值","code":"${arg.NAME}","description":"单个URL参数值","icon":""},{"name":"请求来源URL","code":"${referer}","description":"请求来源Referer URL","icon":""},{"name":"请求来源URL域名","code":"${referer.host}","description":"请求来源Referer URL域名","icon":""},{"name":"Header值","code":"${header.NAME}","description":"单个Header值,比如${header.User-Agent}","icon":""},{"name":"Cookie值","code":"${cookie.NAME}","description":"单个cookie值,比如${cookie.sid}","icon":""},{"name":"状态码","code":"${status}","description":"","icon":""},{"name":"响应的Content-Type值","code":"${response.contentType}","description":"","icon":""}] + +window.IP_ADDR_THRESHOLD_ITEMS = [{"code":"nodeAvgRequests","description":"当前节点在单位时间内接收到的平均请求数。","name":"节点平均请求数","unit":"个"},{"code":"nodeAvgTrafficOut","description":"当前节点在单位时间内发送的下行流量。","name":"节点平均下行流量","unit":"M"},{"code":"nodeAvgTrafficIn","description":"当前节点在单位时间内接收的上行流量。","name":"节点平均上行流量","unit":"M"},{"code":"nodeHealthCheck","description":"当前节点健康检查结果。","name":"节点健康检查结果","unit":""},{"code":"connectivity","description":"通过区域监控得到的当前IP地址的连通性数值,取值在0和100之间。","name":"IP连通性","unit":"%"},{"code":"groupAvgRequests","description":"当前节点所在分组在单位时间内接收到的平均请求数。","name":"分组平均请求数","unit":"个"},{"code":"groupAvgTrafficOut","description":"当前节点所在分组在单位时间内发送的下行流量。","name":"分组平均下行流量","unit":"M"},{"code":"groupAvgTrafficIn","description":"当前节点所在分组在单位时间内接收的上行流量。","name":"分组平均上行流量","unit":"M"},{"code":"clusterAvgRequests","description":"当前节点所在集群在单位时间内接收到的平均请求数。","name":"集群平均请求数","unit":"个"},{"code":"clusterAvgTrafficOut","description":"当前节点所在集群在单位时间内发送的下行流量。","name":"集群平均下行流量","unit":"M"},{"code":"clusterAvgTrafficIn","description":"当前节点所在集群在单位时间内接收的上行流量。","name":"集群平均上行流量","unit":"M"}] + +window.IP_ADDR_THRESHOLD_ACTIONS = [{"code":"up","description":"上线当前IP。","name":"上线"},{"code":"down","description":"下线当前IP。","name":"下线"},{"code":"notify","description":"发送已达到阈值通知。","name":"通知"},{"code":"switch","description":"在DNS中记录中将IP切换到指定的备用IP。","name":"切换"},{"code":"webHook","description":"调用外部的WebHook。","name":"WebHook"}] + diff --git a/web/views/@default/@layout.html b/web/views/@default/@layout.html index 5543a23a..8a5ec53e 100644 --- a/web/views/@default/@layout.html +++ b/web/views/@default/@layout.html @@ -15,7 +15,7 @@ {$TEA.VUE} {$echo "header"} - + diff --git a/web/views/@default/@layout_popup.html b/web/views/@default/@layout_popup.html index fea2295b..4aaa4117 100644 --- a/web/views/@default/@layout_popup.html +++ b/web/views/@default/@layout_popup.html @@ -12,7 +12,7 @@ {$echo "header"} - + diff --git a/web/views/@default/index/index.html b/web/views/@default/index/index.html index e73d8f66..9b0f97cd 100644 --- a/web/views/@default/index/index.html +++ b/web/views/@default/index/index.html @@ -14,7 +14,7 @@ - +
diff --git a/web/views/@default/recover/index.html b/web/views/@default/recover/index.html index 947f343d..466a5402 100644 --- a/web/views/@default/recover/index.html +++ b/web/views/@default/recover/index.html @@ -10,7 +10,7 @@ - + diff --git a/web/views/@default/setup/confirm/index.html b/web/views/@default/setup/confirm/index.html index 372a380a..71e6584c 100644 --- a/web/views/@default/setup/confirm/index.html +++ b/web/views/@default/setup/confirm/index.html @@ -10,7 +10,7 @@ - + diff --git a/web/views/@default/setup/index.html b/web/views/@default/setup/index.html index 4d5b8875..8d9a034d 100644 --- a/web/views/@default/setup/index.html +++ b/web/views/@default/setup/index.html @@ -10,7 +10,7 @@ - +