internal/httpd/flash.go

69d8aa4ff9115d6ca1116e09a232e7a6416e4645
gitbay/internal/httpd/flash.go history · blame · raw

108 lines · 3361 bytes

  1package httpd
  2
  3import (
  4	"net/http"
  5	"net/url"
  6	"strings"
  7)
  8
  9// A form action that fails redirects back to the page it came from with
 10// the reason. The reason used to ride the URL as ?e=, so it survived a
 11// reload and landed in history and bookmarks. It rides a one-shot cookie
 12// now: set on the redirect, read and cleared by the page that renders it
 13// (#119).
 14const flashCookie = "gitbay_notice"
 15
 16// setFlash queues msg for the next page render. An empty msg sets
 17// nothing.
 18func (s *Server) setFlash(w http.ResponseWriter, msg string) {
 19	if msg == "" {
 20		return
 21	}
 22	if len(msg) > 300 {
 23		msg = msg[:300]
 24	}
 25	http.SetCookie(w, &http.Cookie{
 26		Name: flashCookie, Value: url.QueryEscape(msg), Path: "/",
 27		HttpOnly: true, SameSite: http.SameSiteLaxMode,
 28		Secure: s.cfg.HTTP.TLS != "off",
 29		MaxAge: 60,
 30	})
 31}
 32
 33// takeFlash returns the queued message, if any, and clears it.
 34func (s *Server) takeFlash(w http.ResponseWriter, r *http.Request) string {
 35	c, err := r.Cookie(flashCookie)
 36	if err != nil || c.Value == "" {
 37		return ""
 38	}
 39	http.SetCookie(w, s.clearCookie(flashCookie, http.SameSiteLaxMode))
 40	msg, err := url.QueryUnescape(c.Value)
 41	if err != nil {
 42		return ""
 43	}
 44	return msg
 45}
 46
 47const nextCookie = "gitbay_next"
 48
 49// setNext remembers the local path an anonymous visitor asked for, so
 50// the login that follows can return there. Only a GET path is stored:
 51// a POST must not be replayed.
 52func (s *Server) setNext(w http.ResponseWriter, path string) {
 53	if !strings.HasPrefix(path, "/") || strings.HasPrefix(path, "//") || len(path) > 300 {
 54		return
 55	}
 56	http.SetCookie(w, &http.Cookie{
 57		Name: nextCookie, Value: url.QueryEscape(path), Path: "/",
 58		HttpOnly: true, SameSite: http.SameSiteLaxMode,
 59		Secure: s.cfg.HTTP.TLS != "off", MaxAge: 600,
 60	})
 61}
 62
 63// takeNext returns the remembered path once and clears it. Anything
 64// that is not a local path comes back empty.
 65func (s *Server) takeNext(w http.ResponseWriter, r *http.Request) string {
 66	c, err := r.Cookie(nextCookie)
 67	if err != nil || c.Value == "" {
 68		return ""
 69	}
 70	http.SetCookie(w, s.clearCookie(nextCookie, http.SameSiteLaxMode))
 71	p, err := url.QueryUnescape(c.Value)
 72	if err != nil || !strings.HasPrefix(p, "/") || strings.HasPrefix(p, "//") {
 73		return ""
 74	}
 75	return p
 76}
 77
 78// peekNext reads the remembered path without clearing it, for the
 79// login page to say where the visitor is going.
 80func (s *Server) peekNext(r *http.Request) string {
 81	c, err := r.Cookie(nextCookie)
 82	if err != nil {
 83		return ""
 84	}
 85	p, err := url.QueryUnescape(c.Value)
 86	if err != nil || !strings.HasPrefix(p, "/") || strings.HasPrefix(p, "//") {
 87		return ""
 88	}
 89	return p
 90}
 91
 92// clearCookie is the expiring twin of a Set-Cookie, carrying the same
 93// attributes the setting call used.
 94//
 95// Deletion works without them — a cookie is identified by name, domain
 96// and path, not by its flags — so this is consistency rather than a live
 97// bug. It is worth having because a reviewer comparing the set and clear
 98// paths should not have to work out whether the difference is deliberate,
 99// and because a scanner will otherwise flag the bare form every time
100// (go:S2092, go:S3330, #153).
101func (s *Server) clearCookie(name string, sameSite http.SameSite) *http.Cookie {
102	return &http.Cookie{
103		Name: name, Value: "", Path: "/",
104		HttpOnly: true, SameSite: sameSite,
105		Secure: s.cfg.HTTP.TLS != "off",
106		MaxAge: -1,
107	}
108}