Protocol: split client session authority
Verify Protocol / verify (push) Successful in 1m2s
Verify Protocol / module (push) Successful in 1m45s

This commit is contained in:
sechmachine
2026-08-12 11:50:41 +07:00
parent b6a4f773e4
commit afbcea62f9
14 changed files with 536 additions and 4 deletions
+127 -1
View File
@@ -14,7 +14,7 @@ import (
"time"
)
const SchemaSHA256 = "dea3dd210c53d5a2d37050dd6afd8b0ac5bb8edcb7ab25a02e4026489ce8a00f"
const SchemaSHA256 = "762d009c3d25d80c3850d975e45f7a6b3fd8adf5c93c8fa7dd11dfa993f8bbb1"
const ProtocolVersion = "1.0.0"
const CurrentWireVersion = "2"
const NMinus1WireVersion = "1"
@@ -97,6 +97,16 @@ type ChannelFrame struct {
Payload string `json:"payload"`
}
type ClientSessionAuthority struct {
Version string `json:"version"`
SessionID string `json:"session_id"`
GatewayID string `json:"gateway_id"`
Audience string `json:"audience"`
ReconnectSequence int64 `json:"reconnect_sequence"`
ExpiresAt string `json:"expires_at"`
Capabilities CapabilityProfile `json:"capabilities"`
}
type ClipboardPolicy struct {
ClientToProviderEnabled bool `json:"client_to_provider_enabled"`
ProviderToClientEnabled bool `json:"provider_to_client_enabled"`
@@ -1233,6 +1243,122 @@ func EncodeChannelFrame(value ChannelFrame) ([]byte, error) {
return json.Marshal(value)
}
func (v ClientSessionAuthority) 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.GatewayID == "" {
violations = append(violations, FieldViolation{Field: "gateway_id", Code: "required"})
}
if len(v.GatewayID) < 1 && v.GatewayID != "" {
violations = append(violations, FieldViolation{Field: "gateway_id", Code: "min_length"})
}
if len(v.GatewayID) > 128 {
violations = append(violations, FieldViolation{Field: "gateway_id", Code: "max_length"})
}
if v.Audience == "" {
violations = append(violations, FieldViolation{Field: "audience", Code: "required"})
}
if len(v.Audience) < 1 && v.Audience != "" {
violations = append(violations, FieldViolation{Field: "audience", Code: "min_length"})
}
if len(v.Audience) > 256 {
violations = append(violations, FieldViolation{Field: "audience", Code: "max_length"})
}
if v.ReconnectSequence != 0 && v.ReconnectSequence < 0 {
violations = append(violations, FieldViolation{Field: "reconnect_sequence", Code: "minimum"})
}
if v.ExpiresAt == "" {
violations = append(violations, FieldViolation{Field: "expires_at", Code: "required"})
}
if len(v.ExpiresAt) > 64 {
violations = append(violations, FieldViolation{Field: "expires_at", Code: "max_length"})
}
if v.ExpiresAt != "" {
if parsed, err := time.Parse(time.RFC3339Nano, v.ExpiresAt); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.ExpiresAt {
violations = append(violations, FieldViolation{Field: "expires_at", Code: "invalid_time"})
}
}
if reflect.DeepEqual(v.Capabilities, CapabilityProfile{}) {
violations = append(violations, FieldViolation{Field: "capabilities", Code: "required"})
}
if err := v.Capabilities.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "capabilities", Code: "invalid_object"})
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
return nil
}
func DecodeClientSessionAuthority(data []byte) (ClientSessionAuthority, error) {
var value ClientSessionAuthority
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["audience"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "audience", Code: "required"}}}
}
if raw, ok := fields["capabilities"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "capabilities", 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"}}}
}
if raw, ok := fields["gateway_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "gateway_id", Code: "required"}}}
}
if raw, ok := fields["reconnect_sequence"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "reconnect_sequence", 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["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 EncodeClientSessionAuthority(value ClientSessionAuthority) ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
return json.Marshal(value)
}
func (v ClipboardPolicy) Validate() error {
var violations []FieldViolation
if v.MaxTextBytes == 0 {
+1 -1
View File
@@ -14,5 +14,5 @@
},
"generator_sha256": "00c1905fc611ca9e226cd90da761b48b8e203734b10542befea397a30082d360",
"protocol_version": "1.0.0",
"schema_sha256": "dea3dd210c53d5a2d37050dd6afd8b0ac5bb8edcb7ab25a02e4026489ce8a00f"
"schema_sha256": "762d009c3d25d80c3850d975e45f7a6b3fd8adf5c93c8fa7dd11dfa993f8bbb1"
}
Binary file not shown.
+44 -1
View File
@@ -1,6 +1,6 @@
// Code generated by tools/generate.py; DO NOT EDIT.
#![allow(non_snake_case)]
pub const SCHEMA_SHA256: &str = "dea3dd210c53d5a2d37050dd6afd8b0ac5bb8edcb7ab25a02e4026489ce8a00f";
pub const SCHEMA_SHA256: &str = "762d009c3d25d80c3850d975e45f7a6b3fd8adf5c93c8fa7dd11dfa993f8bbb1";
pub const CURRENT_WIRE_VERSION: &str = "2";
pub const N_MINUS_1_WIRE_VERSION: &str = "1";
pub const N_MINUS_2_WIRE_VERSION: &str = "0";
@@ -353,6 +353,49 @@ impl ChannelFrame {
pub fn payload(&self) -> &String { &self.payload }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientSessionAuthority {
version: String,
sessionId: String,
gatewayId: String,
audience: String,
reconnectSequence: i64,
expiresAt: String,
capabilities: CapabilityProfile,
}
impl ClientSessionAuthority {
pub fn new(version: String, sessionId: String, gatewayId: String, audience: String, reconnectSequence: i64, expiresAt: String, capabilities: CapabilityProfile) -> Result<Self, ValidationError> {
let value = Self { version, sessionId, gatewayId, audience, reconnectSequence, expiresAt, capabilities };
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.version != "1" { return Err(ValidationError::new("version", "invalid_value")); }
if self.sessionId.is_empty() { return Err(ValidationError::new("session_id", "required")); }
if !self.sessionId.is_empty() && self.sessionId.len() < 1 { return Err(ValidationError::new("session_id", "min_length")); }
if self.sessionId.len() > 128 { return Err(ValidationError::new("session_id", "max_length")); }
if self.gatewayId.is_empty() { return Err(ValidationError::new("gateway_id", "required")); }
if !self.gatewayId.is_empty() && self.gatewayId.len() < 1 { return Err(ValidationError::new("gateway_id", "min_length")); }
if self.gatewayId.len() > 128 { return Err(ValidationError::new("gateway_id", "max_length")); }
if self.audience.is_empty() { return Err(ValidationError::new("audience", "required")); }
if !self.audience.is_empty() && self.audience.len() < 1 { return Err(ValidationError::new("audience", "min_length")); }
if self.audience.len() > 256 { return Err(ValidationError::new("audience", "max_length")); }
if self.reconnectSequence < 0 { return Err(ValidationError::new("reconnect_sequence", "minimum")); }
if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); }
if !valid_rfc3339_utc(self.expiresAt.as_str()) { return Err(ValidationError::new("expires_at", "invalid_time")); }
self.capabilities.validate().map_err(|_| ValidationError::new("capabilities", "invalid_object"))?;
Ok(())
}
pub fn version(&self) -> &String { &self.version }
pub fn sessionId(&self) -> &String { &self.sessionId }
pub fn gatewayId(&self) -> &String { &self.gatewayId }
pub fn audience(&self) -> &String { &self.audience }
pub fn reconnectSequence(&self) -> &i64 { &self.reconnectSequence }
pub fn expiresAt(&self) -> &String { &self.expiresAt }
pub fn capabilities(&self) -> &CapabilityProfile { &self.capabilities }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClipboardPolicy {
clientToProviderEnabled: bool,
+58 -1
View File
@@ -1,7 +1,7 @@
// Code generated by tools/generate.py; DO NOT EDIT.
import Foundation
public typealias JSONObject = [String: String]
public let schemaSHA256 = "dea3dd210c53d5a2d37050dd6afd8b0ac5bb8edcb7ab25a02e4026489ce8a00f"
public let schemaSHA256 = "762d009c3d25d80c3850d975e45f7a6b3fd8adf5c93c8fa7dd11dfa993f8bbb1"
public let currentWireVersion = "2"
public let nMinus1WireVersion = "1"
public let nMinus2WireVersion = "0"
@@ -433,6 +433,63 @@ public struct ChannelFrame: Codable, Equatable {
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct ClientSessionAuthority: Codable, Equatable {
public let version: String
public let sessionId: String
public let gatewayId: String
public let audience: String
public let reconnectSequence: Int64
public let expiresAt: String
public let capabilities: CapabilityProfile
enum CodingKeys: String, CodingKey {
case version = "version"
case sessionId = "session_id"
case gatewayId = "gateway_id"
case audience = "audience"
case reconnectSequence = "reconnect_sequence"
case expiresAt = "expires_at"
case capabilities = "capabilities"
}
public init(version: String, sessionId: String, gatewayId: String, audience: String, reconnectSequence: Int64, expiresAt: String, capabilities: CapabilityProfile) throws {
self.version = version
self.sessionId = sessionId
self.gatewayId = gatewayId
self.audience = audience
self.reconnectSequence = reconnectSequence
self.expiresAt = expiresAt
self.capabilities = capabilities
try validate()
}
public init(from decoder: Decoder) throws {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(version: try c.decode(String.self, forKey: .version), sessionId: try c.decode(String.self, forKey: .sessionId), gatewayId: try c.decode(String.self, forKey: .gatewayId), audience: try c.decode(String.self, forKey: .audience), reconnectSequence: try c.decode(Int64.self, forKey: .reconnectSequence), expiresAt: try c.decode(String.self, forKey: .expiresAt), capabilities: try c.decode(CapabilityProfile.self, forKey: .capabilities))
}
public func validate() throws {
if self.version != "1" { throw ContractValidationError(field: "version", code: "invalid_value") }
if self.sessionId.isEmpty { throw ContractValidationError(field: "session_id", code: "required") }
if !self.sessionId.isEmpty && self.sessionId.utf8.count < 1 { throw ContractValidationError(field: "session_id", code: "min_length") }
if self.sessionId.utf8.count > 128 { throw ContractValidationError(field: "session_id", code: "max_length") }
if self.gatewayId.isEmpty { throw ContractValidationError(field: "gateway_id", code: "required") }
if !self.gatewayId.isEmpty && self.gatewayId.utf8.count < 1 { throw ContractValidationError(field: "gateway_id", code: "min_length") }
if self.gatewayId.utf8.count > 128 { throw ContractValidationError(field: "gateway_id", code: "max_length") }
if self.audience.isEmpty { throw ContractValidationError(field: "audience", code: "required") }
if !self.audience.isEmpty && self.audience.utf8.count < 1 { throw ContractValidationError(field: "audience", code: "min_length") }
if self.audience.utf8.count > 256 { throw ContractValidationError(field: "audience", code: "max_length") }
if self.reconnectSequence < 0 { throw ContractValidationError(field: "reconnect_sequence", code: "minimum") }
if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") }
if !validRFC3339UTC(self.expiresAt) { throw ContractValidationError(field: "expires_at", code: "invalid_time") }
try self.capabilities.validate()
}
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct ClipboardPolicy: Codable, Equatable {
public let clientToProviderEnabled: Bool
public let providerToClientEnabled: Bool