All checks were successful
Build & push Docker image / build (push) Successful in 1m27s
167 lines
5.7 KiB
Go
167 lines
5.7 KiB
Go
package server_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"misaki/db"
|
|
"misaki/server"
|
|
)
|
|
|
|
func newSecretServer(t *testing.T, secret string) (http.Handler, *db.DB) {
|
|
t.Helper()
|
|
d := newTestDB(t)
|
|
srv := server.New(d, server.ServiceURLs{}, nil, server.SMTPConfig{},
|
|
server.Config{InternalSecret: secret})
|
|
return srv, d
|
|
}
|
|
|
|
func streamAuth(srv http.Handler, secret, user, pass, mount string) *httptest.ResponseRecorder {
|
|
body, _ := json.Marshal(map[string]string{
|
|
"username": user, "password": pass, "mount": mount,
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/internal/stream-auth", bytes.NewReader(body))
|
|
if secret != "" {
|
|
req.Header.Set("Authorization", "Bearer "+secret)
|
|
}
|
|
rr := httptest.NewRecorder()
|
|
srv.ServeHTTP(rr, req)
|
|
return rr
|
|
}
|
|
|
|
func TestInternalStreamAuth(t *testing.T) {
|
|
const secret = "test-secret"
|
|
srv, d := newSecretServer(t, secret)
|
|
|
|
id := makeApprovedUser(t, d, "dj_fm", "hunter2hunter2")
|
|
if err := d.SetUserServiceEnabled(id, "radio", true); err != nil {
|
|
t.Fatalf("grant streaming: %v", err)
|
|
}
|
|
if err := d.SetUserServiceData(id, "radio", "fmradio,live"); err != nil {
|
|
t.Fatalf("set stations: %v", err)
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
secret string
|
|
user, pass, mt string
|
|
want int
|
|
}{
|
|
{"allowed mount", secret, "dj_fm", "hunter2hunter2", "fmradio", http.StatusOK},
|
|
{"other allowed", secret, "dj_fm", "hunter2hunter2", "live", http.StatusOK},
|
|
{"disallowed mount", secret, "dj_fm", "hunter2hunter2", "angelcast", http.StatusForbidden},
|
|
{"wrong password", secret, "dj_fm", "nope", "fmradio", http.StatusUnauthorized},
|
|
{"unknown user", secret, "ghost", "whatever12345", "fmradio", http.StatusUnauthorized},
|
|
{"bad bearer", "wrong", "dj_fm", "hunter2hunter2", "fmradio", http.StatusUnauthorized},
|
|
{"no bearer", "", "dj_fm", "hunter2hunter2", "fmradio", http.StatusUnauthorized},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rr := streamAuth(srv, tc.secret, tc.user, tc.pass, tc.mt)
|
|
if rr.Code != tc.want {
|
|
t.Errorf("status: got %d, want %d", rr.Code, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInternalStreamAuth_NotGranted(t *testing.T) {
|
|
const secret = "test-secret"
|
|
srv, d := newSecretServer(t, secret)
|
|
makeApprovedUser(t, d, "rando", "hunter2hunter2") // approved but no streaming grant
|
|
|
|
rr := streamAuth(srv, secret, "rando", "hunter2hunter2", "fmradio")
|
|
if rr.Code != http.StatusForbidden {
|
|
t.Errorf("status: got %d, want %d", rr.Code, http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
func TestInternalStreamAuth_AdminAnyMount(t *testing.T) {
|
|
const secret = "test-secret"
|
|
srv, d := newSecretServer(t, secret)
|
|
makeAdminUser(t, d, "boss", "hunter2hunter2") // admin, no streaming grant needed
|
|
|
|
rr := streamAuth(srv, secret, "boss", "hunter2hunter2", "angelcast")
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("status: got %d, want %d", rr.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func TestInternalStreamAuth_BruteForceLockout(t *testing.T) {
|
|
const secret = "test-secret"
|
|
srv, d := newSecretServer(t, secret)
|
|
id := makeApprovedUser(t, d, "dj_fm", "hunter2hunter2")
|
|
if err := d.SetUserServiceEnabled(id, "radio", true); err != nil {
|
|
t.Fatalf("grant streaming: %v", err)
|
|
}
|
|
if err := d.SetUserServiceData(id, "radio", "fmradio"); err != nil {
|
|
t.Fatalf("set stations: %v", err)
|
|
}
|
|
|
|
// 10 wrong-password attempts exhaust the per-username budget.
|
|
for i := 0; i < 10; i++ {
|
|
if rr := streamAuth(srv, secret, "dj_fm", "wrong", "fmradio"); rr.Code != http.StatusUnauthorized {
|
|
t.Fatalf("attempt %d: got %d, want 401", i, rr.Code)
|
|
}
|
|
}
|
|
// Further attempts are locked out with 429 — even with the CORRECT password.
|
|
if rr := streamAuth(srv, secret, "dj_fm", "hunter2hunter2", "fmradio"); rr.Code != http.StatusTooManyRequests {
|
|
t.Errorf("after lockout: got %d, want 429", rr.Code)
|
|
}
|
|
|
|
// A different username is unaffected (lockout is per-account).
|
|
id2 := makeApprovedUser(t, d, "dj_angel", "hunter2hunter2")
|
|
if err := d.SetUserServiceEnabled(id2, "radio", true); err != nil {
|
|
t.Fatalf("grant streaming: %v", err)
|
|
}
|
|
if err := d.SetUserServiceData(id2, "radio", "angelcast"); err != nil {
|
|
t.Fatalf("set stations: %v", err)
|
|
}
|
|
if rr := streamAuth(srv, secret, "dj_angel", "hunter2hunter2", "angelcast"); rr.Code != http.StatusOK {
|
|
t.Errorf("other user: got %d, want 200", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestInternalStreamAuth_SuccessNeverLocks(t *testing.T) {
|
|
const secret = "test-secret"
|
|
srv, d := newSecretServer(t, secret)
|
|
id := makeApprovedUser(t, d, "dj_fm", "hunter2hunter2")
|
|
if err := d.SetUserServiceEnabled(id, "radio", true); err != nil {
|
|
t.Fatalf("grant streaming: %v", err)
|
|
}
|
|
if err := d.SetUserServiceData(id, "radio", "fmradio"); err != nil {
|
|
t.Fatalf("set stations: %v", err)
|
|
}
|
|
// Many successful (re)connections must never trip the limiter.
|
|
for i := 0; i < 25; i++ {
|
|
if rr := streamAuth(srv, secret, "dj_fm", "hunter2hunter2", "fmradio"); rr.Code != http.StatusOK {
|
|
t.Fatalf("success %d: got %d, want 200", i, rr.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInternalStreamAuth_WildcardAnyMount(t *testing.T) {
|
|
const secret = "test-secret"
|
|
srv, d := newSecretServer(t, secret)
|
|
id := makeApprovedUser(t, d, "star", "hunter2hunter2") // not a misaki admin
|
|
if err := d.SetUserServiceEnabled(id, "radio", true); err != nil {
|
|
t.Fatalf("grant streaming: %v", err)
|
|
}
|
|
if err := d.SetUserServiceData(id, "radio", "*"); err != nil {
|
|
t.Fatalf("set stations: %v", err)
|
|
}
|
|
|
|
// "*" lets a non-admin push any mount...
|
|
rr := streamAuth(srv, secret, "star", "hunter2hunter2", "fmradio")
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("wildcard push: got %d, want %d", rr.Code, http.StatusOK)
|
|
}
|
|
// ...but the bearer is still required.
|
|
rr = streamAuth(srv, "wrong", "star", "hunter2hunter2", "fmradio")
|
|
if rr.Code != http.StatusUnauthorized {
|
|
t.Errorf("bad bearer: got %d, want %d", rr.Code, http.StatusUnauthorized)
|
|
}
|
|
}
|