Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ public boolean process(Exchange exchange, AsyncCallback callback) {

// 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());
Expand Down Expand Up @@ -178,7 +177,7 @@ private RestBindingAdvice createRestBinding(Operation o) throws Exception {
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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,13 @@ public Set<String> validate(Exchange exchange, RestOpenApiOperation o) {
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");
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +30,7 @@
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;
Expand All @@ -40,8 +40,6 @@
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/>
Expand All @@ -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;
Expand Down Expand Up @@ -90,7 +89,8 @@ public RestBindingAdvice(CamelContext camelContext, DataFormat jsonDataFormat, D
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);
Expand Down Expand Up @@ -144,6 +144,7 @@ public RestBindingAdvice(CamelContext camelContext, DataFormat jsonDataFormat, D
this.requiredQueryParameters = requiredQueryParameters;
this.requiredHeaders = requiredHeaders;
this.enableNoContentResponse = enableNoContentResponse;
this.clientRequestValidator = clientRequestValidator;
}

@Override
Expand Down Expand Up @@ -212,28 +213,22 @@ private void unmarshal(Exchange exchange, Map<String, Object> state) {
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;
}
Expand All @@ -253,7 +248,6 @@ private void unmarshal(Exchange exchange, Map<String, Object> state) {
} 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("<");
Expand All @@ -263,80 +257,6 @@ private void unmarshal(Exchange exchange, Map<String, Object> state) {
}
}

// 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
Expand Down
Loading