Group voice and video calls with MatrixRTC and LiveKit
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
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>
This commit is contained in:
1 parent
325431a5be
commit
76dfc98a86
49 files changed
+3849
-432
No files matched your search
@@ -41,6 +41,11 @@ type Options struct {
|
||||
Logger *slog.Logger
|
||||
// Transport overrides the proxy's round tripper, for tests.
|
||||
Transport http.RoundTripper
|
||||
// CallOrigins are origins outside the homeserver the browser may
|
||||
// connect to for group calls: the LiveKit JWT service and the LiveKit
|
||||
// SFU's WebSocket, e.g. https://livekit-jwt.example and
|
||||
// wss://livekit.example. They are added to the CSP's connect-src.
|
||||
CallOrigins []string
|
||||
}
|
||||
|
||||
// proxiedPrefixes are the parts of the homeserver a Matrix client uses: the
|
||||
@@ -48,6 +53,10 @@ type Options struct {
|
||||
// are not reachable through warp-web.
|
||||
var proxiedPrefixes = []string{"/_matrix/client/", "/_matrix/media/"}
|
||||
|
||||
// wellKnownClientPath is proxied too: it is where the client finds the
|
||||
// group call backend (org.matrix.msc4143.rtc_foci).
|
||||
const wellKnownClientPath = "/.well-known/matrix/client"
|
||||
|
||||
// Config is the body of /config.json, which the client reads on start.
|
||||
type Config struct {
|
||||
// HomeserverURL is where the client sends Matrix requests; empty
|
||||
@@ -78,6 +87,22 @@ func New(opts Options) (http.Handler, error) {
|
||||
cfg.HomeserverURL = strings.TrimRight(u.String(), "/")
|
||||
connect += " " + u.Scheme + "://" + u.Host
|
||||
}
|
||||
for _, raw := range opts.CallOrigins {
|
||||
u, err := url.Parse(strings.TrimRight(raw, "/"))
|
||||
if err != nil || u.Host == "" || u.Path != "" || u.RawQuery != "" ||
|
||||
(u.Scheme != "http" && u.Scheme != "https" && u.Scheme != "ws" && u.Scheme != "wss") {
|
||||
return nil, errors.New("host: a call origin must be an http(s) or ws(s) origin, got " + raw)
|
||||
}
|
||||
connect += " " + u.Scheme + "://" + u.Host
|
||||
// LiveKit falls back from its WebSocket to an HTTP request on
|
||||
// the same host to find out why a connection failed.
|
||||
switch u.Scheme {
|
||||
case "ws":
|
||||
connect += " http://" + u.Host
|
||||
case "wss":
|
||||
connect += " https://" + u.Host
|
||||
}
|
||||
}
|
||||
body, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -167,6 +192,10 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("ok\n"))
|
||||
return
|
||||
|
||||
case p == wellKnownClientPath && h.proxy != nil:
|
||||
h.proxy.ServeHTTP(w, r)
|
||||
return
|
||||
|
||||
case strings.HasPrefix(p, "/_matrix/") || strings.HasPrefix(p, "/_warp/") || strings.HasPrefix(p, "/_synapse/"):
|
||||
// A path that cleans to something else (dot segments, doubled
|
||||
// slashes) is refused rather than normalised: what is allowed
|
||||
|
||||
@@ -304,3 +304,35 @@ func TestNewValidatesHomeserver(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxiesWellKnownClient(t *testing.T) {
|
||||
up := newUpstream(t)
|
||||
h := newHandler(t, up, true)
|
||||
rec := do(h, http.MethodGet, "/.well-known/matrix/client", nil)
|
||||
if rec.Code != 200 || len(up.hits) != 1 || up.hits[0].URL.Path != "/.well-known/matrix/client" {
|
||||
t.Fatalf("status %d, hits %d", rec.Code, len(up.hits))
|
||||
}
|
||||
// The server delegation document stays private.
|
||||
if rec := do(h, http.MethodGet, "/.well-known/matrix/server", nil); rec.Code == 200 && len(up.hits) > 1 {
|
||||
t.Errorf("/.well-known/matrix/server was proxied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallOriginsJoinConnectSrc(t *testing.T) {
|
||||
up := newUpstream(t)
|
||||
target, _ := url.Parse(up.srv.URL)
|
||||
h, err := New(Options{Homeserver: target, Client: clientFS,
|
||||
CallOrigins: []string{"https://livekit-jwt.warp.example/", "wss://livekit.warp.example"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := do(h, http.MethodGet, "/", nil)
|
||||
want := "connect-src 'self' https://livekit-jwt.warp.example wss://livekit.warp.example https://livekit.warp.example;"
|
||||
if csp := rec.Header().Get("Content-Security-Policy"); !strings.Contains(csp, want) {
|
||||
t.Errorf("CSP %q", csp)
|
||||
}
|
||||
|
||||
if _, err := New(Options{Homeserver: target, CallOrigins: []string{"https://x.example/path"}}); err == nil {
|
||||
t.Error("a call origin with a path was accepted")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user