feat(protocol): negotiate registered gateway profiles
Verify Protocol / verify (push) Canceled after 0s
Verify Protocol / module (push) Successful in 2m29s

This commit is contained in:
sechmachine
2026-07-30 04:47:52 +07:00
parent d26f8b60f8
commit e58f1c7c48
17 changed files with 227 additions and 67 deletions
+42 -14
View File
@@ -13,7 +13,7 @@ import (
"time"
)
const SchemaSHA256 = "fe4d6be69665f09f04e2cd89273ebc5d2dddf4cde98b94a9d8c4f726504ffe1b"
const SchemaSHA256 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
const ProtocolVersion = "1.0.0"
const CurrentWireVersion = "1"
const NMinus1WireVersion = "0"
@@ -68,12 +68,12 @@ type BrokerSession struct {
}
type CapabilityProfile struct {
Transport string `json:"transport"`
Framing string `json:"framing"`
Media string `json:"media"`
Audio string `json:"audio"`
SourceRateControl string `json:"source_rate_control"`
ClientDecode string `json:"client_decode"`
Transport string `json:"transport"`
Framing string `json:"framing"`
Media string `json:"media"`
Audio string `json:"audio"`
SourceRateControl string `json:"source_rate_control"`
ClientDecode []string `json:"client_decode"`
}
type ChannelFrame struct {
@@ -884,14 +884,26 @@ func (v CapabilityProfile) Validate() error {
if len(v.SourceRateControl) > 64 {
violations = append(violations, FieldViolation{Field: "source_rate_control", Code: "max_length"})
}
if v.ClientDecode == "" {
if v.ClientDecode == nil {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "required"})
}
if len(v.ClientDecode) < 1 && v.ClientDecode != "" {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "min_length"})
if len(v.ClientDecode) < 1 {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "min_items"})
}
if len(v.ClientDecode) > 64 {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "max_length"})
if len(v.ClientDecode) > 2 {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "max_items"})
}
for _, item := range v.ClientDecode {
if !(item == "h264-opus" || item == "hevc-opus") {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "invalid_item"})
}
}
for index, item := range v.ClientDecode {
for prior := 0; prior < index; prior++ {
if item == v.ClientDecode[prior] {
violations = append(violations, FieldViolation{Field: "client_decode", Code: "duplicate_item"})
}
}
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
@@ -4990,16 +5002,32 @@ func IntersectCapabilityProfiles(profiles ...CapabilityProfile) (CapabilityProfi
if err := selected.Validate(); err != nil {
return CapabilityProfile{}, ErrNoCapabilityOverlap
}
common := append([]string(nil), selected.ClientDecode...)
for _, profile := range profiles[1:] {
if err := profile.Validate(); err != nil || profile != selected {
if err := profile.Validate(); err != nil || profile.Transport != selected.Transport || profile.Framing != selected.Framing || profile.Media != selected.Media || profile.Audio != selected.Audio || profile.SourceRateControl != selected.SourceRateControl {
return CapabilityProfile{}, ErrNoCapabilityOverlap
}
next := common[:0]
for _, candidate := range common {
for _, offered := range profile.ClientDecode {
if candidate == offered {
next = append(next, candidate)
break
}
}
}
common = next
if len(common) == 0 {
return CapabilityProfile{}, ErrNoCapabilityOverlap
}
}
selected.ClientDecode = common
return selected, nil
}
func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {
fields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf("%d", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, v.Capabilities.ClientDecode}
fields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf("%d", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, fmt.Sprintf("%d", len(v.Capabilities.ClientDecode))}
fields = append(fields, v.Capabilities.ClientDecode...)
var transcript strings.Builder
transcript.WriteString("versevdi/tunnel-admission/v1")
for _, field := range fields {
+2 -2
View File
@@ -12,7 +12,7 @@
"2"
]
},
"generator_sha256": "922983e07a8ecc559771778fbf139155b14664742d9873be062880102777dccb",
"generator_sha256": "00fdba050eb924a54dd3d63aac0a38560341b675f0de4e3e9631ee895057a9b6",
"protocol_version": "1.0.0",
"schema_sha256": "fe4d6be69665f09f04e2cd89273ebc5d2dddf4cde98b94a9d8c4f726504ffe1b"
"schema_sha256": "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
}
Binary file not shown.
+15 -10
View File
@@ -1,6 +1,6 @@
// Code generated by tools/generate.py; DO NOT EDIT.
#![allow(non_snake_case)]
pub const SCHEMA_SHA256: &str = "fe4d6be69665f09f04e2cd89273ebc5d2dddf4cde98b94a9d8c4f726504ffe1b";
pub const SCHEMA_SHA256: &str = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716";
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";
@@ -210,11 +210,11 @@ pub struct CapabilityProfile {
media: String,
audio: String,
sourceRateControl: String,
clientDecode: String,
clientDecode: Vec<String>,
}
impl CapabilityProfile {
pub fn new(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: String) -> Result<Self, ValidationError> {
pub fn new(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: Vec<String>) -> Result<Self, ValidationError> {
let value = Self { transport, framing, media, audio, sourceRateControl, clientDecode };
value.validate()?;
Ok(value)
@@ -235,9 +235,10 @@ impl CapabilityProfile {
if self.sourceRateControl.is_empty() { return Err(ValidationError::new("source_rate_control", "required")); }
if !self.sourceRateControl.is_empty() && self.sourceRateControl.len() < 1 { return Err(ValidationError::new("source_rate_control", "min_length")); }
if self.sourceRateControl.len() > 64 { return Err(ValidationError::new("source_rate_control", "max_length")); }
if self.clientDecode.is_empty() { return Err(ValidationError::new("client_decode", "required")); }
if !self.clientDecode.is_empty() && self.clientDecode.len() < 1 { return Err(ValidationError::new("client_decode", "min_length")); }
if self.clientDecode.len() > 64 { return Err(ValidationError::new("client_decode", "max_length")); }
if self.clientDecode.len() < 1 { return Err(ValidationError::new("client_decode", "min_items")); }
if self.clientDecode.len() > 2 { return Err(ValidationError::new("client_decode", "max_items")); }
for item in self.clientDecode.iter() { if item != "h264-opus" && item != "hevc-opus" { return Err(ValidationError::new("client_decode", "invalid_item")); } }
for (index, item) in self.clientDecode.iter().enumerate() { if self.clientDecode[..index].contains(item) { return Err(ValidationError::new("client_decode", "duplicate_item")); } }
Ok(())
}
pub fn transport(&self) -> &String { &self.transport }
@@ -245,7 +246,7 @@ impl CapabilityProfile {
pub fn media(&self) -> &String { &self.media }
pub fn audio(&self) -> &String { &self.audio }
pub fn sourceRateControl(&self) -> &String { &self.sourceRateControl }
pub fn clientDecode(&self) -> &String { &self.clientDecode }
pub fn clientDecode(&self) -> &Vec<String> { &self.clientDecode }
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -1729,7 +1730,9 @@ impl TunnelAdmissionRequest {
pub fn capabilities(&self) -> &CapabilityProfile { &self.capabilities }
pub fn device_admission_transcript(&self) -> Vec<u8> {
let reconnect_sequence = self.reconnectSequence.to_string();
let fields = [&self.sessionId, &self.gatewayId, &self.audience, &self.grant, &reconnect_sequence, &self.clientNonce, &self.capabilities.transport, &self.capabilities.framing, &self.capabilities.media, &self.capabilities.audio, &self.capabilities.sourceRateControl, &self.capabilities.clientDecode];
let client_decode_count = self.capabilities.clientDecode.len().to_string();
let mut fields = vec![self.sessionId.as_str(), self.gatewayId.as_str(), self.audience.as_str(), self.grant.as_str(), reconnect_sequence.as_str(), self.clientNonce.as_str(), self.capabilities.transport.as_str(), self.capabilities.framing.as_str(), self.capabilities.media.as_str(), self.capabilities.audio.as_str(), self.capabilities.sourceRateControl.as_str(), client_decode_count.as_str()];
fields.extend(self.capabilities.clientDecode.iter().map(String::as_str));
let mut transcript = String::from("versevdi/tunnel-admission/v1");
for field in fields { transcript.push_str(&format!("{}:{}", field.as_bytes().len(), field)); }
transcript.into_bytes()
@@ -1759,11 +1762,13 @@ impl VersionNegotiation {
}
pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result<CapabilityProfile, ValidationError> {
let selected = profiles.first().ok_or_else(|| ValidationError::new("capabilities", "no_overlap"))?.clone();
let mut selected = profiles.first().ok_or_else(|| ValidationError::new("capabilities", "no_overlap"))?.clone();
selected.validate().map_err(|_| ValidationError::new("capabilities", "no_overlap"))?;
for profile in &profiles[1..] {
profile.validate().map_err(|_| ValidationError::new("capabilities", "no_overlap"))?;
if profile != &selected { return Err(ValidationError::new("capabilities", "no_overlap")); }
if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new("capabilities", "no_overlap")); }
selected.clientDecode.retain(|candidate| profile.clientDecode.contains(candidate));
if selected.clientDecode.is_empty() { return Err(ValidationError::new("capabilities", "no_overlap")); }
}
Ok(selected)
}
+15 -10
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 = "fe4d6be69665f09f04e2cd89273ebc5d2dddf4cde98b94a9d8c4f726504ffe1b"
public let schemaSHA256 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
public let currentWireVersion = "1"
public let nMinus1WireVersion = "0"
public let nMinus2WireVersion = "-1"
@@ -247,7 +247,7 @@ public struct CapabilityProfile: Codable, Equatable {
public let media: String
public let audio: String
public let sourceRateControl: String
public let clientDecode: String
public let clientDecode: [String]
enum CodingKeys: String, CodingKey {
case transport = "transport"
case framing = "framing"
@@ -257,7 +257,7 @@ public struct CapabilityProfile: Codable, Equatable {
case clientDecode = "client_decode"
}
public init(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: String) throws {
public init(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: [String]) throws {
self.transport = transport
self.framing = framing
self.media = media
@@ -271,7 +271,7 @@ public struct CapabilityProfile: Codable, Equatable {
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(transport: try c.decode(String.self, forKey: .transport), framing: try c.decode(String.self, forKey: .framing), media: try c.decode(String.self, forKey: .media), audio: try c.decode(String.self, forKey: .audio), sourceRateControl: try c.decode(String.self, forKey: .sourceRateControl), clientDecode: try c.decode(String.self, forKey: .clientDecode))
try self.init(transport: try c.decode(String.self, forKey: .transport), framing: try c.decode(String.self, forKey: .framing), media: try c.decode(String.self, forKey: .media), audio: try c.decode(String.self, forKey: .audio), sourceRateControl: try c.decode(String.self, forKey: .sourceRateControl), clientDecode: try c.decode([String].self, forKey: .clientDecode))
}
public func validate() throws {
@@ -290,9 +290,10 @@ public struct CapabilityProfile: Codable, Equatable {
if self.sourceRateControl.isEmpty { throw ContractValidationError(field: "source_rate_control", code: "required") }
if !self.sourceRateControl.isEmpty && self.sourceRateControl.utf8.count < 1 { throw ContractValidationError(field: "source_rate_control", code: "min_length") }
if self.sourceRateControl.utf8.count > 64 { throw ContractValidationError(field: "source_rate_control", code: "max_length") }
if self.clientDecode.isEmpty { throw ContractValidationError(field: "client_decode", code: "required") }
if !self.clientDecode.isEmpty && self.clientDecode.utf8.count < 1 { throw ContractValidationError(field: "client_decode", code: "min_length") }
if self.clientDecode.utf8.count > 64 { throw ContractValidationError(field: "client_decode", code: "max_length") }
if self.clientDecode.count < 1 { throw ContractValidationError(field: "client_decode", code: "min_items") }
if self.clientDecode.count > 2 { throw ContractValidationError(field: "client_decode", code: "max_items") }
for item in self.clientDecode where !["h264-opus", "hevc-opus"].contains(item) { throw ContractValidationError(field: "client_decode", code: "invalid_item") }
if Set(self.clientDecode).count != self.clientDecode.count { throw ContractValidationError(field: "client_decode", code: "duplicate_item") }
}
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
@@ -2321,7 +2322,8 @@ public struct VersionNegotiation: Codable, Equatable {
public extension TunnelAdmissionRequest {
func deviceAdmissionTranscript() -> Data {
let fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, capabilities.clientDecode]
var fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, String(capabilities.clientDecode.count)]
fields.append(contentsOf: capabilities.clientDecode)
var transcript = "versevdi/tunnel-admission/v1"
for field in fields { transcript += "\(field.utf8.count):\(field)" }
return Data(transcript.utf8)
@@ -2332,10 +2334,13 @@ public extension CapabilityProfile {
static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {
guard let selected = profiles.first else { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
try selected.validate()
var common = selected.clientDecode
for profile in profiles.dropFirst() {
try profile.validate()
if profile != selected { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
common = common.filter { profile.clientDecode.contains($0) }
if common.isEmpty { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
}
return selected
return try CapabilityProfile(transport: selected.transport, framing: selected.framing, media: selected.media, audio: selected.audio, sourceRateControl: selected.sourceRateControl, clientDecode: common)
}
}