Files
Piik/internal/server/protocol/client.go
T
AAA da47189fc3 refactor: unify Go runtime and audited frontend
Squash integration of the reviewed Go core, frontend audit fixes, release tooling, and deployment hygiene. CI passed all Server/Client jobs.
2026-09-06 23:26:01 +08:00

699 lines
23 KiB
Go

package protocol
import (
"errors"
"fmt"
)
// ClientMessage is one member of clientMessageSchema. The interface is sealed:
// only the message structs in this file implement it.
type ClientMessage interface {
isClientMessage()
}
// AuthenticateHostMessage is the host member of authenticateMessageSchema.
type AuthenticateHostMessage struct {
Type string `json:"type"`
Protocol string `json:"protocol"`
RoomID string `json:"roomId"`
Role Role `json:"role"`
Token string `json:"token"`
ClientID string `json:"clientId"`
ShareGeneration string `json:"shareGeneration,omitempty"`
SharingPaused *bool `json:"sharingPaused,omitempty"`
QualitySettings *QualitySettings `json:"qualitySettings,omitempty"`
RoutePolicy RoutePolicy `json:"routePolicy"`
ViewerPresence bool `json:"viewerPresence,omitempty"`
DisplayName *DisplayName `json:"displayName,omitempty"`
}
// AuthenticateViewerMessage is the viewer member of authenticateMessageSchema.
type AuthenticateViewerMessage struct {
Type string `json:"type"`
Protocol string `json:"protocol"`
RoomID string `json:"roomId"`
Role Role `json:"role"`
ClientID string `json:"clientId"`
ViewerGrant string `json:"viewerGrant,omitempty"`
ViewerPassword string `json:"viewerPassword,omitempty"`
DisplayName *DisplayName `json:"displayName,omitempty"`
ViewerPresence bool `json:"viewerPresence,omitempty"`
}
// SignalingChallengeMessage is { type: "signaling-challenge", sequence }.
type SignalingChallengeMessage struct {
Type string `json:"type"`
Sequence Int `json:"sequence"`
}
// ClientSignalMessage is the client "signal" message.
type ClientSignalMessage struct {
Type string `json:"type"`
TargetPeerID string `json:"targetPeerId,omitempty"`
Payload SignalPayload `json:"payload"`
}
// ClientRestartRequestMessage is the client "restart-request" message.
type ClientRestartRequestMessage struct {
Type string `json:"type"`
TargetPeerID string `json:"targetPeerId,omitempty"`
ConnectionID string `json:"connectionId"`
Rebuild bool `json:"rebuild"`
}
// SetQualitySettingsMessage is { type: "set-quality-settings", qualitySettings }.
type SetQualitySettingsMessage struct {
Type string `json:"type"`
QualitySettings QualitySettings `json:"qualitySettings"`
}
// RelayCapacityMessage is { type: "relay-capacity", downstreamEdges }.
type RelayCapacityMessage struct {
Type string `json:"type"`
DownstreamEdges Int `json:"downstreamEdges"`
}
// RouteReadyMessage is { type: "route-ready", revision, phase, qualityApproved? }.
type RouteReadyMessage struct {
Type string `json:"type"`
Revision Int `json:"revision"`
Phase string `json:"phase"`
QualityApproved bool `json:"qualityApproved,omitempty"`
}
// RouteTransportConnectedMessage is the "route-transport-connected" message.
type RouteTransportConnectedMessage struct {
Type string `json:"type"`
Revision Int `json:"revision"`
ConnectionID string `json:"connectionId"`
}
// RouteMediaUnavailableMessage is the "route-media-unavailable" message.
type RouteMediaUnavailableMessage struct {
Type string `json:"type"`
Revision Int `json:"revision"`
}
// RouteFailedMessage is { type: "route-failed", revision, phase, connectionId }.
type RouteFailedMessage struct {
Type string `json:"type"`
Revision Int `json:"revision"`
Phase string `json:"phase"`
ConnectionID *string `json:"connectionId"`
}
// RefreshSfuMessage is { type: "refresh-sfu", revision }.
type RefreshSfuMessage struct {
Type string `json:"type"`
Revision Int `json:"revision"`
}
// RequestRouteDiagnosticMessage is { type: "request-route-diagnostic" }.
type RequestRouteDiagnosticMessage struct {
Type string `json:"type"`
}
// ViewerQualityEvidenceMessage is viewerQualityEvidenceMessageSchema.
type ViewerQualityEvidenceMessage struct {
Type string `json:"type"`
Guard ViewerQualityEvidenceGuard `json:"guard"`
Sequence Int `json:"sequence"`
WindowMs Int `json:"windowMs"`
Metrics ViewerQualityEvidenceMetrics `json:"metrics"`
}
// SenderQualityEvidenceMessage is senderQualityEvidenceMessageSchema.
type SenderQualityEvidenceMessage struct {
Type string `json:"type"`
ChildPeerID string `json:"childPeerId"`
ConnectionID string `json:"connectionId"`
RtpStatsID *string `json:"rtpStatsId"`
TrackIdentifier *string `json:"trackIdentifier"`
SampleTimestampMs *Num `json:"sampleTimestampMs"`
RouteRevision Int `json:"routeRevision"`
State string `json:"state"`
Diagnostics SenderQualityDiagnostics `json:"diagnostics"`
}
// SfuPublisherQualityEvidenceMessage is
// sfuPublisherQualityEvidenceMessageSchema.
type SfuPublisherQualityEvidenceMessage struct {
Type string `json:"type"`
PublicationGeneration string `json:"publicationGeneration"`
RouteRevision Int `json:"routeRevision"`
State string `json:"state"`
SampleTimestampMs *Num `json:"sampleTimestampMs"`
Diagnostics SenderQualityDiagnostics `json:"diagnostics"`
}
// ResetSenderQualityMessage is { type: "reset-sender-quality" }.
type ResetSenderQualityMessage struct {
Type string `json:"type"`
}
// SetDisplayNameMessage is { type: "set-display-name", displayName }.
type SetDisplayNameMessage struct {
Type string `json:"type"`
DisplayName DisplayName `json:"displayName"`
}
// SetSharingPausedMessage is { type: "set-sharing-paused", shareGeneration, paused }.
type SetSharingPausedMessage struct {
Type string `json:"type"`
ShareGeneration string `json:"shareGeneration"`
Paused bool `json:"paused"`
}
// StopSharingMessage is { type: "stop-sharing", shareGeneration? }.
type StopSharingMessage struct {
Type string `json:"type"`
ShareGeneration string `json:"shareGeneration,omitempty"`
}
// AbandonRoomMessage is { type: "abandon-room" }.
type AbandonRoomMessage struct {
Type string `json:"type"`
}
func (AuthenticateHostMessage) isClientMessage() {}
func (AuthenticateViewerMessage) isClientMessage() {}
func (SignalingChallengeMessage) isClientMessage() {}
func (ClientSignalMessage) isClientMessage() {}
func (ClientRestartRequestMessage) isClientMessage() {}
func (SetQualitySettingsMessage) isClientMessage() {}
func (RelayCapacityMessage) isClientMessage() {}
func (RouteReadyMessage) isClientMessage() {}
func (RouteTransportConnectedMessage) isClientMessage() {}
func (RouteMediaUnavailableMessage) isClientMessage() {}
func (RouteFailedMessage) isClientMessage() {}
func (RefreshSfuMessage) isClientMessage() {}
func (RequestRouteDiagnosticMessage) isClientMessage() {}
func (ViewerQualityEvidenceMessage) isClientMessage() {}
func (SenderQualityEvidenceMessage) isClientMessage() {}
func (SfuPublisherQualityEvidenceMessage) isClientMessage() {}
func (ResetSenderQualityMessage) isClientMessage() {}
func (SetDisplayNameMessage) isClientMessage() {}
func (SetSharingPausedMessage) isClientMessage() {}
func (StopSharingMessage) isClientMessage() {}
func (AbandonRoomMessage) isClientMessage() {}
// DecodeClientMessage ports decodeClientMessage: JSON.parse followed by
// clientMessageSchema.parse. Every member of the union carries a distinct
// `type` literal, so the union is resolved by that key.
func DecodeClientMessage(data []byte) (ClientMessage, error) {
messageType, err := typeOf(data, "type")
if err != nil {
return nil, err
}
switch messageType {
case "authenticate":
return decodeAuthenticate(data)
case "signaling-challenge":
return decodeSignalingChallenge(data)
case "signal":
return decodeClientSignal(data)
case "restart-request":
return decodeClientRestartRequest(data)
case "set-quality-settings":
return decodeSetQualitySettings(data)
case "relay-capacity":
return decodeRelayCapacity(data)
case "route-ready":
return decodeRouteReady(data)
case "route-transport-connected":
return decodeRouteTransportConnected(data)
case "route-media-unavailable":
return decodeRouteMediaUnavailable(data)
case "route-failed":
return decodeRouteFailed(data)
case "refresh-sfu":
return decodeRefreshSfu(data)
case "request-route-diagnostic":
return decodeEmptyClientMessage(data, RequestRouteDiagnosticMessage{Type: messageType})
case "viewer-quality-evidence":
return decodeViewerQualityEvidence(data)
case "sender-quality-evidence":
return decodeSenderQualityEvidence(data)
case "sfu-publisher-quality-evidence":
return decodeSfuPublisherQualityEvidence(data)
case "reset-sender-quality":
return decodeEmptyClientMessage(data, ResetSenderQualityMessage{Type: messageType})
case "set-display-name":
return decodeSetDisplayName(data)
case "set-sharing-paused":
return decodeSetSharingPaused(data)
case "stop-sharing":
return decodeStopSharing(data)
case "abandon-room":
return decodeEmptyClientMessage(data, AbandonRoomMessage{Type: messageType})
}
return nil, fmt.Errorf("unknown client message type %q", messageType)
}
func decodeEmptyClientMessage(data []byte, message ClientMessage) (ClientMessage, 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 decodeAuthenticate(data []byte) (ClientMessage, error) {
role, err := typeOf(data, "role")
if err != nil {
return nil, err
}
switch role {
case RoleHost:
var message AuthenticateHostMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "protocol", "roomId", "role", "token", "clientId"); err != nil {
return nil, err
}
if err := present.optional("shareGeneration", "sharingPaused", "qualitySettings",
"routePolicy", "viewerPresence", "displayName"); err != nil {
return nil, err
}
if message.Protocol != SignalingProtocol {
return nil, errors.New("protocol is not the current signaling generation")
}
if !ValidRoomCode(message.RoomID) {
return nil, errors.New("roomId is not a room code")
}
if !ValidToken(message.Token) {
return nil, errors.New("token is malformed")
}
if !ValidOpaqueID(message.ClientID) {
return nil, errors.New("clientId is not an opaque id")
}
if present.has("shareGeneration") && !ValidOpaqueID(message.ShareGeneration) {
return nil, errors.New("shareGeneration is not an opaque id")
}
if present.has("viewerPresence") && !message.ViewerPresence {
return nil, errors.New("viewerPresence must be true when present")
}
if !present.has("routePolicy") {
message.RoutePolicy = DefaultRoutePolicy
}
return message, nil
case RoleViewer:
var message AuthenticateViewerMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "protocol", "roomId", "role", "clientId"); err != nil {
return nil, err
}
if err := present.optional(
"viewerGrant", "viewerPassword", "displayName", "viewerPresence"); err != nil {
return nil, err
}
if message.Protocol != SignalingProtocol {
return nil, errors.New("protocol is not the current signaling generation")
}
if !ValidRoomCode(message.RoomID) {
return nil, errors.New("roomId is not a room code")
}
if !ValidOpaqueID(message.ClientID) {
return nil, errors.New("clientId is not an opaque id")
}
if present.has("viewerGrant") && !ValidViewerGrant(message.ViewerGrant) {
return nil, errors.New("viewerGrant is malformed")
}
if present.has("viewerPassword") && !ValidViewerPassword(message.ViewerPassword) {
return nil, errors.New("viewerPassword is malformed")
}
if present.has("viewerPresence") && !message.ViewerPresence {
return nil, errors.New("viewerPresence must be true when present")
}
return message, nil
}
return nil, fmt.Errorf("unknown authenticate role %q", role)
}
func decodeSignalingChallenge(data []byte) (ClientMessage, error) {
var message SignalingChallengeMessage
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 decodeClientSignal(data []byte) (ClientMessage, error) {
var message ClientSignalMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "payload"); err != nil {
return nil, err
}
if err := present.optional("targetPeerId"); err != nil {
return nil, err
}
if present.has("targetPeerId") && !ValidOpaqueID(message.TargetPeerID) {
return nil, errors.New("targetPeerId is not an opaque id")
}
return message, nil
}
func decodeClientRestartRequest(data []byte) (ClientMessage, error) {
var message ClientRestartRequestMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "connectionId", "rebuild"); err != nil {
return nil, err
}
if err := present.optional("targetPeerId"); err != nil {
return nil, err
}
if present.has("targetPeerId") && !ValidOpaqueID(message.TargetPeerID) {
return nil, errors.New("targetPeerId is not an opaque id")
}
if !ValidOpaqueID(message.ConnectionID) {
return nil, errors.New("connectionId is not an opaque id")
}
return message, nil
}
func decodeSetQualitySettings(data []byte) (ClientMessage, error) {
var message SetQualitySettingsMessage
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 decodeRelayCapacity(data []byte) (ClientMessage, error) {
var message RelayCapacityMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "downstreamEdges"); err != nil {
return nil, err
}
if !inRangeInt(message.DownstreamEdges, 0, MaxEndpointMediaCopyCapacity) {
return nil, errors.New("downstreamEdges is out of range")
}
return message, nil
}
func decodeRouteReady(data []byte) (ClientMessage, error) {
var message RouteReadyMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "revision", "phase"); err != nil {
return nil, err
}
if err := present.optional("qualityApproved"); err != nil {
return nil, err
}
if !validRevision(message.Revision) {
return nil, errors.New("revision is out of range")
}
if !enumOf(message.Phase, mediaRoutePhases...) {
return nil, fmt.Errorf("unknown phase %q", message.Phase)
}
if present.has("qualityApproved") && !message.QualityApproved {
return nil, errors.New("qualityApproved must be true when present")
}
return message, nil
}
func decodeRouteTransportConnected(data []byte) (ClientMessage, error) {
var message RouteTransportConnectedMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "revision", "connectionId"); err != nil {
return nil, err
}
if !validRevision(message.Revision) {
return nil, errors.New("revision is out of range")
}
if !ValidOpaqueID(message.ConnectionID) {
return nil, errors.New("connectionId is not an opaque id")
}
return message, nil
}
func decodeRouteMediaUnavailable(data []byte) (ClientMessage, error) {
var message RouteMediaUnavailableMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "revision"); err != nil {
return nil, err
}
if !validRevision(message.Revision) {
return nil, errors.New("revision is out of range")
}
return message, nil
}
func decodeRouteFailed(data []byte) (ClientMessage, error) {
var message RouteFailedMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "revision", "phase"); err != nil {
return nil, err
}
if err := present.requireNullable("connectionId"); err != nil {
return nil, err
}
if !validRevision(message.Revision) {
return nil, errors.New("revision is out of range")
}
if !enumOf(message.Phase, mediaRoutePhases...) {
return nil, fmt.Errorf("unknown phase %q", message.Phase)
}
if message.ConnectionID != nil && !ValidOpaqueID(*message.ConnectionID) {
return nil, errors.New("connectionId is not an opaque id")
}
return message, nil
}
func decodeRefreshSfu(data []byte) (ClientMessage, error) {
var message RefreshSfuMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "revision"); err != nil {
return nil, err
}
if !validRevision(message.Revision) {
return nil, errors.New("revision is out of range")
}
return message, nil
}
func decodeViewerQualityEvidence(data []byte) (ClientMessage, error) {
var message ViewerQualityEvidenceMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "guard", "sequence", "windowMs", "metrics"); err != nil {
return nil, err
}
if err := validateEvidenceWindow(message.Sequence, message.WindowMs); err != nil {
return nil, err
}
return message, nil
}
func validateEvidenceWindow(sequence, windowMs Int) error {
if !inRangeInt(sequence, 0, MaxSafeInteger) {
return errors.New("sequence is out of range")
}
if !inRangeInt(windowMs, 1_000, 5_000) {
return errors.New("windowMs is out of range")
}
return nil
}
func decodeSenderQualityEvidence(data []byte) (ClientMessage, error) {
var message SenderQualityEvidenceMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "childPeerId", "connectionId", "routeRevision",
"state", "diagnostics"); err != nil {
return nil, err
}
if err := present.requireNullable(
"rtpStatsId", "trackIdentifier", "sampleTimestampMs"); err != nil {
return nil, err
}
if !ValidOpaqueID(message.ChildPeerID) || !ValidOpaqueID(message.ConnectionID) {
return nil, errors.New("sender quality evidence ids are not opaque ids")
}
if err := boundedIdentity(message.RtpStatsID, "rtpStatsId"); err != nil {
return nil, err
}
if err := boundedIdentity(message.TrackIdentifier, "trackIdentifier"); err != nil {
return nil, err
}
if message.SampleTimestampMs != nil &&
!inRangeNum(*message.SampleTimestampMs, 0, MaxSafeInteger) {
return nil, errors.New("sampleTimestampMs is out of range")
}
if !validRevision(message.RouteRevision) {
return nil, errors.New("routeRevision is out of range")
}
if !enumOf(message.State, senderQualityStates...) {
return nil, fmt.Errorf("unknown sender quality state %q", message.State)
}
// superRefine: known sender quality needs exact RTP identity.
if message.State != "unknown" &&
(message.RtpStatsID == nil || message.TrackIdentifier == nil ||
message.SampleTimestampMs == nil) {
return nil, errors.New("Known sender quality needs exact RTP identity")
}
if err := senderQualityReasonMatchesState(
message.State, message.Diagnostics.Reason,
"Sender quality reason must match its native state"); err != nil {
return nil, err
}
return message, nil
}
func decodeSfuPublisherQualityEvidence(data []byte) (ClientMessage, error) {
var message SfuPublisherQualityEvidenceMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require(
"type", "publicationGeneration", "routeRevision", "state", "diagnostics"); err != nil {
return nil, err
}
if err := present.requireNullable("sampleTimestampMs"); err != nil {
return nil, err
}
if !ValidOpaqueID(message.PublicationGeneration) {
return nil, errors.New("publicationGeneration is not an opaque id")
}
if !validRevision(message.RouteRevision) {
return nil, errors.New("routeRevision is out of range")
}
if !enumOf(message.State, senderQualityStates...) {
return nil, fmt.Errorf("unknown sfu publisher quality state %q", message.State)
}
if message.SampleTimestampMs != nil &&
!inRangeNum(*message.SampleTimestampMs, 0, MaxSafeInteger) {
return nil, errors.New("sampleTimestampMs is out of range")
}
// superRefine: known SFU publisher quality needs an exact sample.
if message.State != "unknown" && message.SampleTimestampMs == nil {
return nil, errors.New("Known SFU publisher quality needs an exact sample")
}
if err := senderQualityReasonMatchesState(
message.State, message.Diagnostics.Reason,
"SFU publisher reason must match its native state"); err != nil {
return nil, err
}
return message, nil
}
func senderQualityReasonMatchesState(state string, reason *string, message string) error {
value := ""
if reason != nil {
value = *reason
}
invalid := (state == "healthy" && value != "none") ||
(state == "degraded" && value != "bandwidth" && value != "cpu") ||
(state == "unknown" && reason != nil)
if invalid {
return errors.New(message)
}
return nil
}
func boundedIdentity(value *string, key string) error {
if value == nil {
return nil
}
if length := UTF16Length(*value); length < 1 || length > 256 {
return fmt.Errorf("%s length is out of range", key)
}
return nil
}
func decodeSetDisplayName(data []byte) (ClientMessage, error) {
var message SetDisplayNameMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "displayName"); err != nil {
return nil, err
}
return message, nil
}
func decodeSetSharingPaused(data []byte) (ClientMessage, error) {
var message SetSharingPausedMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type", "shareGeneration", "paused"); err != nil {
return nil, err
}
if !ValidOpaqueID(message.ShareGeneration) {
return nil, errors.New("shareGeneration is not an opaque id")
}
return message, nil
}
func decodeStopSharing(data []byte) (ClientMessage, error) {
var message StopSharingMessage
present, err := decodeObject(data, &message)
if err != nil {
return nil, err
}
if err := present.require("type"); err != nil {
return nil, err
}
if err := present.optional("shareGeneration"); err != nil {
return nil, err
}
if present.has("shareGeneration") && !ValidOpaqueID(message.ShareGeneration) {
return nil, errors.New("shareGeneration is not an opaque id")
}
return message, nil
}