diff --git a/actions.go b/actions.go index d7c4df9..9874d9b 100644 --- a/actions.go +++ b/actions.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "encoding/json" "errors" + "fmt" "io/ioutil" "log" "net/http" @@ -19,9 +20,9 @@ import ( func writeDomains() error { b := new(bytes.Buffer) - err := json.NewEncoder(b).Encode(domains) + err := json.NewEncoder(b).Encode(certgroups) if err != nil { - return errors.New("Couldn't encode domains list into JSON: " + err.Error()) + return errors.New("Couldn't encode certgroups struct into JSON: " + err.Error()) } err = ioutil.WriteFile(configDir+"/domains.json", b.Bytes(), 0644) @@ -136,14 +137,16 @@ func sendFileToServer(filePath, server string) error { return nil } -func renew() error { +func renew(cert_idx int) error { log.Println("Renew operation initiated...") //BUILD/SET GETSSL ENVIRONMENT VARIABLES THEN EXECUTE GETSSL //domain list var domainlist string + cg := certgroups[cert_idx] + domains := cg.Domains for _, d := range domains { - if d == appconf.PrimaryDomain { //ignore primary domain + if d == cg.PrimaryDomain { //ignore primary domain continue } domainlist = domainlist + "," + d @@ -165,7 +168,6 @@ func renew() error { 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" @@ -182,68 +184,68 @@ func renew() error { } //Cert and key locations - domain_cert_location := appconf.TLSCertFile + domain_cert_location := appconf.TLSCertPath + fmt.Sprintf("%02d", cert_idx) + ".crt" 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 += ";ssh:" + appconf.Username + "@" + server + ":" + appconf.TLSCertPath + fmt.Sprintf("%02d", cert_idx) + ".crt" //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" + domain_cert_location += ";davs:" + appconf.Username + ":" + appconf.SecretKey + ":" + appconf.Hostname + ":" + appconf.HTTPS_ServerPort + ":/api/file/sync/cert/" + fmt.Sprintf("%02d", cert_idx) } 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 + domain_key_location := appconf.TLSKeyPath + fmt.Sprintf("%02d", cert_idx) + ".key" 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 += ";ssh:" + appconf.Username + "@" + server + ":" + appconf.TLSKeyPath + fmt.Sprintf("%02d", cert_idx) + ".key" //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" + domain_key_location += ";davs:" + appconf.Username + ":" + appconf.SecretKey + ":" + appconf.Hostname + ":" + appconf.HTTPS_ServerPort + ":/api/file/sync/key/" + fmt.Sprintf("%02d", cert_idx) } 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 + domain_chain_location := appconf.TLSChainPath + fmt.Sprintf("%02d", cert_idx) + ".crt" 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" + domain_chain_location += ";ssh:" + appconf.Username + "@" + server + ":" + appconf.TLSChainPath + fmt.Sprintf("%02d", cert_idx) + ".crt" + //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" + domain_chain_location += ";davs:" + appconf.Username + ":" + appconf.SecretKey + ":" + appconf.Hostname + ":" + appconf.HTTPS_ServerPort + ":/api/file/sync/chain/" + fmt.Sprintf("%02d", cert_idx) } 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 + domain_pem_location := appconf.TLSPEMPath 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 += ";ssh:" + appconf.Username + "@" + server + ":" + appconf.TLSPEMPath + fmt.Sprintf("%02d", cert_idx) + ".pem" //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" + domain_pem_location += ";davs:" + appconf.Username + ":" + appconf.SecretKey + ":" + appconf.Hostname + ":" + appconf.HTTPS_ServerPort + ":/api/file/sync/pem/" + fmt.Sprintf("%02d", cert_idx) } err = os.Setenv("DOMAIN_PEM_LOCATION", domain_pem_location) if err != nil { @@ -251,17 +253,17 @@ func renew() 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 + ca_cert_location := appconf.TLSCAPath 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 += ";ssh:" + appconf.Username + "@" + server + ":" + appconf.TLSCAPath + fmt.Sprintf("%02d", cert_idx) + ".crt" //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" + ca_cert_location += ";davs:" + appconf.Username + ":" + appconf.SecretKey + ":" + appconf.Hostname + ":" + appconf.HTTPS_ServerPort + ":/api/file/sync/ca/" + fmt.Sprintf("%02d", cert_idx) } reload_command := appconf.ReloadCommand @@ -285,7 +287,7 @@ func renew() error { configFile = "CA=\"" + ca_server + "\"\n" configFile += "USE_SINGLE_ACL=\"true\"\n" - configFile += "CA_CERT_LOCATION=\"" + appconf.TLSCAFile + "\"\n" + configFile += "CA_CERT_LOCATION=\"" + appconf.TLSCAPath + "\"\n" configFile += "RELOAD_CMD=\"" + reload_command + "\"\n" configFile += "RENEW_ALLOW=\"" + appconf.RenewAllow + "\"\n" configFile += "CHECK_REMOTE=\"true\"\n" @@ -293,9 +295,9 @@ func renew() error { configFile += "CHECK_REMOTE_WAIT=\"5\"\n" //write config file - err = ioutil.WriteFile(configDir+"/"+appconf.PrimaryDomain+"/getssl.cfg", []byte(configFile), 0644) + err = ioutil.WriteFile(configDir+"/"+cg.PrimaryDomain+"/getssl.cfg", []byte(configFile), 0644) if err != nil { - return errors.New("Couldn't write getssl config file: " + configDir + "/" + appconf.PrimaryDomain + "/getssl.cfg") + return errors.New("Couldn't write getssl config file: " + configDir + "/" + cg.PrimaryDomain + "/getssl.cfg") } if appconf.Debug { @@ -316,9 +318,9 @@ func renew() 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) + cmd = exec.Command(appconf.SrvDir+"/getssl", "-d", "-w", appconf.SrvDir, cg.PrimaryDomain) } else { - cmd = exec.Command(appconf.SrvDir+"/getssl", "-w", appconf.SrvDir, appconf.PrimaryDomain) + cmd = exec.Command(appconf.SrvDir+"/getssl", "-w", appconf.SrvDir, cg.PrimaryDomain) } output, err = cmd.CombinedOutput() if err != nil { diff --git a/api.go b/api.go index 6ebe47a..54d6e5b 100644 --- a/api.go +++ b/api.go @@ -2,6 +2,7 @@ package main import ( + "fmt" "io" "io/ioutil" "log" @@ -28,9 +29,11 @@ func uptimeCheck(c echo.Context) error { } func apiRenew(c echo.Context) error { - err := renew() - if err != nil { - return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error())) + for n, _ := range certgroups { + err := renew(n) + if err != nil { + return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error())) + } } return c.JSON(okOut()) } @@ -43,22 +46,24 @@ func apiReload(c echo.Context) error { return c.JSON(okOut()) } +//Receive file and store it func apiUpload(c echo.Context) error { fileType := c.Param("fileType") + cert_idx := c.Param("cert_idx") r := c.Request() var filePath string switch fileType { case "ca": - filePath = appconf.TLSCAFile + filePath = appconf.TLSCAPath + fmt.Sprintf("%02d", cert_idx) + ".crt" case "chain": - filePath = appconf.TLSChainFile + filePath = appconf.TLSChainPath + fmt.Sprintf("%02d", cert_idx) + ".crt" case "key": - filePath = appconf.TLSKeyFile + filePath = appconf.TLSKeyPath + fmt.Sprintf("%02d", cert_idx) + ".key" case "cert": - filePath = appconf.TLSCertFile + filePath = appconf.TLSCertPath + fmt.Sprintf("%02d", cert_idx) + ".crt" case "pem": - filePath = appconf.TLSPEMFile + filePath = appconf.TLSPEMPath + fmt.Sprintf("%02d", cert_idx) + ".pem" default: //ACL //return c.JSON(errorOut(http.StatusBadRequest, "Invalid filetype/URL.")) filePath = appconf.LetsEncryptValidationPath + "/" + fileType @@ -131,7 +136,9 @@ func apiListDomains(c echo.Context) error { var out APIOutput out.Status = http.StatusOK out.Message = "domains list" - out.Data = domains + for _, cg := range certgroups { + out.Data = append(out.Data, cg) + } return c.JSON(out.Status, out) } @@ -139,14 +146,26 @@ func apiPutDomain(c echo.Context) error { domain := c.Param("domain") //check for dups - for _, d := range domains { - if d == domain { + for _, cg := range certgroups { + if cg.PrimaryDomain == domain { return c.JSON(errorOut(http.StatusBadRequest, "Bad request: Domain already exists.")) } + for _, d := range cg.Domains { + if d == domain { + return c.JSON(errorOut(http.StatusBadRequest, "Bad request: Domain already exists.")) + } + } } + var certgroup_slot int //add domain to list - domains = append(domains, domain) + for n, cg := range certgroups { + if len(cg.Domains) < 99 { //can't have more than 100 names on a single cert + cg.Domains = append(cg.Domains, domain) + certgroups[n] = cg //replace with appended version + certgroup_slot = n //set slot we need to run renewal for + } + } //write list to disk err := writeDomains() @@ -163,7 +182,7 @@ func apiPutDomain(c echo.Context) error { } //renew cert - err = renew() + err = renew(certgroup_slot) if err != nil { log.Println(err.Error()) return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error())) @@ -175,14 +194,20 @@ func apiPutDomain(c echo.Context) error { func apiDeleteDomain(c echo.Context) error { deleteDomain := c.Param("domain") var newlist []string - for _, d := range domains { - if d != deleteDomain { - newlist = append(newlist, d) + var certgroup_slot int + + for n, cg := range certgroups { + for _, d := range cg.Domains { + if d == deleteDomain { + certgroup_slot = n //save the slot this deleted domain is in + } else { //rebuild the list with domains not deleted + newlist = append(newlist, d) + } } + cg.Domains = newlist + certgroups[n] = cg //replace this slot } - domains = newlist - //write list to disk err := writeDomains() if err != nil { @@ -198,7 +223,7 @@ func apiDeleteDomain(c echo.Context) error { } //renew cert - err = renew() + err = renew(certgroup_slot) if err != nil { log.Println(err.Error()) return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error())) @@ -211,7 +236,9 @@ func apiListServers(c echo.Context) error { var out APIOutput out.Status = http.StatusOK out.Message = "servers list" - out.Data = servers + for _, server := range servers { + out.Data = append(out.Data, server) + } return c.JSON(out.Status, out) } @@ -243,10 +270,12 @@ func apiPutServer(c echo.Context) error { } //renew cert - err = renew() - if err != nil { - log.Println(err.Error()) - return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error())) + for n, _ := range certgroups { + err = renew(n) + if err != nil { + log.Println(err.Error()) + return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error())) + } } return c.JSON(okOut()) @@ -278,10 +307,12 @@ func apiDeleteServer(c echo.Context) error { } //renew cert - err = renew() - if err != nil { - log.Println(err.Error()) - return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error())) + for n, _ := range certgroups { + err = renew(n) + if err != nil { + log.Println(err.Error()) + return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error())) + } } return c.JSON(okOut()) diff --git a/main.go b/main.go index 8686ccd..7576487 100644 --- a/main.go +++ b/main.go @@ -35,7 +35,7 @@ var appconf LEAPIConfig var startupTime time.Time var configDir string -var domains []string +var certgroups []CertGroup var servers []string var syncScheme string = "http://" var syncPort string @@ -60,11 +60,11 @@ type LEAPIConfig struct { Debug bool `json:"debug"` HTTP_ServerPort string `json:"http_server_port"` HTTPS_ServerPort string `json:"https_server_port"` - TLSCertFile string `json:"tls_cert_path"` - TLSKeyFile string `json:"tls_key_path"` - TLSChainFile string `json:"tls_chain_path"` - TLSPEMFile string `json:"tls_pem_path"` - TLSCAFile string `json:"tls_ca_path"` + TLSCertPath string `json:"tls_cert_path"` + TLSKeyPath string `json:"tls_key_path"` + TLSChainPath string `json:"tls_chain_path"` + TLSPEMPath string `json:"tls_pem_path"` + TLSCAPath string `json:"tls_ca_path"` FrontEndURL string `json:"frontend_url"` PrimaryDomain string `json:"primary_domain"` LetsEncryptValidationPath string `json:"letsencrypt_validation_path"` @@ -75,6 +75,12 @@ type LEAPIConfig struct { CheckPort string `json:"check_port"` } +type CertGroup struct { + //CertPrefix string `json:"cert_prefix"` + PrimaryDomain string `json:"primary_domain"` + Domains []string `json:"domains"` +} + type UpOut struct { Up bool `json:"up,omitempty"` StartTime time.Time `json:"start_time,omitempty"` @@ -82,9 +88,9 @@ type UpOut struct { } type APIOutput struct { - Status int `json:"status,omitempty"` - Message string `json:"message,omitempty"` - Data []string `json:"data,omitempty"` + Status int `json:"status,omitempty"` + Message string `json:"message,omitempty"` + Data []interface{} `json:"data,omitempty"` } type keypairReloader struct { @@ -164,7 +170,7 @@ func main() { } defer jsonFile.Close() fileBytes, err := ioutil.ReadAll(jsonFile) - err = json.Unmarshal(fileBytes, &domains) + err = json.Unmarshal(fileBytes, &certgroups) if err != nil { log.Fatal("Could not parse domains.json file: " + err.Error()) } @@ -268,9 +274,13 @@ func main() { apiFile.OPTIONS("/upload/:fileType", apiUpload) apiFile.PUT("/upload/:fileType", apiUpload) + apiFile.OPTIONS("/upload/:fileType/:cert_idx", apiUpload) + apiFile.PUT("/upload/:fileType/:cert_idx", apiUpload) apiFile.OPTIONS("/sync/:fileType", apiUploadSync) apiFile.PUT("/sync/:fileType", apiUploadSync) + apiFile.OPTIONS("/sync/:fileType/:cert_idx", apiUploadSync) + apiFile.PUT("/sync/:fileType/:cert_idx", apiUploadSync) ///////////////////////////////////////////// // HTTP SERVERS CONFIG: @@ -281,14 +291,14 @@ func main() { syncScheme = "https://" syncPort = appconf.HTTPS_ServerPort - //certPair, err := tls.LoadX509KeyPair(appconf.TLSCertificateFile, appconf.TLSKeyFile) - if !fileExists(appconf.TLSChainFile) || !fileExists(appconf.TLSKeyFile) { + //certPair, err := tls.LoadX509KeyPair(appconf.TLSCertificateFile, appconf.TLSKeyPath) + if !fileExists(appconf.TLSChainPath) || !fileExists(appconf.TLSKeyPath) { 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(appconf.TLSChainFile, appconf.TLSKeyFile) + kpr, err := NewKeypairReloader(appconf.TLSChainPath, appconf.TLSKeyPath) if err != nil { log.Fatal(err) } @@ -397,8 +407,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", appconf.TLSChainFile, appconf.TLSKeyFile) - fmt.Printf("Received SIGHUP, reloading TLS certificate and key from %q and %q\n", appconf.TLSChainFile, appconf.TLSKeyFile) + log.Printf("Received SIGHUP, reloading TLS certificate and key from %q and %q", appconf.TLSChainPath, appconf.TLSKeyPath) + fmt.Printf("Received SIGHUP, reloading TLS certificate and key from %q and %q\n", appconf.TLSChainPath, appconf.TLSKeyPath) 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) diff --git a/sync.go b/sync.go index 4bbff9a..1bb1464 100644 --- a/sync.go +++ b/sync.go @@ -123,7 +123,12 @@ func syncServersFromHost(host string) error { log.Println(err.Error()) return errors.New("Couldn't parse response body from host " + host + ": " + err.Error()) } - servers = result.Data + for _, data := range result.Data { + switch server := data.(type) { + case string: + servers = append(servers, server) + } + } err = writeServers() if err != nil { @@ -171,7 +176,12 @@ func syncDomainsFromHost(host string) error { log.Println(err.Error()) return errors.New("Couldn't parse response body from host " + host + ": " + err.Error()) } - domains = result.Data + for _, data := range result.Data { + switch cg := data.(type) { + case interface{}: + certgroups = append(certgroups, cg.(CertGroup)) + } + } err = writeDomains() if err != nil {