feat: SSL management support different types of certificates of a same doamin name #309

This commit is contained in:
Jacky 2024-04-30 16:05:25 +08:00
parent 464e84a64f
commit 3e90b838fd
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
11 changed files with 54 additions and 24 deletions

View file

@ -6,6 +6,7 @@ import (
"github.com/0xJacky/Nginx-UI/internal/nginx" "github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/model" "github.com/0xJacky/Nginx-UI/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"net/http" "net/http"
"strings" "strings"
@ -18,10 +19,11 @@ const (
) )
type IssueCertResponse struct { type IssueCertResponse struct {
Status string `json:"status"` Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
SSLCertificate string `json:"ssl_certificate,omitempty"` SSLCertificate string `json:"ssl_certificate,omitempty"`
SSLCertificateKey string `json:"ssl_certificate_key,omitempty"` SSLCertificateKey string `json:"ssl_certificate_key,omitempty"`
KeyType certcrypto.KeyType `json:"key_type"`
} }
func handleIssueCertLogChan(conn *websocket.Conn, log *cert.Logger, logChan chan string) { func handleIssueCertLogChan(conn *websocket.Conn, log *cert.Logger, logChan chan string) {
@ -75,8 +77,7 @@ func IssueCert(c *gin.Context) {
return return
} }
certModel, err := model.FirstOrCreateCert(c.Param("name")) certModel, err := model.FirstOrCreateCert(c.Param("name"), payload.GetKeyType())
if err != nil { if err != nil {
logger.Error(err) logger.Error(err)
return return
@ -113,7 +114,7 @@ func IssueCert(c *gin.Context) {
return return
} }
certDirName := strings.Join(payload.ServerName, "_") certDirName := strings.Join(payload.ServerName, "_") + "_" + string(payload.GetKeyType())
sslCertificatePath := nginx.GetConfPath("ssl", certDirName, "fullchain.cer") sslCertificatePath := nginx.GetConfPath("ssl", certDirName, "fullchain.cer")
sslCertificateKeyPath := nginx.GetConfPath("ssl", certDirName, "private.key") sslCertificateKeyPath := nginx.GetConfPath("ssl", certDirName, "private.key")
@ -144,6 +145,7 @@ func IssueCert(c *gin.Context) {
Message: "Issued certificate successfully", Message: "Issued certificate successfully",
SSLCertificate: sslCertificatePath, SSLCertificate: sslCertificatePath,
SSLCertificateKey: sslCertificateKeyPath, SSLCertificateKey: sslCertificateKeyPath,
KeyType: payload.GetKeyType(),
}) })
if err != nil { if err != nil {

View file

@ -2,8 +2,10 @@ package sites
import ( import (
"github.com/0xJacky/Nginx-UI/api" "github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/model" "github.com/0xJacky/Nginx-UI/model"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-acme/lego/v4/certcrypto"
"net/http" "net/http"
) )
@ -11,16 +13,17 @@ func AddDomainToAutoCert(c *gin.Context) {
name := c.Param("name") name := c.Param("name")
var json struct { var json struct {
DnsCredentialID int `json:"dns_credential_id"` DnsCredentialID int `json:"dns_credential_id"`
ChallengeMethod string `json:"challenge_method"` ChallengeMethod string `json:"challenge_method"`
Domains []string `json:"domains"` Domains []string `json:"domains"`
KeyType certcrypto.KeyType `json:"key_type"`
} }
if !api.BindAndValid(c, &json) { if !api.BindAndValid(c, &json) {
return return
} }
certModel, err := model.FirstOrCreateCert(name) certModel, err := model.FirstOrCreateCert(name, helper.GetKeyType(json.KeyType))
if err != nil { if err != nil {
api.ErrHandler(c, err) api.ErrHandler(c, err)

View file

@ -2,6 +2,7 @@ import type { ModelBase } from '@/api/curd'
import Curd from '@/api/curd' import Curd from '@/api/curd'
import type { DnsCredential } from '@/api/dns_credential' import type { DnsCredential } from '@/api/dns_credential'
import type { AcmeUser } from '@/api/acme_user' import type { AcmeUser } from '@/api/acme_user'
import type { PrivateKeyType } from '@/constants'
export interface Cert extends ModelBase { export interface Cert extends ModelBase {
name: string name: string
@ -32,6 +33,7 @@ export interface CertificateInfo {
export interface CertificateResult { export interface CertificateResult {
ssl_certificate: string ssl_certificate: string
ssl_certificate_key: string ssl_certificate_key: string
key_type: PrivateKeyType
} }
const cert: Curd<Cert> = new Curd('/cert') const cert: Curd<Cert> = new Curd('/cert')

View file

@ -3,6 +3,7 @@ import http from '@/lib/http'
import type { ChatComplicationMessage } from '@/api/openai' import type { ChatComplicationMessage } from '@/api/openai'
import type { CertificateInfo } from '@/api/cert' import type { CertificateInfo } from '@/api/cert'
import type { NgxConfig } from '@/api/ngx' import type { NgxConfig } from '@/api/ngx'
import type { PrivateKeyType } from '@/constants'
export interface Site { export interface Site {
modified_at: string modified_at: string
@ -22,6 +23,7 @@ export interface AutoCertRequest {
dns_credential_id: number | null dns_credential_id: number | null
challenge_method: string challenge_method: string
domains: string[] domains: string[]
key_type: PrivateKeyType
} }
class Domain extends Curd<Site> { class Domain extends Curd<Site> {

View file

@ -36,3 +36,5 @@ export const PrivateKeyTypeMask = {
} as const } as const
export const PrivateKeyTypeList = Object.entries(PrivateKeyTypeMask).map(([key, name]) => ({ key, name })) export const PrivateKeyTypeList = Object.entries(PrivateKeyTypeMask).map(([key, name]) => ({ key, name }))
export type PrivateKeyType = keyof typeof PrivateKeyTypeMask

View file

@ -8,6 +8,7 @@ import type { Props } from '@/views/domain/cert/IssueCert.vue'
import type { DnsChallenge } from '@/api/auto_cert' import type { DnsChallenge } from '@/api/auto_cert'
import ObtainCertLive from '@/views/domain/cert/components/ObtainCertLive.vue' import ObtainCertLive from '@/views/domain/cert/components/ObtainCertLive.vue'
import type { CertificateResult } from '@/api/cert' import type { CertificateResult } from '@/api/cert'
import type { PrivateKeyType } from '@/constants'
const emit = defineEmits(['update:auto_cert']) const emit = defineEmits(['update:auto_cert'])
@ -48,20 +49,21 @@ const issue_cert = (config_name: string, server_name: string) => {
refObtainCertLive.value.issue_cert(config_name, server_name.trim().split(' ')).then(resolveCert) refObtainCertLive.value.issue_cert(config_name, server_name.trim().split(' ')).then(resolveCert)
} }
async function resolveCert({ ssl_certificate, ssl_certificate_key }: CertificateResult) { async function resolveCert({ ssl_certificate, ssl_certificate_key, key_type }: CertificateResult) {
directivesMap.value.ssl_certificate[0].params = ssl_certificate directivesMap.value.ssl_certificate[0].params = ssl_certificate
directivesMap.value.ssl_certificate_key[0].params = ssl_certificate_key directivesMap.value.ssl_certificate_key[0].params = ssl_certificate_key
await save_config() await save_config()
change_auto_cert(true) change_auto_cert(true, key_type)
emit('update:auto_cert', true) emit('update:auto_cert', true)
} }
function change_auto_cert(status: boolean) { function change_auto_cert(status: boolean, key_type?: PrivateKeyType) {
if (status) { if (status) {
domain.add_auto_cert(props.configName, { domain.add_auto_cert(props.configName, {
domains: name.value.trim().split(' '), domains: name.value.trim().split(' '),
challenge_method: data.value.challenge_method, challenge_method: data.value.challenge_method,
dns_credential_id: data.value.dns_credential_id, dns_credential_id: data.value.dns_credential_id,
key_type: key_type!,
}).then(() => { }).then(() => {
message.success($gettext('Auto-renewal enabled for %{name}', { name: name.value })) message.success($gettext('Auto-renewal enabled for %{name}', { name: name.value }))
}).catch(e => { }).catch(e => {

View file

@ -106,7 +106,11 @@ const issue_cert = async (config_name: string, server_name: string[], key_type:
if (r.status === 'success' && r.ssl_certificate !== undefined && r.ssl_certificate_key !== undefined) { if (r.status === 'success' && r.ssl_certificate !== undefined && r.ssl_certificate_key !== undefined) {
progressStatus.value = 'success' progressStatus.value = 'success'
progressPercent.value = 100 progressPercent.value = 100
resolve({ ssl_certificate: r.ssl_certificate, ssl_certificate_key: r.ssl_certificate_key }) resolve({
ssl_certificate: r.ssl_certificate,
ssl_certificate_key: r.ssl_certificate_key,
key_type: r.key_type,
})
} }
else { else {
progressStatus.value = 'exception' progressStatus.value = 'exception'

View file

@ -167,7 +167,7 @@ func IssueCert(payload *ConfigPayload, logChan chan string, errChan chan error)
return return
} }
name := strings.Join(domain, "_") name := strings.Join(domain, "_")
saveDir := nginx.GetConfPath("ssl/" + name) saveDir := nginx.GetConfPath("ssl/" + name + "_" + string(payload.KeyType))
if _, err = os.Stat(saveDir); os.IsNotExist(err) { if _, err = os.Stat(saveDir); os.IsNotExist(err) {
err = os.MkdirAll(saveDir, 0755) err = os.MkdirAll(saveDir, 0755)
if err != nil { if err != nil {

View file

@ -1,6 +1,7 @@
package cert package cert
import ( import (
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certcrypto"
) )
@ -10,3 +11,7 @@ type ConfigPayload struct {
DNSCredentialID int `json:"dns_credential_id"` DNSCredentialID int `json:"dns_credential_id"`
KeyType certcrypto.KeyType `json:"key_type"` KeyType certcrypto.KeyType `json:"key_type"`
} }
func (c *ConfigPayload) GetKeyType() certcrypto.KeyType {
return helper.GetKeyType(c.KeyType)
}

View file

@ -0,0 +1,12 @@
package helper
import "github.com/go-acme/lego/v4/certcrypto"
func GetKeyType(keyType certcrypto.KeyType) certcrypto.KeyType {
switch keyType {
case certcrypto.RSA2048, certcrypto.RSA3072, certcrypto.RSA4096,
certcrypto.EC256, certcrypto.EC384:
return keyType
}
return certcrypto.RSA2048
}

View file

@ -1,6 +1,7 @@
package model package model
import ( import (
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx" "github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certcrypto"
"github.com/lib/pq" "github.com/lib/pq"
@ -41,9 +42,9 @@ func FirstCert(confName string) (c Cert, err error) {
return return
} }
func FirstOrCreateCert(confName string) (c Cert, err error) { func FirstOrCreateCert(confName string, keyType certcrypto.KeyType) (c Cert, err error) {
// Filename is used to check whether this site is enabled // Filename is used to check whether this site is enabled
err = db.FirstOrCreate(&c, &Cert{Name: confName, Filename: confName}).Error err = db.FirstOrCreate(&c, &Cert{Name: confName, Filename: confName, KeyType: keyType}).Error
return return
} }
@ -96,10 +97,5 @@ func (c *Cert) Remove() error {
} }
func (c *Cert) GetKeyType() certcrypto.KeyType { func (c *Cert) GetKeyType() certcrypto.KeyType {
switch c.KeyType { return helper.GetKeyType(c.KeyType)
case certcrypto.RSA2048, certcrypto.RSA3072, certcrypto.RSA4096,
certcrypto.EC256, certcrypto.EC384:
return c.KeyType
}
return certcrypto.RSA2048
} }