diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 1dfb971d9..025848302 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -22,12 +22,12 @@ jobs: - uses: actions/checkout@v2 - name: Build latest run: ./gha_build.sh jersey true true - - name: Build Jersey 2.26 - run: ./gha_build.sh jersey false false -Djersey.version=2.26 - name: Build Jersey 2.27 run: ./gha_build.sh jersey false false -Djersey.version=2.27 - name: Build Jersey 2.28 run: ./gha_build.sh jersey false false -Djersey.version=2.28 + - name: Build Jersey 2.29 + run: ./gha_build.sh jersey false false -Djersey.version=2.29.1 build_spark: name: Build and test Spark @@ -45,17 +45,15 @@ jobs: steps: - uses: actions/checkout@v2 - name: Build latest - run: ./gha_build.sh spring true true + # we reduce the minCoverage for this run because it will skip the SpringBoot 1.5 tests since they are no longer compatible with + # Spring core 5.2 and above. SpringBoot 1.5 is deprecated + run: ./gha_build.sh spring true true -Djacoco.minCoverage=0.4 - name: Build Spring 4.3 run: ./gha_build.sh spring false false -Dspring.version=4.3.25.RELEASE -Dspring-security.version=4.2.13.RELEASE - name: Build Spring 5.0 - run: ./gha_build.sh spring false false -Dspring.version=5.0.15.RELEASE -Dspring-security.version=5.0.13.RELEASE + run: ./gha_build.sh spring false false -Dspring.version=5.0.16.RELEASE -Dspring-security.version=5.0.14.RELEASE - name: Build Spring 5.1 run: ./gha_build.sh spring false false -Dspring.version=5.1.14.RELEASE -Dspring-security.version=5.1.8.RELEASE - - name: Build Spring 5.2 - # we reduce the minCoverage for this run because it will skip the SpringBoot 1.5 tests since they are no longer compatible with - # Spring core 5.2 and above. SpringBoot 1.5 is deprecated - run: ./gha_build.sh spring false false -Dspring.version=5.2.5.RELEASE -Dspring-security.version=5.2.2.RELEASE -Djacoco.minCoverage=0.4 build_springboot2: name: Build and test SpringBoot 2 @@ -65,11 +63,9 @@ jobs: - name: Build latest run: ./gha_build.sh springboot2 true true - name: Build Spring Boot 2.0 - run: ./gha_build.sh springboot2 false false -Dspringboot.version=2.0.9.RELEASE -Dspring.version=5.0.13.RELEASE -Dspringsecurity.version=5.0.12.RELEASE + run: ./gha_build.sh springboot2 false false -Dspringboot.version=2.0.9.RELEASE -Dspring.version=5.0.16.RELEASE -Dspringsecurity.version=5.0.14.RELEASE - name: Build Spring Boot 2.1 - run: ./gha_build.sh springboot2 false false -Dspringboot.version=2.1.10.RELEASE -Dspring.version=5.1.11.RELEASE -Dspringsecurity.version=5.1.7.RELEASE - - name: Build Spring Boot 2.2 - run: ./gha_build.sh springboot2 false false -Dspringboot.version=2.2.1.RELEASE -Dspring.version=5.2.1.RELEASE -Dspringsecurity.version=5.2.1.RELEASE + run: ./gha_build.sh springboot2 false false -Dspringboot.version=2.1.12.RELEASE -Dspring.version=5.1.13.RELEASE -Dspringsecurity.version=5.1.8.RELEASE build_struts2: name: Build and test Struts 2 diff --git a/.gitignore b/.gitignore index 870a2d42f..8f6b9d359 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,7 @@ gradlew* # Exclude maven wrapper !/.mvn/wrapper/maven-wrapper.jar + +# SAM files +samconfig.toml +.aws-sam/ diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AsyncInitializationWrapper.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AsyncInitializationWrapper.java index 7ef74fc8b..813d8178e 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AsyncInitializationWrapper.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AsyncInitializationWrapper.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy; import com.amazonaws.serverless.exceptions.ContainerInitializationException; @@ -7,6 +19,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.management.ManagementFactory; import java.time.Instant; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -25,7 +38,7 @@ * seconds has already been used up. */ public class AsyncInitializationWrapper extends InitializationWrapper { - private static final int INIT_GRACE_TIME_MS = 250; + private int INIT_GRACE_TIME_MS = 250; private static final int LAMBDA_MAX_INIT_TIME_MS = 10_000; private CountDownLatch initializationLatch; @@ -41,6 +54,15 @@ public AsyncInitializationWrapper(long startTime) { actualStartTime = startTime; } + /** + * Creates a new instance of the async initializer using the actual JVM start time as the starting point to measure + * the 10 seconds timeout. + */ + public AsyncInitializationWrapper() { + actualStartTime = ManagementFactory.getRuntimeMXBean().getStartTime(); + INIT_GRACE_TIME_MS = 150; + } + @Override public void start(LambdaContainerHandler handler) throws ContainerInitializationException { initializationLatch = new CountDownLatch(1); @@ -50,7 +72,7 @@ public void start(LambdaContainerHandler handler) throws ContainerInitialization try { long curTime = Instant.now().toEpochMilli(); // account for the time it took to call the various constructors with the actual start time + a grace of 500ms - long awaitTime = LAMBDA_MAX_INIT_TIME_MS - (curTime - actualStartTime) - INIT_GRACE_TIME_MS; + long awaitTime = (actualStartTime + LAMBDA_MAX_INIT_TIME_MS) - curTime - INIT_GRACE_TIME_MS; log.info("Async initialization will wait for " + awaitTime + "ms"); if (!initializationLatch.await(awaitTime, TimeUnit.MILLISECONDS)) { log.info("Initialization took longer than " + LAMBDA_MAX_INIT_TIME_MS + ", setting new CountDownLatch and " + @@ -65,6 +87,10 @@ public void start(LambdaContainerHandler handler) throws ContainerInitialization } } + public long getActualStartTimeMs() { + return actualStartTime; + } + @Override public CountDownLatch getInitializationLatch() { return initializationLatch; diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsHttpApiV2SecurityContextWriter.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsHttpApiV2SecurityContextWriter.java new file mode 100644 index 000000000..05d4ed1a2 --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsHttpApiV2SecurityContextWriter.java @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy; + +import com.amazonaws.serverless.proxy.internal.jaxrs.AwsHttpApiV2SecurityContext; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.services.lambda.runtime.Context; + +import javax.ws.rs.core.SecurityContext; + +public class AwsHttpApiV2SecurityContextWriter implements SecurityContextWriter { + @Override + public SecurityContext writeSecurityContext(HttpApiV2ProxyRequest event, Context lambdaContext) { + return new AwsHttpApiV2SecurityContext(lambdaContext, event); + } +} diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/InitializationWrapper.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/InitializationWrapper.java index 188a83bd3..f7c96f8f4 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/InitializationWrapper.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/InitializationWrapper.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy; import com.amazonaws.serverless.exceptions.ContainerInitializationException; diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/LogFormatter.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/LogFormatter.java index cef0514d5..dcd524f91 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/LogFormatter.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/LogFormatter.java @@ -1,9 +1,19 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy; - import javax.ws.rs.core.SecurityContext; - /** * Implementations of the log formatter interface are used by {@link com.amazonaws.serverless.proxy.internal.LambdaContainerHandler} class to log each request * processed in the container. You can set the log formatter using the {@link com.amazonaws.serverless.proxy.internal.LambdaContainerHandler#setLogFormatter(LogFormatter)} diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/RequestReader.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/RequestReader.java index 096d49ca5..e0203d76b 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/RequestReader.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/RequestReader.java @@ -65,7 +65,20 @@ public abstract class RequestReader { */ public static final String JAX_SECURITY_CONTEXT_PROPERTY = "com.amazonaws.serverless.jaxrs.securityContext"; + /** + * The key for the HTTP API request context passed by the services + */ + public static final String HTTP_API_CONTEXT_PROPERTY = "com.amazonaws.httpapi.request.context"; + /** + * The key for the HTTP API stage variables + */ + public static final String HTTP_API_STAGE_VARS_PROPERTY = "com.amazonaws.httpapi.stage.variables"; + + /** + * The key for the HTTP API proxy request event + */ + public static final String HTTP_API_EVENT_PROPERTY = "com.amazonaws.httpapi.request"; //------------------------------------------------------------- // Methods - Abstract diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java index 3bb401d17..d31ae257b 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java @@ -223,7 +223,15 @@ public ResponseType proxy(RequestType request, Context context) { // the latch will do nothing latch.countDown(); - return exceptionHandler.handle(e); + if (getContainerConfig().isDisableExceptionMapper()) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new RuntimeException(e); + } + } else { + return exceptionHandler.handle(e); + } } } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/SecurityUtils.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/SecurityUtils.java index 1d39d849b..3395b56c6 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/SecurityUtils.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/SecurityUtils.java @@ -1,6 +1,17 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.internal; - import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,7 +22,6 @@ import java.util.Locale; import java.util.Set; - /** * This class contains utility methods to address FSB security issues found in the application, such as string sanitization * and file path validation. diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/jaxrs/AwsHttpApiV2SecurityContext.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/jaxrs/AwsHttpApiV2SecurityContext.java new file mode 100644 index 000000000..6060fb2e2 --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/jaxrs/AwsHttpApiV2SecurityContext.java @@ -0,0 +1,102 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.internal.jaxrs; + +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; +import com.amazonaws.serverless.proxy.internal.SecurityUtils; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.services.lambda.runtime.Context; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.SecurityContext; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.util.Base64; + +public class AwsHttpApiV2SecurityContext implements SecurityContext { + public static final String AUTH_SCHEME_JWT = "JWT"; + + private static Logger log = LoggerFactory.getLogger(AwsHttpApiV2SecurityContext.class); + + private Context lambdaContext; + private HttpApiV2ProxyRequest event; + + public AwsHttpApiV2SecurityContext(final Context lambdaCtx, final HttpApiV2ProxyRequest request) { + lambdaContext = lambdaCtx; + event = request; + } + + @Override + public Principal getUserPrincipal() { + if (getAuthenticationScheme() == null || !event.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) { + return null; + } + + String authValue = event.getHeaders().get(HttpHeaders.AUTHORIZATION); + if (authValue.startsWith("Bearer ")) { + authValue = authValue.replace("Bearer ", ""); + } + String[] parts = authValue.split("\\."); + if (parts.length != 3) { + log.warn("Could not parse JWT token for requestId: " + SecurityUtils.crlf(event.getRequestContext().getRequestId())); + return null; + } + String decodedBody = new String(Base64.getMimeDecoder().decode(parts[1]), StandardCharsets.UTF_8); + try { + JsonNode parsedBody = LambdaContainerHandler.getObjectMapper().readTree(decodedBody); + if (!parsedBody.isObject() && parsedBody.has("sub")) { + log.debug("Could not find \"sub\" field in JWT body for requestId: " + SecurityUtils.crlf(event.getRequestContext().getRequestId())); + return null; + } + String subject = parsedBody.get("sub").asText(); + return (() -> { + return subject; + }); + } catch (JsonProcessingException e) { + log.error("Error while attempting to parse JWT body for requestId: " + SecurityUtils.crlf(event.getRequestContext().getRequestId()), e); + return null; + } + + } + + @Override + public boolean isUserInRole(String s) { + if (getAuthenticationScheme() == null) { + return false; + } + + return event.getRequestContext().getAuthorizer().getJwtAuthorizer().getScopes().contains(s) || + event.getRequestContext().getAuthorizer().getJwtAuthorizer().getClaims().containsKey(s); + + } + + @Override + public boolean isSecure() { + return getAuthenticationScheme() != null; + } + + @Override + public String getAuthenticationScheme() { + if (event.getRequestContext().getAuthorizer() == null) { + return null; + } + if (event.getRequestContext().getAuthorizer().isJwt()) { + return AUTH_SCHEME_JWT; + } + return null; + } +} diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/ApacheCombinedServletLogFormatter.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/ApacheCombinedServletLogFormatter.java index 2105ffae3..715e6f8d9 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/ApacheCombinedServletLogFormatter.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/ApacheCombinedServletLogFormatter.java @@ -1,10 +1,22 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.internal.servlet; - import com.amazonaws.serverless.proxy.LogFormatter; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequestContext; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.servlet.http.HttpServletRequest; @@ -16,7 +28,7 @@ import java.time.format.DateTimeFormatterBuilder; import java.util.Locale; -import static com.amazonaws.serverless.proxy.RequestReader.API_GATEWAY_EVENT_PROPERTY; +import static com.amazonaws.serverless.proxy.RequestReader.*; import static java.time.temporal.ChronoField.DAY_OF_MONTH; import static java.time.temporal.ChronoField.HOUR_OF_DAY; import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; @@ -24,7 +36,6 @@ import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; import static java.time.temporal.ChronoField.YEAR; - /** * Default implementation of the log formatter. Based on an HttpServletRequest and HttpServletResponse implementations produced * a log line in the Apache combined log format: https://httpd.apache.org/docs/2.4/logs.html @@ -68,45 +79,45 @@ public ApacheCombinedServletLogFormatter() { public String format(ContainerRequestType servletRequest, ContainerResponseType servletResponse, SecurityContext ctx) { //LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" combined StringBuilder logLineBuilder = new StringBuilder(); - AwsProxyRequest req = (AwsProxyRequest)servletRequest.getAttribute(API_GATEWAY_EVENT_PROPERTY); - AwsProxyRequestContext gatewayContext = req.getRequestContext(); + AwsProxyRequestContext gatewayContext = (AwsProxyRequestContext)servletRequest.getAttribute(API_GATEWAY_CONTEXT_PROPERTY); + HttpApiV2ProxyRequestContext httpApiContext = (HttpApiV2ProxyRequestContext)servletRequest.getAttribute(HTTP_API_CONTEXT_PROPERTY); // %h logLineBuilder.append(servletRequest.getRemoteAddr()); logLineBuilder.append(" "); // %l - if (gatewayContext != null && req.getRequestSource() == AwsProxyRequest.RequestSource.API_GATEWAY) { - if (gatewayContext.getIdentity().getUserArn() != null) { + if (servletRequest.getUserPrincipal() != null) { + logLineBuilder.append(servletRequest.getUserPrincipal().getName()); + } else { + logLineBuilder.append("-"); + } + if (gatewayContext != null && gatewayContext.getIdentity() != null && gatewayContext.getIdentity().getUserArn() != null) { logLineBuilder.append(gatewayContext.getIdentity().getUserArn()); - } else { - logLineBuilder.append("-"); - } } else { logLineBuilder.append("-"); } logLineBuilder.append(" "); // %u - if (ctx != null && ctx.getUserPrincipal().getName() != null) { - logLineBuilder.append(ctx.getUserPrincipal().getName()); - logLineBuilder.append(" "); + if (servletRequest.getUserPrincipal() != null) { + logLineBuilder.append(servletRequest.getUserPrincipal().getName()); } + logLineBuilder.append(" "); // %t + long timeEpoch = ZonedDateTime.now(clock).toEpochSecond(); if (gatewayContext != null && gatewayContext.getRequestTimeEpoch() > 0) { - logLineBuilder.append( - dateFormat.format( - ZonedDateTime.of( - LocalDateTime.ofEpochSecond(gatewayContext.getRequestTimeEpoch() / 1000, 0, ZoneOffset.UTC), - clock.getZone() - ) - ) - ); - } else { - logLineBuilder.append(dateFormat.format(ZonedDateTime.now(clock))); + timeEpoch = gatewayContext.getRequestTimeEpoch() / 1000; + } else if (httpApiContext != null && httpApiContext.getTimeEpoch() > 0) { + timeEpoch = httpApiContext.getTimeEpoch() / 1000; } + logLineBuilder.append( + dateFormat.format(ZonedDateTime.of( + LocalDateTime.ofEpochSecond(timeEpoch, 0, ZoneOffset.UTC), + clock.getZone()) + )); logLineBuilder.append(" "); // %r @@ -155,7 +166,6 @@ public String format(ContainerRequestType servletRequest, ContainerResponseType logLineBuilder.append("combined"); - return logLineBuilder.toString(); } } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsAsyncContext.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsAsyncContext.java index 0b35d7ba5..251624541 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsAsyncContext.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsAsyncContext.java @@ -1,6 +1,17 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.internal.servlet; - import com.amazonaws.serverless.proxy.internal.SecurityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,10 +70,12 @@ public boolean hasOriginalRequestAndResponse() { public void dispatch() { try { log.debug("Dispatching request"); - notifyListeners(NotificationType.START_ASYNC, null); - Servlet servlet = ((AwsServletContext)handler.getServletContext()).getServletForPath(req.getPathInfo()); - handler.doFilter(req, res, servlet); + if (dispatched.get()) { + throw new IllegalStateException("Dispatching already started"); + } dispatched.set(true); + notifyListeners(NotificationType.START_ASYNC, null); + handler.doFilter(req, res, ((AwsServletContext)req.getServletContext()).getServletForPath(req.getRequestURI())); } catch (ServletException | IOException e) { notifyListeners(NotificationType.ERROR, e); } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpApiV2HttpServletRequestReader.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpApiV2HttpServletRequestReader.java new file mode 100644 index 000000000..bacedbaa4 --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpApiV2HttpServletRequestReader.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.internal.servlet; + +import com.amazonaws.serverless.exceptions.InvalidRequestEventException; +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.model.ContainerConfig; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.services.lambda.runtime.Context; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.SecurityContext; + +public class AwsHttpApiV2HttpServletRequestReader extends RequestReader { + static final String INVALID_REQUEST_ERROR = "The incoming event is not a valid HTTP API v2 proxy request"; + + @Override + public HttpServletRequest readRequest(HttpApiV2ProxyRequest request, SecurityContext securityContext, Context lambdaContext, ContainerConfig config) throws InvalidRequestEventException { + if (request.getRequestContext() == null || request.getRequestContext().getHttp().getMethod() == null || request.getRequestContext().getHttp().getMethod().equals("")) { + throw new InvalidRequestEventException(INVALID_REQUEST_ERROR); + } + + // clean out the request path based on the container config + request.setRawPath(stripBasePath(request.getRawPath(), config)); + + AwsHttpApiV2ProxyHttpServletRequest servletRequest = new AwsHttpApiV2ProxyHttpServletRequest(request, lambdaContext, securityContext, config); + servletRequest.setAttribute(HTTP_API_CONTEXT_PROPERTY, request.getRequestContext()); + servletRequest.setAttribute(HTTP_API_STAGE_VARS_PROPERTY, request.getStageVariables()); + servletRequest.setAttribute(HTTP_API_EVENT_PROPERTY, request); + servletRequest.setAttribute(LAMBDA_CONTEXT_PROPERTY, lambdaContext); + servletRequest.setAttribute(JAX_SECURITY_CONTEXT_PROPERTY, securityContext); + + return servletRequest; + } + + @Override + protected Class getRequestClass() { + return HttpApiV2ProxyRequest.class; + } +} diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpApiV2ProxyHttpServletRequest.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpApiV2ProxyHttpServletRequest.java new file mode 100644 index 000000000..e65b506e9 --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpApiV2ProxyHttpServletRequest.java @@ -0,0 +1,521 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.internal.servlet; + +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; +import com.amazonaws.serverless.proxy.internal.SecurityUtils; +import com.amazonaws.serverless.proxy.model.ContainerConfig; +import com.amazonaws.serverless.proxy.model.Headers; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.model.MultiValuedTreeMap; +import com.amazonaws.services.lambda.runtime.Context; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.*; +import javax.servlet.http.*; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.SecurityContext; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.security.Principal; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; +import java.util.*; + +public class AwsHttpApiV2ProxyHttpServletRequest extends AwsHttpServletRequest { + private static Logger log = LoggerFactory.getLogger(AwsHttpApiV2ProxyHttpServletRequest.class); + + private HttpApiV2ProxyRequest request; + private MultiValuedTreeMap queryString; + private Headers headers; + private ContainerConfig config; + private SecurityContext securityContext; + private AwsAsyncContext asyncContext; + + /** + * Protected constructors for implementing classes. This should be called first with the context received from + * AWS Lambda + * + * @param lambdaContext The Lambda function context. This object is used for utility methods such as log + */ + public AwsHttpApiV2ProxyHttpServletRequest(HttpApiV2ProxyRequest req, Context lambdaContext, SecurityContext sc, ContainerConfig cfg) { + super(lambdaContext); + request = req; + config = cfg; + securityContext = sc; + queryString = parseRawQueryString(request.getRawQueryString()); + headers = headersMapToMultiValue(request.getHeaders()); + } + + public HttpApiV2ProxyRequest getRequest() { + return request; + } + + @Override + public String getAuthType() { + // TODO + return null; + } + + @Override + public Cookie[] getCookies() { + if (headers == null || !headers.containsKey(HttpHeaders.COOKIE)) { + return new Cookie[0]; + } + + return parseCookieHeaderValue(headers.getFirst(HttpHeaders.COOKIE)); + } + + @Override + public long getDateHeader(String s) { + if (headers == null) { + return -1L; + } + String dateString = headers.getFirst(s); + if (dateString == null) { + return -1L; + } + try { + return Instant.from(ZonedDateTime.parse(dateString, dateFormatter)).toEpochMilli(); + } catch (DateTimeParseException e) { + log.warn("Invalid date header in request: " + SecurityUtils.crlf(dateString)); + return -1L; + } + } + + @Override + public String getHeader(String s) { + if (headers == null) { + return null; + } + return headers.getFirst(s); + } + + @Override + public Enumeration getHeaders(String s) { + if (headers == null || !headers.containsKey(s)) { + return Collections.emptyEnumeration(); + } + return Collections.enumeration(headers.get(s)); + } + + @Override + public Enumeration getHeaderNames() { + if (headers == null) { + return Collections.emptyEnumeration(); + } + return Collections.enumeration(headers.keySet()); + } + + @Override + public int getIntHeader(String s) { + if (headers == null) { + return -1; + } + String headerValue = headers.getFirst(s); + if (headerValue == null || "".equals(headerValue)) { + return -1; + } + + return Integer.parseInt(headerValue); + } + + @Override + public String getMethod() { + return request.getRequestContext().getHttp().getMethod(); + } + + @Override + public String getPathInfo() { + String pathInfo = cleanUri(request.getRawPath()); + return decodeRequestPath(pathInfo, LambdaContainerHandler.getContainerConfig()); + } + + @Override + public String getPathTranslated() { + // Return null because it is an archive on a remote system + return null; + } + + @Override + public String getContextPath() { + return generateContextPath(config, request.getRequestContext().getStage()); + } + + @Override + public String getQueryString() { + return request.getRawQueryString(); + } + + @Override + public String getRemoteUser() { + if (securityContext == null || securityContext.getUserPrincipal() == null) { + return null; + } + return securityContext.getUserPrincipal().getName(); + } + + @Override + public boolean isUserInRole(String s) { + // TODO: Not supported + return false; + } + + @Override + public Principal getUserPrincipal() { + if (securityContext == null) { + return null; + } + return securityContext.getUserPrincipal(); + } + + @Override + public String getRequestURI() { + return cleanUri(getContextPath()) + cleanUri(request.getRawPath()); + } + + @Override + public StringBuffer getRequestURL() { + return generateRequestURL(request.getRawPath()); + } + + + @Override + public boolean authenticate(HttpServletResponse httpServletResponse) throws IOException, ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public void login(String s, String s1) throws ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public void logout() throws ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public Collection getParts() throws IOException, ServletException { + return getMultipartFormParametersMap().values(); + } + + @Override + public Part getPart(String s) throws IOException, ServletException { + return getMultipartFormParametersMap().get(s); + } + + @Override + public T upgrade(Class aClass) throws IOException, ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public String getCharacterEncoding() { + if (headers == null) { + return config.getDefaultContentCharset(); + } + return parseCharacterEncoding(headers.getFirst(HttpHeaders.CONTENT_TYPE)); + } + + @Override + public void setCharacterEncoding(String s) throws UnsupportedEncodingException { + if (headers == null || !headers.containsKey(HttpHeaders.CONTENT_TYPE)) { + log.debug("Called set character encoding to " + SecurityUtils.crlf(s) + " on a request without a content type. Character encoding will not be set"); + return; + } + String currentContentType = headers.getFirst(HttpHeaders.CONTENT_TYPE); + headers.putSingle(HttpHeaders.CONTENT_TYPE, appendCharacterEncoding(currentContentType, s)); + } + + @Override + public int getContentLength() { + String headerValue = headers.getFirst(HttpHeaders.CONTENT_LENGTH); + if (headerValue == null) { + return -1; + } + return Integer.parseInt(headerValue); + } + + @Override + public long getContentLengthLong() { + String headerValue = headers.getFirst(HttpHeaders.CONTENT_LENGTH); + if (headerValue == null) { + return -1; + } + return Long.parseLong(headerValue); + } + + @Override + public String getContentType() { + if (headers == null) { + return null; + } + return headers.getFirst(HttpHeaders.CONTENT_TYPE); + } + + @Override + public String getParameter(String s) { + String queryStringParameter = getFirstQueryParamValue(queryString, s, config.isQueryStringCaseSensitive()); + if (queryStringParameter != null) { + return queryStringParameter; + } + + String[] bodyParams = getFormBodyParameterCaseInsensitive(s); + if (bodyParams.length == 0) { + return null; + } else { + return bodyParams[0]; + } + } + + @Override + public Enumeration getParameterNames() { + if (queryString == null) { + return Collections.emptyEnumeration(); + } + + return Collections.enumeration(queryString.keySet()); + } + + @Override + @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 + public String[] getParameterValues(String s) { + List values = new ArrayList<>(Arrays.asList(getQueryParamValues(queryString, s, config.isQueryStringCaseSensitive()))); + + values.addAll(Arrays.asList(getFormBodyParameterCaseInsensitive(s))); + + if (values.size() == 0) { + return null; + } else { + return values.toArray(new String[0]); + } + } + + @Override + public Map getParameterMap() { + return generateParameterMap(queryString, config); + } + + @Override + public String getProtocol() { + return request.getRequestContext().getHttp().getProtocol(); + } + + @Override + public String getScheme() { + return getSchemeFromHeader(headers); + } + + @Override + public String getServerName() { + // we match the behavior of the v1 proxy request here. Should we? + String region = System.getenv("AWS_REGION"); + if (region == null) { + // this is not a critical failure, we just put a static region in the URI + region = "us-east-1"; + } + + if (headers != null && headers.containsKey(HOST_HEADER_NAME)) { + String hostHeader = headers.getFirst(HOST_HEADER_NAME); + if (SecurityUtils.isValidHost(hostHeader, request.getRequestContext().getApiId(), region)) { + return hostHeader; + } + } + + return request.getRequestContext().getDomainName(); + } + + @Override + public int getServerPort() { + if (headers == null || !headers.containsKey(PORT_HEADER_NAME)) { + return 443; // we default to 443 as HTTP APIs can only be HTTPS + } + String port = headers.getFirst(PORT_HEADER_NAME); + if (SecurityUtils.isValidPort(port)) { + return Integer.parseInt(port); + } + return 443; // default port + } + + @Override + public ServletInputStream getInputStream() throws IOException { + if (requestInputStream == null) { + requestInputStream = new AwsServletInputStream(bodyStringToInputStream(request.getBody(), request.isBase64Encoded())); + } + return requestInputStream; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new StringReader(request.getBody())); + } + + @Override + public String getRemoteAddr() { + if (request.getRequestContext() == null || request.getRequestContext().getHttp() == null || request.getRequestContext().getHttp().getSourceIp() == null) { + return "127.0.0.1"; + } + return request.getRequestContext().getHttp().getSourceIp(); + } + + @Override + public String getRemoteHost() { + if (headers == null) { + return null; + } + return headers.getFirst(HttpHeaders.HOST); + } + + @Override + public Locale getLocale() { + // Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5 + List values = this.parseHeaderValue( + headers.getFirst(HttpHeaders.ACCEPT_LANGUAGE), ",", ";" + ); + if (values.size() == 0) { + return Locale.getDefault(); + } + return new Locale(values.get(0).getValue()); + } + + @Override + public Enumeration getLocales() { + List values = this.parseHeaderValue( + headers.getFirst(HttpHeaders.ACCEPT_LANGUAGE), ",", ";" + ); + + List locales = new ArrayList<>(); + if (values.size() == 0) { + locales.add(Locale.getDefault()); + } else { + for (HeaderValue locale : values) { + locales.add(new Locale(locale.getValue())); + } + } + + return Collections.enumeration(locales); + } + + @Override + public boolean isSecure() { + return securityContext.isSecure(); + } + + @Override + public RequestDispatcher getRequestDispatcher(String s) { + return getServletContext().getRequestDispatcher(s); + } + + @Override + public String getRealPath(String s) { + // we are in an archive on a remote server + return null; + } + + @Override + public int getRemotePort() { + return 0; + } + + @Override + public boolean isAsyncSupported() { + return true; + } + + @Override + public boolean isAsyncStarted() { + if (asyncContext == null) { + return false; + } + if (asyncContext.isCompleted() || asyncContext.isDispatched()) { + return false; + } + return true; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + asyncContext = new AwsAsyncContext(this, response, containerHandler); + setAttribute(DISPATCHER_TYPE_ATTRIBUTE, DispatcherType.ASYNC); + log.debug("Starting async context for request: " + SecurityUtils.crlf(request.getRequestContext().getRequestId())); + return asyncContext; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException { + asyncContext = new AwsAsyncContext((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse, containerHandler); + setAttribute(DISPATCHER_TYPE_ATTRIBUTE, DispatcherType.ASYNC); + log.debug("Starting async context for request: " + SecurityUtils.crlf(request.getRequestContext().getRequestId())); + return asyncContext; + } + + @Override + public AsyncContext getAsyncContext() { + if (asyncContext == null) { + throw new IllegalStateException("Request " + SecurityUtils.crlf(request.getRequestContext().getRequestId()) + + " is not in asynchronous mode. Call startAsync before attempting to get the async context."); + } + return asyncContext; + } + + private MultiValuedTreeMap parseRawQueryString(String qs) { + if (qs == null || "".equals(qs.trim())) { + return new MultiValuedTreeMap<>(); + } + + MultiValuedTreeMap qsMap = new MultiValuedTreeMap<>(); + for (String value : qs.split(QUERY_STRING_SEPARATOR)) { + if (!value.contains(QUERY_STRING_KEY_VALUE_SEPARATOR)) { + log.warn("Invalid query string parameter: " + SecurityUtils.crlf(value)); + continue; + } + + String[] kv = value.split(QUERY_STRING_KEY_VALUE_SEPARATOR); + try { + qsMap.add(URLDecoder.decode(kv[0], LambdaContainerHandler.getContainerConfig().getUriEncoding()), kv[1]); + } catch (UnsupportedEncodingException e) { + log.error("Unsupported encoding in query string key: " + SecurityUtils.crlf(kv[0]), e); + } + } + return qsMap; + } + + private Headers headersMapToMultiValue(Map headers) { + if (headers == null || headers.size() == 0) { + return new Headers(); + } + + Headers h = new Headers(); + for (Map.Entry hkv : headers.entrySet()) { + // Exceptions for known header values that contain commas + if (hkv.getKey().equalsIgnoreCase(HttpHeaders.DATE) || + hkv.getKey().equalsIgnoreCase(HttpHeaders.IF_MODIFIED_SINCE) || + hkv.getKey().toLowerCase(Locale.getDefault()).startsWith("accept-")) { + h.add(hkv.getKey(), hkv.getValue()); + continue; + } + + for (String value : hkv.getValue().split(",")) { + h.add(hkv.getKey(), value); + } + } + return h; + } +} diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequest.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequest.java index 35773cee9..f02cefd1c 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequest.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequest.java @@ -13,32 +13,36 @@ package com.amazonaws.serverless.proxy.internal.servlet; import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.internal.SecurityUtils; -import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; -import com.amazonaws.serverless.proxy.model.ContainerConfig; -import com.amazonaws.serverless.proxy.model.MultiValuedTreeMap; +import com.amazonaws.serverless.proxy.internal.testutils.Timer; +import com.amazonaws.serverless.proxy.model.*; import com.amazonaws.services.lambda.runtime.Context; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.NullInputStream; import org.apache.http.message.BasicHeaderValueParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.*; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; +import javax.servlet.http.*; +import javax.ws.rs.core.MediaType; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** @@ -59,6 +63,8 @@ public abstract class AwsHttpServletRequest implements HttpServletRequest { static final DateTimeFormatter dateFormatter = DateTimeFormatter.RFC_1123_DATE_TIME; static final String ENCODING_VALUE_KEY = "charset"; static final String DISPATCHER_TYPE_ATTRIBUTE = "com.amazonaws.serverless.javacontainer.dispatchertype"; + static final String QUERY_STRING_SEPARATOR = "&"; + static final String QUERY_STRING_KEY_VALUE_SEPARATOR = "="; // We need this to pickup the protocol from the CloudFront header since Lambda doesn't receive this // information from anywhere else @@ -78,6 +84,13 @@ public abstract class AwsHttpServletRequest implements HttpServletRequest { private AwsHttpSession session; private String queryString; private BasicHeaderValueParser headerParser; + private Map multipartFormParameters; + private Map> urlEncodedFormParameters; + + protected AwsHttpServletResponse response; + protected AwsLambdaServletContainerHandler containerHandler; + protected ServletInputStream requestInputStream; + private static Logger log = LoggerFactory.getLogger(AwsHttpServletRequest.class); @@ -98,6 +111,18 @@ public abstract class AwsHttpServletRequest implements HttpServletRequest { setAttribute(DISPATCHER_TYPE_ATTRIBUTE, DispatcherType.REQUEST); } + public AwsHttpServletResponse getResponse() { + return response; + } + + public void setResponse(AwsHttpServletResponse response) { + this.response = response; + } + + public void setContainerHandler(AwsLambdaServletContainerHandler containerHandler) { + this.containerHandler = containerHandler; + } + //------------------------------------------------------------- // Implementation - HttpServletRequest @@ -247,6 +272,12 @@ public DispatcherType getDispatcherType() { return DispatcherType.REQUEST; } + @Override + public String getServletPath() { + // we always work on the root path + return ""; + } + //------------------------------------------------------------- // Methods - Getter/Setter @@ -323,6 +354,257 @@ protected String generateQueryString(MultiValuedTreeMap paramete return queryString; } + protected String generateContextPath(ContainerConfig config, String apiStage) { + String contextPath = ""; + if (config.isUseStageAsServletContext() && apiStage != null) { + log.debug("Using stage as context path"); + contextPath = cleanUri(apiStage); + } + if (config.getServiceBasePath() != null) { + contextPath += cleanUri(config.getServiceBasePath()); + } + + return contextPath; + } + + protected StringBuffer generateRequestURL(String requestPath) { + String url = ""; + url += getServerName(); + url += cleanUri(getContextPath()); + url += cleanUri(requestPath); + + return new StringBuffer(getScheme() + "://" + url); + } + + protected String parseCharacterEncoding(String contentTypeHeader) { + // we only look at content-type because content-encoding should only be used for + // "binary" requests such as gzip/deflate. + if (contentTypeHeader == null) { + return null; + } + + String[] contentTypeValues = contentTypeHeader.split(HEADER_VALUE_SEPARATOR); + if (contentTypeValues.length <= 1) { + return null; + } + + for (String contentTypeValue : contentTypeValues) { + if (contentTypeValue.trim().startsWith(ENCODING_VALUE_KEY)) { + String[] encodingValues = contentTypeValue.split(HEADER_KEY_VALUE_SEPARATOR); + if (encodingValues.length <= 1) { + return null; + } + return encodingValues[1]; + } + } + return null; + } + + protected String appendCharacterEncoding(String currentContentType, String newEncoding) { + if (currentContentType == null || "".equals(currentContentType.trim())) { + return null; + } + + if (currentContentType.contains(HEADER_VALUE_SEPARATOR)) { + String[] contentTypeValues = currentContentType.split(HEADER_VALUE_SEPARATOR); + StringBuilder contentType = new StringBuilder(contentTypeValues[0]); + + for (int i = 1; i < contentTypeValues.length; i++) { + String contentTypeValue = contentTypeValues[i]; + String contentTypeString = HEADER_VALUE_SEPARATOR + " " + contentTypeValue; + if (contentTypeValue.trim().startsWith(ENCODING_VALUE_KEY)) { + contentTypeString = HEADER_VALUE_SEPARATOR + " " + ENCODING_VALUE_KEY + HEADER_KEY_VALUE_SEPARATOR + newEncoding; + } + contentType.append(contentTypeString); + } + + return contentType.toString(); + } else { + return currentContentType + HEADER_VALUE_SEPARATOR + " " + ENCODING_VALUE_KEY + HEADER_KEY_VALUE_SEPARATOR + newEncoding; + } + } + + protected ServletInputStream bodyStringToInputStream(String body, boolean isBase64Encoded) throws IOException { + if (body == null) { + return new AwsServletInputStream(new NullInputStream(0, false, false)); + } + byte[] bodyBytes; + if (isBase64Encoded) { + bodyBytes = Base64.getMimeDecoder().decode(body); + } else { + String encoding = getCharacterEncoding(); + if (encoding == null) { + encoding = StandardCharsets.ISO_8859_1.name(); + } + try { + bodyBytes = body.getBytes(encoding); + } catch (Exception e) { + log.error("Could not read request with character encoding: " + SecurityUtils.crlf(encoding), e); + bodyBytes = body.getBytes(StandardCharsets.ISO_8859_1.name()); + } + } + ByteArrayInputStream requestBodyStream = new ByteArrayInputStream(bodyBytes); + return new AwsServletInputStream(requestBodyStream); + } + + protected String getFirstQueryParamValue(MultiValuedTreeMap queryString, String key, boolean isCaseSensitive) { + if (queryString != null) { + if (isCaseSensitive) { + return queryString.getFirst(key); + } + + for (String k : queryString.keySet()) { + if (k.toLowerCase(Locale.getDefault()).equals(key.toLowerCase(Locale.getDefault()))) { + return queryString.getFirst(k); + } + } + } + + return null; + } + + protected String[] getFormBodyParameterCaseInsensitive(String key) { + List values = getFormUrlEncodedParametersMap().get(key); + if (values != null) { + String[] valuesArray = new String[values.size()]; + valuesArray = values.toArray(valuesArray); + return valuesArray; + } else { + return new String[0]; + } + } + + + protected Map> getFormUrlEncodedParametersMap() { + if (urlEncodedFormParameters != null) { + return urlEncodedFormParameters; + } + String contentType = getContentType(); + if (contentType == null) { + urlEncodedFormParameters = new HashMap<>(); + return urlEncodedFormParameters; + } + if (!contentType.startsWith(MediaType.APPLICATION_FORM_URLENCODED) || !getMethod().toLowerCase(Locale.ENGLISH).equals("post")) { + urlEncodedFormParameters = new HashMap<>(); + return urlEncodedFormParameters; + } + Timer.start("SERVLET_REQUEST_GET_FORM_PARAMS"); + String rawBodyContent = null; + try { + rawBodyContent = IOUtils.toString(getInputStream()); + } catch (IOException e) { + throw new RuntimeException(e); + } + + urlEncodedFormParameters = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (String parameter : rawBodyContent.split(FORM_DATA_SEPARATOR)) { + String[] parameterKeyValue = parameter.split(HEADER_KEY_VALUE_SEPARATOR); + if (parameterKeyValue.length < 2) { + continue; + } + List values = new ArrayList<>(); + if (urlEncodedFormParameters.containsKey(parameterKeyValue[0])) { + values = urlEncodedFormParameters.get(parameterKeyValue[0]); + } + values.add(decodeValueIfEncoded(parameterKeyValue[1])); + urlEncodedFormParameters.put(decodeValueIfEncoded(parameterKeyValue[0]), values); + } + Timer.stop("SERVLET_REQUEST_GET_FORM_PARAMS"); + return urlEncodedFormParameters; + } + + @SuppressFBWarnings({"FILE_UPLOAD_FILENAME", "WEAK_FILENAMEUTILS"}) + protected Map getMultipartFormParametersMap() { + if (multipartFormParameters != null) { + return multipartFormParameters; + } + if (!ServletFileUpload.isMultipartContent(this)) { // isMultipartContent also checks the content type + multipartFormParameters = new HashMap<>(); + return multipartFormParameters; + } + Timer.start("SERVLET_REQUEST_GET_MULTIPART_PARAMS"); + multipartFormParameters = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory()); + + try { + List items = upload.parseRequest(this); + for (FileItem item : items) { + String fileName = FilenameUtils.getName(item.getName()); + AwsProxyRequestPart newPart = new AwsProxyRequestPart(item.get()); + newPart.setName(item.getFieldName()); + newPart.setSubmittedFileName(fileName); + newPart.setContentType(item.getContentType()); + newPart.setSize(item.getSize()); + item.getHeaders().getHeaderNames().forEachRemaining(h -> { + newPart.addHeader(h, item.getHeaders().getHeader(h)); + }); + + multipartFormParameters.put(item.getFieldName(), newPart); + } + } catch (FileUploadException e) { + Timer.stop("SERVLET_REQUEST_GET_MULTIPART_PARAMS"); + log.error("Could not read multipart upload file", e); + } + Timer.stop("SERVLET_REQUEST_GET_MULTIPART_PARAMS"); + return multipartFormParameters; + } + + protected String[] getQueryParamValues(MultiValuedTreeMap qs, String key, boolean isCaseSensitive) { + if (qs != null) { + if (isCaseSensitive) { + return qs.get(key).toArray(new String[0]); + } + + for (String k : qs.keySet()) { + if (k.toLowerCase(Locale.getDefault()).equals(key.toLowerCase(Locale.getDefault()))) { + return qs.get(k).toArray(new String[0]); + } + } + } + + return new String[0]; + } + + protected Map generateParameterMap(MultiValuedTreeMap qs, ContainerConfig config) { + Map output = new HashMap<>(); + + Map> params = getFormUrlEncodedParametersMap(); + params.entrySet().stream().parallel().forEach(e -> { + output.put(e.getKey(), e.getValue().toArray(new String[0])); + }); + + if (qs != null) { + qs.keySet().stream().parallel().forEach(e -> { + List newValues = new ArrayList<>(); + if (output.containsKey(e)) { + String[] values = output.get(e); + newValues.addAll(Arrays.asList(values)); + } + newValues.addAll(Arrays.asList(getQueryParamValues(qs, e, config.isQueryStringCaseSensitive()))); + output.put(e, newValues.toArray(new String[0])); + }); + } + + return output; + } + + protected String getSchemeFromHeader(Headers headers) { + // if we don't have any headers to deduce the value we assume HTTPS - API Gateway's default + if (headers == null) { + return "https"; + } + String cfScheme = headers.getFirst(CF_PROTOCOL_HEADER_NAME); + if (cfScheme != null && SecurityUtils.isValidScheme(cfScheme)) { + return cfScheme; + } + String gwScheme = headers.getFirst(PROTOCOL_HEADER_NAME); + if (gwScheme != null && SecurityUtils.isValidScheme(gwScheme)) { + return gwScheme; + } + // https is our default scheme + return "https"; + } /** * Prases a header value using the default value separator "," and qualifier separator ";". @@ -426,6 +708,39 @@ static String decodeRequestPath(String requestPath, ContainerConfig config) { } + static String cleanUri(String uri) { + String finalUri = (uri == null ? "/" : uri); + if (finalUri.equals("/")) { + return finalUri; + } + + if (!finalUri.startsWith("/")) { + finalUri = "/" + finalUri; + } + + if (finalUri.endsWith("/")) { + finalUri = finalUri.substring(0, finalUri.length() - 1); + } + + finalUri = finalUri.replaceAll("/+", "/"); + + return finalUri; + } + + static String decodeValueIfEncoded(String value) { + if (value == null) { + return null; + } + + try { + return URLDecoder.decode(value, LambdaContainerHandler.getContainerConfig().getUriEncoding()); + } catch (UnsupportedEncodingException e) { + log.warn("Could not decode body content - proceeding as if it was already decoded", e); + return value; + } + } + + /** * Class that represents a header value. */ diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequestWrapper.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequestWrapper.java index a8ee1840c..e151c03da 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequestWrapper.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequestWrapper.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.internal.servlet; import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletResponse.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletResponse.java index da695c509..153cdb91e 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletResponse.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletResponse.java @@ -419,7 +419,13 @@ public void flushBuffer() throws IOException { if (null != writer) { writer.flush(); } - responseBody = new String(bodyOutputStream.toByteArray(), LambdaContainerHandler.getContainerConfig().getDefaultContentCharset()); + String charset = characterEncoding; + + if(charset == null) { + charset = LambdaContainerHandler.getContainerConfig().getDefaultContentCharset(); + } + + responseBody = new String(bodyOutputStream.toByteArray(), charset); log.debug("Response buffer flushed with {} bytes, latch={}", responseBody.length(), writersCountDownLatch.getCount()); isCommitted = true; writersCountDownLatch.countDown(); diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpSession.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpSession.java index 1ac0908fd..8c3df7f86 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpSession.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpSession.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.internal.servlet; import org.slf4j.Logger; @@ -13,7 +25,6 @@ import java.util.HashMap; import java.util.Map; - /** * This class emulates the behavior of an HTTP session. At the moment a new instance of this class * is created for each request/event. In the future, we may define a session id resolver interface diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsLambdaServletContainerHandler.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsLambdaServletContainerHandler.java index 95af74e67..52631da54 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsLambdaServletContainerHandler.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsLambdaServletContainerHandler.java @@ -12,6 +12,7 @@ */ package com.amazonaws.serverless.proxy.internal.servlet; +import com.amazonaws.serverless.exceptions.ContainerInitializationException; import com.amazonaws.serverless.proxy.ExceptionHandler; import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.RequestReader; @@ -28,7 +29,8 @@ import javax.servlet.http.HttpServletResponseWrapper; import java.io.IOException; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; /** @@ -146,15 +148,38 @@ protected FilterChain getFilterChain(HttpServletRequest req, Servlet servlet) { * @throws ServletException */ protected void doFilter(HttpServletRequest request, HttpServletResponse response, Servlet servlet) throws IOException, ServletException { + if (AwsHttpServletRequest.class.isAssignableFrom(request.getClass())) { + ((AwsHttpServletRequest)request).setContainerHandler(this); + } + FilterChain chain = getFilterChain(request, servlet); chain.doFilter(request, response); - // if for some reason the response wasn't flushed yet, we force it here. - if (!response.isCommitted()) { + // if for some reason the response wasn't flushed yet, we force it here unless it's being processed asynchronously (WebFlux) + if (!response.isCommitted() && request.getDispatcherType() != DispatcherType.ASYNC) { response.flushBuffer(); } } + @Override + public void initialize() throws ContainerInitializationException { + // we expect all servlets to be wrapped in an AwsServletRegistration + ArrayList registrations = new ArrayList<>((Collection)getServletContext().getServletRegistrations().values()); + registrations.sort(AwsServletRegistration::compareTo); + for (AwsServletRegistration r : registrations) { + if (r.getLoadOnStartup() == -1) { // skip Servlets that can be lazily loaded + continue; + } + try { + if (r.getServlet() != null) { + r.getServlet().init(r.getServletConfig()); + } + } catch (ServletException e) { + throw new ContainerInitializationException("Could not initialize servlet " + r.getName(), e); + } + } + } + //------------------------------------------------------------- // Inner Class - //------------------------------------------------------------- diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java index 8d58850cf..97ae055bd 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java @@ -15,52 +15,36 @@ import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.internal.SecurityUtils; -import com.amazonaws.serverless.proxy.internal.testutils.Timer; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.ContainerConfig; import com.amazonaws.serverless.proxy.model.Headers; import com.amazonaws.services.lambda.runtime.Context; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import org.apache.commons.fileupload.FileItem; -import org.apache.commons.fileupload.FileUploadException; -import org.apache.commons.fileupload.disk.DiskFileItemFactory; -import org.apache.commons.fileupload.servlet.ServletFileUpload; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.input.NullInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.*; import javax.servlet.http.*; import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.SecurityContext; import java.io.BufferedReader; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.io.StringReader; import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.security.Principal; import java.time.Instant; import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.TreeMap; /** @@ -77,12 +61,8 @@ public class AwsProxyHttpServletRequest extends AwsHttpServletRequest { private AwsProxyRequest request; private SecurityContext securityContext; private AwsAsyncContext asyncContext; - private Map> urlEncodedFormParameters; - private Map multipartFormParameters; private static Logger log = LoggerFactory.getLogger(AwsProxyHttpServletRequest.class); private ContainerConfig config; - private AwsHttpServletResponse response; - private AwsLambdaServletContainerHandler containerHandler; //------------------------------------------------------------- // Constructors @@ -101,23 +81,10 @@ public AwsProxyHttpServletRequest(AwsProxyRequest awsProxyRequest, Context lambd this.config = config; } - public AwsProxyRequest getAwsProxyRequest() { return this.request; } - public AwsHttpServletResponse getResponse() { - return response; - } - - public void setResponse(AwsHttpServletResponse response) { - this.response = response; - } - - public void setContainerHandler(AwsLambdaServletContainerHandler containerHandler) { - this.containerHandler = containerHandler; - } - //------------------------------------------------------------- // Implementation - HttpServletRequest //------------------------------------------------------------- @@ -224,16 +191,7 @@ public String getPathTranslated() { @Override public String getContextPath() { - String contextPath = ""; - if (config.isUseStageAsServletContext() && request.getRequestContext().getStage() != null) { - log.debug("Using stage as context path"); - contextPath = cleanUri(request.getRequestContext().getStage()); - } - if (config.getServiceBasePath() != null) { - contextPath += cleanUri(config.getServiceBasePath()); - } - - return contextPath; + return generateContextPath(config, request.getRequestContext().getStage()); } @@ -279,19 +237,7 @@ public String getRequestURI() { @Override public StringBuffer getRequestURL() { - String url = ""; - url += getServerName(); - url += cleanUri(getContextPath()); - url += cleanUri(request.getPath()); - - return new StringBuffer(getScheme() + "://" + url); - } - - - @Override - public String getServletPath() { - // we always work on the root path - return ""; + return generateRequestURL(request.getPath()); } @@ -333,7 +279,7 @@ public Part getPart(String s) @Override public T upgrade(Class aClass) throws IOException, ServletException { - return null; + throw new UnsupportedOperationException(); } //------------------------------------------------------------- @@ -343,28 +289,10 @@ public T upgrade(Class aClass) @Override public String getCharacterEncoding() { - // we only look at content-type because content-encoding should only be used for - // "binary" requests such as gzip/deflate. - String contentTypeHeader = request.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE); - if (contentTypeHeader == null) { - return null; - } - - String[] contentTypeValues = contentTypeHeader.split(HEADER_VALUE_SEPARATOR); - if (contentTypeValues.length <= 1) { - return null; - } - - for (String contentTypeValue : contentTypeValues) { - if (contentTypeValue.trim().startsWith(ENCODING_VALUE_KEY)) { - String[] encodingValues = contentTypeValue.split(HEADER_KEY_VALUE_SEPARATOR); - if (encodingValues.length <= 1) { - return null; - } - return encodingValues[1]; - } + if (request.getMultiValueHeaders() == null) { + return config.getDefaultContentCharset(); } - return null; + return parseCharacterEncoding(request.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE)); } @@ -380,25 +308,7 @@ public void setCharacterEncoding(String s) return; } - if (currentContentType.contains(HEADER_VALUE_SEPARATOR)) { - String[] contentTypeValues = currentContentType.split(HEADER_VALUE_SEPARATOR); - StringBuilder contentType = new StringBuilder(contentTypeValues[0]); - - for (int i = 1; i < contentTypeValues.length; i++) { - String contentTypeValue = contentTypeValues[i]; - String contentTypeString = HEADER_VALUE_SEPARATOR + " " + contentTypeValue; - if (contentTypeValue.trim().startsWith(ENCODING_VALUE_KEY)) { - contentTypeString = HEADER_VALUE_SEPARATOR + " " + ENCODING_VALUE_KEY + HEADER_KEY_VALUE_SEPARATOR + s; - } - contentType.append(contentTypeString); - } - - request.getMultiValueHeaders().putSingle(HttpHeaders.CONTENT_TYPE, contentType.toString()); - } else { - request.getMultiValueHeaders().putSingle( - HttpHeaders.CONTENT_TYPE, - currentContentType + HEADER_VALUE_SEPARATOR + " " + ENCODING_VALUE_KEY + HEADER_KEY_VALUE_SEPARATOR + s); - } + request.getMultiValueHeaders().putSingle(HttpHeaders.CONTENT_TYPE, appendCharacterEncoding(currentContentType, s)); } @@ -432,37 +342,9 @@ public String getContentType() { return contentTypeHeader; } - - @Override - public ServletInputStream getInputStream() - throws IOException { - if (request.getBody() == null) { - return new AwsServletInputStream(new NullInputStream(0, false, false)); - } - byte[] bodyBytes; - if (request.isBase64Encoded()) { - bodyBytes = Base64.getMimeDecoder().decode(request.getBody()); - } else { - String encoding = getCharacterEncoding(); - if (encoding == null) { - encoding = StandardCharsets.ISO_8859_1.name(); - } - try { - bodyBytes = request.getBody().getBytes(encoding); - } catch (Exception e) { - log.error("Could not read request with character encoding: " + SecurityUtils.crlf(encoding), e); - bodyBytes = request.getBody().getBytes(StandardCharsets.ISO_8859_1.name()); - } - - } - ByteArrayInputStream requestBodyStream = new ByteArrayInputStream(bodyBytes); - return new AwsServletInputStream(requestBodyStream); - } - - @Override public String getParameter(String s) { - String queryStringParameter = getFirstQueryParamValue(s, config.isQueryStringCaseSensitive()); + String queryStringParameter = getFirstQueryParamValue(request.getMultiValueQueryStringParameters(), s, config.isQueryStringCaseSensitive()); if (queryStringParameter != null) { return queryStringParameter; } @@ -489,7 +371,7 @@ public Enumeration getParameterNames() { @Override @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 public String[] getParameterValues(String s) { - List values = new ArrayList<>(Arrays.asList(getQueryParamValues(s, config.isQueryStringCaseSensitive()))); + List values = new ArrayList<>(Arrays.asList(getQueryParamValues(request.getMultiValueQueryStringParameters(), s, config.isQueryStringCaseSensitive()))); values.addAll(Arrays.asList(getFormBodyParameterCaseInsensitive(s))); @@ -503,26 +385,7 @@ public String[] getParameterValues(String s) { @Override public Map getParameterMap() { - Map output = new HashMap<>(); - - Map> params = getFormUrlEncodedParametersMap(); - params.entrySet().stream().parallel().forEach(e -> { - output.put(e.getKey(), e.getValue().toArray(new String[0])); - }); - - if (request.getMultiValueQueryStringParameters() != null) { - request.getMultiValueQueryStringParameters().keySet().stream().parallel().forEach(e -> { - List newValues = new ArrayList<>(); - if (output.containsKey(e)) { - String[] values = output.get(e); - newValues.addAll(Arrays.asList(values)); - } - newValues.addAll(Arrays.asList(getQueryParamValues(e, config.isQueryStringCaseSensitive()))); - output.put(e, newValues.toArray(new String[0])); - }); - } - - return output; + return generateParameterMap(request.getMultiValueQueryStringParameters(), config); } @@ -534,20 +397,7 @@ public String getProtocol() { @Override public String getScheme() { - // if we don't have any headers to deduce the value we assume HTTPS - API Gateway's default - if (request.getMultiValueHeaders() == null) { - return "https"; - } - String cfScheme = request.getMultiValueHeaders().getFirst(CF_PROTOCOL_HEADER_NAME); - if (cfScheme != null && SecurityUtils.isValidScheme(cfScheme)) { - return cfScheme; - } - String gwScheme = request.getMultiValueHeaders().getFirst(PROTOCOL_HEADER_NAME); - if (gwScheme != null && SecurityUtils.isValidScheme(gwScheme)) { - return gwScheme; - } - // https is our default scheme - return "https"; + return getSchemeFromHeader(request.getMultiValueHeaders()); } @Override @@ -558,9 +408,11 @@ public String getServerName() { region = "us-east-1"; } - String hostHeader = request.getMultiValueHeaders().getFirst(HOST_HEADER_NAME); - if (hostHeader != null && SecurityUtils.isValidHost(hostHeader, request.getRequestContext().getApiId(), region)) { - return hostHeader; + if (request.getMultiValueHeaders() != null && request.getMultiValueHeaders().containsKey(HOST_HEADER_NAME)) { + String hostHeader = request.getMultiValueHeaders().getFirst(HOST_HEADER_NAME); + if (SecurityUtils.isValidHost(hostHeader, request.getRequestContext().getApiId(), region)) { + return hostHeader; + } } return new StringBuilder().append(request.getRequestContext().getApiId()) @@ -582,6 +434,14 @@ public int getServerPort() { } } + @Override + public ServletInputStream getInputStream() throws IOException { + if (requestInputStream == null) { + requestInputStream = new AwsServletInputStream(bodyStringToInputStream(request.getBody(), request.isBase64Encoded())); + } + return requestInputStream; + } + @Override public BufferedReader getReader() @@ -693,8 +553,8 @@ public AsyncContext startAsync() @Override public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException { + servletRequest.setAttribute(DISPATCHER_TYPE_ATTRIBUTE, DispatcherType.ASYNC); asyncContext = new AwsAsyncContext((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse, containerHandler); - setAttribute(DISPATCHER_TYPE_ATTRIBUTE, DispatcherType.ASYNC); log.debug("Starting async context for request: " + SecurityUtils.crlf(request.getRequestContext().getRequestId())); return asyncContext; } @@ -703,7 +563,7 @@ public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse se public AsyncContext getAsyncContext() { if (asyncContext == null) { throw new IllegalStateException("Request " + SecurityUtils.crlf(request.getRequestContext().getRequestId()) - + " is not in asynchronous mode. Call startAsync before atttempting to get the async context."); + + " is not in asynchronous mode. Call startAsync before attempting to get the async context."); } return asyncContext; } @@ -712,128 +572,6 @@ public AsyncContext getAsyncContext() { // Methods - Private //------------------------------------------------------------- - private String[] getFormBodyParameterCaseInsensitive(String key) { - List values = getFormUrlEncodedParametersMap().get(key); - if (values != null) { - String[] valuesArray = new String[values.size()]; - valuesArray = values.toArray(valuesArray); - return valuesArray; - } else { - return new String[0]; - } - } - - - @SuppressFBWarnings({"FILE_UPLOAD_FILENAME", "WEAK_FILENAMEUTILS"}) - private Map getMultipartFormParametersMap() { - if (multipartFormParameters != null) { - return multipartFormParameters; - } - if (!ServletFileUpload.isMultipartContent(this)) { // isMultipartContent also checks the content type - multipartFormParameters = new HashMap<>(); - return multipartFormParameters; - } - Timer.start("SERVLET_REQUEST_GET_MULTIPART_PARAMS"); - multipartFormParameters = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - - ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory()); - - try { - List items = upload.parseRequest(this); - for (FileItem item : items) { - String fileName = FilenameUtils.getName(item.getName()); - AwsProxyRequestPart newPart = new AwsProxyRequestPart(item.get()); - newPart.setName(item.getFieldName()); - newPart.setSubmittedFileName(fileName); - newPart.setContentType(item.getContentType()); - newPart.setSize(item.getSize()); - item.getHeaders().getHeaderNames().forEachRemaining(h -> { - newPart.addHeader(h, item.getHeaders().getHeader(h)); - }); - - multipartFormParameters.put(item.getFieldName(), newPart); - } - } catch (FileUploadException e) { - Timer.stop("SERVLET_REQUEST_GET_MULTIPART_PARAMS"); - log.error("Could not read multipart upload file", e); - } - Timer.stop("SERVLET_REQUEST_GET_MULTIPART_PARAMS"); - return multipartFormParameters; - } - - - static String cleanUri(String uri) { - String finalUri = (uri == null ? "/" : uri); - if (finalUri.equals("/")) { - return finalUri; - } - - if (!finalUri.startsWith("/")) { - finalUri = "/" + finalUri; - } - - if (finalUri.endsWith("/")) { - finalUri = finalUri.substring(0, finalUri.length() - 1); - } - - finalUri = finalUri.replaceAll("/+", "/"); - - return finalUri; - } - - - private Map> getFormUrlEncodedParametersMap() { - if (urlEncodedFormParameters != null) { - return urlEncodedFormParameters; - } - String contentType = getContentType(); - if (contentType == null) { - urlEncodedFormParameters = new HashMap<>(); - return urlEncodedFormParameters; - } - if (!contentType.startsWith(MediaType.APPLICATION_FORM_URLENCODED) || !getMethod().toLowerCase(Locale.ENGLISH).equals("post")) { - urlEncodedFormParameters = new HashMap<>(); - return urlEncodedFormParameters; - } - Timer.start("SERVLET_REQUEST_GET_FORM_PARAMS"); - String rawBodyContent = null; - try { - rawBodyContent = IOUtils.toString(getInputStream()); - } catch (IOException e) { - throw new RuntimeException(e); - } - - urlEncodedFormParameters = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - for (String parameter : rawBodyContent.split(FORM_DATA_SEPARATOR)) { - String[] parameterKeyValue = parameter.split(HEADER_KEY_VALUE_SEPARATOR); - if (parameterKeyValue.length < 2) { - continue; - } - List values = new ArrayList<>(); - if (urlEncodedFormParameters.containsKey(parameterKeyValue[0])) { - values = urlEncodedFormParameters.get(parameterKeyValue[0]); - } - values.add(decodeValueIfEncoded(parameterKeyValue[1])); - urlEncodedFormParameters.put(decodeValueIfEncoded(parameterKeyValue[0]), values); - } - Timer.stop("SERVLET_REQUEST_GET_FORM_PARAMS"); - return urlEncodedFormParameters; - } - - - public static String decodeValueIfEncoded(String value) { - if (value == null) { - return null; - } - - try { - return URLDecoder.decode(value, LambdaContainerHandler.getContainerConfig().getUriEncoding()); - } catch (UnsupportedEncodingException e) { - log.warn("Could not decode body content - proceeding as if it was already decoded", e); - return value; - } - } - private List getHeaderValues(String key) { // special cases for referer and user agent headers List values = new ArrayList<>(); @@ -857,84 +595,4 @@ private List getHeaderValues(String key) { } - private String getFirstQueryParamValue(String key, boolean isCaseSensitive) { - if (request.getMultiValueQueryStringParameters() != null) { - if (isCaseSensitive) { - return request.getMultiValueQueryStringParameters().getFirst(key); - } - - for (String k : request.getMultiValueQueryStringParameters().keySet()) { - if (k.toLowerCase(Locale.getDefault()).equals(key.toLowerCase(Locale.getDefault()))) { - return request.getMultiValueQueryStringParameters().getFirst(k); - } - } - } - - return null; - } - - public String[] getQueryParamValues(String key, boolean isCaseSensitive) { - if (request.getMultiValueQueryStringParameters() != null) { - if (isCaseSensitive) { - return request.getMultiValueQueryStringParameters().get(key).toArray(new String[0]); - } - - for (String k : request.getMultiValueQueryStringParameters().keySet()) { - if (k.toLowerCase(Locale.getDefault()).equals(key.toLowerCase(Locale.getDefault()))) { - return request.getMultiValueQueryStringParameters().get(k).toArray(new String[0]); - } - } - } - - return new String[0]; - } - - - public static class AwsServletInputStream extends ServletInputStream { - - private InputStream bodyStream; - private ReadListener listener; - - public AwsServletInputStream(InputStream body) { - bodyStream = body; - } - - - @Override - public boolean isFinished() { - return true; - } - - - @Override - public boolean isReady() { - return true; - } - - - @Override - public void setReadListener(ReadListener readListener) { - listener = readListener; - try { - listener.onDataAvailable(); - } catch (IOException e) { - log.error("Data not available on input stream", e); - } - } - - - @Override - public int read() - throws IOException { - if (bodyStream == null || bodyStream instanceof NullInputStream) { - return -1; - } - int readByte = bodyStream.read(); - if (bodyStream.available() == 0 && listener != null) { - listener.onAllDataRead(); - } - return readByte; - } - - } } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReader.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReader.java index 405ae9739..932041db9 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReader.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReader.java @@ -19,6 +19,7 @@ import com.amazonaws.services.lambda.runtime.Context; import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.SecurityContext; @@ -26,8 +27,9 @@ * Simple implementation of the RequestReader interface that receives an AwsProxyRequest * object and uses it to initialize a AwsProxyHttpServletRequest object. */ -public class AwsProxyHttpServletRequestReader extends RequestReader { +public class AwsProxyHttpServletRequestReader extends RequestReader { static final String INVALID_REQUEST_ERROR = "The incoming event is not a valid request from Amazon API Gateway or an Application Load Balancer"; + private ServletContext servletContext; //------------------------------------------------------------- // Methods - Implementation @@ -38,7 +40,7 @@ public void setServletContext(ServletContext ctx) { } @Override - public AwsProxyHttpServletRequest readRequest(AwsProxyRequest request, SecurityContext securityContext, Context lambdaContext, ContainerConfig config) + public HttpServletRequest readRequest(AwsProxyRequest request, SecurityContext securityContext, Context lambdaContext, ContainerConfig config) throws InvalidRequestEventException { // Expect the HTTP method and context to be populated. If they are not, we are handling an // unsupported event type. diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletResponseWriter.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletResponseWriter.java index 32a86f667..ba98eedb2 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletResponseWriter.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletResponseWriter.java @@ -57,7 +57,7 @@ public AwsProxyResponse writeResponse(AwsHttpServletResponse containerResponse, awsProxyResponse.setStatusCode(containerResponse.getStatus()); - if (containerResponse.getAwsProxyRequest().getRequestSource() == AwsProxyRequest.RequestSource.ALB) { + if (containerResponse.getAwsProxyRequest() != null && containerResponse.getAwsProxyRequest().getRequestSource() == AwsProxyRequest.RequestSource.ALB) { awsProxyResponse.setStatusDescription(containerResponse.getStatus() + " " + Response.Status.fromStatusCode(containerResponse.getStatus()).getReasonPhrase()); } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyRequestDispatcher.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyRequestDispatcher.java index 2ffcc97de..27a352ebf 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyRequestDispatcher.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyRequestDispatcher.java @@ -1,8 +1,20 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.internal.servlet; - import com.amazonaws.serverless.proxy.internal.SecurityUtils; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,9 +26,9 @@ import java.io.IOException; import static com.amazonaws.serverless.proxy.RequestReader.API_GATEWAY_EVENT_PROPERTY; +import static com.amazonaws.serverless.proxy.RequestReader.HTTP_API_EVENT_PROPERTY; import static com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletRequest.DISPATCHER_TYPE_ATTRIBUTE; - /** * Default RequestDispatcher implementation for the AwsProxyHttpServletRequest type. A new * instance of this object is created each time a framework gets the RequestDispatcher from a servlet request. Behind @@ -115,12 +127,22 @@ void setRequestPath(ServletRequest req, final String destinationPath) { ((AwsProxyHttpServletRequest) req).getAwsProxyRequest().setPath(dispatchTo); return; } + if (req instanceof AwsHttpApiV2ProxyHttpServletRequest) { + ((AwsHttpApiV2ProxyHttpServletRequest) req).getRequest().setRawPath(destinationPath); + return; + } - log.debug("Request is not an AwsProxyHttpServletRequest, attempting to extract the proxy event type"); - if (req.getAttribute(API_GATEWAY_EVENT_PROPERTY) == null || !(req.getAttribute(API_GATEWAY_EVENT_PROPERTY) instanceof AwsProxyRequest)) { - throw new IllegalStateException("ServletRequest object does not contain API Gateway event"); + log.debug("Request is not an proxy request generated by this library, attempting to extract the proxy event type from the request attributes"); + if (req.getAttribute(API_GATEWAY_EVENT_PROPERTY) != null && req.getAttribute(API_GATEWAY_EVENT_PROPERTY) instanceof AwsProxyRequest) { + ((AwsProxyRequest)req.getAttribute(API_GATEWAY_EVENT_PROPERTY)).setPath(dispatchTo); + return; + } + if (req.getAttribute(HTTP_API_EVENT_PROPERTY) != null && req.getAttribute(HTTP_API_EVENT_PROPERTY) instanceof HttpApiV2ProxyRequest) { + ((HttpApiV2ProxyRequest)req.getAttribute(HTTP_API_EVENT_PROPERTY)).setRawPath(destinationPath); + return; } - ((AwsProxyRequest)req.getAttribute(API_GATEWAY_EVENT_PROPERTY)).setPath(dispatchTo); + + throw new IllegalStateException("Could not set new target path for the given ServletRequest object"); } private Servlet getServlet(HttpServletRequest req) { diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletContext.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletContext.java index 97e866e78..cf2e3920b 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletContext.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletContext.java @@ -185,25 +185,21 @@ public Servlet getServletForPath(String path) { String[] pathParts = path.split("/"); for (AwsServletRegistration reg : servletRegistrations.values()) { for (String p : reg.getMappings()) { - try { - if ("".equals(p) || "/".equals(p) || "/*".equals(p)) { - return reg.getServlet(); - } - // if I have no path and I haven't matched something now I'll just move on to the next - if ("".equals(path) || "/".equals(path)) { - continue; + if ("".equals(p) || "/".equals(p) || "/*".equals(p)) { + return reg.getServlet(); + } + // if I have no path and I haven't matched something now I'll just move on to the next + if ("".equals(path) || "/".equals(path)) { + continue; + } + String[] regParts = p.split("/"); + for (int i = 0; i < regParts.length; i++) { + if (!regParts[i].equals(pathParts[i]) && !"*".equals(regParts[i])) { + break; } - String[] regParts = p.split("/"); - for (int i = 0; i < regParts.length; i++) { - if (!regParts[i].equals(pathParts[i]) && !"*".equals(regParts[i])) { - break; - } - if (i == regParts.length - 1 && (regParts[i].equals(pathParts[i]) || "*".equals(regParts[i]))) { - return reg.getServlet(); - } + if (i == regParts.length - 1 && (regParts[i].equals(pathParts[i]) || "*".equals(regParts[i]))) { + return reg.getServlet(); } - } catch (ServletException e) { - return null; } } } @@ -216,12 +212,7 @@ public Servlet getServletForPath(String path) { public Enumeration getServlets() { return Collections.enumeration(servletRegistrations.entrySet().stream() .map((e) -> { - try { - return e.getValue().getServlet(); - } catch (ServletException ex) { - ex.printStackTrace(); - return null; - } + return e.getValue().getServlet(); } ) .collect(Collectors.toList())); } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletInputStream.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletInputStream.java new file mode 100644 index 000000000..552e75a89 --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletInputStream.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.internal.servlet; + +import org.apache.commons.io.input.NullInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class AwsServletInputStream extends ServletInputStream { + private static Logger log = LoggerFactory.getLogger(AwsServletInputStream.class); + private InputStream bodyStream; + private ReadListener listener; + private boolean finished; + + public AwsServletInputStream(InputStream body) { + bodyStream = body; + finished = false; + } + + @Override + public boolean isFinished() { + return finished; + } + + @Override + public boolean isReady() { + if (finished && listener != null) { + try { + listener.onAllDataRead(); + } catch (IOException e) { + log.error("Could not notify listeners that input stream data is ready", e); + throw new RuntimeException(e); + } + } + return !finished; + } + + @Override + public void setReadListener(ReadListener readListener) { + listener = readListener; + try { + listener.onDataAvailable(); + } catch (IOException e) { + log.error("Could not notify listeners that data is available", e); + throw new RuntimeException(e); + } + } + + @Override + public int read() + throws IOException { + if (bodyStream == null || bodyStream instanceof NullInputStream) { + return -1; + } + int readByte = bodyStream.read(); + if (readByte == -1) { + finished = true; + } + return readByte; + } +} diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletRegistration.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletRegistration.java index e3ce620bf..021503c13 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletRegistration.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsServletRegistration.java @@ -1,12 +1,26 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.internal.servlet; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import javax.servlet.*; import java.util.*; /** * Stores information about a servlet registered with Serverless Java Container's ServletContext. */ -public class AwsServletRegistration implements ServletRegistration, ServletRegistration.Dynamic { +public class AwsServletRegistration implements ServletRegistration, ServletRegistration.Dynamic, Comparable { private String servletName; private Servlet servlet; private AwsServletContext ctx; @@ -91,10 +105,7 @@ public Map getInitParameters() { return initParameters; } - public Servlet getServlet() throws ServletException { - if (servlet.getServletConfig() == null) { - servlet.init(getServletConfig()); - } + public Servlet getServlet() { return servlet; } @@ -127,6 +138,20 @@ public void setAsyncSupported(boolean b) { asyncSupported = b; } + @Override + public int compareTo(AwsServletRegistration r) { + return Integer.compare(loadOnStartup, r.getLoadOnStartup()); + } + + @Override + @SuppressFBWarnings("HE_EQUALS_USE_HASHCODE") + public boolean equals(Object r) { + if (r == null || !AwsServletRegistration.class.isAssignableFrom(r.getClass())) { + return false; + } + return ((AwsServletRegistration)r).getName().equals(getName()) && ((AwsServletRegistration)r).getServlet() == getServlet(); + } + public boolean isAsyncSupported() { return asyncSupported; } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/FilterChainManager.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/FilterChainManager.java index 8da92f1b3..92ff55daf 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/FilterChainManager.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/FilterChainManager.java @@ -14,15 +14,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import javax.servlet.DispatcherType; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.Servlet; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; +import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @@ -31,6 +23,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.Predicate; /** * This object in in charge of matching a servlet request to a set of filter, creating the filter chain for a request, @@ -104,12 +97,17 @@ FilterChainHolder getFilterChain(final HttpServletRequest request, Servlet servl return getFilterChainCache(type, targetPath, servlet); } + AwsServletRegistration servletRegistration = (AwsServletRegistration)servletContext.getServletRegistrations() + .values().stream() + .filter((Predicate) servletRegistration1 -> ((AwsServletRegistration) servletRegistration1).getServlet().equals(servlet)) + .findFirst().orElse(null); + FilterChainHolder chainHolder = new FilterChainHolder(); Map registrations = getFilterHolders(); if (registrations == null || registrations.size() == 0) { - if (servlet != null) { - chainHolder.addFilter(new FilterHolder(new ServletExecutionFilter(servlet), servletContext)); + if (servletRegistration != null) { + chainHolder.addFilter(new FilterHolder(new ServletExecutionFilter(servletRegistration), servletContext)); } return chainHolder; } @@ -130,8 +128,8 @@ FilterChainHolder getFilterChain(final HttpServletRequest request, Servlet servl // we assume we only ever have one servlet. } - if (servlet != null) { - chainHolder.addFilter(new FilterHolder(new ServletExecutionFilter(servlet), servletContext)); + if (servletRegistration != null) { + chainHolder.addFilter(new FilterHolder(new ServletExecutionFilter(servletRegistration), servletContext)); } putFilterChainCache(type, targetPath, chainHolder); @@ -332,23 +330,30 @@ void setDispatcherType(DispatcherType dispatcherType) { private class ServletExecutionFilter implements Filter { private FilterConfig config; - private Servlet handlerServlet; + private AwsServletRegistration handlerServlet; + private boolean initialized; - public ServletExecutionFilter(Servlet handler) { - handlerServlet = handler; + public ServletExecutionFilter(AwsServletRegistration servletReg) { + handlerServlet = servletReg; + initialized = handlerServlet.getServlet().getServletInfo() != null; } @Override public void init(FilterConfig filterConfig) throws ServletException { + if (initialized) { + return; + } config = filterConfig; + handlerServlet.getServlet().init(handlerServlet.getServletConfig()); + initialized = true; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { - handlerServlet.service(servletRequest, servletResponse); + handlerServlet.getServlet().service(servletRequest, servletResponse); filterChain.doFilter(servletRequest, servletResponse); } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/FilterHolder.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/FilterHolder.java index 893075d2c..987dff14c 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/FilterHolder.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/FilterHolder.java @@ -83,6 +83,9 @@ public FilterHolder(String name, Filter newFilter, ServletContext context) { * @throws ServletException Propagates any servlet exception thrown by the filter initialization */ public void init() throws ServletException { + if (this.filterInitialized) { + return; + } this.getFilter().init(filterConfig); this.filterInitialized = true; } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/ServletLambdaContainerHandlerBuilder.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/ServletLambdaContainerHandlerBuilder.java index 50743e73f..88e59fb61 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/ServletLambdaContainerHandlerBuilder.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/ServletLambdaContainerHandlerBuilder.java @@ -1,9 +1,22 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.internal.servlet; import com.amazonaws.serverless.exceptions.ContainerInitializationException; import com.amazonaws.serverless.proxy.*; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; @@ -89,6 +102,23 @@ public Builder defaultProxy() { return self(); } + /** + * Sets all of the required fields in the builder to the default settings for a Servlet-compatible framework that wants + * to support HTTP API's v2 proxy event + * @return A populated builder + */ + public Builder defaultHttpApiV2Proxy() { + initializationWrapper(new InitializationWrapper()) + .requestReader((RequestReader) new AwsHttpApiV2HttpServletRequestReader()) + .responseWriter((ResponseWriter) new AwsProxyHttpServletResponseWriter()) + .securityContextWriter((SecurityContextWriter) new AwsHttpApiV2SecurityContextWriter()) + .exceptionHandler((ExceptionHandler) new AwsProxyExceptionHandler()) + .requestTypeClass((Class) HttpApiV2ProxyRequest.class) + .responseTypeClass((Class) AwsProxyResponse.class); + return self(); + + } + /** * Sets the initialization wrapper to be used by the {@link ServletLambdaContainerHandlerBuilder#buildAndInitialize()} * method to start the framework implementations @@ -132,11 +162,29 @@ public Builder responseTypeClass(Class responseType) { return self(); } + /** + * Uses an async initializer with the given start time to calculate the 10 seconds timeout. + * + * @deprecated As of release 1.5 this method is deprecated in favor of the parameters-less one {@link ServletLambdaContainerHandlerBuilder#asyncInit()}. + * @param actualStartTime An epoch in milliseconds that should be used to calculate the 10 seconds timeout since the start of the application + * @return A builder configured to use the async initializer + */ + @Deprecated public Builder asyncInit(long actualStartTime) { this.initializationWrapper = new AsyncInitializationWrapper(actualStartTime); return self(); } + /** + * Uses a new {@link AsyncInitializationWrapper} with the no-parameter constructor that takes the actual JVM + * start time + * @return A builder configured to use an async initializer + */ + public Builder asyncInit() { + this.initializationWrapper = new AsyncInitializationWrapper(); + return self(); + } + /** * Implementations should implement this method to return their type. All of the builder methods in this abstract * class use this method to return the correct builder type. diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java index 3260d433c..8d4dc63f2 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java @@ -13,15 +13,7 @@ package com.amazonaws.serverless.proxy.internal.testutils; import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; -import com.amazonaws.serverless.proxy.model.AlbContext; -import com.amazonaws.serverless.proxy.model.ApiGatewayAuthorizerContext; -import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; -import com.amazonaws.serverless.proxy.model.ApiGatewayRequestIdentity; -import com.amazonaws.serverless.proxy.model.AwsProxyRequest; -import com.amazonaws.serverless.proxy.model.CognitoAuthorizerClaims; -import com.amazonaws.serverless.proxy.model.ContainerConfig; -import com.amazonaws.serverless.proxy.model.Headers; -import com.amazonaws.serverless.proxy.model.MultiValuedTreeMap; +import com.amazonaws.serverless.proxy.model.*; import com.fasterxml.jackson.core.JsonProcessingException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -42,8 +34,13 @@ import java.net.URLEncoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.UUID; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.Collectors; /** @@ -71,6 +68,10 @@ public AwsProxyRequestBuilder(String path) { this(path, null); } + public AwsProxyRequestBuilder(AwsProxyRequest req) { + request = req; + } + public AwsProxyRequestBuilder(String path, String httpMethod) { this.request = new AwsProxyRequest(); @@ -100,6 +101,25 @@ public AwsProxyRequestBuilder alb() { this.request.getRequestContext().getElb().setTargetGroupArn( "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-target/d6190d154bc908a5" ); + + // ALB does not decode query string parameters so we re-encode them all + if (request.getMultiValueQueryStringParameters() != null) { + MultiValuedTreeMap newQs = new MultiValuedTreeMap<>(); + for (Map.Entry> e : request.getMultiValueQueryStringParameters().entrySet()) { + for (String v : e.getValue()) { + try { + // this is a terrible hack. In our Spring tests we use the comma as a control character for lists + // this is allowed by the HTTP specs although not recommended. + String key = URLEncoder.encode(e.getKey(), "UTF-8").replaceAll("%2C", ","); + String value = URLEncoder.encode(v, "UTF-8").replaceAll("%2C", ","); + newQs.add(key, value); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException("Could not encode query string parameters: " + e.getKey() + "=" + v, ex); + } + } + } + request.setMultiValueQueryStringParameters(newQs); + } return this; } @@ -188,6 +208,16 @@ public AwsProxyRequestBuilder header(String key, String value) { return this; } + public AwsProxyRequestBuilder multiValueHeaders(Headers h) { + this.request.setMultiValueHeaders(h); + return this; + } + + public AwsProxyRequestBuilder multiValueQueryString(MultiValuedTreeMap params) { + this.request.setMultiValueQueryStringParameters(params); + return this; + } + public AwsProxyRequestBuilder queryString(String key, String value) { if (this.request.getMultiValueQueryStringParameters() == null) { @@ -228,7 +258,7 @@ public AwsProxyRequestBuilder nullBody() { } public AwsProxyRequestBuilder body(Object body) { - if (request.getMultiValueHeaders() != null && request.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE).equals(MediaType.APPLICATION_JSON)) { + if (request.getMultiValueHeaders() != null && request.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.APPLICATION_JSON)) { try { return body(LambdaContainerHandler.getObjectMapper().writeValueAsString(body)); } catch (JsonProcessingException e) { @@ -239,6 +269,14 @@ public AwsProxyRequestBuilder body(Object body) { } } + public AwsProxyRequestBuilder apiId(String id) { + if (request.getRequestContext() == null) { + request.setRequestContext(new AwsProxyRequestContext()); + } + request.getRequestContext().setApiId(id); + return this; + } + public AwsProxyRequestBuilder binaryBody(InputStream is) throws IOException { this.request.setIsBase64Encoded(true); @@ -400,4 +438,101 @@ public InputStream buildStream() { return null; } } + + public InputStream toHttpApiV2RequestStream() { + HttpApiV2ProxyRequest req = toHttpApiV2Request(); + try { + String requestJson = LambdaContainerHandler.getObjectMapper().writeValueAsString(req); + return new ByteArrayInputStream(requestJson.getBytes(StandardCharsets.UTF_8)); + } catch (JsonProcessingException e) { + return null; + } + } + + public HttpApiV2ProxyRequest toHttpApiV2Request() { + HttpApiV2ProxyRequest req = new HttpApiV2ProxyRequest(); + req.setRawPath(request.getPath()); + req.setBase64Encoded(request.isBase64Encoded()); + req.setBody(request.getBody()); + if (request.getMultiValueHeaders() != null && request.getMultiValueHeaders().containsKey(HttpHeaders.COOKIE)) { + req.setCookies(Arrays.asList(request.getMultiValueHeaders().getFirst(HttpHeaders.COOKIE).split(";"))); + } + req.setHeaders(new HashMap<>()); + if (request.getMultiValueHeaders() != null) { + request.getMultiValueHeaders().forEach((key, value) -> req.getHeaders().put(key, value.get(0))); + } + if (request.getRequestContext() != null && request.getRequestContext().getIdentity() != null) { + if (request.getRequestContext().getIdentity().getCaller() != null) { + req.getHeaders().put("Referer", request.getRequestContext().getIdentity().getCaller()); + } + if (request.getRequestContext().getIdentity().getUserAgent() != null) { + req.getHeaders().put(HttpHeaders.USER_AGENT, request.getRequestContext().getIdentity().getUserAgent()); + } + + } + if (request.getMultiValueQueryStringParameters() != null) { + StringBuilder rawQueryString = new StringBuilder(); + request.getMultiValueQueryStringParameters().forEach((k, v) -> { + for (String s : v) { + rawQueryString.append("&"); + rawQueryString.append(k); + rawQueryString.append("="); + try { + // same terrible hack as the alb() method. Because our spring tests use commas as control characters + // we do not encode it + rawQueryString.append(URLEncoder.encode(s, "UTF-8").replaceAll("%2C", ",")); + } catch (UnsupportedEncodingException e) { + System.out.println("Ex!"); + throw new RuntimeException(e); + } + } + }); + String qs = rawQueryString.toString(); + if (qs.length() > 1) { + req.setRawQueryString(qs.substring(1)); + } + } + req.setRouteKey("$default"); + req.setVersion("2.0"); + req.setStageVariables(request.getStageVariables()); + + HttpApiV2ProxyRequestContext ctx = new HttpApiV2ProxyRequestContext(); + HttpApiV2HttpContext httpCtx = new HttpApiV2HttpContext(); + httpCtx.setMethod(request.getHttpMethod()); + httpCtx.setPath(request.getPath()); + httpCtx.setProtocol("HTTP/1.1"); + if (request.getRequestContext() != null && request.getRequestContext().getIdentity() != null && request.getRequestContext().getIdentity().getSourceIp() != null) { + httpCtx.setSourceIp(request.getRequestContext().getIdentity().getSourceIp()); + } else { + httpCtx.setSourceIp("127.0.0.1"); + } + if (request.getRequestContext() != null && request.getRequestContext().getIdentity() != null && request.getRequestContext().getIdentity().getUserAgent() != null) { + httpCtx.setUserAgent(request.getRequestContext().getIdentity().getUserAgent()); + } + ctx.setHttp(httpCtx); + if (request.getRequestContext() != null) { + ctx.setAccountId(request.getRequestContext().getAccountId()); + ctx.setApiId(request.getRequestContext().getApiId()); + ctx.setDomainName(request.getRequestContext().getApiId() + ".execute-api.us-east-1.apigateway.com"); + ctx.setDomainPrefix(request.getRequestContext().getApiId()); + ctx.setRequestId(request.getRequestContext().getRequestId()); + ctx.setRouteKey("$default"); + ctx.setStage(request.getRequestContext().getStage()); + ctx.setTimeEpoch(request.getRequestContext().getRequestTimeEpoch()); + ctx.setTime(request.getRequestContext().getRequestTime()); + + if (request.getRequestContext().getAuthorizer() != null) { + HttpApiV2AuthorizerMap auth = new HttpApiV2AuthorizerMap(); + HttpApiV2JwtAuthorizer jwt = new HttpApiV2JwtAuthorizer(); + // TODO: Anything we should map here? + jwt.setClaims(new HashMap<>()); + jwt.setScopes(new ArrayList<>()); + auth.putJwtAuthorizer(jwt); + ctx.setAuthorizer(auth); + } + } + req.setRequestContext(ctx); + + return req; + } } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/Timer.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/Timer.java index b8c332aab..b89ca934f 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/Timer.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/Timer.java @@ -1,10 +1,20 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.internal.testutils; - import java.util.LinkedHashMap; import java.util.Map; - public final class Timer { private volatile static Map timers = new LinkedHashMap<>(); private volatile static boolean enabled = false; diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/AlbContext.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/AlbContext.java index 1965231be..527fc222f 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/AlbContext.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/AlbContext.java @@ -1,6 +1,17 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.model; - /*** * Context passed by ALB proxy events */ diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/ContainerConfig.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/ContainerConfig.java index e5c80d635..5edeeebbe 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/ContainerConfig.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/ContainerConfig.java @@ -1,6 +1,17 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.model; - import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; import java.util.ArrayList; @@ -8,7 +19,6 @@ import java.util.HashSet; import java.util.List; - /** * Configuration parameters for the framework */ @@ -16,7 +26,7 @@ public class ContainerConfig { public static final String DEFAULT_URI_ENCODING = "UTF-8"; public static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1"; private static final List DEFAULT_FILE_PATHS = new ArrayList() {{ add("/tmp"); add("/var/task"); }}; - private static final int MAX_INIT_TIMEOUT_MS = 10_000; + private static final int MAX_INIT_TIMEOUT_MS = 20_000; public static ContainerConfig defaultConfig() { ContainerConfig configuration = new ContainerConfig(); @@ -29,6 +39,7 @@ public static ContainerConfig defaultConfig() { configuration.addBinaryContentTypes("application/octet-stream", "image/jpeg", "image/png", "image/gif"); configuration.setDefaultContentCharset(DEFAULT_CONTENT_CHARSET); configuration.setInitializationTimeout(MAX_INIT_TIMEOUT_MS); + configuration.setDisableExceptionMapper(false); return configuration; } @@ -48,6 +59,7 @@ public static ContainerConfig defaultConfig() { private boolean queryStringCaseSensitive; private final HashSet binaryContentTypes; private int initializationTimeout; + private boolean disableExceptionMapper; public ContainerConfig() { validFilePaths = new ArrayList<>(); @@ -297,4 +309,23 @@ public int getInitializationTimeout() { public void setInitializationTimeout(int initializationTimeout) { this.initializationTimeout = initializationTimeout; } + + /** + * Whether the framework will run exception thrown by the application through the implementation of + * {@link com.amazonaws.serverless.proxy.ExceptionHandler}. When this parameter is set to false the Lambda + * container handler object lets the Exception propagate upwards to the Lambda handler class. + * @return true if exception mapping is disabled, false otherwise. + */ + public boolean isDisableExceptionMapper() { + return disableExceptionMapper; + } + + /** + * This configuration parameter tells the container whether it should skip exception mapping and simply let any + * Exception thrown by the underlying application bubble up to the Lambda handler class. + * @param disable Set this value to true to disable exception mapping, false otherwise. + */ + public void setDisableExceptionMapper(boolean disable) { + this.disableExceptionMapper = disable; + } } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/Headers.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/Headers.java index 0de0d204a..73f9d2841 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/Headers.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/Headers.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.model; public class Headers extends MultiValuedTreeMap { diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2AuthorizerMap.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2AuthorizerMap.java new file mode 100644 index 000000000..e7017f60f --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2AuthorizerMap.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.model; + +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; +import java.util.HashMap; + +@JsonSerialize(using = HttpApiV2AuthorizerMap.HttpApiV2AuthorizerSerializer.class) +@JsonDeserialize(using = HttpApiV2AuthorizerMap.HttpApiV2AuthorizerDeserializer.class) +public class HttpApiV2AuthorizerMap extends HashMap { + private static final String JWT_KEY = "jwt"; + private static final long serialVersionUID = 42L; + + public HttpApiV2JwtAuthorizer getJwtAuthorizer() { + return (HttpApiV2JwtAuthorizer)get(JWT_KEY); + } + + public boolean isJwt() { + return containsKey(JWT_KEY); + } + + public void putJwtAuthorizer(HttpApiV2JwtAuthorizer jwt) { + put(JWT_KEY, jwt); + } + + public static class HttpApiV2AuthorizerDeserializer extends StdDeserializer { + private static final long serialVersionUID = 42L; + + public HttpApiV2AuthorizerDeserializer() { + super(HttpApiV2AuthorizerMap.class); + } + + @Override + public HttpApiV2AuthorizerMap deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + HttpApiV2AuthorizerMap map = new HttpApiV2AuthorizerMap(); + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + if (node.get(JWT_KEY) != null) { + HttpApiV2JwtAuthorizer authorizer = LambdaContainerHandler.getObjectMapper().treeToValue(node.get(JWT_KEY), HttpApiV2JwtAuthorizer.class); + map.putJwtAuthorizer(authorizer); + } + // we ignore other, unknown values + return map; + } + } + + public static class HttpApiV2AuthorizerSerializer extends StdSerializer { + private static final long serialVersionUID = 42L; + + public HttpApiV2AuthorizerSerializer() { + super(HttpApiV2AuthorizerMap.class); + } + + @Override + public void serialize(HttpApiV2AuthorizerMap httpApiV2AuthorizerMap, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + if (httpApiV2AuthorizerMap.isJwt()) { + jsonGenerator.writeObjectField(JWT_KEY, httpApiV2AuthorizerMap.getJwtAuthorizer()); + } + jsonGenerator.writeEndObject(); + } + } +} \ No newline at end of file diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2HttpContext.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2HttpContext.java new file mode 100644 index 000000000..e6eb12876 --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2HttpContext.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.model; + +public class HttpApiV2HttpContext { + private String method; + private String path; + private String protocol; + private String sourceIp; + private String userAgent; + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getSourceIp() { + return sourceIp; + } + + public void setSourceIp(String sourceIp) { + this.sourceIp = sourceIp; + } + + public String getUserAgent() { + return userAgent; + } + + public void setUserAgent(String userAgent) { + this.userAgent = userAgent; + } +} diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2JwtAuthorizer.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2JwtAuthorizer.java new file mode 100644 index 000000000..d81a3c77f --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2JwtAuthorizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.model; + +import java.util.List; +import java.util.Map; + +public class HttpApiV2JwtAuthorizer { + private Map claims; + private List scopes; + + public Map getClaims() { + return claims; + } + + public void setClaims(Map claims) { + this.claims = claims; + } + + public List getScopes() { + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } +} diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequest.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequest.java new file mode 100644 index 000000000..6eee7dc4d --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +public class HttpApiV2ProxyRequest { + private String version; + private String routeKey; + private String rawPath; + private String rawQueryString; + private List cookies; + private Map headers; + private Map queryStringParameters; + private String body; + private boolean isBase64Encoded; + private Map stageVariables; + private HttpApiV2ProxyRequestContext requestContext; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getRouteKey() { + return routeKey; + } + + public void setRouteKey(String routeKey) { + this.routeKey = routeKey; + } + + public String getRawPath() { + return rawPath; + } + + public void setRawPath(String rawPath) { + this.rawPath = rawPath; + } + + public String getRawQueryString() { + return rawQueryString; + } + + public void setRawQueryString(String rawQueryString) { + this.rawQueryString = rawQueryString; + } + + public List getCookies() { + return cookies; + } + + public void setCookies(List cookies) { + this.cookies = cookies; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public Map getQueryStringParameters() { + return queryStringParameters; + } + + public void setQueryStringParameters(Map queryStringParameters) { + this.queryStringParameters = queryStringParameters; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + @JsonProperty("isBase64Encoded") + public boolean isBase64Encoded() { + return isBase64Encoded; + } + + public void setBase64Encoded(boolean base64Encoded) { + isBase64Encoded = base64Encoded; + } + + public Map getStageVariables() { + return stageVariables; + } + + public void setStageVariables(Map stageVariables) { + this.stageVariables = stageVariables; + } + + public HttpApiV2ProxyRequestContext getRequestContext() { + return requestContext; + } + + public void setRequestContext(HttpApiV2ProxyRequestContext requestContext) { + this.requestContext = requestContext; + } +} diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestContext.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestContext.java new file mode 100644 index 000000000..55bbe1f8f --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestContext.java @@ -0,0 +1,117 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.model; + +public class HttpApiV2ProxyRequestContext { + private String accountId; + private String apiId; + private String domainName; + private String domainPrefix; + private String requestId; + private String routeKey; + private String stage; + private String time; + private long timeEpoch; + + private HttpApiV2HttpContext http; + private HttpApiV2AuthorizerMap authorizer; + + public String getAccountId() { + return accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public String getApiId() { + return apiId; + } + + public void setApiId(String apiId) { + this.apiId = apiId; + } + + public String getDomainName() { + return domainName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + public String getDomainPrefix() { + return domainPrefix; + } + + public void setDomainPrefix(String domainPrefix) { + this.domainPrefix = domainPrefix; + } + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public String getRouteKey() { + return routeKey; + } + + public void setRouteKey(String routeKey) { + this.routeKey = routeKey; + } + + public String getStage() { + return stage; + } + + public void setStage(String stage) { + this.stage = stage; + } + + public String getTime() { + return time; + } + + public void setTime(String time) { + this.time = time; + } + + public long getTimeEpoch() { + return timeEpoch; + } + + public void setTimeEpoch(long timeEpoch) { + this.timeEpoch = timeEpoch; + } + + public HttpApiV2HttpContext getHttp() { + return http; + } + + public void setHttp(HttpApiV2HttpContext http) { + this.http = http; + } + + public HttpApiV2AuthorizerMap getAuthorizer() { + return authorizer; + } + + public void setAuthorizer(HttpApiV2AuthorizerMap authorizer) { + this.authorizer = authorizer; + } + +} diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/MultiValuedTreeMap.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/MultiValuedTreeMap.java index b45463b9e..feeab6f2b 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/MultiValuedTreeMap.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/MultiValuedTreeMap.java @@ -1,6 +1,17 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.model; - import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.ws.rs.core.MultivaluedMap; @@ -14,7 +25,6 @@ import java.util.Set; import java.util.TreeMap; - /** * Simple implementation of a multi valued tree map to use for case-insensitive headers * diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/AsyncInitializationWrapperTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/AsyncInitializationWrapperTest.java new file mode 100644 index 000000000..3b8a7306c --- /dev/null +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/AsyncInitializationWrapperTest.java @@ -0,0 +1,28 @@ +package com.amazonaws.serverless.proxy; + +import org.junit.Test; + +import java.lang.management.ManagementFactory; +import java.time.Clock; +import java.time.Instant; + +import static org.junit.Assert.assertEquals; + +public class AsyncInitializationWrapperTest { + + @Test + public void initCreate_noStartTime_setsCurrentTime() { + AsyncInitializationWrapper init = new AsyncInitializationWrapper(); + long initTime = ManagementFactory.getRuntimeMXBean().getStartTime(); + assertEquals(initTime, init.getActualStartTimeMs()); + } + + @Test + public void initCreate_withStartTime_storesCustomStartTime() throws InterruptedException { + long initTime = Instant.now().toEpochMilli(); + Thread.sleep(500); + AsyncInitializationWrapper init = new AsyncInitializationWrapper(initTime); + + assertEquals(initTime, init.getActualStartTimeMs()); + } +} diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandlerTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandlerTest.java new file mode 100644 index 000000000..8b10520c3 --- /dev/null +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandlerTest.java @@ -0,0 +1,105 @@ +package com.amazonaws.serverless.proxy.internal; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.*; +import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequestReader; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.services.lambda.runtime.Context; +import org.apache.http.impl.execchain.RequestAbortedException; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import java.util.concurrent.CountDownLatch; + +import static org.junit.Assert.*; + +public class LambdaContainerHandlerTest { + private boolean isRuntimeException = false; + private boolean throwException = false; + + ExceptionContainerHandlerTest handler = new ExceptionContainerHandlerTest( + AwsProxyRequest.class, AwsProxyResponse.class, + new AwsProxyHttpServletRequestReader(), new AwsProxyHttpServletResponseWriter(), + new AwsProxySecurityContextWriter(), new AwsProxyExceptionHandler(), new InitializationWrapper() + ); + + @Test + public void throwRuntime_returnsUnwrappedException() { + try { + isRuntimeException = true; + throwException = true; + LambdaContainerHandler.getContainerConfig().setDisableExceptionMapper(true); + handler.proxy(new AwsProxyRequestBuilder("/test", "GET").build(), new MockLambdaContext()); + } catch (Exception e) { + assertNotNull(e); + assertEquals(ExceptionContainerHandlerTest.RUNTIME_MESSAGE, e.getMessage()); + return; + } + fail("Did not throw runtime exception"); + } + + @Test + public void throwNonRuntime_returnsWrappedException() { + try { + isRuntimeException = false; + throwException = true; + LambdaContainerHandler.getContainerConfig().setDisableExceptionMapper(true); + handler.proxy(new AwsProxyRequestBuilder("/test", "GET").build(), new MockLambdaContext()); + } catch (Exception e) { + assertNotNull(e); + assertNotNull(e.getCause()); + assertTrue(e.getCause() instanceof RequestAbortedException); + assertEquals(ExceptionContainerHandlerTest.NON_RUNTIME_MESSAGE, e.getCause().getMessage()); + return; + } + fail("Did not throw exception"); + } + + @Test + public void noException_returnsResponse() { + throwException = false; + LambdaContainerHandler.getContainerConfig().setDisableExceptionMapper(false); + AwsProxyResponse resp = handler.proxy(new AwsProxyRequestBuilder("/test", "GET").build(), new MockLambdaContext()); + assertEquals(200, resp.getStatusCode()); + assertEquals("OK", resp.getBody()); + } + + public class ExceptionContainerHandlerTest extends LambdaContainerHandler { + + public static final String RUNTIME_MESSAGE = "test RuntimeException"; + public static final String NON_RUNTIME_MESSAGE = "test NonRuntimeException"; + + protected ExceptionContainerHandlerTest(Class requestClass, Class responseClass, RequestReader requestReader, ResponseWriter responseWriter, SecurityContextWriter securityContextWriter, ExceptionHandler exceptionHandler, InitializationWrapper init) { + super(requestClass, responseClass, requestReader, responseWriter, securityContextWriter, exceptionHandler, init); + } + + @Override + protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request, CountDownLatch latch) { + return new AwsHttpServletResponse(request, latch); + } + + @Override + protected void handleRequest(HttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { + if (throwException) { + if (isRuntimeException) { + throw new RuntimeException(RUNTIME_MESSAGE); + } else { + throw new RequestAbortedException(NON_RUNTIME_MESSAGE); + } + } + containerResponse.setStatus(200); + containerResponse.getWriter().print("OK"); + containerResponse.flushBuffer(); + } + + @Override + public void initialize() throws ContainerInitializationException { + + } + } +} diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/jaxrs/HttpApiV2SecurityContextTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/jaxrs/HttpApiV2SecurityContextTest.java new file mode 100644 index 000000000..ae2dcdb53 --- /dev/null +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/jaxrs/HttpApiV2SecurityContextTest.java @@ -0,0 +1,49 @@ +package com.amazonaws.serverless.proxy.internal.jaxrs; + +import com.amazonaws.serverless.proxy.AwsHttpApiV2SecurityContextWriter; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import org.junit.Test; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.SecurityContext; + +import static org.junit.Assert.*; + +public class HttpApiV2SecurityContextTest { + private static final String JWT_SUB_VALUE = "1234567890"; + + HttpApiV2ProxyRequest EMPTY_AUTH = new AwsProxyRequestBuilder("/", "GET").toHttpApiV2Request(); + HttpApiV2ProxyRequest BASIC_AUTH = new AwsProxyRequestBuilder("/", "GET") + .authorizerPrincipal("test").toHttpApiV2Request(); + HttpApiV2ProxyRequest JWT_AUTH = new AwsProxyRequestBuilder("/", "GET") + .authorizerPrincipal("test") + .header(HttpHeaders.AUTHORIZATION, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c") + .toHttpApiV2Request(); + + AwsHttpApiV2SecurityContextWriter contextWriter = new AwsHttpApiV2SecurityContextWriter(); + + @Test + public void getAuthenticationScheme_nullAuth_nullSchema() { + SecurityContext ctx = contextWriter.writeSecurityContext(EMPTY_AUTH, null); + assertNull(ctx.getAuthenticationScheme()); + assertNull(ctx.getUserPrincipal()); + assertFalse(ctx.isSecure()); + } + + @Test + public void getAuthenticationScheme_jwtAuth_correctSchema() { + SecurityContext ctx = contextWriter.writeSecurityContext(BASIC_AUTH, null); + assertEquals(AwsHttpApiV2SecurityContext.AUTH_SCHEME_JWT, ctx.getAuthenticationScheme()); + assertTrue(ctx.isSecure()); + assertNull(ctx.getUserPrincipal()); + } + + @Test + public void getPrincipal_parseJwt_returnsSub() { + SecurityContext ctx = contextWriter.writeSecurityContext(JWT_AUTH, null); + assertEquals(AwsHttpApiV2SecurityContext.AUTH_SCHEME_JWT, ctx.getAuthenticationScheme()); + assertTrue(ctx.isSecure()); + assertEquals(JWT_SUB_VALUE, ctx.getUserPrincipal().getName()); + } +} diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/ApacheCombinedServletLogFormatterTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/ApacheCombinedServletLogFormatterTest.java index 1c9527a32..0d12a5116 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/ApacheCombinedServletLogFormatterTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/ApacheCombinedServletLogFormatterTest.java @@ -14,6 +14,7 @@ import java.time.Instant; import java.time.ZoneId; +import static com.amazonaws.serverless.proxy.RequestReader.API_GATEWAY_CONTEXT_PROPERTY; import static com.amazonaws.serverless.proxy.RequestReader.API_GATEWAY_EVENT_PROPERTY; import static org.hamcrest.CoreMatchers.containsString; import static org.junit.Assert.assertThat; @@ -35,13 +36,13 @@ public void setup() { proxyRequest = new AwsProxyRequest(); Clock fixedClock = Clock.fixed(Instant.ofEpochSecond(665888523L), ZoneId.of("UTC")); mockServletRequest = mock(HttpServletRequest.class); - when(mockServletRequest.getAttribute(eq(API_GATEWAY_EVENT_PROPERTY))) - .thenReturn(proxyRequest); + context = new AwsProxyRequestContext(); + context.setIdentity(new ApiGatewayRequestIdentity()); + when(mockServletRequest.getAttribute(eq(API_GATEWAY_CONTEXT_PROPERTY))) + .thenReturn(context); when(mockServletRequest.getMethod()) .thenReturn("GET"); mockServletResponse = mock(HttpServletResponse.class); - context = new AwsProxyRequestContext(); - context.setIdentity(new ApiGatewayRequestIdentity()); proxyRequest.setRequestContext(context); sut = new ApacheCombinedServletLogFormatter(fixedClock); diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsAsyncContextTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsAsyncContextTest.java index 349a95497..f379ff32a 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsAsyncContextTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsAsyncContextTest.java @@ -1,8 +1,10 @@ package com.amazonaws.serverless.proxy.internal.servlet; import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.exceptions.InvalidRequestEventException; import com.amazonaws.serverless.proxy.AwsProxyExceptionHandler; import com.amazonaws.serverless.proxy.AwsProxySecurityContextWriter; +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; @@ -25,6 +27,7 @@ public class AwsAsyncContextTest { private MockLambdaContext lambdaCtx = new MockLambdaContext(); private MockContainerHandler handler = new MockContainerHandler(); + private AwsProxyHttpServletRequestReader reader = new AwsProxyHttpServletRequestReader(); private AwsServletContextTest.TestServlet srv1 = new AwsServletContextTest.TestServlet("srv1"); private AwsServletContextTest.TestServlet srv2 = new AwsServletContextTest.TestServlet("srv2"); private AwsServletContext ctx = getCtx(); @@ -56,8 +59,8 @@ public void dispatch_sendsToCorrectServlet() { } @Test - public void dispatchNewPath_sendsToCorrectServlet() { - AwsProxyHttpServletRequest req = new AwsProxyHttpServletRequest(new AwsProxyRequestBuilder("/srv1/hello", "GET").build(), lambdaCtx, null); + public void dispatchNewPath_sendsToCorrectServlet() throws InvalidRequestEventException { + AwsProxyHttpServletRequest req = (AwsProxyHttpServletRequest) reader.readRequest(new AwsProxyRequestBuilder("/srv1/hello", "GET").build(), null, lambdaCtx, LambdaContainerHandler.getContainerConfig()); req.setResponse(handler.getContainerResponse(req, new CountDownLatch(1))); req.setServletContext(ctx); req.setContainerHandler(handler); @@ -83,7 +86,7 @@ private AwsServletContext getCtx() { return ctx; } - public static class MockContainerHandler extends AwsLambdaServletContainerHandler { + public static class MockContainerHandler extends AwsLambdaServletContainerHandler { private int desiredStatus; private HttpServletResponse response; private Servlet selectedServlet; @@ -94,7 +97,7 @@ public MockContainerHandler() { } @Override - protected AwsHttpServletResponse getContainerResponse(AwsProxyHttpServletRequest request, CountDownLatch latch) { + protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request, CountDownLatch latch) { return new AwsHttpServletResponse(request, latch); } @@ -115,7 +118,7 @@ protected void doFilter(HttpServletRequest request, HttpServletResponse response } @Override - protected void handleRequest(AwsProxyHttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { + protected void handleRequest(HttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { } diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpApiV2HttpServletRequestReaderTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpApiV2HttpServletRequestReaderTest.java new file mode 100644 index 000000000..004d4d489 --- /dev/null +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpApiV2HttpServletRequestReaderTest.java @@ -0,0 +1,44 @@ +package com.amazonaws.serverless.proxy.internal.servlet; + +import com.amazonaws.serverless.exceptions.InvalidRequestEventException; +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequestContext; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; + +import static org.junit.Assert.*; + +public class AwsHttpApiV2HttpServletRequestReaderTest { + private AwsHttpApiV2HttpServletRequestReader reader = new AwsHttpApiV2HttpServletRequestReader(); + + @Test + public void reflection_getRequestClass_returnsCorrectType() { + assertSame(HttpApiV2ProxyRequest.class, reader.getRequestClass()); + } + + @Test + public void baseRequest_read_populatesSuccessfully() { + HttpApiV2ProxyRequest req = new AwsProxyRequestBuilder("/hello", "GET") + .referer("localhost") + .queryString("param1", "value1") + .header("custom", "value") + .apiId("test").toHttpApiV2Request(); + AwsHttpApiV2HttpServletRequestReader reader = new AwsHttpApiV2HttpServletRequestReader(); + try { + HttpServletRequest servletRequest = reader.readRequest(req, null, null, LambdaContainerHandler.getContainerConfig()); + assertEquals("/hello", servletRequest.getPathInfo()); + assertEquals("value1", servletRequest.getParameter("param1")); + assertEquals("value", servletRequest.getHeader("CUSTOM")); + + assertNotNull(servletRequest.getAttribute(AwsHttpApiV2HttpServletRequestReader.HTTP_API_CONTEXT_PROPERTY)); + assertEquals("test", + ((HttpApiV2ProxyRequestContext)servletRequest.getAttribute(AwsHttpApiV2HttpServletRequestReader.HTTP_API_CONTEXT_PROPERTY)).getApiId()); + } catch (InvalidRequestEventException e) { + e.printStackTrace(); + fail("Could not read request"); + } + } +} diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReaderTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReaderTest.java index de246bc04..166bfb472 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReaderTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReaderTest.java @@ -25,13 +25,6 @@ public class AwsProxyHttpServletRequestReaderTest { private static final String ENCODED_REQUEST_PATH = "/foo/bar/Some%20Thing"; private static final String DECODED_REQUEST_PATH = "/foo/bar/Some Thing"; - @Test - public void readRequest_reflection_returnType() throws NoSuchMethodException { - Method readRequestMethod = AwsProxyHttpServletRequestReader.class.getMethod("readRequest", AwsProxyRequest.class, SecurityContext.class, Context.class, ContainerConfig.class); - - assertTrue(readRequestMethod.getReturnType() == AwsProxyHttpServletRequest.class); - } - @Test public void readRequest_validAwsProxy_populatedRequest() { AwsProxyRequest request = new AwsProxyRequestBuilder("/path", "GET").header(TEST_HEADER_KEY, TEST_HEADER_VALUE).build(); @@ -114,7 +107,7 @@ public void readRequest_contentCharset_appendsCharsetToComplextContentType() { public void readRequest_validEventEmptyPath_expectException() { try { AwsProxyRequest req = new AwsProxyRequestBuilder(null, "GET").build(); - AwsProxyHttpServletRequest servletReq = reader.readRequest(req, null, null, ContainerConfig.defaultConfig()); + HttpServletRequest servletReq = reader.readRequest(req, null, null, ContainerConfig.defaultConfig()); assertNotNull(servletReq); } catch (InvalidRequestEventException e) { e.printStackTrace(); @@ -150,7 +143,7 @@ public void readRequest_nullHeaders_expectSuccess() { AwsProxyRequest req = new AwsProxyRequestBuilder("/path", "GET").build(); req.setMultiValueHeaders(null); try { - AwsProxyHttpServletRequest servletReq = reader.readRequest(req, null, null, ContainerConfig.defaultConfig()); + HttpServletRequest servletReq = reader.readRequest(req, null, null, ContainerConfig.defaultConfig()); String headerValue = servletReq.getHeader(HttpHeaders.CONTENT_TYPE); assertNull(headerValue); } catch (InvalidRequestEventException e) { @@ -163,7 +156,7 @@ public void readRequest_nullHeaders_expectSuccess() { public void readRequest_emptyHeaders_expectSuccess() { AwsProxyRequest req = new AwsProxyRequestBuilder("/path", "GET").build(); try { - AwsProxyHttpServletRequest servletReq = reader.readRequest(req, null, null, ContainerConfig.defaultConfig()); + HttpServletRequest servletReq = reader.readRequest(req, null, null, ContainerConfig.defaultConfig()); String headerValue = servletReq.getHeader(HttpHeaders.CONTENT_TYPE); assertNull(headerValue); } catch (InvalidRequestEventException e) { diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestTest.java index c73e64ab9..732c3f69b 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestTest.java @@ -23,10 +23,11 @@ import java.util.*; import static org.junit.Assert.*; +import static org.junit.Assume.assumeFalse; @RunWith(Parameterized.class) public class AwsProxyHttpServletRequestTest { - private boolean isWrapped; + private String requestType; private static final String CUSTOM_HEADER_KEY = "X-Custom-Header"; private static final String CUSTOM_HEADER_VALUE = "Custom-Header-Value"; @@ -39,58 +40,65 @@ public class AwsProxyHttpServletRequestTest { private static final String REFERER = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox"; private static ZonedDateTime REQUEST_DATE = ZonedDateTime.now(); - private static final AwsProxyRequest REQUEST_FORM_URLENCODED = new AwsProxyRequestBuilder("/hello", "POST") - .form(FORM_PARAM_NAME, FORM_PARAM_NAME_VALUE).build(); - private static final AwsProxyRequest REQUEST_INVALID_FORM_URLENCODED = new AwsProxyRequestBuilder("/hello", "GET") - .form(FORM_PARAM_NAME, FORM_PARAM_NAME_VALUE).build(); - private static final AwsProxyRequest REQUEST_FORM_URLENCODED_AND_QUERY = new AwsProxyRequestBuilder("/hello", "POST") + private static final AwsProxyRequestBuilder REQUEST_FORM_URLENCODED = new AwsProxyRequestBuilder("/hello", "POST") + .form(FORM_PARAM_NAME, FORM_PARAM_NAME_VALUE); + private static final AwsProxyRequestBuilder REQUEST_INVALID_FORM_URLENCODED = new AwsProxyRequestBuilder("/hello", "GET") + .form(FORM_PARAM_NAME, FORM_PARAM_NAME_VALUE); + private static final AwsProxyRequestBuilder REQUEST_FORM_URLENCODED_AND_QUERY = new AwsProxyRequestBuilder("/hello", "POST") .form(FORM_PARAM_NAME, FORM_PARAM_NAME_VALUE) - .queryString(FORM_PARAM_NAME, QUERY_STRING_NAME_VALUE).build(); - private static final AwsProxyRequest REQUEST_SINGLE_COOKIE = new AwsProxyRequestBuilder("/hello", "GET") - .cookie(FORM_PARAM_NAME, FORM_PARAM_NAME_VALUE).build(); - private static final AwsProxyRequest REQUEST_MULTIPLE_COOKIES = new AwsProxyRequestBuilder("/hello", "GET") + .queryString(FORM_PARAM_NAME, QUERY_STRING_NAME_VALUE); + private static final AwsProxyRequestBuilder REQUEST_SINGLE_COOKIE = new AwsProxyRequestBuilder("/hello", "GET") + .cookie(FORM_PARAM_NAME, FORM_PARAM_NAME_VALUE); + private static final AwsProxyRequestBuilder REQUEST_MULTIPLE_COOKIES = new AwsProxyRequestBuilder("/hello", "GET") .cookie(FORM_PARAM_NAME, FORM_PARAM_NAME_VALUE) - .cookie(FORM_PARAM_TEST, FORM_PARAM_NAME_VALUE).build(); - private static final AwsProxyRequest REQUEST_MALFORMED_COOKIE = new AwsProxyRequestBuilder("/hello", "GET") - .header(HttpHeaders.COOKIE, QUERY_STRING_NAME_VALUE).build(); - private static final AwsProxyRequest REQUEST_MULTIPLE_FORM_AND_QUERY = new AwsProxyRequestBuilder("/hello", "POST") + .cookie(FORM_PARAM_TEST, FORM_PARAM_NAME_VALUE); + private static final AwsProxyRequestBuilder REQUEST_MALFORMED_COOKIE = new AwsProxyRequestBuilder("/hello", "GET") + .header(HttpHeaders.COOKIE, QUERY_STRING_NAME_VALUE); + private static final AwsProxyRequestBuilder REQUEST_MULTIPLE_FORM_AND_QUERY = new AwsProxyRequestBuilder("/hello", "POST") .form(FORM_PARAM_NAME, FORM_PARAM_NAME_VALUE) - .queryString(FORM_PARAM_TEST, QUERY_STRING_NAME_VALUE).build(); - private static final AwsProxyRequest REQUEST_USER_AGENT_REFERER = new AwsProxyRequestBuilder("/hello", "POST") + .queryString(FORM_PARAM_TEST, QUERY_STRING_NAME_VALUE); + private static final AwsProxyRequestBuilder REQUEST_USER_AGENT_REFERER = new AwsProxyRequestBuilder("/hello", "POST") .userAgent(USER_AGENT) - .referer(REFERER).build(); - private static final AwsProxyRequest REQUEST_WITH_DATE = new AwsProxyRequestBuilder("/hello", "GET") - .header(HttpHeaders.DATE, AwsHttpServletRequest.dateFormatter.format(REQUEST_DATE)) - .build(); - private static final AwsProxyRequest REQUEST_WITH_LOWERCASE_HEADER = new AwsProxyRequestBuilder("/hello", "POST") - .header(HttpHeaders.CONTENT_TYPE.toLowerCase(Locale.getDefault()), MediaType.APPLICATION_JSON).build(); - - private static final AwsProxyRequest REQUEST_NULL_QUERY_STRING; + .referer(REFERER); + private static final AwsProxyRequestBuilder REQUEST_WITH_DATE = new AwsProxyRequestBuilder("/hello", "GET") + .header(HttpHeaders.DATE, AwsHttpServletRequest.dateFormatter.format(REQUEST_DATE)); + private static final AwsProxyRequestBuilder REQUEST_WITH_LOWERCASE_HEADER = new AwsProxyRequestBuilder("/hello", "POST") + .header(HttpHeaders.CONTENT_TYPE.toLowerCase(Locale.getDefault()), MediaType.APPLICATION_JSON); + + private static final AwsProxyRequestBuilder REQUEST_NULL_QUERY_STRING; static { AwsProxyRequest awsProxyRequest = new AwsProxyRequestBuilder("/hello", "GET").build(); awsProxyRequest.setMultiValueQueryStringParameters(null); - REQUEST_NULL_QUERY_STRING = awsProxyRequest; + REQUEST_NULL_QUERY_STRING = new AwsProxyRequestBuilder(awsProxyRequest); } - private static final AwsProxyRequest REQUEST_QUERY = new AwsProxyRequestBuilder("/hello", "POST") - .queryString(FORM_PARAM_NAME, QUERY_STRING_NAME_VALUE).build(); + private static final AwsProxyRequestBuilder REQUEST_QUERY = new AwsProxyRequestBuilder("/hello", "POST") + .queryString(FORM_PARAM_NAME, QUERY_STRING_NAME_VALUE); - public AwsProxyHttpServletRequestTest(boolean wrap) { - isWrapped = wrap; + public AwsProxyHttpServletRequestTest(String type) { + requestType = type; } @Parameterized.Parameters public static Collection data() { - return Arrays.asList(new Object[] { false, true }); - } - - private HttpServletRequest getRequest(AwsProxyRequest req, Context lambdaCtx, SecurityContext securityCtx) { - HttpServletRequest servletRequest = new AwsProxyHttpServletRequest(req, lambdaCtx, securityCtx); - if (isWrapped) { - servletRequest = new AwsHttpServletRequestWrapper(servletRequest, req.getPath()); + return Arrays.asList(new Object[] { "API_GW", "ALB", "HTTP_API", "WRAP" }); + } + + private HttpServletRequest getRequest(AwsProxyRequestBuilder req, Context lambdaCtx, SecurityContext securityCtx) { + switch (requestType) { + case "API_GW": + return new AwsProxyHttpServletRequest(req.build(), lambdaCtx, securityCtx); + case "ALB": + return new AwsProxyHttpServletRequest(req.alb().build(), lambdaCtx, securityCtx); + case "HTTP_API": + return new AwsHttpApiV2ProxyHttpServletRequest(req.toHttpApiV2Request(), lambdaCtx, securityCtx, LambdaContainerHandler.getContainerConfig()); + case "WRAP": + HttpServletRequest servletRequest = new AwsProxyHttpServletRequest(req.build(), lambdaCtx, securityCtx); + return new AwsHttpServletRequestWrapper(servletRequest, req.build().getPath()); + default: + throw new RuntimeException("Unknown test variant: " + requestType); } - return servletRequest; } @@ -104,6 +112,7 @@ public void headers_getHeader_validRequest() { @Test public void headers_getRefererAndUserAgent_returnsContextValues() { + assumeFalse("ALB".equals(requestType)); HttpServletRequest request = getRequest(REQUEST_USER_AGENT_REFERER, null, null); assertNotNull(request.getHeader("Referer")); assertEquals(REFERER, request.getHeader("Referer")); @@ -344,7 +353,7 @@ public void contentType_lowerCaseHeaderKey_expectUpdatedMediaType() { @Test public void contentType_duplicateCase_expectSingleContentTypeHeader() { - AwsProxyRequest proxyRequest = getRequestWithHeaders(); + AwsProxyRequestBuilder proxyRequest = getRequestWithHeaders(); HttpServletRequest request = getRequest(proxyRequest, null, null); try { @@ -359,8 +368,9 @@ public void contentType_duplicateCase_expectSingleContentTypeHeader() { @Test public void requestURL_getUrl_expectHttpSchemaAndLocalhostForLocalTesting() { - AwsProxyRequest req = getRequestWithHeaders(); - req.getRequestContext().setApiId("test-id"); + assumeFalse("ALB".equals(requestType)); + AwsProxyRequestBuilder req = getRequestWithHeaders(); + req.apiId("test-id"); LambdaContainerHandler.getContainerConfig().enableLocalhost(); HttpServletRequest servletRequest = getRequest(req, null, null); String requestUrl = servletRequest.getRequestURL().toString(); @@ -369,7 +379,7 @@ public void requestURL_getUrl_expectHttpSchemaAndLocalhostForLocalTesting() { assertTrue(requestUrl.endsWith(".com/hello")); // set localhost - req.getMultiValueHeaders().putSingle("Host", "localhost"); + req.header("Host", "localhost"); servletRequest = getRequest(req, null, null); requestUrl = servletRequest.getRequestURL().toString(); assertTrue(requestUrl.contains("http://localhost")); @@ -379,7 +389,7 @@ public void requestURL_getUrl_expectHttpSchemaAndLocalhostForLocalTesting() { @Test public void requestURL_getUrlWithCustomBasePath_expectCustomBasePath() { - AwsProxyRequest req = getRequestWithHeaders(); + AwsProxyRequestBuilder req = getRequestWithHeaders(); LambdaContainerHandler.getContainerConfig().setServiceBasePath("test"); HttpServletRequest servletRequest = getRequest(req, null, null); String requestUrl = servletRequest.getRequestURL().toString(); @@ -389,18 +399,20 @@ public void requestURL_getUrlWithCustomBasePath_expectCustomBasePath() { @Test public void requestURL_getUrlWithContextPath_expectStageAsContextPath() { - AwsProxyRequest req = getRequestWithHeaders(); - req.getRequestContext().setStage("test-stage"); + assumeFalse("ALB".equals(requestType)); + AwsProxyRequestBuilder req = getRequestWithHeaders(); + req.stage("test-stage"); LambdaContainerHandler.getContainerConfig().setUseStageAsServletContext(true); HttpServletRequest servletRequest = getRequest(req, null, null); String requestUrl = servletRequest.getRequestURL().toString(); + System.out.println(requestUrl); assertTrue(requestUrl.contains("/test-stage/")); LambdaContainerHandler.getContainerConfig().setUseStageAsServletContext(false); } @Test public void getLocales_emptyAcceptHeader_expectDefaultLocale() { - AwsProxyRequest req = getRequestWithHeaders(); + AwsProxyRequestBuilder req = getRequestWithHeaders(); HttpServletRequest servletRequest = getRequest(req, null, null); Enumeration locales = servletRequest.getLocales(); int localesNo = 0; @@ -414,8 +426,8 @@ public void getLocales_emptyAcceptHeader_expectDefaultLocale() { @Test public void getLocales_validAcceptHeader_expectSingleLocale() { - AwsProxyRequest req = getRequestWithHeaders(); - req.getMultiValueHeaders().putSingle(HttpHeaders.ACCEPT_LANGUAGE, "fr-CH"); + AwsProxyRequestBuilder req = getRequestWithHeaders(); + req.header(HttpHeaders.ACCEPT_LANGUAGE, "fr-CH"); HttpServletRequest servletRequest = getRequest(req, null, null); Enumeration locales = servletRequest.getLocales(); int localesNo = 0; @@ -429,8 +441,8 @@ public void getLocales_validAcceptHeader_expectSingleLocale() { @Test public void getLocales_validAcceptHeaderMultipleLocales_expectFullLocaleList() { - AwsProxyRequest req = getRequestWithHeaders(); - req.getMultiValueHeaders().putSingle(HttpHeaders.ACCEPT_LANGUAGE, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5"); + AwsProxyRequestBuilder req = getRequestWithHeaders(); + req.header(HttpHeaders.ACCEPT_LANGUAGE, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5"); HttpServletRequest servletRequest = getRequest(req, null, null); Enumeration locales = servletRequest.getLocales(); List localesList = new ArrayList<>(); @@ -450,8 +462,8 @@ public void getLocales_validAcceptHeaderMultipleLocales_expectFullLocaleList() { @Test public void getLocales_validAcceptHeaderMultipleLocales_expectFullLocaleListOrdered() { - AwsProxyRequest req = getRequestWithHeaders(); - req.getMultiValueHeaders().putSingle(HttpHeaders.ACCEPT_LANGUAGE, "fr-CH, en;q=0.8, de;q=0.7, *;q=0.5, fr;q=0.9"); + AwsProxyRequestBuilder req = getRequestWithHeaders(); + req.header(HttpHeaders.ACCEPT_LANGUAGE, "fr-CH, en;q=0.8, de;q=0.7, *;q=0.5, fr;q=0.9"); HttpServletRequest servletRequest = getRequest(req, null, null); Enumeration locales = servletRequest.getLocales(); List localesList = new ArrayList<>(); @@ -468,7 +480,7 @@ public void getLocales_validAcceptHeaderMultipleLocales_expectFullLocaleListOrde @Test public void nullQueryString_expectNoExceptions() { - AwsProxyRequest req = new AwsProxyRequestBuilder("/hello", "GET").build(); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/hello", "GET"); HttpServletRequest servletReq = getRequest(req, null, null); assertNull(servletReq.getQueryString()); assertEquals(0, servletReq.getParameterMap().size()); @@ -479,13 +491,13 @@ public void nullQueryString_expectNoExceptions() { @Test public void inputStream_emptyBody_expectNullInputStream() { - AwsProxyRequest proxyReq = getRequestWithHeaders(); - assertNull(proxyReq.getBody()); + AwsProxyRequestBuilder proxyReq = getRequestWithHeaders(); + assertNull(proxyReq.build().getBody()); HttpServletRequest req = getRequest(proxyReq, null, null); try { InputStream is = req.getInputStream(); - assertTrue(is.getClass() == AwsProxyHttpServletRequest.AwsServletInputStream.class); + assertTrue(is.getClass() == AwsServletInputStream.class); assertEquals(0, is.available()); } catch (IOException e) { fail("Could not get input stream"); @@ -494,7 +506,7 @@ public void inputStream_emptyBody_expectNullInputStream() { @Test public void getHeaders_emptyHeaders_expectEmptyEnumeration() { - AwsProxyRequest proxyReq = new AwsProxyRequestBuilder("/hello", "GET").build(); + AwsProxyRequestBuilder proxyReq = new AwsProxyRequestBuilder("/hello", "GET"); HttpServletRequest req = getRequest(proxyReq, null, null); assertFalse(req.getHeaders("param").hasMoreElements()); } @@ -507,26 +519,43 @@ public void getServerPort_defaultPort_expect443() { @Test public void getServerPort_customPortFromHeader_expectCustomPort() { - AwsProxyRequest proxyReq = getRequestWithHeaders(); - proxyReq.getMultiValueHeaders().putSingle(AwsProxyHttpServletRequest.PORT_HEADER_NAME, "80"); + AwsProxyRequestBuilder proxyReq = getRequestWithHeaders(); + proxyReq.header(AwsProxyHttpServletRequest.PORT_HEADER_NAME, "80"); HttpServletRequest req = getRequest(proxyReq, null, null); assertEquals(80, req.getServerPort()); } @Test public void getServerPort_invalidCustomPortFromHeader_expectDefaultPort() { - AwsProxyRequest proxyReq = getRequestWithHeaders(); - proxyReq.getMultiValueHeaders().putSingle(AwsProxyHttpServletRequest.PORT_HEADER_NAME, "7200"); + AwsProxyRequestBuilder proxyReq = getRequestWithHeaders(); + proxyReq.header(AwsProxyHttpServletRequest.PORT_HEADER_NAME, "7200"); HttpServletRequest req = getRequest(proxyReq, null, null); assertEquals(443, req.getServerPort()); } + @Test + public void serverName_emptyHeaders_doesNotThrowNullPointer() { + AwsProxyRequestBuilder proxyReq = new AwsProxyRequestBuilder("/test", "GET"); + proxyReq.multiValueHeaders(null); + HttpServletRequest servletReq = getRequest(proxyReq, null, null); + String serverName = servletReq.getServerName(); + assertTrue(serverName.startsWith("null.execute-api")); + } + + @Test + public void serverName_hostHeader_returnsHostHeaderOnly() { + AwsProxyRequestBuilder proxyReq = new AwsProxyRequestBuilder("/test", "GET") + .header(HttpHeaders.HOST, "testapi.com"); + LambdaContainerHandler.getContainerConfig().addCustomDomain("testapi.com"); + HttpServletRequest servletReq = getRequest(proxyReq, null, null); + String serverName = servletReq.getServerName(); + assertEquals("testapi.com", serverName); + } - private AwsProxyRequest getRequestWithHeaders() { + private AwsProxyRequestBuilder getRequestWithHeaders() { return new AwsProxyRequestBuilder("/hello", "GET") .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) - .header(AwsProxyHttpServletRequest.CF_PROTOCOL_HEADER_NAME, REQUEST_SCHEME_HTTP) - .build(); + .header(AwsProxyHttpServletRequest.CF_PROTOCOL_HEADER_NAME, REQUEST_SCHEME_HTTP); } } diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyRequestDispatcherTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyRequestDispatcherTest.java index 898f251bf..d061a1115 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyRequestDispatcherTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyRequestDispatcherTest.java @@ -33,7 +33,7 @@ public class AwsProxyRequestDispatcherTest { @Test public void setPath_forwardByPath_proxyRequestObjectInPropertyReferencesSameProxyRequest() throws InvalidRequestEventException { AwsProxyRequest proxyRequest = new AwsProxyRequestBuilder("/hello", "GET").build(); - AwsProxyHttpServletRequest servletRequest = requestReader.readRequest(proxyRequest,null, new MockLambdaContext(), ContainerConfig.defaultConfig()); + HttpServletRequest servletRequest = requestReader.readRequest(proxyRequest,null, new MockLambdaContext(), ContainerConfig.defaultConfig()); AwsProxyRequestDispatcher dispatcher = new AwsProxyRequestDispatcher(FORWARD_PATH, false, null); dispatcher.setRequestPath(servletRequest, FORWARD_PATH); @@ -43,7 +43,7 @@ public void setPath_forwardByPath_proxyRequestObjectInPropertyReferencesSameProx @Test public void setPathForWrappedRequest_forwardByPath_proxyRequestObjectInPropertyReferencesSameProxyRequest() throws InvalidRequestEventException { AwsProxyRequest proxyRequest = new AwsProxyRequestBuilder("/hello", "GET").build(); - AwsProxyHttpServletRequest servletRequest = requestReader.readRequest(proxyRequest,null, new MockLambdaContext(), ContainerConfig.defaultConfig()); + HttpServletRequest servletRequest = requestReader.readRequest(proxyRequest,null, new MockLambdaContext(), ContainerConfig.defaultConfig()); SecurityContextHolderAwareRequestWrapper springSecurityRequest = new SecurityContextHolderAwareRequestWrapper(servletRequest, "ADMIN"); AwsProxyRequestDispatcher dispatcher = new AwsProxyRequestDispatcher(FORWARD_PATH, false, null); @@ -70,7 +70,7 @@ public void setPathForWrappedRequestWithoutGatewayEvent_forwardByPath_throwsExce @Test public void forwardRequest_nullHandler_throwsIllegalStateException() throws InvalidRequestEventException { AwsProxyRequest proxyRequest = new AwsProxyRequestBuilder("/hello", "GET").build(); - AwsProxyHttpServletRequest servletRequest = requestReader.readRequest(proxyRequest,null, new MockLambdaContext(), ContainerConfig.defaultConfig()); + HttpServletRequest servletRequest = requestReader.readRequest(proxyRequest,null, new MockLambdaContext(), ContainerConfig.defaultConfig()); AwsProxyRequestDispatcher dispatcher = new AwsProxyRequestDispatcher(FORWARD_PATH, false, null); try { dispatcher.forward(servletRequest, new AwsHttpServletResponse(servletRequest, new CountDownLatch(1))); @@ -88,7 +88,7 @@ public void forwardRequest_nullHandler_throwsIllegalStateException() throws Inva @Test public void forwardRequest_committedResponse_throwsIllegalStateException() throws InvalidRequestEventException { AwsProxyRequest proxyRequest = new AwsProxyRequestBuilder("/hello", "GET").build(); - AwsProxyHttpServletRequest servletRequest = requestReader.readRequest(proxyRequest,null, new MockLambdaContext(), ContainerConfig.defaultConfig()); + HttpServletRequest servletRequest = requestReader.readRequest(proxyRequest,null, new MockLambdaContext(), ContainerConfig.defaultConfig()); AwsProxyRequestDispatcher dispatcher = new AwsProxyRequestDispatcher(FORWARD_PATH, false, mockLambdaHandler(null)); AwsHttpServletResponse resp = new AwsHttpServletResponse(servletRequest, new CountDownLatch(1)); @@ -109,7 +109,7 @@ public void forwardRequest_committedResponse_throwsIllegalStateException() throw @Test public void forwardRequest_partiallyWrittenResponse_resetsBuffer() throws InvalidRequestEventException { AwsProxyRequest proxyRequest = new AwsProxyRequestBuilder("/hello", "GET").build(); - AwsProxyHttpServletRequest servletRequest = requestReader.readRequest(proxyRequest,null, new MockLambdaContext(), ContainerConfig.defaultConfig()); + HttpServletRequest servletRequest = requestReader.readRequest(proxyRequest,null, new MockLambdaContext(), ContainerConfig.defaultConfig()); AwsProxyRequestDispatcher dispatcher = new AwsProxyRequestDispatcher(FORWARD_PATH, false, mockLambdaHandler(null)); AwsHttpServletResponse resp = new AwsHttpServletResponse(servletRequest, new CountDownLatch(1)); @@ -174,8 +174,8 @@ private interface RequestHandler { } - private AwsLambdaServletContainerHandler mockLambdaHandler(RequestHandler h) { - return new AwsLambdaServletContainerHandler( + private AwsLambdaServletContainerHandler mockLambdaHandler(RequestHandler h) { + return new AwsLambdaServletContainerHandler( AwsProxyRequest.class, AwsProxyResponse.class, new AwsProxyHttpServletRequestReader(), @@ -192,18 +192,18 @@ protected void doFilter(HttpServletRequest request, HttpServletResponse response } @Override - protected AwsHttpServletResponse getContainerResponse(AwsProxyHttpServletRequest request, CountDownLatch latch) { + protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request, CountDownLatch latch) { return new AwsHttpServletResponse(request, latch); } @Override - protected void handleRequest(AwsProxyHttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { + protected void handleRequest(HttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { if (h != null) { setServletContext(new AwsServletContext(this)); - containerRequest.setServletContext(getServletContext()); + ((AwsHttpServletRequest)containerRequest).setServletContext(getServletContext()); - h.handleRequest(containerRequest, containerResponse); + h.handleRequest((AwsProxyHttpServletRequest)containerRequest, containerResponse); } containerResponse.flushBuffer(); } diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/ServletLambdaContainerHandlerBuilderTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/ServletLambdaContainerHandlerBuilderTest.java index a9fef9b19..1481afc7e 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/ServletLambdaContainerHandlerBuilderTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/ServletLambdaContainerHandlerBuilderTest.java @@ -8,6 +8,7 @@ import com.amazonaws.services.lambda.runtime.Context; import org.junit.Test; +import javax.servlet.http.HttpServletRequest; import java.util.concurrent.CountDownLatch; import static org.junit.Assert.*; @@ -44,18 +45,18 @@ public void defaultProxy_setsValuesCorrectly() { assertEquals("test", test.name); } - public static final class TestHandler extends AwsLambdaServletContainerHandler { + public static final class TestHandler extends AwsLambdaServletContainerHandler { public TestHandler() { super(AwsProxyRequest.class, AwsProxyResponse.class, new AwsProxyHttpServletRequestReader(), new AwsProxyHttpServletResponseWriter(), new AwsProxySecurityContextWriter(), new AwsProxyExceptionHandler()); } @Override - protected AwsHttpServletResponse getContainerResponse(AwsProxyHttpServletRequest request, CountDownLatch latch) { + protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request, CountDownLatch latch) { return null; } @Override - protected void handleRequest(AwsProxyHttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { + protected void handleRequest(HttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { } @@ -69,7 +70,7 @@ public static final class TestBuilder extends ServletLambdaContainerHandlerBuilder< AwsProxyRequest, AwsProxyResponse, - AwsProxyHttpServletRequest, + HttpServletRequest, TestHandler, TestBuilder> { diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestTest.java new file mode 100644 index 000000000..420843300 --- /dev/null +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestTest.java @@ -0,0 +1,146 @@ +package com.amazonaws.serverless.proxy.model; + +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.Test; + +import java.util.ArrayList; + +import static org.junit.Assert.*; + +public class HttpApiV2ProxyRequestTest { + + private static final String BASE_PROXY_REQUEST = "{\n" + + " \"version\": \"2.0\",\n" + + " \"routeKey\": \"$default\",\n" + + " \"rawPath\": \"/my/path\",\n" + + " \"rawQueryString\": \"parameter1=value1¶meter1=value2¶meter2=value\",\n" + + " \"cookies\": [ \"cookie1\", \"cookie2\" ],\n" + + " \"headers\": {\n" + + " \"Header1\": \"value1\",\n" + + " \"Header2\": \"value2\"\n" + + " },\n" + + " \"queryStringParameters\": { \"parameter1\": \"value1,value2\", \"parameter2\": \"value\" },\n" + + " \"requestContext\": {\n" + + " \"accountId\": \"123456789012\",\n" + + " \"apiId\": \"api-id\",\n" + + " \"authorizer\": { \"jwt\": {\n" + + " \"claims\": {\"claim1\": \"value1\", \"claim2\": \"value2\"},\n" + + " \"scopes\": [\"scope1\", \"scope2\"]\n" + + " }\n" + + " },\n" + + " \"domainName\": \"id.execute-api.us-east-1.amazonaws.com\",\n" + + " \"domainPrefix\": \"id\",\n" + + " \"http\": {\n" + + " \"method\": \"POST\",\n" + + " \"path\": \"/my/path\",\n" + + " \"protocol\": \"HTTP/1.1\",\n" + + " \"sourceIp\": \"IP\",\n" + + " \"userAgent\": \"agent\"\n" + + " },\n" + + " \"requestId\": \"id\",\n" + + " \"routeKey\": \"$default\",\n" + + " \"stage\": \"$default\",\n" + + " \"time\": \"12/Mar/2020:19:03:58 +0000\",\n" + + " \"timeEpoch\": 1583348638390\n" + + " },\n" + + " \"body\": \"Hello from Lambda\",\n" + + " \"isBase64Encoded\": false,\n" + + " \"stageVariables\": {\"stageVariable1\": \"value1\", \"stageVariable2\": \"value2\"}\n" + + " }\n"; + private static final String NO_AUTH_PROXY = "{\n" + + " \"version\": \"2.0\",\n" + + " \"routeKey\": \"$default\",\n" + + " \"rawPath\": \"/my/path\",\n" + + " \"rawQueryString\": \"parameter1=value1¶meter1=value2¶meter2=value\",\n" + + " \"cookies\": [ \"cookie1\", \"cookie2\" ],\n" + + " \"headers\": {\n" + + " \"Header1\": \"value1\",\n" + + " \"Header2\": \"value2\"\n" + + " },\n" + + " \"queryStringParameters\": { \"parameter1\": \"value1,value2\", \"parameter2\": \"value\" },\n" + + " \"requestContext\": {\n" + + " \"accountId\": \"123456789012\",\n" + + " \"apiId\": \"api-id\",\n" + + " \"authorizer\": {\n " + + " },\n" + + " \"domainName\": \"id.execute-api.us-east-1.amazonaws.com\",\n" + + " \"domainPrefix\": \"id\",\n" + + " \"http\": {\n" + + " \"method\": \"POST\",\n" + + " \"path\": \"/my/path\",\n" + + " \"protocol\": \"HTTP/1.1\",\n" + + " \"sourceIp\": \"IP\",\n" + + " \"userAgent\": \"agent\"\n" + + " },\n" + + " \"requestId\": \"id\",\n" + + " \"routeKey\": \"$default\",\n" + + " \"stage\": \"$default\",\n" + + " \"time\": \"12/Mar/2020:19:03:58 +0000\",\n" + + " \"timeEpoch\": 1583348638390\n" + + " },\n" + + " \"body\": \"Hello from Lambda\",\n" + + " \"isBase64Encoded\": true,\n" + + " \"stageVariables\": {\"stageVariable1\": \"value1\", \"stageVariable2\": \"value2\"}\n" + + " }\n"; + + @Test + public void deserialize_fromJsonString_authorizerPopulatedCorrectly() { + try { + HttpApiV2ProxyRequest req = LambdaContainerHandler.getObjectMapper().readValue(BASE_PROXY_REQUEST, HttpApiV2ProxyRequest.class); + assertTrue(req.getRequestContext().getAuthorizer().getJwtAuthorizer().getClaims().containsKey("claim1")); + assertEquals(2, req.getRequestContext().getAuthorizer().getJwtAuthorizer().getScopes().size()); + } catch (JsonProcessingException e) { + e.printStackTrace(); + fail("Exception while parsing request" + e.getMessage()); + } + } + + @Test + public void deserialize_fromJsonString_authorizerEmptyMap() { + try { + HttpApiV2ProxyRequest req = LambdaContainerHandler.getObjectMapper().readValue(NO_AUTH_PROXY, HttpApiV2ProxyRequest.class); + assertNotNull(req.getRequestContext().getAuthorizer()); + assertFalse(req.getRequestContext().getAuthorizer().isJwt()); + } catch (JsonProcessingException e) { + e.printStackTrace(); + fail("Exception while parsing request" + e.getMessage()); + } + } + + @Test + public void deserialize_fromJsonString_isBase64EncodedPopulates() { + try { + HttpApiV2ProxyRequest req = LambdaContainerHandler.getObjectMapper().readValue(BASE_PROXY_REQUEST, HttpApiV2ProxyRequest.class); + assertFalse(req.isBase64Encoded()); + req = LambdaContainerHandler.getObjectMapper().readValue(NO_AUTH_PROXY, HttpApiV2ProxyRequest.class); + assertTrue(req.isBase64Encoded()); + } catch (JsonProcessingException e) { + e.printStackTrace(); + fail("Exception while parsing request" + e.getMessage()); + } + } + + @Test + public void serialize_toJsonString_authorizerPopulatesCorrectly() { + HttpApiV2ProxyRequest req = new HttpApiV2ProxyRequest(); + req.setBase64Encoded(false); + req.setRequestContext(new HttpApiV2ProxyRequestContext()); + req.getRequestContext().setAuthorizer(new HttpApiV2AuthorizerMap()); + req.getRequestContext().getAuthorizer().putJwtAuthorizer(new HttpApiV2JwtAuthorizer()); + ArrayList scopes = new ArrayList<>(); + scopes.add("first"); + scopes.add("second"); + req.getRequestContext().getAuthorizer().getJwtAuthorizer().setScopes(scopes); + + try { + String reqString = LambdaContainerHandler.getObjectMapper().writeValueAsString(req); + assertTrue(reqString.contains("\"scopes\":[\"first\",\"second\"]")); + assertTrue(reqString.contains("\"authorizer\":{\"jwt\":{")); + assertTrue(reqString.contains("\"isBase64Encoded\":false")); + } catch (JsonProcessingException e) { + e.printStackTrace(); + fail("Exception while serializing request" + e.getMessage()); + } + } +} diff --git a/aws-serverless-java-container-jersey/pom.xml b/aws-serverless-java-container-jersey/pom.xml index 28edd3c9f..b1c9b0318 100644 --- a/aws-serverless-java-container-jersey/pom.xml +++ b/aws-serverless-java-container-jersey/pom.xml @@ -16,7 +16,7 @@ - 2.29.1 + 2.30.1 diff --git a/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyHandlerFilter.java b/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyHandlerFilter.java index cd6aa3e93..27e3157f0 100644 --- a/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyHandlerFilter.java +++ b/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyHandlerFilter.java @@ -1,6 +1,17 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.jersey; - import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.internal.testutils.Timer; import com.amazonaws.serverless.proxy.jersey.suppliers.AwsProxyServletRequestSupplier; @@ -40,7 +51,6 @@ import static com.amazonaws.serverless.proxy.RequestReader.JAX_SECURITY_CONTEXT_PROPERTY; import static com.amazonaws.serverless.proxy.RequestReader.LAMBDA_CONTEXT_PROPERTY; - /** * Servlet filter class that calls Jersey's ApplicationHandler. Given a Jax RS Application object, this class * initializes a Jersey {@link ApplicationHandler} and calls its handle method. Requests are transformed diff --git a/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyLambdaContainerHandler.java b/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyLambdaContainerHandler.java index 83c01a334..bb9d95091 100644 --- a/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyLambdaContainerHandler.java +++ b/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyLambdaContainerHandler.java @@ -13,17 +13,8 @@ package com.amazonaws.serverless.proxy.jersey; -import com.amazonaws.serverless.proxy.AwsProxyExceptionHandler; -import com.amazonaws.serverless.proxy.AwsProxySecurityContextWriter; -import com.amazonaws.serverless.proxy.ExceptionHandler; -import com.amazonaws.serverless.proxy.RequestReader; -import com.amazonaws.serverless.proxy.ResponseWriter; -import com.amazonaws.serverless.proxy.SecurityContextWriter; -import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse; -import com.amazonaws.serverless.proxy.internal.servlet.AwsLambdaServletContainerHandler; -import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; -import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequestReader; -import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.serverless.proxy.*; +import com.amazonaws.serverless.proxy.internal.servlet.*; import com.amazonaws.serverless.proxy.internal.testutils.Timer; import com.amazonaws.serverless.proxy.jersey.suppliers.AwsProxyServletContextSupplier; import com.amazonaws.serverless.proxy.jersey.suppliers.AwsProxyServletRequestSupplier; @@ -31,6 +22,7 @@ import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.services.lambda.runtime.Context; import org.glassfish.jersey.internal.inject.AbstractBinder; @@ -42,6 +34,7 @@ import javax.servlet.FilterRegistration; import javax.servlet.Servlet; import javax.servlet.ServletContext; +import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.Application; @@ -74,7 +67,7 @@ * @param The type for the incoming Lambda event * @param The type for Lambda's return value */ -public class JerseyLambdaContainerHandler extends AwsLambdaServletContainerHandler { +public class JerseyLambdaContainerHandler extends AwsLambdaServletContainerHandler { //------------------------------------------------------------- // Variables - Private @@ -97,13 +90,36 @@ public class JerseyLambdaContainerHandler extends Aws * @return A JerseyLambdaContainerHandler object */ public static JerseyLambdaContainerHandler getAwsProxyHandler(Application jaxRsApplication) { - JerseyLambdaContainerHandler newHandler = new JerseyLambdaContainerHandler<>(AwsProxyRequest.class, - AwsProxyResponse.class, - new AwsProxyHttpServletRequestReader(), - new AwsProxyHttpServletResponseWriter(), - new AwsProxySecurityContextWriter(), - new AwsProxyExceptionHandler(), - jaxRsApplication); + JerseyLambdaContainerHandler newHandler = new JerseyLambdaContainerHandler<>( + AwsProxyRequest.class, + AwsProxyResponse.class, + new AwsProxyHttpServletRequestReader(), + new AwsProxyHttpServletResponseWriter(), + new AwsProxySecurityContextWriter(), + new AwsProxyExceptionHandler(), + jaxRsApplication); + newHandler.initialize(); + return newHandler; + } + + /** + * Returns an initialized JerseyLambdaContainerHandler that includes RequestReader and + * ResponseWriter objects for the HttpApiV2ProxyRequest and AwsProxyResponse + * objects. + * + * @param jaxRsApplication A configured Jax-Rs application object. For Jersey apps this can be the default + * ResourceConfig object + * @return A JerseyLambdaContainerHandler object + */ + public static JerseyLambdaContainerHandler getHttpApiV2ProxyHandler(Application jaxRsApplication) { + JerseyLambdaContainerHandler newHandler = new JerseyLambdaContainerHandler<>( + HttpApiV2ProxyRequest.class, + AwsProxyResponse.class, + new AwsHttpApiV2HttpServletRequestReader(), + new AwsProxyHttpServletResponseWriter(), + new AwsHttpApiV2SecurityContextWriter(), + new AwsProxyExceptionHandler(), + jaxRsApplication); newHandler.initialize(); return newHandler; } @@ -126,7 +142,7 @@ public static JerseyLambdaContainerHandler ge */ public JerseyLambdaContainerHandler(Class requestTypeClass, Class responseTypeClass, - RequestReader requestReader, + RequestReader requestReader, ResponseWriter responseWriter, SecurityContextWriter securityContextWriter, ExceptionHandler exceptionHandler, @@ -167,12 +183,7 @@ protected void configure() { //------------------------------------------------------------- @Override - protected AwsHttpServletResponse getContainerResponse(AwsProxyHttpServletRequest request, CountDownLatch latch) { - return new AwsHttpServletResponse(request, latch); - } - - @Override - protected void handleRequest(AwsProxyHttpServletRequest httpServletRequest, AwsHttpServletResponse httpServletResponse, Context lambdaContext) + protected void handleRequest(HttpServletRequest httpServletRequest, AwsHttpServletResponse httpServletResponse, Context lambdaContext) throws Exception { // we retain the initialized property for backward compatibility if (!initialized) { @@ -180,12 +191,18 @@ protected void handleRequest(AwsProxyHttpServletRequest httpServletRequest, AwsH } Timer.start("JERSEY_HANDLE_REQUEST"); - httpServletRequest.setServletContext(getServletContext()); + if (AwsHttpServletRequest.class.isAssignableFrom(httpServletRequest.getClass())) { + ((AwsHttpServletRequest)httpServletRequest).setServletContext(getServletContext()); + } doFilter(httpServletRequest, httpServletResponse, null); Timer.stop("JERSEY_HANDLE_REQUEST"); } + @Override + protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request, CountDownLatch latch) { + return new AwsHttpServletResponse(request, latch); + } @Override public void initialize() { diff --git a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java index be89c3a06..dbcaac407 100644 --- a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java +++ b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java @@ -22,6 +22,7 @@ import com.amazonaws.serverless.proxy.jersey.providers.ServletRequestFilter; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.services.lambda.runtime.Context; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -45,6 +46,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; /** * Unit test class for the Jersey AWS_PROXY default implementation @@ -66,6 +68,13 @@ public class JerseyAwsProxyTest { .register(new ResourceBinder()) .property(LoggingFeature.LOGGING_FEATURE_VERBOSITY_SERVER, LoggingFeature.Verbosity.PAYLOAD_ANY); + private static ResourceConfig httpApiApp = new ResourceConfig().packages("com.amazonaws.serverless.proxy.jersey") + .register(LoggingFeature.class) + .register(ServletRequestFilter.class) + .register(MultiPartFeature.class) + .register(new ResourceBinder()) + .property(LoggingFeature.LOGGING_FEATURE_VERBOSITY_SERVER, LoggingFeature.Verbosity.PAYLOAD_ANY); + private static ResourceConfig appWithoutRegisteredDependencies = new ResourceConfig() .packages("com.amazonaws.serverless.proxy.jersey") .register(LoggingFeature.class) @@ -73,55 +82,71 @@ public class JerseyAwsProxyTest { .register(MultiPartFeature.class) .property(LoggingFeature.LOGGING_FEATURE_VERBOSITY_SERVER, LoggingFeature.Verbosity.PAYLOAD_ANY); - private static JerseyLambdaContainerHandler handler = JerseyLambdaContainerHandler.getAwsProxyHandler(app); + private static JerseyLambdaContainerHandler handler; + private static JerseyLambdaContainerHandler httpApiHandler; private static JerseyLambdaContainerHandler handlerWithoutRegisteredDependencies = JerseyLambdaContainerHandler.getAwsProxyHandler(appWithoutRegisteredDependencies); private static Context lambdaContext = new MockLambdaContext(); - private boolean isAlb; + private String type; - public JerseyAwsProxyTest(boolean alb) { - isAlb = alb; + public JerseyAwsProxyTest(String reqType) { + type = reqType; } @Parameterized.Parameters public static Collection data() { - return Arrays.asList(new Object[] { false, true }); + return Arrays.asList(new Object[] { "API_GW", "ALB", "HTTP_API" }); } private AwsProxyRequestBuilder getRequestBuilder(String path, String method) { - AwsProxyRequestBuilder builder = new AwsProxyRequestBuilder(path, method); - if (isAlb) builder.alb(); - return builder; + return new AwsProxyRequestBuilder(path, method); } - @Test - public void alb_basicRequest_expectSuccess() { - AwsProxyRequest request = getRequestBuilder("/echo/headers", "GET") - .json() - .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) - .alb() - .build(); - - AwsProxyResponse output = handler.proxy(request, lambdaContext); - assertEquals(200, output.getStatusCode()); - assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type")); - assertNotNull(output.getStatusDescription()); + private AwsProxyResponse executeRequest(AwsProxyRequestBuilder requestBuilder, Context lambdaContext) { + switch (type) { + case "API_GW": + if (handler == null) { + handler = JerseyLambdaContainerHandler.getAwsProxyHandler(app); + } + return handler.proxy(requestBuilder.build(), lambdaContext); + case "ALB": + if (handler == null) { + handler = JerseyLambdaContainerHandler.getAwsProxyHandler(app); + } + return handler.proxy(requestBuilder.alb().build(), lambdaContext); + case "HTTP_API": + if (httpApiHandler == null) { + httpApiHandler = JerseyLambdaContainerHandler.getHttpApiV2ProxyHandler(httpApiApp); + } + return httpApiHandler.proxy(requestBuilder.toHttpApiV2Request(), lambdaContext); + default: + throw new RuntimeException("Unknown request type: " + type); + } + } - validateMapResponseModel(output); + private JerseyLambdaContainerHandler getHandler() { + switch (type) { + case "API_GW": + case "ALB": + return handler; + case "HTTP_API": + return httpApiHandler; + default: + throw new RuntimeException("Unknown request type: " + type); + } } @Test public void headers_getHeaders_echo() { - AwsProxyRequest request = getRequestBuilder("/echo/headers", "GET") + AwsProxyRequestBuilder request = getRequestBuilder("/echo/headers", "GET") .json() - .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) - .build(); + .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type")); @@ -130,12 +155,11 @@ public void headers_getHeaders_echo() { @Test public void headers_servletRequest_echo() { - AwsProxyRequest request = getRequestBuilder("/echo/servlet-headers", "GET") + AwsProxyRequestBuilder request = getRequestBuilder("/echo/servlet-headers", "GET") .json() - .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) - .build(); + .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type")); @@ -144,6 +168,7 @@ public void headers_servletRequest_echo() { @Test public void headers_servletRequest_failedDependencyInjection_expectInternalServerError() { + assumeTrue("API_GW".equals(type)); AwsProxyRequest request = getRequestBuilder("/echo/servlet-headers", "GET") .json() .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) @@ -156,19 +181,18 @@ public void headers_servletRequest_failedDependencyInjection_expectInternalServe @Test public void context_servletResponse_setCustomHeader() { - AwsProxyRequest request = getRequestBuilder("/echo/servlet-response", "GET") - .json() - .build(); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/servlet-response", "GET") + .json(); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertTrue(output.getMultiValueHeaders().containsKey(EchoJerseyResource.SERVLET_RESP_HEADER_KEY)); } @Test public void context_serverInfo_correctContext() { - AwsProxyRequest request = getRequestBuilder("/echo/servlet-context", "GET").build(); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/servlet-context", "GET"); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type")); @@ -177,11 +201,10 @@ public void context_serverInfo_correctContext() { @Test public void requestScheme_valid_expectHttps() { - AwsProxyRequest request = getRequestBuilder("/echo/scheme", "GET") - .json() - .build(); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/scheme", "GET") + .json(); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type")); @@ -190,11 +213,10 @@ public void requestScheme_valid_expectHttps() { @Test public void requestFilter_injectsServletRequest_expectCustomAttribute() { - AwsProxyRequest request = getRequestBuilder("/echo/filter-attribute", "GET") - .json() - .build(); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/filter-attribute", "GET") + .json(); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type")); @@ -203,31 +225,27 @@ public void requestFilter_injectsServletRequest_expectCustomAttribute() { @Test public void authorizer_securityContext_customPrincipalSuccess() { - AwsProxyRequest request = getRequestBuilder("/echo/authorizer-principal", "GET") + assumeTrue("API_GW".equals(type)); // TODO: We should figure out a way to run this for HTTP_API too + AwsProxyRequestBuilder request = getRequestBuilder("/echo/authorizer-principal", "GET") .json() - .authorizerPrincipal(AUTHORIZER_PRINCIPAL_ID) - .build(); - - AwsProxyResponse output = handler.proxy(request, lambdaContext); - if (!isAlb) { - assertEquals(200, output.getStatusCode()); - assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type")); - validateSingleValueModel(output, AUTHORIZER_PRINCIPAL_ID); - } - + .authorizerPrincipal(AUTHORIZER_PRINCIPAL_ID); + AwsProxyResponse output = executeRequest(request, lambdaContext); + assertEquals(200, output.getStatusCode()); + assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type")); + validateSingleValueModel(output, AUTHORIZER_PRINCIPAL_ID); } @Test public void authorizer_securityContext_customAuthorizerContextSuccess() { - AwsProxyRequest request = getRequestBuilder("/echo/authorizer-context", "GET") + assumeTrue("API_GW".equals(type)); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/authorizer-context", "GET") .json() .authorizerPrincipal(AUTHORIZER_PRINCIPAL_ID) .authorizerContextValue(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) - .queryString("key", CUSTOM_HEADER_KEY) - .build(); + .queryString("key", CUSTOM_HEADER_KEY); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type")); @@ -236,31 +254,29 @@ public void authorizer_securityContext_customAuthorizerContextSuccess() { @Test public void errors_unknownRoute_expect404() { - AwsProxyRequest request = getRequestBuilder("/echo/test33", "GET").build(); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/test33", "GET"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(404, output.getStatusCode()); } @Test public void error_contentType_invalidContentType() { - AwsProxyRequest request = getRequestBuilder("/echo/json-body", "POST") + AwsProxyRequestBuilder request = getRequestBuilder("/echo/json-body", "POST") .header("Content-Type", "application/octet-stream") - .body("asdasdasd") - .build(); + .body("asdasdasd"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(415, output.getStatusCode()); } @Test public void error_statusCode_methodNotAllowed() { - AwsProxyRequest request = getRequestBuilder("/echo/status-code", "POST") + AwsProxyRequestBuilder request = getRequestBuilder("/echo/status-code", "POST") .json() - .queryString("status", "201") - .build(); + .queryString("status", "201"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(405, output.getStatusCode()); } @@ -268,12 +284,11 @@ public void error_statusCode_methodNotAllowed() { public void responseBody_responseWriter_validBody() throws JsonProcessingException { SingleValueModel singleValueModel = new SingleValueModel(); singleValueModel.setValue(CUSTOM_HEADER_VALUE); - AwsProxyRequest request = getRequestBuilder("/echo/json-body", "POST") + AwsProxyRequestBuilder request = getRequestBuilder("/echo/json-body", "POST") .json() - .body(objectMapper.writeValueAsString(singleValueModel)) - .build(); + .body(objectMapper.writeValueAsString(singleValueModel)); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertNotNull(output.getBody()); @@ -282,29 +297,28 @@ public void responseBody_responseWriter_validBody() throws JsonProcessingExcepti @Test public void statusCode_responseStatusCode_customStatusCode() { - AwsProxyRequest request = getRequestBuilder("/echo/status-code", "GET") + AwsProxyRequestBuilder request = getRequestBuilder("/echo/status-code", "GET") .json() - .queryString("status", "201") - .build(); + .queryString("status", "201"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(201, output.getStatusCode()); } @Test public void base64_binaryResponse_base64Encoding() { - AwsProxyRequest request = getRequestBuilder("/echo/binary", "GET").build(); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/binary", "GET"); - AwsProxyResponse response = handler.proxy(request, lambdaContext); + AwsProxyResponse response = executeRequest(request, lambdaContext); assertNotNull(response.getBody()); assertTrue(Base64.isBase64(response.getBody())); } @Test public void exception_mapException_mapToNotImplemented() { - AwsProxyRequest request = getRequestBuilder("/echo/exception", "GET").build(); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/exception", "GET"); - AwsProxyResponse response = handler.proxy(request, lambdaContext); + AwsProxyResponse response = executeRequest(request, lambdaContext); assertNotNull(response.getBody()); assertEquals(EchoJerseyResource.EXCEPTION_MESSAGE, response.getBody()); assertEquals(Response.Status.NOT_IMPLEMENTED.getStatusCode(), response.getStatusCode()); @@ -312,60 +326,58 @@ public void exception_mapException_mapToNotImplemented() { @Test public void stripBasePath_route_shouldRouteCorrectly() { - AwsProxyRequest request = getRequestBuilder("/custompath/echo/status-code", "GET") + AwsProxyRequestBuilder request = getRequestBuilder("/custompath/echo/status-code", "GET") .json() - .queryString("status", "201") - .build(); - handler.stripBasePath("/custompath"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + .queryString("status", "201"); + getHandler().stripBasePath("/custompath"); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(201, output.getStatusCode()); - handler.stripBasePath(""); + getHandler().stripBasePath(""); } @Test public void stripBasePath_route_shouldReturn404WithStageAsContext() { - AwsProxyRequest request = getRequestBuilder("/custompath/echo/status-code", "GET") + assumeTrue(!"ALB".equals(type)); + AwsProxyRequestBuilder request = getRequestBuilder("/custompath/echo/status-code", "GET") .stage("prod") .json() - .queryString("status", "201") - .build(); - handler.stripBasePath("/custompath"); + .queryString("status", "201"); + getHandler().stripBasePath("/custompath"); LambdaContainerHandler.getContainerConfig().setUseStageAsServletContext(true); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(404, output.getStatusCode()); - handler.stripBasePath(""); + getHandler().stripBasePath(""); LambdaContainerHandler.getContainerConfig().setUseStageAsServletContext(false); } @Test public void stripBasePath_route_shouldReturn404() { - AwsProxyRequest request = getRequestBuilder("/custompath/echo/status-code", "GET") + AwsProxyRequestBuilder request = getRequestBuilder("/custompath/echo/status-code", "GET") .json() - .queryString("status", "201") - .build(); - handler.stripBasePath("/custom"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + .queryString("status", "201"); + getHandler().stripBasePath("/custom"); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(404, output.getStatusCode()); - handler.stripBasePath(""); + getHandler().stripBasePath(""); } @Test public void securityContext_injectPrincipal_expectPrincipalName() { - AwsProxyRequest request = getRequestBuilder("/echo/security-context", "GET") - .authorizerPrincipal(USER_PRINCIPAL).build(); + assumeTrue("API_GW".equals(type)); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/security-context", "GET") + .authorizerPrincipal(USER_PRINCIPAL); - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertEquals(200, resp.getStatusCode()); validateSingleValueModel(resp, USER_PRINCIPAL); } @Test public void emptyStream_putNullBody_expectPutToSucceed() { - AwsProxyRequest request = getRequestBuilder("/echo/empty-stream/" + CUSTOM_HEADER_KEY + "/test/2", "PUT") + AwsProxyRequestBuilder request = getRequestBuilder("/echo/empty-stream/" + CUSTOM_HEADER_KEY + "/test/2", "PUT") .nullBody() - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) - .build(); - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertEquals(200, resp.getStatusCode()); validateSingleValueModel(resp, CUSTOM_HEADER_KEY); } @@ -373,26 +385,24 @@ public void emptyStream_putNullBody_expectPutToSucceed() { @Test public void refererHeader_headerParam_expectCorrectInjection() { String refererValue = "test-referer"; - AwsProxyRequest request = getRequestBuilder("/echo/referer-header", "GET") + AwsProxyRequestBuilder request = getRequestBuilder("/echo/referer-header", "GET") .nullBody() .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) - .header("Referer", refererValue) - .build(); + .header("Referer", refererValue); - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertEquals(200, resp.getStatusCode()); validateSingleValueModel(resp, refererValue); } @Test public void textPlainContent_plain_responseHonorsContentType() { - AwsProxyRequest req = getRequestBuilder("/echo/plain", "GET") + AwsProxyRequestBuilder req = getRequestBuilder("/echo/plain", "GET") .nullBody() .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) - .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) - .build(); + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN); - AwsProxyResponse resp = handler.proxy(req, lambdaContext); + AwsProxyResponse resp = executeRequest(req, lambdaContext); assertEquals(200, resp.getStatusCode()); assertTrue(resp.getMultiValueHeaders().containsKey(HttpHeaders.CONTENT_TYPE)); assertEquals(MediaType.TEXT_PLAIN, resp.getMultiValueHeaders().get(HttpHeaders.CONTENT_TYPE).get(0)); diff --git a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyParamEncodingTest.java b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyParamEncodingTest.java index d3b951aea..e736cbc1e 100644 --- a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyParamEncodingTest.java +++ b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyParamEncodingTest.java @@ -8,6 +8,7 @@ import com.amazonaws.serverless.proxy.jersey.model.SingleValueModel; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.services.lambda.runtime.Context; import com.fasterxml.jackson.databind.ObjectMapper; @@ -67,37 +68,62 @@ public class JerseyParamEncodingTest { .register(new ResourceBinder()) .property("jersey.config.server.tracing.type", "ALL") .property("jersey.config.server.tracing.threshold", "VERBOSE"); - private static JerseyLambdaContainerHandler handler = JerseyLambdaContainerHandler.getAwsProxyHandler(app); + private static JerseyLambdaContainerHandler handler; + + private static ResourceConfig httpApiApp = new ResourceConfig().packages("com.amazonaws.serverless.proxy.jersey") + .register(MultiPartFeature.class) + .register(new ResourceBinder()) + .property("jersey.config.server.tracing.type", "ALL") + .property("jersey.config.server.tracing.threshold", "VERBOSE"); + private static JerseyLambdaContainerHandler httpApiHandler; private static Context lambdaContext = new MockLambdaContext(); - private boolean isAlb; + private String type; - public JerseyParamEncodingTest(boolean alb) { - isAlb = alb; + public JerseyParamEncodingTest(String reqType) { + type = reqType; LambdaContainerHandler.getContainerConfig().addBinaryContentTypes(MediaType.MULTIPART_FORM_DATA); } @Parameterized.Parameters public static Collection data() { - return Arrays.asList(new Object[] { false, true }); + return Arrays.asList(new Object[] { "API_GW", "ALB", "HTTP_API" }); } private AwsProxyRequestBuilder getRequestBuilder(String path, String method) { - AwsProxyRequestBuilder builder = new AwsProxyRequestBuilder(path, method); - if (isAlb) builder.alb(); + return new AwsProxyRequestBuilder(path, method); + } - return builder; + private AwsProxyResponse executeRequest(AwsProxyRequestBuilder requestBuilder, Context lambdaContext) { + switch (type) { + case "API_GW": + if (handler == null) { + handler = JerseyLambdaContainerHandler.getAwsProxyHandler(app); + } + return handler.proxy(requestBuilder.build(), lambdaContext); + case "ALB": + if (handler == null) { + handler = JerseyLambdaContainerHandler.getAwsProxyHandler(app); + } + return handler.proxy(requestBuilder.alb().build(), lambdaContext); + case "HTTP_API": + if (httpApiHandler == null) { + httpApiHandler = JerseyLambdaContainerHandler.getHttpApiV2ProxyHandler(httpApiApp); + } + return httpApiHandler.proxy(requestBuilder.toHttpApiV2Request(), lambdaContext); + default: + throw new RuntimeException("Unknown request type: " + type); + } } @Test public void queryString_uriInfo_echo() { - AwsProxyRequest request = getRequestBuilder("/echo/query-string", "GET") + AwsProxyRequestBuilder request = getRequestBuilder("/echo/query-string", "GET") .json() - .queryString(QUERY_STRING_KEY, QUERY_STRING_NON_ENCODED_VALUE) - .build(); + .queryString(QUERY_STRING_KEY, QUERY_STRING_NON_ENCODED_VALUE); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type")); @@ -106,12 +132,11 @@ public void queryString_uriInfo_echo() { @Test public void queryString_notEncoded_echo() { - AwsProxyRequest request = getRequestBuilder("/echo/query-string", "GET") + AwsProxyRequestBuilder request = getRequestBuilder("/echo/query-string", "GET") .json() - .queryString(QUERY_STRING_KEY, QUERY_STRING_NON_ENCODED_VALUE) - .build(); + .queryString(QUERY_STRING_KEY, QUERY_STRING_NON_ENCODED_VALUE); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type")); @@ -121,12 +146,11 @@ public void queryString_notEncoded_echo() { @Test @Ignore("We expect to only receive decoded values from API Gateway") public void queryString_encoded_echo() { - AwsProxyRequest request = getRequestBuilder("/echo/query-string", "GET") + AwsProxyRequestBuilder request = getRequestBuilder("/echo/query-string", "GET") .json() - .queryString(QUERY_STRING_KEY, QUERY_STRING_ENCODED_VALUE) - .build(); + .queryString(QUERY_STRING_KEY, QUERY_STRING_ENCODED_VALUE); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type")); @@ -135,46 +159,46 @@ public void queryString_encoded_echo() { @Test public void simpleQueryParam_encoding_expectDecodedParam() { - AwsProxyRequest request = getRequestBuilder("/echo/decoded-param", "GET").queryString("param", SIMPLE_ENCODED_PARAM).build(); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/decoded-param", "GET").queryString("param", SIMPLE_ENCODED_PARAM); - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertEquals(200, resp.getStatusCode()); validateSingleValueModel(resp, SIMPLE_ENCODED_PARAM); } @Test public void jsonQueryParam_encoding_expectDecodedParam() { - AwsProxyRequest request = getRequestBuilder("/echo/decoded-param", "GET").queryString("param", JSON_ENCODED_PARAM).build(); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/decoded-param", "GET").queryString("param", JSON_ENCODED_PARAM); - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertEquals(200, resp.getStatusCode()); validateSingleValueModel(resp, JSON_ENCODED_PARAM); } @Test public void simpleQueryParam_encoding_expectEncodedParam() { - AwsProxyRequest request = getRequestBuilder("/echo/encoded-param", "GET").queryString("param", SIMPLE_ENCODED_PARAM).build(); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/encoded-param", "GET").queryString("param", SIMPLE_ENCODED_PARAM); String encodedVal = ""; try { encodedVal = URLEncoder.encode(SIMPLE_ENCODED_PARAM, "UTF-8"); } catch (UnsupportedEncodingException e) { fail("Could not encode parameter value"); } - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertEquals(200, resp.getStatusCode()); validateSingleValueModel(resp, encodedVal); } @Test public void jsonQueryParam_encoding_expectEncodedParam() { - AwsProxyRequest request = getRequestBuilder("/echo/encoded-param", "GET").queryString("param", JSON_ENCODED_PARAM).build(); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/encoded-param", "GET").queryString("param", JSON_ENCODED_PARAM); String encodedVal = ""; try { encodedVal = URLEncoder.encode(JSON_ENCODED_PARAM, "UTF-8"); } catch (UnsupportedEncodingException e) { fail("Could not encode parameter value"); } - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertEquals(200, resp.getStatusCode()); validateSingleValueModel(resp, encodedVal); } @@ -182,8 +206,8 @@ public void jsonQueryParam_encoding_expectEncodedParam() { @Test public void queryParam_encoding_expectFullyEncodedUrl() { String paramValue = "/+="; - AwsProxyRequest request = getRequestBuilder("/echo/encoded-param", "GET").queryString("param", paramValue).build(); - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/encoded-param", "GET").queryString("param", paramValue); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertNotNull(resp); assertEquals(resp.getStatusCode(), 200); validateSingleValueModel(resp, "%2F%2B%3D"); @@ -193,8 +217,8 @@ public void queryParam_encoding_expectFullyEncodedUrl() { public void pathParam_encoded_routesToCorrectPath() { String encodedParam = "http%3A%2F%2Fhelloresource.com"; String path = "/echo/encoded-path/" + encodedParam; - AwsProxyRequest request = getRequestBuilder(path, "GET").build(); - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyRequestBuilder request = getRequestBuilder(path, "GET"); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertNotNull(resp); assertEquals(resp.getStatusCode(), 200); validateSingleValueModel(resp, encodedParam); @@ -204,8 +228,8 @@ public void pathParam_encoded_routesToCorrectPath() { public void pathParam_encoded_returns404() { String encodedParam = "http://helloresource.com"; String path = "/echo/encoded-path/" + encodedParam; - AwsProxyRequest request = getRequestBuilder(path, "GET").build(); - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyRequestBuilder request = getRequestBuilder(path, "GET"); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertNotNull(resp); assertEquals(resp.getStatusCode(), 404); } @@ -213,8 +237,8 @@ public void pathParam_encoded_returns404() { @Test @Ignore public void queryParam_listOfString_expectCorrectLength() { - AwsProxyRequest request = getRequestBuilder("/echo/list-query-string", "GET").queryString("list", "v1,v2,v3").build(); - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/list-query-string", "GET").queryString("list", "v1,v2,v3"); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertNotNull(resp); assertEquals(resp.getStatusCode(), 200); validateSingleValueModel(resp, "3"); @@ -223,11 +247,9 @@ public void queryParam_listOfString_expectCorrectLength() { @Test public void multipart_getFileSize_expectCorrectLength() throws IOException { - AwsProxyRequest request = getRequestBuilder("/echo/file-size", "POST") - .formFilePart("file", "myfile.jpg", FILE_CONTENTS) - //.formFieldPart("name", QUERY_STRING_ENCODED_VALUE) - .build(); - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyRequestBuilder request = getRequestBuilder("/echo/file-size", "POST") + .formFilePart("file", "myfile.jpg", FILE_CONTENTS); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertNotNull(resp); assertEquals(200, resp.getStatusCode()); validateSingleValueModel(resp, "" + FILE_CONTENTS.length); diff --git a/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/SparkLambdaContainerHandler.java b/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/SparkLambdaContainerHandler.java index 368a5e815..2c16eaa98 100644 --- a/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/SparkLambdaContainerHandler.java +++ b/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/SparkLambdaContainerHandler.java @@ -14,16 +14,12 @@ import com.amazonaws.serverless.exceptions.ContainerInitializationException; -import com.amazonaws.serverless.proxy.AwsProxyExceptionHandler; -import com.amazonaws.serverless.proxy.AwsProxySecurityContextWriter; -import com.amazonaws.serverless.proxy.ExceptionHandler; -import com.amazonaws.serverless.proxy.RequestReader; -import com.amazonaws.serverless.proxy.ResponseWriter; -import com.amazonaws.serverless.proxy.SecurityContextWriter; +import com.amazonaws.serverless.proxy.*; import com.amazonaws.serverless.proxy.internal.testutils.Timer; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.internal.servlet.*; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.serverless.proxy.spark.embeddedserver.LambdaEmbeddedServer; import com.amazonaws.serverless.proxy.spark.embeddedserver.LambdaEmbeddedServerFactory; @@ -38,6 +34,7 @@ import javax.servlet.DispatcherType; import javax.servlet.FilterRegistration; import javax.servlet.Servlet; +import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -75,7 +72,7 @@ * @param The response object produced by the ResponseWriter implementation in the constructor */ public class SparkLambdaContainerHandler - extends AwsLambdaServletContainerHandler { + extends AwsLambdaServletContainerHandler { //------------------------------------------------------------- // Constants @@ -121,6 +118,31 @@ public static SparkLambdaContainerHandler get return newHandler; } + /** + * Returns a new instance of an SparkLambdaContainerHandler initialized to work with HttpApiV2ProxyRequest + * and AwsProxyResponse objects. + * + * @return a new instance of SparkLambdaContainerHandler + * + * @throws ContainerInitializationException Throws this exception if we fail to initialize the Spark container. + * This could be caused by the introspection used to insert the library as the default embedded container + */ + public static SparkLambdaContainerHandler getHttpApiV2ProxyHandler() + throws ContainerInitializationException { + SparkLambdaContainerHandler newHandler = new SparkLambdaContainerHandler<>(HttpApiV2ProxyRequest.class, + AwsProxyResponse.class, + new AwsHttpApiV2HttpServletRequestReader(), + new AwsProxyHttpServletResponseWriter(), + new AwsHttpApiV2SecurityContextWriter(), + new AwsProxyExceptionHandler(), + new LambdaEmbeddedServerFactory()); + + // For Spark we cannot call initialize here. It needs to be called manually after the routes are set + //newHandler.initialize(); + + return newHandler; + } + //------------------------------------------------------------- // Constructors //------------------------------------------------------------- @@ -128,7 +150,7 @@ public static SparkLambdaContainerHandler get public SparkLambdaContainerHandler(Class requestTypeClass, Class responseTypeClass, - RequestReader requestReader, + RequestReader requestReader, ResponseWriter responseWriter, SecurityContextWriter securityContextWriter, ExceptionHandler exceptionHandler, @@ -177,13 +199,13 @@ public SparkLambdaContainerHandler(Class requestTypeClass, @Override - protected AwsHttpServletResponse getContainerResponse(AwsProxyHttpServletRequest request, CountDownLatch latch) { + protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request, CountDownLatch latch) { return new AwsHttpServletResponse(request, latch); } @Override - protected void handleRequest(AwsProxyHttpServletRequest httpServletRequest, AwsHttpServletResponse httpServletResponse, Context lambdaContext) + protected void handleRequest(HttpServletRequest httpServletRequest, AwsHttpServletResponse httpServletResponse, Context lambdaContext) throws Exception { Timer.start("SPARK_HANDLE_REQUEST"); @@ -191,7 +213,9 @@ protected void handleRequest(AwsProxyHttpServletRequest httpServletRequest, AwsH initialize(); } - httpServletRequest.setServletContext(getServletContext()); + if (AwsHttpServletRequest.class.isAssignableFrom(httpServletRequest.getClass())) { + ((AwsHttpServletRequest)httpServletRequest).setServletContext(getServletContext()); + } doFilter(httpServletRequest, httpServletResponse, null); Timer.stop("SPARK_HANDLE_REQUEST"); diff --git a/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/embeddedserver/LambdaEmbeddedServer.java b/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/embeddedserver/LambdaEmbeddedServer.java index f4f227c4e..4be5d8565 100644 --- a/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/embeddedserver/LambdaEmbeddedServer.java +++ b/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/embeddedserver/LambdaEmbeddedServer.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.spark.embeddedserver; import com.amazonaws.serverless.exceptions.ContainerInitializationException; diff --git a/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/embeddedserver/LambdaEmbeddedServerFactory.java b/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/embeddedserver/LambdaEmbeddedServerFactory.java index 3455170fa..9074e5ddf 100644 --- a/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/embeddedserver/LambdaEmbeddedServerFactory.java +++ b/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/embeddedserver/LambdaEmbeddedServerFactory.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.spark.embeddedserver; import com.amazonaws.serverless.proxy.internal.testutils.Timer; diff --git a/aws-serverless-java-container-spark/src/test/java/com/amazonaws/serverless/proxy/spark/HelloWorldSparkStreamTest.java b/aws-serverless-java-container-spark/src/test/java/com/amazonaws/serverless/proxy/spark/HelloWorldSparkStreamTest.java index 74cd55040..0c41f91f7 100644 --- a/aws-serverless-java-container-spark/src/test/java/com/amazonaws/serverless/proxy/spark/HelloWorldSparkStreamTest.java +++ b/aws-serverless-java-container-spark/src/test/java/com/amazonaws/serverless/proxy/spark/HelloWorldSparkStreamTest.java @@ -8,8 +8,9 @@ import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.services.lambda.runtime.Context; import org.junit.AfterClass; -import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -19,7 +20,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.util.Arrays; import java.util.Collection; @@ -43,36 +43,58 @@ public class HelloWorldSparkStreamTest { private static final String COOKIE_PATH = "/"; private static SparkLambdaContainerHandler handler; + private static SparkLambdaContainerHandler httpApiHandler; - private boolean isAlb; + private String type; - public HelloWorldSparkStreamTest(boolean alb) { - isAlb = alb; + public HelloWorldSparkStreamTest(String reqType) { + type = reqType; + try { + switch (type) { + case "API_GW": + case "ALB": + handler = SparkLambdaContainerHandler.getAwsProxyHandler(); + break; + case "HTTP_API": + httpApiHandler = SparkLambdaContainerHandler.getHttpApiV2ProxyHandler(); + break; + default: + throw new RuntimeException("Unknown request type: " + type); + } + + configureRoutes(); + Spark.awaitInitialization(); + } catch (RuntimeException | ContainerInitializationException e) { + e.printStackTrace(); + fail(); + } } @Parameterized.Parameters public static Collection data() { - return Arrays.asList(new Object[] { false, true }); + return Arrays.asList(new Object[] { "API_GW", "ALB", "HTTP_API" }); } private AwsProxyRequestBuilder getRequestBuilder() { - AwsProxyRequestBuilder builder = new AwsProxyRequestBuilder(); - if (isAlb) builder.alb(); - - return builder; + return new AwsProxyRequestBuilder(); } - @BeforeClass - public static void initializeServer() { - try { - handler = SparkLambdaContainerHandler.getAwsProxyHandler(); - - configureRoutes(); - Spark.awaitInitialization(); - } catch (RuntimeException | ContainerInitializationException e) { - e.printStackTrace(); - fail(); + private ByteArrayOutputStream executeRequest(AwsProxyRequestBuilder requestBuilder, Context lambdaContext) throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + switch (type) { + case "API_GW": + handler.proxyStream(requestBuilder.buildStream(), os, lambdaContext); + break; + case "ALB": + handler.proxyStream(requestBuilder.alb().buildStream(), os, lambdaContext); + break; + case "HTTP_API": + httpApiHandler.proxyStream(requestBuilder.toHttpApiV2RequestStream(), os, lambdaContext); + break; + default: + throw new RuntimeException("Unknown request type: " + type); } + return os; } @AfterClass @@ -82,10 +104,8 @@ public static void stopSpark() { @Test public void helloRequest_basicStream_populatesOutputSuccessfully() { - InputStream req = getRequestBuilder().method("GET").path("/hello").buildStream(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { - handler.proxyStream(req, outputStream, new MockLambdaContext()); + ByteArrayOutputStream outputStream = executeRequest(getRequestBuilder().method("GET").path("/hello"), new MockLambdaContext()); AwsProxyResponse response = LambdaContainerHandler.getObjectMapper().readValue(outputStream.toByteArray(), AwsProxyResponse.class); assertEquals(200, response.getStatusCode()); @@ -100,10 +120,8 @@ public void helloRequest_basicStream_populatesOutputSuccessfully() { @Test public void nullPathRequest_doesntFail_expectA404() { - InputStream req = getRequestBuilder().method("GET").path(null).buildStream(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { - handler.proxyStream(req, outputStream, new MockLambdaContext()); + ByteArrayOutputStream outputStream = executeRequest(getRequestBuilder().method("GET").path(null), new MockLambdaContext()); AwsProxyResponse response = LambdaContainerHandler.getObjectMapper().readValue(outputStream.toByteArray(), AwsProxyResponse.class); assertEquals(404, response.getStatusCode()); diff --git a/aws-serverless-java-container-spring/pom.xml b/aws-serverless-java-container-spring/pom.xml index 5b6117cb9..6065d464f 100644 --- a/aws-serverless-java-container-spring/pom.xml +++ b/aws-serverless-java-container-spring/pom.xml @@ -60,6 +60,15 @@ test + + + javax.activation + activation + 1.1.1 + test + + + org.hibernate hibernate-validator diff --git a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java index cb449568c..98841ed44 100644 --- a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java +++ b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java @@ -18,16 +18,20 @@ import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.internal.servlet.*; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.serverless.proxy.spring.embedded.ServerlessServletEmbeddedServerFactory; import com.amazonaws.services.lambda.runtime.Context; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.StandardEnvironment; import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.ConfigurableWebApplicationContext; import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; import java.util.concurrent.CountDownLatch; @@ -47,7 +51,9 @@ * @param The expected return type */ @Deprecated -public class SpringBootLambdaContainerHandler extends AwsLambdaServletContainerHandler { +public class SpringBootLambdaContainerHandler extends AwsLambdaServletContainerHandler { + private static final String DISPATCHER_SERVLET_REGISTRATION_NAME = "dispatcherServlet"; + private final Class springBootInitializer; private static final Logger log = LoggerFactory.getLogger(SpringBootLambdaContainerHandler.class); private String[] springProfiles = null; @@ -78,7 +84,7 @@ public static SpringBootLambdaContainerHandler getInstance() { */ public static SpringBootLambdaContainerHandler getAwsProxyHandler(Class springBootInitializer, String... profiles) throws ContainerInitializationException { - return new SpringBootProxyHandlerBuilder() + return new SpringBootProxyHandlerBuilder() .defaultProxy() .initializationWrapper(new InitializationWrapper()) .springBootApplication(springBootInitializer) @@ -86,6 +92,23 @@ public static SpringBootLambdaContainerHandler getHttpApiV2ProxyHandler(Class springBootInitializer, String... profiles) + throws ContainerInitializationException { + return new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(springBootInitializer) + .profiles(profiles) + .buildAndInitialize(); + } + /** * Creates a new container handler with the given reader and writer objects * @@ -99,7 +122,7 @@ public static SpringBootLambdaContainerHandler requestTypeClass, Class responseTypeClass, - RequestReader requestReader, + RequestReader requestReader, ResponseWriter responseWriter, SecurityContextWriter securityContextWriter, ExceptionHandler exceptionHandler, @@ -130,12 +153,12 @@ public void activateSpringProfiles(String... profiles) { } @Override - protected AwsHttpServletResponse getContainerResponse(AwsProxyHttpServletRequest request, CountDownLatch latch) { + protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request, CountDownLatch latch) { return new AwsHttpServletResponse(request, latch); } @Override - protected void handleRequest(AwsProxyHttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { + protected void handleRequest(HttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { // this method of the AwsLambdaServletContainerHandler sets the servlet context Timer.start("SPRINGBOOT_HANDLE_REQUEST"); @@ -144,11 +167,13 @@ protected void handleRequest(AwsProxyHttpServletRequest containerRequest, AwsHtt initialize(); } - containerRequest.setServletContext(getServletContext()); + if (AwsHttpServletRequest.class.isAssignableFrom(containerRequest.getClass())) { + ((AwsHttpServletRequest)containerRequest).setServletContext(getServletContext()); + ((AwsHttpServletRequest)containerRequest).setResponse(containerResponse); + } // process filters & invoke servlet Servlet reqServlet = ((AwsServletContext)getServletContext()).getServletForPath(containerRequest.getPathInfo()); - containerRequest.setResponse(containerResponse); doFilter(containerRequest, containerResponse, reqServlet); Timer.stop("SPRINGBOOT_HANDLE_REQUEST"); } @@ -169,8 +194,14 @@ public void initialize() springEnv.setActiveProfiles(springProfiles); app.setEnvironment(springEnv); } - app.run(); + ConfigurableApplicationContext applicationContext = app.run(); + ((ConfigurableWebApplicationContext)applicationContext).setServletContext(getServletContext()); + AwsServletRegistration reg = (AwsServletRegistration)getServletContext().getServletRegistration(DISPATCHER_SERVLET_REGISTRATION_NAME); + if (reg != null) { + reg.setLoadOnStartup(1); + } + super.initialize(); initialized = true; Timer.stop("SPRINGBOOT_COLD_START"); } diff --git a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java index 4ead5bc4d..2f06e73e6 100644 --- a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java +++ b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.spring; import com.amazonaws.serverless.exceptions.ContainerInitializationException; @@ -7,38 +19,40 @@ import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import org.springframework.web.WebApplicationInitializer; -public final class SpringBootProxyHandlerBuilder extends ServletLambdaContainerHandlerBuilder< - AwsProxyRequest, +import javax.servlet.http.HttpServletRequest; + +public final class SpringBootProxyHandlerBuilder extends ServletLambdaContainerHandlerBuilder< + RequestType, AwsProxyResponse, - AwsProxyHttpServletRequest, - SpringBootLambdaContainerHandler, - SpringBootProxyHandlerBuilder> { + HttpServletRequest, + SpringBootLambdaContainerHandler, + SpringBootProxyHandlerBuilder> { private Class springBootInitializer; private String[] profiles; @Override - protected SpringBootProxyHandlerBuilder self() { + protected SpringBootProxyHandlerBuilder self() { return this; } - public SpringBootProxyHandlerBuilder springBootApplication(Class app) { + public SpringBootProxyHandlerBuilder springBootApplication(Class app) { springBootInitializer = app; return self(); } - public SpringBootProxyHandlerBuilder profiles(String... profiles) { + public SpringBootProxyHandlerBuilder profiles(String... profiles) { this.profiles = profiles; return self(); } @Override - public SpringBootLambdaContainerHandler build() throws ContainerInitializationException { + public SpringBootLambdaContainerHandler build() throws ContainerInitializationException { validate(); if (springBootInitializer == null) { throw new ContainerInitializationException("Missing spring boot application class in builder", null); } - SpringBootLambdaContainerHandler handler = new SpringBootLambdaContainerHandler<>( + SpringBootLambdaContainerHandler handler = new SpringBootLambdaContainerHandler( requestTypeClass, responseTypeClass, requestReader, @@ -55,8 +69,8 @@ public SpringBootLambdaContainerHandler build } @Override - public SpringBootLambdaContainerHandler buildAndInitialize() throws ContainerInitializationException { - SpringBootLambdaContainerHandler handler = build(); + public SpringBootLambdaContainerHandler buildAndInitialize() throws ContainerInitializationException { + SpringBootLambdaContainerHandler handler = build(); initializationWrapper.start(handler); return handler; } diff --git a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootServletConfigurationSupport.java b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootServletConfigurationSupport.java index d975bd20e..9857c9620 100644 --- a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootServletConfigurationSupport.java +++ b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootServletConfigurationSupport.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.spring; import org.springframework.boot.context.embedded.WebApplicationContextServletContextAwareProcessor; diff --git a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringLambdaContainerHandler.java b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringLambdaContainerHandler.java index 5168fa96b..862df3bdf 100644 --- a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringLambdaContainerHandler.java +++ b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringLambdaContainerHandler.java @@ -18,13 +18,14 @@ import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.internal.servlet.*; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.services.lambda.runtime.Context; import org.springframework.web.context.ConfigurableWebApplicationContext; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; import javax.servlet.Servlet; import javax.servlet.ServletRegistration; +import javax.servlet.http.HttpServletRequest; import java.util.concurrent.CountDownLatch; @@ -35,7 +36,7 @@ * @param The incoming event type * @param The expected return type */ -public class SpringLambdaContainerHandler extends AwsLambdaServletContainerHandler { +public class SpringLambdaContainerHandler extends AwsLambdaServletContainerHandler { private ConfigurableWebApplicationContext appContext; private String[] profiles; @@ -46,10 +47,10 @@ public class SpringLambdaContainerHandler extends Aws * Creates a default SpringLambdaContainerHandler initialized with the `AwsProxyRequest` and `AwsProxyResponse` objects * @param config A set of classes annotated with the Spring @Configuration annotation * @return An initialized instance of the `SpringLambdaContainerHandler` - * @throws ContainerInitializationException + * @throws ContainerInitializationException When the Spring framework fails to start. */ - public static SpringLambdaContainerHandler getAwsProxyHandler(Class... config) throws ContainerInitializationException { - return new SpringProxyHandlerBuilder() + public static SpringLambdaContainerHandler getAwsProxyHandler(Class... config) throws ContainerInitializationException { + return new SpringProxyHandlerBuilder() .defaultProxy() .initializationWrapper(new InitializationWrapper()) .configurationClasses(config) @@ -61,11 +62,11 @@ public static SpringLambdaContainerHandler ge * @param applicationContext A custom ConfigurableWebApplicationContext to be used * @param profiles The spring profiles to activate * @return An initialized instance of the `SpringLambdaContainerHandler` - * @throws ContainerInitializationException + * @throws ContainerInitializationException When the Spring framework fails to start. */ public static SpringLambdaContainerHandler getAwsProxyHandler(ConfigurableWebApplicationContext applicationContext, String... profiles) throws ContainerInitializationException { - return new SpringProxyHandlerBuilder() + return new SpringProxyHandlerBuilder() .defaultProxy() .initializationWrapper(new InitializationWrapper()) .springApplicationContext(applicationContext) @@ -73,6 +74,20 @@ public static SpringLambdaContainerHandler ge .buildAndInitialize(); } + /** + * Creates a default SpringLambdaContainerHandler initialized with the `HttpApiV2ProxyRequest` and `AwsProxyResponse` objects + * @param config A set of classes annotated with the Spring @Configuration annotation + * @return An initialized instance of the `SpringLambdaContainerHandler` + * @throws ContainerInitializationException When the Spring framework fails to start. + */ + public static SpringLambdaContainerHandler getHttpApiV2ProxyHandler(Class... config) throws ContainerInitializationException { + return new SpringProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .configurationClasses(config) + .buildAndInitialize(); + } + /** * Creates a new container handler with the given reader and writer objects * @@ -81,17 +96,15 @@ public static SpringLambdaContainerHandler ge * @param responseWriter An implementation of `ResponseWriter` * @param securityContextWriter An implementation of `SecurityContextWriter` * @param exceptionHandler An implementation of `ExceptionHandler` - * @throws ContainerInitializationException */ public SpringLambdaContainerHandler(Class requestTypeClass, Class responseTypeClass, - RequestReader requestReader, + RequestReader requestReader, ResponseWriter responseWriter, SecurityContextWriter securityContextWriter, ExceptionHandler exceptionHandler, ConfigurableWebApplicationContext applicationContext, - InitializationWrapper init) - throws ContainerInitializationException { + InitializationWrapper init) { super(requestTypeClass, responseTypeClass, requestReader, responseWriter, securityContextWriter, exceptionHandler); Timer.start("SPRING_CONTAINER_HANDLER_CONSTRUCTOR"); appContext = applicationContext; @@ -111,7 +124,7 @@ public void setRefreshContext(boolean refresh) { @Override - protected AwsHttpServletResponse getContainerResponse(AwsProxyHttpServletRequest request, CountDownLatch latch) { + protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request, CountDownLatch latch) { return new AwsHttpServletResponse(request, latch); } @@ -131,7 +144,7 @@ public void activateSpringProfiles(String... p) throws ContainerInitializationEx } @Override - protected void handleRequest(AwsProxyHttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { + protected void handleRequest(HttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { Timer.start("SPRING_HANDLE_REQUEST"); if (refreshContext) { @@ -139,11 +152,13 @@ protected void handleRequest(AwsProxyHttpServletRequest containerRequest, AwsHtt refreshContext = false; } - containerRequest.setServletContext(getServletContext()); + if (AwsHttpServletRequest.class.isAssignableFrom(containerRequest.getClass())) { + ((AwsHttpServletRequest)containerRequest).setServletContext(getServletContext()); + ((AwsHttpServletRequest)containerRequest).setResponse(containerResponse); + } // process filters Servlet reqServlet = ((AwsServletContext)getServletContext()).getServletForPath(containerRequest.getPathInfo()); - containerRequest.setResponse(containerResponse); doFilter(containerRequest, containerResponse, reqServlet); Timer.stop("SPRING_HANDLE_REQUEST"); } @@ -160,6 +175,9 @@ public void initialize() DispatcherServlet dispatcher = new DispatcherServlet(appContext); ServletRegistration.Dynamic reg = getServletContext().addServlet("dispatcherServlet", dispatcher); reg.addMapping("/"); + reg.setLoadOnStartup(1); + // call initialize on AwsLambdaServletContainerHandler to initialize servlets that are set to load on startup + super.initialize(); Timer.stop("SPRING_COLD_START"); } } diff --git a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringProxyHandlerBuilder.java b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringProxyHandlerBuilder.java index 9bfbf3cbc..689b6de28 100644 --- a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringProxyHandlerBuilder.java +++ b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringProxyHandlerBuilder.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.spring; import com.amazonaws.serverless.exceptions.ContainerInitializationException; @@ -9,39 +21,41 @@ import org.springframework.web.context.ConfigurableWebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -public final class SpringProxyHandlerBuilder extends ServletLambdaContainerHandlerBuilder< - AwsProxyRequest, +import javax.servlet.http.HttpServletRequest; + +public final class SpringProxyHandlerBuilder extends ServletLambdaContainerHandlerBuilder< + RequestType, AwsProxyResponse, - AwsProxyHttpServletRequest, - SpringLambdaContainerHandler, - SpringProxyHandlerBuilder> { + HttpServletRequest, + SpringLambdaContainerHandler, + SpringProxyHandlerBuilder> { private ConfigurableWebApplicationContext springContext; private Class[] configurationClasses; private String[] profiles; @Override - protected SpringProxyHandlerBuilder self() { + protected SpringProxyHandlerBuilder self() { return this; } - public SpringProxyHandlerBuilder springApplicationContext(ConfigurableWebApplicationContext app) { + public SpringProxyHandlerBuilder springApplicationContext(ConfigurableWebApplicationContext app) { springContext = app; return self(); } - public SpringProxyHandlerBuilder configurationClasses(Class... config) { + public SpringProxyHandlerBuilder configurationClasses(Class... config) { configurationClasses = config; return self(); } - public SpringProxyHandlerBuilder profiles(String... profiles) { + public SpringProxyHandlerBuilder profiles(String... profiles) { this.profiles = profiles; return self(); } @Override - public SpringLambdaContainerHandler build() throws ContainerInitializationException { + public SpringLambdaContainerHandler build() throws ContainerInitializationException { validate(); if (springContext == null && (configurationClasses == null || configurationClasses.length == 0)) { throw new ContainerInitializationException("Missing both configuration classes and application context, at least" + @@ -55,7 +69,7 @@ public SpringLambdaContainerHandler build() t } } - SpringLambdaContainerHandler handler = new SpringLambdaContainerHandler<>( + SpringLambdaContainerHandler handler = new SpringLambdaContainerHandler( requestTypeClass, responseTypeClass, requestReader, @@ -72,8 +86,8 @@ public SpringLambdaContainerHandler build() t } @Override - public SpringLambdaContainerHandler buildAndInitialize() throws ContainerInitializationException { - SpringLambdaContainerHandler handler = build(); + public SpringLambdaContainerHandler buildAndInitialize() throws ContainerInitializationException { + SpringLambdaContainerHandler handler = build(); initializationWrapper.start(handler); return handler; } diff --git a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java index 4e9a8836c..bd567ed7d 100644 --- a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java +++ b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.spring.embedded; import com.amazonaws.serverless.proxy.internal.servlet.AwsLambdaServletContainerHandler; diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java index b9cb728d6..c1365a5b1 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java @@ -1,8 +1,10 @@ package com.amazonaws.serverless.proxy.spring; +import com.amazonaws.serverless.exceptions.ContainerInitializationException; import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; -import com.amazonaws.serverless.proxy.model.AwsProxyRequest; -import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.internal.servlet.AwsLambdaServletContainerHandler; +import com.amazonaws.serverless.proxy.internal.servlet.AwsServletRegistration; +import com.amazonaws.serverless.proxy.model.*; import com.amazonaws.serverless.proxy.internal.servlet.AwsServletContext; import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; @@ -12,12 +14,14 @@ import com.amazonaws.serverless.proxy.spring.echoapp.UnauthenticatedFilter; import com.amazonaws.serverless.proxy.spring.echoapp.model.MapResponseModel; import com.amazonaws.serverless.proxy.spring.echoapp.model.SingleValueModel; +import com.amazonaws.services.lambda.runtime.Context; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.codec.binary.Base64; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; @@ -25,35 +29,88 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.servlet.DispatcherServlet; +import javax.servlet.DispatcherType; +import javax.servlet.FilterRegistration; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import java.io.IOException; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; import java.util.UUID; import static org.junit.Assert.*; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {EchoSpringAppConfig.class}) -@WebAppConfiguration -@TestExecutionListeners(inheritListeners = false, listeners = {DependencyInjectionTestExecutionListener.class}) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +@RunWith(Parameterized.class) public class SpringAwsProxyTest { private static final String CUSTOM_HEADER_KEY = "x-custom-header"; private static final String CUSTOM_HEADER_VALUE = "my-custom-value"; private static final String AUTHORIZER_PRINCIPAL_ID = "test-principal-" + UUID.randomUUID().toString(); + private static final String UNICODE_VALUE = "שלום לכולם"; - @Autowired - private ObjectMapper objectMapper; + private ObjectMapper objectMapper = new ObjectMapper(); + private MockLambdaContext lambdaContext = new MockLambdaContext(); + private static SpringLambdaContainerHandler handler; + private static SpringLambdaContainerHandler httpApiHandler; - @Autowired - private MockLambdaContext lambdaContext; + private AwsLambdaServletContainerHandler.StartupHandler h = (c -> { + FilterRegistration.Dynamic registration = c.addFilter("UnauthenticatedFilter", UnauthenticatedFilter.class); + // update the registration to map to a path + registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/echo/*"); + // servlet name mappings are disabled and will throw an exception - @Autowired - private SpringLambdaContainerHandler handler; + //handler.getApplicationInitializer().getDispatcherServlet().setThrowExceptionIfNoHandlerFound(true); + ((DispatcherServlet)((AwsServletRegistration)c.getServletRegistration("dispatcherServlet")).getServlet()).setThrowExceptionIfNoHandlerFound(true); + }); + + private String type; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[] { "API_GW", "ALB", "HTTP_API" }); + } + + public SpringAwsProxyTest(String reqType) { + type = reqType; + } + + private AwsProxyResponse executeRequest(AwsProxyRequestBuilder requestBuilder, Context lambdaContext) { + try { + switch (type) { + case "API_GW": + if (handler == null) { + handler = SpringLambdaContainerHandler.getAwsProxyHandler(EchoSpringAppConfig.class); + handler.onStartup(h); + } + return handler.proxy(requestBuilder.build(), lambdaContext); + case "ALB": + if (handler == null) { + handler = SpringLambdaContainerHandler.getAwsProxyHandler(EchoSpringAppConfig.class); + handler.onStartup(h); + } + return handler.proxy(requestBuilder.alb().build(), lambdaContext); + case "HTTP_API": + if (httpApiHandler == null) { + httpApiHandler = SpringLambdaContainerHandler.getHttpApiV2ProxyHandler(EchoSpringAppConfig.class); + httpApiHandler.onStartup(h); + } + return httpApiHandler.proxy(requestBuilder.toHttpApiV2Request(), lambdaContext); + default: + throw new RuntimeException("Unknown request type: " + type); + } + } catch (ContainerInitializationException e) { + e.printStackTrace(); + fail("Could not execute request"); + throw new RuntimeException(e); + } + } @Before public void clearServletContextCache() { @@ -62,12 +119,11 @@ public void clearServletContextCache() { @Test public void controllerAdvice_invalidPath_returnAdvice() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo2", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo2", "GET") .json() - .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) - .build(); + .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertNotNull(output); assertEquals(404, output.getStatusCode()); validateSingleValueModel(output, RestControllerAdvice.ERROR_MESSAGE); @@ -76,12 +132,11 @@ public void controllerAdvice_invalidPath_returnAdvice() { @Test public void headers_getHeaders_echo() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/headers", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/headers", "GET") .json() - .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) - .build(); + .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type").split(";")[0]); @@ -90,12 +145,11 @@ public void headers_getHeaders_echo() { @Test public void headers_servletRequest_echo() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/servlet-headers", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/servlet-headers", "GET") .json() - .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) - .build(); + .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type").split(";")[0]); @@ -104,12 +158,11 @@ public void headers_servletRequest_echo() { @Test public void queryString_uriInfo_echo() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/query-string", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/query-string", "GET") .json() - .queryString(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) - .build(); + .queryString(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type").split(";")[0]); @@ -118,12 +171,11 @@ public void queryString_uriInfo_echo() { @Test public void queryString_listParameter_expectCorrectLength() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/list-query-string", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/list-query-string", "GET") .json() - .queryString("list", "v1,v2,v3") - .build(); + .queryString("list", "v1,v2,v3"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); validateSingleValueModel(output, "3"); @@ -132,13 +184,12 @@ public void queryString_listParameter_expectCorrectLength() { @Test public void queryString_multiParam_expectCorrectValueCount() throws IOException { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/multivalue-query-string", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/multivalue-query-string", "GET") .json() .queryString("multiple", "first") - .queryString("multiple", "second") - .build(); + .queryString("multiple", "second"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); MapResponseModel response = objectMapper.readValue(output.getBody(), MapResponseModel.class); @@ -149,42 +200,40 @@ public void queryString_multiParam_expectCorrectValueCount() @Test public void dateHeader_notModified_expect304() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/last-modified", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/last-modified", "GET") .json() .header( HttpHeaders.IF_MODIFIED_SINCE, DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now().minus(1, ChronoUnit.SECONDS)) - ) - .build(); + ); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(304, output.getStatusCode()); assertEquals("", output.getBody()); } @Test public void dateHeader_notModified_expect200() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/last-modified", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/last-modified", "GET") .json() .header( HttpHeaders.IF_MODIFIED_SINCE, DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now().minus(5, ChronoUnit.DAYS)) - ) - .build(); + ); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals(EchoResource.STRING_BODY, output.getBody()); } @Test public void authorizer_securityContext_customPrincipalSuccess() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/authorizer-principal", "GET") + assumeTrue("API_GW".equals(type)); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/authorizer-principal", "GET") .json() - .authorizerPrincipal(AUTHORIZER_PRINCIPAL_ID) - .build(); + .authorizerPrincipal(AUTHORIZER_PRINCIPAL_ID); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("application/json", output.getMultiValueHeaders().getFirst("Content-Type").split(";")[0]); @@ -193,43 +242,40 @@ public void authorizer_securityContext_customPrincipalSuccess() { @Test public void errors_unknownRoute_expect404() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/test33", "GET").build(); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/test33", "GET"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(404, output.getStatusCode()); } @Test public void error_contentType_invalidContentType() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/json-body", "POST") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/json-body", "POST") .header("Content-Type", "application/octet-stream") - .body("asdasdasd") - .build(); + .body("asdasdasd"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(415, output.getStatusCode()); } @Test public void error_statusCode_methodNotAllowed() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/status-code", "POST") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/status-code", "POST") .json() - .queryString("status", "201") - .build(); + .queryString("status", "201"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(405, output.getStatusCode()); } @Test public void error_unauthenticatedCall_filterStepsRequest() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/status-code", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/status-code", "GET") .header(UnauthenticatedFilter.HEADER_NAME, "1") .json() - .queryString("status", "201") - .build(); + .queryString("status", "201"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(401, output.getStatusCode()); } @@ -237,46 +283,58 @@ public void error_unauthenticatedCall_filterStepsRequest() { public void responseBody_responseWriter_validBody() throws JsonProcessingException { SingleValueModel singleValueModel = new SingleValueModel(); singleValueModel.setValue(CUSTOM_HEADER_VALUE); - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/json-body", "POST") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/json-body", "POST") .json() .header("Content-Type", "application/json") - .body(objectMapper.writeValueAsString(singleValueModel)) - .build(); + .body(objectMapper.writeValueAsString(singleValueModel)); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertNotNull(output.getBody()); validateSingleValueModel(output, CUSTOM_HEADER_VALUE); } + @Test + public void responseBody_responseWriter_validBody_UTF() throws JsonProcessingException { + SingleValueModel singleValueModel = new SingleValueModel(); + singleValueModel.setValue(UNICODE_VALUE); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/json-body", "POST") + .header("Content-Type", "application/json; charset=UTF-8") + .body(objectMapper.writeValueAsString(singleValueModel)); + LambdaContainerHandler.getContainerConfig().setDefaultContentCharset("UTF-8"); + AwsProxyResponse output = executeRequest(request, lambdaContext); + assertEquals(200, output.getStatusCode()); + assertNotNull(output.getBody()); + validateSingleValueModel(output, UNICODE_VALUE); + LambdaContainerHandler.getContainerConfig().setDefaultContentCharset(ContainerConfig.DEFAULT_CONTENT_CHARSET); + } + @Test public void statusCode_responseStatusCode_customStatusCode() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/status-code", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/status-code", "GET") .json() - .queryString("status", "201") - .build(); + .queryString("status", "201"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(201, output.getStatusCode()); } @Test public void base64_binaryResponse_base64Encoding() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/binary", "GET").build(); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/binary", "GET"); - AwsProxyResponse response = handler.proxy(request, lambdaContext); + AwsProxyResponse response = executeRequest(request, lambdaContext); assertNotNull(response.getBody()); assertTrue(Base64.isBase64(response.getBody())); } @Test public void injectBody_populatedResponse_noException() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/request-body", "POST") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/request-body", "POST") .header(HttpHeaders.CONTENT_TYPE, "text/plain") - .body("This is a populated body") - .build(); + .body("This is a populated body"); - AwsProxyResponse response = handler.proxy(request, lambdaContext); + AwsProxyResponse response = executeRequest(request, lambdaContext); assertNotNull(response.getBody()); assertEquals(200, response.getStatusCode()); try { @@ -287,9 +345,8 @@ public void injectBody_populatedResponse_noException() { fail(); } - AwsProxyRequest emptyReq = new AwsProxyRequestBuilder("/echo/request-body", "POST") - .build(); - AwsProxyResponse emptyResp = handler.proxy(emptyReq, lambdaContext); + AwsProxyRequestBuilder emptyReq = new AwsProxyRequestBuilder("/echo/request-body", "POST"); + AwsProxyResponse emptyResp = executeRequest(emptyReq, lambdaContext); try { SingleValueModel output = objectMapper.readValue(emptyResp.getBody(), SingleValueModel.class); assertEquals(null, output.getValue()); @@ -303,28 +360,26 @@ public void injectBody_populatedResponse_noException() { public void servletRequestEncoding_acceptEncoding_okStatusCode() { SingleValueModel singleValueModel = new SingleValueModel(); singleValueModel.setValue(CUSTOM_HEADER_VALUE); - AwsProxyRequest request = null; + AwsProxyRequestBuilder request = null; try { request = new AwsProxyRequestBuilder("/echo/json-body", "POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) .header(HttpHeaders.ACCEPT_ENCODING, "gzip, deflate") .queryString("status", "200") - .body(objectMapper.writeValueAsString(singleValueModel)) - .build(); + .body(objectMapper.writeValueAsString(singleValueModel)); } catch (JsonProcessingException e) { fail("Could not serialize object to JSON"); } - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); } @Test public void request_requestURI() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/request-URI", "GET") - .build(); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/request-URI", "GET"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); validateSingleValueModel(output, "/echo/request-URI"); @@ -332,13 +387,12 @@ public void request_requestURI() { @Test public void request_requestURL() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/request-url", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/request-url", "GET") .scheme("https") .serverName("api.myserver.com") - .stage("prod") - .build(); + .stage("prod"); handler.stripBasePath(""); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); validateSingleValueModel(output, "https://api.myserver.com/echo/request-url"); @@ -346,13 +400,12 @@ public void request_requestURL() { @Test public void request_encodedPath_returnsDecodedPath() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/encoded-request-uri/Some%20Thing", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/encoded-request-uri/Some%20Thing", "GET") .scheme("https") .serverName("api.myserver.com") - .stage("prod") - .build(); + .stage("prod"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); validateSingleValueModel(output, "Some Thing"); @@ -361,15 +414,15 @@ public void request_encodedPath_returnsDecodedPath() { @Test public void contextPath_generateLink_returnsCorrectPath() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/generate-uri", "GET") + assumeFalse("ALB".equals(type)); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/generate-uri", "GET") .scheme("https") .serverName("api.myserver.com") - .stage("prod") - .build(); + .stage("prod"); LambdaContainerHandler.getContainerConfig().addCustomDomain("api.myserver.com"); SpringLambdaContainerHandler.getContainerConfig().setUseStageAsServletContext(true); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); String expectedUri = "https://api.myserver.com/prod/echo/encoded-request-uri/" + EchoResource.TEST_GENERATE_URI; @@ -382,11 +435,10 @@ public void contextPath_generateLink_returnsCorrectPath() { @Test public void multipart_getFileName_returnsCorrectFileName() throws IOException { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/attachment", "POST") - .formFilePart("testFile", "myFile.txt", "hello".getBytes()) - .build(); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/attachment", "POST") + .formFilePart("testFile", "myFile.txt", "hello".getBytes()); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals("testFile", output.getBody()); diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringBootAppTest.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringBootAppTest.java index d453dfe43..2a345b6ff 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringBootAppTest.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringBootAppTest.java @@ -15,10 +15,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.springframework.core.SpringVersion; import javax.ws.rs.core.HttpHeaders; import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; import java.util.Objects; import static com.amazonaws.serverless.proxy.spring.springbootapp.TestController.CUSTOM_HEADER_NAME; @@ -27,11 +31,19 @@ import static org.junit.Assume.assumeFalse; +@RunWith(Parameterized.class) public class SpringBootAppTest { - private LambdaHandler handler = new LambdaHandler(); + private LambdaHandler handler; private MockLambdaContext context = new MockLambdaContext(); private ObjectMapper mapper = new ObjectMapper(); + private String type; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[] { "API_GW", "ALB", "HTTP_API" }); + } + @BeforeClass public static void before() { // We skip the tests if we are running against Spring 5.2.x - SpringBoot 1.5 is deprecated and no longer @@ -40,9 +52,14 @@ public static void before() { assumeFalse(Objects.requireNonNull(SpringVersion.getVersion()).startsWith("5.2")); } + public SpringBootAppTest(String reqType) { + type = reqType; + handler = new LambdaHandler(type); + } + @Test public void testMethod_springSecurity_doesNotThrowException() { - AwsProxyRequest req = new AwsProxyRequestBuilder("/test", "GET").build(); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/test", "GET"); AwsProxyResponse resp = handler.handleRequest(req, context); assertNotNull(resp); assertEquals(200, resp.getStatusCode()); @@ -51,7 +68,7 @@ public void testMethod_springSecurity_doesNotThrowException() { @Test public void testMethod_testRequestFromString_doesNotThrowNpe() throws IOException { - AwsProxyRequest req = new AwsProxyRequestBuilder().fromJsonString("{\n" + + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder().fromJsonString("{\n" + " \"resource\": \"/missing-params\",\n" + " \"path\": \"/missing-params\",\n" + " \"httpMethod\": \"GET\",\n" + @@ -81,25 +98,25 @@ public void testMethod_testRequestFromString_doesNotThrowNpe() throws IOExceptio " },\n" + " \"body\": \"{ \\\"Key1\\\": \\\"Value1\\\", \\\"Key2\\\": \\\"Value2\\\", \\\"Key3\\\": \\\"Vaue3\\\" }\",\n" + " \"isBase64Encoded\": \"false\"\n" + - "}").build(); + "}"); AwsProxyResponse resp = handler.handleRequest(req, context); assertNotNull(resp); // Spring identifies the missing header assertEquals(400, resp.getStatusCode()); - req.setMultiValueHeaders(new Headers()); - req.getMultiValueHeaders().add(CUSTOM_HEADER_NAME, "val"); + req.multiValueHeaders(new Headers()); + req.header(CUSTOM_HEADER_NAME, "val"); resp = handler.handleRequest(req, context); assertEquals(400, resp.getStatusCode()); - req.setMultiValueQueryStringParameters(new MultiValuedTreeMap<>()); - req.getMultiValueQueryStringParameters().add(CUSTOM_QS_NAME, "val"); + req.multiValueQueryString(new MultiValuedTreeMap<>()); + req.queryString(CUSTOM_QS_NAME, "val"); resp = handler.handleRequest(req, context); assertEquals(200, resp.getStatusCode()); } @Test public void defaultError_requestForward_springBootForwardsToDefaultErrorPage() { - AwsProxyRequest req = new AwsProxyRequestBuilder("/test2", "GET").build(); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/test2", "GET"); AwsProxyResponse resp = handler.handleRequest(req, context); assertNotNull(resp); assertEquals(404, resp.getStatusCode()); @@ -108,7 +125,7 @@ public void defaultError_requestForward_springBootForwardsToDefaultErrorPage() { @Test public void requestUri_dotInPathParam_expectRoutingToMethod() { - AwsProxyRequest req = new AwsProxyRequestBuilder("/test/testdomain.com", "GET").build(); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/test/testdomain.com", "GET"); AwsProxyResponse resp = handler.handleRequest(req, context); assertNotNull(resp); @@ -118,8 +135,8 @@ public void requestUri_dotInPathParam_expectRoutingToMethod() { @Test public void queryString_commaSeparatedList_expectUnmarshalAsList() { - AwsProxyRequest req = new AwsProxyRequestBuilder("/test/query-string", "GET") - .queryString("list", "v1,v2,v3").build(); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/test/query-string", "GET") + .queryString("list", "v1,v2,v3"); AwsProxyResponse resp = handler.handleRequest(req, context); assertNotNull(resp); assertEquals(200, resp.getStatusCode()); @@ -128,8 +145,8 @@ public void queryString_commaSeparatedList_expectUnmarshalAsList() { @Test public void queryString_multipleParamsWithSameName_expectUnmarshalAsList() { - AwsProxyRequest req = new AwsProxyRequestBuilder("/test/query-string", "GET") - .queryString("list", "v1").queryString("list", "v2").build(); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/test/query-string", "GET") + .queryString("list", "v1").queryString("list", "v2"); AwsProxyResponse resp = handler.handleRequest(req, context); assertNotNull(resp); assertEquals(200, resp.getStatusCode()); @@ -138,11 +155,10 @@ public void queryString_multipleParamsWithSameName_expectUnmarshalAsList() { @Test public void staticContent_getHtmlFile_returnsHtmlContent() { - LambdaContainerHandler.getContainerConfig().addValidFilePath("/Users/bulianis/workspace/aws-serverless-java-container/aws-serverless-java-container-spring"); - AwsProxyRequest request = new AwsProxyRequestBuilder("/static.html", "GET") + LambdaContainerHandler.getContainerConfig().addValidFilePath(System.getProperty("user.dir")); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/static.html", "GET") .header(HttpHeaders.ACCEPT, "text/html") - .header(HttpHeaders.CONTENT_TYPE, "text/plain") - .build(); + .header(HttpHeaders.CONTENT_TYPE, "text/plain"); AwsProxyResponse output = handler.handleRequest(request, context); assertEquals(200, output.getStatusCode()); assertTrue(output.getBody().contains("

Static

")); @@ -151,8 +167,19 @@ public void staticContent_getHtmlFile_returnsHtmlContent() { @Test public void utf8_returnUtf8String_expectCorrectHeaderMediaAndCharset() { LambdaContainerHandler.getContainerConfig().setDefaultContentCharset("UTF-8"); - AwsProxyRequest request = new AwsProxyRequestBuilder("/test/utf8", "GET") - .build(); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/test/utf8", "GET"); + AwsProxyResponse output = handler.handleRequest(request, context); + validateSingleValueModel(output, TestController.UTF8_TEST_STRING); + assertTrue(output.getMultiValueHeaders().containsKey(HttpHeaders.CONTENT_TYPE)); + assertTrue(output.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE).contains(";")); + assertTrue(output.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE).contains("charset=UTF-8")); + } + + @Test + public void utf8_returnUtf8String_expectCorrectHeaderMediaAndCharsetNoDefault() { + + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/test/utf8", "GET") + .header("Content-Type", "application/json; charset=UTF-8"); AwsProxyResponse output = handler.handleRequest(request, context); validateSingleValueModel(output, TestController.UTF8_TEST_STRING); assertTrue(output.getMultiValueHeaders().containsKey(HttpHeaders.CONTENT_TYPE)); diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoSpringAppConfig.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoSpringAppConfig.java index c32935c70..a4d5f069c 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoSpringAppConfig.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoSpringAppConfig.java @@ -1,65 +1,18 @@ package com.amazonaws.serverless.proxy.spring.echoapp; -import com.amazonaws.serverless.exceptions.ContainerInitializationException; -import com.amazonaws.serverless.proxy.AwsProxyExceptionHandler; -import com.amazonaws.serverless.proxy.AwsProxySecurityContextWriter; -import com.amazonaws.serverless.proxy.InitializationWrapper; -import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequestReader; -import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; -import com.amazonaws.serverless.proxy.internal.servlet.AwsServletRegistration; import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; -import com.amazonaws.serverless.proxy.model.AwsProxyRequest; -import com.amazonaws.serverless.proxy.model.AwsProxyResponse; -import com.amazonaws.serverless.proxy.spring.SpringLambdaContainerHandler; -import com.amazonaws.serverless.proxy.spring.SpringProxyHandlerBuilder; import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.context.annotation.PropertySource; -import org.springframework.web.context.ConfigurableWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; - -import javax.servlet.DispatcherType; -import javax.servlet.FilterRegistration; -import javax.servlet.ServletException; - -import java.util.EnumSet; @Configuration @ComponentScan("com.amazonaws.serverless.proxy.spring.echoapp") @PropertySource("classpath:application.properties") public class EchoSpringAppConfig { - - @Autowired - private ConfigurableWebApplicationContext applicationContext; - - @Bean - public SpringLambdaContainerHandler springLambdaContainerHandler() throws ContainerInitializationException { - SpringLambdaContainerHandler handler = new SpringProxyHandlerBuilder() - .defaultProxy() - .initializationWrapper(new InitializationWrapper()) - .springApplicationContext(applicationContext) - .buildAndInitialize(); - handler.onStartup(c -> { - FilterRegistration.Dynamic registration = c.addFilter("UnauthenticatedFilter", UnauthenticatedFilter.class); - // update the registration to map to a path - registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/echo/*"); - // servlet name mappings are disabled and will throw an exception - - //handler.getApplicationInitializer().getDispatcherServlet().setThrowExceptionIfNoHandlerFound(true); - try { - ((DispatcherServlet)((AwsServletRegistration)c.getServletRegistration("dispatcherServlet")).getServlet()).setThrowExceptionIfNoHandlerFound(true); - } catch (ServletException e) { - e.printStackTrace(); - } - }); - return handler; - } - @Bean public ObjectMapper objectMapper() { return new ObjectMapper(); diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/springbootapp/LambdaHandler.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/springbootapp/LambdaHandler.java index 4ca9c4b2c..aee0bd035 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/springbootapp/LambdaHandler.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/springbootapp/LambdaHandler.java @@ -2,8 +2,10 @@ import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; import com.amazonaws.serverless.proxy.spring.springbootapp.TestApplication; import com.amazonaws.services.lambda.runtime.Context; @@ -11,24 +13,46 @@ public class LambdaHandler - implements RequestHandler + implements RequestHandler { SpringBootLambdaContainerHandler handler; - boolean isinitialized = false; + SpringBootLambdaContainerHandler httpApiHandler; + private String type; - public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) + public LambdaHandler(String reqType) { + type = reqType; + switch (type) { + case "API_GW": + case "ALB": + try { + handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(TestApplication.class); + } catch (ContainerInitializationException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + break; + case "HTTP_API": + try { + httpApiHandler = SpringBootLambdaContainerHandler.getHttpApiV2ProxyHandler(TestApplication.class); + } catch (ContainerInitializationException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + } + + public AwsProxyResponse handleRequest(AwsProxyRequestBuilder awsProxyRequest, Context context) { - if (!isinitialized) { - isinitialized = true; - try { - handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(TestApplication.class); - } catch (ContainerInitializationException e) { - e.printStackTrace(); - return null; - } + switch (type) { + case "API_GW": + return handler.proxy(awsProxyRequest.build(), context); + case "ALB": + return handler.proxy(awsProxyRequest.alb().build(), context); + case "HTTP_API": + return httpApiHandler.proxy(awsProxyRequest.toHttpApiV2Request(), context); + default: + throw new RuntimeException("Unknown request type: " + type); } - AwsProxyResponse res = handler.proxy(awsProxyRequest, context); - return res; } } diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/springbootslowapp/SBLambdaHandler.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/springbootslowapp/SBLambdaHandler.java index 64a399a22..157b6c438 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/springbootslowapp/SBLambdaHandler.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/springbootslowapp/SBLambdaHandler.java @@ -20,9 +20,9 @@ public class SBLambdaHandler public SBLambdaHandler() throws ContainerInitializationException { long startTime = Instant.now().toEpochMilli(); - handler = new SpringBootProxyHandlerBuilder() + handler = new SpringBootProxyHandlerBuilder() .defaultProxy() - .asyncInit(startTime) + .asyncInit(Instant.now().toEpochMilli()) .springBootApplication(TestApplication.class) .buildAndInitialize(); constructorTime = Instant.now().toEpochMilli() - startTime; diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/springslowapp/LambdaHandler.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/springslowapp/LambdaHandler.java index b61a7191f..935954d0f 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/springslowapp/LambdaHandler.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/springslowapp/LambdaHandler.java @@ -16,9 +16,9 @@ public class LambdaHandler implements RequestHandler() .defaultProxy() - .asyncInit(startTime) + .asyncInit() .configurationClasses(SlowAppConfig.class) .buildAndInitialize(); constructorTime = Instant.now().toEpochMilli() - startTime; diff --git a/aws-serverless-java-container-springboot2/pom.xml b/aws-serverless-java-container-springboot2/pom.xml index 90c98f009..10d995fee 100644 --- a/aws-serverless-java-container-springboot2/pom.xml +++ b/aws-serverless-java-container-springboot2/pom.xml @@ -1,5 +1,6 @@ - + aws-serverless-java-container com.amazonaws.serverless @@ -15,10 +16,9 @@ 1.5-SNAPSHOT - 5.1.9.RELEASE - 5.1.6.RELEASE - 2.1.8.RELEASE - + 5.2.5.RELEASE + 2.2.6.RELEASE + 5.2.2.RELEASE 1.8 1.8 @@ -42,6 +42,16 @@ spring-boot ${springboot.version} true + + + org.springframework + spring-context + + + org.springframework + spring-core + + org.springframework.boot @@ -49,16 +59,105 @@ ${springboot.version} true + + org.springframework + spring-core + ${spring.version} + true + + + org.springframework + spring-context + ${spring.version} + true + + + org.springframework + spring-webmvc + ${spring.version} + true + + + org.springframework + spring-aop + + + org.springframework + spring-expression + + + + org.springframework.security spring-security-config ${springsecurity.version} + + + org.springframework + spring-context + + + org.springframework + spring-beans + + + org.springframework + spring-core + + + org.springframework + spring-expression + + + org.springframework + spring-aop + + test org.springframework.security spring-security-web ${springsecurity.version} + + + org.springframework + spring-core + + + org.springframework + spring-web + + + org.springframework + spring-beans + + + org.springframework + spring-context + + + org.springframework + spring-expression + + + org.springframework + spring-aop + + + test + + + javax.validation + validation-api + 2.0.1.Final + test + + + org.hibernate + hibernate-validator + 6.1.2.Final test @@ -67,6 +166,13 @@ 17.0.0 compile + + + javax.activation + activation + 1.1.1 + test + @@ -101,16 +207,18 @@ true - - BUNDLE - - - INSTRUCTION - COVEREDRATIO - ${jacoco.minCoverage} - - - + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + ${jacoco.minCoverage} + + + + diff --git a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java index 5b56e58b1..0eaf58f97 100644 --- a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java +++ b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java @@ -14,20 +14,30 @@ import com.amazonaws.serverless.exceptions.ContainerInitializationException; import com.amazonaws.serverless.proxy.*; +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.internal.servlet.*; import com.amazonaws.serverless.proxy.internal.testutils.Timer; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.serverless.proxy.spring.embedded.ServerlessReactiveServletEmbeddedServerFactory; import com.amazonaws.serverless.proxy.spring.embedded.ServerlessServletEmbeddedServerFactory; import com.amazonaws.services.lambda.runtime.Context; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.StandardEnvironment; +import org.springframework.web.context.ConfigurableWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; import javax.servlet.Servlet; +import javax.servlet.ServletRegistration; +import javax.servlet.http.HttpServletRequest; import java.util.concurrent.CountDownLatch; /** @@ -40,10 +50,14 @@ * @param The incoming event type * @param The expected return type */ -public class SpringBootLambdaContainerHandler extends AwsLambdaServletContainerHandler { +public class SpringBootLambdaContainerHandler extends AwsLambdaServletContainerHandler { + private static final String DISPATCHER_SERVLET_REGISTRATION_NAME = "dispatcherServlet"; + private final Class springBootInitializer; private static final Logger log = LoggerFactory.getLogger(SpringBootLambdaContainerHandler.class); private String[] springProfiles = null; + private WebApplicationType springWebApplicationType; + private ConfigurableApplicationContext applicationContext; private static SpringBootLambdaContainerHandler instance; @@ -71,7 +85,7 @@ public static SpringBootLambdaContainerHandler getInstance() { */ public static SpringBootLambdaContainerHandler getAwsProxyHandler(Class springBootInitializer, String... profiles) throws ContainerInitializationException { - return new SpringBootProxyHandlerBuilder() + return new SpringBootProxyHandlerBuilder() .defaultProxy() .initializationWrapper(new InitializationWrapper()) .springBootApplication(springBootInitializer) @@ -79,6 +93,23 @@ public static SpringBootLambdaContainerHandler getHttpApiV2ProxyHandler(Class springBootInitializer, String... profiles) + throws ContainerInitializationException { + return new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(springBootInitializer) + .profiles(profiles) + .buildAndInitialize(); + } + /** * Creates a new container handler with the given reader and writer objects * @@ -90,19 +121,22 @@ public static SpringBootLambdaContainerHandler requestTypeClass, Class responseTypeClass, - RequestReader requestReader, + RequestReader requestReader, ResponseWriter responseWriter, SecurityContextWriter securityContextWriter, ExceptionHandler exceptionHandler, Class springBootInitializer, - InitializationWrapper init) { + InitializationWrapper init, + WebApplicationType applicationType) { super(requestTypeClass, responseTypeClass, requestReader, responseWriter, securityContextWriter, exceptionHandler); Timer.start("SPRINGBOOT2_CONTAINER_HANDLER_CONSTRUCTOR"); initialized = false; this.springBootInitializer = springBootInitializer; + springWebApplicationType = applicationType; setInitializationWrapper(init); SpringBootLambdaContainerHandler.setInstance(this); @@ -123,12 +157,12 @@ public void activateSpringProfiles(String... profiles) { } @Override - protected AwsHttpServletResponse getContainerResponse(AwsProxyHttpServletRequest request, CountDownLatch latch) { + protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request, CountDownLatch latch) { return new AwsHttpServletResponse(request, latch); } @Override - protected void handleRequest(AwsProxyHttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { + protected void handleRequest(HttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { // this method of the AwsLambdaServletContainerHandler sets the servlet context Timer.start("SPRINGBOOT2_HANDLE_REQUEST"); @@ -139,7 +173,10 @@ protected void handleRequest(AwsProxyHttpServletRequest containerRequest, AwsHtt // process filters & invoke servlet Servlet reqServlet = ((AwsServletContext)getServletContext()).getServletForPath(containerRequest.getPathInfo()); - containerRequest.setResponse(containerResponse); + if (AwsHttpServletRequest.class.isAssignableFrom(containerRequest.getClass())) { + ((AwsHttpServletRequest)containerRequest).setServletContext(getServletContext()); + ((AwsHttpServletRequest)containerRequest).setResponse(containerResponse); + } doFilter(containerRequest, containerResponse, reqServlet); Timer.stop("SPRINGBOOT2_HANDLE_REQUEST"); } @@ -150,27 +187,37 @@ public void initialize() throws ContainerInitializationException { Timer.start("SPRINGBOOT2_COLD_START"); - SpringApplication app = new SpringApplication(getEmbeddedContainerClasses()); - if (springProfiles != null && springProfiles.length > 0) { - ConfigurableEnvironment springEnv = new StandardEnvironment(); - springEnv.setActiveProfiles(springProfiles); - app.setEnvironment(springEnv); + SpringApplicationBuilder builder = new SpringApplicationBuilder(getEmbeddedContainerClasses()) + .web(springWebApplicationType); // .REACTIVE, .SERVLET + if (springProfiles != null) { + builder.profiles(springProfiles); } - - app.run(); - + applicationContext = builder.run(); + if (springWebApplicationType == WebApplicationType.SERVLET) { + ((AnnotationConfigServletWebServerApplicationContext)applicationContext).setServletContext(getServletContext()); + AwsServletRegistration reg = (AwsServletRegistration)getServletContext().getServletRegistration(DISPATCHER_SERVLET_REGISTRATION_NAME); + if (reg != null) { + reg.setLoadOnStartup(1); + } + } + super.initialize(); initialized = true; Timer.stop("SPRINGBOOT2_COLD_START"); } private Class[] getEmbeddedContainerClasses() { Class[] classes = new Class[2]; - try { - // if HandlerAdapter is available we assume they are using WebFlux. Otherwise plain servlet. - this.getClass().getClassLoader().loadClass("org.springframework.web.reactive.HandlerAdapter"); - log.debug("Found WebFlux HandlerAdapter on classpath, using reactive server factory"); - classes[0] = ServerlessReactiveServletEmbeddedServerFactory.class; - } catch (ClassNotFoundException e) { + if (springWebApplicationType == WebApplicationType.REACTIVE) { + try { + // if HandlerAdapter is available we assume they are using WebFlux. Otherwise plain servlet. + this.getClass().getClassLoader().loadClass("org.springframework.web.reactive.HandlerAdapter"); + log.debug("Found WebFlux HandlerAdapter on classpath, using reactive server factory"); + classes[0] = ServerlessReactiveServletEmbeddedServerFactory.class; + } catch (ClassNotFoundException e) { + springWebApplicationType = WebApplicationType.SERVLET; + classes[0] = ServerlessServletEmbeddedServerFactory.class; + } + } else { classes[0] = ServerlessServletEmbeddedServerFactory.class; } diff --git a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java index 943cc69ff..b9894834c 100644 --- a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java +++ b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.spring; import com.amazonaws.serverless.exceptions.ContainerInitializationException; @@ -5,39 +17,48 @@ import com.amazonaws.serverless.proxy.internal.servlet.ServletLambdaContainerHandlerBuilder; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import org.springframework.boot.WebApplicationType; -public final class SpringBootProxyHandlerBuilder extends ServletLambdaContainerHandlerBuilder< - AwsProxyRequest, +import javax.servlet.http.HttpServletRequest; + +public final class SpringBootProxyHandlerBuilder extends ServletLambdaContainerHandlerBuilder< + RequestType, AwsProxyResponse, - AwsProxyHttpServletRequest, - SpringBootLambdaContainerHandler, - SpringBootProxyHandlerBuilder> { + HttpServletRequest, + SpringBootLambdaContainerHandler, + SpringBootProxyHandlerBuilder> { private Class springBootInitializer; private String[] profiles; + private WebApplicationType applicationType = WebApplicationType.REACTIVE; @Override - protected SpringBootProxyHandlerBuilder self() { + protected SpringBootProxyHandlerBuilder self() { return this; } - public SpringBootProxyHandlerBuilder springBootApplication(Class app) { + public SpringBootProxyHandlerBuilder springBootApplication(Class app) { springBootInitializer = app; return self(); } - public SpringBootProxyHandlerBuilder profiles(String... profiles) { + public SpringBootProxyHandlerBuilder profiles(String... profiles) { this.profiles = profiles; return self(); } + public SpringBootProxyHandlerBuilder servletApplication() { + this.applicationType = WebApplicationType.SERVLET; + return self(); + } + @Override - public SpringBootLambdaContainerHandler build() throws ContainerInitializationException { + public SpringBootLambdaContainerHandler build() throws ContainerInitializationException { validate(); if (springBootInitializer == null) { throw new ContainerInitializationException("Missing spring boot application class in builder", null); } - SpringBootLambdaContainerHandler handler = new SpringBootLambdaContainerHandler<>( + SpringBootLambdaContainerHandler handler = new SpringBootLambdaContainerHandler( requestTypeClass, responseTypeClass, requestReader, @@ -45,7 +66,8 @@ public SpringBootLambdaContainerHandler build securityContextWriter, exceptionHandler, springBootInitializer, - initializationWrapper + initializationWrapper, + applicationType ); if (profiles != null) { handler.activateSpringProfiles(profiles); @@ -54,8 +76,8 @@ public SpringBootLambdaContainerHandler build } @Override - public SpringBootLambdaContainerHandler buildAndInitialize() throws ContainerInitializationException { - SpringBootLambdaContainerHandler handler = build(); + public SpringBootLambdaContainerHandler buildAndInitialize() throws ContainerInitializationException { + SpringBootLambdaContainerHandler handler = build(); initializationWrapper.start(handler); return handler; } diff --git a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessReactiveServletEmbeddedServerFactory.java b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessReactiveServletEmbeddedServerFactory.java index 7fa366dd9..9b2c04948 100644 --- a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessReactiveServletEmbeddedServerFactory.java +++ b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessReactiveServletEmbeddedServerFactory.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.spring.embedded; import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; diff --git a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java index b47b081c7..ad228f7d4 100644 --- a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java +++ b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java @@ -1,6 +1,17 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.spring.embedded; -import com.amazonaws.serverless.exceptions.ContainerInitializationException; import com.amazonaws.serverless.proxy.internal.servlet.AwsLambdaServletContainerHandler; import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; import org.springframework.boot.autoconfigure.AutoConfigureOrder; diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/SecurityAppTest.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/SecurityAppTest.java index 34a593fd2..e9682d4c4 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/SecurityAppTest.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/SecurityAppTest.java @@ -1,14 +1,11 @@ package com.amazonaws.serverless.proxy.spring; -import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.spring.securityapp.LambdaHandler; -import com.amazonaws.serverless.proxy.spring.securityapp.MessageController; import com.amazonaws.serverless.proxy.spring.securityapp.SecurityConfig; -import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.Test; import javax.ws.rs.core.HttpHeaders; @@ -39,8 +36,4 @@ public void helloRequest_withAuth_respondsWithSingleMessage() { resp = handler.handleRequest(req, lambdaContext); assertEquals(200, resp.getStatusCode()); } - - public void helloRequest_withoutAuith_respondsWithError() { - - } } diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java index 4d5797e5f..88cf735dc 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java @@ -1,25 +1,111 @@ package com.amazonaws.serverless.proxy.spring; +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; -import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.spring.servletapp.LambdaHandler; import com.amazonaws.serverless.proxy.spring.servletapp.MessageController; +import com.amazonaws.serverless.proxy.spring.servletapp.MessageData; +import com.amazonaws.serverless.proxy.spring.servletapp.UserData; +import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.Assert; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; + +import java.util.Arrays; +import java.util.Collection; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +@RunWith(Parameterized.class) public class ServletAppTest { - LambdaHandler handler = new LambdaHandler(); + LambdaHandler handler; MockLambdaContext lambdaContext = new MockLambdaContext(); + private String type; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[] { "API_GW", "ALB", "HTTP_API" }); + } + + public ServletAppTest(String reqType) { + type = reqType; + handler = new LambdaHandler(type); + } + @Test public void helloRequest_respondsWithSingleMessage() { - AwsProxyRequest req = new AwsProxyRequestBuilder("/hello", "GET").build(); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/hello", "GET"); AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); Assert.assertEquals(MessageController.HELLO_MESSAGE, resp.getBody()); } + + @Test + public void validateRequest_invalidData_respondsWith400() { + UserData ud = new UserData(); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/validate", "POST") + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .body(ud); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + try { + System.out.println(LambdaContainerHandler.getObjectMapper().writeValueAsString(resp)); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + assertEquals("3", resp.getBody()); + assertEquals(400, resp.getStatusCode()); + + UserData ud2 = new UserData(); + ud2.setFirstName("Test"); + ud2.setLastName("Test"); + ud2.setEmail("Test"); + req = new AwsProxyRequestBuilder("/validate", "POST") + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .body(ud2); + resp = handler.handleRequest(req, lambdaContext); + assertEquals("1", resp.getBody()); + assertEquals(400, resp.getStatusCode()); + } + + @Test + public void messageObject_parsesObject_returnsCorrectMessage() { + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/message", "POST") + .json() + .body(new MessageData("test message")); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("test message", resp.getBody()); + } + + @Test + public void messageObject_propertiesInContentType_returnsCorrectMessage() { + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/message", "POST") + .header(HttpHeaders.CONTENT_TYPE, "application/json;v=1") + .header(HttpHeaders.ACCEPT, "application/json;v=1") + .body(new MessageData("test message")); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("test message", resp.getBody()); + } + + @Test + public void echoMessage_fileNameLikeParameter_returnsMessage() { + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/echo/test.test.test", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("test.test.test", resp.getBody()); + } } diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java index 7148e3b67..811a1ab55 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java @@ -1,33 +1,67 @@ package com.amazonaws.serverless.proxy.spring; +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.spring.webfluxapp.LambdaHandler; import com.amazonaws.serverless.proxy.spring.webfluxapp.MessageController; +import com.amazonaws.serverless.proxy.spring.webfluxapp.MessageData; +import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.Assert; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.Collection; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +@RunWith(Parameterized.class) public class WebFluxAppTest { - LambdaHandler handler = new LambdaHandler(); + LambdaHandler handler; MockLambdaContext lambdaContext = new MockLambdaContext(); + private String type; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[] { "API_GW", "ALB", "HTTP_API" }); + } + + public WebFluxAppTest(String reqType) { + type = reqType; + handler = new LambdaHandler(type); + } + @Test public void helloRequest_respondsWithSingleMessage() { - AwsProxyRequest req = new AwsProxyRequestBuilder("/single", "GET").build(); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/single", "GET"); AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + System.out.println(resp.getBody()); Assert.assertEquals(MessageController.MESSAGE, resp.getBody()); } @Test public void helloDoubleRequest_respondsWithDoubleMessage() { - AwsProxyRequest req = new AwsProxyRequestBuilder("/double", "GET").build(); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/double", "GET"); AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); assertEquals(MessageController.MESSAGE + MessageController.MESSAGE, resp.getBody()); } + + @Test + public void messageObject_parsesObject_returnsCorrectMessage() throws JsonProcessingException { + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/message", "POST") + .json() + .body(new MessageData("test message")); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("test message", resp.getBody()); + } } diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactoryTest.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactoryTest.java index 010bcbdcf..11779420c 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactoryTest.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactoryTest.java @@ -10,6 +10,7 @@ import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; import org.junit.Test; +import org.springframework.boot.WebApplicationType; import org.springframework.boot.web.servlet.ServletContextInitializer; import javax.servlet.ServletContext; @@ -26,7 +27,8 @@ public class ServerlessServletEmbeddedServerFactoryTest { new AwsProxySecurityContextWriter(), new AwsProxyExceptionHandler(), null, - new InitializationWrapper() + new InitializationWrapper(), + WebApplicationType.REACTIVE ); public ServerlessServletEmbeddedServerFactoryTest() throws ContainerInitializationException { diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/LambdaHandler.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/LambdaHandler.java index 75dc158ab..ae8ba21ac 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/LambdaHandler.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/LambdaHandler.java @@ -1,7 +1,6 @@ package com.amazonaws.serverless.proxy.spring.securityapp; import com.amazonaws.serverless.exceptions.ContainerInitializationException; -import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; @@ -13,7 +12,7 @@ public class LambdaHandler implements RequestHandler hello() { + return Mono.just(HELLO_MESSAGE); } } diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityApplication.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityApplication.java new file mode 100644 index 000000000..cafcd4000 --- /dev/null +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityApplication.java @@ -0,0 +1,14 @@ +package com.amazonaws.serverless.proxy.spring.securityapp; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.web.reactive.config.EnableWebFlux; + +@SpringBootApplication +@EnableWebFluxSecurity +@EnableWebFlux +@Import(SecurityConfig.class) +public class SecurityApplication { +} diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityConfig.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityConfig.java index 128ae3dd8..d83b81db6 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityConfig.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityConfig.java @@ -1,7 +1,5 @@ package com.amazonaws.serverless.proxy.spring.securityapp; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; @@ -10,52 +8,37 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.server.SecurityWebFilterChain; @Configuration @EnableWebFluxSecurity -public class SecurityConfig { - public static final String USERNAME = "user"; - public static String PASSWORD = "testPassword"; - public static BCryptPasswordEncoder pEncoder = new BCryptPasswordEncoder(); +public class SecurityConfig +{ + public static final String USERNAME = "admin"; + public static final String PASSWORD = "{noop}password"; + private static BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); @Bean public SecurityWebFilterChain securitygWebFilterChain( ServerHttpSecurity http) { return http.authorizeExchange() - .anyExchange().authenticated() - .and().httpBasic() + .anyExchange().authenticated().and().csrf().disable() + .httpBasic() .and().build(); } @Bean - public MapReactiveUserDetailsService reactiveUserDetailsService(SecurityProperties properties, ObjectProvider passwordEncoder) { - return new MapReactiveUserDetailsService(getUser()); - } - - private UserDetails getUser() { - return User.builder().username(USERNAME).password(passwordEncoder().encode(PASSWORD)).authorities("USER").build(); + public static BCryptPasswordEncoder passwordEncoder() { + return passwordEncoder; } @Bean - public PasswordEncoder passwordEncoder() { - return pEncoder; + public MapReactiveUserDetailsService userDetailsService() { + UserDetails user = User + .withUsername(USERNAME) + .password(passwordEncoder.encode(PASSWORD)) + .roles("USER") + .build(); + return new MapReactiveUserDetailsService(user); } - - /*@Autowired - public void configureGlobal(AuthenticationManagerBuilder authentication) - throws Exception - { - authentication.inMemoryAuthentication() - .withUser(USERNAME) - .password(passwordEncoder().encode(PASSWORD)) - .authorities("ROLE_USER"); - if (userService != null) { - if (userService.loadUserByUsername("user") != null) { - System.out.println("Setting password in configureGlobal"); - PASSWORD = userService.loadUserByUsername("user").getPassword().replace("{noop}", ""); - } - } - }*/ -} +} \ No newline at end of file diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/ServletApplication.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/ServletApplication.java deleted file mode 100644 index 2f1def12a..000000000 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/ServletApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.amazonaws.serverless.proxy.spring.securityapp; - -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; -import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; - -@SpringBootApplication() -public class ServletApplication { -} diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java index 7faf9b1b8..88441988e 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java @@ -1,26 +1,60 @@ package com.amazonaws.serverless.proxy.spring.servletapp; import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.InitializationWrapper; import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -public class LambdaHandler implements RequestHandler { +public class LambdaHandler implements RequestHandler { private static SpringBootLambdaContainerHandler handler; + private static SpringBootLambdaContainerHandler httpApiHandler; + private String type; - static { + public LambdaHandler(String reqType) { + type = reqType; try { - handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(ServletApplication.class); + switch (type) { + case "API_GW": + case "ALB": + handler = new SpringBootProxyHandlerBuilder() + .defaultProxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(ServletApplication.class) + .buildAndInitialize(); + break; + case "HTTP_API": + httpApiHandler = new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(ServletApplication.class) + .buildAndInitialize(); + break; + } } catch (ContainerInitializationException e) { e.printStackTrace(); } } @Override - public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) { - return handler.proxy(awsProxyRequest, context); + public AwsProxyResponse handleRequest(AwsProxyRequestBuilder awsProxyRequest, Context context) { + switch (type) { + case "API_GW": + return handler.proxy(awsProxyRequest.build(), context); + case "ALB": + return handler.proxy(awsProxyRequest.alb().build(), context); + case "HTTP_API": + return httpApiHandler.proxy(awsProxyRequest.toHttpApiV2Request(), context); + default: + throw new RuntimeException("Unknown request type: " + type); + } } } diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageController.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageController.java index 937b2432b..d46806b61 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageController.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageController.java @@ -1,15 +1,39 @@ package com.amazonaws.serverless.proxy.spring.servletapp; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; @RestController public class MessageController { public static final String HELLO_MESSAGE = "Hello"; + public static final String VALID_MESSAGE = "VALID"; - @RequestMapping(path="/hello", method= RequestMethod.GET) + @RequestMapping(path="/hello", method=RequestMethod.GET, produces = {"text/plain"}) public String hello() { return HELLO_MESSAGE; } + + @RequestMapping(path="/validate", method=RequestMethod.POST, produces = {"text/plain"}) + public ResponseEntity validateBody(@RequestBody @Valid UserData userData, Errors errors) { + if (errors != null && errors.hasErrors()) { + return ResponseEntity.badRequest().body(errors.getErrorCount() + ""); + } + return ResponseEntity.ok(VALID_MESSAGE); + } + + @RequestMapping(path="/message", method = RequestMethod.POST) + public String returnMessage(@RequestBody MessageData data) { + if (data == null) { + throw new RuntimeException("No message data"); + } + return data.getMessage(); + } + + @RequestMapping(path="/echo/{message}", method=RequestMethod.GET) + public String returnPathMessage(@PathVariable(value="message") String message) { + return message; + } } diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageData.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageData.java new file mode 100644 index 000000000..129101cbe --- /dev/null +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageData.java @@ -0,0 +1,20 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + +public class MessageData { + private String message; + + public MessageData() { + } + + public MessageData(String m) { + setMessage(m); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java index 927dd6ce5..07ddbab43 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java @@ -2,6 +2,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; @SpringBootApplication(exclude = { org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration.class, @@ -9,5 +11,6 @@ org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration.class, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class }) -public class ServletApplication extends SpringBootServletInitializer { +@Import(MessageController.class) +public class ServletApplication { } diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/UserData.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/UserData.java new file mode 100644 index 000000000..e180c1738 --- /dev/null +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/UserData.java @@ -0,0 +1,50 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +public class UserData { + @NotBlank + private String firstName; + @NotBlank + private String lastName; + @NotNull @Email + private String email; + private String error; + + public UserData() { + + } + + public UserData(String err) { + error = err; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getError() { return error; } +} diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/LambdaHandler.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/LambdaHandler.java index bd696a990..ec6993a7d 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/LambdaHandler.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/LambdaHandler.java @@ -19,9 +19,9 @@ public LambdaHandler() { try { long startTime = Instant.now().toEpochMilli(); System.out.println("startCall: " + startTime); - handler = new SpringBootProxyHandlerBuilder() + handler = new SpringBootProxyHandlerBuilder() .defaultProxy() - .asyncInit(startTime) + .asyncInit() .springBootApplication(SlowTestApplication.class) .buildAndInitialize(); constructorTime = Instant.now().toEpochMilli() - startTime; diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java index 913da5bbc..0eb52a7bc 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java @@ -1,26 +1,58 @@ package com.amazonaws.serverless.proxy.spring.webfluxapp; import com.amazonaws.serverless.exceptions.ContainerInitializationException; -import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; +import com.amazonaws.serverless.proxy.InitializationWrapper; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -public class LambdaHandler implements RequestHandler { +public class LambdaHandler implements RequestHandler { private static SpringBootLambdaContainerHandler handler; + private static SpringBootLambdaContainerHandler httpApiHandler; - static { + private String type; + + public LambdaHandler(String reqType) { + type = reqType; try { - handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(WebFluxTestApplication.class); + switch (type) { + case "API_GW": + case "ALB": + handler = new SpringBootProxyHandlerBuilder() + .defaultProxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(WebFluxTestApplication.class) + .buildAndInitialize(); + break; + case "HTTP_API": + httpApiHandler = new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(WebFluxTestApplication.class) + .buildAndInitialize(); + break; + } } catch (ContainerInitializationException e) { e.printStackTrace(); } } @Override - public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) { - return handler.proxy(awsProxyRequest, context); + public AwsProxyResponse handleRequest(AwsProxyRequestBuilder awsProxyRequest, Context context) { + switch (type) { + case "API_GW": + return handler.proxy(awsProxyRequest.build(), context); + case "ALB": + return handler.proxy(awsProxyRequest.alb().build(), context); + case "HTTP_API": + return httpApiHandler.proxy(awsProxyRequest.toHttpApiV2Request(), context); + default: + throw new RuntimeException("Unknown request type: " + type); + } } } diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java index 65ce07862..a04604618 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java @@ -1,26 +1,36 @@ package com.amazonaws.serverless.proxy.spring.webfluxapp; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; import reactor.core.publisher.Flux; @RestController public class MessageController { public static final String MESSAGE = "Hello"; - @RequestMapping(path="/single", method= RequestMethod.GET) + @RequestMapping(path="/single", method= RequestMethod.GET, produces = {"text/plain"}) Flux singleMessage(){ return Flux.just( MESSAGE ); } - @RequestMapping(path="/double", method= RequestMethod.GET) + @RequestMapping(path="/double", method= RequestMethod.GET, produces={"text/plain"}) Flux doubleMessage(){ return Flux.just( MESSAGE, MESSAGE ); } + + @RequestMapping(path="/message", method = RequestMethod.POST, produces={"text/plain"}, consumes = {"application/json"}) + public Flux returnMessage(@RequestBody MessageData data) { + if (data == null) { + throw new RuntimeException("No message data"); + } + return Flux.just(data.getMessage()); + } } \ No newline at end of file diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageData.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageData.java new file mode 100644 index 000000000..2be6b4f2d --- /dev/null +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageData.java @@ -0,0 +1,20 @@ +package com.amazonaws.serverless.proxy.spring.webfluxapp; + +public class MessageData { + private String message; + + public MessageData() { + } + + public MessageData(String m) { + setMessage(m); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/WebFluxTestApplication.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/WebFluxTestApplication.java index 04d48ff3d..70e0c9934 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/WebFluxTestApplication.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/WebFluxTestApplication.java @@ -1,12 +1,20 @@ package com.amazonaws.serverless.proxy.spring.webfluxapp; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; @SpringBootApplication(exclude = { org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration.class, - org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration.class + org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration.class, + org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration.class, + org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class }) public class WebFluxTestApplication { + } diff --git a/aws-serverless-java-container-struts2/src/main/java/com/amazonaws/serverless/proxy/struts2/Struts2LambdaContainerHandler.java b/aws-serverless-java-container-struts2/src/main/java/com/amazonaws/serverless/proxy/struts2/Struts2LambdaContainerHandler.java index 25a2f7f96..3a536c9b2 100644 --- a/aws-serverless-java-container-struts2/src/main/java/com/amazonaws/serverless/proxy/struts2/Struts2LambdaContainerHandler.java +++ b/aws-serverless-java-container-struts2/src/main/java/com/amazonaws/serverless/proxy/struts2/Struts2LambdaContainerHandler.java @@ -1,20 +1,24 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.struts2; import com.amazonaws.serverless.exceptions.ContainerInitializationException; -import com.amazonaws.serverless.proxy.AwsProxyExceptionHandler; -import com.amazonaws.serverless.proxy.AwsProxySecurityContextWriter; -import com.amazonaws.serverless.proxy.ExceptionHandler; -import com.amazonaws.serverless.proxy.RequestReader; -import com.amazonaws.serverless.proxy.ResponseWriter; -import com.amazonaws.serverless.proxy.SecurityContextWriter; -import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse; -import com.amazonaws.serverless.proxy.internal.servlet.AwsLambdaServletContainerHandler; -import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; -import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequestReader; -import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.serverless.proxy.*; +import com.amazonaws.serverless.proxy.internal.servlet.*; import com.amazonaws.serverless.proxy.internal.testutils.Timer; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.services.lambda.runtime.Context; import org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter; import org.slf4j.Logger; @@ -23,6 +27,7 @@ import javax.servlet.DispatcherType; import javax.servlet.FilterRegistration; import javax.servlet.Servlet; +import javax.servlet.http.HttpServletRequest; import java.util.EnumSet; import java.util.concurrent.CountDownLatch; @@ -32,7 +37,7 @@ * @param request type * @param response type */ -public class Struts2LambdaContainerHandler extends AwsLambdaServletContainerHandler { +public class Struts2LambdaContainerHandler extends AwsLambdaServletContainerHandler { private static final Logger log = LoggerFactory.getLogger(Struts2LambdaContainerHandler.class); @@ -55,9 +60,19 @@ public static Struts2LambdaContainerHandler g new AwsProxyExceptionHandler()); } + public static Struts2LambdaContainerHandler getHttpApiV2ProxyHandler() { + return new Struts2LambdaContainerHandler( + HttpApiV2ProxyRequest.class, + AwsProxyResponse.class, + new AwsHttpApiV2HttpServletRequestReader(), + new AwsProxyHttpServletResponseWriter(), + new AwsHttpApiV2SecurityContextWriter(), + new AwsProxyExceptionHandler()); + } + public Struts2LambdaContainerHandler(Class requestTypeClass, Class responseTypeClass, - RequestReader requestReader, + RequestReader requestReader, ResponseWriter responseWriter, SecurityContextWriter securityContextWriter, ExceptionHandler exceptionHandler) { @@ -68,12 +83,13 @@ public Struts2LambdaContainerHandler(Class requestTypeClass, Timer.stop(TIMER_STRUTS_2_CONTAINER_CONSTRUCTOR); } - protected AwsHttpServletResponse getContainerResponse(AwsProxyHttpServletRequest request, CountDownLatch latch) { + @Override + protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request, CountDownLatch latch) { return new AwsHttpServletResponse(request, latch); } @Override - protected void handleRequest(AwsProxyHttpServletRequest httpServletRequest, + protected void handleRequest(HttpServletRequest httpServletRequest, AwsHttpServletResponse httpServletResponse, Context lambdaContext) throws Exception { Timer.start(TIMER_STRUTS_2_HANDLE_REQUEST); @@ -81,7 +97,9 @@ protected void handleRequest(AwsProxyHttpServletRequest httpServletRequest, initialize(); } - httpServletRequest.setServletContext(this.getServletContext()); + if (AwsHttpServletRequest.class.isAssignableFrom(httpServletRequest.getClass())) { + ((AwsHttpServletRequest)httpServletRequest).setServletContext(this.getServletContext()); + } this.doFilter(httpServletRequest, httpServletResponse, null); String responseStatusCode = httpServletResponse.getHeader(HEADER_STRUTS_STATUS_CODE); if (responseStatusCode != null) { diff --git a/aws-serverless-java-container-struts2/src/main/java/com/amazonaws/serverless/proxy/struts2/Struts2LambdaHandler.java b/aws-serverless-java-container-struts2/src/main/java/com/amazonaws/serverless/proxy/struts2/Struts2LambdaHandler.java index 1e63daf99..26a01bd90 100644 --- a/aws-serverless-java-container-struts2/src/main/java/com/amazonaws/serverless/proxy/struts2/Struts2LambdaHandler.java +++ b/aws-serverless-java-container-struts2/src/main/java/com/amazonaws/serverless/proxy/struts2/Struts2LambdaHandler.java @@ -1,3 +1,15 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.struts2; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; diff --git a/aws-serverless-java-container-struts2/src/test/java/com/amazonaws/serverless/proxy/struts2/Struts2AwsProxyTest.java b/aws-serverless-java-container-struts2/src/test/java/com/amazonaws/serverless/proxy/struts2/Struts2AwsProxyTest.java index 45e14ccdb..447b907bc 100644 --- a/aws-serverless-java-container-struts2/src/test/java/com/amazonaws/serverless/proxy/struts2/Struts2AwsProxyTest.java +++ b/aws-serverless-java-container-struts2/src/test/java/com/amazonaws/serverless/proxy/struts2/Struts2AwsProxyTest.java @@ -17,6 +17,7 @@ import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.serverless.proxy.struts2.echoapp.EchoAction; import com.amazonaws.services.lambda.runtime.Context; import com.fasterxml.jackson.core.JsonProcessingException; @@ -25,23 +26,25 @@ import org.apache.commons.codec.binary.Base64; import org.apache.struts2.StrutsJUnit4TestCase; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import javax.ws.rs.core.Response; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; +import java.util.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; /** * Unit test class for the Struts2 AWS_PROXY default implementation */ +@RunWith(Parameterized.class) public class Struts2AwsProxyTest extends StrutsJUnit4TestCase { private static final String CUSTOM_HEADER_KEY = "x-custom-header"; private static final String CUSTOM_HEADER_VALUE = "my-custom-value"; @@ -55,17 +58,42 @@ public class Struts2AwsProxyTest extends StrutsJUnit4TestCase { private static ObjectMapper objectMapper = new ObjectMapper(); private final Struts2LambdaContainerHandler handler = Struts2LambdaContainerHandler .getAwsProxyHandler(); + private final Struts2LambdaContainerHandler httpApiHandler = Struts2LambdaContainerHandler + .getHttpApiV2ProxyHandler(); private static Context lambdaContext = new MockLambdaContext(); + private String type; + + public Struts2AwsProxyTest(String reqType) { + type = reqType; + } + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[] { "API_GW", "ALB", "HTTP_API" }); + } + + private AwsProxyResponse executeRequest(AwsProxyRequestBuilder requestBuilder, Context lambdaContext) { + switch (type) { + case "API_GW": + return handler.proxy(requestBuilder.build(), lambdaContext); + case "ALB": + return handler.proxy(requestBuilder.alb().build(), lambdaContext); + case "HTTP_API": + return httpApiHandler.proxy(requestBuilder.toHttpApiV2Request(), lambdaContext); + default: + throw new RuntimeException("Unknown request type: " + type); + } + } + @Test public void headers_getHeaders_echo() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo-request-info", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo-request-info", "GET") .queryString("mode", "headers") .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) - .json() - .build(); + .json(); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals(CONTENT_TYPE_APPLICATION_JSON, output.getMultiValueHeaders().getFirst("Content-Type")); @@ -74,24 +102,23 @@ public void headers_getHeaders_echo() { @Test public void context_servletResponse_setCustomHeader() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo", "GET") .queryString("customHeader", "true") - .json() - .build(); + .json(); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertTrue(output.getMultiValueHeaders().containsKey("XX")); } @Test public void context_serverInfo_correctContext() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo", "GET") + assumeTrue("API_GW".equals(type)); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo", "GET") .queryString(QUERY_STRING_KEY, "Hello Struts2") .header("Content-Type", "application/json") - .queryString("contentType", "true") - .build(); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + .queryString("contentType", "true"); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals(CONTENT_TYPE_APPLICATION_JSON, output.getMultiValueHeaders().getFirst("Content-Type")); @@ -100,14 +127,13 @@ public void context_serverInfo_correctContext() { @Test public void queryString_uriInfo_echo() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo-request-info", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo-request-info", "GET") .queryString("mode", "query-string") .queryString(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) - .json() - .build(); + .json(); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals(CONTENT_TYPE_APPLICATION_JSON, output.getMultiValueHeaders().getFirst("Content-Type")); @@ -116,13 +142,12 @@ public void queryString_uriInfo_echo() { @Test public void requestScheme_valid_expectHttps() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo-request-info", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo-request-info", "GET") .queryString("mode", "scheme") .queryString(QUERY_STRING_KEY, QUERY_STRING_ENCODED_VALUE) - .json() - .build(); + .json(); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals(CONTENT_TYPE_APPLICATION_JSON, output.getMultiValueHeaders().getFirst("Content-Type")); @@ -131,13 +156,13 @@ public void requestScheme_valid_expectHttps() { @Test public void authorizer_securityContext_customPrincipalSuccess() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo-request-info", "GET") + assumeTrue("API_GW".equals(type)); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo-request-info", "GET") .queryString("mode", "principal") .json() - .authorizerPrincipal(AUTHORIZER_PRINCIPAL_ID) - .build(); + .authorizerPrincipal(AUTHORIZER_PRINCIPAL_ID); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertEquals(CONTENT_TYPE_APPLICATION_JSON, output.getMultiValueHeaders().getFirst("Content-Type")); @@ -146,32 +171,30 @@ public void authorizer_securityContext_customPrincipalSuccess() { @Test public void errors_unknownRoute_expect404() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/unknown", "GET").build(); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/unknown", "GET"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(404, output.getStatusCode()); } @Test public void error_contentType_invalidContentType() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo-request-info", "POST") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo-request-info", "POST") .queryString("mode", "content-type") .header("Content-Type", "application/octet-stream") - .body("asdasdasd") - .build(); + .body("asdasdasd"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(415, output.getStatusCode()); } @Test public void error_statusCode_methodNotAllowed() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo-request-info", "POST") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo-request-info", "POST") .queryString("mode", "not-allowed") - .json() - .build(); + .json(); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(405, output.getStatusCode()); } @@ -180,12 +203,11 @@ public void error_statusCode_methodNotAllowed() { public void responseBody_responseWriter_validBody() throws JsonProcessingException { Map value = new HashMap<>(); value.put(QUERY_STRING_KEY, CUSTOM_HEADER_VALUE); - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo", "POST") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo", "POST") .json() - .body(objectMapper.writeValueAsString(value)) - .build(); + .body(objectMapper.writeValueAsString(value)); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); assertNotNull(output.getBody()); @@ -194,32 +216,30 @@ public void responseBody_responseWriter_validBody() throws JsonProcessingExcepti @Test public void statusCode_responseStatusCode_customStatusCode() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo-request-info", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo-request-info", "GET") .queryString("mode", "custom-status-code") .queryString("status", "201") - .json() - .build(); + .json(); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(201, output.getStatusCode()); } @Test public void base64_binaryResponse_base64Encoding() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo", "GET").build(); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo", "GET"); - AwsProxyResponse response = handler.proxy(request, lambdaContext); + AwsProxyResponse response = executeRequest(request, lambdaContext); assertNotNull(response.getBody()); assertTrue(Base64.isBase64(response.getBody())); } @Test public void exception_mapException_mapToNotImplemented() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo-request-info", "POST") - .queryString("mode", "not-implemented") - .build(); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo-request-info", "POST") + .queryString("mode", "not-implemented"); - AwsProxyResponse response = handler.proxy(request, lambdaContext); + AwsProxyResponse response = executeRequest(request, lambdaContext); assertNotNull(response.getBody()); assertEquals("null", response.getBody()); assertEquals(Response.Status.NOT_IMPLEMENTED.getStatusCode(), response.getStatusCode()); @@ -227,12 +247,11 @@ public void exception_mapException_mapToNotImplemented() { @Test public void stripBasePath_route_shouldRouteCorrectly() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/custompath/echo", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/custompath/echo", "GET") .json() - .queryString(QUERY_STRING_KEY, "stripped") - .build(); + .queryString(QUERY_STRING_KEY, "stripped"); handler.stripBasePath("/custompath"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(200, output.getStatusCode()); validateSingleValueModel(output, "stripped"); handler.stripBasePath(""); @@ -240,29 +259,30 @@ public void stripBasePath_route_shouldRouteCorrectly() { @Test public void stripBasePath_route_shouldReturn404() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/custompath/echo/status-code", "GET") + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/custompath/echo/status-code", "GET") .json() - .queryString("status", "201") - .build(); + .queryString("status", "201"); handler.stripBasePath("/custom"); - AwsProxyResponse output = handler.proxy(request, lambdaContext); + AwsProxyResponse output = executeRequest(request, lambdaContext); assertEquals(404, output.getStatusCode()); handler.stripBasePath(""); } @Test public void securityContext_injectPrincipal_expectPrincipalName() { - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo-request-info", "GET") + assumeTrue("API_GW".equals(type)); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo-request-info", "GET") .queryString("mode", "principal") - .authorizerPrincipal(USER_PRINCIPAL).build(); + .authorizerPrincipal(USER_PRINCIPAL); - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertEquals(200, resp.getStatusCode()); validateSingleValueModel(resp, USER_PRINCIPAL); } @Test public void queryParam_encoding_expectUnencodedParam() { + assumeTrue("API_GW".equals(type)); String paramValue = "p%2Fz%2B3"; String decodedParam = ""; try { @@ -271,21 +291,20 @@ public void queryParam_encoding_expectUnencodedParam() { e.printStackTrace(); fail("Could not decode parameter"); } - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo", "GET").queryString(QUERY_STRING_KEY, decodedParam) - .build(); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo", "GET").queryString(QUERY_STRING_KEY, decodedParam); - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertEquals(200, resp.getStatusCode()); validateSingleValueModel(resp, decodedParam); } @Test public void queryParam_encoding_expectEncodedParam() { + assumeTrue("API_GW".equals(type)); String paramValue = "p%2Fz%2B3"; - AwsProxyRequest request = new AwsProxyRequestBuilder("/echo", "GET").queryString(QUERY_STRING_KEY, paramValue) - .build(); + AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo", "GET").queryString(QUERY_STRING_KEY, paramValue); - AwsProxyResponse resp = handler.proxy(request, lambdaContext); + AwsProxyResponse resp = executeRequest(request, lambdaContext); assertEquals(200, resp.getStatusCode()); validateSingleValueModel(resp, paramValue); } diff --git a/aws-serverless-jersey-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/aws-serverless-jersey-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml index b934c4a4a..dc8cb6204 100644 --- a/aws-serverless-jersey-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml +++ b/aws-serverless-jersey-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -24,7 +24,7 @@ - sam.yaml + template.yml README.md build.gradle diff --git a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/README.md b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/README.md index de5f4671a..1dc39a0a2 100644 --- a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/README.md +++ b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/README.md @@ -20,37 +20,40 @@ The \${artifactId} project, created with [`aws-serverless-java-container`](https The starter project defines a simple `/ping` resource that can accept `GET` requests with its tests. -The project folder also includes a `sam.yaml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with [SAM Local](https://github.com/awslabs/aws-sam-local). +The project folder also includes a `template.yml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with the [SAM CLI](https://github.com/awslabs/aws-sam-cli). -## Building the project -Using [Maven](https://maven.apache.org/), you can create an AWS Lambda-compatible zip file simply by running the maven package command from the project folder. +#[[##]]# Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +#[[##]]# Building the project +You can use the SAM CLI to quickly build the project ```bash $ mvn archetype:generate -DartifactId=\${artifactId} -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-jersey-archetype -DarchetypeVersion=${project.version} -DgroupId=\${groupId} -Dversion=\${version} -Dinteractive=false $ cd \${artifactId} -$ mvn clean package - -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 6.546 s -[INFO] Finished at: 2018-02-15T08:39:33-08:00 -[INFO] Final Memory: XXM/XXXM -[INFO] ------------------------------------------------------------------------ -``` +$ sam build +Building resource '\${resourceName}Function' +Running JavaGradleWorkflow:GradleBuild +Running JavaGradleWorkflow:CopyArtifacts -## Testing locally with SAM local -You can use [AWS SAM Local](https://github.com/awslabs/aws-sam-local) to start your project. +Build Succeeded -First, install SAM local: +Built Artifacts : .aws-sam/build +Built Template : .aws-sam/build/template.yaml -```bash -$ npm install -g aws-sam-local +Commands you can use next +========================= +[*] Invoke Function: sam local invoke +[*] Deploy: sam deploy --guided ``` -Next, from the project root folder - where the `sam.yaml` file is located - start the API with the SAM Local CLI. +#[[##]]# Testing locally with the SAM CLI + +From the project root folder - where the `template.yml` file is located - start the API with the SAM CLI. ```bash -$ sam local start-api --template sam.yaml +$ sam local start-api ... Mounting ${groupId}.StreamLambdaHandler::handleRequest (java8) at http://127.0.0.1:3000/{proxy+} [OPTIONS GET HEAD POST PUT DELETE PATCH] @@ -67,54 +70,22 @@ $ curl -s http://127.0.0.1:3000/ping | python -m json.tool } ``` -## Deploying to AWS -You can use the [AWS CLI](https://aws.amazon.com/cli/) to quickly deploy your application to AWS Lambda and Amazon API Gateway with your SAM template. - -You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the project's root folder - where the `sam.yaml` file is located: - -``` -$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket -Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) -Successfully packaged artifacts and wrote output template to file output-sam.yaml. -Execute the following command to deploy the packaged template -aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name -``` +#[[##]]# Deploying to AWS +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen -As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. - ``` -$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessJerseyApi --capabilities CAPABILITY_IAM +$ sam deploy --guided ``` -Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `ServerlessJerseyApi` key of the `Outputs` property: +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL ``` -$ aws cloudformation describe-stacks --stack-name ServerlessJerseyApi -{ - "Stacks": [ - { - "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/ServerlessJerseyApi/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", - "Description": "AWS Serverless Jersey API - ${groupId}::${artifactId}", - "Tags": [], - "Outputs": [ - { - "Description": "URL for application", - "ExportName": "\${resourceName}Api", - "OutputKey": "\${resourceName}Api", - "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/ping" - } - ], - "CreationTime": "2016-12-13T22:59:31.552Z", - "Capabilities": [ - "CAPABILITY_IAM" - ], - "StackName": "ServerlessJerseyApi", - "NotificationARNs": [], - "StackStatus": "UPDATE_COMPLETE" - } - ] -} - +... +------------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +------------------------------------------------------------------------------------------------------------- +\${resourceName}Api - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets +------------------------------------------------------------------------------------------------------------- ``` Copy the `OutputValue` into a browser or use curl to test your first request: diff --git a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/pom.xml b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/pom.xml index 3282eb25d..9fe135430 100644 --- a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/pom.xml +++ b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/pom.xml @@ -14,8 +14,8 @@ 1.8 1.8 - 2.29.1 - 2.9.10 + 2.30.1 + 2.10.3 diff --git a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/sam.yaml b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/template.yml similarity index 94% rename from aws-serverless-jersey-archetype/src/main/resources/archetype-resources/sam.yaml rename to aws-serverless-jersey-archetype/src/main/resources/archetype-resources/template.yml index 9df80ae88..db6ecbaff 100644 --- a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/sam.yaml +++ b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/template.yml @@ -33,12 +33,12 @@ Resources: Properties: Handler: ${groupId}.StreamLambdaHandler::handleRequest Runtime: java8 - CodeUri: target/${artifactId}-${version}-lambda-package.zip + CodeUri: . MemorySize: 512 Policies: AWSLambdaBasicExecutionRole Timeout: 15 Events: - GetResource: + ProxyResource: Type: Api Properties: Path: /{proxy+} diff --git a/aws-serverless-spark-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/aws-serverless-spark-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml index 45dc2c1a7..2ea9dca13 100644 --- a/aws-serverless-spark-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml +++ b/aws-serverless-spark-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -24,7 +24,7 @@ - sam.yaml + template.yml README.md build.gradle diff --git a/aws-serverless-spark-archetype/src/main/resources/archetype-resources/README.md b/aws-serverless-spark-archetype/src/main/resources/archetype-resources/README.md index 82a59465b..1dc39a0a2 100644 --- a/aws-serverless-spark-archetype/src/main/resources/archetype-resources/README.md +++ b/aws-serverless-spark-archetype/src/main/resources/archetype-resources/README.md @@ -20,37 +20,40 @@ The \${artifactId} project, created with [`aws-serverless-java-container`](https The starter project defines a simple `/ping` resource that can accept `GET` requests with its tests. -The project folder also includes a `sam.yaml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with [SAM Local](https://github.com/awslabs/aws-sam-local). +The project folder also includes a `template.yml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with the [SAM CLI](https://github.com/awslabs/aws-sam-cli). -## Building the project -Using [Maven](https://maven.apache.org/), you can create an AWS Lambda-compatible zip file simply by running the maven package command from the project folder. +#[[##]]# Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +#[[##]]# Building the project +You can use the SAM CLI to quickly build the project ```bash -$ mvn archetype:generate -DartifactId=\${artifactId} -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-spark-archetype -DarchetypeVersion=${project.version} -DgroupId=\${groupId} -Dversion=\${version} -Dinteractive=false +$ mvn archetype:generate -DartifactId=\${artifactId} -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-jersey-archetype -DarchetypeVersion=${project.version} -DgroupId=\${groupId} -Dversion=\${version} -Dinteractive=false $ cd \${artifactId} -$ mvn clean package - -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 6.546 s -[INFO] Finished at: 2018-02-15T08:39:33-08:00 -[INFO] Final Memory: XXM/XXXM -[INFO] ------------------------------------------------------------------------ -``` +$ sam build +Building resource '\${resourceName}Function' +Running JavaGradleWorkflow:GradleBuild +Running JavaGradleWorkflow:CopyArtifacts -## Testing locally with SAM local -You can use [AWS SAM Local](https://github.com/awslabs/aws-sam-local) to start your project. +Build Succeeded -First, install SAM local: +Built Artifacts : .aws-sam/build +Built Template : .aws-sam/build/template.yaml -```bash -$ npm install -g aws-sam-local +Commands you can use next +========================= +[*] Invoke Function: sam local invoke +[*] Deploy: sam deploy --guided ``` -Next, from the project root folder - where the `sam.yaml` file is located - start the API with the SAM Local CLI. +#[[##]]# Testing locally with the SAM CLI + +From the project root folder - where the `template.yml` file is located - start the API with the SAM CLI. ```bash -$ sam local start-api --template sam.yaml +$ sam local start-api ... Mounting ${groupId}.StreamLambdaHandler::handleRequest (java8) at http://127.0.0.1:3000/{proxy+} [OPTIONS GET HEAD POST PUT DELETE PATCH] @@ -67,54 +70,22 @@ $ curl -s http://127.0.0.1:3000/ping | python -m json.tool } ``` -## Deploying to AWS -You can use the [AWS CLI](https://aws.amazon.com/cli/) to quickly deploy your application to AWS Lambda and Amazon API Gateway with your SAM template. - -You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the project's root folder - where the `sam.yaml` file is located: - -``` -$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket -Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) -Successfully packaged artifacts and wrote output template to file output-sam.yaml. -Execute the following command to deploy the packaged template -aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name -``` +#[[##]]# Deploying to AWS +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen -As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. - ``` -$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessSparkApi --capabilities CAPABILITY_IAM +$ sam deploy --guided ``` -Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `ServerlessSparkApi` key of the `Outputs` property: +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL ``` -$ aws cloudformation describe-stacks --stack-name ServerlessSparkApi -{ - "Stacks": [ - { - "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/ServerlessSparkApi/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", - "Description": "AWS Serverless Spark API - ${groupId}::${artifactId}", - "Tags": [], - "Outputs": [ - { - "Description": "URL for application", - "ExportName": "\${resourceName}Api", - "OutputKey": "\${resourceName}Api", - "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/ping" - } - ], - "CreationTime": "2016-12-13T22:59:31.552Z", - "Capabilities": [ - "CAPABILITY_IAM" - ], - "StackName": "ServerlessSparkApi", - "NotificationARNs": [], - "StackStatus": "UPDATE_COMPLETE" - } - ] -} - +... +------------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +------------------------------------------------------------------------------------------------------------- +\${resourceName}Api - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets +------------------------------------------------------------------------------------------------------------- ``` Copy the `OutputValue` into a browser or use curl to test your first request: diff --git a/aws-serverless-spark-archetype/src/main/resources/archetype-resources/build.gradle b/aws-serverless-spark-archetype/src/main/resources/archetype-resources/build.gradle index f4e280ce7..e915d4511 100644 --- a/aws-serverless-spark-archetype/src/main/resources/archetype-resources/build.gradle +++ b/aws-serverless-spark-archetype/src/main/resources/archetype-resources/build.gradle @@ -9,7 +9,7 @@ dependencies { compile ( 'com.sparkjava:spark-core:2.9.1', 'com.amazonaws.serverless:aws-serverless-java-container-spark:[1.0,)', - 'com.fasterxml.jackson.core:jackson-databind:2.9.10', + 'com.fasterxml.jackson.core:jackson-databind:2.10.3', 'io.symphonia:lambda-logging:1.0.1' ) diff --git a/aws-serverless-spark-archetype/src/main/resources/archetype-resources/pom.xml b/aws-serverless-spark-archetype/src/main/resources/archetype-resources/pom.xml index 055fc4321..579d10607 100644 --- a/aws-serverless-spark-archetype/src/main/resources/archetype-resources/pom.xml +++ b/aws-serverless-spark-archetype/src/main/resources/archetype-resources/pom.xml @@ -16,7 +16,7 @@ 1.8 1.8 - 2.9.10 + 2.10.3 2.9.1 diff --git a/aws-serverless-spark-archetype/src/main/resources/archetype-resources/sam.yaml b/aws-serverless-spark-archetype/src/main/resources/archetype-resources/template.yml similarity index 94% rename from aws-serverless-spark-archetype/src/main/resources/archetype-resources/sam.yaml rename to aws-serverless-spark-archetype/src/main/resources/archetype-resources/template.yml index 674c769af..d9e83ceba 100644 --- a/aws-serverless-spark-archetype/src/main/resources/archetype-resources/sam.yaml +++ b/aws-serverless-spark-archetype/src/main/resources/archetype-resources/template.yml @@ -33,12 +33,12 @@ Resources: Properties: Handler: ${groupId}.StreamLambdaHandler::handleRequest Runtime: java8 - CodeUri: target/${artifactId}-${version}-lambda-package.zip + CodeUri: . MemorySize: 512 Policies: AWSLambdaBasicExecutionRole Timeout: 15 Events: - GetResource: + ProxyResource: Type: Api Properties: Path: /{proxy+} diff --git a/aws-serverless-spring-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/aws-serverless-spring-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml index 9295c6457..e279efa50 100644 --- a/aws-serverless-spring-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml +++ b/aws-serverless-spring-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -30,7 +30,7 @@ - sam.yaml + template.yml README.md build.gradle diff --git a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/README.md b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/README.md index 1bb929910..1dc39a0a2 100644 --- a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/README.md +++ b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/README.md @@ -20,37 +20,40 @@ The \${artifactId} project, created with [`aws-serverless-java-container`](https The starter project defines a simple `/ping` resource that can accept `GET` requests with its tests. -The project folder also includes a `sam.yaml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with [SAM Local](https://github.com/awslabs/aws-sam-local). +The project folder also includes a `template.yml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with the [SAM CLI](https://github.com/awslabs/aws-sam-cli). -## Building the project -Using [Maven](https://maven.apache.org/), you can create an AWS Lambda-compatible zip file simply by running the maven package command from the project folder. +#[[##]]# Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +#[[##]]# Building the project +You can use the SAM CLI to quickly build the project ```bash -$ mvn archetype:generate -DartifactId=\${artifactId} -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-spring-archetype -DarchetypeVersion=${project.version} -DgroupId=\${groupId} -Dversion=\${version} -Dinteractive=false +$ mvn archetype:generate -DartifactId=\${artifactId} -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-jersey-archetype -DarchetypeVersion=${project.version} -DgroupId=\${groupId} -Dversion=\${version} -Dinteractive=false $ cd \${artifactId} -$ mvn clean package - -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 6.546 s -[INFO] Finished at: 2018-02-15T08:39:33-08:00 -[INFO] Final Memory: XXM/XXXM -[INFO] ------------------------------------------------------------------------ -``` +$ sam build +Building resource '\${resourceName}Function' +Running JavaGradleWorkflow:GradleBuild +Running JavaGradleWorkflow:CopyArtifacts -## Testing locally with SAM local -You can use [AWS SAM Local](https://github.com/awslabs/aws-sam-local) to start your project. +Build Succeeded -First, install SAM local: +Built Artifacts : .aws-sam/build +Built Template : .aws-sam/build/template.yaml -```bash -$ npm install -g aws-sam-local +Commands you can use next +========================= +[*] Invoke Function: sam local invoke +[*] Deploy: sam deploy --guided ``` -Next, from the project root folder - where the `sam.yaml` file is located - start the API with the SAM Local CLI. +#[[##]]# Testing locally with the SAM CLI + +From the project root folder - where the `template.yml` file is located - start the API with the SAM CLI. ```bash -$ sam local start-api --template sam.yaml +$ sam local start-api ... Mounting ${groupId}.StreamLambdaHandler::handleRequest (java8) at http://127.0.0.1:3000/{proxy+} [OPTIONS GET HEAD POST PUT DELETE PATCH] @@ -67,54 +70,22 @@ $ curl -s http://127.0.0.1:3000/ping | python -m json.tool } ``` -## Deploying to AWS -You can use the [AWS CLI](https://aws.amazon.com/cli/) to quickly deploy your application to AWS Lambda and Amazon API Gateway with your SAM template. - -You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the project's root folder - where the `sam.yaml` file is located: - -``` -$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket -Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) -Successfully packaged artifacts and wrote output template to file output-sam.yaml. -Execute the following command to deploy the packaged template -aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name -``` +#[[##]]# Deploying to AWS +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen -As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. - ``` -$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessSpringApi --capabilities CAPABILITY_IAM +$ sam deploy --guided ``` -Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `ServerlessSpringApi` key of the `Outputs` property: +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL ``` -$ aws cloudformation describe-stacks --stack-name ServerlessSpringApi -{ - "Stacks": [ - { - "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/ServerlessSpringApi/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", - "Description": "AWS Serverless Spring API - ${groupId}::${artifactId}", - "Tags": [], - "Outputs": [ - { - "Description": "URL for application", - "ExportName": "\${resourceName}Api", - "OutputKey": "\${resourceName}Api", - "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/ping" - } - ], - "CreationTime": "2016-12-13T22:59:31.552Z", - "Capabilities": [ - "CAPABILITY_IAM" - ], - "StackName": "ServerlessSpringApi", - "NotificationARNs": [], - "StackStatus": "UPDATE_COMPLETE" - } - ] -} - +... +------------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +------------------------------------------------------------------------------------------------------------- +\${resourceName}Api - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets +------------------------------------------------------------------------------------------------------------- ``` Copy the `OutputValue` into a browser or use curl to test your first request: diff --git a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/build.gradle b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/build.gradle index e67eb52e4..e73185939 100644 --- a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/build.gradle +++ b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/build.gradle @@ -7,13 +7,13 @@ repositories { dependencies { compile ( - 'org.springframework:spring-webmvc:5.1.9.RELEASE', - 'org.springframework:spring-context:5.1.9.RELEASE', + 'org.springframework:spring-webmvc:5.2.5.RELEASE', + 'org.springframework:spring-context:5.2.5.RELEASE', 'com.amazonaws.serverless:aws-serverless-java-container-spring:[1.0,)', 'org.apache.logging.log4j:log4j-core:2.8.2', 'org.apache.logging.log4j:log4j-api:2.8.2', 'org.apache.logging.log4j:log4j-slf4j-impl:2.8.2', - 'com.fasterxml.jackson.core:jackson-databind:2.9.10', + 'com.fasterxml.jackson.core:jackson-databind:2.10.3', 'com.amazonaws:aws-lambda-java-log4j2:1.1.0', ) diff --git a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml index a6a574c8b..a41ddd6ec 100644 --- a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml +++ b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml @@ -16,7 +16,7 @@ 1.8 1.8 - 5.1.9.RELEASE + 5.2.5.RELEASE 4.12 2.8.2 diff --git a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/sam.yaml b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/template.yml similarity index 94% rename from aws-serverless-spring-archetype/src/main/resources/archetype-resources/sam.yaml rename to aws-serverless-spring-archetype/src/main/resources/archetype-resources/template.yml index 374dfe593..292d87a9c 100644 --- a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/sam.yaml +++ b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/template.yml @@ -33,12 +33,12 @@ Resources: Properties: Handler: ${groupId}.StreamLambdaHandler::handleRequest Runtime: java8 - CodeUri: target/${artifactId}-${version}-lambda-package.zip + CodeUri: . MemorySize: 512 Policies: AWSLambdaBasicExecutionRole Timeout: 15 Events: - GetResource: + ProxyResource: Type: Api Properties: Path: /{proxy+} diff --git a/aws-serverless-springboot-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/aws-serverless-springboot-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml index ec02bb6ab..5379692ba 100644 --- a/aws-serverless-springboot-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml +++ b/aws-serverless-springboot-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -30,7 +30,7 @@ - sam.yaml + template.yml README.md build.gradle diff --git a/aws-serverless-springboot-archetype/src/main/resources/archetype-resources/README.md b/aws-serverless-springboot-archetype/src/main/resources/archetype-resources/README.md index 43a3683f5..1dc39a0a2 100644 --- a/aws-serverless-springboot-archetype/src/main/resources/archetype-resources/README.md +++ b/aws-serverless-springboot-archetype/src/main/resources/archetype-resources/README.md @@ -20,37 +20,40 @@ The \${artifactId} project, created with [`aws-serverless-java-container`](https The starter project defines a simple `/ping` resource that can accept `GET` requests with its tests. -The project folder also includes a `sam.yaml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with [SAM Local](https://github.com/awslabs/aws-sam-local). +The project folder also includes a `template.yml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with the [SAM CLI](https://github.com/awslabs/aws-sam-cli). -## Building the project -Using [Maven](https://maven.apache.org/), you can create an AWS Lambda-compatible zip file simply by running the maven package command from the project folder. +#[[##]]# Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +#[[##]]# Building the project +You can use the SAM CLI to quickly build the project ```bash -$ mvn archetype:generate -DartifactId=\${artifactId} -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-springboot-archetype -DarchetypeVersion=${project.version} -DgroupId=\${groupId} -Dversion=\${version} -Dinteractive=false +$ mvn archetype:generate -DartifactId=\${artifactId} -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-jersey-archetype -DarchetypeVersion=${project.version} -DgroupId=\${groupId} -Dversion=\${version} -Dinteractive=false $ cd \${artifactId} -$ mvn clean package - -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 6.546 s -[INFO] Finished at: 2018-02-15T08:39:33-08:00 -[INFO] Final Memory: XXM/XXXM -[INFO] ------------------------------------------------------------------------ -``` +$ sam build +Building resource '\${resourceName}Function' +Running JavaGradleWorkflow:GradleBuild +Running JavaGradleWorkflow:CopyArtifacts -## Testing locally with SAM local -You can use [AWS SAM Local](https://github.com/awslabs/aws-sam-local) to start your project. +Build Succeeded -First, install SAM local: +Built Artifacts : .aws-sam/build +Built Template : .aws-sam/build/template.yaml -```bash -$ npm install -g aws-sam-local +Commands you can use next +========================= +[*] Invoke Function: sam local invoke +[*] Deploy: sam deploy --guided ``` -Next, from the project root folder - where the `sam.yaml` file is located - start the API with the SAM Local CLI. +#[[##]]# Testing locally with the SAM CLI + +From the project root folder - where the `template.yml` file is located - start the API with the SAM CLI. ```bash -$ sam local start-api --template sam.yaml +$ sam local start-api ... Mounting ${groupId}.StreamLambdaHandler::handleRequest (java8) at http://127.0.0.1:3000/{proxy+} [OPTIONS GET HEAD POST PUT DELETE PATCH] @@ -67,54 +70,22 @@ $ curl -s http://127.0.0.1:3000/ping | python -m json.tool } ``` -## Deploying to AWS -You can use the [AWS CLI](https://aws.amazon.com/cli/) to quickly deploy your application to AWS Lambda and Amazon API Gateway with your SAM template. - -You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the project's root folder - where the `sam.yaml` file is located: - -``` -$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket -Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) -Successfully packaged artifacts and wrote output template to file output-sam.yaml. -Execute the following command to deploy the packaged template -aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name -``` +#[[##]]# Deploying to AWS +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen -As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. - ``` -$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessSpringApi --capabilities CAPABILITY_IAM +$ sam deploy --guided ``` -Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `ServerlessSpringApi` key of the `Outputs` property: +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL ``` -$ aws cloudformation describe-stacks --stack-name ServerlessSpringApi -{ - "Stacks": [ - { - "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/ServerlessSpringApi/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", - "Description": "AWS Serverless Spring API - ${groupId}::${artifactId}", - "Tags": [], - "Outputs": [ - { - "Description": "URL for application", - "ExportName": "\${resourceName}Api", - "OutputKey": "\${resourceName}Api", - "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/ping" - } - ], - "CreationTime": "2016-12-13T22:59:31.552Z", - "Capabilities": [ - "CAPABILITY_IAM" - ], - "StackName": "ServerlessSpringApi", - "NotificationARNs": [], - "StackStatus": "UPDATE_COMPLETE" - } - ] -} - +... +------------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +------------------------------------------------------------------------------------------------------------- +\${resourceName}Api - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets +------------------------------------------------------------------------------------------------------------- ``` Copy the `OutputValue` into a browser or use curl to test your first request: diff --git a/aws-serverless-springboot-archetype/src/main/resources/archetype-resources/build.gradle b/aws-serverless-springboot-archetype/src/main/resources/archetype-resources/build.gradle index 53c675deb..2a95fdaf4 100644 --- a/aws-serverless-springboot-archetype/src/main/resources/archetype-resources/build.gradle +++ b/aws-serverless-springboot-archetype/src/main/resources/archetype-resources/build.gradle @@ -1,6 +1,3 @@ -plugins { - id 'org.springframework.boot' version '1.5.22.RELEASE' -} apply plugin: 'java' repositories { @@ -11,7 +8,7 @@ repositories { dependencies { compile ( - 'org.springframework.boot:spring-boot-starter-web', + 'org.springframework.boot:spring-boot-starter-web:1.5.22.RELEASE', 'com.amazonaws.serverless:aws-serverless-java-container-spring:[1.0,)', 'io.symphonia:lambda-logging:1.0.1' ) diff --git a/aws-serverless-springboot-archetype/src/main/resources/archetype-resources/sam.yaml b/aws-serverless-springboot-archetype/src/main/resources/archetype-resources/template.yml similarity index 94% rename from aws-serverless-springboot-archetype/src/main/resources/archetype-resources/sam.yaml rename to aws-serverless-springboot-archetype/src/main/resources/archetype-resources/template.yml index 3fbd348b2..832028a20 100644 --- a/aws-serverless-springboot-archetype/src/main/resources/archetype-resources/sam.yaml +++ b/aws-serverless-springboot-archetype/src/main/resources/archetype-resources/template.yml @@ -33,12 +33,12 @@ Resources: Properties: Handler: ${groupId}.StreamLambdaHandler::handleRequest Runtime: java8 - CodeUri: target/${artifactId}-${version}-lambda-package.zip + CodeUri: . MemorySize: 512 Policies: AWSLambdaBasicExecutionRole Timeout: 30 Events: - GetResource: + ProxyResource: Type: Api Properties: Path: /{proxy+} diff --git a/aws-serverless-springboot2-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/aws-serverless-springboot2-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml index ec02bb6ab..5379692ba 100644 --- a/aws-serverless-springboot2-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml +++ b/aws-serverless-springboot2-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -30,7 +30,7 @@ - sam.yaml + template.yml README.md build.gradle diff --git a/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/README.md b/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/README.md index 3f99b3445..1dc39a0a2 100644 --- a/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/README.md +++ b/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/README.md @@ -20,37 +20,40 @@ The \${artifactId} project, created with [`aws-serverless-java-container`](https The starter project defines a simple `/ping` resource that can accept `GET` requests with its tests. -The project folder also includes a `sam.yaml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with [SAM Local](https://github.com/awslabs/aws-sam-local). +The project folder also includes a `template.yml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with the [SAM CLI](https://github.com/awslabs/aws-sam-cli). -## Building the project -Using [Maven](https://maven.apache.org/), you can create an AWS Lambda-compatible zip file simply by running the maven package command from the project folder. +#[[##]]# Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +#[[##]]# Building the project +You can use the SAM CLI to quickly build the project ```bash -$ mvn archetype:generate -DartifactId=\${artifactId} -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-springboot2-archetype -DarchetypeVersion=${project.version} -DgroupId=\${groupId} -Dversion=\${version} -Dinteractive=false +$ mvn archetype:generate -DartifactId=\${artifactId} -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-jersey-archetype -DarchetypeVersion=${project.version} -DgroupId=\${groupId} -Dversion=\${version} -Dinteractive=false $ cd \${artifactId} -$ mvn clean package - -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 6.546 s -[INFO] Finished at: 2018-02-15T08:39:33-08:00 -[INFO] Final Memory: XXM/XXXM -[INFO] ------------------------------------------------------------------------ -``` +$ sam build +Building resource '\${resourceName}Function' +Running JavaGradleWorkflow:GradleBuild +Running JavaGradleWorkflow:CopyArtifacts -## Testing locally with SAM local -You can use [AWS SAM Local](https://github.com/awslabs/aws-sam-local) to start your project. +Build Succeeded -First, install SAM local: +Built Artifacts : .aws-sam/build +Built Template : .aws-sam/build/template.yaml -```bash -$ npm install -g aws-sam-local +Commands you can use next +========================= +[*] Invoke Function: sam local invoke +[*] Deploy: sam deploy --guided ``` -Next, from the project root folder - where the `sam.yaml` file is located - start the API with the SAM Local CLI. +#[[##]]# Testing locally with the SAM CLI + +From the project root folder - where the `template.yml` file is located - start the API with the SAM CLI. ```bash -$ sam local start-api --template sam.yaml +$ sam local start-api ... Mounting ${groupId}.StreamLambdaHandler::handleRequest (java8) at http://127.0.0.1:3000/{proxy+} [OPTIONS GET HEAD POST PUT DELETE PATCH] @@ -67,54 +70,22 @@ $ curl -s http://127.0.0.1:3000/ping | python -m json.tool } ``` -## Deploying to AWS -You can use the [AWS CLI](https://aws.amazon.com/cli/) to quickly deploy your application to AWS Lambda and Amazon API Gateway with your SAM template. - -You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the project's root folder - where the `sam.yaml` file is located: - -``` -$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket -Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) -Successfully packaged artifacts and wrote output template to file output-sam.yaml. -Execute the following command to deploy the packaged template -aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name -``` +#[[##]]# Deploying to AWS +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen -As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. - ``` -$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessSpringApi --capabilities CAPABILITY_IAM +$ sam deploy --guided ``` -Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `ServerlessSpringApi` key of the `Outputs` property: +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL ``` -$ aws cloudformation describe-stacks --stack-name ServerlessSpringApi -{ - "Stacks": [ - { - "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/ServerlessSpringApi/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", - "Description": "AWS Serverless Spring API - ${groupId}::${artifactId}", - "Tags": [], - "Outputs": [ - { - "Description": "URL for application", - "ExportName": "\${resourceName}Api", - "OutputKey": "\${resourceName}Api", - "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/ping" - } - ], - "CreationTime": "2016-12-13T22:59:31.552Z", - "Capabilities": [ - "CAPABILITY_IAM" - ], - "StackName": "ServerlessSpringApi", - "NotificationARNs": [], - "StackStatus": "UPDATE_COMPLETE" - } - ] -} - +... +------------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +------------------------------------------------------------------------------------------------------------- +\${resourceName}Api - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets +------------------------------------------------------------------------------------------------------------- ``` Copy the `OutputValue` into a browser or use curl to test your first request: diff --git a/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/build.gradle b/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/build.gradle index e85ac40d8..66ba6d9a2 100644 --- a/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/build.gradle +++ b/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/build.gradle @@ -1,6 +1,3 @@ -plugins { - id 'org.springframework.boot' version '2.1.1.RELEASE' -} apply plugin: 'java' repositories { @@ -11,7 +8,7 @@ repositories { dependencies { compile ( - 'org.springframework.boot:spring-boot-starter-web:2.1.8.RELEASE', + 'org.springframework.boot:spring-boot-starter-web:2.2.6.RELEASE', 'com.amazonaws.serverless:aws-serverless-java-container-springboot2:[1.0,)', 'io.symphonia:lambda-logging:1.0.1' ) diff --git a/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/pom.xml b/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/pom.xml index 3dfaa1235..e25916482 100644 --- a/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/pom.xml +++ b/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/pom.xml @@ -16,7 +16,7 @@ org.springframework.boot spring-boot-starter-parent - 2.1.8.RELEASE + 2.2.6.RELEASE diff --git a/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/sam.yaml b/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/template.yml similarity index 94% rename from aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/sam.yaml rename to aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/template.yml index 970025c41..ae2adf28c 100644 --- a/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/sam.yaml +++ b/aws-serverless-springboot2-archetype/src/main/resources/archetype-resources/template.yml @@ -33,12 +33,12 @@ Resources: Properties: Handler: ${groupId}.StreamLambdaHandler::handleRequest Runtime: java8 - CodeUri: target/${artifactId}-${version}-lambda-package.zip + CodeUri: . MemorySize: 512 Policies: AWSLambdaBasicExecutionRole Timeout: 30 Events: - GetResource: + ProxyResource: Type: Api Properties: Path: /{proxy+} diff --git a/aws-serverless-struts2-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/aws-serverless-struts2-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml index 91a53e562..ad8b86248 100644 --- a/aws-serverless-struts2-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml +++ b/aws-serverless-struts2-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -31,7 +31,7 @@ - sam.yaml + template.yml README.md build.gradle diff --git a/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/README.md b/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/README.md index 7be306683..1dc39a0a2 100644 --- a/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/README.md +++ b/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/README.md @@ -20,37 +20,40 @@ The \${artifactId} project, created with [`aws-serverless-java-container`](https The starter project defines a simple `/ping` resource that can accept `GET` requests with its tests. -The project folder also includes a `sam.yaml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with [SAM Local](https://github.com/awslabs/aws-sam-local). +The project folder also includes a `template.yml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with the [SAM CLI](https://github.com/awslabs/aws-sam-cli). -## Building the project -Using [Maven](https://maven.apache.org/), you can create an AWS Lambda-compatible zip file simply by running the maven package command from the project folder. +#[[##]]# Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +#[[##]]# Building the project +You can use the SAM CLI to quickly build the project ```bash -$ mvn archetype:generate -DartifactId=\${artifactId} -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-struts2-archetype -DarchetypeVersion=${project.version} -DgroupId=\${groupId} -Dversion=\${version} -Dinteractive=false +$ mvn archetype:generate -DartifactId=\${artifactId} -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-jersey-archetype -DarchetypeVersion=${project.version} -DgroupId=\${groupId} -Dversion=\${version} -Dinteractive=false $ cd \${artifactId} -$ mvn clean package - -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 6.546 s -[INFO] Finished at: 2018-02-15T08:39:33-08:00 -[INFO] Final Memory: XXM/XXXM -[INFO] ------------------------------------------------------------------------ -``` +$ sam build +Building resource '\${resourceName}Function' +Running JavaGradleWorkflow:GradleBuild +Running JavaGradleWorkflow:CopyArtifacts -## Testing locally with SAM local -You can use [AWS SAM Local](https://github.com/awslabs/aws-sam-local) to start your project. +Build Succeeded -First, install SAM local: +Built Artifacts : .aws-sam/build +Built Template : .aws-sam/build/template.yaml -```bash -$ npm install -g aws-sam-local +Commands you can use next +========================= +[*] Invoke Function: sam local invoke +[*] Deploy: sam deploy --guided ``` -Next, from the project root folder - where the `sam.yaml` file is located - start the API with the SAM Local CLI. +#[[##]]# Testing locally with the SAM CLI + +From the project root folder - where the `template.yml` file is located - start the API with the SAM CLI. ```bash -$ sam local start-api --template sam.yaml +$ sam local start-api ... Mounting ${groupId}.StreamLambdaHandler::handleRequest (java8) at http://127.0.0.1:3000/{proxy+} [OPTIONS GET HEAD POST PUT DELETE PATCH] @@ -67,54 +70,22 @@ $ curl -s http://127.0.0.1:3000/ping | python -m json.tool } ``` -## Deploying to AWS -You can use the [AWS CLI](https://aws.amazon.com/cli/) to quickly deploy your application to AWS Lambda and Amazon API Gateway with your SAM template. - -You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the project's root folder - where the `sam.yaml` file is located: - -``` -$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket -Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) -Successfully packaged artifacts and wrote output template to file output-sam.yaml. -Execute the following command to deploy the packaged template -aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name -``` +#[[##]]# Deploying to AWS +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen -As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. - ``` -$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessSpringApi --capabilities CAPABILITY_IAM +$ sam deploy --guided ``` -Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `ServerlessSpringApi` key of the `Outputs` property: +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL ``` -$ aws cloudformation describe-stacks --stack-name ServerlessSpringApi -{ - "Stacks": [ - { - "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/ServerlessSpringApi/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", - "Description": "AWS Serverless Spring API - ${groupId}::${artifactId}", - "Tags": [], - "Outputs": [ - { - "Description": "URL for application", - "ExportName": "\${resourceName}Api", - "OutputKey": "\${resourceName}Api", - "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/ping" - } - ], - "CreationTime": "2016-12-13T22:59:31.552Z", - "Capabilities": [ - "CAPABILITY_IAM" - ], - "StackName": "ServerlessSpringApi", - "NotificationARNs": [], - "StackStatus": "UPDATE_COMPLETE" - } - ] -} - +... +------------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +------------------------------------------------------------------------------------------------------------- +\${resourceName}Api - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets +------------------------------------------------------------------------------------------------------------- ``` Copy the `OutputValue` into a browser or use curl to test your first request: diff --git a/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/build.gradle b/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/build.gradle index ee08f5d4c..9e4b4ab18 100644 --- a/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/build.gradle +++ b/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/build.gradle @@ -8,13 +8,13 @@ repositories { dependencies { compile ( 'com.amazonaws.serverless:aws-serverless-java-container-struts2:[1.0,)', - 'org.apache.struts:struts2-convention-plugin:2.5.20', - 'org.apache.struts:struts2-rest-plugin:2.5.20', - 'org.apache.struts:struts2-bean-validation-plugin:2.5.20', - 'org.apache.struts:struts2-junit-plugin:2.5.20', + 'org.apache.struts:struts2-convention-plugin:2.5.22', + 'org.apache.struts:struts2-rest-plugin:2.5.22', + 'org.apache.struts:struts2-bean-validation-plugin:2.5.22', + 'org.apache.struts:struts2-junit-plugin:2.5.22', 'com.jgeppert.struts2:struts2-aws-lambda-support-plugin:1.0.0', 'org.hibernate:hibernate-validator:4.3.2.Final', - 'com.fasterxml.jackson.core:jackson-databind:2.9.10', + 'com.fasterxml.jackson.core:jackson-databind:2.10.3', 'org.apache.logging.log4j:log4j-core:2.8.2', 'org.apache.logging.log4j:log4j-api:2.8.2', 'org.apache.logging.log4j:log4j-slf4j-impl:2.8.2', diff --git a/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/pom.xml b/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/pom.xml index 3e224f4a5..935c0e9f2 100644 --- a/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/pom.xml +++ b/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/pom.xml @@ -15,8 +15,8 @@ 1.8 1.8 - 2.5.20 - 2.9.9 + 2.5.22 + 2.10.3 4.12 2.11.1 diff --git a/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/sam.yaml b/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/template.yml similarity index 95% rename from aws-serverless-struts2-archetype/src/main/resources/archetype-resources/sam.yaml rename to aws-serverless-struts2-archetype/src/main/resources/archetype-resources/template.yml index 023266fc7..11e06d07c 100644 --- a/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/sam.yaml +++ b/aws-serverless-struts2-archetype/src/main/resources/archetype-resources/template.yml @@ -33,12 +33,12 @@ Resources: Properties: Handler: com.amazonaws.serverless.proxy.struts2.Struts2LambdaHandler::handleRequest Runtime: java8 - CodeUri: target/${artifactId}-${version}-lambda.zip + CodeUri: . MemorySize: 512 Policies: AWSLambdaBasicExecutionRole Timeout: 30 Events: - GetResource: + ProxyResource: Type: Api Properties: Path: /{proxy+} diff --git a/gha_build.sh b/gha_build.sh index d4d2d7237..9867bf509 100755 --- a/gha_build.sh +++ b/gha_build.sh @@ -80,18 +80,6 @@ function sample { if [[ "$?" -ne 0 ]]; then exit 1 fi - - SAM_FILE=${SAMPLE_FOLDER}/sam.yaml - if [[ -f "$SAM_FILE" ]]; then - TARGET_ZIP=$(cat ${SAM_FILE} | grep CodeUri | sed -e 's/^.*:\ //g') - if [[ ! -f "${SAMPLE_FOLDER}/${TARGET_ZIP}" ]]; then - echo "COULD NOT FIND TARGET ZIP FILE $TARGET_ZIP FOR $1 SAMPLE" - exit 1 - fi - else - echo "COULD NOT FIND SAM FILE: '${SAM_FILE}'" - exit 1 - fi } # set up the master pom otherwise we won't be able to find new dependencies diff --git a/owasp-suppression.xml b/owasp-suppression.xml index fe70f749b..0997df5c3 100644 --- a/owasp-suppression.xml +++ b/owasp-suppression.xml @@ -27,14 +27,4 @@ cpe:/a:restful_web_services_project:restful_web_services:7.x-2.1::~~~drupal~~ - - - - - - cpe:/a:slf4j:slf4j:1.8.0 - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index 298bdc8b2..e61b9b739 100644 --- a/pom.xml +++ b/pom.xml @@ -45,8 +45,9 @@ 0.7 - 5.2.2 - 2.10.1 + 5.3.2 + 2.10.3 + 1.8.0-beta4 @@ -62,14 +63,14 @@ org.slf4j slf4j-api - 1.8.0-beta2 + ${slf4j.version} org.slf4j slf4j-simple - 1.8.0-beta2 + ${slf4j.version} test diff --git a/samples/jersey/pet-store/README.md b/samples/jersey/pet-store/README.md index bfb441648..d85d56284 100644 --- a/samples/jersey/pet-store/README.md +++ b/samples/jersey/pet-store/README.md @@ -1,62 +1,36 @@ # Serverless Jersey example -A basic pet store written with the [Jersey framework](https://jersey.java.net/). The `LambdaHandler` object is the main entry point for Lambda. +A basic pet store written with the [Jersey framework](https://jersey.java.net/). The `StreamLambdaHandler` object is the main entry point for Lambda. -The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `sam.yaml` file in the root folder contains the application definition +The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. -## Installation -To build and install the sample application you will need [Maven](https://maven.apache.org/) and the [AWS CLI](https://aws.amazon.com/cli/) installed on your computer. +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) -In a shell, navigate to the sample's folder and use maven to build a deployable jar. +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package ``` -$ mvn package +$ sam build ``` -This command should generate a `serverless-jersey-example-1.0-SNAPSHOT.jar` in the `target` folder. Now that we have generated the jar file, we can use the AWS CLI to package the template for deployment. +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. -You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the sample's folder: +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen ``` -$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket -Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) -Successfully packaged artifacts and wrote output template to file output-sam.yaml. -Execute the following command to deploy the packaged template -aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name +$ sam deploy --guided ``` -As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. - -``` -$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessJerseySample --capabilities CAPABILITY_IAM -``` - -Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `JerseyPetStoreApi` key of the `Outputs` property: - -``` -$ aws cloudformation describe-stacks --stack-name ServerlessJerseySample -{ - "Stacks": [ - { - "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/JerseySample/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", - "Description": "Example Pet Store API written in jersey with the aws-serverless-java-container library", - "Tags": [], - "Outputs": [ - { - "Description": "URL for application", - "OutputKey": "JerseyPetStoreApi", - "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets" - } - ], - "CreationTime": "2016-12-13T22:59:31.552Z", - "Capabilities": [ - "CAPABILITY_IAM" - ], - "StackName": "JerseySample", - "NotificationARNs": [], - "StackStatus": "UPDATE_COMPLETE" - } - ] -} +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL ``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +--------------------------------------------------------------------------------------------------------- -Copy the `OutputValue` into a browser to test a first request. \ No newline at end of file +$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +``` \ No newline at end of file diff --git a/samples/jersey/pet-store/build.gradle b/samples/jersey/pet-store/build.gradle index 064fdf373..88e34767e 100644 --- a/samples/jersey/pet-store/build.gradle +++ b/samples/jersey/pet-store/build.gradle @@ -9,17 +9,17 @@ dependencies { compile ( 'com.amazonaws:aws-lambda-java-core:1.2.0', 'com.amazonaws.serverless:aws-serverless-java-container-jersey:[1.0,)', - 'com.fasterxml.jackson.core:jackson-databind:2.9.10', + 'com.fasterxml.jackson.core:jackson-databind:2.10.3', 'io.symphonia:lambda-logging:1.0.1' ) - compile("org.glassfish.jersey.media:jersey-media-json-jackson:2.10.1") { + compile("org.glassfish.jersey.media:jersey-media-json-jackson:2.30.1") { exclude group: 'com.fasterxml.jackson.core', module: "jackson-annotations" exclude group: 'com.fasterxml.jackson.core', module: "jackson-databind" exclude group: 'com.fasterxml.jackson.core', module: "jackson-core" } - compile("org.glassfish.jersey.inject:jersey-hk2:2.29.1") { + compile("org.glassfish.jersey.inject:jersey-hk2:2.30.1") { exclude group: 'javax.inject', module: "javax.inject" } } diff --git a/samples/jersey/pet-store/pom.xml b/samples/jersey/pet-store/pom.xml index 6761c6210..bacdbe042 100644 --- a/samples/jersey/pet-store/pom.xml +++ b/samples/jersey/pet-store/pom.xml @@ -26,8 +26,8 @@ 1.8 1.8 - 2.29.1 - 2.10.1 + 2.30.1 + 2.10.3 diff --git a/samples/jersey/pet-store/sam.yaml b/samples/jersey/pet-store/template.yml similarity index 71% rename from samples/jersey/pet-store/sam.yaml rename to samples/jersey/pet-store/template.yml index 7ca060dae..ababb78a8 100644 --- a/samples/jersey/pet-store/sam.yaml +++ b/samples/jersey/pet-store/template.yml @@ -13,20 +13,20 @@ Resources: Properties: Handler: com.amazonaws.serverless.sample.jersey.StreamLambdaHandler::handleRequest Runtime: java8 - CodeUri: target/serverless-jersey-example-1.0-SNAPSHOT-lambda-package.zip + CodeUri: . MemorySize: 512 Policies: AWSLambdaBasicExecutionRole Timeout: 20 Events: - GetResource: - Type: Api + HttpApiEvent: + Type: HttpApi Properties: - Path: /{proxy+} - Method: any + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' Outputs: JerseyPetStoreApi: Description: URL for application - Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/pets' + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' Export: Name: JerseyPetStoreApi diff --git a/samples/micronaut/pet-store/build.gradle b/samples/micronaut/pet-store/build.gradle index 94f3d1b5f..8ed45b3d7 100644 --- a/samples/micronaut/pet-store/build.gradle +++ b/samples/micronaut/pet-store/build.gradle @@ -3,7 +3,7 @@ plugins { id "com.github.johnrengelman.shadow" version "5.0.0" id "application" id "net.ltgt.apt-eclipse" version "0.21" - id "org.springframework.boot" version "2.1.8.RELEASE" + id "org.springframework.boot" version "2.1.12.RELEASE" id "io.spring.dependency-management" version "1.0.6.RELEASE" } diff --git a/samples/spark/pet-store/README.md b/samples/spark/pet-store/README.md index 5d3e34d5a..2bfec99de 100644 --- a/samples/spark/pet-store/README.md +++ b/samples/spark/pet-store/README.md @@ -1,62 +1,36 @@ # Serverless Spark example -A basic pet store written with the [Spark framework](http://sparkjava.com/). The `LambdaHandler` object is the main entry point for Lambda. +A basic pet store written with the [Spark framework](http://sparkjava.com/). The `StreamLambdaHandler` object is the main entry point for Lambda. -The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `sam.yaml` file in the root folder contains the application definition +The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. -## Installation -To build and install the sample application you will need [Maven](https://maven.apache.org/) and the [AWS CLI](https://aws.amazon.com/cli/) installed on your computer. +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) -In a shell, navigate to the sample's folder and use maven to build a deployable jar. +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package ``` -$ mvn package +$ sam build ``` -This command should generate a `serverless-spark-example-1.0-SNAPSHOT.jar` in the `target` folder. Now that we have generated the jar file, we can use the AWS CLI to package the template for deployment. +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. -You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the sample's folder: +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen ``` -$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket -Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) -Successfully packaged artifacts and wrote output template to file output-sam.yaml. -Execute the following command to deploy the packaged template -aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name +$ sam deploy --guided ``` -As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. - -``` -$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessSparkSample --capabilities CAPABILITY_IAM -``` - -Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `SparkPetStoreApi` key of the `Outputs` property: - -``` -$ aws cloudformation describe-stacks --stack-name ServerlessSparkSample -{ - "Stacks": [ - { - "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/SparkSample/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", - "Description": "Example Pet Store API written with spark with the aws-serverless-java-container library", - "Tags": [], - "Outputs": [ - { - "Description": "URL for application", - "OutputKey": "PetStoreApi", - "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets" - } - ], - "CreationTime": "2016-12-13T22:59:31.552Z", - "Capabilities": [ - "CAPABILITY_IAM" - ], - "StackName": "SparkSample", - "NotificationARNs": [], - "StackStatus": "UPDATE_COMPLETE" - } - ] -} +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL ``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +--------------------------------------------------------------------------------------------------------- -Copy the `OutputValue` into a browser to test a first request. +$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +``` \ No newline at end of file diff --git a/samples/spark/pet-store/build.gradle b/samples/spark/pet-store/build.gradle index 00ac4797c..01605a978 100644 --- a/samples/spark/pet-store/build.gradle +++ b/samples/spark/pet-store/build.gradle @@ -9,7 +9,7 @@ dependencies { compile ( 'com.sparkjava:spark-core:2.9.1', 'com.amazonaws.serverless:aws-serverless-java-container-spark:[1.0,)', - 'com.fasterxml.jackson.core:jackson-databind:2.10.1', + 'com.fasterxml.jackson.core:jackson-databind:2.10.3', 'io.symphonia:lambda-logging:1.0.1' ) } diff --git a/samples/spark/pet-store/pom.xml b/samples/spark/pet-store/pom.xml index 4bdd90956..26a2f256b 100644 --- a/samples/spark/pet-store/pom.xml +++ b/samples/spark/pet-store/pom.xml @@ -26,7 +26,7 @@ 1.8 1.8 - 2.10.1 + 2.10.3 2.9.1 diff --git a/samples/spark/pet-store/sam.yaml b/samples/spark/pet-store/template.yml similarity index 71% rename from samples/spark/pet-store/sam.yaml rename to samples/spark/pet-store/template.yml index b16086533..26e0a6535 100644 --- a/samples/spark/pet-store/sam.yaml +++ b/samples/spark/pet-store/template.yml @@ -13,20 +13,20 @@ Resources: Properties: Handler: com.amazonaws.serverless.sample.spark.StreamLambdaHandler::handleRequest Runtime: java8 - CodeUri: target/serverless-spark-example-1.0-SNAPSHOT-lambda-package.zip + CodeUri: . MemorySize: 512 Policies: AWSLambdaBasicExecutionRole Timeout: 20 Events: - GetResource: - Type: Api + HttpApiEvent: + Type: HttpApi Properties: - Path: /{proxy+} - Method: any + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' Outputs: SparkPetStoreApi: Description: URL for application - Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/pets' + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' Export: Name: SparkPetStoreApi diff --git a/samples/spring/pet-store/README.md b/samples/spring/pet-store/README.md index 17592941b..5637b6b69 100644 --- a/samples/spring/pet-store/README.md +++ b/samples/spring/pet-store/README.md @@ -1,62 +1,36 @@ # Serverless Spring example -A basic pet store written with the [Spring framework](https://projects.spring.io/spring-framework/). The `LambdaHandler` object is the main entry point for Lambda. +A basic pet store written with the [Spring framework](https://projects.spring.io/spring-framework/). The `StreamLambdaHandler` object is the main entry point for Lambda. -The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `sam.yaml` file in the root folder contains the application definition +The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. -## Installation -To build and install the sample application you will need [Maven](https://maven.apache.org/) and the [AWS CLI](https://aws.amazon.com/cli/) installed on your computer. +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) -In a shell, navigate to the sample's folder and use maven to build a deployable jar. +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package ``` -$ mvn package +$ sam build ``` -This command should generate a `serverless-spring-example-1.0-SNAPSHOT.jar` in the `target` folder. Now that we have generated the jar file, we can use the AWS CLI to package the template for deployment. +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. -You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the sample's folder: +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen ``` -$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket -Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) -Successfully packaged artifacts and wrote output template to file output-sam.yaml. -Execute the following command to deploy the packaged template -aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name +$ sam deploy --guided ``` -As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. - -``` -$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessSpringSample --capabilities CAPABILITY_IAM -``` - -Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `SparkPetStoreApi` key of the `Outputs` property: - -``` -$ aws cloudformation describe-stacks --stack-name ServerlessSpringSample -{ - "Stacks": [ - { - "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/JerseySample/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", - "Description": "Example Pet Store API written with spark with the aws-serverless-java-container library", - "Tags": [], - "Outputs": [ - { - "Description": "URL for application", - "OutputKey": "PetStoreApi", - "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets" - } - ], - "CreationTime": "2016-12-13T22:59:31.552Z", - "Capabilities": [ - "CAPABILITY_IAM" - ], - "StackName": "JerseySample", - "NotificationARNs": [], - "StackStatus": "UPDATE_COMPLETE" - } - ] -} +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL ``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +--------------------------------------------------------------------------------------------------------- -Copy the `OutputValue` into a browser to test a first request. +$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +``` \ No newline at end of file diff --git a/samples/spring/pet-store/build.gradle b/samples/spring/pet-store/build.gradle index 7d7a74c44..433f5741b 100644 --- a/samples/spring/pet-store/build.gradle +++ b/samples/spring/pet-store/build.gradle @@ -7,13 +7,13 @@ repositories { dependencies { compile ( - 'org.springframework:spring-webmvc:5.1.9.RELEASE', - 'org.springframework:spring-context:5.1.9.RELEASE', + 'org.springframework:spring-webmvc:5.2.5.RELEASE', + 'org.springframework:spring-context:5.2.5.RELEASE', 'com.amazonaws.serverless:aws-serverless-java-container-spring:[1.0,)', 'org.apache.logging.log4j:log4j-core:2.8.2', 'org.apache.logging.log4j:log4j-api:2.8.2', 'org.apache.logging.log4j:log4j-slf4j-impl:2.8.2', - 'com.fasterxml.jackson.core:jackson-databind:2.9.10', + 'com.fasterxml.jackson.core:jackson-databind:2.10.3', 'com.amazonaws:aws-lambda-java-log4j2:1.1.0', ) } diff --git a/samples/spring/pet-store/pom.xml b/samples/spring/pet-store/pom.xml index 86d26471d..2798eafa1 100644 --- a/samples/spring/pet-store/pom.xml +++ b/samples/spring/pet-store/pom.xml @@ -26,7 +26,7 @@ 1.8 1.8 - 5.2.3.RELEASE + 5.2.5.RELEASE 4.12 2.8.2 diff --git a/samples/spring/pet-store/sam.yaml b/samples/spring/pet-store/template.yml similarity index 71% rename from samples/spring/pet-store/sam.yaml rename to samples/spring/pet-store/template.yml index 6a6f07e47..8c3dec021 100644 --- a/samples/spring/pet-store/sam.yaml +++ b/samples/spring/pet-store/template.yml @@ -13,20 +13,20 @@ Resources: Properties: Handler: com.amazonaws.serverless.sample.spring.StreamLambdaHandler::handleRequest Runtime: java8 - CodeUri: target/serverless-spring-example-1.0-SNAPSHOT-lambda-package.zip + CodeUri: . MemorySize: 512 Policies: AWSLambdaBasicExecutionRole Timeout: 30 Events: - GetResource: - Type: Api + HttpApiEvent: + Type: HttpApi Properties: - Path: /{proxy+} - Method: any + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' Outputs: SpringPetStoreApi: Description: URL for application - Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/pets' + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' Export: Name: SpringPetStoreApi diff --git a/samples/springboot/pet-store/README.md b/samples/springboot/pet-store/README.md index ae9100cb3..4462f1592 100644 --- a/samples/springboot/pet-store/README.md +++ b/samples/springboot/pet-store/README.md @@ -1,62 +1,36 @@ # Serverless Spring Boot example -A basic pet store written with the [Spring Boot framework](https://projects.spring.io/spring-boot/). The `LambdaHandler` object is the main entry point for Lambda. +A basic pet store written with the [Spring Boot framework](https://projects.spring.io/spring-boot/). The `StreamLambdaHandler` object is the main entry point for Lambda. -The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `sam.yaml` file in the root folder contains the application definition +The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. -## Installation -To build and install the sample application you will need [Maven](https://maven.apache.org/) and the [AWS CLI](https://aws.amazon.com/cli/) installed on your computer. +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) -In a shell, navigate to the sample's folder and use maven to build a deployable jar. +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package ``` -$ mvn package +$ sam build ``` -This command should generate a `serverless-spring-boot-example-1.0-SNAPSHOT.jar` in the `target` folder. Now that we have generated the jar file, we can use the AWS CLI to package the template for deployment. +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. -You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the sample's folder: +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen ``` -$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket -Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) -Successfully packaged artifacts and wrote output template to file output-sam.yaml. -Execute the following command to deploy the packaged template -aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name +$ sam deploy --guided ``` -As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. - -``` -$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessSpringBootSample --capabilities CAPABILITY_IAM -``` - -Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `SpringBootPetStoreApi` key of the `Outputs` property: - -``` -$ aws cloudformation describe-stacks --stack-name ServerlessSpringBootSample -{ - "Stacks": [ - { - "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/JerseySample/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", - "Description": "Example Pet Store API written with spark with the aws-serverless-java-container library", - "Tags": [], - "Outputs": [ - { - "Description": "URL for application", - "OutputKey": "SpringBootPetStoreApi", - "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets" - } - ], - "CreationTime": "2016-12-13T22:59:31.552Z", - "Capabilities": [ - "CAPABILITY_IAM" - ], - "StackName": "JerseySample", - "NotificationARNs": [], - "StackStatus": "UPDATE_COMPLETE" - } - ] -} +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL ``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +--------------------------------------------------------------------------------------------------------- -Copy the `OutputValue` into a browser to test a first request. +$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +``` \ No newline at end of file diff --git a/samples/springboot/pet-store/build.gradle b/samples/springboot/pet-store/build.gradle index 53295266a..caaeea89c 100644 --- a/samples/springboot/pet-store/build.gradle +++ b/samples/springboot/pet-store/build.gradle @@ -1,6 +1,3 @@ -plugins { - id 'org.springframework.boot' version '1.5.22.RELEASE' -} apply plugin: 'java' repositories { @@ -11,7 +8,7 @@ repositories { dependencies { compile ( - 'org.springframework.boot:spring-boot-starter-web', + 'org.springframework.boot:spring-boot-starter-web:1.5.22.RELEASE', 'com.amazonaws.serverless:aws-serverless-java-container-spring:[1.0,)', 'io.symphonia:lambda-logging:1.0.1' ) diff --git a/samples/springboot/pet-store/sam.yaml b/samples/springboot/pet-store/template.yml similarity index 72% rename from samples/springboot/pet-store/sam.yaml rename to samples/springboot/pet-store/template.yml index 23e143a95..de08aa74b 100644 --- a/samples/springboot/pet-store/sam.yaml +++ b/samples/springboot/pet-store/template.yml @@ -13,20 +13,20 @@ Resources: Properties: Handler: com.amazonaws.serverless.sample.springboot.StreamLambdaHandler::handleRequest Runtime: java8 - CodeUri: target/serverless-spring-boot-example-1.0-SNAPSHOT-lambda-package.zip + CodeUri: . MemorySize: 1512 Policies: AWSLambdaBasicExecutionRole Timeout: 60 Events: - GetResource: - Type: Api + HttpApiEvent: + Type: HttpApi Properties: - Path: /{proxy+} - Method: any + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' Outputs: SpringBootPetStoreApi: Description: URL for application - Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/pets' + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' Export: Name: SpringBootPetStoreApi diff --git a/samples/springboot2/pet-store/README.md b/samples/springboot2/pet-store/README.md index 5f8edb68b..8e15b3773 100644 --- a/samples/springboot2/pet-store/README.md +++ b/samples/springboot2/pet-store/README.md @@ -1,62 +1,36 @@ # Serverless Spring Boot 2 example -A basic pet store written with the [Spring Boot 2 framework](https://projects.spring.io/spring-boot/). The `LambdaHandler` object is the main entry point for Lambda. +A basic pet store written with the [Spring Boot 2 framework](https://projects.spring.io/spring-boot/). The `StreamLambdaHandler` object is the main entry point for Lambda. -The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `sam.yaml` file in the root folder contains the application definition +The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. -## Installation -To build and install the sample application you will need [Maven](https://maven.apache.org/) and the [AWS CLI](https://aws.amazon.com/cli/) installed on your computer. +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) -In a shell, navigate to the sample's folder and use maven to build a deployable jar. +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package ``` -$ mvn package +$ sam build ``` -This command should generate a `serverless-spring-boot-example-1.0-SNAPSHOT.jar` in the `target` folder. Now that we have generated the jar file, we can use the AWS CLI to package the template for deployment. +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. -You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the sample's folder: +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen ``` -$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket -Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) -Successfully packaged artifacts and wrote output template to file output-sam.yaml. -Execute the following command to deploy the packaged template -aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name +$ sam deploy --guided ``` -As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. - -``` -$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessSpringBootSample --capabilities CAPABILITY_IAM -``` - -Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `SpringBootPetStoreApi` key of the `Outputs` property: - -``` -$ aws cloudformation describe-stacks --stack-name ServerlessSpringBootSample -{ - "Stacks": [ - { - "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/JerseySample/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", - "Description": "Example Pet Store API written with spark with the aws-serverless-java-container library", - "Tags": [], - "Outputs": [ - { - "Description": "URL for application", - "OutputKey": "SpringBootPetStoreApi", - "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets" - } - ], - "CreationTime": "2016-12-13T22:59:31.552Z", - "Capabilities": [ - "CAPABILITY_IAM" - ], - "StackName": "JerseySample", - "NotificationARNs": [], - "StackStatus": "UPDATE_COMPLETE" - } - ] -} +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL ``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +--------------------------------------------------------------------------------------------------------- -Copy the `OutputValue` into a browser to test a first request. +$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +``` \ No newline at end of file diff --git a/samples/springboot2/pet-store/build.gradle b/samples/springboot2/pet-store/build.gradle index 65826e900..365f3ac22 100644 --- a/samples/springboot2/pet-store/build.gradle +++ b/samples/springboot2/pet-store/build.gradle @@ -1,6 +1,3 @@ -plugins { - id 'org.springframework.boot' version '2.1.8.RELEASE' -} apply plugin: 'java' repositories { @@ -11,10 +8,10 @@ repositories { dependencies { compile ( - implementation('org.springframework.boot:spring-boot-starter-web:2.1.8.RELEASE') { + implementation('org.springframework.boot:spring-boot-starter-web:2.2.6.RELEASE') { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' }, - 'com.amazonaws.serverless:aws-serverless-java-container-springboot2:[1.0,)', + 'com.amazonaws.serverless:aws-serverless-java-container-springboot2:[1.4,)', 'io.symphonia:lambda-logging:1.0.1' ) testCompile("junit:junit") diff --git a/samples/springboot2/pet-store/pom.xml b/samples/springboot2/pet-store/pom.xml index 547a7ead1..f9444cbf4 100644 --- a/samples/springboot2/pet-store/pom.xml +++ b/samples/springboot2/pet-store/pom.xml @@ -10,7 +10,7 @@ org.springframework.boot spring-boot-starter-parent - 2.1.8.RELEASE + 2.2.6.RELEASE @@ -33,7 +33,7 @@ com.amazonaws.serverless aws-serverless-java-container-springboot2 - [0.1,) + [1.4,) diff --git a/samples/springboot2/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/filter/CognitoIdentityFilter.java b/samples/springboot2/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/filter/CognitoIdentityFilter.java index 3097bb80b..5c3482e40 100644 --- a/samples/springboot2/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/filter/CognitoIdentityFilter.java +++ b/samples/springboot2/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/filter/CognitoIdentityFilter.java @@ -41,6 +41,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo if (apiGwContext == null) { log.warn("API Gateway context is null"); filterChain.doFilter(servletRequest, servletResponse); + return; } if (!AwsProxyRequestContext.class.isAssignableFrom(apiGwContext.getClass())) { log.warn("API Gateway context object is not of valid type"); diff --git a/samples/springboot2/pet-store/sam.yaml b/samples/springboot2/pet-store/template.yml similarity index 72% rename from samples/springboot2/pet-store/sam.yaml rename to samples/springboot2/pet-store/template.yml index c00d45c3f..a82974790 100644 --- a/samples/springboot2/pet-store/sam.yaml +++ b/samples/springboot2/pet-store/template.yml @@ -13,20 +13,20 @@ Resources: Properties: Handler: com.amazonaws.serverless.sample.springboot2.StreamLambdaHandler::handleRequest Runtime: java8 - CodeUri: target/serverless-springboot2-example-1.0-SNAPSHOT-lambda-package.zip + CodeUri: . MemorySize: 1512 Policies: AWSLambdaBasicExecutionRole Timeout: 60 Events: - GetResource: - Type: Api + HttpApiEvent: + Type: HttpApi Properties: - Path: /{proxy+} - Method: any + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' Outputs: SpringBootPetStoreApi: Description: URL for application - Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/pets' + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' Export: Name: SpringBootPetStoreApi diff --git a/samples/struts2/pet-store/README.md b/samples/struts2/pet-store/README.md index 9203dd896..89cc1676b 100644 --- a/samples/struts2/pet-store/README.md +++ b/samples/struts2/pet-store/README.md @@ -1,62 +1,36 @@ # Serverless Struts2 example A basic pet store written with the [Apache Struts framework](https://struts.apache.org). The `Struts2LambdaHandler` object provided by the `aws-serverless-java-container-struts2` is the main entry point for Lambda. -The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `sam.yaml` file in the root folder contains the application definition +The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition -## Installation -To build and install the sample application you will need [Maven](https://maven.apache.org/) and the [AWS CLI](https://aws.amazon.com/cli/) installed on your computer. +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) -In a shell, navigate to the sample's folder and use maven to build a deployable jar. +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package ``` -$ mvn package +$ sam build ``` -This command should generate a `serverless-struts-example-1.0-SNAPSHOT.jar` in the `target` folder. Now that we have generated the zip file, we can use the AWS CLI to package the template for deployment. +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. -You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the sample's folder: +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen ``` -$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket -Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) -Successfully packaged artifacts and wrote output template to file output-sam.yaml. -Execute the following command to deploy the packaged template -aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name +$ sam deploy --guided ``` -As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. - -``` -$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessSpringSample --capabilities CAPABILITY_IAM -``` - -Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `SparkPetStoreApi` key of the `Outputs` property: - -``` -$ aws cloudformation describe-stacks --stack-name ServerlessSpringSample -{ - "Stacks": [ - { - "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/JerseySample/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", - "Description": "Example Pet Store API written with Apache Struts with the aws-serverless-java-container library", - "Tags": [], - "Outputs": [ - { - "Description": "URL for application", - "OutputKey": "PetStoreApi", - "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets" - } - ], - "CreationTime": "2016-12-13T22:59:31.552Z", - "Capabilities": [ - "CAPABILITY_IAM" - ], - "StackName": "StrutsSample", - "NotificationARNs": [], - "StackStatus": "UPDATE_COMPLETE" - } - ] -} +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL ``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +--------------------------------------------------------------------------------------------------------- -Copy the `OutputValue` into a browser to test a first request. +$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +``` \ No newline at end of file diff --git a/samples/struts2/pet-store/build.gradle b/samples/struts2/pet-store/build.gradle index 2d7e42500..24f018973 100644 --- a/samples/struts2/pet-store/build.gradle +++ b/samples/struts2/pet-store/build.gradle @@ -8,13 +8,13 @@ repositories { dependencies { compile ( 'com.amazonaws.serverless:aws-serverless-java-container-struts2:[1.0,)', - 'org.apache.struts:struts2-convention-plugin:2.5.20', - 'org.apache.struts:struts2-rest-plugin:2.5.20', - 'org.apache.struts:struts2-bean-validation-plugin:2.5.20', - 'org.apache.struts:struts2-junit-plugin:2.5.20', + 'org.apache.struts:struts2-convention-plugin:2.5.22', + 'org.apache.struts:struts2-rest-plugin:2.5.22', + 'org.apache.struts:struts2-bean-validation-plugin:2.5.22', + 'org.apache.struts:struts2-junit-plugin:2.5.22', 'com.jgeppert.struts2:struts2-aws-lambda-support-plugin:1.1.0', 'org.hibernate:hibernate-validator:4.3.2.Final', - 'com.fasterxml.jackson.core:jackson-databind:2.10.1', + 'com.fasterxml.jackson.core:jackson-databind:2.10.3', 'org.apache.logging.log4j:log4j-core:2.8.2', 'org.apache.logging.log4j:log4j-api:2.8.2', 'org.apache.logging.log4j:log4j-slf4j-impl:2.8.2', diff --git a/samples/struts2/pet-store/pom.xml b/samples/struts2/pet-store/pom.xml index 9689fc6a7..b761207d6 100644 --- a/samples/struts2/pet-store/pom.xml +++ b/samples/struts2/pet-store/pom.xml @@ -26,8 +26,8 @@ 1.8 1.8 - 2.5.20 - 2.10.1 + 2.5.22 + 2.10.3 4.12 2.11.1 diff --git a/samples/struts2/pet-store/sam.yaml b/samples/struts2/pet-store/template.yml similarity index 72% rename from samples/struts2/pet-store/sam.yaml rename to samples/struts2/pet-store/template.yml index c44aa3201..72128dd5b 100644 --- a/samples/struts2/pet-store/sam.yaml +++ b/samples/struts2/pet-store/template.yml @@ -13,20 +13,20 @@ Resources: Properties: Handler: com.amazonaws.serverless.proxy.struts2.Struts2LambdaHandler::handleRequest Runtime: java8 - CodeUri: target/serverless-struts-example-1.0-SNAPSHOT-lambda.zip + CodeUri: . MemorySize: 256 Policies: AWSLambdaBasicExecutionRole Timeout: 30 Events: - GetResource: - Type: Api + HttpApiEvent: + Type: HttpApi Properties: - Path: /{proxy+} - Method: any + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' Outputs: SpringPetStoreApi: Description: URL for application - Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/pets' + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' Export: Name: Struts2PetStoreApi