internal/httpd/flash.go

128 lines · 3929 bytes

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