Skip to content

Commit a4aeae1

Browse files
authored
Fix NPE thrown by MultiValueMap endpoints (#63)
* Implement tests for the HECRestController - Expects same behavior for sendEvents endpoints * Initial design for MultiValueMapRequest - Will replace RequestBodyCleaner * Implement AcknowledgementResponse - Aligns the response creation with event endpoints - Changes the MultiValueMap parameters from raw types to "String, String" - Sadly the ResponseEntities use the builder pattern - Removes the old RequestBodyCleaner.java * Add JMeter test for application/x-www-form-urlencoded events * Update JavaDoc for MultiValueMapRequest * Fix variable names in HECRestControllerTest - "variable" renamed to "responseEntity" - "jsonNodeResponseEntity" renamed to "expectedResponseEntity" * Make sure that MultiValueMapRequest.asCleanedJsonString does not modify the underlying field - Added unit test for this, tested before and after the copying of MultiValueMapRequest.multiValueMap * Fix variable names in HECRestControllerTest
1 parent 3195e49 commit a4aeae1

10 files changed

Lines changed: 681 additions & 125 deletions

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* HTTP Event Capture to RFC5424 CFE_16
3+
* Copyright (C) 2021-2025 Suomen Kanuuna Oy
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*
18+
*
19+
* Additional permission under GNU Affero General Public License version 3
20+
* section 7
21+
*
22+
* If you modify this Program, or any covered work, by linking or combining it
23+
* with other code, such other code is not for that reason alone subject to any
24+
* of the requirements of the GNU Affero GPL version 3 as long as this Program
25+
* is the same Program as licensed from Suomen Kanuuna Oy without any additional
26+
* modifications.
27+
*
28+
* Supplemented terms under GNU Affero General Public License version 3
29+
* section 7
30+
*
31+
* Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified
32+
* versions must be marked as "Modified version of" The Program.
33+
*
34+
* Names of the licensors and authors may not be used for publicity purposes.
35+
*
36+
* No rights are granted for use of trade names, trademarks, or service marks
37+
* which are in The Program if any.
38+
*
39+
* Licensee must indemnify licensors and authors for any liability that these
40+
* contractual assumptions impose on licensors and authors.
41+
*
42+
* To the extent this program is licensed as part of the Commercial versions of
43+
* Teragrep, the applicable Commercial License may apply to this file if you as
44+
* a licensee so wish it.
45+
*/
46+
package com.teragrep.cfe_16;
47+
48+
import java.util.Objects;
49+
import java.util.Set;
50+
import org.springframework.util.LinkedMultiValueMap;
51+
import org.springframework.util.MultiValueMap;
52+
53+
/**
54+
* Cleans the request body so, that only the body of the request is left in the string. This is needed when calling the
55+
* endpoint that consumes MediaType.APPLICATION_FORM_URLENCODED_VALUE <b>The actual body needed is stored as a key, and
56+
* not a value for a key</b> Example of body sent as a parameter: {channel=[CHANNEL_11111], {"sourcetype":
57+
* "mysourcetype", "event": "Hello, world!"}=[]}
58+
*/
59+
public final class MultiValueMapRequest {
60+
61+
private final MultiValueMap<String, String> multiValueMap;
62+
63+
public MultiValueMapRequest(final MultiValueMap<String, String> multiValueMap) {
64+
this.multiValueMap = multiValueMap;
65+
}
66+
67+
/**
68+
* @return Example of cleaned body: {"sourcetype": "mysourcetype", "event": "Hello, world!"}
69+
* @throws IllegalStateException if the MultiValueMap contains more than one key after the channel has been removed
70+
*/
71+
public String asCleanedJsonString() throws IllegalStateException {
72+
final String valueToReturn;
73+
//
74+
final MultiValueMap<String, String> copyOfMultiValueMap = new LinkedMultiValueMap<>(multiValueMap);
75+
// Remove the channel, if it is present
76+
copyOfMultiValueMap.remove("channel");
77+
final Set<String> keys = copyOfMultiValueMap.keySet();
78+
79+
// Check if the parameters contains more entries than expected from the request
80+
if (keys.size() != 1) {
81+
throw new IllegalStateException(
82+
"application/x-www-form-urlencoded request contains more parameters than expected"
83+
);
84+
}
85+
else {
86+
// Get the value of the first key
87+
valueToReturn = keys.iterator().next();
88+
}
89+
90+
return valueToReturn;
91+
}
92+
93+
@Override
94+
public boolean equals(final Object o) {
95+
if (o == null || getClass() != o.getClass()) {
96+
return false;
97+
}
98+
final MultiValueMapRequest that = (MultiValueMapRequest) o;
99+
return Objects.equals(multiValueMap, that.multiValueMap);
100+
}
101+
102+
@Override
103+
public int hashCode() {
104+
return Objects.hashCode(multiValueMap);
105+
}
106+
}

src/main/java/com/teragrep/cfe_16/RequestBodyCleaner.java renamed to src/main/java/com/teragrep/cfe_16/response/AcknowledgementResponse.java

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,39 +43,47 @@
4343
* Teragrep, the applicable Commercial License may apply to this file if you as
4444
* a licensee so wish it.
4545
*/
46-
package com.teragrep.cfe_16;
46+
package com.teragrep.cfe_16.response;
4747

48-
import org.springframework.stereotype.Component;
48+
import java.util.Objects;
49+
import org.springframework.http.MediaType;
50+
import org.springframework.http.ResponseEntity;
51+
import tools.jackson.databind.JsonNode;
52+
import tools.jackson.databind.ObjectMapper;
53+
import tools.jackson.databind.node.ObjectNode;
4954

50-
import java.util.regex.Pattern;
55+
public final class AcknowledgementResponse implements Response {
5156

52-
/*
53-
* Cleans the request body so, that only the body of the request is left in the string.
54-
* This is needed when calling the endpoint that consumes MediaType.APPLICATION_FORM_URLENCODED_VALUE
55-
* Example of body sent as a parameter: {channel=[CHANNEL_11111], {"sourcetype": "mysourcetype", "event": "Hello, world!"}=[]}
56-
* Example of cleaned body returned by the cleanAckRequestBody(): {"sourcetype": "mysourcetype", "event": "Hello, world!"}
57-
* TODO: Try to implement a better way to get the body of the request.
58-
*
59-
*/
60-
@Component
61-
public class RequestBodyCleaner {
57+
/**
58+
* This field is expected to be in a format provided by
59+
* {@link com.teragrep.cfe_16.Acknowledgements#getRequestedAckStatuses(String, String, JsonNode)}
60+
*/
61+
private final JsonNode acknowledgementsJsonNode;
6262

63-
public String cleanAckRequestBody(String body, String channel) {
64-
String bodyWithoutChannel = body.replaceAll("channel=\\[" + Pattern.quote(channel) + "\\]\\, ", "");
65-
String bodywithoutChannelLastCharRemoved = removeLastChar(bodyWithoutChannel);
66-
67-
String cleanedBody = bodywithoutChannelLastCharRemoved.substring(1);
63+
public AcknowledgementResponse(final JsonNode acknowledgementsJsonNode) {
64+
this.acknowledgementsJsonNode = acknowledgementsJsonNode;
65+
}
6866

69-
cleanedBody = removeEqualsArrayFromEnd(cleanedBody);
67+
@Override
68+
public ResponseEntity<JsonNode> asJsonNodeResponseEntity() {
69+
final ObjectMapper objectMapper = new ObjectMapper();
70+
final ObjectNode jsonNode = objectMapper.createObjectNode();
71+
jsonNode.set("acks", this.acknowledgementsJsonNode);
7072

71-
return cleanedBody;
73+
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(jsonNode);
7274
}
7375

74-
private String removeLastChar(String str) {
75-
return str.substring(0, str.length() - 1);
76+
@Override
77+
public boolean equals(final Object o) {
78+
if (o == null || getClass() != o.getClass()) {
79+
return false;
80+
}
81+
final AcknowledgementResponse that = (AcknowledgementResponse) o;
82+
return Objects.equals(acknowledgementsJsonNode, that.acknowledgementsJsonNode);
7683
}
7784

78-
private String removeEqualsArrayFromEnd(String str) {
79-
return str.replace("=[]", "");
85+
@Override
86+
public int hashCode() {
87+
return Objects.hashCode(acknowledgementsJsonNode);
8088
}
8189
}

src/main/java/com/teragrep/cfe_16/rest/HECRestController.java

Lines changed: 82 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,16 @@
4545
*/
4646
package com.teragrep.cfe_16.rest;
4747

48+
import com.teragrep.cfe_16.MultiValueMapRequest;
49+
import com.teragrep.cfe_16.bo.HeaderInfo;
50+
import com.teragrep.cfe_16.response.ExceptionEvent;
51+
import com.teragrep.cfe_16.response.ExceptionEventContext;
52+
import com.teragrep.cfe_16.response.ExceptionJsonResponse;
53+
import com.teragrep.cfe_16.response.JsonResponse;
54+
import java.util.UUID;
55+
import tools.jackson.core.JacksonException;
4856
import tools.jackson.databind.JsonNode;
4957
import tools.jackson.databind.ObjectMapper;
50-
import com.teragrep.cfe_16.RequestBodyCleaner;
5158
import com.teragrep.cfe_16.config.Configuration;
5259
import com.teragrep.cfe_16.response.Response;
5360
import com.teragrep.cfe_16.service.HECService;
@@ -69,37 +76,52 @@ public class HECRestController {
6976
@Autowired
7077
private HECService service;
7178

72-
@Autowired
73-
private RequestBodyCleaner requestBodyCleaner;
74-
7579
@Autowired
7680
private Configuration configuration;
7781

78-
@SuppressWarnings("rawtypes")
7982
@RequestMapping(
8083
value = "services/collector",
8184
method = RequestMethod.POST,
8285
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
8386
produces = MediaType.APPLICATION_JSON_VALUE
8487
)
8588
public ResponseEntity<JsonNode> sendEvents(
86-
HttpServletRequest request,
87-
@RequestBody MultiValueMap body,
88-
@RequestParam(required = false) String channel
89+
final HttpServletRequest request,
90+
@RequestBody final MultiValueMap<String, String> body,
91+
@RequestParam(required = false) final String channel
8992
) {
90-
// TODO: Try to think an alternative way to implement getting the body of the
91-
// call
92-
final String eventInJson = requestBodyCleaner.cleanAckRequestBody(body.toString(), channel);
93-
94-
long t1 = System.nanoTime();
95-
final Response response = service.sendEvents(request, channel, eventInJson);
96-
long t2 = System.nanoTime();
97-
long dt = t2 - t1;
98-
double us = (double) dt / 1000.0;
99-
if (this.configuration.getPrintTimes()) {
100-
LOGGER.info("sendEvents took <{}> nanoseconds, that is <{}> microseconds", dt, us);
93+
ResponseEntity<JsonNode> responseEntity;
94+
try {
95+
final MultiValueMapRequest eventInJson = new MultiValueMapRequest(body);
96+
final long t1 = System.nanoTime();
97+
final Response response = service.sendEvents(request, channel, eventInJson.asCleanedJsonString());
98+
final long t2 = System.nanoTime();
99+
final long dt = t2 - t1;
100+
final double us = (double) dt / 1000.0;
101+
if (this.configuration.getPrintTimes()) {
102+
LOGGER.info("sendEvents took <{}> nanoseconds, that is <{}> microseconds", dt, us);
103+
}
104+
responseEntity = response.asJsonNodeResponseEntity();
101105
}
102-
return response.asJsonNodeResponseEntity();
106+
catch (final IllegalStateException illegalStateException) {
107+
final HeaderInfo headerInfo = new HeaderInfo(request);
108+
final ExceptionEventContext exceptionEventContext = new ExceptionEventContext(
109+
headerInfo,
110+
request.getHeader("user-agent"),
111+
request.getRequestURI(),
112+
request.getRemoteHost()
113+
);
114+
final ExceptionEvent event = new ExceptionEvent(
115+
exceptionEventContext,
116+
UUID.randomUUID(),
117+
illegalStateException
118+
);
119+
event.logException();
120+
final Response response = new ExceptionJsonResponse(event);
121+
responseEntity = response.asJsonNodeResponseEntity();
122+
}
123+
124+
return responseEntity;
103125
}
104126

105127
@RequestMapping(
@@ -132,79 +154,84 @@ public ResponseEntity<JsonNode> sendEvents(
132154
},
133155
consumes = MediaType.APPLICATION_JSON_VALUE
134156
)
135-
public JsonNode getAcksWithPostMethod(
157+
public ResponseEntity<JsonNode> getAcksWithPostMethod(
136158
@RequestBody JsonNode requestedAcksInJson,
137159
HttpServletRequest request,
138160
@RequestParam(required = false) String channel
139161
) {
140162

141163
long t1 = System.nanoTime();
142-
JsonNode response = service.getAcks(request, channel, requestedAcksInJson);
164+
final Response response = service.getAcks(request, channel, requestedAcksInJson);
143165
long t2 = System.nanoTime();
144166
long dt = t2 - t1;
145167
double us = (double) dt / 1000.0;
146168
if (this.configuration.getPrintTimes()) {
147169
LOGGER.info("getAcks took <{}> nanoseconds, that is <{}> microseconds", dt, us);
148170
}
149-
return response;
171+
return response.asJsonNodeResponseEntity();
150172
}
151173

152-
// @LogAnnotation(type = LogType.METRIC_DURATION)
153-
@SuppressWarnings("rawtypes")
154174
@RequestMapping(
155175
value = "services/collector/ack",
156176
method = {
157177
RequestMethod.POST, RequestMethod.GET
158178
},
159179
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
160180
)
161-
public @ResponseBody JsonNode getAcks(
162-
@RequestBody MultiValueMap body,
163-
HttpServletRequest request,
164-
@RequestParam(required = false) String channel
181+
public ResponseEntity<JsonNode> getAcks(
182+
@RequestBody final MultiValueMap<String, String> body,
183+
final HttpServletRequest request,
184+
@RequestParam(required = false) final String channel
165185
) {
166-
// TODO: Try to think an alternative way to implement getting the body of the
167-
// call
168-
String bodyString = requestBodyCleaner.cleanAckRequestBody(body.toString(), channel);
186+
ResponseEntity<JsonNode> responseEntity;
169187

170-
JsonNode requestedAcksInJson = null;
171188
try {
172-
requestedAcksInJson = objectMapper.readValue(bodyString, JsonNode.class);
189+
final MultiValueMapRequest multiValueMapRequest = new MultiValueMapRequest(body);
190+
191+
final JsonNode requestedAcksInJson = objectMapper
192+
.readValue(multiValueMapRequest.asCleanedJsonString(), JsonNode.class);
193+
194+
final long t1 = System.nanoTime();
195+
final Response response = service.getAcks(request, channel, requestedAcksInJson);
196+
final long t2 = System.nanoTime();
197+
final long dt = t2 - t1;
198+
final double us = (double) dt / 1000.0;
199+
if (this.configuration.getPrintTimes()) {
200+
LOGGER.info("getAcks took <{}> nanoseconds, that is <{}> microseconds", dt, us);
201+
}
202+
responseEntity = new JsonResponse(response.toString()).asJsonNodeResponseEntity();
173203
}
174-
catch (Exception e) {
175-
// TODO: handle the error in a proper way
176-
LOGGER.warn("Failed to handle response: ", e);
204+
catch (final IllegalStateException | JacksonException exception) {
205+
final HeaderInfo headerInfo = new HeaderInfo(request);
206+
final ExceptionEventContext exceptionEventContext = new ExceptionEventContext(
207+
headerInfo,
208+
request.getHeader("user-agent"),
209+
request.getRequestURI(),
210+
request.getRemoteHost()
211+
);
212+
final ExceptionEvent event = new ExceptionEvent(exceptionEventContext, UUID.randomUUID(), exception);
213+
event.logException();
214+
final Response response = new ExceptionJsonResponse(event);
215+
responseEntity = response.asJsonNodeResponseEntity();
177216
}
178217

179-
long t1 = System.nanoTime();
180-
JsonNode response = service.getAcks(request, channel, requestedAcksInJson);
181-
long t2 = System.nanoTime();
182-
long dt = t2 - t1;
183-
double us = (double) dt / 1000.0;
184-
if (this.configuration.getPrintTimes()) {
185-
LOGGER.info("getAcks took <{}> nanoseconds, that is <{}> microseconds", dt, us);
186-
}
187-
return response;
218+
return responseEntity;
188219
}
189220

190-
@SuppressWarnings("rawtypes")
191221
@RequestMapping(
192222
value = "services/collector/event",
193223
method = RequestMethod.POST,
194224
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
195225
)
196226
public ResponseEntity<JsonNode> sendEventsWithFormatOption(
197-
HttpServletRequest request,
198-
@RequestBody MultiValueMap body,
199-
@RequestParam(required = false) String channel
227+
final HttpServletRequest request,
228+
@RequestBody final MultiValueMap<String, String> body,
229+
@RequestParam(required = false) final String channel
200230
) {
201-
202-
// TODO: Try to think an alternative way to implement getting the body of the
203-
// call
204-
String eventInJson = requestBodyCleaner.cleanAckRequestBody(body.toString(), channel);
231+
final MultiValueMapRequest multiValueMapRequest = new MultiValueMapRequest(body);
205232

206233
long t1 = System.nanoTime();
207-
final Response response = service.sendEvents(request, channel, eventInJson);
234+
final Response response = service.sendEvents(request, channel, multiValueMapRequest.asCleanedJsonString());
208235
long t2 = System.nanoTime();
209236
long dt = t2 - t1;
210237
double us = (double) dt / 1000.0;

src/main/java/com/teragrep/cfe_16/service/HECService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public interface HECService {
7171
* @param requestedAcksInJson
7272
* @return
7373
*/
74-
public JsonNode getAcks(HttpServletRequest request, String channel, JsonNode requestedAcksInJson);
74+
public Response getAcks(HttpServletRequest request, String channel, JsonNode requestedAcksInJson);
7575

7676
/**
7777
* Ping.

0 commit comments

Comments
 (0)