eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "encoding/json"
5 "net/http"
6 "net/http/httptest"
7 "net/url"
8 "testing"
9 "time"
10)
11
12func newTestLinksServer(t *testing.T) *httptest.Server {
13 t.Helper()
14 allowPrivatePreviewHosts = true // reuse the shared SSRF-guard test escape hatch
15 linksInsecureSkipVerifyCert = true
16 t.Cleanup(func() {
17 allowPrivatePreviewHosts = false
18 linksInsecureSkipVerifyCert = false
19 })
20 l := newLinksServer()
21 ts := httptest.NewServer(l.handler())
22 t.Cleanup(ts.Close)
23 return ts
24}
25
26// fakeLchatInstance stands up an httptest TLS server that answers
27// /api/version like a real instance (probeLchatInstance always fetches
28// https://), so its shape check can pass — self-signed, hence
29// linksInsecureSkipVerifyCert in tests.
30func fakeLchatInstance(t *testing.T, build, version string) *httptest.Server {
31 t.Helper()
32 ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33 if r.URL.Path != "/api/version" {
34 http.NotFound(w, r)
35 return
36 }
37 json.NewEncoder(w).Encode(map[string]string{"build": build, "version": version})
38 }))
39 t.Cleanup(ts.Close)
40 return ts
41}
42
43func noRedirectClient() *http.Client {
44 return &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }}
45}
46
47func TestLinksRootRedirectsToMarketingSite(t *testing.T) {
48 ts := newTestLinksServer(t)
49 resp, err := noRedirectClient().Get(ts.URL + "/")
50 if err != nil {
51 t.Fatalf("GET /: %v", err)
52 }
53 defer resp.Body.Close()
54 if resp.StatusCode != http.StatusFound {
55 t.Fatalf("status = %d, want 302", resp.StatusCode)
56 }
57 if loc := resp.Header.Get("Location"); loc != linksMarketingURL {
58 t.Fatalf("Location = %q, want %q", loc, linksMarketingURL)
59 }
60}
61
62func TestLinksArbitraryPathAlsoRedirectsToMarketingSite(t *testing.T) {
63 ts := newTestLinksServer(t)
64 resp, err := noRedirectClient().Get(ts.URL + "/whatever/else")
65 if err != nil {
66 t.Fatalf("GET: %v", err)
67 }
68 defer resp.Body.Close()
69 if resp.StatusCode != http.StatusFound || resp.Header.Get("Location") != linksMarketingURL {
70 t.Fatalf("status=%d location=%q", resp.StatusCode, resp.Header.Get("Location"))
71 }
72}
73
74func TestLinksServesAppleAppSiteAssociation(t *testing.T) {
75 ts := newTestLinksServer(t)
76 resp, err := http.Get(ts.URL + "/.well-known/apple-app-site-association")
77 if err != nil {
78 t.Fatalf("GET aasa: %v", err)
79 }
80 defer resp.Body.Close()
81 var body struct {
82 Applinks struct {
83 Details []struct {
84 AppIDs []string `json:"appIDs"`
85 Components []struct {
86 Path string `json:"/"`
87 } `json:"components"`
88 } `json:"details"`
89 } `json:"applinks"`
90 }
91 if json.NewDecoder(resp.Body).Decode(&body) != nil || len(body.Applinks.Details) != 1 {
92 t.Fatalf("unexpected aasa body")
93 }
94 d := body.Applinks.Details[0]
95 if len(d.AppIDs) != 1 || d.AppIDs[0] != appleAppID {
96 t.Fatalf("appIDs = %v, want [%q]", d.AppIDs, appleAppID)
97 }
98 if len(d.Components) != 1 || d.Components[0].Path != "/l/*" {
99 t.Fatalf("components = %v, want /l/*", d.Components)
100 }
101}
102
103func TestLinksRedirectsToAVerifiedInstance(t *testing.T) {
104 ts := newTestLinksServer(t)
105 instance := fakeLchatInstance(t, "abc123", "def456")
106 u, _ := url.Parse(instance.URL)
107
108 resp, err := noRedirectClient().Get(ts.URL + "/l/" + u.Host)
109 if err != nil {
110 t.Fatalf("GET /l/: %v", err)
111 }
112 defer resp.Body.Close()
113 if resp.StatusCode != http.StatusFound {
114 t.Fatalf("status = %d, want 302", resp.StatusCode)
115 }
116 want := "https://" + u.Host + "/"
117 if loc := resp.Header.Get("Location"); loc != want {
118 t.Fatalf("Location = %q, want %q (no fragment — the caller's own #l= must reattach on its own)", loc, want)
119 }
120}
121
122func TestLinksRejectsUnverifiedHost(t *testing.T) {
123 ts := newTestLinksServer(t)
124 resp, err := noRedirectClient().Get(ts.URL + "/l/nothing-answers-here.invalid")
125 if err != nil {
126 t.Fatalf("GET: %v", err)
127 }
128 defer resp.Body.Close()
129 if resp.StatusCode != http.StatusNotFound {
130 t.Fatalf("status = %d, want 404", resp.StatusCode)
131 }
132}
133
134func TestLinksRejectsMalformedHostWithoutAnyNetworkAttempt(t *testing.T) {
135 allowPrivatePreviewHosts = false // deliberately NOT the test escape hatch: this must 404 on shape alone
136 l := newLinksServer()
137 ts := httptest.NewServer(l.handler())
138 defer ts.Close()
139
140 // "../.." isn't tested here — net/http's ServeMux cleans dot-segments out
141 // of the path before a handler ever sees them, so it never reaches our
142 // own validation at all (a different, earlier-stage protection).
143 for _, bad := range []string{"not_a_host!!", "a..b", "-leading-dash", "no-dot-at-all"} {
144 resp, err := noRedirectClient().Get(ts.URL + "/l/" + url.PathEscape(bad))
145 if err != nil {
146 t.Fatalf("GET %q: %v", bad, err)
147 }
148 resp.Body.Close()
149 if resp.StatusCode != http.StatusNotFound {
150 t.Fatalf("host %q: status = %d, want 404", bad, resp.StatusCode)
151 }
152 }
153}
154
155func TestLinksVerifyCacheServesFreshHitsWithoutReprobing(t *testing.T) {
156 allowPrivatePreviewHosts = true
157 linksInsecureSkipVerifyCert = true
158 defer func() {
159 allowPrivatePreviewHosts = false
160 linksInsecureSkipVerifyCert = false
161 }()
162
163 calls := 0
164 instance := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
165 calls++
166 json.NewEncoder(w).Encode(map[string]string{"build": "b", "version": "v"})
167 }))
168 defer instance.Close()
169 u, _ := url.Parse(instance.URL)
170
171 c := newLinksVerifyCache()
172 if !c.verify(u.Host) {
173 t.Fatal("first verify should succeed")
174 }
175 if !c.verify(u.Host) {
176 t.Fatal("second verify (cache hit) should still report ok")
177 }
178 if calls != 1 {
179 t.Fatalf("expected exactly 1 live probe (second call served from cache), got %d", calls)
180 }
181}
182
183func TestLinksVerifyCacheFailEntryRetriesSoonerThanFreshEntry(t *testing.T) {
184 c := newLinksVerifyCache()
185 c.entries["dead.example"] = linksCacheEntry{ok: false, checkedAt: time.Now().Add(-2 * time.Hour)}
186 c.entries["alive.example"] = linksCacheEntry{ok: true, checkedAt: time.Now().Add(-2 * time.Hour)}
187
188 // Both entries are 2h stale: the fail TTL (1h) has elapsed so "dead" is due
189 // for a re-probe, but the fresh TTL (7d) hasn't for "alive" — it should
190 // still serve the cached true without a network attempt.
191 allowPrivatePreviewHosts = false // no real network should happen for the fresh hit
192 if !c.verify("alive.example") {
193 t.Fatal("a fresh (within 7d) cached ok=true entry should serve from cache, not re-probe")
194 }
195}
196
197func TestLinksRateLimiterBlocksAFlood(t *testing.T) {
198 ts := newTestLinksServer(t)
199 var lastStatus int
200 for i := 0; i < 40; i++ {
201 resp, err := noRedirectClient().Get(ts.URL + "/l/nothing-answers-here.invalid")
202 if err != nil {
203 t.Fatalf("GET: %v", err)
204 }
205 lastStatus = resp.StatusCode
206 resp.Body.Close()
207 if lastStatus == http.StatusTooManyRequests {
208 return
209 }
210 }
211 t.Fatalf("expected a 429 within 40 rapid requests from one IP, last status was %d", lastStatus)
212}
213