Files
nginx-proxy-manager/backend/internal/acme/acmesh.go
Jamie Curnow 556f8b773b Work on acme.sh hander
and dns providers
2021-08-20 08:21:19 +10:00

151 lines
3.6 KiB
Go

package acme
// Some light reading:
// https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"npm/embed"
"npm/internal/config"
"npm/internal/entity/dnsprovider"
"npm/internal/logger"
)
var acmeShFile string
// 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 ""
}
// shExec executes the acme.sh with arguments
func shExec(args []string, envs []string) (string, error) {
if _, err := os.Stat(acmeShFile); os.IsNotExist(err) {
e := fmt.Errorf("%s does not exist", acmeShFile)
logger.Error("AcmeShError", e)
return "", e
}
logger.Debug("CMD: %s %v", acmeShFile, args)
// nolint: gosec
c := exec.Command(acmeShFile, args...)
c.Env = envs
b, e := c.Output()
if e != nil {
logger.Error("AcmeShError", fmt.Errorf("Command error: %s -- %v\n%+v", acmeShFile, args, e))
logger.Warn(string(b))
}
return string(b), e
}
// WriteAcmeSh this will write our embedded acme.sh script to the data directory
// and give it write permissions
func WriteAcmeSh() {
if config.Configuration.DataFolder == "" {
logger.Error("AcmeShWriteError", fmt.Errorf("Configuration folder location is not set"))
return
}
acmeShFile = filepath.Clean(fmt.Sprintf("%s/acme.sh", config.Configuration.DataFolder))
// nolint: gosec
if err := ioutil.WriteFile(acmeShFile, []byte(embed.AcmeSh), 0755); err != nil {
logger.Error("AcmeShWriteError", err)
} else {
logger.Info("Wrote %s", acmeShFile)
}
}
// RequestCert does all the heavy lifting
func RequestCert(domains []string, method, caBundle, outputFullchainFile, outputKeyFile string, dnsProvider *dnsprovider.Model) error {
// TODO log file location configurable
args, err := buildCertRequestArgs(domains, method, caBundle, outputFullchainFile, outputKeyFile, dnsProvider)
if err != nil {
return err
}
envs := make([]string, 0)
if dnsProvider != nil {
envs, err = dnsProvider.GetAcmeShEnvVars()
if err != nil {
return err
}
}
ret, err := shExec(args, envs)
if err != nil {
return err
}
logger.Debug("ret: %+v", ret)
return nil
}
// This is split out into it's own function so it's testable
func buildCertRequestArgs(domains []string, method, caBundle, outputFullchainFile, outputKeyFile string, dnsProvider *dnsprovider.Model) ([]string, error) {
// TODO log file location configurable
args := []string{"--issue", "--log", "/data/logs/acme.sh.log"}
if caBundle != "" {
args = append(args, "--ca-bundle", caBundle)
}
if outputFullchainFile != "" {
args = append(args, "--fullchain-file", outputFullchainFile)
}
if outputKeyFile != "" {
args = append(args, "--key-file", outputKeyFile)
}
// TODO webroot location configurable
webroot := "/data/acme/wellknown"
methodArgs := make([]string, 0)
switch method {
case "dns":
if dnsProvider == nil {
return nil, ErrDNSNeedsDNSProvider
}
methodArgs = append(methodArgs, "--dns", dnsProvider.AcmeShName)
case "http":
if dnsProvider != nil {
return nil, ErrHTTPHasDNSProvider
}
methodArgs = append(methodArgs, "-w", webroot)
default:
return nil, ErrMethodNotSupported
}
hasMethod := false
// Add domains to args
for _, domain := range domains {
args = append(args, "-d", domain)
// Method has to appear after first domain, but does not need to be repeated
// for other domains.
if !hasMethod {
args = append(args, methodArgs...)
hasMethod = true
}
}
return args, nil
}