Skip to content
quarry Artifact [f676cdbeac]
Login

Artifact [f676cdbeac]

Artifact f676cdbeac27e2f339ae18113d36892d4a4cb535043920df92cc4cb7290f91cb:


package web

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/ops"
)

// A forge reached over https is not usable over http through its proxy:
// a page load is sent to https, and anything carrying a credential or a
// body is refused, not redirected, because it has already been sent.
func TestRequireHTTPSSendsPagesToHTTPSAndRefusesTheRest(t *testing.T) {
	ran := false
	h := RequireHTTPS("https://forge.example", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		ran = true
	}))
	for _, tc := range []struct {
		name, method, proto string
		wantCode            int
		wantRan             bool
		wantLoc, wantHSTS   bool
	}{
		{"a page over http is redirected", "GET", "http", http.StatusPermanentRedirect, false, true, false},
		{"a HEAD over http is redirected", "HEAD", "http", http.StatusPermanentRedirect, false, true, false},
		{"a sync or form post over http is refused", "POST", "http", http.StatusForbidden, false, false, false},
		{"https is served and told to stay https", "GET", "https", http.StatusOK, true, false, true},
		{"a direct loopback request is served as before", "POST", "", http.StatusOK, true, false, false},
	} {
		ran = false
		req := httptest.NewRequest(tc.method, "/r/quarry/xfer?x=1", nil)
		if tc.proto != "" {
			req.Header.Set("X-Forwarded-Proto", tc.proto)
		}
		rec := httptest.NewRecorder()
		h.ServeHTTP(rec, req)
		loc := rec.Header().Get("Location")
		hsts := rec.Header().Get("Strict-Transport-Security") != ""
		switch {
		case rec.Code != tc.wantCode:
			t.Errorf("FAIL: %s: status %d, want %d", tc.name, rec.Code, tc.wantCode)
		case ran != tc.wantRan:
			t.Errorf("FAIL: %s: handler ran = %v, want %v", tc.name, ran, tc.wantRan)
		case tc.wantLoc && loc != "https://forge.example/r/quarry/xfer?x=1":
			t.Errorf("FAIL: %s: redirected to %q", tc.name, loc)
		case hsts != tc.wantHSTS:
			t.Errorf("FAIL: %s: HSTS present = %v, want %v", tc.name, hsts, tc.wantHSTS)
		default:
			t.Logf("ok: %s", tc.name)
		}
	}
}

// A forge that is not served over https has nothing to upgrade to.
func TestRequireHTTPSLeavesAnHTTPForgeAlone(t *testing.T) {
	ran := false
	h := RequireHTTPS("http://127.0.0.1:8977", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ran = true }))
	req := httptest.NewRequest("POST", "/login", nil)
	req.Header.Set("X-Forwarded-Proto", "http")
	h.ServeHTTP(httptest.NewRecorder(), req)
	if !ran {
		t.Error("FAIL: a forge with an http origin refused its own scheme")
	} else {
		t.Log("ok: an http forge is served over http")
	}
}

// Bytes a signed-in reader fetched may be a private repository's, and
// the edge cache in front of the forge must not keep them for the next
// stranger. Only an anonymous reader's response may be shared.
func TestOnlyAnAnonymousReadersBytesMayBeSharedByACache(t *testing.T) {
	anon := httptest.NewRequest("GET", "/r/x/doc/trunk/a.png", nil)
	user := anon.WithContext(ops.WithUser(anon.Context(), "dan"))
	basic := httptest.NewRequest("GET", "/r/x/doc/trunk/a.png", nil)
	basic.SetBasicAuth("dan", "token")
	cookie := httptest.NewRequest("GET", "/r/x/doc/trunk/a.png", nil)
	cookie.AddCookie(&http.Cookie{Name: sessionCookie, Value: "t"})
	for _, tc := range []struct {
		name   string
		req    *http.Request
		public bool
	}{
		{"an anonymous reader", anon, true},
		{"a signed-in reader", user, false},
		{"a reader with a Basic credential", basic, false},
		{"a reader with a session cookie", cookie, false},
	} {
		got := cacheFor(tc.req, 60, false)
		if strings.HasPrefix(got, "public") != tc.public {
			t.Errorf("FAIL: %s got %q", tc.name, got)
		} else {
			t.Logf("ok: %s gets %q", tc.name, got)
		}
	}

	// And the inline attachment route, which serves by hash, follows it.
	rec := httptest.NewRecorder()
	serveInlineMedia(rec, user, "a.png", "image/png", []byte("x"))
	if cc := rec.Header().Get("Cache-Control"); !strings.HasPrefix(cc, "private") {
		t.Errorf("FAIL: a signed-in reader's attachment is cached as %q", cc)
	} else {
		t.Log("ok: a signed-in reader's attachment is private")
	}
}

// A response that says nothing about caching is private (#223): the
// edge caches by URL extension when the origin is silent, and a page
// about one reader's view must not be held for another. A handler that
// states its own policy keeps it, and a stream can still flush.
func TestAResponseThatSaysNothingIsPrivate(t *testing.T) {
	for _, tc := range []struct {
		name, set, want string
	}{
		{"a silent handler", "", "private, no-cache"},
		{"a bare no-cache", "no-cache", "private, no-cache"},
		{"an immutable asset", "public, max-age=31536000, immutable", "public, max-age=31536000, immutable"},
		{"a stream", "no-store", "no-store"},
	} {
		h := PrivateByDefault(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			if tc.set != "" {
				w.Header().Set("Cache-Control", tc.set)
			}
			w.Write([]byte("x"))
		}))
		rec := httptest.NewRecorder()
		h.ServeHTTP(rec, httptest.NewRequest("GET", "/r/x/file/trunk/logo.gif", nil))
		if got := rec.Header().Get("Cache-Control"); got != tc.want {
			t.Errorf("FAIL: %s: %q, want %q", tc.name, got, tc.want)
		} else {
			t.Logf("ok: %s: %q", tc.name, got)
		}
	}
	flushed := false
	h := PrivateByDefault(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		f, ok := w.(http.Flusher)
		if !ok {
			return
		}
		f.Flush()
		flushed = true
	}))
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, httptest.NewRequest("GET", "/r/x/tree.frag", nil))
	if !flushed || !rec.Flushed || rec.Header().Get("Cache-Control") != "private, no-cache" {
		t.Errorf("FAIL: a stream through the wrapper: flushed=%v recorder=%v cc=%q", flushed, rec.Flushed, rec.Header().Get("Cache-Control"))
	} else {
		t.Log("ok: a stream still flushes, and its header went out private")
	}
}