package server import ( "context" "fmt" "net/http" "time" "housecarl/internal/api" "housecarl/internal/auth" "housecarl/internal/certs" "housecarl/internal/validate" ) func (s *Server) certificate(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := requireAdmin(p); err != nil { return err } out := api.CertificateResponse{Source: "none"} switch { case s.cfg.ACME != nil: st := s.cfg.ACME.Status() out.Configured, out.Source, out.Status = true, "acme", &st case s.cfg.TLSCertFile != "": out.Source = "file" } writeJSON(w, http.StatusOK, out) return nil } func (s *Server) acmeManager() (*certs.Manager, error) { if s.cfg.ACME == nil { return nil, conflict("housecarld doesn't obtain its certificate: set tls.acme and jarl in housecarl.yaml (docs/certificates.md)") } return s.cfg.ACME, nil } // certificateCheck: POST /certificate/check runs the preflight check now // (it takes up to a few minutes when the record is slow to appear). func (s *Server) certificateCheck(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := requireAdmin(p); err != nil { return err } m, err := s.acmeManager() if err != nil { return err } ctx, cancel := context.WithTimeout(r.Context(), 4*time.Minute) defer cancel() c := m.Preflight(ctx) s.eng.Audit(r.Context(), p, "certificate.check", m.Status().Domain, map[bool]string{true: "success", false: "failure"}[c.OK], "") writeJSON(w, http.StatusOK, c) return nil } // certificateRenew: POST /certificate/renew gets a new certificate now, in // the background (GET /certificate follows it). func (s *Server) certificateRenew(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := requireAdmin(p); err != nil { return err } m, err := s.acmeManager() if err != nil { return err } m.Kick() s.eng.Audit(r.Context(), p, "certificate.renew", m.Status().Domain, "success", "") st := m.Status() writeJSON(w, http.StatusAccepted, api.CertificateResponse{Configured: true, Source: "acme", Status: &st}) return nil } // certificateProblems are the certificate's problems for /status. func (s *Server) certificateProblems() []validate.Problem { if s.cfg.ACME == nil { return nil } st := s.cfg.ACME.Status() obj := "certificate " + st.Domain var out []validate.Problem if st.LastError != "" { out = append(out, validate.Problem{Severity: validate.Warning, Code: "certificate_failed", Object: obj, Message: "getting the web UI's certificate failed: " + st.LastError}) } if st.NotAfter != nil && time.Until(*st.NotAfter) < 14*24*time.Hour { out = append(out, validate.Problem{Severity: validate.Warning, Code: "certificate_expiring", Object: obj, Message: fmt.Sprintf("the certificate ends %s and hasn't been renewed", st.NotAfter.Local().Format("2006-01-02 15:04"))}) } if st.State == "ok" && st.Staging { out = append(out, validate.Problem{Severity: validate.Warning, Code: "certificate_staging", Object: obj, Message: "the certificate is from Let's Encrypt's staging service, which browsers don't trust: once it works, set tls.acme.directory: letsencrypt and restart housecarld"}) } return out }