mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-31 15:53:33 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			203 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			203 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package acme
 | |
| 
 | |
| // Some light reading:
 | |
| // https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 	"strings"
 | |
| 
 | |
| 	"npm/internal/config"
 | |
| 	"npm/internal/entity/certificateauthority"
 | |
| 	"npm/internal/entity/dnsprovider"
 | |
| 	"npm/internal/logger"
 | |
| 
 | |
| 	"github.com/rotisserie/eris"
 | |
| )
 | |
| 
 | |
| func getAcmeShFilePath() (string, error) {
 | |
| 	path, err := exec.LookPath("acme.sh")
 | |
| 	if err != nil {
 | |
| 		return path, eris.Wrapf(err, "Cannot find acme.sh execuatable script in PATH")
 | |
| 	}
 | |
| 	return path, nil
 | |
| }
 | |
| 
 | |
| func getCommonEnvVars() []string {
 | |
| 	return []string{
 | |
| 		fmt.Sprintf("ACMESH_CONFIG_HOME=%s", os.Getenv("ACMESH_CONFIG_HOME")),
 | |
| 		fmt.Sprintf("ACMESH_HOME=%s", os.Getenv("ACMESH_HOME")),
 | |
| 		fmt.Sprintf("CERT_HOME=%s", os.Getenv("CERT_HOME")),
 | |
| 		fmt.Sprintf("LE_CONFIG_HOME=%s", os.Getenv("LE_CONFIG_HOME")),
 | |
| 		fmt.Sprintf("LE_WORKING_DIR=%s", os.Getenv("LE_WORKING_DIR")),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // GetAcmeShVersion will return the acme.sh script version
 | |
| func GetAcmeShVersion() string {
 | |
| 	if r, err := shExec([]string{"--version"}, nil); err == nil {
 | |
| 		// modify the output
 | |
| 		r = strings.Trim(r, "\n")
 | |
| 		v := strings.Split(r, "\n")
 | |
| 		return v[len(v)-1]
 | |
| 	}
 | |
| 	return ""
 | |
| }
 | |
| 
 | |
| // CreateAccountKey is required for each server initially
 | |
| func CreateAccountKey(ca *certificateauthority.Model) error {
 | |
| 	args := []string{"--create-account-key", "--accountkeylength", "2048"}
 | |
| 	if ca != nil {
 | |
| 		logger.Info("Acme.sh CreateAccountKey for %s", ca.AcmeshServer)
 | |
| 		args = append(args, "--server", ca.AcmeshServer)
 | |
| 		if ca.CABundle != "" {
 | |
| 			args = append(args, "--ca-bundle", ca.CABundle)
 | |
| 		}
 | |
| 	} else {
 | |
| 		logger.Info("Acme.sh CreateAccountKey")
 | |
| 	}
 | |
| 
 | |
| 	args = append(args, getCommonArgs()...)
 | |
| 	ret, err := shExec(args, nil)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	logger.Debug("CreateAccountKey returned:\n%+v", ret)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // RequestCert does all the heavy lifting
 | |
| func RequestCert(domains []string, method, outputFullchainFile, outputKeyFile string, dnsProvider *dnsprovider.Model, ca *certificateauthority.Model, force bool) (string, error) {
 | |
| 	args, err := buildCertRequestArgs(domains, method, outputFullchainFile, outputKeyFile, dnsProvider, ca, force)
 | |
| 	if err != nil {
 | |
| 		return err.Error(), err
 | |
| 	}
 | |
| 
 | |
| 	envs := make([]string, 0)
 | |
| 	if dnsProvider != nil {
 | |
| 		envs, err = dnsProvider.GetAcmeShEnvVars()
 | |
| 		if err != nil {
 | |
| 			return err.Error(), err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	ret, err := shExec(args, envs)
 | |
| 	if err != nil {
 | |
| 		return ret, err
 | |
| 	}
 | |
| 
 | |
| 	return "", nil
 | |
| }
 | |
| 
 | |
| // shExec executes the acme.sh with arguments
 | |
| func shExec(args []string, envs []string) (string, error) {
 | |
| 	acmeSh, err := getAcmeShFilePath()
 | |
| 	if err != nil {
 | |
| 		logger.Error("AcmeShError", err)
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	logger.Debug("CMD: %s %v", acmeSh, args)
 | |
| 	// nolint: gosec
 | |
| 	c := exec.Command(acmeSh, args...)
 | |
| 	c.Env = append(getCommonEnvVars(), envs...)
 | |
| 
 | |
| 	b, e := c.CombinedOutput()
 | |
| 
 | |
| 	if e != nil {
 | |
| 		// logger.Error("AcmeShError", eris.Wrapf(e, "Command error: %s -- %v\n%+v", acmeSh, args, e))
 | |
| 		logger.Warn(string(b))
 | |
| 	}
 | |
| 
 | |
| 	return string(b), e
 | |
| }
 | |
| 
 | |
| func getCommonArgs() []string {
 | |
| 	args := make([]string, 0)
 | |
| 
 | |
| 	if config.Configuration.Acmesh.Home != "" {
 | |
| 		args = append(args, "--home", config.Configuration.Acmesh.Home)
 | |
| 	}
 | |
| 	if config.Configuration.Acmesh.ConfigHome != "" {
 | |
| 		args = append(args, "--config-home", config.Configuration.Acmesh.ConfigHome)
 | |
| 	}
 | |
| 	if config.Configuration.Acmesh.CertHome != "" {
 | |
| 		args = append(args, "--cert-home", config.Configuration.Acmesh.CertHome)
 | |
| 	}
 | |
| 
 | |
| 	args = append(args, "--log", "/data/logs/acme.sh.log")
 | |
| 	args = append(args, "--debug", "2")
 | |
| 
 | |
| 	return args
 | |
| }
 | |
| 
 | |
| // This is split out into it's own function so it's testable
 | |
| func buildCertRequestArgs(domains []string, method, outputFullchainFile, outputKeyFile string, dnsProvider *dnsprovider.Model, ca *certificateauthority.Model, force bool) ([]string, error) {
 | |
| 	// The argument order matters.
 | |
| 	// see https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert#3-multiple-domains-san-mode--hybrid-mode
 | |
| 	// for multiple domains and note that the method of validation is required just after the domain arg, each time.
 | |
| 
 | |
| 	// TODO log file location configurable
 | |
| 	args := []string{"--issue"}
 | |
| 
 | |
| 	if ca != nil {
 | |
| 		args = append(args, "--server", ca.AcmeshServer)
 | |
| 		if ca.CABundle != "" {
 | |
| 			args = append(args, "--ca-bundle", ca.CABundle)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if outputFullchainFile != "" {
 | |
| 		args = append(args, "--fullchain-file", outputFullchainFile)
 | |
| 	}
 | |
| 
 | |
| 	if outputKeyFile != "" {
 | |
| 		args = append(args, "--key-file", outputKeyFile)
 | |
| 	}
 | |
| 
 | |
| 	methodArgs := make([]string, 0)
 | |
| 	switch method {
 | |
| 	case "dns":
 | |
| 		if dnsProvider == nil {
 | |
| 			return nil, ErrDNSNeedsDNSProvider
 | |
| 		}
 | |
| 		methodArgs = append(methodArgs, "--dns", dnsProvider.AcmeshName)
 | |
| 		if dnsProvider.DNSSleep > 0 {
 | |
| 			// See: https://github.com/acmesh-official/acme.sh/wiki/dnscheck
 | |
| 			methodArgs = append(methodArgs, "--dnssleep", fmt.Sprintf("%d", dnsProvider.DNSSleep))
 | |
| 		}
 | |
| 
 | |
| 	case "http":
 | |
| 		if dnsProvider != nil {
 | |
| 			return nil, ErrHTTPHasDNSProvider
 | |
| 		}
 | |
| 		methodArgs = append(methodArgs, "-w", config.Configuration.Acmesh.GetWellknown())
 | |
| 	default:
 | |
| 		return nil, ErrMethodNotSupported
 | |
| 	}
 | |
| 
 | |
| 	hasMethod := false
 | |
| 
 | |
| 	// Add domains to args
 | |
| 	for _, domain := range domains {
 | |
| 		args = append(args, "-d", domain)
 | |
| 		// Method has to appear after each domain
 | |
| 		if !hasMethod {
 | |
| 			args = append(args, methodArgs...)
 | |
| 			hasMethod = true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if force {
 | |
| 		args = append(args, "--force")
 | |
| 	}
 | |
| 
 | |
| 	args = append(args, getCommonArgs()...)
 | |
| 
 | |
| 	return args, nil
 | |
| }
 |