internal/httpd/flash.go
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}