Skip to content

Commit 3bcca84

Browse files
authored
Merge pull request #18 from awslabs/servlet-improvements
Servlet filters support
2 parents 7a16f04 + 407a813 commit 3bcca84

File tree

31 files changed

+2247
-282
lines changed

31 files changed

+2247
-282
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,15 @@ public String echoServletHeaders(@Context HttpServletRequest context) {
141141
return "servlet";
142142
}
143143
```
144+
145+
## Servlet Filters
146+
You can register [`Filter`](https://docs.oracle.com/javaee/7/api/javax/servlet/Filter.html) implementations by implementing a `StartupsHandler` as defined in the `AwsLambdaServletContainerHandler` class. The `onStartup` methods receives a reference to the current `ServletContext`.
147+
148+
```java
149+
handler.onStartup(c -> {
150+
FilterRegistration.Dynamic registration = c.addFilter("CustomHeaderFilter", CustomHeaderFilter.class);
151+
// update the registration to map to a path
152+
registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
153+
// servlet name mappings are disabled and will throw an exception
154+
});
155+
```

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
*/
3131
public abstract class LambdaContainerHandler<RequestType, ResponseType, ContainerRequestType, ContainerResponseType> {
3232

33+
//-------------------------------------------------------------
34+
// Constants
35+
//-------------------------------------------------------------
36+
37+
public static final String SERVER_INFO = "aws-serverless-java-container";
38+
3339
//-------------------------------------------------------------
3440
// Variables - Private
3541
//-------------------------------------------------------------
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
5+
* with the License. A copy of the License is located at
6+
*
7+
* http://aws.amazon.com/apache2.0/
8+
*
9+
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
10+
* OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
11+
* and limitations under the License.
12+
*/
13+
package com.amazonaws.serverless.proxy.internal.servlet;
14+
15+
import java.util.Map;
16+
17+
/**
18+
* This implementation of the <code>FilterChainManager</code> object uses the <code>AwsServletContext</code> object
19+
* to extract a list of <code>FilterHolder</code>s
20+
*/
21+
public class AwsFilterChainManager extends FilterChainManager<AwsServletContext> {
22+
23+
//-------------------------------------------------------------
24+
// Constructors
25+
//-------------------------------------------------------------
26+
27+
/**
28+
* Creates a new FilterChainManager for the given servlet context
29+
* @param context An initialized AwsServletContext object
30+
*/
31+
AwsFilterChainManager(AwsServletContext context) {
32+
super(context);
33+
}
34+
35+
//-------------------------------------------------------------
36+
// Implementation - FilterChainManager
37+
//-------------------------------------------------------------
38+
39+
/**
40+
* Returns the filter holders stored in the <code>AwsServletContext</code> object
41+
* @return The map of filter holders
42+
*/
43+
protected Map<String, FilterHolder> getFilterHolders() {
44+
return servletContext.getFilterHolders();
45+
}
46+
}
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
/*
2+
* Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
5+
* with the License. A copy of the License is located at
6+
*
7+
* http://aws.amazon.com/apache2.0/
8+
*
9+
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
10+
* OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
11+
* and limitations under the License.
12+
*/
13+
package com.amazonaws.serverless.proxy.internal.servlet;
14+
15+
import com.amazonaws.services.lambda.runtime.Context;
16+
17+
import javax.servlet.AsyncContext;
18+
import javax.servlet.DispatcherType;
19+
import javax.servlet.ServletContext;
20+
import javax.servlet.http.Cookie;
21+
import javax.servlet.http.HttpServletRequest;
22+
import javax.servlet.http.HttpSession;
23+
import java.io.UnsupportedEncodingException;
24+
import java.net.URLEncoder;
25+
import java.nio.charset.StandardCharsets;
26+
import java.util.*;
27+
import java.util.stream.Collectors;
28+
29+
30+
/**
31+
* Base HttpServletRequest object. This object exposes some utility methods to work with request values such as headers
32+
* and query string parameters. New implementations of <code>HttpServletRequest</code> can extend this class to reuse
33+
* the utility methods
34+
*/
35+
public abstract class AwsHttpServletRequest implements HttpServletRequest {
36+
37+
//-------------------------------------------------------------
38+
// Constants
39+
//-------------------------------------------------------------
40+
41+
static final String HEADER_KEY_VALUE_SEPARATOR = "=";
42+
static final String HEADER_VALUE_SEPARATOR = ";";
43+
static final String FORM_DATA_SEPARATOR = "&";
44+
static final String DEFAULT_CHARACTER_ENCODING = "UTF-8";
45+
static final String HEADER_DATE_FORMAT = "EEE, d MMM yyyy HH:mm:ss z";
46+
static final String ENCODING_VALUE_KEY = "charset";
47+
48+
// We need this to pickup the protocol from the CloudFront header since Lambda doesn't receive this
49+
// information from anywhere else
50+
static final String CF_PROTOCOL_HEADER_NAME = "CloudFront-Forwarded-Proto";
51+
52+
53+
//-------------------------------------------------------------
54+
// Variables - Private
55+
//-------------------------------------------------------------
56+
57+
private Context lambdaContext;
58+
private Map<String, Object> attributes;
59+
60+
61+
//-------------------------------------------------------------
62+
// Constructors
63+
//-------------------------------------------------------------
64+
65+
/**
66+
* Protected constructors for implemnenting classes. This should be called first with the context received from
67+
* AWS Lambda
68+
* @param lambdaContext The Lambda function context. This object is used for utility methods such as log
69+
*/
70+
AwsHttpServletRequest(Context lambdaContext) {
71+
this.lambdaContext = lambdaContext;
72+
attributes = new HashMap<>();
73+
}
74+
75+
//-------------------------------------------------------------
76+
// Implementation - HttpServletRequest
77+
//-------------------------------------------------------------
78+
79+
@Override
80+
public String getRequestedSessionId() {
81+
throw new UnsupportedOperationException();
82+
}
83+
84+
85+
@Override
86+
public HttpSession getSession(boolean b) {
87+
return null;
88+
}
89+
90+
91+
@Override
92+
public HttpSession getSession() {
93+
return null;
94+
}
95+
96+
97+
@Override
98+
public String changeSessionId() {
99+
return null;
100+
}
101+
102+
103+
@Override
104+
public boolean isRequestedSessionIdValid() {
105+
return false;
106+
}
107+
108+
109+
@Override
110+
public boolean isRequestedSessionIdFromCookie() {
111+
return false;
112+
}
113+
114+
115+
@Override
116+
public boolean isRequestedSessionIdFromURL() {
117+
return false;
118+
}
119+
120+
121+
@Override
122+
public boolean isRequestedSessionIdFromUrl() {
123+
return false;
124+
}
125+
126+
127+
//-------------------------------------------------------------
128+
// Implementation - ServletRequest
129+
//-------------------------------------------------------------
130+
131+
132+
@Override
133+
public Object getAttribute(String s) {
134+
return attributes.get(s);
135+
}
136+
137+
138+
@Override
139+
public Enumeration<String> getAttributeNames() {
140+
return Collections.enumeration(attributes.keySet());
141+
}
142+
143+
144+
@Override
145+
public String getServerName() {
146+
return "lambda.amazonaws.com";
147+
}
148+
149+
150+
@Override
151+
public int getServerPort() {
152+
return 0;
153+
}
154+
155+
156+
@Override
157+
public void setAttribute(String s, Object o) {
158+
attributes.put(s, o);
159+
}
160+
161+
162+
@Override
163+
public void removeAttribute(String s) {
164+
attributes.remove(s);
165+
}
166+
167+
168+
@Override
169+
public String getLocalName() {
170+
return "lambda.amazonaws.com";
171+
}
172+
173+
174+
@Override
175+
public String getLocalAddr() {
176+
return null;
177+
}
178+
179+
180+
@Override
181+
public int getLocalPort() {
182+
return 0;
183+
}
184+
185+
186+
@Override
187+
public ServletContext getServletContext() {
188+
return AwsServletContext.getInstance(lambdaContext);
189+
}
190+
191+
192+
@Override
193+
public boolean isAsyncStarted() {
194+
return false;
195+
}
196+
197+
198+
@Override
199+
public boolean isAsyncSupported() {
200+
return false;
201+
}
202+
203+
204+
@Override
205+
public AsyncContext getAsyncContext() {
206+
return null;
207+
}
208+
209+
210+
@Override
211+
public DispatcherType getDispatcherType() {
212+
return DispatcherType.REQUEST;
213+
}
214+
215+
216+
//-------------------------------------------------------------
217+
// Methods - Protected
218+
//-------------------------------------------------------------
219+
220+
/**
221+
* Given the Cookie header value, parses it and creates a Cookie object
222+
* @param headerValue The string value of the HTTP Cookie header
223+
* @return An array of Cookie objects from the header
224+
*/
225+
protected Cookie[] parseCookieHeaderValue(String headerValue) {
226+
227+
List<Map.Entry<String, String>> parsedHeaders = this.parseHeaderValue(headerValue);
228+
229+
return parsedHeaders.stream()
230+
.filter(e -> e.getKey() != null)
231+
.map(e -> new Cookie(e.getKey(), e.getValue()))
232+
.toArray(Cookie[]::new);
233+
}
234+
235+
/**
236+
* Given a map of key/values query string parameters from API Gateway, creates a query string as it would have
237+
* been in the original url.
238+
* @param parameters A Map<String, String> of query string parameters
239+
* @return The generated query string for the URI
240+
*/
241+
protected String generateQueryString(Map<String, String> parameters) {
242+
if (parameters == null || parameters.size() == 0) {
243+
return null;
244+
}
245+
246+
return parameters.keySet().stream()
247+
.map(key -> {
248+
String newKey = key;
249+
String newValue = parameters.get(key);
250+
try {
251+
if (!URLEncoder.encode(newKey, StandardCharsets.UTF_8.name()).equals(newKey)) {
252+
newKey = URLEncoder.encode(key, StandardCharsets.UTF_8.name());
253+
}
254+
255+
if (!URLEncoder.encode(newValue, StandardCharsets.UTF_8.name()).equals(newValue)) {
256+
newValue = URLEncoder.encode(newValue, StandardCharsets.UTF_8.name());
257+
}
258+
} catch (UnsupportedEncodingException e) {
259+
lambdaContext.getLogger().log("Could not URLEncode: " + newKey + "\n" + e.getLocalizedMessage());
260+
e.printStackTrace();
261+
262+
}
263+
return newKey + "=" + newValue;
264+
})
265+
.collect(Collectors.joining("&"));
266+
}
267+
268+
269+
/**
270+
* Generic method to parse an HTTP header value and split it into a list of key/values for all its components.
271+
* When the property in the header does not specify a key the key field in the output pair is null and only the value
272+
* is populated. For example, The header <code>Accept: application/json; application/xml</code> will contain two
273+
* key value pairs with key null and the value set to application/json and application/xml respectively.
274+
*
275+
* @param headerValue The string value for the HTTP header
276+
* @return A list of SimpleMapEntry objects with all of the possible values for the header.
277+
*/
278+
protected List<Map.Entry<String, String>> parseHeaderValue(String headerValue) {
279+
List<Map.Entry<String, String>> values = new ArrayList<>();
280+
if (headerValue == null) {
281+
return values;
282+
}
283+
for (String kv : headerValue.split(HEADER_VALUE_SEPARATOR)) {
284+
String[] kvSplit = kv.split(HEADER_KEY_VALUE_SEPARATOR);
285+
286+
if (kvSplit.length != 2) {
287+
values.add(new AbstractMap.SimpleEntry<>(null, kv.trim()));
288+
} else {
289+
values.add(new AbstractMap.SimpleEntry<>(kvSplit[0].trim(), kvSplit[1].trim()));
290+
}
291+
}
292+
return values;
293+
}
294+
}

0 commit comments

Comments
 (0)