oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1package callscore
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/json"
8 "net/http"
9 "net/http/httptest"
10 "strings"
11 "sync/atomic"
12 "testing"
13 "time"
14)
15
16func TestAuthenticationProofAndCancellation(t *testing.T) {
17 id := base64.RawURLEncoding.EncodeToString(make([]byte, 32))
18 challengeReceived := make(chan string, 1)
19 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20 if r.Header.Get("Cookie") != "" {
21 t.Error("authentication sent ambient credentials")
22 }
23 var in map[string]string
24 json.NewDecoder(r.Body).Decode(&in)
25 if r.URL.Path != "/native/request" || !authToken(in["challenge"]) || in["target"] != "/m/abc" {
26 t.Error("invalid authentication request")
27 }
28 challengeReceived <- in["challenge"]
29 json.NewEncoder(w).Encode(map[string]string{"id": id})
30 }))
31 defer server.Close()
32 auth, err := BeginAuthentication(t.Context(), server.URL+"/m/abc/join")
33 if err != nil {
34 t.Fatal(err)
35 }
36 challenge := <-challengeReceived
37 sum := sha256.Sum256([]byte(auth.verifier))
38 if challenge != base64.RawURLEncoding.EncodeToString(sum[:]) {
39 t.Fatal("challenge does not bind proof")
40 }
41 if strings.Contains(auth.BrowserURL(), auth.verifier) || strings.Contains(auth.BrowserURL(), challenge) {
42 t.Fatal("proof leaked into browser URL")
43 }
44 if auth.ConfirmationCode() == "" {
45 t.Fatal("missing comparison code")
46 }
47 // Cancellation must interrupt the wait promptly, including while idle between polls.
48 auth.Cancel()
49 done := make(chan error, 1)
50 go func() { _, e := auth.Wait(); done <- e }()
51 select {
52 case e := <-done:
53 if e == nil {
54 t.Fatal("cancel succeeded")
55 }
56 case <-time.After(time.Second):
57 t.Fatal("cancel did not stop polling")
58 }
59}
60func TestAuthenticationNeverFollowsRedirect(t *testing.T) {
61 var calls atomic.Int32
62 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1); http.Redirect(w, r, "/signin", 303) }))
63 defer server.Close()
64 if _, err := BeginAuthentication(t.Context(), server.URL+"/m/abc"); err == nil {
65 t.Fatal("old server accepted")
66 }
67 if calls.Load() != 1 {
68 t.Fatal("followed authentication redirect")
69 }
70}
71func TestAuthenticationRejectsUnsafeAddress(t *testing.T) {
72 for _, address := range []string{"http://example.test/m/abc", "https://alice:secret@example.test/m/abc", "https://example.test/m/abc?token=secret", "https://example.test/j/a#secret", "https://example.test/signin", "https://example.test/j/%2f"} {
73 if _, err := BeginAuthentication(context.Background(), address); err == nil {
74 t.Errorf("accepted %q", address)
75 }
76 }
77}
78
79func TestAuthenticationWithoutAPathTargetsTheMeetingList(t *testing.T) {
80 // The person who has no link, because the meeting they are about to be
81 // in does not exist yet. The target is the list, and the result carries
82 // no room: the host reads that as "signed in, nowhere yet".
83 var target atomic.Value
84 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
85 var in map[string]string
86 json.NewDecoder(r.Body).Decode(&in)
87 switch r.URL.Path {
88 case "/native/request":
89 target.Store(in["target"])
90 json.NewEncoder(w).Encode(map[string]string{"id": strings.Repeat("A", 43)})
91 case "/native/exchange":
92 json.NewEncoder(w).Encode(map[string]string{
93 // 43 base64url zeros: the only 43-character run that both
94 // decodes to thirty-two bytes and re-encodes to itself,
95 // which is what authToken insists on.
96 "name": "rastrillo_session", "value": strings.Repeat("A", 43), "target": "/",
97 })
98 }
99 }))
100 defer server.Close()
101 for _, address := range []string{server.URL, server.URL + "/"} {
102 a, err := BeginAuthentication(t.Context(), address)
103 if err != nil {
104 t.Fatal(address, err)
105 }
106 if target.Load() != "/" {
107 t.Fatal("wrong target", target.Load())
108 }
109 result, err := a.Wait()
110 if err != nil {
111 t.Fatal(err)
112 }
113 if result.Room != "" {
114 t.Fatal("a list sign-in named a meeting", result.Room)
115 }
116 if len(result.Cookies) != 1 {
117 t.Fatal("no session")
118 }
119 }
120}
121
122func TestAuthenticationStillRejectsAPathThatIsNotAMeeting(t *testing.T) {
123 // Widening the empty path to mean "the instance" must not widen
124 // anything else: a path that is present and wrong is still an error,
125 // not a quiet fallback to the list.
126 for _, address := range []string{"https://example.test/settings", "https://example.test/m", "https://example.test/m/a/b", "https://example.test/x/abc"} {
127 if _, err := BeginAuthentication(t.Context(), address); err == nil {
128 t.Errorf("accepted %q", address)
129 }
130 }
131}
132