DNS服务支持密钥管理/“域名服务”改为“自建DNS”

This commit is contained in:
GoEdgeLab
2021-07-25 09:44:29 +08:00
parent 12023f6792
commit e21e76e872
22 changed files with 621 additions and 29 deletions

View File

@@ -15,7 +15,7 @@ func main() {
app := apps.NewAppCmd().
Version(teaconst.Version).
Product(teaconst.ProductName).
Usage(teaconst.ProcessName+" [-v|start|stop|restart|service|daemon|reset|recover]").
Usage(teaconst.ProcessName+" [-v|start|stop|restart|service|daemon|reset|recover|demo]").
Option("-h", "show this help").
Option("-v", "show version").
Option("start", "start the service").

View File

@@ -182,7 +182,7 @@ func AllModuleMaps() []maps.Map {
}
if teaconst.IsPlus {
m = append(m, maps.Map{
"name": "域名服务",
"name": "自建DNS",
"code": AdminModuleCodeNS,
"url": "/ns",
})

View File

@@ -388,6 +388,10 @@ func (this *RPCClient) NSRecordRPC() pb.NSRecordServiceClient {
return pb.NewNSRecordServiceClient(this.pickConn())
}
func (this *RPCClient) NSKeyRPC() pb.NSKeyServiceClient {
return pb.NewNSKeyServiceClient(this.pickConn())
}
func (this *RPCClient) NSRouteRPC() pb.NSRouteServiceClient {
return pb.NewNSRouteServiceClient(this.pickConn())
}

View File

@@ -4,6 +4,7 @@ package domains
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ns/domains/domainutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
@@ -19,6 +20,14 @@ func (this *DomainAction) Init() {
func (this *DomainAction) RunGet(params struct {
DomainId int64
}) {
err := domainutils.InitDomain(this.Parent(), params.DomainId)
if err != nil {
this.ErrorPage(err)
return
}
var countRecords = this.Data.GetMap("domain").GetInt64("countRecords")
var countKeys = this.Data.GetMap("domain").GetInt64("countKeys")
// 域名信息
domainResp, err := this.RPC().NSDomainRPC().FindEnabledNSDomain(this.AdminContext(), &pb.FindEnabledNSDomainRequest{NsDomainId: params.DomainId})
if err != nil {
@@ -55,6 +64,8 @@ func (this *DomainAction) RunGet(params struct {
"isOn": domain.IsOn,
"cluster": clusterMap,
"user": userMap,
"countRecords": countRecords,
"countKeys": countKeys,
}
this.Show()

View File

@@ -0,0 +1,55 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package domainutils
import (
"errors"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
)
// InitDomain 初始化域名信息
func InitDomain(parent *actionutils.ParentAction, domainId int64) error {
rpcClient, err := rpc.SharedRPC()
if err != nil {
return err
}
domainResp, err := rpcClient.NSDomainRPC().FindEnabledNSDomain(parent.AdminContext(), &pb.FindEnabledNSDomainRequest{NsDomainId: domainId})
if err != nil {
return err
}
var domain = domainResp.NsDomain
if domain == nil {
return errors.New("InitDomain: can not find domain with id '" + types.String(domainId) + "'")
}
// 记录数量
countRecordsResp, err := rpcClient.NSRecordRPC().CountAllEnabledNSRecords(parent.AdminContext(), &pb.CountAllEnabledNSRecordsRequest{
NsDomainId: domainId,
})
if err != nil {
return err
}
var countRecords = countRecordsResp.Count
// Key数量
countKeysResp, err := rpcClient.NSKeyRPC().CountAllEnabledNSKeys(parent.AdminContext(), &pb.CountAllEnabledNSKeysRequest{
NsDomainId: domainId,
})
if err != nil {
return err
}
var countKeys = countKeysResp.Count
parent.Data["domain"] = maps.Map{
"id": domain.Id,
"name": domain.Name,
"countRecords": countRecords,
"countKeys": countKeys,
}
return nil
}

View File

@@ -0,0 +1,86 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package keys
import (
"encoding/base64"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/dnsconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type CreatePopupAction struct {
actionutils.ParentAction
}
func (this *CreatePopupAction) Init() {
this.Nav("", "", "")
}
func (this *CreatePopupAction) RunGet(params struct {
DomainId int64
}) {
this.Data["domainId"] = params.DomainId
// 所有算法
var algorithmMaps = []maps.Map{}
for _, algo := range dnsconfigs.FindAllKeyAlgorithmTypes() {
algorithmMaps = append(algorithmMaps, maps.Map{
"name": algo.Name,
"code": algo.Code,
})
}
this.Data["algorithms"] = algorithmMaps
this.Show()
}
func (this *CreatePopupAction) RunPost(params struct {
DomainId int64
Name string
Algo string
Secret string
SecretType string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
var keyId int64 = 0
defer func() {
this.CreateLogInfo("创建DNS密钥 %d", keyId)
}()
params.Must.
Field("name", params.Name).
Require("请输入密钥名称").
Field("algo", params.Algo).
Require("请选择算法").
Field("secret", params.Secret).
Require("请输入密码")
// 校验密码
if params.SecretType == dnsconfigs.NSKeySecretTypeBase64 {
_, err := base64.StdEncoding.DecodeString(params.Secret)
if err != nil {
this.FailField("secret", "请输入BASE64格式的密码或者选择明文")
}
}
createResp, err := this.RPC().NSKeyRPC().CreateNSKey(this.AdminContext(), &pb.CreateNSKeyRequest{
NsDomainId: params.DomainId,
NsZoneId: 0,
Name: params.Name,
Algo: params.Algo,
Secret: params.Secret,
SecretType: params.SecretType,
})
if err != nil {
this.ErrorPage(err)
return
}
keyId = createResp.NsKeyId
this.Success()
}

View File

@@ -0,0 +1,26 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package keys
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type DeleteAction struct {
actionutils.ParentAction
}
func (this *DeleteAction) RunPost(params struct {
KeyId int64
}) {
defer this.CreateLogInfo("删除DNS密钥 %d", params.KeyId)
_, err := this.RPC().NSKeyRPC().DeleteNSKey(this.AdminContext(), &pb.DeleteNSKeyRequest{NsKeyId: params.KeyId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,29 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package keys
import (
"encoding/base64"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/dnsconfigs"
"github.com/iwind/TeaGo/rands"
)
type GenerateSecretAction struct {
actionutils.ParentAction
}
func (this *GenerateSecretAction) RunPost(params struct {
SecretType string
}) {
switch params.SecretType {
case dnsconfigs.NSKeySecretTypeClear:
this.Data["secret"] = rands.HexString(128)
case dnsconfigs.NSKeySecretTypeBase64:
this.Data["secret"] = base64.StdEncoding.EncodeToString([]byte(rands.HexString(128)))
default:
this.Data["secret"] = rands.HexString(128)
}
this.Success()
}

View File

@@ -0,0 +1,68 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package keys
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ns/domains/domainutils"
"github.com/TeaOSLab/EdgeCommon/pkg/dnsconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "key")
}
func (this *IndexAction) RunGet(params struct {
DomainId int64
}) {
// 初始化域名信息
err := domainutils.InitDomain(this.Parent(), params.DomainId)
if err != nil {
this.ErrorPage(err)
return
}
// 数量
countResp, err := this.RPC().NSKeyRPC().CountAllEnabledNSKeys(this.AdminContext(), &pb.CountAllEnabledNSKeysRequest{
NsDomainId: params.DomainId,
NsZoneId: 0,
})
if err != nil {
this.ErrorPage(err)
return
}
var page = this.NewPage(countResp.Count)
this.Data["page"] = page.AsHTML()
// 列表
keysResp, err := this.RPC().NSKeyRPC().ListEnabledNSKeys(this.AdminContext(), &pb.ListEnabledNSKeysRequest{
NsDomainId: params.DomainId,
NsZoneId: 0,
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
return
}
var keyMaps = []maps.Map{}
for _, key := range keysResp.NsKeys {
keyMaps = append(keyMaps, maps.Map{
"id": key.Id,
"name": key.Name,
"secret": key.Secret,
"secretTypeName": dnsconfigs.FindKeySecretTypeName(key.SecretType),
"algoName": dnsconfigs.FindKeyAlgorithmTypeName(key.Algo),
"isOn": key.IsOn,
})
}
this.Data["keys"] = keyMaps
this.Show()
}

View File

@@ -0,0 +1,100 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package keys
import (
"encoding/base64"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/dnsconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type UpdatePopupAction struct {
actionutils.ParentAction
}
func (this *UpdatePopupAction) Init() {
this.Nav("", "", "")
}
func (this *UpdatePopupAction) RunGet(params struct {
KeyId int64
}) {
keyResp, err := this.RPC().NSKeyRPC().FindEnabledNSKey(this.AdminContext(), &pb.FindEnabledNSKeyRequest{NsKeyId: params.KeyId})
if err != nil {
this.ErrorPage(err)
return
}
var key = keyResp.NsKey
if key == nil {
return
}
this.Data["key"] = maps.Map{
"id": key.Id,
"name": key.Name,
"algo": key.Algo,
"secret": key.Secret,
"secretType": key.SecretType,
"isOn": key.IsOn,
}
// 所有算法
var algorithmMaps = []maps.Map{}
for _, algo := range dnsconfigs.FindAllKeyAlgorithmTypes() {
algorithmMaps = append(algorithmMaps, maps.Map{
"name": algo.Name,
"code": algo.Code,
})
}
this.Data["algorithms"] = algorithmMaps
this.Show()
}
func (this *UpdatePopupAction) RunPost(params struct {
KeyId int64
Name string
Algo string
Secret string
SecretType string
IsOn bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
this.CreateLogInfo("修改DNS密钥 %d", params.KeyId)
params.Must.
Field("name", params.Name).
Require("请输入密钥名称").
Field("algo", params.Algo).
Require("请选择算法").
Field("secret", params.Secret).
Require("请输入密码")
// 校验密码
if params.SecretType == dnsconfigs.NSKeySecretTypeBase64 {
_, err := base64.StdEncoding.DecodeString(params.Secret)
if err != nil {
this.FailField("secret", "请输入BASE64格式的密码或者选择明文")
}
}
_, err := this.RPC().NSKeyRPC().UpdateNSKey(this.AdminContext(), &pb.UpdateNSKeyRequest{
NsKeyId: params.KeyId,
Name: params.Name,
Algo: params.Algo,
Secret: params.Secret,
SecretType: params.SecretType,
IsOn: params.IsOn,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -4,6 +4,7 @@ package records
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ns/domains/domainutils"
"github.com/TeaOSLab/EdgeCommon/pkg/dnsconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
@@ -23,25 +24,16 @@ func (this *IndexAction) RunGet(params struct {
Keyword string
RouteId int64
}) {
this.Data["type"] = params.Type
this.Data["keyword"] = params.Keyword
this.Data["routeId"] = params.RouteId
// 域名信息
domainResp, err := this.RPC().NSDomainRPC().FindEnabledNSDomain(this.AdminContext(), &pb.FindEnabledNSDomainRequest{NsDomainId: params.DomainId})
// 初始化域名信息
err := domainutils.InitDomain(this.Parent(), params.DomainId)
if err != nil {
this.ErrorPage(err)
return
}
domain := domainResp.NsDomain
if domain == nil {
this.NotFound("nsDomain", params.DomainId)
return
}
this.Data["domain"] = maps.Map{
"id": domain.Id,
"name": domain.Name,
}
this.Data["type"] = params.Type
this.Data["keyword"] = params.Keyword
this.Data["routeId"] = params.RouteId
// 记录
countResp, err := this.RPC().NSRecordRPC().CountAllEnabledNSRecords(this.AdminContext(), &pb.CountAllEnabledNSRecordsRequest{

View File

@@ -4,6 +4,7 @@ package domains
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ns/domains/domainutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
@@ -20,6 +21,15 @@ func (this *UpdateAction) Init() {
func (this *UpdateAction) RunGet(params struct {
DomainId int64
}) {
// 初始化域名信息
err := domainutils.InitDomain(this.Parent(), params.DomainId)
if err != nil {
this.ErrorPage(err)
return
}
var countRecords = this.Data.GetMap("domain").GetInt64("countRecords")
var countKeys = this.Data.GetMap("domain").GetInt64("countKeys")
// 域名信息
domainResp, err := this.RPC().NSDomainRPC().FindEnabledNSDomain(this.AdminContext(), &pb.FindEnabledNSDomainRequest{NsDomainId: params.DomainId})
if err != nil {
@@ -49,6 +59,8 @@ func (this *UpdateAction) RunGet(params struct {
"isOn": domain.IsOn,
"clusterId": clusterId,
"userId": userId,
"countRecords": countRecords,
"countKeys": countKeys,
}
this.Show()

View File

@@ -3,6 +3,7 @@ package ns
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ns/domains"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ns/domains/keys"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ns/domains/records"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
@@ -23,6 +24,14 @@ func init() {
Get("/domain", new(domains.DomainAction)).
GetPost("/update", new(domains.UpdateAction)).
// 域名密钥
Prefix("/ns/domains/keys").
Get("", new(keys.IndexAction)).
GetPost("/createPopup", new(keys.CreatePopupAction)).
GetPost("/updatePopup", new(keys.UpdatePopupAction)).
Post("/delete", new(keys.DeleteAction)).
Post("/generateSecret", new(keys.GenerateSecretAction)).
// 记录相关
Prefix("/ns/domains/records").
Get("", new(records.IndexAction)).

View File

@@ -257,7 +257,7 @@ func (this *userMustAuth) modules(adminId int64) []maps.Map {
{
"code": "ns",
"module": configloaders.AdminModuleCodeNS,
"name": "域名服务",
"name": "自建DNS",
"subtitle": "域名列表",
"icon": "cubes",
"isOn": teaconst.IsPlus,

View File

@@ -215,6 +215,10 @@ window.teaweb = {
}
},
popup: function (url, options) {
if (url != null && url.length > 0 && url.substring(0, 1) == '.') {
url = Tea.url(url)
}
if (options == null) {
options = {};
}

View File

@@ -2,6 +2,7 @@
<menu-item href="/ns">所有域名</menu-item>
<span class="item disabled">|</span>
<menu-item :href="'/ns/domains/domain?domainId=' + domain.id" code="index">详情<span class="grey small">{{domain.name}}</span></menu-item>
<menu-item :href="'/ns/domains/records?domainId=' + domain.id" code="record">记录</menu-item>
<menu-item :href="'/ns/domains/records?domainId=' + domain.id" code="record">记录({{domain.countRecords}})</menu-item>
<menu-item :href="'/ns/domains/keys?domainId=' + domain.id" code="key">密钥({{domain.countKeys}})</menu-item>
<menu-item :href="'/ns/domains/update?domainId=' + domain.id" code="update">修改</menu-item>
</first-menu>

View File

@@ -0,0 +1,40 @@
{$layout "layout_popup"}
<h3>创建密钥</h3>
<form class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="domainId" :value="domainId"/>
<table class="ui table definition selectable">
<tr>
<td class="title">密钥名称 *</td>
<td>
<input type="text" name="name" ref="focus"/>
</td>
</tr>
<tr>
<td>算法 *</td>
<td>
<select class="ui dropdown auto-width" name="algo">
<option value="">[选择算法]</option>
<option v-for="algo in algorithms" :value="algo.code">{{algo.name}}</option>
</select>
</td>
</tr>
<tr>
<td>密码 *</td>
<td>
<div style="margin-bottom: 1em">
<radio name="secretType" value="clear" :v-value="'clear'" v-model="secretType">明文密码</radio>
&nbsp; &nbsp;
<radio name="secretType" value="base64" :v-value="'base64'" v-model="secretType">Base64密码</radio>
</div>
<textarea name="secret" maxlength="256" v-model="secret" rows="3"></textarea>
<p class="comment" v-if="secretType == 'clear'">客户端需要将明文转换为BASE64使用。</p>
<div style="margin-top: 1em">
<a href="" @click.prevent="generateSecret">[随机生成]</a>
</div>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,20 @@
Tea.context(function () {
this.secretType = "clear"
this.secret = ""
this.$delay(function () {
this.$watch("secretType", function () {
this.secret = ""
})
})
this.generateSecret = function () {
this.$post(".generateSecret")
.params({
secretType: this.secretType
})
.success(function (resp) {
this.secret = resp.data.secret
})
}
})

View File

@@ -0,0 +1,38 @@
{$layout}
{$template "../domain_menu"}
<second-menu>
<menu-item @click.prevent="createKey">[创建密钥]</menu-item>
</second-menu>
<tip-message-box>这里的密钥可以用于TSIG通讯校验。</tip-message-box>
<p class="comment" v-if="keys.length == 0">暂时还没有密钥。</p>
<table class="ui table celled selectable" v-if="keys.length > 0">
<thead>
<tr>
<th class="two wide">密钥名称</th>
<th class="two wide">算法</th>
<th>密码</th>
<th class="two wide">密码类型</th>
<th class="two wide">状态</th>
<th class="two op">操作</th>
</tr>
</thead>
<tr v-for="key in keys">
<td>{{key.name}}</td>
<td>{{key.algoName}}</td>
<td>{{key.secret}}</td>
<td>{{key.secretTypeName}}</td>
<td>
<label-on :v-is-on="key.isOn"></label-on>
</td>
<td>
<a href="" @click.prevent="updateKey(key.id)">修改</a> &nbsp;
<a href="" @click.prevent="deleteKey(key.id)">删除</a>
</td>
</tr>
</table>
<div class="page" v-html="page"></div>

View File

@@ -0,0 +1,31 @@
Tea.context(function () {
this.createKey = function () {
teaweb.popup(Tea.url(".createPopup?domainId=" + this.domain.id), {
height: "24em",
callback: function () {
teaweb.successRefresh("保存成功")
}
})
}
this.updateKey = function (keyId) {
teaweb.popup(Tea.url(".updatePopup?keyId=" + keyId), {
height: "26em",
callback: function () {
teaweb.successRefresh("保存成功")
}
})
}
this.deleteKey = function (keyId) {
teaweb.confirm("确定要删除这个密钥吗?", function () {
this.$post(".delete")
.params({
keyId: keyId
})
.success(function () {
teaweb.successRefresh("删除成功")
})
})
}
})

View File

@@ -0,0 +1,46 @@
{$layout "layout_popup"}
<h3>修改密钥</h3>
<form class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="keyId" :value="key.id"/>
<table class="ui table definition selectable">
<tr>
<td class="title">密钥名称 *</td>
<td>
<input type="text" name="name" ref="focus" v-model="key.name"/>
</td>
</tr>
<tr>
<td>算法 *</td>
<td>
<select class="ui dropdown auto-width" name="algo" v-model="key.algo">
<option value="">[选择算法]</option>
<option v-for="algo in algorithms" :value="algo.code">{{algo.name}}</option>
</select>
</td>
</tr>
<tr>
<td>密码 *</td>
<td>
<div style="margin-bottom: 1em">
<radio name="secretType" value="clear" :v-value="'clear'" v-model="secretType">明文密码</radio>
&nbsp; &nbsp;
<radio name="secretType" value="base64" :v-value="'base64'" v-model="secretType">Base64密码</radio>
</div>
<textarea name="secret" maxlength="256" v-model="secret" rows="3"></textarea>
<p class="comment" v-if="secretType == 'clear'">客户端需要将明文转换为BASE64使用。</p>
<div style="margin-top: 1em">
<a href="" @click.prevent="generateSecret">[随机生成]</a>
</div>
</td>
</tr>
<tr>
<td>是否启用</td>
<td>
<checkbox name="isOn" v-model="key.isOn"></checkbox>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,20 @@
Tea.context(function () {
this.secretType = this.key.secretType
this.secret = this.key.secret
this.$delay(function () {
this.$watch("secretType", function () {
this.secret = ""
})
})
this.generateSecret = function () {
this.$post(".generateSecret")
.params({
secretType: this.secretType
})
.success(function (resp) {
this.secret = resp.data.secret
})
}
})