This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 68727802289 rest client validator (#14400)
68727802289 is described below
commit 6872780228938c9a7337eb8f5b90b70db2dee3bb
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Jun 5 16:06:12 2024 +0200
rest client validator (#14400)
CAMEL-20832: rest-dsl - Add SPI for plugin client request validator.
Default validator to use existing validation code.
---
.../rest/openapi/RestOpenApiProcessor.java | 3 +-
.../openapi/validator/DefaultRequestValidator.java | 12 ++-
.../camel/spi/RestClientRequestValidator.java | 70 ++++++++++++
.../DefaultRestClientRequestValidator.java | 103 ++++++++++++++++++
.../camel/support/processor/RestBindingAdvice.java | 118 ++++-----------------
.../processor/RestBindingAdviceFactory.java | 26 ++++-
6 files changed, 225 insertions(+), 107 deletions(-)
diff --git
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
index 29b6bd222b4..8304028d1eb 100644
---
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
+++
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
@@ -118,7 +118,6 @@ public class RestOpenApiProcessor extends
DelegateAsyncProcessor implements Came
// binding mode
RestConfiguration config = camelContext.getRestConfiguration();
- RestConfiguration.RestBindingMode bindingMode =
config.getBindingMode();
// map path-parameters from operation to camel headers
HttpHelper.evalPlaceholders(exchange.getMessage().getHeaders(),
uri, rcp.getConsumerPath());
@@ -178,7 +177,7 @@ public class RestOpenApiProcessor extends
DelegateAsyncProcessor implements Came
bc.setBindingMode(mode.name());
bc.setEnableCORS(config.isEnableCORS());
bc.setCorsHeaders(config.getCorsHeaders());
- bc.setClientRequestValidation(config.isClientRequestValidation());
+ bc.setClientRequestValidation(config.isClientRequestValidation() ||
endpoint.isClientRequestValidation());
bc.setEnableNoContentResponse(config.isEnableNoContentResponse());
bc.setSkipBindingOnErrorCode(config.isSkipBindingOnErrorCode());
diff --git
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/DefaultRequestValidator.java
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/DefaultRequestValidator.java
index 7a3474db655..af20dc487bf 100644
---
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/DefaultRequestValidator.java
+++
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/DefaultRequestValidator.java
@@ -84,11 +84,13 @@ public class DefaultRequestValidator implements
RequestValidator {
Object body = message.getBody();
if (body != null) {
String text = MessageHelper.extractBodyAsString(message);
- JsonMapper om = new JsonMapper();
- try {
- om.readTree(text);
- } catch (Exception e) {
- validationErrors.add("Unable to parse JSON");
+ if (text != null) {
+ JsonMapper om = new JsonMapper();
+ try {
+ om.readTree(text);
+ } catch (Exception e) {
+ validationErrors.add("Unable to parse JSON");
+ }
}
}
}
diff --git
a/core/camel-api/src/main/java/org/apache/camel/spi/RestClientRequestValidator.java
b/core/camel-api/src/main/java/org/apache/camel/spi/RestClientRequestValidator.java
new file mode 100644
index 00000000000..a07ed516eeb
--- /dev/null
+++
b/core/camel-api/src/main/java/org/apache/camel/spi/RestClientRequestValidator.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.spi;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.Exchange;
+
+/**
+ * Used for validating incoming client requests with Camel Rest DSL.
+ * <p>
+ * This allows to plugin different validators.
+ */
+public interface RestClientRequestValidator {
+
+ String FACTORY = "rest-client-validator-factory";
+
+ /**
+ * Validation error
+ *
+ * @param statusCode to use a specific HTTP status code for this
validation error
+ * @param body to use a specific message body for this validation
error
+ */
+ record ValidationError(int statusCode, String body) {
+ }
+
+ /**
+ * Validation context to use during validation
+ *
+ * @param consumes content-type this service can consume
+ * @param produces content-type this service can produce
+ * @param requiredBody whether the message body is required
+ * @param queryDefaultValues default values for HTTP query parameters
+ * @param queryAllowedValues allowed values for HTTP query parameters
+ * @param requiredQueryParameters names of HTTP query parameters that are
required
+ * @param requiredHeaders names of HTTP headers parameters that
are required
+ */
+ record ValidationContext(String consumes, String produces,
+ boolean requiredBody,
+ Map<String, String> queryDefaultValues,
+ Map<String, String> queryAllowedValues,
+ Set<String> requiredQueryParameters,
+ Set<String> requiredHeaders) {
+ }
+
+ /**
+ * Validates the incoming client request
+ *
+ * @param exchange the current exchange
+ * @param validationContent validation context
+ * @return the validation error, or <tt>null</tt> if success
+ */
+ ValidationError validate(Exchange exchange, ValidationContext
validationContent);
+
+}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultRestClientRequestValidator.java
b/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultRestClientRequestValidator.java
new file mode 100644
index 00000000000..756cfe0a426
--- /dev/null
+++
b/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultRestClientRequestValidator.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.support.processor;
+
+import java.util.Arrays;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.spi.RestClientRequestValidator;
+import org.apache.camel.support.ExchangeHelper;
+import org.apache.camel.support.MessageHelper;
+import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.json.DeserializationException;
+import org.apache.camel.util.json.Jsoner;
+
+import static
org.apache.camel.support.http.RestUtil.isValidOrAcceptedContentType;
+
+public class DefaultRestClientRequestValidator implements
RestClientRequestValidator {
+
+ @Override
+ public ValidationError validate(Exchange exchange, ValidationContext
validationContext) {
+ String contentType = ExchangeHelper.getContentType(exchange);
+
+ // check if the content-type is accepted according to consumes
+ if (!isValidOrAcceptedContentType(validationContext.consumes(),
contentType)) {
+ return new ValidationError(415, null);
+ }
+ // check if what is produces is accepted by the client
+ String accept = exchange.getMessage().getHeader("Accept",
String.class);
+ if (!isValidOrAcceptedContentType(validationContext.produces(),
accept)) {
+ return new ValidationError(406, null);
+ }
+ // check for required query parameters
+ if (validationContext.requiredQueryParameters() != null
+ &&
!exchange.getIn().getHeaders().keySet().containsAll(validationContext.requiredQueryParameters()))
{
+ // this is a bad request, the client did not include some required
query parameters
+ return new ValidationError(400, "Some of the required query
parameters are missing.");
+ }
+ // check for required http headers
+ if (validationContext.requiredHeaders() != null
+ &&
!exchange.getIn().getHeaders().keySet().containsAll(validationContext.requiredHeaders()))
{
+ // this is a bad request, the client did not include some required
query parameters
+ return new ValidationError(400, "Some of the required HTTP headers
are missing.");
+ }
+ // allowed values for query/header parameters
+ if (validationContext.queryAllowedValues() != null) {
+ for (var e : validationContext.queryAllowedValues().entrySet()) {
+ String k = e.getKey();
+ Object v = exchange.getMessage().getHeader(k);
+ if (v != null) {
+ String[] parts = e.getValue().split(",");
+ if (Arrays.stream(parts).noneMatch(v::equals)) {
+ // this is a bad request, the client did not include
some required query parameters
+ return new ValidationError(400, "Some of the query
parameters or HTTP headers has a not-allowed value.");
+ }
+ }
+ }
+ }
+
+ Object body = exchange.getMessage().getBody();
+ if (validationContext.requiredBody()) {
+ // the body is required, so we need to know if we have a body or
not
+ // so force reading the body as a String which we can work with
+ body = MessageHelper.extractBodyAsString(exchange.getIn());
+ if (ObjectHelper.isNotEmpty(body)) {
+ exchange.getIn().setBody(body);
+ }
+ if (ObjectHelper.isEmpty(body)) {
+ // this is a bad request, the client did not include a message
body
+ return new ValidationError(400, "The request body is
missing.");
+ }
+ }
+ // if content-type is json then lets validate the message body can be
parsed to json
+ if (body != null && contentType != null &&
isValidOrAcceptedContentType("application/json", contentType)) {
+ String json = MessageHelper.extractBodyAsString(exchange.getIn());
+ if (json != null) {
+ try {
+ Jsoner.deserialize(json);
+ } catch (DeserializationException e) {
+ // request payload is not json
+ return new ValidationError(400, "Invalid JSon payload.");
+ }
+ }
+ }
+
+ // success
+ return null;
+ }
+
+}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java
b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java
index 24a9752433e..32b5bc27775 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java
@@ -16,7 +16,6 @@
*/
package org.apache.camel.support.processor;
-import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -31,6 +30,7 @@ import org.apache.camel.spi.CamelInternalProcessorAdvice;
import org.apache.camel.spi.DataFormat;
import org.apache.camel.spi.DataType;
import org.apache.camel.spi.DataTypeAware;
+import org.apache.camel.spi.RestClientRequestValidator;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.support.ExchangeHelper;
import org.apache.camel.support.MessageHelper;
@@ -40,8 +40,6 @@ import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import static
org.apache.camel.support.http.RestUtil.isValidOrAcceptedContentType;
-
/**
* Used for Rest DSL with binding to json/xml for incoming requests and
outgoing responses.
* <p/>
@@ -60,6 +58,7 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
private static final String STATE_JSON = "json";
private static final String STATE_XML = "xml";
+ private final RestClientRequestValidator clientRequestValidator;
private final AsyncProcessor jsonUnmarshal;
private final AsyncProcessor xmlUnmarshal;
private final AsyncProcessor jsonMarshal;
@@ -90,7 +89,8 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
Map<String, String> queryDefaultValues,
Map<String, String> queryAllowedValues,
boolean requiredBody, Set<String>
requiredQueryParameters,
- Set<String> requiredHeaders) throws Exception {
+ Set<String> requiredHeaders,
+ RestClientRequestValidator
clientRequestValidator) throws Exception {
if (jsonDataFormat != null) {
this.jsonUnmarshal = new UnmarshalProcessor(jsonDataFormat);
@@ -144,6 +144,7 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
this.requiredQueryParameters = requiredQueryParameters;
this.requiredHeaders = requiredHeaders;
this.enableNoContentResponse = enableNoContentResponse;
+ this.clientRequestValidator = clientRequestValidator;
}
@Override
@@ -212,28 +213,22 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
String accept = exchange.getMessage().getHeader("Accept",
String.class);
state.put(STATE_KEY_ACCEPT, accept);
- // perform client request validation
- if (clientRequestValidation) {
- // check if the content-type is accepted according to consumes
- if (!isValidOrAcceptedContentType(consumes, contentType)) {
- LOG.trace("Consuming content type does not match contentType
header {}. Stopping routing.", contentType);
- // the content-type is not something we can process so its a
HTTP_ERROR 415
- exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE,
415);
- // set empty response body as http error code indicate the
problem
- exchange.getMessage().setBody(null);
- // stop routing and return
- exchange.setRouteStop(true);
- return;
+ // add missing default values which are mapped as headers
+ if (queryDefaultValues != null) {
+ for (Map.Entry<String, String> entry :
queryDefaultValues.entrySet()) {
+ if (exchange.getIn().getHeader(entry.getKey()) == null) {
+ exchange.getIn().setHeader(entry.getKey(),
entry.getValue());
+ }
}
+ }
- // check if what is produces is accepted by the client
- if (!isValidOrAcceptedContentType(produces, accept)) {
- LOG.trace("Produced content type does not match accept header
{}. Stopping routing.", contentType);
- // the response type is not accepted by the client so its a
HTTP_ERROR 406
- exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE,
406);
- // set empty response body as http error code indicate the
problem
- exchange.getMessage().setBody(null);
- // stop routing and return
+ // perform client request validation
+ if (clientRequestValidation) {
+ RestClientRequestValidator.ValidationContext vc = new
RestClientRequestValidator.ValidationContext(consumes, produces, requiredBody,
queryDefaultValues, queryAllowedValues, requiredQueryParameters,
requiredHeaders);
+ RestClientRequestValidator.ValidationError error =
clientRequestValidator.validate(exchange, vc);
+ if (error != null) {
+ exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE,
error.statusCode());
+ exchange.getMessage().setBody(error.body());
exchange.setRouteStop(true);
return;
}
@@ -253,7 +248,6 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
} else {
exchange.getIn().setBody(body);
}
-
if (isXml && isJson) {
// we have still not determined between xml or json,
so check the body if its xml based or not
isXml = body.startsWith("<");
@@ -263,80 +257,6 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
}
}
- // add missing default values which are mapped as headers
- if (queryDefaultValues != null) {
- for (Map.Entry<String, String> entry :
queryDefaultValues.entrySet()) {
- if (exchange.getIn().getHeader(entry.getKey()) == null) {
- exchange.getIn().setHeader(entry.getKey(),
entry.getValue());
- }
- }
- }
-
- // check for required
- if (clientRequestValidation) {
- if (requiredBody) {
- // the body is required so we need to know if we have a body
or not
- // so force reading the body as a String which we can work with
- if (body == null) {
- body = MessageHelper.extractBodyAsString(exchange.getIn());
- if (ObjectHelper.isNotEmpty(body)) {
- exchange.getIn().setBody(body);
- }
- }
- if (ObjectHelper.isEmpty(body)) {
- // this is a bad request, the client did not include a
message body
-
exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 400);
- exchange.getMessage().setBody("The request body is
missing.");
- // stop routing and return
- exchange.setRouteStop(true);
- return;
- }
- // special check if binding mode is off and then incoming body
is json based
- // then we still want to ensure the body can be parsed as json
- if (bindingMode.equals("off") && !isXml) {
- if (isValidOrAcceptedContentType("application/json",
contentType)) {
- isJson = true;
- }
- }
- }
- if (requiredQueryParameters != null
- &&
!exchange.getIn().getHeaders().keySet().containsAll(requiredQueryParameters)) {
- // this is a bad request, the client did not include some
required query parameters
- exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE,
400);
- exchange.getMessage().setBody("Some of the required query
parameters are missing.");
- // stop routing and return
- exchange.setRouteStop(true);
- return;
- }
- if (requiredHeaders != null &&
!exchange.getIn().getHeaders().keySet().containsAll(requiredHeaders)) {
- // this is a bad request, the client did not include some
required http headers
- exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE,
400);
- exchange.getMessage().setBody("Some of the required HTTP
headers are missing.");
- // stop routing and return
- exchange.setRouteStop(true);
- return;
- }
- // allowed values for query/header parameters
- if (queryAllowedValues != null) {
- for (var e : queryAllowedValues.entrySet()) {
- String k = e.getKey();
- Object v = exchange.getMessage().getHeader(k);
- if (v != null) {
- String[] parts = e.getValue().split(",");
- if (Arrays.stream(parts).noneMatch(v::equals)) {
- // this is a bad request, the client did not
include some required query parameters
-
exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 400);
- exchange.getMessage()
- .setBody("Some of the query parameters or
HTTP headers has a not-allowed value.");
- // stop routing and return
- exchange.setRouteStop(true);
- return;
- }
- }
- }
- }
- }
-
// favor json over xml
if (isJson && jsonUnmarshal != null) {
// add reverse operation
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdviceFactory.java
b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdviceFactory.java
index 4a909f83027..969f7dc0136 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdviceFactory.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdviceFactory.java
@@ -18,14 +18,18 @@ package org.apache.camel.support.processor;
import java.util.HashMap;
import java.util.Map;
+import java.util.Optional;
import org.apache.camel.CamelContext;
import org.apache.camel.spi.BeanIntrospection;
import org.apache.camel.spi.DataFormat;
+import org.apache.camel.spi.RestClientRequestValidator;
import org.apache.camel.spi.RestConfiguration;
+import org.apache.camel.support.CamelContextHelper;
import org.apache.camel.support.EndpointHelper;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.PropertyBindingSupport;
+import org.apache.camel.support.ResolverHelper;
/**
* Factory to create {@link RestBindingAdvice} from the given configuration.
@@ -114,13 +118,18 @@ public class RestBindingAdviceFactory {
}
}
+ RestClientRequestValidator validator = null;
+ if (bc.isClientRequestValidation()) {
+ validator = lookupRestClientRequestValidator(camelContext);
+ }
+
return new RestBindingAdvice(
camelContext, json, jaxb, outJson, outJaxb,
bc.getConsumes(), bc.getProduces(), mode,
bc.isSkipBindingOnErrorCode(), bc.isClientRequestValidation(),
bc.isEnableCORS(),
bc.isEnableNoContentResponse(), bc.getCorsHeaders(),
bc.getQueryDefaultValues(), bc.getQueryAllowedValues(),
bc.isRequiredBody(), bc.getRequiredQueryParameters(),
- bc.getRequiredHeaders());
+ bc.getRequiredHeaders(), validator);
}
protected static void setupJson(
@@ -171,6 +180,21 @@ public class RestBindingAdviceFactory {
setAdditionalConfiguration(camelContext, config, outJson, "json.out.");
}
+ protected static RestClientRequestValidator
lookupRestClientRequestValidator(CamelContext camelContext) {
+ RestClientRequestValidator answer =
CamelContextHelper.findSingleByType(camelContext,
RestClientRequestValidator.class);
+ if (answer == null) {
+ // lookup via classpath to find custom factory
+ Optional<RestClientRequestValidator> result =
ResolverHelper.resolveService(
+ camelContext,
+
camelContext.getCamelContextExtension().getBootstrapFactoryFinder(),
+ RestClientRequestValidator.FACTORY,
+ RestClientRequestValidator.class);
+ // else use a default implementation
+ answer = result.orElseGet(DefaultRestClientRequestValidator::new);
+ }
+ return answer;
+ }
+
private static void setAdditionalConfiguration(
CamelContext camelContext, RestConfiguration config, DataFormat
dataFormat, String prefix) {
if (config.getDataFormatProperties() != null &&
!config.getDataFormatProperties().isEmpty()) {