package clientapi_test import ( "crypto/hmac" "crypto/sha1" //nolint:gosec // The TURN REST API is HMAC-SHA1. "encoding/base64" "net/http" "strconv" "strings" "testing" "time" "github.com/dariuszjaniak/warp/server/internal/clientapi" "github.com/dariuszjaniak/warp/server/internal/config" ) const turnPath = "/_matrix/client/v3/voip/turnServer" type turnResponse struct { URIs []string `json:"uris"` Username string `json:"username"` Password string `json:"password"` TTL int64 `json:"ttl"` } func TestTurnServerNeedsAToken(t *testing.T) { ts := newTestServer(t, nil) expectError(t, ts.get(turnPath, ""), http.StatusUnauthorized, "M_MISSING_TOKEN") } func TestTurnServerUnconfiguredIsEmpty(t *testing.T) { ts := newTestServer(t, nil) acc := ts.register("alice", "correct horse battery") rec := ts.get(turnPath, acc.AccessToken) expectStatus(t, rec, http.StatusOK) if body := jsonBody(t, rec); len(body) != 0 { t.Errorf("body = %v, want an empty object", body) } } func TestTurnServerSharedSecret(t *testing.T) { const secret = "s3cret" ts := newTestServer(t, func(o *clientapi.Options) { o.TURN = config.TURN{ URIs: []string{"turn:turn.warp.example:3478?transport=udp"}, SharedSecret: secret, UserLifetime: time.Hour, } }) acc := ts.register("alice", "correct horse battery") before := time.Now().Unix() rec := ts.get(turnPath, acc.AccessToken) expectStatus(t, rec, http.StatusOK) got := decode[turnResponse](t, rec) if len(got.URIs) != 1 || got.TTL != 3600 { t.Fatalf("response = %+v", got) } expiry, user, ok := strings.Cut(got.Username, ":") if !ok || user != acc.UserID { t.Fatalf("username = %q, want :%s", got.Username, acc.UserID) } if n, err := strconv.ParseInt(expiry, 10, 64); err != nil || n < before+3600 || n > before+3600+60 { t.Errorf("expiry = %q, want about an hour from now", expiry) } mac := hmac.New(sha1.New, []byte(secret)) mac.Write([]byte(got.Username)) if want := base64.StdEncoding.EncodeToString(mac.Sum(nil)); got.Password != want { t.Errorf("password = %q, want %q", got.Password, want) } } func TestTurnServerStatic(t *testing.T) { ts := newTestServer(t, func(o *clientapi.Options) { o.TURN = config.TURN{ URIs: []string{"turns:turn.warp.example:5349", "stun:turn.warp.example:3478"}, Username: "warp", Password: "hunter2", } }) acc := ts.register("alice", "correct horse battery") got := decode[turnResponse](t, ts.get(turnPath, acc.AccessToken)) if len(got.URIs) != 2 || got.Username != "warp" || got.Password != "hunter2" || got.TTL <= 0 { t.Errorf("response = %+v", got) } } func TestRTCTransports(t *testing.T) { const path = "/_matrix/client/unstable/org.matrix.msc4143/rtc/transports" ts := newTestServer(t, func(o *clientapi.Options) { o.RTC = config.RTC{LiveKitServiceURL: "https://livekit-jwt.warp.example/"} }) acc := ts.register("alice", "correct horse battery") expectError(t, ts.get(path, ""), http.StatusUnauthorized, "M_MISSING_TOKEN") rec := ts.get(path, acc.AccessToken) expectStatus(t, rec, http.StatusOK) got := decode[struct { Transports []map[string]string `json:"rtc_transports"` }](t, rec) if len(got.Transports) != 1 || got.Transports[0]["type"] != "livekit" || got.Transports[0]["livekit_service_url"] != "https://livekit-jwt.warp.example" { t.Errorf("rtc_transports = %v", got.Transports) } // Without a backend there is no endpoint, which is how a client knows. ts = newTestServer(t, nil) acc = ts.register("alice", "correct horse battery") expectError(t, ts.get(path, acc.AccessToken), http.StatusNotFound, "M_UNRECOGNIZED") }