Hello,

For one of our projects, we offer a set of REST/JSON APIs using Camel
and wanted to activate the clientRequestValidation feature on those
endpoints for Camel to reject invalid requests.
But we encountered some limitations due to the way RestBindingAdvice
handles the Accept header when the client specifies either multiple MIME
types or JSON subtypes.

The current implementation includes some code to match JSON or XML
subtypes (using String.contains()) but it is buggy and rejects all cases
(no return true).

All theses cases are described in the unit test below that includes 3
implementations of RestBindingAdvice.isValidOrAcceptedContentType():
- "Legacy" : isLegacyValidOrAcceptedContentType() = Camel 4.4.1
implementation
- "Patched Legacy" : isPatchedLegacyValidOrAcceptedContentType() = a
simple patch to fix for the "contains("xml/json") tests but with
side-effects due to the whole Accept header not being parsed to process
each MIME type separately
- "Proposed" : isProposedValidOrAcceptedContentType() = an alternative
implementation that parses the Accept header to test separately each of
the MIME types specified by the client

The "Proposed" implementation may be overkill for the Content-Type
header as this header contains a single MIME type.

(This is my first post, I do not know of the mailing list accepts mail
attachments => code in the mail body below)

Thank you,

Laurent


 >>>>>

import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import org.apache.camel.util.StringHelper;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class RestBindingAdvicePatchProposalTest
{
     public final static String JSON = "application/json";
     public final static String XML  = "application/xml";
     public final static String SPARQL_RESULT_JSON =
"application/sparql-results+json";
     public final static String JSON_LD = "application/ld+json";

     @Test
     void testLegacyHeaderMatching() {
         // Supported cases (no patch)
         // 1. Exact match (single target type)
         assertTrue(isLegacyValidOrAcceptedContentType(JSON, JSON));
         assertFalse(isLegacyValidOrAcceptedContentType(XML, JSON));
         // 2. Exact match (single target type with parameter)
         assertTrue(isLegacyValidOrAcceptedContentType(JSON, JSON +
";charset=utf-8"));
         // 3. Wildcard match but supports for spec w/ multiple type =
side-effect?
         assertTrue(isLegacyValidOrAcceptedContentType(JSON, XML + ",
*/*;q=0.5, text/plain"));
         // 4. But invalid "*/*foo" is also accepted => should not
         assertTrue(isLegacyValidOrAcceptedContentType(JSON, XML + ",
*/*foo;q=0.5, text/plain"));

         // Wildcard match: expected to work but does not
         assertFalse(isLegacyValidOrAcceptedContentType(JSON, XML + ",
*/json;q=0.5, text/plain"));
         assertFalse(isLegacyValidOrAcceptedContentType(JSON, JSON_LD));

         // With patch: match any JSON-related type (spec w/ multiple
types accepted but not fully parsed)
         assertTrue(isPatchedLegacyValidOrAcceptedContentType(JSON, XML
+ ", */json;q=0.5, text/plain"));
         // But...
         // Invalid match: image/webjsonp => Accepted once patch accepts
all "json" string
         assertTrue(isPatchedLegacyValidOrAcceptedContentType(JSON, XML
+ ", image/webjsonp"));
         // JSON formatting of any type (e.g. application/xxx+json) is
also accepted: OK ?
         assertTrue(isPatchedLegacyValidOrAcceptedContentType(JSON,
JSON_LD));
         assertTrue(isPatchedLegacyValidOrAcceptedContentType(JSON, XML
+ ", " + SPARQL_RESULT_JSON + ";charset=utf-8"));
     }

     @Test
     void testNewHeaderMatching() {
         // Supported cases, no regression
         // 1. Exact match (single target type)
         assertTrue(isProposedValidOrAcceptedContentType(JSON, JSON));
         assertFalse(isProposedValidOrAcceptedContentType(XML, JSON));
         // 2. Exact match (single target type with parameter)
         assertTrue(isProposedValidOrAcceptedContentType(JSON, JSON +
";charset=utf-8"));
         // 3. Wildcard match
         assertTrue(isProposedValidOrAcceptedContentType(JSON, XML + ",
*/*;q=0.5, text/plain"));

         // Cases now rejected
         // "*/*foo" shall be rejected
         assertFalse(isProposedValidOrAcceptedContentType(JSON, XML + ",
*/*foo;q=0.5, text/plain"));
         // "image/webjsonp" shall be rejected
         assertTrue(isPatchedLegacyValidOrAcceptedContentType(JSON, XML
+ ", image/webjsonp"));
         // JSON formatting of any type (e.g. application/xxx+json) no
longer accepted: OK ?
         assertFalse(isProposedValidOrAcceptedContentType(JSON, JSON_LD));
         assertFalse(isProposedValidOrAcceptedContentType(JSON, XML + ",
" + SPARQL_RESULT_JSON + ";charset=utf-8"));

         // New (extended) behaviour
         // Exact match (multiple target types)
         assertTrue(isProposedValidOrAcceptedContentType(JSON, XML + ",
" + JSON + ", text/plain"));
         assertTrue(isProposedValidOrAcceptedContentType(XML,
"text/html, application/xhtml, " + XML + ";q=0.9"));
         // Wildcard match
         assertTrue(isProposedValidOrAcceptedContentType(JSON, XML + ",
*/json;q=0.5, text/plain"));
         assertTrue(isProposedValidOrAcceptedContentType(JSON, XML + ",
application/*;q=0.5"));
         // Extended types
         assertTrue(isProposedValidOrAcceptedContentType(JSON_LD, JSON +
", application/*+json"));
     }

     private static boolean isProposedValidOrAcceptedContentType(String
expected, String target) {
         if (expected == null || target == null) {
             return true;
         }
         String valid = expected.toLowerCase(Locale.ENGLISH);
         target = target.toLowerCase(Locale.ENGLISH);
         // Parse media range and strip any parameters (e.g. charset)
for all found media types
         List<String> targetTypes = Arrays.stream(target.split("\\s*,\\s*"))
                                          .map(t ->
StringHelper.before(t, ';', t)).toList();
         // Search for exact match first
         if (targetTypes.contains(valid)) {
             return true;
         }
         else {
             // Match wildcard types ("*/*" or "application/*") or type
extensions ("application/*+json")
             return targetTypes.stream().filter(t -> t.indexOf('*') != -1)
                               .map(t -> t.replace("+",
"\\+").replace("*", ".+"))
                               .anyMatch(t -> valid.matches(t));
         }
     }

     private static boolean
isPatchedLegacyValidOrAcceptedContentType(String valid, String target) {
         if (valid == null || target == null) {
             return true;
         }

         // Any MIME type
         //
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept#Directives
         if (target.contains("*/*")) {
             return true;
         }

         //  content-type is before optional charset
         target = StringHelper.before(target, ";", target);

         valid = valid.toLowerCase(Locale.ENGLISH);
         target = target.toLowerCase(Locale.ENGLISH);

         if (valid.contains(target)) {
             return true;
         }

         // The 2 tests below are wrong: from this point on, method
always returns false!
         boolean isXml = valid.contains("xml");
         //if (isXml && !target.contains("xml")) {
         //    return false;
         //}
         if (isXml && target.contains("xml")) {
             return true;
         }

         boolean isJson = valid.contains("json");
         //if (isJson && !target.contains("json")) {
         //    return false;
         //}
         if (isJson && target.contains("json")) {
             return true;
         }

         return false;
     }

     private static boolean isLegacyValidOrAcceptedContentType(String
valid, String target) {
         if (valid == null || target == null) {
             return true;
         }

         // Any MIME type
         //
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept#Directives
         if (target.contains("*/*")) {
             return true;
         }

         //  content-type is before optional charset
         target = StringHelper.before(target, ";", target);

         valid = valid.toLowerCase(Locale.ENGLISH);
         target = target.toLowerCase(Locale.ENGLISH);

         if (valid.contains(target)) {
             return true;
         }

         boolean isXml = valid.contains("xml");
         if (isXml && !target.contains("xml")) {
             return false;
         }

         boolean isJson = valid.contains("json");
         if (isJson && !target.contains("json")) {
             return false;
         }

         return false;
     }
}
<<<<<
Worldline, Cardlink, GoPay and Santeos are registered trademarks and trade 
names owned by the Worldline Group. This e-mail and any documents attached are 
confidential and intended solely for the addressee. It may also be privileged. 
If you are not the intended recipient of this e-mail, you are not authorized to 
copy, disclose, use or retain it. Please notify the sender immediately and 
delete this e-mail (including any attachments) from your systems. As e-mails 
may be intercepted, amended or lost, they are not secure. Therefore, 
Worldline’s and its subsidiaries’ liability cannot be triggered for the message 
content. Although the Worldline Group endeavors to maintain a virus-free 
network, we do not warrant that this e-mail is virus-free and do not accept 
liability for any damages, losses or consequences resulting from any 
transmitted virus if any. The risks are deemed to be accepted by anyone who 
communicates with Worldline or its subsidiaries by e-mail.
Please consider the environment before printing, sending or forwarding this 
email.

Reply via email to