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>
61 lines
2.2 KiB
Go
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
|
|
}
|