From 6d2701dd6378e93e8e77a6a4d656bcd69991df6d Mon Sep 17 00:00:00 2001 From: JialinXin Date: Thu, 29 Apr 2021 12:18:40 +0800 Subject: [PATCH] Web PubSub function bindings sdk. (#20459) * Init version for Web PubSub function bindings * fix warns and dependencies. * fix styling check. * fix typo in readme and minor update. * remove deploy files. * fix and resolve comments * add changelog. * fix bug * refactor and resolve some comments. * resolve a few comments * resolve comments. * add generated api. * Add package description and update changelog * update readme. * update authenticate part * minor improve description. * remove snippet. --- eng/Packages.Data.props | 1 + .../CHANGELOG.md | 7 + .../Directory.Build.props | 7 + ...oft.Azure.WebJobs.Extensions.WebPubSub.sln | 31 ++ .../README.md | 152 ++++++++ ...obs.Extensions.WebPubSub.netstandard2.0.cs | 234 ++++++++++++ .../src/Bindings/ConnectionContext.cs | 50 +++ .../src/Bindings/MessageDataType.cs | 21 ++ .../Bindings/Output/AddConnectionToGroup.cs | 16 + .../src/Bindings/Output/AddUserToGroup.cs | 16 + .../Bindings/Output/BinaryDataExtensions.cs | 42 +++ .../Output/BinaryDataJsonConverter.cs | 54 +++ .../Bindings/Output/CloseClientConnection.cs | 16 + .../Bindings/Output/GrantGroupPermission.cs | 19 + .../Output/RemoveConnectionFromGroup.cs | 16 + .../Output/RemoveUserFromAllGroups.cs | 14 + .../Bindings/Output/RemoveUserFromGroup.cs | 16 + .../Bindings/Output/RevokeGroupPermission.cs | 19 + .../src/Bindings/Output/SendToAll.cs | 19 + .../src/Bindings/Output/SendToConnection.cs | 19 + .../src/Bindings/Output/SendToGroup.cs | 21 ++ .../src/Bindings/Output/SendToUser.cs | 19 + .../src/Bindings/Output/WebPubSubOperation.cs | 25 ++ .../src/Bindings/Response/ConnectResponse.cs | 24 ++ .../src/Bindings/Response/ErrorResponse.cs | 29 ++ .../src/Bindings/Response/MessageResponse.cs | 19 + .../src/Bindings/Response/ServiceResponse.cs | 9 + .../Bindings/Response/WebPubSubErrorCode.cs | 21 ++ .../src/Bindings/WebPubSubAsyncCollector.cs | 47 +++ .../src/Bindings/WebPubSubConnection.cs | 28 ++ .../src/Bindings/WebPubSubEventType.cs | 19 + .../src/Config/WebPubSubConfigProvider.cs | 249 +++++++++++++ .../Config/WebPubSubJobsBuilderExtensions.cs | 33 ++ .../src/Config/WebPubSubOptions.cs | 36 ++ .../src/Constants.cs | 56 +++ ....Azure.WebJobs.Extensions.WebPubSub.csproj | 18 + .../src/Properties/AssemblyInfo.cs | 7 + .../src/Services/IWebPubSubService.cs | 34 ++ .../Protocols/ClientCertificateInfo.cs | 13 + .../Services/Protocols/ConnectEventRequest.cs | 23 ++ .../Protocols/ConnectEventResponse.cs | 22 ++ .../Protocols/DisconnectEventRequest.cs | 13 + .../src/Services/RequestType.cs | 13 + .../src/Services/ServiceConfigParser.cs | 60 ++++ .../src/Services/WebPubSubService.cs | 106 ++++++ .../Trigger/IWebPubSubTriggerDispatcher.cs | 17 + .../src/Trigger/WebPubSubListener.cs | 46 +++ .../src/Trigger/WebPubSubTriggerAttribute.cs | 54 +++ .../src/Trigger/WebPubSubTriggerBinding.cs | 245 +++++++++++++ .../WebPubSubTriggerBindingProvider.cs | 40 +++ .../src/Trigger/WebPubSubTriggerDispatcher.cs | 334 ++++++++++++++++++ .../src/Trigger/WebPubSubTriggerEvent.cs | 36 ++ .../src/Utilities.cs | 145 ++++++++ .../src/WebPubSubAttribute.cs | 19 + .../src/WebPubSubConnectionAttribute.cs | 25 ++ .../src/WebPubSubWebJobsStartup.cs | 17 + .../tests/Common/FakeTypeLocator.cs | 17 + .../tests/Common/TestExtensionConfig.cs | 30 ++ .../tests/Common/TestHelpers.cs | 124 +++++++ .../tests/Common/TestListener.cs | 30 ++ .../tests/Common/TestListenerBase.cs | 9 + .../tests/JObjectTests.cs | 126 +++++++ .../tests/JobHostEndToEndTests.cs | 13 + ....WebJobs.Extensions.WebPubSub.Tests.csproj | 20 ++ .../tests/WebPubSubAsyncCollectorTests.cs | 62 ++++ .../tests/WebPubSubServiceTests.cs | 36 ++ .../tests/WebPubSubTriggerDispatcherTests.cs | 127 +++++++ .../WebPubSubTriggerValueProviderTests.cs | 37 ++ 68 files changed, 3322 insertions(+) create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/CHANGELOG.md create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/Directory.Build.props create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/Microsoft.Azure.WebJobs.Extensions.WebPubSub.sln create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/README.md create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/api/Microsoft.Azure.WebJobs.Extensions.WebPubSub.netstandard2.0.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/ConnectionContext.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/MessageDataType.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/AddConnectionToGroup.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/AddUserToGroup.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/BinaryDataExtensions.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/BinaryDataJsonConverter.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/CloseClientConnection.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/GrantGroupPermission.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RemoveConnectionFromGroup.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RemoveUserFromAllGroups.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RemoveUserFromGroup.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RevokeGroupPermission.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToAll.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToConnection.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToGroup.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToUser.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/WebPubSubOperation.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/ConnectResponse.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/ErrorResponse.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/MessageResponse.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/ServiceResponse.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/WebPubSubErrorCode.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/WebPubSubAsyncCollector.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/WebPubSubConnection.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/WebPubSubEventType.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Config/WebPubSubConfigProvider.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Config/WebPubSubJobsBuilderExtensions.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Config/WebPubSubOptions.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Constants.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Microsoft.Azure.WebJobs.Extensions.WebPubSub.csproj create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Properties/AssemblyInfo.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/IWebPubSubService.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/ClientCertificateInfo.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/ConnectEventRequest.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/ConnectEventResponse.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/DisconnectEventRequest.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/RequestType.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/ServiceConfigParser.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/WebPubSubService.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/IWebPubSubTriggerDispatcher.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubListener.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerAttribute.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerBinding.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerBindingProvider.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerDispatcher.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerEvent.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Utilities.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/WebPubSubAttribute.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/WebPubSubConnectionAttribute.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/WebPubSubWebJobsStartup.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/FakeTypeLocator.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestExtensionConfig.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestHelpers.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestListener.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestListenerBase.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/JObjectTests.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/JobHostEndToEndTests.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests.csproj create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubAsyncCollectorTests.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubServiceTests.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubTriggerDispatcherTests.cs create mode 100644 sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubTriggerValueProviderTests.cs diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props index 1c43302c0b333..c6a8021960da2 100644 --- a/eng/Packages.Data.props +++ b/eng/Packages.Data.props @@ -80,6 +80,7 @@ + diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/CHANGELOG.md b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/CHANGELOG.md new file mode 100644 index 0000000000000..9a5a07edb942f --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/CHANGELOG.md @@ -0,0 +1,7 @@ +# Release History + +## 1.0.0-beta.2 (Unreleased) + +## 1.0.0-beta.1 (2021-04-26) + +- The initial beta release of Microsoft.Azure.WebJobs.Extensions.WebPubSub 1.0.0 \ No newline at end of file diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/Directory.Build.props b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/Directory.Build.props new file mode 100644 index 0000000000000..805ca8beaf230 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/Directory.Build.props @@ -0,0 +1,7 @@ + + + true + + + + diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/Microsoft.Azure.WebJobs.Extensions.WebPubSub.sln b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/Microsoft.Azure.WebJobs.Extensions.WebPubSub.sln new file mode 100644 index 0000000000000..f6d288f1fdd4f --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/Microsoft.Azure.WebJobs.Extensions.WebPubSub.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30803.129 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebJobs.Extensions.WebPubSub", "src\Microsoft.Azure.WebJobs.Extensions.WebPubSub.csproj", "{6CF5F6B5-F8D6-4D92-9AE5-F766B1C83EED}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests", "tests\Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests.csproj", "{89759B66-5CF1-4A86-9D07-D3DA48300AE9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6CF5F6B5-F8D6-4D92-9AE5-F766B1C83EED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CF5F6B5-F8D6-4D92-9AE5-F766B1C83EED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CF5F6B5-F8D6-4D92-9AE5-F766B1C83EED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CF5F6B5-F8D6-4D92-9AE5-F766B1C83EED}.Release|Any CPU.Build.0 = Release|Any CPU + {89759B66-5CF1-4A86-9D07-D3DA48300AE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89759B66-5CF1-4A86-9D07-D3DA48300AE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89759B66-5CF1-4A86-9D07-D3DA48300AE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89759B66-5CF1-4A86-9D07-D3DA48300AE9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AFE7981B-5322-4E4D-BACC-3380116A52F5} + EndGlobalSection +EndGlobal diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/README.md b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/README.md new file mode 100644 index 0000000000000..8548d66881c6b --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/README.md @@ -0,0 +1,152 @@ +# Azure WebJobs Web PubSub client library for .NET + +This extension provides functionality for receiving Web PubSub webhook calls in Azure Functions, allowing you to easily write functions that respond to any event published to Web PubSub. + +## Getting started + +### Install the package + +Install the Web PubSub extension with [NuGet][nuget]: + +```Powershell +dotnet add package Microsoft.Azure.WebJobs.Extensions.WebPubSub --prerelease +``` + +### Prerequisites + +You must have an [Azure subscription](https://azure.microsoft.com/free/) and an Azure resource group with a Web PubSub resource. Follow this [step-by-step tutorial](https://review.docs.microsoft.com/azure/azure-web-pubsub/howto-develop-create-instance?branch=release-azure-web-pubsub) to create an Azure Web PubSub instance. + +### Authenticate the client + +In order to let the extension work with Azure Web PubSub service, you will need to provide a valid `ConnectionString`. + +You can find the **Keys** for you Azure Web PubSub service in the [Azure Portal](https://portal.azure.com/). + +The `AzureWebJobsStorage` connection string is used to preserve the processing checkpoint information as required refer to [Storage considerations](https://docs.microsoft.com/azure/azure-functions/storage-considerations#storage-account-requirements) + +For the local development use the `local.settings.json` file to store the connection string, `` can be set to `WebPubSubConnectionString` as default supported in the extension, or you can set customized names by mapping it with `ConnectionStringSetting = ` in function binding attributes: + +```json +{ + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "": "Endpoint=https://.webpubsub.azure.com;AccessKey=;Version=1.0;" + } +} +``` +When deployed use the [application settings](https://docs.microsoft.com/azure/azure-functions/functions-how-to-use-azure-function-app-settings) to set the connection string. + +## Key concepts + +### Using Web PubSub input binding + +Please follow the [input binding tutorial](#functions-that-uses-web-pubsub-input-binding) to learn about using this extension for building `WebPubSubConnection` to create Websockets connection to service with input binding. + +### Using Web PubSub output binding + +Please follow the [output binding tutorial](#functions-that-uses-web-pubsub-output-binding) to learn about using this extension for publishing Web PubSub messages. + +### Using Web PubSub trigger + +Please follow the [trigger binding tutorial](#functions-that-uses-web-pubsub-trigger) to learn about triggering an Azure Function when an event is sent from service upstream. + +In `Connect` and `Message` events, function will respect return values to send back service. Then service will depend on the response to proceed the request or else. The responses and events are paired. For example, `Connect` will only respect `ConnectResponse` or `ErrorResponse`, and ignore other returns. When `ErrorResponse` is returned, service will drop client connection. Please follow the [trigger binding return value tutorial](#functions-that-uses-web-pubsub-trigger-return-value) to learn about using the trigger return value. + +## Examples + +### Functions that uses Web PubSub input binding + +```cs +[FunctionName("WebPubSubInputBindingFunction")] +public static WebPubSubConnection Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req, + [WebPubSubConnection(Hub = "simplechat", UserId = "{query.userid}")] WebPubSubConnection connection) +{ + Console.WriteLine("login"); + return connection; +} +``` + +### Functions that uses Web PubSub output binding + +```cs +[FunctionName("WebPubSubOutputBindingFunction")] +public static async Task RunAsync( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req, + [WebPubSub(Hub = "simplechat")] IAsyncCollector operation) +{ + await operation.AddAsync(new SendToAll + { + Message = BinaryData.FromString("Hello Web PubSub"), + DataType = MessageDataType.Text + }); +} +``` + +### Functions that uses Web PubSub trigger + +```cs +[FunctionName("WebPubSubTriggerFunction")] +public static void Run( + [WebPubSubTrigger("message", WebPubSubEventType.User)] + ConnectionContext context, + string message, + MessageDataType dataType) +{ + Console.WriteLine($"Request from: {context.userId}"); + Console.WriteLine($"Request message: {message}"); + Console.WriteLine($"Request message DataType: {dataType}"); +} +``` + +### Functions that uses Web PubSub trigger return value + +```cs +[FunctionName("WebPubSubTriggerReturnValueFunction")] +public static MessageResponse RunAsync( + [WebPubSubTrigger("message", WebPubSubEventType.User)] ConnectionContext context) +{ + return new MessageResponse + { + Message = BinaryData.FromString("ack"), + DataType = MessageDataType.Text + }; +} +``` + +## Troubleshooting + +Please refer to [Monitor Azure Functions](https://docs.microsoft.com/azure/azure-functions/functions-monitoring) for troubleshooting guidance. + +## Next steps + +Read the [introduction to Azure Function](https://docs.microsoft.com/azure/azure-functions/functions-overview) or [creating an Azure Function guide](https://docs.microsoft.com/azure/azure-functions/functions-create-first-azure-function). + +## Contributing + +See our [CONTRIBUTING.md][contrib] for details on building, +testing, and contributing to this library. + +This project welcomes contributions and suggestions. Most contributions require +you to agree to a Contributor License Agreement (CLA) declaring that you have +the right to, and actually do, grant us the rights to use your contribution. For +details, visit [cla.microsoft.com][cla]. + +This project has adopted the [Microsoft Open Source Code of Conduct][coc]. +For more information see the [Code of Conduct FAQ][coc_faq] +or contact [opencode@microsoft.com][coc_contact] with any +additional questions or comments. + +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-net%2Fsdk%2Fsearch%2FMicrosoft.Azure.WebJobs.Extensions.WebPubSub%2FREADME.png) + + +[source]: https://github.com/Azure/azure-sdk-for-net/tree/master/sdk/search/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src +[package]: https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.WebPubSub/ +[docs]: https://docs.microsoft.com/dotnet/api/Microsoft.Azure.WebJobs.Extensions.WebPubSub +[nuget]: https://www.nuget.org/ + +[contrib]: https://github.com/Azure/azure-sdk-for-net/tree/master/CONTRIBUTING.md +[cla]: https://cla.microsoft.com +[coc]: https://opensource.microsoft.com/codeofconduct/ +[coc_faq]: https://opensource.microsoft.com/codeofconduct/faq/ +[coc_contact]: mailto:opencode@microsoft.com diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/api/Microsoft.Azure.WebJobs.Extensions.WebPubSub.netstandard2.0.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/api/Microsoft.Azure.WebJobs.Extensions.WebPubSub.netstandard2.0.cs new file mode 100644 index 0000000000000..6d1b87d8a4e95 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/api/Microsoft.Azure.WebJobs.Extensions.WebPubSub.netstandard2.0.cs @@ -0,0 +1,234 @@ +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class AddConnectionToGroup : Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubOperation + { + public AddConnectionToGroup() { } + public string ConnectionId { get { throw null; } set { } } + public string Group { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class AddUserToGroup : Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubOperation + { + public AddUserToGroup() { } + public string Group { get { throw null; } set { } } + public string UserId { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class CloseClientConnection : Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubOperation + { + public CloseClientConnection() { } + public string ConnectionId { get { throw null; } set { } } + public string Reason { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class ConnectionContext + { + public ConnectionContext() { } + public string ConnectionId { get { throw null; } } + public string EventName { get { throw null; } } + public Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubEventType EventType { get { throw null; } } + public System.Collections.Generic.Dictionary Headers { get { throw null; } } + public string Hub { get { throw null; } } + public string Signature { get { throw null; } } + public string UserId { get { throw null; } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class ConnectResponse : Microsoft.Azure.WebJobs.Extensions.WebPubSub.ServiceResponse + { + public ConnectResponse() { } + [Newtonsoft.Json.JsonPropertyAttribute(Required=Newtonsoft.Json.Required.Default)] + public string[] Groups { get { throw null; } set { } } + [Newtonsoft.Json.JsonPropertyAttribute(Required=Newtonsoft.Json.Required.Default)] + public string[] Roles { get { throw null; } set { } } + [Newtonsoft.Json.JsonPropertyAttribute(Required=Newtonsoft.Json.Required.Default)] + public string Subprotocol { get { throw null; } set { } } + [Newtonsoft.Json.JsonPropertyAttribute(Required=Newtonsoft.Json.Required.Default)] + public string UserId { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class ErrorResponse : Microsoft.Azure.WebJobs.Extensions.WebPubSub.ServiceResponse + { + [Newtonsoft.Json.JsonConstructorAttribute] + public ErrorResponse() { } + public ErrorResponse(Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubErrorCode code, string message = null) { } + [Newtonsoft.Json.JsonPropertyAttribute(Required=Newtonsoft.Json.Required.Always)] + public Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubErrorCode Code { get { throw null; } set { } } + [Newtonsoft.Json.JsonPropertyAttribute(Required=Newtonsoft.Json.Required.Default)] + public string ErrorMessage { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class GrantGroupPermission : Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubOperation + { + public GrantGroupPermission() { } + public string ConnectionId { get { throw null; } set { } } + public Azure.Messaging.WebPubSub.WebPubSubPermission Permission { get { throw null; } set { } } + public string TargetName { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonConverterAttribute(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public enum MessageDataType + { + [System.Runtime.Serialization.EnumMemberAttribute(Value="binary")] + Binary = 0, + [System.Runtime.Serialization.EnumMemberAttribute(Value="json")] + Json = 1, + [System.Runtime.Serialization.EnumMemberAttribute(Value="text")] + Text = 2, + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class MessageResponse : Microsoft.Azure.WebJobs.Extensions.WebPubSub.ServiceResponse + { + public MessageResponse() { } + public Microsoft.Azure.WebJobs.Extensions.WebPubSub.MessageDataType DataType { get { throw null; } set { } } + [Newtonsoft.Json.JsonPropertyAttribute(Required=Newtonsoft.Json.Required.Always)] + public System.BinaryData Message { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class RemoveConnectionFromGroup : Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubOperation + { + public RemoveConnectionFromGroup() { } + public string ConnectionId { get { throw null; } set { } } + public string Group { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class RemoveUserFromAllGroups : Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubOperation + { + public RemoveUserFromAllGroups() { } + public string UserId { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class RemoveUserFromGroup : Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubOperation + { + public RemoveUserFromGroup() { } + public string Group { get { throw null; } set { } } + public string UserId { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class RevokeGroupPermission : Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubOperation + { + public RevokeGroupPermission() { } + public string ConnectionId { get { throw null; } set { } } + public Azure.Messaging.WebPubSub.WebPubSubPermission Permission { get { throw null; } set { } } + public string TargetName { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class SendToAll : Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubOperation + { + public SendToAll() { } + public Microsoft.Azure.WebJobs.Extensions.WebPubSub.MessageDataType DataType { get { throw null; } set { } } + public string[] Excluded { get { throw null; } set { } } + public System.BinaryData Message { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class SendToConnection : Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubOperation + { + public SendToConnection() { } + public string ConnectionId { get { throw null; } set { } } + public Microsoft.Azure.WebJobs.Extensions.WebPubSub.MessageDataType DataType { get { throw null; } set { } } + public System.BinaryData Message { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class SendToGroup : Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubOperation + { + public SendToGroup() { } + public Microsoft.Azure.WebJobs.Extensions.WebPubSub.MessageDataType DataType { get { throw null; } set { } } + public string[] Excluded { get { throw null; } set { } } + public string Group { get { throw null; } set { } } + public System.BinaryData Message { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class SendToUser : Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubOperation + { + public SendToUser() { } + public Microsoft.Azure.WebJobs.Extensions.WebPubSub.MessageDataType DataType { get { throw null; } set { } } + public System.BinaryData Message { get { throw null; } set { } } + public string UserId { get { throw null; } set { } } + } + public abstract partial class ServiceResponse + { + protected ServiceResponse() { } + } + [Microsoft.Azure.WebJobs.Description.BindingAttribute] + [System.AttributeUsageAttribute(System.AttributeTargets.Parameter | System.AttributeTargets.ReturnValue)] + public partial class WebPubSubAttribute : System.Attribute + { + public WebPubSubAttribute() { } + [Microsoft.Azure.WebJobs.Description.ConnectionStringAttribute] + public string ConnectionStringSetting { get { throw null; } set { } } + [Microsoft.Azure.WebJobs.Description.AutoResolveAttribute] + public string Hub { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public partial class WebPubSubConnection + { + public WebPubSubConnection(System.Uri url) { } + public string AccessToken { get { throw null; } } + public string BaseUrl { get { throw null; } } + public string Url { get { throw null; } } + } + [Microsoft.Azure.WebJobs.Description.BindingAttribute] + [System.AttributeUsageAttribute(System.AttributeTargets.Parameter | System.AttributeTargets.ReturnValue)] + public partial class WebPubSubConnectionAttribute : System.Attribute + { + public WebPubSubConnectionAttribute() { } + [Microsoft.Azure.WebJobs.Description.ConnectionStringAttribute] + public string ConnectionStringSetting { get { throw null; } set { } } + [Microsoft.Azure.WebJobs.Description.AutoResolveAttribute] + public string Hub { get { throw null; } set { } } + [Microsoft.Azure.WebJobs.Description.AutoResolveAttribute] + public string UserId { get { throw null; } set { } } + } + [Newtonsoft.Json.JsonConverterAttribute(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public enum WebPubSubErrorCode + { + [System.Runtime.Serialization.EnumMemberAttribute(Value="unauthorized")] + Unauthorized = 0, + [System.Runtime.Serialization.EnumMemberAttribute(Value="userError")] + UserError = 1, + [System.Runtime.Serialization.EnumMemberAttribute(Value="serverError")] + ServerError = 2, + } + [System.Text.Json.Serialization.JsonConverterAttribute(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public enum WebPubSubEventType + { + [System.Runtime.Serialization.EnumMemberAttribute(Value="system")] + System = 0, + [System.Runtime.Serialization.EnumMemberAttribute(Value="user")] + User = 1, + } + public static partial class WebPubSubJobsBuilderExtensions + { + public static Microsoft.Azure.WebJobs.IWebJobsBuilder AddWebPubSub(this Microsoft.Azure.WebJobs.IWebJobsBuilder builder) { throw null; } + } + [Newtonsoft.Json.JsonObjectAttribute(NamingStrategyType=typeof(Newtonsoft.Json.Serialization.CamelCaseNamingStrategy))] + public abstract partial class WebPubSubOperation + { + protected WebPubSubOperation() { } + public string OperationKind { get { throw null; } set { } } + } + public partial class WebPubSubOptions : Microsoft.Azure.WebJobs.Hosting.IOptionsFormatter + { + public WebPubSubOptions() { } + public string Hub { get { throw null; } set { } } + public string Format() { throw null; } + } + [Microsoft.Azure.WebJobs.Description.BindingAttribute(TriggerHandlesReturnValue=true)] + [System.AttributeUsageAttribute(System.AttributeTargets.Parameter)] + public partial class WebPubSubTriggerAttribute : System.Attribute + { + public WebPubSubTriggerAttribute(Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubEventType eventType, string eventName) { } + public WebPubSubTriggerAttribute(string hub, Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubEventType eventType, string eventName) { } + [Microsoft.Azure.WebJobs.Description.AutoResolveAttribute] + [System.ComponentModel.DataAnnotations.RequiredAttribute] + public string EventName { get { throw null; } } + [Microsoft.Azure.WebJobs.Description.AutoResolveAttribute] + public Microsoft.Azure.WebJobs.Extensions.WebPubSub.WebPubSubEventType EventType { get { throw null; } } + [Microsoft.Azure.WebJobs.Description.AutoResolveAttribute] + public string Hub { get { throw null; } } + } + public partial class WebPubSubWebJobsStartup : Microsoft.Azure.WebJobs.Hosting.IWebJobsStartup + { + public WebPubSubWebJobsStartup() { } + public void Configure(Microsoft.Azure.WebJobs.IWebJobsBuilder builder) { } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/ConnectionContext.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/ConnectionContext.cs new file mode 100644 index 0000000000000..708d0e8c923ca --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/ConnectionContext.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class ConnectionContext + { + /// + /// The type of the message. + /// + public WebPubSubEventType EventType { get; internal set; } + + /// + /// The event name of the message. + /// + public string EventName { get; internal set; } + + /// + /// The hub which the message belongs to. + /// + public string Hub { get; internal set; } + + /// + /// The connection-id of the client which send the message. + /// + public string ConnectionId { get; internal set; } + + /// + /// The user identity of the client which send the message. + /// + public string UserId { get; internal set; } + + /// + /// The signature for validation + /// + public string Signature { get; internal set; } + + /// + /// The headers of request. + /// + public Dictionary Headers { get; internal set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/MessageDataType.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/MessageDataType.cs new file mode 100644 index 0000000000000..680fdfffe1cb9 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/MessageDataType.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.Serialization; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonConverter(typeof(StringEnumConverter))] + public enum MessageDataType + { + [EnumMember(Value = "binary")] + Binary, + [EnumMember(Value = "json")] + Json, + [EnumMember(Value = "text")] + Text + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/AddConnectionToGroup.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/AddConnectionToGroup.cs new file mode 100644 index 0000000000000..de465f53f9a0a --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/AddConnectionToGroup.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class AddConnectionToGroup : WebPubSubOperation + { + public string ConnectionId { get; set; } + + public string Group { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/AddUserToGroup.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/AddUserToGroup.cs new file mode 100644 index 0000000000000..98d6d58e65b98 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/AddUserToGroup.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class AddUserToGroup : WebPubSubOperation + { + public string UserId { get; set; } + + public string Group { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/BinaryDataExtensions.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/BinaryDataExtensions.cs new file mode 100644 index 0000000000000..66a6d41722b93 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/BinaryDataExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal static class BinaryDataExtensions + { + public static object Convert(this BinaryData message, Type targetType) + { + if (targetType == typeof(JObject)) + { + return JObject.FromObject(message.ToArray()); + } + + if (targetType == typeof(Stream)) + { + return message.ToStream(); + } + + if (targetType == typeof(byte[])) + { + return message.ToArray(); + } + + if (targetType == typeof(string)) + { + return message.ToString(); + } + + if (targetType == typeof(BinaryData)) + { + return message; + } + + return null; + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/BinaryDataJsonConverter.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/BinaryDataJsonConverter.cs new file mode 100644 index 0000000000000..e482070658c00 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/BinaryDataJsonConverter.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal class BinaryDataJsonConverter : JsonConverter + { + public override BinaryData ReadJson(JsonReader reader, Type objectType, BinaryData existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String) + { + return BinaryData.FromString(serializer.Deserialize(reader)); + } + + var value = JToken.Load(reader); + + if (TryLoadBinary(value, out var bytes)) + { + return BinaryData.FromBytes(bytes); + } + // string JObject + return BinaryData.FromString(value.ToString()); + } + + public override void WriteJson(JsonWriter writer, BinaryData value, JsonSerializer serializer) + { + serializer.Serialize(writer, value.ToString()); + } + + private static bool TryLoadBinary(JToken input, out byte[] output) + { + if (input["type"] != null) + { + var target = input.ToObject(); + output = target.Data; + return true; + } + output = null; + return false; + } + + private sealed class ArrayBuffer + { + public string Type { get; set; } + public byte[] Data { get; set; } + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/CloseClientConnection.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/CloseClientConnection.cs new file mode 100644 index 0000000000000..25582768a5f34 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/CloseClientConnection.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class CloseClientConnection : WebPubSubOperation + { + public string ConnectionId { get; set; } + + public string Reason { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/GrantGroupPermission.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/GrantGroupPermission.cs new file mode 100644 index 0000000000000..44c5bb85b243c --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/GrantGroupPermission.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Azure.Messaging.WebPubSub; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class GrantGroupPermission : WebPubSubOperation + { + public string ConnectionId { get; set; } + + public WebPubSubPermission Permission { get; set; } + + public string TargetName { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RemoveConnectionFromGroup.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RemoveConnectionFromGroup.cs new file mode 100644 index 0000000000000..8f7d00bf10b1b --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RemoveConnectionFromGroup.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class RemoveConnectionFromGroup : WebPubSubOperation + { + public string ConnectionId { get; set; } + + public string Group { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RemoveUserFromAllGroups.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RemoveUserFromAllGroups.cs new file mode 100644 index 0000000000000..2695fb8b14f87 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RemoveUserFromAllGroups.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class RemoveUserFromAllGroups : WebPubSubOperation + { + public string UserId { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RemoveUserFromGroup.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RemoveUserFromGroup.cs new file mode 100644 index 0000000000000..1eadb87f8be0b --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RemoveUserFromGroup.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class RemoveUserFromGroup : WebPubSubOperation + { + public string UserId { get; set; } + + public string Group { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RevokeGroupPermission.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RevokeGroupPermission.cs new file mode 100644 index 0000000000000..a50b1a0f53e72 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/RevokeGroupPermission.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Azure.Messaging.WebPubSub; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class RevokeGroupPermission : WebPubSubOperation + { + public string ConnectionId { get; set; } + + public WebPubSubPermission Permission { get; set; } + + public string TargetName { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToAll.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToAll.cs new file mode 100644 index 0000000000000..b4bfc7b900f83 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToAll.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class SendToAll : WebPubSubOperation + { + public BinaryData Message { get; set; } + + public MessageDataType DataType { get; set; } = MessageDataType.Binary; + + public string[] Excluded { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToConnection.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToConnection.cs new file mode 100644 index 0000000000000..db8624af74010 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToConnection.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class SendToConnection : WebPubSubOperation + { + public string ConnectionId { get; set; } + + public BinaryData Message { get; set; } + + public MessageDataType DataType { get; set; } = MessageDataType.Binary; + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToGroup.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToGroup.cs new file mode 100644 index 0000000000000..37c0f6c4edab3 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToGroup.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class SendToGroup : WebPubSubOperation + { + public string Group { get; set; } + + public BinaryData Message { get; set; } + + public MessageDataType DataType { get; set; } = MessageDataType.Binary; + + public string[] Excluded { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToUser.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToUser.cs new file mode 100644 index 0000000000000..ca37f6bdfbf90 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/SendToUser.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class SendToUser : WebPubSubOperation + { + public string UserId { get; set; } + + public BinaryData Message { get; set; } + + public MessageDataType DataType { get; set; } = MessageDataType.Binary; + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/WebPubSubOperation.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/WebPubSubOperation.cs new file mode 100644 index 0000000000000..70d99b17fe0bf --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Output/WebPubSubOperation.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public abstract class WebPubSubOperation + { + public string OperationKind + { + get + { + return GetType().Name; + } + set + { + // used in type-less for deserialize. + _ = value; + } + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/ConnectResponse.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/ConnectResponse.cs new file mode 100644 index 0000000000000..769f2dd3b3f2f --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/ConnectResponse.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class ConnectResponse : ServiceResponse + { + [JsonProperty(Required = Required.Default)] + public string UserId { get; set; } + + [JsonProperty(Required = Required.Default)] + public string[] Groups { get; set; } + + [JsonProperty(Required = Required.Default)] + public string Subprotocol { get; set; } + + [JsonProperty(Required = Required.Default)] + public string[] Roles { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/ErrorResponse.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/ErrorResponse.cs new file mode 100644 index 0000000000000..1cdb5ed3cef91 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/ErrorResponse.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class ErrorResponse : ServiceResponse + { + [JsonProperty(Required = Required.Always)] + public WebPubSubErrorCode Code { get; set; } + + [JsonProperty(Required = Required.Default)] + public string ErrorMessage { get; set; } + + public ErrorResponse(WebPubSubErrorCode code, string message = null) + { + Code = code; + ErrorMessage = message; + } + + [JsonConstructor] + public ErrorResponse() + { + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/MessageResponse.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/MessageResponse.cs new file mode 100644 index 0000000000000..04662a2c53b41 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/MessageResponse.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class MessageResponse : ServiceResponse + { + [JsonProperty(Required = Required.Always)] + [JsonConverter(typeof(BinaryDataJsonConverter))] + public BinaryData Message { get; set; } + + public MessageDataType DataType { get; set; } = MessageDataType.Text; + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/ServiceResponse.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/ServiceResponse.cs new file mode 100644 index 0000000000000..a51147948f503 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/ServiceResponse.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + public abstract class ServiceResponse + { + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/WebPubSubErrorCode.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/WebPubSubErrorCode.cs new file mode 100644 index 0000000000000..c1051d6d155bb --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/Response/WebPubSubErrorCode.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.Serialization; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonConverter(typeof(StringEnumConverter))] + public enum WebPubSubErrorCode + { + [EnumMember(Value = "unauthorized")] + Unauthorized, + [EnumMember(Value = "userError")] + UserError, + [EnumMember(Value = "serverError")] + ServerError + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/WebPubSubAsyncCollector.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/WebPubSubAsyncCollector.cs new file mode 100644 index 0000000000000..7242477bbdb61 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/WebPubSubAsyncCollector.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Reflection; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal class WebPubSubAsyncCollector : IAsyncCollector + { + private readonly IWebPubSubService _service; + + internal WebPubSubAsyncCollector(IWebPubSubService service) + { + _service = service; + } + + public async Task AddAsync(WebPubSubOperation item, CancellationToken cancellationToken = default) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + try + { + var method = typeof(IWebPubSubService).GetMethod(item.OperationKind.ToString(), + BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + + var task = (Task)method.Invoke(_service, new object[] { item }); + + await task.ConfigureAwait(false); + } + catch (Exception ex) + { + throw new ArgumentException($"Not supported operation: {item.OperationKind}, exception: {ex}"); + } + } + + public Task FlushAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/WebPubSubConnection.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/WebPubSubConnection.cs new file mode 100644 index 0000000000000..e7e1f06333881 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/WebPubSubConnection.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Web; + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class WebPubSubConnection + { + public WebPubSubConnection(Uri url) + { + Url = url.ToString(); + BaseUrl = $"{url.Scheme}://{url.Authority}{url.AbsolutePath}"; + AccessToken = HttpUtility.ParseQueryString(url.Query)["access_token"]; + } + + public string BaseUrl { get;} + + public string Url { get;} + + public string AccessToken { get;} + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/WebPubSubEventType.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/WebPubSubEventType.cs new file mode 100644 index 0000000000000..74b61a045f2f9 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Bindings/WebPubSubEventType.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +using Newtonsoft.Json.Converters; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [JsonConverter(typeof(StringEnumConverter))] + public enum WebPubSubEventType + { + [EnumMember(Value = "system")] + System, + [EnumMember(Value = "user")] + User, + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Config/WebPubSubConfigProvider.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Config/WebPubSubConfigProvider.cs new file mode 100644 index 0000000000000..541fe080ddc7a --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Config/WebPubSubConfigProvider.cs @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Azure.WebJobs.Description; +using Microsoft.Azure.WebJobs.Host.Config; +using Microsoft.Azure.WebJobs.Logging; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [Extension("WebPubSub", "webpubsub")] + internal class WebPubSubConfigProvider : IExtensionConfigProvider, IAsyncConverter + { + private readonly IConfiguration _configuration; + private readonly INameResolver _nameResolver; + private readonly ILogger _logger; + private readonly WebPubSubOptions _options; + private readonly IWebPubSubTriggerDispatcher _dispatcher; + + public WebPubSubConfigProvider( + IOptions options, + INameResolver nameResolver, + ILoggerFactory loggerFactory, + IConfiguration configuration) + { + _options = options.Value; + _logger = loggerFactory.CreateLogger(LogCategories.CreateTriggerCategory("WebPubSub")); + _nameResolver = nameResolver; + _configuration = configuration; + _dispatcher = new WebPubSubTriggerDispatcher(_logger); + } + + public void Initialize(ExtensionConfigContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(_options.ConnectionString)) + { + _options.ConnectionString = _nameResolver.Resolve(Constants.WebPubSubConnectionStringName); + AddSettings(_options.ConnectionString); + } + + if (string.IsNullOrEmpty(_options.Hub)) + { + _options.Hub = _nameResolver.Resolve(Constants.HubNameStringName); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var url = context.GetWebhookHandler(); +#pragma warning restore CS0618 // Type or member is obsolete + _logger.LogInformation($"Registered Web PubSub negotiate Endpoint = {url?.GetLeftPart(UriPartial.Path)}"); + + // register JsonConverters + RegisterJsonConverter(); + + // bindings + context + .AddConverter(JObject.FromObject) + .AddConverter(ConvertToWebPubSubOperation) + .AddConverter(ConvertToWebPubSubOperationArray); + + // Trigger binding + context.AddBindingRule() + .BindToTrigger(new WebPubSubTriggerBindingProvider(_dispatcher, _options)); + + var webpubsubConnectionAttributeRule = context.AddBindingRule(); + webpubsubConnectionAttributeRule.AddValidator(ValidateWebPubSubConnectionAttributeBinding); + webpubsubConnectionAttributeRule.BindToInput(GetClientConnection); + + var webPubSubAttributeRule = context.AddBindingRule(); + webPubSubAttributeRule.AddValidator(ValidateWebPubSubAttributeBinding); + webPubSubAttributeRule.BindToCollector(CreateCollector); + + _logger.LogInformation("Azure Web PubSub binding initialized"); + } + + public Task ConvertAsync(HttpRequestMessage input, CancellationToken cancellationToken) + { + return _dispatcher.ExecuteAsync(input, _options.AllowedHosts, _options.AccessKeys, cancellationToken); + } + + private void ValidateWebPubSubConnectionAttributeBinding(WebPubSubConnectionAttribute attribute, Type type) + { + ValidateConnectionString( + attribute.ConnectionStringSetting, + $"{nameof(WebPubSubConnectionAttribute)}.{nameof(WebPubSubConnectionAttribute.ConnectionStringSetting)}"); + } + + private void ValidateWebPubSubAttributeBinding(WebPubSubAttribute attribute, Type type) + { + ValidateConnectionString( + attribute.ConnectionStringSetting, + $"{nameof(WebPubSubAttribute)}.{nameof(WebPubSubAttribute.ConnectionStringSetting)}"); + } + + internal WebPubSubService GetService(WebPubSubAttribute attribute) + { + var connectionString = Utilities.FirstOrDefault(attribute.ConnectionStringSetting, _options.ConnectionString); + var hubName = Utilities.FirstOrDefault(attribute.Hub, _options.Hub); + return new WebPubSubService(connectionString, hubName); + } + + private IAsyncCollector CreateCollector(WebPubSubAttribute attribute) + { + return new WebPubSubAsyncCollector(GetService(attribute)); + } + + private WebPubSubConnection GetClientConnection(WebPubSubConnectionAttribute attribute) + { + var hub = Utilities.FirstOrDefault(attribute.Hub, _options.Hub); + var service = new WebPubSubService(attribute.ConnectionStringSetting, hub); + return service.GetClientConnection(attribute.UserId); + } + + private void ValidateConnectionString(string attributeConnectionString, string attributeConnectionStringName) + { + AddSettings(attributeConnectionString); + var connectionString = Utilities.FirstOrDefault(attributeConnectionString, _options.ConnectionString); + + if (string.IsNullOrEmpty(connectionString)) + { + throw new InvalidOperationException($"The Service connection string must be set either via an '{Constants.WebPubSubConnectionStringName}' app setting, via an '{Constants.WebPubSubConnectionStringName}' environment variable, or directly in code via {nameof(WebPubSubOptions)}.{nameof(WebPubSubOptions.ConnectionString)} or {attributeConnectionStringName}."); + } + } + + private void AddSettings(string connectionString) + { + if (!string.IsNullOrEmpty(connectionString)) + { + var item = new ServiceConfigParser(connectionString); + _options.AllowedHosts.Add(item.Endpoint.Host); + _options.AccessKeys.Add(item.AccessKey); + } + } + + internal static void RegisterJsonConverter() + { + JsonConvert.DefaultSettings = () => new JsonSerializerSettings + { + Converters = new List + { + new StringEnumConverter(), + new BinaryDataJsonConverter() + } + }; + } + + internal static WebPubSubOperation ConvertToWebPubSubOperation(JObject input) + { + if (input.TryGetValue("operationKind", StringComparison.OrdinalIgnoreCase, out var kind)) + { + if (kind.ToString().Equals(nameof(SendToAll), StringComparison.OrdinalIgnoreCase)) + { + CheckDataType(input); + return input.ToObject(); + } + else if (kind.ToString().Equals(nameof(SendToConnection), StringComparison.OrdinalIgnoreCase)) + { + CheckDataType(input); + return input.ToObject(); + } + else if (kind.ToString().Equals(nameof(SendToUser), StringComparison.OrdinalIgnoreCase)) + { + CheckDataType(input); + return input.ToObject(); + } + else if (kind.ToString().Equals(nameof(SendToGroup), StringComparison.OrdinalIgnoreCase)) + { + CheckDataType(input); + return input.ToObject(); + } + else if (kind.ToString().Equals(nameof(AddUserToGroup), StringComparison.OrdinalIgnoreCase)) + { + return input.ToObject(); + } + else if (kind.ToString().Equals(nameof(RemoveUserFromGroup), StringComparison.OrdinalIgnoreCase)) + { + return input.ToObject(); + } + else if (kind.ToString().Equals(nameof(RemoveUserFromAllGroups), StringComparison.OrdinalIgnoreCase)) + { + return input.ToObject(); + } + else if (kind.ToString().Equals(nameof(AddConnectionToGroup), StringComparison.OrdinalIgnoreCase)) + { + return input.ToObject(); + } + else if (kind.ToString().Equals(nameof(RemoveConnectionFromGroup), StringComparison.OrdinalIgnoreCase)) + { + return input.ToObject(); + } + else if (kind.ToString().Equals(nameof(CloseClientConnection), StringComparison.OrdinalIgnoreCase)) + { + return input.ToObject(); + } + else if (kind.ToString().Equals(nameof(GrantGroupPermission), StringComparison.OrdinalIgnoreCase)) + { + return input.ToObject(); + } + else if (kind.ToString().Equals(nameof(RevokeGroupPermission), StringComparison.OrdinalIgnoreCase)) + { + return input.ToObject(); + } + } + return input.ToObject(); + } + + internal static WebPubSubOperation[] ConvertToWebPubSubOperationArray(JArray input) + { + var result = new List(); + foreach (var item in input) + { + result.Add(ConvertToWebPubSubOperation((JObject)item)); + } + return result.ToArray(); + } + + // Binary data accepts ArrayBuffer only. + private static void CheckDataType(JObject input) + { + if (input.TryGetValue("dataType", StringComparison.OrdinalIgnoreCase, out var value)) + { + var dataType = value.ToObject(); + + input.TryGetValue("message", StringComparison.OrdinalIgnoreCase, out var message); + + if (dataType == MessageDataType.Binary && + !(message["type"] != null && message["type"].ToString().Equals("Buffer", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException("MessageDataType is binary, please use ArrayBuffer as message type."); + } + } + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Config/WebPubSubJobsBuilderExtensions.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Config/WebPubSubJobsBuilderExtensions.cs new file mode 100644 index 0000000000000..56635b0f83bc8 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Config/WebPubSubJobsBuilderExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + public static class WebPubSubJobsBuilderExtensions + { + public static IWebJobsBuilder AddWebPubSub(this IWebJobsBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddExtension() + .ConfigureOptions(ApplyConfiguration); + return builder; + } + + private static void ApplyConfiguration(IConfiguration config, WebPubSubOptions options) + { + if (config == null) + { + return; + } + + config.Bind(options); + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Config/WebPubSubOptions.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Config/WebPubSubOptions.cs new file mode 100644 index 0000000000000..bf77105d9e979 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Config/WebPubSubOptions.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using Microsoft.Azure.WebJobs.Hosting; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + public class WebPubSubOptions : IOptionsFormatter + { + public string Hub { get; set; } + + internal string ConnectionString { get; set; } + + internal HashSet AllowedHosts { get; set; } = new HashSet(); + + internal HashSet AccessKeys { get; set; } = new HashSet(); + + /// + /// Formats the options as JSON objects for display. + /// + /// Options formatted as JSON. + public string Format() + { + // Not expose ConnectionString in logging. + JObject options = new JObject + { + { nameof(Hub), Hub } + }; + + return options.ToString(Formatting.Indented); + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Constants.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Constants.cs new file mode 100644 index 0000000000000..45492ece15c00 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Constants.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal static class Constants + { + // WebPubSubOptions can be set by customers. + public const string WebPubSubConnectionStringName = "WebPubSubConnectionString"; + public const string HubNameStringName = "WebPubSubHub"; + + public static class ContentTypes + { + public const string JsonContentType = "application/json"; + public const string BinaryContentType = "application/octet-stream"; + public const string PlainTextContentType = "text/plain"; + } + + public static class Events + { + public const string ConnectEvent = "connect"; + public const string ConnectedEvent = "connected"; + public const string MessageEvent = "message"; + public const string DisconnectedEvent = "disconnected"; + } + + public static class Headers + { + public static class CloudEvents + { + private const string Prefix = "ce-"; + public const string Signature = Prefix + "signature"; + public const string Hub = Prefix + "hub"; + public const string ConnectionId = Prefix + "connectionId"; + public const string Id = Prefix + "id"; + public const string Time = Prefix + "time"; + public const string SpecVersion = Prefix + "specversion"; + public const string Type = Prefix + "type"; + public const string Source = Prefix + "source"; + public const string EventName = Prefix + "eventName"; + public const string UserId = Prefix + "userId"; + + public const string TypeSystemPrefix = "azure.webpubsub.sys."; + public const string TypeUserPrefix = "azure.webpubsub.user."; + } + + public const string WebHookRequestOrigin = "WebHook-Request-Origin"; + public const string WebHookAllowedOrigin = "WebHook-Allowed-Origin"; + } + + public class ErrorMessages + { + public const string NotSupportedDataType = "Message only supports text, binary, json. Current value is: "; + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Microsoft.Azure.WebJobs.Extensions.WebPubSub.csproj b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Microsoft.Azure.WebJobs.Extensions.WebPubSub.csproj new file mode 100644 index 0000000000000..41afb714b0087 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Microsoft.Azure.WebJobs.Extensions.WebPubSub.csproj @@ -0,0 +1,18 @@ + + + + $(RequiredTargetFrameworks) + Microsoft.Azure.WebJobs.Extensions.WebPubSub + This Azure Functions extension for Web PubSub + 1.0.0-beta.2 + $(NoWarn);AZC0001;CS1591;SA1636;CA1056 + true + + + + + + + + + diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Properties/AssemblyInfo.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000..d60aea4c5502a --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d15ddcb29688295338af4b7686603fe614abd555e09efba8fb88ee09e1f7b1ccaeed2e8f823fa9eef3fdd60217fc012ea67d2479751a0b8c087a4185541b851bd8b16f8d91b840e51b1cb0ba6fe647997e57429265e85ef62d565db50a69ae1647d54d7bd855e4db3d8a91510e5bcbd0edfbbecaa20a7bd9ae74593daa7b11b4")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] \ No newline at end of file diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/IWebPubSubService.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/IWebPubSubService.cs new file mode 100644 index 0000000000000..2e6ef67bec4fd --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/IWebPubSubService.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal interface IWebPubSubService + { + Task SendToAll(SendToAll webPubSubEvent); + + Task CloseClientConnection(CloseClientConnection webPubSubEvent); + + Task SendToConnection(SendToConnection webPubSubEvent); + + Task SendToGroup(SendToGroup webPubSubEvent); + + Task AddConnectionToGroup(AddConnectionToGroup webPubSubEvent); + + Task RemoveConnectionFromGroup(RemoveConnectionFromGroup webPubSubEvent); + + Task SendToUser(SendToUser webPubSubEvent); + + Task AddUserToGroup(AddUserToGroup webPubSubEvent); + + Task RemoveUserFromGroup(RemoveUserFromGroup webPubSubEvent); + + Task RemoveUserFromAllGroups(RemoveUserFromAllGroups webPubSubEvent); + + Task GrantGroupPermission(GrantGroupPermission webPubSubEvent); + + Task RevokeGroupPermission(RevokeGroupPermission webPubSubEvent); + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/ClientCertificateInfo.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/ClientCertificateInfo.cs new file mode 100644 index 0000000000000..969c808ab2689 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/ClientCertificateInfo.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal sealed class ClientCertificateInfo + { + [JsonProperty("thumbprint")] + public string Thumbprint { get; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/ConnectEventRequest.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/ConnectEventRequest.cs new file mode 100644 index 0000000000000..b40233b6c2bcf --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/ConnectEventRequest.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal sealed class ConnectEventRequest + { + [JsonProperty("claims")] + public IDictionary Claims { get; set; } + + [JsonProperty("query")] + public IDictionary Query { get; set; } + + [JsonProperty("subprotocols")] + public string[] Subprotocols { get; set; } + + [JsonProperty("clientCertificates")] + public ClientCertificateInfo[] ClientCertificates { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/ConnectEventResponse.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/ConnectEventResponse.cs new file mode 100644 index 0000000000000..036545abd572c --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/ConnectEventResponse.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal sealed class ConnectEventResponse + { + [JsonProperty("subprotocol")] + public string Subprotocol { get; set; } + + [JsonProperty("roles")] + public string[] Roles { get; set; } + + [JsonProperty("userId")] + public string UserId { get; set; } + + [JsonProperty("groups")] + public string[] Groups { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/DisconnectEventRequest.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/DisconnectEventRequest.cs new file mode 100644 index 0000000000000..14c88faa3d5ec --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/Protocols/DisconnectEventRequest.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal sealed class DisconnectEventRequest + { + [JsonProperty("reason")] + public string Reason { get; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/RequestType.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/RequestType.cs new file mode 100644 index 0000000000000..b80bdb04315de --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/RequestType.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal enum RequestType + { + Ignored, + Connect, + Disconnect, + User + } +} \ No newline at end of file diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/ServiceConfigParser.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/ServiceConfigParser.cs new file mode 100644 index 0000000000000..981c3de311ac5 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/ServiceConfigParser.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal class ServiceConfigParser + { + private static readonly char[] _valueSeparator = new char[] { '=' }; + private const char _partSeparator = ';'; + + public Uri Endpoint { get; } + + public string AccessKey { get; } + + public string Version { get; } + + public int Port { get; } + + public ServiceConfigParser(string connectionString) + { + var settings = ParseConnectionString(connectionString); + + Endpoint = settings.ContainsKey("endpoint") ? + new Uri(settings["endpoint"]) : + throw new ArgumentException(nameof(Endpoint)); + AccessKey = settings.ContainsKey("accesskey") ? + settings["accesskey"] : + throw new ArgumentException(nameof(AccessKey)); + + Version = settings.ContainsKey("version") ? settings["version"] : null; + Port = settings.ContainsKey("port") ? int.Parse(settings["port"], CultureInfo.InvariantCulture) : 80; + } + + private static Dictionary ParseConnectionString(string connectionString) + { + if (string.IsNullOrEmpty(connectionString)) + { + throw new ArgumentException("Web PubSub Service connection string is empty"); + } + + var setting = new Dictionary(); + var items = connectionString.Split(_partSeparator); + + try + { + setting = items.Where(x => x.Length > 0).ToDictionary(x => x.Split(_valueSeparator, 2)[0], y => y.Split(_valueSeparator, 2)[1], StringComparer.InvariantCultureIgnoreCase); + } + catch (Exception) + { + throw new ArgumentException($"Invalid Web PubSub connection string, please check."); + } + return setting; + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/WebPubSubService.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/WebPubSubService.cs new file mode 100644 index 0000000000000..3a290bb6b208f --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Services/WebPubSubService.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Threading.Tasks; + +using Azure.Core; +using Azure.Messaging.WebPubSub; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal class WebPubSubService : IWebPubSubService + { + private readonly WebPubSubServiceClient _client; + private readonly ServiceConfigParser _serviceConfig; + + public WebPubSubService(string connectionString, string hub) + { + _serviceConfig = new ServiceConfigParser(connectionString); + _client = new WebPubSubServiceClient(connectionString, hub); + } + + internal WebPubSubConnection GetClientConnection(string userId = null, string[] roles = null) + { + var url = _client.GetClientAccessUri(userId, roles); + + #region TODO: Remove after SDK fix. Work-around to support http. + if (!_serviceConfig.Endpoint.Scheme.StartsWith("https", StringComparison.OrdinalIgnoreCase)) + { + var replaced = url.AbsoluteUri.Replace("wss", "ws"); + url = new Uri(replaced); + } + #endregion + + return new WebPubSubConnection(url); + } + + public async Task AddConnectionToGroup(AddConnectionToGroup webPubSubEvent) + { + await _client.AddConnectionToGroupAsync(webPubSubEvent.Group, webPubSubEvent.ConnectionId).ConfigureAwait(false); + } + + public async Task AddUserToGroup(AddUserToGroup webPubSubEvent) + { + await _client.AddUserToGroupAsync(webPubSubEvent.Group, webPubSubEvent.UserId).ConfigureAwait(false); + } + + public async Task CloseClientConnection(CloseClientConnection webPubSubEvent) + { + await _client.CloseClientConnectionAsync(webPubSubEvent.ConnectionId, webPubSubEvent.Reason).ConfigureAwait(false); + } + + public async Task GrantGroupPermission(GrantGroupPermission webPubSubEvent) + { + await _client.GrantPermissionAsync(webPubSubEvent.Permission, webPubSubEvent.ConnectionId).ConfigureAwait(false); + } + + public async Task RemoveConnectionFromGroup(RemoveConnectionFromGroup webPubSubEvent) + { + await _client.RemoveConnectionFromGroupAsync(webPubSubEvent.Group, webPubSubEvent.ConnectionId).ConfigureAwait(false); + } + + public async Task RemoveUserFromAllGroups(RemoveUserFromAllGroups webPubSubEvent) + { + await _client.RemoveUserFromAllGroupsAsync(webPubSubEvent.UserId).ConfigureAwait(false); + } + + public async Task RemoveUserFromGroup(RemoveUserFromGroup webPubSubEvent) + { + await _client.RemoveUserFromGroupAsync(webPubSubEvent.Group, webPubSubEvent.UserId).ConfigureAwait(false); + } + + public async Task RevokeGroupPermission(RevokeGroupPermission webPubSubEvent) + { + await _client.RevokePermissionAsync(webPubSubEvent.Permission, webPubSubEvent.ConnectionId).ConfigureAwait(false); + } + + public async Task SendToAll(SendToAll webPubSubEvent) + { + var content = RequestContent.Create(webPubSubEvent.Message); + var contentType = Utilities.GetContentType(webPubSubEvent.DataType); + await _client.SendToAllAsync(content, contentType, webPubSubEvent.Excluded).ConfigureAwait(false); + } + + public async Task SendToConnection(SendToConnection webPubSubEvent) + { + var content = RequestContent.Create(webPubSubEvent.Message); + var contentType = Utilities.GetContentType(webPubSubEvent.DataType); + await _client.SendToConnectionAsync(webPubSubEvent.ConnectionId, content, contentType).ConfigureAwait(false); + } + + public async Task SendToGroup(SendToGroup webPubSubEvent) + { + var content = RequestContent.Create(webPubSubEvent.Message); + var contentType = Utilities.GetContentType(webPubSubEvent.DataType); + await _client.SendToGroupAsync(webPubSubEvent.Group, content, contentType, webPubSubEvent.Excluded).ConfigureAwait(false); + } + + public async Task SendToUser(SendToUser webPubSubEvent) + { + var content = RequestContent.Create(webPubSubEvent.Message); + var contentType = Utilities.GetContentType(webPubSubEvent.DataType); + await _client.SendToUserAsync(webPubSubEvent.UserId, content, contentType).ConfigureAwait(false); + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/IWebPubSubTriggerDispatcher.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/IWebPubSubTriggerDispatcher.cs new file mode 100644 index 0000000000000..e728b4ef37510 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/IWebPubSubTriggerDispatcher.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal interface IWebPubSubTriggerDispatcher + { + void AddListener(string key, WebPubSubListener listener); + + Task ExecuteAsync(HttpRequestMessage req, HashSet allowedHosts, HashSet AccessTokens, CancellationToken token = default); + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubListener.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubListener.cs new file mode 100644 index 0000000000000..04ae6431079e8 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubListener.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Azure.WebJobs.Host.Executors; +using Microsoft.Azure.WebJobs.Host.Listeners; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal class WebPubSubListener : IListener + { + public ITriggeredFunctionExecutor Executor { get; private set; } + + private readonly string _listenerKey; + private readonly IWebPubSubTriggerDispatcher _dispatcher; + + public WebPubSubListener(ITriggeredFunctionExecutor executor, string listenerKey, IWebPubSubTriggerDispatcher dispatcher) + { + _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + _listenerKey = listenerKey ?? throw new ArgumentNullException(nameof(listenerKey)); + Executor = executor ?? throw new ArgumentNullException(nameof(executor)); + } + + public void Cancel() + { + } + + public void Dispose() + { + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _dispatcher.AddListener(_listenerKey, this); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerAttribute.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerAttribute.cs new file mode 100644 index 0000000000000..9f4b03a2fb8a7 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerAttribute.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.ComponentModel.DataAnnotations; + +using Microsoft.Azure.WebJobs.Description; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [AttributeUsage(AttributeTargets.Parameter)] +#pragma warning disable CS0618 // Type or member is obsolete + [Binding(TriggerHandlesReturnValue = true)] +#pragma warning restore CS0618 // Type or member is obsolete + public class WebPubSubTriggerAttribute : Attribute + { + /// + /// Used to map to method name automatically + /// + /// + /// + /// + public WebPubSubTriggerAttribute(string hub, WebPubSubEventType eventType, string eventName) + { + Hub = hub; + EventName = eventName; + EventType = eventType; + } + + public WebPubSubTriggerAttribute(WebPubSubEventType eventType, string eventName) + : this ("", eventType, eventName) + { + } + + /// + /// The hub of request. + /// + [AutoResolve] + public string Hub { get; } + + /// + /// The event of the request + /// + [Required] + [AutoResolve] + public string EventName { get; } + + /// + /// The event type, allowed value is system or user + /// + [AutoResolve] + public WebPubSubEventType EventType { get; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerBinding.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerBinding.cs new file mode 100644 index 0000000000000..a89a1d6ea4a39 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerBinding.cs @@ -0,0 +1,245 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Azure.WebJobs.Host.Bindings; +using Microsoft.Azure.WebJobs.Host.Listeners; +using Microsoft.Azure.WebJobs.Host.Protocols; +using Microsoft.Azure.WebJobs.Host.Triggers; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal class WebPubSubTriggerBinding : ITriggerBinding + { + private readonly ParameterInfo _parameterInfo; + private readonly WebPubSubTriggerAttribute _attribute; + private readonly IWebPubSubTriggerDispatcher _dispatcher; + private readonly WebPubSubOptions _options; + + public WebPubSubTriggerBinding(ParameterInfo parameterInfo, WebPubSubTriggerAttribute attribute, WebPubSubOptions options, IWebPubSubTriggerDispatcher dispatcher) + { + _parameterInfo = parameterInfo ?? throw new ArgumentNullException(nameof(parameterInfo)); + _attribute = attribute ?? throw new ArgumentNullException(nameof(attribute)); + _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + + BindingDataContract = CreateBindingContract(parameterInfo); + } + + public Type TriggerValueType => typeof(WebPubSubTriggerEvent); + + public IReadOnlyDictionary BindingDataContract { get; } + + public Task BindAsync(object value, ValueBindingContext context) + { + var bindingData = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (value is WebPubSubTriggerEvent triggerEvent) + { + AddBindingData(bindingData, triggerEvent); + + return Task.FromResult(new TriggerData(new WebPubSubTriggerValueProvider(_parameterInfo, triggerEvent), bindingData) + { + ReturnValueProvider = new TriggerReturnValueProvider(triggerEvent.TaskCompletionSource), + }); + } + + return Task.FromResult(null); + } + + public Task CreateListenerAsync(ListenerFactoryContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Get listener key from attributes. + var hub = Utilities.FirstOrDefault(_attribute.Hub, _options.Hub); + if (string.IsNullOrEmpty(hub)) + { + throw new ArgumentException("Hub name should be configured in either attribute or appsettings."); + } + var attributeName = $"{hub}.{_attribute.EventType}.{_attribute.EventName}"; + var listernerKey = attributeName; + + return Task.FromResult(new WebPubSubListener(context.Executor, listernerKey, _dispatcher)); + } + + public ParameterDescriptor ToParameterDescriptor() + { + return new ParameterDescriptor + { + Name = _parameterInfo.Name, + }; + } + + private static void AddBindingData(Dictionary bindingData, WebPubSubTriggerEvent triggerEvent) + { + bindingData.Add(nameof(triggerEvent.ConnectionContext), triggerEvent.ConnectionContext); + bindingData.Add(nameof(triggerEvent.Message), triggerEvent.Message); + bindingData.Add(nameof(triggerEvent.DataType), triggerEvent.DataType); + bindingData.Add(nameof(triggerEvent.Claims), triggerEvent.Claims); + bindingData.Add(nameof(triggerEvent.Query), triggerEvent.Query); + bindingData.Add(nameof(triggerEvent.Reason), triggerEvent.Reason); + bindingData.Add(nameof(triggerEvent.Subprotocols), triggerEvent.Subprotocols); + bindingData.Add(nameof(triggerEvent.ClientCertificates), triggerEvent.ClientCertificates); + } + + /// + /// Defined what other bindings can use and return value. + /// + private static IReadOnlyDictionary CreateBindingContract(ParameterInfo parameterInfo) + { + var contract = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "$return", typeof(object).MakeByRefType() }, + }; + + contract.Add(parameterInfo.Name, parameterInfo.ParameterType); + SafeAddContract(() => contract.Add("ConnectionContext", parameterInfo.ParameterType)); + SafeAddContract(() => contract.Add("Message", typeof(BinaryData))); + SafeAddContract(() => contract.Add("DataType", typeof(MessageDataType))); + SafeAddContract(() => contract.Add("Claims", typeof(IDictionary))); + SafeAddContract(() => contract.Add("Query", typeof(IDictionary))); + SafeAddContract(() => contract.Add("Reason", typeof(string))); + SafeAddContract(() => contract.Add("Subprotocols", typeof(string[]))); + SafeAddContract(() => contract.Add("ClientCertificates", typeof(ClientCertificateInfo[]))); + + return contract; + } + + private static void SafeAddContract(Action addValue) + { + try + { + addValue(); + } + catch + { + // ignore dup + } + } + + /// + /// A provider that responsible for providing value in various type to be bond to function method parameter. + /// + internal class WebPubSubTriggerValueProvider : IValueBinder + { + private readonly ParameterInfo _parameter; + private readonly WebPubSubTriggerEvent _triggerEvent; + + public WebPubSubTriggerValueProvider(ParameterInfo parameter, WebPubSubTriggerEvent triggerEvent) + { + _parameter = parameter ?? throw new ArgumentNullException(nameof(parameter)); + _triggerEvent = triggerEvent ?? throw new ArgumentNullException(nameof(triggerEvent)); + } + + public Task GetValueAsync() + { + // Bind un-restrict name to default ConnectionContext with type recognized. + if (_parameter.ParameterType == typeof(ConnectionContext)) + { + return Task.FromResult(_triggerEvent.ConnectionContext); + } + + // Bind rest with name and type repected. + return Task.FromResult(GetValueByName(_parameter.Name, _parameter.ParameterType)); + } + + public string ToInvokeString() + { + return _parameter.Name; + } + + public Type Type => _parameter.ParameterType; + + // No use here + public Task SetValueAsync(object value, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + private object GetValueByName(string parameterName, Type targetType) + { + var property = Utilities.GetProperty(typeof(WebPubSubTriggerEvent), parameterName); + if (property != null) + { + var value = property.GetValue(_triggerEvent); + if (value == null || value.GetType() == targetType) + { + return value; + } + return ConvertTypeIfPossible(value, targetType); + } + return null; + } + + private static object ConvertTypeIfPossible(object source, Type target) + { + if (source is BinaryData message) + { + return message.Convert(target); + } + if (target == typeof(JObject)) + { + return JToken.FromObject(source); + } + if (target == typeof(string)) + { + return JToken.FromObject(source).ToString(); + } + if (target == typeof(byte[])) + { + return Encoding.UTF8.GetBytes(JToken.FromObject(source).ToString()); + } + if (target == typeof(Stream)) + { + return new MemoryStream(Encoding.UTF8.GetBytes(JToken.FromObject(source).ToString())); + } + return null; + } + } + + /// + /// A provider to handle return value. + /// + internal class TriggerReturnValueProvider : IValueBinder + { + private readonly TaskCompletionSource _tcs; + + public TriggerReturnValueProvider(TaskCompletionSource tcs) + { + _tcs = tcs; + } + + public Task GetValueAsync() + { + // Useless for return value provider + return null; + } + + public string ToInvokeString() + { + // Useless for return value provider + return string.Empty; + } + + public Type Type => typeof(object).MakeByRefType(); + + public Task SetValueAsync(object value, CancellationToken cancellationToken) + { + _tcs.TrySetResult(value); + return Task.CompletedTask; + } + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerBindingProvider.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerBindingProvider.cs new file mode 100644 index 0000000000000..8cd1c7e0cd09f --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerBindingProvider.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Threading.Tasks; + +using Microsoft.Azure.WebJobs.Host.Triggers; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal class WebPubSubTriggerBindingProvider : ITriggerBindingProvider + { + private readonly IWebPubSubTriggerDispatcher _dispatcher; + private readonly WebPubSubOptions _options; + + public WebPubSubTriggerBindingProvider(IWebPubSubTriggerDispatcher dispatcher, WebPubSubOptions options) + { + _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public Task TryCreateAsync(TriggerBindingProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var parameterInfo = context.Parameter; + var attribute = parameterInfo.GetCustomAttribute(false); + if (attribute == null) + { + return Task.FromResult(null); + } + + return Task.FromResult(new WebPubSubTriggerBinding(parameterInfo, attribute, _options, _dispatcher)); + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerDispatcher.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerDispatcher.cs new file mode 100644 index 0000000000000..dc7fb474f929c --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerDispatcher.cs @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Azure.WebJobs.Host.Executors; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal class WebPubSubTriggerDispatcher : IWebPubSubTriggerDispatcher + { + private readonly Dictionary _listeners = new(StringComparer.InvariantCultureIgnoreCase); + private readonly ILogger _logger; + + public WebPubSubTriggerDispatcher(ILogger logger) + { + _logger = logger; + } + + public void AddListener(string key, WebPubSubListener listener) + { + if (_listeners.ContainsKey(key)) + { + throw new ArgumentException($"Duplicated binding attribute find: {string.Join(",", key.Split('.'))}"); + } + _listeners.Add(key, listener); + } + + public async Task ExecuteAsync(HttpRequestMessage req, + HashSet allowedHosts, + HashSet accessKeys, + CancellationToken token = default) + { + // Handle service abuse check. + if (RespondToServiceAbuseCheck(req, allowedHosts, out var abuseResponse)) + { + return abuseResponse; + } + + if (!TryParseRequest(req, out var context)) + { + return new HttpResponseMessage(HttpStatusCode.BadRequest); + } + + if (!ValidateSignature(context.ConnectionId, context.Signature, accessKeys)) + { + return new HttpResponseMessage(HttpStatusCode.Unauthorized); + } + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var function = GetFunctionName(context); + + if (_listeners.TryGetValue(function, out var executor)) + { + BinaryData message = null; + MessageDataType dataType = MessageDataType.Text; + IDictionary claims = null; + IDictionary query = null; + string[] subprotocols = null; + ClientCertificateInfo[] certificates = null; + string reason = null; + + var requestType = Utilities.GetRequestType(context.EventType, context.EventName); + switch (requestType) + { + case RequestType.Connect: + { + var content = await req.Content.ReadAsStringAsync().ConfigureAwait(false); + var request = JsonConvert.DeserializeObject(content); + claims = request.Claims; + subprotocols = request.Subprotocols; + query = request.Query; + certificates = request.ClientCertificates; + break; + } + case RequestType.Disconnect: + { + var content = await req.Content.ReadAsStringAsync().ConfigureAwait(false); + var request = JsonConvert.DeserializeObject(content); + reason = request.Reason; + break; + } + case RequestType.User: + { + if (!ValidateContentType(req.Content.Headers.ContentType.MediaType, out dataType)) + { + return new HttpResponseMessage(HttpStatusCode.BadRequest) + { + Content = new StringContent($"{Constants.ErrorMessages.NotSupportedDataType}{req.Content.Headers.ContentType.MediaType}") + }; + } + + var payload = await req.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + message = BinaryData.FromBytes(payload); + break; + } + default: + break; + } + + var triggerEvent = new WebPubSubTriggerEvent + { + ConnectionContext = context, + Message = message, + DataType = dataType, + Claims = claims, + Query = query, + Subprotocols = subprotocols, + ClientCertificates = certificates, + Reason = reason, + TaskCompletionSource = tcs + }; + await executor.Executor.TryExecuteAsync(new TriggeredFunctionData + { + TriggerValue = triggerEvent + }, token).ConfigureAwait(false); + + // After function processed, return on-hold event reponses. + if (requestType == RequestType.Connect || requestType == RequestType.User) + { + try + { + using (token.Register(() => tcs.TrySetCanceled())) + { + var response = await tcs.Task.ConfigureAwait(false); + + // Skip no returns + if (response != null) + { + var validResponse = BuildValidResponse(response, requestType); + + if (validResponse != null) + { + return validResponse; + } + _logger.LogWarning($"Invalid response type {response.GetType()} regarding current request: {requestType}"); + } + } + } + catch (Exception ex) + { + var error = new ErrorResponse(WebPubSubErrorCode.ServerError, ex.Message); + return Utilities.BuildErrorResponse(error); + } + } + + return new HttpResponseMessage(HttpStatusCode.OK); + } + // No function map to current request + return new HttpResponseMessage(HttpStatusCode.NotFound); + } + + private static bool TryParseRequest(HttpRequestMessage request, out ConnectionContext context) + { + // ConnectionId is required in upstream request, and method is POST. + if (!request.Headers.Contains(Constants.Headers.CloudEvents.ConnectionId) + || request.Method != HttpMethod.Post) + { + context = null; + return false; + } + + context = new ConnectionContext(); + try + { + context.ConnectionId = request.Headers.GetValues(Constants.Headers.CloudEvents.ConnectionId).SingleOrDefault(); + context.Hub = request.Headers.GetValues(Constants.Headers.CloudEvents.Hub).SingleOrDefault(); + context.EventType = Utilities.GetEventType(request.Headers.GetValues(Constants.Headers.CloudEvents.Type).SingleOrDefault()); + context.EventName = request.Headers.GetValues(Constants.Headers.CloudEvents.EventName).SingleOrDefault(); + context.Signature = request.Headers.GetValues(Constants.Headers.CloudEvents.Signature).SingleOrDefault(); + context.Headers = request.Headers.ToDictionary(x => x.Key, v => new StringValues(v.Value.ToArray()), StringComparer.OrdinalIgnoreCase); + + // UserId is optional, e.g. connect + if (request.Headers.TryGetValues(Constants.Headers.CloudEvents.UserId, out var values)) + { + context.UserId = values.SingleOrDefault(); + } + } + catch (Exception) + { + return false; + } + + return true; + } + + private static bool ValidateSignature(string connectionId, string signature, HashSet accessKeys) + { + foreach (var accessKey in accessKeys) + { + var signatures = Utilities.GetSignatureList(signature); + if (signatures == null) + { + continue; + } + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(accessKey)); + var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(connectionId)); + var hash = "sha256=" + BitConverter.ToString(hashBytes).Replace("-", ""); + if (signatures.Contains(hash, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + else + { + continue; + } + } + return false; + } + + private static bool ValidateContentType(string mediaType, out MessageDataType dataType) + { + try + { + dataType = Utilities.GetDataType(mediaType); + return true; + } + catch (Exception) + { + dataType = MessageDataType.Binary; + return false; + } + } + + private static string GetFunctionName(ConnectionContext context) + { + return $"{context.Hub}.{context.EventType}.{context.EventName}"; + } + + private static bool RespondToServiceAbuseCheck(HttpRequestMessage req, HashSet allowedHosts, out HttpResponseMessage response) + { + response = new HttpResponseMessage(); + // TODO: remove Get when function core is fully supported and AWPS service is updated. + if (req.Method == HttpMethod.Options || req.Method == HttpMethod.Get) + { + var hosts = req.Headers.GetValues(Constants.Headers.WebHookRequestOrigin); + if (hosts != null && hosts.Any()) + { + foreach (var item in allowedHosts) + { + if (hosts.Contains(item)) + { + response.Headers.Add(Constants.Headers.WebHookAllowedOrigin, hosts); + return true; + } + } + response.StatusCode = HttpStatusCode.BadRequest; + } + return true; + } + return false; + } + + private static bool TryConvertResponse(JObject item, out T response) + { + try + { + response = item.ToObject(); + return true; + } + catch (JsonSerializationException) + { + // ignore invalid response + } + response = default; + return false; + } + + internal static HttpResponseMessage BuildValidResponse(object response, RequestType requestType) + { + JObject converted = null; + bool needConvert = false; + if (response is JObject jObject) + { + converted = jObject; + needConvert = true; + } + else if (response is string str) + { + converted = JObject.Parse(str); + needConvert = true; + } + + // Check error + if (needConvert && TryConvertResponse(converted, out ErrorResponse error)) + { + return Utilities.BuildErrorResponse(error); + } + else if (response is ErrorResponse errorResponse) + { + return Utilities.BuildErrorResponse(errorResponse); + } + + if (requestType == RequestType.Connect) + { + if (needConvert) + { + return Utilities.BuildResponse(converted.ToString()); + } + else if (response is ConnectResponse connectResponse) + { + return Utilities.BuildResponse(connectResponse); + } + } + + if (requestType == RequestType.User) + { + if (needConvert && TryConvertResponse(converted, out MessageResponse msgResponse)) + { + return Utilities.BuildResponse(msgResponse); + } + else if (response is MessageResponse messageResponse) + { + return Utilities.BuildResponse(messageResponse); + } + } + + return null; + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerEvent.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerEvent.cs new file mode 100644 index 0000000000000..8b973c70e00f4 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Trigger/WebPubSubTriggerEvent.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal class WebPubSubTriggerEvent + { + /// + /// Web PubSub common request context from cloud event headers. + /// + public ConnectionContext ConnectionContext { get; set; } + + public BinaryData Message { get; set; } + + public MessageDataType DataType { get; set; } + + public string[] Subprotocols { get; set; } + + public IDictionary Claims { get; set; } + + public IDictionary Query { get; set; } + + public ClientCertificateInfo[] ClientCertificates { get; set; } + + public string Reason { get; set; } + + /// + /// A TaskCompletionSource will set result when the function invocation has finished. + /// + public TaskCompletionSource TaskCompletionSource { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Utilities.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Utilities.cs new file mode 100644 index 0000000000000..6ede0caf544bd --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Utilities.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; +using Newtonsoft.Json; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + internal static class Utilities + { + private static readonly char[] HeaderSeparator = { ',' }; + + public static MediaTypeHeaderValue GetMediaType(MessageDataType dataType) => new MediaTypeHeaderValue(GetContentType(dataType)); + + public static string GetContentType(MessageDataType dataType) => + dataType switch + { + MessageDataType.Binary => Constants.ContentTypes.BinaryContentType, + MessageDataType.Text => Constants.ContentTypes.PlainTextContentType, + MessageDataType.Json => Constants.ContentTypes.JsonContentType, + // Default set binary type to align with service side logic + _ => Constants.ContentTypes.BinaryContentType + }; + + public static MessageDataType GetDataType(string mediaType) => + mediaType switch + { + Constants.ContentTypes.BinaryContentType => MessageDataType.Binary, + Constants.ContentTypes.JsonContentType => MessageDataType.Json, + Constants.ContentTypes.PlainTextContentType => MessageDataType.Text, + _ => throw new ArgumentException($"{Constants.ErrorMessages.NotSupportedDataType}{mediaType}") + }; + + public static WebPubSubEventType GetEventType(string ceType) + { + return ceType.StartsWith(Constants.Headers.CloudEvents.TypeSystemPrefix, StringComparison.OrdinalIgnoreCase) ? + WebPubSubEventType.System : + WebPubSubEventType.User; + } + + public static HttpResponseMessage BuildResponse(MessageResponse response) + { + HttpResponseMessage result = new HttpResponseMessage(); + + if (response.Message != null) + { + result.Content = new StreamContent(response.Message.ToStream()); + } + result.Content.Headers.ContentType = GetMediaType(response.DataType); + + return result; + } + + public static HttpResponseMessage BuildResponse(ConnectResponse response) + { + HttpResponseMessage result = new HttpResponseMessage(); + + var connectEvent = new ConnectEventResponse + { + UserId = response.UserId, + Groups = response.Groups, + Subprotocol = response.Subprotocol, + Roles = response.Roles + }; + + return BuildResponse(JsonConvert.SerializeObject(connectEvent), MessageDataType.Json); + } + + public static HttpResponseMessage BuildResponse(string response, MessageDataType dataType = MessageDataType.Text) + { + HttpResponseMessage result = new HttpResponseMessage(); + + result.Content = new StringContent(response); + result.Content.Headers.ContentType = GetMediaType(dataType); + + return result; + } + + public static HttpResponseMessage BuildErrorResponse(ErrorResponse error) + { + HttpResponseMessage result = new HttpResponseMessage(); + + result.StatusCode = GetStatusCode(error.Code); + result.Content = new StringContent(error.ErrorMessage); + return result; + } + + public static HttpStatusCode GetStatusCode(WebPubSubErrorCode errorCode) => + errorCode switch + { + WebPubSubErrorCode.UserError => HttpStatusCode.BadRequest, + WebPubSubErrorCode.Unauthorized => HttpStatusCode.Unauthorized, + WebPubSubErrorCode.ServerError => HttpStatusCode.InternalServerError, + _ => HttpStatusCode.InternalServerError + }; + + public static IReadOnlyList GetSignatureList(string signatures) + { + if (string.IsNullOrEmpty(signatures)) + { + return default; + } + + return signatures.Split(HeaderSeparator, StringSplitOptions.RemoveEmptyEntries); + } + + public static PropertyInfo[] GetProperties(Type type) + { + return type.GetProperties(BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + } + + public static PropertyInfo GetProperty(Type type, string name) + { + return type.GetProperty(name, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + } + + public static string FirstOrDefault(params string[] values) + { + return values.FirstOrDefault(v => !string.IsNullOrEmpty(v)); + } + + public static RequestType GetRequestType(WebPubSubEventType eventType, string eventName) + { + if (eventType == WebPubSubEventType.User) + { + return RequestType.User; + } + if (eventName.Equals(Constants.Events.ConnectEvent)) + { + return RequestType.Connect; + } + if (eventName.Equals(Constants.Events.DisconnectedEvent)) + { + return RequestType.Disconnect; + } + return RequestType.Ignored; + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/WebPubSubAttribute.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/WebPubSubAttribute.cs new file mode 100644 index 0000000000000..befee1569ca6d --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/WebPubSubAttribute.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.Azure.WebJobs.Description; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue)] + [Binding] + public class WebPubSubAttribute : Attribute + { + [ConnectionString] + public string ConnectionStringSetting { get; set; } = Constants.WebPubSubConnectionStringName; + + [AutoResolve] + public string Hub { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/WebPubSubConnectionAttribute.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/WebPubSubConnectionAttribute.cs new file mode 100644 index 0000000000000..fc6e27a2d838c --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/WebPubSubConnectionAttribute.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; + +using Microsoft.Azure.WebJobs.Description; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + [AttributeUsage(AttributeTargets.ReturnValue | AttributeTargets.Parameter)] + [Binding] + public class WebPubSubConnectionAttribute : Attribute + { + [ConnectionString] + public string ConnectionStringSetting { get; set; } = Constants.WebPubSubConnectionStringName; + + [AutoResolve] + public string Hub { get; set; } + + [AutoResolve] + public string UserId { get; set; } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/WebPubSubWebJobsStartup.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/WebPubSubWebJobsStartup.cs new file mode 100644 index 0000000000000..066a3345bba17 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/WebPubSubWebJobsStartup.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Azure.WebJobs.Extensions.WebPubSub; +using Microsoft.Azure.WebJobs.Hosting; + +[assembly: WebJobsStartup(typeof(WebPubSubWebJobsStartup))] +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub +{ + public class WebPubSubWebJobsStartup : IWebJobsStartup + { + public void Configure(IWebJobsBuilder builder) + { + builder.AddWebPubSub(); + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/FakeTypeLocator.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/FakeTypeLocator.cs new file mode 100644 index 0000000000000..40ed9e4ad7ba2 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/FakeTypeLocator.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests +{ + internal class FakeTypeLocator + { + private Type type; + + public FakeTypeLocator(Type type) + { + this.type = type; + } + } +} \ No newline at end of file diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestExtensionConfig.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestExtensionConfig.cs new file mode 100644 index 0000000000000..09bc2ac6ec8fd --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestExtensionConfig.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.Azure.WebJobs.Description; +using Microsoft.Azure.WebJobs.Host.Config; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests +{ + public class TestExtensionConfig : IExtensionConfigProvider + { + public void Initialize(ExtensionConfigContext context) + { + context.AddBindingRule(). + BindToInput(attr => attr.ToBeAutoResolve); + } + + [Binding] + private sealed class BindingDataAttribute : Attribute + { + public BindingDataAttribute(string toBeAutoResolve) + { + ToBeAutoResolve = toBeAutoResolve; + } + + [AutoResolve] + public string ToBeAutoResolve { get; set; } + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestHelpers.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestHelpers.cs new file mode 100644 index 0000000000000..d60df22e72de3 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestHelpers.cs @@ -0,0 +1,124 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Azure.WebJobs.Host.Config; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests +{ + internal static class TestHelpers + { + public static IHost NewHost(Type type, WebPubSubConfigProvider ext = null, Dictionary configuration = null, ILoggerProvider loggerProvider = null) + { + var builder = new HostBuilder() + .ConfigureServices(services => + { + services.AddSingleton(new FakeTypeLocator(type)); + if (ext != null) + { + services.AddSingleton(ext); + } + services.AddSingleton(new TestExtensionConfig()); + }) + .ConfigureWebJobs(webJobsBuilder => + { + webJobsBuilder.AddWebPubSub(); + webJobsBuilder.UseHostId(Guid.NewGuid().ToString("n")); + }) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddProvider(loggerProvider); + }); + + if (configuration != null) + { + builder.ConfigureAppConfiguration(b => + { + b.AddInMemoryCollection(configuration); + }); + } + + return builder.Build(); + } + + private sealed class FakeTypeLocator : ITypeLocator + { + private readonly Type _type; + + public FakeTypeLocator(Type type) + { + _type = type; + } + + public IReadOnlyList GetTypes() + { + return new Type[] { _type }; + } + } + + public static JobHost GetJobHost(this IHost host) + { + return host.Services.GetService() as JobHost; + } + + private static string GetFormedType(WebPubSubEventType type, string eventName) + { + return type == WebPubSubEventType.User ? + $"{Constants.Headers.CloudEvents.TypeUserPrefix}{eventName}" : + $"{Constants.Headers.CloudEvents.TypeSystemPrefix}{eventName}"; + } + + public static HttpRequestMessage CreateHttpRequestMessage( + string hub, + WebPubSubEventType type, + string eventName, + string connectionId, + string[] signatures, + string contentType = Constants.ContentTypes.PlainTextContentType, + string httpMethod = "Post", + string host = null, + string userId = "testuser", + byte[] payload = null) + { + var context = new HttpRequestMessage() + { + Method = new HttpMethod(httpMethod) + }; + context.Headers.Add(Constants.Headers.CloudEvents.Hub, hub); + context.Headers.Add(Constants.Headers.CloudEvents.Type, GetFormedType(type, eventName)); + context.Headers.Add(Constants.Headers.CloudEvents.EventName, eventName); + context.Headers.Add(Constants.Headers.CloudEvents.ConnectionId, connectionId); + context.Headers.Add(Constants.Headers.CloudEvents.Signature, string.Join(",", signatures)); + if (host != null) + { + context.Headers.Add(Constants.Headers.WebHookRequestOrigin, host); + } + if (userId != null) + { + context.Headers.Add(Constants.Headers.CloudEvents.UserId, userId); + } + + if (payload != null) + { + context.Content = new StreamContent(new MemoryStream(payload)); + context.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + } + + foreach (var header in context.Headers) + { + context.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return context; + } + } +} \ No newline at end of file diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestListener.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestListener.cs new file mode 100644 index 0000000000000..f71badc53ff96 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestListener.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Azure.WebJobs.Host.Listeners; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests +{ + public class TestListener : IListener + { + public void Cancel() + { + } + + public void Dispose() + { + } + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestListenerBase.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestListenerBase.cs new file mode 100644 index 0000000000000..e35c18a3080d3 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Common/TestListenerBase.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests +{ + public class TestListenerBase + { + } +} \ No newline at end of file diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/JObjectTests.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/JObjectTests.cs new file mode 100644 index 0000000000000..be4f632c3f5dc --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/JObjectTests.cs @@ -0,0 +1,126 @@ +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests +{ + public class JObjectTests + { + [TestCase(nameof(SendToAll))] + [TestCase(nameof(SendToConnection))] + [TestCase(nameof(SendToGroup))] + [TestCase(nameof(SendToUser))] + [TestCase(nameof(AddConnectionToGroup))] + [TestCase(nameof(AddUserToGroup))] + [TestCase(nameof(RemoveConnectionFromGroup))] + [TestCase(nameof(RemoveUserFromAllGroups))] + [TestCase(nameof(RemoveUserFromGroup))] + [TestCase(nameof(CloseClientConnection))] + [TestCase(nameof(GrantGroupPermission))] + [TestCase(nameof(RevokeGroupPermission))] + public void TestOutputConvert(string operationKind) + { + WebPubSubConfigProvider.RegisterJsonConverter(); + + var input = @"{ ""operationKind"":""{0}"",""userId"":""user"", ""group"":""group1"",""connectionId"":""connection"",""message"":""test"",""dataType"":""text"", ""reason"":""close""}"; + + var replacedInput = input.Replace("{0}", operationKind); + + var jObject = JObject.Parse(replacedInput); + + var converted = WebPubSubConfigProvider.ConvertToWebPubSubOperation(jObject); + + Assert.AreEqual(operationKind, converted.OperationKind.ToString()); + } + + [TestCase] + public void TestBinaryDataConvertFromByteArray() + { + var testData = @"{""type"":""Buffer"", ""data"": [66, 105, 110, 97, 114, 121, 68, 97, 116, 97]}"; + + var converted = JsonConvert.DeserializeObject(testData, new BinaryDataJsonConverter()); + + Assert.AreEqual("BinaryData", converted.ToString()); + } + + [TestCase] + public async Task ParseErrorResponse() + { + var test = @"{""code"":""unauthorized"",""errorMessage"":""not valid user.""}"; + + var result = BuildResponse(test, RequestType.Connect); + + Assert.NotNull(result); + Assert.AreEqual(HttpStatusCode.Unauthorized, result.StatusCode); + + var message = await result.Content.ReadAsStringAsync(); + Assert.AreEqual("not valid user.", message); + } + + [TestCase] + public async Task ParseConnectResponse() + { + var test = @"{""userId"":""aaa""}"; + + var result = BuildResponse(test, RequestType.Connect); + + Assert.NotNull(result); + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); + + var response = await result.Content.ReadAsStringAsync(); + var message = (JObject.Parse(response)).ToObject(); + Assert.AreEqual("aaa", message.UserId); + } + + [TestCase] + public async Task ParseMessageResponse() + { + var test = @"{""message"":""test"", ""dataType"":""text""}"; + + var result = BuildResponse(test, RequestType.User); + + Assert.NotNull(result); + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); + + var message = await result.Content.ReadAsStringAsync(); + Assert.AreEqual("test", message); + Assert.AreEqual(Constants.ContentTypes.PlainTextContentType, result.Content.Headers.ContentType.MediaType); + } + + [TestCase] + public void ParseMessageResponse_InvalidReturnNull() + { + var test = @"{""message"":""test"", ""dataType"":""hello""}"; + + var result = BuildResponse(test, RequestType.User); + + Assert.Null(result); + } + + [TestCase] + public async Task ParseConnectResponse_ContentMatches() + { + var test = @"{""test"":""test"",""errorMessage"":""not valid user.""}"; + var expected = JObject.Parse(test); + + var result = BuildResponse(test, RequestType.Connect); + var content = await result.Content.ReadAsStringAsync(); + var actual = JObject.Parse(content); + + Assert.NotNull(result); + Assert.AreEqual(expected, actual); + } + + private static HttpResponseMessage BuildResponse(string input, RequestType requestType) + { + return WebPubSubTriggerDispatcher.BuildValidResponse(input, requestType); + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/JobHostEndToEndTests.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/JobHostEndToEndTests.cs new file mode 100644 index 0000000000000..1ac11c4a18a3b --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/JobHostEndToEndTests.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. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests +{ + public class JobHostEndToEndTests + { + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests.csproj b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests.csproj new file mode 100644 index 0000000000000..502eea7d5a0c0 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests.csproj @@ -0,0 +1,20 @@ + + + + $(RequiredTargetFrameworks) + SA1636 + false + + + + + + + + + + + + + + diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubAsyncCollectorTests.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubAsyncCollectorTests.cs new file mode 100644 index 0000000000000..90fc58cd1d1b6 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubAsyncCollectorTests.cs @@ -0,0 +1,62 @@ +// 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.Threading.Tasks; +using Moq; +using NUnit.Framework; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests +{ + public class WebPubSubAsyncCollectorTests + { + [TestCase] + public async Task AddAsync_WebPubSubEvent_SendAll() + { + var serviceMock = new Mock(); + var collector = new WebPubSubAsyncCollector(serviceMock.Object); + + var message = "new message"; + await collector.AddAsync(new SendToAll + { + Message = BinaryData.FromString(message), + DataType = MessageDataType.Text + }); + + serviceMock.Verify(c => c.SendToAll(It.IsAny()), Times.Once); + serviceMock.VerifyNoOtherCalls(); + + var actualData = (SendToAll)serviceMock.Invocations[0].Arguments[0]; + Assert.AreEqual(MessageDataType.Text, actualData.DataType); + Assert.AreEqual(message, actualData.Message.ToString()); + } + + //[Fact] + //public async Task AddAsync_WebPubSubEvent_SendAll() + //{ + // var serviceMock = new Mock(); + // var collector = new WebPubSubAsyncCollector(serviceMock.Object, "testhub"); + // + // var payload = Encoding.UTF8.GetBytes("new message"); + // await collector.AddAsync(new WebPubSubEvent + // { + // Operation = WebPubSubOperation.SendToAll, + // Message = new MemoryStream(payload), + // DataType = MessageDataType.Text + // }); + // + // serviceMock.Verify(c => c.SendToAll(It.IsAny()), Times.Once); + // serviceMock.VerifyNoOtherCalls(); + // + // var actualData = (WebPubSubEvent)serviceMock.Invocations[0].Arguments[0]; + // Assert.Equal(MessageDataType.Text, actualData.DataType); + // byte[] message = null; + // using (var memoryStream = new MemoryStream()) + // { + // actualData.Message.CopyTo(memoryStream); + // message = memoryStream.ToArray(); + // } + // Assert.Equal(payload, message); + //} + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubServiceTests.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubServiceTests.cs new file mode 100644 index 0000000000000..0ea70bdd2b6ce --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubServiceTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using NUnit.Framework; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests +{ + public class WebPubSubServiceTests + { + private const string NormConnectionString = "Endpoint=http://localhost;Port=8080;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH;Version=1.0;"; + private const string SecConnectionString = "Endpoint=https://abc;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH;Version=1.0;"; + + [TestCase(NormConnectionString, "ws://localhost:8080/client/hubs/testHub")] + [TestCase(SecConnectionString, "wss://abc/client/hubs/testHub")] + public void TestWebPubSubConnection_Scheme(string connectionString, string expectedBaseUrl) + { + var service = new WebPubSubService(connectionString, "testHub"); + + var clientConnection = service.GetClientConnection(); + + Assert.NotNull(clientConnection); + Assert.AreEqual(expectedBaseUrl, clientConnection.BaseUrl); + } + + [TestCase] + public void TestConfigParser() + { + var testconnection = "Endpoint=http://abc;Port=888;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH==A;Version=1.0;"; + var configs = new ServiceConfigParser(testconnection); + + Assert.AreEqual("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH==A", configs.AccessKey); + Assert.AreEqual("http://abc/", configs.Endpoint.ToString()); + Assert.AreEqual(888, configs.Port); + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubTriggerDispatcherTests.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubTriggerDispatcherTests.cs new file mode 100644 index 0000000000000..f2d397318ed31 --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubTriggerDispatcherTests.cs @@ -0,0 +1,127 @@ +// 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.Generic; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Host.Executors; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using NUnit.Framework; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests +{ + public class WebPubSubTriggerDispatcherTests + { + private static (string ConnectionId, string AccessKey, string Signature) TestKey = ("0f9c97a2f0bf4706afe87a14e0797b11", "7aab239577fd4f24bc919802fb629f5f", "sha256=7767effcb3946f3e1de039df4b986ef02c110b1469d02c0a06f41b3b727ab561"); + private const string TestHub = "testhub"; + private const WebPubSubEventType TestType = WebPubSubEventType.System; + private const string TestEvent = Constants.Events.ConnectedEvent; + + private static readonly HashSet EmptySetting = new(); + private static readonly HashSet ValidAccessKeys = new(new string[] { TestKey.AccessKey }); + private static readonly string[] ValidSignature = new string[] { TestKey.Signature }; + + [TestCase] + public async Task TestProcessRequest_ValidRequest() + { + var dispatcher = SetupDispatcher(); + var request = TestHelpers.CreateHttpRequestMessage(TestHub, TestType, TestEvent, TestKey.ConnectionId, ValidSignature); + var response = await dispatcher.ExecuteAsync(request, EmptySetting, ValidAccessKeys); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + } + + [TestCase] + public async Task TestProcessRequest_AllowNullUserId() + { + var dispatcher = SetupDispatcher(); + var request = TestHelpers.CreateHttpRequestMessage(TestHub, TestType, TestEvent, TestKey.ConnectionId, ValidSignature, userId: null); + var response = await dispatcher.ExecuteAsync(request, EmptySetting, ValidAccessKeys); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + } + + [TestCase] + public async Task TestProcessRequest_RouteNotFound() + { + var dispatcher = SetupDispatcher(); + var request = TestHelpers.CreateHttpRequestMessage("hub1", TestType, TestEvent, TestKey.ConnectionId, ValidSignature); + var response = await dispatcher.ExecuteAsync(request, EmptySetting, ValidAccessKeys); + Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); + } + + [TestCase] + public async Task TestProcessRequest_SignatureInvalid() + { + var dispatcher = SetupDispatcher(); + var request = TestHelpers.CreateHttpRequestMessage(TestHub, TestType, TestEvent, TestKey.ConnectionId, new string[] { "abc" }); + var response = await dispatcher.ExecuteAsync(request, EmptySetting, ValidAccessKeys); + Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [TestCase] + public async Task TestProcessRequest_ConnectionIdNullBadRequest() + { + var dispatcher = SetupDispatcher(); + var request = TestHelpers.CreateHttpRequestMessage(TestHub, TestType, TestEvent, null, ValidSignature, httpMethod: "Delete"); + var response = await dispatcher.ExecuteAsync(request, EmptySetting, ValidAccessKeys); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + } + + [TestCase] + public async Task TestProcessRequest_DeleteMethodBadRequest() + { + var dispatcher = SetupDispatcher(); + var request = TestHelpers.CreateHttpRequestMessage(TestHub, TestType, TestEvent, TestKey.ConnectionId, ValidSignature, httpMethod: "Delete"); + var response = await dispatcher.ExecuteAsync(request, EmptySetting, ValidAccessKeys); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + } + + [TestCase("OPTIONS", "abc.com")] + [TestCase("GET", "abc.com")] + public async Task TestProcessRequest_AbuseProtectionValidOK(string method, string host) + { + var allowedHost = new HashSet(new string[] { host }); + var dispatcher = SetupDispatcher(); + var request = TestHelpers.CreateHttpRequestMessage(TestHub, TestType, TestEvent, TestKey.ConnectionId, new string[] { TestKey.Signature }, httpMethod: method, host: host); + var response = await dispatcher.ExecuteAsync(request, allowedHost, ValidAccessKeys); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + } + + [TestCase("OPTIONS", "abc.com")] + [TestCase("GET", "abc.com")] + public async Task TestProcessRequest_AbuseProtectionInvalidBadRequest(string method, string allowedHost) + { + var allowedHosts = new HashSet(new string[] { allowedHost }); + var testhost = "def.com"; + var dispatcher = SetupDispatcher(); + var request = TestHelpers.CreateHttpRequestMessage(TestHub, TestType, TestEvent, TestKey.ConnectionId, new string[] { TestKey.Signature }, httpMethod: method, host: testhost); + var response = await dispatcher.ExecuteAsync(request, allowedHosts, ValidAccessKeys); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + } + + [TestCase("application/xml", HttpStatusCode.BadRequest)] + public async Task TestProcessRequest_MessageMediaTypes(string mediaType, HttpStatusCode expectedCode) + { + var dispatcher = SetupDispatcher(TestHub, WebPubSubEventType.User, Constants.Events.MessageEvent); + var request = TestHelpers.CreateHttpRequestMessage(TestHub, WebPubSubEventType.User, Constants.Events.MessageEvent, TestKey.ConnectionId, ValidSignature, contentType: mediaType, payload: Encoding.UTF8.GetBytes("Hello")); + var response = await dispatcher.ExecuteAsync(request, EmptySetting, ValidAccessKeys).ConfigureAwait(false); + Assert.AreEqual(expectedCode, response.StatusCode); + } + + private static WebPubSubTriggerDispatcher SetupDispatcher(string hub = TestHub, WebPubSubEventType type = TestType, string eventName = TestEvent) + { + var funcName = $"{hub}.{type}.{eventName}".ToLower(); + var dispatcher = new WebPubSubTriggerDispatcher(NullLogger.Instance); + var executor = new Mock(); + executor.Setup(f => f.TryExecuteAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new FunctionResult(true))); + var listener = new WebPubSubListener(executor.Object, funcName, dispatcher); + + dispatcher.AddListener(funcName, listener); + + return dispatcher; + } + } +} diff --git a/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubTriggerValueProviderTests.cs b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubTriggerValueProviderTests.cs new file mode 100644 index 0000000000000..d681c13b6a1ea --- /dev/null +++ b/sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/tests/WebPubSubTriggerValueProviderTests.cs @@ -0,0 +1,37 @@ +// 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.Reflection; +using NUnit.Framework; + +namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub.Tests +{ + public class WebPubSubTriggerValueProviderTests + { + [TestCase("connectioncontext")] + [TestCase("reason")] + [TestCase("message")] + public void TestGetValueByName_Valid(string name) + { + var triggerEvent = new WebPubSubTriggerEvent + { + ConnectionContext = new ConnectionContext + { + ConnectionId = "000000", + EventName = "message", + EventType = WebPubSubEventType.User, + Hub = "testhub", + UserId = "user1" + }, + Reason = "reason", + Message = BinaryData.FromString("message"), + }; + + var value = typeof(WebPubSubTriggerEvent) + .GetProperty(name, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .GetValue(triggerEvent); + Assert.NotNull(value); + } + } +}