package httpserver import ( "context" "crypto/hmac" "crypto/sha256" "crypto/subtle" "encoding/hex" "errors" "fmt" "net/http" "os" "strconv" "time" "github.com/example/notify-gateway/internal/gateway" "github.com/example/notify-gateway/internal/mailingress" "github.com/example/notify-gateway/internal/outbox" ) func (s *Server) UseQueue(q *gateway.Queue, p *mailingress.Poller) { s.queue = q; s.mail = p } func (s *Server) ready(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() if s.queue == nil || s.queue.Store.Ping(ctx) != nil { writeJSON(w, 503, map[string]any{"ok": false}) return } writeJSON(w, 200, map[string]any{"ok": true}) } func (s *Server) deliveries(w http.ResponseWriter, r *http.Request) { if s.queue == nil { http.Error(w, "outbox unavailable", 503) return } offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) jobs, err := s.queue.Store.List(r.Context(), 50, offset) if err != nil { http.Error(w, "outbox unavailable", 503) return } counts, err := s.queue.Store.Counts(r.Context()) if err != nil { http.Error(w, "outbox unavailable", 503) return } statuses := []mailingress.Status{} if s.mail != nil { statuses = s.mail.Statuses() } writeJSON(w, 200, map[string]any{"deliveries": jobs, "counts": counts, "mail": statuses}) } func (s *Server) deliveryHistory(w http.ResponseWriter, r *http.Request) { if s.queue == nil { http.Error(w, "outbox unavailable", 503) return } rows, err := s.queue.Store.History(r.Context(), r.PathValue("id")) if err != nil { http.Error(w, "outbox unavailable", 503) return } writeJSON(w, 200, rows) } func (s *Server) retryDelivery(w http.ResponseWriter, r *http.Request) { if s.queue == nil { http.Error(w, "outbox unavailable", 503) return } err := s.queue.Store.Retry(r.Context(), r.PathValue("id")) if errors.Is(err, outbox.ErrNotRetryable) { http.Error(w, err.Error(), 409) return } if err != nil { http.Error(w, "outbox unavailable", 503) return } writeJSON(w, 200, map[string]any{"ok": true}) } func (s *Server) metrics(w http.ResponseWriter, r *http.Request) { token := os.Getenv("GATEWAY_METRICS_TOKEN") if token == "" { s.requireAdmin(s.writeMetrics)(w, r) return } got := r.Header.Get("Authorization") if subtle.ConstantTimeCompare([]byte(got), []byte("Bearer "+token)) != 1 { http.Error(w, "unauthorized", 401) return } s.writeMetrics(w, r) } func (s *Server) writeMetrics(w http.ResponseWriter, r *http.Request) { if s.queue == nil { http.Error(w, "outbox unavailable", 503) return } counts, err := s.queue.Store.Counts(r.Context()) if err != nil { http.Error(w, "outbox unavailable", 503) return } w.Header().Set("Content-Type", "text/plain; version=0.0.4") fmt.Fprintln(w, "# HELP notify_gateway_deliveries Persisted deliveries by current state.\n# TYPE notify_gateway_deliveries gauge") for _, state := range []string{"pending", "sending", "succeeded", "dry_run", "dead"} { fmt.Fprintf(w, "notify_gateway_deliveries{state=%q} %d\n", state, counts[state]) } } func csrfToken(secret, session string) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte("csrf:" + session)) return hex.EncodeToString(mac.Sum(nil)) } func (s *Server) csrf(w http.ResponseWriter, r *http.Request) { c, _ := r.Cookie("ng_session") writeJSON(w, 200, map[string]string{"token": csrfToken(s.store.Get().Server.SessionSecret, c.Value)}) }