前端页面
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
)
|
||||
|
||||
type AppSettingsResetAESSecretAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *AppSettingsResetAESSecretAction) RunPost(params struct {
|
||||
AppId int64
|
||||
|
||||
Must *actions.Must
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
|
||||
|
||||
app := pickApp(params.AppId)
|
||||
resetAESSecret(app)
|
||||
this.Success()
|
||||
}
|
||||
@@ -17,7 +17,6 @@ var appSettingsStore = struct {
|
||||
|
||||
func defaultAppSettings(app maps.Map) maps.Map {
|
||||
signSecretPlain := randomPlainSecret("ss")
|
||||
aesSecretPlain := randomPlainSecret("as")
|
||||
return maps.Map{
|
||||
"appId": app.GetString("appId"),
|
||||
"primaryClusterId": app.GetInt64("clusterId"),
|
||||
@@ -25,9 +24,6 @@ func defaultAppSettings(app maps.Map) maps.Map {
|
||||
"signSecretPlain": signSecretPlain,
|
||||
"signSecretMasked": maskSecret(signSecretPlain),
|
||||
"signSecretUpdatedAt": "2026-02-20 12:30:00",
|
||||
"aesSecretPlain": aesSecretPlain,
|
||||
"aesSecretMasked": maskSecret(aesSecretPlain),
|
||||
"aesSecretUpdatedAt": "2026-02-12 09:45:00",
|
||||
"appStatus": app.GetBool("isOn"),
|
||||
"defaultTTL": 30,
|
||||
"fallbackTimeoutMs": 300,
|
||||
@@ -52,9 +48,6 @@ func cloneSettings(settings maps.Map) maps.Map {
|
||||
"signSecretPlain": settings.GetString("signSecretPlain"),
|
||||
"signSecretMasked": settings.GetString("signSecretMasked"),
|
||||
"signSecretUpdatedAt": settings.GetString("signSecretUpdatedAt"),
|
||||
"aesSecretPlain": settings.GetString("aesSecretPlain"),
|
||||
"aesSecretMasked": settings.GetString("aesSecretMasked"),
|
||||
"aesSecretUpdatedAt": settings.GetString("aesSecretUpdatedAt"),
|
||||
"appStatus": settings.GetBool("appStatus"),
|
||||
"defaultTTL": settings.GetInt("defaultTTL"),
|
||||
"fallbackTimeoutMs": settings.GetInt("fallbackTimeoutMs"),
|
||||
@@ -110,16 +103,6 @@ func resetSignSecret(app maps.Map) maps.Map {
|
||||
return settings
|
||||
}
|
||||
|
||||
func resetAESSecret(app maps.Map) maps.Map {
|
||||
settings := loadAppSettings(app)
|
||||
aesSecretPlain := randomPlainSecret("as")
|
||||
settings["aesSecretPlain"] = aesSecretPlain
|
||||
settings["aesSecretMasked"] = maskSecret(aesSecretPlain)
|
||||
settings["aesSecretUpdatedAt"] = nowDateTime()
|
||||
saveAppSettings(app.GetInt64("id"), settings)
|
||||
return settings
|
||||
}
|
||||
|
||||
func nowDateTime() string {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
@@ -182,21 +165,6 @@ func ensureSettingsFields(settings maps.Map) bool {
|
||||
changed = true
|
||||
}
|
||||
|
||||
aesSecretPlain := settings.GetString("aesSecretPlain")
|
||||
if len(aesSecretPlain) == 0 {
|
||||
aesSecretPlain = randomPlainSecret("as")
|
||||
settings["aesSecretPlain"] = aesSecretPlain
|
||||
changed = true
|
||||
}
|
||||
if len(settings.GetString("aesSecretMasked")) == 0 {
|
||||
settings["aesSecretMasked"] = maskSecret(aesSecretPlain)
|
||||
changed = true
|
||||
}
|
||||
if len(settings.GetString("aesSecretUpdatedAt")) == 0 {
|
||||
settings["aesSecretUpdatedAt"] = nowDateTime()
|
||||
changed = true
|
||||
}
|
||||
|
||||
if len(settings.GetString("sniPolicy")) == 0 {
|
||||
settings["sniPolicy"] = "level2"
|
||||
changed = true
|
||||
|
||||
@@ -19,7 +19,6 @@ func init() {
|
||||
GetPost("/app/settings", new(AppSettingsAction)).
|
||||
Post("/app/settings/toggleSignEnabled", new(AppSettingsToggleSignEnabledAction)).
|
||||
Post("/app/settings/resetSignSecret", new(AppSettingsResetSignSecretAction)).
|
||||
Post("/app/settings/resetAESSecret", new(AppSettingsResetAESSecretAction)).
|
||||
Get("/domains", new(DomainsAction)).
|
||||
Get("/customRecords", new(CustomRecordsAction)).
|
||||
GetPost("/createPopup", new(CreatePopupAction)).
|
||||
|
||||
@@ -45,7 +45,7 @@ func (this *ClusterSettingsAction) RunGet(params struct {
|
||||
cid := strconv.FormatInt(params.ClusterId, 10)
|
||||
this.Data["leftMenuItems"] = []map[string]interface{}{
|
||||
{"name": "基础设置", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "§ion=basic", "isActive": section == "basic"},
|
||||
{"name": "TLS", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "§ion=tls", "isActive": section == "tls"},
|
||||
{"name": "端口设置", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "§ion=tls", "isActive": section == "tls"},
|
||||
}
|
||||
|
||||
settings["isDefaultCluster"] = (policies.LoadDefaultClusterID() == cluster.GetInt64("id"))
|
||||
@@ -88,7 +88,8 @@ func (this *ClusterSettingsAction) RunGet(params struct {
|
||||
}
|
||||
} else {
|
||||
sslPolicy = &sslconfigs.SSLPolicy{
|
||||
IsOn: true,
|
||||
IsOn: true,
|
||||
MinVersion: "TLS 1.1",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package sandbox
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
@@ -15,21 +16,29 @@ func (this *IndexAction) Init() {
|
||||
|
||||
func (this *IndexAction) RunGet(params struct{}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
|
||||
this.Data["clusters"] = policies.LoadAvailableDeployClusters()
|
||||
this.Data["apps"] = []map[string]interface{}{
|
||||
{
|
||||
"id": int64(1),
|
||||
"name": "主站移动业务",
|
||||
"appId": "ab12xc34s2",
|
||||
"id": int64(1),
|
||||
"name": "主站移动业务",
|
||||
"appId": "ab12xc34s2",
|
||||
"clusterId": int64(1),
|
||||
"domains": []string{"api.business.com", "www.aliyun.com"},
|
||||
},
|
||||
{
|
||||
"id": int64(2),
|
||||
"name": "视频网关业务",
|
||||
"appId": "vd8992ksm1",
|
||||
"id": int64(2),
|
||||
"name": "支付网关业务",
|
||||
"appId": "vd8992ksm1",
|
||||
"clusterId": int64(2),
|
||||
"domains": []string{"payment.business.com"},
|
||||
},
|
||||
{
|
||||
"id": int64(3),
|
||||
"name": "海外灰度测试",
|
||||
"appId": "ov7711hkq9",
|
||||
"id": int64(3),
|
||||
"name": "海外灰度测试",
|
||||
"appId": "ov7711hkq9",
|
||||
"clusterId": int64(1),
|
||||
"domains": []string{"global.example.com", "edge.example.com"},
|
||||
},
|
||||
}
|
||||
this.Show()
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
@@ -14,10 +15,11 @@ type TestAction struct {
|
||||
}
|
||||
|
||||
func (this *TestAction) RunPost(params struct {
|
||||
AppId string
|
||||
Domain string
|
||||
ClientIp string
|
||||
Qtype string
|
||||
AppId string
|
||||
ClusterId int64
|
||||
Domain string
|
||||
ClientIp string
|
||||
Qtype string
|
||||
}) {
|
||||
if len(params.ClientIp) == 0 {
|
||||
params.ClientIp = "203.0.113.100"
|
||||
@@ -40,7 +42,11 @@ func (this *TestAction) RunPost(params struct {
|
||||
query.Set("dn", params.Domain)
|
||||
query.Set("cip", params.ClientIp)
|
||||
query.Set("qtype", params.Qtype)
|
||||
requestURL := "https://api.httpdns.example.com/resolve?" + query.Encode()
|
||||
clusterServiceDomain := policies.LoadClusterGatewayByID(params.ClusterId)
|
||||
if len(clusterServiceDomain) == 0 {
|
||||
clusterServiceDomain = "gw.httpdns.example.com"
|
||||
}
|
||||
requestURL := "https://" + clusterServiceDomain + "/resolve?" + query.Encode()
|
||||
|
||||
this.Data["result"] = maps.Map{
|
||||
"code": 0,
|
||||
|
||||
@@ -116,24 +116,6 @@
|
||||
<p class="comment httpdns-note">用于生成鉴权接口的安全密钥。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">数据加密 Secret</td>
|
||||
<td>
|
||||
<div class="httpdns-secret-line">
|
||||
<code>{{aesSecretVisible ? settings.aesSecretPlain : settings.aesSecretMasked}}</code>
|
||||
<a href="" class="httpdns-mini-icon" @click.prevent="aesSecretVisible = !aesSecretVisible"
|
||||
:title="aesSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon"
|
||||
:class="aesSecretVisible ? 'eye slash' : 'eye'"></i></a>
|
||||
<a href="" class="httpdns-mini-icon" title="复制数据加密 Secret"
|
||||
@click.prevent="copySecret(settings.aesSecretPlain, 'AES Secret')"><i
|
||||
class="copy outline icon"></i></a>
|
||||
<a href="" class="httpdns-mini-icon" title="重置数据加密 Secret" @click.prevent="resetAESSecret"><i
|
||||
class="redo icon"></i></a>
|
||||
</div>
|
||||
<p class="comment httpdns-note">最近更新:{{settings.aesSecretUpdatedAt}}</p>
|
||||
<p class="comment httpdns-note">用于解析接口数据加密的密钥。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<submit-btn></submit-btn>
|
||||
|
||||
@@ -2,14 +2,13 @@ Tea.context(function () {
|
||||
this.activeSection = this.activeSection || "basic";
|
||||
this.success = NotifyReloadSuccess("保存成功");
|
||||
this.signSecretVisible = false;
|
||||
this.aesSecretVisible = false;
|
||||
|
||||
this.toggleSignEnabled = function () {
|
||||
let that = this;
|
||||
let targetIsOn = !this.settings.signEnabled;
|
||||
|
||||
if (targetIsOn) {
|
||||
teaweb.confirm("html:开启后,服务端会对解析请求进行签名鉴权,<span class='red'>未签名、签名无效或过期的请求都解析失败</span>,确认开启吗?", function () {
|
||||
teaweb.confirm("html:开启后,服务端会对解析请求进行签名鉴权,<span class='red'>未签名、签名无效或过期的请求都会解析失败</span>,确认开启吗?", function () {
|
||||
that.$post("/httpdns/apps/app/settings/toggleSignEnabled")
|
||||
.params({
|
||||
appId: that.app.id,
|
||||
@@ -93,20 +92,4 @@ Tea.context(function () {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.resetAESSecret = function () {
|
||||
let that = this;
|
||||
teaweb.confirm("确定要重置 AES Secret 吗?", function () {
|
||||
that.$post("/httpdns/apps/app/settings/resetAESSecret")
|
||||
.params({
|
||||
appId: that.app.id
|
||||
})
|
||||
.success(function () {
|
||||
teaweb.success("AES Secret 已重置", function () {
|
||||
teaweb.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
return;
|
||||
}
|
||||
teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id, {
|
||||
width: "56em",
|
||||
height: "40em",
|
||||
width: "42em",
|
||||
height: "33em",
|
||||
title: "新增自定义解析规则"
|
||||
});
|
||||
};
|
||||
@@ -19,8 +19,8 @@
|
||||
return;
|
||||
}
|
||||
teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id + "&recordId=" + recordId, {
|
||||
width: "56em",
|
||||
height: "40em",
|
||||
width: "42em",
|
||||
height: "33em",
|
||||
title: "编辑自定义解析规则"
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
{$layout "layout_popup"}
|
||||
|
||||
<style>
|
||||
.httpdns-inline-actions {
|
||||
margin-top: .6em;
|
||||
}
|
||||
.httpdns-inline-actions .count {
|
||||
color: #8f9aa6;
|
||||
margin-left: .4em;
|
||||
}
|
||||
.httpdns-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5em;
|
||||
margin-bottom: .45em;
|
||||
}
|
||||
.httpdns-row .field {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.httpdns-row .field.flex {
|
||||
flex: 1;
|
||||
}
|
||||
.httpdns-line-row {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: .5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.httpdns-inline-actions {
|
||||
margin-top: .6em;
|
||||
}
|
||||
|
||||
.httpdns-inline-actions .count {
|
||||
color: #8f9aa6;
|
||||
margin-left: .4em;
|
||||
}
|
||||
|
||||
.httpdns-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5em;
|
||||
margin-bottom: .45em;
|
||||
}
|
||||
|
||||
.httpdns-row .field {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.httpdns-row .field.flex {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.httpdns-line-row {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: .5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h3 v-if="isEditing">编辑自定义解析规则</h3>
|
||||
@@ -40,97 +45,115 @@
|
||||
<input type="hidden" name="recordId" :value="record.id" />
|
||||
<input type="hidden" name="recordItemsJSON" :value="JSON.stringify(recordItems)" />
|
||||
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">域名 *</td>
|
||||
<td><strong>{{record.domain}}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">规则名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="ruleName" maxlength="50" v-model="record.ruleName" ref="focus" />
|
||||
<p class="comment">例如:上海电信灰度-v2。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">线路</td>
|
||||
<td>
|
||||
<div class="httpdns-line-row">
|
||||
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineScope" v-model="record.lineScope" @change="onLineScopeChange">
|
||||
<option value="china">中国大陆</option>
|
||||
<option value="overseas">港澳台及境外</option>
|
||||
</select>
|
||||
<table class="ui table definition selectable small">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">域名 *</td>
|
||||
<td><strong>{{record.domain}}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">规则名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="ruleName" maxlength="50" v-model="record.ruleName" ref="focus" />
|
||||
<p class="comment">例如:上海电信灰度-v2。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">线路</td>
|
||||
<td>
|
||||
<div class="httpdns-line-row">
|
||||
<select class="ui dropdown auto-width"
|
||||
style="display:inline-block !important;width:auto !important;margin:0 !important;"
|
||||
name="lineScope" v-model="record.lineScope" @change="onLineScopeChange">
|
||||
<option value="china">中国地区</option>
|
||||
<option value="overseas">境外</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width"
|
||||
style="display:inline-block !important;width:auto !important;margin:0 !important;"
|
||||
name="lineCarrier" v-if="record.lineScope == 'china'" v-model="record.lineCarrier">
|
||||
<option v-for="carrier in chinaCarriers" :value="carrier">{{carrier}}</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width"
|
||||
style="display:inline-block !important;width:auto !important;margin:0 !important;"
|
||||
name="lineRegion" v-if="record.lineScope == 'china'" v-model="record.lineRegion"
|
||||
@change="onChinaRegionChange">
|
||||
<option v-for="region in chinaRegions" :value="region">{{region}}</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width"
|
||||
style="display:inline-block !important;width:auto !important;margin:0 !important;"
|
||||
name="lineProvince" v-if="record.lineScope == 'china'" v-model="record.lineProvince">
|
||||
<option v-for="province in provinceOptions" :value="province">{{province}}</option>
|
||||
</select>
|
||||
|
||||
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineCarrier" v-if="record.lineScope == 'china'" v-model="record.lineCarrier">
|
||||
<option v-for="carrier in chinaCarriers" :value="carrier">{{carrier}}</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineRegion" v-if="record.lineScope == 'china'" v-model="record.lineRegion" @change="onChinaRegionChange">
|
||||
<option v-for="region in chinaRegions" :value="region">{{region}}</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineProvince" v-if="record.lineScope == 'china'" v-model="record.lineProvince">
|
||||
<option v-for="province in provinceOptions" :value="province">{{province}}</option>
|
||||
</select>
|
||||
|
||||
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineContinent" v-if="record.lineScope == 'overseas'" v-model="record.lineContinent" @change="onContinentChange">
|
||||
<option v-for="continent in continents" :value="continent">{{continent}}</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineCountry" v-if="record.lineScope == 'overseas'" v-model="record.lineCountry">
|
||||
<option v-for="country in countryOptions" :value="country">{{country}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">解析记录值 *</td>
|
||||
<td>
|
||||
<div style="float:right;">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="weightEnabled" value="1" v-model="record.weightEnabled" />
|
||||
<label>按权重调度</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="httpdns-row" v-for="(item, index) in recordItems">
|
||||
<div class="field">
|
||||
<select class="ui dropdown auto-width" v-model="item.type">
|
||||
<option value="A">A</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
<select class="ui dropdown auto-width"
|
||||
style="display:inline-block !important;width:auto !important;margin:0 !important;"
|
||||
name="lineContinent" v-if="record.lineScope == 'overseas'" v-model="record.lineContinent"
|
||||
@change="onContinentChange">
|
||||
<option v-for="continent in continents" :value="continent">{{continent}}</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width"
|
||||
style="display:inline-block !important;width:auto !important;margin:0 !important;"
|
||||
name="lineCountry" v-if="record.lineScope == 'overseas'" v-model="record.lineCountry">
|
||||
<option v-for="country in countryOptions" :value="country">{{country}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field flex">
|
||||
<input type="text" placeholder="记录值(与记录类型匹配)" v-model="item.value" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">解析记录值 *</td>
|
||||
<td>
|
||||
<div style="margin-bottom: 0.5em;">
|
||||
<div class="ui checkbox" style="margin-bottom: 0.5em;">
|
||||
<input type="checkbox" name="weightEnabled" value="1" v-model="record.weightEnabled" />
|
||||
<label>开启权重调度设置</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" v-if="record.weightEnabled">
|
||||
<input type="text" style="width:8em;" placeholder="权重(1-100)" v-model="item.weight" />
|
||||
|
||||
<div class="httpdns-row" v-for="(item, index) in recordItems">
|
||||
<div class="field">
|
||||
<select class="ui dropdown auto-width" v-model="item.type">
|
||||
<option value="A">A</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field flex">
|
||||
<input type="text" placeholder="记录值" v-model="item.value" />
|
||||
</div>
|
||||
<div class="field" v-if="record.weightEnabled">
|
||||
<input type="text" style="width:7em;" placeholder="权重1-100" v-model="item.weight" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<a href="" @click.prevent="removeRecordItem(index)" title="删除"><i
|
||||
class="icon trash alternate outline"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="" @click.prevent="removeRecordItem(index)" title="删除"><i class="icon trash alternate outline"></i></a>
|
||||
</div>
|
||||
<div class="httpdns-inline-actions">
|
||||
<a href="" @click.prevent="addRecordItem" :class="{disabled: recordItems.length >= 10}">
|
||||
<i class="icon plus circle"></i>添加记录值
|
||||
</a>
|
||||
<span class="count">{{recordItems.length}}/10</span>
|
||||
</div>
|
||||
<p class="comment" v-if="record.weightEnabled">开启后每条记录可配置权重,范围 1-100。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">TTL *</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" name="ttl" maxlength="5" style="width:8em;" v-model="record.ttl" />
|
||||
<span class="ui basic label">秒</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">规则状态</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="isOn" value="1" v-model="record.isOn" />
|
||||
<label>启用</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<div class="httpdns-inline-actions">
|
||||
<a href="" @click.prevent="addRecordItem" :class="{disabled: recordItems.length >= 10}">
|
||||
<i class="icon plus circle"></i>添加记录值
|
||||
</a>
|
||||
<span class="count">{{recordItems.length}}/10</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">TTL *</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" name="ttl" maxlength="5" style="width:8em;" v-model="record.ttl" />
|
||||
<span class="ui basic label">秒</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">状态</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="isOn" value="1" v-model="record.isOn" />
|
||||
<label>启用当前规则</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<submit-btn></submit-btn>
|
||||
|
||||
@@ -26,11 +26,11 @@
|
||||
|
||||
<table class="ui table selectable celled httpdns-apps-table" v-if="apps.length > 0">
|
||||
<colgroup>
|
||||
<col style="width:34%;" />
|
||||
<col style="width:28%;" />
|
||||
<col style="width:32%;" />
|
||||
<col style="width:26%;" />
|
||||
<col style="width:12%;" />
|
||||
<col style="width:10%;" />
|
||||
<col style="width:16%;" />
|
||||
<col style="width:20%;" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -87,14 +87,6 @@
|
||||
<a href="" class="httpdns-mini-action" title="复制加签 Secret" @click.prevent="copyText(selectedApp.signSecret, '加签 Secret')"><i class="copy outline icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AES 数据加密 Secret</td>
|
||||
<td>
|
||||
<code>{{aesSecretVisible ? selectedApp.aesSecret : selectedApp.aesSecretMasked}}</code>
|
||||
<a href="" class="httpdns-mini-action" @click.prevent="aesSecretVisible = !aesSecretVisible" :title="aesSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon" :class="aesSecretVisible ? 'eye slash' : 'eye'"></i></a>
|
||||
<a href="" class="httpdns-mini-action" title="复制 AES 数据加密 Secret" @click.prevent="copyText(selectedApp.aesSecret, 'AES Secret')"><i class="copy outline icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>请求验签</td>
|
||||
<td>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
this.selectedApp = {}
|
||||
this.currentStep = 1
|
||||
this.signSecretVisible = false
|
||||
this.aesSecretVisible = false
|
||||
|
||||
if (typeof this.apps == "undefined") {
|
||||
this.apps = []
|
||||
@@ -29,7 +28,6 @@
|
||||
}
|
||||
|
||||
this.signSecretVisible = false
|
||||
this.aesSecretVisible = false
|
||||
this.currentStep = 1
|
||||
}
|
||||
|
||||
|
||||
@@ -4,21 +4,31 @@
|
||||
|
||||
<div>
|
||||
<div class="ui grid stackable">
|
||||
<!-- Left: 解析配置 -->
|
||||
<!-- Left: 解析测试 -->
|
||||
<div class="six wide column">
|
||||
<div class="ui segment">
|
||||
<h4 class="ui header" style="margin-top:0;">解析配置</h4>
|
||||
<h4 class="ui header" style="margin-top:0;">解析测试</h4>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>目标应用 *</label>
|
||||
<select class="ui dropdown" name="appId" v-model="request.appId">
|
||||
<select class="ui dropdown" name="appId" v-model="request.appId" @change="onAppChanged">
|
||||
<option value="">[请选择应用]</option>
|
||||
<option v-for="app in apps" :value="app.appId">{{app.name}} ({{app.appId}})</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>所属集群 *</label>
|
||||
<select class="ui dropdown" name="clusterId" v-model="request.clusterId">
|
||||
<option value="">[请选择所属集群]</option>
|
||||
<option v-for="cluster in clusters" :value="String(cluster.id)">{{cluster.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>解析域名 *</label>
|
||||
<input type="text" v-model="request.domain" placeholder="例如 www.example.com" />
|
||||
<select class="ui dropdown" name="domain" v-model="request.domain">
|
||||
<option value="">[请选择域名]</option>
|
||||
<option v-for="domain in currentDomains" :value="domain">{{domain}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>模拟客户端 IP</label>
|
||||
|
||||
@@ -2,6 +2,7 @@ Tea.context(function () {
|
||||
this.newRequest = function () {
|
||||
return {
|
||||
appId: "",
|
||||
clusterId: "",
|
||||
domain: "",
|
||||
clientIp: "",
|
||||
qtype: "A"
|
||||
@@ -20,13 +21,48 @@ Tea.context(function () {
|
||||
}
|
||||
|
||||
this.isRequesting = false
|
||||
this.currentDomains = []
|
||||
|
||||
if (typeof this.apps == "undefined") {
|
||||
if (typeof this.apps === "undefined") {
|
||||
this.apps = []
|
||||
}
|
||||
if (typeof this.clusters === "undefined") {
|
||||
this.clusters = []
|
||||
}
|
||||
|
||||
this.onAppChanged = function () {
|
||||
let selectedApp = null
|
||||
for (let i = 0; i < this.apps.length; i++) {
|
||||
if (this.apps[i].appId === this.request.appId) {
|
||||
selectedApp = this.apps[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedApp == null) {
|
||||
this.currentDomains = []
|
||||
this.request.domain = ""
|
||||
this.request.clusterId = ""
|
||||
return
|
||||
}
|
||||
|
||||
this.currentDomains = Array.isArray(selectedApp.domains) ? selectedApp.domains : []
|
||||
|
||||
if (this.currentDomains.length > 0) {
|
||||
if (this.currentDomains.indexOf(this.request.domain) < 0) {
|
||||
this.request.domain = this.currentDomains[0]
|
||||
}
|
||||
} else {
|
||||
this.request.domain = ""
|
||||
}
|
||||
|
||||
if (typeof selectedApp.clusterId !== "undefined" && selectedApp.clusterId !== null) {
|
||||
this.request.clusterId = String(selectedApp.clusterId)
|
||||
}
|
||||
}
|
||||
|
||||
this.normalizeResultRows = function (data) {
|
||||
if (typeof data == "undefined" || data == null) {
|
||||
if (typeof data === "undefined" || data == null) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -35,10 +71,7 @@ Tea.context(function () {
|
||||
}
|
||||
|
||||
let rows = []
|
||||
let ips = []
|
||||
if (Array.isArray(data.ips)) {
|
||||
ips = data.ips
|
||||
}
|
||||
let ips = Array.isArray(data.ips) ? data.ips : []
|
||||
let domain = this.request.domain
|
||||
let qtype = this.request.qtype
|
||||
let ttl = data.ttl || 0
|
||||
@@ -55,16 +88,21 @@ Tea.context(function () {
|
||||
line: line
|
||||
})
|
||||
})
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
this.sendTestRequest = function () {
|
||||
if (this.request.appId.length == 0) {
|
||||
teaweb.warn("请选择目标应用")
|
||||
if (this.request.appId.length === 0) {
|
||||
teaweb.warn("Please select target app")
|
||||
return
|
||||
}
|
||||
if (this.request.domain.length == 0) {
|
||||
teaweb.warn("请填写解析域名")
|
||||
if (this.request.clusterId.length === 0) {
|
||||
teaweb.warn("Please select cluster")
|
||||
return
|
||||
}
|
||||
if (this.request.domain.length === 0) {
|
||||
teaweb.warn("Please select domain")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -87,6 +125,7 @@ Tea.context(function () {
|
||||
|
||||
this.resetForm = function () {
|
||||
this.request = this.newRequest()
|
||||
this.currentDomains = []
|
||||
this.response = {
|
||||
hasResult: false,
|
||||
code: -1,
|
||||
|
||||
@@ -1,148 +1,147 @@
|
||||
# HTTPDNS 主计划(V1.2 现行版)
|
||||
# HTTPDNS 主计划(V1.2 现行设计)
|
||||
|
||||
## 1. 目标
|
||||
1. 形成 HTTPDNS 管理端可用闭环:集群、全局配置、应用、域名、自定义解析、日志、解析测试。
|
||||
2. 保持独立 HTTPDNS 模块设计,不挂载到传统 DNS(53)职责中。
|
||||
3. 文档仅保留当前已采用的设计与页面,不包含未落地方案。
|
||||
1. 构建独立的 HTTPDNS 管理闭环:集群、应用、域名、自定义解析、访问日志、运行日志、解析测试。
|
||||
2. 方案以当前已确认的页面与交互为准,不保留历史分支设计。
|
||||
3. 优先保证可运维、可灰度、可观测,先落地稳定版本。
|
||||
|
||||
## 2. 当前范围(仅管理平台)
|
||||
1. 菜单顺序固定为:
|
||||
## 2. 信息架构(菜单)
|
||||
1. 左侧 HTTPDNS 菜单顺序:
|
||||
- 集群管理
|
||||
- 全局配置
|
||||
- 应用管理
|
||||
- SDK接入引导
|
||||
- 访问日志
|
||||
- 运行日志
|
||||
- 解析测试
|
||||
2. 现阶段以管理端页面与 Mock 数据联调为主。
|
||||
2. 不再保留独立“全局配置”菜单。
|
||||
3. 不再保留独立“SDK接入引导”菜单。
|
||||
|
||||
## 3. 核心设计约束
|
||||
1. 应用侧 SNI 防护仅采用“隐匿 SNI”固定策略(不再提供 level1/level3 选择)。
|
||||
2. 服务入口按“集群服务域名”管理,不使用全局单一网关地址。
|
||||
3. 应用认证采用:
|
||||
- 请求验签开关
|
||||
- 加签 Secret
|
||||
- AES 数据加密 Secret
|
||||
4. 自定义解析规则支持 SDNS 参数与解析记录多条配置(各最多 10 条)。
|
||||
1. SNI 防护策略固定为“隐匿 SNI”(不提供 level1/level3、mask/empty切换入口)。
|
||||
2. 服务入口按“集群服务域名”管理,不使用全局单入口。
|
||||
3. 回源协议默认 HTTPS,不提供开关。
|
||||
4. 域名校验默认开启,不在用户侧暴露开关。
|
||||
5. 自定义解析先做精简版:不支持 SDNS 参数匹配。
|
||||
|
||||
## 4. 页面设计(现状)
|
||||
## 4. 集群管理
|
||||
|
||||
### 4.1 集群管理
|
||||
1. 集群列表字段:
|
||||
### 4.1 集群列表
|
||||
1. 字段:集群名称、服务域名、节点数、在线节点数、状态、操作。
|
||||
2. 操作:节点列表、集群设置、删除集群。
|
||||
|
||||
### 4.2 节点列表
|
||||
1. 字段:节点名称、IP、CPU、内存、负载、状态、操作。
|
||||
2. IP 列不展示“[下线]/[宕机]”附加标识。
|
||||
|
||||
### 4.3 集群设置
|
||||
1. 页面布局对齐智能DNS:左侧菜单 + 右侧配置区(`left-box with-menu` / `right-box with-menu`)。
|
||||
2. 配置分组:
|
||||
- 基础设置
|
||||
- TLS
|
||||
3. 基础设置字段:
|
||||
- 集群名称
|
||||
- 服务域名
|
||||
- 节点数
|
||||
- 在线节点数
|
||||
- 状态
|
||||
- 操作
|
||||
2. 集群设置字段:
|
||||
- 集群名称
|
||||
- 集群服务域名
|
||||
- 默认解析 TTL(秒)
|
||||
- 降级超时容忍度(毫秒)
|
||||
- 本地内存缓存(秒)
|
||||
- 节点安装根目录(默认 `/opt/edge-httpdns`)
|
||||
- 启用当前集群
|
||||
3. 节点页提供基础筛选、状态展示、详情与删除。
|
||||
- 默认集群(勾选)
|
||||
4. TLS 配置:
|
||||
- 样式与交互对齐智能DNS TLS页
|
||||
- 维护并绑定该集群服务域名使用的证书
|
||||
- 保证节点回源链路为 HTTPS
|
||||
|
||||
### 4.2 全局配置
|
||||
1. 用户设置:
|
||||
- 默认部署集群
|
||||
- 启用用户域名校验
|
||||
2. 基础默认:
|
||||
- SNI 防护配置(文案展示为“隐匿 SNI”)
|
||||
- 全局默认解析 TTL
|
||||
- 全局降级超时
|
||||
## 5. 应用管理
|
||||
|
||||
### 4.3 应用管理
|
||||
1. 应用列表字段:
|
||||
- 应用名称
|
||||
- AppID
|
||||
- 绑定域名数(可点击)
|
||||
- 状态
|
||||
- 操作(域名管理 / 应用设置)
|
||||
2. 添加应用:
|
||||
- 应用名称
|
||||
- 所属集群
|
||||
- 所属用户(可选)
|
||||
3. 应用设置分两块:
|
||||
- 基础配置:AppID、应用启用、SNI 防护配置(隐匿 SNI)
|
||||
- 认证与密钥:请求验签开关、加签 Secret、AES Secret(查看/复制/重置/更新时间)
|
||||
### 5.1 应用列表
|
||||
1. 字段:应用名称、AppID、绑定域名数、状态、操作。
|
||||
2. 操作:域名列表、应用设置、SDK集成、删除应用。
|
||||
3. 删除应用页面交互风格对齐“删除集群”页面。
|
||||
|
||||
### 4.4 域名管理与自定义解析
|
||||
1. 域名管理:
|
||||
### 5.2 应用设置
|
||||
1. 页面布局对齐智能DNS设置页风格(左侧菜单 + 右侧配置区)。
|
||||
2. 分组:
|
||||
- 基础配置
|
||||
- 认证与密钥
|
||||
3. 基础配置字段:
|
||||
- AppID(只读)
|
||||
- 主集群
|
||||
- 备集群(可选)
|
||||
- 应用启用
|
||||
- SNI 防护配置(文案展示:隐匿 SNI)
|
||||
4. 认证与密钥字段:
|
||||
- 请求验签(状态展示 + 独立启停按钮 + 确认提示)
|
||||
- 加签 Secret(查看、复制、重置、最近更新时间)
|
||||
5. 交互约束:
|
||||
- 强制 HTTPS 传输,不再提供独立“数据加密 Secret”配置项。
|
||||
- 密钥操作区使用紧凑图标样式,减少视觉噪音。
|
||||
|
||||
## 6. 域名管理与自定义解析
|
||||
|
||||
### 6.1 域名管理
|
||||
1. 页面采用框架标准顶部 tab + 面包屑样式。
|
||||
2. 字段:
|
||||
- 域名列表
|
||||
- 规则策略(显示该域名自定义解析规则数)
|
||||
- 规则策略(仅展示数字,表示该域名规则数,可点击)
|
||||
- 操作(自定义解析、解绑)
|
||||
2. 自定义解析列表字段:
|
||||
- 线路
|
||||
3. 入口:在域名行操作中直接进入“自定义解析”。
|
||||
|
||||
### 6.2 自定义解析(精简版)
|
||||
1. 规则字段:
|
||||
- 规则名称
|
||||
- SDNS 参数
|
||||
- 解析记录
|
||||
- 线路
|
||||
- 解析记录值
|
||||
- TTL
|
||||
- 状态
|
||||
- 操作(编辑/启停/删除)
|
||||
3. 新增/编辑规则弹窗:
|
||||
- 规则名称
|
||||
- 线路联动:
|
||||
- 中国大陆:运营商 -> 大区 -> 省份
|
||||
- 港澳台及境外:洲 -> 国家/地区(亚洲含中国香港/中国澳门/中国台湾)
|
||||
- SDNS 参数配置:最多 10 条
|
||||
- 解析记录值:最多 10 条,支持 A/AAAA,可开启权重(1-100)
|
||||
- TTL(1-86400)
|
||||
- 规则状态
|
||||
2. 线路联动:
|
||||
- 第一层:`中国地区` / `境外`
|
||||
- 中国地区:运营商 -> 大区 -> 省份
|
||||
- 境外:洲 -> 国家/地区(亚洲内使用中国香港/中国澳门/中国台湾)
|
||||
3. 解析记录:
|
||||
- 每条规则最多 10 条
|
||||
- 支持 A / AAAA
|
||||
- 可开启权重调度
|
||||
4. 不包含 SDNS 参数配置。
|
||||
|
||||
## 5. SDK接入引导(现状)
|
||||
1. 仅保留两步:
|
||||
- 01 查看配置
|
||||
- 02 开发接入
|
||||
2. 查看配置展示:
|
||||
- App ID(可复制)
|
||||
- 应用名称
|
||||
- 集群服务地址(可复制)
|
||||
- 加签 Secret(查看/复制)
|
||||
- AES 数据加密 Secret(查看/复制)
|
||||
- 请求验签状态
|
||||
## 7. 访问日志
|
||||
1. 菜单与页面文案统一为“访问日志”(不再使用“解析日志”)。
|
||||
2. 页面结构:筛选区与列表区分离,间距与智能DNS访问日志风格一致。
|
||||
3. 列字段:集群、节点、域名、类型、概要。
|
||||
4. 概要展示:
|
||||
- 使用单行拼接
|
||||
- 按“访问信息 -> 解析结果”顺序
|
||||
- 不显示字段名堆叠
|
||||
|
||||
## 6. 日志页面(现状)
|
||||
## 8. 运行日志
|
||||
1. 字段与智能DNS运行日志保持一致。
|
||||
2. 级别样式使用文字着色,不使用大块背景色。
|
||||
|
||||
### 6.1 访问日志
|
||||
1. 主列:
|
||||
- 集群
|
||||
- 节点
|
||||
- 域名
|
||||
- 类型
|
||||
- 概要
|
||||
2. 概要按单行拼接展示,按“访问信息 -> 解析结果”顺序输出,不使用字段名堆叠。
|
||||
## 9. 解析测试
|
||||
1. 去掉“API在线沙盒”标题与右侧等待占位图标区。
|
||||
2. 解析配置区标题统一为“解析测试”。
|
||||
3. 测试参数保留:目标应用、所属集群、解析域名、模拟客户端IP、解析类型(A/AAAA)。
|
||||
4. 所属集群使用下拉框选择。
|
||||
5. 解析域名使用下拉框选择(按当前目标应用联动展示)。
|
||||
6. 去掉 SDNS 参数。
|
||||
7. 结果区保留核心结果展示,参考阿里云风格的简洁结果布局。
|
||||
|
||||
### 6.2 运行日志
|
||||
1. 字段:
|
||||
- 时间
|
||||
- 集群
|
||||
- 节点
|
||||
- 级别
|
||||
- 类型
|
||||
- 模块
|
||||
- 详情
|
||||
- 次数
|
||||
- 请求ID
|
||||
## 10. SDK 集成
|
||||
1. SDK 集成不作为左侧独立菜单。
|
||||
2. 在“应用管理”操作列进入 SDK 集成页。
|
||||
3. 页面只保留:
|
||||
- SDK 下载
|
||||
- 集成文档
|
||||
4. 卡片与按钮采用紧凑布局,避免按钮区域拥挤。
|
||||
|
||||
## 7. 解析测试(现状)
|
||||
1. 左侧配置项:
|
||||
- 目标应用
|
||||
- 解析域名
|
||||
- 模拟客户端 IP
|
||||
- 解析类型(A/AAAA)
|
||||
- SDNS 参数(最多 10 条)
|
||||
2. 右侧结果区:
|
||||
- 解析成功/失败
|
||||
- Request ID
|
||||
- 请求URL
|
||||
- 客户端 IP / 地区 / 线路
|
||||
- 解析记录表(解析域名、解析类型、IP地址、TTL、地区、线路)
|
||||
3. 已去掉无关展示块(如安全策略、验签响应头详情块)。
|
||||
## 11. SDK 与服务端接口(现行)
|
||||
1. 解析接口:`/resolve`
|
||||
- SDK 请求域名解析结果的主接口。
|
||||
2. 启动配置接口:`/bootstrap`(规划/联调口径)
|
||||
- 用于 SDK 获取可用服务域名与策略参数(替代节点IP调度模式)。
|
||||
3. SDK 地址策略:优先主集群服务域名,不可用时切备集群;失败后按客户端降级策略处理。
|
||||
|
||||
## 8. 本文不包含的内容
|
||||
1. 不包含 ECH 控制台与 ECH 分阶段交付设计。
|
||||
2. 不包含 SNI level1/level3、多级切换与 Public SNI 域名池页面设计。
|
||||
3. 不包含第三方 DNS 凭证复用与相关编排描述。
|
||||
4. 不包含用户平台(User)页面方案。
|
||||
## 12. 本文明确不包含
|
||||
1. 不包含全局配置独立页面设计。
|
||||
2. 不包含 SDK 接入向导独立菜单设计。
|
||||
3. 不包含 SNI level1/level3、ECH 控制台、Public SNI 池分级配置。
|
||||
4. 不包含 SDNS 参数匹配链路。
|
||||
5. 不包含第三方 DNS 依赖与复用方案。
|
||||
|
||||
254
EdgeHttpDNS/HTTPDNS后端开发计划.md
Normal file
254
EdgeHttpDNS/HTTPDNS后端开发计划.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# HTTPDNS后端开发计划(V1.0)
|
||||
|
||||
## 0. 文档信息
|
||||
- 目标文件:`EdgeHttpDNS/HTTPDNS后端开发计划.md`
|
||||
- 交付范围:`EdgeAdmin + EdgeAPI + EdgeNode + SDK对接接口`
|
||||
- 交付策略:一次性全量交付(非分阶段)
|
||||
- 核心约束:
|
||||
- 仅新协议
|
||||
- 无 `/bootstrap`
|
||||
- `/resolve` 使用 GET 参数
|
||||
- 线路匹配仅基于客户端 IP 归属
|
||||
- 独立 `edgeHTTPDNS*` 数据表
|
||||
- 新增独立节点角色 `NodeRoleHTTPDNS`
|
||||
- 访问日志 MySQL + ClickHouse 双写(查询优先 ClickHouse)
|
||||
|
||||
## 1. 目标与成功标准
|
||||
1. 将 HTTPDNS 从当前 Admin 侧 mock/store 方案落地为真实后端能力。
|
||||
2. 打通“配置 -> 下发 -> 解析 -> 日志 -> 查询”闭环。
|
||||
3. 与当前前端设计严格对齐:
|
||||
- 菜单:集群管理、应用管理、访问日志、运行日志、解析测试
|
||||
- SNI 固定为“隐匿 SNI”
|
||||
- 自定义解析不含 SDNS 参数
|
||||
4. 成功标准:
|
||||
- 管理页面均通过 RPC 读取真实数据,无本地 mock 依赖
|
||||
- `/resolve` 可按应用/域名/线路返回解析结果
|
||||
- 访问日志与运行日志可查询、可筛选、可分页
|
||||
- 节点配置与状态可下发和回传
|
||||
- 主备集群服务域名可在应用设置中配置并生效
|
||||
- EdgeNode 支持 SNI 与 Host 解耦路由,并可执行 WAF 动态验签与隐匿 SNI 转发
|
||||
|
||||
## 2. 架构与边界
|
||||
### 2.1 服务边界
|
||||
1. EdgeAdmin:仅负责页面动作与 RPC 编排,不存业务状态。
|
||||
2. EdgeAPI:负责数据存储、策略匹配、接口服务、日志汇聚。
|
||||
3. EdgeNode(HTTPDNS节点):负责执行解析、接收策略任务、上报运行日志/访问日志,并执行 SNI/Host 解耦路由、WAF 动态验签、隐匿 SNI 转发。
|
||||
4. SDK:手动配置应用关联主备服务域名,调用 `/resolve` 获取结果。
|
||||
|
||||
### 2.2 不做项
|
||||
1. 不做 `/bootstrap` 接口。
|
||||
2. 不做 ECH / SNI 分级策略。
|
||||
3. 不做 SDNS 参数匹配。
|
||||
4. 不做第三方 DNS 依赖复用。
|
||||
|
||||
## 3. 公开接口与契约(需新增/调整)
|
||||
### 3.1 HTTP 解析接口
|
||||
1. `GET /resolve`
|
||||
2. 请求参数:
|
||||
- `appId`(必填)
|
||||
- `dn`(必填)
|
||||
- `qtype`(可选,A/AAAA,默认 A)
|
||||
- `cip`(可选)
|
||||
- `sid`、`sdk_version`、`os`(可选,用于日志)
|
||||
- `nonce`、`exp`、`sign`(可选,验签开启时必需)
|
||||
3. 响应结构(统一):
|
||||
- `code`、`message`、`requestId`
|
||||
- `data`:
|
||||
- `domain`
|
||||
- `qtype`
|
||||
- `ttl`
|
||||
- `records[]`(`type`,`ip`,`weight?`,`line?`,`region?`)
|
||||
- `client`(`ip`,`region`,`carrier`,`country`)
|
||||
- `summary`(命中规则摘要)
|
||||
4. 错误码最低集合:
|
||||
- app 无效/禁用
|
||||
- 域名未绑定
|
||||
- 验签失败
|
||||
- 无可用解析记录
|
||||
- 内部解析失败/超时
|
||||
|
||||
### 3.2 管理 RPC(EdgeAdmin -> EdgeAPI)
|
||||
1. 新增服务:
|
||||
- `HTTPDNSClusterService`
|
||||
- `HTTPDNSNodeService`
|
||||
- `HTTPDNSAppService`
|
||||
- `HTTPDNSDomainService`
|
||||
- `HTTPDNSRuleService`
|
||||
- `HTTPDNSAccessLogService`
|
||||
- `HTTPDNSRuntimeLogService`
|
||||
- `HTTPDNSSandboxService`
|
||||
2. 最小方法集:
|
||||
- 集群:增删改查、设置默认集群、TLS证书绑定、节点列表/状态
|
||||
- 应用:增删改查、主备集群设置、启停、验签开关、密钥重置
|
||||
- 域名:绑定/解绑/列表
|
||||
- 自定义解析:规则增删改查、启停、排序
|
||||
- 日志:访问日志分页查询、运行日志分页查询
|
||||
- 测试:在线解析测试调用(入参包含 appId、clusterId、domain、qtype、clientIp)
|
||||
|
||||
### 3.3 节点日志上报 RPC(EdgeNode -> EdgeAPI)
|
||||
1. `CreateHTTPDNSAccessLogs`(批量)
|
||||
2. `CreateHTTPDNSRuntimeLogs`(批量)
|
||||
3. 幂等键:`requestId + nodeId`
|
||||
4. 支持高吞吐批量提交和失败重试
|
||||
|
||||
### 3.4 节点路由与 WAF 策略下发契约(EdgeAPI -> EdgeNode)
|
||||
1. 下发内容最小集合:
|
||||
- `appId/domain/serviceDomain`
|
||||
- `sniMode`(固定为隐匿 SNI)
|
||||
- `hostRouteMode`(SNI 与 Host 解耦)
|
||||
- `wafVerifyEnabled`
|
||||
- `wafVerifyPolicy`(验签字段、时效窗口、失败动作)
|
||||
2. 节点执行口径:
|
||||
- 入站按 `serviceDomain` 接入,按 Host/业务域名做路由匹配
|
||||
- TLS 握手与业务 Host 解耦,避免真实业务域名暴露在 SNI
|
||||
- 命中需要验签的应用时,先执行 WAF 动态验签,再继续解析链路
|
||||
3. 失败处置:
|
||||
- 验签失败按策略拒绝并记录运行日志、访问日志错误码
|
||||
- 路由未命中返回统一错误码并上报审计日志
|
||||
|
||||
## 4. 数据模型设计(独立 HTTPDNS 表)
|
||||
1. `edgeHTTPDNSClusters`
|
||||
- `id,name,isOn,isDefault,serviceDomain,defaultTTL,fallbackTimeoutMs,installDir,tlsPolicyJSON,createdAt,updatedAt`
|
||||
2. `edgeHTTPDNSNodes`
|
||||
- `id,clusterId,name,isOn,isUp,isInstalled,isActive,statusJSON,installStatusJSON,installDir,uniqueId,secret,createdAt,updatedAt`
|
||||
3. `edgeHTTPDNSApps`
|
||||
- `id,name,appId,isOn,primaryClusterId,backupClusterId,sniMode(fixed_hide),createdAt,updatedAt`
|
||||
4. `edgeHTTPDNSAppSecrets`
|
||||
- `id,appId,signEnabled,signSecretEnc,signUpdatedAt,updatedAt`
|
||||
5. `edgeHTTPDNSDomains`
|
||||
- `id,appId,domain,isOn,createdAt,updatedAt`
|
||||
6. `edgeHTTPDNSCustomRules`
|
||||
- `id,appId,domainId,ruleName,lineScope,lineCarrier,lineRegion,lineProvince,lineContinent,lineCountry,ttl,isOn,priority,updatedAt`
|
||||
7. `edgeHTTPDNSCustomRuleRecords`
|
||||
- `id,ruleId,recordType,recordValue,weight,sort`
|
||||
8. `edgeHTTPDNSAccessLogs`
|
||||
- `id,requestId,clusterId,nodeId,appId,domain,qtype,clientIP,clientRegion,carrier,sdkVersion,os,resultIPs,status,errorCode,costMs,createdAt,day`
|
||||
9. `edgeHTTPDNSRuntimeLogs`
|
||||
- `id,clusterId,nodeId,level,type,tag,description,count,createdAt,day`
|
||||
|
||||
### 4.1 索引与唯一约束
|
||||
1. 唯一:`edgeHTTPDNSApps.appId`
|
||||
2. 唯一:`edgeHTTPDNSDomains(appId,domain)`
|
||||
3. 唯一:`edgeHTTPDNSAccessLogs(requestId,nodeId)`
|
||||
4. 索引:
|
||||
- 访问日志:`day,clusterId,nodeId,domain,status,createdAt`
|
||||
- 规则匹配:`domainId,isOn,priority,lineScope,...`
|
||||
- 应用查询:`name,appId,isOn`
|
||||
|
||||
## 5. 解析引擎实现(EdgeAPI)
|
||||
1. 输入校验:`appId/dn/qtype`
|
||||
2. 应用与域名校验:
|
||||
- app 存在且启用
|
||||
- 域名已绑定到 app
|
||||
3. 线路归属:
|
||||
- `cip` 优先,其次 remote IP
|
||||
- 映射字段:运营商/大区/省份/洲/国家
|
||||
4. 规则匹配:
|
||||
- 精确匹配 > 半精确 > 默认
|
||||
- 同级按 `priority` 从小到大
|
||||
5. 记录返回:
|
||||
- 权重关闭:返回全部记录
|
||||
- 权重开启:按权重算法返回单条或子集(固定口径)
|
||||
6. TTL 取值:
|
||||
- 命中规则取规则 TTL
|
||||
- 未命中规则取集群默认 TTL
|
||||
7. 验签:
|
||||
- `signEnabled=true` 时必须通过签名校验
|
||||
- `signEnabled=false` 时跳过签名校验
|
||||
8. 访问日志:
|
||||
- 解析结束异步写日志
|
||||
- 双写 MySQL/ClickHouse
|
||||
|
||||
## 6. 节点与任务链路
|
||||
1. 在 `EdgeCommon/pkg/nodeconfigs` 增加 `NodeRoleHTTPDNS`。
|
||||
2. 在任务系统增加 HTTPDNS 任务类型:
|
||||
- 配置变更
|
||||
- 应用变更
|
||||
- 域名变更
|
||||
- 规则变更
|
||||
- 证书变更
|
||||
- 路由与 WAF 策略变更
|
||||
3. EdgeNode 增加 HTTPDNS 子服务:
|
||||
- 接收配置快照
|
||||
- 执行解析
|
||||
- 上报运行/访问日志
|
||||
- 执行 SNI/Host 解耦路由
|
||||
- 执行 WAF 动态验签
|
||||
- 执行隐匿 SNI 转发
|
||||
4. 复用现有安装升级框架,但节点角色、任务通道、日志 tag 独立。
|
||||
|
||||
## 7. EdgeAdmin 后端改造
|
||||
1. 将 `internal/web/actions/default/httpdns/*` 的 store/mock 读写改为 RPC。
|
||||
2. 删除/停用旧能力路由:
|
||||
- `/httpdns/policies`
|
||||
- `/httpdns/guide`
|
||||
- `/httpdns/ech`
|
||||
3. 保留必要跳转,避免旧链接 404。
|
||||
4. `sandbox/test` 改为调用真实解析服务(可保留测试开关)。
|
||||
5. 解析测试页面交互固定为:
|
||||
- 配置区标题“解析测试”
|
||||
- 所属集群使用下拉框
|
||||
- 解析域名使用下拉框并按目标应用联动
|
||||
|
||||
## 8. 安全与审计
|
||||
1. Secret 持久化使用加密存储(至少密文列),返回时脱敏。
|
||||
2. 操作审计记录:
|
||||
- 验签开关启停
|
||||
- Sign Secret 重置
|
||||
- 应用主备集群修改
|
||||
3. 验签失败日志保留 `requestId,errorCode,sourceIP`。
|
||||
4. 防滥用:
|
||||
- `/resolve` 增加基础限流与异常请求过滤(按 appId + IP 维度)。
|
||||
5. 节点侧安全执行:
|
||||
- WAF 动态验签失败必须留存审计日志(含 requestId/appId/sourceIP)
|
||||
- SNI/Host 解耦路由命中结果需可追踪(用于问题回溯)
|
||||
|
||||
## 9. 测试与验收
|
||||
### 9.1 单元测试
|
||||
1. 规则匹配优先级与线路匹配
|
||||
2. 验签成功/失败路径
|
||||
3. 权重返回算法
|
||||
4. DAO CRUD 与唯一约束
|
||||
|
||||
### 9.2 集成测试
|
||||
1. EdgeAdmin -> RPC -> DB 全链路
|
||||
2. `/resolve` 各错误码分支
|
||||
3. 节点日志上报双写(MySQL+CH)
|
||||
4. CH 不可用时 MySQL 回退查询
|
||||
5. EdgeAPI 策略下发 -> EdgeNode 路由/WAF 执行 -> 日志落库全链路
|
||||
|
||||
### 9.3 回归测试
|
||||
1. 智能DNS功能不受影响
|
||||
2. 菜单与权限不串模块
|
||||
3. 旧入口跳转正确,无新增 404
|
||||
|
||||
### 9.4 验收用例
|
||||
1. 应用配置主备服务域名后,SDK可解析成功
|
||||
2. 主集群故障时可切备集群
|
||||
3. 自定义解析按线路返回预期 IP
|
||||
4. 访问日志筛选与概要展示正确
|
||||
5. 运行日志级别/字段与智能DNS一致
|
||||
6. EdgeNode 可在 SNI 与 Host 解耦场景下正确路由到目标应用
|
||||
7. 开启 WAF 动态验签后,合法请求通过、非法签名请求被拒绝且有审计日志
|
||||
|
||||
## 10. 发布与回滚
|
||||
1. 发布顺序:
|
||||
- DB migration
|
||||
- EdgeAPI(DAO+RPC+resolve)
|
||||
- EdgeNode(角色+上报)
|
||||
- EdgeAdmin(RPC切换)
|
||||
2. 开关控制:
|
||||
- `httpdns.resolve.enabled`
|
||||
- `httpdns.log.clickhouse.enabled`
|
||||
3. 回滚策略:
|
||||
- 关闭 `resolve` 新实现开关
|
||||
- 访问日志读取切回 MySQL
|
||||
- Admin 保留只读能力
|
||||
|
||||
## 11. 默认值与固定决策
|
||||
1. 无 `/bootstrap`,SDK手动配置主备服务域名。
|
||||
2. `SNI` 固定“隐匿 SNI”。
|
||||
3. 自定义解析无 SDNS 参数。
|
||||
4. 线路仅客户端 IP 归属。
|
||||
5. 日志双写,查询优先 ClickHouse。
|
||||
6. 节点角色独立:`NodeRoleHTTPDNS`。
|
||||
@@ -1,114 +0,0 @@
|
||||
在域名管理中 加一个 自定义解析功能。
|
||||
功能简介:若需为域名提供特定的解析结果,可以使用HTTPDNS提供的自定义解析功能。该功能支持通过配置规则来实现对特定域名的自定义解析。
|
||||
应用场景:灰度测试:假设您的域名是 www.example.com,因为业务增长发布了新的服务,新的服务IP为1.1.X.X,在服务全量发布前,您希望对电信_上海访问域名的流量进行特定APP版本号的灰度测试。对于这些流量访问www.example.com时发起的域名解析请求返回1.1.X.X。
|
||||
|
||||
流量调度:假设某个汽车企业服务域名是 www.example.com,希望DNS解析过程中可以根据特定的业务逻辑返回位于不同区域的服务器的 IP 地址。例如根据汽车常驻地返回不同的服务IP,某辆汽车的常驻地区在广州,对于该汽车访问 www.example.com 时发起的 DNS 查询请求返回位于广州的服务器的 IP 地址。
|
||||
|
||||
|
||||
策略说明
|
||||
可以通过某种规则来达到自定义解析的目的。您可以对网络线路进行更精细的配置,并通过配置不同的解析参数,使来自不同运营商和地域的用户流量精准路由至不同的服务地址。
|
||||
|
||||
使用方式如下:
|
||||
|
||||
在 HTTPDNS 控制台 中,为指定域名创建一条自定义解析规则策略。
|
||||
|
||||
客户端通过 SDK 发起 DNS 查询请求时,携带相应的自定义解析参数。
|
||||
|
||||
HTTPDNS 服务端接收到请求后,会根据预设的匹配规则,返回最符合业务需求的解析结果。
|
||||
您可以做以下配置:
|
||||
|
||||
基本信息
|
||||
|
||||
参数
|
||||
|
||||
说明
|
||||
|
||||
域名
|
||||
|
||||
您希望自定义解析的域名,例如:www.aliyun.com。
|
||||
|
||||
说明
|
||||
域名选择下拉的数据源来自于接入域名中已经添加的域名,如果想要自定义解析的域名不在下拉列表中,可以到域名列表中添加后,再为该域名添加自定义解析记录。
|
||||
|
||||
如果要为某个泛域名的子域名添加自定义解析记录,例如:*.aliyun.com,但你想要自定义域名是 a.aliyun.com,则需要将 a.aliyun.com 添加到域名列表中,再为该域名添加自定义解析记录。
|
||||
|
||||
域名选择下拉字段选不到对应的域名有以下几种情况:
|
||||
|
||||
想要添加的域名不在域名列表中,您可以到接入域名中添加对应的域名即可。
|
||||
|
||||
想要添加的域名是某个泛域名的子域名,将该子域名添加到域名列表即可。
|
||||
|
||||
想要添加的域名已经存在自定义解析记录,需要去自定义解析记录列表管理对应的域名。
|
||||
|
||||
线路
|
||||
|
||||
可针对运营商和地域进行线路配置。
|
||||
|
||||
中国内地线路:按“运营商 > 大区 > 省份”进行配置。
|
||||
|
||||
运营商:可以自定义运营商,例如:中国电信。如果运营商设置为默认,表示当前线路覆盖所有运营商。
|
||||
|
||||
大区:按照地域划分,例如:东北、华北、华东等,省份归属在对应大区下。如果大区设置为默认,表示当前线路覆盖所有大区。
|
||||
|
||||
省份:可以自定义省份,例如:北京、河北,如果省份设置为默认,表示当前线路覆盖所有省份。
|
||||
|
||||
海外线路:选择地域为“境外”时生效,按“洲 > 国家或地区”进行配置。
|
||||
|
||||
可以选择大洲,例如:亚洲、欧洲、南美洲等;也可以在大洲下选择具体国家或地区,例如:日本、英国等。
|
||||
|
||||
如果大洲、国家或地区设置为默认,表示当前线路覆盖所选范围内的全部区域。
|
||||
|
||||
说明
|
||||
在同一个域名下,对于相同地域的用户,线路生效的优先级是:运营商>地理位置>默认。例如,电信-华北-北京>电信-华北-默认>默认-华北-北京>默认-默认-默认。
|
||||
|
||||
例如:如果在同一个域名下同时存在两条规则策略,线路分别是电信-华北-北京和电信-华北-默认,那么对于北京的电信用户会使用电信-华北-北京线路的规则策略。
|
||||
|
||||
自定义解析规则
|
||||
|
||||
一条规则策略最多支持配置 10 条自定义解析规则。
|
||||
|
||||
参数
|
||||
|
||||
说明
|
||||
|
||||
规则名称
|
||||
|
||||
说明当前规则的名称,可以用来表达规则的用途,例如:通过SDK版本调度。
|
||||
|
||||
规则排序
|
||||
|
||||
多个规则之间可以调整顺序,匹配的逻辑是从上往下串行匹配,顺序决定了哪个规则会被优先命中,调整顺序后会按照新的顺序匹配。
|
||||
|
||||
SDNS参数配置
|
||||
|
||||
用来匹配客户端请求解析接口携带的SDNS参数,决定该条规则是否被命中,如果匹配成功,则返回该条规则中的解析记录值。详细的匹配逻辑请查看规则策略匹配逻辑说明。
|
||||
|
||||
参数名称:SDNS参数的名称,长度限制为 2 ~ 64 个字符。
|
||||
|
||||
参数值:SDNS参数的值,长度限制为 1 ~ 64 个字符。
|
||||
|
||||
说明
|
||||
一条规则最多添加 10 个 SDNS参数。
|
||||
|
||||
解析接口可以添加SDNS参数,具体查看 客户端传递自定义解析参数。
|
||||
|
||||
解析记录值
|
||||
|
||||
自定义解析的返回值集合,每个记录值代表记录集中的一条解析记录,必填。
|
||||
|
||||
记录类型:返回解析记录值的类型,支持A和AAAA记录。
|
||||
|
||||
记录值:返回的记录值
|
||||
|
||||
您可以添加多个记录值,在未开启权重的情况下,添加的多个记录值将会合并在一起返回。
|
||||
|
||||
还可以按照权重调度,只需要打开按照权重调度开关即可。开启权重后,可以为每个记录值设置权重,权重值设置范围为:1-100,根据记录值的权重,通过负载均衡算法返回一个合适的记录值。
|
||||
|
||||
说明
|
||||
一条规则最多添加 10 个记录值。
|
||||
|
||||
TTL
|
||||
|
||||
必填,自定义解析记录的有效期。有效期越短,HTTPDNS SDK 中的解析记录缓存过期就越快。同时,HTTPDNS SDK 请求新的解析记录的频率就越高。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user