Closes FUNCTIONAL_AUDIT.md §4 #1: WebRTC 1:1 calls had working signaling but no NAT traversal, so calls between two peers behind symmetric NAT (corporate firewalls, mobile carrier CGNAT, Incus container default networking) failed silently after the SDP exchange. Backend: - GET /api/v1/config/webrtc (public) returns {iceServers: [...]} built from WEBRTC_STUN_URLS / WEBRTC_TURN_URLS / *_USERNAME / *_CREDENTIAL env vars. Half-config (URLs without creds, or vice versa) deliberately omits the TURN block — a half-configured TURN surfaces auth errors at call time instead of falling back cleanly to STUN-only. - 4 handler tests cover the matrix. Frontend: - services/api/webrtcConfig.ts caches the config for the page lifetime and falls back to the historical hardcoded Google STUN if the fetch fails. - useWebRTC fetches at mount, hands iceServers synchronously to every RTCPeerConnection, exposes a {hasTurn, loaded} hint. - CallButton tooltip warns up-front when TURN isn't configured instead of letting calls time out silently. Ops: - infra/coturn/turnserver.conf — annotated template with the SSRF- safe denied-peer-ip ranges, prometheus exporter, TLS for TURNS, static lt-cred-mech (REST-secret rotation deferred to v1.1). - infra/coturn/README.md — Incus deploy walkthrough, smoke test via turnutils_uclient, capacity rules of thumb. - docs/ENV_VARIABLES.md gains a 13bis. WebRTC ICE servers section. Coturn deployment itself is a separate ops action — this commit lands the plumbing so the deploy can light up the path with zero code changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
75 lines
2.9 KiB
Go
75 lines
2.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"veza-backend-api/internal/config"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// IceServer mirrors the WebRTC `RTCIceServer` dictionary. Username and
|
|
// credential are omitted (not zero-valued) when empty so STUN-only
|
|
// entries don't carry meaningless auth fields, and so frontends that
|
|
// validate the response don't have to special-case empty strings.
|
|
//
|
|
// Wire shape is intentionally lowercase to match the JS API the
|
|
// frontend feeds directly into `new RTCPeerConnection({ iceServers })`.
|
|
type IceServer struct {
|
|
URLs []string `json:"urls"`
|
|
Username string `json:"username,omitempty"`
|
|
Credential string `json:"credential,omitempty"`
|
|
}
|
|
|
|
// WebRTCConfigResponse is the body of GET /api/v1/config/webrtc. The
|
|
// frontend caches it for the lifetime of the page and uses it to build
|
|
// every RTCPeerConnection (1:1 calls today, screen-share / multi-party
|
|
// later). Public endpoint by design — TURN credentials returned here
|
|
// are short-lived rotation candidates, never long-lived secrets.
|
|
type WebRTCConfigResponse struct {
|
|
IceServers []IceServer `json:"iceServers"`
|
|
}
|
|
|
|
// GetWebRTCConfig returns the ICE-server set the frontend should hand to
|
|
// `new RTCPeerConnection`. v1.0.9 item 1.2 — closes the
|
|
// FUNCTIONAL_AUDIT.md §4 #1 NAT-traversal gap. Before this endpoint the
|
|
// frontend hardcoded `stun:stun.l.google.com:19302`, which works on
|
|
// flat networks but fails behind symmetric NAT (corporate, mobile
|
|
// carrier, Incus container default networking). Returning the operator-
|
|
// configured TURN block lets the browser fall back to a relay when
|
|
// hole-punching fails, which is the only thing that makes WebRTC reach
|
|
// "actually works for end users" status.
|
|
//
|
|
// @Summary WebRTC ICE configuration
|
|
// @Description Public — returns the ICE-server set the SPA feeds to RTCPeerConnection. STUN-only when no TURN is configured. TURN credentials are always emitted as static (REST shared-secret rotation deferred to v1.1).
|
|
// @Tags Config
|
|
// @Produce json
|
|
// @Success 200 {object} handlers.WebRTCConfigResponse "ICE servers"
|
|
// @Router /config/webrtc [get]
|
|
func GetWebRTCConfig(cfg *config.Config) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
resp := WebRTCConfigResponse{
|
|
IceServers: []IceServer{},
|
|
}
|
|
|
|
if cfg != nil && len(cfg.WebRTCStunURLs) > 0 {
|
|
resp.IceServers = append(resp.IceServers, IceServer{URLs: cfg.WebRTCStunURLs})
|
|
}
|
|
|
|
// TURN block is only emitted when fully configured. Half-configured
|
|
// is worse than missing — the browser would surface auth failures
|
|
// instead of falling back cleanly to STUN-only.
|
|
if cfg != nil &&
|
|
len(cfg.WebRTCTurnURLs) > 0 &&
|
|
cfg.WebRTCTurnUsername != "" &&
|
|
cfg.WebRTCTurnCredential != "" {
|
|
resp.IceServers = append(resp.IceServers, IceServer{
|
|
URLs: cfg.WebRTCTurnURLs,
|
|
Username: cfg.WebRTCTurnUsername,
|
|
Credential: cfg.WebRTCTurnCredential,
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
}
|