Initial commit (code only without large binaries)
This commit is contained in:
248
EdgeAdmin/web/public/js/components/server/domains-box.js
Normal file
248
EdgeAdmin/web/public/js/components/server/domains-box.js
Normal file
@@ -0,0 +1,248 @@
|
||||
// 域名列表
|
||||
Vue.component("domains-box", {
|
||||
props: ["v-domains", "name", "v-support-wildcard"],
|
||||
data: function () {
|
||||
let domains = this.vDomains
|
||||
if (domains == null) {
|
||||
domains = []
|
||||
}
|
||||
|
||||
let realName = "domainsJSON"
|
||||
if (this.name != null && typeof this.name == "string") {
|
||||
realName = this.name
|
||||
}
|
||||
|
||||
let supportWildcard = true
|
||||
if (typeof this.vSupportWildcard == "boolean") {
|
||||
supportWildcard = this.vSupportWildcard
|
||||
}
|
||||
|
||||
return {
|
||||
domains: domains,
|
||||
|
||||
mode: "single", // single | batch
|
||||
batchDomains: "",
|
||||
|
||||
isAdding: false,
|
||||
addingDomain: "",
|
||||
|
||||
isEditing: false,
|
||||
editingIndex: -1,
|
||||
|
||||
realName: realName,
|
||||
supportWildcard: supportWildcard
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
vSupportWildcard: function (v) {
|
||||
if (typeof v == "boolean") {
|
||||
this.supportWildcard = v
|
||||
}
|
||||
},
|
||||
mode: function (mode) {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
if (mode == "single") {
|
||||
if (that.$refs.addingDomain != null) {
|
||||
that.$refs.addingDomain.focus()
|
||||
}
|
||||
} else if (mode == "batch") {
|
||||
if (that.$refs.batchDomains != null) {
|
||||
that.$refs.batchDomains.focus()
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
this.isAdding = true
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.addingDomain.focus()
|
||||
}, 100)
|
||||
},
|
||||
confirm: function () {
|
||||
if (this.mode == "batch") {
|
||||
this.confirmBatch()
|
||||
return
|
||||
}
|
||||
|
||||
let that = this
|
||||
|
||||
// 删除其中的空格
|
||||
this.addingDomain = this.addingDomain.replace(/\s/g, "")
|
||||
|
||||
if (this.addingDomain.length == 0) {
|
||||
teaweb.warn("请输入要添加的域名", function () {
|
||||
that.$refs.addingDomain.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 基本校验
|
||||
if (this.supportWildcard) {
|
||||
if (this.addingDomain[0] == "~") {
|
||||
let expr = this.addingDomain.substring(1)
|
||||
try {
|
||||
new RegExp(expr)
|
||||
} catch (e) {
|
||||
teaweb.warn("正则表达式错误:" + e.message, function () {
|
||||
that.$refs.addingDomain.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (/[*~^]/.test(this.addingDomain)) {
|
||||
teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () {
|
||||
that.$refs.addingDomain.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isEditing && this.editingIndex >= 0) {
|
||||
this.domains[this.editingIndex] = this.addingDomain
|
||||
} else {
|
||||
// 分割逗号(,)、顿号(、)
|
||||
if (this.addingDomain.match("[,、,;]")) {
|
||||
let domainList = this.addingDomain.split(new RegExp("[,、,;]"))
|
||||
domainList.forEach(function (v) {
|
||||
if (v.length > 0) {
|
||||
that.domains.push(v)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.domains.push(this.addingDomain)
|
||||
}
|
||||
}
|
||||
this.cancel()
|
||||
this.change()
|
||||
},
|
||||
confirmBatch: function () {
|
||||
let domains = this.batchDomains.split("\n")
|
||||
let realDomains = []
|
||||
let that = this
|
||||
let hasProblems = false
|
||||
domains.forEach(function (domain) {
|
||||
if (hasProblems) {
|
||||
return
|
||||
}
|
||||
if (domain.length == 0) {
|
||||
return
|
||||
}
|
||||
if (that.supportWildcard) {
|
||||
if (domain == "~") {
|
||||
let expr = domain.substring(1)
|
||||
try {
|
||||
new RegExp(expr)
|
||||
} catch (e) {
|
||||
hasProblems = true
|
||||
teaweb.warn("正则表达式错误:" + e.message, function () {
|
||||
that.$refs.batchDomains.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (/[*~^]/.test(domain)) {
|
||||
hasProblems = true
|
||||
teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () {
|
||||
that.$refs.batchDomains.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
realDomains.push(domain)
|
||||
})
|
||||
if (hasProblems) {
|
||||
return
|
||||
}
|
||||
if (realDomains.length == 0) {
|
||||
teaweb.warn("请输入要添加的域名", function () {
|
||||
that.$refs.batchDomains.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
realDomains.forEach(function (domain) {
|
||||
that.domains.push(domain)
|
||||
})
|
||||
this.cancel()
|
||||
this.change()
|
||||
},
|
||||
edit: function (index) {
|
||||
this.addingDomain = this.domains[index]
|
||||
this.isEditing = true
|
||||
this.editingIndex = index
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.addingDomain.focus()
|
||||
}, 50)
|
||||
},
|
||||
remove: function (index) {
|
||||
this.domains.$remove(index)
|
||||
this.change()
|
||||
},
|
||||
cancel: function () {
|
||||
this.isAdding = false
|
||||
this.mode = "single"
|
||||
this.batchDomains = ""
|
||||
this.isEditing = false
|
||||
this.editingIndex = -1
|
||||
this.addingDomain = ""
|
||||
},
|
||||
change: function () {
|
||||
this.$emit("change", this.domains)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" :name="realName" :value="JSON.stringify(domains)"/>
|
||||
<div v-if="domains.length > 0">
|
||||
<span class="ui label small basic" v-for="(domain, index) in domains" :class="{blue: index == editingIndex}">
|
||||
<span v-if="domain.length > 0 && domain[0] == '~'" class="grey" style="font-style: normal">[正则]</span>
|
||||
<span v-if="domain.length > 0 && domain[0] == '.'" class="grey" style="font-style: normal">[后缀]</span>
|
||||
<span v-if="domain.length > 0 && domain[0] == '*'" class="grey" style="font-style: normal">[泛域名]</span>
|
||||
{{domain}}
|
||||
<span v-if="!isAdding && !isEditing">
|
||||
<a href="" title="修改" @click.prevent="edit(index)"><i class="icon pencil small"></i></a>
|
||||
<a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
|
||||
</span>
|
||||
<span v-if="isAdding || isEditing">
|
||||
<a class="disabled"><i class="icon pencil small"></i></a>
|
||||
<a class="disabled"><i class="icon remove small"></i></a>
|
||||
</span>
|
||||
</span>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div v-if="isAdding || isEditing">
|
||||
<div class="ui fields">
|
||||
<div class="ui field" v-if="isAdding">
|
||||
<select class="ui dropdown" v-model="mode">
|
||||
<option value="single">单个</option>
|
||||
<option value="batch">批量</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<div v-show="mode == 'single'">
|
||||
<input type="text" v-model="addingDomain" @keyup.enter="confirm()" @keypress.enter.prevent="1" @keydown.esc="cancel()" ref="addingDomain" :placeholder="supportWildcard ? 'example.com、*.example.com' : 'example.com、www.example.com'" size="30" maxlength="100"/>
|
||||
</div>
|
||||
<div v-show="mode == 'batch'">
|
||||
<textarea cols="30" v-model="batchDomains" placeholder="example1.com\nexample2.com\n每行一个域名" ref="batchDomains"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny" type="button" @click.prevent="confirm">确定</button>
|
||||
<a href="" title="取消" @click.prevent="cancel"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment" v-if="supportWildcard">支持普通域名(<code-label>example.com</code-label>)、泛域名(<code-label>*.example.com</code-label>)<span v-if="vSupportWildcard == undefined">、域名后缀(以点号开头,如<code-label>.example.com</code-label>)和正则表达式(以波浪号开头,如<code-label>~.*.example.com</code-label>)</span>;如果域名后有端口,请加上端口号。</p>
|
||||
<p class="comment" v-if="!supportWildcard">只支持普通域名(<code-label>example.com</code-label>、<code-label>www.example.com</code-label>)。</p>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div style="margin-top: 0.5em" v-if="!isAdding">
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
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: `<div>
|
||||
<select class="ui dropdown auto-width" name="eventLevel" v-model="level" @change="change">
|
||||
<option v-for="level in levels" :value="level.code">{{level.name}}</option>
|
||||
</select>
|
||||
<p class="comment">{{description}}</p>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
Vue.component("firewall-syn-flood-config-box", {
|
||||
props: ["v-syn-flood-config"],
|
||||
data: function () {
|
||||
let config = this.vSynFloodConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
isOn: false,
|
||||
minAttempts: 10,
|
||||
timeoutSeconds: 600,
|
||||
ignoreLocal: true
|
||||
}
|
||||
}
|
||||
return {
|
||||
config: config,
|
||||
isEditing: false,
|
||||
minAttempts: config.minAttempts,
|
||||
timeoutSeconds: config.timeoutSeconds
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
edit: function () {
|
||||
this.isEditing = !this.isEditing
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
minAttempts: function (v) {
|
||||
let count = parseInt(v)
|
||||
if (isNaN(count)) {
|
||||
count = 10
|
||||
}
|
||||
if (count < 5) {
|
||||
count = 5
|
||||
}
|
||||
this.config.minAttempts = count
|
||||
},
|
||||
timeoutSeconds: function (v) {
|
||||
let seconds = parseInt(v)
|
||||
if (isNaN(seconds)) {
|
||||
seconds = 10
|
||||
}
|
||||
if (seconds < 60) {
|
||||
seconds = 60
|
||||
}
|
||||
this.config.timeoutSeconds = seconds
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="synFloodJSON" :value="JSON.stringify(config)"/>
|
||||
<a href="" @click.prevent="edit">
|
||||
<span v-if="config.isOn">
|
||||
已启用 / <span>空连接次数:{{config.minAttempts}}次/分钟</span> / 封禁时长:{{config.timeoutSeconds}}秒 <span v-if="config.ignoreLocal">/ 忽略局域网访问</span>
|
||||
</span>
|
||||
<span v-else>未启用</span>
|
||||
<i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i>
|
||||
</a>
|
||||
|
||||
<table class="ui table selectable" v-show="isEditing">
|
||||
<tr>
|
||||
<td class="title">启用</td>
|
||||
<td>
|
||||
<checkbox v-model="config.isOn"></checkbox>
|
||||
<p class="comment">启用后,WAF将会尝试自动检测并阻止SYN Flood攻击。此功能需要节点已安装并启用nftables或Firewalld。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>空连接次数</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" v-model="minAttempts" style="width: 5em" maxlength="4"/>
|
||||
<span class="ui label">次/分钟</span>
|
||||
</div>
|
||||
<p class="comment">超过此数字的"空连接"将被视为SYN Flood攻击,为了防止误判,此数值默认不小于5。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>封禁时长</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" v-model="timeoutSeconds" style="width: 5em" maxlength="8"/>
|
||||
<span class="ui label">秒</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>忽略局域网访问</td>
|
||||
<td>
|
||||
<checkbox v-model="config.ignoreLocal"></checkbox>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
Vue.component("firewall-syn-flood-config-viewer", {
|
||||
props: ["v-syn-flood-config"],
|
||||
data: function () {
|
||||
let config = this.vSynFloodConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
isOn: false,
|
||||
minAttempts: 10,
|
||||
timeoutSeconds: 600,
|
||||
ignoreLocal: true
|
||||
}
|
||||
}
|
||||
return {
|
||||
config: config
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<span v-if="config.isOn">
|
||||
已启用 / <span>空连接次数:{{config.minAttempts}}次/分钟</span> / 封禁时长:{{config.timeoutSeconds}}秒 <span v-if="config.ignoreLocal">/ 忽略局域网访问</span>
|
||||
</span>
|
||||
<span v-else>未启用</span>
|
||||
</div>`
|
||||
})
|
||||
442
EdgeAdmin/web/public/js/components/server/http-access-log-box.js
Normal file
442
EdgeAdmin/web/public/js/components/server/http-access-log-box.js
Normal file
@@ -0,0 +1,442 @@
|
||||
Vue.component("http-access-log-box", {
|
||||
props: ["v-access-log", "v-keyword", "v-show-server-link"],
|
||||
data: function () {
|
||||
let accessLog = this.vAccessLog
|
||||
if (accessLog.header != null && accessLog.header.Upgrade != null && accessLog.header.Upgrade.values != null && accessLog.header.Upgrade.values.$contains("websocket")) {
|
||||
if (accessLog.scheme == "http") {
|
||||
accessLog.scheme = "ws"
|
||||
} else if (accessLog.scheme == "https") {
|
||||
accessLog.scheme = "wss"
|
||||
}
|
||||
}
|
||||
|
||||
// 对TAG去重
|
||||
if (accessLog.tags != null && accessLog.tags.length > 0) {
|
||||
let tagMap = {}
|
||||
accessLog.tags = accessLog.tags.$filter(function (k, tag) {
|
||||
let b = (typeof (tagMap[tag]) == "undefined")
|
||||
tagMap[tag] = true
|
||||
return b
|
||||
})
|
||||
}
|
||||
|
||||
// 域名
|
||||
accessLog.unicodeHost = ""
|
||||
if (accessLog.host != null && accessLog.host.startsWith("xn--")) {
|
||||
// port
|
||||
let portIndex = accessLog.host.indexOf(":")
|
||||
if (portIndex > 0) {
|
||||
accessLog.unicodeHost = punycode.ToUnicode(accessLog.host.substring(0, portIndex))
|
||||
} else {
|
||||
accessLog.unicodeHost = punycode.ToUnicode(accessLog.host)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
accessLog: accessLog
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatCost: function (seconds) {
|
||||
if (seconds == null) {
|
||||
return "0"
|
||||
}
|
||||
let s = (seconds * 1000).toString();
|
||||
let pieces = s.split(".");
|
||||
if (pieces.length < 2) {
|
||||
return s;
|
||||
}
|
||||
|
||||
return pieces[0] + "." + pieces[1].substring(0, 3);
|
||||
},
|
||||
showLog: function () {
|
||||
let that = this
|
||||
let requestId = this.accessLog.requestId
|
||||
this.$parent.$children.forEach(function (v) {
|
||||
if (v.deselect != null) {
|
||||
v.deselect()
|
||||
}
|
||||
})
|
||||
this.select()
|
||||
teaweb.popup("/servers/server/log/viewPopup?requestId=" + requestId, {
|
||||
width: "50em",
|
||||
height: "28em",
|
||||
onClose: function () {
|
||||
that.deselect()
|
||||
}
|
||||
})
|
||||
},
|
||||
select: function () {
|
||||
this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)"
|
||||
},
|
||||
deselect: function () {
|
||||
this.$refs.box.parentNode.style.cssText = ""
|
||||
},
|
||||
mismatch: function () {
|
||||
teaweb.warn("当前访问没有匹配到任何网站")
|
||||
}
|
||||
},
|
||||
template: `<div style="word-break: break-all" :style="{'color': (accessLog.status >= 400) ? '#dc143c' : ''}" ref="box">
|
||||
<div>
|
||||
<a v-if="accessLog.node != null && accessLog.node.nodeCluster != null" :href="'/clusters/cluster/node?nodeId=' + accessLog.node.id + '&clusterId=' + accessLog.node.nodeCluster.id" title="点击查看节点详情" target="_top"><span class="grey">[{{accessLog.node.name}}<span v-if="!accessLog.node.name.endsWith('节点')">节点</span>]</span></a>
|
||||
|
||||
|
||||
|
||||
<span v-if="accessLog.region != null && accessLog.region.length > 0" class="grey"><ip-box :v-ip="accessLog.remoteAddr">[{{accessLog.region}}]</ip-box></span>
|
||||
<ip-box><keyword :v-word="vKeyword">{{accessLog.remoteAddr}}</keyword></ip-box> [{{accessLog.timeLocal}}] <em>"<keyword :v-word="vKeyword">{{accessLog.requestMethod}}</keyword> {{accessLog.scheme}}://<keyword :v-word="vKeyword">{{accessLog.host}}</keyword><keyword :v-word="vKeyword">{{accessLog.requestURI}}</keyword> <a :href="accessLog.scheme + '://' + accessLog.host + accessLog.requestURI" target="_blank" title="新窗口打开" class="disabled"><i class="external icon tiny"></i> </a> {{accessLog.proto}}" </em> <keyword :v-word="vKeyword">{{accessLog.status}}</keyword>
|
||||
|
||||
<code-label v-if="accessLog.unicodeHost != null && accessLog.unicodeHost.length > 0">{{accessLog.unicodeHost}}</code-label>
|
||||
|
||||
<!-- attrs -->
|
||||
<code-label v-if="accessLog.attrs != null && (accessLog.attrs['cache.status'] == 'HIT' || accessLog.attrs['cache.status'] == 'STALE')">cache {{accessLog.attrs['cache.status'].toLowerCase()}}</code-label>
|
||||
<!-- waf -->
|
||||
<code-label v-if="accessLog.firewallActions != null && accessLog.firewallActions.length > 0">waf {{accessLog.firewallActions}}</code-label>
|
||||
|
||||
<!-- tags -->
|
||||
<span v-if="accessLog.tags != null && accessLog.tags.length > 0">- <code-label v-for="tag in accessLog.tags" :key="tag">{{tag}}</code-label>
|
||||
</span>
|
||||
<span v-if="accessLog.wafInfo != null">
|
||||
<a :href="(accessLog.wafInfo.policy.serverId == 0) ? '/servers/components/waf/group?firewallPolicyId=' + accessLog.firewallPolicyId + '&type=inbound&groupId=' + accessLog.firewallRuleGroupId+ '#set' + accessLog.firewallRuleSetId : '/servers/server/settings/waf/group?serverId=' + accessLog.serverId + '&firewallPolicyId=' + accessLog.firewallPolicyId + '&type=inbound&groupId=' + accessLog.firewallRuleGroupId + '#set' + accessLog.firewallRuleSetId" target="_blank">
|
||||
<code-label-plain>
|
||||
<span>
|
||||
WAF -
|
||||
<span v-if="accessLog.wafInfo.group != null">{{accessLog.wafInfo.group.name}} -</span>
|
||||
<span v-if="accessLog.wafInfo.set != null">{{accessLog.wafInfo.set.name}}</span>
|
||||
</span>
|
||||
</code-label-plain>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<span v-if="accessLog.requestTime != null"> - 耗时:{{formatCost(accessLog.requestTime)}} ms </span><span v-if="accessLog.humanTime != null && accessLog.humanTime.length > 0" class="grey small"> ({{accessLog.humanTime}})</span>
|
||||
<a :href="'/servers/server/log?serverId=' + accessLog.serverId" title="仅看此网站日志" v-if="vShowServerLink && accessLog.serverId > 0" class="ui label tiny blue basic" style="font-weight: normal; margin-left: 0.5em; padding: 2px 5px !important">网站</a>
|
||||
<a href="" @click.prevent="showLog" title="查看详情"><i class="icon expand"></i></a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// Javascript Punycode converter derived from example in RFC3492.
|
||||
// This implementation is created by some@domain.name and released into public domain
|
||||
// 代码来自:https://stackoverflow.com/questions/183485/converting-punycode-with-dash-character-to-unicode
|
||||
var punycode = new function Punycode() {
|
||||
// This object converts to and from puny-code used in IDN
|
||||
//
|
||||
// punycode.ToASCII ( domain )
|
||||
//
|
||||
// Returns a puny coded representation of "domain".
|
||||
// It only converts the part of the domain name that
|
||||
// has non ASCII characters. I.e. it dosent matter if
|
||||
// you call it with a domain that already is in ASCII.
|
||||
//
|
||||
// punycode.ToUnicode (domain)
|
||||
//
|
||||
// Converts a puny-coded domain name to unicode.
|
||||
// It only converts the puny-coded parts of the domain name.
|
||||
// I.e. it dosent matter if you call it on a string
|
||||
// that already has been converted to unicode.
|
||||
//
|
||||
//
|
||||
this.utf16 = {
|
||||
// The utf16-class is necessary to convert from javascripts internal character representation to unicode and back.
|
||||
decode: function (input) {
|
||||
var output = [], i = 0, len = input.length, value, extra;
|
||||
while (i < len) {
|
||||
value = input.charCodeAt(i++);
|
||||
if ((value & 0xF800) === 0xD800) {
|
||||
extra = input.charCodeAt(i++);
|
||||
if (((value & 0xFC00) !== 0xD800) || ((extra & 0xFC00) !== 0xDC00)) {
|
||||
throw new RangeError("UTF-16(decode): Illegal UTF-16 sequence");
|
||||
}
|
||||
value = ((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000;
|
||||
}
|
||||
output.push(value);
|
||||
}
|
||||
return output;
|
||||
},
|
||||
encode: function (input) {
|
||||
var output = [], i = 0, len = input.length, value;
|
||||
while (i < len) {
|
||||
value = input[i++];
|
||||
if ((value & 0xF800) === 0xD800) {
|
||||
throw new RangeError("UTF-16(encode): Illegal UTF-16 value");
|
||||
}
|
||||
if (value > 0xFFFF) {
|
||||
value -= 0x10000;
|
||||
output.push(String.fromCharCode(((value >>> 10) & 0x3FF) | 0xD800));
|
||||
value = 0xDC00 | (value & 0x3FF);
|
||||
}
|
||||
output.push(String.fromCharCode(value));
|
||||
}
|
||||
return output.join("");
|
||||
}
|
||||
}
|
||||
|
||||
//Default parameters
|
||||
var initial_n = 0x80;
|
||||
var initial_bias = 72;
|
||||
var delimiter = "\x2D";
|
||||
var base = 36;
|
||||
var damp = 700;
|
||||
var tmin = 1;
|
||||
var tmax = 26;
|
||||
var skew = 38;
|
||||
var maxint = 0x7FFFFFFF;
|
||||
|
||||
// decode_digit(cp) returns the numeric value of a basic code
|
||||
// point (for use in representing integers) in the range 0 to
|
||||
// base-1, or base if cp is does not represent a value.
|
||||
|
||||
function decode_digit(cp) {
|
||||
return cp - 48 < 10 ? cp - 22 : cp - 65 < 26 ? cp - 65 : cp - 97 < 26 ? cp - 97 : base;
|
||||
}
|
||||
|
||||
// encode_digit(d,flag) returns the basic code point whose value
|
||||
// (when used for representing integers) is d, which needs to be in
|
||||
// the range 0 to base-1. The lowercase form is used unless flag is
|
||||
// nonzero, in which case the uppercase form is used. The behavior
|
||||
// is undefined if flag is nonzero and digit d has no uppercase form.
|
||||
|
||||
function encode_digit(d, flag) {
|
||||
return d + 22 + 75 * (d < 26) - ((flag != 0) << 5);
|
||||
// 0..25 map to ASCII a..z or A..Z
|
||||
// 26..35 map to ASCII 0..9
|
||||
}
|
||||
|
||||
//** Bias adaptation function **
|
||||
function adapt(delta, numpoints, firsttime) {
|
||||
var k;
|
||||
delta = firsttime ? Math.floor(delta / damp) : (delta >> 1);
|
||||
delta += Math.floor(delta / numpoints);
|
||||
|
||||
for (k = 0; delta > (((base - tmin) * tmax) >> 1); k += base) {
|
||||
delta = Math.floor(delta / (base - tmin));
|
||||
}
|
||||
return Math.floor(k + (base - tmin + 1) * delta / (delta + skew));
|
||||
}
|
||||
|
||||
// encode_basic(bcp,flag) forces a basic code point to lowercase if flag is zero,
|
||||
// uppercase if flag is nonzero, and returns the resulting code point.
|
||||
// The code point is unchanged if it is caseless.
|
||||
// The behavior is undefined if bcp is not a basic code point.
|
||||
|
||||
function encode_basic(bcp, flag) {
|
||||
bcp -= (bcp - 97 < 26) << 5;
|
||||
return bcp + ((!flag && (bcp - 65 < 26)) << 5);
|
||||
}
|
||||
|
||||
// Main decode
|
||||
this.decode = function (input, preserveCase) {
|
||||
// Dont use utf16
|
||||
var output = [];
|
||||
var case_flags = [];
|
||||
var input_length = input.length;
|
||||
|
||||
var n, out, i, bias, basic, j, ic, oldi, w, k, digit, t, len;
|
||||
|
||||
// Initialize the state:
|
||||
|
||||
n = initial_n;
|
||||
i = 0;
|
||||
bias = initial_bias;
|
||||
|
||||
// Handle the basic code points: Let basic be the number of input code
|
||||
// points before the last delimiter, or 0 if there is none, then
|
||||
// copy the first basic code points to the output.
|
||||
|
||||
basic = input.lastIndexOf(delimiter);
|
||||
if (basic < 0) basic = 0;
|
||||
|
||||
for (j = 0; j < basic; ++j) {
|
||||
if (preserveCase) case_flags[output.length] = (input.charCodeAt(j) - 65 < 26);
|
||||
if (input.charCodeAt(j) >= 0x80) {
|
||||
throw new RangeError("Illegal input >= 0x80");
|
||||
}
|
||||
output.push(input.charCodeAt(j));
|
||||
}
|
||||
|
||||
// Main decoding loop: Start just after the last delimiter if any
|
||||
// basic code points were copied; start at the beginning otherwise.
|
||||
|
||||
for (ic = basic > 0 ? basic + 1 : 0; ic < input_length;) {
|
||||
|
||||
// ic is the index of the next character to be consumed,
|
||||
|
||||
// Decode a generalized variable-length integer into delta,
|
||||
// which gets added to i. The overflow checking is easier
|
||||
// if we increase i as we go, then subtract off its starting
|
||||
// value at the end to obtain delta.
|
||||
for (oldi = i, w = 1, k = base; ; k += base) {
|
||||
if (ic >= input_length) {
|
||||
throw RangeError("punycode_bad_input(1)");
|
||||
}
|
||||
digit = decode_digit(input.charCodeAt(ic++));
|
||||
|
||||
if (digit >= base) {
|
||||
throw RangeError("punycode_bad_input(2)");
|
||||
}
|
||||
if (digit > Math.floor((maxint - i) / w)) {
|
||||
throw RangeError("punycode_overflow(1)");
|
||||
}
|
||||
i += digit * w;
|
||||
t = k <= bias ? tmin : k >= bias + tmax ? tmax : k - bias;
|
||||
if (digit < t) {
|
||||
break;
|
||||
}
|
||||
if (w > Math.floor(maxint / (base - t))) {
|
||||
throw RangeError("punycode_overflow(2)");
|
||||
}
|
||||
w *= (base - t);
|
||||
}
|
||||
|
||||
out = output.length + 1;
|
||||
bias = adapt(i - oldi, out, oldi === 0);
|
||||
|
||||
// i was supposed to wrap around from out to 0,
|
||||
// incrementing n each time, so we'll fix that now:
|
||||
if (Math.floor(i / out) > maxint - n) {
|
||||
throw RangeError("punycode_overflow(3)");
|
||||
}
|
||||
n += Math.floor(i / out);
|
||||
i %= out;
|
||||
|
||||
// Insert n at position i of the output:
|
||||
// Case of last character determines uppercase flag:
|
||||
if (preserveCase) {
|
||||
case_flags.splice(i, 0, input.charCodeAt(ic - 1) - 65 < 26);
|
||||
}
|
||||
|
||||
output.splice(i, 0, n);
|
||||
i++;
|
||||
}
|
||||
if (preserveCase) {
|
||||
for (i = 0, len = output.length; i < len; i++) {
|
||||
if (case_flags[i]) {
|
||||
output[i] = (String.fromCharCode(output[i]).toUpperCase()).charCodeAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.utf16.encode(output);
|
||||
};
|
||||
|
||||
//** Main encode function **
|
||||
|
||||
this.encode = function (input, preserveCase) {
|
||||
//** Bias adaptation function **
|
||||
|
||||
var n, delta, h, b, bias, j, m, q, k, t, ijv, case_flags;
|
||||
|
||||
if (preserveCase) {
|
||||
// Preserve case, step1 of 2: Get a list of the unaltered string
|
||||
case_flags = this.utf16.decode(input);
|
||||
}
|
||||
// Converts the input in UTF-16 to Unicode
|
||||
input = this.utf16.decode(input.toLowerCase());
|
||||
|
||||
var input_length = input.length; // Cache the length
|
||||
|
||||
if (preserveCase) {
|
||||
// Preserve case, step2 of 2: Modify the list to true/false
|
||||
for (j = 0; j < input_length; j++) {
|
||||
case_flags[j] = input[j] != case_flags[j];
|
||||
}
|
||||
}
|
||||
|
||||
var output = [];
|
||||
|
||||
|
||||
// Initialize the state:
|
||||
n = initial_n;
|
||||
delta = 0;
|
||||
bias = initial_bias;
|
||||
|
||||
// Handle the basic code points:
|
||||
for (j = 0; j < input_length; ++j) {
|
||||
if (input[j] < 0x80) {
|
||||
output.push(
|
||||
String.fromCharCode(
|
||||
case_flags ? encode_basic(input[j], case_flags[j]) : input[j]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
h = b = output.length;
|
||||
|
||||
// h is the number of code points that have been handled, b is the
|
||||
// number of basic code points
|
||||
|
||||
if (b > 0) output.push(delimiter);
|
||||
|
||||
// Main encoding loop:
|
||||
//
|
||||
while (h < input_length) {
|
||||
// All non-basic code points < n have been
|
||||
// handled already. Find the next larger one:
|
||||
|
||||
for (m = maxint, j = 0; j < input_length; ++j) {
|
||||
ijv = input[j];
|
||||
if (ijv >= n && ijv < m) m = ijv;
|
||||
}
|
||||
|
||||
// Increase delta enough to advance the decoder's
|
||||
// <n,i> state to <m,0>, but guard against overflow:
|
||||
|
||||
if (m - n > Math.floor((maxint - delta) / (h + 1))) {
|
||||
throw RangeError("punycode_overflow (1)");
|
||||
}
|
||||
delta += (m - n) * (h + 1);
|
||||
n = m;
|
||||
|
||||
for (j = 0; j < input_length; ++j) {
|
||||
ijv = input[j];
|
||||
|
||||
if (ijv < n) {
|
||||
if (++delta > maxint) return Error("punycode_overflow(2)");
|
||||
}
|
||||
|
||||
if (ijv == n) {
|
||||
// Represent delta as a generalized variable-length integer:
|
||||
for (q = delta, k = base; ; k += base) {
|
||||
t = k <= bias ? tmin : k >= bias + tmax ? tmax : k - bias;
|
||||
if (q < t) break;
|
||||
output.push(String.fromCharCode(encode_digit(t + (q - t) % (base - t), 0)));
|
||||
q = Math.floor((q - t) / (base - t));
|
||||
}
|
||||
output.push(String.fromCharCode(encode_digit(q, preserveCase && case_flags[j] ? 1 : 0)));
|
||||
bias = adapt(delta, h + 1, h == b);
|
||||
delta = 0;
|
||||
++h;
|
||||
}
|
||||
}
|
||||
|
||||
++delta, ++n;
|
||||
}
|
||||
return output.join("");
|
||||
}
|
||||
|
||||
this.ToASCII = function (domain) {
|
||||
var domain_array = domain.split(".");
|
||||
var out = [];
|
||||
for (var i = 0; i < domain_array.length; ++i) {
|
||||
var s = domain_array[i];
|
||||
out.push(
|
||||
s.match(/[^A-Za-z0-9-]/) ?
|
||||
"xn--" + punycode.encode(s) :
|
||||
s
|
||||
);
|
||||
}
|
||||
return out.join(".");
|
||||
}
|
||||
this.ToUnicode = function (domain) {
|
||||
var domain_array = domain.split(".");
|
||||
var out = [];
|
||||
for (var i = 0; i < domain_array.length; ++i) {
|
||||
var s = domain_array[i];
|
||||
out.push(
|
||||
s.match(/^xn--/) ?
|
||||
punycode.decode(s.slice(4)) :
|
||||
s
|
||||
);
|
||||
}
|
||||
return out.join(".");
|
||||
}
|
||||
}();
|
||||
@@ -0,0 +1,144 @@
|
||||
Vue.component("http-access-log-config-box", {
|
||||
props: ["v-access-log-config", "v-fields", "v-default-field-codes", "v-is-location", "v-is-group"],
|
||||
data: function () {
|
||||
let that = this
|
||||
|
||||
// 初始化
|
||||
setTimeout(function () {
|
||||
that.changeFields()
|
||||
}, 100)
|
||||
|
||||
let accessLog = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
fields: [1, 2, 6, 7],
|
||||
status1: true,
|
||||
status2: true,
|
||||
status3: true,
|
||||
status4: true,
|
||||
status5: true,
|
||||
|
||||
firewallOnly: false,
|
||||
enableClientClosed: false
|
||||
}
|
||||
if (this.vAccessLogConfig != null) {
|
||||
accessLog = this.vAccessLogConfig
|
||||
}
|
||||
|
||||
this.vFields.forEach(function (v) {
|
||||
if (that.vAccessLogConfig == null) { // 初始化默认值
|
||||
v.isChecked = that.vDefaultFieldCodes.$contains(v.code)
|
||||
} else {
|
||||
v.isChecked = accessLog.fields.$contains(v.code)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
accessLog: accessLog,
|
||||
hasRequestBodyField: this.vFields.$contains(8),
|
||||
showAdvancedOptions: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeFields: function () {
|
||||
this.accessLog.fields = this.vFields.filter(function (v) {
|
||||
return v.isChecked
|
||||
}).map(function (v) {
|
||||
return v.code
|
||||
})
|
||||
this.hasRequestBodyField = this.accessLog.fields.$contains(8)
|
||||
},
|
||||
changeAdvanced: function (v) {
|
||||
this.showAdvancedOptions = v
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="accessLogJSON" :value="JSON.stringify(accessLog)"/>
|
||||
<table class="ui table definition selectable" :class="{'opacity-mask': this.accessLog.firewallOnly}">
|
||||
<prior-checkbox :v-config="accessLog" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="(!vIsLocation && !vIsGroup) || accessLog.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用访问日志</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="accessLog.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="((!vIsLocation && !vIsGroup) || accessLog.isPrior) && accessLog.isOn">
|
||||
<tr>
|
||||
<td colspan="2"><more-options-indicator @change="changeAdvanced"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="((!vIsLocation && !vIsGroup) || accessLog.isPrior) && accessLog.isOn && showAdvancedOptions">
|
||||
<tr>
|
||||
<td>基础信息</td>
|
||||
<td><p class="comment" style="padding-top: 0">默认记录客户端IP、请求URL等基础信息。</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>高级信息</td>
|
||||
<td>
|
||||
<div class="ui checkbox" v-for="(field, index) in vFields" style="width:10em;margin-bottom:0.8em">
|
||||
<input type="checkbox" v-model="field.isChecked" @change="changeFields" :id="'access-log-field-' + index"/>
|
||||
<label :for="'access-log-field-' + index">{{field.name}}</label>
|
||||
</div>
|
||||
<p class="comment">在基础信息之外要存储的信息。
|
||||
<span class="red" v-if="hasRequestBodyField">记录"请求Body"将会显著消耗更多的系统资源,建议仅在调试时启用,最大记录尺寸为2MiB。</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>要存储的访问日志状态码</td>
|
||||
<td>
|
||||
<div class="ui checkbox" style="width:3.5em">
|
||||
<input type="checkbox" v-model="accessLog.status1"/>
|
||||
<label>1xx</label>
|
||||
</div>
|
||||
<div class="ui checkbox" style="width:3.5em">
|
||||
<input type="checkbox" v-model="accessLog.status2"/>
|
||||
<label>2xx</label>
|
||||
</div>
|
||||
<div class="ui checkbox" style="width:3.5em">
|
||||
<input type="checkbox" v-model="accessLog.status3"/>
|
||||
<label>3xx</label>
|
||||
</div>
|
||||
<div class="ui checkbox" style="width:3.5em">
|
||||
<input type="checkbox" v-model="accessLog.status4"/>
|
||||
<label>4xx</label>
|
||||
</div>
|
||||
<div class="ui checkbox" style="width:3.5em">
|
||||
<input type="checkbox" v-model="accessLog.status5"/>
|
||||
<label>5xx</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>记录客户端中断日志</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="accessLog.enableClientClosed"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">以<code-label>499</code-label>的状态码记录客户端主动中断日志。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-show="((!vIsLocation && !vIsGroup) || accessLog.isPrior) && accessLog.isOn">
|
||||
<h4>WAF相关</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">只记录WAF相关日志</td>
|
||||
<td>
|
||||
<checkbox v-model="accessLog.firewallOnly"></checkbox>
|
||||
<p class="comment">选中后只记录WAF相关的日志。通过此选项可有效减少访问日志数量,降低网络带宽和存储压力。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,97 @@
|
||||
Vue.component("http-access-log-partitions-box", {
|
||||
props: ["v-partition", "v-day", "v-query"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
Tea.action("/servers/logs/partitionData")
|
||||
.params({
|
||||
day: this.vDay
|
||||
})
|
||||
.success(function (resp) {
|
||||
that.partitions = []
|
||||
resp.data.partitions.reverse().forEach(function (v) {
|
||||
that.partitions.push({
|
||||
code: v,
|
||||
isDisabled: false,
|
||||
hasLogs: false
|
||||
})
|
||||
})
|
||||
if (that.partitions.length > 0) {
|
||||
if (that.vPartition == null || that.vPartition < 0) {
|
||||
that.selectedPartition = that.partitions[0].code
|
||||
}
|
||||
|
||||
if (that.partitions.length > 1) {
|
||||
that.checkLogs()
|
||||
}
|
||||
}
|
||||
})
|
||||
.post()
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
partitions: [],
|
||||
selectedPartition: this.vPartition,
|
||||
checkingPartition: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
url: function (p) {
|
||||
let u = window.location.toString()
|
||||
u = u.replace(/\?partition=-?\d+/, "?")
|
||||
u = u.replace(/\?requestId=-?\d+/, "?")
|
||||
u = u.replace(/&partition=-?\d+/, "")
|
||||
u = u.replace(/&requestId=-?\d+/, "")
|
||||
if (u.indexOf("?") > 0) {
|
||||
u += "&partition=" + p
|
||||
} else {
|
||||
u += "?partition=" + p
|
||||
}
|
||||
return u
|
||||
},
|
||||
disable: function (partition) {
|
||||
this.partitions.forEach(function (p) {
|
||||
if (p.code == partition) {
|
||||
p.isDisabled = true
|
||||
}
|
||||
})
|
||||
},
|
||||
checkLogs: function () {
|
||||
let that = this
|
||||
let index = this.checkingPartition
|
||||
let params = {
|
||||
partition: index
|
||||
}
|
||||
let query = this.vQuery
|
||||
if (query == null || query.length == 0) {
|
||||
return
|
||||
}
|
||||
query.split("&").forEach(function (v) {
|
||||
let param = v.split("=")
|
||||
params[param[0]] = decodeURIComponent(param[1])
|
||||
})
|
||||
Tea.action("/servers/logs/hasLogs")
|
||||
.params(params)
|
||||
.post()
|
||||
.success(function (response) {
|
||||
if (response.data.hasLogs) {
|
||||
// 因为是倒序,所以这里需要使用总长度减去index
|
||||
that.partitions[that.partitions.length - 1 - index].hasLogs = true
|
||||
}
|
||||
|
||||
index++
|
||||
if (index >= that.partitions.length) {
|
||||
return
|
||||
}
|
||||
that.checkingPartition = index
|
||||
that.checkLogs()
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div v-if="partitions.length > 1">
|
||||
<div class="ui divider" style="margin-bottom: 0"></div>
|
||||
<div class="ui menu text small blue" style="margin-bottom: 0; margin-top: 0">
|
||||
<a v-for="(p, index) in partitions" :href="url(p.code)" class="item" :class="{active: selectedPartition == p.code, disabled: p.isDisabled}">分表{{p.code+1}} <span v-if="p.hasLogs"> <dot></dot></span> <span class="disabled" v-if="index != partitions.length - 1">|</span></a>
|
||||
</div>
|
||||
<div class="ui divider" style="margin-top: 0"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
// 访问日志搜索框
|
||||
Vue.component("http-access-log-search-box", {
|
||||
props: ["v-ip", "v-domain", "v-keyword", "v-cluster-id", "v-node-id"],
|
||||
data: function () {
|
||||
let ip = this.vIp
|
||||
if (ip == null) {
|
||||
ip = ""
|
||||
}
|
||||
|
||||
let domain = this.vDomain
|
||||
if (domain == null) {
|
||||
domain = ""
|
||||
}
|
||||
|
||||
let keyword = this.vKeyword
|
||||
if (keyword == null) {
|
||||
keyword = ""
|
||||
}
|
||||
|
||||
return {
|
||||
ip: ip,
|
||||
domain: domain,
|
||||
keyword: keyword,
|
||||
clusterId: this.vClusterId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cleanIP: function () {
|
||||
this.ip = ""
|
||||
this.submit()
|
||||
},
|
||||
cleanDomain: function () {
|
||||
this.domain = ""
|
||||
this.submit()
|
||||
},
|
||||
cleanKeyword: function () {
|
||||
this.keyword = ""
|
||||
this.submit()
|
||||
},
|
||||
submit: function () {
|
||||
let parent = this.$el.parentNode
|
||||
while (true) {
|
||||
if (parent == null) {
|
||||
break
|
||||
}
|
||||
if (parent.tagName == "FORM") {
|
||||
break
|
||||
}
|
||||
parent = parent.parentNode
|
||||
}
|
||||
if (parent != null) {
|
||||
setTimeout(function () {
|
||||
parent.submit()
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
changeCluster: function (clusterId) {
|
||||
this.clusterId = clusterId
|
||||
}
|
||||
},
|
||||
template: `<div style="z-index: 10">
|
||||
<div class="margin"></div>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<div class="ui input left right labeled small">
|
||||
<span class="ui label basic" style="font-weight: normal">IP</span>
|
||||
<input type="text" name="ip" placeholder="x.x.x.x" size="15" v-model="ip"/>
|
||||
<a class="ui label basic" :class="{disabled: ip.length == 0}" @click.prevent="cleanIP"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<div class="ui input left right labeled small" >
|
||||
<span class="ui label basic" style="font-weight: normal">域名</span>
|
||||
<input type="text" name="domain" placeholder="example.com" size="15" v-model="domain"/>
|
||||
<a class="ui label basic" :class="{disabled: domain.length == 0}" @click.prevent="cleanDomain"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<div class="ui input left right labeled small">
|
||||
<span class="ui label basic" style="font-weight: normal">关键词</span>
|
||||
<input type="text" name="keyword" v-model="keyword" placeholder="路径、UserAgent、请求ID等..." size="30"/>
|
||||
<a class="ui label basic" :class="{disabled: keyword.length == 0}" @click.prevent="cleanKeyword"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui field"><tip-icon content="一些特殊的关键词:<br/>单个状态码:status:200<br/>状态码范围:status:500-504<br/>查询IP:ip:192.168.1.100<br/>查询URL:https://goedge.cn/docs<br/>查询路径部分:requestPath:/hello/world<br/>查询协议版本:proto:HTTP/1.1<br/>协议:scheme:http<br/>请求方法:method:POST<br/>请求来源:referer:example.com"></tip-icon></div>
|
||||
</div>
|
||||
<div class="ui fields inline" style="margin-top: 0.5em">
|
||||
<div class="ui field">
|
||||
<node-cluster-combo-box :v-cluster-id="clusterId" @change="changeCluster"></node-cluster-combo-box>
|
||||
</div>
|
||||
<div class="ui field" v-if="clusterId > 0">
|
||||
<node-combo-box :v-cluster-id="clusterId" :v-node-id="vNodeId"></node-combo-box>
|
||||
</div>
|
||||
<slot></slot>
|
||||
<div class="ui field">
|
||||
<button class="ui button small" type="submit">搜索日志</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,101 @@
|
||||
// 基本认证用户配置
|
||||
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: `<div>
|
||||
<input type="hidden" name="httpAuthBasicAuthUsersJSON" :value="JSON.stringify(users)"/>
|
||||
<div v-if="users.length > 0">
|
||||
<div class="ui label small basic" v-for="(user, index) in users">
|
||||
{{user.username}} <a href="" title="修改" @click.prevent="update(index, user)"><i class="icon pencil tiny"></i></a>
|
||||
<a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div v-show="isAdding">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" placeholder="用户名" v-model="username" size="15" ref="username"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<input type="password" placeholder="密码" v-model="password" size="15" ref="password"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny" type="button" @click.prevent="confirm">确定</button>
|
||||
<a href="" title="取消" @click.prevent="cancel"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isAdding" style="margin-top: 1em">
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
// 认证设置
|
||||
Vue.component("http-auth-config-box", {
|
||||
props: ["v-auth-config", "v-is-location"],
|
||||
data: function () {
|
||||
let authConfig = this.vAuthConfig
|
||||
if (authConfig == null) {
|
||||
authConfig = {
|
||||
isPrior: false,
|
||||
isOn: false
|
||||
}
|
||||
}
|
||||
if (authConfig.policyRefs == null) {
|
||||
authConfig.policyRefs = []
|
||||
}
|
||||
return {
|
||||
authConfig: authConfig
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isOn: function () {
|
||||
return (!this.vIsLocation || this.authConfig.isPrior) && this.authConfig.isOn
|
||||
},
|
||||
add: function () {
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/access/createPopup", {
|
||||
callback: function (resp) {
|
||||
that.authConfig.policyRefs.push(resp.data.policyRef)
|
||||
that.change()
|
||||
},
|
||||
height: "28em"
|
||||
})
|
||||
},
|
||||
update: function (index, policyId) {
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/access/updatePopup?policyId=" + policyId, {
|
||||
callback: function (resp) {
|
||||
teaweb.success("保存成功", function () {
|
||||
teaweb.reload()
|
||||
})
|
||||
},
|
||||
height: "28em"
|
||||
})
|
||||
},
|
||||
remove: function (index) {
|
||||
this.authConfig.policyRefs.$remove(index)
|
||||
this.change()
|
||||
},
|
||||
methodName: function (methodType) {
|
||||
switch (methodType) {
|
||||
case "basicAuth":
|
||||
return "BasicAuth"
|
||||
case "subRequest":
|
||||
return "子请求"
|
||||
case "typeA":
|
||||
return "URL鉴权A"
|
||||
case "typeB":
|
||||
return "URL鉴权B"
|
||||
case "typeC":
|
||||
return "URL鉴权C"
|
||||
case "typeD":
|
||||
return "URL鉴权D"
|
||||
}
|
||||
return ""
|
||||
},
|
||||
change: function () {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
// 延时通知,是为了让表单有机会变更数据
|
||||
that.$emit("change", this.authConfig)
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="authJSON" :value="JSON.stringify(authConfig)"/>
|
||||
<table class="ui table selectable definition">
|
||||
<prior-checkbox :v-config="authConfig" v-if="vIsLocation"></prior-checkbox>
|
||||
<tbody v-show="!vIsLocation || authConfig.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用鉴权</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="authConfig.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
<!-- 鉴权方式 -->
|
||||
<div v-show="isOn()">
|
||||
<h4>鉴权方式</h4>
|
||||
<table class="ui table selectable celled" v-show="authConfig.policyRefs.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="three wide">名称</th>
|
||||
<th class="three wide">鉴权方法</th>
|
||||
<th>参数</th>
|
||||
<th class="two wide">状态</th>
|
||||
<th class="two op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-for="(ref, index) in authConfig.policyRefs" :key="ref.authPolicyId">
|
||||
<tr>
|
||||
<td>{{ref.authPolicy.name}}</td>
|
||||
<td>
|
||||
{{methodName(ref.authPolicy.type)}}
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="ref.authPolicy.type == 'basicAuth'">{{ref.authPolicy.params.users.length}}个用户</span>
|
||||
<span v-if="ref.authPolicy.type == 'subRequest'">
|
||||
<span v-if="ref.authPolicy.params.method.length > 0" class="grey">[{{ref.authPolicy.params.method}}]</span>
|
||||
{{ref.authPolicy.params.url}}
|
||||
</span>
|
||||
<span v-if="ref.authPolicy.type == 'typeA'">{{ref.authPolicy.params.signParamName}}/有效期{{ref.authPolicy.params.life}}秒</span>
|
||||
<span v-if="ref.authPolicy.type == 'typeB'">有效期{{ref.authPolicy.params.life}}秒</span>
|
||||
<span v-if="ref.authPolicy.type == 'typeC'">有效期{{ref.authPolicy.params.life}}秒</span>
|
||||
<span v-if="ref.authPolicy.type == 'typeD'">{{ref.authPolicy.params.signParamName}}/{{ref.authPolicy.params.timestampParamName}}/有效期{{ref.authPolicy.params.life}}秒</span>
|
||||
|
||||
<div v-if="(ref.authPolicy.params.exts != null && ref.authPolicy.params.exts.length > 0) || (ref.authPolicy.params.domains != null && ref.authPolicy.params.domains.length > 0)">
|
||||
<grey-label v-if="ref.authPolicy.params.exts != null" v-for="ext in ref.authPolicy.params.exts">扩展名:{{ext}}</grey-label>
|
||||
<grey-label v-if="ref.authPolicy.params.domains != null" v-for="domain in ref.authPolicy.params.domains">域名:{{domain}}</grey-label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<label-on :v-is-on="ref.authPolicy.isOn"></label-on>
|
||||
</td>
|
||||
<td>
|
||||
<a href="" @click.prevent="update(index, ref.authPolicyId)">修改</a>
|
||||
<a href="" @click.prevent="remove(index)">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="ui button small" type="button" @click.prevent="add">+添加鉴权方式</button>
|
||||
</div>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,233 @@
|
||||
Vue.component("http-cache-config-box", {
|
||||
props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy", "v-web-id"],
|
||||
data: function () {
|
||||
let cacheConfig = this.vCacheConfig
|
||||
if (cacheConfig == null) {
|
||||
cacheConfig = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
addStatusHeader: true,
|
||||
addAgeHeader: false,
|
||||
enableCacheControlMaxAge: false,
|
||||
cacheRefs: [],
|
||||
purgeIsOn: false,
|
||||
purgeKey: "",
|
||||
disablePolicyRefs: false
|
||||
}
|
||||
}
|
||||
if (cacheConfig.cacheRefs == null) {
|
||||
cacheConfig.cacheRefs = []
|
||||
}
|
||||
|
||||
let maxBytes = null
|
||||
if (this.vCachePolicy != null && this.vCachePolicy.maxBytes != null) {
|
||||
maxBytes = this.vCachePolicy.maxBytes
|
||||
}
|
||||
|
||||
// key
|
||||
if (cacheConfig.key == null) {
|
||||
// use Vue.set to activate vue events
|
||||
Vue.set(cacheConfig, "key", {
|
||||
isOn: false,
|
||||
scheme: "https",
|
||||
host: ""
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
cacheConfig: cacheConfig,
|
||||
moreOptionsVisible: false,
|
||||
enablePolicyRefs: !cacheConfig.disablePolicyRefs,
|
||||
maxBytes: maxBytes,
|
||||
|
||||
searchBoxVisible: false,
|
||||
searchKeyword: "",
|
||||
|
||||
keyOptionsVisible: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
enablePolicyRefs: function (v) {
|
||||
this.cacheConfig.disablePolicyRefs = !v
|
||||
},
|
||||
searchKeyword: function (v) {
|
||||
this.$refs.cacheRefsConfigBoxRef.search(v)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isOn: function () {
|
||||
return ((!this.vIsLocation && !this.vIsGroup) || this.cacheConfig.isPrior) && this.cacheConfig.isOn
|
||||
},
|
||||
isPlus: function () {
|
||||
return Tea.Vue.teaIsPlus
|
||||
},
|
||||
generatePurgeKey: function () {
|
||||
let r = Math.random().toString() + Math.random().toString()
|
||||
let s = r.replace(/0\./g, "")
|
||||
.replace(/\./g, "")
|
||||
let result = ""
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
result += String.fromCharCode(parseInt(s.substring(i, i + 1)) + ((Math.random() < 0.5) ? "a" : "A").charCodeAt(0))
|
||||
}
|
||||
this.cacheConfig.purgeKey = result
|
||||
},
|
||||
showMoreOptions: function () {
|
||||
this.moreOptionsVisible = !this.moreOptionsVisible
|
||||
},
|
||||
changeStale: function (stale) {
|
||||
this.cacheConfig.stale = stale
|
||||
},
|
||||
|
||||
showSearchBox: function () {
|
||||
this.searchBoxVisible = !this.searchBoxVisible
|
||||
if (this.searchBoxVisible) {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.searchBox.focus()
|
||||
})
|
||||
} else {
|
||||
this.searchKeyword = ""
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="cacheJSON" :value="JSON.stringify(cacheConfig)"/>
|
||||
|
||||
<table class="ui table definition selectable" v-show="!vIsGroup">
|
||||
<tr>
|
||||
<td class="title">全局缓存策略</td>
|
||||
<td>
|
||||
<div v-if="vCachePolicy != null">{{vCachePolicy.name}} <link-icon :href="'/servers/components/cache/policy?cachePolicyId=' + vCachePolicy.id"></link-icon>
|
||||
<p class="comment">使用当前网站所在集群的设置。</p>
|
||||
</div>
|
||||
<span v-else class="red">当前集群没有设置缓存策略,当前配置无法生效。</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="cacheConfig" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="(!vIsLocation && !vIsGroup) || cacheConfig.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用缓存</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="cacheConfig.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="isOn() && !vIsGroup">
|
||||
<tr>
|
||||
<td>缓存主域名</td>
|
||||
<td>
|
||||
<div v-show="!cacheConfig.key.isOn">默认 <a href="" @click.prevent="keyOptionsVisible = !keyOptionsVisible"><span class="small">[修改]</span></a></div>
|
||||
<div v-show="cacheConfig.key.isOn">使用主域名:{{cacheConfig.key.scheme}}://{{cacheConfig.key.host}} <a href="" @click.prevent="keyOptionsVisible = !keyOptionsVisible"><span class="small">[修改]</span></a></div>
|
||||
<div v-show="keyOptionsVisible" style="margin-top: 1em">
|
||||
<div class="ui divider"></div>
|
||||
<table class="ui table definition">
|
||||
<tr>
|
||||
<td class="title">启用主域名</td>
|
||||
<td><checkbox v-model="cacheConfig.key.isOn"></checkbox>
|
||||
<p class="comment">启用主域名后,所有缓存键值中的协议和域名部分都会修改为主域名,用来实现缓存不区分域名。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="cacheConfig.key.isOn">
|
||||
<td>主域名 *</td>
|
||||
<td>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" v-model="cacheConfig.key.scheme">
|
||||
<option value="https">https://</option>
|
||||
<option value="http">http://</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<input type="text" v-model="cacheConfig.key.host" placeholder="example.com" @keyup.enter="keyOptionsVisible = false" @keypress.enter.prevent="1"/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment">此域名<strong>必须</strong>是当前网站已绑定域名,在刷新缓存时也需要使用此域名。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button class="ui button tiny" type="button" @click.prevent="keyOptionsVisible = false">完成</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="isOn()">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a href="" @click.prevent="showMoreOptions"><span v-if="moreOptionsVisible">收起选项</span><span v-else>更多选项</span><i class="icon angle" :class="{up: moreOptionsVisible, down:!moreOptionsVisible}"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="isOn() && moreOptionsVisible">
|
||||
<tr>
|
||||
<td>使用默认缓存条件</td>
|
||||
<td>
|
||||
<checkbox v-model="enablePolicyRefs"></checkbox>
|
||||
<p class="comment">选中后使用系统全局缓存策略中已经定义的默认缓存条件。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>添加X-Cache报头</td>
|
||||
<td>
|
||||
<checkbox v-model="cacheConfig.addStatusHeader"></checkbox>
|
||||
<p class="comment">选中后自动在响应报头中增加<code-label>X-Cache: BYPASS|MISS|HIT|PURGE</code-label>;在浏览器端查看X-Cache值时请先禁用浏览器缓存,避免影响观察。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>添加Age Header</td>
|
||||
<td>
|
||||
<checkbox v-model="cacheConfig.addAgeHeader"></checkbox>
|
||||
<p class="comment">选中后自动在响应Header中增加<code-label>Age: [存活时间秒数]</code-label>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持源站控制有效时间</td>
|
||||
<td>
|
||||
<checkbox v-model="cacheConfig.enableCacheControlMaxAge"></checkbox>
|
||||
<p class="comment">选中后表示支持源站在Header中设置的<code-label>Cache-Control: max-age=[有效时间秒数]</code-label>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="color-border">允许PURGE</td>
|
||||
<td>
|
||||
<checkbox v-model="cacheConfig.purgeIsOn"></checkbox>
|
||||
<p class="comment">允许使用PURGE方法清除某个URL缓存。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="cacheConfig.purgeIsOn">
|
||||
<td class="color-border">PURGE Key *</td>
|
||||
<td>
|
||||
<input type="text" maxlength="200" v-model="cacheConfig.purgeKey"/>
|
||||
<p class="comment"><a href="" @click.prevent="generatePurgeKey">[随机生成]</a>。需要在PURGE方法调用时加入<code-label>X-Edge-Purge-Key: {{cacheConfig.purgeKey}}</code-label> Header。只能包含字符、数字、下划线。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-if="isOn() && moreOptionsVisible && isPlus()">
|
||||
<h4>过时缓存策略</h4>
|
||||
<http-cache-stale-config :v-cache-stale-config="cacheConfig.stale" @change="changeStale"></http-cache-stale-config>
|
||||
</div>
|
||||
|
||||
<div v-show="isOn()">
|
||||
<submit-btn></submit-btn>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
|
||||
<div v-show="isOn()" style="margin-top: 1em">
|
||||
<h4 style="position: relative">缓存条件 <a href="" style="font-size: 0.8em" @click.prevent="$refs.cacheRefsConfigBoxRef.addRef(false)">[添加]</a> <a href="" style="font-size: 0.8em" @click.prevent="showSearchBox" v-show="!searchBoxVisible">[搜索]</a>
|
||||
<div class="ui input small right labeled" style="position: absolute; top: -0.4em; margin-left: 0.5em; zoom: 0.9" v-show="searchBoxVisible">
|
||||
<input type="text" placeholder="搜索..." ref="searchBox" @keypress.enter.prevent="1" @keydown.esc="showSearchBox" v-model="searchKeyword" size="20"/>
|
||||
<a href="" class="ui label blue" @click.prevent="showSearchBox"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</h4>
|
||||
<http-cache-refs-config-box ref="cacheRefsConfigBoxRef" :v-cache-config="cacheConfig" :v-cache-refs="cacheConfig.cacheRefs" :v-web-id="vWebId" :v-max-bytes="maxBytes"></http-cache-refs-config-box>
|
||||
</div>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
Vue.component("http-cache-policy-selector", {
|
||||
props: ["v-cache-policy"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
Tea.action("/servers/components/cache/count")
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
that.count = resp.data.count
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
let cachePolicy = this.vCachePolicy
|
||||
return {
|
||||
count: 0,
|
||||
cachePolicy: cachePolicy
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
remove: function () {
|
||||
this.cachePolicy = null
|
||||
},
|
||||
select: function () {
|
||||
let that = this
|
||||
teaweb.popup("/servers/components/cache/selectPopup", {
|
||||
width: "42em",
|
||||
height: "26em",
|
||||
callback: function (resp) {
|
||||
that.cachePolicy = resp.data.cachePolicy
|
||||
}
|
||||
})
|
||||
},
|
||||
create: function () {
|
||||
let that = this
|
||||
teaweb.popup("/servers/components/cache/createPopup", {
|
||||
height: "26em",
|
||||
callback: function (resp) {
|
||||
that.cachePolicy = resp.data.cachePolicy
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div v-if="cachePolicy != null" class="ui label basic">
|
||||
<input type="hidden" name="cachePolicyId" :value="cachePolicy.id"/>
|
||||
{{cachePolicy.name}} <a :href="'/servers/components/cache/update?cachePolicyId=' + cachePolicy.id" target="_blank" title="修改"><i class="icon pencil small"></i></a> <a href="" @click.prevent="remove()" title="删除"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
<div v-if="cachePolicy == null">
|
||||
<span v-if="count > 0"><a href="" @click.prevent="select">[选择已有策略]</a> </span><a href="" @click.prevent="create">[创建新策略]</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
360
EdgeAdmin/web/public/js/components/server/http-cache-ref-box.js
Normal file
360
EdgeAdmin/web/public/js/components/server/http-cache-ref-box.js
Normal file
@@ -0,0 +1,360 @@
|
||||
// 单个缓存条件设置
|
||||
Vue.component("http-cache-ref-box", {
|
||||
props: ["v-cache-ref", "v-is-reverse"],
|
||||
mounted: function () {
|
||||
this.$refs.variablesDescriber.update(this.ref.key)
|
||||
if (this.ref.simpleCond != null) {
|
||||
this.condType = this.ref.simpleCond.type
|
||||
this.changeCondType(this.ref.simpleCond.type, true)
|
||||
this.condCategory = "simple"
|
||||
} else if (this.ref.conds != null && this.ref.conds.groups != null) {
|
||||
this.condCategory = "complex"
|
||||
}
|
||||
this.changeCondCategory(this.condCategory)
|
||||
},
|
||||
data: function () {
|
||||
let ref = this.vCacheRef
|
||||
if (ref == null) {
|
||||
ref = {
|
||||
isOn: true,
|
||||
cachePolicyId: 0,
|
||||
key: "${scheme}://${host}${requestPath}${isArgs}${args}",
|
||||
life: {count: 1, unit: "day"},
|
||||
status: [200],
|
||||
maxSize: {count: 128, unit: "mb"},
|
||||
minSize: {count: 0, unit: "kb"},
|
||||
skipCacheControlValues: ["private", "no-cache", "no-store"],
|
||||
skipSetCookie: true,
|
||||
enableRequestCachePragma: false,
|
||||
conds: null, // 复杂条件
|
||||
simpleCond: null, // 简单条件
|
||||
allowChunkedEncoding: true,
|
||||
allowPartialContent: true,
|
||||
forcePartialContent: false,
|
||||
enableIfNoneMatch: false,
|
||||
enableIfModifiedSince: false,
|
||||
enableReadingOriginAsync: false,
|
||||
isReverse: this.vIsReverse,
|
||||
methods: [],
|
||||
expiresTime: {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
overwrite: true,
|
||||
autoCalculate: true,
|
||||
duration: {count: -1, "unit": "hour"}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ref.key == null) {
|
||||
ref.key = ""
|
||||
}
|
||||
if (ref.methods == null) {
|
||||
ref.methods = []
|
||||
}
|
||||
|
||||
if (ref.life == null) {
|
||||
ref.life = {count: 2, unit: "hour"}
|
||||
}
|
||||
if (ref.maxSize == null) {
|
||||
ref.maxSize = {count: 32, unit: "mb"}
|
||||
}
|
||||
if (ref.minSize == null) {
|
||||
ref.minSize = {count: 0, unit: "kb"}
|
||||
}
|
||||
|
||||
let condType = "url-extension"
|
||||
let condComponent = window.REQUEST_COND_COMPONENTS.$find(function (k, v) {
|
||||
return v.type == "url-extension"
|
||||
})
|
||||
|
||||
return {
|
||||
ref: ref,
|
||||
|
||||
keyIgnoreArgs: typeof ref.key == "string" && ref.key.indexOf("${args}") < 0,
|
||||
|
||||
moreOptionsVisible: false,
|
||||
|
||||
condCategory: "simple", // 条件分类:simple|complex
|
||||
condType: condType,
|
||||
condComponent: condComponent,
|
||||
condIsCaseInsensitive: (ref.simpleCond != null) ? ref.simpleCond.isCaseInsensitive : true,
|
||||
|
||||
components: window.REQUEST_COND_COMPONENTS
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
keyIgnoreArgs: function (b) {
|
||||
if (typeof this.ref.key != "string") {
|
||||
return
|
||||
}
|
||||
if (b) {
|
||||
this.ref.key = this.ref.key.replace("${isArgs}${args}", "")
|
||||
return;
|
||||
}
|
||||
if (this.ref.key.indexOf("${isArgs}") < 0) {
|
||||
this.ref.key = this.ref.key + "${isArgs}"
|
||||
}
|
||||
if (this.ref.key.indexOf("${args}") < 0) {
|
||||
this.ref.key = this.ref.key + "${args}"
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeOptionsVisible: function (v) {
|
||||
this.moreOptionsVisible = v
|
||||
},
|
||||
changeLife: function (v) {
|
||||
this.ref.life = v
|
||||
},
|
||||
changeMaxSize: function (v) {
|
||||
this.ref.maxSize = v
|
||||
},
|
||||
changeMinSize: function (v) {
|
||||
this.ref.minSize = v
|
||||
},
|
||||
changeConds: function (v) {
|
||||
this.ref.conds = v
|
||||
this.ref.simpleCond = null
|
||||
},
|
||||
changeStatusList: function (list) {
|
||||
let result = []
|
||||
list.forEach(function (status) {
|
||||
let statusNumber = parseInt(status)
|
||||
if (isNaN(statusNumber) || statusNumber < 100 || statusNumber > 999) {
|
||||
return
|
||||
}
|
||||
result.push(statusNumber)
|
||||
})
|
||||
this.ref.status = result
|
||||
},
|
||||
changeMethods: function (methods) {
|
||||
this.ref.methods = methods.map(function (v) {
|
||||
return v.toUpperCase()
|
||||
})
|
||||
},
|
||||
changeKey: function (key) {
|
||||
this.$refs.variablesDescriber.update(key)
|
||||
},
|
||||
changeExpiresTime: function (expiresTime) {
|
||||
this.ref.expiresTime = expiresTime
|
||||
},
|
||||
|
||||
// 切换条件类型
|
||||
changeCondCategory: function (condCategory) {
|
||||
this.condCategory = condCategory
|
||||
|
||||
// resize window
|
||||
let dialog = window.parent.document.querySelector("*[role='dialog']")
|
||||
if (dialog == null) {
|
||||
return
|
||||
}
|
||||
switch (condCategory) {
|
||||
case "simple":
|
||||
dialog.style.width = "45em"
|
||||
break
|
||||
case "complex":
|
||||
let width = window.parent.innerWidth
|
||||
if (width > 1024) {
|
||||
width = 1024
|
||||
}
|
||||
|
||||
dialog.style.width = width + "px"
|
||||
if (this.ref.conds != null) {
|
||||
this.ref.conds.isOn = true
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
changeCondType: function (condType, isInit) {
|
||||
if (!isInit && this.ref.simpleCond != null) {
|
||||
this.ref.simpleCond.value = null
|
||||
}
|
||||
let def = this.components.$find(function (k, component) {
|
||||
return component.type == condType
|
||||
})
|
||||
if (def != null) {
|
||||
this.condComponent = def
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<tbody>
|
||||
<tr v-if="condCategory == 'simple'">
|
||||
<td class="title">缓存对象 *</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" name="condType" v-model="condType" @change="changeCondType(condType, false)">
|
||||
<option value="url-extension">文件扩展名</option>
|
||||
<option value="url-eq-index">首页</option>
|
||||
<option value="url-all">全站</option>
|
||||
<option value="url-prefix">URL目录前缀</option>
|
||||
<option value="url-eq">URL完整路径</option>
|
||||
<option value="url-wildcard-match">URL通配符</option>
|
||||
<option value="url-regexp">URL正则匹配</option>
|
||||
<option value="params">参数匹配</option>
|
||||
</select>
|
||||
<p class="comment"><a href="" @click.prevent="changeCondCategory('complex')">切换到复杂条件 »</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="condCategory == 'simple'">
|
||||
<td>{{condComponent.paramsTitle}} *</td>
|
||||
<td>
|
||||
<component :is="condComponent.component" :v-cond="ref.simpleCond" v-if="condComponent.type != 'params'"></component>
|
||||
<table class="ui table" v-if="condComponent.type == 'params'">
|
||||
<component :is="condComponent.component" :v-cond="ref.simpleCond"></component>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="condCategory == 'simple' && condComponent.caseInsensitive">
|
||||
<td>不区分大小写</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="condIsCaseInsensitive" value="1" v-model="condIsCaseInsensitive"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中后表示对比时忽略参数值的大小写。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="condCategory == 'complex'">
|
||||
<td class="title">匹配条件分组 *</td>
|
||||
<td>
|
||||
<http-request-conds-box :v-conds="ref.conds" @change="changeConds"></http-request-conds-box>
|
||||
<p class="comment"><a href="" @click.prevent="changeCondCategory('simple')">« 切换到简单条件</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="!vIsReverse">
|
||||
<td>缓存有效期 *</td>
|
||||
<td>
|
||||
<time-duration-box :v-value="ref.life" @change="changeLife" :v-min-unit="'minute'" maxlength="4"></time-duration-box>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="!vIsReverse">
|
||||
<td>忽略URI参数</td>
|
||||
<td>
|
||||
<checkbox v-model="keyIgnoreArgs"></checkbox>
|
||||
<p class="comment">选中后,表示缓存Key中不包含URI参数(即问号(?))后面的内容。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="!vIsReverse">
|
||||
<td colspan="2"><more-options-indicator @change="changeOptionsVisible"></more-options-indicator></td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>缓存Key *</td>
|
||||
<td>
|
||||
<input type="text" v-model="ref.key" @input="changeKey(ref.key)"/>
|
||||
<p class="comment">用来区分不同缓存内容的唯一Key。<request-variables-describer ref="variablesDescriber"></request-variables-describer>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>请求方法限制</td>
|
||||
<td>
|
||||
<values-box size="5" maxlength="10" :values="ref.methods" @change="changeMethods"></values-box>
|
||||
<p class="comment">允许请求的缓存方法,默认支持所有的请求方法。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>客户端过期时间<em>(Expires)</em></td>
|
||||
<td>
|
||||
<http-expires-time-config-box :v-expires-time="ref.expiresTime" @change="changeExpiresTime"></http-expires-time-config-box>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>可缓存的最大内容尺寸</td>
|
||||
<td>
|
||||
<size-capacity-box :v-value="ref.maxSize" @change="changeMaxSize"></size-capacity-box>
|
||||
<p class="comment">内容尺寸如果高于此值则不缓存。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>可缓存的最小内容尺寸</td>
|
||||
<td>
|
||||
<size-capacity-box :v-value="ref.minSize" @change="changeMinSize"></size-capacity-box>
|
||||
<p class="comment">内容尺寸如果低于此值则不缓存。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>支持缓存分片内容</td>
|
||||
<td>
|
||||
<checkbox name="allowPartialContent" value="1" v-model="ref.allowPartialContent"></checkbox>
|
||||
<p class="comment">选中后,支持缓存源站返回的某个分片的内容,该内容通过<code-label>206 Partial Content</code-label>状态码返回。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse && ref.allowPartialContent && !ref.alwaysForwardRangeReques">
|
||||
<td>强制返回分片内容</td>
|
||||
<td>
|
||||
<checkbox name="forcePartialContent" value="1" v-model="ref.forcePartialContent"></checkbox>
|
||||
<p class="comment">选中后,表示无论客户端是否发送<code-label>Range</code-label>报头,都会优先尝试返回已缓存的分片内容;如果你的应用有不支持分片内容的客户端(比如有些下载软件不支持<code-label>206 Partial Content</code-label>),请务必关闭此功能。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>强制Range回源</td>
|
||||
<td>
|
||||
<checkbox v-model="ref.alwaysForwardRangeRequest"></checkbox>
|
||||
<p class="comment">选中后,表示把所有包含Range报头的请求都转发到源站,而不是尝试从缓存中读取。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>状态码列表</td>
|
||||
<td>
|
||||
<values-box name="statusList" size="3" maxlength="3" :values="ref.status" @change="changeStatusList"></values-box>
|
||||
<p class="comment">允许缓存的HTTP状态码列表。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>跳过的Cache-Control值</td>
|
||||
<td>
|
||||
<values-box name="skipResponseCacheControlValues" size="10" maxlength="100" :values="ref.skipCacheControlValues"></values-box>
|
||||
<p class="comment">当响应的Cache-Control为这些值时不缓存响应内容,而且不区分大小写。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>跳过Set-Cookie</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="ref.skipSetCookie"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中后,当响应的报头中有Set-Cookie时不缓存响应内容,防止动态内容被缓存。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>支持请求no-cache刷新</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="enableRequestCachePragma" value="1" v-model="ref.enableRequestCachePragma"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中后,当请求的报头中含有Pragma: no-cache或Cache-Control: no-cache时,会跳过缓存直接读取源内容,一般仅用于调试。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>允许If-None-Match回源</td>
|
||||
<td>
|
||||
<checkbox v-model="ref.enableIfNoneMatch"></checkbox>
|
||||
<p class="comment">特殊情况下才需要开启,可能会降低缓存命中率。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>允许If-Modified-Since回源</td>
|
||||
<td>
|
||||
<checkbox v-model="ref.enableIfModifiedSince"></checkbox>
|
||||
<p class="comment">特殊情况下才需要开启,可能会降低缓存命中率。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>允许异步读取源站</td>
|
||||
<td>
|
||||
<checkbox v-model="ref.enableReadingOriginAsync"></checkbox>
|
||||
<p class="comment">试验功能。允许客户端中断连接后,仍然继续尝试从源站读取内容并缓存。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="moreOptionsVisible && !vIsReverse">
|
||||
<td>支持分段内容</td>
|
||||
<td>
|
||||
<checkbox name="allowChunkedEncoding" value="1" v-model="ref.allowChunkedEncoding"></checkbox>
|
||||
<p class="comment">选中后,Gzip等压缩后的Chunked内容可以直接缓存,无需检查内容长度。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="false">
|
||||
<td colspan="2"><input type="hidden" name="cacheRefJSON" :value="JSON.stringify(ref)"/></td>
|
||||
</tr>
|
||||
</tbody>`
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
// 缓存条件列表
|
||||
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: `<div>
|
||||
<input type="hidden" name="refsJSON" :value="JSON.stringify(refs)"/>
|
||||
|
||||
<p class="comment" v-if="refs.length == 0">暂时还没有缓存条件。</p>
|
||||
<div v-show="refs.length > 0">
|
||||
<table class="ui table selectable celled">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>缓存条件</th>
|
||||
<th class="width6">缓存时间</th>
|
||||
</tr>
|
||||
<tr v-for="(cacheRef, index) in refs">
|
||||
<td :class="{'color-border': cacheRef.conds != null && cacheRef.conds.connector == 'and', disabled: !cacheRef.isOn}" :style="{'border-left':cacheRef.isReverse ? '1px #db2828 solid' : ''}">
|
||||
<http-request-conds-view :v-conds="cacheRef.conds" :class="{disabled: !cacheRef.isOn}" v-if="cacheRef.conds != null && cacheRef.conds.groups != null"></http-request-conds-view>
|
||||
<http-request-cond-view :v-cond="cacheRef.simpleCond" v-if="cacheRef.simpleCond != null"></http-request-cond-view>
|
||||
|
||||
<!-- 特殊参数 -->
|
||||
<grey-label v-if="cacheRef.key != null && cacheRef.key.indexOf('\${args}') < 0">忽略URI参数</grey-label>
|
||||
<grey-label v-if="cacheRef.minSize != null && cacheRef.minSize.count > 0">
|
||||
{{cacheRef.minSize.count}}{{cacheRef.minSize.unit}}
|
||||
<span v-if="cacheRef.maxSize != null && cacheRef.maxSize.count > 0">- {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}}</span>
|
||||
</grey-label>
|
||||
<grey-label v-else-if="cacheRef.maxSize != null && cacheRef.maxSize.count > 0">0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}}</grey-label>
|
||||
<grey-label v-if="cacheRef.methods != null && cacheRef.methods.length > 0">{{cacheRef.methods.join(", ")}}</grey-label>
|
||||
<grey-label v-if="cacheRef.expiresTime != null && cacheRef.expiresTime.isPrior && cacheRef.expiresTime.isOn">Expires</grey-label>
|
||||
<grey-label v-if="cacheRef.status != null && cacheRef.status.length > 0 && (cacheRef.status.length > 1 || cacheRef.status[0] != 200)">状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}}</grey-label>
|
||||
<grey-label v-if="cacheRef.allowPartialContent">分片缓存</grey-label>
|
||||
<grey-label v-if="cacheRef.alwaysForwardRangeRequest">Range回源</grey-label>
|
||||
<grey-label v-if="cacheRef.enableIfNoneMatch">If-None-Match</grey-label>
|
||||
<grey-label v-if="cacheRef.enableIfModifiedSince">If-Modified-Since</grey-label>
|
||||
<grey-label v-if="cacheRef.enableReadingOriginAsync">支持异步</grey-label>
|
||||
</td>
|
||||
<td :class="{disabled: !cacheRef.isOn}">
|
||||
<span v-if="!cacheRef.isReverse">{{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}}</span>
|
||||
<span v-else class="red">不缓存</span>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,277 @@
|
||||
Vue.component("http-cache-refs-config-box", {
|
||||
props: ["v-cache-refs", "v-cache-config", "v-cache-policy-id", "v-web-id", "v-max-bytes"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
sortTable(function (ids) {
|
||||
let newRefs = []
|
||||
ids.forEach(function (id) {
|
||||
that.refs.forEach(function (ref) {
|
||||
if (ref.id == id) {
|
||||
newRefs.push(ref)
|
||||
}
|
||||
})
|
||||
})
|
||||
that.updateRefs(newRefs)
|
||||
that.change()
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
let refs = this.vCacheRefs
|
||||
if (refs == null) {
|
||||
refs = []
|
||||
}
|
||||
|
||||
let maxBytes = this.vMaxBytes
|
||||
|
||||
let id = 0
|
||||
refs.forEach(function (ref) {
|
||||
// preset variables
|
||||
id++
|
||||
ref.id = id
|
||||
ref.visible = true
|
||||
|
||||
// check max size
|
||||
if (ref.maxSize != null && maxBytes != null && maxBytes.count > 0 && teaweb.compareSizeCapacity(ref.maxSize, maxBytes) > 0) {
|
||||
ref.overMaxSize = maxBytes
|
||||
}
|
||||
})
|
||||
return {
|
||||
refs: refs,
|
||||
id: id // 用来对条件进行排序
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addRef: function (isReverse) {
|
||||
window.UPDATING_CACHE_REF = null
|
||||
|
||||
let height = window.innerHeight
|
||||
if (height > 500) {
|
||||
height = 500
|
||||
}
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/cache/createPopup?isReverse=" + (isReverse ? 1 : 0), {
|
||||
height: height + "px",
|
||||
callback: function (resp) {
|
||||
let newRef = resp.data.cacheRef
|
||||
if (newRef.conds == null) {
|
||||
return
|
||||
}
|
||||
|
||||
that.id++
|
||||
newRef.id = that.id
|
||||
|
||||
if (newRef.isReverse) {
|
||||
let newRefs = []
|
||||
let isAdded = false
|
||||
that.refs.forEach(function (v) {
|
||||
if (!v.isReverse && !isAdded) {
|
||||
newRefs.push(newRef)
|
||||
isAdded = true
|
||||
}
|
||||
newRefs.push(v)
|
||||
})
|
||||
if (!isAdded) {
|
||||
newRefs.push(newRef)
|
||||
}
|
||||
|
||||
that.updateRefs(newRefs)
|
||||
} else {
|
||||
that.refs.push(newRef)
|
||||
}
|
||||
|
||||
// move to bottom
|
||||
var afterChangeCallback = function () {
|
||||
setTimeout(function () {
|
||||
let rightBox = document.querySelector(".right-box")
|
||||
if (rightBox != null) {
|
||||
rightBox.scrollTo(0, isReverse ? 0 : 100000)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
that.change(afterChangeCallback)
|
||||
}
|
||||
})
|
||||
},
|
||||
updateRef: function (index, cacheRef) {
|
||||
window.UPDATING_CACHE_REF = teaweb.clone(cacheRef)
|
||||
|
||||
let height = window.innerHeight
|
||||
if (height > 500) {
|
||||
height = 500
|
||||
}
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/cache/createPopup", {
|
||||
height: height + "px",
|
||||
callback: function (resp) {
|
||||
resp.data.cacheRef.id = that.refs[index].id
|
||||
Vue.set(that.refs, index, resp.data.cacheRef)
|
||||
that.change()
|
||||
that.$refs.cacheRef[index].updateConds(resp.data.cacheRef.conds, resp.data.cacheRef.simpleCond)
|
||||
that.$refs.cacheRef[index].notifyChange()
|
||||
}
|
||||
})
|
||||
},
|
||||
disableRef: function (ref) {
|
||||
ref.isOn = false
|
||||
this.change()
|
||||
},
|
||||
enableRef: function (ref) {
|
||||
ref.isOn = true
|
||||
this.change()
|
||||
},
|
||||
removeRef: function (index) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要删除此缓存设置吗?", function () {
|
||||
that.refs.$remove(index)
|
||||
that.change()
|
||||
})
|
||||
},
|
||||
updateRefs: function (newRefs) {
|
||||
this.refs = newRefs
|
||||
if (this.vCacheConfig != null) {
|
||||
this.vCacheConfig.cacheRefs = newRefs
|
||||
}
|
||||
},
|
||||
timeUnitName: function (unit) {
|
||||
switch (unit) {
|
||||
case "ms":
|
||||
return "毫秒"
|
||||
case "second":
|
||||
return "秒"
|
||||
case "minute":
|
||||
return "分钟"
|
||||
case "hour":
|
||||
return "小时"
|
||||
case "day":
|
||||
return "天"
|
||||
case "week":
|
||||
return "周 "
|
||||
}
|
||||
return unit
|
||||
},
|
||||
change: function (callback) {
|
||||
this.$forceUpdate()
|
||||
|
||||
// 自动保存
|
||||
if (this.vCachePolicyId != null && this.vCachePolicyId > 0) { // 缓存策略
|
||||
Tea.action("/servers/components/cache/updateRefs")
|
||||
.params({
|
||||
cachePolicyId: this.vCachePolicyId,
|
||||
refsJSON: JSON.stringify(this.refs)
|
||||
})
|
||||
.post()
|
||||
} else if (this.vWebId != null && this.vWebId > 0) { // Server Web or Group Web
|
||||
Tea.action("/servers/server/settings/cache/updateRefs")
|
||||
.params({
|
||||
webId: this.vWebId,
|
||||
refsJSON: JSON.stringify(this.refs)
|
||||
})
|
||||
.success(function (resp) {
|
||||
if (resp.data.isUpdated) {
|
||||
teaweb.successToast("保存成功", null, function () {
|
||||
if (typeof callback == "function") {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.post()
|
||||
}
|
||||
},
|
||||
search: function (keyword) {
|
||||
if (typeof keyword != "string") {
|
||||
keyword = ""
|
||||
}
|
||||
|
||||
this.refs.forEach(function (ref) {
|
||||
if (keyword.length == 0) {
|
||||
ref.visible = true
|
||||
return
|
||||
}
|
||||
ref.visible = false
|
||||
|
||||
// simple cond
|
||||
if (ref.simpleCond != null && typeof ref.simpleCond.value == "string" && teaweb.match(ref.simpleCond.value, keyword)) {
|
||||
ref.visible = true
|
||||
return
|
||||
}
|
||||
|
||||
// composed conds
|
||||
if (ref.conds == null || ref.conds.groups == null || ref.conds.groups.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
ref.conds.groups.forEach(function (group) {
|
||||
if (group.conds != null) {
|
||||
group.conds.forEach(function (cond) {
|
||||
if (typeof cond.value == "string" && teaweb.match(cond.value, keyword)) {
|
||||
ref.visible = true
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
this.$forceUpdate()
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="refsJSON" :value="JSON.stringify(refs)"/>
|
||||
|
||||
<div>
|
||||
<p class="comment" v-if="refs.length == 0">暂时还没有缓存条件。</p>
|
||||
<table class="ui table selectable celled" v-show="refs.length > 0" id="sortable-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:1em"></th>
|
||||
<th>缓存条件</th>
|
||||
<th style="width: 7em">缓存时间</th>
|
||||
<th class="three op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-for="(cacheRef, index) in refs" :key="cacheRef.id" :v-id="cacheRef.id" v-show="cacheRef.visible !== false">
|
||||
<tr>
|
||||
<td style="text-align: center;"><i class="icon bars handle grey"></i> </td>
|
||||
<td :class="{'color-border': cacheRef.conds != null && cacheRef.conds.connector == 'and', disabled: !cacheRef.isOn}" :style="{'border-left':cacheRef.isReverse ? '1px #db2828 solid' : ''}">
|
||||
<http-request-conds-view :v-conds="cacheRef.conds" ref="cacheRef" :class="{disabled: !cacheRef.isOn}" v-if="cacheRef.conds != null && cacheRef.conds.groups != null"></http-request-conds-view>
|
||||
<http-request-cond-view :v-cond="cacheRef.simpleCond" ref="cacheRef" v-if="cacheRef.simpleCond != null"></http-request-cond-view>
|
||||
|
||||
<!-- 特殊参数 -->
|
||||
<grey-label v-if="cacheRef.key != null && cacheRef.key.indexOf('\${args}') < 0">忽略URI参数</grey-label>
|
||||
|
||||
<grey-label v-if="cacheRef.minSize != null && cacheRef.minSize.count > 0">
|
||||
{{cacheRef.minSize.count}}{{cacheRef.minSize.unit}}
|
||||
<span v-if="cacheRef.maxSize != null && cacheRef.maxSize.count > 0">- {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit.toUpperCase()}}</span>
|
||||
</grey-label>
|
||||
<grey-label v-else-if="cacheRef.maxSize != null && cacheRef.maxSize.count > 0">0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit.toUpperCase()}}</grey-label>
|
||||
|
||||
<grey-label v-if="cacheRef.overMaxSize != null"><span class="red">系统限制{{cacheRef.overMaxSize.count}}{{cacheRef.overMaxSize.unit.toUpperCase()}}</span> </grey-label>
|
||||
|
||||
<grey-label v-if="cacheRef.methods != null && cacheRef.methods.length > 0">{{cacheRef.methods.join(", ")}}</grey-label>
|
||||
<grey-label v-if="cacheRef.expiresTime != null && cacheRef.expiresTime.isPrior && cacheRef.expiresTime.isOn">Expires</grey-label>
|
||||
<grey-label v-if="cacheRef.status != null && cacheRef.status.length > 0 && (cacheRef.status.length > 1 || cacheRef.status[0] != 200)">状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}}</grey-label>
|
||||
<grey-label v-if="cacheRef.allowPartialContent">分片缓存</grey-label>
|
||||
<grey-label v-if="cacheRef.alwaysForwardRangeRequest">Range回源</grey-label>
|
||||
<grey-label v-if="cacheRef.enableIfNoneMatch">If-None-Match</grey-label>
|
||||
<grey-label v-if="cacheRef.enableIfModifiedSince">If-Modified-Since</grey-label>
|
||||
<grey-label v-if="cacheRef.enableReadingOriginAsync">支持异步</grey-label>
|
||||
</td>
|
||||
<td :class="{disabled: !cacheRef.isOn}">
|
||||
<span v-if="!cacheRef.isReverse">{{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}}</span>
|
||||
<span v-else class="red">不缓存</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="" @click.prevent="updateRef(index, cacheRef)">修改</a>
|
||||
<a href="" v-if="cacheRef.isOn" @click.prevent="disableRef(cacheRef)">暂停</a><a href="" v-if="!cacheRef.isOn" @click.prevent="enableRef(cacheRef)"><span class="red">恢复</span></a>
|
||||
<a href="" @click.prevent="removeRef(index)">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="comment" v-if="refs.length > 1">所有条件匹配顺序为从上到下,可以拖动左侧的<i class="icon bars"></i>排序。服务设置的优先级比全局缓存策略设置的优先级要高。</p>
|
||||
|
||||
<button class="ui button tiny" @click.prevent="addRef(false)" type="button">+添加缓存条件</button> <a href="" @click.prevent="addRef(true)" style="font-size: 0.8em">+添加不缓存条件</a>
|
||||
</div>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
Vue.component("http-cache-stale-config", {
|
||||
props: ["v-cache-stale-config"],
|
||||
data: function () {
|
||||
let config = this.vCacheStaleConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
status: [],
|
||||
supportStaleIfErrorHeader: true,
|
||||
life: {
|
||||
count: 1,
|
||||
unit: "day"
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
config: config
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
config: {
|
||||
deep: true,
|
||||
handler: function () {
|
||||
this.$emit("change", this.config)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
template: `<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">启用过时缓存</td>
|
||||
<td>
|
||||
<checkbox v-model="config.isOn"></checkbox>
|
||||
<p class="comment"><plus-label></plus-label>选中后,在更新缓存失败后会尝试读取过时的缓存。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="config.isOn">
|
||||
<td>有效期</td>
|
||||
<td>
|
||||
<time-duration-box :v-value="config.life"></time-duration-box>
|
||||
<p class="comment">缓存在过期之后,仍然保留的时间。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="config.isOn">
|
||||
<td>状态码</td>
|
||||
<td><http-status-box :v-status-list="config.status"></http-status-box>
|
||||
<p class="comment">在这些状态码出现时使用过时缓存,默认支持<code-label>50x</code-label>状态码。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="config.isOn">
|
||||
<td>支持stale-if-error</td>
|
||||
<td>
|
||||
<checkbox v-model="config.supportStaleIfErrorHeader"></checkbox>
|
||||
<p class="comment">选中后,支持在Cache-Control中通过<code-label>stale-if-error</code-label>指定过时缓存有效期。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`
|
||||
})
|
||||
@@ -0,0 +1,207 @@
|
||||
// HTTP CC防护配置
|
||||
Vue.component("http-cc-config-box", {
|
||||
props: ["v-cc-config", "v-is-location", "v-is-group"],
|
||||
data: function () {
|
||||
let config = this.vCcConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
enableFingerprint: true,
|
||||
enableGET302: true,
|
||||
onlyURLPatterns: [],
|
||||
exceptURLPatterns: [],
|
||||
useDefaultThresholds: true,
|
||||
ignoreCommonFiles: true
|
||||
}
|
||||
}
|
||||
|
||||
if (config.thresholds == null || config.thresholds.length == 0) {
|
||||
config.thresholds = [
|
||||
{
|
||||
maxRequests: 0
|
||||
},
|
||||
{
|
||||
maxRequests: 0
|
||||
},
|
||||
{
|
||||
maxRequests: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (typeof config.enableFingerprint != "boolean") {
|
||||
config.enableFingerprint = true
|
||||
}
|
||||
if (typeof config.enableGET302 != "boolean") {
|
||||
config.enableGET302 = true
|
||||
}
|
||||
|
||||
if (config.onlyURLPatterns == null) {
|
||||
config.onlyURLPatterns = []
|
||||
}
|
||||
if (config.exceptURLPatterns == null) {
|
||||
config.exceptURLPatterns = []
|
||||
}
|
||||
return {
|
||||
config: config,
|
||||
moreOptionsVisible: false,
|
||||
minQPSPerIP: config.minQPSPerIP,
|
||||
useCustomThresholds: !config.useDefaultThresholds,
|
||||
|
||||
thresholdMaxRequests0: this.maxRequestsStringAtThresholdIndex(config, 0),
|
||||
thresholdMaxRequests1: this.maxRequestsStringAtThresholdIndex(config, 1),
|
||||
thresholdMaxRequests2: this.maxRequestsStringAtThresholdIndex(config, 2)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
minQPSPerIP: function (v) {
|
||||
let qps = parseInt(v.toString())
|
||||
if (isNaN(qps) || qps < 0) {
|
||||
qps = 0
|
||||
}
|
||||
this.config.minQPSPerIP = qps
|
||||
},
|
||||
thresholdMaxRequests0: function (v) {
|
||||
this.setThresholdMaxRequests(0, v)
|
||||
},
|
||||
thresholdMaxRequests1: function (v) {
|
||||
this.setThresholdMaxRequests(1, v)
|
||||
},
|
||||
thresholdMaxRequests2: function (v) {
|
||||
this.setThresholdMaxRequests(2, v)
|
||||
},
|
||||
useCustomThresholds: function (b) {
|
||||
this.config.useDefaultThresholds = !b
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
maxRequestsStringAtThresholdIndex: function (config, index) {
|
||||
if (config.thresholds == null) {
|
||||
return ""
|
||||
}
|
||||
if (index < config.thresholds.length) {
|
||||
let s = config.thresholds[index].maxRequests.toString()
|
||||
if (s == "0") {
|
||||
s = ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
},
|
||||
setThresholdMaxRequests: function (index, v) {
|
||||
let maxRequests = parseInt(v)
|
||||
if (isNaN(maxRequests) || maxRequests < 0) {
|
||||
maxRequests = 0
|
||||
}
|
||||
if (index < this.config.thresholds.length) {
|
||||
this.config.thresholds[index].maxRequests = maxRequests
|
||||
}
|
||||
},
|
||||
showMoreOptions: function () {
|
||||
this.moreOptionsVisible = !this.moreOptionsVisible
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="ccJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="config" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="((!vIsLocation && !vIsGroup) || config.isPrior)">
|
||||
<tr>
|
||||
<td class="title">启用CC无感防护</td>
|
||||
<td>
|
||||
<checkbox v-model="config.isOn"></checkbox>
|
||||
<p class="comment"><plus-label></plus-label>启用后,自动检测并拦截CC攻击。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="config.isOn">
|
||||
<tr>
|
||||
<td colspan="2"><more-options-indicator @change="showMoreOptions"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="moreOptionsVisible && config.isOn">
|
||||
<tr>
|
||||
<td>例外URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.exceptURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了例外URL,表示这些URL跳过CC防护不做处理。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>限制URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.onlyURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了限制URL,表示只对这些URL进行CC防护处理;如果不填则表示支持所有的URL。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>忽略常用文件</td>
|
||||
<td>
|
||||
<checkbox v-model="config.ignoreCommonFiles"></checkbox>
|
||||
<p class="comment">忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>检查请求来源指纹</td>
|
||||
<td>
|
||||
<checkbox v-model="config.enableFingerprint"></checkbox>
|
||||
<p class="comment">在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>启用GET302校验</td>
|
||||
<td>
|
||||
<checkbox v-model="config.enableGET302"></checkbox>
|
||||
<p class="comment">选中后,表示自动通过GET302方法来校验客户端。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>单IP最低QPS</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" name="minQPSPerIP" maxlength="6" style="width: 6em" v-model="minQPSPerIP"/>
|
||||
<span class="ui label">请求数/秒</span>
|
||||
</div>
|
||||
<p class="comment">当某个IP在1分钟内平均QPS达到此值时,才会开始检测;如果设置为0,表示任何访问都会检测。(注意这里设置的是检测开启阈值,不是拦截阈值,拦截阈值在当前表单下方可以设置)</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="color-border">使用自定义拦截阈值</td>
|
||||
<td>
|
||||
<checkbox v-model="useCustomThresholds"></checkbox>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="!config.useDefaultThresholds">
|
||||
<td class="color-border">自定义拦截阈值设置</td>
|
||||
<td>
|
||||
<div>
|
||||
<div class="ui input left right labeled">
|
||||
<span class="ui label basic">单IP每5秒最多</span>
|
||||
<input type="text" style="width: 6em" maxlength="6" v-model="thresholdMaxRequests0"/>
|
||||
<span class="ui label basic">请求</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1em">
|
||||
<div class="ui input left right labeled">
|
||||
<span class="ui label basic">单IP每60秒</span>
|
||||
<input type="text" style="width: 6em" maxlength="6" v-model="thresholdMaxRequests1"/>
|
||||
<span class="ui label basic">请求</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 1em">
|
||||
<div class="ui input left right labeled">
|
||||
<span class="ui label basic">单IP每300秒</span>
|
||||
<input type="text" style="width: 6em" maxlength="6" v-model="thresholdMaxRequests2"/>
|
||||
<span class="ui label basic">请求</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
Vue.component("http-charsets-box", {
|
||||
props: ["v-usual-charsets", "v-all-charsets", "v-charset-config", "v-is-location", "v-is-group"],
|
||||
data: function () {
|
||||
let charsetConfig = this.vCharsetConfig
|
||||
if (charsetConfig == null) {
|
||||
charsetConfig = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
charset: "",
|
||||
isUpper: false,
|
||||
force: false
|
||||
}
|
||||
}
|
||||
return {
|
||||
charsetConfig: charsetConfig,
|
||||
advancedVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeAdvancedVisible: function (v) {
|
||||
this.advancedVisible = v
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="charsetJSON" :value="JSON.stringify(charsetConfig)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="charsetConfig" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="(!vIsLocation && !vIsGroup) || charsetConfig.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用字符编码</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="charsetConfig.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="((!vIsLocation && !vIsGroup) || charsetConfig.isPrior) && charsetConfig.isOn">
|
||||
<tr>
|
||||
<td class="title">选择字符编码</td>
|
||||
<td><select class="ui dropdown" style="width:20em" name="charset" v-model="charsetConfig.charset">
|
||||
<option value="">[未选择]</option>
|
||||
<optgroup label="常用字符编码"></optgroup>
|
||||
<option v-for="charset in vUsualCharsets" :value="charset.charset">{{charset.charset}}({{charset.name}})</option>
|
||||
<optgroup label="全部字符编码"></optgroup>
|
||||
<option v-for="charset in vAllCharsets" :value="charset.charset">{{charset.charset}}({{charset.name}})</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<more-options-tbody @change="changeAdvancedVisible" v-if="((!vIsLocation && !vIsGroup) || charsetConfig.isPrior) && charsetConfig.isOn"></more-options-tbody>
|
||||
<tbody v-show="((!vIsLocation && !vIsGroup) || charsetConfig.isPrior) && charsetConfig.isOn && advancedVisible">
|
||||
<tr>
|
||||
<td>强制替换</td>
|
||||
<td>
|
||||
<checkbox v-model="charsetConfig.force"></checkbox>
|
||||
<p class="comment">选中后,表示强制覆盖已经设置的字符集;不选中,表示如果源站已经设置了字符集,则保留不修改。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>字符编码大写</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="charsetConfig.isUpper"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中后将指定的字符编码转换为大写,比如默认为<code-label>utf-8</code-label>,选中后将改为<code-label>UTF-8</code-label>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
// 通用设置
|
||||
Vue.component("http-common-config-box", {
|
||||
props: ["v-common-config"],
|
||||
data: function () {
|
||||
let config = this.vCommonConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
mergeSlashes: false
|
||||
}
|
||||
}
|
||||
return {
|
||||
config: config
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">合并重复的路径分隔符</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="mergeSlashes" value="1" v-model="config.mergeSlashes"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">合并URL中重复的路径分隔符为一个,比如<code-label>//hello/world</code-label>中的<code-label>//</code-label>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,262 @@
|
||||
// 压缩配置
|
||||
Vue.component("http-compression-config-box", {
|
||||
props: ["v-compression-config", "v-is-location", "v-is-group"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
sortLoad(function () {
|
||||
that.initSortableTypes()
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
let config = this.vCompressionConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
useDefaultTypes: true,
|
||||
types: ["brotli", "gzip", "zstd", "deflate"],
|
||||
level: 0,
|
||||
decompressData: false,
|
||||
gzipRef: null,
|
||||
deflateRef: null,
|
||||
brotliRef: null,
|
||||
minLength: {count: 1, "unit": "kb"},
|
||||
maxLength: {count: 32, "unit": "mb"},
|
||||
mimeTypes: ["text/*", "application/javascript", "application/json", "application/atom+xml", "application/rss+xml", "application/xhtml+xml", "font/*", "image/svg+xml"],
|
||||
extensions: [".js", ".json", ".html", ".htm", ".xml", ".css", ".woff2", ".txt"],
|
||||
exceptExtensions: [".apk", ".ipa"],
|
||||
conds: null,
|
||||
enablePartialContent: false,
|
||||
onlyURLPatterns: [],
|
||||
exceptURLPatterns: []
|
||||
}
|
||||
}
|
||||
|
||||
if (config.types == null) {
|
||||
config.types = []
|
||||
}
|
||||
if (config.mimeTypes == null) {
|
||||
config.mimeTypes = []
|
||||
}
|
||||
if (config.extensions == null) {
|
||||
config.extensions = []
|
||||
}
|
||||
|
||||
let allTypes = [
|
||||
{
|
||||
name: "Gzip",
|
||||
code: "gzip",
|
||||
isOn: true
|
||||
},
|
||||
{
|
||||
name: "Deflate",
|
||||
code: "deflate",
|
||||
isOn: true
|
||||
},
|
||||
{
|
||||
name: "Brotli",
|
||||
code: "brotli",
|
||||
isOn: true
|
||||
},
|
||||
{
|
||||
name: "ZSTD",
|
||||
code: "zstd",
|
||||
isOn: true
|
||||
}
|
||||
]
|
||||
|
||||
let configTypes = []
|
||||
config.types.forEach(function (typeCode) {
|
||||
allTypes.forEach(function (t) {
|
||||
if (typeCode == t.code) {
|
||||
t.isOn = true
|
||||
configTypes.push(t)
|
||||
}
|
||||
})
|
||||
})
|
||||
allTypes.forEach(function (t) {
|
||||
if (!config.types.$contains(t.code)) {
|
||||
t.isOn = false
|
||||
configTypes.push(t)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
config: config,
|
||||
moreOptionsVisible: false,
|
||||
allTypes: configTypes
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isOn: function () {
|
||||
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
|
||||
},
|
||||
changeExtensions: function (values) {
|
||||
values.forEach(function (v, k) {
|
||||
if (v.length > 0 && v[0] != ".") {
|
||||
values[k] = "." + v
|
||||
}
|
||||
})
|
||||
this.config.extensions = values
|
||||
},
|
||||
changeExceptExtensions: function (values) {
|
||||
values.forEach(function (v, k) {
|
||||
if (v.length > 0 && v[0] != ".") {
|
||||
values[k] = "." + v
|
||||
}
|
||||
})
|
||||
this.config.exceptExtensions = values
|
||||
},
|
||||
changeMimeTypes: function (values) {
|
||||
this.config.mimeTypes = values
|
||||
},
|
||||
changeAdvancedVisible: function () {
|
||||
this.moreOptionsVisible = !this.moreOptionsVisible
|
||||
},
|
||||
changeConds: function (conds) {
|
||||
this.config.conds = conds
|
||||
},
|
||||
changeType: function () {
|
||||
this.config.types = []
|
||||
let that = this
|
||||
this.allTypes.forEach(function (v) {
|
||||
if (v.isOn) {
|
||||
that.config.types.push(v.code)
|
||||
}
|
||||
})
|
||||
},
|
||||
initSortableTypes: function () {
|
||||
let box = document.querySelector("#compression-types-box")
|
||||
let that = this
|
||||
Sortable.create(box, {
|
||||
draggable: ".checkbox",
|
||||
handle: ".icon.handle",
|
||||
onStart: function () {
|
||||
|
||||
},
|
||||
onUpdate: function (event) {
|
||||
let checkboxes = box.querySelectorAll(".checkbox")
|
||||
let codes = []
|
||||
checkboxes.forEach(function (checkbox) {
|
||||
let code = checkbox.getAttribute("data-code")
|
||||
codes.push(code)
|
||||
})
|
||||
that.config.types = codes
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="compressionJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="config" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="(!vIsLocation && !vIsGroup) || config.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用内容压缩</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="isOn()">
|
||||
<tr>
|
||||
<td>支持的扩展名</td>
|
||||
<td>
|
||||
<values-box :values="config.extensions" @change="changeExtensions" placeholder="比如 .html"></values-box>
|
||||
<p class="comment">含有这些扩展名的URL将会被压缩,不区分大小写。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>例外扩展名</td>
|
||||
<td>
|
||||
<values-box :values="config.exceptExtensions" @change="changeExceptExtensions" placeholder="比如 .html"></values-box>
|
||||
<p class="comment">含有这些扩展名的URL将<strong>不会</strong>被压缩,不区分大小写。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持的MimeType</td>
|
||||
<td>
|
||||
<values-box :values="config.mimeTypes" @change="changeMimeTypes" placeholder="比如 text/*"></values-box>
|
||||
<p class="comment">响应的Content-Type里包含这些MimeType的内容将会被压缩。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<more-options-tbody @change="changeAdvancedVisible" v-if="isOn()"></more-options-tbody>
|
||||
<tbody v-show="isOn() && moreOptionsVisible">
|
||||
<tr>
|
||||
<td>压缩算法</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="config.useDefaultTypes" id="compression-use-default"/>
|
||||
<label v-if="config.useDefaultTypes" for="compression-use-default">使用默认顺序<span class="grey small">(brotli、gzip、 zstd、deflate)</span></label>
|
||||
<label v-if="!config.useDefaultTypes" for="compression-use-default">使用默认顺序</label>
|
||||
</div>
|
||||
<div v-show="!config.useDefaultTypes">
|
||||
<div class="ui divider"></div>
|
||||
<div id="compression-types-box">
|
||||
<div class="ui checkbox" v-for="t in allTypes" style="margin-right: 2em" :data-code="t.code">
|
||||
<input type="checkbox" v-model="t.isOn" :id="'compression-type-' + t.code" @change="changeType"/>
|
||||
<label :for="'compression-type-' + t.code">{{t.name}} <i class="icon list small grey handle"></i></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="comment" v-show="!config.useDefaultTypes">选择支持的压缩算法和优先顺序,拖动<i class="icon list small grey"></i>图表排序。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持已压缩内容</td>
|
||||
<td>
|
||||
<checkbox v-model="config.decompressData"></checkbox>
|
||||
<p class="comment">支持对已压缩内容尝试重新使用新的算法压缩;不选中表示保留当前的压缩格式。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>内容最小长度</td>
|
||||
<td>
|
||||
<size-capacity-box :v-name="'minLength'" :v-value="config.minLength" :v-unit="'kb'"></size-capacity-box>
|
||||
<p class="comment">0表示不限制,内容长度从文件尺寸或Content-Length中获取。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>内容最大长度</td>
|
||||
<td>
|
||||
<size-capacity-box :v-name="'maxLength'" :v-value="config.maxLength" :v-unit="'mb'"></size-capacity-box>
|
||||
<p class="comment">0表示不限制,内容长度从文件尺寸或Content-Length中获取。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持Partial<br/>Content</td>
|
||||
<td>
|
||||
<checkbox v-model="config.enablePartialContent"></checkbox>
|
||||
<p class="comment">支持对分片内容(PartialContent)的压缩;除非客户端有特殊要求,一般不需要启用。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>例外URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.exceptURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了例外URL,表示这些URL跳过不做处理。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>限制URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.onlyURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了限制URL,表示只对这些URL进行压缩处理;如果不填则表示支持所有的URL。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>匹配条件</td>
|
||||
<td>
|
||||
<http-request-conds-box :v-conds="config.conds" @change="changeConds"></http-request-conds-box>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,907 @@
|
||||
// URL扩展名条件
|
||||
Vue.component("http-cond-url-extension", {
|
||||
props: ["v-cond"],
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${requestPathLowerExtension}",
|
||||
operator: "in",
|
||||
value: "[]"
|
||||
}
|
||||
if (this.vCond != null && this.vCond.param == cond.param) {
|
||||
cond.value = this.vCond.value
|
||||
}
|
||||
|
||||
let extensions = []
|
||||
try {
|
||||
extensions = JSON.parse(cond.value)
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
cond: cond,
|
||||
extensions: extensions, // TODO 可以拖动排序
|
||||
|
||||
isAdding: false,
|
||||
addingExt: ""
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
extensions: function () {
|
||||
this.cond.value = JSON.stringify(this.extensions)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addExt: function () {
|
||||
this.isAdding = !this.isAdding
|
||||
|
||||
if (this.isAdding) {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.addingExt.focus()
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
cancelAdding: function () {
|
||||
this.isAdding = false
|
||||
this.addingExt = ""
|
||||
},
|
||||
confirmAdding: function () {
|
||||
// TODO 做更详细的校验
|
||||
// TODO 如果有重复的则提示之
|
||||
|
||||
if (this.addingExt.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let that = this
|
||||
this.addingExt.split(/[,;,;|]/).forEach(function (ext) {
|
||||
ext = ext.trim()
|
||||
if (ext.length > 0) {
|
||||
if (ext[0] != ".") {
|
||||
ext = "." + ext
|
||||
}
|
||||
ext = ext.replace(/\s+/g, "").toLowerCase()
|
||||
that.extensions.push(ext)
|
||||
}
|
||||
})
|
||||
|
||||
// 清除状态
|
||||
this.cancelAdding()
|
||||
},
|
||||
removeExt: function (index) {
|
||||
this.extensions.$remove(index)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<div v-if="extensions.length > 0">
|
||||
<div class="ui label small basic" v-for="(ext, index) in extensions">{{ext}} <a href="" title="删除" @click.prevent="removeExt(index)"><i class="icon remove small"></i></a></div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div class="ui fields inline" v-if="isAdding">
|
||||
<div class="ui field">
|
||||
<input type="text" size="20" maxlength="100" v-model="addingExt" ref="addingExt" placeholder=".xxx, .yyy" @keyup.enter="confirmAdding" @keypress.enter.prevent="1" />
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny basic" type="button" @click.prevent="confirmAdding">确认</button>
|
||||
<a href="" title="取消" @click.prevent="cancelAdding"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 1em" v-show="!isAdding">
|
||||
<button class="ui button tiny basic" type="button" @click.prevent="addExt()">+添加扩展名</button>
|
||||
</div>
|
||||
<p class="comment">扩展名需要包含点(.)符号,例如<code-label>.jpg</code-label>、<code-label>.png</code-label>之类;多个扩展名用逗号分割。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// 排除URL扩展名条件
|
||||
Vue.component("http-cond-url-not-extension", {
|
||||
props: ["v-cond"],
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${requestPathLowerExtension}",
|
||||
operator: "not in",
|
||||
value: "[]"
|
||||
}
|
||||
if (this.vCond != null && this.vCond.param == cond.param) {
|
||||
cond.value = this.vCond.value
|
||||
}
|
||||
|
||||
let extensions = []
|
||||
try {
|
||||
extensions = JSON.parse(cond.value)
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
cond: cond,
|
||||
extensions: extensions, // TODO 可以拖动排序
|
||||
|
||||
isAdding: false,
|
||||
addingExt: ""
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
extensions: function () {
|
||||
this.cond.value = JSON.stringify(this.extensions)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addExt: function () {
|
||||
this.isAdding = !this.isAdding
|
||||
|
||||
if (this.isAdding) {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.addingExt.focus()
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
cancelAdding: function () {
|
||||
this.isAdding = false
|
||||
this.addingExt = ""
|
||||
},
|
||||
confirmAdding: function () {
|
||||
// TODO 做更详细的校验
|
||||
// TODO 如果有重复的则提示之
|
||||
|
||||
if (this.addingExt.length == 0) {
|
||||
return
|
||||
}
|
||||
if (this.addingExt[0] != ".") {
|
||||
this.addingExt = "." + this.addingExt
|
||||
}
|
||||
this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase()
|
||||
this.extensions.push(this.addingExt)
|
||||
|
||||
// 清除状态
|
||||
this.cancelAdding()
|
||||
},
|
||||
removeExt: function (index) {
|
||||
this.extensions.$remove(index)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<div v-if="extensions.length > 0">
|
||||
<div class="ui label small basic" v-for="(ext, index) in extensions">{{ext}} <a href="" title="删除" @click.prevent="removeExt(index)"><i class="icon remove"></i></a></div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div class="ui fields inline" v-if="isAdding">
|
||||
<div class="ui field">
|
||||
<input type="text" size="6" maxlength="100" v-model="addingExt" ref="addingExt" placeholder=".xxx" @keyup.enter="confirmAdding" @keypress.enter.prevent="1" />
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny basic" type="button" @click.prevent="confirmAdding">确认</button>
|
||||
<a href="" title="取消" @click.prevent="cancelAdding"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 1em" v-show="!isAdding">
|
||||
<button class="ui button tiny basic" type="button" @click.prevent="addExt()">+添加扩展名</button>
|
||||
</div>
|
||||
<p class="comment">扩展名需要包含点(.)符号,例如<code-label>.jpg</code-label>、<code-label>.png</code-label>之类。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// 根据URL前缀
|
||||
Vue.component("http-cond-url-prefix", {
|
||||
props: ["v-cond"],
|
||||
mounted: function () {
|
||||
this.$refs.valueInput.focus()
|
||||
},
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${requestPath}",
|
||||
operator: "prefix",
|
||||
value: "",
|
||||
isCaseInsensitive: false
|
||||
}
|
||||
if (this.vCond != null && typeof (this.vCond.value) == "string") {
|
||||
cond.value = this.vCond.value
|
||||
}
|
||||
return {
|
||||
cond: cond
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCaseInsensitive: function (isCaseInsensitive) {
|
||||
this.cond.isCaseInsensitive = isCaseInsensitive
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<input type="text" v-model="cond.value" ref="valueInput"/>
|
||||
<p class="comment">URL前缀,有此前缀的URL都将会被匹配,通常以<code-label>/</code-label>开头,比如<code-label>/static</code-label>、<code-label>/images</code-label>,不需要带域名。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
Vue.component("http-cond-url-not-prefix", {
|
||||
props: ["v-cond"],
|
||||
mounted: function () {
|
||||
this.$refs.valueInput.focus()
|
||||
},
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${requestPath}",
|
||||
operator: "prefix",
|
||||
value: "",
|
||||
isReverse: true,
|
||||
isCaseInsensitive: false
|
||||
}
|
||||
if (this.vCond != null && typeof this.vCond.value == "string") {
|
||||
cond.value = this.vCond.value
|
||||
}
|
||||
return {
|
||||
cond: cond
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCaseInsensitive: function (isCaseInsensitive) {
|
||||
this.cond.isCaseInsensitive = isCaseInsensitive
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<input type="text" v-model="cond.value" ref="valueInput"/>
|
||||
<p class="comment">要排除的URL前缀,有此前缀的URL都将会被匹配,通常以<code-label>/</code-label>开头,比如<code-label>/static</code-label>、<code-label>/images</code-label>,不需要带域名。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// 首页
|
||||
Vue.component("http-cond-url-eq-index", {
|
||||
props: ["v-cond"],
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${requestPath}",
|
||||
operator: "eq",
|
||||
value: "/",
|
||||
isCaseInsensitive: false
|
||||
}
|
||||
if (this.vCond != null && typeof this.vCond.value == "string") {
|
||||
cond.value = this.vCond.value
|
||||
}
|
||||
return {
|
||||
cond: cond
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCaseInsensitive: function (isCaseInsensitive) {
|
||||
this.cond.isCaseInsensitive = isCaseInsensitive
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<input type="text" v-model="cond.value" disabled="disabled" style="background: #eee"/>
|
||||
<p class="comment">检查URL路径是为<code-label>/</code-label>,不需要带域名。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// 全站
|
||||
Vue.component("http-cond-url-all", {
|
||||
props: ["v-cond"],
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${requestPath}",
|
||||
operator: "prefix",
|
||||
value: "/",
|
||||
isCaseInsensitive: false
|
||||
}
|
||||
if (this.vCond != null && typeof this.vCond.value == "string") {
|
||||
cond.value = this.vCond.value
|
||||
}
|
||||
return {
|
||||
cond: cond
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCaseInsensitive: function (isCaseInsensitive) {
|
||||
this.cond.isCaseInsensitive = isCaseInsensitive
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<input type="text" v-model="cond.value" disabled="disabled" style="background: #eee"/>
|
||||
<p class="comment">支持全站所有URL。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// URL精准匹配
|
||||
Vue.component("http-cond-url-eq", {
|
||||
props: ["v-cond"],
|
||||
mounted: function () {
|
||||
this.$refs.valueInput.focus()
|
||||
},
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${requestPath}",
|
||||
operator: "eq",
|
||||
value: "",
|
||||
isCaseInsensitive: false
|
||||
}
|
||||
if (this.vCond != null && typeof this.vCond.value == "string") {
|
||||
cond.value = this.vCond.value
|
||||
}
|
||||
return {
|
||||
cond: cond
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCaseInsensitive: function (isCaseInsensitive) {
|
||||
this.cond.isCaseInsensitive = isCaseInsensitive
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<input type="text" v-model="cond.value" ref="valueInput"/>
|
||||
<p class="comment">完整的URL路径,通常以<code-label>/</code-label>开头,比如<code-label>/static/ui.js</code-label>,不需要带域名。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
Vue.component("http-cond-url-not-eq", {
|
||||
props: ["v-cond"],
|
||||
mounted: function () {
|
||||
this.$refs.valueInput.focus()
|
||||
},
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${requestPath}",
|
||||
operator: "eq",
|
||||
value: "",
|
||||
isReverse: true,
|
||||
isCaseInsensitive: false
|
||||
}
|
||||
if (this.vCond != null && typeof this.vCond.value == "string") {
|
||||
cond.value = this.vCond.value
|
||||
}
|
||||
return {
|
||||
cond: cond
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCaseInsensitive: function (isCaseInsensitive) {
|
||||
this.cond.isCaseInsensitive = isCaseInsensitive
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<input type="text" v-model="cond.value" ref="valueInput"/>
|
||||
<p class="comment">要排除的完整的URL路径,通常以<code-label>/</code-label>开头,比如<code-label>/static/ui.js</code-label>,不需要带域名。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// URL正则匹配
|
||||
Vue.component("http-cond-url-regexp", {
|
||||
props: ["v-cond"],
|
||||
mounted: function () {
|
||||
this.$refs.valueInput.focus()
|
||||
},
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${requestPath}",
|
||||
operator: "regexp",
|
||||
value: "",
|
||||
isCaseInsensitive: false
|
||||
}
|
||||
if (this.vCond != null && typeof this.vCond.value == "string") {
|
||||
cond.value = this.vCond.value
|
||||
}
|
||||
return {
|
||||
cond: cond
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCaseInsensitive: function (isCaseInsensitive) {
|
||||
this.cond.isCaseInsensitive = isCaseInsensitive
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<input type="text" v-model="cond.value" ref="valueInput"/>
|
||||
<p class="comment">匹配URL的正则表达式,比如<code-label>^/static/(.*).js$</code-label>,不需要带域名。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// 排除URL正则匹配
|
||||
Vue.component("http-cond-url-not-regexp", {
|
||||
props: ["v-cond"],
|
||||
mounted: function () {
|
||||
this.$refs.valueInput.focus()
|
||||
},
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${requestPath}",
|
||||
operator: "not regexp",
|
||||
value: "",
|
||||
isCaseInsensitive: false
|
||||
}
|
||||
if (this.vCond != null && typeof this.vCond.value == "string") {
|
||||
cond.value = this.vCond.value
|
||||
}
|
||||
return {
|
||||
cond: cond
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCaseInsensitive: function (isCaseInsensitive) {
|
||||
this.cond.isCaseInsensitive = isCaseInsensitive
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<input type="text" v-model="cond.value" ref="valueInput"/>
|
||||
<p class="comment"><strong>不要</strong>匹配URL的正则表达式,意即只要匹配成功则排除此条件,比如<code-label>^/static/(.*).js$</code-label>,不需要带域名。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// URL通配符
|
||||
Vue.component("http-cond-url-wildcard-match", {
|
||||
props: ["v-cond"],
|
||||
mounted: function () {
|
||||
this.$refs.valueInput.focus()
|
||||
},
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${requestPath}",
|
||||
operator: "wildcard match",
|
||||
value: "",
|
||||
isCaseInsensitive: false
|
||||
}
|
||||
if (this.vCond != null && typeof this.vCond.value == "string") {
|
||||
cond.value = this.vCond.value
|
||||
}
|
||||
return {
|
||||
cond: cond
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCaseInsensitive: function (isCaseInsensitive) {
|
||||
this.cond.isCaseInsensitive = isCaseInsensitive
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<input type="text" v-model="cond.value" ref="valueInput"/>
|
||||
<p class="comment">匹配URL的通配符,用星号(<code-label>*</code-label>)表示任意字符,比如(<code-label>/images/*.png</code-label>、<code-label>/static/*</code-label>,不需要带域名。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// User-Agent正则匹配
|
||||
Vue.component("http-cond-user-agent-regexp", {
|
||||
props: ["v-cond"],
|
||||
mounted: function () {
|
||||
this.$refs.valueInput.focus()
|
||||
},
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${userAgent}",
|
||||
operator: "regexp",
|
||||
value: "",
|
||||
isCaseInsensitive: false
|
||||
}
|
||||
if (this.vCond != null && typeof this.vCond.value == "string") {
|
||||
cond.value = this.vCond.value
|
||||
}
|
||||
return {
|
||||
cond: cond
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCaseInsensitive: function (isCaseInsensitive) {
|
||||
this.cond.isCaseInsensitive = isCaseInsensitive
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<input type="text" v-model="cond.value" ref="valueInput"/>
|
||||
<p class="comment">匹配User-Agent的正则表达式,比如<code-label>Android|iPhone</code-label>。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// User-Agent正则不匹配
|
||||
Vue.component("http-cond-user-agent-not-regexp", {
|
||||
props: ["v-cond"],
|
||||
mounted: function () {
|
||||
this.$refs.valueInput.focus()
|
||||
},
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${userAgent}",
|
||||
operator: "not regexp",
|
||||
value: "",
|
||||
isCaseInsensitive: false
|
||||
}
|
||||
if (this.vCond != null && typeof this.vCond.value == "string") {
|
||||
cond.value = this.vCond.value
|
||||
}
|
||||
return {
|
||||
cond: cond
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCaseInsensitive: function (isCaseInsensitive) {
|
||||
this.cond.isCaseInsensitive = isCaseInsensitive
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<input type="text" v-model="cond.value" ref="valueInput"/>
|
||||
<p class="comment">匹配User-Agent的正则表达式,比如<code-label>Android|iPhone</code-label>,如果匹配,则排除此条件。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// 根据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: `<div>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<div v-if="mimeTypes.length > 0">
|
||||
<div class="ui label small" v-for="(mimeType, index) in mimeTypes">{{mimeType}} <a href="" title="删除" @click.prevent="removeMimeType(index)"><i class="icon remove"></i></a></div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div class="ui fields inline" v-if="isAdding">
|
||||
<div class="ui field">
|
||||
<input type="text" size="16" maxlength="100" v-model="addingMimeType" ref="addingMimeType" placeholder="类似于image/png" @keyup.enter="confirmAdding" @keypress.enter.prevent="1" />
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny basic" type="button" @click.prevent="confirmAdding">确认</button>
|
||||
<a href="" title="取消" @click.prevent="cancelAdding"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 1em">
|
||||
<button class="ui button tiny basic" type="button" @click.prevent="addMimeType()">+添加MimeType</button>
|
||||
</div>
|
||||
<p class="comment">服务器返回的内容的MimeType,比如<span class="ui label tiny">text/html</span>、<span class="ui label tiny">image/*</span>等。</p>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// 参数匹配
|
||||
Vue.component("http-cond-params", {
|
||||
props: ["v-cond"],
|
||||
mounted: function () {
|
||||
let cond = this.vCond
|
||||
if (cond == null) {
|
||||
return
|
||||
}
|
||||
this.operator = cond.operator
|
||||
|
||||
// stringValue
|
||||
if (["regexp", "not regexp", "eq", "not", "prefix", "suffix", "contains", "not contains", "eq ip", "gt ip", "gte ip", "lt ip", "lte ip", "ip range"].$contains(cond.operator)) {
|
||||
this.stringValue = cond.value
|
||||
return
|
||||
}
|
||||
|
||||
// numberValue
|
||||
if (["eq int", "eq float", "gt", "gte", "lt", "lte", "mod 10", "ip mod 10", "mod 100", "ip mod 100"].$contains(cond.operator)) {
|
||||
this.numberValue = cond.value
|
||||
return
|
||||
}
|
||||
|
||||
// modValue
|
||||
if (["mod", "ip mod"].$contains(cond.operator)) {
|
||||
let pieces = cond.value.split(",")
|
||||
this.modDivValue = pieces[0]
|
||||
if (pieces.length > 1) {
|
||||
this.modRemValue = pieces[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// stringValues
|
||||
let that = this
|
||||
if (["in", "not in", "file ext", "mime type"].$contains(cond.operator)) {
|
||||
try {
|
||||
let arr = JSON.parse(cond.value)
|
||||
if (arr != null && (arr instanceof Array)) {
|
||||
arr.forEach(function (v) {
|
||||
that.stringValues.push(v)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// versionValue
|
||||
if (["version range"].$contains(cond.operator)) {
|
||||
let pieces = cond.value.split(",")
|
||||
this.versionRangeMinValue = pieces[0]
|
||||
if (pieces.length > 1) {
|
||||
this.versionRangeMaxValue = pieces[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "",
|
||||
operator: window.REQUEST_COND_OPERATORS[0].op,
|
||||
value: "",
|
||||
isCaseInsensitive: false
|
||||
}
|
||||
if (this.vCond != null) {
|
||||
cond = this.vCond
|
||||
}
|
||||
return {
|
||||
cond: cond,
|
||||
operators: window.REQUEST_COND_OPERATORS,
|
||||
operator: window.REQUEST_COND_OPERATORS[0].op,
|
||||
operatorDescription: window.REQUEST_COND_OPERATORS[0].description,
|
||||
variables: window.REQUEST_VARIABLES,
|
||||
variable: "",
|
||||
|
||||
// 各种类型的值
|
||||
stringValue: "",
|
||||
numberValue: "",
|
||||
|
||||
modDivValue: "",
|
||||
modRemValue: "",
|
||||
|
||||
stringValues: [],
|
||||
|
||||
versionRangeMinValue: "",
|
||||
versionRangeMaxValue: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeVariable: function () {
|
||||
let v = this.cond.param
|
||||
if (v == null) {
|
||||
v = ""
|
||||
}
|
||||
this.cond.param = v + this.variable
|
||||
},
|
||||
changeOperator: function () {
|
||||
let that = this
|
||||
this.operators.forEach(function (v) {
|
||||
if (v.op == that.operator) {
|
||||
that.operatorDescription = v.description
|
||||
}
|
||||
})
|
||||
|
||||
this.cond.operator = this.operator
|
||||
|
||||
// 移动光标
|
||||
let box = document.getElementById("variables-value-box")
|
||||
if (box != null) {
|
||||
setTimeout(function () {
|
||||
let input = box.getElementsByTagName("INPUT")
|
||||
if (input.length > 0) {
|
||||
input[0].focus()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
changeStringValues: function (v) {
|
||||
this.stringValues = v
|
||||
this.cond.value = JSON.stringify(v)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
stringValue: function (v) {
|
||||
this.cond.value = v
|
||||
},
|
||||
numberValue: function (v) {
|
||||
// TODO 校验数字
|
||||
this.cond.value = v
|
||||
},
|
||||
modDivValue: function (v) {
|
||||
if (v.length == 0) {
|
||||
return
|
||||
}
|
||||
let div = parseInt(v)
|
||||
if (isNaN(div)) {
|
||||
div = 1
|
||||
}
|
||||
this.modDivValue = div
|
||||
this.cond.value = div + "," + this.modRemValue
|
||||
},
|
||||
modRemValue: function (v) {
|
||||
if (v.length == 0) {
|
||||
return
|
||||
}
|
||||
let rem = parseInt(v)
|
||||
if (isNaN(rem)) {
|
||||
rem = 0
|
||||
}
|
||||
this.modRemValue = rem
|
||||
this.cond.value = this.modDivValue + "," + rem
|
||||
},
|
||||
versionRangeMinValue: function (v) {
|
||||
this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue
|
||||
},
|
||||
versionRangeMaxValue: function (v) {
|
||||
this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue
|
||||
}
|
||||
},
|
||||
template: `<tbody>
|
||||
<tr>
|
||||
<td style="width: 8em">参数值</td>
|
||||
<td>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<div>
|
||||
<div class="ui field">
|
||||
<input type="text" placeholder="\${xxx}" v-model="cond.param"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" style="width: 16em; color: grey" v-model="variable" @change="changeVariable">
|
||||
<option value="">[常用参数]</option>
|
||||
<option v-for="v in variables" :value="v.code">{{v.code}} - {{v.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment">其中可以使用变量,类似于<code-label>\${requestPath}</code-label>,也可以是多个变量的组合。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>操作符</td>
|
||||
<td>
|
||||
<div>
|
||||
<select class="ui dropdown auto-width" v-model="operator" @change="changeOperator">
|
||||
<option v-for="operator in operators" :value="operator.op">{{operator.name}}</option>
|
||||
</select>
|
||||
<p class="comment" v-html="operatorDescription"></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="!['file exist', 'file not exist'].$contains(cond.operator)">
|
||||
<td>对比值</td>
|
||||
<td id="variables-value-box">
|
||||
<!-- 正则表达式 -->
|
||||
<div v-if="['regexp', 'not regexp'].$contains(cond.operator)">
|
||||
<input type="text" v-model="stringValue"/>
|
||||
<p class="comment">要匹配的正则表达式,比如<code-label>^/static/(.+).js</code-label>。</p>
|
||||
</div>
|
||||
|
||||
<!-- 数字相关 -->
|
||||
<div v-if="['eq int', 'eq float', 'gt', 'gte', 'lt', 'lte'].$contains(cond.operator)">
|
||||
<input type="text" maxlength="11" size="11" style="width: 5em" v-model="numberValue"/>
|
||||
<p class="comment">要对比的数字。</p>
|
||||
</div>
|
||||
|
||||
<!-- 取模 -->
|
||||
<div v-if="['mod 10'].$contains(cond.operator)">
|
||||
<input type="text" maxlength="11" size="11" style="width: 5em" v-model="numberValue"/>
|
||||
<p class="comment">参数值除以10的余数,在0-9之间。</p>
|
||||
</div>
|
||||
<div v-if="['mod 100'].$contains(cond.operator)">
|
||||
<input type="text" maxlength="11" size="11" style="width: 5em" v-model="numberValue"/>
|
||||
<p class="comment">参数值除以100的余数,在0-99之间。</p>
|
||||
</div>
|
||||
<div v-if="['mod', 'ip mod'].$contains(cond.operator)">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">除:</div>
|
||||
<div class="ui field">
|
||||
<input type="text" maxlength="11" size="11" style="width: 5em" v-model="modDivValue" placeholder="除数"/>
|
||||
</div>
|
||||
<div class="ui field">余:</div>
|
||||
<div class="ui field">
|
||||
<input type="text" maxlength="11" size="11" style="width: 5em" v-model="modRemValue" placeholder="余数"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字符串相关 -->
|
||||
<div v-if="['eq', 'not', 'prefix', 'suffix', 'contains', 'not contains'].$contains(cond.operator)">
|
||||
<input type="text" v-model="stringValue"/>
|
||||
<p class="comment" v-if="cond.operator == 'eq'">和参数值一致的字符串。</p>
|
||||
<p class="comment" v-if="cond.operator == 'not'">和参数值不一致的字符串。</p>
|
||||
<p class="comment" v-if="cond.operator == 'prefix'">参数值的前缀。</p>
|
||||
<p class="comment" v-if="cond.operator == 'suffix'">参数值的后缀为此字符串。</p>
|
||||
<p class="comment" v-if="cond.operator == 'contains'">参数值包含此字符串。</p>
|
||||
<p class="comment" v-if="cond.operator == 'not contains'">参数值不包含此字符串。</p>
|
||||
</div>
|
||||
<div v-if="['in', 'not in', 'file ext', 'mime type'].$contains(cond.operator)">
|
||||
<values-box @change="changeStringValues" :values="stringValues" size="15"></values-box>
|
||||
<p class="comment" v-if="cond.operator == 'in'">添加参数值列表。</p>
|
||||
<p class="comment" v-if="cond.operator == 'not in'">添加参数值列表。</p>
|
||||
<p class="comment" v-if="cond.operator == 'file ext'">添加扩展名列表,比如<code-label>png</code-label>、<code-label>html</code-label>,不包括点。</p>
|
||||
<p class="comment" v-if="cond.operator == 'mime type'">添加MimeType列表,类似于<code-label>text/html</code-label>、<code-label>image/*</code-label>。</p>
|
||||
</div>
|
||||
<div v-if="['version range'].$contains(cond.operator)">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field"><input type="text" v-model="versionRangeMinValue" maxlength="200" placeholder="最小版本" style="width: 10em"/></div>
|
||||
<div class="ui field">-</div>
|
||||
<div class="ui field"><input type="text" v-model="versionRangeMaxValue" maxlength="200" placeholder="最大版本" style="width: 10em"/></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP相关 -->
|
||||
<div v-if="['eq ip', 'gt ip', 'gte ip', 'lt ip', 'lte ip', 'ip range'].$contains(cond.operator)">
|
||||
<input type="text" style="width: 10em" v-model="stringValue" placeholder="x.x.x.x"/>
|
||||
<p class="comment">要对比的IP。</p>
|
||||
</div>
|
||||
<div v-if="['ip mod 10'].$contains(cond.operator)">
|
||||
<input type="text" maxlength="11" size="11" style="width: 5em" v-model="numberValue"/>
|
||||
<p class="comment">参数中IP转换成整数后除以10的余数,在0-9之间。</p>
|
||||
</div>
|
||||
<div v-if="['ip mod 100'].$contains(cond.operator)">
|
||||
<input type="text" maxlength="11" size="11" style="width: 5em" v-model="numberValue"/>
|
||||
<p class="comment">参数中IP转换成整数后除以100的余数,在0-99之间。</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="['regexp', 'not regexp', 'eq', 'not', 'prefix', 'suffix', 'contains', 'not contains', 'in', 'not in'].$contains(cond.operator)">
|
||||
<td>不区分大小写</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="condIsCaseInsensitive" v-model="cond.isCaseInsensitive"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中后表示对比时忽略参数值的大小写。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
`
|
||||
})
|
||||
@@ -0,0 +1,121 @@
|
||||
Vue.component("http-cors-header-config-box", {
|
||||
props: ["value"],
|
||||
data: function () {
|
||||
let config = this.value
|
||||
if (config == null) {
|
||||
config = {
|
||||
isOn: false,
|
||||
allowMethods: [],
|
||||
allowOrigin: "",
|
||||
allowCredentials: true,
|
||||
exposeHeaders: [],
|
||||
maxAge: 0,
|
||||
requestHeaders: [],
|
||||
requestMethod: "",
|
||||
optionsMethodOnly: false
|
||||
}
|
||||
}
|
||||
if (config.allowMethods == null) {
|
||||
config.allowMethods = []
|
||||
}
|
||||
if (config.exposeHeaders == null) {
|
||||
config.exposeHeaders = []
|
||||
}
|
||||
|
||||
let maxAgeSecondsString = config.maxAge.toString()
|
||||
if (maxAgeSecondsString == "0") {
|
||||
maxAgeSecondsString = ""
|
||||
}
|
||||
|
||||
return {
|
||||
config: config,
|
||||
|
||||
maxAgeSecondsString: maxAgeSecondsString,
|
||||
|
||||
moreOptionsVisible: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
maxAgeSecondsString: function (v) {
|
||||
let seconds = parseInt(v)
|
||||
if (isNaN(seconds)) {
|
||||
seconds = 0
|
||||
}
|
||||
this.config.maxAge = seconds
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeMoreOptions: function (visible) {
|
||||
this.moreOptionsVisible = visible
|
||||
},
|
||||
addDefaultAllowMethods: function () {
|
||||
let that = this
|
||||
let defaultMethods = ["PUT", "GET", "POST", "DELETE", "HEAD", "OPTIONS", "PATCH"]
|
||||
defaultMethods.forEach(function (method) {
|
||||
if (!that.config.allowMethods.$contains(method)) {
|
||||
that.config.allowMethods.push(method)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="corsJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">启用CORS自适应跨域</td>
|
||||
<td>
|
||||
<checkbox v-model="config.isOn"></checkbox>
|
||||
<p class="comment">启用后,自动在响应报头中增加对应的<code-label>Access-Control-*</code-label>相关内容。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="config.isOn">
|
||||
<tr>
|
||||
<td colspan="2"><more-options-indicator @change="changeMoreOptions"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="config.isOn && moreOptionsVisible">
|
||||
<tr>
|
||||
<td>允许的请求方法列表</td>
|
||||
<td>
|
||||
<http-methods-box :v-methods="config.allowMethods"></http-methods-box>
|
||||
<p class="comment"><a href="" @click.prevent="addDefaultAllowMethods">[添加默认]</a>。<code-label>Access-Control-Allow-Methods</code-label>值设置。所访问资源允许使用的方法列表,不设置则表示默认为<code-label>PUT</code-label>、<code-label>GET</code-label>、<code-label>POST</code-label>、<code-label>DELETE</code-label>、<code-label>HEAD</code-label>、<code-label>OPTIONS</code-label>、<code-label>PATCH</code-label>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>预检结果缓存时间</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" style="width: 6em" maxlength="6" v-model="maxAgeSecondsString"/>
|
||||
<span class="ui label">秒</span>
|
||||
</div>
|
||||
<p class="comment"><code-label>Access-Control-Max-Age</code-label>值设置。预检结果缓存时间,0或者不填表示使用浏览器默认设置。注意每个浏览器有不同的缓存时间上限。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>允许服务器暴露的报头</td>
|
||||
<td>
|
||||
<values-box :v-values="config.exposeHeaders"></values-box>
|
||||
<p class="comment"><code-label>Access-Control-Expose-Headers</code-label>值设置。允许服务器暴露的报头,请注意报头的大小写。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>实际请求方法</td>
|
||||
<td>
|
||||
<input type="text" v-model="config.requestMethod"/>
|
||||
<p class="comment"><code-label>Access-Control-Request-Method</code-label>值设置。实际请求服务器时使用的方法,比如<code-label>POST</code-label>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>仅OPTIONS有效</td>
|
||||
<td>
|
||||
<checkbox v-model="config.optionsMethodOnly"></checkbox>
|
||||
<p class="comment">选中后,表示当前CORS设置仅在OPTIONS方法请求时有效。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,215 @@
|
||||
// 页面动态加密配置
|
||||
Vue.component("http-encryption-config-box", {
|
||||
props: ["v-encryption-config", "v-is-location", "v-is-group"],
|
||||
data: function () {
|
||||
let config = this.vEncryptionConfig
|
||||
|
||||
return {
|
||||
config: config,
|
||||
htmlMoreOptions: false,
|
||||
javascriptMoreOptions: false,
|
||||
keyPolicyMoreOptions: false,
|
||||
cacheMoreOptions: false,
|
||||
encryptionMoreOptions: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isOn: function () {
|
||||
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="encryptionJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table definition selectable" v-if="vIsLocation || vIsGroup">
|
||||
<prior-checkbox :v-config="config"></prior-checkbox>
|
||||
</table>
|
||||
|
||||
<div v-show="(!vIsLocation && !vIsGroup) || config.isPrior">
|
||||
<div class="margin"></div>
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">启用页面动态加密</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">启用后,将对 HTML 页面中的 JavaScript 进行动态加密,有效抵御批量爬虫和脚本工具。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="config.isOn">
|
||||
<td colspan="2"><more-options-indicator v-model="encryptionMoreOptions" label="更多选项"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="encryptionMoreOptions">
|
||||
<tr>
|
||||
<td class="title">排除 URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.excludeURLs"></url-patterns-box>
|
||||
<p class="comment">这些 URL 将跳过加密处理,支持正则表达式。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-show="config.isOn">
|
||||
<div class="margin"></div>
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">HTML 加密</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.html.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">加密 HTML 页面中的 JavaScript 脚本。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="config.html.isOn">
|
||||
<td colspan="2"><more-options-indicator v-model="htmlMoreOptions"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="htmlMoreOptions">
|
||||
<tr>
|
||||
<td class="title">加密内联脚本</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.html.encryptInlineScripts"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">加密 HTML 中的内联 <script> 标签内容。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">加密外部脚本</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.html.encryptExternalScripts"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">加密通过 src 属性引入的外部 JavaScript 文件。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">URL 匹配规则</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.html.urlPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了匹配规则,表示只对这些 URL 进行加密处理;如果不填则表示支持所有的 URL。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="margin"></div>
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">JavaScript 文件加密</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.javascript.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">加密独立的 JavaScript 文件(.js 文件)。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="config.javascript.isOn">
|
||||
<td colspan="2"><more-options-indicator v-model="javascriptMoreOptions"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="javascriptMoreOptions">
|
||||
<tr>
|
||||
<td class="title">URL 匹配规则</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.javascript.urlPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了匹配规则,表示只对这些 URL 进行加密处理;如果不填则表示支持所有的 URL。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="margin"></div>
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">服务器端密钥</td>
|
||||
<td>
|
||||
<input type="text" v-model="config.keyPolicy.serverSecret" maxlength="128"/>
|
||||
<p class="comment">用于生成加密密钥的密码,建议使用复杂的随机字符串。默认密钥仅用于测试,生产环境请务必修改!</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><more-options-indicator v-model="keyPolicyMoreOptions" label="更多选项"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="keyPolicyMoreOptions">
|
||||
<tr>
|
||||
<td class="title">时间分片(秒)</td>
|
||||
<td>
|
||||
<input type="number" v-model.number="config.keyPolicy.timeBucket" min="30" max="300" step="10"/>
|
||||
<p class="comment">加密密钥每隔多少秒更换一次。时间越短越安全,但可能影响性能。建议 60-120 秒,默认 60 秒。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">IP CIDR 前缀长度</td>
|
||||
<td>
|
||||
<input type="number" v-model.number="config.keyPolicy.ipCIDR" min="16" max="32" step="1"/>
|
||||
<p class="comment">将用户 IP 地址的前多少位作为识别依据。例如设置为 24 时,192.168.1.1 和 192.168.1.2 会被视为同一用户。默认 24。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">简化 User-Agent</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.keyPolicy.uaSimplify"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">开启后,只识别浏览器类型(如 Chrome、Firefox),忽略版本号等细节,避免因浏览器自动更新导致解密失败。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="margin"></div>
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">启用缓存</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.cache.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">开启后,相同内容的加密结果会被缓存,减少重复计算,提升响应速度。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="config.cache.isOn">
|
||||
<td colspan="2"><more-options-indicator v-model="cacheMoreOptions" label="更多选项"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="cacheMoreOptions">
|
||||
<tr>
|
||||
<td class="title">缓存 TTL(秒)</td>
|
||||
<td>
|
||||
<input type="number" v-model.number="config.cache.ttl" min="30" max="300" step="10"/>
|
||||
<p class="comment">缓存的有效期,超过这个时间后缓存会自动失效。建议与上面的"时间分片"保持一致。默认 60 秒。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">最大缓存条目数</td>
|
||||
<td>
|
||||
<input type="number" v-model.number="config.cache.maxSize" min="100" max="10000" step="100"/>
|
||||
<p class="comment">最多缓存多少个加密结果。数量越大占用内存越多,建议根据服务器内存情况调整。默认 1000。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
Vue.component("http-expires-time-config-box", {
|
||||
props: ["v-expires-time"],
|
||||
data: function () {
|
||||
let expiresTime = this.vExpiresTime
|
||||
if (expiresTime == null) {
|
||||
expiresTime = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
overwrite: true,
|
||||
autoCalculate: true,
|
||||
duration: {count: -1, "unit": "hour"}
|
||||
}
|
||||
}
|
||||
return {
|
||||
expiresTime: expiresTime
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"expiresTime.isPrior": function () {
|
||||
this.notifyChange()
|
||||
},
|
||||
"expiresTime.isOn": function () {
|
||||
this.notifyChange()
|
||||
},
|
||||
"expiresTime.overwrite": function () {
|
||||
this.notifyChange()
|
||||
},
|
||||
"expiresTime.autoCalculate": function () {
|
||||
this.notifyChange()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
notifyChange: function () {
|
||||
this.$emit("change", this.expiresTime)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<table class="ui table">
|
||||
<prior-checkbox :v-config="expiresTime"></prior-checkbox>
|
||||
<tbody v-show="expiresTime.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用</td>
|
||||
<td><checkbox v-model="expiresTime.isOn"></checkbox>
|
||||
<p class="comment">启用后,将会在响应的Header中添加<code-label>Expires</code-label>字段,浏览器据此会将内容缓存在客户端;同时,在管理后台执行清理缓存时,也将无法清理客户端已有的缓存。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="expiresTime.isPrior && expiresTime.isOn">
|
||||
<td>覆盖源站设置</td>
|
||||
<td>
|
||||
<checkbox v-model="expiresTime.overwrite"></checkbox>
|
||||
<p class="comment">选中后,会覆盖源站Header中已有的<code-label>Expires</code-label>字段。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="expiresTime.isPrior && expiresTime.isOn">
|
||||
<td>自动计算时间</td>
|
||||
<td><checkbox v-model="expiresTime.autoCalculate"></checkbox>
|
||||
<p class="comment">根据已设置的缓存有效期进行计算。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="expiresTime.isPrior && expiresTime.isOn && !expiresTime.autoCalculate">
|
||||
<td>强制缓存时间</td>
|
||||
<td>
|
||||
<time-duration-box :v-value="expiresTime.duration" @change="notifyChange"></time-duration-box>
|
||||
<p class="comment">从客户端访问的时间开始要缓存的时长。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
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: `<div>
|
||||
<input type="hidden" name="fastcgiRefJSON" :value="JSON.stringify(fastcgiRef)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="fastcgiRef" v-if="vIsLocation"></prior-checkbox>
|
||||
<tbody v-show="(!this.vIsLocation || this.fastcgiRef.isPrior)">
|
||||
<tr>
|
||||
<td class="title">启用配置</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="fastcgiRef.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-if="isOn()">
|
||||
<tr>
|
||||
<td>Fastcgi服务</td>
|
||||
<td>
|
||||
<div v-show="fastcgiConfigs.length > 0" style="margin-bottom: 0.5em">
|
||||
<div class="ui label basic small" :class="{disabled: !fastcgi.isOn}" v-for="(fastcgi, index) in fastcgiConfigs">
|
||||
{{fastcgi.address}} <a href="" title="修改" @click.prevent="updateFastcgi(fastcgi.id, index)"><i class="ui icon pencil small"></i></a> <a href="" title="删除" @click.prevent="removeFastcgi(index)"><i class="ui icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divided"></div>
|
||||
</div>
|
||||
<button type="button" class="ui button tiny" @click.prevent="createFastcgi()">+</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
// Action列表
|
||||
Vue.component("http-firewall-actions-view", {
|
||||
props: ["v-actions"],
|
||||
template: `<div>
|
||||
<div v-for="action in vActions" style="margin-bottom: 0.3em">
|
||||
<span :class="{red: action.category == 'block', orange: action.category == 'verify', green: action.category == 'allow'}">{{action.name}} ({{action.code.toUpperCase()}})
|
||||
<div v-if="action.options != null">
|
||||
<span class="grey small" v-if="action.code.toLowerCase() == 'page'">[{{action.options.status}}]</span>
|
||||
<span class="grey small" v-if="action.code.toLowerCase() == 'allow' && action.options != null && action.options.scope != null && action.options.scope.length > 0">
|
||||
<span v-if="action.options.scope == 'group'">[分组]</span>
|
||||
<span v-if="action.options.scope == 'server'">[网站]</span>
|
||||
<span v-if="action.options.scope == 'global'">[网站和策略]</span>
|
||||
</span>
|
||||
<span class="grey small" v-if="action.code.toLowerCase() == 'record_ip'">
|
||||
<span v-if="action.options.type == 'black'" class="red">黑名单</span>
|
||||
<span v-if="action.options.type == 'white'" class="green">白名单</span>
|
||||
<span v-if="action.options.type == 'grey'" class="grey">灰名单</span>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
Vue.component("http-firewall-block-options-viewer", {
|
||||
props: ["v-block-options"],
|
||||
data: function () {
|
||||
return {
|
||||
options: this.vBlockOptions
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<span v-if="options == null">默认设置</span>
|
||||
<div v-else>
|
||||
状态码:{{options.statusCode}} / 提示内容:<span v-if="options.body != null && options.body.length > 0">[{{options.body.length}}字符]</span><span v-else class="disabled">[无]</span> / 超时时间:{{options.timeout}}秒 <span v-if="options.timeoutMax > options.timeout">/ 最大封禁时长:{{options.timeoutMax}}秒</span>
|
||||
<span v-if="options.failBlockScopeAll"> / 尝试全局封禁</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
Vue.component("http-firewall-block-options", {
|
||||
props: ["v-block-options"],
|
||||
data: function () {
|
||||
return {
|
||||
options: this.vBlockOptions,
|
||||
statusCode: this.vBlockOptions.statusCode,
|
||||
timeout: this.vBlockOptions.timeout,
|
||||
timeoutMax: this.vBlockOptions.timeoutMax,
|
||||
isEditing: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
statusCode: function (v) {
|
||||
let statusCode = parseInt(v)
|
||||
if (isNaN(statusCode)) {
|
||||
this.options.statusCode = 403
|
||||
} else {
|
||||
this.options.statusCode = statusCode
|
||||
}
|
||||
},
|
||||
timeout: function (v) {
|
||||
let timeout = parseInt(v)
|
||||
if (isNaN(timeout)) {
|
||||
this.options.timeout = 0
|
||||
} else {
|
||||
this.options.timeout = timeout
|
||||
}
|
||||
},
|
||||
timeoutMax: function (v) {
|
||||
let timeoutMax = parseInt(v)
|
||||
if (isNaN(timeoutMax)) {
|
||||
this.options.timeoutMax = 0
|
||||
} else {
|
||||
this.options.timeoutMax = timeoutMax
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
edit: function () {
|
||||
this.isEditing = !this.isEditing
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="blockOptionsJSON" :value="JSON.stringify(options)"/>
|
||||
<a href="" @click.prevent="edit">状态码:{{statusCode}} / 提示内容:<span v-if="options.body != null && options.body.length > 0">[{{options.body.length}}字符]</span><span v-else class="disabled">[无]</span> <span v-if="timeout > 0"> / 封禁时长:{{timeout}}秒</span>
|
||||
<span v-if="timeoutMax > timeout"> / 最大封禁时长:{{timeoutMax}}秒</span>
|
||||
<span v-if="options.failBlockScopeAll"> / 尝试全局封禁</span>
|
||||
<i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i></a>
|
||||
<table class="ui table" v-show="isEditing">
|
||||
<tr>
|
||||
<td class="title">状态码</td>
|
||||
<td>
|
||||
<input type="text" v-model="statusCode" style="width:4.5em" maxlength="3"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>提示内容</td>
|
||||
<td>
|
||||
<textarea rows="3" v-model="options.body"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>封禁时长</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" v-model="timeout" style="width: 5em" maxlength="6"/>
|
||||
<span class="ui label">秒</span>
|
||||
</div>
|
||||
<p class="comment">触发阻止动作时,封禁客户端IP的时间。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>最大封禁时长</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" v-model="timeoutMax" style="width: 5em" maxlength="6"/>
|
||||
<span class="ui label">秒</span>
|
||||
</div>
|
||||
<p class="comment">如果最大封禁时长大于封禁时长({{timeout}}秒),那么表示每次封禁的时候,将会在这两个时长数字之间随机选取一个数字作为最终的封禁时长。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>失败全局封禁</td>
|
||||
<td>
|
||||
<checkbox v-model="options.failBlockScopeAll"></checkbox>
|
||||
<p class="comment">选中后,表示允许系统尝试全局封禁某个IP,以提升封禁性能。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
Vue.component("http-firewall-captcha-options-viewer", {
|
||||
props: ["v-captcha-options"],
|
||||
mounted: function () {
|
||||
this.updateSummary()
|
||||
},
|
||||
data: function () {
|
||||
let options = this.vCaptchaOptions
|
||||
if (options == null) {
|
||||
options = {
|
||||
life: 0,
|
||||
maxFails: 0,
|
||||
failBlockTimeout: 0,
|
||||
failBlockScopeAll: false,
|
||||
uiIsOn: false,
|
||||
uiTitle: "",
|
||||
uiPrompt: "",
|
||||
uiButtonTitle: "",
|
||||
uiShowRequestId: false,
|
||||
uiCss: "",
|
||||
uiFooter: "",
|
||||
uiBody: "",
|
||||
cookieId: "",
|
||||
lang: ""
|
||||
}
|
||||
}
|
||||
return {
|
||||
options: options,
|
||||
summary: "",
|
||||
captchaTypes: window.WAF_CAPTCHA_TYPES
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateSummary: function () {
|
||||
let summaryList = []
|
||||
if (this.options.life > 0) {
|
||||
summaryList.push("有效时间" + this.options.life + "秒")
|
||||
}
|
||||
if (this.options.maxFails > 0) {
|
||||
summaryList.push("最多失败" + this.options.maxFails + "次")
|
||||
}
|
||||
if (this.options.failBlockTimeout > 0) {
|
||||
summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒")
|
||||
}
|
||||
if (this.options.failBlockScopeAll) {
|
||||
summaryList.push("全局封禁")
|
||||
}
|
||||
let that = this
|
||||
let typeDef = this.captchaTypes.$find(function (k, v) {
|
||||
return v.code == that.options.captchaType
|
||||
})
|
||||
if (typeDef != null) {
|
||||
summaryList.push("默认验证方式:" + typeDef.name)
|
||||
}
|
||||
|
||||
if (this.options.captchaType == "default") {
|
||||
if (this.options.uiIsOn) {
|
||||
summaryList.push("定制UI")
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.geeTestConfig != null && this.options.geeTestConfig.isOn) {
|
||||
summaryList.push("已配置极验")
|
||||
}
|
||||
|
||||
if (summaryList.length == 0) {
|
||||
this.summary = "默认配置"
|
||||
} else {
|
||||
this.summary = summaryList.join(" / ")
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div>{{summary}}</div>
|
||||
`
|
||||
})
|
||||
@@ -0,0 +1,297 @@
|
||||
Vue.component("http-firewall-captcha-options", {
|
||||
props: ["v-captcha-options"],
|
||||
mounted: function () {
|
||||
this.updateSummary()
|
||||
},
|
||||
data: function () {
|
||||
let options = this.vCaptchaOptions
|
||||
if (options == null) {
|
||||
options = {
|
||||
captchaType: "default",
|
||||
countLetters: 0,
|
||||
life: 0,
|
||||
maxFails: 0,
|
||||
failBlockTimeout: 0,
|
||||
failBlockScopeAll: false,
|
||||
uiIsOn: false,
|
||||
uiTitle: "",
|
||||
uiPrompt: "",
|
||||
uiButtonTitle: "",
|
||||
uiShowRequestId: true,
|
||||
uiCss: "",
|
||||
uiFooter: "",
|
||||
uiBody: "",
|
||||
cookieId: "",
|
||||
lang: "",
|
||||
geeTestConfig: {
|
||||
isOn: false,
|
||||
captchaId: "",
|
||||
captchaKey: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.countLetters <= 0) {
|
||||
options.countLetters = 6
|
||||
}
|
||||
|
||||
if (options.captchaType == null || options.captchaType.length == 0) {
|
||||
options.captchaType = "default"
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
options: options,
|
||||
isEditing: false,
|
||||
summary: "",
|
||||
uiBodyWarning: "",
|
||||
captchaTypes: window.WAF_CAPTCHA_TYPES
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"options.countLetters": function (v) {
|
||||
let i = parseInt(v, 10)
|
||||
if (isNaN(i)) {
|
||||
i = 0
|
||||
} else if (i < 0) {
|
||||
i = 0
|
||||
} else if (i > 10) {
|
||||
i = 10
|
||||
}
|
||||
this.options.countLetters = i
|
||||
},
|
||||
"options.life": function (v) {
|
||||
let i = parseInt(v, 10)
|
||||
if (isNaN(i)) {
|
||||
i = 0
|
||||
}
|
||||
this.options.life = i
|
||||
this.updateSummary()
|
||||
},
|
||||
"options.maxFails": function (v) {
|
||||
let i = parseInt(v, 10)
|
||||
if (isNaN(i)) {
|
||||
i = 0
|
||||
}
|
||||
this.options.maxFails = i
|
||||
this.updateSummary()
|
||||
},
|
||||
"options.failBlockTimeout": function (v) {
|
||||
let i = parseInt(v, 10)
|
||||
if (isNaN(i)) {
|
||||
i = 0
|
||||
}
|
||||
this.options.failBlockTimeout = i
|
||||
this.updateSummary()
|
||||
},
|
||||
"options.failBlockScopeAll": function (v) {
|
||||
this.updateSummary()
|
||||
},
|
||||
"options.captchaType": function (v) {
|
||||
this.updateSummary()
|
||||
},
|
||||
"options.uiIsOn": function (v) {
|
||||
this.updateSummary()
|
||||
},
|
||||
"options.uiBody": function (v) {
|
||||
if (/<form(>|\s).+\$\{body}.*<\/form>/s.test(v)) {
|
||||
this.uiBodyWarning = "页面模板中不能使用<form></form>标签包裹\${body}变量,否则将导致验证码表单无法提交。"
|
||||
} else {
|
||||
this.uiBodyWarning = ""
|
||||
}
|
||||
},
|
||||
"options.geeTestConfig.isOn": function (v) {
|
||||
this.updateSummary()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
edit: function () {
|
||||
this.isEditing = !this.isEditing
|
||||
},
|
||||
updateSummary: function () {
|
||||
let summaryList = []
|
||||
if (this.options.life > 0) {
|
||||
summaryList.push("有效时间" + this.options.life + "秒")
|
||||
}
|
||||
if (this.options.maxFails > 0) {
|
||||
summaryList.push("最多失败" + this.options.maxFails + "次")
|
||||
}
|
||||
if (this.options.failBlockTimeout > 0) {
|
||||
summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒")
|
||||
}
|
||||
if (this.options.failBlockScopeAll) {
|
||||
summaryList.push("尝试全局封禁")
|
||||
}
|
||||
|
||||
let that = this
|
||||
let typeDef = this.captchaTypes.$find(function (k, v) {
|
||||
return v.code == that.options.captchaType
|
||||
})
|
||||
if (typeDef != null) {
|
||||
summaryList.push("默认验证方式:" + typeDef.name)
|
||||
}
|
||||
|
||||
if (this.options.captchaType == "default") {
|
||||
if (this.options.uiIsOn) {
|
||||
summaryList.push("定制UI")
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.geeTestConfig != null && this.options.geeTestConfig.isOn) {
|
||||
summaryList.push("已配置极验")
|
||||
}
|
||||
|
||||
if (summaryList.length == 0) {
|
||||
this.summary = "默认配置"
|
||||
} else {
|
||||
this.summary = summaryList.join(" / ")
|
||||
}
|
||||
},
|
||||
confirm: function () {
|
||||
this.isEditing = false
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="captchaOptionsJSON" :value="JSON.stringify(options)"/>
|
||||
<a href="" @click.prevent="edit">{{summary}} <i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i></a>
|
||||
<div v-show="isEditing" style="margin-top: 0.5em">
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>默认验证方式</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="options.captchaType">
|
||||
<option v-for="captchaDef in captchaTypes" :value="captchaDef.code">{{captchaDef.name}}</option>
|
||||
</select>
|
||||
<p class="comment" v-for="captchaDef in captchaTypes" v-if="captchaDef.code == options.captchaType">{{captchaDef.description}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">有效时间</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" style="width: 5em" maxlength="9" v-model="options.life" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
||||
<span class="ui label">秒</span>
|
||||
</div>
|
||||
<p class="comment">验证通过后在这个时间内不再验证,默认600秒。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>最多失败次数</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" style="width: 5em" maxlength="9" v-model="options.maxFails" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
||||
<span class="ui label">次</span>
|
||||
</div>
|
||||
<p class="comment"><span v-if="options.maxFails > 0 && options.maxFails < 5" class="red">建议填入一个不小于5的数字,以减少误判几率。</span>允许用户失败尝试的最多次数,超过这个次数将被自动加入黑名单。如果为空或者为0,表示不限制。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>失败拦截时间</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" style="width: 5em" maxlength="9" v-model="options.failBlockTimeout" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
||||
<span class="ui label">秒</span>
|
||||
</div>
|
||||
<p class="comment">在达到最多失败次数(大于0)时,自动拦截的时长;如果为0表示不自动拦截。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>失败全局封禁</td>
|
||||
<td>
|
||||
<checkbox v-model="options.failBlockScopeAll"></checkbox>
|
||||
<p class="comment">选中后,表示允许系统尝试全局封禁某个IP,以提升封禁性能。</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-show="options.captchaType == 'default'">
|
||||
<td>验证码中数字个数</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="options.countLetters">
|
||||
<option v-for="i in 10" :value="i">{{i}}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="options.captchaType == 'default'">
|
||||
<td class="color-border">定制UI</td>
|
||||
<td><checkbox v-model="options.uiIsOn"></checkbox></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="options.uiIsOn && options.captchaType == 'default'">
|
||||
<tr>
|
||||
<td class="color-border">页面标题</td>
|
||||
<td>
|
||||
<input type="text" v-model="options.uiTitle" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="color-border">按钮标题</td>
|
||||
<td>
|
||||
<input type="text" v-model="options.uiButtonTitle" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
||||
<p class="comment">类似于<code-label>提交验证</code-label>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="color-border">显示请求ID</td>
|
||||
<td>
|
||||
<checkbox v-model="options.uiShowRequestId"></checkbox>
|
||||
<p class="comment">在界面上显示请求ID,方便用户报告问题。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="color-border">CSS样式</td>
|
||||
<td>
|
||||
<textarea spellcheck="false" v-model="options.uiCss" rows="2"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="color-border">页头提示</td>
|
||||
<td>
|
||||
<textarea spellcheck="false" v-model="options.uiPrompt" rows="2"></textarea>
|
||||
<p class="comment">类似于<code-label>请输入上面的验证码</code-label>,支持HTML。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="color-border">页尾提示</td>
|
||||
<td>
|
||||
<textarea spellcheck="false" v-model="options.uiFooter" rows="2"></textarea>
|
||||
<p class="comment">支持HTML。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="color-border">页面模板</td>
|
||||
<td>
|
||||
<textarea spellcheck="false" rows="2" v-model="options.uiBody"></textarea>
|
||||
<p class="comment"><span v-if="uiBodyWarning.length > 0" class="red">警告:{{uiBodyWarning}}</span><span v-if="options.uiBody.length > 0 && options.uiBody.indexOf('\${body}') < 0 " class="red">模板中必须包含\${body}表示验证码表单!</span>整个页面的模板,支持HTML,其中必须使用<code-label>\${body}</code-label>变量代表验证码表单,否则将无法正常显示验证码。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">允许用户使用极验</td>
|
||||
<td><checkbox v-model="options.geeTestConfig.isOn"></checkbox>
|
||||
<p class="comment">选中后,表示允许用户在WAF设置中选择极验。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tbody v-show="options.geeTestConfig.isOn">
|
||||
<tr>
|
||||
<td class="color-border">极验-验证ID *</td>
|
||||
<td>
|
||||
<input type="text" maxlength="100" name="geetestCaptchaId" v-model="options.geeTestConfig.captchaId" spellcheck="false"/>
|
||||
<p class="comment">在极验控制台--业务管理中获取。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="color-border">极验-验证Key *</td>
|
||||
<td>
|
||||
<input type="text" maxlength="100" name="geetestCaptchaKey" v-model="options.geeTestConfig.captchaKey" spellcheck="false"/>
|
||||
<p class="comment">在极验控制台--业务管理中获取。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
Vue.component("http-firewall-config-box", {
|
||||
props: ["v-firewall-config", "v-is-location", "v-is-group", "v-firewall-policy"],
|
||||
data: function () {
|
||||
let firewall = this.vFirewallConfig
|
||||
if (firewall == null) {
|
||||
firewall = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
firewallPolicyId: 0,
|
||||
ignoreGlobalRules: false,
|
||||
defaultCaptchaType: "none"
|
||||
}
|
||||
}
|
||||
|
||||
if (firewall.defaultCaptchaType == null || firewall.defaultCaptchaType.length == 0) {
|
||||
firewall.defaultCaptchaType = "none"
|
||||
}
|
||||
|
||||
let allCaptchaTypes = window.WAF_CAPTCHA_TYPES.$copy()
|
||||
|
||||
// geetest
|
||||
let geeTestIsOn = false
|
||||
if (this.vFirewallPolicy != null && this.vFirewallPolicy.captchaAction != null && this.vFirewallPolicy.captchaAction.geeTestConfig != null) {
|
||||
geeTestIsOn = this.vFirewallPolicy.captchaAction.geeTestConfig.isOn
|
||||
}
|
||||
|
||||
// 如果没有启用geetest,则还原
|
||||
if (!geeTestIsOn && firewall.defaultCaptchaType == "geetest") {
|
||||
firewall.defaultCaptchaType = "none"
|
||||
}
|
||||
|
||||
return {
|
||||
firewall: firewall,
|
||||
moreOptionsVisible: false,
|
||||
execGlobalRules: !firewall.ignoreGlobalRules,
|
||||
captchaTypes: allCaptchaTypes,
|
||||
geeTestIsOn: geeTestIsOn
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
execGlobalRules: function (v) {
|
||||
this.firewall.ignoreGlobalRules = !v
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeOptionsVisible: function (v) {
|
||||
this.moreOptionsVisible = v
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="firewallJSON" :value="JSON.stringify(firewall)"/>
|
||||
|
||||
<table class="ui table selectable definition" v-show="!vIsGroup">
|
||||
<tr>
|
||||
<td class="title">全局WAF策略</td>
|
||||
<td>
|
||||
<div v-if="vFirewallPolicy != null">{{vFirewallPolicy.name}} <span v-if="vFirewallPolicy.modeInfo != null"> <span :class="{green: vFirewallPolicy.modeInfo.code == 'defend', blue: vFirewallPolicy.modeInfo.code == 'observe', grey: vFirewallPolicy.modeInfo.code == 'bypass'}">[{{vFirewallPolicy.modeInfo.name}}]</span> </span> <link-icon :href="'/servers/components/waf/policy?firewallPolicyId=' + vFirewallPolicy.id"></link-icon>
|
||||
<p class="comment">当前网站所在集群的设置。</p>
|
||||
</div>
|
||||
<span v-else class="red">当前集群没有设置WAF策略,当前配置无法生效。</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="ui table selectable definition">
|
||||
<prior-checkbox :v-config="firewall" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="(!vIsLocation && !vIsGroup) || firewall.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用Web防火墙</td>
|
||||
<td>
|
||||
<checkbox v-model="firewall.isOn"></checkbox>
|
||||
<p class="comment">选中后,表示启用当前网站的WAF功能。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<more-options-tbody @change="changeOptionsVisible" v-show="firewall.isOn"></more-options-tbody>
|
||||
<tbody v-show="moreOptionsVisible">
|
||||
<tr>
|
||||
<td>人机识别验证方式</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="firewall.defaultCaptchaType">
|
||||
<option value="none">默认</option>
|
||||
<option v-for="captchaType in captchaTypes" v-if="captchaType.code != 'geetest' || geeTestIsOn" :value="captchaType.code">{{captchaType.name}}</option>
|
||||
</select>
|
||||
<p class="comment" v-if="firewall.defaultCaptchaType == 'none'">使用系统默认的设置。你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。</p>
|
||||
<p class="comment" v-for="captchaType in captchaTypes" v-if="captchaType.code == firewall.defaultCaptchaType">{{captchaType.description}}你需要在入站规则中添加规则集来决定哪些请求需要人机识别验证。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>启用系统全局规则</td>
|
||||
<td>
|
||||
<checkbox v-model="execGlobalRules"></checkbox>
|
||||
<p class="comment">选中后,表示使用系统全局WAF策略中定义的规则。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
Vue.component("http-firewall-js-cookie-options-viewer", {
|
||||
props: ["v-js-cookie-options"],
|
||||
mounted: function () {
|
||||
this.updateSummary()
|
||||
},
|
||||
data: function () {
|
||||
let options = this.vJsCookieOptions
|
||||
if (options == null) {
|
||||
options = {
|
||||
life: 0,
|
||||
maxFails: 0,
|
||||
failBlockTimeout: 0,
|
||||
failBlockScopeAll: false,
|
||||
scope: ""
|
||||
}
|
||||
}
|
||||
return {
|
||||
options: options,
|
||||
summary: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateSummary: function () {
|
||||
let summaryList = []
|
||||
if (this.options.life > 0) {
|
||||
summaryList.push("有效时间" + this.options.life + "秒")
|
||||
}
|
||||
if (this.options.maxFails > 0) {
|
||||
summaryList.push("最多失败" + this.options.maxFails + "次")
|
||||
}
|
||||
if (this.options.failBlockTimeout > 0) {
|
||||
summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒")
|
||||
}
|
||||
if (this.options.failBlockScopeAll) {
|
||||
summaryList.push("尝试全局封禁")
|
||||
}
|
||||
|
||||
if (summaryList.length == 0) {
|
||||
this.summary = "默认配置"
|
||||
} else {
|
||||
this.summary = summaryList.join(" / ")
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div>{{summary}}</div>
|
||||
`
|
||||
})
|
||||
@@ -0,0 +1,130 @@
|
||||
Vue.component("http-firewall-js-cookie-options", {
|
||||
props: ["v-js-cookie-options"],
|
||||
mounted: function () {
|
||||
this.updateSummary()
|
||||
},
|
||||
data: function () {
|
||||
let options = this.vJsCookieOptions
|
||||
if (options == null) {
|
||||
options = {
|
||||
life: 0,
|
||||
maxFails: 0,
|
||||
failBlockTimeout: 0,
|
||||
failBlockScopeAll: false,
|
||||
scope: "service"
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
options: options,
|
||||
isEditing: false,
|
||||
summary: ""
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"options.life": function (v) {
|
||||
let i = parseInt(v, 10)
|
||||
if (isNaN(i)) {
|
||||
i = 0
|
||||
}
|
||||
this.options.life = i
|
||||
this.updateSummary()
|
||||
},
|
||||
"options.maxFails": function (v) {
|
||||
let i = parseInt(v, 10)
|
||||
if (isNaN(i)) {
|
||||
i = 0
|
||||
}
|
||||
this.options.maxFails = i
|
||||
this.updateSummary()
|
||||
},
|
||||
"options.failBlockTimeout": function (v) {
|
||||
let i = parseInt(v, 10)
|
||||
if (isNaN(i)) {
|
||||
i = 0
|
||||
}
|
||||
this.options.failBlockTimeout = i
|
||||
this.updateSummary()
|
||||
},
|
||||
"options.failBlockScopeAll": function (v) {
|
||||
this.updateSummary()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
edit: function () {
|
||||
this.isEditing = !this.isEditing
|
||||
},
|
||||
updateSummary: function () {
|
||||
let summaryList = []
|
||||
if (this.options.life > 0) {
|
||||
summaryList.push("有效时间" + this.options.life + "秒")
|
||||
}
|
||||
if (this.options.maxFails > 0) {
|
||||
summaryList.push("最多失败" + this.options.maxFails + "次")
|
||||
}
|
||||
if (this.options.failBlockTimeout > 0) {
|
||||
summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒")
|
||||
}
|
||||
if (this.options.failBlockScopeAll) {
|
||||
summaryList.push("尝试全局封禁")
|
||||
}
|
||||
|
||||
if (summaryList.length == 0) {
|
||||
this.summary = "默认配置"
|
||||
} else {
|
||||
this.summary = summaryList.join(" / ")
|
||||
}
|
||||
},
|
||||
confirm: function () {
|
||||
this.isEditing = false
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="jsCookieOptionsJSON" :value="JSON.stringify(options)"/>
|
||||
<a href="" @click.prevent="edit">{{summary}} <i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i></a>
|
||||
<div v-show="isEditing" style="margin-top: 0.5em">
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">有效时间</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" style="width: 5em" maxlength="9" v-model="options.life" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
||||
<span class="ui label">秒</span>
|
||||
</div>
|
||||
<p class="comment">验证通过后在这个时间内不再验证,默认3600秒。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>最多失败次数</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" style="width: 5em" maxlength="9" v-model="options.maxFails" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
||||
<span class="ui label">次</span>
|
||||
</div>
|
||||
<p class="comment"><span v-if="options.maxFails > 0 && options.maxFails < 5" class="red">建议填入一个不小于5的数字,以减少误判几率。</span>允许用户失败尝试的最多次数,超过这个次数将被自动加入黑名单。如果为空或者为0,表示不限制。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>失败拦截时间</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" style="width: 5em" maxlength="9" v-model="options.failBlockTimeout" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
||||
<span class="ui label">秒</span>
|
||||
</div>
|
||||
<p class="comment">在达到最多失败次数(大于0)时,自动拦截的时长;如果为0表示不自动拦截。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>失败全局封禁</td>
|
||||
<td>
|
||||
<checkbox v-model="options.failBlockScopeAll"></checkbox>
|
||||
<p class="comment">选中后,表示允许系统尝试全局封禁某个IP,以提升封禁性能。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
Vue.component("http-firewall-page-options-viewer", {
|
||||
props: ["v-page-options"],
|
||||
data: function () {
|
||||
return {
|
||||
options: this.vPageOptions
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<span v-if="options == null">默认设置</span>
|
||||
<div v-else>
|
||||
状态码:{{options.status}} / 提示内容:<span v-if="options.body != null && options.body.length > 0">[{{options.body.length}}字符]</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
Vue.component("http-firewall-page-options", {
|
||||
props: ["v-page-options"],
|
||||
data: function () {
|
||||
var defaultPageBody = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>403 Forbidden</title>
|
||||
<style>
|
||||
address { line-height: 1.8; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>403 Forbidden By WAF</h1>
|
||||
<address>Connection: \${remoteAddr} (Client) -> \${serverAddr} (Server)</address>
|
||||
<address>Request ID: \${requestId}</address>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return {
|
||||
pageOptions: this.vPageOptions,
|
||||
status: this.vPageOptions.status,
|
||||
body: this.vPageOptions.body,
|
||||
defaultPageBody: defaultPageBody,
|
||||
isEditing: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
status: function (v) {
|
||||
if (typeof v === "string" && v.length != 3) {
|
||||
return
|
||||
}
|
||||
let statusCode = parseInt(v)
|
||||
if (isNaN(statusCode)) {
|
||||
this.pageOptions.status = 403
|
||||
} else {
|
||||
this.pageOptions.status = statusCode
|
||||
}
|
||||
},
|
||||
body: function (v) {
|
||||
this.pageOptions.body = v
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
edit: function () {
|
||||
this.isEditing = !this.isEditing
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="pageOptionsJSON" :value="JSON.stringify(pageOptions)"/>
|
||||
<a href="" @click.prevent="edit">状态码:{{status}} / 提示内容:<span v-if="pageOptions.body != null && pageOptions.body.length > 0">[{{pageOptions.body.length}}字符]</span><span v-else class="disabled">[无]</span>
|
||||
<i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i></a>
|
||||
<table class="ui table" v-show="isEditing">
|
||||
<tr>
|
||||
<td class="title">状态码 *</td>
|
||||
<td><input type="text" style="width: 4em" maxlength="3" v-model="status"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>网页内容</td>
|
||||
<td>
|
||||
<textarea v-model="body"></textarea>
|
||||
<p class="comment"><a href="" @click.prevent="body = defaultPageBody">[使用模板]</a> </p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
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: `<div>
|
||||
<input type="hidden" name="paramFiltersJSON" :value="JSON.stringify(filters)" />
|
||||
<div v-if="filters.length > 0">
|
||||
<div v-for="(filter, index) in filters" class="ui label small basic">
|
||||
{{filter.name}} <a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div v-if="isAdding">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown auto-width" v-model="addingCode">
|
||||
<option value="">[请选择]</option>
|
||||
<option v-for="option in options" :value="option.code">{{option.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny" type="button" @click.prevent="confirm()">确定</button>
|
||||
<a href="" @click.prevent="cancel()" title="取消"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isAdding">
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
<p class="comment">可以对参数值进行特定的编解码处理。</p>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
Vue.component("http-firewall-policy-selector", {
|
||||
props: ["v-http-firewall-policy"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
Tea.action("/servers/components/waf/count")
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
that.count = resp.data.count
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
let firewallPolicy = this.vHttpFirewallPolicy
|
||||
return {
|
||||
count: 0,
|
||||
firewallPolicy: firewallPolicy
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
remove: function () {
|
||||
this.firewallPolicy = null
|
||||
},
|
||||
select: function () {
|
||||
let that = this
|
||||
teaweb.popup("/servers/components/waf/selectPopup", {
|
||||
height: "26em",
|
||||
callback: function (resp) {
|
||||
that.firewallPolicy = resp.data.firewallPolicy
|
||||
}
|
||||
})
|
||||
},
|
||||
create: function () {
|
||||
let that = this
|
||||
teaweb.popup("/servers/components/waf/createPopup", {
|
||||
height: "26em",
|
||||
callback: function (resp) {
|
||||
that.firewallPolicy = resp.data.firewallPolicy
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div v-if="firewallPolicy != null" class="ui label basic">
|
||||
<input type="hidden" name="httpFirewallPolicyId" :value="firewallPolicy.id"/>
|
||||
{{firewallPolicy.name}} <a :href="'/servers/components/waf/policy?firewallPolicyId=' + firewallPolicy.id" target="_blank" title="修改"><i class="icon pencil small"></i></a> <a href="" @click.prevent="remove()" title="删除"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
<div v-if="firewallPolicy == null">
|
||||
<span v-if="count > 0"><a href="" @click.prevent="select">[选择已有策略]</a> </span><a href="" @click.prevent="create">[创建新策略]</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
Vue.component("http-firewall-province-selector", {
|
||||
props: ["v-type", "v-provinces"],
|
||||
data: function () {
|
||||
let provinces = this.vProvinces
|
||||
if (provinces == null) {
|
||||
provinces = []
|
||||
}
|
||||
|
||||
return {
|
||||
listType: this.vType,
|
||||
provinces: provinces
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addProvince: function () {
|
||||
let selectedProvinceIds = this.provinces.map(function (province) {
|
||||
return province.id
|
||||
})
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/waf/ipadmin/selectProvincesPopup?type=" + this.listType + "&selectedProvinceIds=" + selectedProvinceIds.join(","), {
|
||||
width: "50em",
|
||||
height: "26em",
|
||||
callback: function (resp) {
|
||||
that.provinces = resp.data.selectedProvinces
|
||||
that.$forceUpdate()
|
||||
that.notifyChange()
|
||||
}
|
||||
})
|
||||
},
|
||||
removeProvince: function (index) {
|
||||
this.provinces.$remove(index)
|
||||
this.notifyChange()
|
||||
},
|
||||
resetProvinces: function () {
|
||||
this.provinces = []
|
||||
this.notifyChange()
|
||||
},
|
||||
notifyChange: function () {
|
||||
this.$emit("change", {
|
||||
"provinces": this.provinces
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<span v-if="provinces.length == 0" class="disabled">暂时没有选择<span v-if="listType =='allow'">允许</span><span v-else>封禁</span>的省份。</span>
|
||||
<div v-show="provinces.length > 0">
|
||||
<div class="ui label tiny basic" v-for="(province, index) in provinces" style="margin-bottom: 0.5em">
|
||||
<input type="hidden" :name="listType + 'ProvinceIds'" :value="province.id"/>
|
||||
{{province.name}} <a href="" @click.prevent="removeProvince(index)" title="删除"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<button type="button" class="ui button tiny" @click.prevent="addProvince">修改</button> <button type="button" class="ui button tiny" v-show="provinces.length > 0" @click.prevent="resetProvinces">清空</button>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
Vue.component("http-firewall-region-selector", {
|
||||
props: ["v-type", "v-countries"],
|
||||
data: function () {
|
||||
let countries = this.vCountries
|
||||
if (countries == null) {
|
||||
countries = []
|
||||
}
|
||||
|
||||
return {
|
||||
listType: this.vType,
|
||||
countries: countries
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addCountry: function () {
|
||||
let selectedCountryIds = this.countries.map(function (country) {
|
||||
return country.id
|
||||
})
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/waf/ipadmin/selectCountriesPopup?type=" + this.listType + "&selectedCountryIds=" + selectedCountryIds.join(","), {
|
||||
width: "52em",
|
||||
height: "30em",
|
||||
callback: function (resp) {
|
||||
that.countries = resp.data.selectedCountries
|
||||
that.$forceUpdate()
|
||||
that.notifyChange()
|
||||
}
|
||||
})
|
||||
},
|
||||
removeCountry: function (index) {
|
||||
this.countries.$remove(index)
|
||||
this.notifyChange()
|
||||
},
|
||||
resetCountries: function () {
|
||||
this.countries = []
|
||||
this.notifyChange()
|
||||
},
|
||||
notifyChange: function () {
|
||||
this.$emit("change", {
|
||||
"countries": this.countries
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<span v-if="countries.length == 0" class="disabled">暂时没有选择<span v-if="listType =='allow'">允许</span><span v-else>封禁</span>的区域。</span>
|
||||
<div v-show="countries.length > 0">
|
||||
<div class="ui label tiny basic" v-for="(country, index) in countries" style="margin-bottom: 0.5em">
|
||||
<input type="hidden" :name="listType + 'CountryIds'" :value="country.id"/>
|
||||
({{country.letter}}){{country.name}} <a href="" @click.prevent="removeCountry(index)" title="删除"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<button type="button" class="ui button tiny" @click.prevent="addCountry">修改</button> <button type="button" class="ui button tiny" v-show="countries.length > 0" @click.prevent="resetCountries">清空</button>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,107 @@
|
||||
// 显示WAF规则的标签
|
||||
Vue.component("http-firewall-rule-label", {
|
||||
props: ["v-rule"],
|
||||
data: function () {
|
||||
return {
|
||||
rule: this.vRule
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showErr: function (err) {
|
||||
teaweb.popupTip("规则校验错误,请修正:<span class=\"red\">" + teaweb.encodeHTML(err) + "</span>")
|
||||
},
|
||||
calculateParamName: function (param) {
|
||||
let paramName = ""
|
||||
if (param != null) {
|
||||
window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) {
|
||||
if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) {
|
||||
paramName = checkpoint.name
|
||||
}
|
||||
})
|
||||
}
|
||||
return paramName
|
||||
},
|
||||
calculateParamDescription: function (param) {
|
||||
let paramName = ""
|
||||
let paramDescription = ""
|
||||
if (param != null) {
|
||||
window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) {
|
||||
if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) {
|
||||
paramName = checkpoint.name
|
||||
paramDescription = checkpoint.description
|
||||
}
|
||||
})
|
||||
}
|
||||
return paramName + ": " + paramDescription
|
||||
},
|
||||
operatorName: function (operatorCode) {
|
||||
let operatorName = operatorCode
|
||||
if (typeof (window.WAF_RULE_OPERATORS) != null) {
|
||||
window.WAF_RULE_OPERATORS.forEach(function (v) {
|
||||
if (v.code == operatorCode) {
|
||||
operatorName = v.name
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return operatorName
|
||||
},
|
||||
operatorDescription: function (operatorCode) {
|
||||
let operatorName = operatorCode
|
||||
let operatorDescription = ""
|
||||
if (typeof (window.WAF_RULE_OPERATORS) != null) {
|
||||
window.WAF_RULE_OPERATORS.forEach(function (v) {
|
||||
if (v.code == operatorCode) {
|
||||
operatorName = v.name
|
||||
operatorDescription = v.description
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return operatorName + ": " + operatorDescription
|
||||
},
|
||||
operatorDataType: function (operatorCode) {
|
||||
let operatorDataType = "none"
|
||||
if (typeof (window.WAF_RULE_OPERATORS) != null) {
|
||||
window.WAF_RULE_OPERATORS.forEach(function (v) {
|
||||
if (v.code == operatorCode) {
|
||||
operatorDataType = v.dataType
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return operatorDataType
|
||||
},
|
||||
isEmptyString: function (v) {
|
||||
return typeof v == "string" && v.length == 0
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div class="ui label small basic" style="line-height: 1.5">
|
||||
{{rule.name}} <span :title="calculateParamDescription(rule.param)" class="hover">{{calculateParamName(rule.param)}}<span class="small grey"> {{rule.param}}</span></span>
|
||||
|
||||
<!-- cc2 -->
|
||||
<span v-if="rule.param == '\${cc2}'">
|
||||
{{rule.checkpointOptions.period}}秒内请求数
|
||||
</span>
|
||||
|
||||
<!-- refererBlock -->
|
||||
<span v-if="rule.param == '\${refererBlock}'">
|
||||
<span v-if="rule.checkpointOptions.allowDomains != null && rule.checkpointOptions.allowDomains.length > 0">允许{{rule.checkpointOptions.allowDomains}}</span>
|
||||
<span v-if="rule.checkpointOptions.denyDomains != null && rule.checkpointOptions.denyDomains.length > 0">禁止{{rule.checkpointOptions.denyDomains}}</span>
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span>
|
||||
<span class="hover" :class="{dash:!rule.isComposed && rule.isCaseInsensitive}" :title="operatorDescription(rule.operator) + ((!rule.isComposed && rule.isCaseInsensitive) ? '\\n[大小写不敏感] ':'')"><{{operatorName(rule.operator)}}></span>
|
||||
<span class="hover" v-if="!isEmptyString(rule.value)">{{rule.value}}</span>
|
||||
<span v-else-if="operatorDataType(rule.operator) != 'none'" class="disabled" style="font-weight: normal" title="空字符串">[空]</span>
|
||||
</span>
|
||||
|
||||
<!-- description -->
|
||||
<span v-if="rule.description != null && rule.description.length > 0" class="grey small">({{rule.description}})</span>
|
||||
|
||||
<a href="" v-if="rule.err != null && rule.err.length > 0" @click.prevent="showErr(rule.err)" style="color: #db2828; opacity: 1; border-bottom: 1px #db2828 dashed; margin-left: 0.5em">规则错误</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,138 @@
|
||||
Vue.component("http-firewall-rules-box", {
|
||||
props: ["v-rules", "v-type"],
|
||||
data: function () {
|
||||
let rules = this.vRules
|
||||
if (rules == null) {
|
||||
rules = []
|
||||
}
|
||||
return {
|
||||
rules: rules
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addRule: function () {
|
||||
window.UPDATING_RULE = null
|
||||
let that = this
|
||||
teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, {
|
||||
height: "30em",
|
||||
callback: function (resp) {
|
||||
that.rules.push(resp.data.rule)
|
||||
}
|
||||
})
|
||||
},
|
||||
updateRule: function (index, rule) {
|
||||
window.UPDATING_RULE = teaweb.clone(rule)
|
||||
let that = this
|
||||
teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, {
|
||||
height: "30em",
|
||||
callback: function (resp) {
|
||||
Vue.set(that.rules, index, resp.data.rule)
|
||||
}
|
||||
})
|
||||
},
|
||||
removeRule: function (index) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要删除此规则吗?", function () {
|
||||
that.rules.$remove(index)
|
||||
})
|
||||
},
|
||||
operatorName: function (operatorCode) {
|
||||
let operatorName = operatorCode
|
||||
if (typeof (window.WAF_RULE_OPERATORS) != null) {
|
||||
window.WAF_RULE_OPERATORS.forEach(function (v) {
|
||||
if (v.code == operatorCode) {
|
||||
operatorName = v.name
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return operatorName
|
||||
},
|
||||
operatorDescription: function (operatorCode) {
|
||||
let operatorName = operatorCode
|
||||
let operatorDescription = ""
|
||||
if (typeof (window.WAF_RULE_OPERATORS) != null) {
|
||||
window.WAF_RULE_OPERATORS.forEach(function (v) {
|
||||
if (v.code == operatorCode) {
|
||||
operatorName = v.name
|
||||
operatorDescription = v.description
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return operatorName + ": " + operatorDescription
|
||||
},
|
||||
operatorDataType: function (operatorCode) {
|
||||
let operatorDataType = "none"
|
||||
if (typeof (window.WAF_RULE_OPERATORS) != null) {
|
||||
window.WAF_RULE_OPERATORS.forEach(function (v) {
|
||||
if (v.code == operatorCode) {
|
||||
operatorDataType = v.dataType
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return operatorDataType
|
||||
},
|
||||
calculateParamName: function (param) {
|
||||
let paramName = ""
|
||||
if (param != null) {
|
||||
window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) {
|
||||
if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) {
|
||||
paramName = checkpoint.name
|
||||
}
|
||||
})
|
||||
}
|
||||
return paramName
|
||||
},
|
||||
calculateParamDescription: function (param) {
|
||||
let paramName = ""
|
||||
let paramDescription = ""
|
||||
if (param != null) {
|
||||
window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) {
|
||||
if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) {
|
||||
paramName = checkpoint.name
|
||||
paramDescription = checkpoint.description
|
||||
}
|
||||
})
|
||||
}
|
||||
return paramName + ": " + paramDescription
|
||||
},
|
||||
isEmptyString: function (v) {
|
||||
return typeof v == "string" && v.length == 0
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="rulesJSON" :value="JSON.stringify(rules)"/>
|
||||
<div v-if="rules.length > 0">
|
||||
<div v-for="(rule, index) in rules" class="ui label small basic" style="margin-bottom: 0.5em; line-height: 1.5">
|
||||
{{rule.name}} <span :title="calculateParamDescription(rule.param)" class="hover">{{calculateParamName(rule.param)}}<span class="small grey"> {{rule.param}}</span></span>
|
||||
|
||||
<!-- cc2 -->
|
||||
<span v-if="rule.param == '\${cc2}'">
|
||||
{{rule.checkpointOptions.period}}秒内请求数
|
||||
</span>
|
||||
|
||||
<!-- refererBlock -->
|
||||
<span v-if="rule.param == '\${refererBlock}'">
|
||||
<span v-if="rule.checkpointOptions.allowDomains != null && rule.checkpointOptions.allowDomains.length > 0">允许{{rule.checkpointOptions.allowDomains}}</span>
|
||||
<span v-if="rule.checkpointOptions.denyDomains != null && rule.checkpointOptions.denyDomains.length > 0">禁止{{rule.checkpointOptions.denyDomains}}</span>
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span> <span class="hover" :title="operatorDescription(rule.operator) + ((!rule.isComposed && rule.isCaseInsensitive) ? '\\n[大小写不敏感] ':'')"><{{operatorName(rule.operator)}}></span>
|
||||
<span v-if="!isEmptyString(rule.value)" class="hover">{{rule.value}}</span>
|
||||
<span v-else-if="operatorDataType(rule.operator) != 'none'" class="disabled" style="font-weight: normal" title="空字符串">[空]</span>
|
||||
</span>
|
||||
|
||||
<!-- description -->
|
||||
<span v-if="rule.description != null && rule.description.length > 0" class="grey small">({{rule.description}})</span>
|
||||
|
||||
<a href="" title="修改" @click.prevent="updateRule(index, rule)"><i class="icon pencil small"></i></a>
|
||||
<a href="" title="删除" @click.prevent="removeRule(index)"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="addRule()">+</button>
|
||||
</div>`
|
||||
})
|
||||
391
EdgeAdmin/web/public/js/components/server/http-firewall-rules.js
Normal file
391
EdgeAdmin/web/public/js/components/server/http-firewall-rules.js
Normal file
@@ -0,0 +1,391 @@
|
||||
// 通用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: `<div>
|
||||
<table class="ui table">
|
||||
<tr>
|
||||
<td class="title">通用Header列表</td>
|
||||
<td>
|
||||
<values-box :values="headers" :placeholder="'Header'" @change="change"></values-box>
|
||||
<p class="comment">需要检查的Header列表。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Header值超出长度</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" name="" style="width: 5em" v-model="length" maxlength="6"/>
|
||||
<span class="ui label">字节</span>
|
||||
</div>
|
||||
<p class="comment">超出此长度认为匹配成功,0表示不限制。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// CC
|
||||
Vue.component("http-firewall-checkpoint-cc", {
|
||||
props: ["v-checkpoint"],
|
||||
data: function () {
|
||||
let keys = []
|
||||
let period = 60
|
||||
let threshold = 1000
|
||||
let ignoreCommonFiles = true
|
||||
let enableFingerprint = true
|
||||
|
||||
let options = {}
|
||||
if (window.parent.UPDATING_RULE != null) {
|
||||
options = window.parent.UPDATING_RULE.checkpointOptions
|
||||
}
|
||||
|
||||
if (options == null) {
|
||||
options = {}
|
||||
}
|
||||
if (options.keys != null) {
|
||||
keys = options.keys
|
||||
}
|
||||
if (keys.length == 0) {
|
||||
keys = ["${remoteAddr}", "${requestPath}"]
|
||||
}
|
||||
if (options.period != null) {
|
||||
period = options.period
|
||||
}
|
||||
if (options.threshold != null) {
|
||||
threshold = options.threshold
|
||||
}
|
||||
if (options.ignoreCommonFiles != null && typeof (options.ignoreCommonFiles) == "boolean") {
|
||||
ignoreCommonFiles = options.ignoreCommonFiles
|
||||
}
|
||||
if (options.enableFingerprint != null && typeof (options.enableFingerprint) == "boolean") {
|
||||
enableFingerprint = options.enableFingerprint
|
||||
}
|
||||
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.change()
|
||||
}, 100)
|
||||
|
||||
return {
|
||||
keys: keys,
|
||||
period: period,
|
||||
threshold: threshold,
|
||||
ignoreCommonFiles: ignoreCommonFiles,
|
||||
enableFingerprint: enableFingerprint,
|
||||
options: {},
|
||||
value: threshold
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
period: function () {
|
||||
this.change()
|
||||
},
|
||||
threshold: function () {
|
||||
this.change()
|
||||
},
|
||||
ignoreCommonFiles: function () {
|
||||
this.change()
|
||||
},
|
||||
enableFingerprint: function () {
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeKeys: function (keys) {
|
||||
this.keys = keys
|
||||
this.change()
|
||||
},
|
||||
change: function () {
|
||||
let period = parseInt(this.period.toString())
|
||||
if (isNaN(period) || period <= 0) {
|
||||
period = 60
|
||||
}
|
||||
|
||||
let threshold = parseInt(this.threshold.toString())
|
||||
if (isNaN(threshold) || threshold <= 0) {
|
||||
threshold = 1000
|
||||
}
|
||||
this.value = threshold
|
||||
|
||||
let ignoreCommonFiles = this.ignoreCommonFiles
|
||||
if (typeof ignoreCommonFiles != "boolean") {
|
||||
ignoreCommonFiles = false
|
||||
}
|
||||
|
||||
let enableFingerprint = this.enableFingerprint
|
||||
if (typeof enableFingerprint != "boolean") {
|
||||
enableFingerprint = true
|
||||
}
|
||||
|
||||
this.vCheckpoint.options = [
|
||||
{
|
||||
code: "keys",
|
||||
value: this.keys
|
||||
},
|
||||
{
|
||||
code: "period",
|
||||
value: period,
|
||||
},
|
||||
{
|
||||
code: "threshold",
|
||||
value: threshold
|
||||
},
|
||||
{
|
||||
code: "ignoreCommonFiles",
|
||||
value: ignoreCommonFiles
|
||||
},
|
||||
{
|
||||
code: "enableFingerprint",
|
||||
value: enableFingerprint
|
||||
}
|
||||
]
|
||||
},
|
||||
thresholdTooLow: function () {
|
||||
let threshold = parseInt(this.threshold.toString())
|
||||
if (isNaN(threshold) || threshold <= 0) {
|
||||
threshold = 1000
|
||||
}
|
||||
return threshold > 0 && threshold < 5
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="operator" value="gt"/>
|
||||
<input type="hidden" name="value" :value="value"/>
|
||||
<table class="ui table">
|
||||
<tr>
|
||||
<td class="title">统计对象组合 *</td>
|
||||
<td>
|
||||
<metric-keys-config-box :v-keys="keys" @change="changeKeys"></metric-keys-config-box>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>统计周期 *</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" v-model="period" style="width: 6em" maxlength="8"/>
|
||||
<span class="ui label">秒</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>阈值 *</td>
|
||||
<td>
|
||||
<input type="text" v-model="threshold" style="width: 6em" maxlength="8"/>
|
||||
<p class="comment" v-if="thresholdTooLow()"><span class="red">对于网站类应用来说,当前阈值设置的太低,有可能会影响用户正常访问。</span></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>检查请求来源指纹</td>
|
||||
<td>
|
||||
<checkbox v-model="enableFingerprint"></checkbox>
|
||||
<p class="comment">在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击;如果你在网站前面放置了别的反向代理服务,请取消此选项。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>忽略常用文件</td>
|
||||
<td>
|
||||
<checkbox v-model="ignoreCommonFiles"></checkbox>
|
||||
<p class="comment">忽略js、css、jpg等常在网页里被引用的文件名,即对这些文件的访问不加入计数,可以减少误判几率。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
|
||||
// 防盗链
|
||||
Vue.component("http-firewall-checkpoint-referer-block", {
|
||||
props: ["v-checkpoint"],
|
||||
data: function () {
|
||||
let allowEmpty = true
|
||||
let allowSameDomain = true
|
||||
let allowDomains = []
|
||||
let denyDomains = []
|
||||
let checkOrigin = true
|
||||
|
||||
let options = {}
|
||||
if (window.parent.UPDATING_RULE != null) {
|
||||
options = window.parent.UPDATING_RULE.checkpointOptions
|
||||
}
|
||||
|
||||
if (options == null) {
|
||||
options = {}
|
||||
}
|
||||
if (typeof (options.allowEmpty) == "boolean") {
|
||||
allowEmpty = options.allowEmpty
|
||||
}
|
||||
if (typeof (options.allowSameDomain) == "boolean") {
|
||||
allowSameDomain = options.allowSameDomain
|
||||
}
|
||||
if (options.allowDomains != null && typeof (options.allowDomains) == "object") {
|
||||
allowDomains = options.allowDomains
|
||||
}
|
||||
if (options.denyDomains != null && typeof (options.denyDomains) == "object") {
|
||||
denyDomains = options.denyDomains
|
||||
}
|
||||
if (typeof options.checkOrigin == "boolean") {
|
||||
checkOrigin = options.checkOrigin
|
||||
}
|
||||
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.change()
|
||||
}, 100)
|
||||
|
||||
return {
|
||||
allowEmpty: allowEmpty,
|
||||
allowSameDomain: allowSameDomain,
|
||||
allowDomains: allowDomains,
|
||||
denyDomains: denyDomains,
|
||||
checkOrigin: checkOrigin,
|
||||
options: {},
|
||||
value: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
allowEmpty: function () {
|
||||
this.change()
|
||||
},
|
||||
allowSameDomain: function () {
|
||||
this.change()
|
||||
},
|
||||
checkOrigin: function () {
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeAllowDomains: function (values) {
|
||||
this.allowDomains = values
|
||||
this.change()
|
||||
},
|
||||
changeDenyDomains: function (values) {
|
||||
this.denyDomains = values
|
||||
this.change()
|
||||
},
|
||||
change: function () {
|
||||
this.vCheckpoint.options = [
|
||||
{
|
||||
code: "allowEmpty",
|
||||
value: this.allowEmpty
|
||||
},
|
||||
{
|
||||
code: "allowSameDomain",
|
||||
value: this.allowSameDomain,
|
||||
},
|
||||
{
|
||||
code: "allowDomains",
|
||||
value: this.allowDomains
|
||||
},
|
||||
{
|
||||
code: "denyDomains",
|
||||
value: this.denyDomains
|
||||
},
|
||||
{
|
||||
code: "checkOrigin",
|
||||
value: this.checkOrigin
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="operator" value="eq"/>
|
||||
<input type="hidden" name="value" :value="value"/>
|
||||
<table class="ui table">
|
||||
<tr>
|
||||
<td class="title">来源域名允许为空</td>
|
||||
<td>
|
||||
<checkbox v-model="allowEmpty"></checkbox>
|
||||
<p class="comment">允许不带来源的访问。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>来源域名允许一致</td>
|
||||
<td>
|
||||
<checkbox v-model="allowSameDomain"></checkbox>
|
||||
<p class="comment">允许来源域名和当前访问的域名一致,相当于在站内访问。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>允许的来源域名</td>
|
||||
<td>
|
||||
<values-box :values="allowDomains" @change="changeAllowDomains"></values-box>
|
||||
<p class="comment">允许的来源域名列表,比如<code-label>example.com</code-label>(顶级域名)、<code-label>*.example.com</code-label>(example.com的所有二级域名)。单个星号<code-label>*</code-label>表示允许所有域名。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>禁止的来源域名</td>
|
||||
<td>
|
||||
<values-box :values="denyDomains" @change="changeDenyDomains"></values-box>
|
||||
<p class="comment">禁止的来源域名列表,比如<code-label>example.org</code-label>(顶级域名)、<code-label>*.example.org</code-label>(example.org的所有二级域名);除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>同时检查Origin</td>
|
||||
<td>
|
||||
<checkbox v-model="checkOrigin"></checkbox>
|
||||
<p class="comment">如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
80
EdgeAdmin/web/public/js/components/server/http-gzip-box.js
Normal file
80
EdgeAdmin/web/public/js/components/server/http-gzip-box.js
Normal file
@@ -0,0 +1,80 @@
|
||||
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: `<div>
|
||||
<input type="hidden" name="gzipRefJSON" :value="JSON.stringify(vGzipRef)"/>
|
||||
<table class="ui table selectable definition">
|
||||
<prior-checkbox :v-config="vGzipRef" v-if="vIsLocation"></prior-checkbox>
|
||||
<tbody v-show="!vIsLocation || vGzipRef.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用Gzip压缩</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="vGzipRef.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="isOn()">
|
||||
<tr>
|
||||
<td class="title">压缩级别</td>
|
||||
<td>
|
||||
<select class="dropdown auto-width" name="level" v-model="gzip.level">
|
||||
<option value="0">不压缩</option>
|
||||
<option v-for="i in 9" :value="i">{{i}}</option>
|
||||
</select>
|
||||
<p class="comment">级别越高,压缩比例越大。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<more-options-tbody @change="changeAdvancedVisible" v-if="isOn()"></more-options-tbody>
|
||||
<tbody v-show="isOn() && advancedVisible">
|
||||
<tr>
|
||||
<td>Gzip内容最小长度</td>
|
||||
<td>
|
||||
<size-capacity-box :v-name="'minLength'" :v-value="gzip.minLength" :v-unit="'kb'"></size-capacity-box>
|
||||
<p class="comment">0表示不限制,内容长度从文件尺寸或Content-Length中获取。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gzip内容最大长度</td>
|
||||
<td>
|
||||
<size-capacity-box :v-name="'maxLength'" :v-value="gzip.maxLength" :v-unit="'mb'"></size-capacity-box>
|
||||
<p class="comment">0表示不限制,内容长度从文件尺寸或Content-Length中获取。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>匹配条件</td>
|
||||
<td>
|
||||
<http-request-conds-box :v-conds="gzip.conds"></http-request-conds-box>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
Vue.component("http-header-assistant", {
|
||||
props: ["v-type", "v-value"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
Tea.action("/servers/headers/options?type=" + this.vType)
|
||||
.post()
|
||||
.success(function (resp) {
|
||||
that.allHeaders = resp.data.headers
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
allHeaders: [],
|
||||
matchedHeaders: [],
|
||||
|
||||
selectedHeaderName: ""
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
vValue: function (v) {
|
||||
if (v != this.selectedHeaderName) {
|
||||
this.selectedHeaderName = ""
|
||||
}
|
||||
|
||||
if (v.length == 0) {
|
||||
this.matchedHeaders = []
|
||||
return
|
||||
}
|
||||
this.matchedHeaders = this.allHeaders.filter(function (header) {
|
||||
return teaweb.match(header, v)
|
||||
}).slice(0, 10)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
select: function (header) {
|
||||
this.$emit("select", header)
|
||||
this.selectedHeaderName = header
|
||||
}
|
||||
},
|
||||
template: `<span v-if="selectedHeaderName.length == 0">
|
||||
<a href="" v-for="header in matchedHeaders" class="ui label basic tiny blue" style="font-weight: normal; margin-bottom: 0.3em" @click.prevent="select(header)">{{header}}</a>
|
||||
<span v-if="matchedHeaders.length > 0"> </span>
|
||||
</span>`
|
||||
})
|
||||
@@ -0,0 +1,337 @@
|
||||
Vue.component("http-header-policy-box", {
|
||||
props: ["v-request-header-policy", "v-request-header-ref", "v-response-header-policy", "v-response-header-ref", "v-params", "v-is-location", "v-is-group", "v-has-group-request-config", "v-has-group-response-config", "v-group-setting-url"],
|
||||
data: function () {
|
||||
let type = "response"
|
||||
let hash = window.location.hash
|
||||
if (hash == "#request") {
|
||||
type = "request"
|
||||
}
|
||||
|
||||
// ref
|
||||
let requestHeaderRef = this.vRequestHeaderRef
|
||||
if (requestHeaderRef == null) {
|
||||
requestHeaderRef = {
|
||||
isPrior: false,
|
||||
isOn: true,
|
||||
headerPolicyId: 0
|
||||
}
|
||||
}
|
||||
|
||||
let responseHeaderRef = this.vResponseHeaderRef
|
||||
if (responseHeaderRef == null) {
|
||||
responseHeaderRef = {
|
||||
isPrior: false,
|
||||
isOn: true,
|
||||
headerPolicyId: 0
|
||||
}
|
||||
}
|
||||
|
||||
// 请求相关
|
||||
let requestSettingHeaders = []
|
||||
let requestDeletingHeaders = []
|
||||
let requestNonStandardHeaders = []
|
||||
|
||||
let requestPolicy = this.vRequestHeaderPolicy
|
||||
if (requestPolicy != null) {
|
||||
if (requestPolicy.setHeaders != null) {
|
||||
requestSettingHeaders = requestPolicy.setHeaders
|
||||
}
|
||||
if (requestPolicy.deleteHeaders != null) {
|
||||
requestDeletingHeaders = requestPolicy.deleteHeaders
|
||||
}
|
||||
if (requestPolicy.nonStandardHeaders != null) {
|
||||
requestNonStandardHeaders = requestPolicy.nonStandardHeaders
|
||||
}
|
||||
}
|
||||
|
||||
// 响应相关
|
||||
let responseSettingHeaders = []
|
||||
let responseDeletingHeaders = []
|
||||
let responseNonStandardHeaders = []
|
||||
|
||||
let responsePolicy = this.vResponseHeaderPolicy
|
||||
if (responsePolicy != null) {
|
||||
if (responsePolicy.setHeaders != null) {
|
||||
responseSettingHeaders = responsePolicy.setHeaders
|
||||
}
|
||||
if (responsePolicy.deleteHeaders != null) {
|
||||
responseDeletingHeaders = responsePolicy.deleteHeaders
|
||||
}
|
||||
if (responsePolicy.nonStandardHeaders != null) {
|
||||
responseNonStandardHeaders = responsePolicy.nonStandardHeaders
|
||||
}
|
||||
}
|
||||
|
||||
let responseCORS = {
|
||||
isOn: false
|
||||
}
|
||||
if (responsePolicy.cors != null) {
|
||||
responseCORS = responsePolicy.cors
|
||||
}
|
||||
|
||||
return {
|
||||
type: type,
|
||||
typeName: (type == "request") ? "请求" : "响应",
|
||||
|
||||
requestHeaderRef: requestHeaderRef,
|
||||
responseHeaderRef: responseHeaderRef,
|
||||
requestSettingHeaders: requestSettingHeaders,
|
||||
requestDeletingHeaders: requestDeletingHeaders,
|
||||
requestNonStandardHeaders: requestNonStandardHeaders,
|
||||
|
||||
responseSettingHeaders: responseSettingHeaders,
|
||||
responseDeletingHeaders: responseDeletingHeaders,
|
||||
responseNonStandardHeaders: responseNonStandardHeaders,
|
||||
responseCORS: responseCORS
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectType: function (type) {
|
||||
this.type = type
|
||||
window.location.hash = "#" + type
|
||||
window.location.reload()
|
||||
},
|
||||
addSettingHeader: function (policyId) {
|
||||
teaweb.popup("/servers/server/settings/headers/createSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + this.type, {
|
||||
height: "22em",
|
||||
callback: function () {
|
||||
teaweb.successRefresh("保存成功")
|
||||
}
|
||||
})
|
||||
},
|
||||
addDeletingHeader: function (policyId, type) {
|
||||
teaweb.popup("/servers/server/settings/headers/createDeletePopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + type, {
|
||||
callback: function () {
|
||||
teaweb.successRefresh("保存成功")
|
||||
}
|
||||
})
|
||||
},
|
||||
addNonStandardHeader: function (policyId, type) {
|
||||
teaweb.popup("/servers/server/settings/headers/createNonStandardPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + type, {
|
||||
callback: function () {
|
||||
teaweb.successRefresh("保存成功")
|
||||
}
|
||||
})
|
||||
},
|
||||
updateSettingPopup: function (policyId, headerId) {
|
||||
teaweb.popup("/servers/server/settings/headers/updateSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&headerId=" + headerId + "&type=" + this.type, {
|
||||
height: "22em",
|
||||
callback: function () {
|
||||
teaweb.successRefresh("保存成功")
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteDeletingHeader: function (policyId, headerName) {
|
||||
teaweb.confirm("确定要删除'" + headerName + "'吗?", function () {
|
||||
Tea.action("/servers/server/settings/headers/deleteDeletingHeader")
|
||||
.params({
|
||||
headerPolicyId: policyId,
|
||||
headerName: headerName
|
||||
})
|
||||
.post()
|
||||
.refresh()
|
||||
})
|
||||
},
|
||||
deleteNonStandardHeader: function (policyId, headerName) {
|
||||
teaweb.confirm("确定要删除'" + headerName + "'吗?", function () {
|
||||
Tea.action("/servers/server/settings/headers/deleteNonStandardHeader")
|
||||
.params({
|
||||
headerPolicyId: policyId,
|
||||
headerName: headerName
|
||||
})
|
||||
.post()
|
||||
.refresh()
|
||||
})
|
||||
},
|
||||
deleteHeader: function (policyId, type, headerId) {
|
||||
teaweb.confirm("确定要删除此报头吗?", function () {
|
||||
this.$post("/servers/server/settings/headers/delete")
|
||||
.params({
|
||||
headerPolicyId: policyId,
|
||||
type: type,
|
||||
headerId: headerId
|
||||
})
|
||||
.refresh()
|
||||
}
|
||||
)
|
||||
},
|
||||
updateCORS: function (policyId) {
|
||||
teaweb.popup("/servers/server/settings/headers/updateCORSPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + this.type, {
|
||||
height: "30em",
|
||||
callback: function () {
|
||||
teaweb.successRefresh("保存成功")
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div class="ui menu tabular small">
|
||||
<a class="item" :class="{active:type == 'response'}" @click.prevent="selectType('response')">响应报头<span v-if="responseSettingHeaders.length > 0">({{responseSettingHeaders.length}})</span></a>
|
||||
<a class="item" :class="{active:type == 'request'}" @click.prevent="selectType('request')">请求报头<span v-if="requestSettingHeaders.length > 0">({{requestSettingHeaders.length}})</span></a>
|
||||
</div>
|
||||
|
||||
<div class="margin"></div>
|
||||
|
||||
<input type="hidden" name="type" :value="type"/>
|
||||
|
||||
<!-- 请求 -->
|
||||
<div v-if="(vIsLocation || vIsGroup) && type == 'request'">
|
||||
<input type="hidden" name="requestHeaderJSON" :value="JSON.stringify(requestHeaderRef)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="requestHeaderRef"></prior-checkbox>
|
||||
</table>
|
||||
<submit-btn></submit-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="((!vIsLocation && !vIsGroup) || requestHeaderRef.isPrior) && type == 'request'">
|
||||
<div v-if="vHasGroupRequestConfig">
|
||||
<div class="margin"></div>
|
||||
<warning-message>由于已经在当前<a :href="vGroupSettingUrl + '#request'">网站分组</a>中进行了对应的配置,在这里的配置将不会生效。</warning-message>
|
||||
</div>
|
||||
<div :class="{'opacity-mask': vHasGroupRequestConfig}">
|
||||
<h4>设置请求报头 <a href="" @click.prevent="addSettingHeader(vRequestHeaderPolicy.id)" style="font-size: 0.8em">[添加新报头]</a></h4>
|
||||
<p class="comment" v-if="requestSettingHeaders.length == 0">暂时还没有自定义报头。</p>
|
||||
<table class="ui table selectable celled" v-if="requestSettingHeaders.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>值</th>
|
||||
<th class="two op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-for="header in requestSettingHeaders">
|
||||
<tr>
|
||||
<td class="five wide">
|
||||
<a href="" @click.prevent="updateSettingPopup(vRequestHeaderPolicy.id, header.id)">{{header.name}} <i class="icon expand small"></i></a>
|
||||
<div>
|
||||
<span v-if="header.status != null && header.status.codes != null && !header.status.always"><grey-label v-for="code in header.status.codes" :key="code">{{code}}</grey-label></span>
|
||||
<span v-if="header.methods != null && header.methods.length > 0"><grey-label v-for="method in header.methods" :key="method">{{method}}</grey-label></span>
|
||||
<span v-if="header.domains != null && header.domains.length > 0"><grey-label v-for="domain in header.domains" :key="domain">{{domain}}</grey-label></span>
|
||||
<grey-label v-if="header.shouldAppend">附加</grey-label>
|
||||
<grey-label v-if="header.disableRedirect">跳转禁用</grey-label>
|
||||
<grey-label v-if="header.shouldReplace && header.replaceValues != null && header.replaceValues.length > 0">替换</grey-label>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{header.value}}</td>
|
||||
<td><a href="" @click.prevent="updateSettingPopup(vRequestHeaderPolicy.id, header.id)">修改</a> <a href="" @click.prevent="deleteHeader(vRequestHeaderPolicy.id, 'setHeader', header.id)">删除</a> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>其他设置</h4>
|
||||
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">删除报头 <tip-icon content="可以通过此功能删除转发到源站的请求报文中不需要的报头"></tip-icon></td>
|
||||
<td>
|
||||
<div v-if="requestDeletingHeaders.length > 0">
|
||||
<div class="ui label small basic" v-for="headerName in requestDeletingHeaders">{{headerName}} <a href=""><i class="icon remove" title="删除" @click.prevent="deleteDeletingHeader(vRequestHeaderPolicy.id, headerName)"></i></a> </div>
|
||||
<div class="ui divider" ></div>
|
||||
</div>
|
||||
<button class="ui button small" type="button" @click.prevent="addDeletingHeader(vRequestHeaderPolicy.id, 'request')">+</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">非标报头 <tip-icon content="可以通过此功能设置转发到源站的请求报文中非标准的报头,比如hello_world"></tip-icon></td>
|
||||
<td>
|
||||
<div v-if="requestNonStandardHeaders.length > 0">
|
||||
<div class="ui label small basic" v-for="headerName in requestNonStandardHeaders">{{headerName}} <a href=""><i class="icon remove" title="删除" @click.prevent="deleteNonStandardHeader(vRequestHeaderPolicy.id, headerName)"></i></a> </div>
|
||||
<div class="ui divider" ></div>
|
||||
</div>
|
||||
<button class="ui button small" type="button" @click.prevent="addNonStandardHeader(vRequestHeaderPolicy.id, 'request')">+</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 响应 -->
|
||||
<div v-if="(vIsLocation || vIsGroup) && type == 'response'">
|
||||
<input type="hidden" name="responseHeaderJSON" :value="JSON.stringify(responseHeaderRef)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="responseHeaderRef"></prior-checkbox>
|
||||
</table>
|
||||
<submit-btn></submit-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="((!vIsLocation && !vIsGroup) || responseHeaderRef.isPrior) && type == 'response'">
|
||||
<div v-if="vHasGroupResponseConfig">
|
||||
<div class="margin"></div>
|
||||
<warning-message>由于已经在当前<a :href="vGroupSettingUrl + '#response'">网站分组</a>中进行了对应的配置,在这里的配置将不会生效。</warning-message>
|
||||
</div>
|
||||
<div :class="{'opacity-mask': vHasGroupResponseConfig}">
|
||||
<h4>设置响应报头 <a href="" @click.prevent="addSettingHeader(vResponseHeaderPolicy.id)" style="font-size: 0.8em">[添加新报头]</a></h4>
|
||||
<p class="comment" style="margin-top: 0; padding-top: 0">将会覆盖已有的同名报头。</p>
|
||||
<p class="comment" v-if="responseSettingHeaders.length == 0">暂时还没有自定义报头。</p>
|
||||
<table class="ui table selectable celled" v-if="responseSettingHeaders.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>值</th>
|
||||
<th class="two op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-for="header in responseSettingHeaders">
|
||||
<tr>
|
||||
<td class="five wide">
|
||||
<a href="" @click.prevent="updateSettingPopup(vResponseHeaderPolicy.id, header.id)">{{header.name}} <i class="icon expand small"></i></a>
|
||||
<div>
|
||||
<span v-if="header.status != null && header.status.codes != null && !header.status.always"><grey-label v-for="code in header.status.codes" :key="code">{{code}}</grey-label></span>
|
||||
<span v-if="header.methods != null && header.methods.length > 0"><grey-label v-for="method in header.methods" :key="method">{{method}}</grey-label></span>
|
||||
<span v-if="header.domains != null && header.domains.length > 0"><grey-label v-for="domain in header.domains" :key="domain">{{domain}}</grey-label></span>
|
||||
<grey-label v-if="header.shouldAppend">附加</grey-label>
|
||||
<grey-label v-if="header.disableRedirect">跳转禁用</grey-label>
|
||||
<grey-label v-if="header.shouldReplace && header.replaceValues != null && header.replaceValues.length > 0">替换</grey-label>
|
||||
</div>
|
||||
|
||||
<!-- CORS -->
|
||||
<div v-if="header.name == 'Access-Control-Allow-Origin' && header.value == '*'">
|
||||
<span class="red small">建议使用当前页面下方的"CORS自适应跨域"功能代替Access-Control-*-*相关报头。</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{header.value}}</td>
|
||||
<td><a href="" @click.prevent="updateSettingPopup(vResponseHeaderPolicy.id, header.id)">修改</a> <a href="" @click.prevent="deleteHeader(vResponseHeaderPolicy.id, 'setHeader', header.id)">删除</a> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>其他设置</h4>
|
||||
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">删除报头 <tip-icon content="可以通过此功能删除响应报文中不需要的报头"></tip-icon></td>
|
||||
<td>
|
||||
<div v-if="responseDeletingHeaders.length > 0">
|
||||
<div class="ui label small basic" v-for="headerName in responseDeletingHeaders">{{headerName}} <a href=""><i class="icon remove small" title="删除" @click.prevent="deleteDeletingHeader(vResponseHeaderPolicy.id, headerName)"></i></a></div>
|
||||
<div class="ui divider" ></div>
|
||||
</div>
|
||||
<button class="ui button small" type="button" @click.prevent="addDeletingHeader(vResponseHeaderPolicy.id, 'response')">+</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>非标报头 <tip-icon content="可以通过此功能设置响应报文中非标准的报头,比如hello_world"></tip-icon></td>
|
||||
<td>
|
||||
<div v-if="responseNonStandardHeaders.length > 0">
|
||||
<div class="ui label small basic" v-for="headerName in responseNonStandardHeaders">{{headerName}} <a href=""><i class="icon remove small" title="删除" @click.prevent="deleteNonStandardHeader(vResponseHeaderPolicy.id, headerName)"></i></a></div>
|
||||
<div class="ui divider" ></div>
|
||||
</div>
|
||||
<button class="ui button small" type="button" @click.prevent="addNonStandardHeader(vResponseHeaderPolicy.id, 'response')">+</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">CORS自适应跨域</td>
|
||||
<td>
|
||||
<span v-if="responseCORS.isOn" class="green">已启用</span><span class="disabled" v-else="">未启用</span> <a href="" @click.prevent="updateCORS(vResponseHeaderPolicy.id)">[修改]</a>
|
||||
<p class="comment"><span v-if="!responseCORS.isOn">启用后,服务器可以</span><span v-else>服务器会</span>自动生成<code-label>Access-Control-*-*</code-label>相关的报头。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,77 @@
|
||||
Vue.component("http-header-replace-values", {
|
||||
props: ["v-replace-values"],
|
||||
data: function () {
|
||||
let values = this.vReplaceValues
|
||||
if (values == null) {
|
||||
values = []
|
||||
}
|
||||
return {
|
||||
values: values,
|
||||
isAdding: false,
|
||||
addingValue: {"pattern": "", "replacement": "", "isCaseInsensitive": false, "isRegexp": false}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
this.isAdding = true
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.pattern.focus()
|
||||
})
|
||||
},
|
||||
remove: function (index) {
|
||||
this.values.$remove(index)
|
||||
},
|
||||
confirm: function () {
|
||||
let that = this
|
||||
if (this.addingValue.pattern.length == 0) {
|
||||
teaweb.warn("替换前内容不能为空", function () {
|
||||
that.$refs.pattern.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.values.push(this.addingValue)
|
||||
this.cancel()
|
||||
},
|
||||
cancel: function () {
|
||||
this.isAdding = false
|
||||
this.addingValue = {"pattern": "", "replacement": "", "isCaseInsensitive": false, "isRegexp": false}
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="replaceValuesJSON" :value="JSON.stringify(values)"/>
|
||||
<div>
|
||||
<div v-for="(value, index) in values" class="ui label small" style="margin-bottom: 0.5em">
|
||||
<var>{{value.pattern}}</var><sup v-if="value.isCaseInsensitive" title="不区分大小写"><i class="icon info tiny"></i></sup> => <var v-if="value.replacement.length > 0">{{value.replacement}}</var><var v-else><span class="small grey">[空]</span></var>
|
||||
<a href="" @click.prevent="remove(index)" title="删除"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isAdding">
|
||||
<table class="ui table">
|
||||
<tr>
|
||||
<td class="title">替换前内容 *</td>
|
||||
<td><input type="text" v-model="addingValue.pattern" placeholder="替换前内容" ref="pattern" @keyup.enter="confirm()" @keypress.enter.prevent="1"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>替换后内容</td>
|
||||
<td><input type="text" v-model="addingValue.replacement" placeholder="替换后内容" @keyup.enter="confirm()" @keypress.enter.prevent="1"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>是否忽略大小写</td>
|
||||
<td>
|
||||
<checkbox v-model="addingValue.isCaseInsensitive"></checkbox>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div>
|
||||
<button type="button" class="ui button tiny" @click.prevent="confirm">确定</button>
|
||||
<a href="" title="取消" @click.prevent="cancel"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isAdding">
|
||||
<button type="button" class="ui button tiny" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,77 @@
|
||||
Vue.component("http-hls-config-box", {
|
||||
props: ["value", "v-is-location", "v-is-group"],
|
||||
data: function () {
|
||||
let config = this.value
|
||||
if (config == null) {
|
||||
config = {
|
||||
isPrior: false
|
||||
}
|
||||
}
|
||||
|
||||
let encryptingConfig = config.encrypting
|
||||
if (encryptingConfig == null) {
|
||||
encryptingConfig = {
|
||||
isOn: false,
|
||||
onlyURLPatterns: [],
|
||||
exceptURLPatterns: []
|
||||
}
|
||||
config.encrypting = encryptingConfig
|
||||
}
|
||||
|
||||
return {
|
||||
config: config,
|
||||
|
||||
encryptingConfig: encryptingConfig,
|
||||
encryptingMoreOptionsVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isOn: function () {
|
||||
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior)
|
||||
},
|
||||
|
||||
showEncryptingMoreOptions: function () {
|
||||
this.encryptingMoreOptionsVisible = !this.encryptingMoreOptionsVisible
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="hlsJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table definition selectable" v-show="vIsLocation || vIsGroup">
|
||||
<prior-checkbox :v-config="config" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
</table>
|
||||
|
||||
<table class="ui table definition selectable" v-show="isOn()">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">启用HLS加密配置</td>
|
||||
<td>
|
||||
<checkbox v-model="encryptingConfig.isOn"></checkbox>
|
||||
<p class="comment">启用后,系统会自动在<code-label>.m3u8</code-label>文件中加入<code-label>#EXT-X-KEY:METHOD=AES-128...</code-label>,并将其中的<code-label>.ts</code-label>文件内容进行加密。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="encryptingConfig.isOn">
|
||||
<tr>
|
||||
<td colspan="2"><more-options-indicator @change="showEncryptingMoreOptions"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="encryptingConfig.isOn && encryptingMoreOptionsVisible">
|
||||
<tr>
|
||||
<td>例外URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="encryptingConfig.exceptURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了例外URL,表示这些URL跳过不做处理。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>限制URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="encryptingConfig.onlyURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了限制URL,表示只对这些URL进行加密处理;如果不填则表示支持所有的URL。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,176 @@
|
||||
Vue.component("http-host-redirect-box", {
|
||||
props: ["v-redirects"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
sortTable(function (ids) {
|
||||
let newRedirects = []
|
||||
ids.forEach(function (id) {
|
||||
that.redirects.forEach(function (redirect) {
|
||||
if (redirect.id == id) {
|
||||
newRedirects.push(redirect)
|
||||
}
|
||||
})
|
||||
})
|
||||
that.updateRedirects(newRedirects)
|
||||
})
|
||||
},
|
||||
data: function () {
|
||||
let redirects = this.vRedirects
|
||||
if (redirects == null) {
|
||||
redirects = []
|
||||
}
|
||||
|
||||
let id = 0
|
||||
redirects.forEach(function (v) {
|
||||
id++
|
||||
v.id = id
|
||||
})
|
||||
|
||||
return {
|
||||
redirects: redirects,
|
||||
statusOptions: [
|
||||
{"code": 301, "text": "Moved Permanently"},
|
||||
{"code": 308, "text": "Permanent Redirect"},
|
||||
{"code": 302, "text": "Found"},
|
||||
{"code": 303, "text": "See Other"},
|
||||
{"code": 307, "text": "Temporary Redirect"}
|
||||
],
|
||||
id: id
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
let that = this
|
||||
window.UPDATING_REDIRECT = null
|
||||
|
||||
teaweb.popup("/servers/server/settings/redirects/createPopup", {
|
||||
width: "50em",
|
||||
height: "36em",
|
||||
callback: function (resp) {
|
||||
that.id++
|
||||
resp.data.redirect.id = that.id
|
||||
that.redirects.push(resp.data.redirect)
|
||||
that.change()
|
||||
}
|
||||
})
|
||||
},
|
||||
update: function (index, redirect) {
|
||||
let that = this
|
||||
window.UPDATING_REDIRECT = redirect
|
||||
|
||||
teaweb.popup("/servers/server/settings/redirects/createPopup", {
|
||||
width: "50em",
|
||||
height: "36em",
|
||||
callback: function (resp) {
|
||||
resp.data.redirect.id = redirect.id
|
||||
Vue.set(that.redirects, index, resp.data.redirect)
|
||||
that.change()
|
||||
}
|
||||
})
|
||||
},
|
||||
remove: function (index) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要删除这条跳转规则吗?", function () {
|
||||
that.redirects.$remove(index)
|
||||
that.change()
|
||||
})
|
||||
},
|
||||
change: function () {
|
||||
let that = this
|
||||
setTimeout(function (){
|
||||
that.$emit("change", that.redirects)
|
||||
}, 100)
|
||||
},
|
||||
updateRedirects: function (newRedirects) {
|
||||
this.redirects = newRedirects
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="hostRedirectsJSON" :value="JSON.stringify(redirects)"/>
|
||||
|
||||
<first-menu>
|
||||
<menu-item @click.prevent="add">[创建]</menu-item>
|
||||
</first-menu>
|
||||
<div class="margin"></div>
|
||||
|
||||
<p class="comment" v-if="redirects.length == 0">暂时还没有URL跳转规则。</p>
|
||||
<div v-show="redirects.length > 0">
|
||||
<table class="ui table celled selectable" id="sortable-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 1em"></th>
|
||||
<th>跳转前</th>
|
||||
<th style="width: 1em"></th>
|
||||
<th>跳转后</th>
|
||||
<th>HTTP状态码</th>
|
||||
<th class="two wide">状态</th>
|
||||
<th class="two op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-for="(redirect, index) in redirects" :key="redirect.id" :v-id="redirect.id">
|
||||
<tr>
|
||||
<td style="text-align: center;"><i class="icon bars handle grey"></i> </td>
|
||||
<td>
|
||||
<div v-if="redirect.type == '' || redirect.type == 'url'">
|
||||
{{redirect.beforeURL}}
|
||||
<div style="margin-top: 0.4em">
|
||||
<grey-label><strong>URL跳转</strong></grey-label>
|
||||
<grey-label v-if="redirect.matchPrefix">匹配前缀</grey-label>
|
||||
<grey-label v-if="redirect.matchRegexp">正则匹配</grey-label>
|
||||
<grey-label v-if="!redirect.matchPrefix && !redirect.matchRegexp">精准匹配</grey-label>
|
||||
<grey-label v-if="redirect.exceptDomains != null && redirect.exceptDomains.length > 0" v-for="domain in redirect.exceptDomains">排除:{{domain}}</grey-label>
|
||||
<grey-label v-if="redirect.onlyDomains != null && redirect.onlyDomains.length > 0" v-for="domain in redirect.onlyDomains">仅限:{{domain}}</grey-label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="redirect.type == 'domain'">
|
||||
<span v-if="redirect.domainsAll">所有域名</span>
|
||||
<span v-if="!redirect.domainsAll && redirect.domainsBefore != null">
|
||||
<span v-if="redirect.domainsBefore.length == 1">{{redirect.domainsBefore[0]}}</span>
|
||||
<span v-if="redirect.domainsBefore.length > 1">{{redirect.domainsBefore[0]}}等{{redirect.domainsBefore.length}}个域名</span>
|
||||
</span>
|
||||
<div style="margin-top: 0.4em">
|
||||
<grey-label><strong>域名跳转</strong></grey-label>
|
||||
<grey-label v-if="redirect.domainAfterScheme != null && redirect.domainAfterScheme.length > 0">{{redirect.domainAfterScheme}}</grey-label>
|
||||
<grey-label v-if="redirect.domainBeforeIgnorePorts">忽略端口</grey-label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="redirect.type == 'port'">
|
||||
<span v-if="redirect.portsAll">所有端口</span>
|
||||
<span v-if="!redirect.portsAll && redirect.portsBefore != null">
|
||||
<span v-if="redirect.portsBefore.length <= 5">{{redirect.portsBefore.join(", ")}}</span>
|
||||
<span v-if="redirect.portsBefore.length > 5">{{redirect.portsBefore.slice(0, 5).join(", ")}}等{{redirect.portsBefore.length}}个端口</span>
|
||||
</span>
|
||||
<div style="margin-top: 0.4em">
|
||||
<grey-label><strong>端口跳转</strong></grey-label>
|
||||
<grey-label v-if="redirect.portAfterScheme != null && redirect.portAfterScheme.length > 0">{{redirect.portAfterScheme}}</grey-label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 0.5em" v-if="redirect.conds != null && redirect.conds.groups != null && redirect.conds.groups.length > 0">
|
||||
<grey-label>匹配条件</grey-label>
|
||||
</div>
|
||||
</td>
|
||||
<td nowrap="">-></td>
|
||||
<td>
|
||||
<span v-if="redirect.type == '' || redirect.type == 'url'">{{redirect.afterURL}}</span>
|
||||
<span v-if="redirect.type == 'domain'">{{redirect.domainAfter}}</span>
|
||||
<span v-if="redirect.type == 'port'">{{redirect.portAfter}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="redirect.status > 0">{{redirect.status}}</span>
|
||||
<span v-else class="disabled">默认</span>
|
||||
</td>
|
||||
<td><label-on :v-is-on="redirect.isOn"></label-on></td>
|
||||
<td>
|
||||
<a href="" @click.prevent="update(index, redirect)">修改</a>
|
||||
<a href="" @click.prevent="remove(index)">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="comment" v-if="redirects.length > 1">所有规则匹配顺序为从上到下,可以拖动左侧的<i class="icon bars"></i>排序。</p>
|
||||
</div>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,106 @@
|
||||
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: ` <div class="labels-box">
|
||||
<!-- 基本信息 -->
|
||||
<http-location-labels-label v-if="location.name != null && location.name.length > 0" :class="'olive'" :href="url('/location')">{{location.name}}</http-location-labels-label>
|
||||
|
||||
<!-- domains -->
|
||||
<div v-if="location.domains != null && location.domains.length > 0">
|
||||
<grey-label v-for="domain in location.domains">{{domain}}</grey-label>
|
||||
</div>
|
||||
|
||||
<!-- break -->
|
||||
<http-location-labels-label v-if="location.isBreak" :href="url('/location')">BREAK</http-location-labels-label>
|
||||
|
||||
<!-- redirectToHTTPS -->
|
||||
<http-location-labels-label v-if="location.web != null && configIsOn(location.web.redirectToHTTPS)" :href="url('/http')">自动跳转HTTPS</http-location-labels-label>
|
||||
|
||||
<!-- Web -->
|
||||
<http-location-labels-label v-if="location.web != null && configIsOn(location.web.root)" :href="url('/web')">文档根目录</http-location-labels-label>
|
||||
|
||||
<!-- 反向代理 -->
|
||||
<http-location-labels-label v-if="refIsOn(location.reverseProxyRef, location.reverseProxy)" :v-href="url('/reverseProxy')">源站</http-location-labels-label>
|
||||
|
||||
<!-- UAM -->
|
||||
<http-location-labels-label v-if="location.web != null && location.web.uam != null && location.web.uam.isPrior"><span :class="{disabled: !location.web.uam.isOn, red:location.web.uam.isOn}">5秒盾</span></http-location-labels-label>
|
||||
|
||||
<!-- CC -->
|
||||
<http-location-labels-label v-if="location.web != null && location.web.cc != null && location.web.cc.isPrior"><span :class="{disabled: !location.web.cc.isOn, red:location.web.cc.isOn}">CC防护</span></http-location-labels-label>
|
||||
|
||||
<!-- WAF -->
|
||||
<!-- TODO -->
|
||||
|
||||
<!-- Cache -->
|
||||
<http-location-labels-label v-if="location.web != null && configIsOn(location.web.cache)" :v-href="url('/cache')">CACHE</http-location-labels-label>
|
||||
|
||||
<!-- Charset -->
|
||||
<http-location-labels-label v-if="location.web != null && configIsOn(location.web.charset) && location.web.charset.charset.length > 0" :href="url('/charset')">{{location.web.charset.charset}}</http-location-labels-label>
|
||||
|
||||
<!-- 访问日志 -->
|
||||
<!-- TODO -->
|
||||
|
||||
<!-- 统计 -->
|
||||
<!-- TODO -->
|
||||
|
||||
<!-- Gzip -->
|
||||
<http-location-labels-label v-if="location.web != null && refIsOn(location.web.gzipRef, location.web.gzip) && location.web.gzip.level > 0" :href="url('/gzip')">Gzip:{{location.web.gzip.level}}</http-location-labels-label>
|
||||
|
||||
<!-- HTTP Header -->
|
||||
<http-location-labels-label v-if="location.web != null && refIsOn(location.web.requestHeaderPolicyRef, location.web.requestHeaderPolicy) && (len(location.web.requestHeaderPolicy.addHeaders) > 0 || len(location.web.requestHeaderPolicy.setHeaders) > 0 || len(location.web.requestHeaderPolicy.replaceHeaders) > 0 || len(location.web.requestHeaderPolicy.deleteHeaders) > 0)" :href="url('/headers')">请求Header</http-location-labels-label>
|
||||
<http-location-labels-label v-if="location.web != null && refIsOn(location.web.responseHeaderPolicyRef, location.web.responseHeaderPolicy) && (len(location.web.responseHeaderPolicy.addHeaders) > 0 || len(location.web.responseHeaderPolicy.setHeaders) > 0 || len(location.web.responseHeaderPolicy.replaceHeaders) > 0 || len(location.web.responseHeaderPolicy.deleteHeaders) > 0)" :href="url('/headers')">响应Header</http-location-labels-label>
|
||||
|
||||
<!-- Websocket -->
|
||||
<http-location-labels-label v-if="location.web != null && refIsOn(location.web.websocketRef, location.web.websocket)" :href="url('/websocket')">Websocket</http-location-labels-label>
|
||||
|
||||
<!-- 请求脚本 -->
|
||||
<http-location-labels-label v-if="location.web != null && location.web.requestScripts != null && ((location.web.requestScripts.initGroup != null && location.web.requestScripts.initGroup.isPrior) || (location.web.requestScripts.requestGroup != null && location.web.requestScripts.requestGroup.isPrior))" :href="url('/requestScripts')">请求脚本</http-location-labels-label>
|
||||
|
||||
<!-- 自定义访客IP地址 -->
|
||||
<http-location-labels-label v-if="location.web != null && location.web.remoteAddr != null && location.web.remoteAddr.isPrior" :href="url('/remoteAddr')" :class="{disabled: !location.web.remoteAddr.isOn}">访客IP地址</http-location-labels-label>
|
||||
|
||||
<!-- 请求限制 -->
|
||||
<http-location-labels-label v-if="location.web != null && location.web.requestLimit != null && location.web.requestLimit.isPrior" :href="url('/requestLimit')" :class="{disabled: !location.web.requestLimit.isOn}">请求限制</http-location-labels-label>
|
||||
|
||||
<!-- 自定义页面 -->
|
||||
<div v-if="location.web != null && location.web.pages != null && location.web.pages.length > 0">
|
||||
<div v-for="page in location.web.pages" :key="page.id"><http-location-labels-label :href="url('/pages')">PAGE [状态码{{page.status[0]}}] -> {{page.url}}</http-location-labels-label></div>
|
||||
</div>
|
||||
<div v-if="location.web != null && configIsOn(location.web.shutdown)">
|
||||
<http-location-labels-label :v-class="'red'" :href="url('/pages')">临时关闭</http-location-labels-label>
|
||||
</div>
|
||||
|
||||
<!-- 重写规则 -->
|
||||
<div v-if="location.web != null && location.web.rewriteRules != null && location.web.rewriteRules.length > 0">
|
||||
<div v-for="rewriteRule in location.web.rewriteRules">
|
||||
<http-location-labels-label :href="url('/rewrite')">REWRITE {{rewriteRule.pattern}} -> {{rewriteRule.replace}}</http-location-labels-label>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
|
||||
Vue.component("http-location-labels-label", {
|
||||
props: ["v-class", "v-href"],
|
||||
template: `<a :href="vHref" class="ui label tiny basic" :class="vClass" style="font-size:0.7em;padding:4px;margin-top:0.3em;margin-bottom:0.3em"><slot></slot></a>`
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
// 请求方法列表
|
||||
Vue.component("http-methods-box", {
|
||||
props: ["v-methods"],
|
||||
data: function () {
|
||||
let methods = this.vMethods
|
||||
if (methods == null) {
|
||||
methods = []
|
||||
}
|
||||
return {
|
||||
methods: methods,
|
||||
isAdding: false,
|
||||
addingMethod: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
this.isAdding = true
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.addingMethod.focus()
|
||||
}, 100)
|
||||
},
|
||||
confirm: function () {
|
||||
let that = this
|
||||
|
||||
// 删除其中的空格
|
||||
this.addingMethod = this.addingMethod.replace(/\s/g, "").toUpperCase()
|
||||
|
||||
if (this.addingMethod.length == 0) {
|
||||
teaweb.warn("请输入要添加的请求方法", function () {
|
||||
that.$refs.addingMethod.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 是否已经存在
|
||||
if (this.methods.$contains(this.addingMethod)) {
|
||||
teaweb.warn("此请求方法已经存在,无需重复添加", function () {
|
||||
that.$refs.addingMethod.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.methods.push(this.addingMethod)
|
||||
this.cancel()
|
||||
},
|
||||
remove: function (index) {
|
||||
this.methods.$remove(index)
|
||||
},
|
||||
cancel: function () {
|
||||
this.isAdding = false
|
||||
this.addingMethod = ""
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="methodsJSON" :value="JSON.stringify(methods)"/>
|
||||
<div v-if="methods.length > 0">
|
||||
<span class="ui label small basic" v-for="(method, index) in methods">
|
||||
{{method}}
|
||||
<a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
|
||||
</span>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div v-if="isAdding">
|
||||
<div class="ui fields">
|
||||
<div class="ui field">
|
||||
<input type="text" v-model="addingMethod" @keyup.enter="confirm()" @keypress.enter.prevent="1" ref="addingMethod" placeholder="如GET" size="10"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny" type="button" @click.prevent="confirm">确定</button>
|
||||
<a href="" title="取消" @click.prevent="cancel"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment">格式为大写,比如<code-label>GET</code-label>、<code-label>POST</code-label>等。</p>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div style="margin-top: 0.5em" v-if="!isAdding">
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,132 @@
|
||||
// 压缩配置
|
||||
Vue.component("http-optimization-config-box", {
|
||||
props: ["v-optimization-config", "v-is-location", "v-is-group"],
|
||||
data: function () {
|
||||
let config = this.vOptimizationConfig
|
||||
|
||||
return {
|
||||
config: config,
|
||||
htmlMoreOptions: false,
|
||||
javascriptMoreOptions: false,
|
||||
cssMoreOptions: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isOn: function () {
|
||||
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="optimizationJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table definition selectable" v-if="vIsLocation || vIsGroup">
|
||||
<prior-checkbox :v-config="config"></prior-checkbox>
|
||||
</table>
|
||||
|
||||
<div v-show="(!vIsLocation && !vIsGroup) || config.isPrior">
|
||||
<div class="margin"></div>
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">HTML优化</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.html.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">可以自动优化HTML中包含的空白、注释、空标签等。只有文件可以缓存时才会被优化。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="config.html.isOn">
|
||||
<td colspan="2"><more-options-indicator v-model="htmlMoreOptions"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="htmlMoreOptions">
|
||||
<tr>
|
||||
<td>HTML例外URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.html.exceptURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了例外URL,表示这些URL跳过不做处理。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>HTML限制URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.html.onlyURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">Javascript优化</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.javascript.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">可以自动缩短Javascript中变量、函数名称等。只有文件可以缓存时才会被优化。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="config.javascript.isOn">
|
||||
<td colspan="2"><more-options-indicator v-model="javascriptMoreOptions"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="javascriptMoreOptions">
|
||||
<tr>
|
||||
<td>Javascript例外URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.javascript.exceptURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了例外URL,表示这些URL跳过不做处理。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Javascript限制URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.javascript.onlyURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">CSS优化</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.css.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">可以自动去除CSS中包含的空白。只有文件可以缓存时才会被优化。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="config.css.isOn">
|
||||
<td colspan="2"><more-options-indicator v-model="cssMoreOptions"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="cssMoreOptions">
|
||||
<tr>
|
||||
<td>CSS例外URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.css.exceptURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了例外URL,表示这些URL跳过不做处理。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CSS限制URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.css.onlyURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了限制URL,表示只对这些URL进行优化处理;如果不填则表示支持所有的URL。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
Vue.component("http-oss-bucket-params", {
|
||||
props: ["v-oss-config", "v-params", "name"],
|
||||
data: function () {
|
||||
let params = this.vParams
|
||||
if (params == null) {
|
||||
params = []
|
||||
}
|
||||
|
||||
let ossConfig = this.vOssConfig
|
||||
if (ossConfig == null) {
|
||||
ossConfig = {
|
||||
bucketParam: "input",
|
||||
bucketName: "",
|
||||
bucketArgName: ""
|
||||
}
|
||||
} else {
|
||||
// 兼容以往
|
||||
if (ossConfig.bucketParam != null && ossConfig.bucketParam.length == 0) {
|
||||
ossConfig.bucketParam = "input"
|
||||
}
|
||||
if (ossConfig.options != null && ossConfig.options.bucketName != null && ossConfig.options.bucketName.length > 0) {
|
||||
ossConfig.bucketName = ossConfig.options.bucketName
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
params: params,
|
||||
ossConfig: ossConfig
|
||||
}
|
||||
},
|
||||
template: `<tbody>
|
||||
<tr>
|
||||
<td>{{name}}名称获取方式 *</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" name="bucketParam" v-model="ossConfig.bucketParam">
|
||||
<option v-for="param in params" :value="param.code" v-if="param.example.length == 0">{{param.name.replace("\${optionName}", name)}}</option>
|
||||
<option v-for="param in params" :value="param.code" v-if="param.example.length > 0">{{param.name}} - {{param.example}}</option>
|
||||
</select>
|
||||
<p class="comment" v-for="param in params" v-if="param.code == ossConfig.bucketParam">{{param.description.replace("\${optionName}", name)}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="ossConfig.bucketParam == 'input'">
|
||||
<td>{{name}}名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="bucketName" maxlength="100" v-model="ossConfig.bucketName"/>
|
||||
<p class="comment">{{name}}名称,类似于<code-label>bucket-12345678</code-label>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="ossConfig.bucketParam == 'arg'">
|
||||
<td>{{name}}参数名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="bucketArgName" maxlength="100" v-model="ossConfig.bucketArgName"/>
|
||||
<p class="comment">{{name}}参数名称,比如<code-label>?myBucketName=BUCKET-NAME</code-label>中的<code-label>myBucketName</code-label>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>`
|
||||
})
|
||||
@@ -0,0 +1,262 @@
|
||||
Vue.component("http-pages-and-shutdown-box", {
|
||||
props: ["v-enable-global-pages", "v-pages", "v-shutdown-config", "v-is-location"],
|
||||
data: function () {
|
||||
let pages = []
|
||||
if (this.vPages != null) {
|
||||
pages = this.vPages
|
||||
}
|
||||
let shutdownConfig = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
bodyType: "html",
|
||||
url: "",
|
||||
body: "",
|
||||
status: 0
|
||||
}
|
||||
if (this.vShutdownConfig != null) {
|
||||
if (this.vShutdownConfig.body == null) {
|
||||
this.vShutdownConfig.body = ""
|
||||
}
|
||||
if (this.vShutdownConfig.bodyType == null) {
|
||||
this.vShutdownConfig.bodyType = "html"
|
||||
}
|
||||
shutdownConfig = this.vShutdownConfig
|
||||
}
|
||||
|
||||
let shutdownStatus = ""
|
||||
if (shutdownConfig.status > 0) {
|
||||
shutdownStatus = shutdownConfig.status.toString()
|
||||
}
|
||||
|
||||
return {
|
||||
pages: pages,
|
||||
shutdownConfig: shutdownConfig,
|
||||
shutdownStatus: shutdownStatus,
|
||||
enableGlobalPages: this.vEnableGlobalPages
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
shutdownStatus: function (status) {
|
||||
let statusInt = parseInt(status)
|
||||
if (!isNaN(statusInt) && statusInt > 0 && statusInt < 1000) {
|
||||
this.shutdownConfig.status = statusInt
|
||||
} else {
|
||||
this.shutdownConfig.status = 0
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addPage: function () {
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/pages/createPopup", {
|
||||
height: "30em",
|
||||
callback: function (resp) {
|
||||
that.pages.push(resp.data.page)
|
||||
that.notifyChange()
|
||||
}
|
||||
})
|
||||
},
|
||||
updatePage: function (pageIndex, pageId) {
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/pages/updatePopup?pageId=" + pageId, {
|
||||
height: "30em",
|
||||
callback: function (resp) {
|
||||
Vue.set(that.pages, pageIndex, resp.data.page)
|
||||
that.notifyChange()
|
||||
}
|
||||
})
|
||||
},
|
||||
removePage: function (pageIndex) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要删除此自定义页面吗?", function () {
|
||||
that.pages.$remove(pageIndex)
|
||||
that.notifyChange()
|
||||
})
|
||||
},
|
||||
addShutdownHTMLTemplate: function () {
|
||||
this.shutdownConfig.body = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
\t<title>升级中</title>
|
||||
\t<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
\t<style>
|
||||
\t\taddress { line-height: 1.8; }
|
||||
\t</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>网站升级中</h1>
|
||||
<p>为了给您提供更好的服务,我们正在升级网站,请稍后重新访问。</p>
|
||||
|
||||
<address>Connection: \${remoteAddr} (Client) -> \${serverAddr} (Server)</address>
|
||||
<address>Request ID: \${requestId}</address>
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
},
|
||||
notifyChange: function () {
|
||||
let parent = this.$el.parentNode
|
||||
while (true) {
|
||||
if (parent == null) {
|
||||
break
|
||||
}
|
||||
if (parent.tagName == "FORM") {
|
||||
break
|
||||
}
|
||||
parent = parent.parentNode
|
||||
}
|
||||
if (parent != null) {
|
||||
setTimeout(function () {
|
||||
Tea.runActionOn(parent)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="pagesJSON" :value="JSON.stringify(pages)"/>
|
||||
<input type="hidden" name="shutdownJSON" :value="JSON.stringify(shutdownConfig)"/>
|
||||
|
||||
<h4 style="margin-bottom: 0.5em">自定义页面</h4>
|
||||
|
||||
<p class="comment" style="padding-top: 0; margin-top: 0">根据响应状态码返回一些自定义页面,比如404,500等错误页面。</p>
|
||||
|
||||
<div v-if="pages.length > 0">
|
||||
<table class="ui table selectable celled">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="two wide">响应状态码</th>
|
||||
<th>页面类型</th>
|
||||
<th class="two wide">新状态码</th>
|
||||
<th>例外URL</th>
|
||||
<th>限制URL</th>
|
||||
<th class="two op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr v-for="(page,index) in pages">
|
||||
<td>
|
||||
<a href="" @click.prevent="updatePage(index, page.id)">
|
||||
<span v-if="page.status != null && page.status.length == 1">{{page.status[0]}}</span>
|
||||
<span v-else>{{page.status}}</span>
|
||||
|
||||
<i class="icon expand small"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td style="word-break: break-all">
|
||||
<div v-if="page.bodyType == 'url'">
|
||||
{{page.url}}
|
||||
<div>
|
||||
<grey-label>读取URL</grey-label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="page.bodyType == 'redirectURL'">
|
||||
{{page.url}}
|
||||
<div>
|
||||
<grey-label>跳转URL</grey-label>
|
||||
<grey-label v-if="page.newStatus > 0">{{page.newStatus}}</grey-label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="page.bodyType == 'html'">
|
||||
[HTML内容]
|
||||
<div>
|
||||
<grey-label v-if="page.newStatus > 0">{{page.newStatus}}</grey-label>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="page.newStatus > 0">{{page.newStatus}}</span>
|
||||
<span v-else class="disabled">保持</span>
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="page.exceptURLPatterns != null && page.exceptURLPatterns">
|
||||
<span v-for="urlPattern in page.exceptURLPatterns" class="ui basic label small">{{urlPattern.pattern}}</span>
|
||||
</div>
|
||||
<span v-else class="disabled">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="page.onlyURLPatterns != null && page.onlyURLPatterns">
|
||||
<span v-for="urlPattern in page.onlyURLPatterns" class="ui basic label small">{{urlPattern.pattern}}</span>
|
||||
</div>
|
||||
<span v-else class="disabled">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="" title="修改" @click.prevent="updatePage(index, page.id)">修改</a>
|
||||
<a href="" title="删除" @click.prevent="removePage(index)">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin-top: 1em">
|
||||
<button class="ui button small" type="button" @click.prevent="addPage()">+添加自定义页面</button>
|
||||
</div>
|
||||
|
||||
<h4 style="margin-top: 2em;">临时关闭页面</h4>
|
||||
<p class="comment" style="margin-top: 0; padding-top: 0">开启临时关闭页面时,所有请求都会直接显示此页面。可用于临时升级网站或者禁止用户访问某个网页。</p>
|
||||
<div>
|
||||
<table class="ui table selectable definition">
|
||||
<prior-checkbox :v-config="shutdownConfig" v-if="vIsLocation"></prior-checkbox>
|
||||
<tbody v-show="!vIsLocation || shutdownConfig.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用临时关闭网站</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="shutdownConfig.isOn" />
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中后,表示临时关闭当前网站,并显示自定义内容。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="(!vIsLocation || shutdownConfig.isPrior) && shutdownConfig.isOn">
|
||||
<tr>
|
||||
<td>显示内容类型 *</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="shutdownConfig.bodyType">
|
||||
<option value="html">HTML</option>
|
||||
<option value="url">读取URL</option>
|
||||
<option value="redirectURL">跳转URL</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="shutdownConfig.bodyType == 'url'">
|
||||
<td class="title">显示页面URL *</td>
|
||||
<td>
|
||||
<input type="text" v-model="shutdownConfig.url" placeholder="类似于 https://example.com/page.html"/>
|
||||
<p class="comment">将从此URL中读取内容。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="shutdownConfig.bodyType == 'redirectURL'">
|
||||
<td class="title">跳转到URL *</td>
|
||||
<td>
|
||||
<input type="text" v-model="shutdownConfig.url" placeholder="类似于 https://example.com/page.html"/>
|
||||
<p class="comment">将会跳转到此URL。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="shutdownConfig.bodyType == 'html'">
|
||||
<td>显示页面HTML *</td>
|
||||
<td>
|
||||
<textarea name="body" ref="shutdownHTMLBody" v-model="shutdownConfig.body"></textarea>
|
||||
<p class="comment"><a href="" @click.prevent="addShutdownHTMLTemplate">[使用模板]</a>。填写页面的HTML内容,支持请求变量。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>状态码</td>
|
||||
<td><input type="text" size="3" maxlength="3" name="shutdownStatus" style="width:5.2em" placeholder="状态码" v-model="shutdownStatus"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h4 style="margin-top: 2em;">其他设置</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">启用系统自定义页面</td>
|
||||
<td>
|
||||
<checkbox name="enableGlobalPages" v-model="enableGlobalPages"></checkbox>
|
||||
<p class="comment">选中后,表示如果当前网站没有自定义页面,则尝试使用系统对应的自定义页面。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="ui margin"></div>
|
||||
|
||||
</div>`
|
||||
})
|
||||
132
EdgeAdmin/web/public/js/components/server/http-pages-box.js
Normal file
132
EdgeAdmin/web/public/js/components/server/http-pages-box.js
Normal file
@@ -0,0 +1,132 @@
|
||||
Vue.component("http-pages-box", {
|
||||
props: ["v-pages"],
|
||||
data: function () {
|
||||
let pages = []
|
||||
if (this.vPages != null) {
|
||||
pages = this.vPages
|
||||
}
|
||||
|
||||
return {
|
||||
pages: pages
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addPage: function () {
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/pages/createPopup", {
|
||||
height: "26em",
|
||||
callback: function (resp) {
|
||||
that.pages.push(resp.data.page)
|
||||
that.notifyChange()
|
||||
}
|
||||
})
|
||||
},
|
||||
updatePage: function (pageIndex, pageId) {
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/pages/updatePopup?pageId=" + pageId, {
|
||||
height: "26em",
|
||||
callback: function (resp) {
|
||||
Vue.set(that.pages, pageIndex, resp.data.page)
|
||||
that.notifyChange()
|
||||
}
|
||||
})
|
||||
},
|
||||
removePage: function (pageIndex) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要移除此页面吗?", function () {
|
||||
that.pages.$remove(pageIndex)
|
||||
that.notifyChange()
|
||||
})
|
||||
},
|
||||
notifyChange: function () {
|
||||
let parent = this.$el.parentNode
|
||||
while (true) {
|
||||
if (parent == null) {
|
||||
break
|
||||
}
|
||||
if (parent.tagName == "FORM") {
|
||||
break
|
||||
}
|
||||
parent = parent.parentNode
|
||||
}
|
||||
if (parent != null) {
|
||||
setTimeout(function () {
|
||||
Tea.runActionOn(parent)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="pagesJSON" :value="JSON.stringify(pages)"/>
|
||||
|
||||
<div v-if="pages.length > 0">
|
||||
<table class="ui table selectable celled">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="two wide">响应状态码</th>
|
||||
<th>页面类型</th>
|
||||
<th class="two wide">新状态码</th>
|
||||
<th>例外URL</th>
|
||||
<th>限制URL</th>
|
||||
<th class="two op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr v-for="(page,index) in pages">
|
||||
<td>
|
||||
<a href="" @click.prevent="updatePage(index, page.id)">
|
||||
<span v-if="page.status != null && page.status.length == 1">{{page.status[0]}}</span>
|
||||
<span v-else>{{page.status}}</span>
|
||||
|
||||
<i class="icon expand small"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td style="word-break: break-all">
|
||||
<div v-if="page.bodyType == 'url'">
|
||||
{{page.url}}
|
||||
<div>
|
||||
<grey-label>读取URL</grey-label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="page.bodyType == 'redirectURL'">
|
||||
{{page.url}}
|
||||
<div>
|
||||
<grey-label>跳转URL</grey-label>
|
||||
<grey-label v-if="page.newStatus > 0">{{page.newStatus}}</grey-label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="page.bodyType == 'html'">
|
||||
[HTML内容]
|
||||
<div>
|
||||
<grey-label v-if="page.newStatus > 0">{{page.newStatus}}</grey-label>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="page.newStatus > 0">{{page.newStatus}}</span>
|
||||
<span v-else class="disabled">保持</span>
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="page.exceptURLPatterns != null && page.exceptURLPatterns">
|
||||
<span v-for="urlPattern in page.exceptURLPatterns" class="ui basic label small">{{urlPattern.pattern}}</span>
|
||||
</div>
|
||||
<span v-else class="disabled">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="page.onlyURLPatterns != null && page.onlyURLPatterns">
|
||||
<span v-for="urlPattern in page.onlyURLPatterns" class="ui basic label small">{{urlPattern.pattern}}</span>
|
||||
</div>
|
||||
<span v-else class="disabled">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="" title="修改" @click.prevent="updatePage(index, page.id)">修改</a>
|
||||
<a href="" title="删除" @click.prevent="removePage(index)">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin-top: 1em">
|
||||
<button class="ui button small" type="button" @click.prevent="addPage()">+添加自定义页面</button>
|
||||
</div>
|
||||
<div class="ui margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,160 @@
|
||||
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: `<div>
|
||||
<input type="hidden" name="redirectToHTTPSJSON" :value="JSON.stringify(redirectToHttpsConfig)"/>
|
||||
|
||||
<!-- Location -->
|
||||
<table class="ui table selectable definition" v-if="vIsLocation">
|
||||
<prior-checkbox :v-config="redirectToHttpsConfig"></prior-checkbox>
|
||||
<tbody v-show="redirectToHttpsConfig.isPrior">
|
||||
<tr>
|
||||
<td class="title">自动跳转到HTTPS</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="redirectToHttpsConfig.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">开启后,所有HTTP的请求都会自动跳转到对应的HTTPS URL上,<more-options-angle @change="changeMoreOptions"></more-options-angle></p>
|
||||
|
||||
<!-- TODO 如果已经设置了特殊设置,需要在界面上显示 -->
|
||||
<table class="ui table" v-show="moreOptionsVisible">
|
||||
<tr>
|
||||
<td class="title">状态码</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="redirectToHttpsConfig.status">
|
||||
<option value="0">[使用默认]</option>
|
||||
<option v-for="option in statusOptions" :value="option.code">{{option.code}} {{option.text}}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>域名或IP地址</td>
|
||||
<td>
|
||||
<input type="text" name="host" v-model="redirectToHttpsConfig.host"/>
|
||||
<p class="comment">默认和用户正在访问的域名或IP地址一致。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>端口</td>
|
||||
<td>
|
||||
<input type="text" name="port" v-model="portString" maxlength="5" style="width:6em"/>
|
||||
<p class="comment">默认端口为443。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 非Location -->
|
||||
<div v-if="!vIsLocation">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="redirectToHttpsConfig.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">开启后,所有HTTP的请求都会自动跳转到对应的HTTPS URL上,<more-options-angle @change="changeMoreOptions"></more-options-angle></p>
|
||||
|
||||
<!-- TODO 如果已经设置了特殊设置,需要在界面上显示 -->
|
||||
<table class="ui table" v-show="moreOptionsVisible">
|
||||
<tr>
|
||||
<td class="title">状态码</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="redirectToHttpsConfig.status">
|
||||
<option value="0">[使用默认]</option>
|
||||
<option v-for="option in statusOptions" :value="option.code">{{option.code}} {{option.text}}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>跳转后域名或IP地址</td>
|
||||
<td>
|
||||
<input type="text" name="host" v-model="redirectToHttpsConfig.host"/>
|
||||
<p class="comment">默认和用户正在访问的域名或IP地址一致,不填写就表示使用当前的域名。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>端口</td>
|
||||
<td>
|
||||
<input type="text" name="port" v-model="portString" maxlength="5" style="width:6em"/>
|
||||
<p class="comment">默认端口为443。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>允许的域名</td>
|
||||
<td>
|
||||
<domains-box :v-domains="redirectToHttpsConfig.onlyDomains" @change="changeOnlyDomains"></domains-box>
|
||||
<p class="comment">如果填写了允许的域名,那么只有这些域名可以自动跳转。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>排除的域名</td>
|
||||
<td>
|
||||
<domains-box :v-domains="redirectToHttpsConfig.exceptDomains" @change="changeExceptDomains"></domains-box>
|
||||
<p class="comment">如果填写了排除的域名,那么这些域名将不跳转。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,122 @@
|
||||
Vue.component("http-referers-config-box", {
|
||||
props: ["v-referers-config", "v-is-location", "v-is-group"],
|
||||
data: function () {
|
||||
let config = this.vReferersConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
allowEmpty: true,
|
||||
allowSameDomain: true,
|
||||
allowDomains: [],
|
||||
denyDomains: [],
|
||||
checkOrigin: true
|
||||
}
|
||||
}
|
||||
if (config.allowDomains == null) {
|
||||
config.allowDomains = []
|
||||
}
|
||||
if (config.denyDomains == null) {
|
||||
config.denyDomains = []
|
||||
}
|
||||
return {
|
||||
config: config,
|
||||
moreOptionsVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isOn: function () {
|
||||
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
|
||||
},
|
||||
changeAllowDomains: function (domains) {
|
||||
if (typeof (domains) == "object") {
|
||||
this.config.allowDomains = domains
|
||||
this.$forceUpdate()
|
||||
}
|
||||
},
|
||||
changeDenyDomains: function (domains) {
|
||||
if (typeof (domains) == "object") {
|
||||
this.config.denyDomains = domains
|
||||
this.$forceUpdate()
|
||||
}
|
||||
},
|
||||
showMoreOptions: function () {
|
||||
this.moreOptionsVisible = !this.moreOptionsVisible
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="referersJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table selectable definition">
|
||||
<prior-checkbox :v-config="config" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="(!vIsLocation && !vIsGroup) || config.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用防盗链</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中后表示开启防盗链。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="isOn()">
|
||||
<tr>
|
||||
<td class="title">允许直接访问网站</td>
|
||||
<td>
|
||||
<checkbox v-model="config.allowEmpty"></checkbox>
|
||||
<p class="comment">允许用户直接访问网站,用户第一次访问网站时来源域名通常为空。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>来源域名允许一致</td>
|
||||
<td>
|
||||
<checkbox v-model="config.allowSameDomain"></checkbox>
|
||||
<p class="comment">允许来源域名和当前访问的域名一致,相当于在站内访问。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>允许的来源域名</td>
|
||||
<td>
|
||||
<domains-box :v-domains="config.allowDomains" @change="changeAllowDomains">></domains-box>
|
||||
<p class="comment">允许的其他来源域名列表,比如<code-label>example.com</code-label>、<code-label>*.example.com</code-label>。单个星号<code-label>*</code-label>表示允许所有域名。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>禁止的来源域名</td>
|
||||
<td>
|
||||
<domains-box :v-domains="config.denyDomains" @change="changeDenyDomains"></domains-box>
|
||||
<p class="comment">禁止的来源域名列表,比如<code-label>example.org</code-label>、<code-label>*.example.org</code-label>;除了这些禁止的来源域名外,其他域名都会被允许,除非限定了允许的来源域名。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><more-options-indicator @change="showMoreOptions"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="moreOptionsVisible && isOn()">
|
||||
<tr>
|
||||
<td>同时检查Origin</td>
|
||||
<td>
|
||||
<checkbox v-model="config.checkOrigin"></checkbox>
|
||||
<p class="comment">如果请求没有指定Referer Header,则尝试检查Origin Header,多用于跨站调用。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>例外URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.exceptURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了例外URL,表示这些URL跳过不做处理。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>限制URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.onlyURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了限制URL,表示只对这些URL进行处理;如果不填则表示支持所有的URL。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ui margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,161 @@
|
||||
Vue.component("http-remote-addr-config-box", {
|
||||
props: ["v-remote-addr-config", "v-is-location", "v-is-group"],
|
||||
data: function () {
|
||||
let config = this.vRemoteAddrConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
value: "${rawRemoteAddr}",
|
||||
type: "default",
|
||||
|
||||
requestHeaderName: ""
|
||||
}
|
||||
}
|
||||
|
||||
// type
|
||||
if (config.type == null || config.type.length == 0) {
|
||||
config.type = "default"
|
||||
switch (config.value) {
|
||||
case "${rawRemoteAddr}":
|
||||
config.type = "default"
|
||||
break
|
||||
case "${remoteAddrValue}":
|
||||
config.type = "default"
|
||||
break
|
||||
case "${remoteAddr}":
|
||||
config.type = "proxy"
|
||||
break
|
||||
default:
|
||||
if (config.value != null && config.value.length > 0) {
|
||||
config.type = "variable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// value
|
||||
if (config.value == null || config.value.length == 0) {
|
||||
config.value = "${rawRemoteAddr}"
|
||||
}
|
||||
|
||||
return {
|
||||
config: config,
|
||||
options: [
|
||||
{
|
||||
name: "直接获取",
|
||||
description: "用户直接访问边缘节点,即 \"用户 --> 边缘节点\" 模式,这时候系统会试图从直接的连接中读取到客户端IP地址。",
|
||||
value: "${rawRemoteAddr}",
|
||||
type: "default"
|
||||
},
|
||||
{
|
||||
name: "从上级代理中获取",
|
||||
description: "用户和边缘节点之间有别的代理服务转发,即 \"用户 --> [第三方代理服务] --> 边缘节点\",这时候只能从上级代理中获取传递的IP地址;上级代理传递的请求报头中必须包含 X-Forwarded-For 或 X-Real-IP 信息。",
|
||||
value: "${remoteAddr}",
|
||||
type: "proxy"
|
||||
},
|
||||
{
|
||||
name: "从请求报头中读取",
|
||||
description: "从自定义请求报头读取客户端IP。",
|
||||
value: "",
|
||||
type: "requestHeader"
|
||||
},
|
||||
{
|
||||
name: "[自定义变量]",
|
||||
description: "通过自定义变量来获取客户端真实的IP地址。",
|
||||
value: "",
|
||||
type: "variable"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"config.requestHeaderName": function (value) {
|
||||
if (this.config.type == "requestHeader"){
|
||||
this.config.value = "${header." + value.trim() + "}"
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isOn: function () {
|
||||
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
|
||||
},
|
||||
changeOptionType: function () {
|
||||
let that = this
|
||||
|
||||
switch(this.config.type) {
|
||||
case "default":
|
||||
this.config.value = "${rawRemoteAddr}"
|
||||
break
|
||||
case "proxy":
|
||||
this.config.value = "${remoteAddr}"
|
||||
break
|
||||
case "requestHeader":
|
||||
this.config.value = ""
|
||||
if (this.requestHeaderName != null && this.requestHeaderName.length > 0) {
|
||||
this.config.value = "${header." + this.requestHeaderName + "}"
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
that.$refs.requestHeaderInput.focus()
|
||||
})
|
||||
break
|
||||
case "variable":
|
||||
this.config.value = "${rawRemoteAddr}"
|
||||
|
||||
setTimeout(function () {
|
||||
that.$refs.variableInput.focus()
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="remoteAddrJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="config" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="(!vIsLocation && !vIsGroup) || config.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用访客IP设置</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中后,表示使用自定义的请求变量获取客户端IP。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="isOn()">
|
||||
<tr>
|
||||
<td>获取IP方式 *</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="config.type" @change="changeOptionType">
|
||||
<option v-for="option in options" :value="option.type">{{option.name}}</option>
|
||||
</select>
|
||||
<p class="comment" v-for="option in options" v-if="option.type == config.type && option.description.length > 0">{{option.description}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- read from request header -->
|
||||
<tr v-show="config.type == 'requestHeader'">
|
||||
<td>请求报头 *</td>
|
||||
<td>
|
||||
<input type="text" name="requestHeaderName" v-model="config.requestHeaderName" maxlength="100" ref="requestHeaderInput"/>
|
||||
<p class="comment">请输入包含有客户端IP的请求报头,需要注意大小写,常见的有<code-label>X-Forwarded-For</code-label>、<code-label>X-Real-IP</code-label>、<code-label>X-Client-IP</code-label>等。</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- read from variable -->
|
||||
<tr v-show="config.type == 'variable'">
|
||||
<td>读取IP变量值 *</td>
|
||||
<td>
|
||||
<input type="text" name="value" v-model="config.value" maxlength="100" ref="variableInput"/>
|
||||
<p class="comment">通过此变量获取用户的IP地址。具体可用的请求变量列表可参考官方网站文档;比如通过报头传递IP的情形,可以使用<code-label>\${header.你的自定义报头}</code-label>(类似于<code-label>\${header.X-Forwarded-For}</code-label>,需要注意大小写规范)。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
Vue.component("http-request-cond-view", {
|
||||
props: ["v-cond"],
|
||||
data: function () {
|
||||
return {
|
||||
cond: this.vCond,
|
||||
components: window.REQUEST_COND_COMPONENTS
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
typeName: function (cond) {
|
||||
let c = this.components.$find(function (k, v) {
|
||||
return v.type == cond.type
|
||||
})
|
||||
if (c != null) {
|
||||
return c.name;
|
||||
}
|
||||
return cond.param + " " + cond.operator
|
||||
},
|
||||
updateConds: function (conds, simpleCond) {
|
||||
for (let k in simpleCond) {
|
||||
if (simpleCond.hasOwnProperty(k)) {
|
||||
this.cond[k] = simpleCond[k]
|
||||
}
|
||||
}
|
||||
},
|
||||
notifyChange: function () {
|
||||
|
||||
}
|
||||
},
|
||||
template: `<div style="margin-bottom: 0.5em">
|
||||
<span class="ui label small basic">
|
||||
<var v-if="cond.type.length == 0 || cond.type == 'params'" style="font-style: normal">{{cond.param}} <var>{{cond.operator}}</var></var>
|
||||
<var v-if="cond.type.length > 0 && cond.type != 'params'" style="font-style: normal">{{typeName(cond)}}: </var>
|
||||
{{cond.value}}
|
||||
<sup v-if="cond.isCaseInsensitive" title="不区分大小写"><i class="icon info small"></i></sup>
|
||||
</span>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,112 @@
|
||||
Vue.component("http-request-conds-box", {
|
||||
props: ["v-conds"],
|
||||
data: function () {
|
||||
let conds = this.vConds
|
||||
if (conds == null) {
|
||||
conds = {
|
||||
isOn: true,
|
||||
connector: "or",
|
||||
groups: []
|
||||
}
|
||||
}
|
||||
if (conds.groups == null) {
|
||||
conds.groups = []
|
||||
}
|
||||
return {
|
||||
conds: conds,
|
||||
components: window.REQUEST_COND_COMPONENTS
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("change", this.conds)
|
||||
},
|
||||
addGroup: function () {
|
||||
window.UPDATING_COND_GROUP = null
|
||||
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/conds/addGroupPopup", {
|
||||
height: "30em",
|
||||
callback: function (resp) {
|
||||
that.conds.groups.push(resp.data.group)
|
||||
that.change()
|
||||
}
|
||||
})
|
||||
},
|
||||
updateGroup: function (groupIndex, group) {
|
||||
window.UPDATING_COND_GROUP = group
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/conds/addGroupPopup", {
|
||||
height: "30em",
|
||||
callback: function (resp) {
|
||||
Vue.set(that.conds.groups, groupIndex, resp.data.group)
|
||||
that.change()
|
||||
}
|
||||
})
|
||||
},
|
||||
removeGroup: function (groupIndex) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要删除这一组条件吗?", function () {
|
||||
that.conds.groups.$remove(groupIndex)
|
||||
that.change()
|
||||
})
|
||||
},
|
||||
typeName: function (cond) {
|
||||
let c = this.components.$find(function (k, v) {
|
||||
return v.type == cond.type
|
||||
})
|
||||
if (c != null) {
|
||||
return c.name;
|
||||
}
|
||||
return cond.param + " " + cond.operator
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="condsJSON" :value="JSON.stringify(conds)"/>
|
||||
<div v-if="conds.groups.length > 0">
|
||||
<table class="ui table">
|
||||
<tr v-for="(group, groupIndex) in conds.groups">
|
||||
<td class="title" :class="{'color-border':conds.connector == 'and'}" :style="{'border-bottom':(groupIndex < conds.groups.length-1) ? '1px solid rgba(34,36,38,.15)':''}">分组{{groupIndex+1}}</td>
|
||||
<td style="background: white; word-break: break-all" :style="{'border-bottom':(groupIndex < conds.groups.length-1) ? '1px solid rgba(34,36,38,.15)':''}">
|
||||
<var v-for="(cond, index) in group.conds" style="font-style: normal;display: inline-block; margin-bottom:0.5em">
|
||||
<span class="ui label tiny">
|
||||
<var v-if="cond.type.length == 0 || cond.type == 'params'" style="font-style: normal">{{cond.param}} <var>{{cond.operator}}</var></var>
|
||||
<var v-if="cond.type.length > 0 && cond.type != 'params'" style="font-style: normal">{{typeName(cond)}}: </var>
|
||||
{{cond.value}}
|
||||
<sup v-if="cond.isCaseInsensitive" title="不区分大小写"><i class="icon info small"></i></sup>
|
||||
</span>
|
||||
|
||||
<var v-if="index < group.conds.length - 1"> {{group.connector}} </var>
|
||||
</var>
|
||||
</td>
|
||||
<td style="width: 5em; background: white" :style="{'border-bottom':(groupIndex < conds.groups.length-1) ? '1px solid rgba(34,36,38,.15)':''}">
|
||||
<a href="" title="修改分组" @click.prevent="updateGroup(groupIndex, group)"><i class="icon pencil small"></i></a> <a href="" title="删除分组" @click.prevent="removeGroup(groupIndex)"><i class="icon remove"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
|
||||
<!-- 分组之间关系 -->
|
||||
<table class="ui table" v-if="conds.groups.length > 1">
|
||||
<tr>
|
||||
<td class="title">分组之间关系</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" v-model="conds.connector">
|
||||
<option value="and">和</option>
|
||||
<option value="or">或</option>
|
||||
</select>
|
||||
<p class="comment">
|
||||
<span v-if="conds.connector == 'or'">只要满足其中一个条件分组即可。</span>
|
||||
<span v-if="conds.connector == 'and'">需要满足所有条件分组。</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div>
|
||||
<button class="ui button tiny basic" type="button" @click.prevent="addGroup()">+添加分组</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
// 浏览条件列表
|
||||
Vue.component("http-request-conds-view", {
|
||||
props: ["v-conds"],
|
||||
data: function () {
|
||||
let conds = this.vConds
|
||||
if (conds == null) {
|
||||
conds = {
|
||||
isOn: true,
|
||||
connector: "or",
|
||||
groups: []
|
||||
}
|
||||
}
|
||||
if (conds.groups == null) {
|
||||
conds.groups = []
|
||||
}
|
||||
|
||||
let that = this
|
||||
conds.groups.forEach(function (group) {
|
||||
group.conds.forEach(function (cond) {
|
||||
cond.typeName = that.typeName(cond)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
initConds: conds
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 之所以使用computed,是因为需要动态更新
|
||||
conds: function () {
|
||||
return this.initConds
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
typeName: function (cond) {
|
||||
let c = window.REQUEST_COND_COMPONENTS.$find(function (k, v) {
|
||||
return v.type == cond.type
|
||||
})
|
||||
if (c != null) {
|
||||
return c.name;
|
||||
}
|
||||
return cond.param + " " + cond.operator
|
||||
},
|
||||
updateConds: function (conds) {
|
||||
this.initConds = conds
|
||||
},
|
||||
notifyChange: function () {
|
||||
let that = this
|
||||
if (this.initConds.groups != null) {
|
||||
this.initConds.groups.forEach(function (group) {
|
||||
group.conds.forEach(function (cond) {
|
||||
cond.typeName = that.typeName(cond)
|
||||
})
|
||||
})
|
||||
this.$forceUpdate()
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div v-if="conds.groups.length > 0">
|
||||
<div v-for="(group, groupIndex) in conds.groups">
|
||||
<var v-for="(cond, index) in group.conds" style="font-style: normal;display: inline-block; margin-bottom:0.5em">
|
||||
<span class="ui label small basic" style="line-height: 1.5">
|
||||
<var v-if="cond.type.length == 0 || cond.type == 'params'" style="font-style: normal">{{cond.param}} <var>{{cond.operator}}</var></var>
|
||||
<var v-if="cond.type.length > 0 && cond.type != 'params'" style="font-style: normal">{{cond.typeName}}: </var>
|
||||
{{cond.value}}
|
||||
<sup v-if="cond.isCaseInsensitive" title="不区分大小写"><i class="icon info small"></i></sup>
|
||||
</span>
|
||||
|
||||
<var v-if="index < group.conds.length - 1"> {{group.connector}} </var>
|
||||
</var>
|
||||
<div class="ui divider" v-if="groupIndex != conds.groups.length - 1" style="margin-top:0.3em;margin-bottom:0.5em"></div>
|
||||
<div>
|
||||
<span class="ui label tiny olive" v-if="group.description != null && group.description.length > 0">{{group.description}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
// 请求限制
|
||||
Vue.component("http-request-limit-config-box", {
|
||||
props: ["v-request-limit-config", "v-is-group", "v-is-location"],
|
||||
data: function () {
|
||||
let config = this.vRequestLimitConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
maxConns: 0,
|
||||
maxConnsPerIP: 0,
|
||||
maxBodySize: {
|
||||
count: -1,
|
||||
unit: "kb"
|
||||
},
|
||||
outBandwidthPerConn: {
|
||||
count: -1,
|
||||
unit: "kb"
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
config: config,
|
||||
maxConns: config.maxConns,
|
||||
maxConnsPerIP: config.maxConnsPerIP
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
maxConns: function (v) {
|
||||
let conns = parseInt(v, 10)
|
||||
if (isNaN(conns)) {
|
||||
this.config.maxConns = 0
|
||||
return
|
||||
}
|
||||
if (conns < 0) {
|
||||
this.config.maxConns = 0
|
||||
} else {
|
||||
this.config.maxConns = conns
|
||||
}
|
||||
},
|
||||
maxConnsPerIP: function (v) {
|
||||
let conns = parseInt(v, 10)
|
||||
if (isNaN(conns)) {
|
||||
this.config.maxConnsPerIP = 0
|
||||
return
|
||||
}
|
||||
if (conns < 0) {
|
||||
this.config.maxConnsPerIP = 0
|
||||
} else {
|
||||
this.config.maxConnsPerIP = conns
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isOn: function () {
|
||||
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="requestLimitJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table selectable definition">
|
||||
<prior-checkbox :v-config="config" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="(!vIsLocation && !vIsGroup) || config.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用请求限制</td>
|
||||
<td>
|
||||
<checkbox v-model="config.isOn"></checkbox>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="isOn()">
|
||||
<tr>
|
||||
<td>最大并发连接数</td>
|
||||
<td>
|
||||
<input type="text" maxlength="6" v-model="maxConns"/>
|
||||
<p class="comment">当前网站最大并发连接数,超出此限制则响应用户<code-label>429</code-label>代码。为0表示不限制。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>单IP最大并发连接数</td>
|
||||
<td>
|
||||
<input type="text" maxlength="6" v-model="maxConnsPerIP"/>
|
||||
<p class="comment">单IP最大连接数,统计单个IP总连接数时不区分网站,超出此限制则响应用户<code-label>429</code-label>代码。为0表示不限制。<span v-if="maxConnsPerIP > 0 && maxConnsPerIP <= 3" class="red">当前设置的并发连接数过低,可能会影响正常用户访问,建议不小于3。</span></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>单连接带宽限制</td>
|
||||
<td>
|
||||
<size-capacity-box :v-value="config.outBandwidthPerConn" :v-supported-units="['byte', 'kb', 'mb']"></size-capacity-box>
|
||||
<p class="comment">客户端单个请求每秒可以读取的下行流量。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>单请求最大尺寸</td>
|
||||
<td>
|
||||
<size-capacity-box :v-value="config.maxBodySize" :v-supported-units="['byte', 'kb', 'mb', 'gb']"></size-capacity-box>
|
||||
<p class="comment">单个请求能发送的最大内容尺寸。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
Vue.component("http-request-scripts-config-box", {
|
||||
props: ["vRequestScriptsConfig", "v-auditing-status", "v-is-location"],
|
||||
data: function () {
|
||||
let config = this.vRequestScriptsConfig
|
||||
if (config == null) {
|
||||
config = {}
|
||||
}
|
||||
|
||||
return {
|
||||
config: config
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeInitGroup: function (group) {
|
||||
this.config.initGroup = group
|
||||
this.$forceUpdate()
|
||||
},
|
||||
changeRequestGroup: function (group) {
|
||||
this.config.requestGroup = group
|
||||
this.$forceUpdate()
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="requestScriptsJSON" :value="JSON.stringify(config)"/>
|
||||
<div class="margin"></div>
|
||||
<h4 style="margin-bottom: 0">请求初始化</h4>
|
||||
<p class="comment">在请求刚初始化时调用,此时自定义报头等尚未生效。</p>
|
||||
<div>
|
||||
<script-group-config-box :v-group="config.initGroup" :v-auditing-status="vAuditingStatus" @change="changeInitGroup" :v-is-location="vIsLocation"></script-group-config-box>
|
||||
</div>
|
||||
<h4 style="margin-bottom: 0">准备发送请求</h4>
|
||||
<p class="comment">在准备执行请求或者转发请求之前调用,此时自定义报头、源站等已准备好。</p>
|
||||
<div>
|
||||
<script-group-config-box :v-group="config.requestGroup" :v-auditing-status="vAuditingStatus" @change="changeRequestGroup" :v-is-location="vIsLocation"></script-group-config-box>
|
||||
</div>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,101 @@
|
||||
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: `<div>
|
||||
<div class="margin"></div>
|
||||
<p class="comment" v-if="rewriteRules.length == 0">暂时还没有重写规则。</p>
|
||||
<table class="ui table selectable" v-if="rewriteRules.length > 0" id="sortable-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:1em"></th>
|
||||
<th>匹配规则</th>
|
||||
<th>转发目标</th>
|
||||
<th>转发方式</th>
|
||||
<th class="two wide">状态</th>
|
||||
<th class="two op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-for="rule in rewriteRules" :v-id="rule.id">
|
||||
<tr>
|
||||
<td><i class="icon bars grey handle"></i></td>
|
||||
<td>{{rule.pattern}}
|
||||
<br/>
|
||||
<http-rewrite-labels-label class="ui label tiny" v-if="rule.isBreak">BREAK</http-rewrite-labels-label>
|
||||
<http-rewrite-labels-label class="ui label tiny" v-if="rule.mode == 'redirect' && rule.redirectStatus != 307">{{rule.redirectStatus}}</http-rewrite-labels-label>
|
||||
<http-rewrite-labels-label class="ui label tiny" v-if="rule.proxyHost.length > 0">Host: {{rule.proxyHost}}</http-rewrite-labels-label>
|
||||
</td>
|
||||
<td>{{rule.replace}}</td>
|
||||
<td>
|
||||
<span v-if="rule.mode == 'proxy'">隐式</span>
|
||||
<span v-if="rule.mode == 'redirect'">显示</span>
|
||||
</td>
|
||||
<td>
|
||||
<label-on :v-is-on="rule.isOn"></label-on>
|
||||
</td>
|
||||
<td>
|
||||
<a href="" @click.prevent="updateRewriteRule(rule.id)">修改</a>
|
||||
<a href="" @click.prevent="deleteRewriteRule(rule.id)">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="comment" v-if="rewriteRules.length > 0">拖动左侧的<i class="icon bars grey"></i>图标可以对重写规则进行排序。</p>
|
||||
|
||||
</div>`
|
||||
})
|
||||
|
||||
Vue.component("http-rewrite-labels-label", {
|
||||
props: ["v-class"],
|
||||
template: `<span class="ui label tiny" :class="vClass" style="font-size:0.7em;padding:4px;margin-top:0.3em;margin-bottom:0.3em"><slot></slot></span>`
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
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: `<div>
|
||||
<input type="hidden" name="statJSON" :value="JSON.stringify(stat)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="stat" v-if="vIsLocation || vIsGroup" ></prior-checkbox>
|
||||
<tbody v-show="(!vIsLocation && !vIsGroup) || stat.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用统计</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="stat.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
89
EdgeAdmin/web/public/js/components/server/http-status-box.js
Normal file
89
EdgeAdmin/web/public/js/components/server/http-status-box.js
Normal file
@@ -0,0 +1,89 @@
|
||||
// 请求方法列表
|
||||
Vue.component("http-status-box", {
|
||||
props: ["v-status-list"],
|
||||
data: function () {
|
||||
let statusList = this.vStatusList
|
||||
if (statusList == null) {
|
||||
statusList = []
|
||||
}
|
||||
return {
|
||||
statusList: statusList,
|
||||
isAdding: false,
|
||||
addingStatus: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add: function () {
|
||||
this.isAdding = true
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.addingStatus.focus()
|
||||
}, 100)
|
||||
},
|
||||
confirm: function () {
|
||||
let that = this
|
||||
|
||||
// 删除其中的空格
|
||||
this.addingStatus = this.addingStatus.replace(/\s/g, "").toUpperCase()
|
||||
|
||||
if (this.addingStatus.length == 0) {
|
||||
teaweb.warn("请输入要添加的状态码", function () {
|
||||
that.$refs.addingStatus.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 是否已经存在
|
||||
if (this.statusList.$contains(this.addingStatus)) {
|
||||
teaweb.warn("此状态码已经存在,无需重复添加", function () {
|
||||
that.$refs.addingStatus.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 格式
|
||||
if (!this.addingStatus.match(/^\d{3}$/)) {
|
||||
teaweb.warn("请输入正确的状态码", function () {
|
||||
that.$refs.addingStatus.focus()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.statusList.push(parseInt(this.addingStatus, 10))
|
||||
this.cancel()
|
||||
},
|
||||
remove: function (index) {
|
||||
this.statusList.$remove(index)
|
||||
},
|
||||
cancel: function () {
|
||||
this.isAdding = false
|
||||
this.addingStatus = ""
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="statusListJSON" :value="JSON.stringify(statusList)"/>
|
||||
<div v-if="statusList.length > 0">
|
||||
<span class="ui label small basic" v-for="(status, index) in statusList">
|
||||
{{status}}
|
||||
<a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
|
||||
</span>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div v-if="isAdding">
|
||||
<div class="ui fields">
|
||||
<div class="ui field">
|
||||
<input type="text" v-model="addingStatus" @keyup.enter="confirm()" @keypress.enter.prevent="1" ref="addingStatus" placeholder="如200" size="3" maxlength="3" style="width: 5em"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny" type="button" @click.prevent="confirm">确定</button>
|
||||
<a href="" title="取消" @click.prevent="cancel"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment">格式为三位数字,比如<code-label>200</code-label>、<code-label>404</code-label>等。</p>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div style="margin-top: 0.5em" v-if="!isAdding">
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
148
EdgeAdmin/web/public/js/components/server/http-web-root-box.js
Normal file
148
EdgeAdmin/web/public/js/components/server/http-web-root-box.js
Normal file
@@ -0,0 +1,148 @@
|
||||
Vue.component("http-web-root-box", {
|
||||
props: ["v-root-config", "v-is-location", "v-is-group"],
|
||||
data: function () {
|
||||
let config = this.vRootConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
dir: "",
|
||||
indexes: [],
|
||||
stripPrefix: "",
|
||||
decodePath: false,
|
||||
isBreak: false,
|
||||
exceptHiddenFiles: true,
|
||||
onlyURLPatterns: [],
|
||||
exceptURLPatterns: []
|
||||
}
|
||||
}
|
||||
if (config.indexes == null) {
|
||||
config.indexes = []
|
||||
}
|
||||
|
||||
if (config.onlyURLPatterns == null) {
|
||||
config.onlyURLPatterns = []
|
||||
}
|
||||
if (config.exceptURLPatterns == null) {
|
||||
config.exceptURLPatterns = []
|
||||
}
|
||||
|
||||
return {
|
||||
config: config,
|
||||
advancedVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeAdvancedVisible: function (v) {
|
||||
this.advancedVisible = v
|
||||
},
|
||||
addIndex: function () {
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/web/createIndex", {
|
||||
height: "10em",
|
||||
callback: function (resp) {
|
||||
that.config.indexes.push(resp.data.index)
|
||||
}
|
||||
})
|
||||
},
|
||||
removeIndex: function (i) {
|
||||
this.config.indexes.$remove(i)
|
||||
},
|
||||
isOn: function () {
|
||||
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="rootJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table selectable definition">
|
||||
<prior-checkbox :v-config="config" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="(!vIsLocation && !vIsGroup) || config.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用静态资源分发</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="config.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="isOn()">
|
||||
<tr>
|
||||
<td class="title">静态资源根目录</td>
|
||||
<td>
|
||||
<input type="text" name="root" v-model="config.dir" ref="focus" placeholder="类似于 /home/www"/>
|
||||
<p class="comment">可以访问此根目录下的静态资源。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<more-options-tbody @change="changeAdvancedVisible" v-if="isOn()"></more-options-tbody>
|
||||
|
||||
<tbody v-show="isOn() && advancedVisible">
|
||||
<tr>
|
||||
<td>首页文件</td>
|
||||
<td>
|
||||
<!-- TODO 支持排序 -->
|
||||
<div v-if="config.indexes.length > 0">
|
||||
<div v-for="(index, i) in config.indexes" class="ui label small basic">
|
||||
{{index}} <a href="" title="删除" @click.prevent="removeIndex(i)"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="addIndex()">+</button>
|
||||
<p class="comment">在URL中只有目录没有文件名时默认查找的首页文件。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>例外URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.exceptURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了例外URL,表示不支持通过这些URL访问。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>限制URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.onlyURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了限制URL,表示仅支持通过这些URL访问。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>排除隐藏文件</td>
|
||||
<td>
|
||||
<checkbox v-model="config.exceptHiddenFiles"></checkbox>
|
||||
<p class="comment">排除以点(.)符号开头的隐藏目录或文件,比如<code-label>/.git/logs/HEAD</code-label></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>去除URL前缀</td>
|
||||
<td>
|
||||
<input type="text" v-model="config.stripPrefix" placeholder="/PREFIX"/>
|
||||
<p class="comment">可以把请求的路径部分前缀去除后再查找文件,比如把 <span class="ui label tiny">/web/app/index.html</span> 去除前缀 <span class="ui label tiny">/web</span> 后就变成 <span class="ui label tiny">/app/index.html</span>。 </p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>路径解码</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="config.decodePath"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">是否对请求路径进行URL解码,比如把 <span class="ui label tiny">/Web+App+Browser.html</span> 解码成 <span class="ui label tiny">/Web App Browser.html</span> 再查找文件。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>终止请求</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="config.isBreak"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">在找不到要访问的文件的情况下是否终止请求并返回404,如果选择终止请求,则不再尝试反向代理等设置。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,107 @@
|
||||
Vue.component("http-webp-config-box", {
|
||||
props: ["v-webp-config", "v-is-location", "v-is-group", "v-require-cache"],
|
||||
data: function () {
|
||||
let config = this.vWebpConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
minLength: {count: 0, "unit": "kb"},
|
||||
maxLength: {count: 0, "unit": "kb"},
|
||||
mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico"],
|
||||
extensions: [".png", ".jpeg", ".jpg", ".bmp", ".ico"],
|
||||
conds: null
|
||||
}
|
||||
}
|
||||
|
||||
if (config.mimeTypes == null) {
|
||||
config.mimeTypes = []
|
||||
}
|
||||
if (config.extensions == null) {
|
||||
config.extensions = []
|
||||
}
|
||||
|
||||
return {
|
||||
config: config,
|
||||
moreOptionsVisible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isOn: function () {
|
||||
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
|
||||
},
|
||||
changeExtensions: function (values) {
|
||||
values.forEach(function (v, k) {
|
||||
if (v.length > 0 && v[0] != ".") {
|
||||
values[k] = "." + v
|
||||
}
|
||||
})
|
||||
this.config.extensions = values
|
||||
},
|
||||
changeMimeTypes: function (values) {
|
||||
this.config.mimeTypes = values
|
||||
},
|
||||
changeAdvancedVisible: function () {
|
||||
this.moreOptionsVisible = !this.moreOptionsVisible
|
||||
},
|
||||
changeConds: function (conds) {
|
||||
this.config.conds = conds
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="webpJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="config" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="(!vIsLocation && !vIsGroup) || config.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用WebP压缩</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中后表示开启自动WebP压缩;图片的宽和高均不能超过16383像素<span v-if="vRequireCache">;只有满足缓存条件的图片内容才会被转换</span>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<more-options-tbody @change="changeAdvancedVisible" v-if="isOn()"></more-options-tbody>
|
||||
<tbody v-show="isOn() && moreOptionsVisible">
|
||||
<tr>
|
||||
<td>支持的扩展名</td>
|
||||
<td>
|
||||
<values-box :values="config.extensions" @change="changeExtensions" placeholder="比如 .html"></values-box>
|
||||
<p class="comment">含有这些扩展名的URL将会被转成WebP,不区分大小写。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持的MimeType</td>
|
||||
<td>
|
||||
<values-box :values="config.mimeTypes" @change="changeMimeTypes" placeholder="比如 text/*"></values-box>
|
||||
<p class="comment">响应的Content-Type里包含这些MimeType的内容将会被转成WebP。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>内容最小长度</td>
|
||||
<td>
|
||||
<size-capacity-box :v-name="'minLength'" :v-value="config.minLength" :v-unit="'kb'"></size-capacity-box>
|
||||
<p class="comment">0表示不限制,内容长度从文件尺寸或Content-Length中获取。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>内容最大长度</td>
|
||||
<td>
|
||||
<size-capacity-box :v-name="'maxLength'" :v-value="config.maxLength" :v-unit="'mb'"></size-capacity-box>
|
||||
<p class="comment">0表示不限制,内容长度从文件尺寸或Content-Length中获取。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>匹配条件</td>
|
||||
<td>
|
||||
<http-request-conds-box :v-conds="config.conds" @change="changeConds"></http-request-conds-box>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ui margin"></div>
|
||||
</div>`
|
||||
})
|
||||
161
EdgeAdmin/web/public/js/components/server/http-websocket-box.js
Normal file
161
EdgeAdmin/web/public/js/components/server/http-websocket-box.js
Normal file
@@ -0,0 +1,161 @@
|
||||
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: `<div>
|
||||
<input type="hidden" name="websocketRefJSON" :value="JSON.stringify(websocketRef)"/>
|
||||
<input type="hidden" name="websocketJSON" :value="JSON.stringify(websocketConfig)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="websocketRef" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="((!vIsLocation && !vIsGroup) || websocketRef.isPrior)">
|
||||
<tr>
|
||||
<td class="title">启用Websocket</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="websocketRef.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="isOn()">
|
||||
<tr>
|
||||
<td class="color-border">允许所有来源域<em>(Origin)</em></td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="websocketConfig.allowAllOrigins"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中表示允许所有的来源域。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="isOn() && !websocketConfig.allowAllOrigins">
|
||||
<tr>
|
||||
<td class="color-border">允许的来源域列表<em>(Origin)</em></td>
|
||||
<td>
|
||||
<div v-if="websocketConfig.allowedOrigins.length > 0">
|
||||
<div class="ui label small basic" v-for="(origin, index) in websocketConfig.allowedOrigins">
|
||||
{{origin}} <a href="" title="删除" @click.prevent="removeOrigin(index)"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="createOrigin()">+</button>
|
||||
<p class="comment">只允许在列表中的来源域名访问Websocket服务。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<more-options-tbody @change="changeAdvancedVisible" v-show="isOn()"></more-options-tbody>
|
||||
<tbody v-show="isOn() && advancedVisible">
|
||||
<tr>
|
||||
<td class="color-border">传递请求来源域</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="websocketConfig.requestSameOrigin"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中后,表示把接收到的请求中的<code-label>Origin</code-label>字段传递到源站。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="isOn() && advancedVisible && !websocketConfig.requestSameOrigin">
|
||||
<tr>
|
||||
<td class="color-border">指定传递的来源域</td>
|
||||
<td>
|
||||
<input type="text" v-model="websocketConfig.requestOrigin" maxlength="200"/>
|
||||
<p class="comment">指定向源站传递的<span class="ui label tiny">Origin</span>字段值。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<!-- TODO 这个选项暂时保留 -->
|
||||
<tbody v-show="isOn() && false">
|
||||
<tr>
|
||||
<td>握手超时时间<em>(Handshake)</em></td>
|
||||
<td>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" maxlength="10" v-model="handshakeTimeoutCountString" style="width:6em"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
秒
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment">0表示使用默认的时间设置。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
426
EdgeAdmin/web/public/js/components/server/metric-charts.js
Normal file
426
EdgeAdmin/web/public/js/components/server/metric-charts.js
Normal file
@@ -0,0 +1,426 @@
|
||||
// 指标图表
|
||||
Vue.component("metric-chart", {
|
||||
props: ["v-chart", "v-stats", "v-item", "v-column" /** in column? **/],
|
||||
mounted: function () {
|
||||
this.load()
|
||||
},
|
||||
data: function () {
|
||||
let stats = this.vStats
|
||||
if (stats == null) {
|
||||
stats = []
|
||||
}
|
||||
if (stats.length > 0) {
|
||||
let sum = stats.$sum(function (k, v) {
|
||||
return v.value
|
||||
})
|
||||
if (sum < stats[0].total) {
|
||||
if (this.vChart.type == "pie") {
|
||||
stats.push({
|
||||
keys: ["其他"],
|
||||
value: stats[0].total - sum,
|
||||
total: stats[0].total,
|
||||
time: stats[0].time
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.vChart.maxItems > 0) {
|
||||
stats = stats.slice(0, this.vChart.maxItems)
|
||||
} else {
|
||||
stats = stats.slice(0, 10)
|
||||
}
|
||||
|
||||
stats.$rsort(function (v1, v2) {
|
||||
return v1.value - v2.value
|
||||
})
|
||||
|
||||
let widthPercent = 100
|
||||
if (this.vChart.widthDiv > 0) {
|
||||
widthPercent = 100 / this.vChart.widthDiv
|
||||
}
|
||||
|
||||
return {
|
||||
chart: this.vChart,
|
||||
stats: stats,
|
||||
item: this.vItem,
|
||||
width: widthPercent + "%",
|
||||
chartId: "metric-chart-" + this.vChart.id,
|
||||
valueTypeName: (this.vItem != null && this.vItem.valueTypeName != null && this.vItem.valueTypeName.length > 0) ? this.vItem.valueTypeName : ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
load: function () {
|
||||
var el = document.getElementById(this.chartId)
|
||||
if (el == null || el.offsetWidth == 0 || el.offsetHeight == 0) {
|
||||
setTimeout(this.load, 100)
|
||||
} else {
|
||||
this.render(el)
|
||||
}
|
||||
},
|
||||
render: function (el) {
|
||||
let chart = echarts.init(el)
|
||||
window.addEventListener("resize", function () {
|
||||
chart.resize()
|
||||
})
|
||||
switch (this.chart.type) {
|
||||
case "pie":
|
||||
this.renderPie(chart)
|
||||
break
|
||||
case "bar":
|
||||
this.renderBar(chart)
|
||||
break
|
||||
case "timeBar":
|
||||
this.renderTimeBar(chart)
|
||||
break
|
||||
case "timeLine":
|
||||
this.renderTimeLine(chart)
|
||||
break
|
||||
case "table":
|
||||
this.renderTable(chart)
|
||||
break
|
||||
}
|
||||
},
|
||||
renderPie: function (chart) {
|
||||
let values = this.stats.map(function (v) {
|
||||
return {
|
||||
name: v.keys[0],
|
||||
value: v.value
|
||||
}
|
||||
})
|
||||
let that = this
|
||||
chart.setOption({
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: "item",
|
||||
formatter: function (data) {
|
||||
let stat = that.stats[data.dataIndex]
|
||||
let percent = 0
|
||||
if (stat.total > 0) {
|
||||
percent = Math.round((stat.value * 100 / stat.total) * 100) / 100
|
||||
}
|
||||
let value = stat.value
|
||||
switch (that.item.valueType) {
|
||||
case "byte":
|
||||
value = teaweb.formatBytes(value)
|
||||
break
|
||||
case "count":
|
||||
value = teaweb.formatNumber(value)
|
||||
break
|
||||
}
|
||||
return stat.keys[0] + "<br/>" + that.valueTypeName + ": " + value + "<br/>占比:" + percent + "%"
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: name,
|
||||
type: "pie",
|
||||
data: values,
|
||||
areaStyle: {},
|
||||
color: ["#9DD3E8", "#B2DB9E", "#F39494", "#FBD88A", "#879BD7"]
|
||||
}
|
||||
]
|
||||
})
|
||||
},
|
||||
renderTimeBar: function (chart) {
|
||||
this.stats.$sort(function (v1, v2) {
|
||||
return (v1.time < v2.time) ? -1 : 1
|
||||
})
|
||||
let values = this.stats.map(function (v) {
|
||||
return v.value
|
||||
})
|
||||
|
||||
let axis = {unit: "", divider: 1}
|
||||
switch (this.item.valueType) {
|
||||
case "count":
|
||||
axis = teaweb.countAxis(values, function (v) {
|
||||
return v
|
||||
})
|
||||
break
|
||||
case "byte":
|
||||
axis = teaweb.bytesAxis(values, function (v) {
|
||||
return v
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
let that = this
|
||||
chart.setOption({
|
||||
xAxis: {
|
||||
data: this.stats.map(function (v) {
|
||||
return that.formatTime(v.time)
|
||||
})
|
||||
},
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: function (value) {
|
||||
return value + axis.unit
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: "item",
|
||||
formatter: function (data) {
|
||||
let stat = that.stats[data.dataIndex]
|
||||
let value = stat.value
|
||||
switch (that.item.valueType) {
|
||||
case "byte":
|
||||
value = teaweb.formatBytes(value)
|
||||
break
|
||||
}
|
||||
return that.formatTime(stat.time) + ": " + value
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 50,
|
||||
top: 10,
|
||||
right: 20,
|
||||
bottom: 25
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: name,
|
||||
type: "bar",
|
||||
data: values.map(function (v) {
|
||||
return v / axis.divider
|
||||
}),
|
||||
itemStyle: {
|
||||
color: teaweb.DefaultChartColor
|
||||
},
|
||||
areaStyle: {},
|
||||
barWidth: "10em"
|
||||
}
|
||||
]
|
||||
})
|
||||
},
|
||||
renderTimeLine: function (chart) {
|
||||
this.stats.$sort(function (v1, v2) {
|
||||
return (v1.time < v2.time) ? -1 : 1
|
||||
})
|
||||
let values = this.stats.map(function (v) {
|
||||
return v.value
|
||||
})
|
||||
|
||||
let axis = {unit: "", divider: 1}
|
||||
switch (this.item.valueType) {
|
||||
case "count":
|
||||
axis = teaweb.countAxis(values, function (v) {
|
||||
return v
|
||||
})
|
||||
break
|
||||
case "byte":
|
||||
axis = teaweb.bytesAxis(values, function (v) {
|
||||
return v
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
let that = this
|
||||
chart.setOption({
|
||||
xAxis: {
|
||||
data: this.stats.map(function (v) {
|
||||
return that.formatTime(v.time)
|
||||
})
|
||||
},
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: function (value) {
|
||||
return value + axis.unit
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: "item",
|
||||
formatter: function (data) {
|
||||
let stat = that.stats[data.dataIndex]
|
||||
let value = stat.value
|
||||
switch (that.item.valueType) {
|
||||
case "byte":
|
||||
value = teaweb.formatBytes(value)
|
||||
break
|
||||
}
|
||||
return that.formatTime(stat.time) + ": " + value
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 50,
|
||||
top: 10,
|
||||
right: 20,
|
||||
bottom: 25
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: name,
|
||||
type: "line",
|
||||
data: values.map(function (v) {
|
||||
return v / axis.divider
|
||||
}),
|
||||
itemStyle: {
|
||||
color: teaweb.DefaultChartColor
|
||||
},
|
||||
areaStyle: {}
|
||||
}
|
||||
]
|
||||
})
|
||||
},
|
||||
renderBar: function (chart) {
|
||||
let values = this.stats.map(function (v) {
|
||||
return v.value
|
||||
})
|
||||
let axis = {unit: "", divider: 1}
|
||||
switch (this.item.valueType) {
|
||||
case "count":
|
||||
axis = teaweb.countAxis(values, function (v) {
|
||||
return v
|
||||
})
|
||||
break
|
||||
case "byte":
|
||||
axis = teaweb.bytesAxis(values, function (v) {
|
||||
return v
|
||||
})
|
||||
break
|
||||
}
|
||||
let bottom = 24
|
||||
let rotate = 0
|
||||
let result = teaweb.xRotation(chart, this.stats.map(function (v) {
|
||||
return v.keys[0]
|
||||
}))
|
||||
if (result != null) {
|
||||
bottom = result[0]
|
||||
rotate = result[1]
|
||||
}
|
||||
let that = this
|
||||
chart.setOption({
|
||||
xAxis: {
|
||||
data: this.stats.map(function (v) {
|
||||
return v.keys[0]
|
||||
}),
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: rotate
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: "item",
|
||||
formatter: function (data) {
|
||||
let stat = that.stats[data.dataIndex]
|
||||
let percent = 0
|
||||
if (stat.total > 0) {
|
||||
percent = Math.round((stat.value * 100 / stat.total) * 100) / 100
|
||||
}
|
||||
let value = stat.value
|
||||
switch (that.item.valueType) {
|
||||
case "byte":
|
||||
value = teaweb.formatBytes(value)
|
||||
break
|
||||
case "count":
|
||||
value = teaweb.formatNumber(value)
|
||||
break
|
||||
}
|
||||
return stat.keys[0] + "<br/>" + that.valueTypeName + ":" + value + "<br/>占比:" + percent + "%"
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: function (value) {
|
||||
return value + axis.unit
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 40,
|
||||
top: 10,
|
||||
right: 20,
|
||||
bottom: bottom
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: name,
|
||||
type: "bar",
|
||||
data: values.map(function (v) {
|
||||
return v / axis.divider
|
||||
}),
|
||||
itemStyle: {
|
||||
color: teaweb.DefaultChartColor
|
||||
},
|
||||
areaStyle: {},
|
||||
barWidth: "10em"
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
if (this.item.keys != null) {
|
||||
// IP相关操作
|
||||
if (this.item.keys.$contains("${remoteAddr}")) {
|
||||
let that = this
|
||||
chart.on("click", function (args) {
|
||||
let index = that.item.keys.$indexesOf("${remoteAddr}")[0]
|
||||
let value = that.stats[args.dataIndex].keys[index]
|
||||
teaweb.popup("/servers/ipbox?ip=" + value, {
|
||||
width: "50em",
|
||||
height: "30em"
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
renderTable: function (chart) {
|
||||
let table = `<table class="ui table celled">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>对象</th>
|
||||
<th>数值</th>
|
||||
<th>占比</th>
|
||||
</tr>
|
||||
</thead>`
|
||||
let that = this
|
||||
this.stats.forEach(function (v) {
|
||||
let value = v.value
|
||||
switch (that.item.valueType) {
|
||||
case "byte":
|
||||
value = teaweb.formatBytes(value)
|
||||
break
|
||||
}
|
||||
table += "<tr><td>" + v.keys[0] + "</td><td>" + value + "</td>"
|
||||
let percent = 0
|
||||
if (v.total > 0) {
|
||||
percent = Math.round((v.value * 100 / v.total) * 100) / 100
|
||||
}
|
||||
table += "<td><div class=\"ui progress blue\"><div class=\"bar\" style=\"min-width: 0; height: 4px; width: " + percent + "%\"></div></div>" + percent + "%</td>"
|
||||
table += "</tr>"
|
||||
})
|
||||
|
||||
table += `</table>`
|
||||
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: `<div style="float: left" :style="{'width': this.vColumn ? '' : width}" :class="{'ui column':this.vColumn}">
|
||||
<h4>{{chart.name}} <span>({{valueTypeName}})</span></h4>
|
||||
<div class="ui divider"></div>
|
||||
<div style="height: 14em; padding-bottom: 1em; " :id="chartId" :class="{'scroll-box': chart.type == 'table'}"></div>
|
||||
</div>`
|
||||
})
|
||||
|
||||
Vue.component("metric-board", {
|
||||
template: `<div><slot></slot></div>`
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
// 显示指标对象名
|
||||
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: `<div class="ui label basic small">
|
||||
{{keyName(this.vKey)}}
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,145 @@
|
||||
// 指标对象
|
||||
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: `<div>
|
||||
<input type="hidden" name="keysJSON" :value="JSON.stringify(keys)"/>
|
||||
<div>
|
||||
<div v-for="(key, index) in keys" class="ui label small basic">
|
||||
{{keyName(key)}} <a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isAdding" style="margin-top: 1em">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" v-model="key" @change="changeKey">
|
||||
<option value="">[选择对象]</option>
|
||||
<option v-for="def in keyDefs" :value="def.code">{{def.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field" v-if="key == '\${arg.NAME}'">
|
||||
<input type="text" v-model="subKey" placeholder="参数名" size="15"/>
|
||||
</div>
|
||||
<div class="ui field" v-if="key == '\${header.NAME}'">
|
||||
<input type="text" v-model="subKey" placeholder="Header名" size="15">
|
||||
</div>
|
||||
<div class="ui field" v-if="key == '\${cookie.NAME}'">
|
||||
<input type="text" v-model="subKey" placeholder="Cookie名" size="15">
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button type="button" class="ui button tiny" @click.prevent="confirm">确定</button>
|
||||
<a href="" @click.prevent="cancel"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment" v-if="keyDescription.length > 0">{{keyDescription}}</p>
|
||||
</div>
|
||||
<div style="margin-top: 1em" v-if="!isAdding">
|
||||
<button type="button" class="ui button tiny" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
// 指标周期设置
|
||||
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: `<div>
|
||||
<input type="hidden" name="periodJSON" :value="JSON.stringify(periodConfig)"/>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" v-model="periodConfig.period" maxlength="4" size="4"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" v-model="periodConfig.unit">
|
||||
<option value="minute">分钟</option>
|
||||
<option value="hour">小时</option>
|
||||
<option value="day">天</option>
|
||||
<option value="week">周</option>
|
||||
<option value="month">月</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment">在此周期内同一对象累积为同一数据。</p>
|
||||
</div>`
|
||||
})
|
||||
154
EdgeAdmin/web/public/js/components/server/origin-list-box.js
Normal file
154
EdgeAdmin/web/public/js/components/server/origin-list-box.js
Normal file
@@ -0,0 +1,154 @@
|
||||
Vue.component("origin-list-box", {
|
||||
props: ["v-primary-origins", "v-backup-origins", "v-server-type", "v-params"],
|
||||
data: function () {
|
||||
return {
|
||||
primaryOrigins: this.vPrimaryOrigins,
|
||||
backupOrigins: this.vBackupOrigins
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createPrimaryOrigin: function () {
|
||||
teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, {
|
||||
width: "45em",
|
||||
height: "27em",
|
||||
callback: function (resp) {
|
||||
teaweb.success("保存成功", function () {
|
||||
window.location.reload()
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
createBackupOrigin: function () {
|
||||
teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, {
|
||||
width: "45em",
|
||||
height: "27em",
|
||||
callback: function (resp) {
|
||||
teaweb.success("保存成功", function () {
|
||||
window.location.reload()
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
updateOrigin: function (originId, originType) {
|
||||
teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, {
|
||||
width: "45em",
|
||||
height: "27em",
|
||||
callback: function (resp) {
|
||||
teaweb.success("保存成功", function () {
|
||||
window.location.reload()
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteOrigin: function (originId, originAddr, originType) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要删除此源站(" + originAddr + ")吗?", function () {
|
||||
Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType)
|
||||
.post()
|
||||
.success(function () {
|
||||
teaweb.success("删除成功", function () {
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
updateOriginIsOn: function (originId, originAddr, isOn) {
|
||||
let message
|
||||
let resultMessage
|
||||
if (isOn) {
|
||||
message = "确定要启用此源站(" + originAddr + ")吗?"
|
||||
resultMessage = "启用成功"
|
||||
} else {
|
||||
message = "确定要停用此源站(" + originAddr + ")吗?"
|
||||
resultMessage = "停用成功"
|
||||
}
|
||||
let that = this
|
||||
teaweb.confirm(message, function () {
|
||||
Tea.action("/servers/server/settings/origins/updateIsOn?" + that.vParams + "&originId=" + originId + "&isOn=" + (isOn ? 1 : 0))
|
||||
.post()
|
||||
.success(function () {
|
||||
teaweb.success(resultMessage, function () {
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<h3>主要源站 <a href="" @click.prevent="createPrimaryOrigin()">[添加主要源站]</a> </h3>
|
||||
<p class="comment" v-if="primaryOrigins.length == 0">暂时还没有主要源站。</p>
|
||||
<origin-list-table v-if="primaryOrigins.length > 0" :v-origins="vPrimaryOrigins" :v-origin-type="'primary'" @deleteOrigin="deleteOrigin" @updateOrigin="updateOrigin" @updateOriginIsOn="updateOriginIsOn"></origin-list-table>
|
||||
|
||||
<h3>备用源站 <a href="" @click.prevent="createBackupOrigin()">[添加备用源站]</a></h3>
|
||||
<p class="comment" v-if="backupOrigins.length == 0">暂时还没有备用源站。</p>
|
||||
<origin-list-table v-if="backupOrigins.length > 0" :v-origins="backupOrigins" :v-origin-type="'backup'" @deleteOrigin="deleteOrigin" @updateOrigin="updateOrigin" @updateOriginIsOn="updateOriginIsOn"></origin-list-table>
|
||||
</div>`
|
||||
})
|
||||
|
||||
Vue.component("origin-list-table", {
|
||||
props: ["v-origins", "v-origin-type"],
|
||||
data: function () {
|
||||
let hasMatchedDomains = false
|
||||
let origins = this.vOrigins
|
||||
if (origins != null && origins.length > 0) {
|
||||
origins.forEach(function (origin) {
|
||||
if (origin.domains != null && origin.domains.length > 0) {
|
||||
hasMatchedDomains = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
hasMatchedDomains: hasMatchedDomains
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteOrigin: function (originId, originAddr) {
|
||||
this.$emit("deleteOrigin", originId, originAddr, this.vOriginType)
|
||||
},
|
||||
updateOrigin: function (originId) {
|
||||
this.$emit("updateOrigin", originId, this.vOriginType)
|
||||
},
|
||||
updateOriginIsOn: function (originId, originAddr, isOn) {
|
||||
this.$emit("updateOriginIsOn", originId, originAddr, isOn)
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<table class="ui table selectable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>源站地址</th>
|
||||
<th class="width5">权重</th>
|
||||
<th class="width6">状态</th>
|
||||
<th class="three op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="origin in vOrigins">
|
||||
<td :class="{disabled:!origin.isOn}">
|
||||
<a href="" @click.prevent="updateOrigin(origin.id)" :class="{disabled:!origin.isOn}">{{origin.addr}} <i class="icon expand small"></i></a>
|
||||
<div style="margin-top: 0.3em">
|
||||
<tiny-basic-label class="grey border-grey" v-if="origin.isOSS"><i class="icon hdd outline"></i>对象存储</tiny-basic-label>
|
||||
<tiny-basic-label class="grey border-grey" v-if="origin.name.length > 0">{{origin.name}}</tiny-basic-label>
|
||||
<tiny-basic-label class="grey border-grey" v-if="origin.hasCert">证书</tiny-basic-label>
|
||||
<tiny-basic-label class="grey border-grey" v-if="origin.host != null && origin.host.length > 0">主机名: {{origin.host}}</tiny-basic-label>
|
||||
<tiny-basic-label class="grey border-grey" v-if="origin.followPort">端口跟随</tiny-basic-label>
|
||||
<tiny-basic-label class="grey border-grey" v-if="origin.addr != null && origin.addr.startsWith('https://') && origin.http2Enabled">HTTP/2</tiny-basic-label>
|
||||
|
||||
<span v-if="origin.domains != null && origin.domains.length > 0"><tiny-basic-label class="grey border-grey" v-for="domain in origin.domains">匹配: {{domain}}</tiny-basic-label></span>
|
||||
<span v-else-if="hasMatchedDomains"><tiny-basic-label class="grey border-grey">匹配: 所有域名</tiny-basic-label></span>
|
||||
</div>
|
||||
</td>
|
||||
<td :class="{disabled:!origin.isOn}">{{origin.weight}}</td>
|
||||
<td>
|
||||
<label-on :v-is-on="origin.isOn"></label-on>
|
||||
</td>
|
||||
<td>
|
||||
<a href="" @click.prevent="updateOrigin(origin.id)">修改</a>
|
||||
<a href="" v-if="origin.isOn" @click.prevent="updateOriginIsOn(origin.id, origin.addr, false)">停用</a><a href="" v-if="!origin.isOn" @click.prevent="updateOriginIsOn(origin.id, origin.addr, true)"><span class="red">启用</span></a>
|
||||
<a href="" @click.prevent="deleteOrigin(origin.id, origin.addr)">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
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: `<div>
|
||||
<div class="margin"></div>
|
||||
<table class="ui table selectable definition">
|
||||
<tr>
|
||||
<td class="title">当前正在使用的算法</td>
|
||||
<td>
|
||||
{{scheduling.name}} <a href="" @click.prevent="update()"><span>[修改]</span></a>
|
||||
<p class="comment">{{scheduling.description}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
30
EdgeAdmin/web/public/js/components/server/prior-checkbox.js
Normal file
30
EdgeAdmin/web/public/js/components/server/prior-checkbox.js
Normal file
@@ -0,0 +1,30 @@
|
||||
Vue.component("prior-checkbox", {
|
||||
props: ["v-config", "description"],
|
||||
data: function () {
|
||||
let description = this.description
|
||||
if (description == null) {
|
||||
description = "打开后可以覆盖父级或子级配置"
|
||||
}
|
||||
return {
|
||||
isPrior: this.vConfig.isPrior,
|
||||
realDescription: description
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isPrior: function (v) {
|
||||
this.vConfig.isPrior = v
|
||||
}
|
||||
},
|
||||
template: `<tbody>
|
||||
<tr :class="{active:isPrior}">
|
||||
<td class="title">打开独立配置</td>
|
||||
<td>
|
||||
<div class="ui toggle checkbox">
|
||||
<input type="checkbox" v-model="isPrior"/>
|
||||
<label class="red"></label>
|
||||
</div>
|
||||
<p class="comment"><strong v-if="isPrior">[已打开]</strong> {{realDescription}}。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>`
|
||||
})
|
||||
340
EdgeAdmin/web/public/js/components/server/reverse-proxy-box.js
Normal file
340
EdgeAdmin/web/public/js/components/server/reverse-proxy-box.js
Normal file
@@ -0,0 +1,340 @@
|
||||
Vue.component("reverse-proxy-box", {
|
||||
props: ["v-reverse-proxy-ref", "v-reverse-proxy-config", "v-is-location", "v-is-group", "v-family"],
|
||||
data: function () {
|
||||
let reverseProxyRef = this.vReverseProxyRef
|
||||
if (reverseProxyRef == null) {
|
||||
reverseProxyRef = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
reverseProxyId: 0
|
||||
}
|
||||
}
|
||||
|
||||
let reverseProxyConfig = this.vReverseProxyConfig
|
||||
if (reverseProxyConfig == null) {
|
||||
reverseProxyConfig = {
|
||||
requestPath: "",
|
||||
stripPrefix: "",
|
||||
requestURI: "",
|
||||
requestHost: "",
|
||||
requestHostType: 0,
|
||||
requestHostExcludingPort: false,
|
||||
addHeaders: [],
|
||||
connTimeout: {count: 0, unit: "second"},
|
||||
readTimeout: {count: 0, unit: "second"},
|
||||
idleTimeout: {count: 0, unit: "second"},
|
||||
maxConns: 0,
|
||||
maxIdleConns: 0,
|
||||
followRedirects: false,
|
||||
retry50X: false,
|
||||
retry40X: false
|
||||
}
|
||||
}
|
||||
if (reverseProxyConfig.addHeaders == null) {
|
||||
reverseProxyConfig.addHeaders = []
|
||||
}
|
||||
if (reverseProxyConfig.connTimeout == null) {
|
||||
reverseProxyConfig.connTimeout = {count: 0, unit: "second"}
|
||||
}
|
||||
if (reverseProxyConfig.readTimeout == null) {
|
||||
reverseProxyConfig.readTimeout = {count: 0, unit: "second"}
|
||||
}
|
||||
if (reverseProxyConfig.idleTimeout == null) {
|
||||
reverseProxyConfig.idleTimeout = {count: 0, unit: "second"}
|
||||
}
|
||||
|
||||
if (reverseProxyConfig.proxyProtocol == null) {
|
||||
// 如果直接赋值Vue将不会触发变更通知
|
||||
Vue.set(reverseProxyConfig, "proxyProtocol", {
|
||||
isOn: false,
|
||||
version: 1
|
||||
})
|
||||
}
|
||||
|
||||
let forwardHeaders = [
|
||||
{
|
||||
name: "X-Real-IP",
|
||||
isChecked: false
|
||||
},
|
||||
{
|
||||
name: "X-Forwarded-For",
|
||||
isChecked: false
|
||||
},
|
||||
{
|
||||
name: "X-Forwarded-By",
|
||||
isChecked: false
|
||||
},
|
||||
{
|
||||
name: "X-Forwarded-Host",
|
||||
isChecked: false
|
||||
},
|
||||
{
|
||||
name: "X-Forwarded-Proto",
|
||||
isChecked: false
|
||||
}
|
||||
]
|
||||
forwardHeaders.forEach(function (v) {
|
||||
v.isChecked = reverseProxyConfig.addHeaders.$contains(v.name)
|
||||
})
|
||||
|
||||
return {
|
||||
reverseProxyRef: reverseProxyRef,
|
||||
reverseProxyConfig: reverseProxyConfig,
|
||||
advancedVisible: false,
|
||||
family: this.vFamily,
|
||||
forwardHeaders: forwardHeaders
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"reverseProxyConfig.requestHostType": function (v) {
|
||||
let requestHostType = parseInt(v)
|
||||
if (isNaN(requestHostType)) {
|
||||
requestHostType = 0
|
||||
}
|
||||
this.reverseProxyConfig.requestHostType = requestHostType
|
||||
},
|
||||
"reverseProxyConfig.connTimeout.count": function (v) {
|
||||
let count = parseInt(v)
|
||||
if (isNaN(count) || count < 0) {
|
||||
count = 0
|
||||
}
|
||||
this.reverseProxyConfig.connTimeout.count = count
|
||||
},
|
||||
"reverseProxyConfig.readTimeout.count": function (v) {
|
||||
let count = parseInt(v)
|
||||
if (isNaN(count) || count < 0) {
|
||||
count = 0
|
||||
}
|
||||
this.reverseProxyConfig.readTimeout.count = count
|
||||
},
|
||||
"reverseProxyConfig.idleTimeout.count": function (v) {
|
||||
let count = parseInt(v)
|
||||
if (isNaN(count) || count < 0) {
|
||||
count = 0
|
||||
}
|
||||
this.reverseProxyConfig.idleTimeout.count = count
|
||||
},
|
||||
"reverseProxyConfig.maxConns": function (v) {
|
||||
let maxConns = parseInt(v)
|
||||
if (isNaN(maxConns) || maxConns < 0) {
|
||||
maxConns = 0
|
||||
}
|
||||
this.reverseProxyConfig.maxConns = maxConns
|
||||
},
|
||||
"reverseProxyConfig.maxIdleConns": function (v) {
|
||||
let maxIdleConns = parseInt(v)
|
||||
if (isNaN(maxIdleConns) || maxIdleConns < 0) {
|
||||
maxIdleConns = 0
|
||||
}
|
||||
this.reverseProxyConfig.maxIdleConns = maxIdleConns
|
||||
},
|
||||
"reverseProxyConfig.proxyProtocol.version": function (v) {
|
||||
let version = parseInt(v)
|
||||
if (isNaN(version)) {
|
||||
version = 1
|
||||
}
|
||||
this.reverseProxyConfig.proxyProtocol.version = version
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isOn: function () {
|
||||
if (this.vIsLocation || this.vIsGroup) {
|
||||
return this.reverseProxyRef.isPrior && this.reverseProxyRef.isOn
|
||||
}
|
||||
return this.reverseProxyRef.isOn
|
||||
},
|
||||
changeAdvancedVisible: function (v) {
|
||||
this.advancedVisible = v
|
||||
},
|
||||
changeAddHeader: function () {
|
||||
this.reverseProxyConfig.addHeaders = this.forwardHeaders.filter(function (v) {
|
||||
return v.isChecked
|
||||
}).map(function (v) {
|
||||
return v.name
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="reverseProxyRefJSON" :value="JSON.stringify(reverseProxyRef)"/>
|
||||
<input type="hidden" name="reverseProxyJSON" :value="JSON.stringify(reverseProxyConfig)"/>
|
||||
<table class="ui table selectable definition">
|
||||
<prior-checkbox :v-config="reverseProxyRef" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="(!vIsLocation && !vIsGroup) || reverseProxyRef.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用源站</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="reverseProxyRef.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中后,所有源站设置才会生效。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="family == null || family == 'http'">
|
||||
<td>回源主机名<em>(Host)</em></td>
|
||||
<td>
|
||||
<radio :v-value="0" v-model="reverseProxyConfig.requestHostType">跟随CDN域名</radio>
|
||||
<radio :v-value="1" v-model="reverseProxyConfig.requestHostType">跟随源站</radio>
|
||||
<radio :v-value="2" v-model="reverseProxyConfig.requestHostType">自定义</radio>
|
||||
<div v-show="reverseProxyConfig.requestHostType == 2" style="margin-top: 0.8em">
|
||||
<input type="text" placeholder="比如example.com" v-model="reverseProxyConfig.requestHost"/>
|
||||
</div>
|
||||
<p class="comment">请求源站时的主机名(Host),用于修改源站接收到的域名
|
||||
<span v-if="reverseProxyConfig.requestHostType == 0">,"跟随CDN域名"是指源站接收到的域名和当前CDN访问域名保持一致</span>
|
||||
<span v-if="reverseProxyConfig.requestHostType == 1">,"跟随源站"是指源站接收到的域名仍然是填写的源站地址中的信息,不随代理服务域名改变而改变</span>
|
||||
<span v-if="reverseProxyConfig.requestHostType == 2">,自定义Host内容中支持请求变量</span>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="family == null || family == 'http'">
|
||||
<td>回源主机名移除端口</td>
|
||||
<td><checkbox v-model="reverseProxyConfig.requestHostExcludingPort"></checkbox>
|
||||
<p class="comment">选中后表示移除回源主机名中的端口部分。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<more-options-tbody @change="changeAdvancedVisible" v-if="isOn()"></more-options-tbody>
|
||||
<tbody v-show="isOn() && advancedVisible">
|
||||
<tr v-show="family == null || family == 'http'">
|
||||
<td>回源跟随</td>
|
||||
<td>
|
||||
<checkbox v-model="reverseProxyConfig.followRedirects"></checkbox>
|
||||
<p class="comment">选中后,自动读取源站跳转后的网页内容。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="family == null || family == 'http'">
|
||||
<td>自动添加报头</td>
|
||||
<td>
|
||||
<div>
|
||||
<div style="width: 14em; float: left; margin-bottom: 1em" v-for="header in forwardHeaders" :key="header.name">
|
||||
<checkbox v-model="header.isChecked" @input="changeAddHeader">{{header.name}}</checkbox>
|
||||
</div>
|
||||
<div style="clear: both"></div>
|
||||
</div>
|
||||
<p class="comment">选中后,会自动向源站请求添加这些报头,以便于源站获取客户端信息。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="family == null || family == 'http'">
|
||||
<td>请求URI<em>(RequestURI)</em></td>
|
||||
<td>
|
||||
<input type="text" placeholder="\${requestURI}" v-model="reverseProxyConfig.requestURI"/>
|
||||
<p class="comment">\${requestURI}为完整的请求URI,可以使用类似于"\${requestURI}?arg1=value1&arg2=value2"的形式添加你的参数。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="family == null || family == 'http'">
|
||||
<td>去除URL前缀<em>(StripPrefix)</em></td>
|
||||
<td>
|
||||
<input type="text" v-model="reverseProxyConfig.stripPrefix" placeholder="/PREFIX"/>
|
||||
<p class="comment">可以把请求的路径部分前缀去除后再查找文件,比如把 <span class="ui label tiny">/web/app/index.html</span> 去除前缀 <span class="ui label tiny">/web</span> 后就变成 <span class="ui label tiny">/app/index.html</span>。 </p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="family == null || family == 'http'">
|
||||
<td>自动刷新缓存区<em>(AutoFlush)</em></td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="reverseProxyConfig.autoFlush"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">开启后将自动刷新缓冲区数据到客户端,在类似于SSE(server-sent events)等场景下很有用。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="family == null || family == 'http'">
|
||||
<td>自动重试50X</td>
|
||||
<td>
|
||||
<checkbox v-model="reverseProxyConfig.retry50X"></checkbox>
|
||||
<p class="comment">选中后,表示当源站返回状态码为50X(比如502、504等)时,自动重试其他源站。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="family == null || family == 'http'">
|
||||
<td>自动重试40X</td>
|
||||
<td>
|
||||
<checkbox v-model="reverseProxyConfig.retry40X"></checkbox>
|
||||
<p class="comment">选中后,表示当源站返回状态码为40X(403或404)时,自动重试其他源站。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="family != 'unix'">
|
||||
<td>PROXY Protocol</td>
|
||||
<td>
|
||||
<checkbox name="proxyProtocolIsOn" v-model="reverseProxyConfig.proxyProtocol.isOn"></checkbox>
|
||||
<p class="comment">选中后表示启用PROXY Protocol,每次连接源站时都会在头部写入客户端地址信息。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="family != 'unix' && reverseProxyConfig.proxyProtocol.isOn">
|
||||
<td>PROXY Protocol版本</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" name="proxyProtocolVersion" v-model="reverseProxyConfig.proxyProtocol.version">
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
</select>
|
||||
<p class="comment" v-if="reverseProxyConfig.proxyProtocol.version == 1">发送类似于<code-label>PROXY TCP4 192.168.1.1 192.168.1.10 32567 443</code-label>的头部信息。</p>
|
||||
<p class="comment" v-if="reverseProxyConfig.proxyProtocol.version == 2">发送二进制格式的头部信息。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="family == null || family == 'http'">
|
||||
<td class="color-border">源站连接失败超时时间</td>
|
||||
<td>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" name="connTimeout" value="10" size="6" v-model="reverseProxyConfig.connTimeout.count"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
秒
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment">连接源站失败的最大超时时间,0表示不限制。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="family == null || family == 'http'">
|
||||
<td class="color-border">源站读取超时时间</td>
|
||||
<td>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" name="readTimeout" value="0" size="6" v-model="reverseProxyConfig.readTimeout.count"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
秒
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment">读取内容时的最大超时时间,0表示不限制。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="family == null || family == 'http'">
|
||||
<td class="color-border">源站最大并发连接数</td>
|
||||
<td>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" name="maxConns" value="0" size="6" maxlength="10" v-model="reverseProxyConfig.maxConns"/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment">源站可以接受到的最大并发连接数,0表示使用系统默认。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="family == null || family == 'http'">
|
||||
<td class="color-border">源站最大空闲连接数</td>
|
||||
<td>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" name="maxIdleConns" value="0" size="6" maxlength="10" v-model="reverseProxyConfig.maxIdleConns"/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment">当没有请求时,源站保持等待的最大空闲连接数量,0表示使用系统默认。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="family == null || family == 'http'">
|
||||
<td class="color-border">源站最大空闲超时时间</td>
|
||||
<td>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" name="idleTimeout" value="0" size="6" v-model="reverseProxyConfig.idleTimeout.count"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
秒
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment">源站保持等待的空闲超时时间,0表示使用默认时间。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,89 @@
|
||||
Vue.component("script-config-box", {
|
||||
props: ["id", "v-script-config", "comment", "v-auditing-status"],
|
||||
mounted: function () {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$forceUpdate()
|
||||
}, 100)
|
||||
},
|
||||
data: function () {
|
||||
let config = this.vScriptConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
code: "",
|
||||
auditingCode: ""
|
||||
}
|
||||
}
|
||||
|
||||
let auditingStatus = null
|
||||
if (config.auditingCodeMD5 != null && config.auditingCodeMD5.length > 0 && config.auditingCode != null && config.auditingCode.length > 0) {
|
||||
config.code = config.auditingCode
|
||||
|
||||
if (this.vAuditingStatus != null) {
|
||||
for (let i = 0; i < this.vAuditingStatus.length; i++) {
|
||||
let status = this.vAuditingStatus[i]
|
||||
if (status.md5 == config.auditingCodeMD5) {
|
||||
auditingStatus = status
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.code.length == 0) {
|
||||
config.code = "\n\n\n\n"
|
||||
}
|
||||
|
||||
return {
|
||||
config: config,
|
||||
auditingStatus: auditingStatus
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"config.isOn": function () {
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function () {
|
||||
this.$emit("change", this.config)
|
||||
},
|
||||
changeCode: function (code) {
|
||||
this.config.code = code
|
||||
this.change()
|
||||
},
|
||||
isPlus: function () {
|
||||
if (Tea == null || Tea.Vue == null) {
|
||||
return false
|
||||
}
|
||||
return Tea.Vue.teaIsPlus
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">启用脚本设置</td>
|
||||
<td><checkbox v-model="config.isOn"></checkbox></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr :style="{opacity: !config.isOn ? 0.5 : 1}">
|
||||
<td>脚本代码</td>
|
||||
<td>
|
||||
<p class="comment" v-if="auditingStatus != null">
|
||||
<span class="green" v-if="auditingStatus.isPassed">管理员审核结果:审核通过。</span>
|
||||
<span class="red" v-else-if="auditingStatus.isRejected">管理员审核结果:驳回 驳回理由:{{auditingStatus.rejectedReason}}</span>
|
||||
<span class="red" v-else>当前脚本将在审核后生效,请耐心等待审核结果。 <a href="/servers/user-scripts" target="_blank" v-if="isPlus()">去审核 »</a></span>
|
||||
</p>
|
||||
<p class="comment" v-if="auditingStatus == null"><span class="green">管理员审核结果:审核通过。</span></p>
|
||||
<source-code-box :id="id" type="text/javascript" :read-only="false" @change="changeCode">{{config.code}}</source-code-box>
|
||||
<p class="comment">{{comment}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
Vue.component("script-group-config-box", {
|
||||
props: ["v-group", "v-auditing-status", "v-is-location"],
|
||||
data: function () {
|
||||
let group = this.vGroup
|
||||
if (group == null) {
|
||||
group = {
|
||||
isPrior: false,
|
||||
isOn: true,
|
||||
scripts: []
|
||||
}
|
||||
}
|
||||
if (group.scripts == null) {
|
||||
group.scripts = []
|
||||
}
|
||||
|
||||
let script = null
|
||||
if (group.scripts.length > 0) {
|
||||
script = group.scripts[group.scripts.length - 1]
|
||||
}
|
||||
|
||||
return {
|
||||
group: group,
|
||||
script: script
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeScript: function (script) {
|
||||
this.group.scripts = [script] // 目前只支持单个脚本
|
||||
this.change()
|
||||
},
|
||||
change: function () {
|
||||
this.$emit("change", this.group)
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="group" v-if="vIsLocation"></prior-checkbox>
|
||||
</table>
|
||||
<div :style="{opacity: (!vIsLocation || group.isPrior) ? 1 : 0.5}">
|
||||
<script-config-box :v-script-config="script" :v-auditing-status="vAuditingStatus" comment="在接收到客户端请求之后立即调用。预置req、resp变量。" @change="changeScript" :v-is-location="vIsLocation"></script-config-box>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
Vue.component("server-config-copy-link", {
|
||||
props: ["v-server-id", "v-config-code"],
|
||||
data: function () {
|
||||
return {
|
||||
serverId: this.vServerId,
|
||||
configCode: this.vConfigCode
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copy: function () {
|
||||
teaweb.popup("/servers/server/settings/copy?serverId=" + this.serverId + "&configCode=" + this.configCode, {
|
||||
height: "25em",
|
||||
callback: function () {
|
||||
teaweb.success("批量复制成功")
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<a href=\"" class="item" @click.prevent="copy" style="padding-right:0"><span style="font-size: 0.8em">批量</span> <i class="icon copy small"></i></a>`
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
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: `<div>
|
||||
<div v-if="groups.length > 0">
|
||||
<div class="ui label small basic" v-if="groups.length > 0" v-for="(group, index) in groups">
|
||||
<input type="hidden" name="groupIds" :value="group.id"/>
|
||||
{{group.name}} <a href="" title="删除" @click.prevent="removeGroup(index)"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="" @click.prevent="selectGroup()">[选择分组]</a> <a href="" @click.prevent="addGroup()">[添加分组]</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
125
EdgeAdmin/web/public/js/components/server/server-name-box.js
Normal file
125
EdgeAdmin/web/public/js/components/server/server-name-box.js
Normal file
@@ -0,0 +1,125 @@
|
||||
Vue.component("server-name-box", {
|
||||
props: ["v-server-names"],
|
||||
data: function () {
|
||||
let serverNames = this.vServerNames;
|
||||
if (serverNames == null) {
|
||||
serverNames = []
|
||||
}
|
||||
return {
|
||||
serverNames: serverNames,
|
||||
isSearching: false,
|
||||
keyword: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addServerName: function () {
|
||||
window.UPDATING_SERVER_NAME = null
|
||||
let that = this
|
||||
teaweb.popup("/servers/addServerNamePopup", {
|
||||
callback: function (resp) {
|
||||
var serverName = resp.data.serverName
|
||||
that.serverNames.push(serverName)
|
||||
setTimeout(that.submitForm, 100)
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
removeServerName: function (index) {
|
||||
this.serverNames.$remove(index)
|
||||
},
|
||||
|
||||
updateServerName: function (index, serverName) {
|
||||
window.UPDATING_SERVER_NAME = teaweb.clone(serverName)
|
||||
let that = this
|
||||
teaweb.popup("/servers/addServerNamePopup", {
|
||||
callback: function (resp) {
|
||||
var serverName = resp.data.serverName
|
||||
Vue.set(that.serverNames, index, serverName)
|
||||
setTimeout(that.submitForm, 100)
|
||||
}
|
||||
});
|
||||
},
|
||||
showSearchBox: function () {
|
||||
this.isSearching = !this.isSearching
|
||||
if (this.isSearching) {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.keywordRef.focus()
|
||||
}, 200)
|
||||
} else {
|
||||
this.keyword = ""
|
||||
}
|
||||
},
|
||||
allServerNames: function () {
|
||||
if (this.serverNames == null) {
|
||||
return []
|
||||
}
|
||||
let result = []
|
||||
this.serverNames.forEach(function (serverName) {
|
||||
if (serverName.subNames != null && serverName.subNames.length > 0) {
|
||||
serverName.subNames.forEach(function (subName) {
|
||||
if (subName != null && subName.length > 0) {
|
||||
if (!result.$contains(subName)) {
|
||||
result.push(subName)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (serverName.name != null && serverName.name.length > 0) {
|
||||
if (!result.$contains(serverName.name)) {
|
||||
result.push(serverName.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
return result
|
||||
},
|
||||
submitForm: function () {
|
||||
Tea.runActionOn(this.$refs.serverNamesRef.form)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
keyword: function (v) {
|
||||
this.serverNames.forEach(function (serverName) {
|
||||
if (v.length == 0) {
|
||||
serverName.isShowing = true
|
||||
return
|
||||
}
|
||||
if (serverName.subNames == null || serverName.subNames.length == 0) {
|
||||
if (!teaweb.match(serverName.name, v)) {
|
||||
serverName.isShowing = false
|
||||
}
|
||||
} else {
|
||||
let found = false
|
||||
serverName.subNames.forEach(function (subName) {
|
||||
if (teaweb.match(subName, v)) {
|
||||
found = true
|
||||
}
|
||||
})
|
||||
serverName.isShowing = found
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="serverNames" :value="JSON.stringify(serverNames)" ref="serverNamesRef"/>
|
||||
<div v-if="serverNames.length > 0">
|
||||
<div v-for="(serverName, index) in serverNames" class="ui label small basic" :class="{hidden: serverName.isShowing === false}">
|
||||
<em v-if="serverName.type != 'full'">{{serverName.type}}</em>
|
||||
<span v-if="serverName.subNames == null || serverName.subNames.length == 0" :class="{disabled: serverName.isShowing === false}">{{serverName.name}}</span>
|
||||
<span v-else :class="{disabled: serverName.isShowing === false}">{{serverName.subNames[0]}}等{{serverName.subNames.length}}个域名</span>
|
||||
<a href="" title="修改" @click.prevent="updateServerName(index, serverName)"><i class="icon pencil small"></i></a> <a href="" title="删除" @click.prevent="removeServerName(index)"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field"><a href="" @click.prevent="addServerName()">[添加域名绑定]</a></div>
|
||||
<div class="ui field" v-if="serverNames.length > 0"><span class="grey">|</span> </div>
|
||||
<div class="ui field" v-if="serverNames.length > 0">
|
||||
<a href="" @click.prevent="showSearchBox()" v-if="!isSearching"><i class="icon search small"></i></a>
|
||||
<a href="" @click.prevent="showSearchBox()" v-if="isSearching"><i class="icon close small"></i></a>
|
||||
</div>
|
||||
<div class="ui field" v-if="isSearching">
|
||||
<input type="text" placeholder="搜索域名" ref="keywordRef" class="ui input tiny" v-model="keyword"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
Vue.component("server-traffic-limit-status-viewer", {
|
||||
props: ["value"],
|
||||
data: function () {
|
||||
let targetTypeName = "流量"
|
||||
if (this.value != null) {
|
||||
targetTypeName = this.targetTypeToName(this.value.targetType)
|
||||
}
|
||||
|
||||
return {
|
||||
status: this.value,
|
||||
targetTypeName: targetTypeName
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
targetTypeToName: function (targetType) {
|
||||
switch (targetType) {
|
||||
case "traffic":
|
||||
return "流量"
|
||||
case "request":
|
||||
return "请求数"
|
||||
case "websocketConnections":
|
||||
return "Websocket连接数"
|
||||
}
|
||||
return "流量"
|
||||
}
|
||||
},
|
||||
template: `<span v-if="status != null">
|
||||
<span v-if="status.dateType == 'day'" class="small red">已达到<span v-if="status.planId > 0">套餐</span>当日{{targetTypeName}}限制</span>
|
||||
<span v-if="status.dateType == 'month'" class="small red">已达到<span v-if="status.planId > 0">套餐</span>当月{{targetTypeName}}限制</span>
|
||||
<span v-if="status.dateType == 'total'" class="small red">已达到<span v-if="status.planId > 0">套餐</span>总体{{targetTypeName}}限制</span>
|
||||
</span>`
|
||||
})
|
||||
170
EdgeAdmin/web/public/js/components/server/ssl-certs-box.js
Normal file
170
EdgeAdmin/web/public/js/components/server/ssl-certs-box.js
Normal file
@@ -0,0 +1,170 @@
|
||||
Vue.component("ssl-certs-box", {
|
||||
props: [
|
||||
"v-certs", // 证书列表
|
||||
"v-cert", // 单个证书
|
||||
"v-protocol", // 协议:https|tls
|
||||
"v-view-size", // 弹窗尺寸:normal, mini
|
||||
"v-single-mode", // 单证书模式
|
||||
"v-description", // 描述文字
|
||||
"v-domains", // 搜索的域名列表或者函数
|
||||
"v-user-id" // 用户ID
|
||||
],
|
||||
data: function () {
|
||||
let certs = this.vCerts
|
||||
if (certs == null) {
|
||||
certs = []
|
||||
}
|
||||
if (this.vCert != null) {
|
||||
certs.push(this.vCert)
|
||||
}
|
||||
|
||||
let description = this.vDescription
|
||||
if (description == null || typeof (description) != "string") {
|
||||
description = ""
|
||||
}
|
||||
|
||||
return {
|
||||
certs: certs,
|
||||
description: description
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
certIds: function () {
|
||||
return this.certs.map(function (v) {
|
||||
return v.id
|
||||
})
|
||||
},
|
||||
// 删除证书
|
||||
removeCert: function (index) {
|
||||
let that = this
|
||||
teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () {
|
||||
that.certs.$remove(index)
|
||||
})
|
||||
},
|
||||
|
||||
// 选择证书
|
||||
selectCert: function () {
|
||||
let that = this
|
||||
let width = "54em"
|
||||
let height = "32em"
|
||||
let viewSize = this.vViewSize
|
||||
if (viewSize == null) {
|
||||
viewSize = "normal"
|
||||
}
|
||||
if (viewSize == "mini") {
|
||||
width = "35em"
|
||||
height = "20em"
|
||||
}
|
||||
|
||||
let searchingDomains = []
|
||||
if (this.vDomains != null) {
|
||||
if (typeof this.vDomains == "function") {
|
||||
let resultDomains = this.vDomains()
|
||||
if (resultDomains != null && typeof resultDomains == "object" && (resultDomains instanceof Array)) {
|
||||
searchingDomains = resultDomains
|
||||
}
|
||||
} else if (typeof this.vDomains == "object" && (this.vDomains instanceof Array)) {
|
||||
searchingDomains = this.vDomains
|
||||
}
|
||||
if (searchingDomains.length > 10000) {
|
||||
searchingDomains = searchingDomains.slice(0, 10000)
|
||||
}
|
||||
}
|
||||
|
||||
let selectedCertIds = this.certs.map(function (cert) {
|
||||
return cert.id
|
||||
})
|
||||
let userId = this.vUserId
|
||||
if (userId == null) {
|
||||
userId = 0
|
||||
}
|
||||
|
||||
teaweb.popup("/servers/certs/selectPopup?viewSize=" + viewSize + "&searchingDomains=" + window.encodeURIComponent(searchingDomains.join(",")) + "&selectedCertIds=" + selectedCertIds.join(",") + "&userId=" + userId, {
|
||||
width: width,
|
||||
height: height,
|
||||
callback: function (resp) {
|
||||
if (resp.data.cert != null) {
|
||||
that.certs.push(resp.data.cert)
|
||||
}
|
||||
if (resp.data.certs != null) {
|
||||
that.certs.$pushAll(resp.data.certs)
|
||||
}
|
||||
that.$forceUpdate()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 上传证书
|
||||
uploadCert: function () {
|
||||
let that = this
|
||||
let userId = this.vUserId
|
||||
if (typeof userId != "number" && typeof userId != "string") {
|
||||
userId = 0
|
||||
}
|
||||
teaweb.popup("/servers/certs/uploadPopup?userId=" + userId, {
|
||||
height: "28em",
|
||||
callback: function (resp) {
|
||||
teaweb.success("上传成功", function () {
|
||||
if (resp.data.cert != null) {
|
||||
that.certs.push(resp.data.cert)
|
||||
}
|
||||
if (resp.data.certs != null) {
|
||||
that.certs.$pushAll(resp.data.certs)
|
||||
}
|
||||
that.$forceUpdate()
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 批量上传
|
||||
uploadBatch: function () {
|
||||
let that = this
|
||||
let userId = this.vUserId
|
||||
if (typeof userId != "number" && typeof userId != "string") {
|
||||
userId = 0
|
||||
}
|
||||
teaweb.popup("/servers/certs/uploadBatchPopup?userId=" + userId, {
|
||||
callback: function (resp) {
|
||||
if (resp.data.cert != null) {
|
||||
that.certs.push(resp.data.cert)
|
||||
}
|
||||
if (resp.data.certs != null) {
|
||||
that.certs.$pushAll(resp.data.certs)
|
||||
}
|
||||
that.$forceUpdate()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime: function (timestamp) {
|
||||
return new Date(timestamp * 1000).format("Y-m-d")
|
||||
},
|
||||
|
||||
// 判断是否显示选择|上传按钮
|
||||
buttonsVisible: function () {
|
||||
return this.vSingleMode == null || !this.vSingleMode || this.certs == null || this.certs.length == 0
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="certIdsJSON" :value="JSON.stringify(certIds())"/>
|
||||
<div v-if="certs != null && certs.length > 0">
|
||||
<div class="ui label small basic" v-for="(cert, index) in certs">
|
||||
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}} <a href="" title="删除" @click.prevent="removeCert(index)"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider" v-if="buttonsVisible()"></div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="red" v-if="description.length == 0">选择或上传证书后<span v-if="vProtocol == 'https'">HTTPS</span><span v-if="vProtocol == 'tls'">TLS</span>服务才能生效。</span>
|
||||
<span class="grey" v-if="description.length > 0">{{description}}</span>
|
||||
<div class="ui divider" v-if="buttonsVisible()"></div>
|
||||
</div>
|
||||
<div v-if="buttonsVisible()">
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
33
EdgeAdmin/web/public/js/components/server/ssl-certs-view.js
Normal file
33
EdgeAdmin/web/public/js/components/server/ssl-certs-view.js
Normal file
@@ -0,0 +1,33 @@
|
||||
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: `<div>
|
||||
<div v-if="certs != null && certs.length > 0">
|
||||
<div class="ui label small basic" v-for="(cert, index) in certs">
|
||||
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}} <a href="" title="查看" @click.prevent="viewCert(cert.id)"><i class="icon expand blue"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
604
EdgeAdmin/web/public/js/components/server/ssl-config-box.js
Normal file
604
EdgeAdmin/web/public/js/components/server/ssl-config-box.js
Normal file
@@ -0,0 +1,604 @@
|
||||
Vue.component("ssl-config-box", {
|
||||
props: [
|
||||
"v-ssl-policy",
|
||||
"v-protocol",
|
||||
"v-server-id",
|
||||
"v-support-http3"
|
||||
],
|
||||
created: function () {
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.sortableCipherSuites()
|
||||
}, 100)
|
||||
},
|
||||
data: function () {
|
||||
let policy = this.vSslPolicy
|
||||
if (policy == null) {
|
||||
policy = {
|
||||
id: 0,
|
||||
isOn: true,
|
||||
certRefs: [],
|
||||
certs: [],
|
||||
clientCARefs: [],
|
||||
clientCACerts: [],
|
||||
clientAuthType: 0,
|
||||
minVersion: "TLS 1.1",
|
||||
hsts: null,
|
||||
cipherSuitesIsOn: false,
|
||||
cipherSuites: [],
|
||||
http2Enabled: true,
|
||||
http3Enabled: false,
|
||||
ocspIsOn: false
|
||||
}
|
||||
} else {
|
||||
if (policy.certRefs == null) {
|
||||
policy.certRefs = []
|
||||
}
|
||||
if (policy.certs == null) {
|
||||
policy.certs = []
|
||||
}
|
||||
if (policy.clientCARefs == null) {
|
||||
policy.clientCARefs = []
|
||||
}
|
||||
if (policy.clientCACerts == null) {
|
||||
policy.clientCACerts = []
|
||||
}
|
||||
if (policy.cipherSuites == null) {
|
||||
policy.cipherSuites = []
|
||||
}
|
||||
}
|
||||
|
||||
let hsts = policy.hsts
|
||||
let hstsMaxAgeString = "31536000"
|
||||
if (hsts == null) {
|
||||
hsts = {
|
||||
isOn: false,
|
||||
maxAge: 31536000,
|
||||
includeSubDomains: false,
|
||||
preload: false,
|
||||
domains: []
|
||||
}
|
||||
}
|
||||
if (hsts.maxAge != null) {
|
||||
hstsMaxAgeString = hsts.maxAge.toString()
|
||||
}
|
||||
|
||||
return {
|
||||
policy: policy,
|
||||
|
||||
// hsts
|
||||
hsts: hsts,
|
||||
hstsOptionsVisible: false,
|
||||
hstsDomainAdding: false,
|
||||
hstsMaxAgeString: hstsMaxAgeString,
|
||||
addingHstsDomain: "",
|
||||
hstsDomainEditingIndex: -1,
|
||||
|
||||
// 相关数据
|
||||
allVersions: window.SSL_ALL_VERSIONS,
|
||||
allCipherSuites: window.SSL_ALL_CIPHER_SUITES.$copy(),
|
||||
modernCipherSuites: window.SSL_MODERN_CIPHER_SUITES,
|
||||
intermediateCipherSuites: window.SSL_INTERMEDIATE_CIPHER_SUITES,
|
||||
allClientAuthTypes: window.SSL_ALL_CLIENT_AUTH_TYPES,
|
||||
cipherSuitesVisible: false,
|
||||
|
||||
// 高级选项
|
||||
moreOptionsVisible: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
hsts: {
|
||||
deep: true,
|
||||
handler: function () {
|
||||
this.policy.hsts = this.hsts
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 删除证书
|
||||
removeCert: function (index) {
|
||||
let that = this
|
||||
teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () {
|
||||
that.policy.certRefs.$remove(index)
|
||||
that.policy.certs.$remove(index)
|
||||
})
|
||||
},
|
||||
|
||||
// 选择证书
|
||||
selectCert: function () {
|
||||
let that = this
|
||||
let selectedCertIds = []
|
||||
if (this.policy != null && this.policy.certs.length > 0) {
|
||||
this.policy.certs.forEach(function (cert) {
|
||||
selectedCertIds.push(cert.id.toString())
|
||||
})
|
||||
}
|
||||
let serverId = this.vServerId
|
||||
if (serverId == null) {
|
||||
serverId = 0
|
||||
}
|
||||
teaweb.popup("/servers/certs/selectPopup?selectedCertIds=" + selectedCertIds + "&serverId=" + serverId, {
|
||||
width: "50em",
|
||||
height: "30em",
|
||||
callback: function (resp) {
|
||||
if (resp.data.cert != null && resp.data.certRef != null) {
|
||||
that.policy.certRefs.push(resp.data.certRef)
|
||||
that.policy.certs.push(resp.data.cert)
|
||||
}
|
||||
if (resp.data.certs != null && resp.data.certRefs != null) {
|
||||
that.policy.certRefs.$pushAll(resp.data.certRefs)
|
||||
that.policy.certs.$pushAll(resp.data.certs)
|
||||
}
|
||||
that.$forceUpdate()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 上传证书
|
||||
uploadCert: function () {
|
||||
let that = this
|
||||
let serverId = this.vServerId
|
||||
if (typeof serverId != "number" && typeof serverId != "string") {
|
||||
serverId = 0
|
||||
}
|
||||
teaweb.popup("/servers/certs/uploadPopup?serverId=" + serverId, {
|
||||
height: "30em",
|
||||
callback: function (resp) {
|
||||
teaweb.success("上传成功", function () {
|
||||
that.policy.certRefs.push(resp.data.certRef)
|
||||
that.policy.certs.push(resp.data.cert)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 批量上传
|
||||
uploadBatch: function () {
|
||||
let that = this
|
||||
let serverId = this.vServerId
|
||||
if (typeof serverId != "number" && typeof serverId != "string") {
|
||||
serverId = 0
|
||||
}
|
||||
teaweb.popup("/servers/certs/uploadBatchPopup?serverId=" + serverId, {
|
||||
callback: function (resp) {
|
||||
if (resp.data.cert != null) {
|
||||
that.policy.certRefs.push(resp.data.certRef)
|
||||
that.policy.certs.push(resp.data.cert)
|
||||
}
|
||||
if (resp.data.certs != null) {
|
||||
that.policy.certRefs.$pushAll(resp.data.certRefs)
|
||||
that.policy.certs.$pushAll(resp.data.certs)
|
||||
}
|
||||
that.$forceUpdate()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 申请证书
|
||||
requestCert: function () {
|
||||
// 已经在证书中的域名
|
||||
let excludeServerNames = []
|
||||
if (this.policy != null && this.policy.certs.length > 0) {
|
||||
this.policy.certs.forEach(function (cert) {
|
||||
excludeServerNames.$pushAll(cert.dnsNames)
|
||||
})
|
||||
}
|
||||
|
||||
let that = this
|
||||
teaweb.popup("/servers/server/settings/https/requestCertPopup?serverId=" + this.vServerId + "&excludeServerNames=" + excludeServerNames.join(","), {
|
||||
callback: function () {
|
||||
that.policy.certRefs.push(resp.data.certRef)
|
||||
that.policy.certs.push(resp.data.cert)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 更多选项
|
||||
changeOptionsVisible: function () {
|
||||
this.moreOptionsVisible = !this.moreOptionsVisible
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime: function (timestamp) {
|
||||
return new Date(timestamp * 1000).format("Y-m-d")
|
||||
},
|
||||
|
||||
// 格式化加密套件
|
||||
formatCipherSuite: function (cipherSuite) {
|
||||
return cipherSuite.replace(/(AES|3DES)/, "<var style=\"font-weight: bold\">$1</var>")
|
||||
},
|
||||
|
||||
// 添加单个套件
|
||||
addCipherSuite: function (cipherSuite) {
|
||||
if (!this.policy.cipherSuites.$contains(cipherSuite)) {
|
||||
this.policy.cipherSuites.push(cipherSuite)
|
||||
}
|
||||
this.allCipherSuites.$removeValue(cipherSuite)
|
||||
},
|
||||
|
||||
// 删除单个套件
|
||||
removeCipherSuite: function (cipherSuite) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要删除此套件吗?", function () {
|
||||
that.policy.cipherSuites.$removeValue(cipherSuite)
|
||||
that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$findAll(function (k, v) {
|
||||
return !that.policy.cipherSuites.$contains(v)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 清除所选套件
|
||||
clearCipherSuites: function () {
|
||||
let that = this
|
||||
teaweb.confirm("确定要清除所有已选套件吗?", function () {
|
||||
that.policy.cipherSuites = []
|
||||
that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$copy()
|
||||
})
|
||||
},
|
||||
|
||||
// 批量添加套件
|
||||
addBatchCipherSuites: function (suites) {
|
||||
var that = this
|
||||
teaweb.confirm("确定要批量添加套件?", function () {
|
||||
suites.$each(function (k, v) {
|
||||
if (that.policy.cipherSuites.$contains(v)) {
|
||||
return
|
||||
}
|
||||
that.policy.cipherSuites.push(v)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 套件拖动排序
|
||||
*/
|
||||
sortableCipherSuites: function () {
|
||||
var box = document.querySelector(".cipher-suites-box")
|
||||
Sortable.create(box, {
|
||||
draggable: ".label",
|
||||
handle: ".icon.handle",
|
||||
onStart: function () {
|
||||
|
||||
},
|
||||
onUpdate: function (event) {
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 显示所有套件
|
||||
showAllCipherSuites: function () {
|
||||
this.cipherSuitesVisible = !this.cipherSuitesVisible
|
||||
},
|
||||
|
||||
// 显示HSTS更多选项
|
||||
showMoreHSTS: function () {
|
||||
this.hstsOptionsVisible = !this.hstsOptionsVisible;
|
||||
if (this.hstsOptionsVisible) {
|
||||
this.changeHSTSMaxAge()
|
||||
}
|
||||
},
|
||||
|
||||
// 监控HSTS有效期修改
|
||||
changeHSTSMaxAge: function () {
|
||||
var v = parseInt(this.hstsMaxAgeString)
|
||||
if (isNaN(v) || v < 0) {
|
||||
this.hsts.maxAge = 0
|
||||
this.hsts.days = "-"
|
||||
return
|
||||
}
|
||||
this.hsts.maxAge = v
|
||||
this.hsts.days = v / 86400
|
||||
if (this.hsts.days == 0) {
|
||||
this.hsts.days = "-"
|
||||
}
|
||||
},
|
||||
|
||||
// 设置HSTS有效期
|
||||
setHSTSMaxAge: function (maxAge) {
|
||||
this.hstsMaxAgeString = maxAge.toString()
|
||||
this.changeHSTSMaxAge()
|
||||
},
|
||||
|
||||
// 添加HSTS域名
|
||||
addHstsDomain: function () {
|
||||
this.hstsDomainAdding = true
|
||||
this.hstsDomainEditingIndex = -1
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.addingHstsDomain.focus()
|
||||
}, 100)
|
||||
},
|
||||
|
||||
// 修改HSTS域名
|
||||
editHstsDomain: function (index) {
|
||||
this.hstsDomainEditingIndex = index
|
||||
this.addingHstsDomain = this.hsts.domains[index]
|
||||
this.hstsDomainAdding = true
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.addingHstsDomain.focus()
|
||||
}, 100)
|
||||
},
|
||||
|
||||
// 确认HSTS域名添加
|
||||
confirmAddHstsDomain: function () {
|
||||
this.addingHstsDomain = this.addingHstsDomain.trim()
|
||||
if (this.addingHstsDomain.length == 0) {
|
||||
return;
|
||||
}
|
||||
if (this.hstsDomainEditingIndex > -1) {
|
||||
this.hsts.domains[this.hstsDomainEditingIndex] = this.addingHstsDomain
|
||||
} else {
|
||||
this.hsts.domains.push(this.addingHstsDomain)
|
||||
}
|
||||
this.cancelHstsDomainAdding()
|
||||
},
|
||||
|
||||
// 取消HSTS域名添加
|
||||
cancelHstsDomainAdding: function () {
|
||||
this.hstsDomainAdding = false
|
||||
this.addingHstsDomain = ""
|
||||
this.hstsDomainEditingIndex = -1
|
||||
},
|
||||
|
||||
// 删除HSTS域名
|
||||
removeHstsDomain: function (index) {
|
||||
this.cancelHstsDomainAdding()
|
||||
this.hsts.domains.$remove(index)
|
||||
},
|
||||
|
||||
// 选择客户端CA证书
|
||||
selectClientCACert: function () {
|
||||
let that = this
|
||||
teaweb.popup("/servers/certs/selectPopup?isCA=1", {
|
||||
width: "50em",
|
||||
height: "30em",
|
||||
callback: function (resp) {
|
||||
if (resp.data.cert != null && resp.data.certRef != null) {
|
||||
that.policy.clientCARefs.push(resp.data.certRef)
|
||||
that.policy.clientCACerts.push(resp.data.cert)
|
||||
}
|
||||
if (resp.data.certs != null && resp.data.certRefs != null) {
|
||||
that.policy.clientCARefs.$pushAll(resp.data.certRefs)
|
||||
that.policy.clientCACerts.$pushAll(resp.data.certs)
|
||||
}
|
||||
that.$forceUpdate()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 上传CA证书
|
||||
uploadClientCACert: function () {
|
||||
let that = this
|
||||
teaweb.popup("/servers/certs/uploadPopup?isCA=1", {
|
||||
height: "28em",
|
||||
callback: function (resp) {
|
||||
teaweb.success("上传成功", function () {
|
||||
that.policy.clientCARefs.push(resp.data.certRef)
|
||||
that.policy.clientCACerts.push(resp.data.cert)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 删除客户端CA证书
|
||||
removeClientCACert: function (index) {
|
||||
let that = this
|
||||
teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () {
|
||||
that.policy.clientCARefs.$remove(index)
|
||||
that.policy.clientCACerts.$remove(index)
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<h4>SSL/TLS相关配置</h4>
|
||||
<input type="hidden" name="sslPolicyJSON" :value="JSON.stringify(policy)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<tbody>
|
||||
<tr v-show="vProtocol == 'https'">
|
||||
<td class="title">启用HTTP/2</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="policy.http2Enabled"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="vProtocol == 'https' && vSupportHttp3">
|
||||
<td class="title">启用HTTP/3</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="policy.http3Enabled"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">设置证书</td>
|
||||
<td>
|
||||
<div v-if="policy.certs != null && policy.certs.length > 0">
|
||||
<div class="ui label small basic" v-for="(cert, index) in policy.certs" style="margin-top: 0.2em">
|
||||
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}} <a href="" title="删除" @click.prevent="removeCert(index)"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="red">选择或上传证书后<span v-if="vProtocol == 'https'">HTTPS</span><span v-if="vProtocol == 'tls'">TLS</span>服务才能生效。</span>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="requestCert()" v-if="vServerId != null && vServerId > 0">申请免费证书</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TLS最低版本</td>
|
||||
<td>
|
||||
<select v-model="policy.minVersion" class="ui dropdown auto-width">
|
||||
<option v-for="version in allVersions" :value="version">{{version}}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<more-options-tbody @change="changeOptionsVisible"></more-options-tbody>
|
||||
<tbody v-show="moreOptionsVisible">
|
||||
<!-- 加密套件 -->
|
||||
<tr>
|
||||
<td>加密算法套件<em>(CipherSuites)</em></td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="policy.cipherSuitesIsOn" />
|
||||
<label>是否要自定义</label>
|
||||
</div>
|
||||
<div v-show="policy.cipherSuitesIsOn">
|
||||
<div class="ui divider"></div>
|
||||
<div class="cipher-suites-box">
|
||||
已添加套件({{policy.cipherSuites.length}}):
|
||||
<div v-for="cipherSuite in policy.cipherSuites" class="ui label tiny basic" style="margin-bottom: 0.5em">
|
||||
<input type="hidden" name="cipherSuites" :value="cipherSuite"/>
|
||||
<span v-html="formatCipherSuite(cipherSuite)"></span> <a href="" title="删除套件" @click.prevent="removeCipherSuite(cipherSuite)"><i class="icon remove"></i></a>
|
||||
<a href="" title="拖动改变顺序"><i class="icon bars handle"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="ui divider"></div>
|
||||
<span v-if="policy.cipherSuites.length > 0"><a href="" @click.prevent="clearCipherSuites()">[清除所有已选套件]</a> </span>
|
||||
<a href="" @click.prevent="addBatchCipherSuites(modernCipherSuites)">[添加推荐套件]</a>
|
||||
<a href="" @click.prevent="addBatchCipherSuites(intermediateCipherSuites)">[添加兼容套件]</a>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
|
||||
<div class="cipher-all-suites-box">
|
||||
<a href="" @click.prevent="showAllCipherSuites()"><span v-if="policy.cipherSuites.length == 0">所有</span>可选套件({{allCipherSuites.length}}) <i class="icon angle" :class="{down:!cipherSuitesVisible, up:cipherSuitesVisible}"></i></a>
|
||||
<a href="" v-if="cipherSuitesVisible" v-for="cipherSuite in allCipherSuites" class="ui label tiny" title="点击添加到自定义套件中" @click.prevent="addCipherSuite(cipherSuite)" v-html="formatCipherSuite(cipherSuite)" style="margin-bottom:0.5em"></a>
|
||||
</div>
|
||||
<p class="comment" v-if="cipherSuitesVisible">点击可选套件添加。</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- HSTS -->
|
||||
<tr v-show="vProtocol == 'https'">
|
||||
<td :class="{'color-border':hsts.isOn}">开启HSTS</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="hstsOn" v-model="hsts.isOn" value="1"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">
|
||||
开启后,会自动在响应Header中加入
|
||||
<span class="ui label small">Strict-Transport-Security:
|
||||
<var v-if="!hsts.isOn">...</var>
|
||||
<var v-if="hsts.isOn"><span>max-age=</span>{{hsts.maxAge}}</var>
|
||||
<var v-if="hsts.isOn && hsts.includeSubDomains">; includeSubDomains</var>
|
||||
<var v-if="hsts.isOn && hsts.preload">; preload</var>
|
||||
</span>
|
||||
<span v-if="hsts.isOn">
|
||||
<a href="" @click.prevent="showMoreHSTS()">修改<i class="icon angle" :class="{down:!hstsOptionsVisible, up:hstsOptionsVisible}"></i> </a>
|
||||
</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="hsts.isOn && hstsOptionsVisible">
|
||||
<td class="color-border">HSTS有效时间<em>(max-age)</em></td>
|
||||
<td>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" name="hstsMaxAge" v-model="hstsMaxAgeString" maxlength="10" size="10" @input="changeHSTSMaxAge()"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
秒
|
||||
</div>
|
||||
<div class="ui field">{{hsts.days}}天</div>
|
||||
</div>
|
||||
<p class="comment">
|
||||
<a href="" @click.prevent="setHSTSMaxAge(31536000)" :class="{active:hsts.maxAge == 31536000}">[1年/365天]</a>
|
||||
<a href="" @click.prevent="setHSTSMaxAge(15768000)" :class="{active:hsts.maxAge == 15768000}">[6个月/182.5天]</a>
|
||||
<a href="" @click.prevent="setHSTSMaxAge(2592000)" :class="{active:hsts.maxAge == 2592000}">[1个月/30天]</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="hsts.isOn && hstsOptionsVisible">
|
||||
<td class="color-border">HSTS包含子域名<em>(includeSubDomains)</em></td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="hstsIncludeSubDomains" value="1" v-model="hsts.includeSubDomains"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="hsts.isOn && hstsOptionsVisible">
|
||||
<td class="color-border">HSTS预加载<em>(preload)</em></td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="hstsPreload" value="1" v-model="hsts.preload"/>
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="hsts.isOn && hstsOptionsVisible">
|
||||
<td class="color-border">HSTS生效的域名</td>
|
||||
<td colspan="2">
|
||||
<div class="names-box">
|
||||
<span class="ui label tiny basic" v-for="(domain, arrayIndex) in hsts.domains" :class="{blue:hstsDomainEditingIndex == arrayIndex}">{{domain}}
|
||||
<input type="hidden" name="hstsDomains" :value="domain"/>
|
||||
<a href="" @click.prevent="editHstsDomain(arrayIndex)" title="修改"><i class="icon pencil"></i></a>
|
||||
<a href="" @click.prevent="removeHstsDomain(arrayIndex)" title="删除"><i class="icon remove"></i></a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="ui fields inline" v-if="hstsDomainAdding" style="margin-top:0.8em">
|
||||
<div class="ui field">
|
||||
<input type="text" name="addingHstsDomain" ref="addingHstsDomain" style="width:16em" maxlength="100" placeholder="域名,比如example.com" @keyup.enter="confirmAddHstsDomain()" @keypress.enter.prevent="1" v-model="addingHstsDomain" />
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button tiny" type="button" @click="confirmAddHstsDomain()">确定</button>
|
||||
<a href="" @click.prevent="cancelHstsDomainAdding()">取消</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui field" style="margin-top: 1em">
|
||||
<button class="ui button tiny" type="button" @click="addHstsDomain()">+</button>
|
||||
</div>
|
||||
<p class="comment">如果没有设置域名的话,则默认支持所有的域名。</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- OCSP -->
|
||||
<tr>
|
||||
<td>OCSP Stapling</td>
|
||||
<td><checkbox name="ocspIsOn" v-model="policy.ocspIsOn"></checkbox>
|
||||
<p class="comment">选中表示启用OCSP Stapling。</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 客户端认证 -->
|
||||
<tr>
|
||||
<td>客户端认证方式</td>
|
||||
<td>
|
||||
<select name="clientAuthType" v-model="policy.clientAuthType" class="ui dropdown auto-width">
|
||||
<option v-for="authType in allClientAuthTypes" :value="authType.type">{{authType.name}}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>客户端认证CA证书</td>
|
||||
<td>
|
||||
<div v-if="policy.clientCACerts != null && policy.clientCACerts.length > 0">
|
||||
<div class="ui label small basic" v-for="(cert, index) in policy.clientCACerts">
|
||||
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}} <a href="" title="删除" @click.prevent="removeClientCACert()"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectClientCACert()">选择已有证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadClientCACert()">上传新证书</button>
|
||||
<p class="comment">用来校验客户端证书以增强安全性,通常不需要设置。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ui margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
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 = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Traffic Limit Exceeded Warning</title>
|
||||
<body>
|
||||
|
||||
<h1>Traffic Limit Exceeded Warning</h1>
|
||||
<p>The site traffic has exceeded the limit. Please contact with the site administrator.</p>
|
||||
<address>Request ID: \${requestId}.</address>
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="trafficLimitJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table selectable definition">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">启用流量限制</td>
|
||||
<td>
|
||||
<checkbox v-model="config.isOn"></checkbox>
|
||||
<p class="comment">注意:由于流量统计是每5分钟统计一次,所以超出流量限制后,对用户的提醒也会有所延迟。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="config.isOn">
|
||||
<tr>
|
||||
<td>日流量限制</td>
|
||||
<td>
|
||||
<size-capacity-box :v-value="config.dailySize"></size-capacity-box>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>月流量限制</td>
|
||||
<td>
|
||||
<size-capacity-box :v-value="config.monthlySize"></size-capacity-box>
|
||||
</td>
|
||||
</tr>
|
||||
<!--<tr>
|
||||
<td>总体限制</td>
|
||||
<td>
|
||||
<size-capacity-box :v-value="config.totalSize"></size-capacity-box>
|
||||
<p class="comment"></p>
|
||||
</td>
|
||||
</tr>-->
|
||||
<tr>
|
||||
<td>网页提示内容</td>
|
||||
<td>
|
||||
<textarea v-model="config.noticePageBody"></textarea>
|
||||
<p class="comment"><a href="" @click.prevent="showBodyTemplate">[使用模板]</a>。当达到流量限制时网页显示的HTML内容,不填写则显示默认的提示内容,适用于网站类服务。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
124
EdgeAdmin/web/public/js/components/server/uam-config-box-plus.js
Normal file
124
EdgeAdmin/web/public/js/components/server/uam-config-box-plus.js
Normal file
@@ -0,0 +1,124 @@
|
||||
// UAM模式配置
|
||||
Vue.component("uam-config-box", {
|
||||
props: ["v-uam-config", "v-is-location", "v-is-group"],
|
||||
data: function () {
|
||||
let config = this.vUamConfig
|
||||
if (config == null) {
|
||||
config = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
addToWhiteList: true,
|
||||
onlyURLPatterns: [],
|
||||
exceptURLPatterns: [],
|
||||
minQPSPerIP: 0,
|
||||
keyLife: 0
|
||||
}
|
||||
}
|
||||
if (config.onlyURLPatterns == null) {
|
||||
config.onlyURLPatterns = []
|
||||
}
|
||||
if (config.exceptURLPatterns == null) {
|
||||
config.exceptURLPatterns = []
|
||||
}
|
||||
return {
|
||||
config: config,
|
||||
moreOptionsVisible: false,
|
||||
minQPSPerIP: config.minQPSPerIP,
|
||||
keyLife: config.keyLife
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
minQPSPerIP: function (v) {
|
||||
let qps = parseInt(v.toString())
|
||||
if (isNaN(qps) || qps < 0) {
|
||||
qps = 0
|
||||
}
|
||||
this.config.minQPSPerIP = qps
|
||||
},
|
||||
keyLife: function (v) {
|
||||
let keyLife = parseInt(v)
|
||||
if (isNaN(keyLife) || keyLife <= 0) {
|
||||
keyLife = 0
|
||||
}
|
||||
this.config.keyLife = keyLife
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showMoreOptions: function () {
|
||||
this.moreOptionsVisible = !this.moreOptionsVisible
|
||||
},
|
||||
changeConds: function (conds) {
|
||||
this.config.conds = conds
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="uamJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="config" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="((!vIsLocation && !vIsGroup) || config.isPrior)">
|
||||
<tr>
|
||||
<td class="title">启用5秒盾</td>
|
||||
<td>
|
||||
<checkbox v-model="config.isOn"></checkbox>
|
||||
<p class="comment"><plus-label></plus-label>启用后,访问网站时,自动检查浏览器环境,阻止非正常访问。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="config.isOn">
|
||||
<tr>
|
||||
<td colspan="2"><more-options-indicator @change="showMoreOptions"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="moreOptionsVisible && config.isOn">
|
||||
<tr>
|
||||
<td>验证有效期</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" name="keyLife" v-model="keyLife" maxlength="6" size="6" style="width: 6em"/>
|
||||
<span class="ui label">秒</span>
|
||||
</div>
|
||||
<p class="comment">单个客户端验证通过后,在这个有效期内不再重复验证;如果为0则表示系统默认。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>单IP最低QPS</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" name="minQPSPerIP" maxlength="6" style="width: 6em" v-model="minQPSPerIP"/>
|
||||
<span class="ui label">请求数/秒</span>
|
||||
</div>
|
||||
<p class="comment">当某个IP在1分钟内平均QPS达到此值时,才会触发5秒盾;如果设置为0,表示任何访问都会触发。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>加入IP白名单</td>
|
||||
<td>
|
||||
<checkbox v-model="config.addToWhiteList"></checkbox>
|
||||
<p class="comment">选中后,表示验证通过后,将访问者IP加入到临时白名单中,此IP下次访问时不再校验5秒盾;此白名单只对5秒盾有效,不影响其他规则。此选项主要用于可能无法正常使用Cookie的网站。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>例外URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.exceptURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了例外URL,表示这些URL跳过5秒盾不做处理。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>限制URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.onlyURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了限制URL,表示只对这些URL进行5秒盾处理;如果不填则表示支持所有的URL。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>匹配条件</td>
|
||||
<td>
|
||||
<http-request-conds-box :v-conds="config.conds" @change="changeConds"></http-request-conds-box>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
@@ -0,0 +1,179 @@
|
||||
Vue.component("user-agent-config-box", {
|
||||
props: ["v-is-location", "v-is-group", "value"],
|
||||
data: function () {
|
||||
let config = this.value
|
||||
if (config == null) {
|
||||
config = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
filters: []
|
||||
}
|
||||
}
|
||||
if (config.filters == null) {
|
||||
config.filters = []
|
||||
}
|
||||
return {
|
||||
config: config,
|
||||
isAdding: false,
|
||||
addingFilter: {
|
||||
keywords: [],
|
||||
action: "deny"
|
||||
},
|
||||
moreOptionsVisible: false,
|
||||
batchKeywords: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isOn: function () {
|
||||
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
|
||||
},
|
||||
remove: function (index) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要删除此名单吗?", function () {
|
||||
that.config.filters.$remove(index)
|
||||
})
|
||||
},
|
||||
add: function () {
|
||||
this.isAdding = true
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.batchKeywords.focus()
|
||||
})
|
||||
},
|
||||
confirm: function () {
|
||||
if (this.addingFilter.action == "deny") {
|
||||
this.config.filters.push(this.addingFilter)
|
||||
} else {
|
||||
let index = -1
|
||||
this.config.filters.forEach(function (filter, filterIndex) {
|
||||
if (filter.action == "allow") {
|
||||
index = filterIndex
|
||||
}
|
||||
})
|
||||
|
||||
if (index < 0) {
|
||||
this.config.filters.unshift(this.addingFilter)
|
||||
} else {
|
||||
this.config.filters.$insert(index + 1, this.addingFilter)
|
||||
}
|
||||
}
|
||||
|
||||
this.cancel()
|
||||
},
|
||||
cancel: function () {
|
||||
this.isAdding = false
|
||||
this.addingFilter = {
|
||||
keywords: [],
|
||||
action: "deny"
|
||||
}
|
||||
this.batchKeywords = ""
|
||||
},
|
||||
changeKeywords: function (keywords) {
|
||||
let arr = keywords.split(/\n/)
|
||||
let resultKeywords = []
|
||||
arr.forEach(function (keyword){
|
||||
keyword = keyword.trim()
|
||||
if (!resultKeywords.$contains(keyword)) {
|
||||
resultKeywords.push(keyword)
|
||||
}
|
||||
})
|
||||
this.addingFilter.keywords = resultKeywords
|
||||
},
|
||||
showMoreOptions: function () {
|
||||
this.moreOptionsVisible = !this.moreOptionsVisible
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<input type="hidden" name="userAgentJSON" :value="JSON.stringify(config)"/>
|
||||
<table class="ui table definition selectable">
|
||||
<prior-checkbox :v-config="config" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<tbody v-show="(!vIsLocation && !vIsGroup) || config.isPrior">
|
||||
<tr>
|
||||
<td class="title">启用UA名单</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" value="1" v-model="config.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">选中后表示开启UserAgent名单。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="isOn()">
|
||||
<tr>
|
||||
<td>UA名单</td>
|
||||
<td>
|
||||
<div v-if="config.filters.length > 0">
|
||||
<table class="ui table celled">
|
||||
<thead class="full-width">
|
||||
<tr>
|
||||
<th>UA关键词</th>
|
||||
<th class="two wide">动作</th>
|
||||
<th class="one op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-for="(filter, index) in config.filters">
|
||||
<tr>
|
||||
<td style="background: white">
|
||||
<span v-for="keyword in filter.keywords" class="ui label basic tiny">
|
||||
<span v-if="keyword.length > 0">{{keyword}}</span>
|
||||
<span v-if="keyword.length == 0" class="disabled">[空]</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="filter.action == 'allow'" class="green">允许</span><span v-if="filter.action == 'deny'" class="red">不允许</span>
|
||||
</td>
|
||||
<td><a href="" @click.prevent="remove(index)">删除</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-if="isAdding" style="margin-top: 0.5em">
|
||||
<table class="ui table definition">
|
||||
<tr>
|
||||
<td class="title">UA关键词</td>
|
||||
<td>
|
||||
<textarea v-model="batchKeywords" @input="changeKeywords(batchKeywords)" ref="batchKeywords" style="width: 20em" placeholder="*浏览器标识*"></textarea>
|
||||
<p class="comment">每行一个关键词;不区分大小写,比如<code-label>Chrome</code-label>;支持<code-label>*</code-label>通配符,比如<code-label>*Firefox*</code-label>;也支持空行关键词,表示空UserAgent。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>动作</td>
|
||||
<td><select class="ui dropdown auto-width" v-model="addingFilter.action">
|
||||
<option value="deny">不允许</option>
|
||||
<option value="allow">允许</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button type="button" class="ui button tiny" @click.prevent="confirm">保存</button> <a href="" @click.prevent="cancel" title="取消"><i class="icon remove small"></i></a>
|
||||
</div>
|
||||
<div v-show="!isAdding" style="margin-top: 0.5em">
|
||||
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><more-options-indicator @change="showMoreOptions"></more-options-indicator></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-show="moreOptionsVisible && isOn()">
|
||||
<tr>
|
||||
<td>例外URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.exceptURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了例外URL,表示这些URL跳过不做处理。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>限制URL</td>
|
||||
<td>
|
||||
<url-patterns-box v-model="config.onlyURLPatterns"></url-patterns-box>
|
||||
<p class="comment">如果填写了限制URL,表示只对这些URL进行处理;如果不填则表示支持所有的URL。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="margin"></div>
|
||||
</div>`
|
||||
})
|
||||
35
EdgeAdmin/web/public/js/components/server/user-selector.js
Normal file
35
EdgeAdmin/web/public/js/components/server/user-selector.js
Normal file
@@ -0,0 +1,35 @@
|
||||
Vue.component("user-selector", {
|
||||
props: ["v-user-id", "data-url"],
|
||||
data: function () {
|
||||
let userId = this.vUserId
|
||||
if (userId == null) {
|
||||
userId = 0
|
||||
}
|
||||
|
||||
let dataURL = this.dataUrl
|
||||
if (dataURL == null || dataURL.length == 0) {
|
||||
dataURL = "/servers/users/options"
|
||||
}
|
||||
|
||||
return {
|
||||
users: [],
|
||||
userId: userId,
|
||||
dataURL: dataURL
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change: function(item) {
|
||||
if (item != null) {
|
||||
this.$emit("change", item.id)
|
||||
} else {
|
||||
this.$emit("change", 0)
|
||||
}
|
||||
},
|
||||
clear: function () {
|
||||
this.$refs.comboBox.clear()
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<combo-box placeholder="选择用户" :data-url="dataURL" :data-key="'users'" data-search="on" name="userId" :v-value="userId" @change="change" ref="comboBox"></combo-box>
|
||||
</div>`
|
||||
})
|
||||
Reference in New Issue
Block a user