package server_test import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "fmt" "net/http" "sort" "strconv" "strings" "testing" "time" "misaki/db" "misaki/server" ) func sessionGroups(t *testing.T, srv http.Handler, d *db.DB, user, pass string) (int, []string) { t.Helper() cookie := doLogin(t, srv, user, pass) rr := doGet(srv, "/internal/session-check", cookie) raw := rr.Header().Get("X-Auth-Groups") if raw == "" { return rr.Code, nil } g := strings.Split(raw, ",") sort.Strings(g) return rr.Code, g } func eqGroups(a, b []string) bool { if len(a) != len(b) { return false } sort.Strings(b) for i := range a { if a[i] != b[i] { return false } } return true } func TestSessionCheck_FilesGroups(t *testing.T) { srv, d := newTestServer(t) makeApprovedUser(t, d, "plain", "hunter2hunter2") if code, g := sessionGroups(t, srv, d, "plain", "hunter2hunter2"); code != http.StatusOK || !eqGroups(g, []string{"users"}) { t.Errorf("plain user: code=%d groups=%v, want 200 [users]", code, g) } id := makeApprovedUser(t, d, "dj", "hunter2hunter2") if err := d.SetUserServiceEnabled(id, "files", true); err != nil { t.Fatalf("grant files: %v", err) } if err := d.SetUserServiceData(id, "files", "share,fmradio"); err != nil { t.Fatalf("set mounts: %v", err) } if code, g := sessionGroups(t, srv, d, "dj", "hunter2hunter2"); code != http.StatusOK || !eqGroups(g, []string{"users", "share", "fmradio"}) { t.Errorf("files user: code=%d groups=%v, want [users share fmradio]", code, g) } id2 := makeApprovedUser(t, d, "star", "hunter2hunter2") if err := d.SetUserServiceEnabled(id2, "files", true); err != nil { t.Fatalf("grant files: %v", err) } if err := d.SetUserServiceData(id2, "files", "*"); err != nil { t.Fatalf("set mounts: %v", err) } if code, g := sessionGroups(t, srv, d, "star", "hunter2hunter2"); code != http.StatusOK || !eqGroups(g, []string{"users", "admin"}) { t.Errorf("wildcard files user: code=%d groups=%v, want [users admin]", code, g) } makeAdminUser(t, d, "boss", "hunter2hunter2") if code, g := sessionGroups(t, srv, d, "boss", "hunter2hunter2"); code != http.StatusOK || !eqGroups(g, []string{"users", "admin"}) { t.Errorf("admin user: code=%d groups=%v, want [users admin]", code, g) } } func TestSessionCheck_SignedIdentity(t *testing.T) { const secret = "test-auth-secret" d := newTestDB(t) srv := server.New(d, server.ServiceURLs{}, nil, server.SMTPConfig{}, server.Config{AuthSecret: secret}) makeApprovedUser(t, d, "alice", "hunter2hunter2") cookie := doLogin(t, srv, "alice", "hunter2hunter2") rr := doGet(srv, "/internal/session-check", cookie) if rr.Code != http.StatusOK { t.Fatalf("session-check: got %d, want 200", rr.Code) } sig := rr.Header().Get("X-Auth-Sig") expRaw := rr.Header().Get("X-Auth-Exp") if sig == "" || expRaw == "" { t.Fatalf("missing signature headers: sig=%q exp=%q", sig, expRaw) } exp, err := strconv.ParseInt(expRaw, 10, 64) if err != nil { t.Fatalf("X-Auth-Exp not an int: %v", err) } if exp <= time.Now().Unix() { t.Errorf("X-Auth-Exp %d is not in the future", exp) } mac := hmac.New(sha256.New, []byte(secret)) fmt.Fprintf(mac, "v1:%s:%d", "alice", exp) want := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) if sig != want { t.Errorf("signature mismatch: got %q, want %q", sig, want) } mac2 := hmac.New(sha256.New, []byte(secret)) fmt.Fprintf(mac2, "v1:%s:%d", "admin", exp) if forged := base64.RawURLEncoding.EncodeToString(mac2.Sum(nil)); forged == sig { t.Error("signature collides for a different username") } } func TestSessionCheck_NoSecretNoSignature(t *testing.T) { srv, d := newTestServer(t) makeApprovedUser(t, d, "bob", "hunter2hunter2") cookie := doLogin(t, srv, "bob", "hunter2hunter2") rr := doGet(srv, "/internal/session-check", cookie) if rr.Code != http.StatusOK { t.Fatalf("session-check: got %d, want 200", rr.Code) } if sig := rr.Header().Get("X-Auth-Sig"); sig != "" { t.Errorf("unexpected signature when no secret configured: %q", sig) } } func TestServicesAreIndependent(t *testing.T) { const secret = "test-secret" srv2, d := newSecretServer(t, secret) id := makeApprovedUser(t, d, "onlyradio", "hunter2hunter2") if err := d.SetUserServiceEnabled(id, "radio", true); err != nil { t.Fatalf("grant radio: %v", err) } if err := d.SetUserServiceData(id, "radio", "fmradio"); err != nil { t.Fatalf("set stations: %v", err) } if rr := streamAuth(srv2, secret, "onlyradio", "hunter2hunter2", "fmradio"); rr.Code != http.StatusOK { t.Errorf("radio push: got %d, want 200", rr.Code) } if code, g := sessionGroups(t, srv2, d, "onlyradio", "hunter2hunter2"); code != http.StatusOK || !eqGroups(g, []string{"users"}) { t.Errorf("radio user copyparty groups: code=%d groups=%v, want [users]", code, g) } id2 := makeApprovedUser(t, d, "onlyfiles", "hunter2hunter2") if err := d.SetUserServiceEnabled(id2, "files", true); err != nil { t.Fatalf("grant files: %v", err) } if err := d.SetUserServiceData(id2, "files", "fmradio"); err != nil { t.Fatalf("set mounts: %v", err) } if rr := streamAuth(srv2, secret, "onlyfiles", "hunter2hunter2", "fmradio"); rr.Code != http.StatusForbidden { t.Errorf("files user harbor push: got %d, want 403", rr.Code) } }