Skip to content

Commit 68ff1db

Browse files
Add ApiGatewayHttpApiV2ProxyRequestTranslator and ApiGatewayProxyRequestTranslator (#1901)
1 parent edd2698 commit 68ff1db

14 files changed

+2207
-189
lines changed

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ApiGatewayResponseExtensions.cs

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Amazon.Lambda.APIGatewayEvents;
55
using Amazon.Lambda.TestTool.Models;
6+
using Amazon.Lambda.TestTool.Utilities;
67
using Microsoft.Extensions.Primitives;
78
using System.Text;
89

@@ -102,49 +103,18 @@ private static Dictionary<string, string> GetDefaultApiGatewayHeaders(ApiGateway
102103
{
103104
case ApiGatewayEmulatorMode.Rest:
104105
headers.Add("x-amzn-RequestId", Guid.NewGuid().ToString("D"));
105-
headers.Add("x-amz-apigw-id", GenerateRequestId());
106-
headers.Add("X-Amzn-Trace-Id", GenerateTraceId());
106+
headers.Add("x-amz-apigw-id", HttpRequestUtility.GenerateRequestId());
107+
headers.Add("X-Amzn-Trace-Id", HttpRequestUtility.GenerateTraceId());
107108
break;
108109
case ApiGatewayEmulatorMode.HttpV1:
109110
case ApiGatewayEmulatorMode.HttpV2:
110-
headers.Add("Apigw-Requestid", GenerateRequestId());
111+
headers.Add("Apigw-Requestid", HttpRequestUtility.GenerateRequestId());
111112
break;
112113
}
113114

114115
return headers;
115116
}
116117

117-
/// <summary>
118-
/// Generates a random X-Amzn-Trace-Id for REST API mode.
119-
/// </summary>
120-
/// <returns>A string representing a random X-Amzn-Trace-Id in the format used by API Gateway for REST APIs.</returns>
121-
/// <remarks>
122-
/// The generated trace ID includes:
123-
/// - A root ID with a timestamp and two partial GUIDs
124-
/// - A parent ID
125-
/// - A sampling decision (always set to 0 in this implementation)
126-
/// - A lineage identifier
127-
/// </remarks>
128-
private static string GenerateTraceId()
129-
{
130-
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString("x");
131-
var guid1 = Guid.NewGuid().ToString("N");
132-
var guid2 = Guid.NewGuid().ToString("N");
133-
return $"Root=1-{timestamp}-{guid1.Substring(0, 12)}{guid2.Substring(0, 12)};Parent={Guid.NewGuid().ToString("N").Substring(0, 16)};Sampled=0;Lineage=1:{Guid.NewGuid().ToString("N").Substring(0, 8)}:0";
134-
}
135-
136-
/// <summary>
137-
/// Generates a random API Gateway request ID for HTTP API v1 and v2.
138-
/// </summary>
139-
/// <returns>A string representing a random request ID in the format used by API Gateway for HTTP APIs.</returns>
140-
/// <remarks>
141-
/// The generated ID is a 15-character string consisting of lowercase letters and numbers, followed by an equals sign.
142-
/// </remarks>
143-
private static string GenerateRequestId()
144-
{
145-
return Guid.NewGuid().ToString("N").Substring(0, 8) + Guid.NewGuid().ToString("N").Substring(0, 7) + "=";
146-
}
147-
148118
/// <summary>
149119
/// Sets the response body on the <see cref="HttpResponse"/>.
150120
/// </summary>
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
namespace Amazon.Lambda.TestTool.Extensions;
5+
6+
using System.Text;
7+
using System.Web;
8+
using Amazon.Lambda.APIGatewayEvents;
9+
using Amazon.Lambda.TestTool.Models;
10+
using Amazon.Lambda.TestTool.Utilities;
11+
using static Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest;
12+
13+
/// <summary>
14+
/// Provides extension methods to translate an <see cref="HttpContext"/> to different types of API Gateway requests.
15+
/// </summary>
16+
public static class HttpContextExtensions
17+
{
18+
/// <summary>
19+
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayHttpApiV2ProxyRequest"/>.
20+
/// </summary>
21+
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
22+
/// <param name="apiGatewayRouteConfig">The configuration of the API Gateway route, including the HTTP method, path, and other metadata.</param>
23+
/// <returns>An <see cref="APIGatewayHttpApiV2ProxyRequest"/> object representing the translated request.</returns>
24+
public static async Task<APIGatewayHttpApiV2ProxyRequest> ToApiGatewayHttpV2Request(
25+
this HttpContext context,
26+
ApiGatewayRouteConfig apiGatewayRouteConfig)
27+
{
28+
var request = context.Request;
29+
var currentTime = DateTimeOffset.UtcNow;
30+
var body = await HttpRequestUtility.ReadRequestBody(request);
31+
var contentLength = HttpRequestUtility.CalculateContentLength(request, body);
32+
33+
var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);
34+
35+
// Format 2.0 doesn't have multiValueHeaders or multiValueQueryStringParameters fields. Duplicate headers are combined with commas and included in the headers field.
36+
// 2.0 also lowercases all header keys
37+
var (_, allHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers, true);
38+
var headers = allHeaders.ToDictionary(
39+
kvp => kvp.Key,
40+
kvp => string.Join(", ", kvp.Value)
41+
);
42+
43+
// Duplicate query strings are combined with commas and included in the queryStringParameters field.
44+
var (_, allQueryParams) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);
45+
var queryStringParameters = allQueryParams.ToDictionary(
46+
kvp => kvp.Key,
47+
kvp => string.Join(",", kvp.Value)
48+
);
49+
50+
string userAgent = request.Headers.UserAgent.ToString();
51+
52+
if (!headers.ContainsKey("content-length"))
53+
{
54+
headers["content-length"] = contentLength.ToString();
55+
}
56+
57+
if (!headers.ContainsKey("content-type"))
58+
{
59+
headers["content-type"] = "text/plain; charset=utf-8";
60+
}
61+
62+
var httpApiV2ProxyRequest = new APIGatewayHttpApiV2ProxyRequest
63+
{
64+
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}",
65+
RawPath = request.Path.Value, // this should be decoded value
66+
Body = body,
67+
IsBase64Encoded = false,
68+
RequestContext = new ProxyRequestContext
69+
{
70+
Http = new HttpDescription
71+
{
72+
Method = request.Method,
73+
Path = request.Path.Value, // this should be decoded value
74+
Protocol = !string.IsNullOrEmpty(request.Protocol) ? request.Protocol : "HTTP/1.1", // defaults to http 1.1 if not provided
75+
UserAgent = userAgent
76+
},
77+
Time = currentTime.ToString("dd/MMM/yyyy:HH:mm:ss") + " +0000",
78+
TimeEpoch = currentTime.ToUnixTimeMilliseconds(),
79+
RequestId = HttpRequestUtility.GenerateRequestId(),
80+
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}",
81+
},
82+
Version = "2.0"
83+
};
84+
85+
if (request.Cookies.Any())
86+
{
87+
httpApiV2ProxyRequest.Cookies = request.Cookies.Select(c => $"{c.Key}={c.Value}").ToArray();
88+
}
89+
90+
if (headers.Any())
91+
{
92+
httpApiV2ProxyRequest.Headers = headers;
93+
}
94+
95+
httpApiV2ProxyRequest.RawQueryString = string.Empty; // default is empty string
96+
97+
if (queryStringParameters.Any())
98+
{
99+
// this should be decoded value
100+
httpApiV2ProxyRequest.QueryStringParameters = queryStringParameters;
101+
102+
// this should be the url encoded value and not include the "?"
103+
// e.g. key=%2b%2b%2b
104+
httpApiV2ProxyRequest.RawQueryString = HttpUtility.UrlPathEncode(request.QueryString.Value?.Substring(1));
105+
106+
}
107+
108+
if (pathParameters.Any())
109+
{
110+
// this should be decoded value
111+
httpApiV2ProxyRequest.PathParameters = pathParameters;
112+
}
113+
114+
if (HttpRequestUtility.IsBinaryContent(request.ContentType))
115+
{
116+
// we already converted it when we read the body so we dont need to re-convert it
117+
httpApiV2ProxyRequest.IsBase64Encoded = true;
118+
}
119+
120+
return httpApiV2ProxyRequest;
121+
}
122+
123+
/// <summary>
124+
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayProxyRequest"/>.
125+
/// </summary>
126+
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
127+
/// <param name="apiGatewayRouteConfig">The configuration of the API Gateway route, including the HTTP method, path, and other metadata.</param>
128+
/// <returns>An <see cref="APIGatewayProxyRequest"/> object representing the translated request.</returns>
129+
public static async Task<APIGatewayProxyRequest> ToApiGatewayRequest(
130+
this HttpContext context,
131+
ApiGatewayRouteConfig apiGatewayRouteConfig,
132+
ApiGatewayEmulatorMode emulatorMode)
133+
{
134+
var request = context.Request;
135+
var body = await HttpRequestUtility.ReadRequestBody(request);
136+
var contentLength = HttpRequestUtility.CalculateContentLength(request, body);
137+
138+
var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);
139+
140+
var (headers, multiValueHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers);
141+
var (queryStringParameters, multiValueQueryStringParameters) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);
142+
143+
if (!headers.ContainsKey("content-length") && emulatorMode != ApiGatewayEmulatorMode.Rest) // rest doesnt set content-length by default
144+
{
145+
headers["content-length"] = contentLength.ToString();
146+
multiValueHeaders["content-length"] = [contentLength.ToString()];
147+
}
148+
149+
if (!headers.ContainsKey("content-type"))
150+
{
151+
headers["content-type"] = "text/plain; charset=utf-8";
152+
multiValueHeaders["content-type"] = ["text/plain; charset=utf-8"];
153+
}
154+
155+
// This is the decoded value
156+
var path = request.Path.Value;
157+
158+
if (emulatorMode == ApiGatewayEmulatorMode.HttpV1 || emulatorMode == ApiGatewayEmulatorMode.Rest) // rest and httpv1 uses the encoded value for path an
159+
{
160+
path = request.Path.ToUriComponent();
161+
}
162+
163+
if (emulatorMode == ApiGatewayEmulatorMode.Rest) // rest uses encoded value for the path params
164+
{
165+
var encodedPathParameters = pathParameters.ToDictionary(
166+
kvp => kvp.Key,
167+
kvp => Uri.EscapeUriString(kvp.Value)); // intentionally using EscapeURiString over EscapeDataString since EscapeURiString correctly handles reserved characters :/?#[]@!$&'()*+,;= in this case
168+
pathParameters = encodedPathParameters;
169+
}
170+
171+
var proxyRequest = new APIGatewayProxyRequest
172+
{
173+
Resource = apiGatewayRouteConfig.Path,
174+
Path = path,
175+
HttpMethod = request.Method,
176+
Body = body,
177+
IsBase64Encoded = false
178+
};
179+
180+
if (headers.Any())
181+
{
182+
proxyRequest.Headers = headers;
183+
}
184+
185+
if (multiValueHeaders.Any())
186+
{
187+
proxyRequest.MultiValueHeaders = multiValueHeaders;
188+
}
189+
190+
if (queryStringParameters.Any())
191+
{
192+
// this should be decoded value
193+
proxyRequest.QueryStringParameters = queryStringParameters;
194+
}
195+
196+
if (multiValueQueryStringParameters.Any())
197+
{
198+
// this should be decoded value
199+
proxyRequest.MultiValueQueryStringParameters = multiValueQueryStringParameters;
200+
}
201+
202+
if (pathParameters.Any())
203+
{
204+
proxyRequest.PathParameters = pathParameters;
205+
}
206+
207+
if (HttpRequestUtility.IsBinaryContent(request.ContentType))
208+
{
209+
proxyRequest.IsBase64Encoded = true;
210+
}
211+
212+
return proxyRequest;
213+
}
214+
}

0 commit comments

Comments
 (0)