package commands import ( "fmt" "io" "os" "path/filepath" "strings" "time" "git.nevets.tech/Keys/CertManager/internal" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5/storage/memory" ) var mgr *internal.ACMEManager func RenewCertCmd(domain string, noPush bool, certmanMode internal.CertManMode) error { err := internal.LoadConfig() if err != nil { return err } err = internal.LoadDomainConfigs() if err != nil { return err } switch internal.Config().GetString("App.mode") { case "server": mgr, err = internal.NewACMEManager() if err != nil { return err } err = renewCerts(domain, noPush, certmanMode) if err != nil { return err } return ReloadDaemonCmd() case "client": return pullCerts(domain, certmanMode) default: return fmt.Errorf("invalid operating mode %s", internal.Config().GetString("App.mode")) } } func renewCerts(domain string, noPush bool, certmanMode internal.CertManMode) error { _, err := mgr.RenewForDomain(domain) if err != nil { // if no existing cert, obtain instead _, err = mgr.ObtainForDomain(domain) if err != nil { return fmt.Errorf("error obtaining domain certificates for domain %s: %v", domain, err) } } domainConfig, exists := internal.DomainStore().Get(domain) if !exists { return fmt.Errorf("domain %s does not exist", domain) } domainConfig.Set("Internal.last_issued", time.Now().UTC().Unix()) err = internal.WriteDomainConfig(domainConfig) if err != nil { return fmt.Errorf("error saving domain config %s: %v", domain, err) } err = internal.EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), filepath.Join(mgr.CertsRoot, domain, domain+".crt"), filepath.Join(mgr.CertsRoot, domain, domain+".crt.crpt"), nil) if err != nil { return fmt.Errorf("error encrypting domain cert for domain %s: %v", domain, err) } err = internal.EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), filepath.Join(mgr.CertsRoot, domain, domain+".key"), filepath.Join(mgr.CertsRoot, domain, domain+".key.crpt"), nil) if err != nil { return fmt.Errorf("error encrypting domain key for domain %s: %v", domain, err) } if !noPush { giteaClient := internal.CreateGiteaClient() if giteaClient == nil { return fmt.Errorf("error creating gitea client for domain %s: %v", domain, err) } gitWorkspace := &internal.GitWorkspace{ Storage: memory.NewStorage(), FS: memfs.New(), } var repoUrl string if !domainConfig.GetBool("Internal.repo_exists") { repoUrl = internal.CreateGiteaRepo(domain, giteaClient) if repoUrl == "" { return fmt.Errorf("error creating Gitea repo for domain %s", domain) } domainConfig.Set("Internal.repo_exists", true) err = internal.WriteDomainConfig(domainConfig) if err != nil { return fmt.Errorf("error saving domain config %s: %v", domain, err) } err = internal.InitRepo(repoUrl, gitWorkspace) if err != nil { return fmt.Errorf("error initializing repo for domain %s: %v", domain, err) } } else { repoUrl = internal.Config().GetString("Git.server") + "/" + internal.Config().GetString("Git.org_name") + "/" + domain + domainConfig.GetString("Repo.repo_suffix") + ".git" err = internal.CloneRepo(repoUrl, gitWorkspace, certmanMode) if err != nil { return fmt.Errorf("error cloning repo for domain %s: %v", domain, err) } } err = internal.AddAndPushCerts(domain, gitWorkspace) if err != nil { return fmt.Errorf("error pushing certificates for domain %s: %v", domain, err) } fmt.Printf("Successfully pushed certificates for domain %s\n", domain) } return nil } func pullCerts(domain string, certmanMode internal.CertManMode) error { gitWorkspace := &internal.GitWorkspace{ Storage: memory.NewStorage(), FS: memfs.New(), } domainConfig, exists := internal.DomainStore().Get(domain) if !exists { return fmt.Errorf("domain %s does not exist", domain) } // Ex: https://git.example.com/Org/Repo-suffix.git // Clones repo and stores in gitWorkspace, skip if clone fails (doesn't exist?) repoUrl := internal.Config().GetString("Git.server") + "/" + internal.Config().GetString("Git.org_name") + "/" + domain + domainConfig.GetString("Repo.repo_suffix") + ".git" err := internal.CloneRepo(repoUrl, gitWorkspace, certmanMode) if err != nil { return fmt.Errorf("Error cloning domain repo %s: %v\n", domain, err) } certsDir, err := internal.DomainCertsDirWConf(domain, domainConfig) if err != nil { return fmt.Errorf("Error getting certificates dir for domain %s: %v\n", domain, err) } // Get files in repo fileInfos, err := gitWorkspace.FS.ReadDir("/") if err != nil { return fmt.Errorf("Error reading directory in memFS on domain %s: %v\n", domain, err) } // Iterate over files, filtering by .crpt (encrypted) files in case other files were accidentally added for _, fileInfo := range fileInfos { if strings.HasSuffix(fileInfo.Name(), ".crpt") { filename, _ := strings.CutSuffix(fileInfo.Name(), ".crpt") file, err := gitWorkspace.FS.Open(fileInfo.Name()) if err != nil { fmt.Printf("Error opening file in memFS on domain %s: %v\n", domain, err) continue } fileBytes, err := io.ReadAll(file) if err != nil { fmt.Printf("Error reading file in memFS on domain %s: %v\n", domain, err) file.Close() continue } err = file.Close() if err != nil { fmt.Printf("Error closing file on domain %s: %v\n", domain, err) continue } err = internal.DecryptFileFromBytes(domainConfig.GetString("Certificates.crypto_key"), fileBytes, filepath.Join(certsDir, filename), nil) if err != nil { fmt.Printf("Error decrypting file %s in domain %s: %v\n", filename, domain, err) continue } headRef, err := gitWorkspace.Repo.Head() if err != nil { fmt.Printf("Error getting head reference for domain %s: %v\n", domain, err) continue } err = internal.WriteCommitHash(headRef.Hash().String(), domainConfig) if err != nil { fmt.Printf("Error writing commit hash: %v\n", err) continue } certLinks := domainConfig.GetStringSlice("Certificates.cert_symlinks") for _, certLink := range certLinks { if certLink == "" { continue } linkInfo, err := os.Stat(certLink) if err != nil { if !os.IsNotExist(err) { fmt.Printf("Error stating cert link %s: %v\n", certLink, err) continue } } if linkInfo.IsDir() { certLink = filepath.Join(certLink, domain+".crt") } err = os.Link(filepath.Join(certsDir, domain+".crt"), certLink) if err != nil { fmt.Printf("Error linking cert %s to %s: %v\n", certLink, domain, err) continue } } keyLinks := domainConfig.GetStringSlice("Certificates.key_symlinks") for _, keyLink := range keyLinks { if keyLink == "" { continue } linkInfo, err := os.Stat(keyLink) if err != nil { if !os.IsNotExist(err) { fmt.Printf("Error stating key link %s: %v\n", keyLink, err) continue } } if linkInfo.IsDir() { keyLink = filepath.Join(keyLink, domain+".crt") } err = os.Link(filepath.Join(certsDir, domain+".crt"), keyLink) if err != nil { fmt.Printf("Error linking cert %s to %s: %v\n", keyLink, domain, err) continue } } } } return nil } func UpdateLinksCmd(domain string) error { domainConfig, exists := internal.DomainStore().Get(domain) if !exists { return fmt.Errorf("domain %s does not exist", domain) } certsDir, err := internal.DomainCertsDirWConf(domain, domainConfig) if err != nil { return fmt.Errorf("error getting certificates dir for domain %s: %v", domain, err) } certLinks := domainConfig.GetStringSlice("Certificates.cert_symlinks") for _, certLink := range certLinks { err = internal.LinkFile(filepath.Join(certsDir, domain+".crt"), certLink, domain, ".crt") if err != nil { fmt.Printf("Error linking cert %s to %s: %v", certLink, domain, err) continue } } keyLinks := domainConfig.GetStringSlice("Certificates.key_symlinks") for _, keyLink := range keyLinks { err = internal.LinkFile(filepath.Join(certsDir, domain+".crt"), keyLink, domain, ".key") if err != nil { fmt.Printf("Error linking cert %s to %s: %v", keyLink, domain, err) continue } } return nil }