package connect import ( "encoding/base64" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" ) func navigationRequest(path string) *http.Request { return httptest.NewRequest("GET", navigationOrigin+path, nil) } func navigationMarker(t *testing.T, n *localLinkNavigation) *http.Cookie { t.Helper() w := httptest.NewRecorder() n.Begin(w, navigationRequest("/account/home")) for _, c := range w.Result().Cookies() { if c.Name == localLinkNavigationCookie && c.MaxAge > 0 { return c } } t.Fatal("marker missing") return nil } func assertNavigation(t *testing.T, n *localLinkNavigation, r *http.Request, want string) { t.Helper() rec := httptest.NewRecorder() n.Complete(rec, r) if rec.Code != 303 || rec.Header().Get("Location") != want || rec.Header().Get("Cache-Control") != "no-store" { t.Fatalf("navigation status=%d target=%q wanted%q", rec.Code, rec.Header().Get("Location"), want) } cleared := false for _, c := range rec.Result().Cookies() { if c.Name == localLinkNavigationCookie && c.MaxAge < 0 { cleared = true } } if !cleared { t.Fatal("marker not cleared") } } func TestLocalNavigationRejectsInvalidMarkersAndOtherHosts(t *testing.T) { local, n := navigationFixture(t) now := time.Now().Truncate(time.Second) local.At = now n.now = func() time.Time { return now } cookie := &http.Cookie{Name: n.sessionCookie, Value: "verified"} valid := navigationMarker(t, n).Value positive := navigationRequest(localLinkCompletePath) positive.AddCookie(cookie) positive.AddCookie(&http.Cookie{Name: localLinkNavigationCookie, Value: valid}) assertNavigation(t, n, positive, "/account/home") plain := func(m localLinkMarker) string { b, _ := json.Marshal(m); return string(b) } base := localLinkMarker{1, "link-start", now.Unix(), now.Unix() + 600} badJSON := []string{ "{}", `{"v":1,"v":1,"action":"link-start","issued":1,"expires":2}`, plain(base) + " {}", strings.TrimSuffix(plain(base), "}") + `,"unknown":true}`, } for _, m := range []localLinkMarker{{2, "link-start", now.Unix(), now.Unix() + 600}, {1, "other", now.Unix(), now.Unix() + 600}, {1, "link-start", 0, now.Unix() + 600}, {1, "link-start", now.Unix() + 1, now.Unix() + 600}, {1, "link-start", now.Unix() - 600, now.Unix()}, {1, "link-start", now.Unix(), now.Unix() + 601}} { badJSON = append(badJSON, plain(m)) } badValues := []string{"", strings.Repeat("a", 1025), valid + "=", "%" + valid, valid[:len(valid)-2]} for _, p := range badJSON { sealed, err := n.sealer.Seal(n.purpose(), []byte(p)) if err != nil { t.Fatal(err) } badValues = append(badValues, base64.RawURLEncoding.EncodeToString(sealed)) } otherPurpose, err := n.sealer.Seal(n.namespace+".local-link-navigation.v1:https://other.example", []byte(plain(base))) if err != nil { t.Fatal(err) } badValues = append(badValues, base64.RawURLEncoding.EncodeToString(otherPurpose)) for _, value := range badValues { r := navigationRequest(localLinkCompletePath) r.AddCookie(cookie) r.AddCookie(&http.Cookie{Name: localLinkNavigationCookie, Value: value}) assertNavigation(t, n, r, "/") } for _, change := range []func(*http.Request){func(r *http.Request) { r.Host = "other.example" }, func(r *http.Request) { r.URL.RawQuery = "return_to=https://evil.example" }, func(r *http.Request) { r.URL.Path = "/wrong" }} { r := navigationRequest(localLinkCompletePath) r.AddCookie(cookie) r.AddCookie(&http.Cookie{Name: localLinkNavigationCookie, Value: valid}) change(r) assertNavigation(t, n, r, "/") } // A valid marker expires independently of the browser cookie's MaxAge. n.now = func() time.Time { return now.Add(600 * time.Second) } r := navigationRequest(localLinkCompletePath) r.AddCookie(cookie) r.AddCookie(&http.Cookie{Name: localLinkNavigationCookie, Value: valid}) assertNavigation(t, n, r, "/") } type navigationBrokenSealer struct{} func (navigationBrokenSealer) Seal(string, []byte) ([]byte, error) { return nil, errors.New("private failure") } func (navigationBrokenSealer) Open(string, []byte) ([]byte, error) { return nil, errors.New("private failure") } func TestLocalNavigationStorageFailureAndMethod(t *testing.T) { _, n := navigationFixture(t) n.sealer = navigationBrokenSealer{} rec := httptest.NewRecorder() n.Begin(rec, navigationRequest("/account/home")) if rec.Code != 503 || strings.Contains(rec.Body.String(), "private") || rec.Header().Get("Location") != "" { t.Fatal("storage failure published/leaked navigation") } for _, fn := range []http.HandlerFunc{n.Begin, n.Complete} { rec = httptest.NewRecorder() r := navigationRequest(localLinkCompletePath) r.Method = "POST" fn(rec, r) if rec.Code != 405 || rec.Header().Get("Allow") != "GET" || rec.Header().Get("Cache-Control") != "no-store" { t.Fatal("wrong method accepted") } } }