You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by jk...@apache.org on 2021/04/20 15:34:32 UTC
[unomi] branch unomi-1.5.x updated: request size check (#297)
This is an automated email from the ASF dual-hosted git repository.
jkevan pushed a commit to branch unomi-1.5.x
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/unomi-1.5.x by this push:
new 575e9fc request size check (#297)
575e9fc is described below
commit 575e9fc2229af23c93f5b11c6d293c39724157f4
Author: kevan Jahanshahi <ke...@jahia.com>
AuthorDate: Tue Apr 20 17:33:44 2021 +0200
request size check (#297)
* request size check
* code review adjustments
* improve itests to also test directly endpoints /cxs in addition of old servlets
---
.../org/apache/unomi/itests/InputValidationIT.java | 146 +++++++++++++--------
.../eventcollector_request_size_invalid.json | 17 +++
.../eventcollector_request_size_valid.json | 17 +++
.../main/resources/etc/custom.system.properties | 5 +
...per.java => InvalidRequestExceptionMapper.java} | 22 +---
.../rest/exception/ValidationExceptionMapper.java | 4 +-
.../org/apache/unomi/rest/server/RestServer.java | 50 ++++---
.../rest/service/impl/RestServiceUtilsImpl.java | 8 +-
...ionProvider.java => BeanValidationService.java} | 11 +-
...derImpl.java => BeanValidationServiceImpl.java} | 12 +-
.../InvalidRequestException.java} | 25 ++--
.../request/RequestValidatorInterceptor.java | 69 ++++++++++
.../{cookies => wrapper}/CookieWrapper.java | 2 +-
.../java/org/apache/unomi/web/ContextServlet.java | 7 +-
.../resources/OSGI-INF/blueprint/blueprint.xml | 2 +
wab/src/main/resources/org.apache.unomi.web.cfg | 7 +-
16 files changed, 289 insertions(+), 115 deletions(-)
diff --git a/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java b/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java
index 071dbe1..4ccc600 100644
--- a/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java
@@ -22,6 +22,7 @@ import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
+import org.apache.http.util.EntityUtils;
import org.apache.unomi.itests.tools.httpclient.HttpClientThatWaitsForUnomi;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -45,129 +46,156 @@ public class InputValidationIT extends BaseIT {
private final static String CONTEXT_JS_URL = "/context.js";
private final static String CONTEXT_JSON_URL = "/context.json";
+ private final static String ERROR_MESSAGE_REQUEST_SIZE_LIMIT_EXCEEDED = "Request rejected by the server because: Request size exceed the limit";
+ private final static String ERROR_MESSAGE_INVALID_DATA_RECEIVED = "Request rejected by the server because: Invalid received data";
+
@Test
public void test_param_EventsCollectorRequestNotNull() throws IOException {
- doPOSTRequestTest(URL + EVENT_COLLECTOR_URL, null, null, 400);
- doGETRequestTest(URL + EVENT_COLLECTOR_URL, null, null, 400);
+ doPOSTRequestTest(EVENT_COLLECTOR_URL, null, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doGETRequestTest(EVENT_COLLECTOR_URL, null, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
}
@Test
public void test_param_EventsNotEmpty() throws IOException {
- doPOSTRequestTest(URL + EVENT_COLLECTOR_URL, null, "/validation/eventcollector_emptyEvents.json", 400);
- doGETRequestTest(URL + EVENT_COLLECTOR_URL, null, "/validation/eventcollector_emptyEvents.json", 400);
+ doPOSTRequestTest(EVENT_COLLECTOR_URL, null, "/validation/eventcollector_emptyEvents.json", 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doGETRequestTest(EVENT_COLLECTOR_URL, null, "/validation/eventcollector_emptyEvents.json", 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
}
@Test
public void test_param_SessionIDPattern() throws IOException {
- doPOSTRequestTest(URL + EVENT_COLLECTOR_URL, null, "/validation/eventcollector_invalidSessionId.json", 400);
- doGETRequestTest(URL + EVENT_COLLECTOR_URL, null, "/validation/eventcollector_invalidSessionId.json", 400);
+ doPOSTRequestTest(EVENT_COLLECTOR_URL, null, "/validation/eventcollector_invalidSessionId.json", 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doGETRequestTest(EVENT_COLLECTOR_URL, null, "/validation/eventcollector_invalidSessionId.json", 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
}
@Test
public void test_eventCollector_valid() throws IOException {
- doPOSTRequestTest(URL + EVENT_COLLECTOR_URL, null, "/validation/eventcollector_valid.json", 200);
- doGETRequestTest(URL + EVENT_COLLECTOR_URL, null, "/validation/eventcollector_valid.json", 200);
+ doPOSTRequestTest(EVENT_COLLECTOR_URL, null, "/validation/eventcollector_valid.json", 200, null);
+ doGETRequestTest(EVENT_COLLECTOR_URL, null, "/validation/eventcollector_valid.json", 200, null);
}
@Test
public void test_contextRequest_SessionIDPattern() throws IOException {
- doPOSTRequestTest(URL + CONTEXT_JSON_URL, null, "/validation/contextRequest_invalidSessionId.json", 400);
- doPOSTRequestTest(URL + CONTEXT_JS_URL, null, "/validation/contextRequest_invalidSessionId.json", 400);
- doGETRequestTest(URL + CONTEXT_JSON_URL, null, "/validation/contextRequest_invalidSessionId.json", 400);
- doGETRequestTest(URL + CONTEXT_JS_URL, null, "/validation/contextRequest_invalidSessionId.json", 400);
+ doPOSTRequestTest(CONTEXT_JSON_URL, null, "/validation/contextRequest_invalidSessionId.json", 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doPOSTRequestTest(CONTEXT_JS_URL, null, "/validation/contextRequest_invalidSessionId.json", 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doGETRequestTest(CONTEXT_JSON_URL, null, "/validation/contextRequest_invalidSessionId.json", 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doGETRequestTest(CONTEXT_JS_URL, null, "/validation/contextRequest_invalidSessionId.json", 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
}
@Test
public void test_contextRequest_ProfileIDPattern() throws IOException {
- doPOSTRequestTest(URL + CONTEXT_JSON_URL, null, "/validation/contextRequest_invalidProfileId.json", 400);
- doPOSTRequestTest(URL + CONTEXT_JS_URL, null, "/validation/contextRequest_invalidProfileId.json", 400);
- doGETRequestTest(URL + CONTEXT_JSON_URL, null, "/validation/contextRequest_invalidProfileId.json", 400);
- doGETRequestTest(URL + CONTEXT_JS_URL, null, "/validation/contextRequest_invalidProfileId.json", 400);
+ doPOSTRequestTest(CONTEXT_JSON_URL, null, "/validation/contextRequest_invalidProfileId.json", 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doPOSTRequestTest(CONTEXT_JS_URL, null, "/validation/contextRequest_invalidProfileId.json", 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doGETRequestTest(CONTEXT_JSON_URL, null, "/validation/contextRequest_invalidProfileId.json", 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doGETRequestTest(CONTEXT_JS_URL, null, "/validation/contextRequest_invalidProfileId.json", 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
}
@Test
public void test_contextRequest_valid() throws IOException {
- doPOSTRequestTest(URL + CONTEXT_JSON_URL, null, "/validation/contextRequest_valid.json", 200);
- doPOSTRequestTest(URL + CONTEXT_JS_URL, null, "/validation/contextRequest_valid.json", 200);
- doGETRequestTest(URL + CONTEXT_JSON_URL, null, "/validation/contextRequest_valid.json", 200);
- doGETRequestTest(URL + CONTEXT_JS_URL, null, "/validation/contextRequest_valid.json", 200);
+ doPOSTRequestTest(CONTEXT_JSON_URL, null, "/validation/contextRequest_valid.json", 200, null);
+ doPOSTRequestTest(CONTEXT_JS_URL, null, "/validation/contextRequest_valid.json", 200, null);
+ doGETRequestTest(CONTEXT_JSON_URL, null, "/validation/contextRequest_valid.json", 200, null);
+ doGETRequestTest(CONTEXT_JS_URL, null, "/validation/contextRequest_valid.json", 200, null);
+ }
+
+ @Test
+ public void test_eventCollector_request_size_exceed_limit() throws IOException {
+ doPOSTRequestTest(EVENT_COLLECTOR_URL, null, "/validation/eventcollector_request_size_invalid.json", 400, ERROR_MESSAGE_REQUEST_SIZE_LIMIT_EXCEEDED);
+ doPOSTRequestTest(EVENT_COLLECTOR_URL, null, "/validation/eventcollector_request_size_valid.json", 200, null);
}
@Test
public void test_contextJSON_SessionIDPattern() throws IOException {
- String baseUrl = URL + CONTEXT_JS_URL;
+ String baseUrl = CONTEXT_JS_URL;
String queryString = "?sessionId=" + URLEncoder.encode("<script>alert();</script>", StandardCharsets.UTF_8.toString());
- doPOSTRequestTest(baseUrl + queryString, null, null, 400);
- doGETRequestTest(baseUrl + queryString, null, null, 400);
+ doPOSTRequestTest(baseUrl + queryString, null, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doGETRequestTest(baseUrl + queryString, null, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
- baseUrl = URL + CONTEXT_JSON_URL;
- doPOSTRequestTest(baseUrl + queryString, null, null, 400);
- doGETRequestTest(baseUrl + queryString, null, null, 400);
+ baseUrl = CONTEXT_JSON_URL;
+ doPOSTRequestTest(baseUrl + queryString, null, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doGETRequestTest(baseUrl + queryString, null, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
- baseUrl = URL + CONTEXT_JS_URL;
+ baseUrl = CONTEXT_JS_URL;
queryString = "?sessionId=" + URLEncoder.encode("dummy-session-id", StandardCharsets.UTF_8.toString());
- doPOSTRequestTest(baseUrl + queryString, null, null, 200);
- doGETRequestTest(baseUrl + queryString, null, null, 200);
+ doPOSTRequestTest(baseUrl + queryString, null, null, 200, null);
+ doGETRequestTest(baseUrl + queryString, null, null, 200, null);
- baseUrl = URL + CONTEXT_JSON_URL;
- doPOSTRequestTest(baseUrl + queryString, null, null, 200);
- doGETRequestTest(baseUrl + queryString, null, null, 200);
+ baseUrl = CONTEXT_JSON_URL;
+ doPOSTRequestTest(baseUrl + queryString, null, null, 200, null);
+ doGETRequestTest(baseUrl + queryString, null, null, 200, null);
}
@Test
public void test_contextJSON_PersonaIdPattern() throws IOException {
- String baseUrl = URL + CONTEXT_JS_URL;
+ String baseUrl = CONTEXT_JS_URL;
String queryString = "?personaId=" + URLEncoder.encode("<script>alert();</script>", StandardCharsets.UTF_8.toString());
- doPOSTRequestTest(baseUrl + queryString, null, null, 400);
- doGETRequestTest(baseUrl + queryString, null, null, 400);
+ doPOSTRequestTest(baseUrl + queryString, null, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doGETRequestTest(baseUrl + queryString, null, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
- baseUrl = URL + CONTEXT_JSON_URL;
- doPOSTRequestTest(baseUrl + queryString, null, null, 400);
- doGETRequestTest(baseUrl + queryString, null, null, 400);
+ baseUrl = CONTEXT_JSON_URL;
+ doPOSTRequestTest(baseUrl + queryString, null, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doGETRequestTest(baseUrl + queryString, null, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
- baseUrl = URL + CONTEXT_JS_URL;
+ baseUrl = CONTEXT_JS_URL;
queryString = "?personaId=" + URLEncoder.encode("dummy-persona-id", StandardCharsets.UTF_8.toString());
- doPOSTRequestTest(baseUrl + queryString, null, null, 200);
- doGETRequestTest(baseUrl + queryString, null, null, 200);
+ doPOSTRequestTest(baseUrl + queryString, null, null, 200, null);
+ doGETRequestTest(baseUrl + queryString, null, null, 200, null);
- baseUrl = URL + CONTEXT_JSON_URL;
- doPOSTRequestTest(baseUrl + queryString, null,null, 200);
- doGETRequestTest(baseUrl + queryString, null, null, 200);
+ baseUrl = CONTEXT_JSON_URL;
+ doPOSTRequestTest(baseUrl + queryString, null,null, 200, null);
+ doGETRequestTest(baseUrl + queryString, null, null, 200, null);
}
@Test
public void test_cookie_profileIdPattern() throws IOException, InterruptedException {
Map<String, String> headers = new HashMap<>();
headers.put("Cookie", "context-profile-id=<script>alert();</script>");
- doPOSTRequestTest(URL + CONTEXT_JSON_URL, headers, null, 400);
- doPOSTRequestTest(URL + CONTEXT_JS_URL, headers, null, 400);
- doGETRequestTest(URL + CONTEXT_JSON_URL, headers, null, 400);
- doGETRequestTest(URL + CONTEXT_JS_URL, headers, null, 400);
+ doPOSTRequestTest(CONTEXT_JSON_URL, headers, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doPOSTRequestTest(CONTEXT_JS_URL, headers, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doGETRequestTest(CONTEXT_JSON_URL, headers, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
+ doGETRequestTest(CONTEXT_JS_URL, headers, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
headers.put("Cookie", "context-profile-id=dummy-profile-id");
- doPOSTRequestTest(URL + CONTEXT_JSON_URL, headers, null, 200);
- doPOSTRequestTest(URL + CONTEXT_JS_URL, headers, null, 200);
- doGETRequestTest(URL + CONTEXT_JSON_URL, headers, null, 200);
- doGETRequestTest(URL + CONTEXT_JS_URL, headers, null, 200);
+ doPOSTRequestTest(CONTEXT_JSON_URL, headers, null, 200, null);
+ doPOSTRequestTest(CONTEXT_JS_URL, headers, null, 200, null);
+ doGETRequestTest(CONTEXT_JSON_URL, headers, null, 200, null);
+ doGETRequestTest(CONTEXT_JS_URL, headers, null, 200, null);
+ }
+
+ private void doGETRequestTest(String uri, Map<String, String> headers, String entityResourcePath, int expectedHTTPStatusCode, String expectedErrorMessage) throws IOException {
+ // test old servlets
+ performGETRequestTest(URL + uri, headers, entityResourcePath, expectedHTTPStatusCode, expectedErrorMessage);
+ // test directly CXS endpoints
+ performGETRequestTest(URL + "/cxs" + uri, headers, entityResourcePath, expectedHTTPStatusCode, expectedErrorMessage);
}
- private void doGETRequestTest(String url, Map<String, String> headers, String entityResourcePath, int expectedHTTPStatusCode) throws IOException {
+ private void performGETRequestTest(String url, Map<String, String> headers, String entityResourcePath, int expectedHTTPStatusCode, String expectedErrorMessage) throws IOException {
if (entityResourcePath != null) {
String payload = getValidatedBundleJSON(entityResourcePath, new HashMap<>());
url += (url.contains("?") ? "&" : "?") + "payload=" + URLEncoder.encode(payload, StandardCharsets.UTF_8.toString());
}
- performRequest(new HttpGet(url), headers, expectedHTTPStatusCode);
+ performRequest(new HttpGet(url), headers, expectedHTTPStatusCode, expectedErrorMessage);
+ }
+
+ private void doPOSTRequestTest(String uri, Map<String, String> headers, String entityResourcePath, int expectedHTTPStatusCode, String expectedErrorMessage) throws IOException {
+ // test old servlets
+ performPOSTRequestTest(URL + uri, headers, entityResourcePath, expectedHTTPStatusCode, expectedErrorMessage);
+ // test directly CXS endpoints
+ performPOSTRequestTest(URL + "/cxs" + uri, headers, entityResourcePath, expectedHTTPStatusCode, expectedErrorMessage);
}
- private void doPOSTRequestTest(String url, Map<String, String> headers, String entityResourcePath, int expectedHTTPStatusCode) throws IOException {
+ private void performPOSTRequestTest(String url, Map<String, String> headers, String entityResourcePath, int expectedHTTPStatusCode, String expectedErrorMessage) throws IOException {
HttpPost request = new HttpPost(url);
+ if (headers == null) {
+ headers = new HashMap<>();
+ }
+ headers.put("Content-Type", "application/json");
if (entityResourcePath != null) {
request.setEntity(new StringEntity(getValidatedBundleJSON(entityResourcePath, new HashMap<>()), ContentType.create("application/json")));
}
- performRequest(request, headers, expectedHTTPStatusCode);
+ performRequest(request, headers, expectedHTTPStatusCode, expectedErrorMessage);
}
- private void performRequest(HttpUriRequest request, Map<String, String> headers, int expectedHTTPStatusCode) {
+ private void performRequest(HttpUriRequest request, Map<String, String> headers, int expectedHTTPStatusCode, String expectedErrorMessage) throws IOException {
CloseableHttpResponse response;
if (headers != null && !headers.isEmpty()) {
for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
@@ -182,5 +210,9 @@ public class InputValidationIT extends BaseIT {
}
assertEquals("Invalid response code", expectedHTTPStatusCode, response.getStatusLine().getStatusCode());
+ if (expectedErrorMessage != null) {
+ String responseMessage = EntityUtils.toString(response.getEntity());
+ assertEquals("Invalid response message", expectedErrorMessage, responseMessage);
+ }
}
}
diff --git a/itests/src/test/resources/validation/eventcollector_request_size_invalid.json b/itests/src/test/resources/validation/eventcollector_request_size_invalid.json
new file mode 100644
index 0000000..9813ba3
--- /dev/null
+++ b/itests/src/test/resources/validation/eventcollector_request_size_invalid.json
@@ -0,0 +1,17 @@
+{
+ "sessionId": "dummy-session-id",
+ "events":[
+ {
+ "eventType":"dummy_event_type",
+ "scope":"dummy_scope",
+ "source":{
+ "itemType":"pageView",
+ "scope":"dummy_scope",
+ "itemId":"pageView",
+ "properties":{
+ "myProperty":"Where doesWhere does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random text. Ithere does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random text. It here does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random text. It here does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random text. It here does it come from?\nContrary to popular belief, Lorem Ipsum is not simply [...]
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/itests/src/test/resources/validation/eventcollector_request_size_valid.json b/itests/src/test/resources/validation/eventcollector_request_size_valid.json
new file mode 100644
index 0000000..f146c0f
--- /dev/null
+++ b/itests/src/test/resources/validation/eventcollector_request_size_valid.json
@@ -0,0 +1,17 @@
+{
+ "sessionId": "dummy-session-id",
+ "events":[
+ {
+ "eventType":"dummy_event_type",
+ "scope":"dummy_scope",
+ "source":{
+ "itemType":"pageView",
+ "scope":"dummy_scope",
+ "itemId":"pageView",
+ "properties":{
+ "myProperty":"Where does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum c [...]
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/package/src/main/resources/etc/custom.system.properties b/package/src/main/resources/etc/custom.system.properties
index 6521528..a09c922 100644
--- a/package/src/main/resources/etc/custom.system.properties
+++ b/package/src/main/resources/etc/custom.system.properties
@@ -197,6 +197,11 @@ org.apache.unomi.profile.cookie.name=${env:UNOMI_PROFILE_COOKIE_NAME:-context-pr
org.apache.unomi.profile.cookie.maxAgeInSeconds=${env:UNOMI_PROFILE_COOKIE_MAXAGEINSECONDS:-31536000}
#Allowed profile download formats, actually only csv (horizontal and vertical), json, text and yaml are allowed.
org.apache.unomi.profile.download.formats=${env:UNOMI_PROFILE_DOWNLOAD_FORMATS:-csv,yaml,json,text}
+# This setting allow for request size (Content-length) protection. Checking that the requests do not exceed the limit.
+# It's only applied on POST requests on public endpoints like /context.js or /eventcollector. (limit set to 200000 bytes by default)
+# In case a request exceed this limit, it will be rejected with the HTTP status code 400 with an error message returned.
+# To disable this limit you can set the value to 0 or a negative value.
+org.apache.unomi.public.post.request.bytes.limit=${env:UNOMI_PUBLIC_POST_REQUEST_BYTES_LIMIT:-200000}
#######################################################################################################################
## SSH Server settings ##
diff --git a/rest/src/main/java/org/apache/unomi/rest/exception/ValidationExceptionMapper.java b/rest/src/main/java/org/apache/unomi/rest/exception/InvalidRequestExceptionMapper.java
similarity index 57%
copy from rest/src/main/java/org/apache/unomi/rest/exception/ValidationExceptionMapper.java
copy to rest/src/main/java/org/apache/unomi/rest/exception/InvalidRequestExceptionMapper.java
index a42f47d..9dfd29e 100644
--- a/rest/src/main/java/org/apache/unomi/rest/exception/ValidationExceptionMapper.java
+++ b/rest/src/main/java/org/apache/unomi/rest/exception/InvalidRequestExceptionMapper.java
@@ -16,11 +16,11 @@
*/
package org.apache.unomi.rest.exception;
+import org.apache.unomi.rest.validation.request.InvalidRequestException;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.validation.ConstraintViolationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
@@ -28,22 +28,14 @@ import javax.ws.rs.ext.Provider;
@Provider
@Component(service = ExceptionMapper.class)
-public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
- private static final Logger logger = LoggerFactory.getLogger(ValidationExceptionMapper.class.getName());
+public class InvalidRequestExceptionMapper implements ExceptionMapper<InvalidRequestException> {
+ private static final Logger logger = LoggerFactory.getLogger(InvalidRequestExceptionMapper.class.getName());
@Override
- public Response toResponse(ConstraintViolationException exception) {
- exception.getConstraintViolations().forEach(constraintViolation -> {
- if (logger.isDebugEnabled()) {
- logger.debug(String.format("value %s from %s %s", constraintViolation.getInvalidValue(),
- constraintViolation.getPropertyPath().toString(), constraintViolation.getMessage()));
- }
- logger.error(constraintViolation.getPropertyPath().toString() + " " + constraintViolation.getMessage() + ". Enable debug log "
- + "level for more informations about the invalid value received", exception);
- });
+ public Response toResponse(InvalidRequestException exception) {
+ logger.error(exception.getMessage(), exception);
- return Response.status(Response.Status.BAD_REQUEST).header("Content-Type", MediaType.TEXT_PLAIN).entity("Invalid received data")
- .build();
+ return Response.status(Response.Status.BAD_REQUEST).header("Content-Type", MediaType.TEXT_PLAIN)
+ .entity("Request rejected by the server because: " + exception.getResponseMessage()).build();
}
-
}
diff --git a/rest/src/main/java/org/apache/unomi/rest/exception/ValidationExceptionMapper.java b/rest/src/main/java/org/apache/unomi/rest/exception/ValidationExceptionMapper.java
index a42f47d..a403abf 100644
--- a/rest/src/main/java/org/apache/unomi/rest/exception/ValidationExceptionMapper.java
+++ b/rest/src/main/java/org/apache/unomi/rest/exception/ValidationExceptionMapper.java
@@ -42,8 +42,8 @@ public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViol
+ "level for more informations about the invalid value received", exception);
});
- return Response.status(Response.Status.BAD_REQUEST).header("Content-Type", MediaType.TEXT_PLAIN).entity("Invalid received data")
- .build();
+ return Response.status(Response.Status.BAD_REQUEST).header("Content-Type", MediaType.TEXT_PLAIN)
+ .entity("Request rejected by the server because: Invalid received data").build();
}
}
diff --git a/rest/src/main/java/org/apache/unomi/rest/server/RestServer.java b/rest/src/main/java/org/apache/unomi/rest/server/RestServer.java
index a296823..d910012 100644
--- a/rest/src/main/java/org/apache/unomi/rest/server/RestServer.java
+++ b/rest/src/main/java/org/apache/unomi/rest/server/RestServer.java
@@ -20,18 +20,22 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import org.apache.cxf.Bus;
import org.apache.cxf.endpoint.Server;
+import org.apache.cxf.interceptor.Interceptor;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.openapi.OpenApiCustomizer;
import org.apache.cxf.jaxrs.openapi.OpenApiFeature;
import org.apache.cxf.jaxrs.security.SimpleAuthorizingFilter;
import org.apache.cxf.jaxrs.validation.JAXRSBeanValidationInInterceptor;
import org.apache.cxf.jaxrs.validation.JAXRSBeanValidationOutInterceptor;
+import org.apache.cxf.message.Message;
+import org.apache.unomi.api.services.ConfigSharingService;
import org.apache.unomi.rest.authentication.AuthenticationFilter;
import org.apache.unomi.rest.authentication.AuthorizingInterceptor;
import org.apache.unomi.rest.authentication.RestAuthenticationConfig;
import org.apache.unomi.rest.server.provider.RetroCompatibilityParamConverterProvider;
import org.apache.unomi.rest.validation.JAXRSBeanValidationInInterceptorOverride;
-import org.apache.unomi.rest.validation.LocalBeanValidationProvider;
+import org.apache.unomi.rest.validation.BeanValidationService;
+import org.apache.unomi.rest.validation.request.RequestValidatorInterceptor;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Filter;
import org.osgi.framework.ServiceReference;
@@ -49,7 +53,6 @@ import org.slf4j.LoggerFactory;
import javax.ws.rs.ext.ExceptionMapper;
import javax.xml.namespace.QName;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Timer;
@@ -64,15 +67,19 @@ public class RestServer {
private Server server;
private BundleContext bundleContext;
private ServiceTracker jaxRSServiceTracker;
+ final List<Object> serviceBeans = new CopyOnWriteArrayList<>();
+
+ // services
private Bus serverBus;
private RestAuthenticationConfig restAuthenticationConfig;
private List<ExceptionMapper> exceptionMappers = new ArrayList<>();
+ private BeanValidationService beanValidationService;
+ private ConfigSharingService configSharingService;
+
+ // refresh
private long timeOfLastUpdate = System.currentTimeMillis();
private Timer refreshTimer = null;
private long startupDelay = 1000L;
- private LocalBeanValidationProvider localBeanValidationProvider;
-
- final List<Object> serviceBeans = new CopyOnWriteArrayList<>();
private static final QName UNOMI_REST_SERVER_END_POINT_NAME = new QName("http://rest.unomi.apache.org/", "UnomiRestServerEndPoint");
@@ -86,6 +93,11 @@ public class RestServer {
this.restAuthenticationConfig = restAuthenticationConfig;
}
+ @Reference(cardinality = ReferenceCardinality.MANDATORY)
+ public void setConfigSharingService(ConfigSharingService configSharingService) {
+ this.configSharingService = configSharingService;
+ }
+
@Reference(cardinality = ReferenceCardinality.MULTIPLE)
public void addExceptionMapper(ExceptionMapper exceptionMapper) {
this.exceptionMappers.add(exceptionMapper);
@@ -94,8 +106,8 @@ public class RestServer {
}
@Reference(cardinality = ReferenceCardinality.MANDATORY)
- public void setLocalBeanValidationProvider(LocalBeanValidationProvider localBeanValidationProvider) {
- this.localBeanValidationProvider = localBeanValidationProvider;
+ public void setBeanValidationService(BeanValidationService beanValidationService) {
+ this.beanValidationService = beanValidationService;
}
public void removeExceptionMapper(ExceptionMapper exceptionMapper) {
@@ -180,6 +192,9 @@ public class RestServer {
logger.info("JAX RS Server: Configuring server...");
+ List<Interceptor<? extends Message>> inInterceptors = new ArrayList<>();
+ List<Interceptor<? extends Message>> outInterceptors = new ArrayList<>();
+
// Build the server
ObjectMapper objectMapper = new org.apache.unomi.persistence.spi.CustomObjectMapper();
JAXRSServerFactoryBean jaxrsServerFactoryBean = new JAXRSServerFactoryBean();
@@ -216,14 +231,19 @@ public class RestServer {
jaxrsServerFactoryBean.getFeatures().add(openApiFeature);
// Hibernate validator config
- JAXRSBeanValidationInInterceptor inInterceptor = new JAXRSBeanValidationInInterceptorOverride();
- inInterceptor.setProvider(localBeanValidationProvider.get());
- jaxrsServerFactoryBean.setInInterceptors(Collections.singletonList(inInterceptor));
- JAXRSBeanValidationOutInterceptor outInterceptor = new JAXRSBeanValidationOutInterceptor();
- outInterceptor.setProvider(localBeanValidationProvider.get());
- jaxrsServerFactoryBean.setOutInterceptors(Collections.singletonList(outInterceptor));
-
- // Register service beans (end points)
+ JAXRSBeanValidationInInterceptor beanValidationInInterceptor = new JAXRSBeanValidationInInterceptorOverride();
+ JAXRSBeanValidationOutInterceptor beanValidationOutInterceptor = new JAXRSBeanValidationOutInterceptor();
+ beanValidationInInterceptor.setProvider(beanValidationService.getBeanValidationProvider());
+ beanValidationOutInterceptor.setProvider(beanValidationService.getBeanValidationProvider());
+ inInterceptors.add(beanValidationInInterceptor);
+ outInterceptors.add(beanValidationOutInterceptor);
+
+ // Request validator
+ inInterceptors.add(new RequestValidatorInterceptor(configSharingService));
+
+ // Register service beans (end points) and interceptors
+ jaxrsServerFactoryBean.setInInterceptors(inInterceptors);
+ jaxrsServerFactoryBean.setOutInterceptors(outInterceptors);
jaxrsServerFactoryBean.setServiceBeans(serviceBeans);
logger.info("JAX RS Server: Starting server with {} JAX RS EndPoints registered", serviceBeans.size());
diff --git a/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java b/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
index ab8b027..9632f3d 100644
--- a/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
+++ b/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
@@ -24,8 +24,8 @@ import org.apache.unomi.api.services.ConfigSharingService;
import org.apache.unomi.api.services.EventService;
import org.apache.unomi.api.services.PrivacyService;
import org.apache.unomi.rest.service.RestServiceUtils;
-import org.apache.unomi.rest.validation.LocalBeanValidationProvider;
-import org.apache.unomi.rest.validation.cookies.CookieWrapper;
+import org.apache.unomi.rest.validation.BeanValidationService;
+import org.apache.unomi.rest.validation.wrapper.CookieWrapper;
import org.apache.unomi.utils.Changes;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@@ -48,7 +48,7 @@ public class RestServiceUtilsImpl implements RestServiceUtils {
private ConfigSharingService configSharingService;
@Reference
- private LocalBeanValidationProvider localBeanValidationProvider;
+ private BeanValidationService beanValidationService;
@Reference
private PrivacyService privacyService;
@@ -64,7 +64,7 @@ public class RestServiceUtilsImpl implements RestServiceUtils {
if (cookies != null) {
for (Cookie cookie : cookies) {
if (configSharingService.getProperty("profileIdCookieName").equals(cookie.getName())) {
- localBeanValidationProvider.get().validateBean(new CookieWrapper(cookie.getValue()));
+ beanValidationService.getBeanValidationProvider().validateBean(new CookieWrapper(cookie.getValue()));
cookieProfileId = cookie.getValue();
}
}
diff --git a/rest/src/main/java/org/apache/unomi/rest/validation/LocalBeanValidationProvider.java b/rest/src/main/java/org/apache/unomi/rest/validation/BeanValidationService.java
similarity index 73%
rename from rest/src/main/java/org/apache/unomi/rest/validation/LocalBeanValidationProvider.java
rename to rest/src/main/java/org/apache/unomi/rest/validation/BeanValidationService.java
index 5785142..b691fe7 100644
--- a/rest/src/main/java/org/apache/unomi/rest/validation/LocalBeanValidationProvider.java
+++ b/rest/src/main/java/org/apache/unomi/rest/validation/BeanValidationService.java
@@ -19,7 +19,14 @@ package org.apache.unomi.rest.validation;
import org.apache.cxf.validation.BeanValidationProvider;
-public interface LocalBeanValidationProvider {
+/**
+ * This service is just here to provide the CXF BeanValidationProvider in the OSGI context as a service.
+ */
+public interface BeanValidationService {
- BeanValidationProvider get();
+ /**
+ * Get the CXF BeanValidationProvider instance
+ * @return the CXF BeanValidationProvider instance
+ */
+ BeanValidationProvider getBeanValidationProvider();
}
diff --git a/rest/src/main/java/org/apache/unomi/rest/validation/impl/LocalBeanValidationProviderImpl.java b/rest/src/main/java/org/apache/unomi/rest/validation/impl/BeanValidationServiceImpl.java
similarity index 85%
rename from rest/src/main/java/org/apache/unomi/rest/validation/impl/LocalBeanValidationProviderImpl.java
rename to rest/src/main/java/org/apache/unomi/rest/validation/impl/BeanValidationServiceImpl.java
index 7362f5a..2bccc18 100644
--- a/rest/src/main/java/org/apache/unomi/rest/validation/impl/LocalBeanValidationProviderImpl.java
+++ b/rest/src/main/java/org/apache/unomi/rest/validation/impl/BeanValidationServiceImpl.java
@@ -19,15 +19,17 @@ package org.apache.unomi.rest.validation.impl;
import org.apache.cxf.validation.BeanValidationProvider;
import org.apache.unomi.rest.validation.HibernateValidationProviderResolver;
-import org.apache.unomi.rest.validation.LocalBeanValidationProvider;
+import org.apache.unomi.rest.validation.BeanValidationService;
import org.hibernate.validator.HibernateValidator;
+import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
-@Component(service = LocalBeanValidationProvider.class)
-public class LocalBeanValidationProviderImpl implements LocalBeanValidationProvider {
+@Component(service = BeanValidationService.class)
+public class BeanValidationServiceImpl implements BeanValidationService {
private BeanValidationProvider beanValidationProvider;
- public LocalBeanValidationProviderImpl() {
+ @Activate
+ public void activate() {
// This is a TCCL (Thread context class loader) hack to for the javax.el.FactoryFinder to use Class.forName(className)
// instead of tccl.loadClass(className) to load the class "com.sun.el.ExpressionFactoryImpl".
ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader();
@@ -40,7 +42,7 @@ public class LocalBeanValidationProviderImpl implements LocalBeanValidationProvi
}
}
- public BeanValidationProvider get() {
+ public BeanValidationProvider getBeanValidationProvider() {
return beanValidationProvider;
}
}
diff --git a/rest/src/main/java/org/apache/unomi/rest/validation/cookies/CookieWrapper.java b/rest/src/main/java/org/apache/unomi/rest/validation/request/InvalidRequestException.java
similarity index 59%
copy from rest/src/main/java/org/apache/unomi/rest/validation/cookies/CookieWrapper.java
copy to rest/src/main/java/org/apache/unomi/rest/validation/request/InvalidRequestException.java
index 87cad89..b41a657 100644
--- a/rest/src/main/java/org/apache/unomi/rest/validation/cookies/CookieWrapper.java
+++ b/rest/src/main/java/org/apache/unomi/rest/validation/request/InvalidRequestException.java
@@ -14,23 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.unomi.rest.validation.cookies;
-
-import org.apache.unomi.api.utils.ValidationPattern;
-
-import javax.validation.constraints.Pattern;
+package org.apache.unomi.rest.validation.request;
/**
- * This class exists to allow to wrap a cookie value into an object and validate this object trough
- * the bean validation
+ * Exception throw when the request exceed the configured limit size
*/
-public class CookieWrapper {
+public class InvalidRequestException extends RuntimeException {
- @Pattern(regexp = ValidationPattern.TEXT_VALID_CHARACTERS_PATTERN)
- private String cookie;
+ /**
+ * This will be the message return to the client in the body request in case
+ */
+ private final String responseMessage;
- public CookieWrapper(String cookie) {
- this.cookie = cookie;
+ public InvalidRequestException(String message, String responseMessage) {
+ super(message);
+ this.responseMessage = responseMessage;
}
+ public String getResponseMessage() {
+ return responseMessage;
+ }
}
diff --git a/rest/src/main/java/org/apache/unomi/rest/validation/request/RequestValidatorInterceptor.java b/rest/src/main/java/org/apache/unomi/rest/validation/request/RequestValidatorInterceptor.java
new file mode 100644
index 0000000..4cc2343
--- /dev/null
+++ b/rest/src/main/java/org/apache/unomi/rest/validation/request/RequestValidatorInterceptor.java
@@ -0,0 +1,69 @@
+/*
+ * 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.unomi.rest.validation.request;
+
+import org.apache.cxf.helpers.HttpHeaderHelper;
+import org.apache.cxf.interceptor.Fault;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.phase.AbstractPhaseInterceptor;
+import org.apache.cxf.phase.Phase;
+import org.apache.unomi.api.services.ConfigSharingService;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This interceptor is made to validate that the incoming post request on the public endpoints do not exceed the configured limit.
+ */
+public class RequestValidatorInterceptor extends AbstractPhaseInterceptor<Message> {
+
+ private final ConfigSharingService configSharingService;
+
+ private static final List<String> PROTECTED_URIS = new ArrayList<String>(){{
+ add("/cxs/eventcollector");
+ add("/cxs/context.js");
+ add("/cxs/context.json");
+ }};
+
+ public RequestValidatorInterceptor(ConfigSharingService configSharingService) {
+ super(Phase.RECEIVE);
+ this.configSharingService = configSharingService;
+ }
+
+ @Override
+ public void handleMessage(Message message) throws Fault {
+ int bytesLimit = (int) configSharingService.getProperty("publicPostRequestBytesLimit");
+ if (bytesLimit > 0) {
+ HttpServletRequest request = ((HttpServletRequest) message.get("HTTP.REQUEST"));
+ if (request != null &&
+ request.getMethod().equalsIgnoreCase("POST") &&
+ PROTECTED_URIS.contains(request.getRequestURI())) {
+ try {
+ int contentLength = Integer.parseInt(request.getHeader(HttpHeaderHelper.CONTENT_LENGTH));
+ if (contentLength > bytesLimit) {
+ throw new InvalidRequestException(
+ String.format("Incoming POST request blocked because exceeding maximum bytes size allowed on: %s (limit: %s, request size: %s)",
+ request.getRequestURI(), bytesLimit, contentLength), "Request size exceed the limit");
+ }
+ } catch (NumberFormatException nfe) {
+ // no content length, nothing will be deserialize by jackson, so nothing to check.
+ }
+ }
+ }
+ }
+}
diff --git a/rest/src/main/java/org/apache/unomi/rest/validation/cookies/CookieWrapper.java b/rest/src/main/java/org/apache/unomi/rest/validation/wrapper/CookieWrapper.java
similarity index 96%
rename from rest/src/main/java/org/apache/unomi/rest/validation/cookies/CookieWrapper.java
rename to rest/src/main/java/org/apache/unomi/rest/validation/wrapper/CookieWrapper.java
index 87cad89..6a5040e 100644
--- a/rest/src/main/java/org/apache/unomi/rest/validation/cookies/CookieWrapper.java
+++ b/rest/src/main/java/org/apache/unomi/rest/validation/wrapper/CookieWrapper.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.unomi.rest.validation.cookies;
+package org.apache.unomi.rest.validation.wrapper;
import org.apache.unomi.api.utils.ValidationPattern;
diff --git a/wab/src/main/java/org/apache/unomi/web/ContextServlet.java b/wab/src/main/java/org/apache/unomi/web/ContextServlet.java
index 8588c0b..84bd840 100644
--- a/wab/src/main/java/org/apache/unomi/web/ContextServlet.java
+++ b/wab/src/main/java/org/apache/unomi/web/ContextServlet.java
@@ -42,6 +42,7 @@ public class ContextServlet extends HttpServlet {
private String profileIdCookieName = "context-profile-id";
private String profileIdCookieDomain;
private int profileIdCookieMaxAgeInSeconds = MAX_COOKIE_AGE_IN_SECONDS;
+ private int publicPostRequestBytesLimit = 200000;
private ConfigSharingService configSharingService;
@@ -51,6 +52,7 @@ public class ContextServlet extends HttpServlet {
configSharingService.setProperty("profileIdCookieName", profileIdCookieName);
configSharingService.setProperty("profileIdCookieDomain", profileIdCookieDomain);
configSharingService.setProperty("profileIdCookieMaxAgeInSeconds", (Integer) profileIdCookieMaxAgeInSeconds);
+ configSharingService.setProperty("publicPostRequestBytesLimit", publicPostRequestBytesLimit);
logger.info("ContextServlet initialized.");
}
@@ -76,8 +78,11 @@ public class ContextServlet extends HttpServlet {
this.profileIdCookieMaxAgeInSeconds = profileIdCookieMaxAgeInSeconds;
}
+ public void setPublicPostRequestBytesLimit(int publicPostRequestBytesLimit) {
+ this.publicPostRequestBytesLimit = publicPostRequestBytesLimit;
+ }
+
public void setConfigSharingService(ConfigSharingService configSharingService) {
this.configSharingService = configSharingService;
}
-
}
diff --git a/wab/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/wab/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index b4ea919..d06d5e0 100644
--- a/wab/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/wab/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -36,6 +36,7 @@
<cm:property name="contextserver.profileIdCookieName" value="context-profile-id"/>
<cm:property name="contextserver.profileIdCookieMaxAgeInSeconds" value="31536000"/> <!-- 1 year by default -->
<cm:property name="allowed.profile.download.formats" value="yaml"/>
+ <cm:property name="public.post.request.bytes.limit" value="200000"/>
</cm:default-properties>
</cm:property-placeholder>
@@ -45,6 +46,7 @@
<property name="profileIdCookieDomain" value="${web.contextserver.domain}" />
<property name="profileIdCookieName" value="${web.contextserver.profileIdCookieName}"/>
<property name="profileIdCookieMaxAgeInSeconds" value="${web.contextserver.profileIdCookieMaxAgeInSeconds}"/>
+ <property name="publicPostRequestBytesLimit" value="${web.public.post.request.bytes.limit}"/>
</bean>
<service id="contextServletService" ref="contextServlet">
diff --git a/wab/src/main/resources/org.apache.unomi.web.cfg b/wab/src/main/resources/org.apache.unomi.web.cfg
index b786782..5167916 100644
--- a/wab/src/main/resources/org.apache.unomi.web.cfg
+++ b/wab/src/main/resources/org.apache.unomi.web.cfg
@@ -23,4 +23,9 @@ contextserver.profileIdCookieName=${org.apache.unomi.profile.cookie.name:-contex
# This setting controls the maximum age of the profile cookie. By default it is set to a year.
contextserver.profileIdCookieMaxAgeInSeconds=${org.apache.unomi.profile.cookie.maxAgeInSeconds:-31536000}
#Allowed profile download formats, actually only csv (horizontal and vertical), json, text and yaml are allowed.
-allowed.profile.download.formats=${org.apache.unomi.profile.download.formats:-csv,yaml,json,text}
\ No newline at end of file
+allowed.profile.download.formats=${org.apache.unomi.profile.download.formats:-csv,yaml,json,text}
+# This setting allow for request size (Content-length) protection. Checking that the requests do not exceed the limit.
+# It's only applied on POST requests on public endpoints like /context.js or /eventcollector. (limit set to 200000 bytes by default)
+# In case a request exceed this limit, it will be rejected with the HTTP status code 400 and an error message returned.
+# To disable this limit you can set the value to 0 or a negative value.
+public.post.request.bytes.limit=${org.apache.unomi.public.post.request.bytes.limit:-200000}
\ No newline at end of file