Vue.component("ad-instance-objects-box", {
props: ["v-objects", "v-user-id"],
mounted: function () {
this.getUserServers(1)
},
data: function () {
let objects = this.vObjects
if (objects == null) {
objects = []
}
let objectCodes = []
objects.forEach(function (v) {
objectCodes.push(v.code)
})
return {
userId: this.vUserId,
objects: objects,
objectCodes: objectCodes,
isAdding: true,
servers: [],
serversIsLoading: false
}
},
methods: {
add: function () {
this.isAdding = true
},
cancel: function () {
this.isAdding = false
},
remove: function (index) {
let that = this
teaweb.confirm("确定要删除此防护对象吗?", function () {
that.objects.$remove(index)
that.notifyChange()
})
},
removeObjectCode: function (objectCode) {
let index = -1
this.objectCodes.forEach(function (v, k) {
if (objectCode == v) {
index = k
}
})
if (index >= 0) {
this.objects.$remove(index)
this.notifyChange()
}
},
getUserServers: function (page) {
if (Tea.Vue == null) {
let that = this
setTimeout(function () {
that.getUserServers(page)
}, 100)
return
}
let that = this
this.serversIsLoading = true
Tea.Vue.$post(".userServers")
.params({
userId: this.userId,
page: page,
pageSize: 5
})
.success(function (resp) {
that.servers = resp.data.servers
that.$refs.serverPage.updateMax(resp.data.page.max)
that.serversIsLoading = false
})
.error(function () {
that.serversIsLoading = false
})
},
changeServerPage: function (page) {
this.getUserServers(page)
},
selectServerObject: function (server) {
if (this.existObjectCode("server:" + server.id)) {
return
}
this.objects.push({
"type": "server",
"code": "server:" + server.id,
"id": server.id,
"name": server.name
})
this.notifyChange()
},
notifyChange: function () {
let objectCodes = []
this.objects.forEach(function (v) {
objectCodes.push(v.code)
})
this.objectCodes = objectCodes
},
existObjectCode: function (objectCode) {
let found = false
this.objects.forEach(function (v) {
if (v.code == objectCode) {
found = true
}
})
return found
}
},
template: `
对象类型
网站
网站列表
加载中...
暂时还没有可选的网站。
网站名称
操作
{{server.name}}
选中
取消
+
`
})
Vue.component("bandwidth-size-capacity-box", {
props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"],
data: function () {
let v = this.vValue
if (v == null) {
v = {
count: this.vCount,
unit: this.vUnit
}
}
if (v.unit == null || v.unit.length == 0) {
v.unit = "mb"
}
if (typeof (v["count"]) != "number") {
v["count"] = -1
}
let vSize = this.size
if (vSize == null) {
vSize = 6
}
let vMaxlength = this.maxlength
if (vMaxlength == null) {
vMaxlength = 10
}
let supportedUnits = this.vSupportedUnits
if (supportedUnits == null) {
supportedUnits = []
}
return {
capacity: v,
countString: (v.count >= 0) ? v.count.toString() : "",
vSize: vSize,
vMaxlength: vMaxlength,
supportedUnits: supportedUnits
}
},
watch: {
"countString": function (newValue) {
let value = newValue.trim()
if (value.length == 0) {
this.capacity.count = -1
this.change()
return
}
let count = parseInt(value)
if (!isNaN(count)) {
this.capacity.count = count
}
this.change()
}
},
methods: {
change: function () {
this.$emit("change", this.capacity)
}
},
template: ``
})
Vue.component("bandwidth-size-capacity-view", {
props: ["v-value"],
data: function () {
let capacity = this.vValue
if (capacity != null && capacity.count > 0 && typeof capacity.unit === "string") {
capacity.unit = capacity.unit[0].toUpperCase() + capacity.unit.substring(1) + "ps"
}
return {
capacity: capacity
}
},
template: `
{{capacity.count}}{{capacity.unit}}
`
})
Vue.component("bits-var", {
props: ["v-bits"],
data: function () {
let bits = this.vBits
if (typeof bits != "number") {
bits = 0
}
let format = teaweb.splitFormat(teaweb.formatBits(bits))
return {
format: format
}
},
template:`
{{format[0]}} {{format[1]}}
`
})
Vue.component("bytes-var", {
props: ["v-bytes"],
data: function () {
let bytes = this.vBytes
if (typeof bytes != "number") {
bytes = 0
}
let format = teaweb.splitFormat(teaweb.formatBytes(bytes))
return {
format: format
}
},
template:`
{{format[0]}} {{format[1]}}
`
})
let checkboxId = 0
Vue.component("checkbox", {
props: ["name", "value", "v-value", "id", "checked"],
data: function () {
checkboxId++
let elementId = this.id
if (elementId == null) {
elementId = "checkbox" + checkboxId
}
let elementValue = this.vValue
if (elementValue == null) {
elementValue = "1"
}
let checkedValue = this.value
if (checkedValue == null && this.checked == "checked") {
checkedValue = elementValue
}
return {
elementId: elementId,
elementValue: elementValue,
newValue: checkedValue
}
},
methods: {
change: function () {
this.$emit("input", this.newValue)
},
check: function () {
this.newValue = this.elementValue
},
uncheck: function () {
this.newValue = ""
},
isChecked: function () {
return (typeof (this.newValue) == "boolean" && this.newValue) || this.newValue == this.elementValue
}
},
watch: {
value: function (v) {
if (typeof v == "boolean") {
this.newValue = v
}
}
},
template: `
`
})
Vue.component("columns-grid", {
props: [],
mounted: function () {
this.columns = this.calculateColumns()
let that = this
window.addEventListener("resize", function () {
that.columns = that.calculateColumns()
})
},
data: function () {
return {
columns: "four"
}
},
methods: {
calculateColumns: function () {
let w = window.innerWidth
let columns = Math.floor(w / 250)
if (columns == 0) {
columns = 1
}
let columnElements = this.$el.getElementsByClassName("column")
if (columnElements.length == 0) {
return
}
let maxColumns = columnElements.length
if (columns > maxColumns) {
columns = maxColumns
}
// 添加右侧边框
for (let index = 0; index < columnElements.length; index++) {
let el = columnElements[index]
el.className = el.className.replace("with-border", "")
if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) {
el.className += " with-border"
}
}
switch (columns) {
case 1:
return "one"
case 2:
return "two"
case 3:
return "three"
case 4:
return "four"
case 5:
return "five"
case 6:
return "six"
case 7:
return "seven"
case 8:
return "eight"
case 9:
return "nine"
case 10:
return "ten"
default:
return "ten"
}
}
},
template: `
`
})
Vue.component("combo-box", {
// data-url 和 data-key 成对出现
props: [
"name", "title", "placeholder", "size", "v-items", "v-value",
"data-url", // 数据源URL
"data-key", // 数据源中数据的键名
"data-search", // 是否启用动态搜索,如果值为on或true,则表示启用
"width"
],
mounted: function () {
if (this.dataURL.length > 0) {
this.search("")
}
// 设定菜单宽度
let searchBox = this.$refs.searchBox
if (searchBox != null) {
let inputWidth = searchBox.offsetWidth
if (inputWidth != null && inputWidth > 0) {
this.$refs.menu.style.width = inputWidth + "px"
} else if (this.styleWidth.length > 0) {
this.$refs.menu.style.width = this.styleWidth
}
}
},
data: function () {
let items = this.vItems
if (items == null || !(items instanceof Array)) {
items = []
}
items = this.formatItems(items)
// 当前选中项
let selectedItem = null
if (this.vValue != null) {
let that = this
items.forEach(function (v) {
if (v.value == that.vValue) {
selectedItem = v
}
})
}
let width = this.width
if (width == null || width.length == 0) {
width = "11em"
} else {
if (/\d+$/.test(width)) {
width += "em"
}
}
// data url
let dataURL = ""
if (typeof this.dataUrl == "string" && this.dataUrl.length > 0) {
dataURL = this.dataUrl
}
return {
allItems: items, // 原始的所有的items
items: items.$copy(), // 候选的items
selectedItem: selectedItem, // 选中的item
keyword: "",
visible: false,
hideTimer: null,
hoverIndex: 0,
styleWidth: width,
isInitial: true,
dataURL: dataURL,
urlRequestId: 0 // 记录URL请求ID,防止并行冲突
}
},
methods: {
search: function (keyword) {
// 从URL中获取选项数据
let dataUrl = this.dataURL
let dataKey = this.dataKey
let that = this
let requestId = Math.random()
this.urlRequestId = requestId
Tea.action(dataUrl)
.params({
keyword: (keyword == null) ? "" : keyword
})
.post()
.success(function (resp) {
if (requestId != that.urlRequestId) {
return
}
if (resp.data != null) {
if (typeof (resp.data[dataKey]) == "object") {
let items = that.formatItems(resp.data[dataKey])
that.allItems = items
that.items = items.$copy()
if (that.isInitial) {
that.isInitial = false
if (that.vValue != null) {
items.forEach(function (v) {
if (v.value == that.vValue) {
that.selectedItem = v
}
})
}
}
}
}
})
},
formatItems: function (items) {
items.forEach(function (v) {
if (v.value == null) {
v.value = v.id
}
})
return items
},
reset: function () {
this.selectedItem = null
this.change()
this.hoverIndex = 0
let that = this
setTimeout(function () {
if (that.$refs.searchBox) {
that.$refs.searchBox.focus()
}
})
},
clear: function () {
this.selectedItem = null
this.change()
this.hoverIndex = 0
},
changeKeyword: function () {
let shouldSearch = this.dataURL.length > 0 && (this.dataSearch == "on" || this.dataSearch == "true")
this.hoverIndex = 0
let keyword = this.keyword
if (keyword.length == 0) {
if (shouldSearch) {
this.search(keyword)
} else {
this.items = this.allItems.$copy()
}
return
}
if (shouldSearch) {
this.search(keyword)
} else {
this.items = this.allItems.$copy().filter(function (v) {
if (v.fullname != null && v.fullname.length > 0 && teaweb.match(v.fullname, keyword)) {
return true
}
return teaweb.match(v.name, keyword)
})
}
},
selectItem: function (item) {
this.selectedItem = item
this.change()
this.hoverIndex = 0
this.keyword = ""
this.changeKeyword()
},
confirm: function () {
if (this.items.length > this.hoverIndex) {
this.selectItem(this.items[this.hoverIndex])
}
},
show: function () {
this.visible = true
// 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换
},
hide: function () {
let that = this
this.hideTimer = setTimeout(function () {
that.visible = false
}, 500)
},
downItem: function () {
this.hoverIndex++
if (this.hoverIndex > this.items.length - 1) {
this.hoverIndex = 0
}
this.focusItem()
},
upItem: function () {
this.hoverIndex--
if (this.hoverIndex < 0) {
this.hoverIndex = 0
}
this.focusItem()
},
focusItem: function () {
if (this.hoverIndex < this.items.length) {
this.$refs.itemRef[this.hoverIndex].focus()
let that = this
setTimeout(function () {
that.$refs.searchBox.focus()
if (that.hideTimer != null) {
clearTimeout(that.hideTimer)
that.hideTimer = null
}
})
}
},
change: function () {
this.$emit("change", this.selectedItem)
let that = this
setTimeout(function () {
if (that.$refs.selectedLabel != null) {
that.$refs.selectedLabel.focus()
}
})
},
submitForm: function (event) {
if (event.target.tagName != "A") {
return
}
let parentBox = this.$refs.selectedLabel.parentNode
while (true) {
parentBox = parentBox.parentNode
if (parentBox == null || parentBox.tagName == "BODY") {
return
}
if (parentBox.tagName == "FORM") {
parentBox.submit()
break
}
}
},
setDataURL: function (dataURL) {
this.dataURL = dataURL
},
reloadData: function () {
this.search("")
}
},
template: ``
})
Vue.component("copy-to-clipboard", {
props: ["v-target"],
created: function () {
if (typeof ClipboardJS == "undefined") {
let jsFile = document.createElement("script")
jsFile.setAttribute("src", "/js/clipboard.min.js")
document.head.appendChild(jsFile)
}
},
methods: {
copy: function () {
new ClipboardJS('[data-clipboard-target]');
teaweb.success("已复制到剪切板")
}
},
template: ` `
})
Vue.component("countries-selector", {
props: ["v-countries"],
data: function () {
let countries = this.vCountries
if (countries == null) {
countries = []
}
let countryIds = countries.$map(function (k, v) {
return v.id
})
return {
countries: countries,
countryIds: countryIds
}
},
methods: {
add: function () {
let countryStringIds = this.countryIds.map(function (v) {
return v.toString()
})
let that = this
teaweb.popup("/ui/selectCountriesPopup?countryIds=" + countryStringIds.join(","), {
width: "48em",
height: "23em",
callback: function (resp) {
that.countries = resp.data.countries
that.change()
}
})
},
remove: function (index) {
this.countries.$remove(index)
this.change()
},
change: function () {
this.countryIds = this.countries.$map(function (k, v) {
return v.id
})
}
},
template: ``
})
Vue.component("csrf-token", {
created: function () {
this.refreshToken()
},
mounted: function () {
let that = this
var form = this.$refs.token.form
// 监听表单提交,在提交前刷新token并确保更新到 DOM
form.addEventListener("submit", function (e) {
// 如果正在刷新,等待刷新完成
if (that.refreshing) {
e.preventDefault()
e.stopPropagation()
return false
}
// 阻止默认提交,先刷新 token
e.preventDefault()
e.stopPropagation()
that.refreshing = true
// 刷新 token
that.refreshToken(function () {
// 确保 DOM 中的 token 值是最新的
that.$forceUpdate()
that.$nextTick(function () {
var tokenInput = form.querySelector('input[name="csrfToken"]')
if (tokenInput) {
tokenInput.value = that.token
}
if (that.$refs.token) {
that.$refs.token.value = that.token
}
// 确保 DOM 已更新后,再触发表单提交
setTimeout(function () {
that.refreshing = false
// 重新触发表单提交
Tea.runActionOn(form)
}, 50)
})
})
return false
})
// 自动刷新
setInterval(function () {
that.refreshToken()
}, 10 * 60 * 1000)
// 监听表单提交失败,如果是 CSRF token 错误,自动刷新 token 并重试
this.setupAutoRetry(form)
},
data: function () {
return {
token: "",
retrying: false,
refreshing: false
}
},
methods: {
refreshToken: function (callback) {
let that = this
Tea.action("/csrf/token")
.get()
.success(function (resp) {
that.token = resp.data.token
if (callback) {
callback()
}
})
.fail(function () {
if (callback) {
callback()
}
})
},
setupAutoRetry: function (form) {
let that = this
var originalFail = form.getAttribute("data-tea-fail")
// 确保 Tea.Vue 存在
if (typeof Tea === "undefined" || Tea.Vue == null) {
if (typeof Tea === "undefined") {
window.Tea = {}
}
if (Tea.Vue == null) {
Tea.Vue = {}
}
}
// 创建一个包装的 fail 函数
var wrappedFailName = "csrfAutoRetryFail_" + Math.random().toString(36).substr(2, 9)
form.setAttribute("data-tea-fail", wrappedFailName)
Tea.Vue[wrappedFailName] = function (resp) {
// 检查是否是 CSRF token 错误
var isCSRFError = false
if (resp && resp.message) {
// 检查消息是否包含 "表单已失效" 或 "001"
if (resp.message.indexOf("表单已失效") >= 0 || resp.message.indexOf("(001)") >= 0) {
isCSRFError = true
}
}
// 检查 HTTP 状态码是否为 403 或 400
if (!isCSRFError && resp && (resp.statusCode === 403 || resp.status === 403 || resp.statusCode === 400 || resp.status === 400)) {
isCSRFError = true
}
if (isCSRFError) {
// 如果不是正在重试,则立即刷新 token 并自动重试
if (!that.retrying) {
that.retrying = true
// 立即刷新 token
that.refreshToken(function () {
// 强制更新 Vue,确保响应式数据已更新
that.$forceUpdate()
// 使用 $nextTick 等待 Vue 完成 DOM 更新
that.$nextTick(function () {
// 直接查找并更新 DOM 中的 input 元素(通过 name 属性)
var tokenInput = form.querySelector('input[name="csrfToken"]')
if (tokenInput) {
tokenInput.value = that.token
}
// 如果 ref 存在,也更新它
if (that.$refs.token) {
that.$refs.token.value = that.token
}
// 使用 setTimeout 确保 DOM 已完全更新
setTimeout(function () {
// 再次确认 token 值已更新
var finalTokenInput = form.querySelector('input[name="csrfToken"]')
if (finalTokenInput && finalTokenInput.value !== that.token) {
finalTokenInput.value = that.token
}
that.retrying = false
// 重新触发表单提交
Tea.runActionOn(form)
}, 150)
})
})
return // 不调用原始 fail 函数
} else {
// 如果正在重试,说明已经刷新过 token,直接调用原始 fail 函数
if (originalFail && typeof Tea.Vue[originalFail] === "function") {
return Tea.Vue[originalFail].call(Tea.Vue, resp)
} else {
Tea.failResponse(resp)
}
}
}
// 不是 CSRF 错误,调用原始 fail 函数或默认处理
if (originalFail && typeof Tea.Vue[originalFail] === "function") {
return Tea.Vue[originalFail].call(Tea.Vue, resp)
} else {
Tea.failResponse(resp)
}
}
}
},
template: ` `
})
Vue.component("datepicker", {
props: ["value", "v-name", "name", "v-value", "v-bottom-left", "placeholder"],
mounted: function () {
let that = this
teaweb.datepicker(this.$refs.dayInput, function (v) {
that.day = v
that.change()
}, !!this.vBottomLeft)
},
data: function () {
let name = this.vName
if (name == null) {
name = this.name
}
if (name == null) {
name = "day"
}
let day = this.vValue
if (day == null) {
day = this.value
if (day == null) {
day = ""
}
}
let placeholder = "YYYY-MM-DD"
if (this.placeholder != null) {
placeholder = this.placeholder
}
return {
realName: name,
realPlaceholder: placeholder,
day: day
}
},
watch: {
value: function (v) {
this.day = v
let picker = this.$refs.dayInput.picker
if (picker != null) {
if (v != null && /^\d+-\d+-\d+$/.test(v)) {
picker.setDate(v)
}
}
}
},
methods: {
change: function () {
this.$emit("input", this.day) // support v-model,事件触发需要在 change 之前
this.$emit("change", this.day)
}
},
template: `
`
})
Vue.component("datetime-input", {
props: ["v-name", "v-timestamp"],
mounted: function () {
let that = this
teaweb.datepicker(this.$refs.dayInput, function (v) {
that.day = v
that.hour = "23"
that.minute = "59"
that.second = "59"
that.change()
})
},
data: function () {
let timestamp = this.vTimestamp
if (timestamp != null) {
timestamp = parseInt(timestamp)
if (isNaN(timestamp)) {
timestamp = 0
}
} else {
timestamp = 0
}
let day = ""
let hour = ""
let minute = ""
let second = ""
if (timestamp > 0) {
let date = new Date()
date.setTime(timestamp * 1000)
let year = date.getFullYear().toString()
let month = this.leadingZero((date.getMonth() + 1).toString(), 2)
day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2)
hour = this.leadingZero(date.getHours().toString(), 2)
minute = this.leadingZero(date.getMinutes().toString(), 2)
second = this.leadingZero(date.getSeconds().toString(), 2)
}
return {
timestamp: timestamp,
day: day,
hour: hour,
minute: minute,
second: second,
hasDayError: false,
hasHourError: false,
hasMinuteError: false,
hasSecondError: false
}
},
methods: {
change: function () {
// day
if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) {
this.hasDayError = true
return
}
let pieces = this.day.split("-")
let year = parseInt(pieces[0])
let month = parseInt(pieces[1])
if (month < 1 || month > 12) {
this.hasDayError = true
return
}
let day = parseInt(pieces[2])
if (day < 1 || day > 32) {
this.hasDayError = true
return
}
this.hasDayError = false
// hour
if (!/^\d+$/.test(this.hour)) {
this.hasHourError = true
return
}
let hour = parseInt(this.hour)
if (isNaN(hour)) {
this.hasHourError = true
return
}
if (hour < 0 || hour >= 24) {
this.hasHourError = true
return
}
this.hasHourError = false
// minute
if (!/^\d+$/.test(this.minute)) {
this.hasMinuteError = true
return
}
let minute = parseInt(this.minute)
if (isNaN(minute)) {
this.hasMinuteError = true
return
}
if (minute < 0 || minute >= 60) {
this.hasMinuteError = true
return
}
this.hasMinuteError = false
// second
if (!/^\d+$/.test(this.second)) {
this.hasSecondError = true
return
}
let second = parseInt(this.second)
if (isNaN(second)) {
this.hasSecondError = true
return
}
if (second < 0 || second >= 60) {
this.hasSecondError = true
return
}
this.hasSecondError = false
let date = new Date(year, month - 1, day, hour, minute, second)
this.timestamp = Math.floor(date.getTime() / 1000)
},
leadingZero: function (s, l) {
s = s.toString()
if (l <= s.length) {
return s
}
for (let i = 0; i < l - s.length; i++) {
s = "0" + s
}
return s
},
resultTimestamp: function () {
return this.timestamp
},
nextYear: function () {
let date = new Date()
date.setFullYear(date.getFullYear()+1)
this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2)
this.hour = this.leadingZero(date.getHours(), 2)
this.minute = this.leadingZero(date.getMinutes(), 2)
this.second = this.leadingZero(date.getSeconds(), 2)
this.change()
},
nextDays: function (days) {
let date = new Date()
date.setTime(date.getTime() + days * 86400 * 1000)
this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2)
this.hour = this.leadingZero(date.getHours(), 2)
this.minute = this.leadingZero(date.getMinutes(), 2)
this.second = this.leadingZero(date.getSeconds(), 2)
this.change()
},
nextHours: function (hours) {
let date = new Date()
date.setTime(date.getTime() + hours * 3600 * 1000)
this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2)
this.hour = this.leadingZero(date.getHours(), 2)
this.minute = this.leadingZero(date.getMinutes(), 2)
this.second = this.leadingZero(date.getSeconds(), 2)
this.change()
}
},
template: ``
})
Vue.component("download-link", {
props: ["v-element", "v-file"],
created: function () {
let that = this
setTimeout(function () {
that.url = that.composeURL()
}, 1000)
},
data: function () {
let filename = this.vFile
if (filename == null || filename.length == 0) {
filename = "unknown-file"
}
return {
file: filename,
url: this.composeURL()
}
},
methods: {
composeURL: function () {
let e = document.getElementById(this.vElement)
if (e == null) {
teaweb.warn("找不到要下载的内容")
return
}
let text = e.innerText
if (text == null) {
text = e.textContent
}
return Tea.url("/ui/download", {
file: this.file,
text: text
})
}
},
template: ` `,
})
Vue.component("file-textarea", {
props: ["value"],
data: function () {
let value = this.value
if (typeof value != "string") {
value = ""
}
return {
realValue: value
}
},
mounted: function () {
},
methods: {
dragover: function () {},
drop: function (e) {
let that = this
e.dataTransfer.items[0].getAsFile().text().then(function (data) {
that.setValue(data)
})
},
setValue: function (value) {
this.realValue = value
},
focus: function () {
this.$refs.textarea.focus()
}
},
template: ``
})
/**
* 一级菜单
*/
Vue.component("first-menu", {
props: [],
template: ' \
'
});
/**
* 菜单项
*/
Vue.component("inner-menu-item", {
props: ["href", "active", "code"],
data: function () {
var active = this.active;
if (typeof(active) =="undefined") {
var itemCode = "";
if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") {
itemCode = window.TEA.ACTION.data.firstMenuItem;
}
active = (itemCode == this.code);
}
return {
vHref: (this.href == null) ? "" : this.href,
vActive: active
};
},
template: '\
[ ] \
'
});
/**
* 二级菜单
*/
Vue.component("inner-menu", {
template: `
`
});
Vue.component("js-page", {
props: ["v-max"],
data: function () {
let max = this.vMax
if (max == null) {
max = 0
}
return {
max: max,
page: 1
}
},
methods: {
updateMax: function (max) {
this.max = max
},
selectPage: function(page) {
this.page = page
this.$emit("change", page)
}
},
template:``
})
Vue.component("keyword", {
props: ["v-word"],
data: function () {
let word = this.vWord
if (word == null) {
word = ""
} else {
word = word.replace(/\)/g, "\\)")
word = word.replace(/\(/g, "\\(")
word = word.replace(/\+/g, "\\+")
word = word.replace(/\^/g, "\\^")
word = word.replace(/\$/g, "\\$")
word = word.replace(/\?/g, "\\?")
word = word.replace(/\*/g, "\\*")
word = word.replace(/\[/g, "\\[")
word = word.replace(/{/g, "\\{")
word = word.replace(/\./g, "\\.")
}
let slot = this.$slots["default"][0]
let text = slot.text
if (word.length > 0) {
let that = this
let m = [] // replacement => tmp
let tmpIndex = 0
text = text.replaceAll(new RegExp("(" + word + ")", "ig"), function (replacement) {
tmpIndex++
let s = "" + that.encodeHTML(replacement) + " "
let tmpKey = "$TMP__KEY__" + tmpIndex.toString() + "$"
m.push([tmpKey, s])
return tmpKey
})
text = this.encodeHTML(text)
m.forEach(function (r) {
text = text.replace(r[0], r[1])
})
} else {
text = this.encodeHTML(text)
}
return {
word: word,
text: text
}
},
methods: {
encodeHTML: function (s) {
s = s.replace(/&/g, "&")
s = s.replace(//g, ">")
s = s.replace(/"/g, """)
return s
}
},
template: ` `
})
Vue.component("labeled-input", {
props: ["name", "size", "maxlength", "label", "value"],
template: ' \
\
{{label}} \
'
});
// 启用状态标签
Vue.component("label-on", {
props: ["v-is-on"],
template: '已启用 已停用
'
})
// 文字代码标签
Vue.component("code-label", {
methods: {
click: function (args) {
this.$emit("click", args)
}
},
template: ` `
})
// tiny标签
Vue.component("tiny-label", {
template: ` `
})
Vue.component("tiny-basic-label", {
template: ` `
})
// 更小的标签
Vue.component("micro-basic-label", {
template: ` `
})
// 灰色的Label
Vue.component("grey-label", {
template: ` `
})
// Plus专属
Vue.component("plus-label", {
template: ` `
})
// 使用Icon的链接方式
Vue.component("link-icon", {
props: ["href", "title"],
data: function () {
return {
vTitle: (this.title == null) ? "打开链接" : this.title
}
},
template: ` `
})
// 带有下划虚线的连接
Vue.component("link-red", {
props: ["href", "title"],
data: function () {
let href = this.href
if (href == null) {
href = ""
}
return {
vHref: href
}
},
methods: {
clickPrevent: function () {
emitClick(this, arguments)
}
},
template: ` `
})
// 会弹出窗口的链接
Vue.component("link-popup", {
props: ["title"],
methods: {
clickPrevent: function () {
emitClick(this, arguments)
}
},
template: ` `
})
Vue.component("popup-icon", {
props: ["title", "href", "height"],
methods: {
clickPrevent: function () {
teaweb.popup(this.href, {
height: this.height
})
}
},
template: ` `
})
// 小提示
Vue.component("tip-icon", {
props: ["content"],
methods: {
showTip: function () {
teaweb.popupTip(this.content)
}
},
template: ` `
})
// 提交点击事件
function emitClick(obj, arguments) {
let event = "click"
let newArgs = [event]
for (let i = 0; i < arguments.length; i++) {
newArgs.push(arguments[i])
}
obj.$emit.apply(obj, newArgs)
}
/**
* 菜单项
*/
Vue.component("menu-item", {
props: ["href", "active", "code"],
data: function () {
let active = this.active
if (typeof (active) == "undefined") {
var itemCode = ""
if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") {
itemCode = window.TEA.ACTION.data.firstMenuItem
}
if (itemCode != null && itemCode.length > 0 && this.code != null && this.code.length > 0) {
if (itemCode.indexOf(",") > 0) {
active = itemCode.split(",").$contains(this.code)
} else {
active = (itemCode == this.code)
}
}
}
let href = (this.href == null) ? "" : this.href
if (typeof (href) == "string" && href.length > 0 && href.startsWith(".")) {
let qIndex = href.indexOf("?")
if (qIndex >= 0) {
href = Tea.url(href.substring(0, qIndex)) + href.substring(qIndex)
} else {
href = Tea.url(href)
}
}
return {
vHref: href,
vActive: active
}
},
methods: {
click: function (e) {
this.$emit("click", e)
}
},
template: '\
\
'
});
Vue.component("more-options-angle", {
data: function () {
return {
isVisible: false
}
},
methods: {
show: function () {
this.isVisible = !this.isVisible
this.$emit("change", this.isVisible)
}
},
template: `更多选项 收起选项 `
})
/**
* 更多选项
*/
Vue.component("more-options-indicator", {
data: function () {
return {
visible: false
}
},
methods: {
changeVisible: function () {
this.visible = !this.visible
if (Tea.Vue != null) {
Tea.Vue.moreOptionsVisible = this.visible
}
this.$emit("change", this.visible)
this.$emit("input", this.visible)
}
},
template: '更多选项 收起选项 '
});
Vue.component("more-options-tbody", {
data: function () {
return {
isVisible: false
}
},
methods: {
show: function () {
this.isVisible = !this.isVisible
this.$emit("change", this.isVisible)
}
},
template: `
更多选项 收起选项
`
})
Vue.component("network-addresses-box", {
props: ["v-server-type", "v-addresses", "v-protocol", "v-name"],
data: function () {
let addresses = this.vAddresses
if (addresses == null) {
addresses = []
}
let protocol = this.vProtocol
if (protocol == null) {
protocol = ""
}
let name = this.vName
if (name == null) {
name = "addresses"
}
return {
addresses: addresses,
protocol: protocol,
name: name
}
},
watch: {
"vServerType": function () {
this.addresses = []
}
},
methods: {
addAddr: function () {
let that = this
window.UPDATING_ADDR = null
teaweb.popup("/servers/addPortPopup?serverType=" + this.vServerType + "&protocol=" + this.protocol, {
height: "16em",
callback: function (resp) {
var addr = resp.data.address
that.addresses.push(addr)
if (["https", "https4", "https6"].$contains(addr.protocol)) {
this.tlsProtocolName = "HTTPS"
} else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) {
this.tlsProtocolName = "TLS"
}
// 发送事件
that.$emit("change", that.addresses)
}
})
},
removeAddr: function (index) {
this.addresses.$remove(index);
// 发送事件
this.$emit("change", this.addresses)
},
updateAddr: function (index, addr) {
let that = this
window.UPDATING_ADDR = addr
teaweb.popup("/servers/addPortPopup?serverType=" + this.vServerType + "&protocol=" + this.protocol, {
height: "16em",
callback: function (resp) {
var addr = resp.data.address
Vue.set(that.addresses, index, addr)
if (["https", "https4", "https6"].$contains(addr.protocol)) {
this.tlsProtocolName = "HTTPS"
} else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) {
this.tlsProtocolName = "TLS"
}
// 发送事件
that.$emit("change", that.addresses)
}
})
// 发送事件
this.$emit("change", this.addresses)
}
},
template: `
{{addr.protocol}}://
{{addr.host}} * :{{addr.portRange}}
[添加端口绑定]
`
})
Vue.component("network-addresses-view", {
props: ["v-addresses"],
template: `
{{addr.protocol}}://{{addr.host}}:{{addr.portRange}}
`
})
Vue.component("not-found-box", {
props: ["message"],
template: ``
})
Vue.component("page-box", {
data: function () {
return {
page: ""
}
},
created: function () {
let that = this;
setTimeout(function () {
if (Tea && Tea.Vue && Tea.Vue.page) {
that.page = Tea.Vue.page;
}
})
},
template: ``
})
Vue.component("provinces-selector", {
props: ["v-provinces"],
data: function () {
let provinces = this.vProvinces
if (provinces == null) {
provinces = []
}
let provinceIds = provinces.$map(function (k, v) {
return v.id
})
return {
provinces: provinces,
provinceIds: provinceIds
}
},
methods: {
add: function () {
let provinceStringIds = this.provinceIds.map(function (v) {
return v.toString()
})
let that = this
teaweb.popup("/ui/selectProvincesPopup?provinceIds=" + provinceStringIds.join(","), {
width: "48em",
height: "23em",
callback: function (resp) {
that.provinces = resp.data.provinces
that.change()
}
})
},
remove: function (index) {
this.provinces.$remove(index)
this.change()
},
change: function () {
this.provinceIds = this.provinces.$map(function (k, v) {
return v.id
})
}
},
template: ``
})
let radioId = 0
Vue.component("radio", {
props: ["name", "value", "v-value", "id"],
data: function () {
radioId++
let elementId = this.id
if (elementId == null) {
elementId = "radio" + radioId
}
return {
"elementId": elementId
}
},
methods: {
change: function () {
this.$emit("input", this.vValue)
}
},
template: `
`
})
// 将变量转换为中文
Vue.component("request-variables-describer", {
data: function () {
return {
vars:[]
}
},
methods: {
update: function (variablesString) {
this.vars = []
let that = this
variablesString.replace(/\${.+?}/g, function (v) {
let def = that.findVar(v)
if (def == null) {
return v
}
that.vars.push(def)
})
},
findVar: function (name) {
let def = null
window.REQUEST_VARIABLES.forEach(function (v) {
if (v.code == name) {
def = v
}
})
return def
}
},
template: `
{{v.code}} - {{v.name}};
`
})
Vue.component("search-box", {
props: ["placeholder", "width"],
data: function () {
let width = this.width
if (width == null) {
width = "10em"
}
return {
realWidth: width,
realValue: ""
}
},
methods: {
onInput: function () {
this.$emit("input", { value: this.realValue})
this.$emit("change", { value: this.realValue})
},
clearValue: function () {
this.realValue = ""
this.focus()
this.onInput()
},
focus: function () {
this.$refs.valueRef.focus()
}
},
template: ``
})
/**
* 二级菜单
*/
Vue.component("second-menu", {
template: ' \
'
});
Vue.component("server-group-selector", {
props: ["v-groups"],
data: function () {
let groups = this.vGroups
if (groups == null) {
groups = []
}
return {
groups: groups
}
},
methods: {
selectGroup: function () {
let that = this
let groupIds = this.groups.map(function (v) {
return v.id.toString()
}).join(",")
teaweb.popup("/servers/groups/selectPopup?selectedGroupIds=" + groupIds, {
callback: function (resp) {
that.groups.push(resp.data.group)
}
})
},
addGroup: function () {
let that = this
teaweb.popup("/servers/groups/createPopup", {
callback: function (resp) {
that.groups.push(resp.data.group)
}
})
},
removeGroup: function (index) {
this.groups.$remove(index)
},
groupIds: function () {
return this.groups.map(function (v) {
return v.id
})
}
},
template: ``
})
Vue.component("size-capacity-box", {
props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength"],
data: function () {
let v = this.vValue
if (v == null) {
v = {
count: this.vCount,
unit: this.vUnit
}
}
if (typeof (v["count"]) != "number") {
v["count"] = -1
}
let vSize = this.size
if (vSize == null) {
vSize = 6
}
let vMaxlength = this.maxlength
if (vMaxlength == null) {
vMaxlength = 10
}
return {
capacity: v,
countString: (v.count >= 0) ? v.count.toString() : "",
vSize: vSize,
vMaxlength: vMaxlength
}
},
watch: {
"countString": function (newValue) {
let value = newValue.trim()
if (value.length == 0) {
this.capacity.count = -1
this.change()
return
}
let count = parseInt(value)
if (!isNaN(count)) {
this.capacity.count = count
}
this.change()
}
},
methods: {
change: function () {
this.$emit("change", this.capacity)
}
},
template: ``
})
Vue.component("size-capacity-view", {
props:["v-default-text", "v-value"],
methods: {
composeCapacity: function (capacity) {
return teaweb.convertSizeCapacityToString(capacity)
}
},
template: `
{{composeCapacity(vValue)}}
{{vDefaultText}}
`
})
// 排序使用的箭头
Vue.component("sort-arrow", {
props: ["name"],
data: function () {
let url = window.location.toString()
let order = ""
let iconTitle = ""
let newArgs = []
if (window.location.search != null && window.location.search.length > 0) {
let queryString = window.location.search.substring(1)
let pieces = queryString.split("&")
let that = this
pieces.forEach(function (v) {
let eqIndex = v.indexOf("=")
if (eqIndex > 0) {
let argName = v.substring(0, eqIndex)
let argValue = v.substring(eqIndex + 1)
if (argName == that.name) {
order = argValue
} else if (argName != "page" && argValue != "asc" && argValue != "desc") {
newArgs.push(v)
}
} else {
newArgs.push(v)
}
})
}
if (order == "asc") {
newArgs.push(this.name + "=desc")
iconTitle = "当前正序排列"
} else if (order == "desc") {
newArgs.push(this.name + "=asc")
iconTitle = "当前倒序排列"
} else {
newArgs.push(this.name + "=desc")
iconTitle = "当前正序排列"
}
let qIndex = url.indexOf("?")
if (qIndex > 0) {
url = url.substring(0, qIndex) + "?" + newArgs.join("&")
} else {
url = url + "?" + newArgs.join("&")
}
return {
order: order,
url: url,
iconTitle: iconTitle
}
},
template: ` `
})
// 给Table增加排序功能
function sortTable(callback) {
// 引入js
let jsFile = document.createElement("script")
jsFile.setAttribute("src", "/js/sortable.min.js")
jsFile.addEventListener("load", function () {
// 初始化
let box = document.querySelector("#sortable-table")
if (box == null) {
return
}
Sortable.create(box, {
draggable: "tbody",
handle: ".icon.handle",
onStart: function () {
},
onUpdate: function (event) {
let rows = box.querySelectorAll("tbody")
let rowIds = []
rows.forEach(function (row) {
rowIds.push(parseInt(row.getAttribute("v-id")))
})
callback(rowIds)
}
})
})
document.head.appendChild(jsFile)
}
function sortLoad(callback) {
let jsFile = document.createElement("script")
jsFile.setAttribute("src", "/js/sortable.min.js")
jsFile.addEventListener("load", function () {
if (typeof (callback) == "function") {
callback()
}
})
document.head.appendChild(jsFile)
}
let sourceCodeBoxIndex = 0
Vue.component("source-code-box", {
props: ["name", "type", "id", "read-only", "width", "height", "focus"],
mounted: function () {
let readOnly = this.readOnly
if (typeof readOnly != "boolean") {
readOnly = true
}
let box = document.getElementById("source-code-box-" + this.index)
let valueBox = document.getElementById(this.valueBoxId)
let value = ""
if (valueBox.textContent != null) {
value = valueBox.textContent
} else if (valueBox.innerText != null) {
value = valueBox.innerText
}
this.createEditor(box, value, readOnly)
},
data: function () {
let index = sourceCodeBoxIndex++
let valueBoxId = 'source-code-box-value-' + sourceCodeBoxIndex
if (this.id != null) {
valueBoxId = this.id
}
return {
index: index,
valueBoxId: valueBoxId
}
},
methods: {
createEditor: function (box, value, readOnly) {
let boxEditor = CodeMirror.fromTextArea(box, {
theme: "idea",
lineNumbers: true,
value: "",
readOnly: readOnly,
showCursorWhenSelecting: true,
height: "auto",
//scrollbarStyle: null,
viewportMargin: Infinity,
lineWrapping: true,
highlightFormatting: false,
indentUnit: 4,
indentWithTabs: true,
})
let that = this
boxEditor.on("change", function () {
that.change(boxEditor.getValue())
})
boxEditor.setValue(value)
if (this.focus) {
boxEditor.focus()
}
let width = this.width
let height = this.height
if (width != null && height != null) {
width = parseInt(width)
height = parseInt(height)
if (!isNaN(width) && !isNaN(height)) {
if (width <= 0) {
width = box.parentNode.offsetWidth
}
boxEditor.setSize(width, height)
}
} else if (height != null) {
height = parseInt(height)
if (!isNaN(height)) {
boxEditor.setSize("100%", height)
}
}
let info = CodeMirror.findModeByMIME(this.type)
if (info != null) {
boxEditor.setOption("mode", info.mode)
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js"
CodeMirror.autoLoadMode(boxEditor, info.mode)
}
},
change: function (code) {
this.$emit("change", code)
}
},
template: ``
})
/**
* 保存按钮
*/
Vue.component("submit-btn", {
template: '保存 '
});
Vue.component("theme-color-picker", {
props: ["v-model"],
data: function () {
return {
showPicker: false,
r: 20,
g: 83,
b: 196,
hex: "#14539A",
clickHandler: null
}
},
mounted: function () {
console.log("Theme color picker component mounted, element:", this.$el)
// 从localStorage加载保存的颜色
let savedColor = localStorage.getItem("themeColor")
if (savedColor) {
this.setColor(savedColor)
} else {
// 从当前页面获取默认颜色
let defaultColor = this.getCurrentThemeColor()
if (defaultColor) {
this.setColor(defaultColor)
}
}
// 应用颜色
this.applyColor()
},
beforeDestroy: function() {
// 移除点击监听器
if (this.clickHandler) {
document.removeEventListener("click", this.clickHandler)
this.clickHandler = null
}
},
methods: {
// 打开/关闭颜色选择器
togglePicker: function (e) {
if (e) {
e.preventDefault()
e.stopPropagation()
}
console.log("Toggle picker clicked, current showPicker:", this.showPicker)
this.showPicker = !this.showPicker
console.log("Toggle picker clicked, new showPicker:", this.showPicker)
console.log("Popup should be visible:", this.showPicker)
// 管理外部点击监听器
if (this.showPicker) {
// 打开时,延迟添加监听器
let that = this
setTimeout(function() {
if (!that.clickHandler) {
that.clickHandler = function(e) {
if (that.showPicker && !that.$el.contains(e.target)) {
that.showPicker = false
}
}
document.addEventListener("click", that.clickHandler)
}
}, 100)
} else {
// 关闭时,移除监听器
if (this.clickHandler) {
document.removeEventListener("click", this.clickHandler)
this.clickHandler = null
}
}
},
// RGB转16进制
rgbToHex: function (r, g, b) {
return "#" + [r, g, b].map(function (x) {
x = parseInt(x)
const hex = x.toString(16)
return hex.length === 1 ? "0" + hex : hex
}).join("").toUpperCase()
},
// 16进制转RGB
hexToRgb: function (hex) {
hex = hex.replace("#", "")
const r = parseInt(hex.substring(0, 2), 16)
const g = parseInt(hex.substring(2, 4), 16)
const b = parseInt(hex.substring(4, 6), 16)
return { r: r, g: g, b: b }
},
// 设置颜色(支持hex或rgb对象)
setColor: function (color) {
if (typeof color === "string") {
if (color.startsWith("#")) {
let rgb = this.hexToRgb(color)
this.r = rgb.r
this.g = rgb.g
this.b = rgb.b
this.hex = color.toUpperCase()
} else {
// 假设是16进制不带#号
this.setColor("#" + color)
}
} else if (typeof color === "object" && color.r !== undefined) {
this.r = color.r
this.g = color.g
this.b = color.b
this.hex = this.rgbToHex(color.r, color.g, color.b)
}
},
// RGB值变化时更新hex
updateHex: function () {
this.hex = this.rgbToHex(this.r, this.g, this.b)
this.applyColor()
},
// Hex值变化时更新RGB
updateRgb: function () {
if (this.hex.match(/^#[0-9A-Fa-f]{6}$/)) {
let rgb = this.hexToRgb(this.hex)
this.r = rgb.r
this.g = rgb.g
this.b = rgb.b
this.applyColor()
}
},
// 应用颜色到整个网站
applyColor: function () {
let color = this.rgbToHex(this.r, this.g, this.b)
// 保存到localStorage
localStorage.setItem("themeColor", color)
// 创建或更新样式
let styleId = "theme-color-custom"
let styleEl = document.getElementById(styleId)
if (!styleEl) {
styleEl = document.createElement("style")
styleEl.id = styleId
document.head.appendChild(styleEl)
}
// 应用颜色到主题元素
styleEl.textContent = `
.top-nav, .main-menu, .main-menu .menu {
background: ${color} !important;
}
.main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless {
background: ${color} !important;
}
.main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .item {
background: ${color} !important;
}
.main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .sub-items {
background: ${color} !important;
}
.main-menu .ui.labeled.menu.vertical.blue.inverted.tiny.borderless .sub-items .item {
background: ${color} !important;
}
.main-menu .ui.menu .sub-items {
background: ${color} !important;
}
.main-menu .ui.menu .sub-items .item {
background: ${color} !important;
}
.main-menu .ui.labeled.menu .sub-items {
background: ${color} !important;
}
.main-menu .ui.labeled.menu .sub-items .item {
background: ${color} !important;
}
.main-menu .sub-items {
background: ${color} !important;
}
.main-menu .sub-items .item {
background: ${color} !important;
}
.ui.menu.blue.inverted {
background: ${color} !important;
}
.ui.menu.blue.inverted .item {
background: ${color} !important;
}
.ui.menu.vertical.blue.inverted {
background: ${color} !important;
}
.ui.menu.vertical.blue.inverted .item {
background: ${color} !important;
}
.ui.labeled.menu.vertical.blue.inverted {
background: ${color} !important;
}
.ui.labeled.menu.vertical.blue.inverted .item {
background: ${color} !important;
}
`
// 触发事件通知其他组件
if (window.Tea && window.Tea.Vue) {
window.Tea.Vue.$emit("theme-color-changed", color)
}
// 更新v-model(如果使用)
this.$emit("input", color.replace("#", ""))
},
// 获取当前主题颜色
getCurrentThemeColor: function () {
let topNav = document.querySelector(".top-nav")
if (topNav) {
let bgColor = window.getComputedStyle(topNav).backgroundColor
if (bgColor && bgColor !== "rgba(0, 0, 0, 0)" && bgColor !== "transparent") {
// 转换rgb/rgba为hex
let match = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/)
if (match) {
return this.rgbToHex(parseInt(match[1]), parseInt(match[2]), parseInt(match[3]))
}
}
}
return null
},
// 重置为默认颜色
resetColor: function () {
this.setColor("#14539A") // 默认蓝色
this.applyColor()
}
},
template: `
`
})
Vue.component("time-duration-box", {
props: ["v-name", "v-value", "v-count", "v-unit", "placeholder", "v-min-unit", "maxlength"],
mounted: function () {
this.change()
},
data: function () {
let v = this.vValue
if (v == null) {
v = {
count: this.vCount,
unit: this.vUnit
}
}
if (typeof (v["count"]) != "number") {
v["count"] = -1
}
let minUnit = this.vMinUnit
let units = [
{
code: "ms",
name: "毫秒"
},
{
code: "second",
name: "秒"
},
{
code: "minute",
name: "分钟"
},
{
code: "hour",
name: "小时"
},
{
code: "day",
name: "天"
}
]
let minUnitIndex = -1
if (minUnit != null && typeof minUnit == "string" && minUnit.length > 0) {
for (let i = 0; i < units.length; i++) {
if (units[i].code == minUnit) {
minUnitIndex = i
break
}
}
}
if (minUnitIndex > -1) {
units = units.slice(minUnitIndex)
}
let maxLength = parseInt(this.maxlength)
if (typeof maxLength != "number") {
maxLength = 10
}
return {
duration: v,
countString: (v.count >= 0) ? v.count.toString() : "",
units: units,
realMaxLength: maxLength
}
},
watch: {
"countString": function (newValue) {
let value = newValue.trim()
if (value.length == 0) {
this.duration.count = -1
return
}
let count = parseInt(value)
if (!isNaN(count)) {
this.duration.count = count
}
this.change()
}
},
methods: {
change: function () {
this.$emit("change", this.duration)
}
},
template: ``
})
Vue.component("time-duration-text", {
props: ["v-value"],
methods: {
unitName: function (unit) {
switch (unit) {
case "ms":
return "毫秒"
case "second":
return "秒"
case "minute":
return "分钟"
case "hour":
return "小时"
case "day":
return "天"
}
}
},
template: `
{{vValue.count}} {{unitName(vValue.unit)}}
`
})
Vue.component("url-patterns-box", {
props: ["value"],
data: function () {
let patterns = []
if (this.value != null) {
patterns = this.value
}
return {
patterns: patterns,
isAdding: false,
addingPattern: {"type": "wildcard", "pattern": ""},
editingIndex: -1,
patternIsInvalid: false,
windowIsSmall: window.innerWidth < 600
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.patternInput.focus()
})
},
edit: function (index) {
this.isAdding = true
this.editingIndex = index
this.addingPattern = {
type: this.patterns[index].type,
pattern: this.patterns[index].pattern
}
},
confirm: function () {
if (this.requireURL(this.addingPattern.type)) {
let pattern = this.addingPattern.pattern.trim()
if (pattern.length == 0) {
let that = this
teaweb.warn("请输入URL", function () {
that.$refs.patternInput.focus()
})
return
}
}
if (this.editingIndex < 0) {
this.patterns.push({
type: this.addingPattern.type,
pattern: this.addingPattern.pattern
})
} else {
this.patterns[this.editingIndex].type = this.addingPattern.type
this.patterns[this.editingIndex].pattern = this.addingPattern.pattern
}
this.notifyChange()
this.cancel()
},
remove: function (index) {
this.patterns.$remove(index)
this.cancel()
this.notifyChange()
},
cancel: function () {
this.isAdding = false
this.addingPattern = {"type": "wildcard", "pattern": ""}
this.editingIndex = -1
},
patternTypeName: function (patternType) {
switch (patternType) {
case "wildcard":
return "通配符"
case "regexp":
return "正则"
case "images":
return "常见图片文件"
case "audios":
return "常见音频文件"
case "videos":
return "常见视频文件"
}
return ""
},
notifyChange: function () {
this.$emit("input", this.patterns)
},
changePattern: function () {
this.patternIsInvalid = false
let pattern = this.addingPattern.pattern
switch (this.addingPattern.type) {
case "wildcard":
if (pattern.indexOf("?") >= 0) {
this.patternIsInvalid = true
}
break
case "regexp":
if (pattern.indexOf("?") >= 0) {
let pieces = pattern.split("?")
for (let i = 0; i < pieces.length - 1; i++) {
if (pieces[i].length == 0 || pieces[i][pieces[i].length - 1] != "\\") {
this.patternIsInvalid = true
}
}
}
break
}
},
requireURL: function (patternType) {
return patternType == "wildcard" || patternType == "regexp"
}
},
template: `
[{{patternTypeName(pattern.type)}}] {{pattern.pattern}}
+
`
})
Vue.component("values-box", {
props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"],
data: function () {
let values = this.values;
if (values == null) {
values = [];
}
if (this.vValues != null && typeof this.vValues == "object") {
values = this.vValues
}
return {
"realValues": values,
"isUpdating": false,
"isAdding": false,
"index": 0,
"value": "",
isEditing: false
}
},
methods: {
create: function () {
this.isAdding = true;
var that = this;
setTimeout(function () {
that.$refs.value.focus();
}, 200);
},
update: function (index) {
this.cancel()
this.isUpdating = true;
this.index = index;
this.value = this.realValues[index];
var that = this;
setTimeout(function () {
that.$refs.value.focus();
}, 200);
},
confirm: function () {
if (this.value.length == 0) {
if (typeof(this.vAllowEmpty) != "boolean" || !this.vAllowEmpty) {
return
}
}
// validate
if (typeof(this.validator) == "function") {
let resp = this.validator.call(this, this.value)
if (typeof resp == "object") {
if (typeof resp.isOk == "boolean" && !resp.isOk) {
if (typeof resp.message == "string") {
let that = this
teaweb.warn(resp.message, function () {
that.$refs.value.focus();
})
}
return
}
}
}
if (this.isUpdating) {
Vue.set(this.realValues, this.index, this.value);
} else {
this.realValues.push(this.value);
}
this.cancel()
this.$emit("change", this.realValues)
},
remove: function (index) {
this.realValues.$remove(index)
this.$emit("change", this.realValues)
},
cancel: function () {
this.isUpdating = false;
this.isAdding = false;
this.value = "";
},
updateAll: function (values) {
this.realValues = values
},
addValue: function (v) {
this.realValues.push(v)
},
startEditing: function () {
this.isEditing = !this.isEditing
},
allValues: function () {
return this.realValues
}
},
template: ``
});
// 警告消息
Vue.component("warning-message", {
template: ``
})
Vue.component("ip-box", {
props: ["v-ip"],
methods: {
popup: function () {
let ip = this.vIp
if (ip == null || ip.length == 0) {
let e = this.$refs.container
ip = e.innerText
if (ip == null) {
ip = e.textContent
}
}
teaweb.popup("/servers/ipbox?ip=" + ip, {
width: "50em",
height: "30em"
})
}
},
template: ` `
})
Vue.component("ip-item-text", {
props: ["v-item"],
template: `
*
{{vItem.value}}
{{vItem.ipFrom}}
- {{vItem.ipTo}}
级别:{{vItem.eventLevelName}}
`
})
// 绑定IP列表
Vue.component("ip-list-bind-box", {
props: ["v-http-firewall-policy-id", "v-type"],
mounted: function () {
this.refresh()
},
data: function () {
return {
policyId: this.vHttpFirewallPolicyId,
type: this.vType,
lists: []
}
},
methods: {
bind: function () {
let that = this
teaweb.popup("/servers/iplists/bindHTTPFirewallPopup?httpFirewallPolicyId=" + this.policyId + "&type=" + this.type, {
width: "50em",
height: "34em",
callback: function () {
},
onClose: function () {
that.refresh()
}
})
},
remove: function (index, listId) {
let that = this
teaweb.confirm("确定要删除这个绑定的IP名单吗?", function () {
Tea.action("/servers/iplists/unbindHTTPFirewall")
.params({
httpFirewallPolicyId: that.policyId,
listId: listId
})
.post()
.success(function (resp) {
that.lists.$remove(index)
})
})
},
refresh: function () {
let that = this
Tea.action("/servers/iplists/httpFirewall")
.params({
httpFirewallPolicyId: this.policyId,
type: this.vType
})
.post()
.success(function (resp) {
that.lists = resp.data.lists
})
}
},
template: ``
})
Vue.component("ip-list-table", {
props: ["v-items", "v-keyword", "v-show-search-button"],
data: function () {
return {
items: this.vItems,
keyword: (this.vKeyword != null) ? this.vKeyword : "",
selectedAll: false,
hasSelectedItems: false
}
},
methods: {
updateItem: function (itemId) {
this.$emit("update-item", itemId)
},
deleteItem: function (itemId) {
this.$emit("delete-item", itemId)
},
viewLogs: function (itemId) {
teaweb.popup("/waf/iplists/accessLogsPopup?itemId=" + itemId, {
width: "50em",
height: "30em"
})
},
changeSelectedAll: function () {
let boxes = this.$refs.itemCheckBox
if (boxes == null) {
return
}
let that = this
boxes.forEach(function (box) {
box.checked = that.selectedAll
})
this.hasSelectedItems = this.selectedAll
},
changeSelected: function (e) {
let that = this
that.hasSelectedItems = false
let boxes = that.$refs.itemCheckBox
if (boxes == null) {
return
}
boxes.forEach(function (box) {
if (box.checked) {
that.hasSelectedItems = true
}
})
},
deleteAll: function () {
let boxes = this.$refs.itemCheckBox
if (boxes == null) {
return
}
let itemIds = []
boxes.forEach(function (box) {
if (box.checked) {
itemIds.push(box.value)
}
})
if (itemIds.length == 0) {
return
}
Tea.action("/waf/iplists/deleteItems")
.post()
.params({
itemIds: itemIds
})
.success(function () {
teaweb.successToast("批量删除成功", 1200, teaweb.reload)
})
},
formatSeconds: function (seconds) {
if (seconds < 60) {
return seconds + "秒"
}
if (seconds < 3600) {
return Math.ceil(seconds / 60) + "分钟"
}
if (seconds < 86400) {
return Math.ceil(seconds / 3600) + "小时"
}
return Math.ceil(seconds / 86400) + "天"
}
},
template: `
IP
类型
级别
过期时间
备注
操作
{{item.value}}
{{item.ipFrom}} New
- {{item.ipTo}}
{{item.region}}
| {{item.isp}}
{{item.isp}}
IPv4
IPv4
IPv6
所有IP
{{item.eventLevelName}}
-
{{item.expiredTime}}
已过期
{{formatSeconds(item.lifeSeconds)}}
已过期
不过期
{{item.reason}}
-
修改
删除
`
})
Vue.component("message-row", {
props: ["v-message"],
data: function () {
let paramsJSON = this.vMessage.params
let params = null
if (paramsJSON != null && paramsJSON.length > 0) {
params = JSON.parse(paramsJSON)
}
return {
message: this.vMessage,
params: params
}
},
methods: {
viewCert: function (certId) {
teaweb.popup("/servers/certs/certPopup?certId=" + certId, {
height: "28em",
width: "48em"
})
},
readMessage: function (messageId) {
Tea.action("/messages/readPage")
.params({"messageIds": [messageId]})
.post()
.success(function () {
teaweb.reload()
})
}
},
template: ``
})
Vue.component("ns-access-log-box", {
props: ["v-access-log", "v-keyword"],
data: function () {
let accessLog = this.vAccessLog
return {
accessLog: accessLog
}
},
methods: {
showLog: function () {
let that = this
let requestId = this.accessLog.requestId
this.$parent.$children.forEach(function (v) {
if (v.deselect != null) {
v.deselect()
}
})
this.select()
teaweb.popup("/ns/clusters/accessLogs/viewPopup?requestId=" + requestId, {
width: "50em",
height: "24em",
onClose: function () {
that.deselect()
}
})
},
select: function () {
this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)"
},
deselect: function () {
this.$refs.box.parentNode.style.cssText = ""
}
},
template: `
[{{accessLog.region}}] {{accessLog.remoteAddr}} [{{accessLog.timeLocal}}] [{{accessLog.networking}}]
{{accessLog.questionType}} {{accessLog.questionName}} ->
{{accessLog.recordType}} {{accessLog.recordValue}}
[没有记录]
线路: {{route.name}}
递归DNS
错误:[{{accessLog.error}}]
`
})
Vue.component("ns-access-log-ref-box", {
props: ["v-access-log-ref", "v-is-parent"],
data: function () {
let config = this.vAccessLogRef
if (config == null) {
config = {
isOn: false,
isPrior: false,
logMissingDomains: false
}
}
if (typeof (config.logMissingDomains) == "undefined") {
config.logMissingDomains = false
}
return {
config: config
}
},
template: ``
})
Vue.component("ns-create-records-table", {
props: ["v-types", "v-default-ttl"],
data: function () {
let types = this.vTypes
if (types == null) {
types = []
}
let defaultTTL = this.vDefaultTtl
if (defaultTTL != null) {
defaultTTL = parseInt(defaultTTL.toString())
}
if (defaultTTL <= 0) {
defaultTTL = 600
}
return {
types: types,
defaultTTL: defaultTTL,
records: [
{
name: "",
type: "A",
value: "",
routeCodes: [],
ttl: defaultTTL,
index: 0
}
],
lastIndex: 0,
isAddingRoutes: false // 是否正在添加线路
}
},
methods: {
add: function () {
this.records.push({
name: "",
type: "A",
value: "",
routeCodes: [],
ttl: this.defaultTTL,
index: ++this.lastIndex
})
let that = this
setTimeout(function () {
that.$refs.nameInputs.$last().focus()
}, 100)
},
remove: function (index) {
this.records.$remove(index)
},
addRoutes: function () {
this.isAddingRoutes = true
},
cancelRoutes: function () {
let that = this
setTimeout(function () {
that.isAddingRoutes = false
}, 1000)
},
changeRoutes: function (record, routes) {
if (routes == null) {
record.routeCodes = []
} else {
record.routeCodes = routes.map(function (route) {
return route.code
})
}
}
},
template: ``,
})
Vue.component("ns-domain-group-selector", {
props: ["v-domain-group-id"],
data: function () {
let groupId = this.vDomainGroupId
if (groupId == null) {
groupId = 0
}
return {
userId: 0,
groupId: groupId
}
},
methods: {
change: function (group) {
if (group != null) {
this.$emit("change", group.id)
} else {
this.$emit("change", 0)
}
},
reload: function (userId) {
this.userId = userId
this.$refs.comboBox.clear()
this.$refs.comboBox.setDataURL("/ns/domains/groups/options?userId=" + userId)
this.$refs.comboBox.reloadData()
}
},
template: `
`
})
Vue.component("ns-record-health-check-config-box", {
props:["value", "v-parent-config"],
data: function () {
let config = this.value
if (config == null) {
config = {
isOn: false,
port: 0,
timeoutSeconds: 0,
countUp: 0,
countDown: 0
}
}
let parentConfig = this.vParentConfig
return {
config: config,
portString: config.port.toString(),
timeoutSecondsString: config.timeoutSeconds.toString(),
countUpString: config.countUp.toString(),
countDownString: config.countDown.toString(),
portIsEditing: config.port > 0,
timeoutSecondsIsEditing: config.timeoutSeconds > 0,
countUpIsEditing: config.countUp > 0,
countDownIsEditing: config.countDown > 0,
parentConfig: parentConfig
}
},
watch: {
portString: function (value) {
let port = parseInt(value.toString())
if (isNaN(port) || port > 65535 || port < 1) {
this.config.port = 0
} else {
this.config.port = port
}
},
timeoutSecondsString: function (value) {
let timeoutSeconds = parseInt(value.toString())
if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) {
this.config.timeoutSeconds = 0
} else {
this.config.timeoutSeconds = timeoutSeconds
}
},
countUpString: function (value) {
let countUp = parseInt(value.toString())
if (isNaN(countUp) || countUp > 1000 || countUp < 1) {
this.config.countUp = 0
} else {
this.config.countUp = countUp
}
},
countDownString: function (value) {
let countDown = parseInt(value.toString())
if (isNaN(countDown) || countDown > 1000 || countDown < 1) {
this.config.countDown = 0
} else {
this.config.countDown = countDown
}
}
},
template: `
启用当前记录健康检查
检测端口
默认{{parentConfig.port}}
[修改]
超时时间
默认{{parentConfig.timeoutSeconds}}秒
[修改]
默认连续上线次数
默认{{parentConfig.countUp}}次
[修改]
默认连续下线次数
默认{{parentConfig.countDown}}次
[修改]
`
})
Vue.component("ns-records-health-check-config-box", {
props:["value"],
data: function () {
let config = this.value
if (config == null) {
config = {
isOn: false,
port: 80,
timeoutSeconds: 5,
countUp: 1,
countDown: 3
}
}
return {
config: config,
portString: config.port.toString(),
timeoutSecondsString: config.timeoutSeconds.toString(),
countUpString: config.countUp.toString(),
countDownString: config.countDown.toString()
}
},
watch: {
portString: function (value) {
let port = parseInt(value.toString())
if (isNaN(port) || port > 65535 || port < 1) {
this.config.port = 80
} else {
this.config.port = port
}
},
timeoutSecondsString: function (value) {
let timeoutSeconds = parseInt(value.toString())
if (isNaN(timeoutSeconds) || timeoutSeconds > 1000 || timeoutSeconds < 1) {
this.config.timeoutSeconds = 5
} else {
this.config.timeoutSeconds = timeoutSeconds
}
},
countUpString: function (value) {
let countUp = parseInt(value.toString())
if (isNaN(countUp) || countUp > 1000 || countUp < 1) {
this.config.countUp = 1
} else {
this.config.countUp = countUp
}
},
countDownString: function (value) {
let countDown = parseInt(value.toString())
if (isNaN(countDown) || countDown > 1000 || countDown < 1) {
this.config.countDown = 3
} else {
this.config.countDown = countDown
}
}
},
template: ``
})
// 递归DNS设置
Vue.component("ns-recursion-config-box", {
props: ["v-recursion-config"],
data: function () {
let recursion = this.vRecursionConfig
if (recursion == null) {
recursion = {
isOn: false,
hosts: [],
allowDomains: [],
denyDomains: [],
useLocalHosts: false
}
}
if (recursion.hosts == null) {
recursion.hosts = []
}
if (recursion.allowDomains == null) {
recursion.allowDomains = []
}
if (recursion.denyDomains == null) {
recursion.denyDomains = []
}
return {
config: recursion,
hostIsAdding: false,
host: "",
updatingHost: null
}
},
methods: {
changeHosts: function (hosts) {
this.config.hosts = hosts
},
changeAllowDomains: function (domains) {
this.config.allowDomains = domains
},
changeDenyDomains: function (domains) {
this.config.denyDomains = domains
},
removeHost: function (index) {
this.config.hosts.$remove(index)
},
addHost: function () {
this.updatingHost = null
this.host = ""
this.hostIsAdding = !this.hostIsAdding
if (this.hostIsAdding) {
var that = this
setTimeout(function () {
let hostRef = that.$refs.hostRef
if (hostRef != null) {
hostRef.focus()
}
}, 200)
}
},
updateHost: function (host) {
this.updatingHost = host
this.host = host.host
this.hostIsAdding = !this.hostIsAdding
if (this.hostIsAdding) {
var that = this
setTimeout(function () {
let hostRef = that.$refs.hostRef
if (hostRef != null) {
hostRef.focus()
}
}, 200)
}
},
confirmHost: function () {
if (this.host.length == 0) {
teaweb.warn("请输入DNS地址")
return
}
// TODO 校验Host
// TODO 可以输入端口号
// TODO 可以选择协议
this.hostIsAdding = false
if (this.updatingHost == null) {
this.config.hosts.push({
host: this.host
})
} else {
this.updatingHost.host = this.host
}
},
cancelHost: function () {
this.hostIsAdding = false
}
},
template: ``
})
Vue.component("ns-route-ranges-box", {
props: ["v-ranges"],
data: function () {
let ranges = this.vRanges
if (ranges == null) {
ranges = []
}
return {
ranges: ranges,
isAdding: false,
isAddingBatch: false,
// 类型
rangeType: "ipRange",
isReverse: false,
// IP范围
ipRangeFrom: "",
ipRangeTo: "",
batchIPRange: "",
// CIDR
ipCIDR: "",
batchIPCIDR: "",
// region
regions: [],
regionType: "country",
regionConnector: "OR"
}
},
methods: {
addIPRange: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.ipRangeFrom.focus()
}, 100)
},
addCIDR: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.ipCIDR.focus()
}, 100)
},
addRegions: function () {
this.isAdding = true
},
addRegion: function (regionType) {
this.regionType = regionType
},
remove: function (index) {
this.ranges.$remove(index)
},
cancelIPRange: function () {
this.isAdding = false
this.ipRangeFrom = ""
this.ipRangeTo = ""
this.isReverse = false
},
cancelIPCIDR: function () {
this.isAdding = false
this.ipCIDR = ""
this.isReverse = false
},
cancelRegions: function () {
this.isAdding = false
this.regions = []
this.regionType = "country"
this.regionConnector = "OR"
this.isReverse = false
},
confirmIPRange: function () {
// 校验IP
let that = this
this.ipRangeFrom = this.ipRangeFrom.trim()
if (!this.validateIP(this.ipRangeFrom)) {
teaweb.warn("开始IP填写错误", function () {
that.$refs.ipRangeFrom.focus()
})
return
}
this.ipRangeTo = this.ipRangeTo.trim()
if (!this.validateIP(this.ipRangeTo)) {
teaweb.warn("结束IP填写错误", function () {
that.$refs.ipRangeTo.focus()
})
return
}
this.ranges.push({
type: "ipRange",
params: {
ipFrom: this.ipRangeFrom,
ipTo: this.ipRangeTo,
isReverse: this.isReverse
}
})
this.cancelIPRange()
},
confirmIPCIDR: function () {
let that = this
if (this.ipCIDR.length == 0) {
teaweb.warn("请填写CIDR", function () {
that.$refs.ipCIDR.focus()
})
return
}
if (!this.validateCIDR(this.ipCIDR)) {
teaweb.warn("请输入正确的CIDR", function () {
that.$refs.ipCIDR.focus()
})
return
}
this.ranges.push({
type: "cidr",
params: {
cidr: this.ipCIDR,
isReverse: this.isReverse
}
})
this.cancelIPCIDR()
},
confirmRegions: function () {
if (this.regions.length == 0) {
this.cancelRegions()
return
}
this.ranges.push({
type: "region",
connector: this.regionConnector,
params: {
regions: this.regions,
isReverse: this.isReverse
}
})
this.cancelRegions()
},
addBatchIPRange: function () {
this.isAddingBatch = true
let that = this
setTimeout(function () {
that.$refs.batchIPRange.focus()
}, 100)
},
addBatchCIDR: function () {
this.isAddingBatch = true
let that = this
setTimeout(function () {
that.$refs.batchIPCIDR.focus()
}, 100)
},
cancelBatchIPRange: function () {
this.isAddingBatch = false
this.batchIPRange = ""
this.isReverse = false
},
cancelBatchIPCIDR: function () {
this.isAddingBatch = false
this.batchIPCIDR = ""
this.isReverse = false
},
confirmBatchIPRange: function () {
let that = this
let rangesText = this.batchIPRange
if (rangesText.length == 0) {
teaweb.warn("请填写要加入的IP范围", function () {
that.$refs.batchIPRange.focus()
})
return
}
let validRanges = []
let invalidLine = ""
rangesText.split("\n").forEach(function (line) {
line = line.trim()
if (line.length == 0) {
return
}
line = line.replace(",", ",")
let pieces = line.split(",")
if (pieces.length != 2) {
invalidLine = line
return
}
let ipFrom = pieces[0].trim()
let ipTo = pieces[1].trim()
if (!that.validateIP(ipFrom) || !that.validateIP(ipTo)) {
invalidLine = line
return
}
validRanges.push({
type: "ipRange",
params: {
ipFrom: ipFrom,
ipTo: ipTo,
isReverse: that.isReverse
}
})
})
if (invalidLine.length > 0) {
teaweb.warn("'" + invalidLine + "'格式错误", function () {
that.$refs.batchIPRange.focus()
})
return
}
validRanges.forEach(function (v) {
that.ranges.push(v)
})
this.cancelBatchIPRange()
},
confirmBatchIPCIDR: function () {
let that = this
let rangesText = this.batchIPCIDR
if (rangesText.length == 0) {
teaweb.warn("请填写要加入的CIDR", function () {
that.$refs.batchIPCIDR.focus()
})
return
}
let validRanges = []
let invalidLine = ""
rangesText.split("\n").forEach(function (line) {
let cidr = line.trim()
if (cidr.length == 0) {
return
}
if (!that.validateCIDR(cidr)) {
invalidLine = line
return
}
validRanges.push({
type: "cidr",
params: {
cidr: cidr,
isReverse: that.isReverse
}
})
})
if (invalidLine.length > 0) {
teaweb.warn("'" + invalidLine + "'格式错误", function () {
that.$refs.batchIPCIDR.focus()
})
return
}
validRanges.forEach(function (v) {
that.ranges.push(v)
})
this.cancelBatchIPCIDR()
},
selectRegionCountry: function (country) {
if (country == null) {
return
}
this.regions.push({
type: "country",
id: country.id,
name: country.name
})
this.$refs.regionCountryComboBox.clear()
},
selectRegionProvince: function (province) {
if (province == null) {
return
}
this.regions.push({
type: "province",
id: province.id,
name: province.name
})
this.$refs.regionProvinceComboBox.clear()
},
selectRegionCity: function (city) {
if (city == null) {
return
}
this.regions.push({
type: "city",
id: city.id,
name: city.name
})
this.$refs.regionCityComboBox.clear()
},
selectRegionProvider: function (provider) {
if (provider == null) {
return
}
this.regions.push({
type: "provider",
id: provider.id,
name: provider.name
})
this.$refs.regionProviderComboBox.clear()
},
removeRegion: function (index) {
this.regions.$remove(index)
},
validateIP: function (ip) {
if (ip.length == 0) {
return
}
// IPv6
if (ip.indexOf(":") >= 0) {
let pieces = ip.split(":")
if (pieces.length > 8) {
return false
}
let isOk = true
pieces.forEach(function (piece) {
if (!/^[\da-fA-F]{0,4}$/.test(piece)) {
isOk = false
}
})
return isOk
}
if (!ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) {
return false
}
let pieces = ip.split(".")
let isOk = true
pieces.forEach(function (v) {
let v1 = parseInt(v)
if (v1 > 255) {
isOk = false
}
})
return isOk
},
validateCIDR: function (cidr) {
let pieces = cidr.split("/")
if (pieces.length != 2) {
return false
}
let ip = pieces[0]
if (!this.validateIP(ip)) {
return false
}
let mask = pieces[1]
if (!/^\d{1,3}$/.test(mask)) {
return false
}
mask = parseInt(mask, 10)
if (cidr.indexOf(":") >= 0) { // IPv6
return mask <= 128
}
return mask <= 32
},
updateRangeType: function (rangeType) {
this.rangeType = rangeType
}
},
template: `
[排除]
IP范围:
CIDR:
{{range.params.ipFrom}} - {{range.params.ipTo}}
{{range.params.cidr}}
国家/地区
省份
城市
ISP
:{{region.name}}
或
且
已添加
国家/地区
省份
城市
ISP
:{{region.name}}
或
且
添加新国家/地区 省份 城市 ISP
*
添加国家/地区
添加省份
添加城市
ISP
区域之间关系
或
且
排除
确定
添加区域
`
})
// 选择单一线路
Vue.component("ns-route-selector", {
props: ["v-route-code"],
mounted: function () {
let that = this
Tea.action("/ns/routes/options")
.post()
.success(function (resp) {
that.routes = resp.data.routes
})
},
data: function () {
let routeCode = this.vRouteCode
if (routeCode == null) {
routeCode = ""
}
return {
routeCode: routeCode,
routes: []
}
},
template: ``
})
// 选择多个线路
Vue.component("ns-routes-selector", {
props: ["v-routes", "name"],
mounted: function () {
let that = this
Tea.action("/ns/routes/options")
.post()
.success(function (resp) {
that.routes = resp.data.routes
that.supportChinaProvinceRoutes = resp.data.supportChinaProvinceRoutes
that.supportISPRoutes = resp.data.supportISPRoutes
that.supportWorldRegionRoutes = resp.data.supportWorldRegionRoutes
that.supportAgentRoutes = resp.data.supportAgentRoutes
that.publicCategories = resp.data.publicCategories
that.supportPublicRoutes = resp.data.supportPublicRoutes
// provinces
let provinces = {}
if (resp.data.provinces != null && resp.data.provinces.length > 0) {
for (const province of resp.data.provinces) {
let countryCode = province.countryCode
if (typeof provinces[countryCode] == "undefined") {
provinces[countryCode] = []
}
provinces[countryCode].push({
name: province.name,
code: province.code
})
}
}
that.provinces = provinces
})
},
data: function () {
let selectedRoutes = this.vRoutes
if (selectedRoutes == null) {
selectedRoutes = []
}
let inputName = this.name
if (typeof inputName != "string" || inputName.length == 0) {
inputName = "routeCodes"
}
return {
routeCode: "default",
inputName: inputName,
routes: [],
provinces: {}, // country code => [ province1, province2, ... ]
provinceRouteCode: "",
isAdding: false,
routeType: "default",
selectedRoutes: selectedRoutes,
supportChinaProvinceRoutes: false,
supportISPRoutes: false,
supportWorldRegionRoutes: false,
supportAgentRoutes: false,
supportPublicRoutes: false,
publicCategories: []
}
},
watch: {
routeType: function (v) {
this.routeCode = ""
let that = this
this.routes.forEach(function (route) {
if (route.type == v && that.routeCode.length == 0) {
that.routeCode = route.code
}
})
}
},
methods: {
add: function () {
this.isAdding = true
this.routeType = "default"
this.routeCode = "default"
this.provinceRouteCode = ""
this.$emit("add")
},
cancel: function () {
this.isAdding = false
this.$emit("cancel")
},
confirm: function () {
if (this.routeCode.length == 0) {
return
}
let that = this
// route
let selectedRoute = null
for (const route of this.routes) {
if (route.code == this.routeCode) {
selectedRoute = route
break
}
}
if (selectedRoute != null) {
// province route
if (this.provinceRouteCode.length > 0 && this.provinces[this.routeCode] != null) {
for (const province of this.provinces[this.routeCode]) {
if (province.code == this.provinceRouteCode) {
selectedRoute = {
name: selectedRoute.name + "-" + province.name,
code: province.code
}
break
}
}
}
that.selectedRoutes.push(selectedRoute)
}
this.$emit("change", this.selectedRoutes)
this.cancel()
},
remove: function (index) {
this.selectedRoutes.$remove(index)
this.$emit("change", this.selectedRoutes)
}
}
,
template: `
选择类型 *
[默认线路]
自定义线路
运营商
中国省市
全球国家地区
搜索引擎
{{category.name}}
选择线路 *
{{route.name}}
选择省/州
[全域]
{{province.name}}
+
`
})
Vue.component("pay-method-selector", {
mounted: function () {
this.$emit("change", this.currentMethodCode)
let that = this
Tea.action("/finance/methodOptions")
.success(function (resp) {
that.isLoading = false
that.balance = resp.data.balance
that.methods = resp.data.methods
that.canPay = resp.data.enablePay
})
.post()
},
data: function () {
return {
isLoading: true,
canPay: true,
balance: 0,
methods: [],
currentMethodCode: "@balance"
}
},
methods: {
selectMethod: function (method) {
this.currentMethodCode = method.code
this.$emit("change", method.code)
}
},
template: `
余额支付 ({{balance}}元)
{{method.name}}
暂时不支持线上支付,请联系客服购买。
`
})
Vue.component("plan-bandwidth-limit-view", {
props: ["value"],
template: `
带宽限制:
`
})
// 显示流量限制说明
Vue.component("plan-limit-view", {
props: ["value", "v-single-mode"],
data: function () {
let config = this.value
let hasLimit = false
if (!this.vSingleMode) {
if (config.trafficLimit != null && config.trafficLimit.isOn && ((config.trafficLimit.dailySize != null && config.trafficLimit.dailySize.count > 0) || (config.trafficLimit.monthlySize != null && config.trafficLimit.monthlySize.count > 0))) {
hasLimit = true
} else if (config.dailyRequests > 0 || config.monthlyRequests > 0) {
hasLimit = true
}
}
return {
config: config,
hasLimit: hasLimit
}
},
methods: {
formatNumber: function (n) {
return teaweb.formatNumber(n)
},
composeCapacity: function (capacity) {
return teaweb.convertSizeCapacityToString(capacity)
}
},
template: `
日流量限制:{{composeCapacity(config.trafficLimit.dailySize)}}
月流量限制:{{composeCapacity(config.trafficLimit.monthlySize)}}
单日请求数限制:{{formatNumber(config.dailyRequests)}}
单月请求数限制:{{formatNumber(config.monthlyRequests)}}
单日Websocket限制:{{formatNumber(config.dailyWebsocketConnections)}}
单月Websocket限制:{{formatNumber(config.monthlyWebsocketConnections)}}
文件上传限制:{{composeCapacity(config.maxUploadSize)}}
`
})
Vue.component("plan-price-view", {
props: ["v-plan"],
data: function () {
return {
plan: this.vPlan
}
},
template: `
按时间周期计费
月度:¥{{plan.monthlyPrice}}元
季度:¥{{plan.seasonallyPrice}}元
年度:¥{{plan.yearlyPrice}}元
按流量计费
基础价格:¥{{plan.trafficPrice.base}}元/GiB
按{{plan.bandwidthPrice.percentile}}th带宽计费
{{range.minMB}} - {{range.maxMB}}MiB ∞ : {{range.totalPrice}}元 {{range.pricePerMB}}元/MiB
`
})
Vue.component("cache-cond-box", {
data: function () {
return {
conds: [],
addingExt: false,
addingPath: false,
extDuration: null,
pathDuration: null
}
},
methods: {
addExt: function () {
this.addingExt = !this.addingExt
this.addingPath = false
if (this.addingExt) {
let that = this
setTimeout(function () {
if (that.$refs.extInput != null) {
that.$refs.extInput.focus()
}
})
}
},
changeExtDuration: function (duration) {
this.extDuration = duration
},
confirmExt: function () {
let value = this.$refs.extInput.value
if (value.length == 0) {
return
}
let exts = []
let pieces = value.split(/[,,]/)
pieces.forEach(function (v) {
v = v.trim()
v = v.replace(/\s+/, "")
if (v.length > 0) {
if (v[0] != ".") {
v = "." + v
}
exts.push(v)
}
})
this.conds.push({
type: "url-extension",
value: JSON.stringify(exts),
duration: this.extDuration
})
this.$refs.extInput.value = ""
this.cancel()
},
addPath: function () {
this.addingExt = false
this.addingPath = !this.addingPath
if (this.addingPath) {
let that = this
setTimeout(function () {
if (that.$refs.pathInput != null) {
that.$refs.pathInput.focus()
}
})
}
},
changePathDuration: function (duration) {
this.pathDuration = duration
},
confirmPath: function () {
let value = this.$refs.pathInput.value
if (value.length == 0) {
return
}
if (value[0] != "/") {
value = "/" + value
}
this.conds.push({
type: "url-prefix",
value: value,
duration: this.pathDuration
})
this.$refs.pathInput.value = ""
this.cancel()
},
remove: function (index) {
this.conds.$remove(index)
},
cancel: function () {
this.addingExt = false
this.addingPath = false
}
},
template: ``
})
// 域名列表
Vue.component("domains-box", {
props: ["v-domains", "name", "v-support-wildcard"],
data: function () {
let domains = this.vDomains
if (domains == null) {
domains = []
}
let realName = "domainsJSON"
if (this.name != null && typeof this.name == "string") {
realName = this.name
}
let supportWildcard = true
if (typeof this.vSupportWildcard == "boolean") {
supportWildcard = this.vSupportWildcard
}
return {
domains: domains,
mode: "single", // single | batch
batchDomains: "",
isAdding: false,
addingDomain: "",
isEditing: false,
editingIndex: -1,
realName: realName,
supportWildcard: supportWildcard
}
},
watch: {
vSupportWildcard: function (v) {
if (typeof v == "boolean") {
this.supportWildcard = v
}
},
mode: function (mode) {
let that = this
setTimeout(function () {
if (mode == "single") {
if (that.$refs.addingDomain != null) {
that.$refs.addingDomain.focus()
}
} else if (mode == "batch") {
if (that.$refs.batchDomains != null) {
that.$refs.batchDomains.focus()
}
}
}, 100)
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.addingDomain.focus()
}, 100)
},
confirm: function () {
if (this.mode == "batch") {
this.confirmBatch()
return
}
let that = this
// 删除其中的空格
this.addingDomain = this.addingDomain.replace(/\s/g, "")
if (this.addingDomain.length == 0) {
teaweb.warn("请输入要添加的域名", function () {
that.$refs.addingDomain.focus()
})
return
}
// 基本校验
if (this.supportWildcard) {
if (this.addingDomain[0] == "~") {
let expr = this.addingDomain.substring(1)
try {
new RegExp(expr)
} catch (e) {
teaweb.warn("正则表达式错误:" + e.message, function () {
that.$refs.addingDomain.focus()
})
return
}
}
} else {
if (/[*~^]/.test(this.addingDomain)) {
teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () {
that.$refs.addingDomain.focus()
})
return
}
}
if (this.isEditing && this.editingIndex >= 0) {
this.domains[this.editingIndex] = this.addingDomain
} else {
// 分割逗号(,)、顿号(、)
if (this.addingDomain.match("[,、,;]")) {
let domainList = this.addingDomain.split(new RegExp("[,、,;]"))
domainList.forEach(function (v) {
if (v.length > 0) {
that.domains.push(v)
}
})
} else {
this.domains.push(this.addingDomain)
}
}
this.cancel()
this.change()
},
confirmBatch: function () {
let domains = this.batchDomains.split("\n")
let realDomains = []
let that = this
let hasProblems = false
domains.forEach(function (domain) {
if (hasProblems) {
return
}
if (domain.length == 0) {
return
}
if (that.supportWildcard) {
if (domain == "~") {
let expr = domain.substring(1)
try {
new RegExp(expr)
} catch (e) {
hasProblems = true
teaweb.warn("正则表达式错误:" + e.message, function () {
that.$refs.batchDomains.focus()
})
return
}
}
} else {
if (/[*~^]/.test(domain)) {
hasProblems = true
teaweb.warn("当前只支持添加普通域名,域名中不能含有特殊符号", function () {
that.$refs.batchDomains.focus()
})
return
}
}
realDomains.push(domain)
})
if (hasProblems) {
return
}
if (realDomains.length == 0) {
teaweb.warn("请输入要添加的域名", function () {
that.$refs.batchDomains.focus()
})
return
}
realDomains.forEach(function (domain) {
that.domains.push(domain)
})
this.cancel()
this.change()
},
edit: function (index) {
this.addingDomain = this.domains[index]
this.isEditing = true
this.editingIndex = index
let that = this
setTimeout(function () {
that.$refs.addingDomain.focus()
}, 50)
},
remove: function (index) {
this.domains.$remove(index)
this.change()
},
cancel: function () {
this.isAdding = false
this.mode = "single"
this.batchDomains = ""
this.isEditing = false
this.editingIndex = -1
this.addingDomain = ""
},
change: function () {
this.$emit("change", this.domains)
}
},
template: `
[正则]
[后缀]
[泛域名]
{{domain}}
+
`
})
Vue.component("http-access-log-box", {
props: ["v-access-log"],
data: function () {
let accessLog = this.vAccessLog
if (accessLog.header != null && accessLog.header.Upgrade != null && accessLog.header.Upgrade.values != null && accessLog.header.Upgrade.values.$contains("websocket")) {
if (accessLog.scheme == "http") {
accessLog.scheme = "ws"
} else if (accessLog.scheme == "https") {
accessLog.scheme = "wss"
}
}
return {
accessLog: accessLog
}
},
methods: {
formatCost: function (seconds) {
if (seconds == null) {
return "0"
}
let s = (seconds * 1000).toString();
let pieces = s.split(".");
if (pieces.length < 2) {
return s;
}
return pieces[0] + "." + pieces[1].substring(0, 3);
},
showLog: function () {
let that = this
let requestId = this.accessLog.requestId
this.$parent.$children.forEach(function (v) {
if (v.deselect != null) {
v.deselect()
}
})
this.select()
teaweb.popup("/servers/server/log/viewPopup?requestId=" + requestId, {
width: "50em",
height: "24em",
onClose: function () {
that.deselect()
}
})
},
select: function () {
this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)"
},
deselect: function () {
this.$refs.box.parentNode.style.cssText = ""
}
},
template: `
[{{accessLog.region}}] {{accessLog.remoteAddr}} [{{accessLog.timeLocal}}]
"{{accessLog.requestMethod}} {{accessLog.scheme}}://{{accessLog.host}}{{accessLog.requestURI}} {{accessLog.proto}}" {{accessLog.status}}
[cached] waf {{accessLog.firewallActions}} - {{tag}} - 耗时:{{formatCost(accessLog.requestTime)}} ms
`
})
Vue.component("http-access-log-config-box", {
props: ["v-access-log-config", "v-fields", "v-default-field-codes", "v-access-log-policies", "v-is-location", "v-is-group"],
data: function () {
let that = this
// 初始化
setTimeout(function () {
that.changeFields()
that.changePolicy()
}, 100)
let accessLog = {
isPrior: false,
isOn: false,
fields: [],
status1: true,
status2: true,
status3: true,
status4: true,
status5: true,
storageOnly: false,
storagePolicies: [],
firewallOnly: false
}
if (this.vAccessLogConfig != null) {
accessLog = this.vAccessLogConfig
}
this.vFields.forEach(function (v) {
if (that.vAccessLogConfig == null) { // 初始化默认值
v.isChecked = that.vDefaultFieldCodes.$contains(v.code)
} else {
v.isChecked = accessLog.fields.$contains(v.code)
}
})
this.vAccessLogPolicies.forEach(function (v) {
v.isChecked = accessLog.storagePolicies.$contains(v.id)
})
return {
accessLog: accessLog,
showAdvancedOptions: false
}
},
methods: {
changeFields: function () {
this.accessLog.fields = this.vFields.filter(function (v) {
return v.isChecked
}).map(function (v) {
return v.code
})
},
changePolicy: function () {
this.accessLog.storagePolicies = this.vAccessLogPolicies.filter(function (v) {
return v.isChecked
}).map(function (v) {
return v.id
})
},
changeAdvanced: function (v) {
this.showAdvancedOptions = v
}
},
template: ``
})
// 访问日志搜索框
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: ``
})
// 基本认证用户配置
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: ``
})
// 认证设置
Vue.component("http-auth-config-box", {
props: ["v-auth-config", "v-is-location"],
data: function () {
let authConfig = this.vAuthConfig
if (authConfig == null) {
authConfig = {
isPrior: false,
isOn: false
}
}
if (authConfig.policyRefs == null) {
authConfig.policyRefs = []
}
return {
authConfig: authConfig
}
},
methods: {
isOn: function () {
return (!this.vIsLocation || this.authConfig.isPrior) && this.authConfig.isOn
},
add: function () {
let that = this
teaweb.popup("/servers/server/settings/access/createPopup", {
callback: function (resp) {
that.authConfig.policyRefs.push(resp.data.policyRef)
that.change()
},
height: "28em"
})
},
update: function (index, policyId) {
let that = this
teaweb.popup("/servers/server/settings/access/updatePopup?policyId=" + policyId, {
callback: function (resp) {
teaweb.success("保存成功", function () {
teaweb.reload()
})
},
height: "28em"
})
},
remove: function (index) {
this.authConfig.policyRefs.$remove(index)
this.change()
},
methodName: function (methodType) {
switch (methodType) {
case "basicAuth":
return "BasicAuth"
case "subRequest":
return "子请求"
case "typeA":
return "URL鉴权A"
case "typeB":
return "URL鉴权B"
case "typeC":
return "URL鉴权C"
case "typeD":
return "URL鉴权D"
}
return ""
},
change: function () {
let that = this
setTimeout(function () {
// 延时通知,是为了让表单有机会变更数据
that.$emit("change", this.authConfig)
}, 100)
}
},
template: `
鉴权方式
名称
鉴权方法
参数
状态
操作
{{ref.authPolicy.name}}
{{methodName(ref.authPolicy.type)}}
{{ref.authPolicy.params.users.length}}个用户
[{{ref.authPolicy.params.method}}]
{{ref.authPolicy.params.url}}
{{ref.authPolicy.params.signParamName}}/有效期{{ref.authPolicy.params.life}}秒
有效期{{ref.authPolicy.params.life}}秒
有效期{{ref.authPolicy.params.life}}秒
{{ref.authPolicy.params.signParamName}}/{{ref.authPolicy.params.timestampParamName}}/有效期{{ref.authPolicy.params.life}}秒
扩展名:{{ext}}
域名:{{domain}}
修改
删除
+添加鉴权方式
`
})
Vue.component("http-cache-config-box", {
props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy", "v-web-id"],
data: function () {
let cacheConfig = this.vCacheConfig
if (cacheConfig == null) {
cacheConfig = {
isPrior: false,
isOn: false,
addStatusHeader: true,
addAgeHeader: false,
enableCacheControlMaxAge: false,
cacheRefs: [],
purgeIsOn: false,
purgeKey: "",
disablePolicyRefs: false
}
}
if (cacheConfig.cacheRefs == null) {
cacheConfig.cacheRefs = []
}
let maxBytes = null
if (this.vCachePolicy != null && this.vCachePolicy.maxBytes != null) {
maxBytes = this.vCachePolicy.maxBytes
}
// key
if (cacheConfig.key == null) {
// use Vue.set to activate vue events
Vue.set(cacheConfig, "key", {
isOn: false,
scheme: "https",
host: ""
})
}
return {
cacheConfig: cacheConfig,
moreOptionsVisible: false,
enablePolicyRefs: !cacheConfig.disablePolicyRefs,
maxBytes: maxBytes,
searchBoxVisible: false,
searchKeyword: "",
keyOptionsVisible: false
}
},
watch: {
enablePolicyRefs: function (v) {
this.cacheConfig.disablePolicyRefs = !v
},
searchKeyword: function (v) {
this.$refs.cacheRefsConfigBoxRef.search(v)
}
},
methods: {
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.cacheConfig.isPrior) && this.cacheConfig.isOn
},
isPlus: function () {
return true
},
generatePurgeKey: function () {
let r = Math.random().toString() + Math.random().toString()
let s = r.replace(/0\./g, "")
.replace(/\./g, "")
let result = ""
for (let i = 0; i < s.length; i++) {
result += String.fromCharCode(parseInt(s.substring(i, i + 1)) + ((Math.random() < 0.5) ? "a" : "A").charCodeAt(0))
}
this.cacheConfig.purgeKey = result
},
showMoreOptions: function () {
this.moreOptionsVisible = !this.moreOptionsVisible
},
changeStale: function (stale) {
this.cacheConfig.stale = stale
},
showSearchBox: function () {
this.searchBoxVisible = !this.searchBoxVisible
if (this.searchBoxVisible) {
let that = this
setTimeout(function () {
that.$refs.searchBox.focus()
})
} else {
this.searchKeyword = ""
}
}
},
template: ``
})
// 单个缓存条件设置
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: `
缓存对象 *
文件扩展名
首页
全站
URL目录前缀
URL完整路径
URL通配符
URL正则匹配
参数匹配
{{condComponent.paramsTitle}} *
不区分大小写
匹配条件分组 *
缓存有效期 *
忽略URI参数
缓存Key *
请求方法限制
客户端过期时间(Expires)
可缓存的最大内容尺寸
可缓存的最小内容尺寸
支持缓存分片内容
强制返回分片内容
强制Range回源
状态码列表
跳过的Cache-Control值
跳过Set-Cookie
支持请求no-cache刷新
允许If-None-Match回源
允许If-Modified-Since回源
允许异步读取源站
支持分段内容
`
})
// 缓存条件列表
Vue.component("http-cache-refs-box", {
props: ["v-cache-refs"],
data: function () {
let refs = this.vCacheRefs
if (refs == null) {
refs = []
}
return {
refs: refs
}
},
methods: {
timeUnitName: function (unit) {
switch (unit) {
case "ms":
return "毫秒"
case "second":
return "秒"
case "minute":
return "分钟"
case "hour":
return "小时"
case "day":
return "天"
case "week":
return "周 "
}
return unit
}
},
template: `
缓存条件
缓存时间
忽略URI参数
{{cacheRef.minSize.count}}{{cacheRef.minSize.unit}}
- {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}}
0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}}
{{cacheRef.methods.join(", ")}}
Expires
状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}}
分片缓存
Range回源
If-None-Match
If-Modified-Since
支持异步
{{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}}
不缓存
`
})
Vue.component("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) {
id++
ref.id = id
// check max size
if (ref.maxSize != null && maxBytes != null && maxBytes.count > 0 && teaweb.compareSizeCapacity(ref.maxSize, maxBytes) > 0) {
ref.overMaxSize = maxBytes
}
})
return {
refs: refs,
id: id // 用来对条件进行排序
}
},
methods: {
addRef: function (isReverse) {
window.UPDATING_CACHE_REF = null
let height = window.innerHeight
if (height > 500) {
height = 500
}
let that = this
teaweb.popup("/servers/server/settings/cache/createPopup?isReverse=" + (isReverse ? 1 : 0), {
height: height + "px",
callback: function (resp) {
let newRef = resp.data.cacheRef
if (newRef.conds == null) {
return
}
that.id++
newRef.id = that.id
if (newRef.isReverse) {
let newRefs = []
let isAdded = false
that.refs.forEach(function (v) {
if (!v.isReverse && !isAdded) {
newRefs.push(newRef)
isAdded = true
}
newRefs.push(v)
})
if (!isAdded) {
newRefs.push(newRef)
}
that.updateRefs(newRefs)
} else {
that.refs.push(newRef)
}
// move to bottom
var afterChangeCallback = function () {
setTimeout(function () {
let rightBox = document.querySelector(".right-box")
if (rightBox != null) {
rightBox.scrollTo(0, isReverse ? 0 : 100000)
}
}, 100)
}
that.change(afterChangeCallback)
}
})
},
updateRef: function (index, cacheRef) {
window.UPDATING_CACHE_REF = teaweb.clone(cacheRef)
let height = window.innerHeight
if (height > 500) {
height = 500
}
let that = this
teaweb.popup("/servers/server/settings/cache/createPopup", {
height: height + "px",
callback: function (resp) {
resp.data.cacheRef.id = that.refs[index].id
Vue.set(that.refs, index, resp.data.cacheRef)
that.change()
that.$refs.cacheRef[index].updateConds(resp.data.cacheRef.conds, resp.data.cacheRef.simpleCond)
that.$refs.cacheRef[index].notifyChange()
}
})
},
disableRef: function (ref) {
ref.isOn = false
this.change()
},
enableRef: function (ref) {
ref.isOn = true
this.change()
},
removeRef: function (index) {
let that = this
teaweb.confirm("确定要删除此缓存设置吗?", function () {
that.refs.$remove(index)
that.change()
})
},
updateRefs: function (newRefs) {
this.refs = newRefs
if (this.vCacheConfig != null) {
this.vCacheConfig.cacheRefs = newRefs
}
},
timeUnitName: function (unit) {
switch (unit) {
case "ms":
return "毫秒"
case "second":
return "秒"
case "minute":
return "分钟"
case "hour":
return "小时"
case "day":
return "天"
case "week":
return "周 "
}
return unit
},
change: function (callback) {
this.$forceUpdate()
// 自动保存
if (this.vCachePolicyId != null && this.vCachePolicyId > 0) { // 缓存策略
Tea.action("/servers/components/cache/updateRefs")
.params({
cachePolicyId: this.vCachePolicyId,
refsJSON: JSON.stringify(this.refs)
})
.post()
} else if (this.vWebId != null && this.vWebId > 0) { // Server Web or Group Web
Tea.action("/servers/server/settings/cache/updateRefs")
.params({
webId: this.vWebId,
refsJSON: JSON.stringify(this.refs)
})
.success(function (resp) {
if (resp.data.isUpdated) {
teaweb.successToast("保存成功", null, function () {
if (typeof callback == "function") {
callback()
}
})
}
})
.post()
}
},
search: function (keyword) {
if (typeof keyword != "string") {
keyword = ""
}
this.refs.forEach(function (ref) {
if (keyword.length == 0) {
ref.visible = true
return
}
ref.visible = false
// simple cond
if (ref.simpleCond != null && typeof ref.simpleCond.value == "string" && teaweb.match(ref.simpleCond.value, keyword)) {
ref.visible = true
return
}
// composed conds
if (ref.conds == null || ref.conds.groups == null || ref.conds.groups.length == 0) {
return
}
ref.conds.groups.forEach(function (group) {
if (group.conds != null) {
group.conds.forEach(function (cond) {
if (typeof cond.value == "string" && teaweb.match(cond.value, keyword)) {
ref.visible = true
}
})
}
})
})
this.$forceUpdate()
}
},
template: `
缓存条件
缓存时间
操作
忽略URI参数
{{cacheRef.minSize.count}}{{cacheRef.minSize.unit}}
- {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit.toUpperCase()}}
0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit.toUpperCase()}}
系统限制{{cacheRef.overMaxSize.count}}{{cacheRef.overMaxSize.unit.toUpperCase()}}
{{cacheRef.methods.join(", ")}}
Expires
状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}}
分片缓存
Range回源
If-None-Match
If-Modified-Since
支持异步
{{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}}
不缓存
修改
暂停 恢复
删除
+添加缓存条件 +添加不缓存条件
`
})
Vue.component("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: `
启用过时缓存
有效期
状态码
支持stale-if-error
`
})
// HTTP CC防护配置
Vue.component("http-cc-config-box", {
props: ["v-cc-config", "v-is-location", "v-is-group"],
data: function () {
let config = this.vCcConfig
if (config == null) {
config = {
isPrior: false,
isOn: false,
enableFingerprint: true,
enableGET302: true,
onlyURLPatterns: [],
exceptURLPatterns: [],
useDefaultThresholds: true,
ignoreCommonFiles: true
}
}
if (config.thresholds == null || config.thresholds.length == 0) {
config.thresholds = [
{
maxRequests: 0
},
{
maxRequests: 0
},
{
maxRequests: 0
}
]
}
if (typeof config.enableFingerprint != "boolean") {
config.enableFingerprint = true
}
if (typeof config.enableGET302 != "boolean") {
config.enableGET302 = true
}
if (config.onlyURLPatterns == null) {
config.onlyURLPatterns = []
}
if (config.exceptURLPatterns == null) {
config.exceptURLPatterns = []
}
return {
config: config,
moreOptionsVisible: false,
minQPSPerIP: config.minQPSPerIP,
useCustomThresholds: !config.useDefaultThresholds,
thresholdMaxRequests0: this.maxRequestsStringAtThresholdIndex(config, 0),
thresholdMaxRequests1: this.maxRequestsStringAtThresholdIndex(config, 1),
thresholdMaxRequests2: this.maxRequestsStringAtThresholdIndex(config, 2)
}
},
watch: {
minQPSPerIP: function (v) {
let qps = parseInt(v.toString())
if (isNaN(qps) || qps < 0) {
qps = 0
}
this.config.minQPSPerIP = qps
},
thresholdMaxRequests0: function (v) {
this.setThresholdMaxRequests(0, v)
},
thresholdMaxRequests1: function (v) {
this.setThresholdMaxRequests(1, v)
},
thresholdMaxRequests2: function (v) {
this.setThresholdMaxRequests(2, v)
},
useCustomThresholds: function (b) {
this.config.useDefaultThresholds = !b
}
},
methods: {
maxRequestsStringAtThresholdIndex: function (config, index) {
if (config.thresholds == null) {
return ""
}
if (index < config.thresholds.length) {
let s = config.thresholds[index].maxRequests.toString()
if (s == "0") {
s = ""
}
return s
}
return ""
},
setThresholdMaxRequests: function (index, v) {
let maxRequests = parseInt(v)
if (isNaN(maxRequests) || maxRequests < 0) {
maxRequests = 0
}
if (index < this.config.thresholds.length) {
this.config.thresholds[index].maxRequests = maxRequests
}
},
showMoreOptions: function () {
this.moreOptionsVisible = !this.moreOptionsVisible
}
},
template: ``
})
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: ``
})
// 压缩配置
Vue.component("http-compression-config-box", {
props: ["v-compression-config", "v-is-location", "v-is-group"],
mounted: function () {
let that = this
sortLoad(function () {
that.initSortableTypes()
})
},
data: function () {
let config = this.vCompressionConfig
if (config == null) {
config = {
isPrior: false,
isOn: false,
useDefaultTypes: true,
types: ["brotli", "gzip", "zstd", "deflate"],
level: 0,
decompressData: false,
gzipRef: null,
deflateRef: null,
brotliRef: null,
minLength: {count: 1, "unit": "kb"},
maxLength: {count: 32, "unit": "mb"},
mimeTypes: ["text/*", "application/javascript", "application/json", "application/atom+xml", "application/rss+xml", "application/xhtml+xml", "font/*", "image/svg+xml"],
extensions: [".js", ".json", ".html", ".htm", ".xml", ".css", ".woff2", ".txt"],
exceptExtensions: [".apk", ".ipa"],
conds: null,
enablePartialContent: false,
onlyURLPatterns: [],
exceptURLPatterns: []
}
}
if (config.types == null) {
config.types = []
}
if (config.mimeTypes == null) {
config.mimeTypes = []
}
if (config.extensions == null) {
config.extensions = []
}
let allTypes = [
{
name: "Gzip",
code: "gzip",
isOn: true
},
{
name: "Deflate",
code: "deflate",
isOn: true
},
{
name: "Brotli",
code: "brotli",
isOn: true
},
{
name: "ZSTD",
code: "zstd",
isOn: true
}
]
let configTypes = []
config.types.forEach(function (typeCode) {
allTypes.forEach(function (t) {
if (typeCode == t.code) {
t.isOn = true
configTypes.push(t)
}
})
})
allTypes.forEach(function (t) {
if (!config.types.$contains(t.code)) {
t.isOn = false
configTypes.push(t)
}
})
return {
config: config,
moreOptionsVisible: false,
allTypes: configTypes
}
},
methods: {
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
},
changeExtensions: function (values) {
values.forEach(function (v, k) {
if (v.length > 0 && v[0] != ".") {
values[k] = "." + v
}
})
this.config.extensions = values
},
changeExceptExtensions: function (values) {
values.forEach(function (v, k) {
if (v.length > 0 && v[0] != ".") {
values[k] = "." + v
}
})
this.config.exceptExtensions = values
},
changeMimeTypes: function (values) {
this.config.mimeTypes = values
},
changeAdvancedVisible: function () {
this.moreOptionsVisible = !this.moreOptionsVisible
},
changeConds: function (conds) {
this.config.conds = conds
},
changeType: function () {
this.config.types = []
let that = this
this.allTypes.forEach(function (v) {
if (v.isOn) {
that.config.types.push(v.code)
}
})
},
initSortableTypes: function () {
let box = document.querySelector("#compression-types-box")
let that = this
Sortable.create(box, {
draggable: ".checkbox",
handle: ".icon.handle",
onStart: function () {
},
onUpdate: function (event) {
let checkboxes = box.querySelectorAll(".checkbox")
let codes = []
checkboxes.forEach(function (checkbox) {
let code = checkbox.getAttribute("data-code")
codes.push(code)
})
that.config.types = codes
}
})
}
},
template: ``
})
// URL扩展名条件
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: ``
})
// 排除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: ``
})
// 根据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: `
`
})
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: `
`
})
// 首页
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: `
`
})
// 全站
Vue.component("http-cond-url-all", {
props: ["v-cond"],
data: function () {
let cond = {
isRequest: true,
param: "${requestPath}",
operator: "prefix",
value: "/",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
// URL精准匹配
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: `
`
})
Vue.component("http-cond-url-not-eq", {
props: ["v-cond"],
mounted: function () {
this.$refs.valueInput.focus()
},
data: function () {
let cond = {
isRequest: true,
param: "${requestPath}",
operator: "eq",
value: "",
isReverse: true,
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
// URL正则匹配
Vue.component("http-cond-url-regexp", {
props: ["v-cond"],
mounted: function () {
this.$refs.valueInput.focus()
},
data: function () {
let cond = {
isRequest: true,
param: "${requestPath}",
operator: "regexp",
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
// 排除URL正则匹配
Vue.component("http-cond-url-not-regexp", {
props: ["v-cond"],
mounted: function () {
this.$refs.valueInput.focus()
},
data: function () {
let cond = {
isRequest: true,
param: "${requestPath}",
operator: "not regexp",
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
// URL通配符
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: `
`
})
// User-Agent正则匹配
Vue.component("http-cond-user-agent-regexp", {
props: ["v-cond"],
mounted: function () {
this.$refs.valueInput.focus()
},
data: function () {
let cond = {
isRequest: true,
param: "${userAgent}",
operator: "regexp",
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
// User-Agent正则不匹配
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: `
`
})
// 根据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: ``
})
// 参数匹配
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: `
参数值
操作符
{{operator.name}}
对比值
不区分大小写
`
})
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: ``
})
// 页面动态加密配置
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: ``
})
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: ``
})
// 动作选择
Vue.component("http-firewall-actions-box", {
props: ["v-actions", "v-firewall-policy", "v-action-configs", "v-group-type"],
mounted: function () {
let that = this
Tea.action("/servers/iplists/levelOptions")
.success(function (resp) {
that.ipListLevels = resp.data.levels
})
.post()
this.loadJS(function () {
let box = document.getElementById("actions-box")
Sortable.create(box, {
draggable: ".label",
handle: ".icon.handle",
onStart: function () {
that.cancel()
},
onUpdate: function (event) {
let labels = box.getElementsByClassName("label")
let newConfigs = []
for (let i = 0; i < labels.length; i++) {
let index = parseInt(labels[i].getAttribute("data-index"))
newConfigs.push(that.configs[index])
}
that.configs = newConfigs
}
})
})
},
data: function () {
if (this.vFirewallPolicy.inbound == null) {
this.vFirewallPolicy.inbound = {}
}
if (this.vFirewallPolicy.inbound.groups == null) {
this.vFirewallPolicy.inbound.groups = []
}
if (this.vFirewallPolicy.outbound == null) {
this.vFirewallPolicy.outbound = {}
}
if (this.vFirewallPolicy.outbound.groups == null) {
this.vFirewallPolicy.outbound.groups = []
}
let id = 0
let configs = []
if (this.vActionConfigs != null) {
configs = this.vActionConfigs
configs.forEach(function (v) {
v.id = (id++)
})
}
var defaultPageBody = `
\t403 Forbidden
\t
403 Forbidden
Connection: \${remoteAddr} (Client) -> \${serverAddr} (Server)
Request ID: \${requestId}
`
return {
id: id,
actions: this.vActions,
configs: configs,
isAdding: false,
editingIndex: -1,
action: null,
actionCode: "",
actionOptions: {},
// IPList相关
ipListLevels: [],
// 动作参数
allowScope: "global",
blockTimeout: "",
blockTimeoutMax: "",
blockScope: "global",
captchaLife: "",
captchaMaxFails: "",
captchaFailBlockTimeout: "",
get302Life: "",
post307Life: "",
recordIPType: "black",
recordIPLevel: "critical",
recordIPTimeout: "",
recordIPListId: 0,
recordIPListName: "",
tagTags: [],
pageUseDefault: true,
pageStatus: 403,
pageBody: defaultPageBody,
defaultPageBody: defaultPageBody,
redirectStatus: 307,
redirectURL: "",
goGroupName: "",
goGroupId: 0,
goGroup: null,
goSetId: 0,
goSetName: "",
jsCookieLife: "",
jsCookieMaxFails: "",
jsCookieFailBlockTimeout: "",
statusOptions: [
{"code": 301, "text": "Moved Permanently"},
{"code": 308, "text": "Permanent Redirect"},
{"code": 302, "text": "Found"},
{"code": 303, "text": "See Other"},
{"code": 307, "text": "Temporary Redirect"}
]
}
},
watch: {
actionCode: function (code) {
this.action = this.actions.$find(function (k, v) {
return v.code == code
})
this.actionOptions = {}
},
allowScope: function (v) {
this.actionOptions["scope"] = v
},
blockTimeout: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["timeout"] = 0
} else {
this.actionOptions["timeout"] = v
}
},
blockTimeoutMax: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["timeoutMax"] = 0
} else {
this.actionOptions["timeoutMax"] = v
}
},
blockScope: function (v) {
this.actionOptions["scope"] = v
},
captchaLife: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["life"] = 0
} else {
this.actionOptions["life"] = v
}
},
captchaMaxFails: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["maxFails"] = 0
} else {
this.actionOptions["maxFails"] = v
}
},
captchaFailBlockTimeout: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["failBlockTimeout"] = 0
} else {
this.actionOptions["failBlockTimeout"] = v
}
},
get302Life: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["life"] = 0
} else {
this.actionOptions["life"] = v
}
},
post307Life: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["life"] = 0
} else {
this.actionOptions["life"] = v
}
},
recordIPType: function (v) {
this.recordIPListId = 0
},
recordIPTimeout: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["timeout"] = 0
} else {
this.actionOptions["timeout"] = v
}
},
goGroupId: function (groupId) {
let group = this.vFirewallPolicy.inbound.groups.$find(function (k, v) {
return v.id == groupId
})
this.goGroup = group
if (group == null) {
// search outbound groups
group = this.vFirewallPolicy.outbound.groups.$find(function (k, v) {
return v.id == groupId
})
if (group == null) {
this.goGroupName = ""
} else {
this.goGroup = group
this.goGroupName = group.name
}
} else {
this.goGroupName = group.name
}
this.goSetId = 0
this.goSetName = ""
},
goSetId: function (setId) {
if (this.goGroup == null) {
return
}
let set = this.goGroup.sets.$find(function (k, v) {
return v.id == setId
})
if (set == null) {
this.goSetId = 0
this.goSetName = ""
} else {
this.goSetName = set.name
}
},
jsCookieLife: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["life"] = 0
} else {
this.actionOptions["life"] = v
}
},
jsCookieMaxFails: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["maxFails"] = 0
} else {
this.actionOptions["maxFails"] = v
}
},
jsCookieFailBlockTimeout: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["failBlockTimeout"] = 0
} else {
this.actionOptions["failBlockTimeout"] = v
}
},
},
methods: {
add: function () {
this.action = null
this.actionCode = "page"
this.isAdding = true
this.actionOptions = {}
// 动作参数
this.allowScope = "global"
this.blockTimeout = ""
this.blockTimeoutMax = ""
this.blockScope = "global"
this.captchaLife = ""
this.captchaMaxFails = ""
this.captchaFailBlockTimeout = ""
this.jsCookieLife = ""
this.jsCookieMaxFails = ""
this.jsCookieFailBlockTimeout = ""
this.get302Life = ""
this.post307Life = ""
this.recordIPLevel = "critical"
this.recordIPType = "black"
this.recordIPTimeout = ""
this.recordIPListId = 0
this.recordIPListName = ""
this.tagTags = []
this.pageUseDefault = true
this.pageStatus = 403
this.pageBody = this.defaultPageBody
this.redirectStatus = 307
this.redirectURL = ""
this.goGroupName = ""
this.goGroupId = 0
this.goGroup = null
this.goSetId = 0
this.goSetName = ""
let that = this
this.action = this.vActions.$find(function (k, v) {
return v.code == that.actionCode
})
// 滚到界面底部
this.scroll()
},
remove: function (index) {
this.isAdding = false
this.editingIndex = -1
this.configs.$remove(index)
},
update: function (index, config) {
if (this.isAdding && this.editingIndex == index) {
this.cancel()
return
}
this.add()
this.isAdding = true
this.editingIndex = index
this.actionCode = config.code
this.action = this.actions.$find(function (k, v) {
return v.code == config.code
})
switch (config.code) {
case "block":
this.blockTimeout = ""
this.blockTimeoutMax = ""
if (config.options.timeout != null || config.options.timeout > 0) {
this.blockTimeout = config.options.timeout.toString()
}
if (config.options.timeoutMax != null || config.options.timeoutMax > 0) {
this.blockTimeoutMax = config.options.timeoutMax.toString()
}
if (config.options.scope != null && config.options.scope.length > 0) {
this.blockScope = config.options.scope
} else {
this.blockScope = "global" // 兼容先前版本遗留的默认值
}
break
case "allow":
if (config.options != null && config.options.scope != null && config.options.scope.length > 0) {
this.allowScope = config.options.scope
} else {
this.allowScope = "global"
}
break
case "log":
break
case "captcha":
this.captchaLife = ""
if (config.options.life != null || config.options.life > 0) {
this.captchaLife = config.options.life.toString()
}
this.captchaMaxFails = ""
if (config.options.maxFails != null || config.options.maxFails > 0) {
this.captchaMaxFails = config.options.maxFails.toString()
}
this.captchaFailBlockTimeout = ""
if (config.options.failBlockTimeout != null || config.options.failBlockTimeout > 0) {
this.captchaFailBlockTimeout = config.options.failBlockTimeout.toString()
}
break
case "js_cookie":
this.jsCookieLife = ""
if (config.options.life != null || config.options.life > 0) {
this.jsCookieLife = config.options.life.toString()
}
this.jsCookieMaxFails = ""
if (config.options.maxFails != null || config.options.maxFails > 0) {
this.jsCookieMaxFails = config.options.maxFails.toString()
}
this.jsCookieFailBlockTimeout = ""
if (config.options.failBlockTimeout != null || config.options.failBlockTimeout > 0) {
this.jsCookieFailBlockTimeout = config.options.failBlockTimeout.toString()
}
break
case "notify":
break
case "get_302":
this.get302Life = ""
if (config.options.life != null || config.options.life > 0) {
this.get302Life = config.options.life.toString()
}
break
case "post_307":
this.post307Life = ""
if (config.options.life != null || config.options.life > 0) {
this.post307Life = config.options.life.toString()
}
break;
case "record_ip":
if (config.options != null) {
this.recordIPLevel = config.options.level
this.recordIPType = config.options.type
if (config.options.timeout > 0) {
this.recordIPTimeout = config.options.timeout.toString()
}
let that = this
// VUE需要在函数执行完之后才会调用watch函数,这样会导致设置的值被覆盖,所以这里使用setTimeout
setTimeout(function () {
that.recordIPListId = config.options.ipListId
that.recordIPListName = config.options.ipListName
})
}
break
case "tag":
this.tagTags = []
if (config.options.tags != null) {
this.tagTags = config.options.tags
}
break
case "page":
this.pageUseDefault = true
this.pageStatus = 403
this.pageBody = this.defaultPageBody
if (typeof config.options.useDefault === "boolean") {
this.pageUseDefault = config.options.useDefault
} else {
this.pageUseDefault = false
}
if (config.options.status != null) {
this.pageStatus = config.options.status
}
if (config.options.body != null) {
this.pageBody = config.options.body
}
break
case "redirect":
this.redirectStatus = 307
this.redirectURL = ""
if (config.options.status != null) {
this.redirectStatus = config.options.status
}
if (config.options.url != null) {
this.redirectURL = config.options.url
}
break
case "go_group":
if (config.options != null) {
this.goGroupName = config.options.groupName
this.goGroupId = config.options.groupId
this.goGroup = this.vFirewallPolicy.inbound.groups.$find(function (k, v) {
return v.id == config.options.groupId
})
}
break
case "go_set":
if (config.options != null) {
this.goGroupName = config.options.groupName
this.goGroupId = config.options.groupId
this.goGroup = this.vFirewallPolicy.inbound.groups.$find(function (k, v) {
return v.id == config.options.groupId
})
// VUE需要在函数执行完之后才会调用watch函数,这样会导致设置的值被覆盖,所以这里使用setTimeout
let that = this
setTimeout(function () {
that.goSetId = config.options.setId
if (that.goGroup != null) {
let set = that.goGroup.sets.$find(function (k, v) {
return v.id == config.options.setId
})
if (set != null) {
that.goSetName = set.name
}
}
})
}
break
}
// 滚到界面底部
this.scroll()
},
cancel: function () {
this.isAdding = false
this.editingIndex = -1
},
confirm: function () {
if (this.action == null) {
return
}
if (this.actionOptions == null) {
this.actionOptions = {}
}
// record_ip
if (this.actionCode == "record_ip") {
let timeout = parseInt(this.recordIPTimeout)
if (isNaN(timeout)) {
timeout = 0
}
if (this.recordIPListId < 0) { // can be 0
return
}
this.actionOptions = {
type: this.recordIPType,
level: this.recordIPLevel,
timeout: timeout,
ipListId: this.recordIPListId,
ipListName: this.recordIPListName
}
} else if (this.actionCode == "tag") { // tag
if (this.tagTags == null || this.tagTags.length == 0) {
return
}
this.actionOptions = {
tags: this.tagTags
}
} else if (this.actionCode == "page") {
let pageStatus = this.pageStatus.toString()
if (!pageStatus.match(/^\d{3}$/)) {
pageStatus = 403
} else {
pageStatus = parseInt(pageStatus)
}
this.actionOptions = {
useDefault: this.pageUseDefault,
status: pageStatus,
body: this.pageBody
}
} else if (this.actionCode == "redirect") {
let redirectStatus = this.redirectStatus.toString()
if (!redirectStatus.match(/^\d{3}$/)) {
redirectStatus = 307
} else {
redirectStatus = parseInt(redirectStatus)
}
if (this.redirectURL.length == 0) {
teaweb.warn("请输入跳转到URL")
return
}
this.actionOptions = {
status: redirectStatus,
url: this.redirectURL
}
} else if (this.actionCode == "go_group") { // go_group
let groupId = this.goGroupId
if (typeof (groupId) == "string") {
groupId = parseInt(groupId)
if (isNaN(groupId)) {
groupId = 0
}
}
if (groupId <= 0) {
return
}
this.actionOptions = {
groupId: groupId.toString(),
groupName: this.goGroupName
}
} else if (this.actionCode == "go_set") { // go_set
let groupId = this.goGroupId
if (typeof (groupId) == "string") {
groupId = parseInt(groupId)
if (isNaN(groupId)) {
groupId = 0
}
}
let setId = this.goSetId
if (typeof (setId) == "string") {
setId = parseInt(setId)
if (isNaN(setId)) {
setId = 0
}
}
if (setId <= 0) {
return
}
this.actionOptions = {
groupId: groupId.toString(),
groupName: this.goGroupName,
setId: setId.toString(),
setName: this.goSetName
}
}
let options = {}
for (let k in this.actionOptions) {
if (this.actionOptions.hasOwnProperty(k)) {
options[k] = this.actionOptions[k]
}
}
if (this.editingIndex > -1) {
this.configs[this.editingIndex] = {
id: this.configs[this.editingIndex].id,
code: this.actionCode,
name: this.action.name,
options: options
}
} else {
this.configs.push({
id: (this.id++),
code: this.actionCode,
name: this.action.name,
options: options
})
}
this.cancel()
},
removeRecordIPList: function () {
this.recordIPListId = 0
},
selectRecordIPList: function () {
let that = this
teaweb.popup("/servers/iplists/selectPopup?type=" + this.recordIPType, {
width: "50em",
height: "30em",
callback: function (resp) {
that.recordIPListId = resp.data.list.id
that.recordIPListName = resp.data.list.name
}
})
},
changeTags: function (tags) {
this.tagTags = tags
},
loadJS: function (callback) {
if (typeof Sortable != "undefined") {
callback()
return
}
// 引入js
let jsFile = document.createElement("script")
jsFile.setAttribute("src", "/js/sortable.min.js")
jsFile.addEventListener("load", function () {
callback()
})
document.head.appendChild(jsFile)
},
scroll: function () {
setTimeout(function () {
let mainDiv = document.getElementsByClassName("main")
if (mainDiv.length > 0) {
mainDiv[0].scrollTo(0, 1000)
}
}, 10)
}
},
template: `
{{config.name}}
({{config.code.toUpperCase()}})
[分组]
[网站]
[网站和策略]
:封禁时长{{config.options.timeout}}-{{config.options.timeoutMax}} 秒
:有效期{{config.options.life}}秒
/ 最多失败{{config.options.maxFails}}次
:有效期{{config.options.life}}秒
/ 最多失败{{config.options.maxFails}}次
:有效期{{config.options.life}}秒
:有效期{{config.options.life}}秒
::{{config.options.ipListName}} {{config.options.ipListName}}
:{{config.options.tags.join(", ")}}
:[{{config.options.status}}] [默认页面]
:{{config.options.url}}
:{{config.options.groupName}}
:{{config.options.groupName}} / {{config.options.setName}}
[所有网站]
[当前网站]
+
`
})
// Action列表
Vue.component("http-firewall-actions-view", {
props: ["v-actions"],
template: `
{{action.name}} ({{action.code.toUpperCase()}})
[{{action.options.status}}]
[分组]
[网站]
[网站和策略]
黑名单
白名单
灰名单
`
})
Vue.component("http-firewall-config-box", {
props: ["v-firewall-config", "v-is-location", "v-firewall-policy"],
data: function () {
let 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: ``
})
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: ``
})
Vue.component("http-firewall-policy-selector", {
props: ["v-http-firewall-policy"],
mounted: function () {
let that = this
Tea.action("/servers/components/waf/count")
.post()
.success(function (resp) {
that.count = resp.data.count
})
},
data: function () {
let firewallPolicy = this.vHttpFirewallPolicy
return {
count: 0,
firewallPolicy: firewallPolicy
}
},
methods: {
remove: function () {
this.firewallPolicy = null
},
select: function () {
let that = this
teaweb.popup("/servers/components/waf/selectPopup", {
callback: function (resp) {
that.firewallPolicy = resp.data.firewallPolicy
}
})
},
create: function () {
let that = this
teaweb.popup("/servers/components/waf/createPopup", {
height: "26em",
callback: function (resp) {
that.firewallPolicy = resp.data.firewallPolicy
}
})
}
},
template: ``
})
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: ``
})
Vue.component("http-firewall-region-selector", {
props: ["v-type", "v-countries"],
data: function () {
let countries = this.vCountries
if (countries == null) {
countries = []
}
return {
listType: this.vType,
countries: countries
}
},
methods: {
addCountry: function () {
let selectedCountryIds = this.countries.map(function (country) {
return country.id
})
let that = this
teaweb.popup("/servers/server/settings/waf/ipadmin/selectCountriesPopup?type=" + this.listType + "&selectedCountryIds=" + selectedCountryIds.join(","), {
width: "52em",
height: "30em",
callback: function (resp) {
that.countries = resp.data.selectedCountries
that.$forceUpdate()
that.notifyChange()
}
})
},
removeCountry: function (index) {
this.countries.$remove(index)
this.notifyChange()
},
resetCountries: function () {
this.countries = []
this.notifyChange()
},
notifyChange: function () {
this.$emit("change", {
"countries": this.countries
})
}
},
template: `
暂时没有选择允许 封禁 的区域。
({{country.letter}}){{country.name}}
修改 清空
`
})
// 显示WAF规则的标签
Vue.component("http-firewall-rule-label", {
props: ["v-rule"],
data: function () {
return {
rule: this.vRule
}
},
methods: {
showErr: function (err) {
teaweb.popupTip("规则校验错误,请修正:" + teaweb.encodeHTML(err) + " ")
},
calculateParamName: function (param) {
let paramName = ""
if (param != null) {
window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) {
if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) {
paramName = checkpoint.name
}
})
}
return paramName
},
calculateParamDescription: function (param) {
let paramName = ""
let paramDescription = ""
if (param != null) {
window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) {
if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) {
paramName = checkpoint.name
paramDescription = checkpoint.description
}
})
}
return paramName + ": " + paramDescription
},
operatorName: function (operatorCode) {
let operatorName = operatorCode
if (typeof (window.WAF_RULE_OPERATORS) != null) {
window.WAF_RULE_OPERATORS.forEach(function (v) {
if (v.code == operatorCode) {
operatorName = v.name
}
})
}
return operatorName
},
operatorDescription: function (operatorCode) {
let operatorName = operatorCode
let operatorDescription = ""
if (typeof (window.WAF_RULE_OPERATORS) != null) {
window.WAF_RULE_OPERATORS.forEach(function (v) {
if (v.code == operatorCode) {
operatorName = v.name
operatorDescription = v.description
}
})
}
return operatorName + ": " + operatorDescription
},
operatorDataType: function (operatorCode) {
let operatorDataType = "none"
if (typeof (window.WAF_RULE_OPERATORS) != null) {
window.WAF_RULE_OPERATORS.forEach(function (v) {
if (v.code == operatorCode) {
operatorDataType = v.dataType
}
})
}
return operatorDataType
},
isEmptyString: function (v) {
return typeof v == "string" && v.length == 0
}
},
template: `
{{rule.name}}
{{calculateParamName(rule.param)}} {{rule.param}}
{{rule.checkpointOptions.period}}秒内请求数
允许{{rule.checkpointOptions.allowDomains}}
禁止{{rule.checkpointOptions.denyDomains}}
| {{paramFilter.code}}
<{{operatorName(rule.operator)}}>
{{rule.value}}
[空]
({{rule.description}})
规则错误
`
})
Vue.component("http-firewall-rules-box", {
props: ["v-rules", "v-type"],
data: function () {
let rules = this.vRules
if (rules == null) {
rules = []
}
return {
rules: rules
}
},
methods: {
addRule: function () {
window.UPDATING_RULE = null
let that = this
teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, {
height: "30em",
callback: function (resp) {
that.rules.push(resp.data.rule)
}
})
},
updateRule: function (index, rule) {
window.UPDATING_RULE = teaweb.clone(rule)
let that = this
teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, {
height: "30em",
callback: function (resp) {
Vue.set(that.rules, index, resp.data.rule)
}
})
},
removeRule: function (index) {
let that = this
teaweb.confirm("确定要删除此规则吗?", function () {
that.rules.$remove(index)
})
},
operatorName: function (operatorCode) {
let operatorName = operatorCode
if (typeof (window.WAF_RULE_OPERATORS) != null) {
window.WAF_RULE_OPERATORS.forEach(function (v) {
if (v.code == operatorCode) {
operatorName = v.name
}
})
}
return operatorName
},
operatorDescription: function (operatorCode) {
let operatorName = operatorCode
let operatorDescription = ""
if (typeof (window.WAF_RULE_OPERATORS) != null) {
window.WAF_RULE_OPERATORS.forEach(function (v) {
if (v.code == operatorCode) {
operatorName = v.name
operatorDescription = v.description
}
})
}
return operatorName + ": " + operatorDescription
},
operatorDataType: function (operatorCode) {
let operatorDataType = "none"
if (typeof (window.WAF_RULE_OPERATORS) != null) {
window.WAF_RULE_OPERATORS.forEach(function (v) {
if (v.code == operatorCode) {
operatorDataType = v.dataType
}
})
}
return operatorDataType
},
calculateParamName: function (param) {
let paramName = ""
if (param != null) {
window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) {
if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) {
paramName = checkpoint.name
}
})
}
return paramName
},
calculateParamDescription: function (param) {
let paramName = ""
let paramDescription = ""
if (param != null) {
window.WAF_RULE_CHECKPOINTS.forEach(function (checkpoint) {
if (param == "${" + checkpoint.prefix + "}" || param.startsWith("${" + checkpoint.prefix + ".")) {
paramName = checkpoint.name
paramDescription = checkpoint.description
}
})
}
return paramName + ": " + paramDescription
},
isEmptyString: function (v) {
return typeof v == "string" && v.length == 0
}
},
template: `
{{rule.name}}
{{calculateParamName(rule.param)}} {{rule.param}}
{{rule.checkpointOptions.period}}秒内请求数
允许{{rule.checkpointOptions.allowDomains}}
禁止{{rule.checkpointOptions.denyDomains}}
| {{paramFilter.code}} <{{operatorName(rule.operator)}}>
{{rule.value}}
[空]
({{rule.description}})
+
`
})
// 通用Header长度
let defaultGeneralHeaders = ["Cache-Control", "Connection", "Date", "Pragma", "Trailer", "Transfer-Encoding", "Upgrade", "Via", "Warning"]
Vue.component("http-cond-general-header-length", {
props: ["v-checkpoint"],
data: function () {
let headers = null
let length = null
if (window.parent.UPDATING_RULE != null) {
let options = window.parent.UPDATING_RULE.checkpointOptions
if (options.headers != null && Array.$isArray(options.headers)) {
headers = options.headers
}
if (options.length != null) {
length = options.length
}
}
if (headers == null) {
headers = defaultGeneralHeaders
}
if (length == null) {
length = 128
}
let that = this
setTimeout(function () {
that.change()
}, 100)
return {
headers: headers,
length: length
}
},
watch: {
length: function (v) {
let len = parseInt(v)
if (isNaN(len)) {
len = 0
}
if (len < 0) {
len = 0
}
this.length = len
this.change()
}
},
methods: {
change: function () {
this.vCheckpoint.options = [
{
code: "headers",
value: this.headers
},
{
code: "length",
value: this.length
}
]
}
},
template: ``
})
// 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: ``
})
// 防盗链
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: `
来源域名允许为空
来源域名允许一致
允许的来源域名
禁止的来源域名
同时检查Origin
`
})
Vue.component("http-header-assistant", {
props: ["v-type", "v-value"],
mounted: function () {
let that = this
Tea.action("/servers/headers/options?type=" + this.vType)
.post()
.success(function (resp) {
that.allHeaders = resp.data.headers
})
},
data: function () {
return {
allHeaders: [],
matchedHeaders: [],
selectedHeaderName: ""
}
},
watch: {
vValue: function (v) {
if (v != this.selectedHeaderName) {
this.selectedHeaderName = ""
}
if (v.length == 0) {
this.matchedHeaders = []
return
}
this.matchedHeaders = this.allHeaders.filter(function (header) {
return teaweb.match(header, v)
}).slice(0, 10)
}
},
methods: {
select: function (header) {
this.$emit("select", header)
this.selectedHeaderName = header
}
},
template: `
{{header}}
`
})
Vue.component("http-header-policy-box", {
props: ["v-request-header-policy", "v-request-header-ref", "v-response-header-policy", "v-response-header-ref", "v-params", "v-is-location", "v-is-group", "v-has-group-request-config", "v-has-group-response-config", "v-group-setting-url"],
data: function () {
let type = "response"
let hash = window.location.hash
if (hash == "#request") {
type = "request"
}
// ref
let requestHeaderRef = this.vRequestHeaderRef
if (requestHeaderRef == null) {
requestHeaderRef = {
isPrior: false,
isOn: true,
headerPolicyId: 0
}
}
let responseHeaderRef = this.vResponseHeaderRef
if (responseHeaderRef == null) {
responseHeaderRef = {
isPrior: false,
isOn: true,
headerPolicyId: 0
}
}
// 请求相关
let requestSettingHeaders = []
let requestDeletingHeaders = []
let requestNonStandardHeaders = []
let requestPolicy = this.vRequestHeaderPolicy
if (requestPolicy != null) {
if (requestPolicy.setHeaders != null) {
requestSettingHeaders = requestPolicy.setHeaders
}
if (requestPolicy.deleteHeaders != null) {
requestDeletingHeaders = requestPolicy.deleteHeaders
}
if (requestPolicy.nonStandardHeaders != null) {
requestNonStandardHeaders = requestPolicy.nonStandardHeaders
}
}
// 响应相关
let responseSettingHeaders = []
let responseDeletingHeaders = []
let responseNonStandardHeaders = []
let responsePolicy = this.vResponseHeaderPolicy
if (responsePolicy != null) {
if (responsePolicy.setHeaders != null) {
responseSettingHeaders = responsePolicy.setHeaders
}
if (responsePolicy.deleteHeaders != null) {
responseDeletingHeaders = responsePolicy.deleteHeaders
}
if (responsePolicy.nonStandardHeaders != null) {
responseNonStandardHeaders = responsePolicy.nonStandardHeaders
}
}
let responseCORS = {
isOn: false
}
if (responsePolicy.cors != null) {
responseCORS = responsePolicy.cors
}
return {
type: type,
typeName: (type == "request") ? "请求" : "响应",
requestHeaderRef: requestHeaderRef,
responseHeaderRef: responseHeaderRef,
requestSettingHeaders: requestSettingHeaders,
requestDeletingHeaders: requestDeletingHeaders,
requestNonStandardHeaders: requestNonStandardHeaders,
responseSettingHeaders: responseSettingHeaders,
responseDeletingHeaders: responseDeletingHeaders,
responseNonStandardHeaders: responseNonStandardHeaders,
responseCORS: responseCORS
}
},
methods: {
selectType: function (type) {
this.type = type
window.location.hash = "#" + type
window.location.reload()
},
addSettingHeader: function (policyId) {
teaweb.popup("/servers/server/settings/headers/createSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + this.type, {
height: "22em",
callback: function () {
teaweb.successRefresh("保存成功")
}
})
},
addDeletingHeader: function (policyId, type) {
teaweb.popup("/servers/server/settings/headers/createDeletePopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + type, {
callback: function () {
teaweb.successRefresh("保存成功")
}
})
},
addNonStandardHeader: function (policyId, type) {
teaweb.popup("/servers/server/settings/headers/createNonStandardPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + type, {
callback: function () {
teaweb.successRefresh("保存成功")
}
})
},
updateSettingPopup: function (policyId, headerId) {
teaweb.popup("/servers/server/settings/headers/updateSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&headerId=" + headerId + "&type=" + this.type, {
height: "22em",
callback: function () {
teaweb.successRefresh("保存成功")
}
})
},
deleteDeletingHeader: function (policyId, headerName) {
teaweb.confirm("确定要删除'" + headerName + "'吗?", function () {
Tea.action("/servers/server/settings/headers/deleteDeletingHeader")
.params({
headerPolicyId: policyId,
headerName: headerName
})
.post()
.refresh()
})
},
deleteNonStandardHeader: function (policyId, headerName) {
teaweb.confirm("确定要删除'" + headerName + "'吗?", function () {
Tea.action("/servers/server/settings/headers/deleteNonStandardHeader")
.params({
headerPolicyId: policyId,
headerName: headerName
})
.post()
.refresh()
})
},
deleteHeader: function (policyId, type, headerId) {
teaweb.confirm("确定要删除此报头吗?", function () {
this.$post("/servers/server/settings/headers/delete")
.params({
headerPolicyId: policyId,
type: type,
headerId: headerId
})
.refresh()
}
)
},
updateCORS: function (policyId) {
teaweb.popup("/servers/server/settings/headers/updateCORSPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + this.type, {
height: "30em",
callback: function () {
teaweb.successRefresh("保存成功")
}
})
}
},
template: `
由于已经在当前网站分组 中进行了对应的配置,在这里的配置将不会生效。
由于已经在当前网站分组 中进行了对应的配置,在这里的配置将不会生效。
名称
值
操作
{{header.name}}
{{code}}
{{method}}
{{domain}}
附加
跳转禁用
替换
建议使用当前页面下方的"CORS自适应跨域"功能代替Access-Control-*-*相关报头。
{{header.value}}
修改 删除
其他设置
删除报头
+
非标报头
+
CORS自适应跨域
已启用 未启用 [修改]
`
})
Vue.component("http-header-replace-values", {
props: ["v-replace-values"],
data: function () {
let values = this.vReplaceValues
if (values == null) {
values = []
}
return {
values: values,
isAdding: false,
addingValue: {"pattern": "", "replacement": "", "isCaseInsensitive": false, "isRegexp": false}
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.pattern.focus()
})
},
remove: function (index) {
this.values.$remove(index)
},
confirm: function () {
let that = this
if (this.addingValue.pattern.length == 0) {
teaweb.warn("替换前内容不能为空", function () {
that.$refs.pattern.focus()
})
return
}
this.values.push(this.addingValue)
this.cancel()
},
cancel: function () {
this.isAdding = false
this.addingValue = {"pattern": "", "replacement": "", "isCaseInsensitive": false, "isRegexp": false}
}
},
template: `
{{value.pattern}} =>
{{value.replacement}} [空]
+
`
})
Vue.component("http-hls-config-box", {
props: ["value", "v-is-location", "v-is-group"],
data: function () {
let config = this.value
if (config == null) {
config = {
isPrior: false
}
}
let encryptingConfig = config.encrypting
if (encryptingConfig == null) {
encryptingConfig = {
isOn: false,
onlyURLPatterns: [],
exceptURLPatterns: []
}
config.encrypting = encryptingConfig
}
return {
config: config,
encryptingConfig: encryptingConfig,
encryptingMoreOptionsVisible: false
}
},
methods: {
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior)
},
showEncryptingMoreOptions: function () {
this.encryptingMoreOptionsVisible = !this.encryptingMoreOptionsVisible
}
},
template: ``
})
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: `
[创建]
跳转前
跳转后
HTTP状态码
状态
操作
{{redirect.beforeURL}}
URL跳转
匹配前缀
正则匹配
精准匹配
排除:{{domain}}
仅限:{{domain}}
所有域名
{{redirect.domainsBefore[0]}}
{{redirect.domainsBefore[0]}}等{{redirect.domainsBefore.length}}个域名
域名跳转
{{redirect.domainAfterScheme}}
忽略端口
所有端口
{{redirect.portsBefore.join(", ")}}
{{redirect.portsBefore.slice(0, 5).join(", ")}}等{{redirect.portsBefore.length}}个端口
端口跳转
{{redirect.portAfterScheme}}
匹配条件
->
{{redirect.afterURL}}
{{redirect.domainAfter}}
{{redirect.portAfter}}
{{redirect.status}}
默认
修改
删除
`
})
// 请求方法列表
Vue.component("http-methods-box", {
props: ["v-methods"],
data: function () {
let methods = this.vMethods
if (methods == null) {
methods = []
}
return {
methods: methods,
isAdding: false,
addingMethod: ""
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.addingMethod.focus()
}, 100)
},
confirm: function () {
let that = this
// 删除其中的空格
this.addingMethod = this.addingMethod.replace(/\s/g, "").toUpperCase()
if (this.addingMethod.length == 0) {
teaweb.warn("请输入要添加的请求方法", function () {
that.$refs.addingMethod.focus()
})
return
}
// 是否已经存在
if (this.methods.$contains(this.addingMethod)) {
teaweb.warn("此请求方法已经存在,无需重复添加", function () {
that.$refs.addingMethod.focus()
})
return
}
this.methods.push(this.addingMethod)
this.cancel()
},
remove: function (index) {
this.methods.$remove(index)
},
cancel: function () {
this.isAdding = false
this.addingMethod = ""
}
},
template: ``
})
// 压缩配置
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: ``
})
Vue.component("http-oss-bucket-params", {
props: ["v-oss-config", "v-params", "name"],
data: function () {
let params = this.vParams
if (params == null) {
params = []
}
let ossConfig = this.vOssConfig
if (ossConfig == null) {
ossConfig = {
bucketParam: "input",
bucketName: "",
bucketArgName: ""
}
} else {
// 兼容以往
if (ossConfig.bucketParam != null && ossConfig.bucketParam.length == 0) {
ossConfig.bucketParam = "input"
}
if (ossConfig.options != null && ossConfig.options.bucketName != null && ossConfig.options.bucketName.length > 0) {
ossConfig.bucketName = ossConfig.options.bucketName
}
}
return {
params: params,
ossConfig: ossConfig
}
},
template: `
{{name}}名称获取方式 *
{{param.name.replace("\${optionName}", name)}}
{{param.name}} - {{param.example}}
{{name}}名称 *
{{name}}参数名称 *
`
})
Vue.component("http-pages-and-shutdown-box", {
props: ["v-enable-global-pages", "v-pages", "v-shutdown-config", "v-is-location"],
data: function () {
let pages = []
if (this.vPages != null) {
pages = this.vPages
}
let shutdownConfig = {
isPrior: false,
isOn: false,
bodyType: "html",
url: "",
body: "",
status: 0
}
if (this.vShutdownConfig != null) {
if (this.vShutdownConfig.body == null) {
this.vShutdownConfig.body = ""
}
if (this.vShutdownConfig.bodyType == null) {
this.vShutdownConfig.bodyType = "html"
}
shutdownConfig = this.vShutdownConfig
}
let shutdownStatus = ""
if (shutdownConfig.status > 0) {
shutdownStatus = shutdownConfig.status.toString()
}
return {
pages: pages,
shutdownConfig: shutdownConfig,
shutdownStatus: shutdownStatus,
enableGlobalPages: this.vEnableGlobalPages
}
},
watch: {
shutdownStatus: function (status) {
let statusInt = parseInt(status)
if (!isNaN(statusInt) && statusInt > 0 && statusInt < 1000) {
this.shutdownConfig.status = statusInt
} else {
this.shutdownConfig.status = 0
}
}
},
methods: {
addPage: function () {
let that = this
teaweb.popup("/servers/server/settings/pages/createPopup", {
height: "30em",
callback: function (resp) {
that.pages.push(resp.data.page)
that.notifyChange()
}
})
},
updatePage: function (pageIndex, pageId) {
let that = this
teaweb.popup("/servers/server/settings/pages/updatePopup?pageId=" + pageId, {
height: "30em",
callback: function (resp) {
Vue.set(that.pages, pageIndex, resp.data.page)
that.notifyChange()
}
})
},
removePage: function (pageIndex) {
let that = this
teaweb.confirm("确定要删除此自定义页面吗?", function () {
that.pages.$remove(pageIndex)
that.notifyChange()
})
},
addShutdownHTMLTemplate: function () {
this.shutdownConfig.body = `
\t升级中
\t
\t
网站升级中
为了给您提供更好的服务,我们正在升级网站,请稍后重新访问。
Connection: \${remoteAddr} (Client) -> \${serverAddr} (Server)
Request ID: \${requestId}
`
},
notifyChange: function () {
let parent = this.$el.parentNode
while (true) {
if (parent == null) {
break
}
if (parent.tagName == "FORM") {
break
}
parent = parent.parentNode
}
if (parent != null) {
setTimeout(function () {
Tea.runActionOn(parent)
}, 100)
}
}
},
template: `
自定义页面
响应状态码
页面类型
新状态码
例外URL
限制URL
操作
{{page.status[0]}}
{{page.status}}
{{page.url}}
跳转URL
{{page.newStatus}}
[HTML内容]
{{page.newStatus}}
{{page.newStatus}}
保持
{{urlPattern.pattern}}
-
{{urlPattern.pattern}}
-
修改
删除
+添加自定义页面
临时关闭页面
其他设置
`
})
Vue.component("http-redirect-to-https-box", {
props: ["v-redirect-to-https-config", "v-is-location"],
data: function () {
let redirectToHttpsConfig = this.vRedirectToHttpsConfig
if (redirectToHttpsConfig == null) {
redirectToHttpsConfig = {
isPrior: false,
isOn: false,
host: "",
port: 0,
status: 0,
onlyDomains: [],
exceptDomains: []
}
} else {
if (redirectToHttpsConfig.onlyDomains == null) {
redirectToHttpsConfig.onlyDomains = []
}
if (redirectToHttpsConfig.exceptDomains == null) {
redirectToHttpsConfig.exceptDomains = []
}
}
return {
redirectToHttpsConfig: redirectToHttpsConfig,
portString: (redirectToHttpsConfig.port > 0) ? redirectToHttpsConfig.port.toString() : "",
moreOptionsVisible: false,
statusOptions: [
{"code": 301, "text": "Moved Permanently"},
{"code": 308, "text": "Permanent Redirect"},
{"code": 302, "text": "Found"},
{"code": 303, "text": "See Other"},
{"code": 307, "text": "Temporary Redirect"}
]
}
},
watch: {
"redirectToHttpsConfig.status": function () {
this.redirectToHttpsConfig.status = parseInt(this.redirectToHttpsConfig.status)
},
portString: function (v) {
let port = parseInt(v)
if (!isNaN(port)) {
this.redirectToHttpsConfig.port = port
} else {
this.redirectToHttpsConfig.port = 0
}
}
},
methods: {
changeMoreOptions: function (isVisible) {
this.moreOptionsVisible = isVisible
},
changeOnlyDomains: function (values) {
this.redirectToHttpsConfig.onlyDomains = values
this.$forceUpdate()
},
changeExceptDomains: function (values) {
this.redirectToHttpsConfig.exceptDomains = values
this.$forceUpdate()
}
},
template: ``
})
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: ``
})
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: ``
})
Vue.component("http-request-cond-view", {
props: ["v-cond"],
data: function () {
return {
cond: this.vCond,
components: window.REQUEST_COND_COMPONENTS
}
},
methods: {
typeName: function (cond) {
let c = this.components.$find(function (k, v) {
return v.type == cond.type
})
if (c != null) {
return c.name;
}
return cond.param + " " + cond.operator
},
updateConds: function (conds, simpleCond) {
for (let k in simpleCond) {
if (simpleCond.hasOwnProperty(k)) {
this.cond[k] = simpleCond[k]
}
}
},
notifyChange: function () {
}
},
template: `
{{cond.param}} {{cond.operator}}
{{typeName(cond)}}:
{{cond.value}}
`
})
Vue.component("http-request-conds-box", {
props: ["v-conds"],
data: function () {
let conds = this.vConds
if (conds == null) {
conds = {
isOn: true,
connector: "or",
groups: []
}
}
if (conds.groups == null) {
conds.groups = []
}
return {
conds: conds,
components: window.REQUEST_COND_COMPONENTS
}
},
methods: {
change: function () {
this.$emit("change", this.conds)
},
addGroup: function () {
window.UPDATING_COND_GROUP = null
let that = this
teaweb.popup("/servers/server/settings/conds/addGroupPopup", {
height: "30em",
callback: function (resp) {
that.conds.groups.push(resp.data.group)
that.change()
}
})
},
updateGroup: function (groupIndex, group) {
window.UPDATING_COND_GROUP = group
let that = this
teaweb.popup("/servers/server/settings/conds/addGroupPopup", {
height: "30em",
callback: function (resp) {
Vue.set(that.conds.groups, groupIndex, resp.data.group)
that.change()
}
})
},
removeGroup: function (groupIndex) {
let that = this
teaweb.confirm("确定要删除这一组条件吗?", function () {
that.conds.groups.$remove(groupIndex)
that.change()
})
},
typeName: function (cond) {
let c = this.components.$find(function (k, v) {
return v.type == cond.type
})
if (c != null) {
return c.name;
}
return cond.param + " " + cond.operator
}
},
template: `
分组{{groupIndex+1}}
{{cond.param}} {{cond.operator}}
{{typeName(cond)}}:
{{cond.value}}
{{group.connector}}
+添加分组
`
})
// 浏览条件列表
Vue.component("http-request-conds-view", {
props: ["v-conds"],
data: function () {
let conds = this.vConds
if (conds == null) {
conds = {
isOn: true,
connector: "or",
groups: []
}
}
if (conds.groups == null) {
conds.groups = []
}
let that = this
conds.groups.forEach(function (group) {
group.conds.forEach(function (cond) {
cond.typeName = that.typeName(cond)
})
})
return {
initConds: conds
}
},
computed: {
// 之所以使用computed,是因为需要动态更新
conds: function () {
return this.initConds
}
},
methods: {
typeName: function (cond) {
let c = window.REQUEST_COND_COMPONENTS.$find(function (k, v) {
return v.type == cond.type
})
if (c != null) {
return c.name;
}
return cond.param + " " + cond.operator
},
updateConds: function (conds) {
this.initConds = conds
},
notifyChange: function () {
let that = this
if (this.initConds.groups != null) {
this.initConds.groups.forEach(function (group) {
group.conds.forEach(function (cond) {
cond.typeName = that.typeName(cond)
})
})
this.$forceUpdate()
}
}
},
template: `
{{cond.param}} {{cond.operator}}
{{cond.typeName}}:
{{cond.value}}
{{group.connector}}
{{group.description}}
`
})
// 请求限制
Vue.component("http-request-limit-config-box", {
props: ["v-request-limit-config", "v-is-group", "v-is-location"],
data: function () {
let config = this.vRequestLimitConfig
if (config == null) {
config = {
isPrior: false,
isOn: false,
maxConns: 0,
maxConnsPerIP: 0,
maxBodySize: {
count: -1,
unit: "kb"
},
outBandwidthPerConn: {
count: -1,
unit: "kb"
}
}
}
return {
config: config,
maxConns: config.maxConns,
maxConnsPerIP: config.maxConnsPerIP
}
},
watch: {
maxConns: function (v) {
let conns = parseInt(v, 10)
if (isNaN(conns)) {
this.config.maxConns = 0
return
}
if (conns < 0) {
this.config.maxConns = 0
} else {
this.config.maxConns = conns
}
},
maxConnsPerIP: function (v) {
let conns = parseInt(v, 10)
if (isNaN(conns)) {
this.config.maxConnsPerIP = 0
return
}
if (conns < 0) {
this.config.maxConnsPerIP = 0
} else {
this.config.maxConnsPerIP = conns
}
}
},
methods: {
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
}
},
template: ``
})
Vue.component("http-request-scripts-config-box", {
props: ["vRequestScriptsConfig", "v-auditing-status", "v-is-location"],
data: function () {
let config = this.vRequestScriptsConfig
if (config == null) {
config = {}
}
return {
config: config
}
},
methods: {
changeInitGroup: function (group) {
this.config.initGroup = group
this.$forceUpdate()
},
changeRequestGroup: function (group) {
this.config.requestGroup = group
this.$forceUpdate()
}
},
template: ``
})
Vue.component("http-rewrite-rule-list", {
props: ["v-web-id", "v-rewrite-rules"],
mounted: function () {
setTimeout(this.sort, 1000)
},
data: function () {
let rewriteRules = this.vRewriteRules
if (rewriteRules == null) {
rewriteRules = []
}
return {
rewriteRules: rewriteRules
}
},
methods: {
updateRewriteRule: function (rewriteRuleId) {
teaweb.popup("/servers/server/settings/rewrite/updatePopup?webId=" + this.vWebId + "&rewriteRuleId=" + rewriteRuleId, {
height: "26em",
callback: function () {
window.location.reload()
}
})
},
deleteRewriteRule: function (rewriteRuleId) {
let that = this
teaweb.confirm("确定要删除此重写规则吗?", function () {
Tea.action("/servers/server/settings/rewrite/delete")
.params({
webId: that.vWebId,
rewriteRuleId: rewriteRuleId
})
.post()
.refresh()
})
},
// 排序
sort: function () {
if (this.rewriteRules.length == 0) {
return
}
let that = this
sortTable(function (rowIds) {
Tea.action("/servers/server/settings/rewrite/sort")
.post()
.params({
webId: that.vWebId,
rewriteRuleIds: rowIds
})
.success(function () {
teaweb.success("保存成功")
})
})
}
},
template: `
匹配规则
转发目标
转发方式
状态
操作
{{rule.pattern}}
BREAK
{{rule.redirectStatus}}
Host: {{rule.proxyHost}}
{{rule.replace}}
隐式
显示
修改
删除
`
})
Vue.component("http-rewrite-labels-label", {
props: ["v-class"],
template: ` `
})
Vue.component("http-stat-config-box", {
props: ["v-stat-config", "v-is-location"],
data: function () {
let stat = this.vStatConfig
if (stat == null) {
stat = {
isPrior: false,
isOn: false
}
}
return {
stat: stat
}
},
template: ``
})
// 请求方法列表
Vue.component("http-status-box", {
props: ["v-status-list"],
data: function () {
let statusList = this.vStatusList
if (statusList == null) {
statusList = []
}
return {
statusList: statusList,
isAdding: false,
addingStatus: ""
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.addingStatus.focus()
}, 100)
},
confirm: function () {
let that = this
// 删除其中的空格
this.addingStatus = this.addingStatus.replace(/\s/g, "").toUpperCase()
if (this.addingStatus.length == 0) {
teaweb.warn("请输入要添加的状态码", function () {
that.$refs.addingStatus.focus()
})
return
}
// 是否已经存在
if (this.statusList.$contains(this.addingStatus)) {
teaweb.warn("此状态码已经存在,无需重复添加", function () {
that.$refs.addingStatus.focus()
})
return
}
// 格式
if (!this.addingStatus.match(/^\d{3}$/)) {
teaweb.warn("请输入正确的状态码", function () {
that.$refs.addingStatus.focus()
})
return
}
this.statusList.push(parseInt(this.addingStatus, 10))
this.cancel()
},
remove: function (index) {
this.statusList.$remove(index)
},
cancel: function () {
this.isAdding = false
this.addingStatus = ""
}
},
template: ``
})
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: ``
})
Vue.component("http-websocket-box", {
props: ["v-websocket-ref", "v-websocket-config", "v-is-location"],
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.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: ``
})
// 指标对象
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: ``
})
Vue.component("origin-input-box", {
props: ["v-family", "v-oss-types"],
data: function () {
let family = this.vFamily
if (family == null) {
family = "http"
}
let ossTypes = this.vOssTypes
if (ossTypes == null) {
ossTypes = []
}
return {
origins: [],
isAdding: false,
family: family,
ossTypes: ossTypes
}
},
methods: {
add: function () {
let scheme = ""
switch (this.family) {
case "http":
scheme = "http"
break
case "tcp":
scheme = "tcp"
break
case "udp":
scheme = "udp"
break
}
this.origins.push({
id: "",
host: "",
isPrimary: true,
isPrimaryValue: 1,
scheme: scheme
})
let that = this
setTimeout(function () {
let inputs = that.$refs.originHost
if (inputs != null) {
inputs[inputs.length - 1].focus()
}
}, 10)
},
confirm: function () {
},
cancel: function () {
},
remove: function (index) {
this.origins.$remove(index)
},
changePrimary: function (origin) {
origin.isPrimary = origin.isPrimaryValue == 1
},
changeFamily: function (family) {
this.family = family
let that = this
this.origins.forEach(function (origin) {
let scheme = ""
switch (that.family) {
case "http":
scheme = "http"
break
case "tcp":
scheme = "tcp"
break
case "udp":
scheme = "udp"
break
}
origin.scheme = scheme
})
}
},
template: `
http://
https://
{{ossType.name}}
tcp://
tls://
udp://
+
`
})
Vue.component("origin-list-box", {
props: ["v-primary-origins", "v-backup-origins", "v-server-type", "v-params"],
data: function () {
return {
primaryOrigins: this.vPrimaryOrigins,
backupOrigins: this.vBackupOrigins
}
},
methods: {
createPrimaryOrigin: function () {
teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, {
width: "45em",
height: "27em",
callback: function (resp) {
teaweb.success("保存成功", function () {
window.location.reload()
})
}
})
},
createBackupOrigin: function () {
teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, {
width: "45em",
height: "27em",
callback: function (resp) {
teaweb.success("保存成功", function () {
window.location.reload()
})
}
})
},
updateOrigin: function (originId, originType) {
teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, {
width: "45em",
height: "27em",
callback: function (resp) {
teaweb.success("保存成功", function () {
window.location.reload()
})
}
})
},
deleteOrigin: function (originId, originAddr, originType) {
let that = this
teaweb.confirm("确定要删除此源站(" + originAddr + ")吗?", function () {
Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType)
.post()
.success(function () {
teaweb.success("删除成功", function () {
window.location.reload()
})
})
})
},
updateOriginIsOn: function (originId, originAddr, isOn) {
let message
let resultMessage
if (isOn) {
message = "确定要启用此源站(" + originAddr + ")吗?"
resultMessage = "启用成功"
} else {
message = "确定要停用此源站(" + originAddr + ")吗?"
resultMessage = "停用成功"
}
let that = this
teaweb.confirm(message, function () {
Tea.action("/servers/server/settings/origins/updateIsOn?" + that.vParams + "&originId=" + originId + "&isOn=" + (isOn ? 1 : 0))
.post()
.success(function () {
teaweb.success(resultMessage, function () {
window.location.reload()
})
})
})
}
},
template: ``
})
Vue.component("origin-list-table", {
props: ["v-origins", "v-origin-type"],
data: function () {
let hasMatchedDomains = false
let origins = this.vOrigins
if (origins != null && origins.length > 0) {
origins.forEach(function (origin) {
if (origin.domains != null && origin.domains.length > 0) {
hasMatchedDomains = true
}
})
}
return {
hasMatchedDomains: hasMatchedDomains
}
},
methods: {
deleteOrigin: function (originId, originAddr) {
this.$emit("deleteOrigin", originId, originAddr, this.vOriginType)
},
updateOrigin: function (originId) {
this.$emit("updateOrigin", originId, this.vOriginType)
},
updateOriginIsOn: function (originId, originAddr, isOn) {
this.$emit("updateOriginIsOn", originId, originAddr, isOn)
}
},
template: `
源站地址
权重
状态
操作
{{origin.addr}}
对象存储
{{origin.name}}
证书
主机名: {{origin.host}}
端口跟随
HTTP/2
匹配: {{domain}}
匹配: 所有域名
{{origin.weight}}
修改
停用 启用
删除
`
})
Vue.component("origin-scheduling-view-box", {
props: ["v-scheduling", "v-params"],
data: function () {
let scheduling = this.vScheduling
if (scheduling == null) {
scheduling = {}
}
return {
scheduling: scheduling
}
},
methods: {
update: function () {
teaweb.popup("/servers/server/settings/reverseProxy/updateSchedulingPopup?" + this.vParams, {
height: "21em",
callback: function () {
window.location.reload()
},
})
}
},
template: `
当前正在使用的算法
{{scheduling.name}} [修改]
`
})
Vue.component("prior-checkbox", {
props: ["v-config"],
data: function () {
return {
isPrior: this.vConfig.isPrior
}
},
watch: {
isPrior: function (v) {
this.vConfig.isPrior = v
}
},
template: `
打开独立配置
`
})
Vue.component("reverse-proxy-box", {
props: ["v-reverse-proxy-ref", "v-reverse-proxy-config", "v-is-location", "v-family"],
data: function () {
let reverseProxyRef = this.vReverseProxyRef
if (reverseProxyRef == null) {
reverseProxyRef = {
isPrior: false,
isOn: false,
reverseProxyId: 0
}
}
let reverseProxyConfig = this.vReverseProxyConfig
if (reverseProxyConfig == null) {
reverseProxyConfig = {
requestPath: "",
stripPrefix: "",
requestURI: "",
requestHost: "",
requestHostType: 0,
addHeaders: [],
requestHostExcludingPort: false,
retry50X: false
}
} else if (reverseProxyConfig.addHeaders == null) {
reverseProxyConfig.addHeaders = []
}
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,
forwardHeaders: forwardHeaders,
family: this.vFamily
}
},
watch: {
"reverseProxyConfig.requestHostType": function (v) {
let requestHostType = parseInt(v)
if (isNaN(requestHostType)) {
requestHostType = 0
}
this.reverseProxyConfig.requestHostType = requestHostType
}
},
methods: {
isOn: function () {
return (!this.vIsLocation || this.reverseProxyRef.isPrior) && 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: ``
})
Vue.component("script-config-box", {
props: ["id", "v-script-config", "comment", "v-auditing-status"],
data: function () {
let config = this.vScriptConfig
if (config == null) {
config = {
isPrior: false,
isOn: false,
code: "",
auditingCode: ""
}
}
let auditingStatus = null
if (config.auditingCodeMD5 != null && config.auditingCodeMD5.length > 0 && config.auditingCode != null && config.auditingCode.length > 0) {
config.code = config.auditingCode
if (this.vAuditingStatus != null) {
for (let i = 0; i < this.vAuditingStatus.length; i ++ ){
let status = this.vAuditingStatus[i]
if (status.md5 == config.auditingCodeMD5) {
auditingStatus = status
break
}
}
}
}
if (config.code.length == 0) {
config.code = "\n\n\n\n"
}
return {
config: config,
auditingStatus: auditingStatus
}
},
watch: {
"config.isOn": function () {
this.change()
}
},
methods: {
change: function () {
this.$emit("change", this.config)
},
changeCode: function (code) {
this.config.code = code
this.change()
}
},
template: `
启用脚本设置
脚本代码
{{config.code}}
`
})
Vue.component("script-group-config-box", {
props: ["v-group", "v-auditing-status", "v-is-location"],
data: function () {
let group = this.vGroup
if (group == null) {
group = {
isPrior: false,
isOn: true,
scripts: []
}
}
if (group.scripts == null) {
group.scripts = []
}
let script = null
if (group.scripts.length > 0) {
script = group.scripts[group.scripts.length - 1]
}
return {
group: group,
script: script
}
},
methods: {
changeScript: function (script) {
this.group.scripts = [script] // 目前只支持单个脚本
this.change()
},
change: function () {
this.$emit("change", this.group)
}
},
template: ``
})
Vue.component("server-config-copy-link", {
props: ["v-server-id", "v-config-code"],
data: function () {
return {
serverId: this.vServerId,
configCode: this.vConfigCode
}
},
methods: {
copy: function () {
teaweb.popup("/servers/server/settings/copy?serverId=" + this.serverId + "&configCode=" + this.configCode, {
height: "25em",
callback: function () {
teaweb.success("复制成功")
}
})
}
},
template: `批量 `
})
Vue.component("server-feature-required", {
template: `当前网站绑定的套餐或当前用户未开通此功能。 `
})
Vue.component("server-group-selector", {
props: ["v-groups"],
data: function () {
let groups = this.vGroups
if (groups == null) {
groups = []
}
return {
groups: groups
}
},
methods: {
selectGroup: function () {
let that = this
let groupIds = this.groups.map(function (v) {
return v.id.toString()
}).join(",")
teaweb.popup("/servers/groups/selectPopup?selectedGroupIds=" + groupIds, {
callback: function (resp) {
that.groups.push(resp.data.group)
}
})
},
addGroup: function () {
let that = this
teaweb.popup("/servers/groups/createPopup", {
callback: function (resp) {
that.groups.push(resp.data.group)
}
})
},
removeGroup: function (index) {
this.groups.$remove(index)
},
groupIds: function () {
return this.groups.map(function (v) {
return v.id
})
}
},
template: ``
})
Vue.component("server-name-box", {
props: ["v-server-names"],
data: function () {
let serverNames = this.vServerNames;
if (serverNames == null) {
serverNames = []
}
return {
serverNames: serverNames,
isSearching: false,
keyword: ""
}
},
methods: {
addServerName: function () {
window.UPDATING_SERVER_NAME = null
let that = this
teaweb.popup("/servers/addServerNamePopup", {
callback: function (resp) {
var serverName = resp.data.serverName
that.serverNames.push(serverName)
setTimeout(that.submitForm, 100)
}
});
},
removeServerName: function (index) {
this.serverNames.$remove(index)
},
updateServerName: function (index, serverName) {
window.UPDATING_SERVER_NAME = teaweb.clone(serverName)
let that = this
teaweb.popup("/servers/addServerNamePopup", {
callback: function (resp) {
var serverName = resp.data.serverName
Vue.set(that.serverNames, index, serverName)
setTimeout(that.submitForm, 100)
}
});
},
showSearchBox: function () {
this.isSearching = !this.isSearching
if (this.isSearching) {
let that = this
setTimeout(function () {
that.$refs.keywordRef.focus()
}, 200)
} else {
this.keyword = ""
}
},
allServerNames: function () {
if (this.serverNames == null) {
return []
}
let result = []
this.serverNames.forEach(function (serverName) {
if (serverName.subNames != null && serverName.subNames.length > 0) {
serverName.subNames.forEach(function (subName) {
if (subName != null && subName.length > 0) {
if (!result.$contains(subName)) {
result.push(subName)
}
}
})
} else if (serverName.name != null && serverName.name.length > 0) {
if (!result.$contains(serverName.name)) {
result.push(serverName.name)
}
}
})
return result
},
submitForm: function () {
Tea.runActionOn(this.$refs.serverNamesRef.form)
}
},
watch: {
keyword: function (v) {
this.serverNames.forEach(function (serverName) {
if (v.length == 0) {
serverName.isShowing = true
return
}
if (serverName.subNames == null || serverName.subNames.length == 0) {
if (!teaweb.match(serverName.name, v)) {
serverName.isShowing = false
}
} else {
let found = false
serverName.subNames.forEach(function (subName) {
if (teaweb.match(subName, v)) {
found = true
}
})
serverName.isShowing = found
}
})
}
},
template: `
{{serverName.type}}
{{serverName.name}}
{{serverName.subNames[0]}}等{{serverName.subNames.length}}个域名
`
})
Vue.component("server-traffic-limit-status-viewer", {
props: ["value"],
data: function () {
let targetTypeName = "流量"
if (this.value != null) {
targetTypeName = this.targetTypeToName(this.value.targetType)
}
return {
status: this.value,
targetTypeName: targetTypeName
}
},
methods: {
targetTypeToName: function (targetType) {
switch (targetType) {
case "traffic":
return "流量"
case "request":
return "请求数"
case "websocketConnections":
return "Websocket连接数"
}
return "流量"
}
},
template: `
已达到套餐 当日{{targetTypeName}}限制
已达到套餐 当月{{targetTypeName}}限制
已达到套餐 总体{{targetTypeName}}限制
`
})
Vue.component("ssl-certs-box", {
props: [
"v-certs", // 证书列表
"v-cert", // 单个证书
"v-protocol", // 协议:https|tls
"v-view-size", // 弹窗尺寸:normal, mini
"v-single-mode", // 单证书模式
"v-description", // 描述文字
"v-domains", // 搜索的域名列表或者函数
"v-user-id" // 用户ID
],
data: function () {
let certs = this.vCerts
if (certs == null) {
certs = []
}
if (this.vCert != null) {
certs.push(this.vCert)
}
let description = this.vDescription
if (description == null || typeof (description) != "string") {
description = ""
}
return {
certs: certs,
description: description
}
},
methods: {
certIds: function () {
return this.certs.map(function (v) {
return v.id
})
},
// 删除证书
removeCert: function (index) {
let that = this
teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () {
that.certs.$remove(index)
})
},
// 选择证书
selectCert: function () {
let that = this
let width = "54em"
let height = "32em"
let viewSize = this.vViewSize
if (viewSize == null) {
viewSize = "normal"
}
if (viewSize == "mini") {
width = "35em"
height = "20em"
}
let searchingDomains = []
if (this.vDomains != null) {
if (typeof this.vDomains == "function") {
let resultDomains = this.vDomains()
if (resultDomains != null && typeof resultDomains == "object" && (resultDomains instanceof Array)) {
searchingDomains = resultDomains
}
} else if (typeof this.vDomains == "object" && (this.vDomains instanceof Array)) {
searchingDomains = this.vDomains
}
if (searchingDomains.length > 10000) {
searchingDomains = searchingDomains.slice(0, 10000)
}
}
let selectedCertIds = this.certs.map(function (cert) {
return cert.id
})
let userId = this.vUserId
if (userId == null) {
userId = 0
}
teaweb.popup("/servers/certs/selectPopup?viewSize=" + viewSize + "&searchingDomains=" + window.encodeURIComponent(searchingDomains.join(",")) + "&selectedCertIds=" + selectedCertIds.join(",") + "&userId=" + userId, {
width: width,
height: height,
callback: function (resp) {
if (resp.data.cert != null) {
that.certs.push(resp.data.cert)
}
if (resp.data.certs != null) {
that.certs.$pushAll(resp.data.certs)
}
that.$forceUpdate()
}
})
},
// 上传证书
uploadCert: function () {
let that = this
let userId = this.vUserId
if (typeof userId != "number" && typeof userId != "string") {
userId = 0
}
teaweb.popup("/servers/certs/uploadPopup?userId=" + userId, {
height: "28em",
callback: function (resp) {
teaweb.success("上传成功", function () {
if (resp.data.cert != null) {
that.certs.push(resp.data.cert)
}
if (resp.data.certs != null) {
that.certs.$pushAll(resp.data.certs)
}
that.$forceUpdate()
})
}
})
},
// 批量上传
uploadBatch: function () {
let that = this
let userId = this.vUserId
if (typeof userId != "number" && typeof userId != "string") {
userId = 0
}
teaweb.popup("/servers/certs/uploadBatchPopup?userId=" + userId, {
callback: function (resp) {
if (resp.data.cert != null) {
that.certs.push(resp.data.cert)
}
if (resp.data.certs != null) {
that.certs.$pushAll(resp.data.certs)
}
that.$forceUpdate()
}
})
},
// 格式化时间
formatTime: function (timestamp) {
return new Date(timestamp * 1000).format("Y-m-d")
},
// 判断是否显示选择|上传按钮
buttonsVisible: function () {
return this.vSingleMode == null || !this.vSingleMode || this.certs == null || this.certs.length == 0
}
},
template: `
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}
选择或上传证书后HTTPS TLS 服务才能生效。
{{description}}
选择已有证书
|
上传新证书
批量上传证书
`
})
Vue.component("ssl-certs-view", {
props: ["v-certs"],
data: function () {
let certs = this.vCerts
if (certs == null) {
certs = []
}
return {
certs: certs
}
},
methods: {
// 格式化时间
formatTime: function (timestamp) {
return new Date(timestamp * 1000).format("Y-m-d")
},
// 查看详情
viewCert: function (certId) {
teaweb.popup("/servers/certs/certPopup?certId=" + certId, {
height: "28em",
width: "48em"
})
}
},
template: `
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}
`
})
Vue.component("ssl-config-box", {
props: [
"v-ssl-policy",
"v-protocol",
"v-server-id",
"v-support-http3"
],
created: function () {
let that = this
setTimeout(function () {
that.sortableCipherSuites()
}, 100)
},
data: function () {
let policy = this.vSslPolicy
if (policy == null) {
policy = {
id: 0,
isOn: true,
certRefs: [],
certs: [],
clientCARefs: [],
clientCACerts: [],
clientAuthType: 0,
minVersion: "TLS 1.1",
hsts: null,
cipherSuitesIsOn: false,
cipherSuites: [],
http2Enabled: true,
http3Enabled: false,
ocspIsOn: false
}
} else {
if (policy.certRefs == null) {
policy.certRefs = []
}
if (policy.certs == null) {
policy.certs = []
}
if (policy.clientCARefs == null) {
policy.clientCARefs = []
}
if (policy.clientCACerts == null) {
policy.clientCACerts = []
}
if (policy.cipherSuites == null) {
policy.cipherSuites = []
}
}
let hsts = policy.hsts
let hstsMaxAgeString = "31536000"
if (hsts == null) {
hsts = {
isOn: false,
maxAge: 31536000,
includeSubDomains: false,
preload: false,
domains: []
}
}
if (hsts.maxAge != null) {
hstsMaxAgeString = hsts.maxAge.toString()
}
return {
policy: policy,
// hsts
hsts: hsts,
hstsOptionsVisible: false,
hstsDomainAdding: false,
hstsMaxAgeString: hstsMaxAgeString,
addingHstsDomain: "",
hstsDomainEditingIndex: -1,
// 相关数据
allVersions: window.SSL_ALL_VERSIONS,
allCipherSuites: window.SSL_ALL_CIPHER_SUITES.$copy(),
modernCipherSuites: window.SSL_MODERN_CIPHER_SUITES,
intermediateCipherSuites: window.SSL_INTERMEDIATE_CIPHER_SUITES,
allClientAuthTypes: window.SSL_ALL_CLIENT_AUTH_TYPES,
cipherSuitesVisible: false,
// 高级选项
moreOptionsVisible: false
}
},
watch: {
hsts: {
deep: true,
handler: function () {
this.policy.hsts = this.hsts
}
}
},
methods: {
// 删除证书
removeCert: function (index) {
let that = this
teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () {
that.policy.certRefs.$remove(index)
that.policy.certs.$remove(index)
})
},
// 选择证书
selectCert: function () {
let that = this
let selectedCertIds = []
if (this.policy != null && this.policy.certs.length > 0) {
this.policy.certs.forEach(function (cert) {
selectedCertIds.push(cert.id.toString())
})
}
let serverId = this.vServerId
if (serverId == null) {
serverId = 0
}
teaweb.popup("/servers/certs/selectPopup?selectedCertIds=" + selectedCertIds + "&serverId=" + serverId, {
width: "50em",
height: "30em",
callback: function (resp) {
if (resp.data.cert != null && resp.data.certRef != null) {
that.policy.certRefs.push(resp.data.certRef)
that.policy.certs.push(resp.data.cert)
}
if (resp.data.certs != null && resp.data.certRefs != null) {
that.policy.certRefs.$pushAll(resp.data.certRefs)
that.policy.certs.$pushAll(resp.data.certs)
}
that.$forceUpdate()
}
})
},
// 上传证书
uploadCert: function () {
let that = this
let serverId = this.vServerId
if (typeof serverId != "number" && typeof serverId != "string") {
serverId = 0
}
teaweb.popup("/servers/certs/uploadPopup?serverId=" + serverId, {
height: "30em",
callback: function (resp) {
teaweb.success("上传成功", function () {
that.policy.certRefs.push(resp.data.certRef)
that.policy.certs.push(resp.data.cert)
})
}
})
},
// 批量上传
uploadBatch: function () {
let that = this
let serverId = this.vServerId
if (typeof serverId != "number" && typeof serverId != "string") {
serverId = 0
}
teaweb.popup("/servers/certs/uploadBatchPopup?serverId=" + serverId, {
callback: function (resp) {
if (resp.data.cert != null) {
that.policy.certRefs.push(resp.data.certRef)
that.policy.certs.push(resp.data.cert)
}
if (resp.data.certs != null) {
that.policy.certRefs.$pushAll(resp.data.certRefs)
that.policy.certs.$pushAll(resp.data.certs)
}
that.$forceUpdate()
}
})
},
// 申请证书
requestCert: function () {
// 已经在证书中的域名
let excludeServerNames = []
if (this.policy != null && this.policy.certs.length > 0) {
this.policy.certs.forEach(function (cert) {
excludeServerNames.$pushAll(cert.dnsNames)
})
}
let that = this
teaweb.popup("/servers/server/settings/https/requestCertPopup?serverId=" + this.vServerId + "&excludeServerNames=" + excludeServerNames.join(","), {
callback: function () {
that.policy.certRefs.push(resp.data.certRef)
that.policy.certs.push(resp.data.cert)
}
})
},
// 更多选项
changeOptionsVisible: function () {
this.moreOptionsVisible = !this.moreOptionsVisible
},
// 格式化时间
formatTime: function (timestamp) {
return new Date(timestamp * 1000).format("Y-m-d")
},
// 格式化加密套件
formatCipherSuite: function (cipherSuite) {
return cipherSuite.replace(/(AES|3DES)/, "$1 ")
},
// 添加单个套件
addCipherSuite: function (cipherSuite) {
if (!this.policy.cipherSuites.$contains(cipherSuite)) {
this.policy.cipherSuites.push(cipherSuite)
}
this.allCipherSuites.$removeValue(cipherSuite)
},
// 删除单个套件
removeCipherSuite: function (cipherSuite) {
let that = this
teaweb.confirm("确定要删除此套件吗?", function () {
that.policy.cipherSuites.$removeValue(cipherSuite)
that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$findAll(function (k, v) {
return !that.policy.cipherSuites.$contains(v)
})
})
},
// 清除所选套件
clearCipherSuites: function () {
let that = this
teaweb.confirm("确定要清除所有已选套件吗?", function () {
that.policy.cipherSuites = []
that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$copy()
})
},
// 批量添加套件
addBatchCipherSuites: function (suites) {
var that = this
teaweb.confirm("确定要批量添加套件?", function () {
suites.$each(function (k, v) {
if (that.policy.cipherSuites.$contains(v)) {
return
}
that.policy.cipherSuites.push(v)
})
})
},
/**
* 套件拖动排序
*/
sortableCipherSuites: function () {
var box = document.querySelector(".cipher-suites-box")
Sortable.create(box, {
draggable: ".label",
handle: ".icon.handle",
onStart: function () {
},
onUpdate: function (event) {
}
})
},
// 显示所有套件
showAllCipherSuites: function () {
this.cipherSuitesVisible = !this.cipherSuitesVisible
},
// 显示HSTS更多选项
showMoreHSTS: function () {
this.hstsOptionsVisible = !this.hstsOptionsVisible;
if (this.hstsOptionsVisible) {
this.changeHSTSMaxAge()
}
},
// 监控HSTS有效期修改
changeHSTSMaxAge: function () {
var v = parseInt(this.hstsMaxAgeString)
if (isNaN(v) || v < 0) {
this.hsts.maxAge = 0
this.hsts.days = "-"
return
}
this.hsts.maxAge = v
this.hsts.days = v / 86400
if (this.hsts.days == 0) {
this.hsts.days = "-"
}
},
// 设置HSTS有效期
setHSTSMaxAge: function (maxAge) {
this.hstsMaxAgeString = maxAge.toString()
this.changeHSTSMaxAge()
},
// 添加HSTS域名
addHstsDomain: function () {
this.hstsDomainAdding = true
this.hstsDomainEditingIndex = -1
let that = this
setTimeout(function () {
that.$refs.addingHstsDomain.focus()
}, 100)
},
// 修改HSTS域名
editHstsDomain: function (index) {
this.hstsDomainEditingIndex = index
this.addingHstsDomain = this.hsts.domains[index]
this.hstsDomainAdding = true
let that = this
setTimeout(function () {
that.$refs.addingHstsDomain.focus()
}, 100)
},
// 确认HSTS域名添加
confirmAddHstsDomain: function () {
this.addingHstsDomain = this.addingHstsDomain.trim()
if (this.addingHstsDomain.length == 0) {
return;
}
if (this.hstsDomainEditingIndex > -1) {
this.hsts.domains[this.hstsDomainEditingIndex] = this.addingHstsDomain
} else {
this.hsts.domains.push(this.addingHstsDomain)
}
this.cancelHstsDomainAdding()
},
// 取消HSTS域名添加
cancelHstsDomainAdding: function () {
this.hstsDomainAdding = false
this.addingHstsDomain = ""
this.hstsDomainEditingIndex = -1
},
// 删除HSTS域名
removeHstsDomain: function (index) {
this.cancelHstsDomainAdding()
this.hsts.domains.$remove(index)
},
// 选择客户端CA证书
selectClientCACert: function () {
let that = this
teaweb.popup("/servers/certs/selectPopup?isCA=1", {
width: "50em",
height: "30em",
callback: function (resp) {
if (resp.data.cert != null && resp.data.certRef != null) {
that.policy.clientCARefs.push(resp.data.certRef)
that.policy.clientCACerts.push(resp.data.cert)
}
if (resp.data.certs != null && resp.data.certRefs != null) {
that.policy.clientCARefs.$pushAll(resp.data.certRefs)
that.policy.clientCACerts.$pushAll(resp.data.certs)
}
that.$forceUpdate()
}
})
},
// 上传CA证书
uploadClientCACert: function () {
let that = this
teaweb.popup("/servers/certs/uploadPopup?isCA=1", {
height: "28em",
callback: function (resp) {
teaweb.success("上传成功", function () {
that.policy.clientCARefs.push(resp.data.certRef)
that.policy.clientCACerts.push(resp.data.cert)
})
}
})
},
// 删除客户端CA证书
removeClientCACert: function (index) {
let that = this
teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前网站不再使用此证书。", function () {
that.policy.clientCARefs.$remove(index)
that.policy.clientCACerts.$remove(index)
})
}
},
template: ``
})
// 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: ``
})
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: ``
})
window.REQUEST_COND_COMPONENTS = [{"type":"url-extension","name":"文件扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-eq-index","name":"首页","description":"检查URL路径是为\"/\"","component":"http-cond-url-eq-index","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":false},{"type":"url-all","name":"全站","description":"全站所有URL","component":"http-cond-url-all","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":false},{"type":"url-prefix","name":"URL目录前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-prefix","paramsTitle":"URL目录前缀","isRequest":true,"caseInsensitive":true},{"type":"url-eq","name":"URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-regexp","name":"URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致","component":"http-cond-url-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"url-wildcard-match","name":"URL通配符","description":"使用通配符检查URL中的文件路径是否一致","component":"http-cond-url-wildcard-match","paramsTitle":"通配符","isRequest":true,"caseInsensitive":true},{"type":"user-agent-regexp","name":"User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识","component":"http-cond-user-agent-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"params","name":"参数匹配","description":"根据参数值进行匹配","component":"http-cond-params","paramsTitle":"参数配置","isRequest":true,"caseInsensitive":false},{"type":"url-not-extension","name":"排除:URL扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-not-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-not-prefix","name":"排除:URL前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-not-prefix","paramsTitle":"URL前缀","isRequest":true,"caseInsensitive":true},{"type":"url-not-eq","name":"排除:URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-not-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-not-regexp","name":"排除:URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致,如果一致,则不匹配","component":"http-cond-url-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"user-agent-not-regexp","name":"排除:User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识,如果含有,则不匹配","component":"http-cond-user-agent-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"mime-type","name":"内容MimeType","description":"根据服务器返回的内容的MimeType进行过滤。注意:当用于缓存条件时,此条件需要结合别的请求条件使用。","component":"http-cond-mime-type","paramsTitle":"MimeType列表","isRequest":false,"caseInsensitive":false}];
window.REQUEST_COND_OPERATORS = [{"description":"判断是否正则表达式匹配","name":"正则表达式匹配","op":"regexp"},{"description":"判断是否正则表达式不匹配","name":"正则表达式不匹配","op":"not regexp"},{"description":"判断是否和指定的通配符匹配","name":"通配符匹配","op":"wildcard match"},{"description":"判断是否和指定的通配符不匹配","name":"通配符不匹配","op":"wildcard not match"},{"description":"使用字符串对比参数值是否相等于某个值","name":"字符串等于","op":"eq"},{"description":"参数值包含某个前缀","name":"字符串前缀","op":"prefix"},{"description":"参数值包含某个后缀","name":"字符串后缀","op":"suffix"},{"description":"参数值包含另外一个字符串","name":"字符串包含","op":"contains"},{"description":"参数值不包含另外一个字符串","name":"字符串不包含","op":"not contains"},{"description":"使用字符串对比参数值是否不相等于某个值","name":"字符串不等于","op":"not"},{"description":"判断参数值在某个列表中","name":"在列表中","op":"in"},{"description":"判断参数值不在某个列表中","name":"不在列表中","op":"not in"},{"description":"判断小写的扩展名(不带点)在某个列表中","name":"扩展名","op":"file ext"},{"description":"判断MimeType在某个列表中,支持类似于image/*的语法","name":"MimeType","op":"mime type"},{"description":"判断版本号在某个范围内,格式为version1,version2","name":"版本号范围","op":"version range"},{"description":"将参数转换为整数数字后进行对比","name":"整数等于","op":"eq int"},{"description":"将参数转换为可以有小数的浮点数字进行对比","name":"浮点数等于","op":"eq float"},{"description":"将参数转换为数字进行对比","name":"数字大于","op":"gt"},{"description":"将参数转换为数字进行对比","name":"数字大于等于","op":"gte"},{"description":"将参数转换为数字进行对比","name":"数字小于","op":"lt"},{"description":"将参数转换为数字进行对比","name":"数字小于等于","op":"lte"},{"description":"对整数参数值取模,除数为10,对比值为余数","name":"整数取模10","op":"mod 10"},{"description":"对整数参数值取模,除数为100,对比值为余数","name":"整数取模100","op":"mod 100"},{"description":"对整数参数值取模,对比值格式为:除数,余数,比如10,1","name":"整数取模","op":"mod"},{"description":"将参数转换为IP进行对比","name":"IP等于","op":"eq ip"},{"description":"将参数转换为IP进行对比","name":"IP大于","op":"gt ip"},{"description":"将参数转换为IP进行对比","name":"IP大于等于","op":"gte ip"},{"description":"将参数转换为IP进行对比","name":"IP小于","op":"lt ip"},{"description":"将参数转换为IP进行对比","name":"IP小于等于","op":"lte ip"},{"description":"IP在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e,或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e","name":"IP范围","op":"ip range"},{"description":"对IP参数值取模,除数为10,对比值为余数","name":"IP取模10","op":"ip mod 10"},{"description":"对IP参数值取模,除数为100,对比值为余数","name":"IP取模100","op":"ip mod 100"},{"description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1","name":"IP取模","op":"ip mod"}];
window.REQUEST_VARIABLES = [{"code":"${edgeVersion}","description":"","name":"边缘节点版本"},{"code":"${remoteAddr}","description":"会依次根据X-Forwarded-For、X-Real-IP、RemoteAddr获取,适合前端有别的反向代理服务时使用,存在伪造的风险","name":"客户端地址(IP)"},{"code":"${rawRemoteAddr}","description":"返回直接连接服务的客户端原始IP地址","name":"客户端地址(IP)"},{"code":"${remotePort}","description":"","name":"客户端端口"},{"code":"${remoteUser}","description":"","name":"客户端用户名"},{"code":"${requestURI}","description":"比如/hello?name=lily","name":"请求URI"},{"code":"${requestPath}","description":"比如/hello","name":"请求路径(不包括参数)"},{"code":"${requestURL}","description":"比如https://example.com/hello?name=lily","name":"完整的请求URL"},{"code":"${requestLength}","description":"","name":"请求内容长度"},{"code":"${requestMethod}","description":"比如GET、POST","name":"请求方法"},{"code":"${requestFilename}","description":"","name":"请求文件路径"},{"code":"${requestPathExtension}","description":"请求路径中的文件扩展名,包括点符号,比如.html、.png","name":"请求文件扩展名"},{"code":"${requestPathLowerExtension}","description":"请求路径中的文件扩展名,其中大写字母会被自动转换为小写,包括点符号,比如.html、.png","name":"请求文件小写扩展名"},{"code":"${scheme}","description":"","name":"请求协议,http或https"},{"code":"${proto}","description:":"类似于HTTP/1.0","name":"包含版本的HTTP请求协议"},{"code":"${timeISO8601}","description":"比如2018-07-16T23:52:24.839+08:00","name":"ISO 8601格式的时间"},{"code":"${timeLocal}","description":"比如17/Jul/2018:09:52:24 +0800","name":"本地时间"},{"code":"${msec}","description":"比如1531756823.054","name":"带有毫秒的时间"},{"code":"${timestamp}","description":"","name":"unix时间戳,单位为秒"},{"code":"${host}","description":"","name":"主机名"},{"code":"${cname}","description":"比如38b48e4f.goedge.cn","name":"当前网站的CNAME"},{"code":"${serverName}","description":"","name":"接收请求的服务器名"},{"code":"${serverPort}","description":"","name":"接收请求的服务器端口"},{"code":"${referer}","description":"","name":"请求来源URL"},{"code":"${referer.host}","description":"","name":"请求来源URL域名"},{"code":"${userAgent}","description":"","name":"客户端信息"},{"code":"${contentType}","description":"","name":"请求头部的Content-Type"},{"code":"${cookies}","description":"","name":"所有cookie组合字符串"},{"code":"${cookie.NAME}","description":"","name":"单个cookie值"},{"code":"${isArgs}","description":"如果URL有参数,则值为`?`;否则,则值为空","name":"问号(?)标记"},{"code":"${args}","description":"","name":"所有参数组合字符串"},{"code":"${arg.NAME}","description":"","name":"单个参数值"},{"code":"${headers}","description":"","name":"所有Header信息组合字符串"},{"code":"${header.NAME}","description":"","name":"单个Header值"},{"code":"${geo.country.name}","description":"","name":"国家/地区名称"},{"code":"${geo.country.id}","description":"","name":"国家/地区ID"},{"code":"${geo.province.name}","description":"目前只包含中国省份","name":"省份名称"},{"code":"${geo.province.id}","description":"目前只包含中国省份","name":"省份ID"},{"code":"${geo.city.name}","description":"目前只包含中国城市","name":"城市名称"},{"code":"${geo.city.id}","description":"目前只包含中国城市","name":"城市名称"},{"code":"${isp.name}","description":"","name":"ISP服务商名称"},{"code":"${isp.id}","description":"","name":"ISP服务商ID"},{"code":"${browser.os.name}","description":"客户端所在操作系统名称","name":"操作系统名称"},{"code":"${browser.os.version}","description":"客户端所在操作系统版本","name":"操作系统版本"},{"code":"${browser.name}","description":"客户端浏览器名称","name":"浏览器名称"},{"code":"${browser.version}","description":"客户端浏览器版本","name":"浏览器版本"},{"code":"${browser.isMobile}","description":"如果客户端是手机,则值为1,否则为0","name":"手机标识"}];
window.METRIC_HTTP_KEYS = [{"name":"客户端地址(IP)","code":"${remoteAddr}","description":"会依次根据X-Forwarded-For、X-Real-IP、RemoteAddr获取,适用于前端可能有别的反向代理的情形,存在被伪造的可能","icon":""},{"name":"直接客户端地址(IP)","code":"${rawRemoteAddr}","description":"返回直接连接服务的客户端原始IP地址","icon":""},{"name":"客户端用户名","code":"${remoteUser}","description":"通过基本认证填入的用户名","icon":""},{"name":"请求URI","code":"${requestURI}","description":"包含参数,比如/hello?name=lily","icon":""},{"name":"请求路径","code":"${requestPath}","description":"不包含参数,比如/hello","icon":""},{"name":"完整URL","code":"${requestURL}","description":"比如https://example.com/hello?name=lily","icon":""},{"name":"请求方法","code":"${requestMethod}","description":"比如GET、POST等","icon":""},{"name":"请求协议Scheme","code":"${scheme}","description":"http或https","icon":""},{"name":"文件扩展名","code":"${requestPathExtension}","description":"请求路径中的文件扩展名,包括点符号,比如.html、.png","icon":""},{"name":"小写文件扩展名","code":"${requestPathLowerExtension}","description":"请求路径中的文件扩展名小写形式,包括点符号,比如.html、.png","icon":""},{"name":"主机名","code":"${host}","description":"通常是请求的域名","icon":""},{"name":"HTTP协议","code":"${proto}","description":"包含版本的HTTP请求协议,类似于HTTP/1.0","icon":""},{"name":"URL参数值","code":"${arg.NAME}","description":"单个URL参数值","icon":""},{"name":"请求来源URL","code":"${referer}","description":"请求来源Referer URL","icon":""},{"name":"请求来源URL域名","code":"${referer.host}","description":"请求来源Referer URL域名","icon":""},{"name":"Header值","code":"${header.NAME}","description":"单个Header值,比如${header.User-Agent}","icon":""},{"name":"Cookie值","code":"${cookie.NAME}","description":"单个cookie值,比如${cookie.sid}","icon":""},{"name":"状态码","code":"${status}","description":"","icon":""},{"name":"响应的Content-Type值","code":"${response.contentType}","description":"","icon":""}];
window.WAF_RULE_CHECKPOINTS = [{"description":"通用报头比如Cache-Control、Accept之类的长度限制,防止缓冲区溢出攻击。","name":"通用请求报头长度限制","prefix":"requestGeneralHeaderLength"},{"description":"通用报头比如Cache-Control、Date之类的长度限制,防止缓冲区溢出攻击。","name":"通用响应报头长度限制","prefix":"responseGeneralHeaderLength"},{"description":"试图通过分析X-Forwarded-For等报头获取的客户端地址,比如192.168.1.100,存在伪造的可能。","name":"客户端地址(IP)","prefix":"remoteAddr"},{"description":"直接连接的客户端地址,比如192.168.1.100。","name":"客户端源地址(IP)","prefix":"rawRemoteAddr"},{"description":"直接连接的客户端地址端口。","name":"客户端端口","prefix":"remotePort"},{"description":"通过BasicAuth登录的客户端用户名。","name":"客户端用户名","prefix":"remoteUser"},{"description":"包含URL参数的请求URI,类似于 /hello/world?lang=go,不包含域名部分。","name":"请求URI","prefix":"requestURI"},{"description":"不包含URL参数的请求路径,类似于 /hello/world,不包含域名部分。","name":"请求路径","prefix":"requestPath"},{"description":"完整的请求URL,包含协议、域名、请求路径、参数等,类似于 https://example.com/hello?name=lily 。","name":"请求完整URL","prefix":"requestURL"},{"description":"请求报头中的Content-Length。","name":"请求内容长度","prefix":"requestLength"},{"description":"通常在POST或者PUT等操作时会附带请求体,最大限制32M。","name":"请求体内容","prefix":"requestBody"},{"description":"${requestURI}和${requestBody}组合。","name":"请求URI和请求体组合","prefix":"requestAll"},{"description":"获取POST或者其他方法发送的表单参数,最大请求体限制32M。","name":"请求表单参数","prefix":"requestForm"},{"description":"获取POST上传的文件信息,最大请求体限制32M。","name":"上传文件","prefix":"requestUpload"},{"description":"获取POST或者其他方法发送的JSON,最大请求体限制32M,使用点(.)符号表示多级数据。","name":"请求JSON参数","prefix":"requestJSON"},{"description":"比如GET、POST。","name":"请求方法","prefix":"requestMethod"},{"description":"比如http或https。","name":"请求协议","prefix":"scheme"},{"description":"比如HTTP/1.1。","name":"HTTP协议版本","prefix":"proto"},{"description":"比如example.com。","name":"主机名","prefix":"host"},{"description":"当前网站服务CNAME,比如38b48e4f.example.com。","name":"CNAME","prefix":"cname"},{"description":"是否为CNAME,值为1(是)或0(否)。","name":"是否为CNAME","prefix":"isCNAME"},{"description":"请求报头中的Referer和Origin值。","name":"请求来源","prefix":"refererOrigin"},{"description":"请求报头中的Referer值。","name":"请求来源Referer","prefix":"referer"},{"description":"比如Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103。","name":"客户端信息","prefix":"userAgent"},{"description":"请求报头的Content-Type。","name":"内容类型","prefix":"contentType"},{"description":"比如sid=IxZVPFhE\u0026city=beijing\u0026uid=18237。","name":"所有cookie组合字符串","prefix":"cookies"},{"description":"单个cookie值。","name":"单个cookie值","prefix":"cookie"},{"description":"比如name=lu\u0026age=20。","name":"所有URL参数组合","prefix":"args"},{"description":"单个URL参数值。","name":"单个URL参数值","prefix":"arg"},{"description":"使用换行符(\\n)隔开的报头内容字符串,每行均为\"NAME: VALUE格式\"。","name":"所有请求报头内容","prefix":"headers"},{"description":"使用换行符(\\n)隔开的报头名称字符串,每行一个名称。","name":"所有请求报头名称","prefix":"headerNames"},{"description":"单个报头值。","name":"单个请求报头值","prefix":"header"},{"description":"最长的请求报头的长度。","name":"请求报头最大长度","prefix":"headerMaxLength"},{"description":"当前客户端所处国家/地区名称。","name":"国家/地区名称","prefix":"geoCountryName"},{"description":"当前客户端所处中国省份名称。","name":"省份名称","prefix":"geoProvinceName"},{"description":"当前客户端所处中国城市名称。","name":"城市名称","prefix":"geoCityName"},{"description":"当前客户端所处ISP名称。","name":"ISP名称","prefix":"ispName"},{"description":"对统计对象进行统计。","name":"CC统计","prefix":"cc2"},{"description":"对统计对象进行统计。","name":"防盗链","prefix":"refererBlock"},{"description":"响应状态码,比如200、404、500。","name":"响应状态码","prefix":"status"},{"description":"响应报头值。","name":"响应报头","prefix":"responseHeader"},{"description":"响应内容字符串。","name":"响应内容","prefix":"responseBody"},{"description":"响应内容长度,通过响应的报头Content-Length获取。","name":"响应内容长度","prefix":"bytesSent"}];
window.WAF_RULE_OPERATORS = [{"name":"正则匹配","code":"match","description":"使用正则表达式匹配,在头部使用(?i)表示不区分大小写,\u003ca href=\"https://goedge.cn/docs/Appendix/Regexp/Index.md\" target=\"_blank\"\u003e正则表达式语法 \u0026raquo;\u003c/a\u003e。","caseInsensitive":"yes","dataType":"regexp"},{"name":"正则不匹配","code":"not match","description":"使用正则表达式不匹配,在头部使用(?i)表示不区分大小写,\u003ca href=\"https://goedge.cn/docs/Appendix/Regexp/Index.md\" target=\"_blank\"\u003e正则表达式语法 \u0026raquo;\u003c/a\u003e。","caseInsensitive":"yes","dataType":"regexp"},{"name":"通配符匹配","code":"wildcard match","description":"判断是否和指定的通配符匹配,可以在对比值中使用星号通配符(*)表示任意字符。","caseInsensitive":"yes","dataType":"wildcard"},{"name":"通配符不匹配","code":"wildcard not match","description":"判断是否和指定的通配符不匹配,可以在对比值中使用星号通配符(*)表示任意字符。","caseInsensitive":"yes","dataType":"wildcard"},{"name":"字符串等于","code":"eq string","description":"使用字符串对比等于。","caseInsensitive":"no","dataType":"string"},{"name":"字符串不等于","code":"neq string","description":"使用字符串对比不等于。","caseInsensitive":"no","dataType":"string"},{"name":"包含字符串","code":"contains","description":"包含某个字符串,比如Hello World包含了World。","caseInsensitive":"no","dataType":"string"},{"name":"不包含字符串","code":"not contains","description":"不包含某个字符串,比如Hello字符串中不包含Hi。","caseInsensitive":"no","dataType":"string"},{"name":"包含任一字符串","code":"contains any","description":"包含字符串列表中的任意一个,比如/hello/world包含/hello和/hi中的/hello,对比值中每行一个字符串。","caseInsensitive":"no","dataType":"strings"},{"name":"包含所有字符串","code":"contains all","description":"包含字符串列表中的所有字符串,比如/hello/world必须包含/hello和/world,对比值中每行一个字符串。","caseInsensitive":"no","dataType":"strings"},{"name":"包含前缀","code":"prefix","description":"包含字符串前缀部分,比如/hello前缀会匹配/hello, /hello/world等。","caseInsensitive":"no","dataType":"string"},{"name":"包含后缀","code":"suffix","description":"包含字符串后缀部分,比如/hello后缀会匹配/hello, /hi/hello等。","caseInsensitive":"no","dataType":"string"},{"name":"包含任一单词","code":"contains any word","description":"包含某个独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"包含所有单词","code":"contains all words","description":"包含所有的独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"不包含任一单词","code":"not contains any word","description":"不包含某个独立单词,对比值中每行一个单词,比如mozilla firefox里包含了mozilla和firefox两个单词,但是不包含fire和fox这两个单词。","caseInsensitive":"no","dataType":"strings"},{"name":"包含SQL注入","code":"contains sql injection","description":"检测字符串内容是否包含SQL注入。","caseInsensitive":"none","dataType":"none"},{"name":"包含SQL注入-严格模式","code":"contains sql injection strictly","description":"更加严格地检测字符串内容是否包含SQL注入,相对于非严格模式,有一定的误报几率。","caseInsensitive":"none","dataType":"none"},{"name":"包含XSS注入","code":"contains xss","description":"检测字符串内容是否包含XSS注入。","caseInsensitive":"none","dataType":"none"},{"name":"包含XSS注入-严格模式","code":"contains xss strictly","description":"更加严格地检测字符串内容是否包含XSS注入,相对于非严格模式,此时xml、audio、video等标签也会被匹配。","caseInsensitive":"none","dataType":"none"},{"name":"包含二进制数据","code":"contains binary","description":"包含一组二进制数据。","caseInsensitive":"no","dataType":"string"},{"name":"不包含二进制数据","code":"not contains binary","description":"不包含一组二进制数据。","caseInsensitive":"no","dataType":"string"},{"name":"数值大于","code":"gt","description":"使用数值对比大于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值大于等于","code":"gte","description":"使用数值对比大于等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值小于","code":"lt","description":"使用数值对比小于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值小于等于","code":"lte","description":"使用数值对比小于等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值等于","code":"eq","description":"使用数值对比等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"数值不等于","code":"neq","description":"使用数值对比不等于,对比值需要是一个数字。","caseInsensitive":"none","dataType":"number"},{"name":"包含索引","code":"has key","description":"对于一组数据拥有某个键值或者索引。","caseInsensitive":"no","dataType":"string|number"},{"name":"版本号大于","code":"version gt","description":"对比版本号大于。","caseInsensitive":"none","dataType":"version"},{"name":"版本号小于","code":"version lt","description":"对比版本号小于。","caseInsensitive":"none","dataType":"version"},{"name":"版本号范围","code":"version range","description":"判断版本号在某个范围内,格式为 起始version1,结束version2。","caseInsensitive":"none","dataType":"versionRange"},{"name":"IP等于","code":"eq ip","description":"将参数转换为IP进行对比,只能对比单个IP。","caseInsensitive":"none","dataType":"ip"},{"name":"在一组IP中","code":"in ip list","description":"判断参数IP在一组IP内,对比值中每行一个IP。","caseInsensitive":"none","dataType":"ips"},{"name":"IP大于","code":"gt ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP大于等于","code":"gte ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP小于","code":"lt ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP小于等于","code":"lte ip","description":"将参数转换为IP进行对比。","caseInsensitive":"none","dataType":"ip"},{"name":"IP范围","code":"ip range","description":"IP在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e;或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e;或者单个IP。可以填写多行,每行一个IP范围。","caseInsensitive":"none","dataType":"ips"},{"name":"不在IP范围","code":"not ip range","description":"IP不在某个范围之内,范围格式可以是英文逗号分隔的\u003ccode-label\u003e开始IP,结束IP\u003c/code-label\u003e,比如\u003ccode-label\u003e192.168.1.100,192.168.2.200\u003c/code-label\u003e;或者CIDR格式的ip/bits,比如\u003ccode-label\u003e192.168.2.1/24\u003c/code-label\u003e;或者单个IP。可以填写多行,每行一个IP范围。","caseInsensitive":"none","dataType":"ips"},{"name":"IP取模10","code":"ip mod 10","description":"对IP参数值取模,除数为10,对比值为余数。","caseInsensitive":"none","dataType":"number"},{"name":"IP取模100","code":"ip mod 100","description":"对IP参数值取模,除数为100,对比值为余数。","caseInsensitive":"none","dataType":"number"},{"name":"IP取模","code":"ip mod","description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1。","caseInsensitive":"none","dataType":"number"}];
window.WAF_CAPTCHA_TYPES = [{"name":"验证码","code":"default","description":"通过输入验证码来验证人机。","icon":""},{"name":"点击验证","code":"oneClick","description":"通过点击界面元素来验证人机。","icon":""},{"name":"滑动解锁","code":"slide","description":"通过滑动方块解锁来验证人机。","icon":""},{"name":"极验-行为验","code":"geetest","description":"使用极验-行为验提供的人机验证方式。","icon":""}];