carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package edge
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "strings"
7 "testing"
8)
9
10// The "not here YET" answer (#174).
11
12// pendingEdge is an edge serving one route, with the pending answer armed
13// for go.tito.example and apps.example.
14func pendingEdge(t *testing.T, fallback string) *Edge {
15 t.Helper()
16 e := purgeHostsEdge(t, titoAccount(), func(w http.ResponseWriter, r *http.Request) {
17 w.WriteHeader(http.StatusOK)
18 })
19 e.fallbackURL = fallback
20 e.setPlatformDomains(platformDomainsFromEnv(func(k string) string {
21 return map[string]string{
22 "CARLOS_WILDCARD_DOMAINS": "go.tito.example, GO.TITO.EXAMPLE",
23 "PLATFORM_APPS_DOMAIN": "apps.example.",
24 }[k]
25 }))
26 return e
27}
28
29func get(e *Edge, method, url string) *httptest.ResponseRecorder {
30 rec := httptest.NewRecorder()
31 e.ServeHTTP(rec, httptest.NewRequest(method, url, nil))
32 return rec
33}
34
35// TestPendingHostUnderAPlatformDomainIs503 is the headline: a host the edge
36// has not heard of yet, under a platform domain, gets a retryable answer
37// rather than a dead end.
38func TestPendingHostUnderAPlatformDomainIs503(t *testing.T) {
39 e := pendingEdge(t, "")
40 for _, url := range []string{
41 "https://sb-andyet.go.tito.example/dashboard",
42 "https://acct.apps.example/",
43 "https://app.acct.apps.example/x?y=1",
44 } {
45 rec := get(e, "GET", url)
46 if rec.Code != http.StatusServiceUnavailable {
47 t.Fatalf("%s: status %d, want 503", url, rec.Code)
48 }
49 if got := rec.Header().Get("Retry-After"); got != pendingRetryAfter {
50 t.Fatalf("%s: Retry-After %q, want %q", url, got, pendingRetryAfter)
51 }
52 if got := rec.Header().Get("Cache-Control"); got != "no-store" {
53 t.Fatalf("%s: Cache-Control %q, want no-store", url, got)
54 }
55 body := rec.Body.String()
56 if !strings.Contains(body, pendingHeading) || !strings.Contains(body, "location.replace(location.href)") {
57 t.Fatalf("%s: body is not the retrying pending page:\n%s", url, body)
58 }
59 }
60}
61
62// TestPendingPageNeverEchoesTheRequest — the host, path and query are
63// attacker-chosen for an unknown host; none of them may reach the page.
64func TestPendingPageNeverEchoesTheRequest(t *testing.T) {
65 e := pendingEdge(t, "")
66 rec := get(e, "GET", "https://xss-probe.go.tito.example/%3Cscript%3E?q=%3Cimg%3E")
67 body := rec.Body.String()
68 for _, s := range []string{"xss-probe", "<script>alert", "%3C", "<img"} {
69 if strings.Contains(body, s) {
70 t.Fatalf("the pending page echoed %q from the request:\n%s", s, body)
71 }
72 }
73}
74
75// TestPendingHostHEADHasNoBody — the headers alone.
76func TestPendingHostHEADHasNoBody(t *testing.T) {
77 e := pendingEdge(t, "")
78 rec := get(e, "HEAD", "https://sb-new.go.tito.example/")
79 if rec.Code != http.StatusServiceUnavailable || rec.Body.Len() != 0 {
80 t.Fatalf("HEAD: status %d with a %d-byte body, want 503 and none", rec.Code, rec.Body.Len())
81 }
82}
83
84// TestUnknownHostOutsidePlatformDomainsIsUnchanged — a customer domain, and a
85// platform domain's own apex, keep the old answers exactly: 404 without a
86// fallback, the 302 with one.
87func TestUnknownHostOutsidePlatformDomainsIsUnchanged(t *testing.T) {
88 for _, tc := range []struct {
89 fallback string
90 want int
91 }{{"", http.StatusNotFound}, {"https://www.example.com/", http.StatusFound}} {
92 e := pendingEdge(t, tc.fallback)
93 for _, url := range []string{
94 "https://customer.example.org/",
95 "https://apps.example/", // the apex itself (go.tito.example is a served route here)
96 "https://evilgo.tito.example/", // not a label boundary of go.tito.example
97 } {
98 if rec := get(e, "GET", url); rec.Code != tc.want {
99 t.Fatalf("fallback %q, %s: status %d, want %d", tc.fallback, url, rec.Code, tc.want)
100 }
101 }
102 }
103}
104
105// TestPendingPageGivesUpToTheFallback — an edge with a fallback URL still
106// sends a mistyped or retired platform subdomain there, after the retry
107// window, so the fallback keeps doing its job. The URL is embedded as a JS
108// string, escaped.
109func TestPendingPageGivesUpToTheFallback(t *testing.T) {
110 e := pendingEdge(t, `https://www.example.com/?a=1&b="x"</script>`)
111 body := get(e, "GET", "https://typo.go.tito.example/").Body.String()
112 if !strings.Contains(body, "location.replace(f)") {
113 t.Fatalf("the page does not go to the fallback when it gives up:\n%s", body)
114 }
115 if strings.Count(body, "</script>") != 1 {
116 t.Fatalf("the fallback URL broke out of the script element:\n%s", body)
117 }
118 if !strings.Contains(body, `"https://www.example.com/?a=1`+"\\u0026"+`b=\"x\"`+"\\u003c"+`/script`+"\\u003e"+`"`) {
119 t.Fatalf("the fallback URL is not embedded as an escaped JS string:\n%s", body)
120 }
121}
122
123// TestPendingAnswerIsOffWithoutPlatformDomains — New alone (no env) arms
124// nothing: every unknown host is the plain 404, as before #174.
125func TestPendingAnswerIsOffWithoutPlatformDomains(t *testing.T) {
126 e := purgeHostsEdge(t, titoAccount(), func(w http.ResponseWriter, r *http.Request) {})
127 if rec := get(e, "GET", "https://sb-new.go.tito.example/"); rec.Code != http.StatusNotFound {
128 t.Fatalf("status %d, want 404 with no platform domains configured", rec.Code)
129 }
130}
131
132// TestKnownHostUnderAPlatformDomainIsServed — the pending answer is only
133// ever the miss branch.
134func TestKnownHostUnderAPlatformDomainIsServed(t *testing.T) {
135 e := pendingEdge(t, "")
136 if rec := get(e, "GET", "https://acme.go.tito.example/"); rec.Code != http.StatusOK {
137 t.Fatalf("a served host under a platform domain got %d", rec.Code)
138 }
139}
140
141// TestPendingHostOnPlainHTTP — :80 for an unknown platform-domain host gets
142// the same answer rather than a redirect into a handshake.
143func TestPendingHostOnPlainHTTP(t *testing.T) {
144 e := pendingEdge(t, "")
145 rec := httptest.NewRecorder()
146 e.httpFallbackHandler().ServeHTTP(rec, httptest.NewRequest("GET", "http://sb-new.go.tito.example/", nil))
147 if rec.Code != http.StatusServiceUnavailable {
148 t.Fatalf(":80 status %d, want 503", rec.Code)
149 }
150}
151
152// TestPendingAnswerNeedsTheRouteTableForm — routing matches r.Host byte for
153// byte, so an uppercase or trailing-dot spelling of a platform host can miss
154// even when the host is served. Those keep the plain unknown-host answer
155// rather than 90 seconds of "still being set up".
156func TestPendingAnswerNeedsTheRouteTableForm(t *testing.T) {
157 e := pendingEdge(t, "")
158 for _, host := range []string{"Acme.go.tito.example", "acme.go.tito.example.", "SB-NEW.GO.TITO.EXAMPLE"} {
159 req := httptest.NewRequest("GET", "https://x/", nil)
160 req.Host = host
161 rec := httptest.NewRecorder()
162 e.ServeHTTP(rec, req)
163 if rec.Code != http.StatusNotFound {
164 t.Fatalf("%s: status %d, want the plain 404", host, rec.Code)
165 }
166 }
167}
168