using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Security.Claims; using System.Text; 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; using System.Globalization; using System.Security.Cryptography.X509Certificates; namespace Amazon.Lambda.AspNetCoreServer { /// /// APIGatewayProxyFunction is the base class for Lambda functions hosting the ASP.NET Core framework and exposed to the web via API Gateway. /// /// The derived class implements the Init method similar to Main function in the ASP.NET Core. The function handler for the Lambda function will point /// to this base class FunctionHandlerAsync method. /// public abstract class APIGatewayProxyFunction : AbstractAspNetCoreFunction { /// /// Key to access the ILambdaContext object from the HttpContext.Items collection. /// [Obsolete("References should be updated to Amazon.Lambda.AspNetCoreServer.AbstractAspNetCoreFunction.LAMBDA_CONTEXT")] public new const string LAMBDA_CONTEXT = AbstractAspNetCoreFunction.LAMBDA_CONTEXT; /// /// Key to access the APIGatewayProxyRequest object from the HttpContext.Items collection. /// [Obsolete("References should be updated to Amazon.Lambda.AspNetCoreServer.AbstractAspNetCoreFunction.LAMBDA_REQUEST_OBJECT")] public const string APIGATEWAY_REQUEST = AbstractAspNetCoreFunction.LAMBDA_REQUEST_OBJECT; /// /// The modes for when the ASP.NET Core framework will be initialized. /// [Obsolete("References should be updated to Amazon.Lambda.AspNetCoreServer.StartupMode")] public enum AspNetCoreStartupMode { /// /// Initialize during the construction of APIGatewayProxyFunction /// Constructor = StartupMode.Constructor, /// /// Initialize during the first incoming request /// FirstRequest = StartupMode.FirstRequest } /// /// Default Constructor. The ASP.NET Core Framework will be initialized as part of the construction. /// protected APIGatewayProxyFunction() : base() { } /// /// /// /// Configure when the ASP.NET Core framework will be initialized [Obsolete("Calls to the constructor should be replaced with the constructor that takes a Amazon.Lambda.AspNetCoreServer.StartupMode as the parameter.")] protected APIGatewayProxyFunction(AspNetCoreStartupMode startupMode) : base((StartupMode)startupMode) { } /// /// /// /// Configure when the ASP.NET Core framework will be initialized protected APIGatewayProxyFunction(StartupMode startupMode) : base(startupMode) { } /// /// Constructor used by Amazon.Lambda.AspNetCoreServer.Hosting to support ASP.NET Core projects using the Minimal API style. /// /// protected APIGatewayProxyFunction(IServiceProvider hostedServices) : base(hostedServices) { _hostServices = hostedServices; } private protected override void InternalCustomResponseExceptionHandling(APIGatewayProxyResponse apiGatewayResponse, ILambdaContext lambdaContext, Exception ex) { apiGatewayResponse.MultiValueHeaders["ErrorType"] = new List { ex.GetType().Name }; } /// /// Convert the JSON document received from API Gateway into the InvokeFeatures object. /// InvokeFeatures is then passed into IHttpApplication to create the ASP.NET Core request objects. /// /// /// /// protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxyRequest apiGatewayRequest, ILambdaContext lambdaContext) { { var authFeatures = (IHttpAuthenticationFeature)features; 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 => x.Value != null && !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"; requestFeatures.Method = apiGatewayRequest.HttpMethod; 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; } var rawQueryString = Utilities.CreateQueryStringParameters( apiGatewayRequest.QueryStringParameters, apiGatewayRequest.MultiValueQueryStringParameters, true); requestFeatures.RawTarget = apiGatewayRequest.Path + rawQueryString; requestFeatures.QueryString = rawQueryString; requestFeatures.Path = path; requestFeatures.PathBase = string.Empty; if (!string.IsNullOrEmpty(apiGatewayRequest?.RequestContext?.Path)) { // This is to cover the case where the request coming in is https://myapigatewayid.execute-api.us-west-2.amazonaws.com/Prod where // Prod is the stage name and there is no ending '/'. Path will be set to '/' so to make sure we detect the correct base path // append '/' on the end to make the later EndsWith and substring work correctly. var requestContextPath = apiGatewayRequest.RequestContext.Path; if (path.EndsWith("/") && !requestContextPath.EndsWith("/")) { requestContextPath += "/"; } else if (!path.EndsWith("/") && requestContextPath.EndsWith("/")) { // Handle a trailing slash in the request path: e.g. https://myapigatewayid.execute-api.us-west-2.amazonaws.com/Prod/foo/ requestFeatures.Path = path += "/"; } if (requestContextPath.EndsWith(path)) { requestFeatures.PathBase = requestContextPath.Substring(0, requestContextPath.Length - requestFeatures.Path.Length); } } requestFeatures.Path = Utilities.DecodeResourcePath(requestFeatures.Path); 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}"; } if (!string.IsNullOrEmpty(apiGatewayRequest.Body)) { requestFeatures.Body = Utilities.ConvertLambdaRequestBodyToAspNetCoreBody(apiGatewayRequest.Body, apiGatewayRequest.IsBase64Encoded); } // Make sure the content-length header is set if header was not present. const string contentLengthHeaderName = "Content-Length"; if (!requestFeatures.Headers.ContainsKey(contentLengthHeaderName)) { requestFeatures.Headers[contentLengthHeaderName] = requestFeatures.Body == null ? "0" : requestFeatures.Body.Length.ToString(CultureInfo.InvariantCulture); } // Call consumers customize method in case they want to change how API Gateway's request // was marshalled into ASP.NET Core request. PostMarshallRequestFeature(requestFeatures, apiGatewayRequest, lambdaContext); } { // set up connection features var connectionFeatures = (IHttpConnectionFeature)features; if (!string.IsNullOrEmpty(apiGatewayRequest?.RequestContext?.Identity?.SourceIp) && IPAddress.TryParse(apiGatewayRequest.RequestContext.Identity.SourceIp, out var remoteIpAddress)) { connectionFeatures.RemoteIpAddress = remoteIpAddress; } if (apiGatewayRequest?.Headers?.TryGetValue("X-Forwarded-Port", out var forwardedPort) == true) { connectionFeatures.RemotePort = int.Parse(forwardedPort, CultureInfo.InvariantCulture); } // Call consumers customize method in case they want to change how API Gateway's request // was marshalled into ASP.NET Core request. PostMarshallConnectionFeature(connectionFeatures, apiGatewayRequest, lambdaContext); } { var tlsConnectionFeature = (ITlsConnectionFeature)features; var clientCertPem = apiGatewayRequest?.RequestContext?.Identity?.ClientCert?.ClientCertPem; if (clientCertPem != null) { tlsConnectionFeature.ClientCertificate = Utilities.GetX509Certificate2FromPem(clientCertPem); } PostMarshallTlsConnectionFeature(tlsConnectionFeature, apiGatewayRequest, lambdaContext); } } /// /// Convert the response coming from ASP.NET Core into APIGatewayProxyResponse which is /// serialized into the JSON object that API Gateway expects. /// /// /// Sometimes the ASP.NET server doesn't set the status code correctly when successful, so this parameter will be used when the value is 0. /// /// protected override APIGatewayProxyResponse MarshallResponse(IHttpResponseFeature responseFeatures, ILambdaContext lambdaContext, int statusCodeIfNotSet = 200) { var response = new APIGatewayProxyResponse { StatusCode = responseFeatures.StatusCode != 0 ? responseFeatures.StatusCode : statusCodeIfNotSet }; string contentType = null; string contentEncoding = null; if (responseFeatures.Headers != null) { response.MultiValueHeaders = new Dictionary>(); response.Headers = new Dictionary(); foreach (var kvp in responseFeatures.Headers) { response.MultiValueHeaders[kvp.Key] = kvp.Value.ToList(); // Remember the Content-Type for possible later use if (kvp.Key.Equals("Content-Type", StringComparison.CurrentCultureIgnoreCase) && response.MultiValueHeaders[kvp.Key].Count > 0) { contentType = response.MultiValueHeaders[kvp.Key][0]; } else if (kvp.Key.Equals("Content-Encoding", StringComparison.CurrentCultureIgnoreCase) && response.MultiValueHeaders[kvp.Key].Count > 0) { contentEncoding = response.MultiValueHeaders[kvp.Key][0]; } } } if (contentType == null) { response.MultiValueHeaders["Content-Type"] = new List() { null }; } if (responseFeatures.Body != null) { // Figure out how we should treat the response content, check encoding first to see if body is compressed, then check content type var rcEncoding = GetResponseContentEncodingForContentEncoding(contentEncoding); if (rcEncoding != ResponseContentEncoding.Base64) { rcEncoding = GetResponseContentEncodingForContentType(contentType); } (response.Body, response.IsBase64Encoded) = Utilities.ConvertAspNetCoreBodyToLambdaBody(responseFeatures.Body, rcEncoding); } PostMarshallResponseFeature(responseFeatures, response, lambdaContext); _logger.LogDebug($"Response Base 64 Encoded: {response.IsBase64Encoded}"); return response; } } }