[HTTPS]可以直接点击按钮申请免费证书

This commit is contained in:
刘祥超
2020-12-03 21:07:08 +08:00
parent 6f3f3ec369
commit df0cbe1f5c
10 changed files with 280 additions and 10 deletions

View File

@@ -120,8 +120,8 @@ func (this *IndexAction) RunGet(params struct {
// 域名列表 // 域名列表
serverNames := []*serverconfigs.ServerNameConfig{} serverNames := []*serverconfigs.ServerNameConfig{}
if len(server.ServerNamesJON) > 0 { if len(server.ServerNamesJSON) > 0 {
err = json.Unmarshal(server.ServerNamesJON, &serverNames) err = json.Unmarshal(server.ServerNamesJSON, &serverNames)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return

View File

@@ -11,7 +11,6 @@ func init() {
server. server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeServer)). Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeServer)).
Helper(NewHelper()). Helper(NewHelper()).
Data("teaModule", "server").
Prefix("/servers"). Prefix("/servers").
Get("", new(IndexAction)). Get("", new(IndexAction)).
GetPost("/create", new(CreateAction)). GetPost("/create", new(CreateAction)).

View File

@@ -14,6 +14,7 @@ func init() {
Helper(serverutils.NewServerHelper()). Helper(serverutils.NewServerHelper()).
Prefix("/servers/server/settings/https"). Prefix("/servers/server/settings/https").
GetPost("", new(IndexAction)). GetPost("", new(IndexAction)).
GetPost("/requestCertPopup", new(RequestCertPopupAction)).
EndAll() EndAll()
}) })
} }

View File

@@ -0,0 +1,176 @@
package https
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dns/domains/domainutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps"
"strings"
)
type RequestCertPopupAction struct {
actionutils.ParentAction
}
func (this *RequestCertPopupAction) Init() {
this.Nav("", "", "")
}
func (this *RequestCertPopupAction) RunGet(params struct {
ServerId int64
ExcludeServerNames string
}) {
serverNamesResp, err := this.RPC().ServerRPC().FindServerNames(this.AdminContext(), &pb.FindServerNamesRequest{ServerId: params.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
serverNameConfigs := []*serverconfigs.ServerNameConfig{}
err = json.Unmarshal(serverNamesResp.ServerNamesJSON, &serverNameConfigs)
if err != nil {
this.ErrorPage(err)
return
}
excludeServerNames := []string{}
if len(params.ExcludeServerNames) > 0 {
excludeServerNames = strings.Split(params.ExcludeServerNames, ",")
}
serverNames := []string{}
for _, c := range serverNameConfigs {
if len(c.SubNames) == 0 {
if domainutils.ValidateDomainFormat(c.Name) && !lists.ContainsString(excludeServerNames, c.Name) {
serverNames = append(serverNames, c.Name)
}
} else {
for _, subName := range c.SubNames {
if domainutils.ValidateDomainFormat(subName) && !lists.ContainsString(excludeServerNames, subName) {
serverNames = append(serverNames, subName)
}
}
}
}
this.Data["serverNames"] = serverNames
// 用户
acmeUsersResp, err := this.RPC().ACMEUserRPC().FindAllACMEUsers(this.AdminContext(), &pb.FindAllACMEUsersRequest{
AdminId: this.AdminId(),
UserId: 0,
})
userMaps := []maps.Map{}
for _, user := range acmeUsersResp.AcmeUsers {
description := user.Description
if len(description) > 0 {
description = "" + description + ""
}
userMaps = append(userMaps, maps.Map{
"id": user.Id,
"description": description,
"email": user.Email,
})
}
this.Data["users"] = userMaps
this.Show()
}
func (this *RequestCertPopupAction) RunPost(params struct {
ServerNames []string
UserId int64
UserEmail string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
// 检查域名
if len(params.ServerNames) == 0 {
this.Fail("必须包含至少一个或多个域名")
}
// 注册用户
var acmeUserId int64
if params.UserId > 0 {
// TODO 检查当前管理员是否可以使用此用户
acmeUserId = params.UserId
} else if len(params.UserEmail) > 0 {
params.Must.
Field("userEmail", params.UserEmail).
Email("Email格式错误")
createUserResp, err := this.RPC().ACMEUserRPC().CreateACMEUser(this.AdminContext(), &pb.CreateACMEUserRequest{
Email: params.UserEmail,
Description: "",
})
if err != nil {
this.ErrorPage(err)
return
}
defer this.CreateLogInfo("创建ACME用户", createUserResp.AcmeUserId)
acmeUserId = createUserResp.AcmeUserId
this.Data["acmeUser"] = maps.Map{
"id": acmeUserId,
"email": params.UserEmail,
}
} else {
this.Fail("请选择或者填写用户")
}
createTaskResp, err := this.RPC().ACMETaskRPC().CreateACMETask(this.AdminContext(), &pb.CreateACMETaskRequest{
AcmeUserId: acmeUserId,
DnsProviderId: 0,
DnsDomain: "",
Domains: params.ServerNames,
AutoRenew: true,
AuthType: "http",
})
if err != nil {
this.ErrorPage(err)
return
}
taskId := createTaskResp.AcmeTaskId
defer this.CreateLogInfo("自动申请证书,任务 %d", taskId)
runResp, err := this.RPC().ACMETaskRPC().RunACMETask(this.AdminContext(), &pb.RunACMETaskRequest{AcmeTaskId: taskId})
if err != nil {
this.ErrorPage(err)
return
}
if runResp.IsOk {
certId := runResp.SslCertId
configResp, err := this.RPC().SSLCertRPC().FindEnabledSSLCertConfig(this.AdminContext(), &pb.FindEnabledSSLCertConfigRequest{CertId: certId})
if err != nil {
this.ErrorPage(err)
return
}
certConfig := &sslconfigs.SSLCertConfig{}
err = json.Unmarshal(configResp.CertJSON, certConfig)
if err != nil {
this.ErrorPage(err)
return
}
certConfig.CertData = nil // 去掉不必要的数据
certConfig.KeyData = nil // 去掉不必要的数据
this.Data["cert"] = certConfig
this.Data["certRef"] = &sslconfigs.SSLCertRef{
IsOn: true,
CertId: certId,
}
this.Success()
} else {
this.Fail(runResp.Error)
}
}

View File

@@ -28,8 +28,8 @@ func (this *IndexAction) RunGet(params struct {
} }
serverNamesConfig := []*serverconfigs.ServerNameConfig{} serverNamesConfig := []*serverconfigs.ServerNameConfig{}
if len(server.ServerNamesJON) > 0 { if len(server.ServerNamesJSON) > 0 {
err := json.Unmarshal(server.ServerNamesJON, &serverNamesConfig) err := json.Unmarshal(server.ServerNamesJSON, &serverNamesConfig)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return return

View File

@@ -1,10 +1,16 @@
Vue.component("ssl-certs-box", { Vue.component("ssl-certs-box", {
props: ["v-certs", "v-protocol", "v-view-size", "v-single-mode"], props: [
"v-certs", // 证书列表
"v-protocol", // 协议https|tls
"v-view-size", // 弹窗尺寸
"v-single-mode" // 单证书模式
],
data: function () { data: function () {
let certs = this.vCerts let certs = this.vCerts
if (certs == null) { if (certs == null) {
certs = [] certs = []
} }
return { return {
certs: certs certs: certs
} }
@@ -82,7 +88,7 @@ Vue.component("ssl-certs-box", {
</div> </div>
<div v-if="buttonsVisible()"> <div v-if="buttonsVisible()">
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button> &nbsp; <button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button> <button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button> &nbsp;
</div> </div>
</div>` </div>`
}) })

View File

@@ -1,5 +1,5 @@
Vue.component("ssl-config-box", { Vue.component("ssl-config-box", {
props: ["v-ssl-policy", "v-protocol"], props: ["v-ssl-policy", "v-protocol", "v-server-id"],
created: function () { created: function () {
let that = this let that = this
setTimeout(function () { setTimeout(function () {
@@ -119,6 +119,25 @@ Vue.component("ssl-config-box", {
}) })
}, },
// 申请证书
requestCert: function () {
// 已经在证书中的域名
let excludeServerNames = []
if (this.policy != null && this.policy.certs.length > 0) {
this.policy.certs.forEach(function (cert) {
excludeServerNames.$pushAll(cert.dnsNames)
})
}
let that = this
teaweb.popup("/servers/server/settings/https/requestCertPopup?serverId=" + this.vServerId + "&excludeServerNames=" + excludeServerNames.join(","), {
callback: function () {
that.policy.certRefs.push(resp.data.certRef)
that.policy.certs.push(resp.data.cert)
}
})
},
// 更多选项 // 更多选项
changeOptionsVisible: function () { changeOptionsVisible: function () {
this.moreOptionsVisible = !this.moreOptionsVisible this.moreOptionsVisible = !this.moreOptionsVisible
@@ -338,7 +357,8 @@ Vue.component("ssl-config-box", {
<div class="ui divider"></div> <div class="ui divider"></div>
</div> </div>
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button> &nbsp; <button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button> <button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="requestCert()" v-if="vServerId != null && vServerId > 0">申请免费证书</button>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@@ -32,7 +32,7 @@
</table> </table>
<!-- SSL配置 --> <!-- SSL配置 -->
<ssl-config-box :v-ssl-policy="httpsConfig.sslPolicy" :v-protocol="'https'" v-show="httpsConfig.isOn"></ssl-config-box> <ssl-config-box :v-ssl-policy="httpsConfig.sslPolicy" :v-protocol="'https'" v-show="httpsConfig.isOn" :v-server-id="serverId"></ssl-config-box>
<submit-btn></submit-btn> <submit-btn></submit-btn>
</form> </form>

View File

@@ -0,0 +1,43 @@
{$layout "layout_popup"}
<h3>申请免费证书</h3>
<form method="post" class="ui form" data-tea-success="success" data-tea-action="$" data-tea-timeout="300" data-tea-before="beforeSubmit" data-tea-fail="fail">
<csrf-token></csrf-token>
<table class="ui table definition selectable">
<tr>
<td class="title">证书包含的域名 *</td>
<td>
<span v-if="serverNames.length == 0" class="disabled">还没有设置域名,暂时不能申请。</span>
<div v-if="serverNames.length > 0">
<div v-for="(serverName, index) in serverNames" class="ui tiny basic label">
<input type="hidden" name="serverNames" :value="serverName"/>
{{serverName}}
<a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove"></i></a>
</div>
</div>
</td>
</tr>
<tr>
<td>证书用户 *</td>
<td>
<div v-if="users.length > 0">
<select class="ui dropdown auto-width" name="userId" v-model="userId">
<option value="0">[请选择]</option>
<option v-for="user in users" :value="user.id">{{user.email}}{{user.description}}</option>>
</select>
<p class="comment">用来申请证书的用户。</p>
</div>
<div v-if="users.length == 0">
<input type="text" name="userEmail" maxlength="100" placeholder="用户E-mail" ref="focus"/>
<p class="comment">用来申请证书的用户邮箱,可以任意填写,只要格式正确即可。</p>
</div>
</td>
</tr>
</table>
<submit-btn v-if="!isRequesting">提交</submit-btn>
<button class="ui button" type="button" v-if="isRequesting">处理中...</button>
</form>

View File

@@ -0,0 +1,25 @@
Tea.context(function () {
this.isRequesting = false
this.userId = 0
this.remove = function (index) {
this.serverNames.$remove(index)
}
this.beforeSubmit = function () {
this.isRequesting = true
}
this.fail = function (resp) {
this.isRequesting = false
teaweb.warn(resp.message)
if (resp.data.acmeUser != null) {
this.users.push({
id: resp.data.acmeUser.id,
email: resp.data.acmeUser.email,
description: ""
})
this.userId = resp.data.acmeUser.id
}
}
})