oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package connect
2
3import (
4 "bytes"
5 "encoding/base64"
6 "encoding/json"
7 "math"
8 "net/http"
9 "net/url"
10 "time"
11
12 "amadan.net/oficina/home/handoff"
13)
14
15const localLinkNavigationCookie = "__Host-oficina-local-link-navigation"
16const localLinkNavigationLifetime = int64(600)
17const localLinkNavigationLimit = 1024
18const localLinkCompletePath = "/auth/local/complete"
19
20type localLinkMarker struct {
21 Version int `json:"v"`
22 Action string `json:"action"`
23 Issued int64 `json:"issued"`
24 Expires int64 `json:"expires"`
25}
26
27type localLinkNavigation struct {
28 landingPath string
29 sealer handoff.Sealer
30 sessionCookie string
31 readSession func(*http.Request) (*Session, error)
32 namespace string
33 fail func(http.ResponseWriter, *http.Request, int)
34 origin, host string
35 now func() time.Time
36}
37
38func newLocalLinkNavigation(sealer handoff.Sealer, c RoutesConfig) *localLinkNavigation {
39 u, err := url.Parse(c.Origin)
40 if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.Path != "" || u.RawQuery != "" || u.Fragment != "" || sealer == nil || c.ReadSession == nil {
41 return nil
42 }
43 landing := c.LocalLandingPath
44 if landing == "" {
45 landing = "/"
46 }
47 if !handoff.SafePath(landing) {
48 return nil
49 }
50 return &localLinkNavigation{landingPath: landing, sealer: sealer, sessionCookie: c.SessionCookie, readSession: c.ReadSession, namespace: c.Namespace, origin: c.Origin, host: u.Host, now: time.Now, fail: func(w http.ResponseWriter, r *http.Request, status int) { c.Render(w, r, "failed", "", status) }}
51}
52
53func localNavigationCookie(w http.ResponseWriter, name, value string, age int) {
54 http.SetCookie(w, &http.Cookie{Name: name, Value: value, Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: age})
55}
56
57func (n *localLinkNavigation) purpose() string {
58 return n.namespace + ".local-link-navigation.v1:" + n.origin
59}
60
61func (n *localLinkNavigation) guard(w http.ResponseWriter, r *http.Request, path string) bool {
62 w.Header().Set("Cache-Control", "no-store")
63 if r.Method != http.MethodGet {
64 w.Header().Set("Allow", http.MethodGet)
65 n.fail(w, r, http.StatusMethodNotAllowed)
66 return false
67 }
68 if r.Host != n.host || r.URL.Path != path || r.URL.RawQuery != "" {
69 http.Redirect(w, r, "/", http.StatusSeeOther)
70 return false
71 }
72 return true
73}
74
75// Begin abandons browser pointers to an old link. Rotation and freshness also
76// invalidate its binding; clearing cookies does not consume database rows.
77// A cross-site top-level GET may abandon a flow, but cannot create a link.
78func (n *localLinkNavigation) Begin(w http.ResponseWriter, r *http.Request) {
79 localNavigationCookie(w, localLinkNavigationCookie, "", -1)
80 if !n.guard(w, r, "/account/home") {
81 return
82 }
83 localNavigationCookie(w, homeInitCookie, "", -1)
84 localNavigationCookie(w, homeProofCookie, "", -1)
85 now := n.now().Unix()
86 if now <= 0 || now > math.MaxInt64-localLinkNavigationLifetime {
87 http.Redirect(w, r, "/", http.StatusSeeOther)
88 return
89 }
90 payload, _ := json.Marshal(localLinkMarker{Version: 1, Action: "link-start", Issued: now, Expires: now + localLinkNavigationLifetime})
91 sealed, err := n.sealer.Seal(n.purpose(), payload)
92 if err != nil {
93 n.fail(w, r, http.StatusServiceUnavailable)
94 return
95 }
96 encoded := base64.RawURLEncoding.EncodeToString(sealed)
97 if len(encoded) > localLinkNavigationLimit {
98 n.fail(w, r, http.StatusServiceUnavailable)
99 return
100 }
101 localNavigationCookie(w, localLinkNavigationCookie, encoded, int(localLinkNavigationLifetime))
102 http.Redirect(w, r, "/signin/local", http.StatusSeeOther)
103}
104
105func (n *localLinkNavigation) marker(r *http.Request, now int64) bool {
106 value := ""
107 count := 0
108 for _, c := range r.Cookies() {
109 if c.Name == localLinkNavigationCookie {
110 count++
111 value = c.Value
112 }
113 }
114 if count != 1 || value == "" || len(value) > localLinkNavigationLimit {
115 return false
116 }
117 sealed, err := base64.RawURLEncoding.Strict().DecodeString(value)
118 if err != nil || base64.RawURLEncoding.EncodeToString(sealed) != value {
119 return false
120 }
121 payload, err := n.sealer.Open(n.purpose(), sealed)
122 if err != nil || len(payload) > localLinkNavigationLimit {
123 return false
124 }
125 var marker localLinkMarker
126 if json.Unmarshal(payload, &marker) != nil {
127 return false
128 }
129 // Only this helper writes the payload. Exact re-encoding excludes duplicate
130 // and unknown fields, alternate numbers, and any trailing JSON value.
131 canonical, _ := json.Marshal(marker)
132 if !bytes.Equal(payload, canonical) {
133 return false
134 }
135 return marker.Version == 1 && marker.Action == "link-start" && marker.Issued > 0 && marker.Issued <= now && now < marker.Expires && marker.Expires > marker.Issued && marker.Expires-marker.Issued <= localLinkNavigationLifetime
136}
137
138// Complete reads the newly presented session after local verification. It
139// cannot initiate or confirm linking, and never treats a Home mint as local
140// proof. A copied marker can at most repeat navigation to the same page.
141func (n *localLinkNavigation) Complete(w http.ResponseWriter, r *http.Request) {
142 localNavigationCookie(w, localLinkNavigationCookie, "", -1)
143 if !n.guard(w, r, localLinkCompletePath) {
144 return
145 }
146 now := n.now()
147 target := n.landingPath
148 if n.marker(r, now.Unix()) {
149 count := 0
150 for _, c := range r.Cookies() {
151 if c.Name == n.sessionCookie {
152 count++
153 }
154 }
155 if count == 1 {
156 if session, err := n.readSession(r); err == nil && session != nil {
157 if _, fresh := FreshLocal(*session, now); fresh {
158 target = "/account/home"
159 }
160 }
161 }
162 }
163 http.Redirect(w, r, target, http.StatusSeeOther)
164}
165