Work on acme.sh hander

and dns providers
This commit is contained in:
Jamie Curnow
2021-08-19 22:33:01 +10:00
parent 339ee13346
commit 556f8b773b
19 changed files with 518 additions and 81 deletions

View File

@@ -1,5 +1,8 @@
package acme
// Some light reading:
// https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
import (
"fmt"
"io/ioutil"
@@ -10,6 +13,7 @@ import (
"npm/embed"
"npm/internal/config"
"npm/internal/entity/dnsprovider"
"npm/internal/logger"
)
@@ -17,7 +21,7 @@ var acmeShFile string
// GetAcmeShVersion will return the acme.sh script version
func GetAcmeShVersion() string {
if r, err := shExec("--version"); err == nil {
if r, err := shExec([]string{"--version"}, nil); err == nil {
// modify the output
r = strings.Trim(r, "\n")
v := strings.Split(r, "\n")
@@ -27,7 +31,7 @@ func GetAcmeShVersion() string {
}
// shExec executes the acme.sh with arguments
func shExec(args ...string) (string, error) {
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)
@@ -37,6 +41,8 @@ func shExec(args ...string) (string, error) {
logger.Debug("CMD: %s %v", acmeShFile, args)
// nolint: gosec
c := exec.Command(acmeShFile, args...)
c.Env = envs
b, e := c.Output()
if e != nil {
@@ -65,26 +71,22 @@ func WriteAcmeSh() {
}
// RequestCert does all the heavy lifting
func RequestCert(domains []string, method string) error {
args := []string{"--issue"}
webroot := "/home/wwwroot/example.com"
// Add domains to args
for _, domain := range domains {
args = append(args, "-d", domain)
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
}
switch method {
// case "dns":
case "http":
args = append(args, "-w", webroot)
default:
return fmt.Errorf("RequestCert method not supported: %s", method)
envs := make([]string, 0)
if dnsProvider != nil {
envs, err = dnsProvider.GetAcmeShEnvVars()
if err != nil {
return err
}
}
ret, err := shExec(args...)
ret, err := shExec(args, envs)
if err != nil {
return err
}
@@ -93,3 +95,56 @@ func RequestCert(domains []string, method string) error {
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
}

View File

@@ -0,0 +1,190 @@
package acme
import (
"testing"
"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"
const acmeWebroot = "/data/acme/wellknown"
func TestBuildCertRequestArgs(t *testing.T) {
type want struct {
args []string
err error
}
tests := []struct {
name string
domains []string
method string
caBundle string
outputFullchainFile string
outputKeyFile string
dnsProvider *dnsprovider.Model
want want
}{
{
name: "http single domain",
domains: []string{"example.com"},
method: "http",
caBundle: "",
outputFullchainFile: "/data/acme/certs/a.crt",
outputKeyFile: "/data/acme/certs/example.com.key",
dnsProvider: nil,
want: want{
args: []string{
"--issue",
"--log",
acmeLogFile,
"--fullchain-file",
"/data/acme/certs/a.crt",
"--key-file",
"/data/acme/certs/example.com.key",
"-d",
"example.com",
"-w",
acmeWebroot,
},
err: nil,
},
},
{
name: "http multiple domains",
domains: []string{"example.com", "example-two.com", "example-three.com"},
method: "http",
caBundle: "",
outputFullchainFile: "/data/acme/certs/a.crt",
outputKeyFile: "/data/acme/certs/example.com.key",
dnsProvider: nil,
want: want{
args: []string{
"--issue",
"--log",
acmeLogFile,
"--fullchain-file",
"/data/acme/certs/a.crt",
"--key-file",
"/data/acme/certs/example.com.key",
"-d",
"example.com",
"-w",
acmeWebroot,
"-d",
"example-two.com",
"-d",
"example-three.com",
},
err: nil,
},
},
{
name: "http single domain with dns provider",
domains: []string{"example.com"},
method: "http",
caBundle: "",
outputFullchainFile: "/data/acme/certs/a.crt",
outputKeyFile: "/data/acme/certs/example.com.key",
dnsProvider: &dnsprovider.Model{
AcmeShName: "dns_cf",
},
want: want{
args: nil,
err: ErrHTTPHasDNSProvider,
},
},
{
name: "dns single domain",
domains: []string{"example.com"},
method: "dns",
caBundle: "",
outputFullchainFile: "/data/acme/certs/a.crt",
outputKeyFile: "/data/acme/certs/example.com.key",
dnsProvider: &dnsprovider.Model{
AcmeShName: "dns_cf",
},
want: want{
args: []string{
"--issue",
"--log",
acmeLogFile,
"--fullchain-file",
"/data/acme/certs/a.crt",
"--key-file",
"/data/acme/certs/example.com.key",
"-d",
"example.com",
"--dns",
"dns_cf",
},
err: nil,
},
},
{
name: "dns multiple domains",
domains: []string{"example.com", "example-two.com", "example-three.com"},
method: "dns",
caBundle: "",
outputFullchainFile: "/data/acme/certs/a.crt",
outputKeyFile: "/data/acme/certs/example.com.key",
dnsProvider: &dnsprovider.Model{
AcmeShName: "dns_cf",
},
want: want{
args: []string{
"--issue",
"--log",
acmeLogFile,
"--fullchain-file",
"/data/acme/certs/a.crt",
"--key-file",
"/data/acme/certs/example.com.key",
"-d",
"example.com",
"--dns",
"dns_cf",
"-d",
"example-two.com",
"-d",
"example-three.com",
},
err: nil,
},
},
{
name: "dns single domain no provider",
domains: []string{"example.com"},
method: "dns",
caBundle: "",
outputFullchainFile: "/data/acme/certs/a.crt",
outputKeyFile: "/data/acme/certs/example.com.key",
dnsProvider: 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.caBundle, tt.outputFullchainFile, tt.outputKeyFile, tt.dnsProvider)
assert.Equal(t, tt.want.args, args)
assert.Equal(t, tt.want.err, err)
})
}
}

View 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")
)