< Summary

Information
Class: NanoRoute.AwsLambda.DtoMappingExtensions
Assembly: NanoRoute.AwsLambda.dll
File(s): /home/runner/work/nanoroute/nanoroute/Src/NanoRoute.AwsLambda/Private/DtoMappingExtensions.cs
Line coverage
100%
Covered lines: 42
Uncovered lines: 0
Coverable lines: 42
Total lines: 163
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBlocks covered Blocks not covered
DtoMappingExtensions()20
CreateUri(...)360
CreateRequestMessage(...)490

File(s)

/home/runner/work/nanoroute/nanoroute/Src/NanoRoute.AwsLambda/Private/DtoMappingExtensions.cs

#LineLine coverage
 1/********************************************************************************
 2* DtoMappingExtensions.cs                                                       *
 3*                                                                               *
 4* Author: Denes Solti                                                           *
 5********************************************************************************/
 6using System;
 7using System.Collections.Generic;
 8using System.IO;
 9using System.Net.Http;
 10using System.Net.Http.Headers;
 11using System.Text.RegularExpressions;
 12using System.Threading.Tasks;
 13
 14using Amazon.Lambda.APIGatewayEvents;
 15
 16namespace NanoRoute.AwsLambda
 17{
 18    using Properties;
 19
 20    /// <summary>
 21    /// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
 22    /// </summary>
 23    internal static class DtoMappingExtensions
 24    {
 125        private static readonly Regex s_protoMatcher = new(@"(?:^|;\s*)proto=(?:""(?<proto>[^""]+)""|(?<proto>[^;]+))");
 26
 27        public static Uri CreateUri(this APIGatewayHttpApiV2ProxyRequest request)
 128        {
 129            if
 130            (
 131                HostAndPort(request.Headers) is not { Length: > 0 } hostAndPort ||
 132                Scheme(request.Headers) is not { Length: > 0 } scheme ||
 133                // Parse the base URI as a URI so host:port and IPv6 literals are handled correctly
 134                !Uri.TryCreate($"{scheme}://{hostAndPort}", UriKind.Absolute, out Uri? baseUri)
 135            )
 136                throw new InvalidOperationException(Resources.ERR_UNKNOWN_URI);
 37
 138            UriBuilder builder = new(baseUri)
 139            {
 140                Path = request.RawPath is { Length: > 0 } path ? path : "/",
 141                Query = request.RawQueryString is { Length: > 0 } query ? query : null
 142            };
 43
 144            return builder.Uri;
 45
 46            static string? HostAndPort(IDictionary<string, string> headers)
 47            {
 48                if (headers.TryGetValue("host" /*AWS lowercases the header names*/, out string? hostAndPort))
 49                    return hostAndPort;
 50
 51                return null;
 52            }
 53
 54            static string? Scheme(IDictionary<string, string> headers)
 55            {
 56                if (headers.TryGetValue("forwarded", out string? forwarded) && s_protoMatcher.Match(forwarded) is { Succ
 57                    return match.Groups["proto"].Value;
 58
 59                if (headers.TryGetValue("x-forwarded-proto", out string? proto))
 60                    return proto;
 61
 62                return null;
 63            }
 164        }
 65
 66        public static HttpRequestMessage CreateRequestMessage(this APIGatewayHttpApiV2ProxyRequest request)
 167        {
 168            HttpRequestMessage requestMessage = new(new HttpMethod(request.RequestContext.Http.Method), request.CreateUr
 169            {
 170                Content = request switch
 171                {
 172                    { IsBase64Encoded: false } and { Body.Length: > 0 } => new StringContent(request.Body),
 173                    { IsBase64Encoded: true } and { Body.Length: > 0 } => new StreamContent
 174                    (
 175                        new Base64BodyReaderStream(request.Body)
 176                    ),
 177                    _ => null
 178                }
 179            };
 80
 181            foreach (KeyValuePair<string, string> header in request.Headers)
 182            {
 183                HttpHeaders headers = requestMessage.Content is not null && HttpRequestMessage.ContentHeaders.Contains(h
 184                    ? requestMessage.Content.Headers
 185                    : requestMessage.Headers;
 86
 87                // Some header (like Content-Type) has its default value. Without this line we'd just append the value l
 188                headers.Remove(header.Key);
 189                headers.TryAddWithoutValidation(header.Key, header.Value);
 190            }
 91
 192            requestMessage.Options.Set(new HttpRequestOptionsKey<APIGatewayHttpApiV2ProxyRequest>(Router.OriginalRequest
 193            requestMessage.Options.Set(new HttpRequestOptionsKey<string>(Router.TraceIdName), request.RequestContext.Req
 94
 195            return requestMessage;
 196        }
 97
 98        public static async Task<APIGatewayHttpApiV2ProxyResponse> CreateResponse(this HttpResponseMessage responseMessa
 99        {
 100            Dictionary<string, string> headers = new(StringComparer.OrdinalIgnoreCase);
 101            List<string> cookies = new();
 102
 103            APIGatewayHttpApiV2ProxyResponse response = new()
 104            {
 105                StatusCode = (int) responseMessage.StatusCode
 106            };
 107
 108            CopyHeaders(responseMessage.Headers, headers, cookies);
 109
 110            if (responseMessage.Content is { } content)
 111            {
 112                CopyHeaders(content.Headers, headers, cookies);
 113
 114                switch (content)
 115                {
 116                    case StringContent stringContent:
 117                    {
 118                        if (await stringContent.ReadAsStringAsync().ConfigureAwait(false) is { Length: > 0 } body)
 119                            response.Body = body;
 120                        break;
 121                    }
 122                    case { } byteContent:
 123                    {
 124                        using Base64BodyWriterStream destination = new();
 125
 126                        await byteContent.CopyToAsync(destination).ConfigureAwait(false);
 127
 128                        if (destination.GetBody() is { Length: > 0 } body)
 129                        {
 130                            response.Body = body;
 131                            response.IsBase64Encoded = true;
 132                        }
 133
 134                        break;
 135                    }
 136                }
 137            }
 138
 139            response.Headers = headers;
 140            response.Cookies = cookies.ToArray();
 141
 142            return response;
 143
 144            static void CopyHeaders(IEnumerable<KeyValuePair<string, IEnumerable<string>>> source, Dictionary<string, st
 145            {
 146                foreach (KeyValuePair<string, IEnumerable<string>> header in source)
 147                {
 148                    if (string.Equals(header.Key, "Set-Cookie", StringComparison.OrdinalIgnoreCase))
 149                    {
 150                        cookies.AddRange(header.Value);
 151                        continue;
 152                    }
 153
 154                    string value = string.Join(",", header.Value);
 155
 156                    headers[header.Key] = headers.TryGetValue(header.Key, out string? existing)
 157                        ? $"{existing},{value}"
 158                        : value;
 159                }
 160            }
 161        }
 162    }
 163}