Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support API Gateway websocket api via LambdaServer #1804

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,45 @@ public ApplicationLoadBalancerMinimalApi(IServiceProvider serviceProvider)
}
}
}

/// <summary>
/// IServer for handlying Lambda events from an API Gateway Websocket API.
/// </summary>
public class APIGatewayWebsocketApiLambdaRuntimeSupportServer : LambdaRuntimeSupportServer
{
/// <summary>
/// Create instances
/// </summary>
/// <param name="serviceProvider">The IServiceProvider created for the ASP.NET Core application</param>
public APIGatewayWebsocketApiLambdaRuntimeSupportServer(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}

/// <summary>
/// Creates HandlerWrapper for processing events from API Gateway Websocket API
/// </summary>
/// <param name="serviceProvider"></param>
/// <returns></returns>
protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceProvider)
{
var handler = new APIGatewayWebsocketApiMinimalApi(serviceProvider).FunctionHandlerAsync;
return HandlerWrapper.GetHandlerWrapper(handler, this.Serializer);
}

/// <summary>
/// Create the APIGatewayWebsocketApiV2ProxyFunction passing in the ASP.NET Core application's IServiceProvider
/// </summary>
public class APIGatewayWebsocketApiMinimalApi : APIGatewayWebsocketApiProxyFunction
{
/// <summary>
/// Create instances
/// </summary>
/// <param name="serviceProvider">The IServiceProvider created for the ASP.NET Core application</param>
public APIGatewayWebsocketApiMinimalApi(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ public enum LambdaEventSource
/// <summary>
/// ELB Application Load Balancer
/// </summary>
ApplicationLoadBalancer
ApplicationLoadBalancer,

/// <summary>
/// API Gateway HTTP API
/// </summary>
WebsocketApi
}

/// <summary>
Expand Down Expand Up @@ -106,6 +111,7 @@ private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSourc
LambdaEventSource.HttpApi => typeof(APIGatewayHttpApiV2LambdaRuntimeSupportServer),
LambdaEventSource.RestApi => typeof(APIGatewayRestApiLambdaRuntimeSupportServer),
LambdaEventSource.ApplicationLoadBalancer => typeof(ApplicationLoadBalancerLambdaRuntimeSupportServer),
LambdaEventSource.WebsocketApi => typeof(APIGatewayWebsocketApiLambdaRuntimeSupportServer),
_ => throw new ArgumentException($"Event source type {eventSource} unknown")
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Security.Claims;
using System.Text;

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
Expand Down Expand Up @@ -151,33 +152,9 @@ protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxy
{
var requestFeatures = (IHttpRequestFeature)features;
requestFeatures.Scheme = "https";
requestFeatures.Method = apiGatewayRequest.HttpMethod;
requestFeatures.Method = this.ParseHttpMethod(apiGatewayRequest);

string path = null;

// Replaces {proxy+} in path, if exists
if (apiGatewayRequest.PathParameters != null && apiGatewayRequest.PathParameters.TryGetValue("proxy", out var proxy) &&
!string.IsNullOrEmpty(apiGatewayRequest.Resource))
{
var proxyPath = proxy;
path = apiGatewayRequest.Resource.Replace("{proxy+}", proxyPath);

// Adds all the rest of non greedy parameters in apiGateway.Resource to the path
foreach (var pathParameter in apiGatewayRequest.PathParameters.Where(pp => pp.Key != "proxy"))
{
path = path.Replace($"{{{pathParameter.Key}}}", pathParameter.Value);
}
}

if (string.IsNullOrEmpty(path))
{
path = apiGatewayRequest.Path;
}

if (!path.StartsWith("/"))
{
path = "/" + path;
}
string path = this.ParseHttpPath(apiGatewayRequest);

var rawQueryString = Utilities.CreateQueryStringParameters(
apiGatewayRequest.QueryStringParameters, apiGatewayRequest.MultiValueQueryStringParameters, true);
Expand Down Expand Up @@ -214,13 +191,7 @@ protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxy

Utilities.SetHeadersCollection(requestFeatures.Headers, apiGatewayRequest.Headers, apiGatewayRequest.MultiValueHeaders);

if (!requestFeatures.Headers.ContainsKey("Host"))
{
var apiId = apiGatewayRequest?.RequestContext?.ApiId ?? "";
var stage = apiGatewayRequest?.RequestContext?.Stage ?? "";

requestFeatures.Headers["Host"] = $"apigateway-{apiId}-{stage}";
}
requestFeatures.Headers = this.AddMissingRequestHeaders(apiGatewayRequest, requestFeatures.Headers);


if (!string.IsNullOrEmpty(apiGatewayRequest.Body))
Expand Down Expand Up @@ -256,6 +227,7 @@ protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxy
{
connectionFeatures.RemotePort = int.Parse(forwardedPort, CultureInfo.InvariantCulture);
}
connectionFeatures.ConnectionId = apiGatewayRequest.RequestContext?.ConnectionId;

// Call consumers customize method in case they want to change how API Gateway's request
// was marshalled into ASP.NET Core request.
Expand Down Expand Up @@ -335,5 +307,66 @@ protected override APIGatewayProxyResponse MarshallResponse(IHttpResponseFeature

return response;
}

/// <summary>
/// Get the http path from the request.
/// </summary>
/// <param name="apiGatewayRequest"></param>
/// <returns>string</returns>
protected virtual string ParseHttpPath(APIGatewayProxyRequest apiGatewayRequest)
{
string path = null;

// Replaces {proxy+} in path, if exists
if (apiGatewayRequest.PathParameters != null && apiGatewayRequest.PathParameters.TryGetValue("proxy", out var proxy) &&
!string.IsNullOrEmpty(apiGatewayRequest.Resource))
{
var proxyPath = proxy;
path = apiGatewayRequest.Resource.Replace("{proxy+}", proxyPath);

// Adds all the rest of non greedy parameters in apiGateway.Resource to the path
foreach (var pathParameter in apiGatewayRequest.PathParameters.Where(pp => pp.Key != "proxy"))
{
path = path.Replace($"{{{pathParameter.Key}}}", pathParameter.Value);
}
}

if (string.IsNullOrEmpty(path))
{
path = apiGatewayRequest.Path;
}

if (!path.StartsWith("/"))
{
path = "/" + path;
}
return path;
}

/// <summary>
/// Get the http method from the request.
/// </summary>
/// <param name="apiGatewayRequest"></param>
/// <returns>string</returns>
protected virtual string ParseHttpMethod(APIGatewayProxyRequest apiGatewayRequest)
{
return apiGatewayRequest.HttpMethod;
}

/// <summary>
/// Add missing headers to request.
/// </summary>
/// <returns>IHeaderDictionary</returns>
protected virtual IHeaderDictionary AddMissingRequestHeaders(APIGatewayProxyRequest apiGatewayRequest, IHeaderDictionary headers)
{
if (!headers.ContainsKey("Host"))
{
var apiId = apiGatewayRequest?.RequestContext?.ApiId ?? "";
var stage = apiGatewayRequest?.RequestContext?.Stage ?? "";

headers["Host"] = $"apigateway-{apiId}-{stage}";
}
return headers;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;

using Microsoft.AspNetCore.Http;

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.AspNetCoreServer.Internal;

namespace Amazon.Lambda.AspNetCoreServer
{
/// <summary>
/// Base class for ASP.NET Core Lambda functions that are getting request from API Gateway Websocket API V2 payload format.
///
/// The http method is fixed as POST. Requests are handled using the RouteKey, so the same lambda should be referenced by multiple API Gateway routes for the ASP.NET Core IServer to successfully route requests.
/// </summary>
public abstract class APIGatewayWebsocketApiProxyFunction : APIGatewayProxyFunction
{
/// <summary>
/// Default constructor
/// </summary>
protected APIGatewayWebsocketApiProxyFunction()
: base()
{

}

/// <inheritdoc/>
/// <param name="startupMode">Configure when the ASP.NET Core framework will be initialized</param>
protected APIGatewayWebsocketApiProxyFunction(StartupMode startupMode)
: base(startupMode)
{

}

/// <summary>
/// Constructor used by Amazon.Lambda.AspNetCoreServer.Hosting to support ASP.NET Core projects using the Minimal API style.
/// </summary>
/// <param name="hostedServices"></param>
protected APIGatewayWebsocketApiProxyFunction(IServiceProvider hostedServices)
: base(hostedServices)
{
_hostServices = hostedServices;
}

/// <inheritdoc/>
/// <param name="apiGatewayRequest"></param>
/// <returns>string</returns>
protected override string ParseHttpPath(APIGatewayProxyRequest apiGatewayRequest)
{
var path = "/" + Utilities.DecodeResourcePath(apiGatewayRequest.RequestContext.RouteKey);
return path;
}

/// <inheritdoc/>
/// <param name="apiGatewayRequest"></param>
/// <returns>string</returns>
protected override string ParseHttpMethod(APIGatewayProxyRequest apiGatewayRequest)
{
return "POST";
}

/// <inheritdoc/>
/// <returns>IHeaderDictionary</returns>
protected override IHeaderDictionary AddMissingRequestHeaders(APIGatewayProxyRequest apiGatewayRequest, IHeaderDictionary headers)
{
headers = base.AddMissingRequestHeaders(apiGatewayRequest, headers);
headers["Content-Type"] = "application/json";
return headers;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Hosting;
using System.Diagnostics.CodeAnalysis;

namespace Amazon.Lambda.AspNetCoreServer
{
/// <summary>
/// APIGatewayWebsocketApiV2ProxyFunction is the base class that is implemented in a ASP.NET Core Web API. The derived class implements
/// the Init method similar to Main function in the ASP.NET Core and provides typed Startup. The function handler for
/// the Lambda function will point to this base class FunctionHandlerAsync method.
/// </summary>
/// <typeparam name ="TStartup">The type containing the startup methods for the application.</typeparam>
public abstract class APIGatewayWebsocketApiProxyFunction<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] TStartup> : APIGatewayWebsocketApiProxyFunction where TStartup : class
{
/// <summary>
/// Default Constructor. The ASP.NET Core Framework will be initialized as part of the construction.
/// </summary>
protected APIGatewayWebsocketApiProxyFunction()
: base()
{

}


/// <summary>
///
/// </summary>
/// <param name="startupMode">Configure when the ASP.NET Core framework will be initialized</param>
protected APIGatewayWebsocketApiProxyFunction(StartupMode startupMode)
: base(startupMode)
{

}

/// <inheritdoc/>
protected override void Init(IWebHostBuilder builder)
{
builder.UseStartup<TStartup>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.AspNetCoreServer.Test", "Amazon.Lambda.AspNetCoreServer.Test.csproj", "{AE614E81-1148-41E0-9CA7-B1F3EB34B65E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{AE614E81-1148-41E0-9CA7-B1F3EB34B65E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AE614E81-1148-41E0-9CA7-B1F3EB34B65E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AE614E81-1148-41E0-9CA7-B1F3EB34B65E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE614E81-1148-41E0-9CA7-B1F3EB34B65E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FDFB946B-997E-4736-A826-7461752C8DF4}
EndGlobalSection
EndGlobal
Loading