Skip to content

Commit 9d38917

Browse files
committed
Add request parsing into api gateway
1 parent edd2698 commit 9d38917

14 files changed

+2043
-184
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: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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.Web;
7+
using Amazon.Lambda.APIGatewayEvents;
8+
using Amazon.Lambda.TestTool.Models;
9+
using Amazon.Lambda.TestTool.Utilities;
10+
using static Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest;
11+
12+
/// <summary>
13+
/// Provides extension methods to translate an <see cref="HttpContext"/> to different types of API Gateway requests.
14+
/// </summary>
15+
public static class HttpContextExtensions
16+
{
17+
/// <summary>
18+
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayHttpApiV2ProxyRequest"/>.
19+
/// </summary>
20+
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
21+
/// <param name="apiGatewayRouteConfig">The configuration of the API Gateway route, including the HTTP method, path, and other metadata.</param>
22+
/// <returns>An <see cref="APIGatewayHttpApiV2ProxyRequest"/> object representing the translated request.</returns>
23+
public static APIGatewayHttpApiV2ProxyRequest ToApiGatewayHttpV2Request(
24+
this HttpContext context,
25+
ApiGatewayRouteConfig apiGatewayRouteConfig)
26+
{
27+
var request = context.Request;
28+
var currentTime = DateTimeOffset.UtcNow;
29+
var body = HttpRequestUtility.ReadRequestBody(request);
30+
var contentLength = HttpRequestUtility.CalculateContentLength(request, body);
31+
32+
var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);
33+
34+
// Format 2.0 doesn't have multiValueHeaders or multiValueQueryStringParameters fields. Duplicate headers are combined with commas and included in the headers field.
35+
var (_, allHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers);
36+
var headers = allHeaders.ToDictionary(
37+
kvp => kvp.Key,
38+
kvp => string.Join(", ", kvp.Value)
39+
);
40+
41+
// Duplicate query strings are combined with commas and included in the queryStringParameters field.
42+
var (_, allQueryParams) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);
43+
var queryStringParameters = allQueryParams.ToDictionary(
44+
kvp => kvp.Key,
45+
kvp => string.Join(",", kvp.Value)
46+
);
47+
48+
string userAgent = request.Headers.UserAgent.ToString();
49+
50+
if (!headers.ContainsKey("content-length"))
51+
{
52+
headers["content-length"] = contentLength.ToString();
53+
}
54+
55+
if (!headers.ContainsKey("content-type"))
56+
{
57+
headers["content-type"] = "text/plain; charset=utf-8";
58+
}
59+
60+
var httpApiV2ProxyRequest = new APIGatewayHttpApiV2ProxyRequest
61+
{
62+
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}",
63+
RawPath = request.Path.Value, // this should be decoded value
64+
Body = body,
65+
IsBase64Encoded = false,
66+
RequestContext = new ProxyRequestContext
67+
{
68+
Http = new HttpDescription
69+
{
70+
Method = request.Method,
71+
Path = request.Path.Value, // this should be decoded value
72+
Protocol = !string.IsNullOrEmpty(request.Protocol) ? request.Protocol : "HTTP/1.1", // defaults to http 1.1 if not provided
73+
UserAgent = userAgent
74+
},
75+
Time = currentTime.ToString("dd/MMM/yyyy:HH:mm:ss") + " +0000",
76+
TimeEpoch = currentTime.ToUnixTimeMilliseconds(),
77+
RequestId = HttpRequestUtility.GenerateRequestId(),
78+
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}",
79+
},
80+
Version = "2.0"
81+
};
82+
83+
if (request.Cookies.Any())
84+
{
85+
httpApiV2ProxyRequest.Cookies = request.Cookies.Select(c => $"{c.Key}={c.Value}").ToArray();
86+
}
87+
88+
if (headers.Any())
89+
{
90+
httpApiV2ProxyRequest.Headers = headers;
91+
}
92+
93+
httpApiV2ProxyRequest.RawQueryString = string.Empty; // default is empty string
94+
if (queryStringParameters.Any())
95+
{
96+
// this should be decoded value
97+
httpApiV2ProxyRequest.QueryStringParameters = queryStringParameters;
98+
99+
// this should be the url encoded value and not include the "?"
100+
// e.g. key=%2b%2b%2b
101+
httpApiV2ProxyRequest.RawQueryString = HttpUtility.UrlPathEncode(request.QueryString.Value?.Substring(1));
102+
103+
}
104+
105+
if (pathParameters.Any())
106+
{
107+
// this should be decoded value
108+
httpApiV2ProxyRequest.PathParameters = pathParameters;
109+
}
110+
111+
if (HttpRequestUtility.IsBinaryContent(request.ContentType))
112+
{
113+
// we already converted it when we read the body so we dont need to re-convert it
114+
httpApiV2ProxyRequest.IsBase64Encoded = true;
115+
}
116+
117+
return httpApiV2ProxyRequest;
118+
}
119+
120+
/// <summary>
121+
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayProxyRequest"/>.
122+
/// </summary>
123+
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
124+
/// <param name="apiGatewayRouteConfig">The configuration of the API Gateway route, including the HTTP method, path, and other metadata.</param>
125+
/// <returns>An <see cref="APIGatewayProxyRequest"/> object representing the translated request.</returns>
126+
public static APIGatewayProxyRequest ToApiGatewayRequest(
127+
this HttpContext context,
128+
ApiGatewayRouteConfig apiGatewayRouteConfig)
129+
{
130+
var request = context.Request;
131+
var body = HttpRequestUtility.ReadRequestBody(request);
132+
var contentLength = HttpRequestUtility.CalculateContentLength(request, body);
133+
134+
var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);
135+
136+
var (headers, multiValueHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers);
137+
var (queryStringParameters, multiValueQueryStringParameters) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);
138+
139+
if (!headers.ContainsKey("content-length"))
140+
{
141+
headers["content-length"] = contentLength.ToString();
142+
multiValueHeaders["content-length"] = new List<string> { contentLength.ToString() };
143+
}
144+
145+
if (!headers.ContainsKey("content-type"))
146+
{
147+
headers["content-type"] = "text/plain; charset=utf-8";
148+
multiValueHeaders["content-type"] = new List<string> { "text/plain; charset=utf-8" };
149+
}
150+
151+
var proxyRequest = new APIGatewayProxyRequest
152+
{
153+
Resource = apiGatewayRouteConfig.Path,
154+
Path = request.Path.Value,
155+
HttpMethod = request.Method,
156+
Body = body,
157+
IsBase64Encoded = false
158+
};
159+
160+
if (headers.Any())
161+
{
162+
proxyRequest.Headers = headers;
163+
}
164+
165+
if (multiValueHeaders.Any())
166+
{
167+
proxyRequest.MultiValueHeaders = multiValueHeaders;
168+
}
169+
170+
if (queryStringParameters.Any())
171+
{
172+
// this should be decoded value
173+
proxyRequest.QueryStringParameters = queryStringParameters;
174+
}
175+
176+
if (multiValueQueryStringParameters.Any())
177+
{
178+
// this should be decoded value
179+
proxyRequest.MultiValueQueryStringParameters = multiValueQueryStringParameters;
180+
}
181+
182+
if (pathParameters.Any())
183+
{
184+
// this should be decoded value
185+
proxyRequest.PathParameters = pathParameters;
186+
}
187+
188+
if (HttpRequestUtility.IsBinaryContent(request.ContentType))
189+
{
190+
// we already converted it when we read the body so we dont need to re-convert it
191+
proxyRequest.IsBase64Encoded = true;
192+
}
193+
194+
return proxyRequest;
195+
}
196+
}

0 commit comments

Comments
 (0)