Moved v3 code from NginxProxyManager/nginx-proxy-manager-3 to NginxProxyManager/nginx-proxy-manager

This commit is contained in:
Jamie Curnow
2022-05-12 08:47:31 +10:00
parent 4db34f5894
commit 2110ecc382
830 changed files with 38168 additions and 36635 deletions

View File

@ -0,0 +1,25 @@
package certificate
import (
"npm/internal/entity"
)
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
// getFilterMapFunctions is a map of functions that should be executed
// during the filtering process, if a field is defined here then the value in
// the filter will be given to the defined function and it will return a new
// value for use in the sql query.
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
// if len(filterMapFunctions) == 0 {
// TODO: See internal/model/file_item.go:620 for an example
// }
return filterMapFunctions
}
// GetFilterSchema returns filter schema
func GetFilterSchema() string {
var m Model
return entity.GetFilterSchema(m)
}

View File

@ -0,0 +1,174 @@
package certificate
import (
"database/sql"
goerrors "errors"
"fmt"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/errors"
"npm/internal/logger"
"npm/internal/model"
)
// GetByID finds a row by ID
func GetByID(id int) (Model, error) {
var m Model
err := m.LoadByID(id)
return m, err
}
// Create will create a row from this model
func Create(certificate *Model) (int, error) {
if certificate.ID != 0 {
return 0, goerrors.New("Cannot create certificate when model already has an ID")
}
certificate.Touch(true)
db := database.GetInstance()
// nolint: gosec
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
created_on,
modified_on,
user_id,
type,
certificate_authority_id,
dns_provider_id,
name,
domain_names,
expires_on,
status,
meta,
is_ecc,
is_deleted
) VALUES (
:created_on,
:modified_on,
:user_id,
:type,
:certificate_authority_id,
:dns_provider_id,
:name,
:domain_names,
:expires_on,
:status,
:meta,
:is_ecc,
:is_deleted
)`, certificate)
if err != nil {
return 0, err
}
last, lastErr := result.LastInsertId()
if lastErr != nil {
return 0, lastErr
}
return int(last), nil
}
// Update will Update a Auth from this model
func Update(certificate *Model) error {
if certificate.ID == 0 {
return goerrors.New("Cannot update certificate when model doesn't have an ID")
}
certificate.Touch(false)
db := database.GetInstance()
// nolint: gosec
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
created_on = :created_on,
modified_on = :modified_on,
type = :type,
user_id = :user_id,
certificate_authority_id = :certificate_authority_id,
dns_provider_id = :dns_provider_id,
name = :name,
domain_names = :domain_names,
expires_on = :expires_on,
status = :status,
meta = :meta,
is_ecc = :is_ecc,
is_deleted = :is_deleted
WHERE id = :id`, certificate)
return err
}
// List will return a list of certificates
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
var result ListResponse
var exampleModel Model
defaultSort := model.Sort{
Field: "name",
Direction: "ASC",
}
db := database.GetInstance()
if db == nil {
return result, errors.ErrDatabaseUnavailable
}
// Get count of items in this search
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
countRow := db.QueryRowx(query, params...)
var totalRows int
queryErr := countRow.Scan(&totalRows)
if queryErr != nil && queryErr != sql.ErrNoRows {
logger.Debug("%s -- %+v", query, params)
return result, queryErr
}
// Get rows
var items []Model
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
err := db.Select(&items, query, params...)
if err != nil {
logger.Debug("%s -- %+v", query, params)
return result, err
}
result = ListResponse{
Items: items,
Total: totalRows,
Limit: pageInfo.Limit,
Offset: pageInfo.Offset,
Sort: pageInfo.Sort,
Filter: filters,
}
return result, nil
}
// GetByStatus will select rows that are ready for requesting
func GetByStatus(status string) ([]Model, error) {
models := make([]Model, 0)
db := database.GetInstance()
query := fmt.Sprintf(`
SELECT
t.*
FROM "%s" t
INNER JOIN "certificate_authority" c ON c."id" = t."certificate_authority_id"
WHERE
t."type" IN ("http", "dns") AND
t."status" = ? AND
t."certificate_authority_id" > 0 AND
t."is_deleted" = 0
`, tableName)
params := []interface{}{StatusReady}
err := db.Select(&models, query, params...)
if err != nil && err != sql.ErrNoRows {
logger.Error("GetByStatusError", err)
logger.Debug("Query: %s -- %+v", query, params)
}
return models, err
}

View 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
}

View File

@ -0,0 +1,15 @@
package certificate
import (
"npm/internal/model"
)
// ListResponse is the JSON response for users list
type ListResponse struct {
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Sort []model.Sort `json:"sort"`
Filter []model.Filter `json:"filter,omitempty"`
Items []Model `json:"items,omitempty"`
}