mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-30 17:05:19 +08:00
feat: 部署证书时支持开启 HTTPS 及一键签发证书泛域名 DNS 选择
This commit is contained in:
@@ -45,5 +45,5 @@ type CertRepo interface {
|
||||
ObtainSelfSigned(id uint) error
|
||||
Renew(id uint) (*acme.Certificate, error)
|
||||
RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error)
|
||||
Deploy(ID, WebsiteID uint) error
|
||||
Deploy(ID, WebsiteID uint, enableHTTPS bool) error
|
||||
}
|
||||
|
||||
@@ -49,5 +49,5 @@ type WebsiteRepo interface {
|
||||
ResetConfig(id uint) error
|
||||
UpdateStatus(id uint, status bool) error
|
||||
UpdateCert(req *request.WebsiteUpdateCert) error
|
||||
ObtainCert(ctx context.Context, id uint) error
|
||||
ObtainCert(ctx context.Context, id uint, dnsID uint) error
|
||||
}
|
||||
|
||||
+91
-14
@@ -24,6 +24,8 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
"github.com/acepanel/panel/v3/pkg/webserver"
|
||||
webservertypes "github.com/acepanel/panel/v3/pkg/webserver/types"
|
||||
)
|
||||
|
||||
type certRepo struct {
|
||||
@@ -207,15 +209,15 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
|
||||
} else {
|
||||
if cert.Website == nil {
|
||||
return nil, errors.New(r.t.Get("this certificate is not associated with a website and cannot be obtained. You can try to obtain it manually"))
|
||||
} else {
|
||||
for _, domain := range cert.Domains {
|
||||
if strings.Contains(domain, "*") {
|
||||
return nil, errors.New(r.t.Get("wildcard domains cannot use HTTP verification"))
|
||||
}
|
||||
}
|
||||
conf := fmt.Sprintf("%s/sites/%s/config/site/001-acme.conf", app.Root, cert.Website.Name)
|
||||
client.UseHTTP(conf, webServer)
|
||||
}
|
||||
hasWildcard := slices.ContainsFunc(cert.Domains, func(d string) bool {
|
||||
return strings.Contains(d, "*")
|
||||
})
|
||||
if hasWildcard {
|
||||
return nil, errors.New(r.t.Get("wildcard domains cannot use HTTP verification"))
|
||||
}
|
||||
conf := fmt.Sprintf("%s/sites/%s/config/site/001-acme.conf", app.Root, cert.Website.Name)
|
||||
client.UseHTTP(conf, webServer)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
@@ -234,7 +236,7 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
return &ssl, r.Deploy(cert.ID, cert.WebsiteID)
|
||||
return &ssl, r.Deploy(cert.ID, cert.WebsiteID, false)
|
||||
}
|
||||
|
||||
if err = r.runScript(cert); err != nil {
|
||||
@@ -285,7 +287,7 @@ func (r *certRepo) ObtainSelfSigned(id uint) error {
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
return r.Deploy(cert.ID, cert.WebsiteID)
|
||||
return r.Deploy(cert.ID, cert.WebsiteID, false)
|
||||
}
|
||||
|
||||
if err = r.runScript(cert); err != nil {
|
||||
@@ -348,7 +350,7 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
return &ssl, r.Deploy(cert.ID, cert.WebsiteID)
|
||||
return &ssl, r.Deploy(cert.ID, cert.WebsiteID, false)
|
||||
}
|
||||
|
||||
return &ssl, nil
|
||||
@@ -384,7 +386,7 @@ func (r *certRepo) RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error) {
|
||||
return renewInfo, nil
|
||||
}
|
||||
|
||||
func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
func (r *certRepo) Deploy(ID, WebsiteID uint, enableHTTPS bool) error {
|
||||
cert, err := r.Get(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -398,13 +400,62 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
if err = r.db.Where("id", WebsiteID).First(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(fmt.Sprintf("%s/sites/%s/config/fullchain.pem", app.Root, website.Name), cert.Cert, 0600); err != nil {
|
||||
configDir := filepath.Join(app.Root, "sites", website.Name, "config")
|
||||
certPath := filepath.Join(configDir, "fullchain.pem")
|
||||
keyPath := filepath.Join(configDir, "private.key")
|
||||
if err = io.Write(certPath, cert.Cert, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(fmt.Sprintf("%s/sites/%s/config/private.key", app.Root, website.Name), cert.Key, 0600); err != nil {
|
||||
if err = io.Write(keyPath, cert.Key, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 开启 HTTPS
|
||||
if enableHTTPS && !website.SSL {
|
||||
vhost, vhostErr := r.getVhost(website)
|
||||
if vhostErr != nil {
|
||||
return vhostErr
|
||||
}
|
||||
|
||||
// 添加 443 监听
|
||||
listens := vhost.Listen()
|
||||
hasSSL := slices.ContainsFunc(listens, func(l webservertypes.Listen) bool {
|
||||
return slices.Contains(l.Args, "ssl")
|
||||
})
|
||||
if !hasSSL {
|
||||
webServer, _ := r.settingRepo.Get(biz.SettingKeyWebserver)
|
||||
args := []string{"ssl"}
|
||||
if webServer != "apache" {
|
||||
args = append(args, "quic")
|
||||
}
|
||||
listens = append(listens, webservertypes.Listen{Address: "443", Args: args})
|
||||
if err = vhost.SetListen(listens); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 配置 SSL
|
||||
defaultTLSVersions, _ := r.settingRepo.GetSlice(biz.SettingKeyWebsiteTLSVersions)
|
||||
defaultCipherSuites, _ := r.settingRepo.Get(biz.SettingKeyWebsiteCipherSuites)
|
||||
if err = vhost.SetSSLConfig(&webservertypes.SSLConfig{
|
||||
Cert: certPath,
|
||||
Key: keyPath,
|
||||
Protocols: defaultTLSVersions,
|
||||
Ciphers: defaultCipherSuites,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = vhost.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
website.SSL = true
|
||||
if err = r.db.Save(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
webServer, _ := r.settingRepo.Get(biz.SettingKeyWebserver)
|
||||
if webServer == "apache" {
|
||||
if err = systemctl.Reload("apache"); err != nil {
|
||||
@@ -421,6 +472,32 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getVhost 根据网站类型获取虚拟主机配置
|
||||
func (r *certRepo) getVhost(website *biz.Website) (webservertypes.Vhost, error) {
|
||||
webServer, err := r.settingRepo.Get(biz.SettingKeyWebserver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configDir := filepath.Join(app.Root, "sites", website.Name, "config")
|
||||
var vhost webservertypes.Vhost
|
||||
switch website.Type {
|
||||
case biz.WebsiteTypeProxy:
|
||||
vhost, err = webserver.NewProxyVhost(webserver.Type(webServer), configDir)
|
||||
case biz.WebsiteTypePHP:
|
||||
vhost, err = webserver.NewPHPVhost(webserver.Type(webServer), configDir)
|
||||
case biz.WebsiteTypeStatic:
|
||||
vhost, err = webserver.NewStaticVhost(webserver.Type(webServer), configDir)
|
||||
default:
|
||||
return nil, errors.New(r.t.Get("unsupported website type: %s", website.Type))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vhost, nil
|
||||
}
|
||||
|
||||
func (r *certRepo) runScript(cert *biz.Cert) error {
|
||||
if cert.Script == "" {
|
||||
return nil
|
||||
|
||||
@@ -988,13 +988,18 @@ func (r *websiteRepo) UpdateCert(req *request.WebsiteUpdateCert) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
|
||||
func (r *websiteRepo) ObtainCert(ctx context.Context, id uint, dnsID uint) error {
|
||||
website, err := r.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if slices.Contains(website.Domains, "*") {
|
||||
return errors.New(r.t.Get("not support one-key obtain wildcard certificate, please use Cert menu to obtain it with DNS method"))
|
||||
|
||||
// 泛域名必须使用 DNS 验证
|
||||
hasWildcard := slices.ContainsFunc(website.Domains, func(d string) bool {
|
||||
return strings.Contains(d, "*")
|
||||
})
|
||||
if hasWildcard && dnsID == 0 {
|
||||
return errors.New(r.t.Get("wildcard domains require DNS verification, please select a DNS provider"))
|
||||
}
|
||||
|
||||
account, err := r.certAccount.GetDefault(cast.ToUint(ctx.Value("user_id")))
|
||||
@@ -1010,6 +1015,7 @@ func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
|
||||
Domains: website.Domains,
|
||||
AutoRenewal: true,
|
||||
AccountID: account.ID,
|
||||
DNSID: dnsID,
|
||||
WebsiteID: website.ID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -1020,6 +1026,7 @@ func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
|
||||
}
|
||||
}
|
||||
newCert.Domains = website.Domains
|
||||
newCert.DNSID = dnsID
|
||||
if err = r.db.Save(newCert).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1029,7 +1036,7 @@ func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.cert.Deploy(newCert.ID, website.ID)
|
||||
return r.cert.Deploy(newCert.ID, website.ID, false)
|
||||
}
|
||||
|
||||
// customConfigStartNum 自定义配置起始序号
|
||||
|
||||
@@ -42,6 +42,7 @@ func (r *CertUpdate) Rules(_ *http.Request) map[string]string {
|
||||
}
|
||||
|
||||
type CertDeploy struct {
|
||||
ID uint `form:"id" json:"id" validate:"required|exists:certs,id"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id" validate:"required|exists:websites,id"`
|
||||
ID uint `form:"id" json:"id" validate:"required|exists:certs,id"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id" validate:"required|exists:websites,id"`
|
||||
EnableHTTPS bool `form:"enable_https" json:"enable_https"`
|
||||
}
|
||||
|
||||
@@ -127,3 +127,8 @@ type WebsiteUpdateCert struct {
|
||||
Cert string `json:"cert" validate:"required"`
|
||||
Key string `json:"key" validate:"required"`
|
||||
}
|
||||
|
||||
type WebsiteObtainCert struct {
|
||||
ID uint `json:"id" form:"id" uri:"id" validate:"required|exists:websites,id"`
|
||||
DNSID uint `json:"dns_id" form:"dns_id"`
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ func (s *CertService) Deploy(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = s.certRepo.Deploy(req.ID, req.WebsiteID)
|
||||
err = s.certRepo.Deploy(req.ID, req.WebsiteID, req.EnableHTTPS)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
|
||||
@@ -239,13 +239,13 @@ func (s *WebsiteService) UpdateStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *WebsiteService) ObtainCert(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
req, err := Bind[request.WebsiteObtainCert](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.websiteRepo.ObtainCert(r.Context(), req.ID); err != nil {
|
||||
if err = s.websiteRepo.ObtainCert(r.Context(), req.ID, req.DNSID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,6 +47,6 @@ export default {
|
||||
// 续签
|
||||
renew: (id: number): any => http.Post(`/cert/cert/${id}/renew`, { id }),
|
||||
// 部署
|
||||
deploy: (id: number, website_id: number): any =>
|
||||
http.Post(`/cert/cert/${id}/deploy`, { id, website_id })
|
||||
deploy: (id: number, website_id: number, enable_https: boolean = false): any =>
|
||||
http.Post(`/cert/cert/${id}/deploy`, { id, website_id, enable_https })
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ export default {
|
||||
// 修改状态
|
||||
status: (id: number, status: boolean): any => http.Post(`/website/${id}/status`, { status }),
|
||||
// 签发证书
|
||||
obtainCert: (id: number): any => http.Post(`/website/${id}/obtain_cert`),
|
||||
obtainCert: (id: number, dns_id?: number): any =>
|
||||
http.Post(`/website/${id}/obtain_cert`, dns_id ? { dns_id } : {}),
|
||||
// 统计概览
|
||||
statOverview: (start: string, end: string, sites?: string): any =>
|
||||
http.Get('/website/stat/overview', { params: { start, end, sites } }),
|
||||
|
||||
@@ -54,7 +54,8 @@ const showModel = ref<any>({
|
||||
const deployModal = ref(false)
|
||||
const deployModel = ref<any>({
|
||||
id: null,
|
||||
websites: []
|
||||
websites: [],
|
||||
enable_https: true
|
||||
})
|
||||
const obtain = ref(false)
|
||||
const obtainCert = ref(0)
|
||||
@@ -374,13 +375,14 @@ const handleAutoRenewalUpdate = (row: any) => {
|
||||
|
||||
const handleDeployCert = async () => {
|
||||
const promises = deployModel.value.websites.map((website: any) =>
|
||||
cert.deploy(deployModel.value.id, website)
|
||||
cert.deploy(deployModel.value.id, website, deployModel.value.enable_https)
|
||||
)
|
||||
await Promise.all(promises)
|
||||
|
||||
deployModal.value = false
|
||||
deployModel.value.id = null
|
||||
deployModel.value.websites = []
|
||||
deployModel.value.enable_https = true
|
||||
window.$message.success($gettext('Deployment successful'))
|
||||
}
|
||||
|
||||
@@ -540,9 +542,6 @@ onUnmounted(() => {
|
||||
:segmented="false"
|
||||
>
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
{{ $gettext('If website not enabled HTTPS, please enable it after deployment.') }}
|
||||
</n-alert>
|
||||
<n-form :model="deployModel">
|
||||
<n-form-item path="website_id" :label="$gettext('Website')">
|
||||
<n-select
|
||||
@@ -553,6 +552,9 @@ onUnmounted(() => {
|
||||
:options="websites"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="enable_https" :label="$gettext('Enable HTTPS')">
|
||||
<n-switch v-model:value="deployModel.enable_https" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button type="info" block @click="handleDeployCert">{{ $gettext('Submit') }}</n-button>
|
||||
</n-flex>
|
||||
|
||||
@@ -180,12 +180,52 @@ const handleRewrite = (value: string) => {
|
||||
}
|
||||
|
||||
const isObtainCert = ref(false)
|
||||
const dnsModal = ref(false)
|
||||
const dnsList = ref<any>([])
|
||||
const selectedDnsId = ref<number | null>(null)
|
||||
|
||||
const handleObtainCert = () => {
|
||||
// 检测泛域名
|
||||
const hasWildcard = setting.value.domains?.some((domain: string) => domain.includes('*'))
|
||||
if (hasWildcard) {
|
||||
// 加载 DNS 列表并弹窗选择
|
||||
useRequest(cert.dns(1, 10000)).onSuccess(({ data }) => {
|
||||
dnsList.value = data.items.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
}))
|
||||
if (dnsList.value.length === 0) {
|
||||
window.$message.error(
|
||||
$gettext(
|
||||
'Your website contains wildcard domains, which require DNS verification. Please add a DNS provider in Certificate Management first.'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
selectedDnsId.value = null
|
||||
dnsModal.value = true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
doObtainCert()
|
||||
}
|
||||
|
||||
const handleDnsObtainCert = () => {
|
||||
if (!selectedDnsId.value) {
|
||||
window.$message.error($gettext('Please select a DNS provider'))
|
||||
return
|
||||
}
|
||||
dnsModal.value = false
|
||||
doObtainCert(selectedDnsId.value)
|
||||
}
|
||||
|
||||
const doObtainCert = (dnsId?: number) => {
|
||||
isObtainCert.value = true
|
||||
messageReactive = window.$message.loading($gettext('Please wait...'), {
|
||||
duration: 0
|
||||
})
|
||||
useRequest(website.obtainCert(id.value))
|
||||
useRequest(website.obtainCert(id.value, dnsId))
|
||||
.onSuccess(() => {
|
||||
fetchSetting()
|
||||
window.$message.success($gettext('Issued successfully'))
|
||||
@@ -2037,6 +2077,36 @@ const removeCustomConfig = (index: number) => {
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal
|
||||
v-model:show="dnsModal"
|
||||
preset="card"
|
||||
:title="$gettext('Select DNS Provider')"
|
||||
style="width: 60vw"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
>
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'Your website contains wildcard domains (e.g. *.example.com), which require DNS verification to issue certificates.'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('DNS')">
|
||||
<n-select
|
||||
v-model:value="selectedDnsId"
|
||||
:placeholder="$gettext('Select DNS for certificate issuance')"
|
||||
:options="dnsList"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button type="info" block :disabled="!selectedDnsId" @click="handleDnsObtainCert">
|
||||
{{ $gettext('Issue') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user