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 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; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Amazon.Lambda.AspNetCoreServer { /// /// Base class for ASP.NET Core Lambda functions that are getting request from API Gateway HTTP API V2 payload format. /// public abstract class APIGatewayHttpApiV2ProxyFunction : AbstractAspNetCoreFunction { /// /// Default constructor /// protected APIGatewayHttpApiV2ProxyFunction() : base() { } /// /// Configure when the ASP.NET Core framework will be initialized protected APIGatewayHttpApiV2ProxyFunction(StartupMode startupMode) : base(startupMode) { } /// /// Constructor used by Amazon.Lambda.AspNetCoreServer.Hosting to support ASP.NET Core projects using the Minimal API style. /// /// protected APIGatewayHttpApiV2ProxyFunction(IServiceProvider hostedServices) : base(hostedServices) { _hostServices = hostedServices; } private protected override void InternalCustomResponseExceptionHandling(APIGatewayHttpApiV2ProxyResponse apiGatewayResponse, ILambdaContext lambdaContext, Exception ex) { apiGatewayResponse.SetHeaderValues("ErrorType", ex.GetType().Name, false); } /// /// 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, APIGatewayHttpApiV2ProxyRequest 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.Jwt?.Claims != null && authorizer.Jwt.Claims.Count != 0) { var identity = new ClaimsIdentity(authorizer.Jwt.Claims.Select( entry => new Claim(entry.Key, entry.Value.ToString())), "AuthorizerIdentity"); _logger.LogDebug( $"Configuring HttpContext.User with {authorizer.Jwt.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.Jwt?.Claims.Select(entry => new Claim(entry.Key, entry.Value)), "AuthorizerIdentity"); _logger.LogDebug( $"Configuring HttpContext.User with {identity.Claims.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 httpInfo = apiGatewayRequest.RequestContext.Http; var requestFeatures = (IHttpRequestFeature)features; requestFeatures.Scheme = "https"; requestFeatures.Method = httpInfo.Method; if (string.IsNullOrWhiteSpace(apiGatewayRequest.RequestContext?.DomainName)) { _logger.LogWarning($"Request does not contain domain name information but is derived from {nameof(APIGatewayProxyFunction)}."); } var rawQueryString = Utilities.CreateQueryStringParametersFromHttpApiV2(apiGatewayRequest.RawQueryString); requestFeatures.RawTarget = apiGatewayRequest.RawPath + rawQueryString; requestFeatures.QueryString = rawQueryString; requestFeatures.Path = Utilities.DecodeResourcePath(httpInfo.Path); if (!requestFeatures.Path.StartsWith("/")) { requestFeatures.Path = "/" + requestFeatures.Path; } // If there is a stage name in the resource path strip it out and set the stage name as the base path. // This is required so that ASP.NET Core will route request based on the resource path without the stage name. var stageName = apiGatewayRequest.RequestContext.Stage; if (!string.IsNullOrWhiteSpace(stageName)) { if (requestFeatures.Path.StartsWith($"/{stageName}")) { requestFeatures.Path = requestFeatures.Path.Substring(stageName.Length + 1); requestFeatures.PathBase = $"/{stageName}"; } } // API Gateway HTTP API V2 format supports multiple values for headers by comma separating the values. if (apiGatewayRequest.Headers != null) { foreach(var kvp in apiGatewayRequest.Headers) { requestFeatures.Headers[kvp.Key] = new StringValues(kvp.Value?.Split(',')); } } if (!requestFeatures.Headers.ContainsKey("Host")) { requestFeatures.Headers["Host"] = apiGatewayRequest.RequestContext.DomainName; } if (apiGatewayRequest.Cookies != null) { // Add Cookies from the event requestFeatures.Headers["Cookie"] = String.Join("; ", apiGatewayRequest.Cookies); } 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?.Http?.SourceIp) && IPAddress.TryParse(apiGatewayRequest.RequestContext.Http.SourceIp, out var remoteIpAddress)) { connectionFeatures.RemoteIpAddress = remoteIpAddress; } if (apiGatewayRequest?.Headers?.TryGetValue("X-Forwarded-Port", out var port) == true) { connectionFeatures.RemotePort = int.Parse(port, 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?.Authentication?.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 APIGatewayHttpApiV2ProxyResponse MarshallResponse(IHttpResponseFeature responseFeatures, ILambdaContext lambdaContext, int statusCodeIfNotSet = 200) { var response = new APIGatewayHttpApiV2ProxyResponse { StatusCode = responseFeatures.StatusCode != 0 ? responseFeatures.StatusCode : statusCodeIfNotSet }; string contentType = null; string contentEncoding = null; if (responseFeatures.Headers != null) { response.Headers = new Dictionary(); foreach (var kvp in responseFeatures.Headers) { if (kvp.Key.Equals(HeaderNames.SetCookie, StringComparison.CurrentCultureIgnoreCase)) { // Cookies must be passed through the proxy response property and not as a // header to be able to pass back multiple cookies in a single request. response.Cookies = kvp.Value.ToArray(); continue; } response.SetHeaderValues(kvp.Key, kvp.Value.ToArray(), false); // Remember the Content-Type for possible later use if (kvp.Key.Equals("Content-Type", StringComparison.CurrentCultureIgnoreCase) && response.Headers[kvp.Key]?.Length > 0) { contentType = response.Headers[kvp.Key]; } else if (kvp.Key.Equals("Content-Encoding", StringComparison.CurrentCultureIgnoreCase) && response.Headers[kvp.Key]?.Length > 0) { contentEncoding = response.Headers[kvp.Key]; } } } if (contentType == null) { response.Headers["Content-Type"] = 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; } } }