feat(api/cert): enhance certificate info retrieval

- Introduced a new method `GetCertInfos` to fetch details of all available certificates.
- Updated the `Info` handler to return an array of `CertInfo` instead of a single certificate.
- Improved error handling for cases with no available certificates.
- Refactored related error messages for clarity.
This commit is contained in:
yusing
2026-01-07 10:54:33 +08:00
parent f5dcc85b12
commit 17bfc96e3d
4 changed files with 63 additions and 37 deletions

View File

@@ -1,6 +1,7 @@
package certapi package certapi
import ( import (
"errors"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -8,46 +9,33 @@ import (
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
) )
type CertInfo struct {
Subject string `json:"subject"`
Issuer string `json:"issuer"`
NotBefore int64 `json:"not_before"`
NotAfter int64 `json:"not_after"`
DNSNames []string `json:"dns_names"`
EmailAddresses []string `json:"email_addresses"`
} // @name CertInfo
// @x-id "info" // @x-id "info"
// @BasePath /api/v1 // @BasePath /api/v1
// @Summary Get cert info // @Summary Get cert info
// @Description Get cert info // @Description Get cert info
// @Tags cert // @Tags cert
// @Produce json // @Produce json
// @Success 200 {object} CertInfo // @Success 200 {array} autocert.CertInfo
// @Failure 403 {object} apitypes.ErrorResponse // @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
// @Failure 404 {object} apitypes.ErrorResponse // @Failure 404 {object} apitypes.ErrorResponse "No certificates found or autocert is not enabled"
// @Failure 500 {object} apitypes.ErrorResponse // @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
// @Router /cert/info [get] // @Router /cert/info [get]
func Info(c *gin.Context) { func Info(c *gin.Context) {
autocert := autocert.ActiveProvider.Load() provider := autocert.ActiveProvider.Load()
if autocert == nil { if provider == nil {
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled")) c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
return return
} }
cert, err := autocert.GetCert(nil) certInfos, err := provider.GetCertInfos()
if err != nil { if err != nil {
if errors.Is(err, autocert.ErrNoCertificates) {
c.JSON(http.StatusNotFound, apitypes.Error("no certificate found"))
return
}
c.Error(apitypes.InternalServerError(err, "failed to get cert info")) c.Error(apitypes.InternalServerError(err, "failed to get cert info"))
return return
} }
certInfo := CertInfo{ c.JSON(http.StatusOK, certInfos)
Subject: cert.Leaf.Subject.CommonName,
Issuer: cert.Leaf.Issuer.CommonName,
NotBefore: cert.Leaf.NotBefore.Unix(),
NotAfter: cert.Leaf.NotAfter.Unix(),
DNSNames: cert.Leaf.DNSNames,
EmailAddresses: cert.Leaf.EmailAddresses,
}
c.JSON(http.StatusOK, certInfo)
} }

View File

@@ -328,23 +328,26 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/CertInfo" "type": "array",
"items": {
"$ref": "#/definitions/CertInfo"
}
} }
}, },
"403": { "403": {
"description": "Forbidden", "description": "Unauthorized",
"schema": { "schema": {
"$ref": "#/definitions/ErrorResponse" "$ref": "#/definitions/ErrorResponse"
} }
}, },
"404": { "404": {
"description": "Not Found", "description": "No certificates found or autocert is not enabled",
"schema": { "schema": {
"$ref": "#/definitions/ErrorResponse" "$ref": "#/definitions/ErrorResponse"
} }
}, },
"500": { "500": {
"description": "Internal Server Error", "description": "Internal server error",
"schema": { "schema": {
"$ref": "#/definitions/ErrorResponse" "$ref": "#/definitions/ErrorResponse"
} }

View File

@@ -1886,17 +1886,19 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/CertInfo' items:
$ref: '#/definitions/CertInfo'
type: array
"403": "403":
description: Forbidden description: Unauthorized
schema: schema:
$ref: '#/definitions/ErrorResponse' $ref: '#/definitions/ErrorResponse'
"404": "404":
description: Not Found description: No certificates found or autocert is not enabled
schema: schema:
$ref: '#/definitions/ErrorResponse' $ref: '#/definitions/ErrorResponse'
"500": "500":
description: Internal Server Error description: Internal server error
schema: schema:
$ref: '#/definitions/ErrorResponse' $ref: '#/definitions/ErrorResponse'
summary: Get cert info summary: Get cert info

View File

@@ -55,10 +55,20 @@ type (
} }
CertExpiries map[string]time.Time CertExpiries map[string]time.Time
RenewMode uint8
CertInfo struct {
Subject string `json:"subject"`
Issuer string `json:"issuer"`
NotBefore int64 `json:"not_before"`
NotAfter int64 `json:"not_after"`
DNSNames []string `json:"dns_names"`
EmailAddresses []string `json:"email_addresses"`
} // @name CertInfo
RenewMode uint8
) )
var ErrNoCertificate = errors.New("no certificate found") var ErrNoCertificates = errors.New("no certificates found")
const ( const (
// renew failed for whatever reason, 1 hour cooldown // renew failed for whatever reason, 1 hour cooldown
@@ -98,7 +108,7 @@ func NewProvider(cfg *Config, user *User, legoCfg *lego.Config) (*Provider, erro
func (p *Provider) GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { func (p *Provider) GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if p.tlsCert == nil { if p.tlsCert == nil {
return nil, ErrNoCertificate return nil, ErrNoCertificates
} }
if hello == nil || hello.ServerName == "" { if hello == nil || hello.ServerName == "" {
return p.tlsCert, nil return p.tlsCert, nil
@@ -109,6 +119,29 @@ func (p *Provider) GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
return p.tlsCert, nil return p.tlsCert, nil
} }
func (p *Provider) GetCertInfos() ([]CertInfo, error) {
allProviders := p.allProviders()
certInfos := make([]CertInfo, 0, len(allProviders))
for _, provider := range allProviders {
if provider.tlsCert == nil {
continue
}
certInfos = append(certInfos, CertInfo{
Subject: provider.tlsCert.Leaf.Subject.CommonName,
Issuer: provider.tlsCert.Leaf.Issuer.CommonName,
NotBefore: provider.tlsCert.Leaf.NotBefore.Unix(),
NotAfter: provider.tlsCert.Leaf.NotAfter.Unix(),
DNSNames: provider.tlsCert.Leaf.DNSNames,
EmailAddresses: provider.tlsCert.Leaf.EmailAddresses,
})
}
if len(certInfos) == 0 {
return nil, ErrNoCertificates
}
return certInfos, nil
}
func (p *Provider) GetName() string { func (p *Provider) GetName() string {
if p.cfg.idx == 0 { if p.cfg.idx == 0 {
return "main" return "main"