CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
Every call in the web client now goes through MatrixRTC with a LiveKit
SFU, the model Element Call and Element X use; the legacy 1:1 m.call.*
calls are gone. In a room of two the other person is rung and can answer
or decline; in a bigger room a banner offers to join the call. Calls show
a grid of participant tiles with mute, camera, screen sharing and leave,
and media is end-to-end encrypted in encrypted rooms.
Server:
- MSC4140 delayed events (send/state with a delay, plus the
delayed_events list/restart/cancel/send API), stored and scheduled, so
a member whose client drops off is taken out of the call.
- POST /user/{userId}/openid/request_token, which lk-jwt-service uses to
hand out LiveKit tokens.
- org.matrix.msc4143.rtc_foci in /.well-known/matrix/client, configured
under client_api.rtc.
- Room presets let members send call membership state.
- Migration 0014 for both SQL dialects.
Deploy: LiveKit and lk-jwt-service in compose (calls.yml) and the helm
chart. Docs: docs/calls.md, configuration and deployment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
3.6 KiB
Go
114 lines
3.6 KiB
Go
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 <expiry>:%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")
|
|
}
|