Moved v3 code from NginxProxyManager/nginx-proxy-manager-3 to NginxProxyManager/nginx-proxy-manager
This commit is contained in:
200
backend/internal/acme/acmesh.go
Normal file
200
backend/internal/acme/acmesh.go
Normal file
@ -0,0 +1,200 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func getAcmeShFilePath() (string, error) {
|
||||
path, err := exec.LookPath("acme.sh")
|
||||
if err != nil {
|
||||
return path, fmt.Errorf("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.Output()
|
||||
|
||||
if e != nil {
|
||||
logger.Error("AcmeShError", fmt.Errorf("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
|
||||
}
|
204
backend/internal/acme/acmesh_test.go
Normal file
204
backend/internal/acme/acmesh_test.go
Normal file
@ -0,0 +1,204 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"npm/internal/config"
|
||||
"npm/internal/entity/certificateauthority"
|
||||
"npm/internal/entity/dnsprovider"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Tear up/down
|
||||
/*
|
||||
func TestMain(m *testing.M) {
|
||||
config.Init(&version, &commit, &sentryDSN)
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
*/
|
||||
|
||||
// TODO configurable
|
||||
const acmeLogFile = "/data/logs/acme.sh.log"
|
||||
|
||||
func TestBuildCertRequestArgs(t *testing.T) {
|
||||
type want struct {
|
||||
args []string
|
||||
err error
|
||||
}
|
||||
|
||||
wellknown := config.Configuration.Acmesh.GetWellknown()
|
||||
exampleKey := fmt.Sprintf("%s/example.com.key", config.Configuration.Acmesh.CertHome)
|
||||
exampleChain := fmt.Sprintf("%s/a.crt", config.Configuration.Acmesh.CertHome)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
domains []string
|
||||
method string
|
||||
outputFullchainFile string
|
||||
outputKeyFile string
|
||||
dnsProvider *dnsprovider.Model
|
||||
ca *certificateauthority.Model
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "http single domain",
|
||||
domains: []string{"example.com"},
|
||||
method: "http",
|
||||
outputFullchainFile: exampleChain,
|
||||
outputKeyFile: exampleKey,
|
||||
dnsProvider: nil,
|
||||
ca: nil,
|
||||
want: want{
|
||||
args: []string{
|
||||
"--issue",
|
||||
"--fullchain-file",
|
||||
exampleChain,
|
||||
"--key-file",
|
||||
exampleKey,
|
||||
"-d",
|
||||
"example.com",
|
||||
"-w",
|
||||
wellknown,
|
||||
"--log",
|
||||
acmeLogFile,
|
||||
"--debug",
|
||||
"2",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "http multiple domains",
|
||||
domains: []string{"example.com", "example-two.com", "example-three.com"},
|
||||
method: "http",
|
||||
outputFullchainFile: exampleChain,
|
||||
outputKeyFile: exampleKey,
|
||||
dnsProvider: nil,
|
||||
ca: nil,
|
||||
want: want{
|
||||
args: []string{
|
||||
"--issue",
|
||||
"--fullchain-file",
|
||||
exampleChain,
|
||||
"--key-file",
|
||||
exampleKey,
|
||||
"-d",
|
||||
"example.com",
|
||||
"-w",
|
||||
wellknown,
|
||||
"-d",
|
||||
"example-two.com",
|
||||
"-d",
|
||||
"example-three.com",
|
||||
"--log",
|
||||
acmeLogFile,
|
||||
"--debug",
|
||||
"2",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "http single domain with dns provider",
|
||||
domains: []string{"example.com"},
|
||||
method: "http",
|
||||
outputFullchainFile: exampleChain,
|
||||
outputKeyFile: exampleKey,
|
||||
dnsProvider: &dnsprovider.Model{
|
||||
AcmeshName: "dns_cf",
|
||||
},
|
||||
ca: nil,
|
||||
want: want{
|
||||
args: nil,
|
||||
err: ErrHTTPHasDNSProvider,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dns single domain",
|
||||
domains: []string{"example.com"},
|
||||
method: "dns",
|
||||
outputFullchainFile: exampleChain,
|
||||
outputKeyFile: exampleKey,
|
||||
dnsProvider: &dnsprovider.Model{
|
||||
AcmeshName: "dns_cf",
|
||||
},
|
||||
ca: nil,
|
||||
want: want{
|
||||
args: []string{
|
||||
"--issue",
|
||||
"--fullchain-file",
|
||||
exampleChain,
|
||||
"--key-file",
|
||||
exampleKey,
|
||||
"-d",
|
||||
"example.com",
|
||||
"--dns",
|
||||
"dns_cf",
|
||||
"--log",
|
||||
acmeLogFile,
|
||||
"--debug",
|
||||
"2",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dns multiple domains",
|
||||
domains: []string{"example.com", "example-two.com", "example-three.com"},
|
||||
method: "dns",
|
||||
outputFullchainFile: exampleChain,
|
||||
outputKeyFile: exampleKey,
|
||||
dnsProvider: &dnsprovider.Model{
|
||||
AcmeshName: "dns_cf",
|
||||
},
|
||||
ca: nil,
|
||||
want: want{
|
||||
args: []string{
|
||||
"--issue",
|
||||
"--fullchain-file",
|
||||
exampleChain,
|
||||
"--key-file",
|
||||
exampleKey,
|
||||
"-d",
|
||||
"example.com",
|
||||
"--dns",
|
||||
"dns_cf",
|
||||
"-d",
|
||||
"example-two.com",
|
||||
"-d",
|
||||
"example-three.com",
|
||||
"--log",
|
||||
acmeLogFile,
|
||||
"--debug",
|
||||
"2",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dns single domain no provider",
|
||||
domains: []string{"example.com"},
|
||||
method: "dns",
|
||||
outputFullchainFile: exampleChain,
|
||||
outputKeyFile: exampleKey,
|
||||
dnsProvider: nil,
|
||||
ca: nil,
|
||||
want: want{
|
||||
args: nil,
|
||||
err: ErrDNSNeedsDNSProvider,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
args, err := buildCertRequestArgs(tt.domains, tt.method, tt.outputFullchainFile, tt.outputKeyFile, tt.dnsProvider, tt.ca, false)
|
||||
|
||||
assert.Equal(t, tt.want.args, args)
|
||||
assert.Equal(t, tt.want.err, err)
|
||||
})
|
||||
}
|
||||
}
|
10
backend/internal/acme/errors.go
Normal file
10
backend/internal/acme/errors.go
Normal file
@ -0,0 +1,10 @@
|
||||
package acme
|
||||
|
||||
import "errors"
|
||||
|
||||
// All errors relating to Acme.sh use
|
||||
var (
|
||||
ErrDNSNeedsDNSProvider = errors.New("RequestCert dns method requires a dns provider")
|
||||
ErrHTTPHasDNSProvider = errors.New("RequestCert http method does not need a dns provider")
|
||||
ErrMethodNotSupported = errors.New("RequestCert method not supported")
|
||||
)
|
Reference in New Issue
Block a user