Skip to content

Add support for ASP.NET Core 3.0 to Amazon.Lambda.AspNetCoreServer #508

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

Closed
wants to merge 12 commits into from
Closed
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 @@ -5,14 +5,14 @@
using System.Security.Claims;
using System.Text;

using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;

using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.AspNetCoreServer.Internal;
using Microsoft.AspNetCore.Http.Features.Authentication;


namespace Amazon.Lambda.AspNetCoreServer
Expand Down Expand Up @@ -87,40 +87,7 @@ protected APIGatewayProxyFunction(StartupMode startupMode)

}

private protected override void InternalPostCreateContext(
HostingApplication.Context context,
APIGatewayProxyRequest apiGatewayRequest,
ILambdaContext lambdaContext)
{
var authorizer = apiGatewayRequest?.RequestContext?.Authorizer;

if (authorizer != null)
{
// handling claims output from cognito user pool authorizer
if (authorizer.Claims != null && authorizer.Claims.Count != 0)
{
var identity = new ClaimsIdentity(authorizer.Claims.Select(
entry => new Claim(entry.Key, entry.Value.ToString())), "AuthorizerIdentity");

lambdaContext.Logger.LogLine(
$"Configuring HttpContext.User with {authorizer.Claims.Count} claims coming from API Gateway's Request Context");
context.HttpContext.User = new ClaimsPrincipal(identity);
}
else
{
// handling claims output from custom lambda authorizer
var identity = new ClaimsIdentity(
authorizer.Where(x => !string.Equals(x.Key, "claims", StringComparison.OrdinalIgnoreCase))
.Select(entry => new Claim(entry.Key, entry.Value.ToString())), "AuthorizerIdentity");

lambdaContext.Logger.LogLine(
$"Configuring HttpContext.User with {authorizer.Count} claims coming from API Gateway's Request Context");
context.HttpContext.User = new ClaimsPrincipal(identity);
}
}
}

private protected override void InternalCustomResponseExceptionHandling(HostingApplication.Context context, APIGatewayProxyResponse apiGatewayResponse, ILambdaContext lambdaContext, Exception ex)
private protected override void InternalCustomResponseExceptionHandling(APIGatewayProxyResponse apiGatewayResponse, ILambdaContext lambdaContext, Exception ex)
{
apiGatewayResponse.MultiValueHeaders["ErrorType"] = new List<string> { ex.GetType().Name };
}
Expand All @@ -135,6 +102,40 @@ private protected override void InternalCustomResponseExceptionHandling(HostingA
/// <param name="lambdaContext"></param>
protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxyRequest apiGatewayRequest, ILambdaContext lambdaContext)
{
{
var authFeatures = (IHttpAuthenticationFeature) features;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a weird style to work with the concrete InvokeFeatures type but keep downcasting to the individual features. It'd be cleaner if you removed the explicit implementation prefix. E.g. IHttpAuthenticationFeature.User { get; set; } -> User { get; set; }.

Alternatively, change the APIs to take the abstract IFeatureCollection and call Get<IHttpAuthenticationFeature>() when you want to work with a specific feature. This might not quite match your usage here as you are the one populating the initial values and you know exactly which features are implemented.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it is weird but at this point I'm not ready to change the design. I actually think I got this from the ASP.NET Core repo back in the 1.0 days because I wouldn't have thought to do this way.


var authorizer = apiGatewayRequest?.RequestContext?.Authorizer;

if (authorizer != null)
{
// handling claims output from cognito user pool authorizer
if (authorizer.Claims != null && authorizer.Claims.Count != 0)
{
var identity = new ClaimsIdentity(authorizer.Claims.Select(
entry => new Claim(entry.Key, entry.Value.ToString())), "AuthorizerIdentity");

_logger.LogDebug(
$"Configuring HttpContext.User with {authorizer.Claims.Count} claims coming from API Gateway's Request Context");
authFeatures.User = new ClaimsPrincipal(identity);
}
else
{
// handling claims output from custom lambda authorizer
var identity = new ClaimsIdentity(
authorizer.Where(x => !string.Equals(x.Key, "claims", StringComparison.OrdinalIgnoreCase))
.Select(entry => new Claim(entry.Key, entry.Value.ToString())), "AuthorizerIdentity");

_logger.LogDebug(
$"Configuring HttpContext.User with {authorizer.Count} claims coming from API Gateway's Request Context");
authFeatures.User = new ClaimsPrincipal(identity);
}
}

// Call consumers customize method in case they want to change how API Gateway's request
// was marshalled into ASP.NET Core request.
PostMarshallHttpAuthenticationFeature(authFeatures, apiGatewayRequest, lambdaContext);
}
{
var requestFeatures = (IHttpRequestFeature) features;
requestFeatures.Scheme = "https";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Amazon.Lambda.AspNetCoreServer.Internal;
using Amazon.Lambda.Core;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -12,6 +11,11 @@
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features.Authentication;
#if NETCOREAPP_3_0
using Microsoft.Extensions.Hosting;
#endif


namespace Amazon.Lambda.AspNetCoreServer
{
Expand Down Expand Up @@ -183,8 +187,7 @@ protected virtual IWebHostBuilder CreateWebHostBuilder()
.UseDefaultServiceProvider((hostingContext, options) =>
{
options.ValidateScopes = hostingContext.HostingEnvironment.IsDevelopment();
})
.UseLambdaServer();
});


return builder;
Expand All @@ -206,6 +209,9 @@ protected void Start()
var builder = CreateWebHostBuilder();
Init(builder);

// Swap out Kestrel as the webserver and use our implementation of IServer
builder.UseLambdaServer();


_host = builder.Build();
PostCreateWebHost(_host);
Expand All @@ -215,7 +221,7 @@ protected void Start()
_server = _host.Services.GetService(typeof(Microsoft.AspNetCore.Hosting.Server.IServer)) as LambdaServer;
if (_server == null)
{
throw new Exception("Failed to find the implementation Lambda for the IServer registration. This can happen if UseApiGateway was not called.");
throw new Exception("Failed to find the implementation Lambda for the IServer registration. This can happen if UseLambdaServer was not called.");
}
_logger = ActivatorUtilities.CreateInstance<Logger<APIGatewayProxyFunction>>(this._host.Services);
}
Expand All @@ -224,7 +230,7 @@ protected void Start()
/// Creates a <see cref="HostingApplication.Context"/> object using the <see cref="LambdaServer"/> field in the class.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IServer check above could be done more effectively between Build and Start. If they didn't call UseApiGateway then Start is likely to fail for a variety of false positives (e.g. can't start the default Kestrel server).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the call to UseLambdaServer to after the point the end user could customize the IWebHostBuilder. This also pointed out that the comment should say UseLambdaServer since UseApiGateway is obsolete

/// </summary>
/// <param name="features"><see cref="IFeatureCollection"/> implementation.</param>
protected HostingApplication.Context CreateContext(IFeatureCollection features)
protected object CreateContext(IFeatureCollection features)
{
return _server.Application.CreateContext(features);
}
Expand Down Expand Up @@ -291,17 +297,16 @@ public virtual async Task<TRESPONSE> FunctionHandlerAsync(TREQUEST request, ILam

_logger.LogDebug($"ASP.NET Core Request PathBase: {((IHttpRequestFeature)features).PathBase}, Path: {((IHttpRequestFeature)features).Path}");


{
var itemFeatures = (IItemsFeature) features;
itemFeatures.Items = new ItemsDictionary();
itemFeatures.Items[LAMBDA_CONTEXT] = lambdaContext;
itemFeatures.Items[LAMBDA_REQUEST_OBJECT] = request;
PostMarshallItemsFeatureFeature(itemFeatures, request, lambdaContext);
}

var context = this.CreateContext(features);

InternalPostCreateContext(context, request, lambdaContext);

// Add along the Lambda objects to the HttpContext to give access to Lambda to them in the ASP.NET Core application
context.HttpContext.Items[LAMBDA_CONTEXT] = lambdaContext;
context.HttpContext.Items[LAMBDA_REQUEST_OBJECT] = request;

// Allow the context to be customized before passing the request to ASP.NET Core.
PostCreateContext(context, request, lambdaContext);

var response = await this.ProcessRequest(lambdaContext, context, features);

return response;
Expand All @@ -317,85 +322,84 @@ public virtual async Task<TRESPONSE> FunctionHandlerAsync(TREQUEST request, ILam
/// If specified, an unhandled exception will be rethrown for custom error handling.
/// Ensure that the error handling code calls 'this.MarshallResponse(features, 500);' after handling the error to return a the typed Lambda object to the user.
/// </param>
protected async Task<TRESPONSE> ProcessRequest(ILambdaContext lambdaContext, HostingApplication.Context context, InvokeFeatures features, bool rethrowUnhandledError = false)
protected async Task<TRESPONSE> ProcessRequest(ILambdaContext lambdaContext, object context, InvokeFeatures features, bool rethrowUnhandledError = false)
{
var defaultStatusCode = 200;
Exception ex = null;
try
{
await this._server.Application.ProcessRequestAsync(context);
}
catch (AggregateException agex)
{
ex = agex;
_logger.LogError($"Caught AggregateException: '{agex}'");
var sb = new StringBuilder();
foreach (var newEx in agex.InnerExceptions)
try
{
sb.AppendLine(this.ErrorReport(newEx));
await this._server.Application.ProcessRequestAsync(context);
}

_logger.LogError(sb.ToString());
defaultStatusCode = 500;
}
catch (ReflectionTypeLoadException rex)
{
ex = rex;
_logger.LogError($"Caught ReflectionTypeLoadException: '{rex}'");
var sb = new StringBuilder();
foreach (var loaderException in rex.LoaderExceptions)
catch (AggregateException agex)
{
var fileNotFoundException = loaderException as FileNotFoundException;
if (fileNotFoundException != null && !string.IsNullOrEmpty(fileNotFoundException.FileName))
ex = agex;
_logger.LogError($"Caught AggregateException: '{agex}'");
var sb = new StringBuilder();
foreach (var newEx in agex.InnerExceptions)
{
sb.AppendLine($"Missing file: {fileNotFoundException.FileName}");
sb.AppendLine(this.ErrorReport(newEx));
}
else

_logger.LogError(sb.ToString());
defaultStatusCode = 500;
}
catch (ReflectionTypeLoadException rex)
{
ex = rex;
_logger.LogError($"Caught ReflectionTypeLoadException: '{rex}'");
var sb = new StringBuilder();
foreach (var loaderException in rex.LoaderExceptions)
{
sb.AppendLine(this.ErrorReport(loaderException));
var fileNotFoundException = loaderException as FileNotFoundException;
if (fileNotFoundException != null && !string.IsNullOrEmpty(fileNotFoundException.FileName))
{
sb.AppendLine($"Missing file: {fileNotFoundException.FileName}");
}
else
{
sb.AppendLine(this.ErrorReport(loaderException));
}
}

_logger.LogError(sb.ToString());
defaultStatusCode = 500;
}
catch (Exception e)
{
ex = e;
if (rethrowUnhandledError) throw;
_logger.LogError($"Unknown error responding to request: {this.ErrorReport(e)}");
defaultStatusCode = 500;
}

_logger.LogError(sb.ToString());
defaultStatusCode = 500;
}
catch (Exception e)
{
ex = e;
if (rethrowUnhandledError) throw;
_logger.LogError($"Unknown error responding to request: {this.ErrorReport(e)}");
defaultStatusCode = 500;
}
finally
{
this._server.Application.DisposeContext(context, ex);
}
if (features.ResponseStartingEvents != null)
{
await features.ResponseStartingEvents.ExecuteAsync();
}
var response = this.MarshallResponse(features, lambdaContext, defaultStatusCode);

if (features.ResponseStartingEvents != null)
{
await features.ResponseStartingEvents.ExecuteAsync();
}
var response = this.MarshallResponse(features, lambdaContext, defaultStatusCode);
if (ex != null)
{
InternalCustomResponseExceptionHandling(response, lambdaContext, ex);
}

if (ex != null)
{
InternalCustomResponseExceptionHandling(context, response, lambdaContext, ex);
}
if (features.ResponseCompletedEvents != null)
{
await features.ResponseCompletedEvents.ExecuteAsync();
}

if (features.ResponseCompletedEvents != null)
return response;
}
finally
{
await features.ResponseCompletedEvents.ExecuteAsync();
this._server.Application.DisposeContext(context, ex);
}

return response;
}


private protected virtual void InternalPostCreateContext(HostingApplication.Context context, TREQUEST lambdaRequest, ILambdaContext lambdaContext)
{

}

private protected virtual void InternalCustomResponseExceptionHandling(HostingApplication.Context context, TRESPONSE lambdaReponse, ILambdaContext lambdaContext, Exception ex)
private protected virtual void InternalCustomResponseExceptionHandling(TRESPONSE lambdaReponse, ILambdaContext lambdaContext, Exception ex)
{

}
Expand All @@ -409,18 +413,32 @@ protected virtual void PostCreateWebHost(IWebHost webHost)
{

}

/// <summary>
/// This method is called after the HostingApplication.Context has been created. Derived classes can overwrite this method to alter
/// the context before passing the request to ASP.NET Core to process the request.
/// This method is called after marshalling the incoming Lambda request
/// into ASP.NET Core's IItemsFeature. Derived classes can overwrite this method to alter
/// the how the marshalling was done.
/// </summary>
/// <param name="context"></param>
/// <param name="aspNetCoreItemFeature"></param>
/// <param name="lambdaRequest"></param>
/// <param name="lambdaContext"></param>
protected virtual void PostCreateContext(HostingApplication.Context context, TREQUEST lambdaRequest, ILambdaContext lambdaContext)
protected virtual void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCoreItemFeature, TREQUEST lambdaRequest, ILambdaContext lambdaContext)
{

}

/// <summary>
/// This method is called after marshalling the incoming Lambda request
/// into ASP.NET Core's IHttpAuthenticationFeature. Derived classes can overwrite this method to alter
/// the how the marshalling was done.
/// </summary>
/// <param name="aspNetCoreHttpAuthenticationFeature"></param>
/// <param name="lambdaRequest"></param>
/// <param name="lambdaContext"></param>
protected virtual void PostMarshallHttpAuthenticationFeature(IHttpAuthenticationFeature aspNetCoreHttpAuthenticationFeature, TREQUEST lambdaRequest, ILambdaContext lambdaContext)
{

}

/// <summary>
/// This method is called after marshalling the incoming Lambda request
Expand Down
Loading