fix: validate HTTPS binding limits and response ordering
This commit is contained in:
@@ -397,23 +397,6 @@ fn response_from_json(
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unsupported_batch_descriptor_format(value: &Json, request: &BatchRequest) -> Option<String> {
|
|
||||||
let responses = value.as_object()?.get("responses")?.as_array()?;
|
|
||||||
for item in responses {
|
|
||||||
let fields = item.as_object()?;
|
|
||||||
let id = fields.get("id")?.as_str()?;
|
|
||||||
let request_item = request.requests.iter().find(|item| item.id == id)?;
|
|
||||||
if matches!(request_item.request, BatchRequestKind::FetchInterface(_))
|
|
||||||
&& let Some(result) = fields.get("result")
|
|
||||||
&& let Ok(format) = json::fetch_interface_response_format(result)
|
|
||||||
&& format != INTERFACE_FORMAT_V1
|
|
||||||
{
|
|
||||||
return Some(format);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decodes a batch response, including item-local failures within HTTP 200.
|
/// Decodes a batch response, including item-local failures within HTTP 200.
|
||||||
pub fn decode_batch_response<B: AsRef<[u8]>>(
|
pub fn decode_batch_response<B: AsRef<[u8]>>(
|
||||||
request: &BatchRequest,
|
request: &BatchRequest,
|
||||||
@@ -434,12 +417,6 @@ pub fn decode_batch_response<B: AsRef<[u8]>>(
|
|||||||
}
|
}
|
||||||
return Err(response::transport_failure(value.status()));
|
return Err(response::transport_failure(value.status()));
|
||||||
}
|
}
|
||||||
if value.status().is_success()
|
|
||||||
&& let Ok(json) = json::parse(value.body().as_ref(), BodyKind::Response, limits)
|
|
||||||
&& let Some(format) = unsupported_batch_descriptor_format(&json, request)
|
|
||||||
{
|
|
||||||
return Err(ClientResponseError::UnsupportedDescriptorFormat { format });
|
|
||||||
}
|
|
||||||
let decoded = response::decode(
|
let decoded = response::decode(
|
||||||
value.status(),
|
value.status(),
|
||||||
value.body().as_ref(),
|
value.body().as_ref(),
|
||||||
|
|||||||
@@ -355,13 +355,6 @@ pub fn decode_fetch_interface_response<B: AsRef<[u8]>>(
|
|||||||
limits: Limits,
|
limits: Limits,
|
||||||
) -> Result<DecodedResponse<FetchInterfaceResponse>, ClientResponseError> {
|
) -> Result<DecodedResponse<FetchInterfaceResponse>, ClientResponseError> {
|
||||||
require_response_content_type(value)?;
|
require_response_content_type(value)?;
|
||||||
if value.status().is_success()
|
|
||||||
&& let Ok(json) = json::parse(value.body().as_ref(), BodyKind::Response, limits)
|
|
||||||
&& let Ok(format) = json::fetch_interface_response_format(&json)
|
|
||||||
&& format != INTERFACE_FORMAT_V1
|
|
||||||
{
|
|
||||||
return Err(ClientResponseError::UnsupportedDescriptorFormat { format });
|
|
||||||
}
|
|
||||||
let decoded = response::decode(
|
let decoded = response::decode(
|
||||||
value.status(),
|
value.status(),
|
||||||
value.body().as_ref(),
|
value.body().as_ref(),
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use serde_json::{Map, Number, Value as Json};
|
|||||||
use wip_protocol::{
|
use wip_protocol::{
|
||||||
CallOperationRequest, CallOperationResponse, Documentation, EnumCase, FetchInterfaceRequest,
|
CallOperationRequest, CallOperationResponse, Documentation, EnumCase, FetchInterfaceRequest,
|
||||||
FetchInterfaceResponse, FetchRequest, FetchTreeRequest, FetchTreeResponse, FieldDeclaration,
|
FetchInterfaceResponse, FetchRequest, FetchTreeRequest, FetchTreeResponse, FieldDeclaration,
|
||||||
InterfaceDescriptor, InterfaceTarget, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, Object,
|
INTERFACE_FORMAT_V1, InterfaceDescriptor, InterfaceTarget, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER,
|
||||||
ObjectTreeNode, OperationDeclaration, ParameterDeclaration, ReturnDeclaration, Target,
|
Object, ObjectTreeNode, OperationDeclaration, ParameterDeclaration, ReturnDeclaration, Target,
|
||||||
TypeDeclaration, TypeExpr, UnionCase, Value,
|
TypeDeclaration, TypeExpr, UnionCase, Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,6 +119,7 @@ pub(crate) fn serialize(
|
|||||||
kind: BodyKind,
|
kind: BodyKind,
|
||||||
limits: Limits,
|
limits: Limits,
|
||||||
) -> Result<Vec<u8>, CodecError> {
|
) -> Result<Vec<u8>, CodecError> {
|
||||||
|
check_nesting(value, 0, limits.max_nesting)?;
|
||||||
let body = serde_json::to_vec(value).expect("serde_json::Value serialization cannot fail");
|
let body = serde_json::to_vec(value).expect("serde_json::Value serialization cannot fail");
|
||||||
let maximum = match kind {
|
let maximum = match kind {
|
||||||
BodyKind::Request => limits.max_request_bytes,
|
BodyKind::Request => limits.max_request_bytes,
|
||||||
@@ -759,23 +760,34 @@ pub(crate) fn fetch_interface_response_to_json(response: &FetchInterfaceResponse
|
|||||||
Json::Object(fields)
|
Json::Object(fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fetch_interface_response_format(value: &Json) -> Result<String, CodecError> {
|
|
||||||
let fields = exact_object(value, "response", &["interface", "descriptor", "validator"])?;
|
|
||||||
string(required(fields, "interface")?, "interface")?;
|
|
||||||
let descriptor = object(required(fields, "descriptor")?, "descriptor")?;
|
|
||||||
string(required(descriptor, "format")?, "descriptor.format")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn fetch_interface_response_from_json(
|
pub(crate) fn fetch_interface_response_from_json(
|
||||||
value: &Json,
|
value: &Json,
|
||||||
) -> Result<FetchInterfaceResponse, CodecError> {
|
) -> Result<FetchInterfaceResponse, CodecError> {
|
||||||
let fields = exact_object(value, "response", &["interface", "descriptor", "validator"])?;
|
let fields = exact_object(value, "response", &["interface", "descriptor", "validator"])?;
|
||||||
Ok(FetchInterfaceResponse {
|
let interface = string(required(fields, "interface")?, "interface")?;
|
||||||
interface: string(required(fields, "interface")?, "interface")?,
|
let validator = optional(fields, "validator")?
|
||||||
descriptor: descriptor_from_json(required(fields, "descriptor")?, "descriptor")?,
|
|
||||||
validator: optional(fields, "validator")?
|
|
||||||
.map(|value| base64_decode(value, "validator"))
|
.map(|value| base64_decode(value, "validator"))
|
||||||
.transpose()?,
|
.transpose()?;
|
||||||
|
let descriptor_value = required(fields, "descriptor")?;
|
||||||
|
let descriptor_fields = object(descriptor_value, "descriptor")?;
|
||||||
|
let format = string(required(descriptor_fields, "format")?, "descriptor.format")?;
|
||||||
|
let descriptor = if format == INTERFACE_FORMAT_V1 {
|
||||||
|
descriptor_from_json(descriptor_value, "descriptor")?
|
||||||
|
} else {
|
||||||
|
// Preserve only the format discriminator. The client classifies this
|
||||||
|
// response after validating common response fields and request
|
||||||
|
// correspondence, without interpreting an unknown descriptor schema.
|
||||||
|
InterfaceDescriptor {
|
||||||
|
format,
|
||||||
|
documentation: None,
|
||||||
|
types: Vec::new(),
|
||||||
|
operations: Vec::new(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(FetchInterfaceResponse {
|
||||||
|
interface,
|
||||||
|
descriptor,
|
||||||
|
validator,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,55 @@ fn top_level_batch_errors_use_the_batch_allowed_code_set() {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unsupported_batch_descriptor_waits_for_envelope_correlation() {
|
||||||
|
let duplicate = wip_https::http::Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header("content-type", "application/json; charset=utf-8")
|
||||||
|
.body(
|
||||||
|
br#"{"responses":[{"id":"interface","result":{"interface":"interface-v1","descriptor":{"format":"wip-interface/2","future":true}}},{"id":"interface","result":{"interface":"interface-v1","descriptor":{"format":"wip-interface/2"}}}]}"#
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
decode_batch_response(&request(), &duplicate, limits()).unwrap_err(),
|
||||||
|
ClientResponseError::InvalidResponse {
|
||||||
|
kind: InvalidResponseKind::InvalidBatch,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
let missing = wip_https::http::Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header("content-type", "application/json; charset=utf-8")
|
||||||
|
.body(
|
||||||
|
br#"{"responses":[{"id":"interface","result":{"interface":"interface-v1","descriptor":{"format":"wip-interface/2"}}}]}"#
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
decode_batch_response(&request(), &missing, limits()).unwrap_err(),
|
||||||
|
ClientResponseError::InvalidResponse {
|
||||||
|
kind: InvalidResponseKind::InvalidBatch,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
let request = BatchRequest {
|
||||||
|
requests: vec![BatchRequestItem {
|
||||||
|
id: "interface".into(),
|
||||||
|
request: BatchRequestKind::FetchInterface(FetchInterfaceRequest {
|
||||||
|
interface: "interface-v1".into(),
|
||||||
|
}),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
decode_batch_response(&request, &missing, limits()).unwrap_err(),
|
||||||
|
ClientResponseError::UnsupportedDescriptorFormat { format }
|
||||||
|
if format == "wip-interface/2"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn batch_response_must_have_exactly_the_requested_ids() {
|
fn batch_response_must_have_exactly_the_requested_ids() {
|
||||||
let request = request();
|
let request = request();
|
||||||
|
|||||||
@@ -367,6 +367,27 @@ fn caller_limits_bound_bytes_and_nesting() {
|
|||||||
let nested = br#"{"path":[[["/"]]]}"#;
|
let nested = br#"{"path":[[["/"]]]}"#;
|
||||||
let nesting_limited = Limits::new(1024, 1024, 2, 1).unwrap();
|
let nesting_limited = Limits::new(1024, 1024, 2, 1).unwrap();
|
||||||
assert!(decode_fetch_request(nested, nesting_limited).is_err());
|
assert!(decode_fetch_request(nested, nesting_limited).is_err());
|
||||||
|
|
||||||
|
let outbound_limited = Limits::new(1_000_000, 1_000_000, 1, 10).unwrap();
|
||||||
|
assert!(
|
||||||
|
encode_call_operation_request(
|
||||||
|
&endpoint(),
|
||||||
|
&call_request(),
|
||||||
|
&descriptor(),
|
||||||
|
outbound_limited,
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
encode_fetch_response(
|
||||||
|
&FetchRequest {
|
||||||
|
path: "/item".into(),
|
||||||
|
},
|
||||||
|
&object("item", b"validator"),
|
||||||
|
outbound_limited,
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -400,6 +421,25 @@ fn unsupported_descriptor_format_is_client_local() {
|
|||||||
ClientResponseError::UnsupportedDescriptorFormat { format }
|
ClientResponseError::UnsupportedDescriptorFormat { format }
|
||||||
if format == "wip-interface/2"
|
if format == "wip-interface/2"
|
||||||
));
|
));
|
||||||
|
|
||||||
|
for malformed in [
|
||||||
|
br#"{"interface":"wrong","descriptor":{"format":"wip-interface/2"}}"#.as_slice(),
|
||||||
|
br#"{"interface":"interface-v1","descriptor":{"format":"wip-interface/2"},"validator":null}"#
|
||||||
|
.as_slice(),
|
||||||
|
] {
|
||||||
|
let response = wip_https::http::Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header("content-type", "application/json; charset=utf-8")
|
||||||
|
.body(malformed.to_vec())
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
decode_fetch_interface_response(&request, &response, limits()).unwrap_err(),
|
||||||
|
ClientResponseError::InvalidResponse {
|
||||||
|
kind: InvalidResponseKind::InvalidSuccessBody,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user