feat(protocol): define gateway control envelopes
This commit is contained in:
+310
-18
@@ -12,7 +12,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const SchemaSHA256 = "792abfb9cfe70e79911d499d76c009ab848713278bc240b88576df520580e480"
|
||||
const SchemaSHA256 = "e35414af52d7a097dea05567fdab11f842a42e529fb6cbedd281c48dbf930b17"
|
||||
const ProtocolVersion = "1.0.0"
|
||||
const CurrentWireVersion = "1"
|
||||
const NMinus1WireVersion = "0"
|
||||
@@ -86,6 +86,13 @@ type ChannelFrame struct {
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
|
||||
type ClipboardPolicy struct {
|
||||
ClientToProviderEnabled bool `json:"client_to_provider_enabled"`
|
||||
ProviderToClientEnabled bool `json:"provider_to_client_enabled"`
|
||||
MaxTextBytes int64 `json:"max_text_bytes"`
|
||||
MaxUpdatesPerMinute int64 `json:"max_updates_per_minute"`
|
||||
}
|
||||
|
||||
type ClipboardText struct {
|
||||
Text string `json:"text"`
|
||||
Encoding string `json:"encoding"`
|
||||
@@ -158,6 +165,22 @@ type EventResume struct {
|
||||
LastSequence int64 `json:"last_sequence"`
|
||||
}
|
||||
|
||||
type GatewayClipboardAudit struct {
|
||||
Version string `json:"version"`
|
||||
SessionID string `json:"session_id"`
|
||||
Direction string `json:"direction"`
|
||||
Outcome string `json:"outcome"`
|
||||
TextBytes int64 `json:"text_bytes"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type GatewayClipboardText struct {
|
||||
Direction string `json:"direction"`
|
||||
Text string `json:"text"`
|
||||
Encoding string `json:"encoding"`
|
||||
LoopToken string `json:"loop_token"`
|
||||
}
|
||||
|
||||
type GatewayDrain struct {
|
||||
Version string `json:"version"`
|
||||
GatewayID string `json:"gateway_id"`
|
||||
@@ -241,23 +264,25 @@ type PageInfo struct {
|
||||
}
|
||||
|
||||
type ProviderSessionWork struct {
|
||||
Version string `json:"version"`
|
||||
SessionID string `json:"session_id"`
|
||||
GatewayID string `json:"gateway_id"`
|
||||
ReconnectSequence int64 `json:"reconnect_sequence"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
ProviderProfile string `json:"provider_profile"`
|
||||
ProviderIdentity string `json:"provider_identity"`
|
||||
PolicyVersionID string `json:"policy_version_id"`
|
||||
ApplicationID string `json:"application_id"`
|
||||
ClientID string `json:"client_id"`
|
||||
ManagementHost string `json:"management_host"`
|
||||
ManagementPort int64 `json:"management_port"`
|
||||
StreamHost string `json:"stream_host"`
|
||||
StreamPort int64 `json:"stream_port"`
|
||||
ClientCertificatePem string `json:"client_certificate_pem"`
|
||||
ClientPrivateKeyPem string `json:"client_private_key_pem"`
|
||||
ServerCertificatePem string `json:"server_certificate_pem"`
|
||||
Version string `json:"version"`
|
||||
SessionID string `json:"session_id"`
|
||||
GatewayID string `json:"gateway_id"`
|
||||
ReconnectSequence int64 `json:"reconnect_sequence"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
ProviderProfile string `json:"provider_profile"`
|
||||
ProviderIdentity string `json:"provider_identity"`
|
||||
PolicyVersionID string `json:"policy_version_id"`
|
||||
ApplicationID string `json:"application_id"`
|
||||
ClientID string `json:"client_id"`
|
||||
ManagementHost string `json:"management_host"`
|
||||
ManagementPort int64 `json:"management_port"`
|
||||
StreamHost string `json:"stream_host"`
|
||||
StreamPort int64 `json:"stream_port"`
|
||||
ClientCertificatePem string `json:"client_certificate_pem"`
|
||||
ClientPrivateKeyPem string `json:"client_private_key_pem"`
|
||||
ServerCertificatePem string `json:"server_certificate_pem"`
|
||||
ClipboardPolicy ClipboardPolicy `json:"clipboard_policy"`
|
||||
ProviderApplicationTerminationAllowed bool `json:"provider_application_termination_allowed"`
|
||||
}
|
||||
|
||||
type ProviderState struct {
|
||||
@@ -1013,6 +1038,78 @@ func EncodeChannelFrame(value ChannelFrame) ([]byte, error) {
|
||||
return json.Marshal(value)
|
||||
}
|
||||
|
||||
func (v ClipboardPolicy) Validate() error {
|
||||
var violations []FieldViolation
|
||||
if v.MaxTextBytes == 0 {
|
||||
violations = append(violations, FieldViolation{Field: "max_text_bytes", Code: "required"})
|
||||
}
|
||||
if v.MaxTextBytes != 0 && v.MaxTextBytes < 1 {
|
||||
violations = append(violations, FieldViolation{Field: "max_text_bytes", Code: "minimum"})
|
||||
}
|
||||
if v.MaxTextBytes > 65536 {
|
||||
violations = append(violations, FieldViolation{Field: "max_text_bytes", Code: "maximum"})
|
||||
}
|
||||
if v.MaxUpdatesPerMinute == 0 {
|
||||
violations = append(violations, FieldViolation{Field: "max_updates_per_minute", Code: "required"})
|
||||
}
|
||||
if v.MaxUpdatesPerMinute != 0 && v.MaxUpdatesPerMinute < 1 {
|
||||
violations = append(violations, FieldViolation{Field: "max_updates_per_minute", Code: "minimum"})
|
||||
}
|
||||
if v.MaxUpdatesPerMinute > 120 {
|
||||
violations = append(violations, FieldViolation{Field: "max_updates_per_minute", Code: "maximum"})
|
||||
}
|
||||
if len(violations) > 0 {
|
||||
return ValidationError{Violations: violations}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DecodeClipboardPolicy(data []byte) (ClipboardPolicy, error) {
|
||||
var value ClipboardPolicy
|
||||
if len(data) > 1024*1024 {
|
||||
return value, errors.New("protocol payload exceeds limit")
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &fields); err != nil {
|
||||
return value, err
|
||||
}
|
||||
if raw, ok := fields["client_to_provider_enabled"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "client_to_provider_enabled", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["max_text_bytes"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "max_text_bytes", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["max_updates_per_minute"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "max_updates_per_minute", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["provider_to_client_enabled"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "provider_to_client_enabled", Code: "required"}}}
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
return value, err
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
if err == nil {
|
||||
return value, errors.New("trailing JSON value")
|
||||
}
|
||||
return value, err
|
||||
}
|
||||
if err := value.Validate(); err != nil {
|
||||
return value, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func EncodeClipboardPolicy(value ClipboardPolicy) ([]byte, error) {
|
||||
if err := value.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(value)
|
||||
}
|
||||
|
||||
func (v ClipboardText) Validate() error {
|
||||
var violations []FieldViolation
|
||||
if v.Text == "" {
|
||||
@@ -1934,6 +2031,189 @@ func EncodeFieldViolation(value FieldViolation) ([]byte, error) {
|
||||
return json.Marshal(value)
|
||||
}
|
||||
|
||||
func (v GatewayClipboardAudit) Validate() error {
|
||||
var violations []FieldViolation
|
||||
if v.Version == "" {
|
||||
violations = append(violations, FieldViolation{Field: "version", Code: "required"})
|
||||
}
|
||||
if v.Version != "1" && v.Version != "" {
|
||||
violations = append(violations, FieldViolation{Field: "version", Code: "invalid_value"})
|
||||
}
|
||||
if v.SessionID == "" {
|
||||
violations = append(violations, FieldViolation{Field: "session_id", Code: "required"})
|
||||
}
|
||||
if len(v.SessionID) < 1 && v.SessionID != "" {
|
||||
violations = append(violations, FieldViolation{Field: "session_id", Code: "min_length"})
|
||||
}
|
||||
if len(v.SessionID) > 128 {
|
||||
violations = append(violations, FieldViolation{Field: "session_id", Code: "max_length"})
|
||||
}
|
||||
if v.Direction == "" {
|
||||
violations = append(violations, FieldViolation{Field: "direction", Code: "required"})
|
||||
}
|
||||
if v.Direction != "" && !(v.Direction == "client_to_provider" || v.Direction == "provider_to_client") {
|
||||
violations = append(violations, FieldViolation{Field: "direction", Code: "invalid_value"})
|
||||
}
|
||||
if v.Outcome == "" {
|
||||
violations = append(violations, FieldViolation{Field: "outcome", Code: "required"})
|
||||
}
|
||||
if v.Outcome != "" && !(v.Outcome == "forwarded" || v.Outcome == "suppressed" || v.Outcome == "rejected") {
|
||||
violations = append(violations, FieldViolation{Field: "outcome", Code: "invalid_value"})
|
||||
}
|
||||
if v.TextBytes != 0 && v.TextBytes < 0 {
|
||||
violations = append(violations, FieldViolation{Field: "text_bytes", Code: "minimum"})
|
||||
}
|
||||
if v.TextBytes > 65536 {
|
||||
violations = append(violations, FieldViolation{Field: "text_bytes", Code: "maximum"})
|
||||
}
|
||||
if v.Reason == "" {
|
||||
violations = append(violations, FieldViolation{Field: "reason", Code: "required"})
|
||||
}
|
||||
if v.Reason != "" && !(v.Reason == "forwarded" || v.Reason == "loop" || v.Reason == "policy" || v.Reason == "rate" || v.Reason == "provider" || v.Reason == "malformed") {
|
||||
violations = append(violations, FieldViolation{Field: "reason", Code: "invalid_value"})
|
||||
}
|
||||
if len(violations) > 0 {
|
||||
return ValidationError{Violations: violations}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DecodeGatewayClipboardAudit(data []byte) (GatewayClipboardAudit, error) {
|
||||
var value GatewayClipboardAudit
|
||||
if len(data) > 1024*1024 {
|
||||
return value, errors.New("protocol payload exceeds limit")
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &fields); err != nil {
|
||||
return value, err
|
||||
}
|
||||
if raw, ok := fields["direction"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "direction", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["outcome"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "outcome", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["reason"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "reason", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["session_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "session_id", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["text_bytes"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "text_bytes", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "version", Code: "required"}}}
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
return value, err
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
if err == nil {
|
||||
return value, errors.New("trailing JSON value")
|
||||
}
|
||||
return value, err
|
||||
}
|
||||
if err := value.Validate(); err != nil {
|
||||
return value, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func EncodeGatewayClipboardAudit(value GatewayClipboardAudit) ([]byte, error) {
|
||||
if err := value.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(value)
|
||||
}
|
||||
|
||||
func (v GatewayClipboardText) Validate() error {
|
||||
var violations []FieldViolation
|
||||
if v.Direction == "" {
|
||||
violations = append(violations, FieldViolation{Field: "direction", Code: "required"})
|
||||
}
|
||||
if v.Direction != "" && !(v.Direction == "client_to_provider" || v.Direction == "provider_to_client") {
|
||||
violations = append(violations, FieldViolation{Field: "direction", Code: "invalid_value"})
|
||||
}
|
||||
if v.Text == "" {
|
||||
violations = append(violations, FieldViolation{Field: "text", Code: "required"})
|
||||
}
|
||||
if len(v.Text) > 65536 {
|
||||
violations = append(violations, FieldViolation{Field: "text", Code: "max_length"})
|
||||
}
|
||||
if v.Encoding == "" {
|
||||
violations = append(violations, FieldViolation{Field: "encoding", Code: "required"})
|
||||
}
|
||||
if v.Encoding != "utf-8" && v.Encoding != "" {
|
||||
violations = append(violations, FieldViolation{Field: "encoding", Code: "invalid_value"})
|
||||
}
|
||||
if v.LoopToken == "" {
|
||||
violations = append(violations, FieldViolation{Field: "loop_token", Code: "required"})
|
||||
}
|
||||
if len(v.LoopToken) < 16 && v.LoopToken != "" {
|
||||
violations = append(violations, FieldViolation{Field: "loop_token", Code: "min_length"})
|
||||
}
|
||||
if len(v.LoopToken) > 128 {
|
||||
violations = append(violations, FieldViolation{Field: "loop_token", Code: "max_length"})
|
||||
}
|
||||
if len(violations) > 0 {
|
||||
return ValidationError{Violations: violations}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DecodeGatewayClipboardText(data []byte) (GatewayClipboardText, error) {
|
||||
var value GatewayClipboardText
|
||||
if len(data) > 1024*1024 {
|
||||
return value, errors.New("protocol payload exceeds limit")
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &fields); err != nil {
|
||||
return value, err
|
||||
}
|
||||
if raw, ok := fields["direction"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "direction", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["encoding"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "encoding", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["loop_token"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "loop_token", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["text"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "text", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["text"]; ok && len(raw) > 65536 {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "text", Code: "max_bytes"}}}
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
return value, err
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
if err == nil {
|
||||
return value, errors.New("trailing JSON value")
|
||||
}
|
||||
return value, err
|
||||
}
|
||||
if err := value.Validate(); err != nil {
|
||||
return value, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func EncodeGatewayClipboardText(value GatewayClipboardText) ([]byte, error) {
|
||||
if err := value.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(value)
|
||||
}
|
||||
|
||||
func (v GatewayDrain) Validate() error {
|
||||
var violations []FieldViolation
|
||||
if v.Version == "" {
|
||||
@@ -3082,6 +3362,12 @@ func (v ProviderSessionWork) Validate() error {
|
||||
if len(v.ServerCertificatePem) > 32768 {
|
||||
violations = append(violations, FieldViolation{Field: "server_certificate_pem", Code: "max_length"})
|
||||
}
|
||||
if reflect.DeepEqual(v.ClipboardPolicy, ClipboardPolicy{}) {
|
||||
violations = append(violations, FieldViolation{Field: "clipboard_policy", Code: "required"})
|
||||
}
|
||||
if err := v.ClipboardPolicy.Validate(); err != nil {
|
||||
violations = append(violations, FieldViolation{Field: "clipboard_policy", Code: "invalid_object"})
|
||||
}
|
||||
if len(violations) > 0 {
|
||||
return ValidationError{Violations: violations}
|
||||
}
|
||||
@@ -3109,6 +3395,9 @@ func DecodeProviderSessionWork(data []byte) (ProviderSessionWork, error) {
|
||||
if raw, ok := fields["client_private_key_pem"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "client_private_key_pem", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["clipboard_policy"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "clipboard_policy", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["expires_at"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "expires_at", Code: "required"}}}
|
||||
}
|
||||
@@ -3124,6 +3413,9 @@ func DecodeProviderSessionWork(data []byte) (ProviderSessionWork, error) {
|
||||
if raw, ok := fields["policy_version_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "policy_version_id", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["provider_application_termination_allowed"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "provider_application_termination_allowed", Code: "required"}}}
|
||||
}
|
||||
if raw, ok := fields["provider_identity"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return value, ValidationError{Violations: []FieldViolation{{Field: "provider_identity", Code: "required"}}}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user