带阿里标识的版本

This commit is contained in:
robin
2026-02-28 18:55:33 +08:00
parent 150799f41d
commit 5d0b7c7e91
477 changed files with 10813 additions and 4044 deletions

View File

@@ -0,0 +1,4 @@
<first-menu>
<menu-item href="/httpdns/apps" code="index">应用列表</menu-item>
<a href="/httpdns/apps/create" class="item">[添加应用]</a>
</first-menu>

View File

@@ -0,0 +1,134 @@
{$layout}
{$template "menu"}
{$template "/left_menu_with_menu"}
<style>
.httpdns-mini-action {
display: inline-block;
font-size: 12px;
color: #6b7280;
margin-left: .55em;
line-height: 1.6;
}
.httpdns-mini-action:hover {
color: #1e70bf;
}
.httpdns-mini-action .icon {
margin-right: 0 !important;
font-size: .92em !important;
}
.httpdns-note.comment {
color: #8f9aa6 !important;
font-size: 12px;
margin-top: .45em !important;
}
.httpdns-auth-table td.title {
width: 260px !important;
min-width: 260px !important;
white-space: nowrap;
word-break: keep-all;
}
.httpdns-secret-line {
display: flex;
align-items: center;
gap: .35em;
}
.httpdns-mini-icon {
color: #8c96a3 !important;
font-size: 12px;
line-height: 1;
}
.httpdns-mini-icon:hover {
color: #5e6c7b !important;
}
.httpdns-state-on {
color: #21ba45;
font-weight: 600;
}
.httpdns-state-off {
color: #8f9aa6;
font-weight: 600;
}
</style>
<div class="right-box with-menu">
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="appId" :value="app.id" />
<table class="ui table selectable definition" v-show="activeSection == 'basic'">
<tr>
<td class="title">应用启用</td>
<td>
<checkbox name="appStatus" value="1" v-model="settings.appStatus"></checkbox>
</td>
</tr>
<tr>
<td class="title">SNI 防护配置</td>
<td>
<span class="green">隐匿 SNI</span>
<p class="comment httpdns-note">当前默认采用隐匿 SNI 策略,避免在 TLS 握手阶段暴露业务域名。</p>
</td>
</tr>
</table>
<table class="ui table selectable definition httpdns-auth-table" v-show="activeSection == 'auth'">
<tr>
<td class="title">App ID</td>
<td>
<code>{{settings.appId}}</code>
<a href="" class="httpdns-mini-icon" title="复制 App ID" @click.prevent="copySecret(settings.appId, 'App ID')"><i class="copy outline icon"></i></a>
</td>
</tr>
<tr>
<td class="title">主服务域名</td>
<td>
<code v-if="settings.primaryServiceDomain && settings.primaryServiceDomain.length > 0">{{settings.primaryServiceDomain}}</code>
<span class="grey" v-else>未配置</span>
<a v-if="settings.primaryServiceDomain && settings.primaryServiceDomain.length > 0" href="" class="httpdns-mini-icon" title="复制主服务域名" @click.prevent="copySecret(settings.primaryServiceDomain, '主服务域名')"><i class="copy outline icon"></i></a>
</td>
</tr>
<tr>
<td class="title">备用服务域名</td>
<td>
<code v-if="settings.backupServiceDomain && settings.backupServiceDomain.length > 0">{{settings.backupServiceDomain}}</code>
<span class="grey" v-else>未配置</span>
<a v-if="settings.backupServiceDomain && settings.backupServiceDomain.length > 0" href="" class="httpdns-mini-icon" title="复制备用服务域名" @click.prevent="copySecret(settings.backupServiceDomain, '备用服务域名')"><i class="copy outline icon"></i></a>
</td>
</tr>
<tr>
<td class="title">请求验签</td>
<td>
<span :class="settings.signEnabled ? 'httpdns-state-on' : 'httpdns-state-off'">{{settings.signEnabled ? "已开启" : "已关闭"}}</span>
<a href="" class="ui mini button basic" style="margin-left: .8em;" @click.prevent="toggleSignEnabled">{{settings.signEnabled ? "关闭请求验签" : "开启请求验签"}}</a>
<p class="comment httpdns-note">打开后,服务端会对请求进行签名校验。</p>
</td>
</tr>
<tr>
<td class="title">加签 Secret</td>
<td>
<div class="httpdns-secret-line">
<code>{{signSecretVisible ? settings.signSecretPlain : settings.signSecretMasked}}</code>
<a href="" class="httpdns-mini-icon" @click.prevent="signSecretVisible = !signSecretVisible" :title="signSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon" :class="signSecretVisible ? 'eye slash' : 'eye'"></i></a>
<a href="" class="httpdns-mini-icon" title="复制加签 Secret" @click.prevent="copySecret(settings.signSecretPlain, '加签 Secret')"><i class="copy outline icon"></i></a>
<a href="" class="httpdns-mini-icon" title="重置加签 Secret" @click.prevent="resetSignSecret"><i class="redo icon"></i></a>
</div>
<p class="comment httpdns-note">最近更新:{{settings.signSecretUpdatedAt}}</p>
<p class="comment httpdns-note" v-if="!settings.signEnabled">请求验签已关闭,当前不使用加签 Secret。</p>
<p class="comment httpdns-note">用于生成鉴权接口的安全密钥。</p>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>
</div>

View File

@@ -0,0 +1,95 @@
Tea.context(function () {
this.activeSection = this.activeSection || "basic";
this.success = NotifyReloadSuccess("保存成功");
this.signSecretVisible = false;
this.toggleSignEnabled = function () {
let that = this;
let targetIsOn = !this.settings.signEnabled;
if (targetIsOn) {
teaweb.confirm("html:开启后,服务端会对解析请求进行签名鉴权,<span class='red'>未签名、签名无效或过期的请求都会解析失败</span>,确认开启吗?", function () {
that.$post("/httpdns/apps/app/settings/toggleSignEnabled")
.params({
appId: that.app.id,
isOn: 1
})
.success(function () {
that.settings.signEnabled = true;
teaweb.success("请求验签已开启");
});
});
return;
}
teaweb.confirm("html:关闭后,服务端不会对解析请求进行签名鉴权,可能<span class='red'>存在被刷风险</span>,确认关闭吗?", function () {
that.$post("/httpdns/apps/app/settings/toggleSignEnabled")
.params({
appId: that.app.id,
isOn: 0
})
.success(function () {
that.settings.signEnabled = false;
teaweb.success("请求验签已关闭");
});
});
};
this.copySecret = function (text, name) {
if (typeof text != "string" || text.length == 0) {
teaweb.warn("没有可复制的内容");
return;
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
teaweb.success(name + "已复制");
}).catch(function () {
this.copyByTextarea(text, name);
}.bind(this));
return;
}
this.copyByTextarea(text, name);
};
this.copyByTextarea = function (text, name) {
let input = document.createElement("textarea");
input.value = text;
input.setAttribute("readonly", "readonly");
input.style.position = "fixed";
input.style.left = "-10000px";
input.style.top = "-10000px";
document.body.appendChild(input);
input.select();
let ok = false;
try {
ok = document.execCommand("copy");
} catch (e) {
ok = false;
}
document.body.removeChild(input);
if (ok) {
teaweb.success(name + "已复制");
} else {
teaweb.warn("复制失败,请手动复制");
}
};
this.resetSignSecret = function () {
let that = this;
teaweb.confirm("确定要重置加签 Secret 吗?", function () {
that.$post("/httpdns/apps/app/settings/resetSignSecret")
.params({
appId: that.app.id
})
.success(function () {
teaweb.success("加签 Secret 已重置", function () {
teaweb.reload();
});
});
});
};
});

View File

@@ -0,0 +1,16 @@
{$layout}
{$template "menu"}
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<table class="ui table definition selectable">
<tr>
<td class="title">应用名称 *</td>
<td>
<input type="text" name="name" maxlength="64" ref="focus" />
<p class="comment">为该 HTTPDNS 应用设置一个便于识别的名称。</p>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>

Binary file not shown.

View File

@@ -0,0 +1,55 @@
{$layout}
<style>
.httpdns-col-ttl {
width: 72px;
white-space: nowrap;
}
.httpdns-col-actions {
width: 130px;
white-space: nowrap;
}
</style>
<second-menu>
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">域名列表</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<div class="item"><strong>{{domain.name}}</strong></div>
<a href="" class="item" @click.prevent="createRecord" :class="{disabled: !domain || !domain.id}">创建规则</a>
</second-menu>
<table class="ui table selectable celled" v-if="records.length > 0" style="margin-top: .8em;">
<thead>
<tr>
<th>线路</th>
<th>规则名称</th>
<th>解析记录</th>
<th class="httpdns-col-ttl">TTL</th>
<th class="width10">状态</th>
<th class="httpdns-col-actions">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="record in records">
<td>{{record.lineText}}</td>
<td>{{record.ruleName}}</td>
<td>{{record.recordValueText}}</td>
<td class="httpdns-col-ttl">{{record.ttl}}s</td>
<td>
<span class="green" v-if="record.isOn">已启用</span>
<span class="grey" v-else>已停用</span>
</td>
<td class="httpdns-col-actions">
<a href="" @click.prevent="editRecord(record.id)">编辑</a> &nbsp;|&nbsp;
<a href="" @click.prevent="toggleRecord(record)">{{record.isOn ? "停用" : "启用"}}</a> &nbsp;|&nbsp;
<a href="" @click.prevent="deleteRecord(record.id)">删除</a>
</td>
</tr>
</tbody>
</table>
<not-found-box v-if="!domain || !domain.id">当前应用暂无可用域名,请先到域名管理中添加域名。</not-found-box>
<not-found-box v-else-if="records.length == 0">当前域名还没有自定义解析规则。</not-found-box>

View File

@@ -0,0 +1,60 @@
Tea.context(function () {
if (typeof this.records == "undefined") {
this.records = [];
}
this.createRecord = function () {
if (!this.domain || !this.domain.id) {
return;
}
teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id, {
width: "42em",
height: "33em",
title: "新增自定义解析规则",
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload();
});
}
});
};
this.editRecord = function (recordId) {
if (!this.domain || !this.domain.id) {
return;
}
teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id + "&recordId=" + recordId, {
width: "42em",
height: "33em",
title: "编辑自定义解析规则",
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload();
});
}
});
};
this.deleteRecord = function (recordId) {
let that = this;
teaweb.confirm("确定要删除这条自定义解析规则吗?", function () {
that.$post("/httpdns/apps/customRecords/delete")
.params({
appId: that.app.id,
recordId: recordId
})
.refresh();
});
};
this.toggleRecord = function (record) {
let that = this;
that.$post("/httpdns/apps/customRecords/toggle")
.params({
appId: that.app.id,
recordId: record.id,
isOn: record.isOn ? 0 : 1
})
.refresh();
};
});

View File

@@ -0,0 +1,160 @@
{$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;
}
</style>
<h3 v-if="isEditing">编辑自定义解析规则</h3>
<h3 v-else>新增自定义解析规则</h3>
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="appId" :value="app.id" />
<input type="hidden" name="domainId" :value="domain.id" />
<input type="hidden" name="domain" :value="record.domain" />
<input type="hidden" name="recordId" :value="record.id" />
<input type="hidden" name="recordItemsJSON" :value="JSON.stringify(recordItems)" />
<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="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="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="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>
<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>
</form>

View File

@@ -0,0 +1,155 @@
Tea.context(function () {
var vm = this;
if (typeof vm.record == "undefined" || vm.record == null) {
vm.record = {};
}
vm.chinaCarriers = ["默认", "电信", "联通", "移动", "教育网", "鹏博士", "广电"];
vm.chinaRegions = ["默认", "东北", "华北", "华东", "华南", "华中", "西北", "西南"];
vm.chinaRegionProvinces = {
"默认": ["默认"],
"东北": ["默认", "辽宁", "吉林", "黑龙江"],
"华北": ["默认", "北京", "天津", "河北", "山西", "内蒙古"],
"华东": ["默认", "上海", "江苏", "浙江", "安徽", "福建", "江西", "山东"],
"华南": ["默认", "广东", "广西", "海南"],
"华中": ["默认", "河南", "湖北", "湖南"],
"西北": ["默认", "陕西", "甘肃", "青海", "宁夏", "新疆"],
"西南": ["默认", "重庆", "四川", "贵州", "云南", "西藏"]
};
vm.continents = ["默认", "非洲", "南极洲", "亚洲", "欧洲", "北美洲", "南美洲", "大洋洲"];
vm.continentCountries = {
"默认": ["默认"],
"非洲": ["默认", "南非", "埃及", "尼日利亚", "肯尼亚", "摩洛哥"],
"南极洲": ["默认"],
"亚洲": ["默认", "中国香港", "中国澳门", "中国台湾", "日本", "韩国", "新加坡", "印度", "泰国", "越南"],
"欧洲": ["默认", "德国", "英国", "法国", "荷兰", "西班牙", "意大利", "俄罗斯"],
"北美洲": ["默认", "美国", "加拿大", "墨西哥"],
"南美洲": ["默认", "巴西", "阿根廷", "智利", "哥伦比亚"],
"大洋洲": ["默认", "澳大利亚", "新西兰"]
};
vm.parseJSONList = function (raw) {
if (typeof raw == "undefined" || raw == null || raw == "") {
return [];
}
if (Array.isArray(raw)) {
return raw;
}
try {
var list = JSON.parse(raw);
if (Array.isArray(list)) {
return list;
}
} catch (e) {
}
return [];
};
vm.normalizeBoolean = function (value, defaultValue) {
if (typeof value == "boolean") {
return value;
}
if (typeof value == "number") {
return value > 0;
}
if (typeof value == "string") {
value = value.toLowerCase();
return value == "1" || value == "true" || value == "yes" || value == "on";
}
return defaultValue;
};
vm.record.lineScope = vm.record.lineScope == "overseas" ? "overseas" : "china";
vm.record.lineCarrier = vm.record.lineCarrier || "默认";
vm.record.lineRegion = vm.record.lineRegion || "默认";
vm.record.lineProvince = vm.record.lineProvince || "默认";
vm.record.lineContinent = vm.record.lineContinent || "默认";
vm.record.lineCountry = vm.record.lineCountry || "默认";
vm.record.ruleName = vm.record.ruleName || "";
vm.record.ttl = vm.record.ttl || 30;
vm.record.weightEnabled = vm.normalizeBoolean(vm.record.weightEnabled, false);
vm.record.isOn = vm.normalizeBoolean(vm.record.isOn, true);
vm.recordItems = vm.parseJSONList(vm.record.recordItemsJson);
if (vm.recordItems.length == 0) {
vm.recordItems.push({type: "A", value: "", weight: 100});
} else {
for (var i = 0; i < vm.recordItems.length; i++) {
var item = vm.recordItems[i];
if (item.type != "A" && item.type != "AAAA") {
item.type = "A";
}
if (typeof item.weight == "undefined" || item.weight == null || item.weight === "") {
item.weight = 100;
}
}
}
vm.provinceOptions = ["默认"];
vm.countryOptions = ["默认"];
vm.refreshProvinceOptions = function () {
var provinces = vm.chinaRegionProvinces[vm.record.lineRegion];
if (!Array.isArray(provinces) || provinces.length == 0) {
provinces = ["默认"];
}
vm.provinceOptions = provinces;
if (vm.provinceOptions.indexOf(vm.record.lineProvince) < 0) {
vm.record.lineProvince = vm.provinceOptions[0];
}
};
vm.refreshCountryOptions = function () {
var countries = vm.continentCountries[vm.record.lineContinent];
if (!Array.isArray(countries) || countries.length == 0) {
countries = ["默认"];
}
vm.countryOptions = countries;
if (vm.countryOptions.indexOf(vm.record.lineCountry) < 0) {
vm.record.lineCountry = vm.countryOptions[0];
}
};
vm.onChinaRegionChange = function () {
vm.refreshProvinceOptions();
};
vm.onContinentChange = function () {
vm.refreshCountryOptions();
};
vm.onLineScopeChange = function () {
if (vm.record.lineScope == "overseas") {
vm.record.lineContinent = vm.record.lineContinent || "默认";
vm.refreshCountryOptions();
} else {
vm.record.lineCarrier = vm.record.lineCarrier || "默认";
vm.record.lineRegion = vm.record.lineRegion || "默认";
vm.refreshProvinceOptions();
}
};
vm.addRecordItem = function () {
if (vm.recordItems.length >= 10) {
return;
}
vm.recordItems.push({type: "A", value: "", weight: 100});
};
vm.removeRecordItem = function (index) {
if (index < 0 || index >= vm.recordItems.length) {
return;
}
vm.recordItems.splice(index, 1);
if (vm.recordItems.length == 0) {
vm.recordItems.push({type: "A", value: "", weight: 100});
}
};
vm.onLineScopeChange();
});

View File

@@ -0,0 +1,12 @@
{$layout}
<second-menu>
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<div class="item"><strong>删除应用</strong></div>
</second-menu>
<div class="buttons-box">
<button class="ui button red" type="button" @click.prevent="deleteApp(app.id)">删除当前应用</button>
<p class="comment" style="margin-top: .8em;">包含{{domainCount}}域名</p>
</div>

View File

@@ -0,0 +1,16 @@
Tea.context(function () {
this.deleteApp = function (appId) {
let that = this;
teaweb.confirm("确定要删除此应用吗?", function () {
that.$post("/httpdns/apps/delete")
.params({
appId: appId
})
.success(function () {
teaweb.success("删除成功", function () {
window.location = "/httpdns/apps";
});
});
});
};
});

View File

@@ -0,0 +1,36 @@
# Android SDK
## 初始化
```java
new InitConfig.Builder()
.setContext(context)
.setPrimaryServiceHost("httpdns-a.example.com")
.setBackupServiceHost("httpdns-b.example.com")
.setServicePort(443)
.setSecretKey("your-sign-secret")
.setEnableHttps(true)
.buildFor("app1f1ndpo9");
```
## 解析
```java
HTTPDNSResult result = httpDnsService.getHttpDnsResultForHostSyncNonBlocking(
"api.business.com",
RequestIpType.auto,
null,
null
);
```
## 官方业务适配器
```java
HttpDnsHttpAdapter adapter = HttpDns.buildHttpClientAdapter(httpDnsService);
HttpDnsAdapterResponse resp = adapter.execute(
new HttpDnsAdapterRequest("GET", "https://api.business.com/v1/ping")
);
```
固定策略IP 直连 + 空 SNI + Host=真实域名,不回退到带 SNI。

View File

@@ -0,0 +1,35 @@
# Flutter SDK
## 初始化
```dart
await AliyunHttpdns.init(
appId: 'app1f1ndpo9',
primaryServiceHost: 'httpdns-a.example.com',
backupServiceHost: 'httpdns-b.example.com',
servicePort: 443,
secretKey: 'your-sign-secret',
);
await AliyunHttpdns.build();
```
## 解析
```dart
final result = await AliyunHttpdns.resolveHostSyncNonBlocking(
'api.business.com',
ipType: 'both',
);
```
## 官方业务适配器
```dart
final adapter = AliyunHttpdns.createHttpAdapter();
final resp = await adapter.request(
Uri.parse('https://api.business.com/v1/ping'),
method: 'GET',
);
```
固定策略IP 直连 + 空 SNI + Host=真实域名,不回退到带 SNI。

View File

@@ -0,0 +1,31 @@
# iOS SDK
## 初始化
```objc
HttpdnsEdgeService *service = [[HttpdnsEdgeService alloc]
initWithAppId:@"app1f1ndpo9"
primaryServiceHost:@"httpdns-a.example.com"
backupServiceHost:@"httpdns-b.example.com"
servicePort:443
signSecret:@"your-sign-secret"];
```
## 解析
```objc
[service resolveHost:@"api.business.com" queryType:@"A" completion:^(HttpdnsEdgeResolveResult * _Nullable result, NSError * _Nullable error) {
// result.ipv4s / result.ipv6s
}];
```
## 官方业务适配器
```objc
NSURL *url = [NSURL URLWithString:@"https://api.business.com/v1/ping"];
[service requestURL:url method:@"GET" headers:nil body:nil completion:^(NSData * _Nullable data, NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {
// handle
}];
```
固定策略IP 直连 + 空 SNI + Host=真实域名,不回退到带 SNI。

View File

@@ -0,0 +1,62 @@
{$layout}
<div class="margin"></div>
<style>
.httpdns-domains-table .httpdns-domains-op {
white-space: nowrap;
}
</style>
<second-menu>
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<div class="item"><strong>域名列表</strong></div>
<a href="" class="item" @click.prevent="bindDomain">创建域名</a>
</second-menu>
<form class="ui form small" @submit.prevent="doSearch">
<div class="ui fields inline">
<div class="ui field">
<input type="text" v-model.trim="keywordInput" placeholder="按域名筛选..." />
</div>
<div class="ui field">
<button type="submit" class="ui button small">搜索</button>
&nbsp;
<a href="" v-if="keyword && keyword.length > 0" @click.prevent="clearSearch">[清除条件]</a>
</div>
</div>
</form>
<table class="ui table selectable celled httpdns-domains-table" v-if="filteredDomains().length > 0">
<colgroup>
<col style="width:60%;" />
<col style="width:20%;" />
<col style="width:20%;" />
</colgroup>
<thead>
<tr>
<th>服务域名</th>
<th class="width10">规则策略</th>
<th class="httpdns-domains-op">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="domain in filteredDomains()">
<td>
<div><strong>{{domain.name}}</strong></div>
</td>
<td>
<a
:href="'/httpdns/apps/customRecords?appId=' + app.id + '&domainId=' + domain.id">{{domain.customRecordCount}}</a>
</td>
<td class="httpdns-domains-op">
<a :href="'/httpdns/apps/customRecords?appId=' + app.id + '&domainId=' + domain.id">自定义解析</a>
&nbsp;|&nbsp;
<a href="" @click.prevent="deleteDomain(domain.id)">解绑</a>
</td>
</tr>
</tbody>
</table>
<not-found-box v-if="!domains || domains.length == 0">当前应用尚未绑定域名。</not-found-box>
<not-found-box v-if="domains && domains.length > 0 && filteredDomains().length == 0">没有匹配的域名。</not-found-box>

View File

@@ -0,0 +1,72 @@
Tea.context(function () {
if (typeof this.keywordInput == "undefined" || this.keywordInput == null) {
this.keywordInput = "";
}
if (typeof this.keyword == "undefined" || this.keyword == null) {
this.keyword = "";
}
if (typeof this.domains == "undefined" || this.domains == null) {
this.domains = [];
}
this.keywordInput = String(this.keyword);
this.doSearch = function () {
this.keyword = String(this.keywordInput || "").trim();
};
this.clearSearch = function () {
this.keywordInput = "";
this.keyword = "";
};
this.filteredDomains = function () {
let domains = Array.isArray(this.domains) ? this.domains : [];
let keyword = String(this.keyword || "").trim().toLowerCase();
if (keyword.length == 0) {
return domains;
}
return domains.filter(function (domain) {
let name = (domain.name || "").toLowerCase();
return name.indexOf(keyword) >= 0;
});
};
this.bindDomain = function () {
teaweb.popup("/httpdns/apps/domains/createPopup?appId=" + this.app.id, {
height: "24em",
width: "46em",
title: "添加域名",
callback: function () {
teaweb.success("保存成功", function () {
teaweb.reload();
});
}
});
};
this.deleteApp = function () {
let that = this;
teaweb.confirm("确定要删除当前应用吗?", function () {
that.$post("/httpdns/apps/delete")
.params({
appId: that.app.id
})
.success(function () {
window.location = "/httpdns/apps";
});
});
};
this.deleteDomain = function (domainId) {
let that = this;
teaweb.confirm("确定要解绑这个域名吗?", function () {
that.$post("/httpdns/apps/domains/delete")
.params({
domainId: domainId
})
.refresh();
});
};
});

View File

@@ -0,0 +1,18 @@
{$layout "layout_popup"}
<h3>添加域名</h3>
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="appId" :value="app.id" />
<table class="ui table definition selectable">
<tr>
<td class="title">域名 *</td>
<td>
<input type="text" name="domain" maxlength="255" placeholder="api.example.com" ref="focus" />
<p class="comment">请输入完整 FQDN例如 <code>api.example.com</code></p>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,71 @@
{$layout}
{$template "menu"}
<div class="ui margin"></div>
<style>
.httpdns-apps-table .httpdns-apps-op {
white-space: nowrap;
}
</style>
<div>
<form method="get" class="ui form" action="/httpdns/apps">
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="keyword" style="width:16em" placeholder="AppID 或应用名称..." :value="keyword" />
</div>
<div class="ui field">
<button type="submit" class="ui button">查询</button>
<a href="/httpdns/apps" v-if="keyword.length > 0">[清除条件]</a>
</div>
</div>
</form>
<p class="ui message" v-if="apps.length == 0">暂时没有符合条件的 HTTPDNS 应用。</p>
<table class="ui table selectable celled httpdns-apps-table" v-if="apps.length > 0">
<colgroup>
<col style="width:32%;" />
<col style="width:26%;" />
<col style="width:12%;" />
<col style="width:10%;" />
<col style="width:20%;" />
</colgroup>
<thead>
<tr>
<th>应用名称</th>
<th>AppID</th>
<th class="center">绑定域名数</th>
<th class="center">状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="app in apps">
<td>
<a :href="'/httpdns/apps/app?appId=' + app.id">
<strong>
<keyword :v-word="keyword">{{app.name}}</keyword>
</strong>
</a>
</td>
<td>
<code>{{app.appId}}</code>
<copy-icon :text="app.appId"></copy-icon>
</td>
<td class="center"><a :href="'/httpdns/apps/domains?appId=' + app.id">{{app.domainCount}}</a></td>
<td class="center">
<label-on :v-is-on="app.isOn"></label-on>
</td>
<td class="httpdns-apps-op">
<a :href="'/httpdns/apps/app?appId=' + app.id">域名管理</a> &nbsp;|&nbsp;
<a :href="'/httpdns/apps/app/settings?appId=' + app.id">应用设置</a> &nbsp;|&nbsp;
<a :href="'/httpdns/apps/sdk?appId=' + app.id">SDK集成</a>
</td>
</tr>
</tbody>
</table>
<div class="page" v-html="page"></div>
</div>

View File

@@ -0,0 +1,9 @@
Tea.context(function () {
if (typeof this.apps == "undefined") {
this.apps = [];
}
this.createApp = function () {
window.location = "/httpdns/apps/create";
};
});

View File

@@ -0,0 +1,78 @@
{$layout}
{$template "menu"}
<div class="ui margin"></div>
<h3 class="ui header">HTTPDNS 鍏ㄥ眬绛栫暐</h3>
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<table class="ui table definition selectable">
<tr>
<td class="title">鍏ㄥ眬榛樿瑙f瀽 TTL</td>
<td><input type="text" name="defaultTTL" maxlength="8" v-model="policies.defaultTTL" /> 绉?/td>
</tr>
<tr>
<td class="title">鍏ㄥ眬榛樿 SNI 绛夌骇</td>
<td>
<select name="defaultSniPolicy" class="ui dropdown auto-width" v-model="policies.defaultSniPolicy">
<option value="level1">level1锛堝浐瀹?SNI锛?/option>
<option value="level2">level2锛堥殣鍖?SNI锛?/option>
<option value="level3">level3锛圗CH锛?/option>
</select>
<div class="grey small" v-if="policies.defaultSniPolicy == 'level1'" style="margin-top:.5em;">
level1 浠呬娇鐢ㄥ浐瀹?SNI锛屼笉鍚敤 ECS 鍜岃瘉涔︽牎楠岀瓥鐣ャ€? </div>
<div class="grey small" v-else-if="policies.defaultSniPolicy == 'level2'" style="margin-top:.5em;">
level2 浠呭惎鐢ㄩ殣鍖?SNI锛屼笉瑕佹眰閰嶇疆 ECS 涓庤瘉涔︾瓥鐣ャ€? </div>
<div class="grey small" v-else style="margin-top:.5em;">
level3 鍚敤 ECH锛屽缓璁悓鏃堕厤缃?ECS 涓庤瘉涔︽牎楠岀瓥鐣ャ€? </div>
</td>
</tr>
<tr>
<td class="title">鍏ㄥ眬闄嶇骇瓒呮椂</td>
<td><input type="text" name="defaultFallbackMs" maxlength="8" v-model="policies.defaultFallbackMs" /> 姣</td>
</tr>
<tr v-show="policies.defaultSniPolicy == 'level3'">
<td class="title">全局 ECS 掩码策略</td>
<td>
<select name="ecsMode" class="ui dropdown auto-width" v-model="policies.ecsMode">
<option value="off">鍏抽棴</option>
<option value="auto">鑷姩</option>
<option value="custom">鑷畾涔?/option>
</select>
<span v-if="policies.ecsMode == 'custom'">
<span class="grey" style="margin-left:.8em;">IPv4 /</span>
<input type="text" name="ecsIPv4Prefix" maxlength="3" v-model="policies.ecsIPv4Prefix" style="width:4.5em;" />
<span class="grey" style="margin-left:.8em;">IPv6 /</span>
<input type="text" name="ecsIPv6Prefix" maxlength="3" v-model="policies.ecsIPv6Prefix" style="width:4.5em;" />
</span>
<span v-else class="grey" style="margin-left:.8em;">仅在“自定义”模式下配置掩码。</span>
<input type="hidden" name="ecsIPv4Prefix" v-model="policies.ecsIPv4Prefix" v-if="policies.ecsMode != 'custom'" />
<input type="hidden" name="ecsIPv6Prefix" v-model="policies.ecsIPv6Prefix" v-if="policies.ecsMode != 'custom'" />
</td>
</tr>
<tr v-show="policies.defaultSniPolicy == 'level3'">
<td class="title">鍏ㄥ眬璇佷功鏍¢獙绛栫暐</td>
<td>
<span class="grey">证书指纹校验Pinning</span>
<select name="pinningMode" class="ui dropdown auto-width" v-model="policies.pinningMode" style="margin-left:.4em;">
<option value="off">鍏抽棴</option>
<option value="report">瑙傚療妯″紡</option>
<option value="enforce">寮哄埗鏍¢獙</option>
</select>
<span class="grey" style="margin-left:1.2em;">证书 SAN 域名校验</span>
<select name="sanMode" class="ui dropdown auto-width" v-model="policies.sanMode" style="margin-left:.4em;">
<option value="off">鍏抽棴</option>
<option value="report">瑙傚療妯″紡</option>
<option value="strict">涓ユ牸鏍¢獙</option>
</select>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,40 @@
Tea.context(function () {
this.success = NotifyReloadSuccess("保存成功");
this.$delay(function () {
this.$watch("policies.defaultSniPolicy", function (level) {
if (level == "level1" || level == "level2") {
this.policies.ecsMode = "off";
this.policies.pinningMode = "off";
this.policies.sanMode = "off";
return;
}
if (level == "level3") {
if (this.policies.ecsMode == "off") {
this.policies.ecsMode = "auto";
}
if (this.policies.pinningMode == "off") {
this.policies.pinningMode = "report";
}
if (this.policies.sanMode == "off") {
this.policies.sanMode = "strict";
}
}
});
this.$watch("policies.ecsMode", function (mode) {
if (this.policies.defaultSniPolicy != "level3") {
return;
}
if (mode == "custom") {
if (!this.policies.ecsIPv4Prefix || this.policies.ecsIPv4Prefix <= 0) {
this.policies.ecsIPv4Prefix = 24;
}
if (!this.policies.ecsIPv6Prefix || this.policies.ecsIPv6Prefix <= 0) {
this.policies.ecsIPv6Prefix = 56;
}
}
});
});
});

View File

@@ -0,0 +1,79 @@
{$layout}
<style>
.httpdns-sdk-cards {
margin-top: 1em;
}
.httpdns-sdk-cards > .card {
min-height: 168px;
}
.httpdns-sdk-desc {
margin-top: .6em;
color: #5e6c7b;
}
.httpdns-sdk-meta {
margin-top: .35em;
color: #9aa6b2;
font-size: 12px;
}
.httpdns-sdk-actions {
display: inline-flex;
align-items: center;
gap: .6em;
flex-wrap: wrap;
}
.httpdns-sdk-actions .button {
padding-left: .8em !important;
padding-right: .8em !important;
}
</style>
<second-menu>
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<div class="item"><strong>SDK 集成</strong></div>
</second-menu>
<div class="ui three stackable cards httpdns-sdk-cards">
<div class="card">
<div class="content">
<div class="header"><i class="icon android green"></i> Android SDK</div>
<div class="httpdns-sdk-meta">Java / Kotlin</div>
<div class="description httpdns-sdk-desc">适用于 Android 客户端接入。</div>
</div>
<div class="extra content">
<div class="httpdns-sdk-actions">
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=android" @click="downloadSDK('android', $event)"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=android" @click="downloadDoc('android', $event)"><i class="icon book"></i> 下载文档</a>
</div>
</div>
</div>
<div class="card">
<div class="content">
<div class="header"><i class="icon apple grey"></i> iOS SDK</div>
<div class="httpdns-sdk-meta">Objective-C / Swift</div>
<div class="description httpdns-sdk-desc">适用于 iOS 客户端接入。</div>
</div>
<div class="extra content">
<div class="httpdns-sdk-actions">
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=ios" @click="downloadSDK('ios', $event)"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=ios" @click="downloadDoc('ios', $event)"><i class="icon book"></i> 下载文档</a>
</div>
</div>
</div>
<div class="card">
<div class="content">
<div class="header"><i class="icon mobile alternate blue"></i> Flutter SDK</div>
<div class="httpdns-sdk-meta">Dart / Plugin</div>
<div class="description httpdns-sdk-desc">适用于 Flutter 跨平台接入。</div>
</div>
<div class="extra content">
<div class="httpdns-sdk-actions">
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=flutter" @click="downloadSDK('flutter', $event)"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=flutter" @click="downloadDoc('flutter', $event)"><i class="icon book"></i> 下载文档</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,123 @@
Tea.context(function () {
this.downloadSDK = function (platform, event) {
this.checkAndDownload(platform, "sdk", event)
}
this.downloadDoc = function (platform, event) {
this.checkAndDownload(platform, "doc", event)
}
this.checkAndDownload = function (platform, type, event) {
if (event != null && typeof event.preventDefault == "function") {
event.preventDefault()
}
this.$get("/httpdns/apps/sdk/check")
.params({
platform: platform,
type: type
})
.success(function (resp) {
let data = (resp != null && resp.data != null) ? resp.data : {}
if (!data.exists) {
teaweb.warn(data.message || "当前暂无可下载文件")
return
}
if (typeof data.url == "string" && data.url.length > 0) {
this.downloadByBlob(data.url, platform, type)
return
}
teaweb.warn("下载地址生成失败,请稍后重试")
}.bind(this))
}
this.downloadByBlob = function (url, platform, type) {
let defaultFileName = "httpdns-sdk-" + platform + (type == "doc" ? ".md" : ".zip")
let xhr = new XMLHttpRequest()
xhr.open("GET", url, true)
xhr.responseType = "blob"
xhr.onload = function () {
if (xhr.status < 200 || xhr.status >= 300) {
teaweb.warn("下载失败HTTP " + xhr.status + "")
return
}
if (xhr.status == 204) {
teaweb.warn("下载被浏览器扩展或下载工具拦截,请暂时关闭相关扩展后重试")
return
}
let contentType = (xhr.getResponseHeader("Content-Type") || "").toLowerCase()
if (contentType.indexOf("application/json") >= 0) {
let reader = new FileReader()
reader.onload = function () {
try {
let json = JSON.parse(reader.result)
teaweb.warn((json && json.message) ? json.message : "下载失败,请稍后重试")
} catch (_e) {
teaweb.warn("下载失败,请稍后重试")
}
}
reader.readAsText(xhr.response)
return
}
let disposition = xhr.getResponseHeader("Content-Disposition") || ""
let fileName = xhr.getResponseHeader("X-SDK-Filename") || this.parseFileName(disposition) || defaultFileName
if (xhr.response == null || xhr.response.size <= 0) {
teaweb.warn("下载文件为空,请检查已上传 SDK 包是否完整")
return
}
this.saveBlob(xhr.response, fileName)
}.bind(this)
xhr.onerror = function () {
teaweb.warn("下载失败,请检查网络后重试")
}
xhr.send()
}
this.parseFileName = function (disposition) {
if (typeof disposition != "string" || disposition.length == 0) {
return ""
}
let utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i)
if (utf8Match != null && utf8Match.length > 1) {
try {
return decodeURIComponent(utf8Match[1])
} catch (_e) {
}
}
let plainMatch = disposition.match(/filename="?([^";]+)"?/i)
if (plainMatch != null && plainMatch.length > 1) {
return plainMatch[1]
}
return ""
}
this.saveBlob = function (blob, fileName) {
if (window.navigator != null && typeof window.navigator.msSaveOrOpenBlob == "function") {
window.navigator.msSaveOrOpenBlob(blob, fileName)
return
}
let objectURL = window.URL.createObjectURL(blob)
let a = document.createElement("a")
a.style.display = "none"
a.href = objectURL
a.download = fileName
document.body.appendChild(a)
a.click()
setTimeout(function () {
window.URL.revokeObjectURL(objectURL)
if (a.parentNode != null) {
a.parentNode.removeChild(a)
}
}, 30000)
}
})

View File

@@ -0,0 +1,3 @@
<first-menu>
<menu-item href="/httpdns/resolveLogs" code="index">访问日志</menu-item>
</first-menu>

View File

@@ -0,0 +1,94 @@
{$layout}
<div class="margin"></div>
<style>
.httpdns-log-summary {
color: #556070;
font-size: 12px;
line-height: 1.6;
white-space: nowrap;
}
.httpdns-log-summary code {
font-size: 12px;
}
</style>
<div>
<form method="get" action="/httpdns/resolveLogs" class="ui form small" autocomplete="off">
<div class="ui fields inline">
<div class="ui field">
<select class="ui dropdown" name="clusterId" v-model="clusterId">
<option value="">[HTTPDNS服务域名]</option>
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.displayName}}</option>
</select>
</div>
<div class="ui field">
<input type="text" name="appId" v-model="appId" placeholder="AppID" />
</div>
<div class="ui field">
<input type="text" name="domain" v-model="domain" placeholder="域名" />
</div>
<div class="ui field">
<select class="ui dropdown" name="status" v-model="status">
<option value="">[状态]</option>
<option value="success">解析成功</option>
<option value="failed">解析失败</option>
</select>
</div>
<div class="ui field">
<input type="text" name="keyword" v-model="keyword" placeholder="应用/域名/IP/结果IP" />
</div>
<div class="ui field">
<button type="submit" class="ui button small">查询</button>
</div>
<div class="ui field"
v-if="clusterId.toString().length > 0 || appId.length > 0 || domain.length > 0 || status.length > 0 || keyword.length > 0">
<a href="/httpdns/resolveLogs">[清除条件]</a>
</div>
</div>
</form>
</div>
<div class="margin"></div>
<not-found-box v-if="resolveLogs.length == 0">暂时还没有访问日志。</not-found-box>
<div v-if="resolveLogs.length > 0">
<div style="overflow-x:auto;">
<table class="ui table selectable celled">
<thead>
<tr>
<th>HTTPDNS服务域名</th>
<th>域名</th>
<th>类型</th>
<th>概要</th>
</tr>
</thead>
<tbody>
<tr v-for="log in resolveLogs">
<td>{{log.serviceDomain}}</td>
<td>{{log.domain}}</td>
<td>{{log.query}}</td>
<td>
<div class="httpdns-log-summary">
{{log.time}}
| {{log.appName}} (<code>{{log.appId}}</code>)
| <code>{{log.clientIp}}</code>
| {{log.os}}/{{log.sdkVersion}}
| {{log.query}} {{log.domain}} ->
<code v-if="log.ips && log.ips.length > 0">{{log.ips}}</code>
<span class="grey" v-else>[无记录]</span>
|
<span class="green" v-if="log.status == 'success'"><strong>成功</strong></span>
<span class="red" v-else><strong>失败</strong></span>
<span class="grey" v-if="log.errorCode != 'none'">({{log.errorCode}})</span>
| {{log.costMs}}ms
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -0,0 +1,4 @@
<!-- 左侧菜单由 Go Backend 自动生成注入,此处加首行子菜单使其符合标准平台样式 -->
<first-menu>
<menu-item href="/httpdns/sandbox" code="index">解析测试</menu-item>
</first-menu>

View File

@@ -0,0 +1,147 @@
{$layout}
<div class="ui margin"></div>
<div>
<div class="ui grid stackable">
<!-- Left: 解析测试 -->
<div class="six wide column">
<div class="ui segment">
<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" @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>HTTPDNS服务域名 *</label>
<select class="ui dropdown" name="clusterId" v-model="request.clusterId">
<option value="">[请选择HTTPDNS服务域名]</option>
<option v-for="cluster in currentClusters" :value="String(cluster.id)">{{cluster.displayName || cluster.name}}</option>
</select>
</div>
<div class="field">
<label>解析域名 *</label>
<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>
<input type="text" v-model="request.clientIp" placeholder="留空使用当前 IP" />
<p class="comment">用于测试 ECS 掩码与区域调度效果。</p>
</div>
<div class="field">
<label>解析类型</label>
<div class="ui buttons mini">
<button class="ui button" :class="{primary: request.qtype=='A'}"
@click.prevent="request.qtype='A'" type="button">A</button>
<button class="ui button" :class="{primary: request.qtype=='AAAA'}"
@click.prevent="request.qtype='AAAA'" type="button">AAAA</button>
</div>
</div>
<div class="ui divider"></div>
<button class="ui button primary" type="button" @click.prevent="sendTestRequest"
:class="{loading: isRequesting, disabled: isRequesting}">
<i class="icon search"></i> 在线解析
</button>
<button class="ui button basic" type="button" @click.prevent="resetForm">重置</button>
</div>
</div>
</div>
<!-- Right: 解析结果 -->
<div class="ten wide column">
<!-- Empty state -->
<div v-show="!response.hasResult && !isRequesting" class="ui segment placeholder">
<p class="grey" style="text-align:center;">请在左侧配置参数后点击「在线解析」。</p>
</div>
<!-- Loading -->
<div v-show="isRequesting" class="ui segment" style="min-height:200px;">
<div class="ui active dimmer">
<div class="ui text loader">解析中...</div>
</div>
</div>
<!-- Result -->
<div v-show="response.hasResult && !isRequesting">
<!-- Status banner -->
<div class="ui message small" :class="{positive: response.code === 0, negative: response.code !== 0}">
<div class="header">
<span v-if="response.code === 0"><i class="icon check circle"></i> 解析成功</span>
<span v-else><i class="icon times circle"></i> 解析失败 ({{response.code}})</span>
</div>
<p v-if="response.code !== 0">{{response.message}}</p>
<p class="grey small" style="margin-top:.3em;">Request ID: <code>{{response.requestId}}</code></p>
</div>
<!-- Result details -->
<div v-if="response.code === 0 && response.data" style="margin-top: 1em;">
<h5 class="ui header">解析结果</h5>
<div class="ui segment" style="margin-top:.4em;">
<div style="font-weight: 600; margin-bottom: .5em;">请求URL</div>
<div style="word-break: break-all;">
<code>{{response.data.request_url || '-'}}</code>
</div>
</div>
<h5 class="ui header" style="margin-top:1.2em;">解析结果详情</h5>
<div class="ui three column stackable grid" style="margin-top:0;">
<div class="column">
<div class="ui segment">
<div class="grey">客户端 IP</div>
<div style="margin-top:.4em;">
<code>{{response.data.client_ip || request.clientIp || '-'}}</code></div>
</div>
</div>
<div class="column">
<div class="ui segment">
<div class="grey">地区</div>
<div style="margin-top:.4em;">{{response.data.client_region || '-'}}</div>
</div>
</div>
<div class="column">
<div class="ui segment">
<div class="grey">线路</div>
<div style="margin-top:.4em;">{{response.data.line_name || '-'}}</div>
</div>
</div>
</div>
<!-- IP results table -->
<h5 class="ui header" style="margin-top:1.2em;">解析记录</h5>
<table class="ui table celled compact">
<thead>
<tr>
<th>解析域名</th>
<th>解析类型</th>
<th>IP地址</th>
<th>TTL</th>
<th>地区</th>
<th>线路</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in response.resultRows">
<td v-if="idx === 0" :rowspan="response.resultRows.length">{{row.domain ||
request.domain}}</td>
<td>{{row.type || request.qtype}}</td>
<td><code>{{row.ip}}</code></td>
<td>{{row.ttl}}s</td>
<td>{{row.region || '-'}}</td>
<td>{{row.line || '-'}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,161 @@
Tea.context(function () {
this.newRequest = function () {
return {
appId: "",
clusterId: "",
domain: "",
clientIp: "",
qtype: "A"
}
}
this.request = this.newRequest()
this.response = {
hasResult: false,
code: -1,
message: "",
data: null,
requestId: "",
resultRows: []
}
this.isRequesting = false
this.currentDomains = []
this.currentClusters = []
if (typeof this.apps === "undefined" || this.apps == null) {
this.apps = []
}
if (typeof this.clusters === "undefined" || this.clusters == null) {
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.currentClusters = []
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 = ""
}
let primaryClusterId = (typeof selectedApp.primaryClusterId !== "undefined" && selectedApp.primaryClusterId !== null) ? Number(selectedApp.primaryClusterId) : 0
let backupClusterId = (typeof selectedApp.backupClusterId !== "undefined" && selectedApp.backupClusterId !== null) ? Number(selectedApp.backupClusterId) : 0
let allowed = []
for (let i = 0; i < this.clusters.length; i++) {
let cluster = this.clusters[i]
let clusterId = Number(cluster.id)
if (clusterId <= 0) {
continue
}
if (clusterId === primaryClusterId || clusterId === backupClusterId) {
allowed.push(cluster)
}
}
this.currentClusters = allowed
if (allowed.length > 0) {
if (!allowed.some((c) => String(c.id) === String(this.request.clusterId))) {
this.request.clusterId = String(allowed[0].id)
}
} else {
this.request.clusterId = ""
}
}
this.normalizeResultRows = function (data) {
if (typeof data === "undefined" || data == null) {
return []
}
if (Array.isArray(data.records) && data.records.length > 0) {
return data.records
}
let rows = []
let ips = Array.isArray(data.ips) ? data.ips : []
let domain = this.request.domain
let qtype = this.request.qtype
let ttl = data.ttl || 0
let region = data.client_region || "-"
let line = data.line_name || "-"
ips.forEach(function (ip) {
rows.push({
domain: domain,
type: qtype,
ip: ip,
ttl: ttl,
region: region,
line: line
})
})
return rows
}
this.sendTestRequest = function () {
if (this.request.appId.length === 0) {
teaweb.warn("请选择目标应用")
return
}
if (this.request.clusterId.length === 0) {
teaweb.warn("当前应用未绑定可用的 HTTPDNS 服务域名,请先在应用设置中配置主/备集群")
return
}
if (this.request.domain.length === 0) {
teaweb.warn("请选择要解析的域名")
return
}
this.isRequesting = true
this.response.hasResult = false
let payload = Object.assign({}, this.request)
this.$post("/httpdns/sandbox/test")
.params(payload)
.success(function (resp) {
this.response = resp.data.result
this.response.hasResult = true
this.response.resultRows = this.normalizeResultRows(this.response.data)
})
.done(function () {
this.isRequesting = false
})
}
this.resetForm = function () {
this.request = this.newRequest()
this.currentDomains = []
this.currentClusters = []
this.response = {
hasResult: false,
code: -1,
message: "",
data: null,
requestId: "",
resultRows: []
}
}
})