diff --git a/gen/go/protocol/protocol.go b/gen/go/protocol/protocol.go index 3844f66..24d2017 100644 --- a/gen/go/protocol/protocol.go +++ b/gen/go/protocol/protocol.go @@ -12,7 +12,7 @@ import ( "time" ) -const SchemaSHA256 = "6b8631bf2b2aa12b14d0bc4d136af39a632e85b3237dc5614469ba09d93f5fca" +const SchemaSHA256 = "c075c6fc472ce858337c3ad6c4b2f4dcfa88446d5471afb46c2f6931cbcf1b1d" const ProtocolVersion = "1.0.0" const CurrentWireVersion = "1" const NMinus1WireVersion = "0" @@ -240,6 +240,25 @@ type PageInfo struct { NextCursor string `json:"next_cursor"` } +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"` + 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"` +} + type ProviderState struct { Version string `json:"version"` SessionID string `json:"session_id"` @@ -2917,6 +2936,230 @@ func EncodePageInfo(value PageInfo) ([]byte, error) { return json.Marshal(value) } +func (v ProviderSessionWork) 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.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 v.ProviderProfile == "" { + violations = append(violations, FieldViolation{Field: "provider_profile", Code: "required"}) + } + if v.ProviderProfile != "apollo" && v.ProviderProfile != "" { + violations = append(violations, FieldViolation{Field: "provider_profile", Code: "invalid_value"}) + } + if v.ProviderIdentity == "" { + violations = append(violations, FieldViolation{Field: "provider_identity", Code: "required"}) + } + if len(v.ProviderIdentity) < 1 && v.ProviderIdentity != "" { + violations = append(violations, FieldViolation{Field: "provider_identity", Code: "min_length"}) + } + if len(v.ProviderIdentity) > 256 { + violations = append(violations, FieldViolation{Field: "provider_identity", Code: "max_length"}) + } + if v.PolicyVersionID == "" { + violations = append(violations, FieldViolation{Field: "policy_version_id", Code: "required"}) + } + if len(v.PolicyVersionID) < 1 && v.PolicyVersionID != "" { + violations = append(violations, FieldViolation{Field: "policy_version_id", Code: "min_length"}) + } + if len(v.PolicyVersionID) > 128 { + violations = append(violations, FieldViolation{Field: "policy_version_id", Code: "max_length"}) + } + if v.ApplicationID == "" { + violations = append(violations, FieldViolation{Field: "application_id", Code: "required"}) + } + if len(v.ApplicationID) < 1 && v.ApplicationID != "" { + violations = append(violations, FieldViolation{Field: "application_id", Code: "min_length"}) + } + if len(v.ApplicationID) > 128 { + violations = append(violations, FieldViolation{Field: "application_id", Code: "max_length"}) + } + if v.ManagementHost == "" { + violations = append(violations, FieldViolation{Field: "management_host", Code: "required"}) + } + if len(v.ManagementHost) < 1 && v.ManagementHost != "" { + violations = append(violations, FieldViolation{Field: "management_host", Code: "min_length"}) + } + if len(v.ManagementHost) > 256 { + violations = append(violations, FieldViolation{Field: "management_host", Code: "max_length"}) + } + if v.ManagementPort == 0 { + violations = append(violations, FieldViolation{Field: "management_port", Code: "required"}) + } + if v.ManagementPort != 0 && v.ManagementPort < 1 { + violations = append(violations, FieldViolation{Field: "management_port", Code: "minimum"}) + } + if v.ManagementPort > 65535 { + violations = append(violations, FieldViolation{Field: "management_port", Code: "maximum"}) + } + if v.StreamHost == "" { + violations = append(violations, FieldViolation{Field: "stream_host", Code: "required"}) + } + if len(v.StreamHost) < 1 && v.StreamHost != "" { + violations = append(violations, FieldViolation{Field: "stream_host", Code: "min_length"}) + } + if len(v.StreamHost) > 256 { + violations = append(violations, FieldViolation{Field: "stream_host", Code: "max_length"}) + } + if v.StreamPort == 0 { + violations = append(violations, FieldViolation{Field: "stream_port", Code: "required"}) + } + if v.StreamPort != 0 && v.StreamPort < 1 { + violations = append(violations, FieldViolation{Field: "stream_port", Code: "minimum"}) + } + if v.StreamPort > 65535 { + violations = append(violations, FieldViolation{Field: "stream_port", Code: "maximum"}) + } + if v.ClientCertificatePem == "" { + violations = append(violations, FieldViolation{Field: "client_certificate_pem", Code: "required"}) + } + if len(v.ClientCertificatePem) < 1 && v.ClientCertificatePem != "" { + violations = append(violations, FieldViolation{Field: "client_certificate_pem", Code: "min_length"}) + } + if len(v.ClientCertificatePem) > 32768 { + violations = append(violations, FieldViolation{Field: "client_certificate_pem", Code: "max_length"}) + } + if v.ClientPrivateKeyPem == "" { + violations = append(violations, FieldViolation{Field: "client_private_key_pem", Code: "required"}) + } + if len(v.ClientPrivateKeyPem) < 1 && v.ClientPrivateKeyPem != "" { + violations = append(violations, FieldViolation{Field: "client_private_key_pem", Code: "min_length"}) + } + if len(v.ClientPrivateKeyPem) > 32768 { + violations = append(violations, FieldViolation{Field: "client_private_key_pem", Code: "max_length"}) + } + if v.ServerCertificatePem == "" { + violations = append(violations, FieldViolation{Field: "server_certificate_pem", Code: "required"}) + } + if len(v.ServerCertificatePem) < 1 && v.ServerCertificatePem != "" { + violations = append(violations, FieldViolation{Field: "server_certificate_pem", Code: "min_length"}) + } + if len(v.ServerCertificatePem) > 32768 { + violations = append(violations, FieldViolation{Field: "server_certificate_pem", Code: "max_length"}) + } + if len(violations) > 0 { + return ValidationError{Violations: violations} + } + return nil +} + +func DecodeProviderSessionWork(data []byte) (ProviderSessionWork, error) { + var value ProviderSessionWork + 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["application_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "application_id", Code: "required"}}} + } + if raw, ok := fields["client_certificate_pem"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "client_certificate_pem", Code: "required"}}} + } + 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["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["management_host"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "management_host", Code: "required"}}} + } + if raw, ok := fields["management_port"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "management_port", Code: "required"}}} + } + 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_identity"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "provider_identity", Code: "required"}}} + } + if raw, ok := fields["provider_profile"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "provider_profile", 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["server_certificate_pem"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "server_certificate_pem", 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["stream_host"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "stream_host", Code: "required"}}} + } + if raw, ok := fields["stream_port"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return value, ValidationError{Violations: []FieldViolation{{Field: "stream_port", 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 EncodeProviderSessionWork(value ProviderSessionWork) ([]byte, error) { + if err := value.Validate(); err != nil { + return nil, err + } + return json.Marshal(value) +} + func (v ProviderState) Validate() error { var violations []FieldViolation if v.Version == "" { diff --git a/gen/manifest.json b/gen/manifest.json index 39e0ecd..c7ddd05 100644 --- a/gen/manifest.json +++ b/gen/manifest.json @@ -14,5 +14,5 @@ }, "generator_sha256": "e9c6ee1541585fcb00dcc5e94a5a6d93dbe3a719a5c545f31e5eda268f2638ab", "protocol_version": "1.0.0", - "schema_sha256": "6b8631bf2b2aa12b14d0bc4d136af39a632e85b3237dc5614469ba09d93f5fca" + "schema_sha256": "c075c6fc472ce858337c3ad6c4b2f4dcfa88446d5471afb46c2f6931cbcf1b1d" } diff --git a/gen/protobuf/tunnel-v1.pb b/gen/protobuf/tunnel-v1.pb index 06c2784..9e3c607 100644 Binary files a/gen/protobuf/tunnel-v1.pb and b/gen/protobuf/tunnel-v1.pb differ diff --git a/gen/rust/protocol.rs b/gen/rust/protocol.rs index def05f2..59527ed 100644 --- a/gen/rust/protocol.rs +++ b/gen/rust/protocol.rs @@ -1,6 +1,6 @@ // Code generated by tools/generate.py; DO NOT EDIT. #![allow(non_snake_case)] -pub const SCHEMA_SHA256: &str = "6b8631bf2b2aa12b14d0bc4d136af39a632e85b3237dc5614469ba09d93f5fca"; +pub const SCHEMA_SHA256: &str = "c075c6fc472ce858337c3ad6c4b2f4dcfa88446d5471afb46c2f6931cbcf1b1d"; pub const CURRENT_WIRE_VERSION: &str = "1"; pub const N_MINUS_1_WIRE_VERSION: &str = "0"; pub const N_MINUS_2_WIRE_VERSION: &str = "-1"; @@ -982,6 +982,91 @@ impl PageInfo { pub fn nextCursor(&self) -> &String { &self.nextCursor } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProviderSessionWork { + version: String, + sessionId: String, + gatewayId: String, + reconnectSequence: i64, + expiresAt: String, + providerProfile: String, + providerIdentity: String, + policyVersionId: String, + applicationId: String, + managementHost: String, + managementPort: i64, + streamHost: String, + streamPort: i64, + clientCertificatePem: String, + clientPrivateKeyPem: String, + serverCertificatePem: String, +} + +impl ProviderSessionWork { + pub fn new(version: String, sessionId: String, gatewayId: String, reconnectSequence: i64, expiresAt: String, providerProfile: String, providerIdentity: String, policyVersionId: String, applicationId: String, managementHost: String, managementPort: i64, streamHost: String, streamPort: i64, clientCertificatePem: String, clientPrivateKeyPem: String, serverCertificatePem: String) -> Result { + let value = Self { version, sessionId, gatewayId, reconnectSequence, expiresAt, providerProfile, providerIdentity, policyVersionId, applicationId, managementHost, managementPort, streamHost, streamPort, clientCertificatePem, clientPrivateKeyPem, serverCertificatePem }; + 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.reconnectSequence < 0 { return Err(ValidationError::new("reconnect_sequence", "minimum")); } + if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); } + if self.providerProfile != "apollo" { return Err(ValidationError::new("provider_profile", "invalid_value")); } + if self.providerIdentity.is_empty() { return Err(ValidationError::new("provider_identity", "required")); } + if !self.providerIdentity.is_empty() && self.providerIdentity.len() < 1 { return Err(ValidationError::new("provider_identity", "min_length")); } + if self.providerIdentity.len() > 256 { return Err(ValidationError::new("provider_identity", "max_length")); } + if self.policyVersionId.is_empty() { return Err(ValidationError::new("policy_version_id", "required")); } + if !self.policyVersionId.is_empty() && self.policyVersionId.len() < 1 { return Err(ValidationError::new("policy_version_id", "min_length")); } + if self.policyVersionId.len() > 128 { return Err(ValidationError::new("policy_version_id", "max_length")); } + if self.applicationId.is_empty() { return Err(ValidationError::new("application_id", "required")); } + if !self.applicationId.is_empty() && self.applicationId.len() < 1 { return Err(ValidationError::new("application_id", "min_length")); } + if self.applicationId.len() > 128 { return Err(ValidationError::new("application_id", "max_length")); } + if self.managementHost.is_empty() { return Err(ValidationError::new("management_host", "required")); } + if !self.managementHost.is_empty() && self.managementHost.len() < 1 { return Err(ValidationError::new("management_host", "min_length")); } + if self.managementHost.len() > 256 { return Err(ValidationError::new("management_host", "max_length")); } + if self.managementPort < 1 { return Err(ValidationError::new("management_port", "minimum")); } + if self.managementPort > 65535 { return Err(ValidationError::new("management_port", "maximum")); } + if self.streamHost.is_empty() { return Err(ValidationError::new("stream_host", "required")); } + if !self.streamHost.is_empty() && self.streamHost.len() < 1 { return Err(ValidationError::new("stream_host", "min_length")); } + if self.streamHost.len() > 256 { return Err(ValidationError::new("stream_host", "max_length")); } + if self.streamPort < 1 { return Err(ValidationError::new("stream_port", "minimum")); } + if self.streamPort > 65535 { return Err(ValidationError::new("stream_port", "maximum")); } + if self.clientCertificatePem.is_empty() { return Err(ValidationError::new("client_certificate_pem", "required")); } + if !self.clientCertificatePem.is_empty() && self.clientCertificatePem.len() < 1 { return Err(ValidationError::new("client_certificate_pem", "min_length")); } + if self.clientCertificatePem.len() > 32768 { return Err(ValidationError::new("client_certificate_pem", "max_length")); } + if self.clientPrivateKeyPem.is_empty() { return Err(ValidationError::new("client_private_key_pem", "required")); } + if !self.clientPrivateKeyPem.is_empty() && self.clientPrivateKeyPem.len() < 1 { return Err(ValidationError::new("client_private_key_pem", "min_length")); } + if self.clientPrivateKeyPem.len() > 32768 { return Err(ValidationError::new("client_private_key_pem", "max_length")); } + if self.serverCertificatePem.is_empty() { return Err(ValidationError::new("server_certificate_pem", "required")); } + if !self.serverCertificatePem.is_empty() && self.serverCertificatePem.len() < 1 { return Err(ValidationError::new("server_certificate_pem", "min_length")); } + if self.serverCertificatePem.len() > 32768 { return Err(ValidationError::new("server_certificate_pem", "max_length")); } + Ok(()) + } + pub fn version(&self) -> &String { &self.version } + pub fn sessionId(&self) -> &String { &self.sessionId } + pub fn gatewayId(&self) -> &String { &self.gatewayId } + pub fn reconnectSequence(&self) -> &i64 { &self.reconnectSequence } + pub fn expiresAt(&self) -> &String { &self.expiresAt } + pub fn providerProfile(&self) -> &String { &self.providerProfile } + pub fn providerIdentity(&self) -> &String { &self.providerIdentity } + pub fn policyVersionId(&self) -> &String { &self.policyVersionId } + pub fn applicationId(&self) -> &String { &self.applicationId } + pub fn managementHost(&self) -> &String { &self.managementHost } + pub fn managementPort(&self) -> &i64 { &self.managementPort } + pub fn streamHost(&self) -> &String { &self.streamHost } + pub fn streamPort(&self) -> &i64 { &self.streamPort } + pub fn clientCertificatePem(&self) -> &String { &self.clientCertificatePem } + pub fn clientPrivateKeyPem(&self) -> &String { &self.clientPrivateKeyPem } + pub fn serverCertificatePem(&self) -> &String { &self.serverCertificatePem } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderState { version: String, diff --git a/gen/swift/Protocol.swift b/gen/swift/Protocol.swift index 0879d03..6a349f2 100644 --- a/gen/swift/Protocol.swift +++ b/gen/swift/Protocol.swift @@ -1,7 +1,7 @@ // Code generated by tools/generate.py; DO NOT EDIT. import Foundation public typealias JSONObject = [String: String] -public let schemaSHA256 = "6b8631bf2b2aa12b14d0bc4d136af39a632e85b3237dc5614469ba09d93f5fca" +public let schemaSHA256 = "c075c6fc472ce858337c3ad6c4b2f4dcfa88446d5471afb46c2f6931cbcf1b1d" public let currentWireVersion = "1" public let nMinus1WireVersion = "0" public let nMinus2WireVersion = "-1" @@ -1310,6 +1310,115 @@ public struct PageInfo: Codable, Equatable { public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) } } +public struct ProviderSessionWork: Codable, Equatable { + public let version: String + public let sessionId: String + public let gatewayId: String + public let reconnectSequence: Int64 + public let expiresAt: String + public let providerProfile: String + public let providerIdentity: String + public let policyVersionId: String + public let applicationId: String + public let managementHost: String + public let managementPort: Int64 + public let streamHost: String + public let streamPort: Int64 + public let clientCertificatePem: String + public let clientPrivateKeyPem: String + public let serverCertificatePem: String + enum CodingKeys: String, CodingKey { + case version = "version" + case sessionId = "session_id" + case gatewayId = "gateway_id" + case reconnectSequence = "reconnect_sequence" + case expiresAt = "expires_at" + case providerProfile = "provider_profile" + case providerIdentity = "provider_identity" + case policyVersionId = "policy_version_id" + case applicationId = "application_id" + case managementHost = "management_host" + case managementPort = "management_port" + case streamHost = "stream_host" + case streamPort = "stream_port" + case clientCertificatePem = "client_certificate_pem" + case clientPrivateKeyPem = "client_private_key_pem" + case serverCertificatePem = "server_certificate_pem" + } + + public init(version: String, sessionId: String, gatewayId: String, reconnectSequence: Int64, expiresAt: String, providerProfile: String, providerIdentity: String, policyVersionId: String, applicationId: String, managementHost: String, managementPort: Int64, streamHost: String, streamPort: Int64, clientCertificatePem: String, clientPrivateKeyPem: String, serverCertificatePem: String) throws { + self.version = version + self.sessionId = sessionId + self.gatewayId = gatewayId + self.reconnectSequence = reconnectSequence + self.expiresAt = expiresAt + self.providerProfile = providerProfile + self.providerIdentity = providerIdentity + self.policyVersionId = policyVersionId + self.applicationId = applicationId + self.managementHost = managementHost + self.managementPort = managementPort + self.streamHost = streamHost + self.streamPort = streamPort + self.clientCertificatePem = clientCertificatePem + self.clientPrivateKeyPem = clientPrivateKeyPem + self.serverCertificatePem = serverCertificatePem + 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), reconnectSequence: try c.decode(Int64.self, forKey: .reconnectSequence), expiresAt: try c.decode(String.self, forKey: .expiresAt), providerProfile: try c.decode(String.self, forKey: .providerProfile), providerIdentity: try c.decode(String.self, forKey: .providerIdentity), policyVersionId: try c.decode(String.self, forKey: .policyVersionId), applicationId: try c.decode(String.self, forKey: .applicationId), managementHost: try c.decode(String.self, forKey: .managementHost), managementPort: try c.decode(Int64.self, forKey: .managementPort), streamHost: try c.decode(String.self, forKey: .streamHost), streamPort: try c.decode(Int64.self, forKey: .streamPort), clientCertificatePem: try c.decode(String.self, forKey: .clientCertificatePem), clientPrivateKeyPem: try c.decode(String.self, forKey: .clientPrivateKeyPem), serverCertificatePem: try c.decode(String.self, forKey: .serverCertificatePem)) + } + + 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.reconnectSequence < 0 { throw ContractValidationError(field: "reconnect_sequence", code: "minimum") } + if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") } + if ISO8601DateFormatter().date(from: self.expiresAt) == nil { throw ContractValidationError(field: "expires_at", code: "invalid_time") } + if self.providerProfile != "apollo" { throw ContractValidationError(field: "provider_profile", code: "invalid_value") } + if self.providerIdentity.isEmpty { throw ContractValidationError(field: "provider_identity", code: "required") } + if !self.providerIdentity.isEmpty && self.providerIdentity.utf8.count < 1 { throw ContractValidationError(field: "provider_identity", code: "min_length") } + if self.providerIdentity.utf8.count > 256 { throw ContractValidationError(field: "provider_identity", code: "max_length") } + if self.policyVersionId.isEmpty { throw ContractValidationError(field: "policy_version_id", code: "required") } + if !self.policyVersionId.isEmpty && self.policyVersionId.utf8.count < 1 { throw ContractValidationError(field: "policy_version_id", code: "min_length") } + if self.policyVersionId.utf8.count > 128 { throw ContractValidationError(field: "policy_version_id", code: "max_length") } + if self.applicationId.isEmpty { throw ContractValidationError(field: "application_id", code: "required") } + if !self.applicationId.isEmpty && self.applicationId.utf8.count < 1 { throw ContractValidationError(field: "application_id", code: "min_length") } + if self.applicationId.utf8.count > 128 { throw ContractValidationError(field: "application_id", code: "max_length") } + if self.managementHost.isEmpty { throw ContractValidationError(field: "management_host", code: "required") } + if !self.managementHost.isEmpty && self.managementHost.utf8.count < 1 { throw ContractValidationError(field: "management_host", code: "min_length") } + if self.managementHost.utf8.count > 256 { throw ContractValidationError(field: "management_host", code: "max_length") } + if self.managementPort < 1 { throw ContractValidationError(field: "management_port", code: "minimum") } + if self.managementPort > 65535 { throw ContractValidationError(field: "management_port", code: "maximum") } + if self.streamHost.isEmpty { throw ContractValidationError(field: "stream_host", code: "required") } + if !self.streamHost.isEmpty && self.streamHost.utf8.count < 1 { throw ContractValidationError(field: "stream_host", code: "min_length") } + if self.streamHost.utf8.count > 256 { throw ContractValidationError(field: "stream_host", code: "max_length") } + if self.streamPort < 1 { throw ContractValidationError(field: "stream_port", code: "minimum") } + if self.streamPort > 65535 { throw ContractValidationError(field: "stream_port", code: "maximum") } + if self.clientCertificatePem.isEmpty { throw ContractValidationError(field: "client_certificate_pem", code: "required") } + if !self.clientCertificatePem.isEmpty && self.clientCertificatePem.utf8.count < 1 { throw ContractValidationError(field: "client_certificate_pem", code: "min_length") } + if self.clientCertificatePem.utf8.count > 32768 { throw ContractValidationError(field: "client_certificate_pem", code: "max_length") } + if self.clientPrivateKeyPem.isEmpty { throw ContractValidationError(field: "client_private_key_pem", code: "required") } + if !self.clientPrivateKeyPem.isEmpty && self.clientPrivateKeyPem.utf8.count < 1 { throw ContractValidationError(field: "client_private_key_pem", code: "min_length") } + if self.clientPrivateKeyPem.utf8.count > 32768 { throw ContractValidationError(field: "client_private_key_pem", code: "max_length") } + if self.serverCertificatePem.isEmpty { throw ContractValidationError(field: "server_certificate_pem", code: "required") } + if !self.serverCertificatePem.isEmpty && self.serverCertificatePem.utf8.count < 1 { throw ContractValidationError(field: "server_certificate_pem", code: "min_length") } + if self.serverCertificatePem.utf8.count > 32768 { throw ContractValidationError(field: "server_certificate_pem", code: "max_length") } + } + + 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 ProviderState: Codable, Equatable { public let version: String public let sessionId: String diff --git a/proto/versevdi/tunnel/v1/tunnel.proto b/proto/versevdi/tunnel/v1/tunnel.proto index 2d2a5fc..cbe0888 100644 --- a/proto/versevdi/tunnel/v1/tunnel.proto +++ b/proto/versevdi/tunnel/v1/tunnel.proto @@ -117,6 +117,25 @@ message SessionAuthority { string provider_identity = 9; } +message ProviderSessionWork { + string version = 1; + string session_id = 2; + string gateway_id = 3; + uint64 reconnect_sequence = 4; + google.protobuf.Timestamp expires_at = 5; + string provider_profile = 6; + string provider_identity = 7; + string policy_version_id = 8; + string application_id = 9; + string management_host = 10; + uint32 management_port = 11; + string stream_host = 12; + uint32 stream_port = 13; + string client_certificate_pem = 14; + string client_private_key_pem = 15; + string server_certificate_pem = 16; +} + message ChannelFrame { string version = 1; string flow_id = 2; diff --git a/schemas/control-v1.schema.json b/schemas/control-v1.schema.json index 114ed14..425bcc3 100644 --- a/schemas/control-v1.schema.json +++ b/schemas/control-v1.schema.json @@ -435,6 +435,29 @@ "provider_identity": {"type": "string", "minLength": 1, "maxLength": 256} } }, + "ProviderSessionWork": { + "type": "object", + "additionalProperties": false, + "required": ["version", "session_id", "gateway_id", "reconnect_sequence", "expires_at", "provider_profile", "provider_identity", "policy_version_id", "application_id", "management_host", "management_port", "stream_host", "stream_port", "client_certificate_pem", "client_private_key_pem", "server_certificate_pem"], + "properties": { + "version": {"type": "string", "const": "1"}, + "session_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "gateway_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "reconnect_sequence": {"type": "integer", "minimum": 0}, + "expires_at": {"type": "string", "format": "date-time", "maxLength": 64}, + "provider_profile": {"type": "string", "const": "apollo"}, + "provider_identity": {"type": "string", "minLength": 1, "maxLength": 256}, + "policy_version_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "application_id": {"type": "string", "minLength": 1, "maxLength": 128}, + "management_host": {"type": "string", "minLength": 1, "maxLength": 256}, + "management_port": {"type": "integer", "minimum": 1, "maximum": 65535}, + "stream_host": {"type": "string", "minLength": 1, "maxLength": 256}, + "stream_port": {"type": "integer", "minimum": 1, "maximum": 65535}, + "client_certificate_pem": {"type": "string", "minLength": 1, "maxLength": 32768}, + "client_private_key_pem": {"type": "string", "minLength": 1, "maxLength": 32768}, + "server_certificate_pem": {"type": "string", "minLength": 1, "maxLength": 32768} + } + }, "ChannelFrame": { "type": "object", "additionalProperties": false, diff --git a/tests/go/protocol_test.go b/tests/go/protocol_test.go index 1ae4668..cf402aa 100644 --- a/tests/go/protocol_test.go +++ b/tests/go/protocol_test.go @@ -112,3 +112,13 @@ func TestSessionAuthorityRejectsProviderRoute(t *testing.T) { t.Fatal("session authority accepted a provider route") } } + +func TestProviderSessionWorkIsStrictAndSessionBound(t *testing.T) { + valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","provider_profile":"apollo","provider_identity":"provider-1","policy_version_id":"policy-1","application_id":"42","management_host":"apollo.test","management_port":47990,"stream_host":"apollo.test","stream_port":47984,"client_certificate_pem":"certificate","client_private_key_pem":"private-key","server_certificate_pem":"server-certificate"}` + if _, err := protocol.DecodeProviderSessionWork([]byte(valid)); err != nil { + t.Fatalf("valid provider work rejected: %v", err) + } + if _, err := protocol.DecodeProviderSessionWork([]byte(strings.Replace(valid, `"application_id":"42"`, `"application_id":"42","management_password":"forbidden"`, 1))); err == nil { + t.Fatal("provider work accepted a management credential") + } +}