Skip to content

Commit 7678d3c

Browse files
committed
Changes to address query string encoding/reading from issues #146, #154, and #156
1 parent 0a81cde commit 7678d3c

File tree

10 files changed

+367
-128
lines changed

10 files changed

+367
-128
lines changed

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequest.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -290,17 +290,25 @@ protected Cookie[] parseCookieHeaderValue(String headerValue) {
290290
* @param parameters A Map<String, String> of query string parameters
291291
* @return The generated query string for the URI
292292
*/
293-
protected String generateQueryString(Map<String, String> parameters) {
293+
protected String generateQueryString(EncodingQueryStringParameterMap parameters) {
294294
if (parameters == null || parameters.size() == 0) {
295295
return null;
296296
}
297297
if (queryString != null) {
298298
return queryString;
299299
}
300300

301-
queryString = parameters.keySet().stream()
302-
.map(key -> key + "=" + parameters.get(key))
303-
.collect(Collectors.joining("&"));
301+
StringBuilder queryStringBuilder = new StringBuilder();
302+
303+
parameters.keySet().stream().forEach(k -> parameters.get(k).stream().forEach(v -> {
304+
queryStringBuilder.append("&");
305+
queryStringBuilder.append(k);
306+
queryStringBuilder.append("=");
307+
queryStringBuilder.append(v);
308+
}));
309+
310+
queryString = queryStringBuilder.toString();
311+
queryString = queryString.substring(1); // remove the first & - faster to do it here than adding logic in the Lambda
304312
return queryString;
305313
}
306314

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public class AwsProxyHttpServletRequest extends AwsHttpServletRequest {
8686
private SecurityContext securityContext;
8787
private Map<String, List<String>> urlEncodedFormParameters;
8888
private Map<String, Part> multipartFormParameters;
89+
private EncodingQueryStringParameterMap queryStringParameters;
8990
private static Logger log = LoggerFactory.getLogger(AwsProxyHttpServletRequest.class);
9091
private ContainerConfig config;
9192

@@ -104,6 +105,9 @@ public AwsProxyHttpServletRequest(AwsProxyRequest awsProxyRequest, Context lambd
104105
this.request = awsProxyRequest;
105106
this.securityContext = awsSecurityContext;
106107
this.config = config;
108+
109+
this.queryStringParameters = new EncodingQueryStringParameterMap(config.isQueryStringCaseSensitive(), config.getUriEncoding());
110+
this.queryStringParameters.putAllMapEncoding(request.getQueryStringParameters());
107111
}
108112

109113

@@ -222,7 +226,7 @@ public String getContextPath() {
222226

223227
@Override
224228
public String getQueryString() {
225-
return this.generateQueryString(request.getQueryStringParameters());
229+
return this.generateQueryString(queryStringParameters);
226230
}
227231

228232

@@ -419,7 +423,11 @@ public ServletInputStream getInputStream()
419423

420424
@Override
421425
public String getParameter(String s) {
422-
String queryStringParameter = getQueryStringParameterCaseInsensitive(s);
426+
String paramKey = s;
427+
if (config.isQueryStringCaseSensitive()) {
428+
paramKey = paramKey.toLowerCase(Locale.getDefault());
429+
}
430+
String queryStringParameter = queryStringParameters.getFirst(paramKey);
423431
if (queryStringParameter != null) {
424432
return queryStringParameter;
425433
}
@@ -436,33 +444,31 @@ public String getParameter(String s) {
436444
@Override
437445
public Enumeration<String> getParameterNames() {
438446
List<String> paramNames = new ArrayList<>();
439-
if (request.getQueryStringParameters() != null) {
440-
paramNames.addAll(request.getQueryStringParameters().keySet());
441-
}
447+
paramNames.addAll(queryStringParameters.keySet());
442448
paramNames.addAll(getFormUrlEncodedParametersMap().keySet());
443449
return Collections.enumeration(paramNames);
444450
}
445451

446452

447453
@Override
454+
@SuppressFBWarnings("PZLA_PREFER_ZERO_LENGTH_ARRAYS") // suppressing this as according to the specs we should be returning null here if we can't find params
448455
public String[] getParameterValues(String s) {
449-
List<String> values = new ArrayList<>();
450-
String queryStringValue = getQueryStringParameterCaseInsensitive(s);
451-
if (queryStringValue != null) {
452-
values.add(queryStringValue);
456+
String paramKey = s;
457+
if (config.isQueryStringCaseSensitive()) {
458+
paramKey = paramKey.toLowerCase(Locale.getDefault());
453459
}
460+
List<String> values = new ArrayList<>();
461+
values.addAll(queryStringParameters.get(paramKey));
454462

455463
String[] formBodyValues = getFormBodyParameterCaseInsensitive(s);
456464
if (formBodyValues != null) {
457465
values.addAll(Arrays.asList(formBodyValues));
458466
}
459467

460468
if (values.size() == 0) {
461-
return new String[0];
469+
return null;
462470
} else {
463-
String[] valuesArray = new String[values.size()];
464-
valuesArray = values.toArray(valuesArray);
465-
return valuesArray;
471+
return values.toArray(new String[0]);
466472
}
467473
}
468474

@@ -472,24 +478,14 @@ public Map<String, String[]> getParameterMap() {
472478
Map<String, String[]> output = new HashMap<>();
473479

474480
Map<String, List<String>> params = getFormUrlEncodedParametersMap();
481+
params.entrySet().stream().parallel().forEach(e -> {
482+
output.put(e.getKey(), e.getValue().toArray(new String[0]));
483+
});
475484

476-
if (request.getQueryStringParameters() != null) {
477-
for (Map.Entry<String, String> entry : request.getQueryStringParameters().entrySet()) {
478-
if (params.containsKey(entry.getKey()) && !params.get(entry.getKey()).contains(entry.getValue())) {
479-
params.get(entry.getKey()).add(entry.getValue());
480-
} else {
481-
List<String> valueList = new ArrayList<>();
482-
valueList.add(entry.getValue());
483-
params.put(entry.getKey(), valueList);
484-
}
485-
}
486-
}
485+
queryStringParameters.keySet().stream().parallel().forEach(e -> {
486+
output.put(e, queryStringParameters.get(e).toArray(new String[0]));
487+
});
487488

488-
for (Map.Entry<String, List<String>> entry : params.entrySet()) {
489-
String[] valuesArray = new String[entry.getValue().size()];
490-
valuesArray = entry.getValue().toArray(valuesArray);
491-
output.put(entry.getKey(), valuesArray);
492-
}
493489
return output;
494490
}
495491

@@ -631,6 +627,14 @@ public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse se
631627
return null;
632628
}
633629

630+
//-------------------------------------------------------------
631+
// Methods - Protected
632+
//-------------------------------------------------------------
633+
634+
protected EncodingQueryStringParameterMap getQueryParametersMap() {
635+
return queryStringParameters;
636+
}
637+
634638
//-------------------------------------------------------------
635639
// Methods - Private
636640
//-------------------------------------------------------------
@@ -656,21 +660,6 @@ private String getHeaderCaseInsensitive(String key) {
656660
return null;
657661
}
658662

659-
660-
private String getQueryStringParameterCaseInsensitive(String key) {
661-
if (request.getQueryStringParameters() == null) {
662-
return null;
663-
}
664-
665-
for (String requestParamKey : request.getQueryStringParameters().keySet()) {
666-
if (key.toLowerCase(Locale.ENGLISH).equals(requestParamKey.toLowerCase(Locale.ENGLISH))) {
667-
return request.getQueryStringParameters().get(requestParamKey);
668-
}
669-
}
670-
return null;
671-
}
672-
673-
674663
private String[] getFormBodyParameterCaseInsensitive(String key) {
675664
List<String> values = getFormUrlEncodedParametersMap().get(key);
676665
if (values != null) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.amazonaws.serverless.proxy.internal.servlet;
2+
3+
4+
import com.amazonaws.serverless.proxy.internal.SecurityUtils;
5+
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
9+
import javax.ws.rs.core.MultivaluedHashMap;
10+
11+
import java.io.UnsupportedEncodingException;
12+
import java.net.URLEncoder;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
import java.util.Locale;
16+
import java.util.Map;
17+
18+
19+
/**
20+
* query string parameters container. The main purpose of this object is to apply the required transformation to query
21+
* string parameters coming from API Gateway. Specifically, parameters names and values need to be un-escaped and url-encoded
22+
* again.
23+
*/
24+
public class EncodingQueryStringParameterMap extends MultivaluedHashMap<String, String> {
25+
26+
private boolean isCaseSensitive;
27+
private String encoding;
28+
private static Logger log = LoggerFactory.getLogger(EncodingQueryStringParameterMap.class);
29+
30+
private static final long serialVersionUID = 42L;
31+
32+
/**
33+
* Creates a new instance of the parameters map. This allows the configuration to specify whether query string parameter
34+
* names should be case sensitive or not.
35+
* @param caseSensitive Whether parameters should be case sensitive. If the value is <code>true</code>, parameters names
36+
* are automatically trnasformed to lower case as they are added to the map.
37+
*/
38+
public EncodingQueryStringParameterMap(final boolean caseSensitive, final String enc) {
39+
isCaseSensitive = caseSensitive;
40+
encoding = enc;
41+
}
42+
43+
public void putAllMapEncoding(final Map<String, String> parametersMap) {
44+
if (parametersMap == null) {
45+
return;
46+
}
47+
parametersMap.entrySet().stream().forEach(e -> {
48+
String key = e.getKey();
49+
if (!isCaseSensitive) {
50+
key = key.toLowerCase(Locale.getDefault());
51+
}
52+
key = unescapeAndEncode(key);
53+
54+
String value = unescapeAndEncode(e.getValue());
55+
putSingle(key, value);
56+
});
57+
}
58+
59+
public void putAllMultiValuedMapEncoding(final MultivaluedHashMap<String, String> parametersMap) {
60+
if (parametersMap == null) {
61+
return;
62+
}
63+
parametersMap.entrySet().stream().forEach(e -> {
64+
String key = e.getKey();
65+
if (!isCaseSensitive) {
66+
key = key.toLowerCase(Locale.getDefault());
67+
}
68+
key = unescapeAndEncode(key);
69+
70+
List newValueList = new ArrayList();
71+
// we don't expect the values to be many so we don't parallel()
72+
e.getValue().stream().forEach(v -> {
73+
newValueList.add(unescapeAndEncode(v));
74+
});
75+
76+
put(key, newValueList);
77+
});
78+
}
79+
80+
private String unescapeAndEncode(final String value) {
81+
try {
82+
return URLEncoder.encode(value, encoding);
83+
} catch (UnsupportedEncodingException e) {
84+
log.error("Could not url encode parameter value: " + SecurityUtils.crlf(value), e);
85+
}
86+
87+
return null;
88+
}
89+
}

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ public AwsProxyRequestBuilder body(String body) {
147147
return this;
148148
}
149149

150+
public AwsProxyRequestBuilder nullBody() {
151+
this.request.setBody(null);
152+
return this;
153+
}
154+
150155
public AwsProxyRequestBuilder body(Object body) {
151156
if (request.getHeaders() != null && request.getHeaders().get(HttpHeaders.CONTENT_TYPE).equals(MediaType.APPLICATION_JSON)) {
152157
try {

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/ContainerConfig.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public static ContainerConfig defaultConfig() {
2121
configuration.setConsolidateSetCookieHeaders(true);
2222
configuration.setUseStageAsServletContext(false);
2323
configuration.setValidFilePaths(DEFAULT_FILE_PATHS);
24+
configuration.setQueryStringCaseSensitive(false);
2425

2526
return configuration;
2627
}
@@ -36,6 +37,7 @@ public static ContainerConfig defaultConfig() {
3637
private boolean useStageAsServletContext;
3738
private List<String> validFilePaths;
3839
private List<String> customDomainNames;
40+
private boolean queryStringCaseSensitive;
3941

4042
public ContainerConfig() {
4143
validFilePaths = new ArrayList<>();
@@ -197,4 +199,24 @@ public List<String> getCustomDomainNames() {
197199
public void enableLocalhost() {
198200
customDomainNames.add("localhost");
199201
}
202+
203+
204+
/**
205+
* Whether query string parameters in the request should be case sensitive or not. By default
206+
* this is set to <code>false</code> for backward compatibility.
207+
* @return <code>true</code> if the parameter matching algorithm is case sensitive
208+
*/
209+
public boolean isQueryStringCaseSensitive() {
210+
return queryStringCaseSensitive;
211+
}
212+
213+
214+
/**
215+
* Sets whether query string parameter names should be treated as case sensitive. The default
216+
* value of this option is <code>false</code> for backward compatibility.
217+
* @param queryStringCaseSensitive Tells the framework to treat query string parmaeter names as case sensitive
218+
*/
219+
public void setQueryStringCaseSensitive(boolean queryStringCaseSensitive) {
220+
this.queryStringCaseSensitive = queryStringCaseSensitive;
221+
}
200222
}

aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequestTest.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public class AwsHttpServletRequestTest {
2727
.header(HttpHeaders.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8").build();
2828
private static final AwsProxyRequest queryString = new AwsProxyRequestBuilder("/test", "GET")
2929
.queryString("one", "two").queryString("three", "four").build();
30+
private static final AwsProxyRequest encodedQueryString = new AwsProxyRequestBuilder("/test", "GET")
31+
.queryString("one", "two").queryString("json", "{\"name\":\"faisal\"}").build();
3032

3133
private static final MockLambdaContext mockContext = new MockLambdaContext();
3234

@@ -72,12 +74,23 @@ public void headers_parseHeaderValue_complexAccept() {
7274
}
7375

7476
@Test
75-
public void queyrString_generateQueryString_validQuery() {
77+
public void queryString_generateQueryString_validQuery() {
7678
AwsProxyHttpServletRequest request = new AwsProxyHttpServletRequest(queryString, mockContext, null, config);
7779

78-
String parsedString = request.generateQueryString(queryString.getQueryStringParameters());
79-
assertEquals("one=two&three=four", parsedString);
80+
String parsedString = request.generateQueryString(request.getQueryParametersMap());
81+
System.out.println(parsedString);
82+
assertTrue(parsedString.contains("one=two"));
83+
assertTrue(parsedString.contains("three=four"));
84+
assertTrue(parsedString.contains("&") && parsedString.indexOf("&") > 0 && parsedString.indexOf("&") < parsedString.length());
85+
}
86+
87+
@Test
88+
public void queryStringWithEncodedParams_generateQueryString_validQuery() {
89+
AwsProxyHttpServletRequest request = new AwsProxyHttpServletRequest(encodedQueryString, mockContext, null, config);
8090

81-
// TODO test url encoding, wrong parameters
91+
String parsedString = request.generateQueryString(request.getQueryParametersMap());
92+
assertTrue(parsedString.contains("one=two"));
93+
assertTrue(parsedString.contains("json=%7B%22name%22%3A%22faisal%22%7D"));
94+
assertTrue(parsedString.contains("&") && parsedString.indexOf("&") > 0 && parsedString.indexOf("&") < parsedString.length());
8295
}
8396
}

aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyHandlerFilter.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package com.amazonaws.serverless.proxy.jersey;
22

33

4-
import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest;
54
import com.amazonaws.serverless.proxy.internal.testutils.Timer;
65
import com.amazonaws.serverless.proxy.jersey.suppliers.AwsProxyServletRequestSupplier;
7-
import com.amazonaws.serverless.proxy.model.ApiGatewayRequestContext;
86

97
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
108
import org.glassfish.jersey.internal.MapPropertiesDelegate;
@@ -59,7 +57,7 @@ public class JerseyHandlerFilter implements Filter, Container {
5957

6058
/**
6159
* Constructs a new handler filter with a Jax RS application object.
62-
* @param jaxApplication
60+
* @param jaxApplication The JAX RS application to load
6361
*/
6462
JerseyHandlerFilter(Application jaxApplication) {
6563
Timer.start("JERSEY_FILTER_CONSTRUCTOR");
@@ -138,7 +136,7 @@ private ContainerRequest servletRequestToContainerRequest(ServletRequest request
138136

139137
ContainerRequest requestContext = new ContainerRequest(
140138
null, // jersey uses "/" by default
141-
uriBuilder.build(),
139+
uriBuilder.build(), //requestUri,
142140
servletRequest.getMethod().toUpperCase(Locale.ENGLISH),
143141
(SecurityContext)servletRequest.getAttribute(JAX_SECURITY_CONTEXT_PROPERTY),
144142
apiGatewayProperties);
@@ -161,6 +159,7 @@ private ContainerRequest servletRequestToContainerRequest(ServletRequest request
161159
//requestContext.header(headerKey, servletRequest.getHeader(headerKey));
162160
requestContext.getHeaders().add(headerKey, servletRequest.getHeader(headerKey));
163161
}
162+
164163
Timer.stop("JERSEY_SERVLET_REQUEST_TO_CONTAINER");
165164
return requestContext;
166165
}

0 commit comments

Comments
 (0)