| 1 | package slides |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "encoding/base64" |
| 6 | "encoding/json" |
| 7 | "fmt" |
| 8 | "html" |
| 9 | "net/http" |
| 10 | "net/http/httptest" |
| 11 | "net/url" |
| 12 | "os" |
| 13 | "regexp" |
| 14 | "strings" |
| 15 | "testing" |
| 16 | "time" |
| 17 | |
| 18 | "amadan.net/oficina/home/connect" |
| 19 | "amadan.net/oficina/slides/internal/slides/keymailtest" |
| 20 | assertioncrypto "amadan.net/rastrillo/rastrillo/crypto" |
| 21 | ) |
| 22 | |
| 23 | // TestMain keeps Keymail classification inside the process for this |
| 24 | // package's tests: every DNS query is answered NXDOMAIN and only the |
| 25 | // Keymail fixtures a test registers answer HTTPS (keymailtest). |
| 26 | func TestMain(m *testing.M) { |
| 27 | restore, err := keymailtest.Install() |
| 28 | if err != nil { |
| 29 | fmt.Fprintln(os.Stderr, "keymailtest.Install:", err) |
| 30 | os.Exit(1) |
| 31 | } |
| 32 | code := m.Run() |
| 33 | // No test may leave a sign-in mail worker running: every app and |
| 34 | // every queue a test builds is closed in its cleanup. |
| 35 | if code == 0 { |
| 36 | deadline := time.Now().Add(5 * time.Second) |
| 37 | for mailWorkers() > 0 && time.Now().Before(deadline) { |
| 38 | time.Sleep(10 * time.Millisecond) |
| 39 | } |
| 40 | if n := mailWorkers(); n > 0 { |
| 41 | fmt.Fprintf(os.Stderr, "FAIL: %d sign-in mail queue goroutines still running after the tests\n", n) |
| 42 | code = 1 |
| 43 | } |
| 44 | } |
| 45 | restore() |
| 46 | os.Exit(code) |
| 47 | } |
| 48 | |
| 49 | // TestHomeLocalKeymailIsAContinuationAndRefusesAtTheDoor is the |
| 50 | // Home-mode half of the Keymail sign-in: with Home configured, the |
| 51 | // local form (/signin/local, posting to /signin under LocalGuard) sends |
| 52 | // a permitted and a non-permitted Keymail address to the same |
| 53 | // continuation page — never a 303 out of the form POST, whose |
| 54 | // form-action is 'self' plus the Home issuer — and a non-permitted one |
| 55 | // that Keymail verified is refused on the local door, not in bare text. |
| 56 | func TestHomeLocalKeymailIsAContinuationAndRefusesAtTheDoor(t *testing.T) { |
| 57 | const host = "keymail.home.test" |
| 58 | member, stranger := "owner@"+host, "stranger@"+host |
| 59 | km := keymailtest.Serve(t, host, member, stranger) |
| 60 | |
| 61 | key, err := assertioncrypto.Generate() |
| 62 | if err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | raw, _ := json.Marshal(map[string]string{"test": base64.StdEncoding.EncodeToString(key.SignPub())}) |
| 66 | mail := &homeTestMail{} |
| 67 | mux, a, err := New(testDB(t), Config{Mailer: mail, Env: EnvDevelopment, Origin: homeTestOrigin, Logger: quietLogger(), |
| 68 | AllowedAddresses: []string{member}, |
| 69 | Home: &connect.Credentials{Issuer: "https://home.example.com", ConsumerID: "slides-test", |
| 70 | KeysBase64: base64.StdEncoding.EncodeToString(raw), DEKBase64: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{8}, 32))}}) |
| 71 | if err != nil { |
| 72 | t.Fatal(err) |
| 73 | } |
| 74 | t.Cleanup(func() { a.Close() }) |
| 75 | |
| 76 | do := func(method, path string, form url.Values, cookies []*http.Cookie) *httptest.ResponseRecorder { |
| 77 | r := httptest.NewRequest(method, homeTestOrigin+path, strings.NewReader(form.Encode())) |
| 78 | if method == http.MethodPost { |
| 79 | r.Header.Set("Origin", homeTestOrigin) |
| 80 | r.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
| 81 | } |
| 82 | for _, c := range cookies { |
| 83 | r.AddCookie(c) |
| 84 | } |
| 85 | w := httptest.NewRecorder() |
| 86 | mux.ServeHTTP(w, r) |
| 87 | return w |
| 88 | } |
| 89 | href := regexp.MustCompile(`href="(https://` + regexp.QuoteMeta(host) + `/oauth/authorize\?[^"]+)"`) |
| 90 | begin := func(address string) (*httptest.ResponseRecorder, *url.URL) { |
| 91 | rec := do(http.MethodPost, "/signin", url.Values{"address": {address}}, nil) |
| 92 | if rec.Code != http.StatusOK || rec.Header().Get("Location") != "" { |
| 93 | t.Fatalf("POST /signin %q in Home mode: %d %q, want 200 and the continuation page", address, rec.Code, rec.Header().Get("Location")) |
| 94 | } |
| 95 | m := href.FindStringSubmatch(rec.Body.String()) |
| 96 | if m == nil || !strings.Contains(rec.Body.String(), `http-equiv="refresh"`) { |
| 97 | t.Fatalf("POST /signin %q in Home mode: no Keymail continuation:\n%s", address, rec.Body.String()) |
| 98 | } |
| 99 | u, err := url.Parse(html.UnescapeString(m[1])) |
| 100 | if err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | return rec, u |
| 104 | } |
| 105 | |
| 106 | _, _ = begin(member) |
| 107 | rec, u := begin(stranger) |
| 108 | a.FlushMail() |
| 109 | if mail.body != "" { |
| 110 | t.Fatal("a Keymail address was mailed a link") |
| 111 | } |
| 112 | |
| 113 | cb := do(http.MethodGet, "/auth/callback?"+url.Values{"state": {u.Query().Get("state")}, "code": {km.Approve(stranger)}}.Encode(), nil, rec.Result().Cookies()) |
| 114 | // This mux has no locale middleware, so the catalog key itself is |
| 115 | // what proves the refusal sentence was chosen. |
| 116 | if body := cb.Body.String(); cb.Code != http.StatusForbidden || !strings.Contains(body, "slides.signin.error.refused") || !strings.Contains(body, `name="address"`) { |
| 117 | t.Fatalf("Home-mode callback for a non-permitted address: %d %q, want 403 and the local door with the refusal\n%s", cb.Code, cb.Header().Get("Location"), body) |
| 118 | } |
| 119 | if spoof := do(http.MethodGet, "/signin/local?err=refused", nil, nil).Body.String(); strings.Contains(spoof, "slides.signin.error.refused") { |
| 120 | t.Fatal("anybody can show the refusal by visiting /signin/local?err=refused") |
| 121 | } |
| 122 | } |
| 123 | |