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}