diff --git a/.gitignore b/.gitignore index 68fa9a2..f34b695 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ getssl leapi +leapi_config.json diff --git a/actions.go b/actions.go new file mode 100644 index 0000000..d7c4df9 --- /dev/null +++ b/actions.go @@ -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 +} diff --git a/api.go b/api.go new file mode 100644 index 0000000..6ebe47a --- /dev/null +++ b/api.go @@ -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()) +} diff --git a/main.go b/main.go index 8c19d70..8686ccd 100644 --- a/main.go +++ b/main.go @@ -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()) -} diff --git a/sync.go b/sync.go new file mode 100644 index 0000000..4bbff9a --- /dev/null +++ b/sync.go @@ -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 +}