feat(protocol): define native session credentials
This commit is contained in:
+51
-1
@@ -150,6 +150,12 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
|
||||
if "maxItems" in prop:
|
||||
lines.append(f"\tif len(v.{field}) > {prop['maxItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_items\"}}) }}")
|
||||
items = prop.get("items", {})
|
||||
if items.get("type") == "string" and "minLength" in items:
|
||||
lines.append(f"\tfor _, item := range v.{field} {{ if len(item) < {items['minLength']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"min_item_length\"}}) }} }}")
|
||||
if items.get("type") == "string" and "maxLength" in items:
|
||||
lines.append(f"\tfor _, item := range v.{field} {{ if len(item) > {items['maxLength']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_item_length\"}}) }} }}")
|
||||
if items.get("type") == "string" and "x-max-bytes" in items:
|
||||
lines.append(f"\tfor _, item := range v.{field} {{ if len(item) > {items['x-max-bytes']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_item_bytes\"}}) }} }}")
|
||||
if "enum" in items:
|
||||
allowed = " || ".join(f'item == "{value}"' for value in items["enum"])
|
||||
lines.append(f"\tfor _, item := range v.{field} {{ if !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_item\"}}) }} }}")
|
||||
@@ -359,6 +365,8 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
|
||||
if "enum" in prop:
|
||||
allowed = " && ".join(f'{value} != \"{item}\"' for item in prop["enum"])
|
||||
lines.append(f" {prefix}if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_value\")); }}")
|
||||
if prop.get("format") == "date-time":
|
||||
lines.append(f" {prefix}if !valid_rfc3339_utc({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_time\")); }}")
|
||||
if prop.get("format") == "base64url":
|
||||
lines.append(f" {prefix}if !valid_base64_url({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_format\")); }}")
|
||||
if prop.get("type") == "integer":
|
||||
@@ -372,6 +380,12 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
|
||||
if "maxItems" in prop:
|
||||
lines.append(f" {prefix}if {value}.len() > {prop['maxItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_items\")); }}")
|
||||
items = prop.get("items", {})
|
||||
if items.get("type") == "string" and "minLength" in items:
|
||||
lines.append(f" {prefix}for item in {value}.iter() {{ if item.as_bytes().len() < {items['minLength']} {{ return Err(ValidationError::new(\"{prop_name}\", \"min_item_length\")); }} }}")
|
||||
if items.get("type") == "string" and "maxLength" in items:
|
||||
lines.append(f" {prefix}for item in {value}.iter() {{ if item.as_bytes().len() > {items['maxLength']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_item_length\")); }} }}")
|
||||
if items.get("type") == "string" and "x-max-bytes" in items:
|
||||
lines.append(f" {prefix}for item in {value}.iter() {{ if item.as_bytes().len() > {items['x-max-bytes']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_item_bytes\")); }} }}")
|
||||
if "enum" in items:
|
||||
allowed = " && ".join(f'item != \"{item}\"' for item in items["enum"])
|
||||
lines.append(f" {prefix}for item in {value}.iter() {{ if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_item\")); }} }}")
|
||||
@@ -429,6 +443,19 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
||||
" _ => false,",
|
||||
" }",
|
||||
"}",
|
||||
"fn valid_rfc3339_utc(value: &str) -> bool {",
|
||||
" let bytes = value.as_bytes();",
|
||||
" if bytes.len() < 20 || bytes.len() > 30 || bytes[4] != b'-' || bytes[7] != b'-' || bytes[10] != b'T' || bytes[13] != b':' || bytes[16] != b':' || *bytes.last().unwrap() != b'Z' { return false; }",
|
||||
" let digits = |start: usize, end: usize| -> Option<u32> { bytes.get(start..end)?.iter().try_fold(0u32, |value, byte| if byte.is_ascii_digit() { Some(value * 10 + u32::from(*byte - b'0')) } else { None }) };",
|
||||
" let (year, month, day, hour, minute, second) = match (digits(0, 4), digits(5, 7), digits(8, 10), digits(11, 13), digits(14, 16), digits(17, 19)) { (Some(year), Some(month), Some(day), Some(hour), Some(minute), Some(second)) => (year, month, day, hour, minute, second), _ => return false };",
|
||||
" if hour > 23 || minute > 59 || second > 59 { return false; }",
|
||||
" let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);",
|
||||
" let days = match month { 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, 4 | 6 | 9 | 11 => 30, 2 if leap => 29, 2 => 28, _ => return false };",
|
||||
" if day == 0 || day > days { return false; }",
|
||||
" if bytes.len() == 20 { return true; }",
|
||||
" let fraction = &bytes[20..bytes.len() - 1];",
|
||||
" bytes[19] == b'.' && !fraction.is_empty() && fraction.len() <= 9 && fraction.iter().all(u8::is_ascii_digit) && *fraction.last().unwrap() != b'0'",
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
for name in sorted(defs):
|
||||
@@ -524,7 +551,7 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
|
||||
allowed = ", ".join(f'\"{item}\"' for item in prop["enum"])
|
||||
lines.append(f" {prefix}if ![{allowed}].contains({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_value\") }}")
|
||||
if prop.get("format") == "date-time":
|
||||
lines.append(f" {prefix}if ISO8601DateFormatter().date(from: {value}) == nil {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_time\") }}")
|
||||
lines.append(f" {prefix}if !validRFC3339UTC({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_time\") }}")
|
||||
if prop.get("format") == "base64url":
|
||||
lines.append(f" {prefix}if !validBase64URL({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_format\") }}")
|
||||
if prop.get("type") == "integer":
|
||||
@@ -538,6 +565,12 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
|
||||
if "maxItems" in prop:
|
||||
lines.append(f" {prefix}if {value}.count > {prop['maxItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_items\") }}")
|
||||
items = prop.get("items", {})
|
||||
if items.get("type") == "string" and "minLength" in items:
|
||||
lines.append(f" {prefix}for item in {value} where item.utf8.count < {items['minLength']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"min_item_length\") }}")
|
||||
if items.get("type") == "string" and "maxLength" in items:
|
||||
lines.append(f" {prefix}for item in {value} where item.utf8.count > {items['maxLength']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_item_length\") }}")
|
||||
if items.get("type") == "string" and "x-max-bytes" in items:
|
||||
lines.append(f" {prefix}for item in {value} where item.utf8.count > {items['x-max-bytes']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_item_bytes\") }}")
|
||||
if "enum" in items:
|
||||
allowed = ", ".join(f'\"{item}\"' for item in items["enum"])
|
||||
lines.append(f" {prefix}for item in {value} where ![{allowed}].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_item\") }}")
|
||||
@@ -581,6 +614,23 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
" guard let decoded = Data(base64Encoded: standard) else { return false }",
|
||||
" return decoded.base64EncodedString().replacingOccurrences(of: \"+\", with: \"-\").replacingOccurrences(of: \"/\", with: \"_\").replacingOccurrences(of: \"=\", with: \"\") == value",
|
||||
"}",
|
||||
"private func validRFC3339UTC(_ value: String) -> Bool {",
|
||||
" let bytes = Array(value.utf8)",
|
||||
" guard (20...30).contains(bytes.count), bytes[4] == 45, bytes[7] == 45, bytes[10] == 84, bytes[13] == 58, bytes[16] == 58, bytes.last == 90 else { return false }",
|
||||
" func digits(_ range: Range<Int>) -> Int? {",
|
||||
" var result = 0",
|
||||
" for index in range { guard bytes[index] >= 48 && bytes[index] <= 57 else { return nil }; result = result * 10 + Int(bytes[index] - 48) }",
|
||||
" return result",
|
||||
" }",
|
||||
" guard let year = digits(0..<4), let month = digits(5..<7), let day = digits(8..<10), let hour = digits(11..<13), let minute = digits(14..<16), let second = digits(17..<19), hour <= 23, minute <= 59, second <= 59 else { return false }",
|
||||
" let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)",
|
||||
" let days: Int",
|
||||
" switch month { case 1, 3, 5, 7, 8, 10, 12: days = 31; case 4, 6, 9, 11: days = 30; case 2: days = leap ? 29 : 28; default: return false }",
|
||||
" guard day > 0 && day <= days else { return false }",
|
||||
" if bytes.count == 20 { return true }",
|
||||
" let fraction = bytes[20..<(bytes.count - 1)]",
|
||||
" return bytes[19] == 46 && !fraction.isEmpty && fraction.count <= 9 && fraction.allSatisfy { $0 >= 48 && $0 <= 57 } && fraction.last != 48",
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
for name in sorted(defs):
|
||||
|
||||
@@ -38,7 +38,7 @@ func main() {
|
||||
if len(fields) != 5 {
|
||||
panic("invalid fixture row")
|
||||
}
|
||||
actual := evaluate(fields[2], fields[3])
|
||||
actual := evaluate(fields[1], fields[2], fields[3])
|
||||
if actual != fields[4] {
|
||||
panic(fmt.Sprintf("%s: got %s want %s", fields[0], actual, fields[4]))
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func main() {
|
||||
fmt.Printf("Go conformance passed normalized=%s fixtures=%s\n", normalizedDigest(results), fixtureHash)
|
||||
}
|
||||
|
||||
func evaluate(kind, input string) string {
|
||||
func evaluate(version, kind, input string) string {
|
||||
parts := map[string]string{}
|
||||
for _, item := range strings.Split(input, ";") {
|
||||
pair := strings.SplitN(item, "=", 2)
|
||||
@@ -59,7 +59,7 @@ func evaluate(kind, input string) string {
|
||||
}
|
||||
switch kind {
|
||||
case "version":
|
||||
if input == "1" || input == "0" || input == "-1" {
|
||||
if input == "2" || input == "1" || input == "0" {
|
||||
return "valid"
|
||||
}
|
||||
return "invalid:unsupported_version"
|
||||
@@ -81,7 +81,7 @@ func evaluate(kind, input string) string {
|
||||
Version: parts["version"], Purpose: parts["purpose"], SessionID: "session-1",
|
||||
ReconnectSequence: 0,
|
||||
Gateway: protocol.ManifestGateway{
|
||||
ID: parts["gateway_id"], Addresses: []string{"gateway.control.test:443"}, PublicIdentity: parts["gateway_id"],
|
||||
ID: parts["gateway_id"], Addresses: []string{"gateway.control.test:443"}, PublicIdentity: parts["public_identity"],
|
||||
},
|
||||
Tunnel: protocol.ManifestTunnel{Versions: []string{parts["protocol"] + "/1"}, Features: []string{"control.v1"}},
|
||||
Profile: protocol.ManifestProfile{ID: "standard", Bounds: protocol.ManifestBounds{MinimumKbps: 1, TargetKbps: 2, MaximumKbps: 3}},
|
||||
@@ -99,6 +99,60 @@ func evaluate(kind, input string) string {
|
||||
return "valid"
|
||||
}
|
||||
return "invalid:unsupported_clipboard"
|
||||
case "session_request":
|
||||
if version != "2" {
|
||||
return "invalid:unsupported_version"
|
||||
}
|
||||
if _, supplied := parts["policy_snapshot"]; supplied {
|
||||
return "invalid:forbidden_field"
|
||||
}
|
||||
value := protocol.SessionRequest{
|
||||
ClientDeviceID: parts["client_device_id"], DeviceKeyID: parts["device_key_id"],
|
||||
PoolID: parts["pool_id"], IdempotencyKey: parts["idempotency_key"],
|
||||
}
|
||||
if value.Validate() == nil {
|
||||
return "valid"
|
||||
}
|
||||
return "invalid:required"
|
||||
case "browser_authenticated_session":
|
||||
if _, hasDevice := parts["client_device_id"]; hasDevice {
|
||||
return "invalid:forbidden_field"
|
||||
}
|
||||
if _, hasKey := parts["device_key_id"]; hasKey {
|
||||
return "invalid:forbidden_field"
|
||||
}
|
||||
value := protocol.BrowserAuthenticatedSession{
|
||||
Username: parts["username"], Provider: parts["provider"], Roles: []string{parts["roles"]},
|
||||
Role: parts["role"],
|
||||
}
|
||||
if value.Validate() == nil {
|
||||
return "valid"
|
||||
}
|
||||
return "invalid:invalid_session"
|
||||
case "native_authenticated_session":
|
||||
clientDeviceID, hasDevice := parts["client_device_id"]
|
||||
deviceKeyID, hasKey := parts["device_key_id"]
|
||||
if !hasDevice || !hasKey {
|
||||
return "invalid:required"
|
||||
}
|
||||
value := protocol.NativeAuthenticatedSession{
|
||||
Username: parts["username"], Provider: parts["provider"], Roles: []string{parts["roles"]}, Role: parts["role"],
|
||||
NativeIdentity: protocol.NativeSessionIdentity{ClientDeviceID: clientDeviceID, DeviceKeyID: deviceKeyID},
|
||||
}
|
||||
if value.Validate() == nil {
|
||||
return "valid"
|
||||
}
|
||||
return "invalid:invalid_session"
|
||||
case "native_tunnel_credential":
|
||||
value := protocol.NativeTunnelCredential{
|
||||
ClientDeviceID: parts["client_device_id"], DeviceKeyID: parts["device_key_id"],
|
||||
CertificateChainPem: parts["certificate_chain_pem"], TrustBundlePem: parts["trust_bundle_pem"],
|
||||
ExpiresAt: parts["expires_at"],
|
||||
}
|
||||
if value.Validate() == nil {
|
||||
return "valid"
|
||||
}
|
||||
return "invalid:invalid_credential"
|
||||
case "event":
|
||||
sequence, sequenceErr := strconv.ParseInt(parts["sequence"], 10, 64)
|
||||
payloadBytes, payloadErr := strconv.Atoi(parts["payload_bytes"])
|
||||
@@ -126,8 +180,8 @@ func evaluate(kind, input string) string {
|
||||
return "valid"
|
||||
case "tunnel":
|
||||
feature := parts["feature"]
|
||||
registered := feature == "control.v1" || feature == "display.request.v1" || feature == "input.absolute.v1" || feature == "input.scroll.v1"
|
||||
if (parts["offered"] == "1" || parts["offered"] == "0" || parts["offered"] == "-1") && registered {
|
||||
registered := feature == "control.v1" || feature == "control.v2" || feature == "display.request.v1" || feature == "input.absolute.v1" || feature == "input.scroll.v1"
|
||||
if (parts["offered"] == "2" || parts["offered"] == "1" || parts["offered"] == "0") && registered {
|
||||
return "valid"
|
||||
}
|
||||
if !registered {
|
||||
|
||||
@@ -9,10 +9,10 @@ fn values(input: &str) -> std::collections::BTreeMap<String, String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn evaluate(kind: &str, input: &str) -> &'static str {
|
||||
fn evaluate(version: &str, kind: &str, input: &str) -> &'static str {
|
||||
let values = values(input);
|
||||
match kind {
|
||||
"version" if matches!(input, "1" | "0" | "-1") => "valid",
|
||||
"version" if matches!(input, "2" | "1" | "0") => "valid",
|
||||
"version" => "invalid:unsupported_version",
|
||||
"page" => match values.get("limit").and_then(|value| value.parse::<i64>().ok()) {
|
||||
Some(limit) if (1..=100).contains(&limit) => "valid",
|
||||
@@ -24,12 +24,62 @@ fn evaluate(kind: &str, input: &str) -> &'static str {
|
||||
"manifest"
|
||||
if values.get("version").map(String::as_str) == Some("1")
|
||||
&& values.contains_key("gateway_id")
|
||||
&& values.contains_key("public_identity")
|
||||
&& values.get("grant").map_or(false, |value| value.len() >= 43)
|
||||
&& values.get("purpose").map(String::as_str) == Some("launch") => "valid",
|
||||
"manifest" => "invalid:invalid_manifest",
|
||||
"clipboard" if values.get("encoding").map(String::as_str) == Some("utf-8")
|
||||
&& !values.contains_key("file") => "valid",
|
||||
"clipboard" => "invalid:unsupported_clipboard",
|
||||
"session_request" if version != "2" => "invalid:unsupported_version",
|
||||
"session_request" if values.contains_key("policy_snapshot") => "invalid:forbidden_field",
|
||||
"session_request" => match SessionRequest::new(
|
||||
values.get("client_device_id").cloned().unwrap_or_default(),
|
||||
values.get("device_key_id").cloned().unwrap_or_default(),
|
||||
values.get("pool_id").cloned().unwrap_or_default(),
|
||||
values.get("idempotency_key").cloned().unwrap_or_default(),
|
||||
None,
|
||||
) {
|
||||
Ok(_) => "valid",
|
||||
Err(_) => "invalid:required",
|
||||
},
|
||||
"browser_authenticated_session" if values.contains_key("client_device_id") || values.contains_key("device_key_id") => "invalid:forbidden_field",
|
||||
"browser_authenticated_session" => match BrowserAuthenticatedSession::new(
|
||||
values.get("username").cloned().unwrap_or_default(),
|
||||
values.get("provider").cloned().unwrap_or_default(),
|
||||
vec![values.get("roles").cloned().unwrap_or_default()],
|
||||
values.get("role").cloned().unwrap_or_default(),
|
||||
) {
|
||||
Ok(_) => "valid",
|
||||
Err(_) => "invalid:invalid_session",
|
||||
},
|
||||
"native_authenticated_session" if !values.contains_key("client_device_id") || !values.contains_key("device_key_id") => "invalid:required",
|
||||
"native_authenticated_session" => {
|
||||
let identity = match NativeSessionIdentity::new(values["client_device_id"].clone(), values["device_key_id"].clone()) {
|
||||
Ok(identity) => identity,
|
||||
Err(_) => return "invalid:required",
|
||||
};
|
||||
match NativeAuthenticatedSession::new(
|
||||
values.get("username").cloned().unwrap_or_default(),
|
||||
values.get("provider").cloned().unwrap_or_default(),
|
||||
vec![values.get("roles").cloned().unwrap_or_default()],
|
||||
values.get("role").cloned().unwrap_or_default(),
|
||||
identity,
|
||||
) {
|
||||
Ok(_) => "valid",
|
||||
Err(_) => "invalid:invalid_session",
|
||||
}
|
||||
}
|
||||
"native_tunnel_credential" => match NativeTunnelCredential::new(
|
||||
values.get("client_device_id").cloned().unwrap_or_default(),
|
||||
values.get("device_key_id").cloned().unwrap_or_default(),
|
||||
values.get("certificate_chain_pem").cloned().unwrap_or_default(),
|
||||
values.get("trust_bundle_pem").cloned().unwrap_or_default(),
|
||||
values.get("expires_at").cloned().unwrap_or_default(),
|
||||
) {
|
||||
Ok(_) => "valid",
|
||||
Err(_) => "invalid:invalid_credential",
|
||||
},
|
||||
"event" if values.get("version").map(String::as_str) != Some("1") => {
|
||||
"invalid:unsupported_version"
|
||||
}
|
||||
@@ -46,9 +96,9 @@ fn evaluate(kind: &str, input: &str) -> &'static str {
|
||||
"event" if values.get("sequence").and_then(|value| value.parse::<i64>().ok()).map_or(true, |sequence| sequence < 1)
|
||||
|| !values.contains_key("correlation_id") => "invalid:required",
|
||||
"event" => "valid",
|
||||
"tunnel" if matches!(values.get("offered").map(String::as_str), Some("1") | Some("0") | Some("-1"))
|
||||
&& matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.v1")) => "valid",
|
||||
"tunnel" if !matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.v1")) => {
|
||||
"tunnel" if matches!(values.get("offered").map(String::as_str), Some("2") | Some("1") | Some("0"))
|
||||
&& matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("control.v2") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.v1")) => "valid",
|
||||
"tunnel" if !matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("control.v2") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.v1")) => {
|
||||
"invalid:unsupported_feature"
|
||||
}
|
||||
"tunnel" => "invalid:unsupported_version",
|
||||
@@ -283,7 +333,7 @@ fn main() {
|
||||
for line in lines {
|
||||
let fields: Vec<&str> = line.split('\t').collect();
|
||||
assert_eq!(fields.len(), 5);
|
||||
let actual = evaluate(fields[2], fields[3]);
|
||||
let actual = evaluate(fields[1], fields[2], fields[3]);
|
||||
assert_eq!(actual, fields[4], "{}", fields[0]);
|
||||
results.push(format!("{}\t{}", fields[0], actual));
|
||||
}
|
||||
|
||||
@@ -9,17 +9,49 @@ func values(_ input: String) -> [String: String] {
|
||||
return result
|
||||
}
|
||||
|
||||
func evaluate(_ kind: String, _ input: String) -> String {
|
||||
func evaluate(_ version: String, _ kind: String, _ input: String) -> String {
|
||||
let values = values(input)
|
||||
switch kind {
|
||||
case "version": return ["1", "0", "-1"].contains(input) ? "valid" : "invalid:unsupported_version"
|
||||
case "version": return ["2", "1", "0"].contains(input) ? "valid" : "invalid:unsupported_version"
|
||||
case "page":
|
||||
guard let raw = values["limit"], let limit = Int(raw), (1...100).contains(limit) else { return "invalid:invalid_limit" }
|
||||
return "valid"
|
||||
case "manifest":
|
||||
for key in ["provider_url", "vm_address", "password", "private_key"] where values[key] != nil { return "invalid:forbidden_field" }
|
||||
return values["version"] == "1" && values["gateway_id"] != nil && (values["grant"]?.utf8.count ?? 0) >= 43 && values["purpose"] == "launch" ? "valid" : "invalid:invalid_manifest"
|
||||
return values["version"] == "1" && values["gateway_id"] != nil && values["public_identity"] != nil && (values["grant"]?.utf8.count ?? 0) >= 43 && values["purpose"] == "launch" ? "valid" : "invalid:invalid_manifest"
|
||||
case "clipboard": return values["encoding"] == "utf-8" && values["file"] == nil ? "valid" : "invalid:unsupported_clipboard"
|
||||
case "session_request":
|
||||
guard version == "2" else { return "invalid:unsupported_version" }
|
||||
if values["policy_snapshot"] != nil { return "invalid:forbidden_field" }
|
||||
guard (try? SessionRequest(
|
||||
clientDeviceId: values["client_device_id"] ?? "", deviceKeyId: values["device_key_id"] ?? "",
|
||||
poolId: values["pool_id"] ?? "", idempotencyKey: values["idempotency_key"] ?? "",
|
||||
requestedDisplayMode: nil
|
||||
)) != nil else { return "invalid:required" }
|
||||
return "valid"
|
||||
case "browser_authenticated_session":
|
||||
guard values["client_device_id"] == nil, values["device_key_id"] == nil else { return "invalid:forbidden_field" }
|
||||
guard (try? BrowserAuthenticatedSession(
|
||||
username: values["username"] ?? "", provider: values["provider"] ?? "",
|
||||
roles: [values["roles"] ?? ""], role: values["role"] ?? ""
|
||||
)) != nil else { return "invalid:invalid_session" }
|
||||
return "valid"
|
||||
case "native_authenticated_session":
|
||||
guard let identity = try? NativeSessionIdentity(
|
||||
clientDeviceId: values["client_device_id"] ?? "", deviceKeyId: values["device_key_id"] ?? ""
|
||||
), values["client_device_id"] != nil, values["device_key_id"] != nil else { return "invalid:required" }
|
||||
guard (try? NativeAuthenticatedSession(
|
||||
username: values["username"] ?? "", provider: values["provider"] ?? "",
|
||||
roles: [values["roles"] ?? ""], role: values["role"] ?? "", nativeIdentity: identity
|
||||
)) != nil else { return "invalid:invalid_session" }
|
||||
return "valid"
|
||||
case "native_tunnel_credential":
|
||||
guard (try? NativeTunnelCredential(
|
||||
clientDeviceId: values["client_device_id"] ?? "", deviceKeyId: values["device_key_id"] ?? "",
|
||||
certificateChainPem: values["certificate_chain_pem"] ?? "", trustBundlePem: values["trust_bundle_pem"] ?? "",
|
||||
expiresAt: values["expires_at"] ?? ""
|
||||
)) != nil else { return "invalid:invalid_credential" }
|
||||
return "valid"
|
||||
case "event":
|
||||
guard values["version"] == "1" else { return "invalid:unsupported_version" }
|
||||
if let after = Int(values["after"] ?? ""), let earliest = Int(values["earliest"] ?? ""), after > 0, earliest > 0, after < earliest - 1 { return "invalid:gap" }
|
||||
@@ -27,8 +59,8 @@ func evaluate(_ kind: String, _ input: String) -> String {
|
||||
guard let sequence = Int(values["sequence"] ?? ""), sequence > 0, values["correlation_id"] != nil else { return "invalid:required" }
|
||||
return "valid"
|
||||
case "tunnel":
|
||||
let registered = ["control.v1", "display.request.v1", "input.absolute.v1", "input.scroll.v1"].contains(values["feature"] ?? "")
|
||||
if ["1", "0", "-1"].contains(values["offered"] ?? "") && registered { return "valid" }
|
||||
let registered = ["control.v1", "control.v2", "display.request.v1", "input.absolute.v1", "input.scroll.v1"].contains(values["feature"] ?? "")
|
||||
if ["2", "1", "0"].contains(values["offered"] ?? "") && registered { return "valid" }
|
||||
return registered ? "invalid:unsupported_version" : "invalid:unsupported_feature"
|
||||
case "datagram": return classifyDatagram(values["hex"] ?? "")
|
||||
case "gateway_input": return classifyGatewayInput(values["hex"] ?? "")
|
||||
@@ -197,7 +229,7 @@ struct ConformanceMain {
|
||||
for line in lines {
|
||||
let fields = line.split(separator: "\t", omittingEmptySubsequences: false).map(String.init)
|
||||
precondition(fields.count == 5)
|
||||
let actual = evaluate(fields[2], fields[3])
|
||||
let actual = evaluate(fields[1], fields[2], fields[3])
|
||||
precondition(actual == fields[4], fields[0])
|
||||
results.append("\(fields[0])\t\(actual)")
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ let capability = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||
)
|
||||
guard currentWireVersion == "2", nMinus1WireVersion == "1", nMinus2WireVersion == "0" else {
|
||||
fatalError("unexpected control wire compatibility declaration")
|
||||
}
|
||||
_ = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v2", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||
@@ -114,23 +117,16 @@ for invalid in [
|
||||
fatalError("invalid display mode was accepted")
|
||||
} catch { }
|
||||
}
|
||||
let allocationPolicy = try AllocationPolicy(
|
||||
minimumKbps: 1000, targetKbps: 2000, maximumKbps: 3000, tier: "standard",
|
||||
audience: "versevdi-gateway", protocolValue: "verse", protocolVersion: 1,
|
||||
grantTtlSeconds: 60, reservationLeaseSeconds: 300
|
||||
)
|
||||
let legacyDisplayRequest = try SessionRequest(
|
||||
let policyFreeV2Request = try SessionRequest(
|
||||
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
|
||||
idempotencyKey: "request-1", policySnapshot: allocationPolicy,
|
||||
requestedDisplayMode: nil
|
||||
idempotencyKey: "request-1", requestedDisplayMode: nil
|
||||
).encodeJSON()
|
||||
guard !String(data: legacyDisplayRequest, encoding: .utf8)!.contains("requested_display_mode") else {
|
||||
fatalError("legacy request encoded an absent display mode")
|
||||
guard !String(data: policyFreeV2Request, encoding: .utf8)!.contains("requested_display_mode") else {
|
||||
fatalError("wire-v2 request encoded an absent display mode")
|
||||
}
|
||||
let displayRequest = try SessionRequest(
|
||||
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
|
||||
idempotencyKey: "request-1", policySnapshot: allocationPolicy,
|
||||
requestedDisplayMode: displayMode
|
||||
idempotencyKey: "request-1", requestedDisplayMode: displayMode
|
||||
)
|
||||
guard try SessionRequest.decodeJSON(displayRequest.encodeJSON()).requestedDisplayMode == displayMode else {
|
||||
fatalError("display mode did not round-trip")
|
||||
@@ -141,6 +137,52 @@ do {
|
||||
_ = try SessionRequest.decodeJSON(try JSONSerialization.data(withJSONObject: nullDisplayRequest))
|
||||
fatalError("explicit null display mode was accepted")
|
||||
} catch { }
|
||||
let nativeIdentity = try NativeSessionIdentity(clientDeviceId: "device-1", deviceKeyId: "key-1")
|
||||
let browserSession = try BrowserAuthenticatedSession(
|
||||
username: "alice", provider: "local", roles: ["user"], role: "user"
|
||||
)
|
||||
guard !String(data: try browserSession.encodeJSON(), encoding: .utf8)!.contains("native_identity") else {
|
||||
fatalError("browser session encoded native identity")
|
||||
}
|
||||
let nativeSession = try NativeAuthenticatedSession(
|
||||
username: "alice", provider: "local", roles: ["user"], role: "user", nativeIdentity: nativeIdentity
|
||||
)
|
||||
guard try NativeAuthenticatedSession.decodeJSON(nativeSession.encodeJSON()).nativeIdentity == nativeIdentity else {
|
||||
fatalError("native session identity did not round-trip")
|
||||
}
|
||||
do {
|
||||
_ = try BrowserAuthenticatedSession.decodeJSON(nativeSession.encodeJSON())
|
||||
fatalError("browser session accepted native identity")
|
||||
} catch { }
|
||||
do {
|
||||
_ = try NativeAuthenticatedSession.decodeJSON(browserSession.encodeJSON())
|
||||
fatalError("native session accepted missing identity")
|
||||
} catch { }
|
||||
var partialNativeSession = try JSONSerialization.jsonObject(with: nativeSession.encodeJSON()) as! [String: Any]
|
||||
partialNativeSession["native_identity"] = ["client_device_id": "device-1"]
|
||||
do {
|
||||
_ = try NativeAuthenticatedSession.decodeJSON(try JSONSerialization.data(withJSONObject: partialNativeSession))
|
||||
fatalError("partial native identity was accepted")
|
||||
} catch { }
|
||||
for roles in [[""], [String(repeating: "r", count: 65)]] {
|
||||
do {
|
||||
_ = try BrowserAuthenticatedSession(username: "alice", provider: "local", roles: roles, role: "user")
|
||||
fatalError("invalid role item length was accepted")
|
||||
} catch { }
|
||||
}
|
||||
_ = try NativeTunnelCredential(
|
||||
clientDeviceId: "device-1", deviceKeyId: "key-1", certificateChainPem: "certificate",
|
||||
trustBundlePem: "trust", expiresAt: "2099-01-01T00:00:00Z"
|
||||
)
|
||||
for expiresAt in ["2099-01-01T00:00:00+00:00", "2099-01-01T00:00:00.100Z"] {
|
||||
do {
|
||||
_ = try NativeTunnelCredential(
|
||||
clientDeviceId: "device-1", deviceKeyId: "key-1", certificateChainPem: "certificate",
|
||||
trustBundlePem: "trust", expiresAt: expiresAt
|
||||
)
|
||||
fatalError("noncanonical RFC3339 UTC timestamp was accepted")
|
||||
} catch { }
|
||||
}
|
||||
let streamPolicy = try ProviderStreamPolicy(
|
||||
resolutionWidth: 2560, resolutionHeight: 1440, fps: 120,
|
||||
codec: "HEVC", bitrateKbps: 40000, audioEnabled: true
|
||||
@@ -207,6 +249,9 @@ do {
|
||||
output.write(
|
||||
"""
|
||||
fn main() {
|
||||
assert_eq!(CURRENT_WIRE_VERSION, "2");
|
||||
assert_eq!(N_MINUS_1_WIRE_VERSION, "1");
|
||||
assert_eq!(N_MINUS_2_WIRE_VERSION, "0");
|
||||
let capabilities = CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||
@@ -260,20 +305,39 @@ fn main() {
|
||||
assert!(DisplayMode::new(319, 1440, 120).is_err());
|
||||
assert!(DisplayMode::new(2560, 199, 120).is_err());
|
||||
assert!(DisplayMode::new(2560, 1440, 241).is_err());
|
||||
let allocation_policy = AllocationPolicy::new(
|
||||
1000, 2000, 3000, "standard".into(), "versevdi-gateway".into(),
|
||||
"verse".into(), 1, 60, 300,
|
||||
).unwrap();
|
||||
let legacy_display_request = SessionRequest::new(
|
||||
let policy_free_v2_request = SessionRequest::new(
|
||||
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
|
||||
allocation_policy.clone(), None,
|
||||
None,
|
||||
).unwrap();
|
||||
assert!(legacy_display_request.requestedDisplayMode().is_none());
|
||||
assert!(policy_free_v2_request.requestedDisplayMode().is_none());
|
||||
let display_request = SessionRequest::new(
|
||||
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
|
||||
allocation_policy, Some(display_mode.clone()),
|
||||
Some(display_mode.clone()),
|
||||
).unwrap();
|
||||
assert_eq!(display_request.requestedDisplayMode(), &Some(display_mode));
|
||||
let native_identity = NativeSessionIdentity::new("device-1".into(), "key-1".into()).unwrap();
|
||||
assert!(BrowserAuthenticatedSession::new(
|
||||
"alice".into(), "local".into(), vec!["user".into()], "user".into(),
|
||||
).is_ok());
|
||||
assert!(NativeAuthenticatedSession::new(
|
||||
"alice".into(), "local".into(), vec!["user".into()], "user".into(), native_identity,
|
||||
).is_ok());
|
||||
assert!(BrowserAuthenticatedSession::new(
|
||||
"alice".into(), "local".into(), vec![String::new()], "user".into(),
|
||||
).is_err());
|
||||
assert!(BrowserAuthenticatedSession::new(
|
||||
"alice".into(), "local".into(), vec!["r".repeat(65)], "user".into(),
|
||||
).is_err());
|
||||
assert!(NativeTunnelCredential::new(
|
||||
"device-1".into(), "key-1".into(), "certificate".into(), "trust".into(),
|
||||
"2099-01-01T00:00:00Z".into(),
|
||||
).is_ok());
|
||||
for expires_at in ["2099-01-01T00:00:00+00:00", "2099-01-01T00:00:00.100Z"] {
|
||||
assert!(NativeTunnelCredential::new(
|
||||
"device-1".into(), "key-1".into(), "certificate".into(), "trust".into(),
|
||||
expires_at.into(),
|
||||
).is_err());
|
||||
}
|
||||
assert!(ProviderStreamPolicy::new(
|
||||
2560, 1440, 120, "HEVC".into(), 40000, true,
|
||||
).is_ok());
|
||||
|
||||
+63
-2
@@ -24,7 +24,7 @@ def main() -> int:
|
||||
assert set(definition["required"]).issubset(definition["properties"]), name
|
||||
|
||||
compatibility = json.loads((ROOT / "compatibility.json").read_text(encoding="utf-8"))
|
||||
assert set([compatibility["current"], compatibility["n_minus_1"], compatibility["n_minus_2"]]) == {"1", "0", "-1"}
|
||||
assert [compatibility["current"], compatibility["n_minus_1"], compatibility["n_minus_2"]] == ["2", "1", "0"]
|
||||
assert len(set(compatibility["unsupported"])) == len(compatibility["unsupported"])
|
||||
|
||||
for registry in ("registries/features.json", "registries/datagrams.json"):
|
||||
@@ -38,7 +38,7 @@ def main() -> int:
|
||||
|
||||
feature_registry = json.loads((ROOT / "registries/features.json").read_text(encoding="utf-8"))
|
||||
registered_features = {entry["id"] for entry in feature_registry["features"]}
|
||||
assert {"display.request.v1", "input.absolute.v1", "input.scroll.v1"}.issubset(registered_features)
|
||||
assert {"control.v1", "control.v2", "display.request.v1", "input.absolute.v1", "input.scroll.v1"}.issubset(registered_features)
|
||||
|
||||
display_mode = defs["DisplayMode"]
|
||||
assert display_mode["required"] == ["resolution_width", "resolution_height", "fps"]
|
||||
@@ -54,10 +54,57 @@ def main() -> int:
|
||||
assert field not in defs[owner]["required"]
|
||||
assert defs[owner]["properties"][field] == {"$ref": "#/$defs/DisplayMode"}
|
||||
|
||||
session_request = defs["SessionRequest"]
|
||||
assert "policy_snapshot" not in session_request["required"]
|
||||
assert "policy_snapshot" not in session_request["properties"]
|
||||
assert "policy_snapshot" in defs["BrokerSession"]["required"]
|
||||
assert defs["BrokerSession"]["properties"]["policy_snapshot"] == {"$ref": "#/$defs/AllocationPolicy"}
|
||||
|
||||
native_identity = defs["NativeSessionIdentity"]
|
||||
assert native_identity["required"] == ["client_device_id", "device_key_id"]
|
||||
browser_session = defs["BrowserAuthenticatedSession"]
|
||||
assert browser_session["required"] == ["username", "provider", "roles", "role"]
|
||||
assert "native_identity" not in browser_session["properties"]
|
||||
native_session = defs["NativeAuthenticatedSession"]
|
||||
assert native_session["required"] == ["username", "provider", "roles", "role", "native_identity"]
|
||||
assert native_session["properties"]["native_identity"] == {"$ref": "#/$defs/NativeSessionIdentity"}
|
||||
for session_definition in (browser_session, native_session):
|
||||
assert session_definition["properties"]["roles"]["items"] == {
|
||||
"type": "string", "minLength": 1, "maxLength": 64, "x-max-bytes": 64
|
||||
}
|
||||
tunnel_credential = defs["NativeTunnelCredential"]
|
||||
assert tunnel_credential["required"] == [
|
||||
"client_device_id", "device_key_id", "certificate_chain_pem", "trust_bundle_pem", "expires_at"
|
||||
]
|
||||
|
||||
manifest = json.loads((ROOT / "fixtures/valid/manifest.json").read_text(encoding="utf-8"))
|
||||
assert set(manifest).issubset(set(defs["ConnectionManifest"]["properties"]))
|
||||
public_identity = manifest["gateway"]["public_identity"]
|
||||
assert public_identity == "gateway.control.test"
|
||||
assert public_identity not in {
|
||||
manifest["gateway"]["id"],
|
||||
*manifest["gateway"]["addresses"],
|
||||
"sha256:" + "00" * 32,
|
||||
"apollo-provider-1",
|
||||
}
|
||||
forbidden = json.loads((ROOT / "fixtures/invalid/manifest-provider-field.json").read_text(encoding="utf-8"))
|
||||
assert "provider_url" not in defs["ConnectionManifest"]["properties"] and "provider_url" in forbidden
|
||||
session_request_fixture = json.loads((ROOT / "fixtures/valid/session-request.json").read_text(encoding="utf-8"))
|
||||
assert "policy_snapshot" not in session_request_fixture
|
||||
rejected_policy_fixture = json.loads((ROOT / "fixtures/invalid/session-request-policy-snapshot.json").read_text(encoding="utf-8"))
|
||||
assert "policy_snapshot" in rejected_policy_fixture
|
||||
browser_session_fixture = json.loads((ROOT / "fixtures/valid/authenticated-browser-session.json").read_text(encoding="utf-8"))
|
||||
assert "native_identity" not in browser_session_fixture
|
||||
native_session_fixture = json.loads((ROOT / "fixtures/valid/authenticated-native-session.json").read_text(encoding="utf-8"))
|
||||
assert set(native_session_fixture["native_identity"]) == {"client_device_id", "device_key_id"}
|
||||
partial_identity_fixture = json.loads((ROOT / "fixtures/invalid/authenticated-session-partial-native-identity.json").read_text(encoding="utf-8"))
|
||||
assert set(partial_identity_fixture["native_identity"]) != {"client_device_id", "device_key_id"}
|
||||
browser_native_fixture = json.loads((ROOT / "fixtures/invalid/browser-session-native-identity.json").read_text(encoding="utf-8"))
|
||||
assert "native_identity" in browser_native_fixture
|
||||
native_missing_fixture = json.loads((ROOT / "fixtures/invalid/native-session-missing-identity.json").read_text(encoding="utf-8"))
|
||||
assert "native_identity" not in native_missing_fixture
|
||||
tunnel_credential_fixture = json.loads((ROOT / "fixtures/valid/native-tunnel-credential.json").read_text(encoding="utf-8"))
|
||||
assert set(tunnel_credential_fixture) == set(tunnel_credential["required"])
|
||||
|
||||
expected_header = "id\tversion\tkind\tinput\texpected"
|
||||
ids = set()
|
||||
@@ -87,6 +134,20 @@ def main() -> int:
|
||||
assert "openapi: 3.1.0" in openapi
|
||||
assert "/api/v1/auth/refresh:" in openapi and "/api/v1/resources:" in openapi and "/api/v1/events:" in openapi
|
||||
assert "provider_url" not in openapi and "vm_address" not in openapi
|
||||
session_endpoint = openapi.split(" /api/v1/auth/session:", 1)[1].split("\n /api/", 1)[0]
|
||||
assert "$defs/BrowserAuthenticatedSession" in session_endpoint
|
||||
assert "$defs/NativeAuthenticatedSession" in session_endpoint
|
||||
login_endpoint = openapi.split(" /api/v1/auth/login:", 1)[1].split("\n /api/", 1)[0]
|
||||
assert "$defs/BrowserAuthenticatedSession" in login_endpoint
|
||||
assert "$defs/NativeAuthenticatedSession" not in login_endpoint
|
||||
tunnel_endpoint = openapi.split(" /api/v1/auth/tunnel-credentials:", 1)[1].split("\n /api/", 1)[0]
|
||||
assert "- nativeBearer: []" in tunnel_endpoint
|
||||
assert "browserSession" not in tunnel_endpoint and "requestBody:" not in tunnel_endpoint
|
||||
assert "$defs/NativeTunnelCredential" in tunnel_endpoint
|
||||
assert "Cache-Control:" in tunnel_endpoint and "const: no-store" in tunnel_endpoint
|
||||
assert defs["ManifestGateway"]["properties"]["public_identity"]["description"] == (
|
||||
"Exact TLS server name; distinct from dial addresses, gateway UUIDs, certificate fingerprints, and provider identities."
|
||||
)
|
||||
print("Protocol source validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user