JMAP: an unknown capability is unknownCapability, not notRequest #210
No files matched your search
@@ -553,15 +553,24 @@ impl Capability {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CapabilityIds {
|
||||
// inbuxa: a request's `using`. A capability the server doesn't know is kept,
|
||||
// not a parse error, so the request fails with unknownCapability naming it
|
||||
// (RFC 8620, section 3.6.1) instead of notRequest.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Using {
|
||||
pub ids: CapabilityIds,
|
||||
pub unknown: Option<String>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Using {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct CapabilityIdsVisitor;
|
||||
struct UsingVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for CapabilityIdsVisitor {
|
||||
type Value = CapabilityIds;
|
||||
impl<'de> serde::de::Visitor<'de> for UsingVisitor {
|
||||
type Value = Using;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("an array of capability strings")
|
||||
@@ -571,23 +580,23 @@ impl<'de> Deserialize<'de> for CapabilityIds {
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let mut capability_flags = 0u32;
|
||||
let mut using = Using::default();
|
||||
|
||||
while let Some(capability_str) = seq.next_element::<std::borrow::Cow<str>>()? {
|
||||
let capability =
|
||||
Capability::parse(capability_str.as_ref()).ok_or_else(|| {
|
||||
serde::de::Error::custom(format!(
|
||||
"Unknown capability: {capability_str:?}"
|
||||
))
|
||||
})?;
|
||||
|
||||
capability_flags |= capability as u32;
|
||||
match Capability::parse(capability_str.as_ref()) {
|
||||
Some(capability) => using.ids.0 |= capability as u32,
|
||||
None => {
|
||||
if using.unknown.is_none() {
|
||||
using.unknown = Some(capability_str.into_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CapabilityIds(capability_flags))
|
||||
Ok(using)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_seq(CapabilityIdsVisitor)
|
||||
deserializer.deserialize_seq(UsingVisitor)
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,8 @@ where
|
||||
#[derive(Debug)]
|
||||
pub struct Request<'x> {
|
||||
pub using: CapabilityIds,
|
||||
// inbuxa: the first capability in `using` the server doesn't know.
|
||||
pub unknown_capability: Option<String>,
|
||||
pub method_calls: Vec<Call<RequestMethod<'x>>>,
|
||||
pub created_ids: Option<HashMap<String, AnyId>>,
|
||||
}
|
||||
@@ -298,6 +300,7 @@ impl Default for Request<'_> {
|
||||
fn default() -> Self {
|
||||
Request {
|
||||
using: CapabilityIds::default(),
|
||||
unknown_capability: None,
|
||||
method_calls: Vec::new(),
|
||||
created_ids: None,
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use super::{
|
||||
};
|
||||
use crate::request::{
|
||||
CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryChangesRequestMethod,
|
||||
QueryRequestMethod, SetRequestMethod, deserialize::DeserializeArguments,
|
||||
QueryRequestMethod, SetRequestMethod, capability::Using, deserialize::DeserializeArguments,
|
||||
};
|
||||
use serde::{
|
||||
Deserialize, Deserializer,
|
||||
@@ -24,6 +24,12 @@ impl<'x> Request<'x> {
|
||||
pub fn parse(json: &'x [u8], max_calls: usize, max_size: usize) -> trc::Result<Self> {
|
||||
if json.len() <= max_size {
|
||||
match serde_json::from_slice::<Request>(json) {
|
||||
Ok(Request {
|
||||
unknown_capability: Some(capability),
|
||||
..
|
||||
}) => Err(trc::JmapEvent::UnknownCapability
|
||||
.into_err()
|
||||
.details(capability)),
|
||||
Ok(request) => {
|
||||
if request.method_calls.len() <= max_calls {
|
||||
Ok(request)
|
||||
@@ -49,7 +55,9 @@ impl<'de> DeserializeArguments<'de> for Request<'de> {
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"using" => {
|
||||
self.using = map.next_value()?;
|
||||
let using = map.next_value::<Using>()?;
|
||||
self.using = using.ids;
|
||||
self.unknown_capability = using.unknown;
|
||||
},
|
||||
b"methodCalls" => {
|
||||
self.method_calls = map.next_value()?;
|
||||
@@ -1339,6 +1347,45 @@ mod tests {
|
||||
println!("{:#?}", Request::parse(TEST2.as_bytes(), 10, 10240));
|
||||
}
|
||||
|
||||
// inbuxa: an unknown capability fails the request as unknownCapability,
|
||||
// naming it, and a known one parses as before.
|
||||
#[test]
|
||||
fn unknown_capability_is_named() {
|
||||
let request = r#"{"using": ["urn:ietf:params:jmap:core", "urn:example:nope", "urn:example:other"],
|
||||
"methodCalls": [["Core/echo", {}, "0"]]}"#;
|
||||
let err = Request::parse(request.as_bytes(), 10, 10240).unwrap_err();
|
||||
assert_eq!(
|
||||
*err.as_ref(),
|
||||
trc::EventType::Jmap(trc::JmapEvent::UnknownCapability)
|
||||
);
|
||||
assert_eq!(
|
||||
err.value(trc::Key::Details).and_then(|v| v.as_str()),
|
||||
Some("urn:example:nope")
|
||||
);
|
||||
|
||||
let request = r#"{"using": ["urn:ietf:params:jmap:core", "urn:inbuxa:jmap:registry"],
|
||||
"methodCalls": [["Core/echo", {}, "0"]]}"#;
|
||||
let request = Request::parse(request.as_bytes(), 10, 10240).unwrap();
|
||||
assert!(
|
||||
request
|
||||
.using
|
||||
.contains(crate::request::capability::Capability::Stalwart)
|
||||
);
|
||||
assert!(request.unknown_capability.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_capability_over_websocket_is_named() {
|
||||
let message = r#"{"@type": "Request", "id": "r1", "using": ["urn:example:nope"],
|
||||
"methodCalls": [["Core/echo", {}, "0"]]}"#;
|
||||
let err = crate::request::websocket::WebSocketMessage::parse(message.as_bytes(), 10, 10240)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
*err.as_ref(),
|
||||
trc::EventType::Jmap(trc::JmapEvent::UnknownCapability)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_method_name_with_escaped_solidus() {
|
||||
let request = Request::parse(TEST_ESCAPED_SOLIDUS.as_bytes(), 10, 10240)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*
|
||||
* Modified by Coffey Labs in 2026 for INBUXA.
|
||||
*/
|
||||
|
||||
use super::Request;
|
||||
@@ -107,6 +109,16 @@ impl<'x> WebSocketMessage<'x> {
|
||||
pub fn parse(json: &'x [u8], max_calls: usize, max_size: usize) -> trc::Result<Self> {
|
||||
if json.len() <= max_size {
|
||||
match serde_json::from_slice::<Self>(json) {
|
||||
Ok(WebSocketMessage::Request(WebSocketRequest {
|
||||
request:
|
||||
Request {
|
||||
unknown_capability: Some(capability),
|
||||
..
|
||||
},
|
||||
..
|
||||
})) => Err(trc::JmapEvent::UnknownCapability
|
||||
.into_err()
|
||||
.details(capability)),
|
||||
Ok(WebSocketMessage::Request(req))
|
||||
if req.request.method_calls.len() > max_calls =>
|
||||
{
|
||||
|
||||
Binary file not shown.
Reference in new issue
Block a user