Moved v3 code from NginxProxyManager/nginx-proxy-manager-3 to NginxProxyManager/nginx-proxy-manager
This commit is contained in:
266
backend/internal/entity/certificate/model.go
Normal file
266
backend/internal/entity/certificate/model.go
Normal file
@ -0,0 +1,266 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"npm/internal/acme"
|
||||
"npm/internal/config"
|
||||
"npm/internal/database"
|
||||
"npm/internal/entity/certificateauthority"
|
||||
"npm/internal/entity/dnsprovider"
|
||||
"npm/internal/logger"
|
||||
"npm/internal/types"
|
||||
)
|
||||
|
||||
const (
|
||||
tableName = "certificate"
|
||||
|
||||
// TypeCustom custom cert type
|
||||
TypeCustom = "custom"
|
||||
// TypeHTTP http cert type
|
||||
TypeHTTP = "http"
|
||||
// TypeDNS dns cert type
|
||||
TypeDNS = "dns"
|
||||
// TypeMkcert mkcert cert type
|
||||
TypeMkcert = "mkcert"
|
||||
|
||||
// StatusReady is ready for certificate to be requested
|
||||
StatusReady = "ready"
|
||||
// StatusRequesting is process of being requested
|
||||
StatusRequesting = "requesting"
|
||||
// StatusFailed is a certicifate that failed to request
|
||||
StatusFailed = "failed"
|
||||
// StatusProvided is a certificate provided and ready for actual use
|
||||
StatusProvided = "provided"
|
||||
)
|
||||
|
||||
// Model is the user model
|
||||
type Model struct {
|
||||
ID int `json:"id" db:"id" filter:"id,integer"`
|
||||
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
|
||||
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
|
||||
ExpiresOn types.NullableDBDate `json:"expires_on" db:"expires_on" filter:"expires_on,integer"`
|
||||
Type string `json:"type" db:"type" filter:"type,string"`
|
||||
UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"`
|
||||
CertificateAuthorityID int `json:"certificate_authority_id" db:"certificate_authority_id" filter:"certificate_authority_id,integer"`
|
||||
DNSProviderID int `json:"dns_provider_id" db:"dns_provider_id" filter:"dns_provider_id,integer"`
|
||||
Name string `json:"name" db:"name" filter:"name,string"`
|
||||
DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"`
|
||||
Status string `json:"status" db:"status" filter:"status,string"`
|
||||
ErrorMessage string `json:"error_message" db:"error_message" filter:"error_message,string"`
|
||||
Meta types.JSONB `json:"-" db:"meta"`
|
||||
IsECC int `json:"is_ecc" db:"is_ecc" filter:"is_ecc,integer"`
|
||||
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
|
||||
// Expansions:
|
||||
CertificateAuthority *certificateauthority.Model `json:"certificate_authority,omitempty"`
|
||||
DNSProvider *dnsprovider.Model `json:"dns_provider,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Model) getByQuery(query string, params []interface{}) error {
|
||||
return database.GetByQuery(m, query, params)
|
||||
}
|
||||
|
||||
// LoadByID will load from an ID
|
||||
func (m *Model) LoadByID(id int) error {
|
||||
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
|
||||
params := []interface{}{id, 0}
|
||||
return m.getByQuery(query, params)
|
||||
}
|
||||
|
||||
// Touch will update model's timestamp(s)
|
||||
func (m *Model) Touch(created bool) {
|
||||
var d types.DBDate
|
||||
d.Time = time.Now()
|
||||
if created {
|
||||
m.CreatedOn = d
|
||||
}
|
||||
m.ModifiedOn = d
|
||||
}
|
||||
|
||||
// Save will save this model to the DB
|
||||
func (m *Model) Save() error {
|
||||
var err error
|
||||
|
||||
if m.UserID == 0 {
|
||||
return fmt.Errorf("User ID must be specified")
|
||||
}
|
||||
|
||||
if !m.Validate() {
|
||||
return fmt.Errorf("Certificate data is incorrect or incomplete for this type")
|
||||
}
|
||||
|
||||
if !m.ValidateWildcardSupport() {
|
||||
return fmt.Errorf("Cannot use Wildcard domains with this CA")
|
||||
}
|
||||
|
||||
m.setDefaultStatus()
|
||||
|
||||
if m.ID == 0 {
|
||||
m.ID, err = Create(m)
|
||||
} else {
|
||||
err = Update(m)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete will mark a certificate as deleted
|
||||
func (m *Model) Delete() bool {
|
||||
m.Touch(false)
|
||||
m.IsDeleted = true
|
||||
if err := m.Save(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Validate will make sure the data given is expected. This object is a bit complicated,
|
||||
// as there could be multiple combinations of values.
|
||||
func (m *Model) Validate() bool {
|
||||
switch m.Type {
|
||||
case TypeCustom:
|
||||
// TODO: make sure meta contains required fields
|
||||
return m.DNSProviderID == 0 && m.CertificateAuthorityID == 0
|
||||
|
||||
case TypeHTTP:
|
||||
return m.DNSProviderID == 0 && m.CertificateAuthorityID > 0
|
||||
|
||||
case TypeDNS:
|
||||
return m.DNSProviderID > 0 && m.CertificateAuthorityID > 0
|
||||
|
||||
case TypeMkcert:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateWildcardSupport will ensure that the CA given supports wildcards,
|
||||
// only if the domains on this object have at least 1 wildcard
|
||||
func (m *Model) ValidateWildcardSupport() bool {
|
||||
domains, err := m.DomainNames.AsStringArray()
|
||||
if err != nil {
|
||||
logger.Error("ValidateWildcardSupportError", err)
|
||||
return false
|
||||
}
|
||||
|
||||
hasWildcard := false
|
||||
for _, domain := range domains {
|
||||
if strings.Contains(domain, "*") {
|
||||
hasWildcard = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasWildcard {
|
||||
m.Expand()
|
||||
if !m.CertificateAuthority.IsWildcardSupported {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Model) setDefaultStatus() {
|
||||
if m.ID == 0 {
|
||||
// It's a new certificate
|
||||
if m.Type == TypeCustom {
|
||||
m.Status = StatusProvided
|
||||
} else {
|
||||
m.Status = StatusReady
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand will populate attached objects for the model
|
||||
func (m *Model) Expand() {
|
||||
if m.CertificateAuthorityID > 0 {
|
||||
certificateAuthority, _ := certificateauthority.GetByID(m.CertificateAuthorityID)
|
||||
m.CertificateAuthority = &certificateAuthority
|
||||
}
|
||||
if m.DNSProviderID > 0 {
|
||||
dnsProvider, _ := dnsprovider.GetByID(m.DNSProviderID)
|
||||
m.DNSProvider = &dnsProvider
|
||||
}
|
||||
}
|
||||
|
||||
// GetCertificateLocations will return the paths on disk where the SSL
|
||||
// certs should or would be.
|
||||
// Returns: (key, fullchain, certFolder)
|
||||
func (m *Model) GetCertificateLocations() (string, string, string) {
|
||||
if m.ID == 0 {
|
||||
logger.Error("GetCertificateLocationsError", errors.New("GetCertificateLocations called before certificate was saved"))
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
certFolder := fmt.Sprintf("%s/certificates", config.Configuration.DataFolder)
|
||||
|
||||
// Generate a unique folder name for this cert
|
||||
m1 := regexp.MustCompile(`[^A-Za-z0-9\.]`)
|
||||
|
||||
niceName := m1.ReplaceAllString(m.Name, "_")
|
||||
if len(niceName) > 20 {
|
||||
niceName = niceName[:20]
|
||||
}
|
||||
folderName := fmt.Sprintf("%d-%s", m.ID, niceName)
|
||||
|
||||
return fmt.Sprintf("%s/%s/key.pem", certFolder, folderName),
|
||||
fmt.Sprintf("%s/%s/fullchain.pem", certFolder, folderName),
|
||||
fmt.Sprintf("%s/%s", certFolder, folderName)
|
||||
}
|
||||
|
||||
// Request makes a certificate request
|
||||
func (m *Model) Request() error {
|
||||
logger.Info("Requesting certificate for: #%d %v", m.ID, m.Name)
|
||||
|
||||
m.Expand()
|
||||
m.Status = StatusRequesting
|
||||
if err := m.Save(); err != nil {
|
||||
logger.Error("CertificateSaveError", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// do request
|
||||
domains, err := m.DomainNames.AsStringArray()
|
||||
if err != nil {
|
||||
logger.Error("CertificateRequestError", err)
|
||||
return err
|
||||
}
|
||||
|
||||
certKeyFile, certFullchainFile, certFolder := m.GetCertificateLocations()
|
||||
|
||||
// ensure certFolder is created
|
||||
if err := os.MkdirAll(certFolder, os.ModePerm); err != nil {
|
||||
logger.Error("CreateFolderError", err)
|
||||
return err
|
||||
}
|
||||
|
||||
errMsg, err := acme.RequestCert(domains, m.Type, certFullchainFile, certKeyFile, m.DNSProvider, m.CertificateAuthority, true)
|
||||
if err != nil {
|
||||
m.Status = StatusFailed
|
||||
m.ErrorMessage = errMsg
|
||||
if err := m.Save(); err != nil {
|
||||
logger.Error("CertificateSaveError", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If done
|
||||
m.Status = StatusProvided
|
||||
t := time.Now()
|
||||
m.ExpiresOn.Time = &t // todo
|
||||
if err := m.Save(); err != nil {
|
||||
logger.Error("CertificateSaveError", err)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Request for certificate for: #%d %v was completed", m.ID, m.Name)
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user