From b7089a3740b7c4543ffc694f71348b42d4726131 Mon Sep 17 00:00:00 2001 From: Fabio Cavalcante Date: Thu, 11 Feb 2021 20:21:47 -0800 Subject: [PATCH] Implementing HTTP request data representation --- release_notes.md | 3 +- .../ServiceCollectionExtensions.cs | 4 +- ...xactMatchConverter.cs => TypeConverter.cs} | 8 ++- src/DotNetWorker/DefaultHostRequestHandler.cs | 12 +++- src/DotNetWorker/Grpc/GrpcHttpRequestData.cs | 72 +++++++++++++++++++ src/DotNetWorker/Grpc/RpcHttpCookie.cs | 26 +++++++ src/DotNetWorker/Http/GrpcHttpRequestData.cs | 27 ------- .../Http/HttpHeadersCollection.cs | 32 +++++++++ src/DotNetWorker/Http/HttpRequestData.cs | 40 +++++++++-- .../Http/HttpRequestDataExtensions.cs | 31 ++++++++ src/DotNetWorker/Http/IHttpCookie.cs | 28 ++++++++ src/DotNetWorker/Http/SameSite.cs | 13 ++++ .../protobuf/src/proto/FunctionRpc.proto | 60 +++++++++++++++- .../proto/identity/ClaimsIdentityRpc.proto | 2 +- .../src/proto/shared/NullableTypes.proto | 2 +- 15 files changed, 315 insertions(+), 45 deletions(-) rename src/DotNetWorker/Converters/{ExactMatchConverter.cs => TypeConverter.cs} (67%) create mode 100644 src/DotNetWorker/Grpc/GrpcHttpRequestData.cs create mode 100644 src/DotNetWorker/Grpc/RpcHttpCookie.cs delete mode 100644 src/DotNetWorker/Http/GrpcHttpRequestData.cs create mode 100644 src/DotNetWorker/Http/HttpHeadersCollection.cs create mode 100644 src/DotNetWorker/Http/HttpRequestDataExtensions.cs create mode 100644 src/DotNetWorker/Http/IHttpCookie.cs create mode 100644 src/DotNetWorker/Http/SameSite.cs diff --git a/release_notes.md b/release_notes.md index 1d271ba6f..9c82687e7 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,4 +1,5 @@ ### Release notes \ No newline at end of file +--> +- Enhancements to the HttpRequestData type and APIs (#120) \ No newline at end of file diff --git a/src/DotNetWorker/Configuration/ServiceCollectionExtensions.cs b/src/DotNetWorker/Configuration/ServiceCollectionExtensions.cs index 8f86195e2..3401f9b2c 100644 --- a/src/DotNetWorker/Configuration/ServiceCollectionExtensions.cs +++ b/src/DotNetWorker/Configuration/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -74,7 +74,7 @@ public static IFunctionsWorkerApplicationBuilder AddFunctionsWorker(this IServic internal static IServiceCollection RegisterDefaultConverters(this IServiceCollection services) { return services.AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton(); } diff --git a/src/DotNetWorker/Converters/ExactMatchConverter.cs b/src/DotNetWorker/Converters/TypeConverter.cs similarity index 67% rename from src/DotNetWorker/Converters/ExactMatchConverter.cs rename to src/DotNetWorker/Converters/TypeConverter.cs index 077c69111..a283c635d 100644 --- a/src/DotNetWorker/Converters/ExactMatchConverter.cs +++ b/src/DotNetWorker/Converters/TypeConverter.cs @@ -1,13 +1,17 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System; + namespace Microsoft.Azure.Functions.Worker.Converters { - internal class ExactMatchConverter : IConverter + internal class TypeConverter : IConverter { public bool TryConvert(ConverterContext context, out object? target) { - if (context.Source?.GetType() == context.Parameter.Type) + Type? sourceType = context.Source?.GetType(); + if (sourceType is not null && + context.Parameter.Type.IsAssignableFrom(sourceType)) { target = context.Source; return true; diff --git a/src/DotNetWorker/DefaultHostRequestHandler.cs b/src/DotNetWorker/DefaultHostRequestHandler.cs index 597bde4d4..37e3708b9 100644 --- a/src/DotNetWorker/DefaultHostRequestHandler.cs +++ b/src/DotNetWorker/DefaultHostRequestHandler.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -20,11 +20,19 @@ public DefaultHostRequestHandler(IFunctionBroker functionBroker) public Task InitializeWorkerAsync(WorkerInitRequest request) { - var response = new WorkerInitResponse + var response = new WorkerInitResponse() { Result = new StatusResult { Status = Status.Success } }; + + response.Capabilities.Add("RpcHttpBodyOnly", bool.TrueString); + response.Capabilities.Add("RawHttpBodyBytes", bool.TrueString); + response.Capabilities.Add("RpcHttpTriggerMetadataRemoved", bool.TrueString); + response.Capabilities.Add("UseNullableValueDictionaryForHttp", bool.TrueString); + + response.WorkerVersion = typeof(DefaultHostRequestHandler).Assembly.GetName().Version?.ToString(); + return Task.FromResult(response); } diff --git a/src/DotNetWorker/Grpc/GrpcHttpRequestData.cs b/src/DotNetWorker/Grpc/GrpcHttpRequestData.cs new file mode 100644 index 000000000..cd4db778e --- /dev/null +++ b/src/DotNetWorker/Grpc/GrpcHttpRequestData.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Azure.WebJobs.Script.Grpc.Messages; + +namespace Microsoft.Azure.Functions.Worker +{ + internal class GrpcHttpRequestData : HttpRequestData + { + private readonly RpcHttp _httpData; + private Uri? _url; + private IEnumerable _identities; + private HttpHeadersCollection _headers; + + public GrpcHttpRequestData(RpcHttp httpData) + { + _httpData = httpData ?? throw new ArgumentNullException(nameof(httpData)); + } + + public override ReadOnlyMemory? Body + { + get + { + if (_httpData.Body is null) + { + return null; + } + + // Based on the advertised worker capabilities, the payload should always be binary data + if (_httpData.Body.DataCase != TypedData.DataOneofCase.Bytes) + { + throw new NotSupportedException($"{nameof(GrpcHttpRequestData)} expects binary data only. The provided data type was '{_httpData.Body.DataCase}'."); + } + + return _httpData.Body.Bytes.Memory; + } + } + + public override HttpHeadersCollection Headers => _headers ??= new HttpHeadersCollection(_httpData.NullableHeaders.Select(h => new KeyValuePair(h.Key, h.Value.Value))); + + public override IReadOnlyCollection Cookies => _httpData.Cookies; + + public override Uri Url => _url ??= new Uri(_httpData.Url); + + public override IEnumerable Identities + { + get + { + if (_identities is null) + { + _identities = _httpData.Identities?.Select(id => + { + var identity = new ClaimsIdentity(id.AuthenticationType.Value, id.NameClaimType.Value, id.RoleClaimType.Value); + identity.AddClaims(id.Claims.Select(c => new Claim(c.Type, c.Value))); + + return identity; + }) ?? Enumerable.Empty(); + + } + + return _identities; + } + } + + public override string Method => _httpData.Method; + } +} diff --git a/src/DotNetWorker/Grpc/RpcHttpCookie.cs b/src/DotNetWorker/Grpc/RpcHttpCookie.cs new file mode 100644 index 000000000..5253c946e --- /dev/null +++ b/src/DotNetWorker/Grpc/RpcHttpCookie.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.Azure.Functions.Worker; + +namespace Microsoft.Azure.WebJobs.Script.Grpc.Messages +{ + public sealed partial class RpcHttpCookie : IHttpCookie + { + string IHttpCookie.Domain => Domain.Value; + + DateTimeOffset? IHttpCookie.Expires => Expires?.Value?.ToDateTimeOffset(); + + bool? IHttpCookie.HttpOnly => HttpOnly?.Value; + + double? IHttpCookie.MaxAge => MaxAge?.Value; + + string? IHttpCookie.Path => Path?.Value; + + SameSite IHttpCookie.SameSite => (SameSite)SameSite; + + bool? IHttpCookie.Secure => Secure?.Value; + } +} diff --git a/src/DotNetWorker/Http/GrpcHttpRequestData.cs b/src/DotNetWorker/Http/GrpcHttpRequestData.cs deleted file mode 100644 index 987c908dc..000000000 --- a/src/DotNetWorker/Http/GrpcHttpRequestData.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -using System.Collections.Immutable; -using Microsoft.Azure.WebJobs.Script.Grpc.Messages; - -namespace Microsoft.Azure.Functions.Worker -{ - internal class GrpcHttpRequestData : HttpRequestData - { - private RpcHttp httpData; - - public GrpcHttpRequestData(RpcHttp httpData) - { - this.httpData = httpData; - } - - public override IImmutableDictionary Headers => httpData.Headers.ToImmutableDictionary(); - - // TODO: Custom body type (BodyContent) - public override string Body => httpData.Body.ToString(); - - public override IImmutableDictionary Params => httpData.Params.ToImmutableDictionary(); - - public override IImmutableDictionary Query => httpData.Query.ToImmutableDictionary(); - } -} diff --git a/src/DotNetWorker/Http/HttpHeadersCollection.cs b/src/DotNetWorker/Http/HttpHeadersCollection.cs new file mode 100644 index 000000000..4395e8e5f --- /dev/null +++ b/src/DotNetWorker/Http/HttpHeadersCollection.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.Http.Headers; + +namespace Microsoft.Azure.Functions.Worker.Http +{ + public sealed class HttpHeadersCollection : HttpHeaders + { + public HttpHeadersCollection() + { + } + + public HttpHeadersCollection(IEnumerable>> headers) + { + foreach (var header in headers) + { + base.Add(header.Key, header.Value); + } + } + + public HttpHeadersCollection(IEnumerable> headers) + { + foreach (var header in headers) + { + base.Add(header.Key, header.Value); + } + } + } +} diff --git a/src/DotNetWorker/Http/HttpRequestData.cs b/src/DotNetWorker/Http/HttpRequestData.cs index 7ca7eaf51..65fc85680 100644 --- a/src/DotNetWorker/Http/HttpRequestData.cs +++ b/src/DotNetWorker/Http/HttpRequestData.cs @@ -1,18 +1,46 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. -using System.Collections.Immutable; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.Azure.Functions.Worker.Http; namespace Microsoft.Azure.Functions.Worker { + /// + /// A representation of the HTTP request sent by the host. + /// public abstract class HttpRequestData { - public abstract IImmutableDictionary Headers { get; } + /// + /// A representation of the HTTP request sent by the host. + /// + public abstract ReadOnlyMemory? Body { get; } - public abstract string Body { get; } + /// + /// Gets an containing the request headers. + /// + public abstract HttpHeadersCollection Headers { get; } - public abstract IImmutableDictionary Params { get; } + /// + /// Gets an containing the request cookies. + /// + public abstract IReadOnlyCollection Cookies { get; } - public abstract IImmutableDictionary Query { get; } + /// + /// Gets the for this request. + /// + public abstract Uri Url { get; } + + /// + /// Gets an containing the request identities. + /// + public abstract IEnumerable Identities { get; } + + /// + /// Gets the HTTP method for this request. + /// + public abstract string Method { get; } } } diff --git a/src/DotNetWorker/Http/HttpRequestDataExtensions.cs b/src/DotNetWorker/Http/HttpRequestDataExtensions.cs new file mode 100644 index 000000000..c06565aba --- /dev/null +++ b/src/DotNetWorker/Http/HttpRequestDataExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Text; + +namespace Microsoft.Azure.Functions.Worker.Http +{ + public static class HttpRequestDataExtensions + { + /// + /// Reads the body payload as a string. + /// + /// The request from which to read. + /// The body content as a string, or null if the request body property is null. + public static string? ReadAsString(this HttpRequestData request) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (request.Body is null) + { + return null; + } + + return Encoding.UTF8.GetString(request.Body.Value.Span); + } + } +} diff --git a/src/DotNetWorker/Http/IHttpCookie.cs b/src/DotNetWorker/Http/IHttpCookie.cs new file mode 100644 index 000000000..cde3968b8 --- /dev/null +++ b/src/DotNetWorker/Http/IHttpCookie.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Azure.Functions.Worker +{ + public interface IHttpCookie + { + string Domain { get; } + + DateTimeOffset? Expires { get; } + + bool? HttpOnly { get; } + + double? MaxAge { get; } + + string Name { get; } + + string? Path { get; } + + SameSite SameSite { get; } + + bool? Secure { get; } + + string Value { get; } + } +} diff --git a/src/DotNetWorker/Http/SameSite.cs b/src/DotNetWorker/Http/SameSite.cs new file mode 100644 index 000000000..815956ade --- /dev/null +++ b/src/DotNetWorker/Http/SameSite.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.Azure.Functions.Worker +{ + public enum SameSite + { + None = 0, + Lax = 1, + Strict = 2, + ExplicitNone = 3, + } +} diff --git a/src/DotNetWorker/protobuf/src/proto/FunctionRpc.proto b/src/DotNetWorker/protobuf/src/proto/FunctionRpc.proto index 3ed1f0586..caf43c470 100644 --- a/src/DotNetWorker/protobuf/src/proto/FunctionRpc.proto +++ b/src/DotNetWorker/protobuf/src/proto/FunctionRpc.proto @@ -71,6 +71,10 @@ message StreamingMessage { FunctionEnvironmentReloadRequest function_environment_reload_request = 25; FunctionEnvironmentReloadResponse function_environment_reload_response = 26; + + // Ask the worker to close any open shared memory resources for a given invocation + CloseSharedMemoryResourcesRequest close_shared_memory_resources_request = 27; + CloseSharedMemoryResourcesResponse close_shared_memory_resources_response = 28; } } @@ -96,6 +100,9 @@ message WorkerInitRequest { // inform worker of supported categories and their levels // i.e. Worker = Verbose, Function.MyFunc = None map log_categories = 3; + + // Full path of worker.config.json location + string worker_directory = 4; } // Worker responds with the result of initializing itself @@ -198,6 +205,17 @@ message FunctionEnvironmentReloadResponse { StatusResult result = 3; } +// Tell the out-of-proc worker to close any shared memory maps it allocated for given invocation +message CloseSharedMemoryResourcesRequest { + repeated string map_names = 1; +} + +// Response from the worker indicating which of the shared memory maps have been successfully closed and which have not been closed +// The key (string) is the map name and the value (bool) is true if it was closed, false if not +message CloseSharedMemoryResourcesResponse { + map close_map_results = 1; +} + // Host tells the worker to load a Function message FunctionLoadRequest { // unique function identifier (avoid name collisions, facilitate reload case) @@ -315,6 +333,34 @@ message TypedData { } } +// Specify which type of data is contained in the shared memory region being read +enum RpcDataType { + unknown = 0; + string = 1; + json = 2; + bytes = 3; + stream = 4; + http = 5; + int = 6; + double = 7; + collection_bytes = 8; + collection_string = 9; + collection_double = 10; + collection_sint64 = 11; +} + +// Used to provide metadata about shared memory region to read data from +message RpcSharedMemory { + // Name of the shared memory map containing data + string name = 1; + // Offset in the shared memory map to start reading data from + int64 offset = 2; + // Number of bytes to read (starting from the offset) + int64 count = 3; + // Final type to which the read data (in bytes) is to be interpreted as + RpcDataType type = 4; +} + // Used to encapsulate collection string message CollectionString { repeated string string = 1; @@ -340,8 +386,13 @@ message ParameterBinding { // Name for the binding string name = 1; - // Data for the binding - TypedData data = 2; + oneof rpc_data { + // Data for the binding + TypedData data = 2; + + // Metadata about the shared memory region to read data from + RpcSharedMemory rpc_shared_memory = 3; + } } // Used to describe a given binding on load @@ -481,4 +532,7 @@ message RpcHttp { TypedData rawBody = 17; repeated RpcClaimsIdentity identities = 18; repeated RpcHttpCookie cookies = 19; -} + map nullable_headers = 20; + map nullable_params = 21; + map nullable_query = 22; +} \ No newline at end of file diff --git a/src/DotNetWorker/protobuf/src/proto/identity/ClaimsIdentityRpc.proto b/src/DotNetWorker/protobuf/src/proto/identity/ClaimsIdentityRpc.proto index c3945bb8a..756640cb4 100644 --- a/src/DotNetWorker/protobuf/src/proto/identity/ClaimsIdentityRpc.proto +++ b/src/DotNetWorker/protobuf/src/proto/identity/ClaimsIdentityRpc.proto @@ -23,4 +23,4 @@ message RpcClaimsIdentity { message RpcClaim { string value = 1; string type = 2; -} +} \ No newline at end of file diff --git a/src/DotNetWorker/protobuf/src/proto/shared/NullableTypes.proto b/src/DotNetWorker/protobuf/src/proto/shared/NullableTypes.proto index 4fb476502..11d25f97b 100644 --- a/src/DotNetWorker/protobuf/src/proto/shared/NullableTypes.proto +++ b/src/DotNetWorker/protobuf/src/proto/shared/NullableTypes.proto @@ -27,4 +27,4 @@ message NullableTimestamp { oneof timestamp { google.protobuf.Timestamp value = 1; } -} +} \ No newline at end of file