From f2e0e5d21c281d1083dfb001c159ce11f96bcbaf Mon Sep 17 00:00:00 2001 From: mbfreder Date: Thu, 6 Jul 2023 13:46:22 -0700 Subject: [PATCH 1/9] Added common response class for APGWY and ALB --- .../runtime/events/AwsProxyResponseEvent.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AwsProxyResponseEvent.java diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AwsProxyResponseEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AwsProxyResponseEvent.java new file mode 100644 index 00000000..b8ec7bce --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AwsProxyResponseEvent.java @@ -0,0 +1,33 @@ +package com.amazonaws.services.lambda.runtime.events; + + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + Common response class for APIGateway and ALB + */ +@NoArgsConstructor +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AwsProxyResponseEvent { + private static final long serialVersionUID = 2263167344670024138L; + + private Integer statusCode; + + private String statusDescription; + + private List cookies; + + private Map headers; + + private Map> multiValueHeaders; + + private String body; + + private Boolean isBase64Encoded; +} From 31bd4d4acf0b4a65f1f71a2076369e7d66b53511 Mon Sep 17 00:00:00 2001 From: mbfreder Date: Thu, 6 Jul 2023 15:16:10 -0700 Subject: [PATCH 2/9] Added utility methods --- .../lambda/runtime/events/helper/Utils.java | 321 ++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/Utils.java diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/Utils.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/Utils.java new file mode 100644 index 00000000..de435682 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/Utils.java @@ -0,0 +1,321 @@ +package com.amazonaws.services.lambda.runtime.events.helper; + + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.*; + +/** + * This class exposes some utility methods to work with request values such as headers + * and query string parameters. + */ +public class Utils { + + static final String HEADER_KEY_VALUE_SEPARATOR = "="; + static final String HEADER_VALUE_SEPARATOR = ";"; + static final String HEADER_QUALIFIER_SEPARATOR = ","; + static final String ENCODING_VALUE_KEY = "charset"; + + + //------------------------------------------------------------- + // Methods + //------------------------------------------------------------- + + /** + * Given a map of key/values query string parameters from API Gateway, creates a query string as it would have + * been in the original url. + * @param parameters A Map<String, String> of query string parameters + * @param encode Whether the key and values should be URL encoded + * @param encodeCharset Charset to use for encoding the query string + * @return The generated query string for the URI + */ + public static String generateQueryString(Map> parameters, boolean encode, String encodeCharset) + throws Exception { + + if (parameters == null || parameters.size() == 0) { + return null; + } + + StringBuilder queryStringBuilder = new StringBuilder(); + + try { + for (String key : parameters.keySet()) { + for (String val : parameters.get(key)) { + queryStringBuilder.append("&"); + if (encode) { + queryStringBuilder.append(URLEncoder.encode(key, encodeCharset)); + } else { + queryStringBuilder.append(key); + } + queryStringBuilder.append("="); + if (val != null) { + if (encode) { + queryStringBuilder.append(URLEncoder.encode(val, encodeCharset)); + } else { + queryStringBuilder.append(val); + } + } + } + } + } catch (UnsupportedEncodingException e) { + throw new Exception("Invalid charset passed for query string encoding", e); + } + String queryString = null; + + queryString = queryStringBuilder.toString(); + queryString = queryString.substring(1); // remove the first & - faster to do it here than adding logic in the Lambda + return queryString; + } + + /** + * Parses a header value using the default value separator "," and qualifier separator ";". + * @param headerValue The value to be parsed + * @return A list of SimpleMapEntry objects with all of the possible values for the header. + */ + public static List parseHeaderValue(String headerValue) { + return parseHeaderValue(headerValue, HEADER_VALUE_SEPARATOR, HEADER_QUALIFIER_SEPARATOR); + } + + /** + * Generic method to parse an HTTP header value and split it into a list of key/values for all its components. + * When the property in the header does not specify a key the key field in the output pair is null and only the value + * is populated. For example, The header Accept: application/json; application/xml will contain two + * key value pairs with key null and the value set to application/json and application/xml respectively. + * + * @param headerValue The string value for the HTTP header + * @param valueSeparator The separator to be used for parsing header values + * @return A list of SimpleMapEntry objects with all of the possible values for the header. + */ + public static List parseHeaderValue(String headerValue, String valueSeparator, String qualifierSeparator) { + // Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8 + // Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5 + // Cookie: name=value; name2=value2; name3=value3 + // X-Custom-Header: YQ== + + List values = new ArrayList<>(); + if (headerValue == null) { + return values; + } + + for (String v : headerValue.split(valueSeparator)) { + String curValue = v; + float curPreference = 1.0f; + HeaderValue newValue = new HeaderValue(); + newValue.setRawValue(v); + + for (String q : curValue.split(qualifierSeparator)) { + + String[] kv = q.split(HEADER_KEY_VALUE_SEPARATOR, 2); + String key = null; + String val = null; + // no separator, set the value only + if (kv.length == 1) { + val = q.trim(); + } + // we have a separator + if (kv.length == 2) { + // if the length of the value is 0 we assume that we are looking at a + // base64 encoded value with padding, so we just set the value. This is because + // we assume that empty values in a key/value pair will contain at least a white space + if (kv[1].length() == 0) { + val = q.trim(); + } + // this was a base64 string with an additional = for padding, set the value only + if ("=".equals(kv[1].trim())) { + val = q.trim(); + } else { // it's a proper key/value set both + key = kv[0].trim(); + val = ("".equals(kv[1].trim()) ? null : kv[1].trim()); + } + } + + if (newValue.getValue() == null) { + newValue.setKey(key); + newValue.setValue(val); + } else { + // special case for quality q= + if ("q".equals(key)) { + curPreference = Float.parseFloat(val); + } else { + newValue.addAttribute(key, val); + } + } + } + newValue.setPriority(curPreference); + values.add(newValue); + } + + // sort list by preference + values.sort((HeaderValue first, HeaderValue second) -> { + if ((first.getPriority() - second.getPriority()) < .001f) { + return 0; + } + if (first.getPriority() < second.getPriority()) { + return 1; + } + return -1; + }); + return values; + } + + public static String parseCharacterEncoding(String contentTypeHeader) { + // we only look at content-type because content-encoding should only be used for + // "binary" requests such as gzip/deflate. + if (contentTypeHeader == null) { + return null; + } + + String[] contentTypeValues = contentTypeHeader.split(HEADER_VALUE_SEPARATOR); + if (contentTypeValues.length <= 1) { + return null; + } + + for (String contentTypeValue : contentTypeValues) { + if (contentTypeValue.trim().startsWith(ENCODING_VALUE_KEY)) { + String[] encodingValues = contentTypeValue.split(HEADER_KEY_VALUE_SEPARATOR); + if (encodingValues.length <= 1) { + return null; + } + return encodingValues[1]; + } + } + return null; + } + + public static String getFirstQueryParamValue(Map> queryString, String key, boolean isCaseSensitive) { + if (queryString != null) { + if (isCaseSensitive) { + return getFirst(queryString, key); + } + + for (String k : queryString.keySet()) { + if (k.toLowerCase(Locale.getDefault()).equals(key.toLowerCase(Locale.getDefault()))) { + return getFirst(queryString, k); + } + } + } + + return null; + } + + public static String[] getQueryParamValues(Map> qs, String key, boolean isCaseSensitive) { + if (qs != null) { + if (isCaseSensitive) { + return qs.get(key).toArray(new String[0]); + } + + for (String k : qs.keySet()) { + if (k.toLowerCase(Locale.getDefault()).equals(key.toLowerCase(Locale.getDefault()))) { + return qs.get(k).toArray(new String[0]); + } + } + } + + return new String[0]; + } + + public static String appendCharacterEncoding(String currentContentType, String newEncoding) { + if (currentContentType == null || "".equals(currentContentType.trim())) { + return null; + } + + if (currentContentType.contains(HEADER_VALUE_SEPARATOR)) { + String[] contentTypeValues = currentContentType.split(HEADER_VALUE_SEPARATOR); + StringBuilder contentType = new StringBuilder(contentTypeValues[0]); + + for (int i = 1; i < contentTypeValues.length; i++) { + String contentTypeValue = contentTypeValues[i]; + String contentTypeString = HEADER_VALUE_SEPARATOR + " " + contentTypeValue; + if (contentTypeValue.trim().startsWith(ENCODING_VALUE_KEY)) { + contentTypeString = HEADER_VALUE_SEPARATOR + " " + ENCODING_VALUE_KEY + HEADER_KEY_VALUE_SEPARATOR + newEncoding; + } + contentType.append(contentTypeString); + } + + return contentType.toString(); + } else { + return currentContentType + HEADER_VALUE_SEPARATOR + " " + ENCODING_VALUE_KEY + HEADER_KEY_VALUE_SEPARATOR + newEncoding; + } + } + + private static String getFirst(Map> map, String key) { + List values = map.get(key); + if (values == null || values.size() == 0) { + return null; + } + return values.get(0); + } + + /** + * Class that represents a header value. + */ + public static class HeaderValue { + private String key; + private String value; + private String rawValue; + private float priority; + private Map attributes; + + public HeaderValue() { + attributes = new HashMap<>(); + } + + + public String getKey() { + return key; + } + + + public void setKey(String key) { + this.key = key; + } + + + public String getValue() { + return value; + } + + + public void setValue(String value) { + this.value = value; + } + + + public String getRawValue() { + return rawValue; + } + + + public void setRawValue(String rawValue) { + this.rawValue = rawValue; + } + + + public float getPriority() { + return priority; + } + + + public void setPriority(float priority) { + this.priority = priority; + } + + + public Map getAttributes() { + return attributes; + } + + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + public void addAttribute(String key, String value) { + attributes.put(key, value); + } + + public String getAttribute(String key) { + return attributes.get(key); + } + } +} From d94b6f7cbdb8adf4beefd6750d1da9919db93061 Mon Sep 17 00:00:00 2001 From: mbfreder Date: Thu, 6 Jul 2023 18:23:31 -0700 Subject: [PATCH 3/9] Abstract APIGWY and ALB request classes --- .../events/ApplicationLoadBalancerRequestEvent.java | 11 ++++++++++- .../apigateway/APIGatewayProxyRequestEvent.java | 11 ++++++++++- .../events/apigateway/APIGatewayV2HTTPEvent.java | 11 ++++++++++- .../runtime/events/helper/AwsLambdaRequestEvent.java | 8 ++++++++ .../lambda/runtime/events/helper/RequestSource.java | 7 +++++++ 5 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/AwsLambdaRequestEvent.java create mode 100644 aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/RequestSource.java diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java index fca77846..93fcb154 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java @@ -13,6 +13,9 @@ package com.amazonaws.services.lambda.runtime.events; +import com.amazonaws.services.lambda.runtime.events.helper.AwsLambdaRequestEvent; +import com.amazonaws.services.lambda.runtime.events.helper.RequestSource; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -34,7 +37,13 @@ @Builder(setterPrefix = "with") @NoArgsConstructor @AllArgsConstructor -public class ApplicationLoadBalancerRequestEvent implements Serializable { +public class ApplicationLoadBalancerRequestEvent implements Serializable, AwsLambdaRequestEvent { + + @Override + @JsonIgnore + public RequestSource getRequestSource() { + return RequestSource.ALB; + } @Data @Builder(setterPrefix = "with") diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java index 0d8f3378..22b7c249 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java @@ -13,6 +13,9 @@ package com.amazonaws.services.lambda.runtime.events.apigateway; +import com.amazonaws.services.lambda.runtime.events.helper.AwsLambdaRequestEvent; +import com.amazonaws.services.lambda.runtime.events.helper.RequestSource; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -30,7 +33,7 @@ @Builder(setterPrefix = "with") @NoArgsConstructor @AllArgsConstructor -public class APIGatewayProxyRequestEvent implements Serializable { +public class APIGatewayProxyRequestEvent implements Serializable, AwsLambdaRequestEvent { private static final long serialVersionUID = 4189228800688527467L; @@ -47,6 +50,12 @@ public class APIGatewayProxyRequestEvent implements Serializable { private String body; private Boolean isBase64Encoded; + @Override + @JsonIgnore + public RequestSource getRequestSource() { + return RequestSource.API_GATEWAY_REST; + } + /** * class that represents proxy request context */ diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java index d2f713ad..1bb81261 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java @@ -13,6 +13,9 @@ package com.amazonaws.services.lambda.runtime.events.apigateway; +import com.amazonaws.services.lambda.runtime.events.helper.AwsLambdaRequestEvent; +import com.amazonaws.services.lambda.runtime.events.helper.RequestSource; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -30,7 +33,7 @@ @Builder(setterPrefix = "with") @Data @NoArgsConstructor -public class APIGatewayV2HTTPEvent { +public class APIGatewayV2HTTPEvent implements AwsLambdaRequestEvent { private String version; private String routeKey; @@ -45,6 +48,12 @@ public class APIGatewayV2HTTPEvent { private boolean isBase64Encoded; private RequestContext requestContext; + @Override + @JsonIgnore + public RequestSource getRequestSource() { + return RequestSource.API_GATEWAY_HTTP; + } + @AllArgsConstructor @Builder(setterPrefix = "with") @Data diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/AwsLambdaRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/AwsLambdaRequestEvent.java new file mode 100644 index 00000000..f676e9b7 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/AwsLambdaRequestEvent.java @@ -0,0 +1,8 @@ +package com.amazonaws.services.lambda.runtime.events.helper; + +/** + * Interface to abstract APIGWY and ALB request classes, reduce complexity and increase efficiency. + */ +public interface AwsLambdaRequestEvent { + RequestSource getRequestSource(); +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/RequestSource.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/RequestSource.java new file mode 100644 index 00000000..07761928 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/RequestSource.java @@ -0,0 +1,7 @@ +package com.amazonaws.services.lambda.runtime.events.helper; + +public enum RequestSource { + API_GATEWAY_REST, + API_GATEWAY_HTTP, + ALB +} From 0bfbb350300e1da9f1da5d35c1f3de3d982f2608 Mon Sep 17 00:00:00 2001 From: mbfreder Date: Thu, 7 Sep 2023 13:01:04 -0700 Subject: [PATCH 4/9] Renamed Utils to HeaderUtils --- .../runtime/events/helper/{Utils.java => HeaderUtils.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/{Utils.java => HeaderUtils.java} (99%) diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/Utils.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java similarity index 99% rename from aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/Utils.java rename to aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java index de435682..153c439e 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/Utils.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java @@ -9,7 +9,7 @@ * This class exposes some utility methods to work with request values such as headers * and query string parameters. */ -public class Utils { +public class HeaderUtils { static final String HEADER_KEY_VALUE_SEPARATOR = "="; static final String HEADER_VALUE_SEPARATOR = ";"; From b01e9c2185d1012f74b4d05d9340aebff1e2728f Mon Sep 17 00:00:00 2001 From: mbfreder Date: Mon, 18 Sep 2023 09:31:17 -0700 Subject: [PATCH 5/9] Moved AwsLambdaRequestEvent to events folder --- .../runtime/events/ApplicationLoadBalancerRequestEvent.java | 1 - .../runtime/events/{helper => }/AwsLambdaRequestEvent.java | 4 +++- .../events/apigateway/APIGatewayProxyRequestEvent.java | 2 +- .../runtime/events/apigateway/APIGatewayV2HTTPEvent.java | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) rename aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/{helper => }/AwsLambdaRequestEvent.java (59%) diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java index 93fcb154..847768a2 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java @@ -13,7 +13,6 @@ package com.amazonaws.services.lambda.runtime.events; -import com.amazonaws.services.lambda.runtime.events.helper.AwsLambdaRequestEvent; import com.amazonaws.services.lambda.runtime.events.helper.RequestSource; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/AwsLambdaRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AwsLambdaRequestEvent.java similarity index 59% rename from aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/AwsLambdaRequestEvent.java rename to aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AwsLambdaRequestEvent.java index f676e9b7..cf55f3c7 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/AwsLambdaRequestEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AwsLambdaRequestEvent.java @@ -1,4 +1,6 @@ -package com.amazonaws.services.lambda.runtime.events.helper; +package com.amazonaws.services.lambda.runtime.events; + +import com.amazonaws.services.lambda.runtime.events.helper.RequestSource; /** * Interface to abstract APIGWY and ALB request classes, reduce complexity and increase efficiency. diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java index 22b7c249..8afa67ac 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java @@ -13,7 +13,7 @@ package com.amazonaws.services.lambda.runtime.events.apigateway; -import com.amazonaws.services.lambda.runtime.events.helper.AwsLambdaRequestEvent; +import com.amazonaws.services.lambda.runtime.events.AwsLambdaRequestEvent; import com.amazonaws.services.lambda.runtime.events.helper.RequestSource; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java index 1bb81261..b56ba774 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java @@ -13,7 +13,7 @@ package com.amazonaws.services.lambda.runtime.events.apigateway; -import com.amazonaws.services.lambda.runtime.events.helper.AwsLambdaRequestEvent; +import com.amazonaws.services.lambda.runtime.events.AwsLambdaRequestEvent; import com.amazonaws.services.lambda.runtime.events.helper.RequestSource; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; From be46557ab856cc0527d09941858b75535381e04f Mon Sep 17 00:00:00 2001 From: mbfreder Date: Wed, 20 Sep 2023 10:30:32 -0700 Subject: [PATCH 6/9] Split getFirstQueryParamValue() into two different methods --- .../runtime/events/helper/HeaderUtils.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java index 153c439e..c9e736e2 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java @@ -15,6 +15,7 @@ public class HeaderUtils { static final String HEADER_VALUE_SEPARATOR = ";"; static final String HEADER_QUALIFIER_SEPARATOR = ","; static final String ENCODING_VALUE_KEY = "charset"; + static final Float EPSILON = .001f; //------------------------------------------------------------- @@ -70,7 +71,7 @@ public static String generateQueryString(Map> parameters, b /** * Parses a header value using the default value separator "," and qualifier separator ";". * @param headerValue The value to be parsed - * @return A list of SimpleMapEntry objects with all of the possible values for the header. + * @return A list of HeaderValue objects */ public static List parseHeaderValue(String headerValue) { return parseHeaderValue(headerValue, HEADER_VALUE_SEPARATOR, HEADER_QUALIFIER_SEPARATOR); @@ -84,7 +85,7 @@ public static List parseHeaderValue(String headerValue) { * * @param headerValue The string value for the HTTP header * @param valueSeparator The separator to be used for parsing header values - * @return A list of SimpleMapEntry objects with all of the possible values for the header. + * @return A list of HeaderValue objects with all of the possible values for the header. */ public static List parseHeaderValue(String headerValue, String valueSeparator, String qualifierSeparator) { // Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8 @@ -147,7 +148,8 @@ public static List parseHeaderValue(String headerValue, String valu // sort list by preference values.sort((HeaderValue first, HeaderValue second) -> { - if ((first.getPriority() - second.getPriority()) < .001f) { + // if the floats values are close enough, we consider them to be equal + if ((first.getPriority() - second.getPriority()) < EPSILON) { return 0; } if (first.getPriority() < second.getPriority()) { @@ -182,12 +184,8 @@ public static String parseCharacterEncoding(String contentTypeHeader) { return null; } - public static String getFirstQueryParamValue(Map> queryString, String key, boolean isCaseSensitive) { + public static String getFirstQueryParamValue(Map> queryString, String key) { if (queryString != null) { - if (isCaseSensitive) { - return getFirst(queryString, key); - } - for (String k : queryString.keySet()) { if (k.toLowerCase(Locale.getDefault()).equals(key.toLowerCase(Locale.getDefault()))) { return getFirst(queryString, k); @@ -198,6 +196,10 @@ public static String getFirstQueryParamValue(Map> queryStri return null; } + public static String getFirstCaseSensitiveQueryParamValue(Map> queryString, String key) { + return getFirst(queryString, key); + } + public static String[] getQueryParamValues(Map> qs, String key, boolean isCaseSensitive) { if (qs != null) { if (isCaseSensitive) { From 1f3f1bd971a65ddeb58b5278ee364a1e88b3ecfa Mon Sep 17 00:00:00 2001 From: mbfreder Date: Thu, 28 Sep 2023 00:24:06 -0700 Subject: [PATCH 7/9] Added unit tests --- .../runtime/events/helper/HeaderUtils.java | 10 - .../events/helper/HeaderUtilsTest.java | 171 ++++++++++++++++++ 2 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtilsTest.java diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java index c9e736e2..dee659ba 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java @@ -262,52 +262,42 @@ public HeaderValue() { attributes = new HashMap<>(); } - public String getKey() { return key; } - public void setKey(String key) { this.key = key; } - public String getValue() { return value; } - public void setValue(String value) { this.value = value; } - public String getRawValue() { return rawValue; } - public void setRawValue(String rawValue) { this.rawValue = rawValue; } - public float getPriority() { return priority; } - public void setPriority(float priority) { this.priority = priority; } - public Map getAttributes() { return attributes; } - public void setAttributes(Map attributes) { this.attributes = attributes; } diff --git a/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtilsTest.java b/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtilsTest.java new file mode 100644 index 00000000..d9eb223d --- /dev/null +++ b/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtilsTest.java @@ -0,0 +1,171 @@ +package com.amazonaws.services.lambda.runtime.events.helper; + +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class HeaderUtilsTest { + + private Map> queryString = new HashMap<>(); + private List list1 = Arrays.asList("value1", "value2"); + private List list2 = Arrays.asList("value3", "value4"); + public static String ENCODED_CHARSET = "UTF-8"; + static final String HEADER_VALUE_SEPARATOR = ";"; + static final String HEADER_QUALIFIER_SEPARATOR = ","; + static final String ENCODING_VALUE_KEY = "charset"; + + @Test + public void getFirstQueryParamValue_caseInsensitive_returnsValue() { + queryString.put("key1", list1); + queryString.put("key2", list2); + assertEquals("value1", HeaderUtils.getFirstQueryParamValue(queryString, "key1")); + assertEquals("value1", HeaderUtils.getFirstQueryParamValue(queryString, "KEY1")); + } + + @Test + public void getFirstQueryParamValue_caseSensitive_returnsValue() { + queryString.put("KEY1", list1); + queryString.put("key2", list2); + assertEquals("value1", HeaderUtils.getFirstCaseSensitiveQueryParamValue(queryString, "KEY1")); + assertNotEquals("value3", HeaderUtils.getFirstCaseSensitiveQueryParamValue(queryString, "KEY2")); + } + + @Test + public void queryString_generateQueryString_validQuery() { + queryString.put("key1", list1); + queryString.put("key2", list2); + String parsedString = null; + try { + parsedString = HeaderUtils.generateQueryString(queryString, true, ENCODED_CHARSET); + } catch (Exception e) { + throw new RuntimeException(e); + } + + assertTrue(parsedString.contains("key1=value1")); + assertTrue(parsedString.contains("key2=value3")); + assertTrue(parsedString.contains("&") && parsedString.indexOf("&") > 0 && parsedString.indexOf("&") < parsedString.length()); + } + + @Test + void queryString_generateQueryString_nullParameterIsEmpty() { + list2 = Arrays.asList(null, "value4"); + queryString.put("key2", list2); + String parsedString = null; + try { + parsedString = HeaderUtils.generateQueryString(queryString, true, ENCODED_CHARSET); + } catch (Exception e) { + throw new RuntimeException(e); + } + assertTrue(parsedString.startsWith("key2=&")); + } + + @Test + void queryStringWithEncodedParams_generateQueryString_validQuery() { + list2 = Arrays.asList("{\"name\":\"faisal\"}"); + queryString.put("key2", list2); + queryString.put("key1", list1); + String parsedString = null; + try { + parsedString = HeaderUtils.generateQueryString(queryString, true, ENCODED_CHARSET); + } catch (Exception e) { + throw new RuntimeException(e); + } + + assertTrue(parsedString.contains("key1=value1")); + assertTrue(parsedString.contains("key2=%7B%22name%22%3A%22faisal%22%7D")); + assertTrue(parsedString.contains("&") && parsedString.indexOf("&") > 0 && parsedString.indexOf("&") < parsedString.length()); + } + + @Test + void headers_parseHeaderValue_multiValue() { + String headerValue = "application/xml; charset=utf-8"; + List values = HeaderUtils.parseHeaderValue(headerValue); + + assertEquals(2, values.size()); + assertEquals("application/xml", values.get(0).getValue()); + assertNull(values.get(0).getKey()); + + assertEquals(ENCODING_VALUE_KEY, values.get(1).getKey()); + assertEquals("utf-8", values.get(1).getValue()); + } + + @Test + void headers_parseHeaderValue_validMultipleCookie() { + String headerValue = "yummy_cookie=choco; tasty_cookie=strawberry"; + List values = HeaderUtils.parseHeaderValue(headerValue, HEADER_VALUE_SEPARATOR, HEADER_QUALIFIER_SEPARATOR); + + assertEquals(2, values.size()); + assertEquals("yummy_cookie", values.get(0).getKey()); + assertEquals("choco", values.get(0).getValue()); + assertEquals("tasty_cookie", values.get(1).getKey()); + assertEquals("strawberry", values.get(1).getValue()); + } + + @Test + void headers_parseHeaderValue_complexAccept() { + String headerValue = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; + List values = HeaderUtils.parseHeaderValue(headerValue, HEADER_QUALIFIER_SEPARATOR, HEADER_VALUE_SEPARATOR); + + assertEquals(4, values.size()); + } + + @Test + void headers_parseHeaderValue_encodedContentWithEquals() { + String headerValue = Base64.getUrlEncoder().encodeToString("a".getBytes()); + List values = HeaderUtils.parseHeaderValue(headerValue); + + assertTrue(values.size() > 0); + assertEquals("YQ==", values.get(0).getValue()); + } + + @Test + void headers_parseHeaderValue_base64EncodedCookieValue() { + String value = Base64.getUrlEncoder().encodeToString("a".getBytes()); + String cookieValue = "jwt=" + value + "; secondValue=second"; + + List values = HeaderUtils.parseHeaderValue(cookieValue); + + assertEquals(2, values.size()); + assertEquals("jwt", values.get(0).getKey()); + assertEquals(value, values.get(0).getValue()); + } + + @Test + void headers_parseHeaderValue_cookieWithSeparatorInValue() { + String cookieValue = "jwt==test; secondValue=second"; + + List values = HeaderUtils.parseHeaderValue(cookieValue); + + assertEquals(2, values.size()); + assertEquals("jwt", values.get(0).getKey()); + assertEquals("=test", values.get(0).getValue()); + } + + @Test + void headers_parseHeaderValue_headerWithPaddingButNotBase64Encoded() { + List result = HeaderUtils.parseHeaderValue("hello="); + assertTrue(result.size() > 0); + assertEquals("hello", result.get(0).getKey()); + assertNull(result.get(0).getValue()); + } + + @Test + void contentType_parseCharacterEncoding_validEncodingValue() { + String contentTypeHeader = "application/xml; charset=utf-8"; + String encodingValue = HeaderUtils.parseCharacterEncoding(contentTypeHeader); + + assertEquals("utf-8", encodingValue); + } + + @Test + void contentType_appendCharacterEncoding_validEncodingValue() { + String currentContentType = "application/xml"; + String newEncoding = StandardCharsets.UTF_8.name(); + String contentType = HeaderUtils.appendCharacterEncoding(currentContentType, newEncoding); + + assertEquals("application/xml; charset=UTF-8", contentType); + } +} From 5a5f4199aa10c477746bd097cb8f68a5fd32f97d Mon Sep 17 00:00:00 2001 From: mbfreder Date: Tue, 3 Oct 2023 12:03:08 -0700 Subject: [PATCH 8/9] removed helper functions --- .../ApplicationLoadBalancerRequestEvent.java | 5 +- .../runtime/events/AwsLambdaRequestEvent.java | 10 - .../APIGatewayProxyRequestEvent.java | 4 +- .../apigateway/APIGatewayV2HTTPEvent.java | 4 +- .../events/apigateway/LambdaRequestEvent.java | 10 + .../{helper => apigateway}/RequestSource.java | 2 +- .../runtime/events/helper/HeaderUtils.java | 313 ------------------ .../events/helper/HeaderUtilsTest.java | 171 ---------- 8 files changed, 16 insertions(+), 503 deletions(-) delete mode 100644 aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AwsLambdaRequestEvent.java create mode 100644 aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/LambdaRequestEvent.java rename aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/{helper => apigateway}/RequestSource.java (56%) delete mode 100644 aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java delete mode 100644 aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtilsTest.java diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java index 847768a2..d79ca95a 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java @@ -13,7 +13,8 @@ package com.amazonaws.services.lambda.runtime.events; -import com.amazonaws.services.lambda.runtime.events.helper.RequestSource; +import com.amazonaws.services.lambda.runtime.events.apigateway.LambdaRequestEvent; +import com.amazonaws.services.lambda.runtime.events.apigateway.RequestSource; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Builder; @@ -36,7 +37,7 @@ @Builder(setterPrefix = "with") @NoArgsConstructor @AllArgsConstructor -public class ApplicationLoadBalancerRequestEvent implements Serializable, AwsLambdaRequestEvent { +public class ApplicationLoadBalancerRequestEvent implements Serializable, LambdaRequestEvent { @Override @JsonIgnore diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AwsLambdaRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AwsLambdaRequestEvent.java deleted file mode 100644 index cf55f3c7..00000000 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AwsLambdaRequestEvent.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.amazonaws.services.lambda.runtime.events; - -import com.amazonaws.services.lambda.runtime.events.helper.RequestSource; - -/** - * Interface to abstract APIGWY and ALB request classes, reduce complexity and increase efficiency. - */ -public interface AwsLambdaRequestEvent { - RequestSource getRequestSource(); -} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java index 8afa67ac..08c2523f 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java @@ -13,8 +13,6 @@ package com.amazonaws.services.lambda.runtime.events.apigateway; -import com.amazonaws.services.lambda.runtime.events.AwsLambdaRequestEvent; -import com.amazonaws.services.lambda.runtime.events.helper.RequestSource; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Builder; @@ -33,7 +31,7 @@ @Builder(setterPrefix = "with") @NoArgsConstructor @AllArgsConstructor -public class APIGatewayProxyRequestEvent implements Serializable, AwsLambdaRequestEvent { +public class APIGatewayProxyRequestEvent implements Serializable, LambdaRequestEvent { private static final long serialVersionUID = 4189228800688527467L; diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java index b56ba774..f03987df 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java @@ -13,8 +13,6 @@ package com.amazonaws.services.lambda.runtime.events.apigateway; -import com.amazonaws.services.lambda.runtime.events.AwsLambdaRequestEvent; -import com.amazonaws.services.lambda.runtime.events.helper.RequestSource; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Builder; @@ -33,7 +31,7 @@ @Builder(setterPrefix = "with") @Data @NoArgsConstructor -public class APIGatewayV2HTTPEvent implements AwsLambdaRequestEvent { +public class APIGatewayV2HTTPEvent implements LambdaRequestEvent { private String version; private String routeKey; diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/LambdaRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/LambdaRequestEvent.java new file mode 100644 index 00000000..4c2834c7 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/LambdaRequestEvent.java @@ -0,0 +1,10 @@ +package com.amazonaws.services.lambda.runtime.events.apigateway; + +import com.amazonaws.services.lambda.runtime.events.apigateway.RequestSource; + +/** + * Interface to abstract APIGWY and ALB request classes, reduce complexity and increase efficiency. + */ +public interface LambdaRequestEvent { + RequestSource getRequestSource(); +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/RequestSource.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/RequestSource.java similarity index 56% rename from aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/RequestSource.java rename to aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/RequestSource.java index 07761928..5c0bd19a 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/RequestSource.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/RequestSource.java @@ -1,4 +1,4 @@ -package com.amazonaws.services.lambda.runtime.events.helper; +package com.amazonaws.services.lambda.runtime.events.apigateway; public enum RequestSource { API_GATEWAY_REST, diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java deleted file mode 100644 index dee659ba..00000000 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtils.java +++ /dev/null @@ -1,313 +0,0 @@ -package com.amazonaws.services.lambda.runtime.events.helper; - - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.*; - -/** - * This class exposes some utility methods to work with request values such as headers - * and query string parameters. - */ -public class HeaderUtils { - - static final String HEADER_KEY_VALUE_SEPARATOR = "="; - static final String HEADER_VALUE_SEPARATOR = ";"; - static final String HEADER_QUALIFIER_SEPARATOR = ","; - static final String ENCODING_VALUE_KEY = "charset"; - static final Float EPSILON = .001f; - - - //------------------------------------------------------------- - // Methods - //------------------------------------------------------------- - - /** - * Given a map of key/values query string parameters from API Gateway, creates a query string as it would have - * been in the original url. - * @param parameters A Map<String, String> of query string parameters - * @param encode Whether the key and values should be URL encoded - * @param encodeCharset Charset to use for encoding the query string - * @return The generated query string for the URI - */ - public static String generateQueryString(Map> parameters, boolean encode, String encodeCharset) - throws Exception { - - if (parameters == null || parameters.size() == 0) { - return null; - } - - StringBuilder queryStringBuilder = new StringBuilder(); - - try { - for (String key : parameters.keySet()) { - for (String val : parameters.get(key)) { - queryStringBuilder.append("&"); - if (encode) { - queryStringBuilder.append(URLEncoder.encode(key, encodeCharset)); - } else { - queryStringBuilder.append(key); - } - queryStringBuilder.append("="); - if (val != null) { - if (encode) { - queryStringBuilder.append(URLEncoder.encode(val, encodeCharset)); - } else { - queryStringBuilder.append(val); - } - } - } - } - } catch (UnsupportedEncodingException e) { - throw new Exception("Invalid charset passed for query string encoding", e); - } - String queryString = null; - - queryString = queryStringBuilder.toString(); - queryString = queryString.substring(1); // remove the first & - faster to do it here than adding logic in the Lambda - return queryString; - } - - /** - * Parses a header value using the default value separator "," and qualifier separator ";". - * @param headerValue The value to be parsed - * @return A list of HeaderValue objects - */ - public static List parseHeaderValue(String headerValue) { - return parseHeaderValue(headerValue, HEADER_VALUE_SEPARATOR, HEADER_QUALIFIER_SEPARATOR); - } - - /** - * Generic method to parse an HTTP header value and split it into a list of key/values for all its components. - * When the property in the header does not specify a key the key field in the output pair is null and only the value - * is populated. For example, The header Accept: application/json; application/xml will contain two - * key value pairs with key null and the value set to application/json and application/xml respectively. - * - * @param headerValue The string value for the HTTP header - * @param valueSeparator The separator to be used for parsing header values - * @return A list of HeaderValue objects with all of the possible values for the header. - */ - public static List parseHeaderValue(String headerValue, String valueSeparator, String qualifierSeparator) { - // Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8 - // Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5 - // Cookie: name=value; name2=value2; name3=value3 - // X-Custom-Header: YQ== - - List values = new ArrayList<>(); - if (headerValue == null) { - return values; - } - - for (String v : headerValue.split(valueSeparator)) { - String curValue = v; - float curPreference = 1.0f; - HeaderValue newValue = new HeaderValue(); - newValue.setRawValue(v); - - for (String q : curValue.split(qualifierSeparator)) { - - String[] kv = q.split(HEADER_KEY_VALUE_SEPARATOR, 2); - String key = null; - String val = null; - // no separator, set the value only - if (kv.length == 1) { - val = q.trim(); - } - // we have a separator - if (kv.length == 2) { - // if the length of the value is 0 we assume that we are looking at a - // base64 encoded value with padding, so we just set the value. This is because - // we assume that empty values in a key/value pair will contain at least a white space - if (kv[1].length() == 0) { - val = q.trim(); - } - // this was a base64 string with an additional = for padding, set the value only - if ("=".equals(kv[1].trim())) { - val = q.trim(); - } else { // it's a proper key/value set both - key = kv[0].trim(); - val = ("".equals(kv[1].trim()) ? null : kv[1].trim()); - } - } - - if (newValue.getValue() == null) { - newValue.setKey(key); - newValue.setValue(val); - } else { - // special case for quality q= - if ("q".equals(key)) { - curPreference = Float.parseFloat(val); - } else { - newValue.addAttribute(key, val); - } - } - } - newValue.setPriority(curPreference); - values.add(newValue); - } - - // sort list by preference - values.sort((HeaderValue first, HeaderValue second) -> { - // if the floats values are close enough, we consider them to be equal - if ((first.getPriority() - second.getPriority()) < EPSILON) { - return 0; - } - if (first.getPriority() < second.getPriority()) { - return 1; - } - return -1; - }); - return values; - } - - public static String parseCharacterEncoding(String contentTypeHeader) { - // we only look at content-type because content-encoding should only be used for - // "binary" requests such as gzip/deflate. - if (contentTypeHeader == null) { - return null; - } - - String[] contentTypeValues = contentTypeHeader.split(HEADER_VALUE_SEPARATOR); - if (contentTypeValues.length <= 1) { - return null; - } - - for (String contentTypeValue : contentTypeValues) { - if (contentTypeValue.trim().startsWith(ENCODING_VALUE_KEY)) { - String[] encodingValues = contentTypeValue.split(HEADER_KEY_VALUE_SEPARATOR); - if (encodingValues.length <= 1) { - return null; - } - return encodingValues[1]; - } - } - return null; - } - - public static String getFirstQueryParamValue(Map> queryString, String key) { - if (queryString != null) { - for (String k : queryString.keySet()) { - if (k.toLowerCase(Locale.getDefault()).equals(key.toLowerCase(Locale.getDefault()))) { - return getFirst(queryString, k); - } - } - } - - return null; - } - - public static String getFirstCaseSensitiveQueryParamValue(Map> queryString, String key) { - return getFirst(queryString, key); - } - - public static String[] getQueryParamValues(Map> qs, String key, boolean isCaseSensitive) { - if (qs != null) { - if (isCaseSensitive) { - return qs.get(key).toArray(new String[0]); - } - - for (String k : qs.keySet()) { - if (k.toLowerCase(Locale.getDefault()).equals(key.toLowerCase(Locale.getDefault()))) { - return qs.get(k).toArray(new String[0]); - } - } - } - - return new String[0]; - } - - public static String appendCharacterEncoding(String currentContentType, String newEncoding) { - if (currentContentType == null || "".equals(currentContentType.trim())) { - return null; - } - - if (currentContentType.contains(HEADER_VALUE_SEPARATOR)) { - String[] contentTypeValues = currentContentType.split(HEADER_VALUE_SEPARATOR); - StringBuilder contentType = new StringBuilder(contentTypeValues[0]); - - for (int i = 1; i < contentTypeValues.length; i++) { - String contentTypeValue = contentTypeValues[i]; - String contentTypeString = HEADER_VALUE_SEPARATOR + " " + contentTypeValue; - if (contentTypeValue.trim().startsWith(ENCODING_VALUE_KEY)) { - contentTypeString = HEADER_VALUE_SEPARATOR + " " + ENCODING_VALUE_KEY + HEADER_KEY_VALUE_SEPARATOR + newEncoding; - } - contentType.append(contentTypeString); - } - - return contentType.toString(); - } else { - return currentContentType + HEADER_VALUE_SEPARATOR + " " + ENCODING_VALUE_KEY + HEADER_KEY_VALUE_SEPARATOR + newEncoding; - } - } - - private static String getFirst(Map> map, String key) { - List values = map.get(key); - if (values == null || values.size() == 0) { - return null; - } - return values.get(0); - } - - /** - * Class that represents a header value. - */ - public static class HeaderValue { - private String key; - private String value; - private String rawValue; - private float priority; - private Map attributes; - - public HeaderValue() { - attributes = new HashMap<>(); - } - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public String getRawValue() { - return rawValue; - } - - public void setRawValue(String rawValue) { - this.rawValue = rawValue; - } - - public float getPriority() { - return priority; - } - - public void setPriority(float priority) { - this.priority = priority; - } - - public Map getAttributes() { - return attributes; - } - - public void setAttributes(Map attributes) { - this.attributes = attributes; - } - - public void addAttribute(String key, String value) { - attributes.put(key, value); - } - - public String getAttribute(String key) { - return attributes.get(key); - } - } -} diff --git a/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtilsTest.java b/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtilsTest.java deleted file mode 100644 index d9eb223d..00000000 --- a/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/helper/HeaderUtilsTest.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.amazonaws.services.lambda.runtime.events.helper; - -import org.junit.jupiter.api.Test; - -import java.nio.charset.StandardCharsets; -import java.util.*; - -import static org.junit.jupiter.api.Assertions.*; - -public class HeaderUtilsTest { - - private Map> queryString = new HashMap<>(); - private List list1 = Arrays.asList("value1", "value2"); - private List list2 = Arrays.asList("value3", "value4"); - public static String ENCODED_CHARSET = "UTF-8"; - static final String HEADER_VALUE_SEPARATOR = ";"; - static final String HEADER_QUALIFIER_SEPARATOR = ","; - static final String ENCODING_VALUE_KEY = "charset"; - - @Test - public void getFirstQueryParamValue_caseInsensitive_returnsValue() { - queryString.put("key1", list1); - queryString.put("key2", list2); - assertEquals("value1", HeaderUtils.getFirstQueryParamValue(queryString, "key1")); - assertEquals("value1", HeaderUtils.getFirstQueryParamValue(queryString, "KEY1")); - } - - @Test - public void getFirstQueryParamValue_caseSensitive_returnsValue() { - queryString.put("KEY1", list1); - queryString.put("key2", list2); - assertEquals("value1", HeaderUtils.getFirstCaseSensitiveQueryParamValue(queryString, "KEY1")); - assertNotEquals("value3", HeaderUtils.getFirstCaseSensitiveQueryParamValue(queryString, "KEY2")); - } - - @Test - public void queryString_generateQueryString_validQuery() { - queryString.put("key1", list1); - queryString.put("key2", list2); - String parsedString = null; - try { - parsedString = HeaderUtils.generateQueryString(queryString, true, ENCODED_CHARSET); - } catch (Exception e) { - throw new RuntimeException(e); - } - - assertTrue(parsedString.contains("key1=value1")); - assertTrue(parsedString.contains("key2=value3")); - assertTrue(parsedString.contains("&") && parsedString.indexOf("&") > 0 && parsedString.indexOf("&") < parsedString.length()); - } - - @Test - void queryString_generateQueryString_nullParameterIsEmpty() { - list2 = Arrays.asList(null, "value4"); - queryString.put("key2", list2); - String parsedString = null; - try { - parsedString = HeaderUtils.generateQueryString(queryString, true, ENCODED_CHARSET); - } catch (Exception e) { - throw new RuntimeException(e); - } - assertTrue(parsedString.startsWith("key2=&")); - } - - @Test - void queryStringWithEncodedParams_generateQueryString_validQuery() { - list2 = Arrays.asList("{\"name\":\"faisal\"}"); - queryString.put("key2", list2); - queryString.put("key1", list1); - String parsedString = null; - try { - parsedString = HeaderUtils.generateQueryString(queryString, true, ENCODED_CHARSET); - } catch (Exception e) { - throw new RuntimeException(e); - } - - assertTrue(parsedString.contains("key1=value1")); - assertTrue(parsedString.contains("key2=%7B%22name%22%3A%22faisal%22%7D")); - assertTrue(parsedString.contains("&") && parsedString.indexOf("&") > 0 && parsedString.indexOf("&") < parsedString.length()); - } - - @Test - void headers_parseHeaderValue_multiValue() { - String headerValue = "application/xml; charset=utf-8"; - List values = HeaderUtils.parseHeaderValue(headerValue); - - assertEquals(2, values.size()); - assertEquals("application/xml", values.get(0).getValue()); - assertNull(values.get(0).getKey()); - - assertEquals(ENCODING_VALUE_KEY, values.get(1).getKey()); - assertEquals("utf-8", values.get(1).getValue()); - } - - @Test - void headers_parseHeaderValue_validMultipleCookie() { - String headerValue = "yummy_cookie=choco; tasty_cookie=strawberry"; - List values = HeaderUtils.parseHeaderValue(headerValue, HEADER_VALUE_SEPARATOR, HEADER_QUALIFIER_SEPARATOR); - - assertEquals(2, values.size()); - assertEquals("yummy_cookie", values.get(0).getKey()); - assertEquals("choco", values.get(0).getValue()); - assertEquals("tasty_cookie", values.get(1).getKey()); - assertEquals("strawberry", values.get(1).getValue()); - } - - @Test - void headers_parseHeaderValue_complexAccept() { - String headerValue = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; - List values = HeaderUtils.parseHeaderValue(headerValue, HEADER_QUALIFIER_SEPARATOR, HEADER_VALUE_SEPARATOR); - - assertEquals(4, values.size()); - } - - @Test - void headers_parseHeaderValue_encodedContentWithEquals() { - String headerValue = Base64.getUrlEncoder().encodeToString("a".getBytes()); - List values = HeaderUtils.parseHeaderValue(headerValue); - - assertTrue(values.size() > 0); - assertEquals("YQ==", values.get(0).getValue()); - } - - @Test - void headers_parseHeaderValue_base64EncodedCookieValue() { - String value = Base64.getUrlEncoder().encodeToString("a".getBytes()); - String cookieValue = "jwt=" + value + "; secondValue=second"; - - List values = HeaderUtils.parseHeaderValue(cookieValue); - - assertEquals(2, values.size()); - assertEquals("jwt", values.get(0).getKey()); - assertEquals(value, values.get(0).getValue()); - } - - @Test - void headers_parseHeaderValue_cookieWithSeparatorInValue() { - String cookieValue = "jwt==test; secondValue=second"; - - List values = HeaderUtils.parseHeaderValue(cookieValue); - - assertEquals(2, values.size()); - assertEquals("jwt", values.get(0).getKey()); - assertEquals("=test", values.get(0).getValue()); - } - - @Test - void headers_parseHeaderValue_headerWithPaddingButNotBase64Encoded() { - List result = HeaderUtils.parseHeaderValue("hello="); - assertTrue(result.size() > 0); - assertEquals("hello", result.get(0).getKey()); - assertNull(result.get(0).getValue()); - } - - @Test - void contentType_parseCharacterEncoding_validEncodingValue() { - String contentTypeHeader = "application/xml; charset=utf-8"; - String encodingValue = HeaderUtils.parseCharacterEncoding(contentTypeHeader); - - assertEquals("utf-8", encodingValue); - } - - @Test - void contentType_appendCharacterEncoding_validEncodingValue() { - String currentContentType = "application/xml"; - String newEncoding = StandardCharsets.UTF_8.name(); - String contentType = HeaderUtils.appendCharacterEncoding(currentContentType, newEncoding); - - assertEquals("application/xml; charset=UTF-8", contentType); - } -} From f51cb356625b6652f6fe3adeacdde2acf51a9dba Mon Sep 17 00:00:00 2001 From: mbfreder Date: Wed, 4 Oct 2023 10:22:59 -0700 Subject: [PATCH 9/9] Renamed LambdaRequestEvent to HttpRequestEvent --- .../events/ApplicationLoadBalancerRequestEvent.java | 4 ++-- .../events/apigateway/APIGatewayProxyRequestEvent.java | 2 +- .../events/apigateway/APIGatewayV2HTTPEvent.java | 2 +- .../runtime/events/apigateway/HttpRequestEvent.java | 8 ++++++++ .../runtime/events/apigateway/LambdaRequestEvent.java | 10 ---------- 5 files changed, 12 insertions(+), 14 deletions(-) create mode 100644 aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/HttpRequestEvent.java delete mode 100644 aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/LambdaRequestEvent.java diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java index d79ca95a..4f2308c5 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java @@ -13,7 +13,7 @@ package com.amazonaws.services.lambda.runtime.events; -import com.amazonaws.services.lambda.runtime.events.apigateway.LambdaRequestEvent; +import com.amazonaws.services.lambda.runtime.events.apigateway.HttpRequestEvent; import com.amazonaws.services.lambda.runtime.events.apigateway.RequestSource; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; @@ -37,7 +37,7 @@ @Builder(setterPrefix = "with") @NoArgsConstructor @AllArgsConstructor -public class ApplicationLoadBalancerRequestEvent implements Serializable, LambdaRequestEvent { +public class ApplicationLoadBalancerRequestEvent implements Serializable, HttpRequestEvent { @Override @JsonIgnore diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java index 08c2523f..6c2761cc 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayProxyRequestEvent.java @@ -31,7 +31,7 @@ @Builder(setterPrefix = "with") @NoArgsConstructor @AllArgsConstructor -public class APIGatewayProxyRequestEvent implements Serializable, LambdaRequestEvent { +public class APIGatewayProxyRequestEvent implements Serializable, HttpRequestEvent { private static final long serialVersionUID = 4189228800688527467L; diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java index f03987df..4ba5e5a8 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/APIGatewayV2HTTPEvent.java @@ -31,7 +31,7 @@ @Builder(setterPrefix = "with") @Data @NoArgsConstructor -public class APIGatewayV2HTTPEvent implements LambdaRequestEvent { +public class APIGatewayV2HTTPEvent implements HttpRequestEvent { private String version; private String routeKey; diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/HttpRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/HttpRequestEvent.java new file mode 100644 index 00000000..280ebd58 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/HttpRequestEvent.java @@ -0,0 +1,8 @@ +package com.amazonaws.services.lambda.runtime.events.apigateway; + +/** + * A common interface shared by event sources which produce HTTP as JSON + */ +public interface HttpRequestEvent { + RequestSource getRequestSource(); +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/LambdaRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/LambdaRequestEvent.java deleted file mode 100644 index 4c2834c7..00000000 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/apigateway/LambdaRequestEvent.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.amazonaws.services.lambda.runtime.events.apigateway; - -import com.amazonaws.services.lambda.runtime.events.apigateway.RequestSource; - -/** - * Interface to abstract APIGWY and ALB request classes, reduce complexity and increase efficiency. - */ -public interface LambdaRequestEvent { - RequestSource getRequestSource(); -}