Skip to content

Commit

Permalink
Add request parsing into api gateway
Browse files Browse the repository at this point in the history
  • Loading branch information
gcbeattyAWS committed Dec 10, 2024
1 parent 1af7cf4 commit 77f7679
Show file tree
Hide file tree
Showing 13 changed files with 676 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
<PackageReference Include="Blazored.Modal" Version="7.3.1" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.11" />
<PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
using Amazon.Lambda.TestTool.Models;

namespace Amazon.Lambda.TestTool.Extensions;

using Amazon.Lambda.TestTool.Models;

namespace Amazon.Lambda.TestTool.Extensions;

/// <summary>
/// A class that contains extension methods for the <see cref="Exception"/> class.
/// </summary>
public static class ExceptionExtensions
{
/// <summary>
/// True if the <paramref name="e"/> inherits from
/// <see cref="TestToolException"/>.
/// </summary>
public static bool IsExpectedException(this Exception e) =>
e is TestToolException;

/// </summary>
public static class ExceptionExtensions
{
/// <summary>
/// True if the <paramref name="e"/> inherits from
/// <see cref="TestToolException"/>.
/// </summary>
public static bool IsExpectedException(this Exception e) =>
e is TestToolException;

/// <summary>
/// Prints an exception in a user-friendly way.
/// </summary>
public static string PrettyPrint(this Exception? e)
{
if (null == e)
return string.Empty;

return $"{Environment.NewLine}{e.Message}" +
$"{Environment.NewLine}{e.StackTrace}" +
$"{PrettyPrint(e.InnerException)}";
}
/// </summary>
public static string PrettyPrint(this Exception? e)
{
if (null == e)
return string.Empty;

return $"{Environment.NewLine}{e.Message}" +
$"{Environment.NewLine}{e.StackTrace}" +
$"{PrettyPrint(e.InnerException)}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
namespace Amazon.Lambda.TestTool.Extensions;

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.TestTool.Models;
using Amazon.Lambda.TestTool.Services;
using Amazon.Lambda.TestTool.Utilities;
using System.Text;
using System.Web;
using static Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest;

/// <summary>
/// Provides extension methods to translate an <see cref="HttpContext"/> to different types of API Gateway requests.
/// </summary>
public static class HttpContextExtensions
{

/// <summary>
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayHttpApiV2ProxyRequest"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
/// <returns>An <see cref="APIGatewayHttpApiV2ProxyRequest"/> object representing the translated request.</returns>
public static APIGatewayHttpApiV2ProxyRequest ToApiGatewayHttpV2Request(
this HttpContext context,
ApiGatewayRouteConfig apiGatewayRouteConfig)
{
var request = context.Request;

var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);

var (headers, _) = HttpRequestUtility.ExtractHeaders(request.Headers);
var (queryStringParameters, _) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);

var httpApiV2ProxyRequest = new APIGatewayHttpApiV2ProxyRequest
{
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}",
RawPath = request.Path,
RawQueryString = request.QueryString.Value,
Cookies = request.Cookies.Select(c => $"{c.Key}={c.Value}").ToArray(),
Headers = headers,
QueryStringParameters = queryStringParameters,
PathParameters = pathParameters ?? new Dictionary<string, string>(),
Body = HttpRequestUtility.ReadRequestBody(request),
IsBase64Encoded = false,
RequestContext = new ProxyRequestContext
{
Http = new HttpDescription
{
Method = request.Method,
Path = request.Path,
Protocol = request.Protocol
},
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}"
},
Version = "2.0"
};

if (HttpRequestUtility.IsBinaryContent(request.ContentType))
{
httpApiV2ProxyRequest.Body = Convert.ToBase64String(Encoding.UTF8.GetBytes(httpApiV2ProxyRequest.Body));
httpApiV2ProxyRequest.IsBase64Encoded = true;
}

return httpApiV2ProxyRequest;
}

/// <summary>
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayProxyRequest"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
/// <returns>An <see cref="APIGatewayProxyRequest"/> object representing the translated request.</returns>
public static APIGatewayProxyRequest ToApiGatewayRequest(
this HttpContext context,
ApiGatewayRouteConfig apiGatewayRouteConfig)
{
var request = context.Request;

var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);

var (headers, multiValueHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers);
var (queryStringParameters, multiValueQueryStringParameters) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);

var proxyRequest = new APIGatewayProxyRequest
{
Resource = apiGatewayRouteConfig.Path,
Path = HttpUtility.UrlEncode(request.Path),
HttpMethod = request.Method,
Headers = headers,
MultiValueHeaders = multiValueHeaders,
QueryStringParameters = queryStringParameters,
MultiValueQueryStringParameters = multiValueQueryStringParameters,
PathParameters = pathParameters ?? new Dictionary<string, string>(),
Body = HttpRequestUtility.ReadRequestBody(request),
IsBase64Encoded = false
};

if (HttpRequestUtility.IsBinaryContent(request.ContentType))
{
proxyRequest.Body = Convert.ToBase64String(Encoding.UTF8.GetBytes(proxyRequest.Body));
proxyRequest.IsBase64Encoded = true;
}

return proxyRequest;
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
using Amazon.Lambda.TestTool.Services;
using Amazon.Lambda.TestTool.Services.IO;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Amazon.Lambda.TestTool.Extensions;

/// <summary>
/// A class that contains extension methods for the <see cref="IServiceCollection"/> interface.
/// </summary>
public static class ServiceCollectionExtensions
{
using Amazon.Lambda.TestTool.Services;
using Amazon.Lambda.TestTool.Services.IO;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Amazon.Lambda.TestTool.Extensions;

/// <summary>
/// A class that contains extension methods for the <see cref="IServiceCollection"/> interface.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds a set of services for the .NET CLI portion of this application.
/// </summary>
public static void AddCustomServices(this IServiceCollection serviceCollection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IToolInteractiveService), typeof(ConsoleInteractiveService), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDirectoryManager), typeof(DirectoryManager), lifetime));
/// </summary>
public static void AddCustomServices(this IServiceCollection serviceCollection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IToolInteractiveService), typeof(ConsoleInteractiveService), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDirectoryManager), typeof(DirectoryManager), lifetime));
}

/// <summary>
/// Adds a set of services for the API Gateway emulator portion of this application.
/// </summary>
public static void AddApiGatewayEmulatorServices(this IServiceCollection serviceCollection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IApiGatewayRouteConfigService), typeof(ApiGatewayRouteConfigService), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentManager), typeof(EnvironmentManager), lifetime));
}
/// </summary>
public static void AddApiGatewayEmulatorServices(this IServiceCollection serviceCollection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IApiGatewayRouteConfigService), typeof(ApiGatewayRouteConfigService), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentManager), typeof(EnvironmentManager), lifetime));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ public class ApiGatewayRouteConfig
/// The name of the Lambda function
/// </summary>
public required string LambdaResourceName { get; set; }

/// <summary>
/// The endpoint of the local Lambda Runtime API
/// </summary>
public string? Endpoint { get; set; }

/// <summary>
/// The HTTP Method for the API Gateway endpoint
/// </summary>
public required string HttpMethod { get; set; }

/// <summary>
/// The API Gateway HTTP Path of the Lambda function
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Text.Json;
using Amazon.Lambda.TestTool.Models;
using Amazon.Lambda.TestTool.Services.IO;
using Amazon.Lambda.TestTool.Utilities;
using Microsoft.AspNetCore.Routing.Template;

namespace Amazon.Lambda.TestTool.Services;
Expand Down Expand Up @@ -66,7 +67,7 @@ public ApiGatewayRouteConfigService(
{
var template = TemplateParser.Parse(routeConfig.Path);

var matcher = new TemplateMatcher(template, GetDefaults(template));
var matcher = new TemplateMatcher(template, RouteTemplateUtility.GetDefaults(template));

var routeValueDictionary = new RouteValueDictionary();
if (!matcher.TryMatch(path, routeValueDictionary))
Expand All @@ -80,19 +81,4 @@ public ApiGatewayRouteConfigService(

return null;
}

private RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate)
{
var result = new RouteValueDictionary();

foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.DefaultValue != null)
{
if (parameter.Name != null) result.Add(parameter.Name, parameter.DefaultValue);
}
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
namespace Amazon.Lambda.TestTool
{
/// <summary>
/// Utility class for handling HTTP requests in the context of API Gateway emulation.
/// </summary>
public static class HttpRequestUtility
{
/// <summary>
/// Determines whether the specified content type represents binary content.
/// </summary>
/// <param name="contentType">The content type to check.</param>
/// <returns>True if the content type represents binary content; otherwise, false.</returns>
public static bool IsBinaryContent(string? contentType)
{
if (string.IsNullOrEmpty(contentType))
return false;

return contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) ||
contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) ||
contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase) ||
contentType.Equals("application/octet-stream", StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Reads the body of the HTTP request as a string.
/// </summary>
/// <param name="request">The HTTP request.</param>
/// <returns>The body of the request as a string.</returns>
public static string ReadRequestBody(HttpRequest request)
{
using (var reader = new StreamReader(request.Body))
{
return reader.ReadToEnd();
}
}

/// <summary>
/// Extracts headers from the request, separating them into single-value and multi-value dictionaries.
/// </summary>
/// <param name="headers">The request headers.</param>
/// <returns>A tuple containing single-value and multi-value header dictionaries.</returns>
/// <example>
/// For headers:
/// Accept: text/html
/// Accept: application/xhtml+xml
/// X-Custom-Header: value1
///
/// The method will return:
/// singleValueHeaders: { "Accept": "application/xhtml+xml", "X-Custom-Header": "value1" }
/// multiValueHeaders: { "Accept": ["text/html", "application/xhtml+xml"], "X-Custom-Header": ["value1"] }
/// </example>
public static (IDictionary<string, string>, IDictionary<string, IList<string>>) ExtractHeaders(IHeaderDictionary headers)
{
var singleValueHeaders = new Dictionary<string, string>();
var multiValueHeaders = new Dictionary<string, IList<string>>();

foreach (var header in headers)
{
singleValueHeaders[header.Key] = header.Value.Last() ?? "";
multiValueHeaders[header.Key] = [.. header.Value];
}

return (singleValueHeaders, multiValueHeaders);
}

/// <summary>
/// Extracts query string parameters from the request, separating them into single-value and multi-value dictionaries.
/// </summary>
/// <param name="query">The query string collection.</param>
/// <returns>A tuple containing single-value and multi-value query parameter dictionaries.</returns>
/// <example>
/// For query string: ?param1=value1&amp;param2=value2&amp;param2=value3
///
/// The method will return:
/// singleValueParams: { "param1": "value1", "param2": "value3" }
/// multiValueParams: { "param1": ["value1"], "param2": ["value2", "value3"] }
/// </example>
public static (IDictionary<string, string>, IDictionary<string, IList<string>>) ExtractQueryStringParameters(IQueryCollection query)
{
var singleValueParams = new Dictionary<string, string>();
var multiValueParams = new Dictionary<string, IList<string>>();

foreach (var param in query)
{
singleValueParams[param.Key] = param.Value.Last() ?? "";
multiValueParams[param.Key] = [.. param.Value];
}

return (singleValueParams, multiValueParams);
}
}
}
Loading

0 comments on commit 77f7679

Please sign in to comment.