Files
Warp/server/internal/clientapi/voip.go
T
tajniak81andClaude Opus 5.5 325431a5be Voice and video calls: TURN endpoint and web client calls
Server: GET /voip/turnServer, configured under client_api.turn. A shared
secret (coturn use-auth-secret) gives each user expiring TURN REST API
credentials; static credentials are the fallback; with nothing configured
it answers {} so clients still try a direct connection.

Web client: 1:1 calls (Matrix VoIP via matrix-js-sdk) in rooms with one
other person. Voice and video buttons in the room header, a ringing card
with answer, answer without video and decline, and a call panel that
stays up across rooms: mute, camera, screen sharing, expand, hang up,
ring tones. Call events show in the timeline and room list. Works around
the SDK keeping a failed microphone request forever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 18:04:02 +02:00

61 lines
2.2 KiB
Go

package clientapi
import (
"crypto/hmac"
"crypto/sha1" //nolint:gosec // The TURN REST API is defined with HMAC-SHA1; coturn checks exactly that.
"encoding/base64"
"net/http"
"strconv"
"github.com/dariuszjaniak/warp/server/internal/config"
"github.com/dariuszjaniak/warp/server/internal/httputil"
"github.com/dariuszjaniak/warp/server/internal/userapi"
)
type turnServerResponse struct {
URIs []string `json:"uris"`
Username string `json:"username"`
Password string `json:"password"`
TTL int64 `json:"ttl"`
}
// staticTURNTTL is the ttl given with static credentials. They never
// change, so it only sets how often a client asks again.
const staticTURNTTL = 86400
// handleTurnServer implements GET /_matrix/client/v3/voip/turnServer.
//
// With no TURN server configured it answers an empty object, as Synapse
// does: clients take that as "no relay" and still try a direct connection,
// where an error would have some of them refuse to place a call at all.
func (c *ClientAPI) handleTurnServer(w http.ResponseWriter, _ *http.Request, auth userapi.Authenticated) error {
t := c.turn
if len(t.URIs) == 0 || (t.SharedSecret == "" && t.Username == "") {
httputil.WriteJSON(w, http.StatusOK, struct{}{})
return nil
}
if t.SharedSecret != "" {
username, password, ttl := turnRESTCredentials(t, auth.UserID, c.now().Unix())
httputil.WriteJSON(w, http.StatusOK, turnServerResponse{
URIs: t.URIs, Username: username, Password: password, TTL: ttl,
})
return nil
}
httputil.WriteJSON(w, http.StatusOK, turnServerResponse{
URIs: t.URIs, Username: t.Username, Password: t.Password, TTL: staticTURNTTL,
})
return nil
}
// turnRESTCredentials makes TURN REST API credentials: the username is the
// expiry time and the user, and the password is the username signed with
// the secret the TURN server shares, so it can check them without asking
// anyone.
func turnRESTCredentials(t config.TURN, userID string, now int64) (username, password string, ttl int64) {
ttl = int64(t.UserLifetime.Seconds())
username = strconv.FormatInt(now+ttl, 10) + ":" + userID
mac := hmac.New(sha1.New, []byte(t.SharedSecret))
mac.Write([]byte(username))
return username, base64.StdEncoding.EncodeToString(mac.Sum(nil)), ttl
}