Skip to content

Commit 628ed1b

Browse files
authored
Merge pull request #60 from awslabs/servlet-improvements
Merging servlet improvements fixes for release 0.7
2 parents c72f07c + 292af78 commit 628ed1b

File tree

21 files changed

+551
-65
lines changed

21 files changed

+551
-65
lines changed

aws-serverless-java-container-core/pom.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@
7373
<version>4.5.3</version>
7474
<scope>test</scope>
7575
</dependency>
76-
7776
</dependencies>
7877

7978
</project>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public class AwsProxyExceptionHandler
7373

7474
@Override
7575
public AwsProxyResponse handle(Throwable ex) {
76+
log.error("Called exception handler for:", ex);
7677
if (ex instanceof InvalidRequestEventException) {
7778
return new AwsProxyResponse(500, headers, getErrorJson(INTERNAL_SERVER_ERROR));
7879
} else {

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33

44
/**
5-
* Configuration paramters used by the <code>RequestReader</code> and <code>ResponseWriter</code> objects.
5+
* Configuration parameters for the framework
66
*/
77
public class ContainerConfig {
8+
public static final String DEFAULT_URI_ENCODING = "UTF-8";
89

910
public static ContainerConfig defaultConfig() {
1011
ContainerConfig configuration = new ContainerConfig();
1112
configuration.setStripBasePath(false);
13+
configuration.setUriEncoding(DEFAULT_URI_ENCODING);
14+
configuration.setConsolidateSetCookieHeaders(true);
1215

1316
return configuration;
1417
}
@@ -19,6 +22,8 @@ public static ContainerConfig defaultConfig() {
1922

2023
private String serviceBasePath;
2124
private boolean stripBasePath;
25+
private String uriEncoding;
26+
private boolean consolidateSetCookieHeaders;
2227

2328

2429
//-------------------------------------------------------------
@@ -30,6 +35,11 @@ public String getServiceBasePath() {
3035
}
3136

3237

38+
/**
39+
* Configures a base path that can be strippped from the request path before passing it to the frameowkr-specific implementation. This can be used to
40+
* remove API Gateway's base path mappings from the request.
41+
* @param serviceBasePath The base path mapping to be removed.
42+
*/
3343
public void setServiceBasePath(String serviceBasePath) {
3444
// clean up base path before setting it, we want a "/" at the beginning but not at the end.
3545
String finalBasePath = serviceBasePath;
@@ -48,9 +58,45 @@ public boolean isStripBasePath() {
4858
}
4959

5060

61+
/**
62+
* Whether this framework should strip the base path mapping specified with the {@link #setServiceBasePath(String)} method from a request before
63+
* passing it to the framework-specific implementations
64+
* @param stripBasePath
65+
*/
5166
public void setStripBasePath(boolean stripBasePath) {
5267
this.stripBasePath = stripBasePath;
5368
}
5469

5570

71+
public String getUriEncoding() {
72+
return uriEncoding;
73+
}
74+
75+
76+
/**
77+
* Sets the charset used to URLEncode and Decode request paths.
78+
* @param uriEncoding The charset. By default this is set to UTF-8
79+
*/
80+
public void setUriEncoding(String uriEncoding) {
81+
this.uriEncoding = uriEncoding;
82+
}
83+
84+
85+
public boolean isConsolidateSetCookieHeaders() {
86+
return consolidateSetCookieHeaders;
87+
}
88+
89+
90+
/**
91+
* Tells the library to consolidate multiple Set-Cookie headers into a single Set-Cookie header with multiple, comma-separated values. This is allowed
92+
* by the RFC 2109 (https://tools.ietf.org/html/rfc2109). However, since not all clients support this, we consider it optional. When this value is set
93+
* to true the framework will consolidate all Set-Cookie headers into a single header, when it's set to false, the framework will only return the first
94+
* Set-Cookie header specified in a response.
95+
*
96+
* Because API Gateway needs header keys to be unique, we give an option to configure this.
97+
* @param consolidateSetCookieHeaders Whether to consolidate the cookie headers or not.
98+
*/
99+
public void setConsolidateSetCookieHeaders(boolean consolidateSetCookieHeaders) {
100+
this.consolidateSetCookieHeaders = consolidateSetCookieHeaders;
101+
}
56102
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313
package com.amazonaws.serverless.proxy.internal.servlet;
1414

15+
import com.amazonaws.serverless.proxy.internal.model.ContainerConfig;
1516
import com.amazonaws.services.lambda.runtime.Context;
1617

1718
import org.slf4j.Logger;
@@ -24,6 +25,7 @@
2425
import javax.servlet.http.HttpServletRequest;
2526
import javax.servlet.http.HttpSession;
2627
import java.io.UnsupportedEncodingException;
28+
import java.net.URLDecoder;
2729
import java.net.URLEncoder;
2830
import java.nio.charset.StandardCharsets;
2931
import java.util.AbstractMap;
@@ -320,4 +322,15 @@ protected List<Map.Entry<String, String>> parseHeaderValue(String headerValue) {
320322
}
321323
return values;
322324
}
325+
326+
protected String decodeRequestPath(String requestPath, ContainerConfig config) {
327+
try {
328+
return URLDecoder.decode(requestPath, config.getUriEncoding());
329+
} catch (UnsupportedEncodingException ex) {
330+
log.error("Could not URL decode the request path, configured encoding not supported: " + config.getUriEncoding(), ex);
331+
// we do not fail at this.
332+
return requestPath;
333+
}
334+
335+
}
323336
}

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

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
*/
1313
package com.amazonaws.serverless.proxy.internal.servlet;
1414

15+
import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler;
16+
1517
import org.slf4j.Logger;
1618
import org.slf4j.LoggerFactory;
1719

@@ -94,15 +96,18 @@ public void addCookie(Cookie cookie) {
9496
if (cookie.getDomain() != null && !"".equals(cookie.getDomain().trim())) {
9597
cookieData += "; Domain=" + cookie.getDomain();
9698
}
97-
cookieData += "; Max-Age=" + cookie.getMaxAge();
9899

99-
// we always set the timezone to GMT
100-
TimeZone gmtTimeZone = TimeZone.getTimeZone(COOKIE_DEFAULT_TIME_ZONE);
101-
Calendar currentTimestamp = Calendar.getInstance(gmtTimeZone);
102-
currentTimestamp.add(Calendar.SECOND, cookie.getMaxAge());
103-
SimpleDateFormat cookieDateFormatter = new SimpleDateFormat(HEADER_DATE_PATTERN);
104-
cookieDateFormatter.setTimeZone(gmtTimeZone);
105-
cookieData += "; Expires=" + cookieDateFormatter.format(currentTimestamp.getTime());
100+
if (cookie.getMaxAge() > 0) {
101+
cookieData += "; Max-Age=" + cookie.getMaxAge();
102+
103+
// we always set the timezone to GMT
104+
TimeZone gmtTimeZone = TimeZone.getTimeZone(COOKIE_DEFAULT_TIME_ZONE);
105+
Calendar currentTimestamp = Calendar.getInstance(gmtTimeZone);
106+
currentTimestamp.add(Calendar.SECOND, cookie.getMaxAge());
107+
SimpleDateFormat cookieDateFormatter = new SimpleDateFormat(HEADER_DATE_PATTERN);
108+
cookieDateFormatter.setTimeZone(gmtTimeZone);
109+
cookieData += "; Expires=" + cookieDateFormatter.format(currentTimestamp.getTime());
110+
}
106111

107112
setHeader(HttpHeaders.SET_COOKIE, cookieData, false);
108113
}
@@ -410,7 +415,22 @@ byte[] getAwsResponseBodyBytes() {
410415
Map<String, String> getAwsResponseHeaders() {
411416
Map<String, String> responseHeaders = new HashMap<>();
412417
for (String header : getHeaderNames()) {
413-
responseHeaders.put(header, headers.getFirst(header));
418+
// special behavior for set cookie
419+
// RFC 2109 allows for a comma separated list of cookies in one Set-Cookie header: https://tools.ietf.org/html/rfc2109
420+
if (header.equals(HttpHeaders.SET_COOKIE) && LambdaContainerHandler.getContainerConfig().isConsolidateSetCookieHeaders()) {
421+
StringBuilder cookieHeader = new StringBuilder();
422+
for (String cookieValue : headers.get(header)) {
423+
if (cookieHeader.length() > 0) {
424+
cookieHeader.append(",");
425+
}
426+
427+
cookieHeader.append(" ").append(cookieValue);
428+
}
429+
430+
responseHeaders.put(header, cookieHeader.toString());
431+
} else {
432+
responseHeaders.put(header, headers.getFirst(header));
433+
}
414434
}
415435

416436
return responseHeaders;

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package com.amazonaws.serverless.proxy.internal.servlet;
1414

1515

16+
import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler;
1617
import com.amazonaws.serverless.proxy.internal.model.AwsProxyRequest;
1718
import com.amazonaws.services.lambda.runtime.Context;
1819

@@ -184,7 +185,7 @@ public String getPathInfo() {
184185
if (!pathInfo.startsWith("/")) {
185186
pathInfo = "/" + pathInfo;
186187
}
187-
return pathInfo;
188+
return decodeRequestPath(pathInfo, LambdaContainerHandler.getContainerConfig());
188189
}
189190

190191

@@ -248,7 +249,7 @@ public StringBuffer getRequestURL() {
248249

249250
@Override
250251
public String getServletPath() {
251-
return request.getPath();
252+
return decodeRequestPath(request.getPath(), LambdaContainerHandler.getContainerConfig());
252253
}
253254

254255
@Override

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

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
*/
1313
package com.amazonaws.serverless.proxy.internal.servlet;
1414

15+
import org.slf4j.Logger;
16+
import org.slf4j.LoggerFactory;
17+
1518
import javax.servlet.FilterChain;
1619
import javax.servlet.ServletException;
1720
import javax.servlet.ServletRequest;
@@ -33,7 +36,9 @@ public class FilterChainHolder implements FilterChain {
3336
//-------------------------------------------------------------
3437

3538
private List<FilterHolder> filters;
36-
private int currentFilter;
39+
int currentFilter;
40+
41+
private Logger log = LoggerFactory.getLogger(FilterChainHolder.class);
3742

3843

3944
//-------------------------------------------------------------
@@ -43,7 +48,7 @@ public class FilterChainHolder implements FilterChain {
4348
/**
4449
* Creates a new empty <code>FilterChainHolder</code>
4550
*/
46-
public FilterChainHolder() {
51+
FilterChainHolder() {
4752
this(new ArrayList<>());
4853
}
4954

@@ -52,9 +57,9 @@ public FilterChainHolder() {
5257
* Creates a new instance of a filter chain holder
5358
* @param allFilters A populated list of <code>FilterHolder</code> objects
5459
*/
55-
public FilterChainHolder(List<FilterHolder> allFilters) {
60+
FilterChainHolder(List<FilterHolder> allFilters) {
5661
filters = allFilters;
57-
currentFilter = -1;
62+
resetHolder();
5863
}
5964

6065

@@ -66,16 +71,20 @@ public FilterChainHolder(List<FilterHolder> allFilters) {
6671
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException, ServletException {
6772
currentFilter++;
6873
if (filters == null || filters.size() == 0 || currentFilter > filters.size() - 1) {
74+
log.debug("Could not find filters to execute, returning");
6975
return;
7076
}
7177
// TODO: We do not check for async filters here
7278

7379
FilterHolder holder = filters.get(currentFilter);
7480

81+
// lazily initialize filters when they are needed
7582
if (!holder.isFilterInitialized()) {
7683
holder.init();
7784
}
85+
log.debug("Starting filter " + holder.getFilterName());
7886
holder.getFilter().doFilter(servletRequest, servletResponse, this);
87+
log.debug("Executed filter " + holder.getFilterName());
7988
}
8089

8190

@@ -87,7 +96,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
8796
* Add a filter to the chain.
8897
* @param newFilter The filter to be added at the end of the chain
8998
*/
90-
public void addFilter(FilterHolder newFilter) {
99+
void addFilter(FilterHolder newFilter) {
91100
filters.add(newFilter);
92101
}
93102

@@ -96,7 +105,7 @@ public void addFilter(FilterHolder newFilter) {
96105
* Returns the number of filters loaded in the chain holder
97106
* @return The number of filters in the chain holder. If the filter chain is null then this will return 0
98107
*/
99-
public int filterCount() {
108+
int filterCount() {
100109
if (filters == null) {
101110
return 0;
102111
} else {
@@ -110,11 +119,29 @@ public int filterCount() {
110119
* @param idx The index in the chain. Use the <code>filterCount</code> method to get the filter count
111120
* @return A populated FilterHolder object
112121
*/
113-
public FilterHolder getFilter(int idx) {
122+
FilterHolder getFilter(int idx) {
114123
if (filters == null) {
115124
return null;
116125
} else {
117126
return filters.get(idx);
118127
}
119128
}
129+
130+
131+
/**
132+
* Returns the list of filters in this chain.
133+
* @return The list of filters
134+
*/
135+
public List<FilterHolder> getFilters() {
136+
return filters;
137+
}
138+
139+
140+
/**
141+
* Resets the chain holder to the beginning of the filter chain. This method is used from the constructor as well as when
142+
* the {@link FilterChainManager} return a holder from the cache.
143+
*/
144+
private void resetHolder() {
145+
currentFilter = -1;
146+
}
120147
}

0 commit comments

Comments
 (0)