using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; using Amazon.Lambda.APIGatewayEvents; namespace Amazon.Lambda.AspNetCoreServer.Internal { /// /// /// public static class Utilities { public static void EnsureLambdaServerRegistered(IServiceCollection services) { EnsureLambdaServerRegistered(services, typeof(LambdaServer)); } public static void EnsureLambdaServerRegistered(IServiceCollection services, Type serverType) { IList toRemove = new List(); var serviceDescriptions = services.Where(x => x.ServiceType == typeof(IServer)); int lambdaServiceCount = 0; // There can be more then one IServer implementation registered if the consumer called ConfigureWebHostDefaults in the Init override for the IHostBuilder. // This makes sure there is only one registered IServer using LambdaServer and removes any other registrations. foreach (var serviceDescription in serviceDescriptions) { if (serviceDescription.ImplementationType == serverType) { lambdaServiceCount++; // If more then one LambdaServer registration has occurred then remove the extra registrations. if (lambdaServiceCount > 1) { toRemove.Add(serviceDescription); } } // If there is an IServer registered that isn't LambdaServer then remove it. This is most likely caused // by leaving the UseKestrel call. else { toRemove.Add(serviceDescription); } } foreach (var serviceDescription in toRemove) { services.Remove(serviceDescription); } if (lambdaServiceCount == 0) { services.AddSingleton(typeof(IServer), serverType); } } internal static Stream ConvertLambdaRequestBodyToAspNetCoreBody(string body, bool isBase64Encoded) { Byte[] binaryBody; if (isBase64Encoded) { binaryBody = Convert.FromBase64String(body); } else { binaryBody = UTF8Encoding.UTF8.GetBytes(body); } return new MemoryStream(binaryBody); } internal static (string body, bool isBase64Encoded) ConvertAspNetCoreBodyToLambdaBody(Stream aspNetCoreBody, ResponseContentEncoding rcEncoding) { // Do we encode the response content in Base64 or treat it as UTF-8 if (rcEncoding == ResponseContentEncoding.Base64) { // We want to read the response content "raw" and then Base64 encode it byte[] bodyBytes; if (aspNetCoreBody is MemoryStream) { bodyBytes = ((MemoryStream)aspNetCoreBody).ToArray(); } else { using (var ms = new MemoryStream()) { aspNetCoreBody.CopyTo(ms); bodyBytes = ms.ToArray(); } } return (body: Convert.ToBase64String(bodyBytes), isBase64Encoded: true); } else if (aspNetCoreBody is MemoryStream) { return (body: UTF8Encoding.UTF8.GetString(((MemoryStream)aspNetCoreBody).ToArray()), isBase64Encoded: false); } else { aspNetCoreBody.Position = 0; using (StreamReader reader = new StreamReader(aspNetCoreBody, Encoding.UTF8)) { return (body: reader.ReadToEnd(), isBase64Encoded: false); } } } /// /// Add a '?' to the start of a non-empty query string, otherwise return null. /// /// /// The ASP.NET MVC pipeline expects the query string to be URL-escaped. Since the value in /// should already be escaped this /// method does not perform any escaping itself. This ensures identical behaviour when an MVC app /// is run through an API Gateway with this framework or in a standalone Kestrel instance. /// /// URL-escaped query string without initial '?' /// public static string CreateQueryStringParametersFromHttpApiV2(string queryString) { if (string.IsNullOrEmpty(queryString)) return null; return "?" + queryString; } internal static string CreateQueryStringParameters(IDictionary singleValues, IDictionary> multiValues, bool urlEncodeValue) { if (multiValues?.Count > 0) { StringBuilder sb = new StringBuilder("?"); foreach (var kvp in multiValues) { foreach (var value in kvp.Value) { if (sb.Length > 1) { sb.Append("&"); } sb.Append($"{kvp.Key}={(urlEncodeValue ? WebUtility.UrlEncode(value) : value)}"); } } return sb.ToString(); } else if (singleValues?.Count > 0) { var queryStringParameters = singleValues; if (queryStringParameters != null && queryStringParameters.Count > 0) { StringBuilder sb = new StringBuilder("?"); foreach (var kvp in singleValues) { if (sb.Length > 1) { sb.Append("&"); } sb.Append($"{kvp.Key}={(urlEncodeValue ? WebUtility.UrlEncode(kvp.Value) : kvp.Value)}"); } return sb.ToString(); } } return string.Empty; } internal static void SetHeadersCollection(IHeaderDictionary headers, IDictionary singleValues, IDictionary> multiValues) { if (multiValues?.Count > 0) { foreach (var kvp in multiValues) { headers[kvp.Key] = new StringValues(kvp.Value.ToArray()); } } else if (singleValues?.Count > 0) { foreach (var kvp in singleValues) { headers[kvp.Key] = new StringValues(kvp.Value); } } } // This code is taken from the Apache 2.0 licensed ASP.NET Core repo. // https://github.com/aspnet/AspNetCore/blob/d7bfbb5824b5f8876bcd4afaa29a611efc7aa1c9/src/Http/Shared/StreamCopyOperationInternal.cs internal static async Task CopyToAsync(Stream source, Stream destination, long? count, int bufferSize, CancellationToken cancel) { long? bytesRemaining = count; var buffer = ArrayPool.Shared.Rent(bufferSize); try { Debug.Assert(source != null); Debug.Assert(destination != null); Debug.Assert(!bytesRemaining.HasValue || bytesRemaining.GetValueOrDefault() >= 0); Debug.Assert(buffer != null); while (true) { // The natural end of the range. if (bytesRemaining.HasValue && bytesRemaining.GetValueOrDefault() <= 0) { return; } cancel.ThrowIfCancellationRequested(); int readLength = buffer.Length; if (bytesRemaining.HasValue) { readLength = (int)Math.Min(bytesRemaining.GetValueOrDefault(), (long)readLength); } int read = await source.ReadAsync(buffer, 0, readLength, cancel); if (bytesRemaining.HasValue) { bytesRemaining -= read; } // End of the source stream. if (read == 0) { return; } cancel.ThrowIfCancellationRequested(); await destination.WriteAsync(buffer, 0, read, cancel); } } finally { ArrayPool.Shared.Return(buffer); } } internal static string DecodeResourcePath(string resourcePath) => WebUtility.UrlDecode(resourcePath // Convert any + signs to percent encoding before URL decoding the path. .Replace("+", "%2B") // Double-escape any %2F (encoded / characters) so that they survive URL decoding the path. .Replace("%2F", "%252F") .Replace("%2f", "%252f")); internal static X509Certificate2 GetX509Certificate2FromPem(string clientCertPem) { clientCertPem = clientCertPem.TrimEnd('\n'); if (!clientCertPem.StartsWith("-----BEGIN CERTIFICATE-----") || !clientCertPem.EndsWith("-----END CERTIFICATE-----")) { throw new InvalidOperationException( "Client certificate PEM was invalid. Expected to start with '-----BEGIN CERTIFICATE-----' " + "and end with '-----END CERTIFICATE-----'."); } // Remove "-----BEGIN CERTIFICATE-----\n" and "-----END CERTIFICATE-----" clientCertPem = clientCertPem.Substring(28, clientCertPem.Length - 53); return new X509Certificate2(Convert.FromBase64String(clientCertPem)); } } }