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>
107 lines
3.7 KiB
Go
107 lines
3.7 KiB
Go
package clientapi
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/httputil"
|
|
"github.com/dariuszjaniak/warp/server/internal/storage"
|
|
"github.com/dariuszjaniak/warp/server/internal/userapi"
|
|
)
|
|
|
|
// openIDTokenLifetime is how long an OpenID token is good for. The LiveKit
|
|
// JWT service checks it once, when a call is joined, so an hour is plenty.
|
|
const openIDTokenLifetime = time.Hour
|
|
|
|
// openIDTokenPrefix marks OpenID tokens the way the userapi marks access
|
|
// tokens, so one handed to the wrong endpoint is recognisable.
|
|
const openIDTokenPrefix = "warp_oi_"
|
|
|
|
// openIDUserInfoPath is where a third party exchanges an OpenID token for
|
|
// the user's ID. It is a federation endpoint by name, but needs neither
|
|
// federation nor a signing key: Synapse serves it as a resource of its own
|
|
// for the same reason. It is registered here so that a server with
|
|
// federation off can still let its users into a group call.
|
|
const openIDUserInfoPath = "/_matrix/federation/v1/openid/userinfo"
|
|
|
|
// registerOpenIDRoutes adds the OpenID endpoints, if there is a store for
|
|
// the tokens.
|
|
func (c *ClientAPI) registerOpenIDRoutes(r *httputil.Router) {
|
|
if c.openID == nil {
|
|
return
|
|
}
|
|
c.authed(r, "POST "+v3+"/user/{userID}/openid/request_token", c.limitDefault, c.handleRequestOpenIDToken)
|
|
c.public(r, "GET "+openIDUserInfoPath, c.limitDefault, c.handleOpenIDUserInfo)
|
|
}
|
|
|
|
type openIDTokenResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
TokenType string `json:"token_type"`
|
|
MatrixServerName string `json:"matrix_server_name"`
|
|
ExpiresIn int64 `json:"expires_in"`
|
|
}
|
|
|
|
// handleRequestOpenIDToken implements
|
|
// POST /_matrix/client/v3/user/{userID}/openid/request_token.
|
|
//
|
|
// The token proves who the user is to whoever they hand it to, and nothing
|
|
// else: it is not an access token and opens no other endpoint.
|
|
func (c *ClientAPI) handleRequestOpenIDToken(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error {
|
|
if r.PathValue("userID") != auth.UserID {
|
|
return httputil.Forbidden("You cannot request an OpenID token for another user")
|
|
}
|
|
// The body is an empty object; anything in it is ignored.
|
|
var ignored map[string]any
|
|
if _, err := httputil.DecodeOptionalJSON(w, r, &ignored); err != nil {
|
|
return err
|
|
}
|
|
|
|
raw := make([]byte, 32)
|
|
if _, err := rand.Read(raw); err != nil {
|
|
return fmt.Errorf("generate OpenID token: %w", err)
|
|
}
|
|
token := openIDTokenPrefix + base64.RawURLEncoding.EncodeToString(raw)
|
|
now := c.now()
|
|
// Expired tokens go as new ones are made; nothing else reads them.
|
|
if err := c.openID.PurgeOpenIDTokens(r.Context(), now.UnixMilli()); err != nil {
|
|
c.log.Warn("could not purge expired OpenID tokens", "error", err)
|
|
}
|
|
if err := c.openID.InsertOpenIDToken(r.Context(), userapi.HashToken(token), auth.UserID,
|
|
now.Add(openIDTokenLifetime).UnixMilli()); err != nil {
|
|
return err
|
|
}
|
|
httputil.WriteJSON(w, http.StatusOK, openIDTokenResponse{
|
|
AccessToken: token,
|
|
TokenType: "Bearer",
|
|
MatrixServerName: c.serverName,
|
|
ExpiresIn: int64(openIDTokenLifetime.Seconds()),
|
|
})
|
|
return nil
|
|
}
|
|
|
|
type openIDUserInfoResponse struct {
|
|
Sub string `json:"sub"`
|
|
}
|
|
|
|
// handleOpenIDUserInfo implements
|
|
// GET /_matrix/federation/v1/openid/userinfo.
|
|
func (c *ClientAPI) handleOpenIDUserInfo(w http.ResponseWriter, r *http.Request) error {
|
|
token := r.URL.Query().Get("access_token")
|
|
if token == "" {
|
|
return httputil.MissingToken()
|
|
}
|
|
userID, err := c.openID.OpenIDTokenUser(r.Context(), userapi.HashToken(token), c.now().UnixMilli())
|
|
if errors.Is(err, storage.ErrNoSuchOpenIDToken) {
|
|
return httputil.UnknownToken(false)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
httputil.WriteJSON(w, http.StatusOK, openIDUserInfoResponse{Sub: userID})
|
|
return nil
|
|
}
|