|
|
//LEAPI - ACME Certificate Renewal Control API - Copyright 2022-2024 Ruel Tmeizeh All Rights Reserved
|
|
|
package main
|
|
|
|
|
|
import (
|
|
|
"bytes"
|
|
|
"crypto/tls"
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
"io/ioutil"
|
|
|
"log"
|
|
|
"net/http"
|
|
|
"os"
|
|
|
"os/exec"
|
|
|
"path"
|
|
|
"strconv"
|
|
|
"strings"
|
|
|
"sync"
|
|
|
)
|
|
|
|
|
|
func writeDomains() error {
|
|
|
b := new(bytes.Buffer)
|
|
|
err := json.NewEncoder(b).Encode(domains)
|
|
|
if err != nil {
|
|
|
return errors.New("Couldn't encode domains list into JSON: " + err.Error())
|
|
|
}
|
|
|
|
|
|
err = ioutil.WriteFile(configDir+"/domains.json", b.Bytes(), 0644)
|
|
|
if err != nil {
|
|
|
return errors.New("Couldn't write domains file: " + configDir + "/domains.json")
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func writeServers() error {
|
|
|
b := new(bytes.Buffer)
|
|
|
err := json.NewEncoder(b).Encode(servers)
|
|
|
if err != nil {
|
|
|
return errors.New("Couldn't encode servers list into JSON: " + err.Error())
|
|
|
}
|
|
|
|
|
|
err = ioutil.WriteFile(configDir+"/servers.json", b.Bytes(), 0644)
|
|
|
if err != nil {
|
|
|
return errors.New("Couldn't write servers file: " + configDir + "/servers.json")
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func sendFileToAllServers(filePath string) error {
|
|
|
var theError error
|
|
|
numservers := len(servers)
|
|
|
c := make(chan string)
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
wg.Add(numservers)
|
|
|
for n := 0; n < numservers; n++ {
|
|
|
go func(c chan string) {
|
|
|
for {
|
|
|
srv, more := <-c
|
|
|
if more == false {
|
|
|
wg.Done()
|
|
|
return
|
|
|
}
|
|
|
|
|
|
log.Println("Parallel execution send file to server: " + srv + "...")
|
|
|
err := sendFileToServer(filePath, srv)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
theError = err
|
|
|
}
|
|
|
}
|
|
|
}(c)
|
|
|
}
|
|
|
for _, server := range servers { //send each server to the channel
|
|
|
if server == appconf.Hostname { //don't send myself
|
|
|
continue
|
|
|
}
|
|
|
c <- server
|
|
|
}
|
|
|
close(c)
|
|
|
wg.Wait()
|
|
|
if theError == nil {
|
|
|
log.Println("Finished sending file " + filePath + " to all servers.")
|
|
|
}
|
|
|
|
|
|
err := os.Remove(filePath)
|
|
|
if err != nil {
|
|
|
log.Println("Error deleting temporary file: " + filePath + " - " + err.Error())
|
|
|
}
|
|
|
return theError //if any one or more fail, return an error for it (the last one that fails)
|
|
|
}
|
|
|
|
|
|
func sendFileToServer(filePath, server string) error {
|
|
|
log.Println("Send file " + filePath + " to " + server + " starting...")
|
|
|
|
|
|
_, fileName := path.Split(filePath)
|
|
|
dest := strings.SplitN(fileName, "__", 2)[0] //cert__abcdef1234567890.tmpfile --> "cert"
|
|
|
data, err := os.Open(filePath)
|
|
|
if err != nil {
|
|
|
return errors.New("sendFileToServer: Could not open temporary file " + filePath + ": " + err.Error())
|
|
|
}
|
|
|
url := syncScheme + server + ":" + syncPort + "/api/file/upload/" + dest
|
|
|
log.Println("Send file " + filePath + " to " + url + "...")
|
|
|
|
|
|
req, err := http.NewRequest("PUT", url, data)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't create new HTTP file upload request for server: " + server)
|
|
|
}
|
|
|
req.Close = true
|
|
|
req.Header.Set("User-Agent", myUserAgent)
|
|
|
req.SetBasicAuth(appconf.Username, appconf.SecretKey)
|
|
|
//req.Header.Set("Authorization", "Bearer "+appconf.SecretKey)
|
|
|
//skip verification of cert, since the cert may not be setup properly at first
|
|
|
customTransport := http.DefaultTransport.(*http.Transport).Clone()
|
|
|
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
|
client := &http.Client{Transport: customTransport, Timeout: timeout}
|
|
|
//client := &http.Client{Timeout: timeout}
|
|
|
response, err := client.Do(req)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't do HTTP file upload to server: " + server)
|
|
|
}
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't parse response body on request to server: " + server)
|
|
|
}
|
|
|
if response.StatusCode != 200 {
|
|
|
errorString := "Problem uploading file to server " + server + ". Status code: " + strconv.Itoa(response.StatusCode) + " Body: " + string(body)
|
|
|
log.Println(errorString)
|
|
|
return errors.New(errorString)
|
|
|
}
|
|
|
log.Println("Upload [" + dest + "] to " + server + " success!")
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func renew() error {
|
|
|
log.Println("Renew operation initiated...")
|
|
|
//BUILD/SET GETSSL ENVIRONMENT VARIABLES THEN EXECUTE GETSSL
|
|
|
|
|
|
//domain list
|
|
|
var domainlist string
|
|
|
for _, d := range domains {
|
|
|
if d == appconf.PrimaryDomain { //ignore primary domain
|
|
|
continue
|
|
|
}
|
|
|
domainlist = domainlist + "," + d
|
|
|
}
|
|
|
domainlist = strings.TrimLeft(domainlist, ",") //Take off leading comma
|
|
|
err := os.Setenv("SANS", domainlist)
|
|
|
if err != nil {
|
|
|
return errors.New("RENEW: error setting SANS domains list environment variable: " + err.Error())
|
|
|
}
|
|
|
if appconf.Debug {
|
|
|
log.Println(domainlist)
|
|
|
}
|
|
|
|
|
|
//ACL string
|
|
|
aclstring := appconf.LetsEncryptValidationPath
|
|
|
if appconf.SyncType == "ssh" {
|
|
|
for _, server := range servers {
|
|
|
if server == appconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
aclstring += ";ssh:" + appconf.Username + "@" + server + ":" + appconf.LetsEncryptValidationPath
|
|
|
//aclstring += ";davs:leapi:" + appconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/"
|
|
|
}
|
|
|
} else { //file sync type is HTTPS
|
|
|
aclstring += ";davs:" + appconf.Username + ":" + appconf.SecretKey + ":" + appconf.Hostname + ":" + appconf.HTTPS_ServerPort + ":/api/file/sync"
|
|
|
//aclstring += " ;cmd:\"curl -s -k -u" + appconf.Username + "\\:" + appconf.SecretKey + " " + syncScheme + appconf.Hostname + ":" + syncPort + "/api/file/upload/" + "$destfile" + " -T $src \"" + ":" + appconf.LetsEncryptValidationPath
|
|
|
}
|
|
|
err = os.Setenv("ACL", aclstring)
|
|
|
if err != nil {
|
|
|
return errors.New("RENEW: error setting ACL environment variable: " + err.Error())
|
|
|
}
|
|
|
|
|
|
if appconf.Debug {
|
|
|
log.Println("ACL STRING:")
|
|
|
log.Println(aclstring)
|
|
|
}
|
|
|
|
|
|
//Cert and key locations
|
|
|
domain_cert_location := appconf.TLSCertFile
|
|
|
if appconf.SyncType == "ssh" {
|
|
|
for _, server := range servers {
|
|
|
if server == appconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
domain_cert_location += ";ssh:" + appconf.Username + "@" + server + ":" + appconf.TLSCertFile
|
|
|
//domain_cert_location += ";davs:leapi:" + appconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/cert"
|
|
|
}
|
|
|
} else { //file sync type is HTTPS
|
|
|
domain_cert_location += ";davs:" + appconf.Username + ":" + appconf.SecretKey + ":" + appconf.Hostname + ":" + appconf.HTTPS_ServerPort + ":/api/file/sync/cert"
|
|
|
}
|
|
|
err = os.Setenv("DOMAIN_CERT_LOCATION", domain_cert_location)
|
|
|
if err != nil {
|
|
|
return errors.New("RENEW: error setting DOMAIN_CERT_LOCATION environment variable: " + err.Error())
|
|
|
}
|
|
|
|
|
|
domain_key_location := appconf.TLSKeyFile
|
|
|
if appconf.SyncType == "ssh" {
|
|
|
for _, server := range servers {
|
|
|
if server == appconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
domain_key_location += ";ssh:" + appconf.Username + "@" + server + ":" + appconf.TLSKeyFile
|
|
|
//domain_key_location += ";davs:leapi:" + appconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/key"
|
|
|
}
|
|
|
} else { //file sync type is HTTPS
|
|
|
domain_key_location += ";davs:" + appconf.Username + ":" + appconf.SecretKey + ":" + appconf.Hostname + ":" + appconf.HTTPS_ServerPort + ":/api/file/sync/key"
|
|
|
}
|
|
|
err = os.Setenv("DOMAIN_KEY_LOCATION", domain_key_location)
|
|
|
if err != nil {
|
|
|
return errors.New("RENEW: error setting DOMAIN_KEY_LOCATION environment variable: " + err.Error())
|
|
|
}
|
|
|
|
|
|
domain_chain_location := appconf.TLSChainFile
|
|
|
if appconf.SyncType == "ssh" {
|
|
|
for _, server := range servers {
|
|
|
if server == appconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
domain_chain_location += ";ssh:" + appconf.Username + "@" + server + ":" + appconf.TLSChainFile
|
|
|
//domain_chain_location += ";davs:leapi:" + appconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/chain"
|
|
|
}
|
|
|
} else { //file sync type is HTTPS
|
|
|
domain_chain_location += ";davs:" + appconf.Username + ":" + appconf.SecretKey + ":" + appconf.Hostname + ":" + appconf.HTTPS_ServerPort + ":/api/file/sync/chain"
|
|
|
}
|
|
|
err = os.Setenv("DOMAIN_CHAIN_LOCATION", domain_chain_location)
|
|
|
if err != nil {
|
|
|
return errors.New("RENEW: error setting DOMAIN_CHAIN_LOCATION environment variable: " + err.Error())
|
|
|
}
|
|
|
|
|
|
domain_pem_location := appconf.TLSPEMFile
|
|
|
if appconf.SyncType == "ssh" {
|
|
|
for _, server := range servers {
|
|
|
if server == appconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
domain_pem_location += ";ssh:" + appconf.Username + "@" + server + ":" + appconf.TLSPEMFile
|
|
|
//domain_pem_location += ";davs:leapi:" + appconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/pem"
|
|
|
}
|
|
|
} else { //file sync type is HTTPS
|
|
|
domain_pem_location += ";davs:" + appconf.Username + ":" + appconf.SecretKey + ":" + appconf.Hostname + ":" + appconf.HTTPS_ServerPort + ":/api/file/sync/pem"
|
|
|
}
|
|
|
err = os.Setenv("DOMAIN_PEM_LOCATION", domain_pem_location)
|
|
|
if err != nil {
|
|
|
return errors.New("RENEW: error setting DOMAIN_PEM_LOCATION environment variable: " + err.Error())
|
|
|
}
|
|
|
|
|
|
//these parameters don't seem to be respected by gettssl from environment variables, so write them to config file:
|
|
|
ca_cert_location := appconf.TLSCAFile
|
|
|
if appconf.SyncType == "ssh" {
|
|
|
for _, server := range servers {
|
|
|
if server == appconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
ca_cert_location += ";ssh:" + appconf.Username + "@" + server + ":" + appconf.TLSCAFile
|
|
|
//ca_cert_location += ";davs:leapi:" + appconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/ca"
|
|
|
}
|
|
|
} else { //file sync type is HTTPS
|
|
|
ca_cert_location += ";davs:" + appconf.Username + ":" + appconf.SecretKey + ":" + appconf.Hostname + ":" + appconf.HTTPS_ServerPort + ":/api/file/sync/ca"
|
|
|
}
|
|
|
|
|
|
reload_command := appconf.ReloadCommand
|
|
|
for _, server := range servers {
|
|
|
if server == appconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
//old ssh method; requires ssh key
|
|
|
//reload_command += "; ssh " + appconf.Username + "@" + server + " '" + appconf.ReloadCommand + "'"
|
|
|
//new method; calls LEAPI to trigger reload
|
|
|
reload_command += " ; curl -s -k -X POST -H 'Authorization: Bearer " + appconf.SecretKey + "' " + syncScheme + server + "/api/reload"
|
|
|
//reload_command += "; 'curl -s -X POST -H \\\"Authorization: Bearer " + appconf.SecretKey + "\\\" " + syncScheme + server + "/api/reload"
|
|
|
}
|
|
|
|
|
|
ca_server := "https://acme-staging-v02.api.letsencrypt.org"
|
|
|
if appconf.Production {
|
|
|
ca_server = "https://acme-v02.api.letsencrypt.org"
|
|
|
}
|
|
|
|
|
|
var configFile string
|
|
|
|
|
|
configFile = "CA=\"" + ca_server + "\"\n"
|
|
|
configFile += "USE_SINGLE_ACL=\"true\"\n"
|
|
|
configFile += "CA_CERT_LOCATION=\"" + appconf.TLSCAFile + "\"\n"
|
|
|
configFile += "RELOAD_CMD=\"" + reload_command + "\"\n"
|
|
|
configFile += "RENEW_ALLOW=\"" + appconf.RenewAllow + "\"\n"
|
|
|
configFile += "CHECK_REMOTE=\"true\"\n"
|
|
|
configFile += "SERVER_TYPE=\"" + appconf.CheckPort + "\"\n"
|
|
|
configFile += "CHECK_REMOTE_WAIT=\"5\"\n"
|
|
|
|
|
|
//write config file
|
|
|
err = ioutil.WriteFile(configDir+"/"+appconf.PrimaryDomain+"/getssl.cfg", []byte(configFile), 0644)
|
|
|
if err != nil {
|
|
|
return errors.New("Couldn't write getssl config file: " + configDir + "/" + appconf.PrimaryDomain + "/getssl.cfg")
|
|
|
}
|
|
|
|
|
|
if appconf.Debug {
|
|
|
//////PRINT VARS
|
|
|
for _, e := range os.Environ() {
|
|
|
log.Println(e)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//RUN GETSSL
|
|
|
//first patch getssl to disable cert verification checking:
|
|
|
cmd := exec.Command("/usr/bin/sed", "-i", "s/NOMETER} -u/NOMETER} -k -u/g", appconf.SrvDir+"/getssl")
|
|
|
output, err := cmd.CombinedOutput()
|
|
|
if err != nil {
|
|
|
log.Println(string(output))
|
|
|
return errors.New("RENEW: patching of getssl to disable curl certificate verification during LEAPI sync failed: " + err.Error())
|
|
|
}
|
|
|
//RUN getssl on primary domain to renew
|
|
|
//cmd = exec.Command(appconf.SrvDir+"/getssl", "-u", "-w", appconf.SrvDir, appconf.PrimaryDomain)
|
|
|
if appconf.Debug {
|
|
|
cmd = exec.Command(appconf.SrvDir+"/getssl", "-d", "-w", appconf.SrvDir, appconf.PrimaryDomain)
|
|
|
} else {
|
|
|
cmd = exec.Command(appconf.SrvDir+"/getssl", "-w", appconf.SrvDir, appconf.PrimaryDomain)
|
|
|
}
|
|
|
output, err = cmd.CombinedOutput()
|
|
|
if err != nil {
|
|
|
log.Println("BEGIN GETSSL OUTPUT:")
|
|
|
log.Println(string(output))
|
|
|
log.Println("END GETSSL OUTPUT")
|
|
|
return errors.New("RENEW: execution of getssl failed: " + err.Error() + " Check log file " + appconf.LogFile + " for more details.")
|
|
|
}
|
|
|
log.Println("BEGIN GETSSL OUTPUT:")
|
|
|
log.Println(string(output))
|
|
|
log.Println("END GETSSL OUTPUT")
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func reload() error {
|
|
|
//To avoid problems with spaces in the command, we build a script file and run it in shell, rather than directly.
|
|
|
reloadScript := "#!/bin/sh\n"
|
|
|
reloadScript += appconf.ReloadCommand + "\n"
|
|
|
|
|
|
//write script file to run reload command[s]
|
|
|
err := ioutil.WriteFile(configDir+"/reloadscript.sh", []byte(reloadScript), 0755)
|
|
|
if err != nil {
|
|
|
return errors.New("Couldn't write reload script file: " + configDir + "/reloadscript.sh")
|
|
|
}
|
|
|
|
|
|
cmd := exec.Command(appconf.SrvDir + "/reloadscript.sh")
|
|
|
output, err := cmd.CombinedOutput()
|
|
|
if err != nil {
|
|
|
log.Println("BEGIN RELOADSCRIPT OUTPUT:")
|
|
|
log.Println(string(output))
|
|
|
log.Println("END RELOADSCRIPT OUTPUT")
|
|
|
return errors.New("RELOAD: execution of reload script failed: " + err.Error())
|
|
|
}
|
|
|
log.Println("BEGIN RELOADSCRIPT OUTPUT:")
|
|
|
log.Println(string(output))
|
|
|
log.Println("END RELOADSCRIPT OUTPUT")
|
|
|
|
|
|
return nil
|
|
|
}
|