Squash integration of the reviewed Go core, frontend audit fixes, release tooling, and deployment hygiene. CI passed all Server/Client jobs.
692 lines
24 KiB
Go
692 lines
24 KiB
Go
package protocol
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// ServerMessage is one member of serverMessageSchema. The interface is sealed:
|
|
// only the message structs in this file implement it.
|
|
type ServerMessage interface {
|
|
isServerMessage()
|
|
}
|
|
|
|
// AuthenticatedHostMessage is the host member of authenticatedMessageSchema.
|
|
// The field order is the emission order of the object literal in
|
|
// src/server/signaling.ts (`role` and `viewerPasswordEnabled` are appended
|
|
// after the shared base spread).
|
|
type AuthenticatedHostMessage struct {
|
|
Type string `json:"type"`
|
|
Protocol string `json:"protocol"`
|
|
PeerID string `json:"peerId"`
|
|
RoomExpiresAt *string `json:"roomExpiresAt"`
|
|
MaxViewers Int `json:"maxViewers"`
|
|
EndpointMediaCopyCapacity Int `json:"endpointMediaCopyCapacity"`
|
|
HostOnline bool `json:"hostOnline"`
|
|
HostPaused *bool `json:"hostPaused,omitempty"`
|
|
ConnectionID *string `json:"connectionId"`
|
|
IceConfig IceConfig `json:"iceConfig"`
|
|
RoutePolicy RoutePolicy `json:"routePolicy"`
|
|
CodeEntryPolicy CodeEntryPolicy `json:"codeEntryPolicy"`
|
|
ViewerAuthorizationGeneration string `json:"viewerAuthorizationGeneration"`
|
|
MediaMode string `json:"mediaMode"`
|
|
ShareGeneration *string `json:"shareGeneration"`
|
|
RouteRevision Int `json:"routeRevision"`
|
|
RouteAssignment ParticipantRouteAssignment `json:"routeAssignment"`
|
|
QualitySettings QualitySettings `json:"qualitySettings"`
|
|
SfuStandbyURL *string `json:"sfuStandbyUrl,omitempty"`
|
|
Role Role `json:"role"`
|
|
ViewerPasswordEnabled bool `json:"viewerPasswordEnabled"`
|
|
}
|
|
|
|
// AuthenticatedViewerMessage is the viewer member of
|
|
// authenticatedMessageSchema.
|
|
type AuthenticatedViewerMessage struct {
|
|
Type string `json:"type"`
|
|
Protocol string `json:"protocol"`
|
|
PeerID string `json:"peerId"`
|
|
RoomExpiresAt *string `json:"roomExpiresAt"`
|
|
MaxViewers Int `json:"maxViewers"`
|
|
EndpointMediaCopyCapacity Int `json:"endpointMediaCopyCapacity"`
|
|
HostOnline bool `json:"hostOnline"`
|
|
HostPaused *bool `json:"hostPaused,omitempty"`
|
|
ConnectionID *string `json:"connectionId"`
|
|
IceConfig IceConfig `json:"iceConfig"`
|
|
RoutePolicy RoutePolicy `json:"routePolicy"`
|
|
CodeEntryPolicy CodeEntryPolicy `json:"codeEntryPolicy"`
|
|
ViewerAuthorizationGeneration string `json:"viewerAuthorizationGeneration"`
|
|
MediaMode string `json:"mediaMode"`
|
|
ShareGeneration *string `json:"shareGeneration"`
|
|
RouteRevision Int `json:"routeRevision"`
|
|
RouteAssignment ParticipantRouteAssignment `json:"routeAssignment"`
|
|
QualitySettings QualitySettings `json:"qualitySettings"`
|
|
SfuStandbyURL *string `json:"sfuStandbyUrl,omitempty"`
|
|
Role Role `json:"role"`
|
|
}
|
|
|
|
// SignalingChallengeResponseMessage is the challenge echo.
|
|
type SignalingChallengeResponseMessage struct {
|
|
Type string `json:"type"`
|
|
Sequence Int `json:"sequence"`
|
|
}
|
|
|
|
// ServerSignalMessage is the forwarded "signal" message.
|
|
type ServerSignalMessage struct {
|
|
Type string `json:"type"`
|
|
FromPeerID string `json:"fromPeerId"`
|
|
Payload SignalPayload `json:"payload"`
|
|
}
|
|
|
|
// ServerRestartRequestMessage is the forwarded "restart-request" message.
|
|
type ServerRestartRequestMessage struct {
|
|
Type string `json:"type"`
|
|
FromPeerID string `json:"fromPeerId"`
|
|
ConnectionID string `json:"connectionId"`
|
|
Rebuild bool `json:"rebuild"`
|
|
}
|
|
|
|
// RouteUpdatePrepareMessage is the phase "prepare" member of route-update.
|
|
type RouteUpdatePrepareMessage struct {
|
|
Type string `json:"type"`
|
|
Revision Int `json:"revision"`
|
|
Phase string `json:"phase"`
|
|
Assignment ParticipantRouteAssignment `json:"assignment"`
|
|
Candidate PreparedRouteCandidate `json:"candidate"`
|
|
}
|
|
|
|
// RouteUpdateActiveMessage is the phase "active" member of route-update.
|
|
type RouteUpdateActiveMessage struct {
|
|
Type string `json:"type"`
|
|
Revision Int `json:"revision"`
|
|
Phase string `json:"phase"`
|
|
Assignment ParticipantRouteAssignment `json:"assignment"`
|
|
}
|
|
|
|
// RouteStatusMessage is the crossed (state, reason) union: "waiting" pairs
|
|
// with "sfu-admission" and "failed" pairs with "route-exhausted".
|
|
type RouteStatusMessage struct {
|
|
Type string `json:"type"`
|
|
Revision Int `json:"revision"`
|
|
State string `json:"state"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// RouteDiagnosticSnapshotMessage carries the privacy-safe route snapshot.
|
|
type RouteDiagnosticSnapshotMessage struct {
|
|
Type string `json:"type"`
|
|
Snapshot RouteDiagnosticSnapshot `json:"snapshot"`
|
|
}
|
|
|
|
// SfuConfigMessage carries a LiveKit URL and a short-lived token.
|
|
type SfuConfigMessage struct {
|
|
Type string `json:"type"`
|
|
Revision Int `json:"revision"`
|
|
URL string `json:"url"`
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
// QualitySettingsMessage broadcasts the host's quality settings.
|
|
type QualitySettingsMessage struct {
|
|
Type string `json:"type"`
|
|
QualitySettings QualitySettings `json:"qualitySettings"`
|
|
}
|
|
|
|
// RoutePolicyMessage broadcasts the host's route policy.
|
|
type RoutePolicyMessage struct {
|
|
Type string `json:"type"`
|
|
ShareGeneration string `json:"shareGeneration"`
|
|
RoutePolicy RoutePolicy `json:"routePolicy"`
|
|
SfuStandbyURL *string `json:"sfuStandbyUrl,omitempty"`
|
|
}
|
|
|
|
// PauseSharingSourceMessage asks the host to pause its capture source.
|
|
type PauseSharingSourceMessage struct {
|
|
Type string `json:"type"`
|
|
ShareGeneration string `json:"shareGeneration"`
|
|
}
|
|
|
|
// ServerViewerQualityEvidenceMessage is the forwarded evidence envelope.
|
|
type ServerViewerQualityEvidenceMessage struct {
|
|
Type string `json:"type"`
|
|
ViewerPeerID string `json:"viewerPeerId"`
|
|
Upstream MediaRouteUpstream `json:"upstream"`
|
|
Guard ViewerQualityEvidenceGuard `json:"guard"`
|
|
Sequence Int `json:"sequence"`
|
|
WindowMs Int `json:"windowMs"`
|
|
Metrics ViewerQualityEvidenceMetrics `json:"metrics"`
|
|
}
|
|
|
|
// HostStatusMessage reports host presence and pause state.
|
|
type HostStatusMessage struct {
|
|
Type string `json:"type"`
|
|
Online bool `json:"online"`
|
|
Paused bool `json:"paused"`
|
|
}
|
|
|
|
// ViewerPresenceMessage carries the ordered participant roster.
|
|
type ViewerPresenceMessage struct {
|
|
Type string `json:"type"`
|
|
Viewers []ParticipantPresenceEntry `json:"viewers"`
|
|
}
|
|
|
|
// MarshalJSON implements json.Marshaler.
|
|
func (v ViewerPresenceMessage) MarshalJSON() ([]byte, error) {
|
|
type raw ViewerPresenceMessage
|
|
value := raw(v)
|
|
if value.Viewers == nil {
|
|
value.Viewers = []ParticipantPresenceEntry{}
|
|
}
|
|
return marshalJSON(value)
|
|
}
|
|
|
|
// ViewerGrantRevokedMessage announces a superseded viewer authorization.
|
|
type ViewerGrantRevokedMessage struct {
|
|
Type string `json:"type"`
|
|
ViewerAuthorizationGeneration string `json:"viewerAuthorizationGeneration"`
|
|
}
|
|
|
|
// SharingStoppedMessage is { type: "sharing-stopped" }.
|
|
type SharingStoppedMessage struct {
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// RoomClosedMessage is { type: "room-closed", reason }.
|
|
type RoomClosedMessage struct {
|
|
Type string `json:"type"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// ErrorMessage is { type: "error", code, message }.
|
|
type ErrorMessage struct {
|
|
Type string `json:"type"`
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func (AuthenticatedHostMessage) isServerMessage() {}
|
|
func (AuthenticatedViewerMessage) isServerMessage() {}
|
|
func (SignalingChallengeResponseMessage) isServerMessage() {}
|
|
func (ServerSignalMessage) isServerMessage() {}
|
|
func (ServerRestartRequestMessage) isServerMessage() {}
|
|
func (RouteUpdatePrepareMessage) isServerMessage() {}
|
|
func (RouteUpdateActiveMessage) isServerMessage() {}
|
|
func (RouteStatusMessage) isServerMessage() {}
|
|
func (RouteDiagnosticSnapshotMessage) isServerMessage() {}
|
|
func (SfuConfigMessage) isServerMessage() {}
|
|
func (QualitySettingsMessage) isServerMessage() {}
|
|
func (RoutePolicyMessage) isServerMessage() {}
|
|
func (PauseSharingSourceMessage) isServerMessage() {}
|
|
func (ServerViewerQualityEvidenceMessage) isServerMessage() {}
|
|
func (HostStatusMessage) isServerMessage() {}
|
|
func (ViewerPresenceMessage) isServerMessage() {}
|
|
func (ViewerGrantRevokedMessage) isServerMessage() {}
|
|
func (SharingStoppedMessage) isServerMessage() {}
|
|
func (RoomClosedMessage) isServerMessage() {}
|
|
func (ErrorMessage) isServerMessage() {}
|
|
|
|
// EncodeServerMessage serializes one server message with the key order of the
|
|
// TypeScript object literal that produces it.
|
|
func EncodeServerMessage(message ServerMessage) ([]byte, error) {
|
|
return marshalJSON(message)
|
|
}
|
|
|
|
// DecodeServerMessage ports decodeServerMessage. The server never reads its
|
|
// own messages in production; this exists so the shared wire fixture can be
|
|
// replayed from Go.
|
|
func DecodeServerMessage(data []byte) (ServerMessage, error) {
|
|
messageType, err := typeOf(data, "type")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch messageType {
|
|
case "authenticated":
|
|
return decodeAuthenticated(data)
|
|
case "signaling-challenge-response":
|
|
return decodeChallengeResponse(data)
|
|
case "signal":
|
|
return decodeServerSignal(data)
|
|
case "restart-request":
|
|
return decodeServerRestartRequest(data)
|
|
case "route-update":
|
|
return decodeRouteUpdate(data)
|
|
case "route-status":
|
|
return decodeRouteStatus(data)
|
|
case "route-diagnostic-snapshot":
|
|
return decodeRouteDiagnosticSnapshotMessage(data)
|
|
case "sfu-config":
|
|
return decodeSfuConfig(data)
|
|
case "quality-settings":
|
|
return decodeQualitySettingsMessage(data)
|
|
case "route-policy":
|
|
return decodeRoutePolicyMessage(data)
|
|
case "pause-sharing-source":
|
|
return decodePauseSharingSource(data)
|
|
case "viewer-quality-evidence":
|
|
return decodeServerViewerQualityEvidence(data)
|
|
case "host-status":
|
|
return decodeHostStatus(data)
|
|
case "viewer-presence":
|
|
return decodeViewerPresence(data)
|
|
case "viewer-grant-revoked":
|
|
return decodeViewerGrantRevoked(data)
|
|
case "sharing-stopped":
|
|
return decodeEmptyServerMessage(data, SharingStoppedMessage{Type: messageType})
|
|
case "room-closed":
|
|
return decodeRoomClosed(data)
|
|
case "error":
|
|
return decodeErrorMessage(data)
|
|
}
|
|
return nil, fmt.Errorf("unknown server message type %q", messageType)
|
|
}
|
|
|
|
func decodeEmptyServerMessage(data []byte, message ServerMessage) (ServerMessage, error) {
|
|
var body struct {
|
|
Type string `json:"type"`
|
|
}
|
|
present, err := decodeObject(data, &body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type"); err != nil {
|
|
return nil, err
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func validateAuthenticatedBase(
|
|
present fields,
|
|
protocol string, peerID string, roomExpiresAt *string,
|
|
maxViewers, endpointCapacity Int, connectionID *string,
|
|
codeEntryPolicy, viewerAuthorizationGeneration, mediaMode string,
|
|
shareGeneration *string, routeRevision Int, sfuStandbyURL *string,
|
|
) error {
|
|
if err := present.require("type", "protocol", "peerId", "maxViewers",
|
|
"endpointMediaCopyCapacity", "hostOnline", "iceConfig", "codeEntryPolicy",
|
|
"viewerAuthorizationGeneration", "mediaMode", "routeRevision",
|
|
"routeAssignment", "qualitySettings", "role"); err != nil {
|
|
return err
|
|
}
|
|
if err := present.requireNullable(
|
|
"roomExpiresAt", "connectionId", "shareGeneration"); err != nil {
|
|
return err
|
|
}
|
|
if err := present.optional("hostPaused", "routePolicy", "sfuStandbyUrl"); err != nil {
|
|
return err
|
|
}
|
|
if protocol != SignalingProtocol {
|
|
return errors.New("protocol is not the current signaling generation")
|
|
}
|
|
if !ValidOpaqueID(peerID) {
|
|
return errors.New("peerId is not an opaque id")
|
|
}
|
|
if roomExpiresAt != nil && !ValidISODateTime(*roomExpiresAt) {
|
|
return errors.New("roomExpiresAt is not an ISO instant")
|
|
}
|
|
if !inRangeInt(maxViewers, 1, MaxViewersPerRoomLimit) {
|
|
return errors.New("maxViewers is out of range")
|
|
}
|
|
if !inRangeInt(endpointCapacity, 1, MaxEndpointMediaCopyCapacity) {
|
|
return errors.New("endpointMediaCopyCapacity is out of range")
|
|
}
|
|
if connectionID != nil && !ValidOpaqueID(*connectionID) {
|
|
return errors.New("connectionId is not an opaque id")
|
|
}
|
|
if !enumOf(codeEntryPolicy, CodeEntryOpen, CodeEntryPrivate) {
|
|
return fmt.Errorf("unknown codeEntryPolicy %q", codeEntryPolicy)
|
|
}
|
|
if !ValidOpaqueID(viewerAuthorizationGeneration) {
|
|
return errors.New("viewerAuthorizationGeneration is not an opaque id")
|
|
}
|
|
if mediaMode != "peer-assisted" {
|
|
return errors.New("mediaMode must be peer-assisted")
|
|
}
|
|
if shareGeneration != nil && !ValidOpaqueID(*shareGeneration) {
|
|
return errors.New("shareGeneration is not an opaque id")
|
|
}
|
|
if !validRevision(routeRevision) {
|
|
return errors.New("routeRevision is out of range")
|
|
}
|
|
if sfuStandbyURL != nil && !ValidLiveKitWebSocketURL(*sfuStandbyURL) {
|
|
return errors.New("sfuStandbyUrl is not a LiveKit WebSocket URL")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func decodeAuthenticated(data []byte) (ServerMessage, error) {
|
|
role, err := typeOf(data, "role")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch role {
|
|
case RoleHost:
|
|
var message AuthenticatedHostMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("viewerPasswordEnabled"); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateAuthenticatedBase(present, message.Protocol, message.PeerID,
|
|
message.RoomExpiresAt, message.MaxViewers, message.EndpointMediaCopyCapacity,
|
|
message.ConnectionID, message.CodeEntryPolicy,
|
|
message.ViewerAuthorizationGeneration, message.MediaMode,
|
|
message.ShareGeneration, message.RouteRevision, message.SfuStandbyURL); err != nil {
|
|
return nil, err
|
|
}
|
|
if !present.has("routePolicy") {
|
|
message.RoutePolicy = DefaultRoutePolicy
|
|
}
|
|
return message, nil
|
|
case RoleViewer:
|
|
var message AuthenticatedViewerMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateAuthenticatedBase(present, message.Protocol, message.PeerID,
|
|
message.RoomExpiresAt, message.MaxViewers, message.EndpointMediaCopyCapacity,
|
|
message.ConnectionID, message.CodeEntryPolicy,
|
|
message.ViewerAuthorizationGeneration, message.MediaMode,
|
|
message.ShareGeneration, message.RouteRevision, message.SfuStandbyURL); err != nil {
|
|
return nil, err
|
|
}
|
|
if !present.has("routePolicy") {
|
|
message.RoutePolicy = DefaultRoutePolicy
|
|
}
|
|
return message, nil
|
|
}
|
|
return nil, fmt.Errorf("unknown authenticated role %q", role)
|
|
}
|
|
|
|
func decodeChallengeResponse(data []byte) (ServerMessage, error) {
|
|
var message SignalingChallengeResponseMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "sequence"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !inRangeInt(message.Sequence, 0, MaxSafeInteger) {
|
|
return nil, errors.New("sequence is out of range")
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodeServerSignal(data []byte) (ServerMessage, error) {
|
|
var message ServerSignalMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "fromPeerId", "payload"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !ValidOpaqueID(message.FromPeerID) {
|
|
return nil, errors.New("fromPeerId is not an opaque id")
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodeServerRestartRequest(data []byte) (ServerMessage, error) {
|
|
var message ServerRestartRequestMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "fromPeerId", "connectionId", "rebuild"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !ValidOpaqueID(message.FromPeerID) || !ValidOpaqueID(message.ConnectionID) {
|
|
return nil, errors.New("restart-request ids are not opaque ids")
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodeRouteUpdate(data []byte) (ServerMessage, error) {
|
|
phase, err := typeOf(data, "phase")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch phase {
|
|
case "prepare":
|
|
var message RouteUpdatePrepareMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require(
|
|
"type", "revision", "phase", "assignment", "candidate"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !validRevision(message.Revision) {
|
|
return nil, errors.New("revision is out of range")
|
|
}
|
|
return message, nil
|
|
case "active":
|
|
var message RouteUpdateActiveMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "revision", "phase", "assignment"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !validRevision(message.Revision) {
|
|
return nil, errors.New("revision is out of range")
|
|
}
|
|
return message, nil
|
|
}
|
|
return nil, fmt.Errorf("unknown route-update phase %q", phase)
|
|
}
|
|
|
|
func decodeRouteStatus(data []byte) (ServerMessage, error) {
|
|
var message RouteStatusMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "revision", "state", "reason"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !validRevision(message.Revision) {
|
|
return nil, errors.New("revision is out of range")
|
|
}
|
|
switch {
|
|
case message.State == "waiting" && message.Reason == "sfu-admission":
|
|
case message.State == "failed" && message.Reason == "route-exhausted":
|
|
default:
|
|
return nil, fmt.Errorf(
|
|
"route-status pairs state %q with reason %q", message.State, message.Reason)
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodeRouteDiagnosticSnapshotMessage(data []byte) (ServerMessage, error) {
|
|
var message RouteDiagnosticSnapshotMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "snapshot"); err != nil {
|
|
return nil, err
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodeSfuConfig(data []byte) (ServerMessage, error) {
|
|
var message SfuConfigMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "revision", "url", "token"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !validRevision(message.Revision) {
|
|
return nil, errors.New("revision is out of range")
|
|
}
|
|
if !ValidLiveKitWebSocketURL(message.URL) {
|
|
return nil, errors.New("url is not a LiveKit WebSocket URL")
|
|
}
|
|
if length := UTF16Length(message.Token); length < 1 || length > MaxSfuTokenLength {
|
|
return nil, errors.New("token length is out of range")
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodeQualitySettingsMessage(data []byte) (ServerMessage, error) {
|
|
var message QualitySettingsMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "qualitySettings"); err != nil {
|
|
return nil, err
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodeRoutePolicyMessage(data []byte) (ServerMessage, error) {
|
|
var message RoutePolicyMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "shareGeneration", "routePolicy"); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.optional("sfuStandbyUrl"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !ValidOpaqueID(message.ShareGeneration) {
|
|
return nil, errors.New("shareGeneration is not an opaque id")
|
|
}
|
|
if message.SfuStandbyURL != nil && !ValidLiveKitWebSocketURL(*message.SfuStandbyURL) {
|
|
return nil, errors.New("sfuStandbyUrl is not a LiveKit WebSocket URL")
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodePauseSharingSource(data []byte) (ServerMessage, error) {
|
|
var message PauseSharingSourceMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "shareGeneration"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !ValidOpaqueID(message.ShareGeneration) {
|
|
return nil, errors.New("shareGeneration is not an opaque id")
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodeServerViewerQualityEvidence(data []byte) (ServerMessage, error) {
|
|
var message ServerViewerQualityEvidenceMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "viewerPeerId", "upstream", "guard",
|
|
"sequence", "windowMs", "metrics"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !ValidOpaqueID(message.ViewerPeerID) {
|
|
return nil, errors.New("viewerPeerId is not an opaque id")
|
|
}
|
|
// The forwarded envelope narrows the upstream to the active variants.
|
|
if message.Upstream.Kind != "peer" && message.Upstream.Kind != "sfu" {
|
|
return nil, errors.New("forwarded evidence upstream must be peer or sfu")
|
|
}
|
|
if err := validateEvidenceWindow(message.Sequence, message.WindowMs); err != nil {
|
|
return nil, err
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodeHostStatus(data []byte) (ServerMessage, error) {
|
|
var message HostStatusMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "online", "paused"); err != nil {
|
|
return nil, err
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodeViewerPresence(data []byte) (ServerMessage, error) {
|
|
var message ViewerPresenceMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "viewers"); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(message.Viewers) > MaxParticipantsPerRoomLimit {
|
|
return nil, errors.New("viewer presence holds too many entries")
|
|
}
|
|
// superRefine: viewer presence peer IDs must be unique.
|
|
seen := make(map[string]struct{}, len(message.Viewers))
|
|
for _, viewer := range message.Viewers {
|
|
if _, duplicate := seen[viewer.PeerID]; duplicate {
|
|
return nil, errors.New("Viewer presence peer IDs must be unique")
|
|
}
|
|
seen[viewer.PeerID] = struct{}{}
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodeViewerGrantRevoked(data []byte) (ServerMessage, error) {
|
|
var message ViewerGrantRevokedMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "viewerAuthorizationGeneration"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !ValidOpaqueID(message.ViewerAuthorizationGeneration) {
|
|
return nil, errors.New("viewerAuthorizationGeneration is not an opaque id")
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodeRoomClosed(data []byte) (ServerMessage, error) {
|
|
var message RoomClosedMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "reason"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !enumOf(message.Reason, "host-ended", "expired") {
|
|
return nil, fmt.Errorf("unknown room-closed reason %q", message.Reason)
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func decodeErrorMessage(data []byte) (ServerMessage, error) {
|
|
var message ErrorMessage
|
|
present, err := decodeObject(data, &message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := present.require("type", "code", "message"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !enumOf(message.Code, errorCodes...) {
|
|
return nil, fmt.Errorf("unknown error code %q", message.Code)
|
|
}
|
|
if length := UTF16Length(message.Message); length < 1 || length > 256 {
|
|
return nil, errors.New("error message length is out of range")
|
|
}
|
|
return message, nil
|
|
}
|