using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; #if NET6_0_OR_GREATER using System.Buffers; using System.Text.Json; using System.Text.Json.Serialization; #endif // This assembly is also used in the source generator which needs the .NET attributes and type infos defined in this project. // The source generator requires all dependencies to target .NET Standard 2.0 to be compatible with .NET Framework used // by Visual Studio. Several APIs are used in this file that are not available in .NET Standard 2.0 but the source // generator doesn't use the methods that use those .NET APIs. Several areas in this file put stub implementations // for .NET Standard 2.0 to allow the types to be available in the source generator but the implementations // are not actually called in the source generator. namespace Amazon.Lambda.Annotations.APIGateway { /// /// The options used by the IHttpResult to serialize into the required format for the event source of the Lambda funtion. /// public class HttpResultSerializationOptions { /// /// The API Gateway protocol format used as the event source. /// public enum ProtocolFormat { /// /// Used when a function is defined with the RestApiAttribute. /// RestApi, /// /// Used when a function is defined with the HttpApiAttribute. /// HttpApi } /// /// The API Gateway protocol version. /// public enum ProtocolVersion { /// /// V1 format for API Gateway Proxy responses. Used for functions defined with RestApiAttribute or HttpApiAttribute with explicit setting to V1. /// V1, /// /// V2 format for API Gateway Proxy responses. Used for functions defined with HttpApiAttribute with an implicit version of an explicit setting to V2. /// V2 } /// /// The API Gateway protocol used as the event source. /// RestApi -> RestApiAttrbute /// HttpApi -> HttpApiAttribute /// public ProtocolFormat Format { get; set; } /// /// The API Gateway protocol version used as the event source. /// V1 -> RestApi or HttpApi specifically set as V1 /// V2 -> HttpApi either implicit or explicit set to V2 /// public ProtocolVersion Version { get; set; } } /// /// If this inteface is returned for an API Gateway Lambda function it will serialize itself to the correct JSON format for the /// configured event source's protocol format and version. /// /// Users should use the implementation class HttpResults to construct an instance of IHttpResult with the configured, status code, response body and headers. /// /// /// return HttpResults.Ok("All Good") /// .AddHeader("Custom-Header", "FooBar"); /// /// public interface IHttpResult { /// /// The Status code of the HttpResult /// HttpStatusCode StatusCode { get; } /// /// Used by the Lambda Annotations framework to serialize the IHttpResult to the correct JSON response. /// /// /// Stream Serialize(HttpResultSerializationOptions options); /// /// Add header to the IHttpResult. The AddHeader method can be called multiple times for the same header to add multi values for a header. /// HTTP header names are case insensitive and the AddHeader method will normalize header name casing by calling ToLower on them. /// /// HTTP header name /// HTTP header value /// The same instance to allow fluent call pattern. IHttpResult AddHeader(string name, string value); } /// /// Implementation class for IHttpResult. Consumers should use one of the static methods to create the a result with the desired status code. /// /// /// If a response body is provided it is format using the following rules: /// /// /// For string then returned as is. /// /// /// For Stream, byte[] or IList<byte> the data is consided binary and base 64 encoded. /// /// /// Anything other type is serialized to JSON. /// /// /// /// /// return HttpResults.Ok("All Good") /// .AddHeader("Custom-Header", "FooBar"); /// /// public class HttpResults : IHttpResult { private const string HEADER_NAME_CONTENT_TYPE = "content-type"; private const string CONTENT_TYPE_APPLICATION_JSON = "application/json"; private const string CONTENT_TYPE_TEXT_PLAIN = "text/plain"; private const string CONTENT_TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream"; private string _body; private IDictionary> _headers; private bool _isBase64Encoded; private string _defaultContentType; private HttpResults(HttpStatusCode statusCode, object body = null) { StatusCode = statusCode; FormatBody(body); } /// public IHttpResult AddHeader(string name, string value) { name = name.ToLower(); if (_headers == null) { _headers = new Dictionary>(); } if (!_headers.TryGetValue(name, out var values)) { values = new List(); _headers[name] = values; } values.Add(value); return this; } /// /// Creates an IHttpResult for a Accepted (202) status code. /// /// Optional response body /// public static IHttpResult Accepted(object body = null) { return new HttpResults(HttpStatusCode.Accepted, body); } /// /// Creates an IHttpResult for a Bad Gateway (502) status code. /// /// public static IHttpResult BadGateway() { return new HttpResults(HttpStatusCode.BadGateway); } /// /// Creates an IHttpResult for a BadRequest (400) status code. /// /// Optional response body /// public static IHttpResult BadRequest(object body = null) { return new HttpResults(HttpStatusCode.BadRequest, body); } /// /// Creates an IHttpResult for a Conflict (409) status code. /// /// Optional response body /// public static IHttpResult Conflict(object body = null) { return new HttpResults(HttpStatusCode.Conflict, body); } /// /// Creates an IHttpResult for a Created (201) status code. /// /// Optional URI for the created resource. The value is set to the Location response header. /// Optional response body /// public static IHttpResult Created(string uri = null, object body = null) { var result = new HttpResults(HttpStatusCode.Created, body); if (uri != null) { result.AddHeader("location", uri); } return result; } /// /// Creates an IHttpResult for a Forbidden (403) status code. /// /// Optional response body /// public static IHttpResult Forbid(object body = null) { return new HttpResults(HttpStatusCode.Forbidden, body); } /// /// Creates an IHttpResult for an Internal Server Error (500) status code. /// /// Optional response body /// public static IHttpResult InternalServerError(object body = null) { return new HttpResults(HttpStatusCode.InternalServerError, body); } /// /// Creates an IHttpResult for a NotFound (404) status code. /// /// Optional response body /// public static IHttpResult NotFound(object body = null) { return new HttpResults(HttpStatusCode.NotFound, body); } /// /// Creates an IHttpResult for a Ok (200) status code. /// /// Optional response body /// public static IHttpResult Ok(object body = null) { return new HttpResults(HttpStatusCode.OK, body); } /// /// Creates an IHttpResult for redirect responses. /// /// /// This method uses the same logic for determing the the Http status code as the Microsoft.AspNetCore.Http.TypedResults.Redirect uses. /// https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.redirect /// /// The URI to redirect to. The value will be set in the location header. /// Whether the redirect should be a permanet (301) or temporary (302) redirect. /// Whether the request method should be preserved. If set to true use 308 for permanent or 307 for temporary redirects. /// public static IHttpResult Redirect(string uri, bool permanent = false, bool preserveMethod = false) { HttpStatusCode code; if (permanent && preserveMethod) { code = (HttpStatusCode)308; // .NET Standard 2.0 does not have the enum value for PermanentRedirect so using direct number; } else if (!permanent && preserveMethod) { code = HttpStatusCode.TemporaryRedirect; } else if (permanent && !preserveMethod) { code = HttpStatusCode.MovedPermanently; } else { code = HttpStatusCode.Redirect; } var result = new HttpResults(code, null); if (uri != null) { result.AddHeader("location", uri); } return result; } /// /// Creates an IHttpResult for a Service Unavailable (503) status code. /// /// Optional number of seconds to return in a Retry-After header /// public static IHttpResult ServiceUnavailable(int? delaySeconds = null) { var result = new HttpResults(HttpStatusCode.ServiceUnavailable); if (delaySeconds != null && delaySeconds > 0) { result.AddHeader("Retry-After", delaySeconds.ToString()); } return result; } /// /// Creates an IHttpResult for a Unauthorized (401) status code. /// /// public static IHttpResult Unauthorized() { return new HttpResults(HttpStatusCode.Unauthorized); } /// /// Creates an IHttpResult for the specified status code. /// /// Http status code used to create the IHttpResult instance. /// Optional response body /// public static IHttpResult NewResult(HttpStatusCode statusCode, object body = null) { return new HttpResults(statusCode, body); } #region Serialization // See comment in class documentation on the rules for serializing. If any changes are made in this method be sure to update // the comment above. private void FormatBody(object body) { // See comment at the top about .NET Standard 2.0 #if NETSTANDARD2_0 throw new NotImplementedException(); #else if (body == null) return; if (body is string str) { _defaultContentType = CONTENT_TYPE_TEXT_PLAIN; _body = str; } else if (body is Stream stream) { _defaultContentType = CONTENT_TYPE_APPLICATION_OCTET_STREAM; _isBase64Encoded = true; var buffer = ArrayPool.Shared.Rent((int)stream.Length); try { var readLength = stream.Read(buffer, 0, buffer.Length); _body = Convert.ToBase64String(buffer, 0, readLength); } finally { ArrayPool.Shared.Return(buffer); } } else if (body is byte[] binaryData) { _defaultContentType = CONTENT_TYPE_APPLICATION_OCTET_STREAM; _isBase64Encoded = true; _body = Convert.ToBase64String(binaryData, 0, binaryData.Length); } else if (body is IList listBinaryData) { _defaultContentType = CONTENT_TYPE_APPLICATION_OCTET_STREAM; _isBase64Encoded = true; _body = Convert.ToBase64String(listBinaryData.ToArray(), 0, listBinaryData.Count); } else { _defaultContentType = CONTENT_TYPE_APPLICATION_JSON; _body = JsonSerializer.Serialize(body); } #endif } /// public HttpStatusCode StatusCode { get; } /// /// Serialize the IHttpResult into the expect format for the event source. /// /// /// public Stream Serialize(HttpResultSerializationOptions options) { // See comment at the top about .NET Standard 2.0 #if NETSTANDARD2_0 throw new NotImplementedException(); #else // If the user didn't explicit set the content type then default to application/json if(!string.IsNullOrEmpty(_body) && (_headers == null || !_headers.ContainsKey(HEADER_NAME_CONTENT_TYPE))) { AddHeader(HEADER_NAME_CONTENT_TYPE, _defaultContentType); } var stream = new MemoryStream(); if (options.Format == HttpResultSerializationOptions.ProtocolFormat.RestApi || (options.Format == HttpResultSerializationOptions.ProtocolFormat.HttpApi && options.Version == HttpResultSerializationOptions.ProtocolVersion.V1)) { var response = new APIGatewayV1Response { StatusCode = (int)StatusCode, Body = _body, MultiValueHeaders = _headers, IsBase64Encoded = _isBase64Encoded }; JsonSerializer.Serialize(stream, response); } else { var response = new APIGatewayV2Response { StatusCode = (int)StatusCode, Body = _body, Headers = ConvertToV2MultiValueHeaders(_headers), IsBase64Encoded = _isBase64Encoded }; JsonSerializer.Serialize(stream, response); } stream.Position = 0; return stream; #endif } /// /// The V2 format used by HttpApi handles multi value headers by having the value be comma delimited. This /// utility method handles converting the collection from the V1 format to the V2. /// /// /// private static IDictionary ConvertToV2MultiValueHeaders(IDictionary> v1MultiHeaders) { if (v1MultiHeaders == null) return null; var v2MultiHeaders = new Dictionary(); foreach (var kvp in v1MultiHeaders) { var values = string.Join(",", kvp.Value); v2MultiHeaders[kvp.Key] = values; } return v2MultiHeaders; } // See comment at the top about .NET Standard 2.0 #if !NETSTANDARD2_0 // Class representing the V1 API Gateway response. Very similiar to Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse but this library can // not take a dependency on Amazon.Lambda.APIGatewayEvents so it has to have its own version. private class APIGatewayV1Response { [JsonPropertyName("statusCode")] public int StatusCode { get; set; } [JsonPropertyName("multiValueHeaders")] public IDictionary> MultiValueHeaders { get; set; } [JsonPropertyName("body")] public string Body { get; set; } [JsonPropertyName("isBase64Encoded")] public bool IsBase64Encoded { get; set; } } // Class representing the V2 API Gateway response. Very similiar to Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse but this library can // not take a dependency on Amazon.Lambda.APIGatewayEvents so it has to have its own version. private class APIGatewayV2Response { [JsonPropertyName("statusCode")] public int StatusCode { get; set; } [JsonPropertyName("headers")] public IDictionary Headers { get; set; } public string[] Cookies { get; set; } [JsonPropertyName("body")] public string Body { get; set; } [JsonPropertyName("isBase64Encoded")] public bool IsBase64Encoded { get; set; } } #endif #endregion } }