feat(protocol): bind tunnel admission to device proof
This commit is contained in:
@@ -8,10 +8,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const SchemaSHA256 = "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec"
|
const SchemaSHA256 = "6b8631bf2b2aa12b14d0bc4d136af39a632e85b3237dc5614469ba09d93f5fca"
|
||||||
const ProtocolVersion = "1.0.0"
|
const ProtocolVersion = "1.0.0"
|
||||||
const CurrentWireVersion = "1"
|
const CurrentWireVersion = "1"
|
||||||
const NMinus1WireVersion = "0"
|
const NMinus1WireVersion = "0"
|
||||||
@@ -326,6 +327,7 @@ type TunnelAdmissionRequest struct {
|
|||||||
Grant string `json:"grant"`
|
Grant string `json:"grant"`
|
||||||
ReconnectSequence int64 `json:"reconnect_sequence"`
|
ReconnectSequence int64 `json:"reconnect_sequence"`
|
||||||
ClientNonce string `json:"client_nonce"`
|
ClientNonce string `json:"client_nonce"`
|
||||||
|
DeviceSignature string `json:"device_signature"`
|
||||||
Capabilities CapabilityProfile `json:"capabilities"`
|
Capabilities CapabilityProfile `json:"capabilities"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3923,6 +3925,15 @@ func (v TunnelAdmissionRequest) Validate() error {
|
|||||||
if len(v.ClientNonce) > 128 {
|
if len(v.ClientNonce) > 128 {
|
||||||
violations = append(violations, FieldViolation{Field: "client_nonce", Code: "max_length"})
|
violations = append(violations, FieldViolation{Field: "client_nonce", Code: "max_length"})
|
||||||
}
|
}
|
||||||
|
if v.DeviceSignature == "" {
|
||||||
|
violations = append(violations, FieldViolation{Field: "device_signature", Code: "required"})
|
||||||
|
}
|
||||||
|
if len(v.DeviceSignature) < 86 && v.DeviceSignature != "" {
|
||||||
|
violations = append(violations, FieldViolation{Field: "device_signature", Code: "min_length"})
|
||||||
|
}
|
||||||
|
if len(v.DeviceSignature) > 86 {
|
||||||
|
violations = append(violations, FieldViolation{Field: "device_signature", Code: "max_length"})
|
||||||
|
}
|
||||||
if reflect.DeepEqual(v.Capabilities, CapabilityProfile{}) {
|
if reflect.DeepEqual(v.Capabilities, CapabilityProfile{}) {
|
||||||
violations = append(violations, FieldViolation{Field: "capabilities", Code: "required"})
|
violations = append(violations, FieldViolation{Field: "capabilities", Code: "required"})
|
||||||
}
|
}
|
||||||
@@ -3953,6 +3964,9 @@ func DecodeTunnelAdmissionRequest(data []byte) (TunnelAdmissionRequest, error) {
|
|||||||
if raw, ok := fields["client_nonce"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
if raw, ok := fields["client_nonce"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||||
return value, ValidationError{Violations: []FieldViolation{{Field: "client_nonce", Code: "required"}}}
|
return value, ValidationError{Violations: []FieldViolation{{Field: "client_nonce", Code: "required"}}}
|
||||||
}
|
}
|
||||||
|
if raw, ok := fields["device_signature"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||||
|
return value, ValidationError{Violations: []FieldViolation{{Field: "device_signature", Code: "required"}}}
|
||||||
|
}
|
||||||
if raw, ok := fields["gateway_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
if raw, ok := fields["gateway_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||||
return value, ValidationError{Violations: []FieldViolation{{Field: "gateway_id", Code: "required"}}}
|
return value, ValidationError{Violations: []FieldViolation{{Field: "gateway_id", Code: "required"}}}
|
||||||
}
|
}
|
||||||
@@ -4073,3 +4087,13 @@ func IntersectCapabilityProfiles(profiles ...CapabilityProfile) (CapabilityProfi
|
|||||||
}
|
}
|
||||||
return selected, nil
|
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}
|
||||||
|
var transcript strings.Builder
|
||||||
|
transcript.WriteString("versevdi/tunnel-admission/v1")
|
||||||
|
for _, field := range fields {
|
||||||
|
fmt.Fprintf(&transcript, "%d:%s", len(field), field)
|
||||||
|
}
|
||||||
|
return []byte(transcript.String())
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@
|
|||||||
"2"
|
"2"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"generator_sha256": "88535ecf2b1c926104b10b4ab56f1bc6298e1c3e75490e36ee8581a135bcded7",
|
"generator_sha256": "e9c6ee1541585fcb00dcc5e94a5a6d93dbe3a719a5c545f31e5eda268f2638ab",
|
||||||
"protocol_version": "1.0.0",
|
"protocol_version": "1.0.0",
|
||||||
"schema_sha256": "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec"
|
"schema_sha256": "6b8631bf2b2aa12b14d0bc4d136af39a632e85b3237dc5614469ba09d93f5fca"
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
+15
-3
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by tools/generate.py; DO NOT EDIT.
|
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
pub const SCHEMA_SHA256: &str = "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec";
|
pub const SCHEMA_SHA256: &str = "6b8631bf2b2aa12b14d0bc4d136af39a632e85b3237dc5614469ba09d93f5fca";
|
||||||
pub const CURRENT_WIRE_VERSION: &str = "1";
|
pub const CURRENT_WIRE_VERSION: &str = "1";
|
||||||
pub const N_MINUS_1_WIRE_VERSION: &str = "0";
|
pub const N_MINUS_1_WIRE_VERSION: &str = "0";
|
||||||
pub const N_MINUS_2_WIRE_VERSION: &str = "-1";
|
pub const N_MINUS_2_WIRE_VERSION: &str = "-1";
|
||||||
@@ -1345,12 +1345,13 @@ pub struct TunnelAdmissionRequest {
|
|||||||
grant: String,
|
grant: String,
|
||||||
reconnectSequence: i64,
|
reconnectSequence: i64,
|
||||||
clientNonce: String,
|
clientNonce: String,
|
||||||
|
deviceSignature: String,
|
||||||
capabilities: CapabilityProfile,
|
capabilities: CapabilityProfile,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TunnelAdmissionRequest {
|
impl TunnelAdmissionRequest {
|
||||||
pub fn new(version: String, sessionId: String, gatewayId: String, audience: String, grant: String, reconnectSequence: i64, clientNonce: String, capabilities: CapabilityProfile) -> Result<Self, ValidationError> {
|
pub fn new(version: String, sessionId: String, gatewayId: String, audience: String, grant: String, reconnectSequence: i64, clientNonce: String, deviceSignature: String, capabilities: CapabilityProfile) -> Result<Self, ValidationError> {
|
||||||
let value = Self { version, sessionId, gatewayId, audience, grant, reconnectSequence, clientNonce, capabilities };
|
let value = Self { version, sessionId, gatewayId, audience, grant, reconnectSequence, clientNonce, deviceSignature, capabilities };
|
||||||
value.validate()?;
|
value.validate()?;
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
@@ -1372,6 +1373,9 @@ impl TunnelAdmissionRequest {
|
|||||||
if self.clientNonce.is_empty() { return Err(ValidationError::new("client_nonce", "required")); }
|
if self.clientNonce.is_empty() { return Err(ValidationError::new("client_nonce", "required")); }
|
||||||
if !self.clientNonce.is_empty() && self.clientNonce.len() < 16 { return Err(ValidationError::new("client_nonce", "min_length")); }
|
if !self.clientNonce.is_empty() && self.clientNonce.len() < 16 { return Err(ValidationError::new("client_nonce", "min_length")); }
|
||||||
if self.clientNonce.len() > 128 { return Err(ValidationError::new("client_nonce", "max_length")); }
|
if self.clientNonce.len() > 128 { return Err(ValidationError::new("client_nonce", "max_length")); }
|
||||||
|
if self.deviceSignature.is_empty() { return Err(ValidationError::new("device_signature", "required")); }
|
||||||
|
if !self.deviceSignature.is_empty() && self.deviceSignature.len() < 86 { return Err(ValidationError::new("device_signature", "min_length")); }
|
||||||
|
if self.deviceSignature.len() > 86 { return Err(ValidationError::new("device_signature", "max_length")); }
|
||||||
self.capabilities.validate().map_err(|_| ValidationError::new("capabilities", "invalid_object"))?;
|
self.capabilities.validate().map_err(|_| ValidationError::new("capabilities", "invalid_object"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1382,7 +1386,15 @@ impl TunnelAdmissionRequest {
|
|||||||
pub fn grant(&self) -> &String { &self.grant }
|
pub fn grant(&self) -> &String { &self.grant }
|
||||||
pub fn reconnectSequence(&self) -> &i64 { &self.reconnectSequence }
|
pub fn reconnectSequence(&self) -> &i64 { &self.reconnectSequence }
|
||||||
pub fn clientNonce(&self) -> &String { &self.clientNonce }
|
pub fn clientNonce(&self) -> &String { &self.clientNonce }
|
||||||
|
pub fn deviceSignature(&self) -> &String { &self.deviceSignature }
|
||||||
pub fn capabilities(&self) -> &CapabilityProfile { &self.capabilities }
|
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 mut transcript = String::from("versevdi/tunnel-admission/v1");
|
||||||
|
for field in fields { transcript.push_str(&format!("{}:{}", field.as_bytes().len(), field)); }
|
||||||
|
transcript.into_bytes()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by tools/generate.py; DO NOT EDIT.
|
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||||
import Foundation
|
import Foundation
|
||||||
public typealias JSONObject = [String: String]
|
public typealias JSONObject = [String: String]
|
||||||
public let schemaSHA256 = "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec"
|
public let schemaSHA256 = "6b8631bf2b2aa12b14d0bc4d136af39a632e85b3237dc5614469ba09d93f5fca"
|
||||||
public let currentWireVersion = "1"
|
public let currentWireVersion = "1"
|
||||||
public let nMinus1WireVersion = "0"
|
public let nMinus1WireVersion = "0"
|
||||||
public let nMinus2WireVersion = "-1"
|
public let nMinus2WireVersion = "-1"
|
||||||
@@ -1797,6 +1797,7 @@ public struct TunnelAdmissionRequest: Codable, Equatable {
|
|||||||
public let grant: String
|
public let grant: String
|
||||||
public let reconnectSequence: Int64
|
public let reconnectSequence: Int64
|
||||||
public let clientNonce: String
|
public let clientNonce: String
|
||||||
|
public let deviceSignature: String
|
||||||
public let capabilities: CapabilityProfile
|
public let capabilities: CapabilityProfile
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case version = "version"
|
case version = "version"
|
||||||
@@ -1806,10 +1807,11 @@ public struct TunnelAdmissionRequest: Codable, Equatable {
|
|||||||
case grant = "grant"
|
case grant = "grant"
|
||||||
case reconnectSequence = "reconnect_sequence"
|
case reconnectSequence = "reconnect_sequence"
|
||||||
case clientNonce = "client_nonce"
|
case clientNonce = "client_nonce"
|
||||||
|
case deviceSignature = "device_signature"
|
||||||
case capabilities = "capabilities"
|
case capabilities = "capabilities"
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(version: String, sessionId: String, gatewayId: String, audience: String, grant: String, reconnectSequence: Int64, clientNonce: String, capabilities: CapabilityProfile) throws {
|
public init(version: String, sessionId: String, gatewayId: String, audience: String, grant: String, reconnectSequence: Int64, clientNonce: String, deviceSignature: String, capabilities: CapabilityProfile) throws {
|
||||||
self.version = version
|
self.version = version
|
||||||
self.sessionId = sessionId
|
self.sessionId = sessionId
|
||||||
self.gatewayId = gatewayId
|
self.gatewayId = gatewayId
|
||||||
@@ -1817,6 +1819,7 @@ public struct TunnelAdmissionRequest: Codable, Equatable {
|
|||||||
self.grant = grant
|
self.grant = grant
|
||||||
self.reconnectSequence = reconnectSequence
|
self.reconnectSequence = reconnectSequence
|
||||||
self.clientNonce = clientNonce
|
self.clientNonce = clientNonce
|
||||||
|
self.deviceSignature = deviceSignature
|
||||||
self.capabilities = capabilities
|
self.capabilities = capabilities
|
||||||
try validate()
|
try validate()
|
||||||
}
|
}
|
||||||
@@ -1825,7 +1828,7 @@ public struct TunnelAdmissionRequest: Codable, Equatable {
|
|||||||
let all = try decoder.container(keyedBy: AnyCodingKey.self)
|
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") }
|
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)
|
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), grant: try c.decode(String.self, forKey: .grant), reconnectSequence: try c.decode(Int64.self, forKey: .reconnectSequence), clientNonce: try c.decode(String.self, forKey: .clientNonce), capabilities: try c.decode(CapabilityProfile.self, forKey: .capabilities))
|
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), grant: try c.decode(String.self, forKey: .grant), reconnectSequence: try c.decode(Int64.self, forKey: .reconnectSequence), clientNonce: try c.decode(String.self, forKey: .clientNonce), deviceSignature: try c.decode(String.self, forKey: .deviceSignature), capabilities: try c.decode(CapabilityProfile.self, forKey: .capabilities))
|
||||||
}
|
}
|
||||||
|
|
||||||
public func validate() throws {
|
public func validate() throws {
|
||||||
@@ -1846,6 +1849,9 @@ public struct TunnelAdmissionRequest: Codable, Equatable {
|
|||||||
if self.clientNonce.isEmpty { throw ContractValidationError(field: "client_nonce", code: "required") }
|
if self.clientNonce.isEmpty { throw ContractValidationError(field: "client_nonce", code: "required") }
|
||||||
if !self.clientNonce.isEmpty && self.clientNonce.utf8.count < 16 { throw ContractValidationError(field: "client_nonce", code: "min_length") }
|
if !self.clientNonce.isEmpty && self.clientNonce.utf8.count < 16 { throw ContractValidationError(field: "client_nonce", code: "min_length") }
|
||||||
if self.clientNonce.utf8.count > 128 { throw ContractValidationError(field: "client_nonce", code: "max_length") }
|
if self.clientNonce.utf8.count > 128 { throw ContractValidationError(field: "client_nonce", code: "max_length") }
|
||||||
|
if self.deviceSignature.isEmpty { throw ContractValidationError(field: "device_signature", code: "required") }
|
||||||
|
if !self.deviceSignature.isEmpty && self.deviceSignature.utf8.count < 86 { throw ContractValidationError(field: "device_signature", code: "min_length") }
|
||||||
|
if self.deviceSignature.utf8.count > 86 { throw ContractValidationError(field: "device_signature", code: "max_length") }
|
||||||
try self.capabilities.validate()
|
try self.capabilities.validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1884,6 +1890,15 @@ public struct VersionNegotiation: Codable, Equatable {
|
|||||||
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
|
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 transcript = "versevdi/tunnel-admission/v1"
|
||||||
|
for field in fields { transcript += "\(field.utf8.count):\(field)" }
|
||||||
|
return Data(transcript.utf8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public extension CapabilityProfile {
|
public extension CapabilityProfile {
|
||||||
static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {
|
static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {
|
||||||
guard let selected = profiles.first else { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
|
guard let selected = profiles.first else { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ message TunnelAdmissionRequest {
|
|||||||
uint64 reconnect_sequence = 6;
|
uint64 reconnect_sequence = 6;
|
||||||
string client_nonce = 7;
|
string client_nonce = 7;
|
||||||
CapabilityProfile capabilities = 8;
|
CapabilityProfile capabilities = 8;
|
||||||
|
string device_signature = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SessionAuthority {
|
message SessionAuthority {
|
||||||
|
|||||||
@@ -406,7 +406,7 @@
|
|||||||
"TunnelAdmissionRequest": {
|
"TunnelAdmissionRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["version", "session_id", "gateway_id", "audience", "grant", "reconnect_sequence", "client_nonce", "capabilities"],
|
"required": ["version", "session_id", "gateway_id", "audience", "grant", "reconnect_sequence", "client_nonce", "device_signature", "capabilities"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"version": {"type": "string", "const": "1"},
|
"version": {"type": "string", "const": "1"},
|
||||||
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
@@ -415,6 +415,7 @@
|
|||||||
"grant": {"type": "string", "minLength": 43, "maxLength": 256},
|
"grant": {"type": "string", "minLength": 43, "maxLength": 256},
|
||||||
"reconnect_sequence": {"type": "integer", "minimum": 0},
|
"reconnect_sequence": {"type": "integer", "minimum": 0},
|
||||||
"client_nonce": {"type": "string", "minLength": 16, "maxLength": 128},
|
"client_nonce": {"type": "string", "minLength": 16, "maxLength": 128},
|
||||||
|
"device_signature": {"type": "string", "minLength": 86, "maxLength": 86},
|
||||||
"capabilities": {"$ref": "#/$defs/CapabilityProfile"}
|
"capabilities": {"$ref": "#/$defs/CapabilityProfile"}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,6 +74,35 @@ func TestCapabilityIntersectionRejectsNoOverlap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) {
|
||||||
|
request := protocol.TunnelAdmissionRequest{
|
||||||
|
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
|
||||||
|
Grant: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_", ReconnectSequence: 0,
|
||||||
|
ClientNonce: "0123456789abcdef", Capabilities: protocol.CapabilityProfile{
|
||||||
|
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
|
||||||
|
SourceRateControl: "server", ClientDecode: "h264-opus",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _, err := protocol.EncodeTunnelAdmissionRequest(request); err == nil {
|
||||||
|
t.Fatal("EncodeTunnelAdmissionRequest accepted an unsigned device admission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTunnelAdmissionTranscriptIsDomainSeparatedAndLengthDelimited(t *testing.T) {
|
||||||
|
request := protocol.TunnelAdmissionRequest{
|
||||||
|
Version: "1", SessionID: "session", GatewayID: "gateway", Audience: "audience",
|
||||||
|
Grant: strings.Repeat("g", 43), ReconnectSequence: 0, ClientNonce: strings.Repeat("n", 16),
|
||||||
|
DeviceSignature: strings.Repeat("s", 86), Capabilities: protocol.CapabilityProfile{
|
||||||
|
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
|
||||||
|
SourceRateControl: "server", ClientDecode: "h264-opus",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
want := "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + strings.Repeat("g", 43) + "1:016:" + strings.Repeat("n", 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus"
|
||||||
|
if got := string(request.DeviceAdmissionTranscript()); got != want {
|
||||||
|
t.Fatalf("DeviceAdmissionTranscript() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSessionAuthorityRejectsProviderRoute(t *testing.T) {
|
func TestSessionAuthorityRejectsProviderRoute(t *testing.T) {
|
||||||
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","audience":"versevdi-gateway","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"},"provider_profile":"apollo","provider_identity":"provider-1"}`
|
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","audience":"versevdi-gateway","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"},"provider_profile":"apollo","provider_identity":"provider-1"}`
|
||||||
if _, err := protocol.DecodeSessionAuthority([]byte(valid)); err != nil {
|
if _, err := protocol.DecodeSessionAuthority([]byte(valid)); err != nil {
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
|||||||
"\"errors\"",
|
"\"errors\"",
|
||||||
"\"fmt\"",
|
"\"fmt\"",
|
||||||
"\"reflect\"",
|
"\"reflect\"",
|
||||||
|
"\"strings\"",
|
||||||
"\"time\"",
|
"\"time\"",
|
||||||
")",
|
")",
|
||||||
"",
|
"",
|
||||||
@@ -255,6 +256,16 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
|||||||
"}",
|
"}",
|
||||||
"",
|
"",
|
||||||
])
|
])
|
||||||
|
out.extend([
|
||||||
|
"func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {",
|
||||||
|
"\tfields := []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}",
|
||||||
|
"\tvar transcript strings.Builder",
|
||||||
|
"\ttranscript.WriteString(\"versevdi/tunnel-admission/v1\")",
|
||||||
|
"\tfor _, field := range fields { fmt.Fprintf(&transcript, \"%d:%s\", len(field), field) }",
|
||||||
|
"\treturn []byte(transcript.String())",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
])
|
||||||
# Use io.EOF in generated code without making every generated decoder depend on
|
# Use io.EOF in generated code without making every generated decoder depend on
|
||||||
# error-string comparison; replace the deliberately compact placeholder.
|
# error-string comparison; replace the deliberately compact placeholder.
|
||||||
text = "\n".join(out).replace('"errors"\n"fmt"', '"errors"\n"fmt"\n\"io"')
|
text = "\n".join(out).replace('"errors"\n"fmt"', '"errors"\n"fmt"\n\"io"')
|
||||||
@@ -395,6 +406,16 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
|||||||
if prop_name not in required:
|
if prop_name not in required:
|
||||||
typ = f"Option<{typ}>"
|
typ = f"Option<{typ}>"
|
||||||
out.append(f" pub fn {field}(&self) -> &{typ} {{ &self.{field} }}")
|
out.append(f" pub fn {field}(&self) -> &{typ} {{ &self.{field} }}")
|
||||||
|
if name == "TunnelAdmissionRequest":
|
||||||
|
out.extend([
|
||||||
|
" 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 mut transcript = String::from(\"versevdi/tunnel-admission/v1\");",
|
||||||
|
" for field in fields { transcript.push_str(&format!(\"{}:{}\", field.as_bytes().len(), field)); }",
|
||||||
|
" transcript.into_bytes()",
|
||||||
|
" }",
|
||||||
|
])
|
||||||
out.extend(["}", ""])
|
out.extend(["}", ""])
|
||||||
out.extend([
|
out.extend([
|
||||||
"pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result<CapabilityProfile, ValidationError> {",
|
"pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result<CapabilityProfile, ValidationError> {",
|
||||||
@@ -517,6 +538,15 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
|||||||
out.extend(swift_validation(definition))
|
out.extend(swift_validation(definition))
|
||||||
out.extend([" }", "", " 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) }", "}", ""])
|
out.extend([" }", "", " 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) }", "}", ""])
|
||||||
out.extend([
|
out.extend([
|
||||||
|
"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 transcript = \"versevdi/tunnel-admission/v1\"",
|
||||||
|
" for field in fields { transcript += \"\\(field.utf8.count):\\(field)\" }",
|
||||||
|
" return Data(transcript.utf8)",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
"public extension CapabilityProfile {",
|
"public extension CapabilityProfile {",
|
||||||
" static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {",
|
" static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {",
|
||||||
" guard let selected = profiles.first else { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
" guard let selected = profiles.first else { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||||
|
|||||||
@@ -38,9 +38,13 @@ let capability = try CapabilityProfile(
|
|||||||
let request = try TunnelAdmissionRequest(
|
let request = try TunnelAdmissionRequest(
|
||||||
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
|
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
|
||||||
grant: String(repeating: "g", count: 43), reconnectSequence: 0,
|
grant: String(repeating: "g", count: 43), reconnectSequence: 0,
|
||||||
clientNonce: String(repeating: "n", count: 16), capabilities: capability
|
clientNonce: String(repeating: "n", count: 16), deviceSignature: String(repeating: "s", count: 86), capabilities: capability
|
||||||
)
|
)
|
||||||
_ = request
|
_ = request
|
||||||
|
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus"
|
||||||
|
guard String(data: request.deviceAdmissionTranscript(), encoding: .utf8) == transcript else {
|
||||||
|
fatalError("unexpected device admission transcript")
|
||||||
|
}
|
||||||
let incompatible = try CapabilityProfile(
|
let incompatible = try CapabilityProfile(
|
||||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||||
audio: "encoded", sourceRateControl: "server", clientDecode: "hevc-opus"
|
audio: "encoded", sourceRateControl: "server", clientDecode: "hevc-opus"
|
||||||
@@ -97,17 +101,25 @@ fn main() {
|
|||||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||||
"encoded".into(), "server".into(), "h264-opus".into(),
|
"encoded".into(), "server".into(), "h264-opus".into(),
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
let request = TunnelAdmissionRequest::new(
|
||||||
|
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||||
|
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
|
||||||
|
).unwrap();
|
||||||
|
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:".to_string()
|
||||||
|
+ &"g".repeat(43) + "1:016:" + &"n".repeat(16)
|
||||||
|
+ "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus";
|
||||||
|
assert_eq!(request.device_admission_transcript(), transcript.into_bytes());
|
||||||
assert!(TunnelAdmissionRequest::new(
|
assert!(TunnelAdmissionRequest::new(
|
||||||
"2".into(), "session".into(), "gateway".into(), "audience".into(),
|
"2".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||||
"g".repeat(43), 0, "n".repeat(16), capabilities.clone(),
|
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
|
||||||
).is_err());
|
).is_err());
|
||||||
assert!(TunnelAdmissionRequest::new(
|
assert!(TunnelAdmissionRequest::new(
|
||||||
"0".into(), "session".into(), "gateway".into(), "audience".into(),
|
"0".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||||
"g".repeat(43), 0, "n".repeat(16), capabilities.clone(),
|
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
|
||||||
).is_err());
|
).is_err());
|
||||||
assert!(TunnelAdmissionRequest::new(
|
assert!(TunnelAdmissionRequest::new(
|
||||||
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||||
"g".repeat(43), 0, "short".into(), capabilities.clone(),
|
"g".repeat(43), 0, "short".into(), "s".repeat(86), capabilities.clone(),
|
||||||
).is_err());
|
).is_err());
|
||||||
assert!(intersect_capability_profiles(&[capabilities.clone(), capabilities.clone()]).is_ok());
|
assert!(intersect_capability_profiles(&[capabilities.clone(), capabilities.clone()]).is_ok());
|
||||||
let incompatible = CapabilityProfile::new(
|
let incompatible = CapabilityProfile::new(
|
||||||
|
|||||||
Reference in New Issue
Block a user