diff --git a/aws-serverless-java-container-core/pom.xml b/aws-serverless-java-container-core/pom.xml index a6278f9e3..aa855fdd6 100644 --- a/aws-serverless-java-container-core/pom.xml +++ b/aws-serverless-java-container-core/pom.xml @@ -27,6 +27,13 @@ 1.2.3 + + org.jetbrains + annotations + 24.0.1 + provided + + jakarta.servlet jakarta.servlet-api diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsVPCLatticeV2SecurityContextWriter.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsVPCLatticeV2SecurityContextWriter.java new file mode 100644 index 000000000..0f3d75e27 --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsVPCLatticeV2SecurityContextWriter.java @@ -0,0 +1,13 @@ +package com.amazonaws.serverless.proxy; + +import com.amazonaws.serverless.proxy.internal.jaxrs.AwsVpcLatticeV2SecurityContext; +import com.amazonaws.serverless.proxy.model.VPCLatticeV2RequestEvent; +import com.amazonaws.services.lambda.runtime.Context; +import jakarta.ws.rs.core.SecurityContext; + +public class AwsVPCLatticeV2SecurityContextWriter implements SecurityContextWriter{ + @Override + public SecurityContext writeSecurityContext(VPCLatticeV2RequestEvent event, Context lambdaContext) { + return new AwsVpcLatticeV2SecurityContext(lambdaContext, event); + } +} 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 d8293d649..a2fb32b80 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 @@ -40,6 +40,11 @@ public abstract class RequestReader { */ public static final String API_GATEWAY_CONTEXT_PROPERTY = "com.amazonaws.apigateway.request.context"; + /** + * The key for the VPC Lattice V2 context property in the PropertiesDelegate object + */ + public static final String VPC_LATTICE_V2_CONTEXT_PROPERTY = "com.amazonaws.vpclattice.request.context"; + /** * The key for the API Gateway stage variables property in the PropertiesDelegate object */ @@ -55,6 +60,11 @@ public abstract class RequestReader { */ public static final String API_GATEWAY_EVENT_PROPERTY = "com.amazonaws.apigateway.request"; + /** + * The key to store the entire VPC Lattice V2 event + */ + public static final String VPC_LATTICE_V2_EVENT_PROPERTY = "com.amazonaws.vpclattice.request"; + /** * The key for the AWS Lambda context property in the PropertiesDelegate object */ diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/jaxrs/AwsVpcLatticeV2SecurityContext.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/jaxrs/AwsVpcLatticeV2SecurityContext.java new file mode 100644 index 000000000..71a58d90d --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/jaxrs/AwsVpcLatticeV2SecurityContext.java @@ -0,0 +1,59 @@ +package com.amazonaws.serverless.proxy.internal.jaxrs; + +import com.amazonaws.serverless.proxy.model.VPCLatticeV2RequestEvent; +import com.amazonaws.services.lambda.runtime.Context; +import jakarta.ws.rs.core.SecurityContext; + +import java.security.Principal; +import java.util.Objects; + +/** + * default implementation of the SecurityContext object. This class supports 1 VPC Lattice authentication type: + * AWS_IAM. + */ +public class AwsVpcLatticeV2SecurityContext implements SecurityContext { + + static final String AUTH_SCHEME_AWS_IAM = "AWS_IAM"; + + + private final VPCLatticeV2RequestEvent event; + + public AwsVpcLatticeV2SecurityContext(Context lambdaContext, VPCLatticeV2RequestEvent event) { + this.event = event; + } + + //------------------------------------------------------------- + // Implementation - SecurityContext + //------------------------------------------------------------- + @Override + public Principal getUserPrincipal() { + if (Objects.equals(getAuthenticationScheme(), AUTH_SCHEME_AWS_IAM)) { + return () -> getEvent().getRequestContext().getIdentity().getPrincipal(); + } + return null; + } + + private VPCLatticeV2RequestEvent getEvent() { + return event; + } + + + @Override + public boolean isUserInRole(String role) { + return role.equals(event.getRequestContext().getIdentity().getPrincipal()); + } + + @Override + public boolean isSecure() { + return getAuthenticationScheme() != null; + } + + @Override + public String getAuthenticationScheme() { + if (Objects.equals(getEvent().getRequestContext().getIdentity().getType(), AUTH_SCHEME_AWS_IAM)) { + return AUTH_SCHEME_AWS_IAM; + } else { + return null; + } + } +} 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 index 6fdb31f08..6d2486803 100644 --- 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 @@ -47,7 +47,6 @@ public class AwsHttpApiV2ProxyHttpServletRequest extends AwsHttpServletRequest { private MultiValuedTreeMap queryString; private Headers headers; private ContainerConfig config; - private SecurityContext securityContext; private AwsAsyncContext asyncContext; /** @@ -57,10 +56,9 @@ public class AwsHttpApiV2ProxyHttpServletRequest extends AwsHttpServletRequest { * @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); + super(lambdaContext, sc); request = req; config = cfg; - securityContext = sc; queryString = parseRawQueryString(request.getRawQueryString()); headers = headersMapToMultiValue(request.getHeaders()); } @@ -69,12 +67,6 @@ public HttpApiV2ProxyRequest getRequest() { return request; } - @Override - public String getAuthType() { - // TODO - return null; - } - @Override public Cookie[] getCookies() { Cookie[] rhc; @@ -108,56 +100,27 @@ public Cookie[] getCookies() { @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; - } + return getDateHeader(s, headers); } @Override public String getHeader(String s) { - if (headers == null) { - return null; - } - return headers.getFirst(s); + return getHeader(s, headers); } @Override public Enumeration getHeaders(String s) { - if (headers == null || !headers.containsKey(s)) { - return Collections.emptyEnumeration(); - } - return Collections.enumeration(headers.get(s)); + return getHeaders(s, headers); } @Override public Enumeration getHeaderNames() { - if (headers == null) { - return Collections.emptyEnumeration(); - } - return Collections.enumeration(headers.keySet()); + return getHeaderNames(headers); } @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); + return getIntHeader(s, headers); } @Override @@ -187,28 +150,6 @@ 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()); @@ -219,27 +160,6 @@ 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 T upgrade(Class aClass) throws IOException, ServletException { - throw new UnsupportedOperationException(); - } - @Override public String getCharacterEncoding() { if (headers == null) { @@ -250,30 +170,17 @@ public String getCharacterEncoding() { @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)); + setCharacterEncoding(s, headers); } @Override public int getContentLength() { - String headerValue = headers.getFirst(HttpHeaders.CONTENT_LENGTH); - if (headerValue == null) { - return -1; - } - return Integer.parseInt(headerValue); + return getContentLength(headers); } @Override public long getContentLengthLong() { - String headerValue = headers.getFirst(HttpHeaders.CONTENT_LENGTH); - if (headerValue == null) { - return -1; - } - return Long.parseLong(headerValue); + return getContentLengthLong(headers); } @Override @@ -286,17 +193,7 @@ public String getContentType() { @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]; - } + return getParameter(queryString, s, config.isQueryStringCaseSensitive()); } @Override @@ -315,7 +212,7 @@ public String[] getParameterValues(String s) { values.addAll(Arrays.asList(getFormBodyParameterCaseInsensitive(s))); - if (values.size() == 0) { + if (values.isEmpty()) { return null; } else { return values.toArray(new String[0]); @@ -409,16 +306,6 @@ public Enumeration getLocales() { return Collections.enumeration(locales); } - @Override - public boolean isSecure() { - return securityContext.isSecure(); - } - - @Override - public RequestDispatcher getRequestDispatcher(String s) { - return getServletContext().getRequestDispatcher(s); - } - @Override public int getRemotePort() { return 0; @@ -456,6 +343,8 @@ public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse se return asyncContext; } + + @Override public AsyncContext getAsyncContext() { if (asyncContext == null) { @@ -475,11 +364,6 @@ public String getProtocolRequestId() { return ""; } - @Override - public ServletConnection getServletConnection() { - return null; - } - private MultiValuedTreeMap parseRawQueryString(String qs) { if (qs == null || "".equals(qs.trim())) { return new MultiValuedTreeMap<>(); @@ -505,7 +389,7 @@ private MultiValuedTreeMap parseRawQueryString(String qs) { return qsMap; } - private Headers headersMapToMultiValue(Map headers) { + protected static Headers headersMapToMultiValue(Map headers) { if (headers == null || headers.size() == 0) { return new Headers(); } 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 b76fd216e..05bf1a792 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 @@ -22,6 +22,8 @@ import com.amazonaws.serverless.proxy.model.MultiValuedTreeMap; import com.amazonaws.services.lambda.runtime.Context; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.SecurityContext; import org.apache.commons.fileupload2.core.DiskFileItem; import org.apache.commons.fileupload2.core.FileItem; import org.apache.commons.fileupload2.core.FileUploadException; @@ -43,7 +45,11 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.Charset; +import java.security.Principal; +import java.time.Instant; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -74,6 +80,8 @@ public abstract class AwsHttpServletRequest implements HttpServletRequest { // information from anywhere else static final String CF_PROTOCOL_HEADER_NAME = "CloudFront-Forwarded-Proto"; static final String PROTOCOL_HEADER_NAME = "X-Forwarded-Proto"; + static final String CLIENT_IP_HEADER_NAME = "X-Forwarded-For"; + static final String HOST_HEADER_NAME = "Host"; static final String PORT_HEADER_NAME = "X-Forwarded-Port"; static final String CLIENT_IP_HEADER = "X-Forwarded-For"; @@ -94,6 +102,7 @@ public abstract class AwsHttpServletRequest implements HttpServletRequest { protected AwsHttpServletResponse response; protected AwsLambdaServletContainerHandler containerHandler; protected ServletInputStream requestInputStream; + private final SecurityContext securityContext; private static Logger log = LoggerFactory.getLogger(AwsHttpServletRequest.class); @@ -107,9 +116,11 @@ public abstract class AwsHttpServletRequest implements HttpServletRequest { * 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 + * @param securityContext The security context */ - protected AwsHttpServletRequest(Context lambdaContext) { + protected AwsHttpServletRequest(Context lambdaContext, SecurityContext securityContext) { this.lambdaContext = lambdaContext; + this.securityContext = securityContext; attributes = new HashMap<>(); setAttribute(DISPATCHER_TYPE_ATTRIBUTE, DispatcherType.REQUEST); } @@ -269,14 +280,74 @@ public DispatcherType getDispatcherType() { return DispatcherType.REQUEST; } + @Override + public RequestDispatcher getRequestDispatcher(String path) { + return getServletContext().getRequestDispatcher(path); + } + @Override public String getServletPath() { // we always work on the root path return ""; } + @Override + public ServletConnection getServletConnection() { + return null; + } - //------------------------------------------------------------- + @Override + public String getAuthType() { + return securityContext.getAuthenticationScheme(); + } + + @Override + public boolean isSecure() { + return securityContext.isSecure(); + } + + @Override + public Principal getUserPrincipal() { + if (securityContext == null) { + return null; + } + return securityContext.getUserPrincipal(); + } + + @Override + public String getRemoteUser() { + if (getUserPrincipal() == null) { + return null; + } + return getUserPrincipal().getName(); + } + + @Override + public boolean isUserInRole(String role) { + return securityContext.isUserInRole(role); + } + + @Override + public boolean authenticate(HttpServletResponse httpServletResponse) throws IOException, ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public void login(String username, String password) throws ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public void logout() throws ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public T upgrade(Class handlerClass) throws IOException, ServletException { + throw new UnsupportedOperationException(); + } + +//------------------------------------------------------------- // Methods - Getter/Setter //------------------------------------------------------------- @@ -603,6 +674,18 @@ protected List getQueryParamValuesAsList(MultiValuedTreeMap qs, String s, boolean isCaseSensitive) { + List values = new ArrayList<>(Arrays.asList(getQueryParamValues(qs, s, isCaseSensitive))); + + values.addAll(Arrays.asList(getFormBodyParameterCaseInsensitive(s))); + + if (values.isEmpty()) { + return null; + } else { + return values.toArray(new String[0]); + } + } + protected Map generateParameterMap(MultiValuedTreeMap qs, ContainerConfig config) { Map output; @@ -825,6 +908,99 @@ static String decodeValueIfEncoded(String value) { } + + /** + * These are helper methods meant to reduce duplicated code in servletRequest classes. + */ + + protected long getDateHeader(String s, Headers headers) { + 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; + } + } + + protected String getHeader(String s, Headers headers) { + if (headers == null) { + return null; + } + return headers.getFirst(s); + } + + protected Enumeration getHeaders(String s, Headers headers) { + if (headers == null || !headers.containsKey(s)) { + return Collections.emptyEnumeration(); + } + return Collections.enumeration(headers.get(s)); + } + + protected Enumeration getHeaderNames(Headers headers) { + if (headers == null) { + return Collections.emptyEnumeration(); + } + return Collections.enumeration(headers.keySet()); + } + + protected int getIntHeader(String s, Headers headers) { + if (headers == null) { + return -1; + } + String headerValue = headers.getFirst(s); + if (headerValue == null || "".equals(headerValue)) { + return -1; + } + + return Integer.parseInt(headerValue); + } + + protected void setCharacterEncoding(String s, Headers headers) 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)); + } + + protected int getContentLength(Headers headers) { + String headerValue = headers.getFirst(HttpHeaders.CONTENT_LENGTH); + if (headerValue == null) { + return -1; + } + return Integer.parseInt(headerValue); + } + + protected long getContentLengthLong(Headers headers) { + String headerValue = headers.getFirst(HttpHeaders.CONTENT_LENGTH); + if (headerValue == null) { + return -1; + } + return Long.parseLong(headerValue); + } + + protected String getParameter(MultiValuedTreeMap qs, String s, boolean isCaseSensitive) { + String queryStringParameter = getFirstQueryParamValue(qs, s, isCaseSensitive); + if (queryStringParameter != null) { + return queryStringParameter; + } + + String[] bodyParams = getFormBodyParameterCaseInsensitive(s); + if (bodyParams.length == 0) { + return null; + } else { + return bodyParams[0]; + } + } + /** * Class that represents a header value. */ 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 17d04a57c..2b38213ce 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 @@ -55,7 +55,6 @@ public class AwsProxyHttpServletRequest extends AwsHttpServletRequest { //------------------------------------------------------------- private AwsProxyRequest request; - private SecurityContext securityContext; private AwsAsyncContext asyncContext; private static Logger log = LoggerFactory.getLogger(AwsProxyHttpServletRequest.class); private ContainerConfig config; @@ -71,9 +70,8 @@ public AwsProxyHttpServletRequest(AwsProxyRequest awsProxyRequest, Context lambd public AwsProxyHttpServletRequest(AwsProxyRequest awsProxyRequest, Context lambdaContext, SecurityContext awsSecurityContext, ContainerConfig config) { - super(lambdaContext); + super(lambdaContext, awsSecurityContext); this.request = awsProxyRequest; - this.securityContext = awsSecurityContext; this.config = config; } @@ -85,13 +83,6 @@ public AwsProxyRequest getAwsProxyRequest() { // Implementation - HttpServletRequest //------------------------------------------------------------- - - @Override - public String getAuthType() { - return securityContext.getAuthenticationScheme(); - } - - @Override public Cookie[] getCookies() { if (request.getMultiValueHeaders() == null) { @@ -107,22 +98,9 @@ public Cookie[] getCookies() { @Override public long getDateHeader(String s) { - if (request.getMultiValueHeaders() == null) { - return -1L; - } - String dateString = request.getMultiValueHeaders().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; - } + return getDateHeader(s, request.getMultiValueHeaders()); } - @Override public String getHeader(String s) { List values = getHeaderValues(s); @@ -135,33 +113,19 @@ public String getHeader(String s) { @Override public Enumeration getHeaders(String s) { - if (request.getMultiValueHeaders() == null || request.getMultiValueHeaders().get(s) == null) { - return Collections.emptyEnumeration(); - } - return Collections.enumeration(request.getMultiValueHeaders().get(s)); + return getHeaders(s, request.getMultiValueHeaders()); } @Override public Enumeration getHeaderNames() { - if (request.getMultiValueHeaders() == null) { - return Collections.emptyEnumeration(); - } - return Collections.enumeration(request.getMultiValueHeaders().keySet()); + return getHeaderNames(request.getMultiValueHeaders()); } @Override public int getIntHeader(String s) { - if (request.getMultiValueHeaders() == null) { - return -1; - } - String headerValue = request.getMultiValueHeaders().getFirst(s); - if (headerValue == null) { - return -1; - } - - return Integer.parseInt(headerValue); + return getIntHeader(s, request.getMultiValueHeaders()); } @@ -205,26 +169,6 @@ public String getQueryString() { } } - - @Override - public String getRemoteUser() { - return securityContext.getUserPrincipal().getName(); - } - - - @Override - public boolean isUserInRole(String s) { - // TODO: Not supported? - return false; - } - - - @Override - public Principal getUserPrincipal() { - return securityContext.getUserPrincipal(); - } - - @Override public String getRequestURI() { return cleanUri(getContextPath()) + cleanUri(request.getPath()); @@ -236,33 +180,6 @@ public StringBuffer getRequestURL() { return generateRequestURL(request.getPath()); } - - @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 T upgrade(Class aClass) - throws IOException, ServletException { - throw new UnsupportedOperationException(); - } - //------------------------------------------------------------- // Implementation - ServletRequest //------------------------------------------------------------- @@ -295,21 +212,13 @@ public void setCharacterEncoding(String s) @Override public int getContentLength() { - String headerValue = request.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_LENGTH); - if (headerValue == null) { - return -1; - } - return Integer.parseInt(headerValue); + return getContentLength(request.getMultiValueHeaders()); } @Override public long getContentLengthLong() { - String headerValue = request.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_LENGTH); - if (headerValue == null) { - return -1; - } - return Long.parseLong(headerValue); + return getContentLengthLong(request.getMultiValueHeaders()); } @@ -325,20 +234,9 @@ public String getContentType() { @Override public String getParameter(String s) { - String queryStringParameter = getFirstQueryParamValue(request.getMultiValueQueryStringParameters(), s, config.isQueryStringCaseSensitive()); - if (queryStringParameter != null) { - return queryStringParameter; - } - - String[] bodyParams = getFormBodyParameterCaseInsensitive(s); - if (bodyParams.length == 0) { - return null; - } else { - return bodyParams[0]; - } + return getParameter(request.getMultiValueQueryStringParameters(), s, config.isQueryStringCaseSensitive()); } - @Override public Enumeration getParameterNames() { Set formParameterNames = getFormUrlEncodedParametersMap().keySet(); @@ -469,18 +367,6 @@ public Enumeration getLocales() { return Collections.enumeration(locales); } - @Override - public boolean isSecure() { - return securityContext.isSecure(); - } - - - @Override - public RequestDispatcher getRequestDispatcher(String s) { - return getServletContext().getRequestDispatcher(s); - } - - @Override public int getRemotePort() { if (Objects.nonNull(request.getRequestContext().getElb())) { @@ -548,11 +434,6 @@ public String getProtocolRequestId() { return ""; } - @Override - public ServletConnection getServletConnection() { - return null; - } - //------------------------------------------------------------- // Methods - Private //------------------------------------------------------------- diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsVpcLatticeV2HttpServletRequest.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsVpcLatticeV2HttpServletRequest.java new file mode 100644 index 000000000..638132ab6 --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsVpcLatticeV2HttpServletRequest.java @@ -0,0 +1,343 @@ +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.*; +import com.amazonaws.services.lambda.runtime.Context; +import jakarta.servlet.*; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpUpgradeHandler; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.SecurityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.security.Principal; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Implementation of the HttpServletRequest interface that supports VPCLatticeV2RequestEvent object. + * This object is initialized with an VPCLatticeV2RequestEvent event and a SecurityContext generated + * by an implementation of the SecurityContextWriter. + */ +public class AwsVpcLatticeV2HttpServletRequest extends AwsHttpServletRequest { + + //------------------------------------------------------------- + // Variables - Private + //------------------------------------------------------------- + + + private final VPCLatticeV2RequestEvent request; + private final MultiValuedTreeMap queryString; + private final Headers headers; + private AwsAsyncContext asyncContext; + private final Context lambdaContext; + private static final Logger log = LoggerFactory.getLogger(AwsVpcLatticeV2HttpServletRequest.class); + private final ContainerConfig config; + + //------------------------------------------------------------- + // Constructors + //------------------------------------------------------------- + + + public AwsVpcLatticeV2HttpServletRequest(VPCLatticeV2RequestEvent vpcLatticeV2Request, Context lambdaContext, SecurityContext awsSecurityContext) { + this(vpcLatticeV2Request, lambdaContext, awsSecurityContext, LambdaContainerHandler.getContainerConfig()); + } + + + public AwsVpcLatticeV2HttpServletRequest(VPCLatticeV2RequestEvent vpcLatticeV2Request, Context lambdaContext, SecurityContext awsSecurityContext, ContainerConfig config) { + super(lambdaContext, awsSecurityContext); + this.request = vpcLatticeV2Request; + this.lambdaContext = lambdaContext; + this.config = config; + headers = request.getHeaders(); + queryString = queryStringToMultiValue(request.getQueryStringParameters()); + } + + @Override + public Cookie[] getCookies() { + if (headers == null || !headers.containsKey(HttpHeaders.COOKIE)) { + return new Cookie[0]; + } else { + return parseCookieHeaderValue(headers.getFirst(HttpHeaders.COOKIE)); + } + } + + @Override + public long getDateHeader(String s) { + return getDateHeader(s, headers); + } + + @Override + public String getHeader(String s) { + return getHeader(s, headers); + } + + @Override + public Enumeration getHeaders(String s) { + return getHeaders(s, headers); + } + + @Override + public Enumeration getHeaderNames() { + return getHeaderNames(headers); + } + + @Override + public int getIntHeader(String s) { + return getIntHeader(s, headers); + } + + @Override + public String getMethod() { + return request.getMethod(); + } + + @Override + public String getPathInfo() { + String pathInfo = cleanUri(request.getPath()); + return decodeRequestPath(pathInfo, LambdaContainerHandler.getContainerConfig()); + } + + @Override + public String getPathTranslated() { + return null; + } + + @Override + public String getContextPath() { + return generateContextPath(config, null); + } + + @Override + public String getQueryString() { + + if (Objects.isNull(queryString)) + return null; + + try { + StringBuilder queryStringBuilder = new StringBuilder(); + + try { + for (String key : queryString.keySet()) { + String val = queryString.getFirst(key); + queryStringBuilder.append("&"); + queryStringBuilder.append(URLEncoder.encode(key, config.getUriEncoding())); + queryStringBuilder.append("="); + if (val != null) { + queryStringBuilder.append(URLEncoder.encode(val, config.getUriEncoding())); + } + } + } catch (UnsupportedEncodingException e) { + throw new ServletException("Invalid charset passed for query string encoding", e); + } + + return queryStringBuilder.substring(1); // remove the first & - faster to do it here than adding logic in the Lambda + + } catch (ServletException e) { + log.error("Could not generate query string", e); + return null; + } + } + + @Override + public String getRequestURI() { + return cleanUri(getContextPath()) + cleanUri(request.getPath()); + } + + @Override + public StringBuffer getRequestURL() { + return generateRequestURL(request.getPath()); + } + + @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 { + setCharacterEncoding(s, headers); + } + + @Override + public int getContentLength() { + return getContentLength(headers); + } + + @Override + public long getContentLengthLong() { + return getContentLengthLong(headers); + } + + @Override + public String getContentType() { + return headers.getFirst(HttpHeaders.CONTENT_TYPE); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + if (requestInputStream == null) { + requestInputStream = new AwsServletInputStream(bodyStringToInputStream(request.getBody(), Boolean.TRUE.equals(request.getIsBase64Encoded()))); + } + return requestInputStream; + } + + @Override + public String getParameter(String s) { + return getParameter(queryString, s, config.isQueryStringCaseSensitive()); + } + + @Override + public Enumeration getParameterNames() { + Set formParameterNames = getFormUrlEncodedParametersMap().keySet(); + if (queryString == null) { + return Collections.enumeration(formParameterNames); + } + return Collections.enumeration(Stream.concat(formParameterNames.stream(), + queryString.keySet().stream()).collect(Collectors.toSet())); + } + + @Override + public String[] getParameterValues(String s) { + return getParameterValues(queryString, s, config.isQueryStringCaseSensitive()); + } + + @Override + public Map getParameterMap() { + return generateParameterMap(queryString, config); + } + + @Override + public String getProtocol() { + // No protocol on the request payload. Defaulting to "HTTP/1.1". Should we return UnsupportedOperationException instead? + return "HTTP/1.1"; + } + + @Override + public String getScheme() { + return getSchemeFromHeader(request.getHeaders()); + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new StringReader(request.getBody())); + } + + @Override + public String getRemoteAddr() { + return request.getHeaders().getFirst(CLIENT_IP_HEADER_NAME); + } + + @Override + public String getRemoteHost() { + return request.getHeaders().getFirst(HttpHeaders.HOST); + } + + @Override + public Locale getLocale() { + List locales = parseAcceptLanguageHeader(headers.getFirst(HttpHeaders.ACCEPT_LANGUAGE)); + return locales.isEmpty() ? Locale.getDefault() : locales.get(0); + } + + @Override + public Enumeration getLocales() { + List locales = parseAcceptLanguageHeader(headers.getFirst(HttpHeaders.ACCEPT_LANGUAGE)); + return Collections.enumeration(locales); + } + + @Override + public int getRemotePort() { + return 0; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + asyncContext = new AwsAsyncContext(this, response); + setAttribute(DISPATCHER_TYPE_ATTRIBUTE, DispatcherType.ASYNC); + log.debug("Starting async context for request: " + SecurityUtils.crlf(lambdaContext.getAwsRequestId())); + return asyncContext; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException { + asyncContext = new AwsAsyncContext((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); + setAttribute(DISPATCHER_TYPE_ATTRIBUTE, DispatcherType.ASYNC); + log.debug("Starting async context for request: " + SecurityUtils.crlf(lambdaContext.getAwsRequestId())); + return asyncContext; + } + + @Override + public boolean isAsyncSupported() { + return true; + } + + @Override + public boolean isAsyncStarted() { + if (asyncContext == null) { + return false; + } + return !asyncContext.isCompleted() && !asyncContext.isDispatched(); + } + + @Override + public AsyncContext getAsyncContext() { + if (asyncContext == null) { + throw new IllegalStateException("Request " + SecurityUtils.crlf(lambdaContext.getAwsRequestId()) + + " is not in asynchronous mode. Call startAsync before attempting to get the async context."); + } + return asyncContext; + } + + @Override + public String getRequestId() { + return lambdaContext.getAwsRequestId(); + } + + @Override + public String getProtocolRequestId() { + return ""; + } + + @Override + public int getServerPort() { + if (request.getHeaders() == null) { + return 443; + } + String port = request.getHeaders().getFirst(PORT_HEADER_NAME); + if (SecurityUtils.isValidPort(port)) { + return Integer.parseInt(port); + } else { + return 443; // default port + } + } + + @Override + public String getServerName() { + return request.getHeaders().getFirst(HOST_HEADER_NAME); + } + + protected static MultiValuedTreeMap queryStringToMultiValue(Map qs) { + if (qs == null || qs.isEmpty()) { + return null; + } + MultiValuedTreeMap qsMap = new MultiValuedTreeMap<>(); + for (Map.Entry kv : qs.entrySet()) { + qsMap.add(kv.getKey(), kv.getValue()); + } + return qsMap; + } + +} diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsVpcLatticeV2HttpServletRequestReader.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsVpcLatticeV2HttpServletRequestReader.java new file mode 100644 index 000000000..722e8fbd3 --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsVpcLatticeV2HttpServletRequestReader.java @@ -0,0 +1,36 @@ +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.VPCLatticeV2RequestEvent; +import com.amazonaws.services.lambda.runtime.Context; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.core.SecurityContext; + +public class AwsVpcLatticeV2HttpServletRequestReader 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"; + @Override + public AwsVpcLatticeV2HttpServletRequest readRequest(VPCLatticeV2RequestEvent request, SecurityContext securityContext, Context lambdaContext, ContainerConfig config) throws InvalidRequestEventException { + if ( request.getMethod() == null || request.getMethod().isEmpty() || request.getRequestContext() == null) { + throw new InvalidRequestEventException(INVALID_REQUEST_ERROR); + } + + // clean out the request path based on the container config + request.setPath(stripBasePath(request.getPath(), config)); + + AwsVpcLatticeV2HttpServletRequest servletRequest = new AwsVpcLatticeV2HttpServletRequest(request, lambdaContext, securityContext, config); + servletRequest.setAttribute(VPC_LATTICE_V2_CONTEXT_PROPERTY, request.getRequestContext()); + servletRequest.setAttribute(VPC_LATTICE_V2_EVENT_PROPERTY, request); + servletRequest.setAttribute(LAMBDA_CONTEXT_PROPERTY, lambdaContext); + servletRequest.setAttribute(JAX_SECURITY_CONTEXT_PROPERTY, securityContext); + + return servletRequest; + } + + @Override + protected Class getRequestClass() { + return VPCLatticeV2RequestEvent.class; + } +} 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 a2c1c73ff..25fa09c73 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 @@ -18,6 +18,7 @@ import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.model.VPCLatticeV2RequestEvent; import jakarta.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; @@ -119,6 +120,17 @@ public Builder defaultHttpApiV2Proxy() { } + public Builder defaultVpcLatticeV2Proxy() { + initializationWrapper(new AsyncInitializationWrapper()) + .requestReader((RequestReader) new AwsVpcLatticeV2HttpServletRequestReader()) + .responseWriter((ResponseWriter) new AwsProxyHttpServletResponseWriter(true)) + .securityContextWriter((SecurityContextWriter) new AwsVPCLatticeV2SecurityContextWriter()) + .exceptionHandler(defaultExceptionHandler()) + .requestTypeClass((Class) VPCLatticeV2RequestEvent.class) + .responseTypeClass((Class) AwsProxyResponse.class); + return self(); + } + protected ExceptionHandler defaultExceptionHandler() { return (ExceptionHandler) new AwsProxyExceptionHandler(); } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/RequestSource.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/RequestSource.java index c819fdcc0..7bf292452 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/RequestSource.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/RequestSource.java @@ -14,5 +14,7 @@ public enum RequestSource { ALB, - API_GATEWAY + API_GATEWAY, + VPC_LATTICE_V2, + VPC_LATTICE_V1 } \ No newline at end of file diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/VPCLatticeV2RequestEvent.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/VPCLatticeV2RequestEvent.java new file mode 100644 index 000000000..cc99c4b75 --- /dev/null +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/VPCLatticeV2RequestEvent.java @@ -0,0 +1,249 @@ +package com.amazonaws.serverless.proxy.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Map; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class VPCLatticeV2RequestEvent { + private String version; + private String path; + private String method; + private Headers headers; + @Nullable + private Map queryStringParameters; + private RequestContext requestContext; + private String body; + + /*** + * isBase64Encoded is set if the body is a base64 encoded String. + */ + @Nullable + private Boolean isBase64Encoded; + + @JsonIgnore + public RequestSource getRequestSource() { + return RequestSource.VPC_LATTICE_V2; + } + + public RequestContext getRequestContext() { + return requestContext; + } + + public String getVersion() { + return version; + } + + public String getPath() { + return path; + } + + public String getMethod() { + return method; + } + + public String getBody() { + return body; + } + + public Headers getHeaders() { + return headers; + } + + public @Nullable Boolean getIsBase64Encoded() { + return isBase64Encoded; + } + + public @Nullable Map getQueryStringParameters() { + return queryStringParameters; + } + + public void setPath(String s) { + this.path = s; + } + + public void setVersion(String version) { + this.version = version; + } + + public void setMethod(String method) { + this.method = method; + } + + public void setHeaders(Headers headers) { + this.headers = headers; + } + + public void setQueryStringParameters(@Nullable Map queryStringParameters) { + this.queryStringParameters = queryStringParameters; + } + + public void setBody(String body) { + this.body = body; + } + + public void setBase64Encoded(Boolean base64Encoded) { + isBase64Encoded = base64Encoded; + } + + public void setRequestContext(RequestContext requestContext) { + this.requestContext = requestContext; + } + + public static class RequestContext { + private String serviceNetworkArn; + private String serviceArn; + private String targetGroupArn; + private Identity identity; + private String region; + /** + * Number of microseconds from the epoch + */ + private String timeEpoch; + + @JsonIgnore + public LocalDateTime getLocalDateTime() { + long epochMicroseconds = Long.parseLong(timeEpoch); + long epochMilliseconds = epochMicroseconds / 1000; + + return Instant.ofEpochMilli(epochMilliseconds).atZone(ZoneOffset.UTC).toLocalDateTime(); + } + + public Identity getIdentity() { + return identity; + } + + public String getTimeEpoch() { + return timeEpoch; + } + + public String getRegion() { + return region; + } + + public String getServiceNetworkArn() { + return serviceNetworkArn; + } + + public String getServiceArn() { + return serviceArn; + } + + public String getTargetGroupArn() { + return targetGroupArn; + } + + public void setServiceNetworkArn(String serviceNetworkArn) { + this.serviceNetworkArn = serviceNetworkArn; + } + + public void setServiceArn(String serviceArn) { + this.serviceArn = serviceArn; + } + + public void setTargetGroupArn(String targetGroupArn) { + this.targetGroupArn = targetGroupArn; + } + + public void setIdentity(Identity identity) { + this.identity = identity; + } + + public void setRegion(String region) { + this.region = region; + } + + public void setTimeEpoch(String timeEpoch) { + this.timeEpoch = timeEpoch; + } + } + + public static class Identity { + private String sourceVpcArn; + private String type; + private String principal; + private String sessionName; + private String x509SanDns; + private String x509SanNameCn; + private String x509SubjectCn; + private String x509IssuerOu; + private String x509SanUri; + + public String getPrincipal() { + return principal; + } + + public String getType() { + return type; + } + + public String getSessionName() { + return sessionName; + } + + public String getX509IssuerOu() { + return x509IssuerOu; + } + + public String getX509SanDns() { + return x509SanDns; + } + + public String getX509SanNameCn() { + return x509SanNameCn; + } + + public String getX509SanUri() { + return x509SanUri; + } + + public String getX509SubjectCn() { + return x509SubjectCn; + } + + public String getSourceVpcArn() { + return sourceVpcArn; + } + + public void setSourceVpcArn(String sourceVpcArn) { + this.sourceVpcArn = sourceVpcArn; + } + + public void setType(String type) { + this.type = type; + } + + public void setPrincipal(String principal) { + this.principal = principal; + } + + public void setSessionName(String sessionName) { + this.sessionName = sessionName; + } + + public void setX509SanDns(String x509SanDns) { + this.x509SanDns = x509SanDns; + } + + public void setX509SanNameCn(String x509SanNameCn) { + this.x509SanNameCn = x509SanNameCn; + } + + public void setX509SubjectCn(String x509SubjectCn) { + this.x509SubjectCn = x509SubjectCn; + } + + public void setX509IssuerOu(String x509IssuerOu) { + this.x509IssuerOu = x509IssuerOu; + } + + public void setX509SanUri(String x509SanUri) { + this.x509SanUri = x509SanUri; + } + } +} diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/jaxrs/AwsVpcLatticeV2SecurityContextTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/jaxrs/AwsVpcLatticeV2SecurityContextTest.java new file mode 100644 index 000000000..31a4e9be4 --- /dev/null +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/jaxrs/AwsVpcLatticeV2SecurityContextTest.java @@ -0,0 +1,48 @@ +package com.amazonaws.serverless.proxy.internal.jaxrs; + +import com.amazonaws.serverless.proxy.AwsVPCLatticeV2SecurityContextWriter; +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.model.VPCLatticeV2RequestEvent; +import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.ws.rs.core.SecurityContext; +import org.junit.jupiter.api.Test; + +import static com.amazonaws.serverless.proxy.model.VPCLatticeV2RequestEventTest.BASE_V2_EVENT_AUTH_IAM; +import static org.junit.jupiter.api.Assertions.*; + +public class AwsVpcLatticeV2SecurityContextTest { + + + AwsVPCLatticeV2SecurityContextWriter contextWriter = new AwsVPCLatticeV2SecurityContextWriter(); + VPCLatticeV2RequestEvent NONE_AUTH = new AwsProxyRequestBuilder("/", "GET").toVPCLatticeV2Request(); + + + + private VPCLatticeV2RequestEvent getAuthenticatedEvent() { + try { + return LambdaContainerHandler.getObjectMapper().readValue(BASE_V2_EVENT_AUTH_IAM, VPCLatticeV2RequestEvent.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + + + @Test + void getAuthenticationScheme_noAuth_nullAuthType() { + SecurityContext ctx = contextWriter.writeSecurityContext(NONE_AUTH, null); + assertNull(ctx.getAuthenticationScheme()); + assertNull(ctx.getUserPrincipal()); + assertFalse(ctx.isSecure()); + } + + @Test + void getAuthenticationScheme_iamAuth_AwsIamAuthType() { + VPCLatticeV2RequestEvent req = getAuthenticatedEvent(); + SecurityContext ctx = contextWriter.writeSecurityContext(req, null); + assertNotNull(ctx.getAuthenticationScheme()); + assertEquals(ctx.getUserPrincipal().getName(), "arn:aws:iam::123456789012:assumed-role/my-role/my-session"); + assertTrue(ctx.isSecure()); + } +} 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 45c38843e..5a9412718 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 @@ -83,7 +83,7 @@ public void initAwsProxyHttpServletRequestTest(String type) { } public static Collection data() { - return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API", "WRAP"}); + return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API", "VPC_LATTICE_V2", "WRAP"}); } private HttpServletRequest getRequest(AwsProxyRequestBuilder req, Context lambdaCtx, SecurityContext securityCtx) { @@ -94,6 +94,8 @@ private HttpServletRequest getRequest(AwsProxyRequestBuilder req, Context lambda return new AwsProxyHttpServletRequest(req.alb().build(), lambdaCtx, securityCtx); case "HTTP_API": return new AwsHttpApiV2ProxyHttpServletRequest(req.toHttpApiV2Request(), lambdaCtx, securityCtx, LambdaContainerHandler.getContainerConfig()); + case "VPC_LATTICE_V2": + return new AwsVpcLatticeV2HttpServletRequest(req.toVPCLatticeV2Request(), lambdaCtx, securityCtx); case "WRAP": HttpServletRequest servletRequest = new AwsProxyHttpServletRequest(req.build(), lambdaCtx, securityCtx); return new AwsHttpServletRequestWrapper(servletRequest, req.build().getPath()); @@ -118,6 +120,7 @@ void headers_getHeader_validRequest(String type) { void headers_getRefererAndUserAgent_returnsContextValues(String type) { initAwsProxyHttpServletRequestTest(type); assumeFalse("ALB".equals(requestType)); + assumeFalse("VPC_LATTICE_V2".equals(requestType)); HttpServletRequest request = getRequest(REQUEST_USER_AGENT_REFERER, null, null); assertNotNull(request.getHeader("Referer")); assertEquals(REFERER, request.getHeader("Referer")); @@ -430,6 +433,7 @@ void contentType_duplicateCase_expectSingleContentTypeHeader(String type) { void requestURL_getUrl_expectHttpSchemaAndLocalhostForLocalTesting(String type) { initAwsProxyHttpServletRequestTest(type); assumeFalse("ALB".equals(requestType)); + assumeFalse("VPC_LATTICE_V2".equals(requestType)); AwsProxyRequestBuilder req = getRequestWithHeaders(); req.apiId("test-id"); LambdaContainerHandler.getContainerConfig().enableLocalhost(); @@ -465,6 +469,7 @@ void requestURL_getUrlWithCustomBasePath_expectCustomBasePath(String type) { void requestURL_getUrlWithContextPath_expectStageAsContextPath(String type) { initAwsProxyHttpServletRequestTest(type); assumeFalse("ALB".equals(requestType)); + assumeFalse("VPC_LATTICE_V2".equals(requestType)); AwsProxyRequestBuilder req = getRequestWithHeaders(); req.stage("test-stage"); LambdaContainerHandler.getContainerConfig().setUseStageAsServletContext(true); @@ -574,7 +579,7 @@ void inputStream_emptyBody_expectNullInputStream(String type) { try { InputStream is = req.getInputStream(); - assertTrue(is.getClass() == AwsServletInputStream.class); + assertSame(is.getClass(), AwsServletInputStream.class); assertEquals(0, is.available()); } catch (IOException e) { fail("Could not get input stream"); @@ -622,6 +627,7 @@ void getServerPort_invalidCustomPortFromHeader_expectDefaultPort(String type) { @ParameterizedTest void serverName_emptyHeaders_doesNotThrowNullPointer(String type) { initAwsProxyHttpServletRequestTest(type); + assumeFalse(type.equals("VPC_LATTICE_V2")); AwsProxyRequestBuilder proxyReq = new AwsProxyRequestBuilder("/test", "GET"); proxyReq.multiValueHeaders(null); HttpServletRequest servletReq = getRequest(proxyReq, null, null); diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsVpcLatticeV2HttpServletRequestReaderTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsVpcLatticeV2HttpServletRequestReaderTest.java new file mode 100644 index 000000000..58fb10ec5 --- /dev/null +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsVpcLatticeV2HttpServletRequestReaderTest.java @@ -0,0 +1,38 @@ +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.VPCLatticeV2RequestEvent; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class AwsVpcLatticeV2HttpServletRequestReaderTest { + private AwsVpcLatticeV2HttpServletRequestReader reader = new AwsVpcLatticeV2HttpServletRequestReader(); + + @Test + void reflection_getRequestClass_returnsCorrectType() { + assertSame(VPCLatticeV2RequestEvent.class, reader.getRequestClass()); + } + + @Test + void baseRequest_read_populatesSuccessfully() { + VPCLatticeV2RequestEvent req = new AwsProxyRequestBuilder("/hello", "GET") + .queryString("param1", "value1") + .header("custom", "value") + .toVPCLatticeV2Request(); + AwsVpcLatticeV2HttpServletRequestReader reader = new AwsVpcLatticeV2HttpServletRequestReader(); + 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(AwsVpcLatticeV2HttpServletRequestReader.VPC_LATTICE_V2_CONTEXT_PROPERTY)); + } 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/testutils/AwsProxyRequestBuilder.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java index 40f0ae7ad..3fb51c2d1 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java @@ -26,6 +26,7 @@ import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; +import org.jetbrains.annotations.NotNull; import java.io.ByteArrayInputStream; import java.io.File; @@ -534,4 +535,69 @@ public HttpApiV2ProxyRequest toHttpApiV2Request() { return req; } + + public InputStream toVPCLatticeV2RequestStream() { + VPCLatticeV2RequestEvent req = toVPCLatticeV2Request(); + try { + String requestJson = LambdaContainerHandler.getObjectMapper().writeValueAsString(req); + return new ByteArrayInputStream(requestJson.getBytes(StandardCharsets.UTF_8)); + } catch (JsonProcessingException e) { + return null; + } + } + + public VPCLatticeV2RequestEvent toVPCLatticeV2Request() { + Map qs; + if (Objects.nonNull(request.getMultiValueQueryStringParameters())) { + qs = new HashMap<>(); + request.getMultiValueQueryStringParameters().forEach((k, v) -> { + qs.put(k, v.get(0)); + }); + } else { + qs = null; + } + + VPCLatticeV2RequestEvent.RequestContext requestContext = getVPCLatticev2RequestContext(); + + return getVpcLatticeV2RequestEvent(qs, requestContext); + + } + + @NotNull + private VPCLatticeV2RequestEvent getVpcLatticeV2RequestEvent(Map qs, VPCLatticeV2RequestEvent.RequestContext requestContext) { + VPCLatticeV2RequestEvent latticeV2Event = new VPCLatticeV2RequestEvent(); + latticeV2Event.setVersion("2.0"); + latticeV2Event.setPath(request.getPath()); + latticeV2Event.setMethod(request.getHttpMethod()); + latticeV2Event.setBase64Encoded(request.isBase64Encoded()); + latticeV2Event.setBody(request.getBody()); + latticeV2Event.setHeaders(request.getMultiValueHeaders()); + latticeV2Event.setQueryStringParameters(qs); + latticeV2Event.setRequestContext(requestContext); + return latticeV2Event; + } + + @NotNull + private static VPCLatticeV2RequestEvent.RequestContext getVPCLatticev2RequestContext() { + VPCLatticeV2RequestEvent.RequestContext requestContext = new VPCLatticeV2RequestEvent.RequestContext(); + requestContext.setServiceNetworkArn("arn:aws:vpc-lattice:region:123456789012:servicenetwork/sn-0bf3f2882e9cc805a"); + requestContext.setServiceArn("arn:aws:vpc-lattice:region:123456789012:service/svc-0a40eebed65f8d69c"); + requestContext.setTargetGroupArn("arn:aws:vpc-lattice:region:123456789012:targetgroup/tg-6d0ecf831eec9f09"); + requestContext.setRegion("us-west-2"); + requestContext.setTimeEpoch("1711801592405546"); + + VPCLatticeV2RequestEvent.Identity identity = new VPCLatticeV2RequestEvent.Identity(); + identity.setSourceVpcArn("arn:aws:ec2:us-west-2:422832612717:vpc/vpc-05417e632a7302cb7"); + identity.setType(null); + identity.setPrincipal(null); + identity.setSessionName(null); + identity.setX509SanDns(null); + identity.setX509IssuerOu(null); + identity.setX509SanNameCn(null); + identity.setX509SanUri(null); + identity.setX509SubjectCn(null); + + requestContext.setIdentity(identity); + return requestContext; + } } diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/VPCLatticeV2RequestEventTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/VPCLatticeV2RequestEventTest.java new file mode 100644 index 000000000..b9f4a9813 --- /dev/null +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/VPCLatticeV2RequestEventTest.java @@ -0,0 +1,109 @@ +package com.amazonaws.serverless.proxy.model; + +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.fail; + +public class VPCLatticeV2RequestEventTest { + public static final String BASE_V2_EVENT_AUTH_IAM = "{\n" + + " \"version\": \"2.0\",\n" + + " \"path\": \"/\",\n" + + " \"method\": \"GET\",\n" + + " \"headers\": {\n" + + " \"header-key\": [\"header-value\"]\n" + + " },\n" + + " \"queryStringParameters\": {\n" + + " \"key\": \"value\"\n" + + " },\n" + + " \"body\": \"request-body\",\n" + + " \"requestContext\": {\n" + + " \"serviceNetworkArn\": \"arn:aws:vpc-lattice:region:123456789012:servicenetwork/sn-0bf3f2882e9cc805a\",\n" + + " \"serviceArn\": \"arn:aws:vpc-lattice:region:123456789012:service/svc-0a40eebed65f8d69c\",\n" + + " \"targetGroupArn\": \"arn:aws:vpc-lattice:region:123456789012:targetgroup/tg-6d0ecf831eec9f09\",\n" + + " \"identity\": {\n" + + " \"sourceVpcArn\": \"arn:aws:ec2:region:123456789012:vpc/vpc-0b8276c84697e7339\",\n" + + " \"type\": \"AWS_IAM\",\n" + + " \"principal\": \"arn:aws:iam::123456789012:assumed-role/my-role/my-session\",\n" + + " \"sessionName\": \"i-0c7de02a688bde9f7\"\n" + + " },\n" + + " \"region\": \"region\",\n" + + " \"timeEpoch\": \"1690497599177430\"\n" + + " }\n" + + "}"; + + private static final String BASE_V2_EVENT_AUTH_NONE = "{\n" + + " \"version\": \"2.0\",\n" + + " \"path\": \"/\",\n" + + " \"method\": \"GET\",\n" + + " \"headers\": {\n" + + " \"header-key\": [\"header-value\"]\n" + + " },\n" + + " \"queryStringParameters\": {\n" + + " \"key\": \"value\"\n" + + " },\n" + + " \"body\": \"request-body\",\n" + + " \"requestContext\": {\n" + + " \"serviceNetworkArn\": \"arn:aws:vpc-lattice:region:123456789012:servicenetwork/sn-0bf3f2882e9cc805a\",\n" + + " \"serviceArn\": \"arn:aws:vpc-lattice:region:123456789012:service/svc-0a40eebed65f8d69c\",\n" + + " \"targetGroupArn\": \"arn:aws:vpc-lattice:region:123456789012:targetgroup/tg-6d0ecf831eec9f09\",\n" + + " \"identity\": {\n" + + " \"sourceVpcArn\": \"arn:aws:ec2:region:123456789012:vpc/vpc-0b8276c84697e7339\"\n" + + " },\n" + + " \"region\": \"region\",\n" + + " \"timeEpoch\": \"1690497599177430\"\n" + + " }\n" + + "}"; + + @Test + void deserialize_fromJsonString_withIamAuth_populatesFieldsCorrectly() { + try { + VPCLatticeV2RequestEvent req = LambdaContainerHandler.getObjectMapper().readValue(BASE_V2_EVENT_AUTH_IAM, VPCLatticeV2RequestEvent.class); + assertEquals("AWS_IAM", req.getRequestContext().getIdentity().getType()); + assertEquals("2.0", req.getVersion()); + assertEquals("/", req.getPath()); + assertEquals("GET", req.getMethod()); + assertEquals("request-body", req.getBody()); + assertNotNull(req.getHeaders()); + assertNotEquals(Boolean.TRUE, req.getIsBase64Encoded()); + assertNotNull(req.getQueryStringParameters()); + assertNotNull(req.getRequestContext()); + assertEquals("arn:aws:vpc-lattice:region:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", req.getRequestContext().getServiceNetworkArn()); + assertEquals("arn:aws:vpc-lattice:region:123456789012:service/svc-0a40eebed65f8d69c", req.getRequestContext().getServiceArn()); + assertEquals("arn:aws:vpc-lattice:region:123456789012:targetgroup/tg-6d0ecf831eec9f09", req.getRequestContext().getTargetGroupArn()); + assertNotNull(req.getRequestContext().getIdentity()); + assertEquals("arn:aws:ec2:region:123456789012:vpc/vpc-0b8276c84697e7339", req.getRequestContext().getIdentity().getSourceVpcArn()); + assertEquals("arn:aws:iam::123456789012:assumed-role/my-role/my-session", req.getRequestContext().getIdentity().getPrincipal()); + assertEquals("i-0c7de02a688bde9f7", req.getRequestContext().getIdentity().getSessionName()); + assertNull(req.getRequestContext().getIdentity().getX509IssuerOu()); + assertNull(req.getRequestContext().getIdentity().getX509SanDns()); + assertNull(req.getRequestContext().getIdentity().getX509SanNameCn()); + assertNull(req.getRequestContext().getIdentity().getX509SanUri()); + assertNull(req.getRequestContext().getIdentity().getX509SubjectCn()); + assertEquals("1690497599177430", req.getRequestContext().getTimeEpoch()); + assertEquals("region", req.getRequestContext().getRegion()); + } catch (JsonProcessingException e) { + e.printStackTrace(); + fail("Exception while parsing request" + e.getMessage()); + } + + } + @Test + void deserialize_fromJsonString_populatesAuthTypeCorrectly() { + try { + VPCLatticeV2RequestEvent req = LambdaContainerHandler.getObjectMapper().readValue(BASE_V2_EVENT_AUTH_IAM, VPCLatticeV2RequestEvent.class); + assertEquals("AWS_IAM", req.getRequestContext().getIdentity().getType()); + + VPCLatticeV2RequestEvent req2 = LambdaContainerHandler.getObjectMapper().readValue(BASE_V2_EVENT_AUTH_NONE, VPCLatticeV2RequestEvent.class); + assertNull(req2.getRequestContext().getIdentity().getType()); + assertEquals(RequestSource.VPC_LATTICE_V2, req.getRequestSource()); + } catch (JsonProcessingException e) { + e.printStackTrace(); + fail("Exception while parsing request" + e.getMessage()); + } + } + +} 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 43bd7eac1..7d30f807b 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 @@ -23,6 +23,7 @@ import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.model.VPCLatticeV2RequestEvent; import com.amazonaws.services.lambda.runtime.Context; import org.glassfish.jersey.internal.inject.AbstractBinder; @@ -123,6 +124,28 @@ public static JerseyLambdaContainerHandlerJerseyLambdaContainerHandler that includes RequestReader and + * ResponseWriter objects for the VPCLatticeV2RequestEvent 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 getLatticeV2ProxyHandler(Application jaxRsApplication) { + JerseyLambdaContainerHandler newHandler = new JerseyLambdaContainerHandler<>( + VPCLatticeV2RequestEvent.class, + AwsProxyResponse.class, + new AwsVpcLatticeV2HttpServletRequestReader(), + new AwsProxyHttpServletResponseWriter(true), + new AwsVPCLatticeV2SecurityContextWriter(), + new AwsProxyExceptionHandler(), + jaxRsApplication); + newHandler.initialize(); + return newHandler; + } + //------------------------------------------------------------- // Constructors 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 b845a0b69..a8b16e6bd 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 @@ -23,6 +23,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.serverless.proxy.model.VPCLatticeV2RequestEvent; import com.amazonaws.services.lambda.runtime.Context; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -73,6 +74,13 @@ public class JerseyAwsProxyTest { .register(new ResourceBinder()) .property(LoggingFeature.LOGGING_FEATURE_VERBOSITY_SERVER, LoggingFeature.Verbosity.PAYLOAD_ANY); + private static ResourceConfig latticeApp = 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) @@ -82,6 +90,7 @@ public class JerseyAwsProxyTest { private static JerseyLambdaContainerHandler handler; private static JerseyLambdaContainerHandler httpApiHandler; + private static JerseyLambdaContainerHandler latticeHandler; private static JerseyLambdaContainerHandler handlerWithoutRegisteredDependencies = JerseyLambdaContainerHandler.getAwsProxyHandler(appWithoutRegisteredDependencies); @@ -95,7 +104,7 @@ public void initJerseyAwsProxyTest(String reqType) { } public static Collection data() { - return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API"}); + return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API", "VPC_LATTICE_V2"}); } private AwsProxyRequestBuilder getRequestBuilder(String path, String method) { @@ -120,6 +129,11 @@ private AwsProxyResponse executeRequest(AwsProxyRequestBuilder requestBuilder, C httpApiHandler = JerseyLambdaContainerHandler.getHttpApiV2ProxyHandler(httpApiApp); } return httpApiHandler.proxy(requestBuilder.toHttpApiV2Request(), lambdaContext); + case "VPC_LATTICE_V2": + if (latticeHandler == null) { + latticeHandler = JerseyLambdaContainerHandler.getLatticeV2ProxyHandler(latticeApp); + } + return latticeHandler.proxy(requestBuilder.toVPCLatticeV2Request(), lambdaContext); default: throw new RuntimeException("Unknown request type: " + type); } @@ -132,6 +146,8 @@ private JerseyLambdaContainerHandler getHandler() { return handler; case "HTTP_API": return httpApiHandler; + case "VPC_LATTICE_V2": + return latticeHandler; default: throw new RuntimeException("Unknown request type: " + type); } @@ -371,6 +387,7 @@ void stripBasePath_route_shouldRouteCorrectly(String reqType) { void stripBasePath_route_shouldReturn404WithStageAsContext(String reqType) { initJerseyAwsProxyTest(reqType); assumeTrue(!"ALB".equals(type)); + assumeTrue(!"VPC_LATTICE_V2".equals(type)); AwsProxyRequestBuilder request = getRequestBuilder("/custompath/echo/status-code", "GET") .stage("prod") .json() 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 9dc1ab32a..dd45dee7b 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 @@ -9,6 +9,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.serverless.proxy.model.VPCLatticeV2RequestEvent; import com.amazonaws.services.lambda.runtime.Context; import com.fasterxml.jackson.databind.ObjectMapper; @@ -73,7 +74,14 @@ public class JerseyParamEncodingTest { .register(new ResourceBinder()) .property("jersey.config.server.tracing.type", "ALL") .property("jersey.config.server.tracing.threshold", "VERBOSE"); + + private static ResourceConfig latticeApp = 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 JerseyLambdaContainerHandler latticeHandler; private static Context lambdaContext = new MockLambdaContext(); @@ -85,7 +93,7 @@ public void initJerseyParamEncodingTest(String reqType) { } public static Collection data() { - return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API"}); + return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API", "VPC_LATTICE_V2"}); } private AwsProxyRequestBuilder getRequestBuilder(String path, String method) { @@ -109,6 +117,11 @@ private AwsProxyResponse executeRequest(AwsProxyRequestBuilder requestBuilder, C httpApiHandler = JerseyLambdaContainerHandler.getHttpApiV2ProxyHandler(httpApiApp); } return httpApiHandler.proxy(requestBuilder.toHttpApiV2Request(), lambdaContext); + case "VPC_LATTICE_V2": + if (latticeHandler == null) { + latticeHandler = JerseyLambdaContainerHandler.getLatticeV2ProxyHandler(latticeApp); + } + return latticeHandler.proxy(requestBuilder.toVPCLatticeV2Request(), lambdaContext); default: throw new RuntimeException("Unknown request type: " + type); } 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 5e56dba62..bb145218b 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 @@ -19,6 +19,7 @@ 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.model.VPCLatticeV2RequestEvent; import com.amazonaws.services.lambda.runtime.Context; import org.springframework.web.context.ConfigurableWebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; @@ -88,6 +89,20 @@ public static SpringLambdaContainerHandler getLatticeV2ProxyHandler(Class... config) throws ContainerInitializationException { + return new SpringProxyHandlerBuilder() + .defaultVpcLatticeV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .configurationClasses(config) + .buildAndInitialize(); + } + /** * Creates a new container handler with the given reader and writer objects * 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 9504b6166..5eb352238 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 @@ -50,6 +50,7 @@ public class SpringAwsProxyTest { private MockLambdaContext lambdaContext = new MockLambdaContext(); private static SpringLambdaContainerHandler handler; private static SpringLambdaContainerHandler httpApiHandler; + private static SpringLambdaContainerHandler latticeHandler; private AwsLambdaServletContainerHandler.StartupHandler h = (c -> { FilterRegistration.Dynamic registration = c.addFilter("UnauthenticatedFilter", UnauthenticatedFilter.class); @@ -61,7 +62,7 @@ public class SpringAwsProxyTest { private String type; public static Collection data() { - return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API"}); + return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API", "VPC_LATTICE_V2"}); } public void initSpringAwsProxyTest(String reqType) { @@ -89,6 +90,12 @@ private AwsProxyResponse executeRequest(AwsProxyRequestBuilder requestBuilder, C httpApiHandler.onStartup(h); } return httpApiHandler.proxy(requestBuilder.toHttpApiV2Request(), lambdaContext); + case "VPC_LATTICE_V2": + if (latticeHandler == null) { + latticeHandler = SpringLambdaContainerHandler.getLatticeV2ProxyHandler(EchoSpringAppConfig.class); + latticeHandler.onStartup(h); + } + return latticeHandler.proxy(requestBuilder.toVPCLatticeV2Request(), lambdaContext); default: throw new RuntimeException("Unknown request type: " + type); } @@ -183,6 +190,7 @@ void queryString_listParameter_expectCorrectLength(String reqType) { void queryString_multiParam_expectCorrectValueCount(String reqType) throws IOException { initSpringAwsProxyTest(reqType); + assumeFalse("VPC_LATTICE_V2".equals(reqType)); AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/multivalue-query-string", "GET") .json() .queryString("multiple", "first") @@ -448,6 +456,7 @@ void request_encodedPath_returnsDecodedPath(String reqType) { void contextPath_generateLink_returnsCorrectPath(String reqType) { initSpringAwsProxyTest(reqType); assumeFalse("ALB".equals(type)); + assumeFalse("VPC_LATTICE_V2".equals(type)); AwsProxyRequestBuilder request = new AwsProxyRequestBuilder("/echo/generate-uri", "GET") .scheme("https") .serverName("api.myserver.com") diff --git a/aws-serverless-java-container-springboot3/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java b/aws-serverless-java-container-springboot3/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java index c7e507f39..4e085ffba 100644 --- a/aws-serverless-java-container-springboot3/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java +++ b/aws-serverless-java-container-springboot3/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java @@ -9,6 +9,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.amazonaws.serverless.proxy.*; +import com.amazonaws.serverless.proxy.model.VPCLatticeV2RequestEvent; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.function.serverless.web.ServerlessHttpServletRequest; @@ -18,11 +20,6 @@ import org.springframework.util.MultiValueMapAdapter; import org.springframework.util.StringUtils; -import com.amazonaws.serverless.proxy.AsyncInitializationWrapper; -import com.amazonaws.serverless.proxy.AwsHttpApiV2SecurityContextWriter; -import com.amazonaws.serverless.proxy.AwsProxySecurityContextWriter; -import com.amazonaws.serverless.proxy.RequestReader; -import com.amazonaws.serverless.proxy.SecurityContextWriter; import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse; import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; @@ -107,6 +104,25 @@ public static HttpServletRequest generateHttpServletRequest(String jsonRequest, return httpServletRequest; } + public static HttpServletRequest generateLatticeV2HttpServletRequest(String jsonRequest, Context lambdaContext, + ServletContext servletContext, ObjectMapper mapper) { + SecurityContextWriter securityWriter = new AwsVPCLatticeV2SecurityContextWriter(); + return AwsSpringHttpProcessingUtils.generateRequestLatticeV2(jsonRequest, lambdaContext, securityWriter, mapper, servletContext); + } + + public static HttpServletRequest generateLatticeV2HttpServletRequest(InputStream jsonRequest, Context lambdaContext, + ServletContext servletContext, ObjectMapper mapper) { + try { + String text = new String(FileCopyUtils.copyToByteArray(jsonRequest), StandardCharsets.UTF_8); + if (logger.isDebugEnabled()) { + logger.debug("Creating HttpServletRequest from: " + text); + } + return generateLatticeV2HttpServletRequest(text, lambdaContext, servletContext, mapper); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + @SuppressWarnings({ "unchecked", "rawtypes" }) private static HttpServletRequest generateRequest1(String request, Context lambdaContext, SecurityContextWriter securityWriter, ObjectMapper mapper, ServletContext servletContext) { @@ -147,7 +163,7 @@ private static HttpServletRequest generateRequest2(String request, Context lambd ServerlessHttpServletRequest httpRequest = new ServerlessHttpServletRequest(servletContext, v2Request.getRequestContext().getHttp().getMethod(), v2Request.getRequestContext().getHttp().getPath()); populateQueryStringparameters(v2Request.getQueryStringParameters(), httpRequest); - + v2Request.getHeaders().forEach(httpRequest::setHeader); if (StringUtils.hasText(v2Request.getBody())) { @@ -162,6 +178,32 @@ private static HttpServletRequest generateRequest2(String request, Context lambd securityWriter.writeSecurityContext(v2Request, lambdaContext)); return httpRequest; } + + private static HttpServletRequest generateRequestLatticeV2(String request, Context lambdaContext, + SecurityContextWriter securityWriter, ObjectMapper mapper, ServletContext servletContext) { + VPCLatticeV2RequestEvent v2Request = readValue(request, VPCLatticeV2RequestEvent.class, mapper); + ServerlessHttpServletRequest httpRequest = new ServerlessHttpServletRequest(servletContext, + v2Request.getMethod(), v2Request.getPath()); + populateQueryStringparameters(v2Request.getQueryStringParameters(), httpRequest); + + if (v2Request.getHeaders() != null) { + MultiValueMapAdapter headers = new MultiValueMapAdapter(v2Request.getHeaders()); + httpRequest.setHeaders(headers); + } + + if (StringUtils.hasText(v2Request.getBody())) { + httpRequest.setContentType("application/json"); + httpRequest.setContent(v2Request.getBody().getBytes(StandardCharsets.UTF_8)); + } + + httpRequest.setAttribute(RequestReader.VPC_LATTICE_V2_CONTEXT_PROPERTY, v2Request.getRequestContext()); + httpRequest.setAttribute(RequestReader.VPC_LATTICE_V2_EVENT_PROPERTY, v2Request); + httpRequest.setAttribute(RequestReader.LAMBDA_CONTEXT_PROPERTY, lambdaContext); + httpRequest.setAttribute(RequestReader.JAX_SECURITY_CONTEXT_PROPERTY, + securityWriter.writeSecurityContext(v2Request, lambdaContext)); + + return httpRequest; + } private static void populateQueryStringparameters(Map requestParameters, ServerlessHttpServletRequest httpRequest) { if (!CollectionUtils.isEmpty(requestParameters)) { diff --git a/aws-serverless-java-container-springboot3/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java b/aws-serverless-java-container-springboot3/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java index 6b3dd5ca2..f8217a301 100644 --- a/aws-serverless-java-container-springboot3/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java +++ b/aws-serverless-java-container-springboot3/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java @@ -14,6 +14,7 @@ import java.util.concurrent.CountDownLatch; +import com.amazonaws.serverless.proxy.model.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.WebApplicationType; @@ -33,9 +34,6 @@ import com.amazonaws.serverless.proxy.internal.servlet.AwsServletContext; import com.amazonaws.serverless.proxy.internal.servlet.AwsServletRegistration; 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; @@ -113,6 +111,25 @@ public static SpringBootLambdaContainerHandler getVpcLatticeV2Handler(Class springBootInitializer, String... profiles) + throws ContainerInitializationException { + return new SpringBootProxyHandlerBuilder() + .defaultVpcLatticeV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(springBootInitializer) + .profiles(profiles) + .buildAndInitialize(); + } + + + /** * Creates a new container handler with the given reader and writer objects * diff --git a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtilsTests.java b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtilsTests.java index a04361e7e..b5cb50dc2 100644 --- a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtilsTests.java +++ b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtilsTests.java @@ -7,6 +7,7 @@ import java.util.Arrays; import java.util.Collection; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.SpringApplication; @@ -184,6 +185,40 @@ public class AwsSpringHttpProcessingUtilsTests { " }\n" + "}"; + private static final String VPC_LATTICE_V2_EVENT = "{\n" + + " \"version\": \"2.0\",\n" + + " \"path\": \"/async\",\n" + + " \"method\": \"POST\",\n" + + " \"headers\": {\n" + + " \"x-forwarded-for\": [\n" + + " \"10.0.0.39\"\n" + + " ],\n" + + " \"content-type\": [\n" + + " \"application/json\"\n" + + " ],\n" + + " \"accept-encoding\": [\n" + + " \"identity\"\n" + + " ],\n" + + " \"host\": [\n" + + " \"demo-service-057f55fd2927517c9.7d67968.vpc-lattice-svcs.us-west-2.on.aws\"\n" + + " ],\n" + + " \"content-length\": [\n" + + " \"54\"\n" + + " ]\n" + + " },\n" + + " \"body\": \"{\\\"key1\\\": \\\"value1\\\", \\\"key2\\\": \\\"value2\\\", \\\"key3\\\": \\\"value3\\\"}\",\n" + + " \"requestContext\": {\n" + + " \"serviceNetworkArn\": \"arn:aws:vpc-lattice:us-west-2:422832612717:servicenetwork/sn-0bcd972d368921a67\",\n" + + " \"serviceArn\": \"arn:aws:vpc-lattice:us-west-2:422832612717:service/svc-057f55fd2927517c9\",\n" + + " \"targetGroupArn\": \"arn:aws:vpc-lattice:us-west-2:422832612717:targetgroup/tg-0c8666d87e4ebad20\",\n" + + " \"identity\": {\n" + + " \"sourceVpcArn\": \"arn:aws:ec2:us-west-2:422832612717:vpc/vpc-05417e632a7302cb7\"\n" + + " },\n" + + " \"region\": \"us-west-2\",\n" + + " \"timeEpoch\": \"1711801592405546\"\n" + + " }\n" + + " }"; + private final ObjectMapper mapper = new ObjectMapper(); public static Collection data() { @@ -202,7 +237,7 @@ public void validateHttpServletRequestGenerationWithInputStream(String jsonEvent assertEquals("POST", request.getMethod()); assertEquals("/async", request.getRequestURI()); } - + @MethodSource("data") @ParameterizedTest public void validateHttpServletRequestGenerationWithJson(String jsonEvent) { @@ -229,8 +264,48 @@ public void validateRequestResponse(String jsonEvent) throws Exception { } } - - @EnableAutoConfiguration + + /** + * These tests are specific to VPC Lattice + */ + @Test + public void validateHttpServletRequestGenerationWithInputStream() { + ByteArrayInputStream inputStream = new ByteArrayInputStream(VPC_LATTICE_V2_EVENT.getBytes(StandardCharsets.UTF_8)); + ServerlessServletContext servletContext = new ServerlessServletContext(); + HttpServletRequest request = AwsSpringHttpProcessingUtils.generateLatticeV2HttpServletRequest(inputStream, null, servletContext, mapper); + // spot check some headers + assertEquals("application/json", request.getHeader("content-type")); + assertEquals("10.0.0.39", request.getHeader("X-Forwarded-For")); + assertEquals("POST", request.getMethod()); + assertEquals("/async", request.getRequestURI()); + } + + @Test + public void validateHttpServletRequestGenerationWithJson() { + ServerlessServletContext servletContext = new ServerlessServletContext(); + HttpServletRequest request = AwsSpringHttpProcessingUtils.generateLatticeV2HttpServletRequest(VPC_LATTICE_V2_EVENT, null, servletContext, mapper); + // spot check some headers + assertEquals("application/json", request.getHeader("content-type")); + assertEquals("10.0.0.39", request.getHeader("X-Forwarded-For")); + assertEquals("POST", request.getMethod()); + assertEquals("/async", request.getRequestURI()); + } + + @Test + public void validateRequestResponse() throws Exception { + try (ConfigurableApplicationContext context = SpringApplication.run(EmptyApplication.class);) { + ServerlessMVC mvc = ServerlessMVC.INSTANCE((ServletWebServerApplicationContext) context); + AwsProxyHttpServletResponseWriter responseWriter = new AwsProxyHttpServletResponseWriter(); + AwsProxyResponse awsResponse = AwsSpringHttpProcessingUtils.processRequest( + AwsSpringHttpProcessingUtils.generateLatticeV2HttpServletRequest(VPC_LATTICE_V2_EVENT, null, + mvc.getServletContext(), mapper), mvc, responseWriter); + assertEquals("hello", awsResponse.getBody()); + assertEquals(200, awsResponse.getStatusCode()); + } + + } + + @EnableAutoConfiguration @Configuration public static class EmptyApplication { @RestController diff --git a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/JpaAppTest.java b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/JpaAppTest.java index a111e510a..9a6ed6522 100644 --- a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/JpaAppTest.java +++ b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/JpaAppTest.java @@ -21,7 +21,7 @@ public class JpaAppTest { private String type; public static Collection data() { - return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API"}); + return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API", "VPC_LATTICE_V2"}); } public void initJpaAppTest(String reqType) { diff --git a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java index b87c80ce6..b38a56f5a 100644 --- a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java +++ b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java @@ -16,6 +16,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collection; import java.util.stream.Collectors; @@ -30,7 +31,7 @@ public class ServletAppTest { private String type; public static Collection data() { - return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API"}); + return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API", "VPC_LATTICE_V2"}); } public void initServletAppTest(String reqType) { @@ -173,6 +174,9 @@ void stream_getUtf8String_returnsValidUtf8String(String reqType) throws IOExcept break; case "HTTP_API": req = reqBuilder.toHttpApiV2RequestStream(); + break; + case "VPC_LATTICE_V2": + req = reqBuilder.toVPCLatticeV2RequestStream(); } ByteArrayOutputStream out = new ByteArrayOutputStream(); streamHandler.handleRequest(req, out, lambdaContext); @@ -189,17 +193,13 @@ void stream_getUtf8Json_returnsValidUtf8String(String reqType) throws IOExceptio LambdaContainerHandler.getContainerConfig().setDefaultContentCharset(ContainerConfig.DEFAULT_CONTENT_CHARSET); LambdaStreamHandler streamHandler = new LambdaStreamHandler(type); AwsProxyRequestBuilder reqBuilder = new AwsProxyRequestBuilder("/content-type/jsonutf8", "GET"); - InputStream req = null; - switch (type) { - case "ALB": - req = reqBuilder.alb().buildStream(); - break; - case "API_GW": - req = reqBuilder.buildStream(); - break; - case "HTTP_API": - req = reqBuilder.toHttpApiV2RequestStream(); - } + InputStream req = switch (type) { + case "ALB" -> reqBuilder.alb().buildStream(); + case "API_GW" -> reqBuilder.buildStream(); + case "HTTP_API" -> reqBuilder.toHttpApiV2RequestStream(); + case "VPC_LATTICE_V2" -> reqBuilder.toVPCLatticeV2RequestStream(); + default -> null; + }; ByteArrayOutputStream out = new ByteArrayOutputStream(); streamHandler.handleRequest(req, out, lambdaContext); AwsProxyResponse resp = LambdaContainerHandler.getObjectMapper().readValue(out.toByteArray(), AwsProxyResponse.class); @@ -227,7 +227,7 @@ void echoMessage_populatesSingleValueHeadersForHttpApiV2(String reqType) { .header(HttpHeaders.ACCEPT, "application/json;v=1") .body(new MessageData("test message")); AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); - if ("HTTP_API".equals(type)) { + if ("HTTP_API".equals(type) || "VPC_LATTICE_V2".equals(type)) { assertNotNull(resp.getHeaders()); } else { assertNull(resp.getHeaders()); diff --git a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java index 9c39fd905..e4666be43 100644 --- a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java +++ b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java @@ -25,7 +25,7 @@ public class WebFluxAppTest { private String type; public static Collection data() { - return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API"}); + return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API", "VPC_LATTICE_V2"}); } public void initWebFluxAppTest(String reqType) { diff --git a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/LambdaHandler.java b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/LambdaHandler.java index 0cf67c10f..41a66938a 100644 --- a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/LambdaHandler.java +++ b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/LambdaHandler.java @@ -6,6 +6,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.serverless.proxy.model.VPCLatticeV2RequestEvent; import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; import com.amazonaws.services.lambda.runtime.Context; @@ -14,6 +15,7 @@ public class LambdaHandler implements RequestHandler { private static SpringBootLambdaContainerHandler handler; private static SpringBootLambdaContainerHandler httpApiHandler; + private static SpringBootLambdaContainerHandler latticeV2Handler; private String type; public LambdaHandler(String reqType) { @@ -37,6 +39,13 @@ public LambdaHandler(String reqType) { .springBootApplication(JpaApplication.class) .buildAndInitialize(); break; + case "VPC_LATTICE_V2": + latticeV2Handler = new SpringBootProxyHandlerBuilder() + .defaultVpcLatticeV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(JpaApplication.class) + .buildAndInitialize(); } } catch (ContainerInitializationException e) { e.printStackTrace(); @@ -52,6 +61,8 @@ public AwsProxyResponse handleRequest(AwsProxyRequestBuilder awsProxyRequest, Co return handler.proxy(awsProxyRequest.alb().build(), context); case "HTTP_API": return httpApiHandler.proxy(awsProxyRequest.toHttpApiV2Request(), context); + case "VPC_LATTICE_V2": + return latticeV2Handler.proxy(awsProxyRequest.toVPCLatticeV2Request(), context); default: throw new RuntimeException("Unknown request type: " + type); } diff --git a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java index 88441988e..fcc6ddec6 100644 --- a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java +++ b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java @@ -7,6 +7,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.serverless.proxy.model.VPCLatticeV2RequestEvent; import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; import com.amazonaws.services.lambda.runtime.Context; @@ -15,7 +16,8 @@ public class LambdaHandler implements RequestHandler { private static SpringBootLambdaContainerHandler handler; private static SpringBootLambdaContainerHandler httpApiHandler; - private String type; + private static SpringBootLambdaContainerHandler latticeV2Handler; + private final String type; public LambdaHandler(String reqType) { type = reqType; @@ -38,6 +40,14 @@ public LambdaHandler(String reqType) { .springBootApplication(ServletApplication.class) .buildAndInitialize(); break; + case "VPC_LATTICE_V2": + latticeV2Handler = new SpringBootProxyHandlerBuilder() + .defaultVpcLatticeV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(ServletApplication.class) + .buildAndInitialize(); + break; } } catch (ContainerInitializationException e) { e.printStackTrace(); @@ -53,6 +63,8 @@ public AwsProxyResponse handleRequest(AwsProxyRequestBuilder awsProxyRequest, Co return handler.proxy(awsProxyRequest.alb().build(), context); case "HTTP_API": return httpApiHandler.proxy(awsProxyRequest.toHttpApiV2Request(), context); + case "VPC_LATTICE_V2": + return latticeV2Handler.proxy(awsProxyRequest.toVPCLatticeV2Request(), context); default: throw new RuntimeException("Unknown request type: " + type); } diff --git a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaStreamHandler.java b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaStreamHandler.java index fd7d71d79..7a580e7c3 100644 --- a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaStreamHandler.java +++ b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaStreamHandler.java @@ -6,6 +6,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.serverless.proxy.model.VPCLatticeV2RequestEvent; import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; import com.amazonaws.services.lambda.runtime.Context; @@ -19,6 +20,7 @@ public class LambdaStreamHandler implements RequestStreamHandler { private static SpringBootLambdaContainerHandler handler; private static SpringBootLambdaContainerHandler httpApiHandler; + private static SpringBootLambdaContainerHandler latticeV2Handler; private String type; public LambdaStreamHandler(String reqType) { @@ -42,6 +44,14 @@ public LambdaStreamHandler(String reqType) { .springBootApplication(ServletApplication.class) .buildAndInitialize(); break; + case "VPC_LATTICE_V2": + latticeV2Handler = new SpringBootProxyHandlerBuilder() + .defaultVpcLatticeV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(ServletApplication.class) + .buildAndInitialize(); + break; } } catch (ContainerInitializationException e) { e.printStackTrace(); @@ -57,6 +67,9 @@ public void handleRequest(InputStream inputStream, OutputStream outputStream, Co break; case "HTTP_API": httpApiHandler.proxyStream(inputStream, outputStream, context); + break; + case "VPC_LATTICE_V2": + latticeV2Handler.proxyStream(inputStream, outputStream, context); } } diff --git a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java index 0eb52a7bc..7d93fddd5 100644 --- a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java +++ b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java @@ -6,6 +6,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.serverless.proxy.model.VPCLatticeV2RequestEvent; import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; import com.amazonaws.services.lambda.runtime.Context; @@ -14,6 +15,7 @@ public class LambdaHandler implements RequestHandler { private static SpringBootLambdaContainerHandler handler; private static SpringBootLambdaContainerHandler httpApiHandler; + private static SpringBootLambdaContainerHandler lattice_v2; private String type; @@ -36,6 +38,12 @@ public LambdaHandler(String reqType) { .springBootApplication(WebFluxTestApplication.class) .buildAndInitialize(); break; + case "VPC_LATTICE_V2": + lattice_v2 = new SpringBootProxyHandlerBuilder() + .defaultVpcLatticeV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(WebFluxTestApplication.class) + .buildAndInitialize(); } } catch (ContainerInitializationException e) { e.printStackTrace(); @@ -51,6 +59,8 @@ public AwsProxyResponse handleRequest(AwsProxyRequestBuilder awsProxyRequest, Co return handler.proxy(awsProxyRequest.alb().build(), context); case "HTTP_API": return httpApiHandler.proxy(awsProxyRequest.toHttpApiV2Request(), context); + case "VPC_LATTICE_V2": + return lattice_v2.proxy(awsProxyRequest.toVPCLatticeV2Request(), context); default: throw new RuntimeException("Unknown request type: " + type); } diff --git a/pom.xml b/pom.xml index b1c832040..84f7fa726 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,7 @@ - 0.7 + 0.6 9.2.0 2.17.1 2.0.13 diff --git a/samples/springboot3/vpc-lattice-pet-store/README.md b/samples/springboot3/vpc-lattice-pet-store/README.md new file mode 100644 index 000000000..ba1c78828 --- /dev/null +++ b/samples/springboot3/vpc-lattice-pet-store/README.md @@ -0,0 +1,55 @@ +# Serverless Spring Boot 3 example +A pet store application written with the [Spring Boot 3 framework](https://projects.spring.io/spring-boot/). The `LambdaHandler` object is the main entry point for Lambda. + +The main resources deployed include: + +1. An Outbound Lambda function which acts as a client (the function code is inside the app folder) +2. An Inbound function (the SpringBoot application) that is the target of the VPC Lattice Service. +3. A VPC Lattice Service Network +4. A VPC Lattice Service +5. A VPC Lattice Target Group +6. A VPC + +The application uses [VPC Lattice](https://docs.aws.amazon.com/vpc-lattice/latest/ug/what-is-vpc-lattice.html) as a trigger and 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. + +![latticeDiagram](latticediagram.jpg) + + + + + + + + +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [awscurl](https://github.com/okigan/awscurl) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package +``` +$ sam build +``` + +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. + +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 + +``` +$ sam deploy --guided +``` + +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `awscurl` or a web browser to make a call to the URL + +``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +OutboundFunctionUrlEndpoint - Outbound Lambda Function URL Endpoint https://xxxxxxxxxxxxxx.lambda-url.us-east-1.on.aws/pets +--------------------------------------------------------------------------------------------------------- + +$ awscurl --service lambda https://xxxxxxxxxxxxxx.lambda-url.us-east-1.on.aws/pets +``` diff --git a/samples/springboot3/vpc-lattice-pet-store/app/index.py b/samples/springboot3/vpc-lattice-pet-store/app/index.py new file mode 100644 index 000000000..b666ad822 --- /dev/null +++ b/samples/springboot3/vpc-lattice-pet-store/app/index.py @@ -0,0 +1,12 @@ +import os +import json +import http.client + +def handler(event, context): + conn = http.client.HTTPSConnection(os.environ["ENDPOINT"]) + + conn.request("GET", "/pets") + res = conn.getresponse() + data = res.read() + + return data \ No newline at end of file diff --git a/samples/springboot3/vpc-lattice-pet-store/build.gradle b/samples/springboot3/vpc-lattice-pet-store/build.gradle new file mode 100644 index 000000000..f990b1f68 --- /dev/null +++ b/samples/springboot3/vpc-lattice-pet-store/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'java' + +repositories { + mavenLocal() + mavenCentral() + maven {url "https://repo.spring.io/milestone"} + maven {url "https://repo.spring.io/snapshot"} +} + +dependencies { + implementation ( + implementation('org.springframework.boot:spring-boot-starter-web:3.2.5') { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' + }, + 'com.amazonaws.serverless:aws-serverless-java-container-springboot3:[2.0-SNAPSHOT,)', + ) +} + +task buildZip(type: Zip) { + from compileJava + from processResources + into('lib') { + from(configurations.compileClasspath) { + exclude 'tomcat-embed-*' + } + } +} + +build.dependsOn buildZip diff --git a/samples/springboot3/vpc-lattice-pet-store/latticediagram.jpg b/samples/springboot3/vpc-lattice-pet-store/latticediagram.jpg new file mode 100644 index 000000000..00fdab5d6 Binary files /dev/null and b/samples/springboot3/vpc-lattice-pet-store/latticediagram.jpg differ diff --git a/samples/springboot3/vpc-lattice-pet-store/pom.xml b/samples/springboot3/vpc-lattice-pet-store/pom.xml new file mode 100644 index 000000000..a6adec05e --- /dev/null +++ b/samples/springboot3/vpc-lattice-pet-store/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + + com.amazonaws.serverless.sample + serverless-springboot3-example + 2.0-SNAPSHOT + Spring Boot example for the aws-serverless-java-container library + Simple pet store written with the Spring framework and Spring Boot + https://aws.amazon.com/lambda/ + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + com.amazonaws.serverless + aws-serverless-java-container-springboot3 + 2.1.0-SNAPSHOT + + + + + + shaded-jar + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.2 + + false + + + + package + + shade + + + + + org.apache.tomcat.embed:* + + + + + + + + + + + assembly-zip + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + default-jar + none + + + + + org.apache.maven.plugins + maven-install-plugin + 3.1.1 + + true + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.6.1 + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/lib + runtime + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.6.0 + + + zip-assembly + package + + single + + + ${project.artifactId}-${project.version} + + src${file.separator}assembly${file.separator}bin.xml + + false + + + + + + + + + + + diff --git a/samples/springboot3/vpc-lattice-pet-store/src/assembly/bin.xml b/samples/springboot3/vpc-lattice-pet-store/src/assembly/bin.xml new file mode 100644 index 000000000..1e085057d --- /dev/null +++ b/samples/springboot3/vpc-lattice-pet-store/src/assembly/bin.xml @@ -0,0 +1,27 @@ + + lambda-package + + zip + + false + + + + ${project.build.directory}${file.separator}lib + lib + + tomcat-embed* + + + + + ${project.build.directory}${file.separator}classes + + ** + + ${file.separator} + + + \ No newline at end of file diff --git a/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/Application.java b/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/Application.java new file mode 100644 index 000000000..b548a22a2 --- /dev/null +++ b/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/Application.java @@ -0,0 +1,12 @@ +package com.amazonaws.serverless.sample.springboot3; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} \ No newline at end of file diff --git a/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/LambdaHandler.java b/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/LambdaHandler.java new file mode 100644 index 000000000..1a0e54eda --- /dev/null +++ b/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/LambdaHandler.java @@ -0,0 +1,35 @@ +package com.amazonaws.serverless.sample.springboot3; + + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.model.VPCLatticeV2RequestEvent; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.EnumSet; + + +public class LambdaHandler implements RequestHandler { + + private static final Logger log = LoggerFactory.getLogger(LambdaHandler.class); + private static SpringBootLambdaContainerHandler handler; + static { + try { + handler = SpringBootLambdaContainerHandler.getVpcLatticeV2Handler(Application.class); + } catch (ContainerInitializationException e) { + // if we fail here. We re-throw the exception to force another cold start + e.printStackTrace(); + throw new RuntimeException("Could not initialize Spring Boot application", e); + } + } + + @Override + public AwsProxyResponse handleRequest(VPCLatticeV2RequestEvent event, Context context) { + return handler.proxy(event, context); + } +} diff --git a/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/controller/PetsController.java b/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/controller/PetsController.java new file mode 100644 index 000000000..e2d7d41a1 --- /dev/null +++ b/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/controller/PetsController.java @@ -0,0 +1,38 @@ +package com.amazonaws.serverless.sample.springboot3.controller; + +import com.amazonaws.serverless.sample.springboot3.model.Pet; +import com.amazonaws.serverless.sample.springboot3.model.PetData; +import com.amazonaws.serverless.sample.springboot3.model.Error; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; +import java.util.Optional; +import java.util.UUID; + + +@RestController +public class PetsController { + @RequestMapping(path = "/pets", method = RequestMethod.GET) + public Pet[] listPets(@RequestParam("limit") Optional limit, Principal principal) { + int queryLimit = 10; + if (limit.isPresent()) { + queryLimit = limit.get(); + } + + Pet[] outputPets = new Pet[queryLimit]; + + for (int i = 0; i < queryLimit; i++) { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setName(PetData.getRandomName()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + outputPets[i] = newPet; + } + + return outputPets; + } +} \ No newline at end of file diff --git a/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/model/Error.java b/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/model/Error.java new file mode 100644 index 000000000..502adc97a --- /dev/null +++ b/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/model/Error.java @@ -0,0 +1,17 @@ +package com.amazonaws.serverless.sample.springboot3.model; + +public class Error { + private String message; + + public Error(String errorMessage) { + message = errorMessage; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} \ No newline at end of file diff --git a/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/model/Pet.java b/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/model/Pet.java new file mode 100644 index 000000000..28142ae52 --- /dev/null +++ b/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/model/Pet.java @@ -0,0 +1,42 @@ +package com.amazonaws.serverless.sample.springboot3.model; + +import java.util.Date; + +public class Pet { + private String id; + private String breed; + private String name; + private Date dateOfBirth; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getDateOfBirth() { + return dateOfBirth; + } + + public void setDateOfBirth(Date dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } +} diff --git a/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/model/PetData.java b/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/model/PetData.java new file mode 100644 index 000000000..79f3fb408 --- /dev/null +++ b/samples/springboot3/vpc-lattice-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/model/PetData.java @@ -0,0 +1,100 @@ +package com.amazonaws.serverless.sample.springboot3.model; + +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; + +public class PetData { + private static List breeds = new ArrayList<>(); + static { + breeds.add("Afghan Hound"); + breeds.add("Beagle"); + breeds.add("Bernese Mountain Dog"); + breeds.add("Bloodhound"); + breeds.add("Dalmatian"); + breeds.add("Jack Russell Terrier"); + breeds.add("Norwegian Elkhound"); + } + + private static List names = new ArrayList<>(); + static { + names.add("Bailey"); + names.add("Bella"); + names.add("Max"); + names.add("Lucy"); + names.add("Charlie"); + names.add("Molly"); + names.add("Buddy"); + names.add("Daisy"); + names.add("Rocky"); + names.add("Maggie"); + names.add("Jake"); + names.add("Sophie"); + names.add("Jack"); + names.add("Sadie"); + names.add("Toby"); + names.add("Chloe"); + names.add("Cody"); + names.add("Bailey"); + names.add("Buster"); + names.add("Lola"); + names.add("Duke"); + names.add("Zoe"); + names.add("Cooper"); + names.add("Abby"); + names.add("Riley"); + names.add("Ginger"); + names.add("Harley"); + names.add("Roxy"); + names.add("Bear"); + names.add("Gracie"); + names.add("Tucker"); + names.add("Coco"); + names.add("Murphy"); + names.add("Sasha"); + names.add("Lucky"); + names.add("Lily"); + names.add("Oliver"); + names.add("Angel"); + names.add("Sam"); + names.add("Princess"); + names.add("Oscar"); + names.add("Emma"); + names.add("Teddy"); + names.add("Annie"); + names.add("Winston"); + names.add("Rosie"); + } + + public static List getBreeds() { + return breeds; + } + + public static List getNames() { + return names; + } + + public static String getRandomBreed() { + return breeds.get(ThreadLocalRandom.current().nextInt(0, breeds.size() - 1)); + } + + public static String getRandomName() { + return names.get(ThreadLocalRandom.current().nextInt(0, names.size() - 1)); + } + + public static Date getRandomDoB() { + GregorianCalendar gc = new GregorianCalendar(); + + int year = ThreadLocalRandom.current().nextInt( + Calendar.getInstance().get(Calendar.YEAR) - 15, + Calendar.getInstance().get(Calendar.YEAR) + ); + + gc.set(Calendar.YEAR, year); + + int dayOfYear = ThreadLocalRandom.current().nextInt(1, gc.getActualMaximum(Calendar.DAY_OF_YEAR)); + + gc.set(Calendar.DAY_OF_YEAR, dayOfYear); + return gc.getTime(); + } +} + diff --git a/samples/springboot3/vpc-lattice-pet-store/src/main/resources/logback.xml b/samples/springboot3/vpc-lattice-pet-store/src/main/resources/logback.xml new file mode 100644 index 000000000..14a3a84fa --- /dev/null +++ b/samples/springboot3/vpc-lattice-pet-store/src/main/resources/logback.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/springboot3/vpc-lattice-pet-store/template.yaml b/samples/springboot3/vpc-lattice-pet-store/template.yaml new file mode 100644 index 000000000..69572535c --- /dev/null +++ b/samples/springboot3/vpc-lattice-pet-store/template.yaml @@ -0,0 +1,254 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 + +Description: Amazon VPC Lattice Demo + +Resources: + + # Basic VPC + + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: 10.0.0.0/16 + EnableDnsHostnames: true + EnableDnsSupport: true + + PrivateSubnet1: + Type: AWS::EC2::Subnet + Properties: + CidrBlock: 10.0.0.0/24 + MapPublicIpOnLaunch: false + VpcId: !Ref VPC + Tags: + - Key: Name + Value: Private Subnet (Source Subnet) + AvailabilityZone: !Select + - 0 + - Fn::GetAZs: !Ref AWS::Region + + PrivateSubnet2: + Type: AWS::EC2::Subnet + Properties: + CidrBlock: 10.0.1.0/24 + MapPublicIpOnLaunch: false + VpcId: !Ref VPC + Tags: + - Key: Name + Value: Private Subnet (Destination Subnet) + AvailabilityZone: !Select + - 1 + - Fn::GetAZs: !Ref AWS::Region + + RouteTablePrivate1: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: Private Route Table (Source Subnet) + + RouteTablePrivate1Association1: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref RouteTablePrivate1 + SubnetId: !Ref PrivateSubnet1 + + RouteTablePrivate2: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: Private Route Table (Destination Subnet) + + RouteTablePrivate2Association1: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref RouteTablePrivate2 + SubnetId: !Ref PrivateSubnet2 + + # VPC Lattice + + VPCLatticeServiceNetwork: + Type: AWS::VpcLattice::ServiceNetwork + Properties: + Name: demo-servicenetwork + AuthType: NONE + + VPCLatticeServiceNetworkSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for service network access + VpcId: !Ref VPC + SecurityGroupEgress: [] + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: !GetAtt VPC.CidrBlock + GroupName: demo-servicenetworksg + + VPCLatticeServiceNetworkVPCAssociation: + Type: AWS::VpcLattice::ServiceNetworkVpcAssociation + Properties: + SecurityGroupIds: + - !Ref VPCLatticeServiceNetworkSecurityGroup + ServiceNetworkIdentifier: !Ref VPCLatticeServiceNetwork + VpcIdentifier: !Ref VPC + + VPCLatticeService: + Type: AWS::VpcLattice::Service + Properties: + Name: demo-service + AuthType: NONE + + VPCLatticeServiceNetworkServiceAssociation: + Type: AWS::VpcLattice::ServiceNetworkServiceAssociation + Properties: + ServiceNetworkIdentifier: !Ref VPCLatticeServiceNetwork + ServiceIdentifier: !Ref VPCLatticeService + + VPCLatticeListener: + Type: AWS::VpcLattice::Listener + Properties: + Name: demo-listener + Port: 443 + Protocol: HTTPS + ServiceIdentifier: !Ref VPCLatticeService + DefaultAction: + Forward: + TargetGroups: + - TargetGroupIdentifier: !Ref VPCLatticeTargetGroup + Weight: 100 + + VPCLatticeTargetGroup: + Type: AWS::VpcLattice::TargetGroup + Properties: + Name: demo-targetgroup + Type: LAMBDA + Config: + LambdaEventStructureVersion: V2 + Targets: + - Id: !GetAtt InboundLambdaFunction.Arn + + # Outbound Lambda (Client) + + OutboundLambdaFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: lambda.amazonaws.com + Policies: + - PolicyName: root + PolicyDocument: + Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + - xray:PutTraceSegments + - xray:PutTelemetryRecords + - ec2:CreateNetworkInterface + - ec2:DescribeNetworkInterfaces + - ec2:DeleteNetworkInterface + Resource: '*' + + OutboundLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Role: !GetAtt OutboundLambdaFunctionRole.Arn + Runtime: python3.9 + CodeUri: app/ + MemorySize: 1512 + FunctionUrlConfig: + AuthType: AWS_IAM + InvokeMode: BUFFERED + Environment: + Variables: + ENDPOINT: !GetAtt VPCLatticeServiceNetworkServiceAssociation.DnsEntry.DomainName + VpcConfig: + SecurityGroupIds: + - !Ref OutboundLambdaFunctionSecurityGroup + SubnetIds: + - !Ref PrivateSubnet1 + + OutboundLambdaFunctionSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for OutboundLambdaFunction + VpcId: !Ref VPC + SecurityGroupEgress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 169.254.171.0/24 + SecurityGroupIngress: [] + GroupName: demo-outboundsg + + # Inbound Lambda (Service) + + InboundLambdaFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: lambda.amazonaws.com + Policies: + - PolicyName: root + PolicyDocument: + Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + - xray:PutTraceSegments + - xray:PutTelemetryRecords + - ec2:CreateNetworkInterface + - ec2:DescribeNetworkInterfaces + - ec2:DeleteNetworkInterface + Resource: '*' + + InboundLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.serverless.sample.springboot3.LambdaHandler::handleRequest + Runtime: java21 + CodeUri: . + MemorySize: 1512 + Role: !GetAtt InboundLambdaFunctionRole.Arn + VpcConfig: + SecurityGroupIds: + - !Ref InboundLambdaFunctionSecurityGroup + SubnetIds: + - !Ref PrivateSubnet2 + + InboundLambdaFunctionSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for InboundLambdaFunction + VpcId: !Ref VPC + SecurityGroupEgress: [] + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 169.254.171.0/24 + GroupName: demo-inboundsg + +Outputs: + OutboundFunctionUrlEndpoint: + Description: "Outbound Lambda Function URL Endpoint" + Value: + Fn::GetAtt: OutboundLambdaFunctionUrl.FunctionUrl +