tinyrelayfile
tinyrelay/internal/daemon/tinyclient_integration_test.go/

internal/daemon/tinyclient_integration_test.go raw
1package daemon
2
3import (
4 "context"
5 "encoding/json"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "net/url"
10 "strings"
11 "testing"
12
13 "github.com/FelineStateMachine/tinyrelay/tinyclient"
14)
15
16func TestStandaloneTinyclientAuthenticatesAgainstRealRelay(t *testing.T) {
17 _, tenant := testTenant(t)
18 upstream := httptest.NewServer(tenant)
19 defer upstream.Close()
20 handler, err := tinyclient.NewRemote(tinyclient.RemoteOptions{BackendURL: upstream.URL, PublicURL: "http://relay.test"})
21 if err != nil {
22 t.Fatal(err)
23 }
24 frontend := httptest.NewServer(handler)
25 defer frontend.Close()
26 frontendURL, _ := url.Parse(frontend.URL)
27 client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
28 issue := func(method, path, body string, cookie *http.Cookie, signed bool) (*http.Response, string) {
29 t.Helper()
30 request := httptest.NewRequest(method, "http://relay.test"+path, strings.NewReader(body))
31 request.Header.Set("Content-Type", "application/json")
32 request.Header.Set("Origin", "http://relay.test")
33 if cookie != nil {
34 request.AddCookie(cookie)
35 }
36 if signed {
37 signRequest(t, request, body)
38 }
39 request.URL.Scheme, request.URL.Host = frontendURL.Scheme, frontendURL.Host
40 request.RequestURI = ""
41 response, err := client.Do(request)
42 if err != nil {
43 t.Fatal(err)
44 }
45 raw, err := io.ReadAll(response.Body)
46 response.Body.Close()
47 if err != nil {
48 t.Fatal(err)
49 }
50 return response, string(raw)
51 }
52 login, body := issue("POST", "/session", "", nil, true)
53 if login.StatusCode != 200 || len(login.Cookies()) != 1 {
54 t.Fatalf("signed login: %d %s", login.StatusCode, body)
55 }
56 cookie := login.Cookies()[0]
57 if !cookie.HttpOnly || cookie.SameSite != http.SameSiteStrictMode {
58 t.Fatalf("unsafe cookie: %+v", cookie)
59 }
60 for _, path := range []string{"/", "/account", "/manage/rules", "/chat", "/scripts/bridge.js", "/manifest.webmanifest"} {
61 response, body := issue("GET", path, "", cookie, false)
62 integrated := httptest.NewRecorder()
63 request := httptest.NewRequest("GET", "http://relay.test"+path, nil)
64 request.AddCookie(cookie)
65 tenant.ServeHTTP(integrated, request)
66 if response.StatusCode != integrated.Code || body != integrated.Body.String() {
67 t.Fatalf("standalone parity %s: status=%d want=%d body lengths=%d/%d", path, response.StatusCode, integrated.Code, len(body), integrated.Body.Len())
68 }
69 }
70 denied, _ := issue("POST", "/manage/rpc", `{"method":"stats"}`, cookie, false)
71 if denied.StatusCode != 401 {
72 t.Fatalf("cookie-only write accepted: %d", denied.StatusCode)
73 }
74 signed, body := issue("POST", "/manage/rpc", `{"method":"stats"}`, cookie, true)
75 if signed.StatusCode != 200 {
76 t.Fatalf("signed management request lost URL binding: %d %s", signed.StatusCode, body)
77 }
78 // The new read bridge ignores claimed pubkeys and still requires a real session.
79 forged, body := issue("GET", tinyclient.BackendPath+"?actor="+tenant.Policy().Owner, "", nil, false)
80 var snapshot struct {
81 Actor string `json:"actor"`
82 }
83 if forged.StatusCode != 200 || json.Unmarshal([]byte(body), &snapshot) != nil || snapshot.Actor != "" {
84 t.Fatalf("forged actor accepted: %d %s", forged.StatusCode, body)
85 }
86 logout, body := issue("POST", "/session/logout", "", cookie, false)
87 if logout.StatusCode != 200 {
88 t.Fatalf("logout: %d %s", logout.StatusCode, body)
89 }
90 response, body := issue("GET", "/account", "", cookie, false)
91 if response.StatusCode != 200 || !strings.Contains(body, "Sign in to open your account.") {
92 t.Fatalf("revoked session survived: %d", response.StatusCode)
93 }
94 p := tenant.Policy()
95 p.Features.Grasp, p.Features.Grasp08, p.Reads, p.Name = true, true, "members", "private secret project"
96 if err := tenant.applyPolicy(context.Background(), p); err != nil {
97 t.Fatal(err)
98 }
99 for _, path := range []string{"/", tinyclient.BackendPath} {
100 response, body := issue("GET", path, "", cookie, false)
101 if response.StatusCode != 200 || strings.Contains(body, p.Name) || strings.Contains(body, p.Owner) {
102 t.Fatalf("private metadata leaked through %s: %d", path, response.StatusCode)
103 }
104 }
105 response, _ = issue("GET", "/social", "", cookie, false)
106 if response.StatusCode != 401 {
107 t.Fatalf("private read without membership: %d", response.StatusCode)
108 }
109}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs