package main import ( "errors" "fmt" "log" "os" "path/filepath" "sync" "time" appShared "git.nevets.tech/Steven/certman/app" "git.nevets.tech/Steven/certman/common" "git.nevets.tech/Steven/certman/server" ) type Daemon struct { ACMEManager *server.ACMEManager TickMu sync.Mutex MgrMu sync.Mutex } func (d *Daemon) loadACMEManager() error { d.MgrMu.Lock() defer d.MgrMu.Unlock() if d.ACMEManager == nil { var err error d.ACMEManager, err = server.NewACMEManager(appShared.Config()) if err != nil { return err } } return nil } func (d *Daemon) Init() { fmt.Println("Starting CertManager in server mode...") err := appShared.LoadDomainConfigs() if err != nil { log.Fatalf("Error loading domain configs: %v", err) } d.Tick() } func (d *Daemon) Tick() { d.TickMu.Lock() defer d.TickMu.Unlock() fmt.Println("Tick!") if err := d.loadACMEManager(); err != nil { fmt.Printf("Error getting acme manager: %v\n", err) return } now := time.Now().UTC() config := appShared.Config() localDomainConfigs := appShared.DomainStore().Snapshot() for domainStr, domainConfig := range localDomainConfigs { if !domainConfig.Domain.Enabled { continue } //TODO: have renewPeriod logic default to use certificate expiry if available renewPeriod := domainConfig.Certificates.RenewPeriod lastIssued := time.Unix(domainConfig.Internal.LastIssued, 0).UTC() renewalDue := lastIssued.AddDate(0, 0, renewPeriod) if !now.After(renewalDue) { continue } //TODO extra check if certificate expiry (create cache?) if _, err := d.ACMEManager.RenewForDomain(domainStr); err != nil { if errors.Is(err, os.ErrNotExist) { if _, err := d.ACMEManager.ObtainForDomain(domainStr, config, domainConfig); err != nil { fmt.Printf("Error obtaining domain certificates for domain %s: %v\n", domainStr, err) continue } } else { fmt.Printf("Error: %v\n", err) continue } } domainConfig.Internal.LastIssued = time.Now().UTC().Unix() if err := appShared.WriteDomainConfig(domainConfig); err != nil { fmt.Printf("Error saving domain config %s: %v\n", domainStr, err) continue } certsDir := filepath.Join(d.ACMEManager.CertsRoot, domainStr) if err := common.EncryptFileXChaCha(domainConfig.Certificates.CryptoKey, filepath.Join(certsDir, domainStr+".crt"), filepath.Join(certsDir, domainStr+".crt.crpt"), nil); err != nil { fmt.Printf("Error encrypting domain cert for domain %s: %v\n", domainStr, err) continue } if err := common.EncryptFileXChaCha(domainConfig.Certificates.CryptoKey, filepath.Join(certsDir, domainStr+".key"), filepath.Join(certsDir, domainStr+".key.crpt"), nil); err != nil { fmt.Printf("Error encrypting domain key for domain %s: %v\n", domainStr, err) continue } ws, err := prepareServerWorkspace(config, domainConfig, domainStr) if err != nil { fmt.Printf("Error preparing git workspace for domain %s: %v\n", domainStr, err) continue } if err := server.AddAndPushCerts(ws, certsDir, config); err != nil { fmt.Printf("Error pushing certificates for domain %s: %v\n", domainStr, err) continue } fmt.Printf("Successfully pushed certificates for domain %s\n", domainStr) } if err := appShared.SaveDomainConfigs(); err != nil { fmt.Printf("Error saving domain configs: %v\n", err) } } // prepareServerWorkspace creates or clones the domain's remote repo into a // fresh in-memory workspace. If the repo is being cloned, it verifies that // SERVER_ID matches this server's UUID (or is absent, in which case the // domain is adopted on the next push). func prepareServerWorkspace(config *common.AppConfig, domainConfig *common.DomainConfig, domain string) (*common.GitWorkspace, error) { if !domainConfig.Internal.RepoExists { url, err := server.CreateRepo(config, domainConfig, domain) if err != nil { return nil, fmt.Errorf("create remote repo: %w", err) } domainConfig.Internal.RepoExists = true if err := appShared.WriteDomainConfig(domainConfig); err != nil { return nil, fmt.Errorf("save domain config: %w", err) } ws := common.NewGitWorkspace(domain, url) if err := common.InitRepo(ws); err != nil { return nil, fmt.Errorf("init workspace: %w", err) } return ws, nil } url := common.RepoURL(config, domainConfig, domain) ws := common.NewGitWorkspace(domain, url) if err := common.CloneRepo(ws, config); err != nil { return nil, fmt.Errorf("clone: %w", err) } owned, err := server.VerifyOwnership(ws, config.App.UUID) if err != nil { return nil, err } if !owned { fmt.Printf("Adopting unclaimed repo for domain %s\n", domain) } return ws, nil } func (d *Daemon) Reload() { fmt.Println("Reloading configs...") err := appShared.LoadDomainConfigs() if err != nil { fmt.Printf("Error loading domain configs: %v\n", err) return } d.MgrMu.Lock() d.ACMEManager = nil d.MgrMu.Unlock() fmt.Println("Successfully reloaded configs") } func (d *Daemon) Stop() { fmt.Println("Shutting down server") }