Browse Source

Split into separate files.

master
Ruel Tmeizeh - RuhNet 2 years ago
parent
commit
918fff9f39
5 changed files with 880 additions and 842 deletions
  1. +1
    -0
      .gitignore
  2. +361
    -0
      actions.go
  3. +308
    -0
      api.go
  4. +27
    -842
      main.go
  5. +183
    -0
      sync.go

+ 1
- 0
.gitignore View File

@ -1,4 +1,5 @@
getssl
leapi
leapi_config.json

+ 361
- 0
actions.go View File

@ -0,0 +1,361 @@
//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
}

+ 308
- 0
api.go View File

@ -0,0 +1,308 @@
//LEAPI - ACME Certificate Renewal Control API - Copyright 2022-2024 Ruel Tmeizeh All Rights Reserved
package main
import (
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"github.com/labstack/echo/v4"
)
/////////////////////////////////////////////
///// API ROUTE FUNCTIONS
/////////////////////////////////////////////
func nothingResponse(c echo.Context) error {
return c.NoContent(http.StatusNotFound)
}
func uptimeCheck(c echo.Context) error {
if c.Request().Method == http.MethodHead {
return c.NoContent(http.StatusOK)
}
//return c.String(http.StatusOK, "{\"up\":true}")
return c.JSON(http.StatusOK, uptime())
}
func apiRenew(c echo.Context) error {
err := renew()
if err != nil {
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
}
return c.JSON(okOut())
}
func apiReload(c echo.Context) error {
err := reload()
if err != nil {
return c.JSON(errorOut(http.StatusInternalServerError, "Error reloading services: "+err.Error()))
}
return c.JSON(okOut())
}
func apiUpload(c echo.Context) error {
fileType := c.Param("fileType")
r := c.Request()
var filePath string
switch fileType {
case "ca":
filePath = appconf.TLSCAFile
case "chain":
filePath = appconf.TLSChainFile
case "key":
filePath = appconf.TLSKeyFile
case "cert":
filePath = appconf.TLSCertFile
case "pem":
filePath = appconf.TLSPEMFile
default: //ACL
//return c.JSON(errorOut(http.StatusBadRequest, "Invalid filetype/URL."))
filePath = appconf.LetsEncryptValidationPath + "/" + fileType
}
directory, _ := path.Split(filePath)
//Check and create directory
if _, err := os.Stat(directory); os.IsNotExist(err) {
err = os.MkdirAll(directory, 0755)
if err != nil {
return c.JSON(errorOut(http.StatusInternalServerError, "Filetype "+fileType+" directory does not exist, and could not create: "+err.Error()))
}
}
//Read the upload data
var blimit int64 = 102400 //100k max upload size
body, err := ioutil.ReadAll(io.LimitReader(r.Body, blimit))
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error reading post body: "+err.Error()))
}
//Write the file
err = ioutil.WriteFile(filePath, body, 0644)
if err != nil {
return c.JSON(errorOut(http.StatusInternalServerError, "Could not write file: "+err.Error()))
}
log.Println("Received PUT to " + r.RequestURI)
log.Println("Writing to " + filePath)
return c.JSON(okOut())
}
func apiUploadSync(c echo.Context) error {
fileType := c.Param("fileType")
r := c.Request()
//Read the upload data
var blimit int64 = 102400 //100k max upload size
body, err := ioutil.ReadAll(io.LimitReader(r.Body, blimit))
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error reading post body: "+err.Error()))
}
uuid := generateUUID()
filePath := appconf.SrvDir + "/" + fileType + "__" + uuid + ".tmpfile"
//Write the file
err = ioutil.WriteFile(filePath, body, 0644)
if err != nil {
return c.JSON(errorOut(http.StatusInternalServerError, "Could not write temporary file: "+err.Error()))
}
log.Println("Received PUT for sync to " + r.RequestURI)
log.Println("Writing to " + filePath)
err = sendFileToAllServers(filePath)
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error sending file "+filePath+" to other servers: "+err.Error()))
}
return c.JSON(okOut())
}
func apiListDomains(c echo.Context) error {
var out APIOutput
out.Status = http.StatusOK
out.Message = "domains list"
out.Data = domains
return c.JSON(out.Status, out)
}
func apiPutDomain(c echo.Context) error {
domain := c.Param("domain")
//check for dups
for _, d := range domains {
if d == domain {
return c.JSON(errorOut(http.StatusBadRequest, "Bad request: Domain already exists."))
}
}
//add domain to list
domains = append(domains, domain)
//write list to disk
err := writeDomains()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error writing domains list to disk: "+err.Error()))
}
//sync with other servers
err = syncAllServers()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing to other servers: "+err.Error()))
}
//renew cert
err = renew()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
}
return c.JSON(okOut())
}
func apiDeleteDomain(c echo.Context) error {
deleteDomain := c.Param("domain")
var newlist []string
for _, d := range domains {
if d != deleteDomain {
newlist = append(newlist, d)
}
}
domains = newlist
//write list to disk
err := writeDomains()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error writing domains list to disk: "+err.Error()))
}
//sync with other servers
err = syncAllServers()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing to other servers: "+err.Error()))
}
//renew cert
err = renew()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
}
return c.JSON(okOut())
}
func apiListServers(c echo.Context) error {
var out APIOutput
out.Status = http.StatusOK
out.Message = "servers list"
out.Data = servers
return c.JSON(out.Status, out)
}
func apiPutServer(c echo.Context) error {
server := c.Param("server")
//check for dups
for _, s := range servers {
if s == server {
return c.JSON(errorOut(http.StatusBadRequest, "Bad request: Server already exists."))
}
}
//add servers to list
servers = append(servers, server)
//write list to disk
err := writeServers()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error writing servers list to disk: "+err.Error()))
}
//sync with other servers
err = syncAllServers()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing to other servers: "+err.Error()))
}
//renew cert
err = renew()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
}
return c.JSON(okOut())
}
func apiDeleteServer(c echo.Context) error {
deleteServer := c.Param("server")
var newlist []string
for _, s := range servers {
if s != deleteServer {
newlist = append(newlist, s)
}
}
servers = newlist
//write list to disk
err := writeServers()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error writing servers list to disk: "+err.Error()))
}
//sync with other servers
err = syncAllServers()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing to other servers: "+err.Error()))
}
//renew cert
err = renew()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
}
return c.JSON(okOut())
}
func apiSync(c echo.Context) error {
host := c.Param("host")
log.Println("Received sync request for host: " + host + ". From IP address: " + c.RealIP() + " Syncing...")
err := syncServersFromHost(host)
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing servers from host: "+host+". "+err.Error()))
}
err = syncDomainsFromHost(host)
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing domains from host: "+host+". "+err.Error()))
}
return c.JSON(okOut())
}

+ 27
- 842
main.go View File

@ -1,24 +1,17 @@
//LEAPI - ACME Certificate Renewal Control API - Copyright 2022 Ruel Tmeizeh All Rights Reserved
//LEAPI - ACME Certificate Renewal Control API - Copyright 2022-2024 Ruel Tmeizeh All Rights Reserved
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"path"
"reflect"
"regexp"
"strconv"
"strings"
"sync"
"syscall"
@ -30,7 +23,7 @@ import (
"github.com/labstack/echo/v4/middleware"
)
const version string = "1.1.0"
const version string = "1.1.1"
const serverVersion string = "RuhNet LE API v" + version
const apiVersion int = 1
const website string = "https://ruhnet.co"
@ -38,7 +31,7 @@ const myUserAgent string = "RuhNet LE Cluster API Controller"
const timeout time.Duration = time.Duration(30 * time.Second) //Timeout for outbound requests. Adjust as needed.
var leapiconf LEAPIConfig
var appconf LEAPIConfig
var startupTime time.Time
var configDir string
@ -136,19 +129,19 @@ func main() {
re := regexp.MustCompile(`([\s]//.*)|(^//.*)`)
fileCleanedBytes := re.ReplaceAll(fileBytes, nil)
err = json.Unmarshal(fileCleanedBytes, &leapiconf) //populate the config struct with JSON data from the config file
err = json.Unmarshal(fileCleanedBytes, &appconf) //populate the config struct with JSON data from the config file
if err != nil {
log.Fatal("Could not parse config file: " + configFile + "\n" + err.Error())
}
leapiconf.checkConfig()
appconf.checkConfig()
log.Println("Configuration OK, starting LEAPI...")
fmt.Println()
leapiLogFile, err := os.OpenFile(leapiconf.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
leapiLogFile, err := os.OpenFile(appconf.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
if err != nil {
log.Println("Could not open log file: " + leapiconf.LogFile + "\n" + err.Error())
log.Println("Could not open log file: " + appconf.LogFile + "\n" + err.Error())
leapiLogFile, err = os.OpenFile("/tmp/leapi.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
if err != nil {
log.Fatal("Can't open even /tmp log file!\n" + err.Error())
@ -192,19 +185,19 @@ func main() {
}
}
syncPort = leapiconf.HTTP_ServerPort
if leapiconf.LetsEncryptValidationPath == "-" {
leapiconf.LetsEncryptValidationPath = leapiconf.SrvDir + "/acme-challenge"
syncPort = appconf.HTTP_ServerPort
if appconf.LetsEncryptValidationPath == "-" {
appconf.LetsEncryptValidationPath = appconf.SrvDir + "/acme-challenge"
}
if leapiconf.Hostname == "-" {
if appconf.Hostname == "-" {
hostname, err := os.Hostname()
if err != nil {
log.Fatal("Hostname could not be auto-detected from system: " + err.Error())
}
leapiconf.Hostname = hostname
appconf.Hostname = hostname
}
fmt.Println("My hostname: " + leapiconf.Hostname)
fmt.Println("My hostname: " + appconf.Hostname)
/////////////////////////////////////////////
//Echo config:
@ -246,7 +239,7 @@ func main() {
e.HEAD("/_up", uptimeCheck)
e.GET("/_up", uptimeCheck)
e.Static("/.well-known/acme-challenge", leapiconf.LetsEncryptValidationPath) //Lets Encrypt validation path
e.Static("/.well-known/acme-challenge", appconf.LetsEncryptValidationPath) //Lets Encrypt validation path
/////////////////////////////////
// API Routes //
@ -283,19 +276,19 @@ func main() {
// HTTP SERVERS CONFIG:
//TLS Server
if leapiconf.HTTPS_ServerPort != "-" { //disable HTTPS if port is zero
if appconf.HTTPS_ServerPort != "-" { //disable HTTPS if port is zero
syncScheme = "https://"
syncPort = leapiconf.HTTPS_ServerPort
syncPort = appconf.HTTPS_ServerPort
//certPair, err := tls.LoadX509KeyPair(leapiconf.TLSCertificateFile, leapiconf.TLSKeyFile)
if !fileExists(leapiconf.TLSChainFile) || !fileExists(leapiconf.TLSKeyFile) {
//certPair, err := tls.LoadX509KeyPair(appconf.TLSCertificateFile, appconf.TLSKeyFile)
if !fileExists(appconf.TLSChainFile) || !fileExists(appconf.TLSKeyFile) {
fmt.Println("Provided certificate and/or key file does not exist! Terminating.")
log.Fatal("Provided certificate and/or key file does not exist! Terminating.")
}
//Create loader for cert files
kpr, err := NewKeypairReloader(leapiconf.TLSChainFile, leapiconf.TLSKeyFile)
kpr, err := NewKeypairReloader(appconf.TLSChainFile, appconf.TLSKeyFile)
if err != nil {
log.Fatal(err)
}
@ -312,7 +305,7 @@ func main() {
}
srvTLS := &http.Server{
Addr: ":" + leapiconf.HTTPS_ServerPort,
Addr: ":" + appconf.HTTPS_ServerPort,
ReadTimeout: 180 * time.Second,
WriteTimeout: 180 * time.Second,
IdleTimeout: 180 * time.Second,
@ -327,7 +320,7 @@ func main() {
//HTTP Server
srvHTTP := &http.Server{
Addr: ":" + leapiconf.HTTP_ServerPort,
Addr: ":" + appconf.HTTP_ServerPort,
ReadTimeout: 180 * time.Second,
WriteTimeout: 180 * time.Second,
IdleTimeout: 180 * time.Second,
@ -366,9 +359,9 @@ func (f *LEAPIConfig) checkConfig() {
func serverHeaders(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderServer, serverVersion)
c.Response().Header().Set("Access-Control-Allow-Origin", leapiconf.FrontEndURL)
c.Response().Header().Set("Access-Control-Allow-Origin", appconf.FrontEndURL)
c.Response().Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE")
//c.Response().Header().Set("Access-Control-Allow-Headers", leapiconf.AllowedHeaders)
//c.Response().Header().Set("Access-Control-Allow-Headers", appconf.AllowedHeaders)
c.Response().Header().Set("Access-Control-Allow-Headers",
strings.Join([]string{
echo.HeaderOrigin,
@ -383,11 +376,11 @@ func serverHeaders(next echo.HandlerFunc) echo.HandlerFunc {
}
func apiKeyAuth(key string, c echo.Context) (bool, error) {
return (key == leapiconf.SecretKey), nil
return (key == appconf.SecretKey), nil
}
func apiBasicAuth(username, password string, c echo.Context) (bool, error) {
return ((username == leapiconf.Username) && (password == leapiconf.SecretKey)), nil
return ((username == appconf.Username) && (password == appconf.SecretKey)), nil
}
func NewKeypairReloader(certPath, keyPath string) (*keypairReloader, error) {
@ -404,8 +397,8 @@ func NewKeypairReloader(certPath, keyPath string) (*keypairReloader, error) {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP)
for range c {
log.Printf("Received SIGHUP, reloading TLS certificate and key from %q and %q", leapiconf.TLSChainFile, leapiconf.TLSKeyFile)
fmt.Printf("Received SIGHUP, reloading TLS certificate and key from %q and %q\n", leapiconf.TLSChainFile, leapiconf.TLSKeyFile)
log.Printf("Received SIGHUP, reloading TLS certificate and key from %q and %q", appconf.TLSChainFile, appconf.TLSKeyFile)
fmt.Printf("Received SIGHUP, reloading TLS certificate and key from %q and %q\n", appconf.TLSChainFile, appconf.TLSKeyFile)
if err := result.maybeReload(); err != nil {
log.Printf("Keeping old TLS certificate because the new one could not be loaded: %v", err)
fmt.Printf("Keeping old TLS certificate because the new one could not be loaded: %v", err)
@ -460,519 +453,6 @@ func fileExists(filename string) bool {
return !info.IsDir()
}
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 syncAllServers() 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 sync of server: " + srv + "...")
err := syncOneServer(srv)
if err != nil {
log.Println(err.Error())
theError = err
}
}
}(c)
}
for _, server := range servers { //send each server to the channel
if server == leapiconf.Hostname { //don't send myself
continue
}
c <- server
}
close(c)
wg.Wait()
log.Println("Finished sending sync requests.")
return theError //if any one or more fail, return an error for it (the last one that fails)
}
func syncOneServer(server string) error {
//Make http requests to each other servers' /sync endpoints
// https://server.tld:port/sync
log.Println("SYNC " + server + " starting...")
req, err := http.NewRequest("POST", syncScheme+server+":"+syncPort+"/api/sync/"+leapiconf.Hostname, nil)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't create new HTTP sync request for server: " + server)
}
req.Close = true
req.Header.Set("User-Agent", myUserAgent)
req.Header.Set("Authorization", "Bearer "+leapiconf.SecretKey)
//skip verification of cert for https syncing, 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 perform HTTP sync request 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 syncing to server " + server + ". Status code: " + strconv.Itoa(response.StatusCode) + " Body: " + string(body)
log.Println(errorString)
return errors.New(errorString)
}
log.Println("SYNC " + server + " success!")
return nil
}
func syncServersFromHost(host string) error {
var theError error
req, err := http.NewRequest("GET", syncScheme+host+":"+syncPort+"/api/servers", nil)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't create new HTTP request for syncing servers from host: " + host)
}
req.Close = true
req.Header.Set("User-Agent", myUserAgent)
req.Header.Set("Authorization", "Bearer "+leapiconf.SecretKey)
//skip verification of cert for https syncing, 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 perform HTTP server sync request to host: " + host)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't parse response body from server sync request to server: " + host)
}
if response.StatusCode != 200 {
theError = errors.New("Problem syncing servers from host " + host + ". Status code: " + strconv.Itoa(response.StatusCode) + " Body: " + string(body))
log.Println(theError.Error())
return theError
}
var result APIOutput
err = json.Unmarshal(body, &result)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't parse response body from host " + host + ": " + err.Error())
}
servers = result.Data
err = writeServers()
if err != nil {
log.Println(err.Error())
return err
}
return nil
}
func syncDomainsFromHost(host string) error {
var theError error
req, err := http.NewRequest("GET", syncScheme+host+":"+syncPort+"/api/domains", nil)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't create new HTTP request for syncing domains from host: " + host)
}
req.Close = true
req.Header.Set("User-Agent", myUserAgent)
req.Header.Set("Authorization", "Bearer "+leapiconf.SecretKey)
//skip verification of cert for https syncing, 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 perform HTTP domain sync request to host: " + host)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't parse response body from domain sync request to server: " + host)
}
if response.StatusCode != 200 {
theError = errors.New("Problem syncing domains from host " + host + ". Status code: " + strconv.Itoa(response.StatusCode) + " Body: " + string(body))
log.Println(theError.Error())
return theError
}
var result APIOutput
err = json.Unmarshal(body, &result)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't parse response body from host " + host + ": " + err.Error())
}
domains = result.Data
err = writeDomains()
if err != nil {
log.Println(err.Error())
return err
}
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 == leapiconf.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(leapiconf.Username, leapiconf.SecretKey)
//req.Header.Set("Authorization", "Bearer "+leapiconf.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 == leapiconf.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 leapiconf.Debug {
log.Println(domainlist)
}
//ACL string
aclstring := leapiconf.LetsEncryptValidationPath
if leapiconf.SyncType == "ssh" {
for _, server := range servers {
if server == leapiconf.Hostname {
continue
}
aclstring += ";ssh:" + leapiconf.Username + "@" + server + ":" + leapiconf.LetsEncryptValidationPath
//aclstring += ";davs:leapi:" + leapiconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/"
}
} else { //file sync type is HTTPS
aclstring += ";davs:" + leapiconf.Username + ":" + leapiconf.SecretKey + ":" + leapiconf.Hostname + ":" + leapiconf.HTTPS_ServerPort + ":/api/file/sync"
//aclstring += " ;cmd:\"curl -s -k -u" + leapiconf.Username + "\\:" + leapiconf.SecretKey + " " + syncScheme + leapiconf.Hostname + ":" + syncPort + "/api/file/upload/" + "$destfile" + " -T $src \"" + ":" + leapiconf.LetsEncryptValidationPath
}
err = os.Setenv("ACL", aclstring)
if err != nil {
return errors.New("RENEW: error setting ACL environment variable: " + err.Error())
}
if leapiconf.Debug {
log.Println("ACL STRING:")
log.Println(aclstring)
}
//Cert and key locations
domain_cert_location := leapiconf.TLSCertFile
if leapiconf.SyncType == "ssh" {
for _, server := range servers {
if server == leapiconf.Hostname {
continue
}
domain_cert_location += ";ssh:" + leapiconf.Username + "@" + server + ":" + leapiconf.TLSCertFile
//domain_cert_location += ";davs:leapi:" + leapiconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/cert"
}
} else { //file sync type is HTTPS
domain_cert_location += ";davs:" + leapiconf.Username + ":" + leapiconf.SecretKey + ":" + leapiconf.Hostname + ":" + leapiconf.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 := leapiconf.TLSKeyFile
if leapiconf.SyncType == "ssh" {
for _, server := range servers {
if server == leapiconf.Hostname {
continue
}
domain_key_location += ";ssh:" + leapiconf.Username + "@" + server + ":" + leapiconf.TLSKeyFile
//domain_key_location += ";davs:leapi:" + leapiconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/key"
}
} else { //file sync type is HTTPS
domain_key_location += ";davs:" + leapiconf.Username + ":" + leapiconf.SecretKey + ":" + leapiconf.Hostname + ":" + leapiconf.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 := leapiconf.TLSChainFile
if leapiconf.SyncType == "ssh" {
for _, server := range servers {
if server == leapiconf.Hostname {
continue
}
//domain_chain_location += ";ssh:" + leapiconf.Username + "@" + server + ":" + leapiconf.TLSChainFile
domain_chain_location += ";davs:leapi:" + leapiconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/chain"
}
} else { //file sync type is HTTPS
domain_chain_location += ";davs:" + leapiconf.Username + ":" + leapiconf.SecretKey + ":" + leapiconf.Hostname + ":" + leapiconf.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 := leapiconf.TLSPEMFile
if leapiconf.SyncType == "ssh" {
for _, server := range servers {
if server == leapiconf.Hostname {
continue
}
domain_pem_location += ";ssh:" + leapiconf.Username + "@" + server + ":" + leapiconf.TLSPEMFile
//domain_pem_location += ";davs:leapi:" + leapiconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/pem"
}
} else { //file sync type is HTTPS
domain_pem_location += ";davs:" + leapiconf.Username + ":" + leapiconf.SecretKey + ":" + leapiconf.Hostname + ":" + leapiconf.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 := leapiconf.TLSCAFile
if leapiconf.SyncType == "ssh" {
for _, server := range servers {
if server == leapiconf.Hostname {
continue
}
ca_cert_location += ";ssh:" + leapiconf.Username + "@" + server + ":" + leapiconf.TLSCAFile
//ca_cert_location += ";davs:leapi:" + leapiconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/ca"
}
} else { //file sync type is HTTPS
ca_cert_location += ";davs:" + leapiconf.Username + ":" + leapiconf.SecretKey + ":" + leapiconf.Hostname + ":" + leapiconf.HTTPS_ServerPort + ":/api/file/sync/ca"
}
reload_command := leapiconf.ReloadCommand
for _, server := range servers {
if server == leapiconf.Hostname {
continue
}
//old ssh method; requires ssh key
//reload_command += "; ssh " + leapiconf.Username + "@" + server + " '" + leapiconf.ReloadCommand + "'"
//new method; calls LEAPI to trigger reload
reload_command += " ; curl -s -k -X POST -H 'Authorization: Bearer " + leapiconf.SecretKey + "' " + syncScheme + server + "/api/reload"
//reload_command += "; 'curl -s -X POST -H \\\"Authorization: Bearer " + leapiconf.SecretKey + "\\\" " + syncScheme + server + "/api/reload"
}
ca_server := "https://acme-staging-v02.api.letsencrypt.org"
if leapiconf.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=\"" + leapiconf.TLSCAFile + "\"\n"
configFile += "RELOAD_CMD=\"" + reload_command + "\"\n"
configFile += "RENEW_ALLOW=\"" + leapiconf.RenewAllow + "\"\n"
configFile += "CHECK_REMOTE=\"true\"\n"
configFile += "SERVER_TYPE=\"" + leapiconf.CheckPort + "\"\n"
configFile += "CHECK_REMOTE_WAIT=\"5\"\n"
//write config file
err = ioutil.WriteFile(configDir+"/"+leapiconf.PrimaryDomain+"/getssl.cfg", []byte(configFile), 0644)
if err != nil {
return errors.New("Couldn't write getssl config file: " + configDir + "/" + leapiconf.PrimaryDomain + "/getssl.cfg")
}
if leapiconf.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", leapiconf.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(leapiconf.SrvDir+"/getssl", "-u", "-w", leapiconf.SrvDir, leapiconf.PrimaryDomain)
if leapiconf.Debug {
cmd = exec.Command(leapiconf.SrvDir+"/getssl", "-d", "-w", leapiconf.SrvDir, leapiconf.PrimaryDomain)
} else {
cmd = exec.Command(leapiconf.SrvDir+"/getssl", "-w", leapiconf.SrvDir, leapiconf.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 " + leapiconf.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 += leapiconf.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(leapiconf.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
}
func uptime() UpOut {
uptime := fmt.Sprintf("%s", time.Since(startupTime))
@ -984,298 +464,3 @@ func uptime() UpOut {
return out
}
/////////////////////////////////////////////
///// API ROUTE FUNCTIONS
/////////////////////////////////////////////
func nothingResponse(c echo.Context) error {
return c.NoContent(http.StatusNotFound)
}
func uptimeCheck(c echo.Context) error {
if c.Request().Method == http.MethodHead {
return c.NoContent(http.StatusOK)
}
//return c.String(http.StatusOK, "{\"up\":true}")
return c.JSON(http.StatusOK, uptime())
}
func apiRenew(c echo.Context) error {
err := renew()
if err != nil {
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
}
return c.JSON(okOut())
}
func apiReload(c echo.Context) error {
err := reload()
if err != nil {
return c.JSON(errorOut(http.StatusInternalServerError, "Error reloading services: "+err.Error()))
}
return c.JSON(okOut())
}
func apiUpload(c echo.Context) error {
fileType := c.Param("fileType")
r := c.Request()
var filePath string
switch fileType {
case "ca":
filePath = leapiconf.TLSCAFile
case "chain":
filePath = leapiconf.TLSChainFile
case "key":
filePath = leapiconf.TLSKeyFile
case "cert":
filePath = leapiconf.TLSCertFile
case "pem":
filePath = leapiconf.TLSPEMFile
default: //ACL
//return c.JSON(errorOut(http.StatusBadRequest, "Invalid filetype/URL."))
filePath = leapiconf.LetsEncryptValidationPath + "/" + fileType
}
directory, _ := path.Split(filePath)
//Check and create directory
if _, err := os.Stat(directory); os.IsNotExist(err) {
err = os.MkdirAll(directory, 0755)
if err != nil {
return c.JSON(errorOut(http.StatusInternalServerError, "Filetype "+fileType+" directory does not exist, and could not create: "+err.Error()))
}
}
//Read the upload data
var blimit int64 = 102400 //100k max upload size
body, err := ioutil.ReadAll(io.LimitReader(r.Body, blimit))
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error reading post body: "+err.Error()))
}
//Write the file
err = ioutil.WriteFile(filePath, body, 0644)
if err != nil {
return c.JSON(errorOut(http.StatusInternalServerError, "Could not write file: "+err.Error()))
}
log.Println("Received PUT to " + r.RequestURI)
log.Println("Writing to " + filePath)
return c.JSON(okOut())
}
func apiUploadSync(c echo.Context) error {
fileType := c.Param("fileType")
r := c.Request()
//Read the upload data
var blimit int64 = 102400 //100k max upload size
body, err := ioutil.ReadAll(io.LimitReader(r.Body, blimit))
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error reading post body: "+err.Error()))
}
uuid := generateUUID()
filePath := leapiconf.SrvDir + "/" + fileType + "__" + uuid + ".tmpfile"
//Write the file
err = ioutil.WriteFile(filePath, body, 0644)
if err != nil {
return c.JSON(errorOut(http.StatusInternalServerError, "Could not write temporary file: "+err.Error()))
}
log.Println("Received PUT for sync to " + r.RequestURI)
log.Println("Writing to " + filePath)
err = sendFileToAllServers(filePath)
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error sending file "+filePath+" to other servers: "+err.Error()))
}
return c.JSON(okOut())
}
func apiListDomains(c echo.Context) error {
var out APIOutput
out.Status = http.StatusOK
out.Message = "domains list"
out.Data = domains
return c.JSON(out.Status, out)
}
func apiPutDomain(c echo.Context) error {
domain := c.Param("domain")
//check for dups
for _, d := range domains {
if d == domain {
return c.JSON(errorOut(http.StatusBadRequest, "Bad request: Domain already exists."))
}
}
//add domain to list
domains = append(domains, domain)
//write list to disk
err := writeDomains()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error writing domains list to disk: "+err.Error()))
}
//sync with other servers
err = syncAllServers()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing to other servers: "+err.Error()))
}
//renew cert
err = renew()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
}
return c.JSON(okOut())
}
func apiDeleteDomain(c echo.Context) error {
deleteDomain := c.Param("domain")
var newlist []string
for _, d := range domains {
if d != deleteDomain {
newlist = append(newlist, d)
}
}
domains = newlist
//write list to disk
err := writeDomains()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error writing domains list to disk: "+err.Error()))
}
//sync with other servers
err = syncAllServers()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing to other servers: "+err.Error()))
}
//renew cert
err = renew()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
}
return c.JSON(okOut())
}
func apiListServers(c echo.Context) error {
var out APIOutput
out.Status = http.StatusOK
out.Message = "servers list"
out.Data = servers
return c.JSON(out.Status, out)
}
func apiPutServer(c echo.Context) error {
server := c.Param("server")
//check for dups
for _, s := range servers {
if s == server {
return c.JSON(errorOut(http.StatusBadRequest, "Bad request: Server already exists."))
}
}
//add servers to list
servers = append(servers, server)
//write list to disk
err := writeServers()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error writing servers list to disk: "+err.Error()))
}
//sync with other servers
err = syncAllServers()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing to other servers: "+err.Error()))
}
//renew cert
err = renew()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
}
return c.JSON(okOut())
}
func apiDeleteServer(c echo.Context) error {
deleteServer := c.Param("server")
var newlist []string
for _, s := range servers {
if s != deleteServer {
newlist = append(newlist, s)
}
}
servers = newlist
//write list to disk
err := writeServers()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error writing servers list to disk: "+err.Error()))
}
//sync with other servers
err = syncAllServers()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing to other servers: "+err.Error()))
}
//renew cert
err = renew()
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
}
return c.JSON(okOut())
}
func apiSync(c echo.Context) error {
host := c.Param("host")
log.Println("Received sync request for host: " + host + ". From IP address: " + c.RealIP() + " Syncing...")
err := syncServersFromHost(host)
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing servers from host: "+host+". "+err.Error()))
}
err = syncDomainsFromHost(host)
if err != nil {
log.Println(err.Error())
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing domains from host: "+host+". "+err.Error()))
}
return c.JSON(okOut())
}

+ 183
- 0
sync.go View File

@ -0,0 +1,183 @@
//LEAPI - ACME Certificate Renewal Control API - Copyright 2022-2024 Ruel Tmeizeh All Rights Reserved
package main
import (
"crypto/tls"
"encoding/json"
"errors"
"io/ioutil"
"log"
"net/http"
"strconv"
"sync"
)
func syncAllServers() 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 sync of server: " + srv + "...")
err := syncOneServer(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()
log.Println("Finished sending sync requests.")
return theError //if any one or more fail, return an error for it (the last one that fails)
}
func syncOneServer(server string) error {
//Make http requests to each other servers' /sync endpoints
// https://server.tld:port/sync
log.Println("SYNC " + server + " starting...")
req, err := http.NewRequest("POST", syncScheme+server+":"+syncPort+"/api/sync/"+appconf.Hostname, nil)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't create new HTTP sync request for server: " + server)
}
req.Close = true
req.Header.Set("User-Agent", myUserAgent)
req.Header.Set("Authorization", "Bearer "+appconf.SecretKey)
//skip verification of cert for https syncing, 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 perform HTTP sync request 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 syncing to server " + server + ". Status code: " + strconv.Itoa(response.StatusCode) + " Body: " + string(body)
log.Println(errorString)
return errors.New(errorString)
}
log.Println("SYNC " + server + " success!")
return nil
}
func syncServersFromHost(host string) error {
var theError error
req, err := http.NewRequest("GET", syncScheme+host+":"+syncPort+"/api/servers", nil)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't create new HTTP request for syncing servers from host: " + host)
}
req.Close = true
req.Header.Set("User-Agent", myUserAgent)
req.Header.Set("Authorization", "Bearer "+appconf.SecretKey)
//skip verification of cert for https syncing, 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 perform HTTP server sync request to host: " + host)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't parse response body from server sync request to server: " + host)
}
if response.StatusCode != 200 {
theError = errors.New("Problem syncing servers from host " + host + ". Status code: " + strconv.Itoa(response.StatusCode) + " Body: " + string(body))
log.Println(theError.Error())
return theError
}
var result APIOutput
err = json.Unmarshal(body, &result)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't parse response body from host " + host + ": " + err.Error())
}
servers = result.Data
err = writeServers()
if err != nil {
log.Println(err.Error())
return err
}
return nil
}
func syncDomainsFromHost(host string) error {
var theError error
req, err := http.NewRequest("GET", syncScheme+host+":"+syncPort+"/api/domains", nil)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't create new HTTP request for syncing domains from host: " + host)
}
req.Close = true
req.Header.Set("User-Agent", myUserAgent)
req.Header.Set("Authorization", "Bearer "+appconf.SecretKey)
//skip verification of cert for https syncing, 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 perform HTTP domain sync request to host: " + host)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't parse response body from domain sync request to server: " + host)
}
if response.StatusCode != 200 {
theError = errors.New("Problem syncing domains from host " + host + ". Status code: " + strconv.Itoa(response.StatusCode) + " Body: " + string(body))
log.Println(theError.Error())
return theError
}
var result APIOutput
err = json.Unmarshal(body, &result)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't parse response body from host " + host + ": " + err.Error())
}
domains = result.Data
err = writeDomains()
if err != nil {
log.Println(err.Error())
return err
}
return nil
}

Loading…
Cancel
Save