package main import ( "fmt" "io" "os" "strings" "time" "github.com/spf13/cobra" "housecarl/internal/api" ) func certCmd() *cobra.Command { cmd := &cobra.Command{ Use: "cert", Aliases: []string{"certificate"}, Short: "The web UI's certificate, obtained through Jarl (administrators)", Long: `Shows the certificate housecarld obtains itself for its web UI (tls.acme in housecarl.yaml, with Jarl publishing the DNS-01 challenge; docs/certificates.md). 'check' tests the whole path without asking the CA; 'renew' gets a new certificate now.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return certStatus() }, } cmd.AddCommand(&cobra.Command{Use: "status", Short: "Show the certificate", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return certStatus() }}) cmd.AddCommand(&cobra.Command{ Use: "check", Short: "Check that the challenge can be published and seen, without the CA", Long: `Runs the check housecarld runs before every certificate order: Jarl answers and knows the token, a zone holds _acme-challenge.DOMAIN, the token may write it, a test record is written, the zone's name servers and the public resolvers serve it, and it is removed again. It can take a minute or two.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { c, err := newClient() if err != nil { return err } c.HTTP.Timeout = 5 * time.Minute if !flagQuiet && flagOutput == "table" { fmt.Fprintln(os.Stderr, "checking (this can take a minute or two)…") } var ch api.CertCheck if err := c.Do(ctx(), "POST", "/certificate/check", nil, nil, &ch); err != nil { return err } if err := output(ch, func(w io.Writer) { printCheck(w, &ch) }); err != nil { return err } if !ch.OK { return fmt.Errorf("the check failed") } return nil }, }) var wait bool renew := &cobra.Command{ Use: "renew", Short: "Get a new certificate now (the check runs first)", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { c, err := newClient() if err != nil { return err } var r api.CertificateResponse if err := c.Do(ctx(), "POST", "/certificate/renew", nil, nil, &r); err != nil { return err } if !wait { fmt.Println("renewing in the background; follow it with 'hc cert'") return nil } start := time.Now() for { time.Sleep(3 * time.Second) if err := c.Get(ctx(), "/certificate", nil, &r); err != nil { return err } st := r.Status if st.State != "working" && st.LastAttempt != nil && !st.LastAttempt.Before(start.Add(-time.Second)) { break } } if err := certStatus(); err != nil { return err } if r.Status.LastError != "" { return fmt.Errorf("renewing failed") } return nil }, } renew.Flags().BoolVarP(&wait, "wait", "w", false, "wait until it is done") cmd.AddCommand(renew) return cmd } func certStatus() error { c, err := newClient() if err != nil { return err } var r api.CertificateResponse if err := c.Get(ctx(), "/certificate", nil, &r); err != nil { return err } return output(r, func(w io.Writer) { switch r.Source { case "file": fmt.Fprintln(w, "Source:\ttls.cert_file (renewed by you)") return case "none": fmt.Fprintln(w, "Source:\tnone: the web UI is plain HTTP (set tls.acme or tls.cert_file in housecarl.yaml; docs/certificates.md)") return } st := r.Status fmt.Fprintf(w, "Domain:\t%s\n", st.Domain) ca := st.Directory if st.Staging { ca += " (staging: browsers don't trust it)" } fmt.Fprintf(w, "CA:\t%s\n", ca) fmt.Fprintf(w, "State:\t%s\n", st.State) if st.NotAfter != nil { fmt.Fprintf(w, "Valid:\t%s to %s, from %s\n", st.NotBefore.Local().Format("2006-01-02"), st.NotAfter.Local().Format("2006-01-02 15:04"), st.Issuer) fmt.Fprintf(w, "Renews:\t%s\n", st.RenewAt.Local().Format("2006-01-02")) } if st.LastAttempt != nil { fmt.Fprintf(w, "Last attempt:\t%s\n", st.LastAttempt.Local().Format("2006-01-02 15:04")) } if st.NextAttempt != nil && st.State != "working" { fmt.Fprintf(w, "Next check:\t%s\n", st.NextAttempt.Local().Format("2006-01-02 15:04")) } if st.LastError != "" { fmt.Fprintf(w, "Error:\t%s\n", st.LastError) } if st.Check != nil { fmt.Fprintf(w, "\nLast check, %s:\n", st.Check.Time.Local().Format("2006-01-02 15:04")) printCheck(w, st.Check) } }) } func printCheck(w io.Writer, c *api.CertCheck) { for _, s := range c.Steps { mark := map[string]string{"ok": "ok ", "fail": "FAIL", "warn": "warn", "skip": "skip"}[s.Status] fmt.Fprintf(w, " %s\t%s\t%s\n", mark, s.Name, s.Detail) if s.Status == "fail" && len(s.Queries) > 0 { for _, q := range s.Queries { fmt.Fprintf(w, " \t\t%s\n", strings.TrimSpace(q)) } } } }