diff --git a/eng/.docsettings.yml b/eng/.docsettings.yml index 3f08777dda77..1934bc8a37f4 100644 --- a/eng/.docsettings.yml +++ b/eng/.docsettings.yml @@ -133,6 +133,9 @@ known_content_issues: - ['sdk/storage/Azure.Storage.Files.DataLake/README.md', 'Storage: #11492: Needs auth section'] - ['sdk/storage/Azure.Storage.Files.Shares/README.md', 'Storage: #11492: Needs auth section'] + - ['sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/README.md', 'https://github.com/Azure/azure-sdk-tools/issues/404'] + - ['sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/README.md', 'https://github.com/Azure/azure-sdk-tools/issues/404'] + # .net climbs upwards. placing these to prevent assigning readmes to the wrong project package_indexing_exclusion_list: - 'AutoRest-AzureDotNetSDK' diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props index 961825bab0b8..364166cc9867 100644 --- a/eng/Packages.Data.props +++ b/eng/Packages.Data.props @@ -245,7 +245,7 @@ - + @@ -257,9 +257,9 @@ - - - + + + @@ -357,11 +357,29 @@ - + + + + + + + + + + + + + + + + + + + 1.0.0-dev.20240410.2 diff --git a/sdk/extensions/ci.wcf.yml b/sdk/extensions/ci.wcf.yml new file mode 100644 index 000000000000..8341e812fbf5 --- /dev/null +++ b/sdk/extensions/ci.wcf.yml @@ -0,0 +1,37 @@ +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. + +trigger: + branches: + include: + - main + - hotfix/* + - release/* + paths: + include: + - sdk/extensions/wcf + +pr: + branches: + include: + - main + - feature/* + - hotfix/* + - release/* + paths: + include: + - sdk/extensions/wcf + +extends: + template: /eng/pipelines/templates/stages/archetype-sdk-client.yml + parameters: + SDKType: wcf + ServiceDirectory: extensions/wcf + ArtifactName: packages + Artifacts: + - name: Microsoft.CoreWCF.Azure.StorageQueues + safeName: MicrosoftCoreWCFAzureStorageQueues + - name: Microsoft.WCF.Azure.StorageQueues + safeName: MicrosoftWCFAzureStorageQueues + CheckAOTCompat: false + TestSetupSteps: + - template: /sdk/storage/tests-install-azurite.yml diff --git a/sdk/extensions/ci.yml b/sdk/extensions/ci.yml index e46a26f97a9d..3e7df12c6e67 100644 --- a/sdk/extensions/ci.yml +++ b/sdk/extensions/ci.yml @@ -9,6 +9,8 @@ trigger: paths: include: - sdk/extensions/ + exclude: + - sdk/extensions/wcf/ pr: branches: @@ -20,10 +22,13 @@ pr: paths: include: - sdk/extensions/ + exclude: + - sdk/extensions/wcf/ extends: template: /eng/pipelines/templates/stages/archetype-sdk-client.yml parameters: + SDKType: client ServiceDirectory: extensions ArtifactName: packages Artifacts: diff --git a/sdk/extensions/service.projects b/sdk/extensions/service.projects new file mode 100644 index 000000000000..cc0af1dacfca --- /dev/null +++ b/sdk/extensions/service.projects @@ -0,0 +1,23 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/sdk/extensions/tests.wcf.yml b/sdk/extensions/tests.wcf.yml new file mode 100644 index 000000000000..5faab57e8e03 --- /dev/null +++ b/sdk/extensions/tests.wcf.yml @@ -0,0 +1,8 @@ +trigger: none + +extends: + template: ../../eng/pipelines/templates/stages/archetype-sdk-tests.yml + parameters: + SDKType: wcf + ServiceDirectory: extensions + SupportedClouds: 'Public,Canary,UsGov,China' diff --git a/sdk/extensions/tests.yml b/sdk/extensions/tests.yml index b2f766ee516d..0bbe4229bae4 100644 --- a/sdk/extensions/tests.yml +++ b/sdk/extensions/tests.yml @@ -3,5 +3,6 @@ trigger: none extends: template: ../../eng/pipelines/templates/stages/archetype-sdk-tests.yml parameters: + SDKType: client ServiceDirectory: extensions SupportedClouds: 'Public,Canary,UsGov,China' diff --git a/sdk/extensions/wcf/Directory.Build.props b/sdk/extensions/wcf/Directory.Build.props new file mode 100644 index 000000000000..fb505abd7278 --- /dev/null +++ b/sdk/extensions/wcf/Directory.Build.props @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + true + true + true + + + + diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/CHANGELOG.md b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/CHANGELOG.md new file mode 100644 index 000000000000..f50dd6a1a0bf --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/CHANGELOG.md @@ -0,0 +1,12 @@ +# Release History + +## 1.0.0-beta.1 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/README.md b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/README.md new file mode 100644 index 000000000000..3b8b7b7ce27f --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/README.md @@ -0,0 +1,152 @@ +# CoreWCF Azure Queue Storage library for .NET + +CoreWCF Azure Queue Storage is the service side library that will help existing WCF services to be able to use Azure Queue Storage to communicate with clients as a modern replacement to using MSMQ. + +## Getting started + +### Install the package + +Install the Microsoft.CoreWCF.Azure.StorageQueues library for .NET with [NuGet][nuget]: + +```dotnetcli +dotnet add package Microsoft.CoreWCF.Azure.StorageQueues +``` + +### Prerequisites + +You need an [Azure subscription][azure_sub] and a +[Storage Account][storage_account_docs] to use this package. + +To create a new Storage Account, you can use the [Azure portal][storage_account_create_portal], +[Azure PowerShell][storage_account_create_ps], or the [Azure CLI][storage_account_create_cli]. +Here's an example using the Azure CLI: + +```azurecli +az storage account create --name MyStorageAccount --resource-group MyResourceGroup --location westus --sku Standard_LRS +``` + +### Configure ASP.NET Core to use CoreWCF with Queue transports + +In order to receive requests from the Azure Queue Storage service, you'll need to configure CoreWCF to use queue transports. + +```C# Snippet:Configure_CoreWCF_QueueTransport +public void ConfigureServices(IServiceCollection services) +{ + services.AddServiceModelServices() + .AddQueueTransport(); +} +``` + +### Authenticate the service to Azure Queue Storage + +To receive requests from the Azure Queue Storage service, you'll need to configure CoreWCF with the appropriate endpoint, and configure credentials. The [Azure Identity library][identity] makes it easy to add Microsoft Entra ID support for authenticating with Azure services. + +There are multiple authentication mechanisms for Azure Queue Storage. Which mechanism to use is configured via the property `AzureQueueStorageBinding.Security.Transport.ClientCredentialType`. The default authentication mechanism is `Default`, which uses `DefaultAzureCredential`. + +```C# Snippet:CoreWCF_Azure_Storage_Queues_Sample_DefaultAzureCredential +public void Configure(IApplicationBuilder app) +{ + app.UseServiceModel(serviceBuilder => + { + // Configure CoreWCF to dispatch to service type Service + serviceBuilder.AddService(); + + // Create a binding instance to use Azure Queue Storage, passing an optional queue name for the dead letter queue + // The default client credential type is Default, which uses DefaultAzureCredential + var aqsBinding = new AzureQueueStorageBinding("DEADLETTERQUEUENAME"); + + // Add a service endpoint using the AzureQueueStorageBinding + string queueEndpointString = "https://MYSTORAGEACCOUNT.queue.core.windows.net/QUEUENAME"; + serviceBuilder.AddServiceEndpoint(aqsBinding, queueEndpointString); + }); +} +``` + +Learn more about enabling Microsoft Entra ID for authentication with Azure Storage in [our documentation][storage_ad]. + +### Other authentication credential mechanisms + +If you are using a different credential mechanism such as `StorageSharedKeyCredential`, you configure the appropriate `ClientCredentialType` on the binding and set the credential on an `AzureServiceCredentials` instance via an extension method. + +```C# Snippet:CoreWCF_Azure_Storage_Queus_Sample_StorageSharedKey +public void Configure(IApplicationBuilder app) +{ + app.UseServiceModel(serviceBuilder => + { + // Configure CoreWCF to dispatch to service type Service + serviceBuilder.AddService(); + + // Create a binding instance to use Azure Queue Storage, passing an optional queue name for the dead letter queue + var aqsBinding = new AzureQueueStorageBinding("DEADLETTERQUEUENAME"); + + // Configure the client credential type to use StorageSharedKeyCredential + aqsBinding.Security.Transport.ClientCredentialType = AzureClientCredentialType.StorageSharedKey; + + // Add a service endpoint using the AzureQueueStorageBinding + string queueEndpointString = "https://MYSTORAGEACCOUNT.queue.core.windows.net/QUEUENAME"; + serviceBuilder.AddServiceEndpoint(aqsBinding, queueEndpointString); + + // Use extension method to configure CoreWCF to use AzureServiceCredentials and set the + // StorageSharedKeyCredential instance. + serviceBuilder.UseAzureCredentials(credentials => + { + credentials.StorageSharedKey = GetStorageSharedKey(); + }); + }); +} + +public StorageSharedKeyCredential GetStorageSharedKey() +{ + // Fetch shared key using a secure mechanism such as Azure Key Vault + // and construct an instance of StorageSharedKeyCredential to return; + return default; +} +``` + +Other values for ClientCredentialType are `Sas`, `Token`, and `ConnectionString`. The credentials class `AzureServiceCredentials` has corresponding properties to set each of these credential types. + +## Troubleshooting + +If there are any errors receiving a message from Azure Queue Storage, the CoreWCF transport will log the details at the `Debug` log level. You can configure logging for the category `Microsoft.CoreWCF` at the `Debug` level to see any errors. + +```C# Snippet:CoreWCF_Azure_Storage_Queus_Sample_Logging +.ConfigureLogging((logging) => +{ + logging.AddFilter("Microsoft.CoreWCF", LogLevel.Debug); +}); +``` + +If a message was recevied from the queue, but wasn't able to be processed, it will be placed in the dead letter queue. You can sepcify the dead letter queue name by passing it to the constructor of `AzureQueueStorageBinding`. If not specified, the default value of `default-dead-letter-queue` will be used. + +## Key concepts + +CoreWCF is an implementation of the service side features of Windows Communication Foundation (WCF) for .NET. The goal of this project is to enable migrating existing WCF services to .NET that are currently using MSMQ and wish to deploy their service to Azure, replacing MSMQ with Azure Queue Storage. + +More general samples of using CoreWCF can be found in the [CoreWCF samples repository][corewcf_repo]. To create a client to send messages to an Azure Storage Queue, see the[Microsoft.WCF.Azure.StorageQueues documentation][wcf_docs]. + +## Contributing + +See the [Storage CONTRIBUTING.md][storage_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. + + +[nuget]: https://www.nuget.org/ +[storage_account_docs]: https://learn.microsoft.com/azure/storage/common/storage-account-overview +[storage_account_create_ps]: https://learn.microsoft.com/azure/storage/common/storage-account-create?tabs=azure-powershell +[storage_account_create_cli]: https://learn.microsoft.com/azure/storage/common/storage-account-create?tabs=azure-cli +[storage_account_create_portal]: https://learn.microsoft.com/azure/storage/common/storage-account-create?tabs=azure-portal +[azure_cli]: https://learn.microsoft.com/cli/azure/ +[azure_sub]: https://azure.microsoft.com/free/dotnet/ +[identity]: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/identity/Azure.Identity/README.md +[storage_ad]: https://learn.microsoft.com/azure/storage/blobs/authorize-access-azure-active-directory +[storage_contrib]: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/storage/CONTRIBUTING.md +[cla]: https://opensource.microsoft.com/cla/ +[coc]: https://opensource.microsoft.com/codeofconduct/ +[coc_faq]: https://opensource.microsoft.com/codeofconduct/faq/ +[coc_contact]: mailto:opencode@microsoft.com +[corewcf_repo]: https://github.com/CoreWCF/samples/ +[wcf_docs]: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/GlobalSuppressions.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/GlobalSuppressions.cs new file mode 100644 index 000000000000..0ff9fc06f73e --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/GlobalSuppressions.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Containers, Azure.Core.Expressions, Azure.Data, Azure.DigitalTwins, Azure.Identity, Azure.IoT, Azure.Learn, Azure.Management, Azure.Media, Azure.Messaging, Azure.MixedReality, Azure.Monitor, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.CoreWCF.Azure.StorageQueues")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Containers, Azure.Core.Expressions, Azure.Data, Azure.DigitalTwins, Azure.Identity, Azure.IoT, Azure.Learn, Azure.Management, Azure.Media, Azure.Messaging, Azure.MixedReality, Azure.Monitor, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.CoreWCF.Azure")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Containers, Azure.Core.Expressions, Azure.Data, Azure.DigitalTwins, Azure.Identity, Azure.IoT, Azure.Learn, Azure.Management, Azure.Media, Azure.Messaging, Azure.MixedReality, Azure.Monitor, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.CoreWCF.Azure.Tokens")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Containers, Azure.Core.Expressions, Azure.Data, Azure.DigitalTwins, Azure.Identity, Azure.IoT, Azure.Learn, Azure.Management, Azure.Media, Azure.Messaging, Azure.MixedReality, Azure.Monitor, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.CoreWCF.Azure.StorageQueues.Channels")] diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft.CoreWCF.Azure.StorageQueues.csproj b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft.CoreWCF.Azure.StorageQueues.csproj new file mode 100644 index 000000000000..3452e54a00a9 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft.CoreWCF.Azure.StorageQueues.csproj @@ -0,0 +1,49 @@ + + + + Icon.png + $(RequiredTargetFrameworks) + Microsoft.CoreWCF.Azure.StorageQueues + Microsoft.CoreWCF.Azure.StorageQueues + + True + 1.0.0-beta.1 + True + + + + + none + all + + + + + + + + + + + + + + + + + + + True + True + SR.resx + + + + + + ResXFileCodeGenerator + SR.Designer.cs + Microsoft.CoreWCF.Azure.StorageQueues + + + \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureClientCredentialType.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureClientCredentialType.cs new file mode 100644 index 000000000000..3f5fab6c437b --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureClientCredentialType.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.CoreWCF.Azure +{ + /// + /// Represents the types of credentials that can be passed to authenticate with Azure + /// + public enum AzureClientCredentialType + { + /// + /// Use the Azure.Identity.DefaultAzureCredential credential + /// + Default, + + /// + /// Use a Azure.AzureSasCredential credential + /// + Sas, + + /// + /// Use a Azure.Storage.StorageSharedKeyCredential credential + /// + StorageSharedKey, + + /// + /// Use a Azure.Core.TokenCredential credential + /// + Token, + + /// + /// Use a connection string to provide credentials. See how to Configure Azure Storage connection strings. + /// + ConnectionString + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureClientCredentialTypeHelper.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureClientCredentialTypeHelper.cs new file mode 100644 index 000000000000..8a2ee10a8281 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureClientCredentialTypeHelper.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.CoreWCF.Azure +{ + internal static class AzureClientCredentialTypeHelper + { + internal static bool IsDefined(AzureClientCredentialType value) + { + return (value == AzureClientCredentialType.Default || + value == AzureClientCredentialType.Sas || + value == AzureClientCredentialType.StorageSharedKey || + value == AzureClientCredentialType.Token || + value == AzureClientCredentialType.ConnectionString); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureSecurityTokenProvider.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureSecurityTokenProvider.cs new file mode 100644 index 000000000000..c70f82ec077f --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureSecurityTokenProvider.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Identity; +using CoreWCF.IdentityModel.Selectors; +using CoreWCF.IdentityModel.Tokens; +using Microsoft.CoreWCF.Azure.StorageQueues; +using Microsoft.CoreWCF.Azure.Tokens; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CoreWCF.Azure +{ + internal class AzureSecurityTokenProvider : SecurityTokenProvider + { + private SecurityToken _securityToken; + + public AzureSecurityTokenProvider(AzureServiceCredentials azureServiceCredentials, SecurityTokenRequirement tokenRequirement) + { + InitToken(azureServiceCredentials, tokenRequirement); + } + + private void InitToken(AzureServiceCredentials azureServiceCredentials, SecurityTokenRequirement tokenRequirement) + { + switch (tokenRequirement.TokenType) + { + case AzureSecurityTokenTypes.DefaultTokenType: + _securityToken = CreateDefaultSecurityToken(azureServiceCredentials); + break; + case AzureSecurityTokenTypes.SasTokenType: + _securityToken = CreateSasSecurityToken(azureServiceCredentials); + break; + case AzureSecurityTokenTypes.StorageSharedKeyTokenType: + _securityToken = CreateStorageSharedKeySecurityToken(azureServiceCredentials); + break; + case AzureSecurityTokenTypes.TokenTokenType: + _securityToken = CreateTokenCredentialSecurityToken(azureServiceCredentials); + break; + case AzureSecurityTokenTypes.ConnectionStringTokenType: + _securityToken = CreateConnectionStringSecurityToken(azureServiceCredentials); + break; + default: + _securityToken = null; + break; + } + } + + protected override Task GetTokenCoreAsync(CancellationToken cancellationToken) + { + return Task.FromResult(_securityToken); + } + + protected override SecurityToken GetTokenCore(TimeSpan timeout) + { + return _securityToken; + } + + private SecurityToken CreateConnectionStringSecurityToken(AzureServiceCredentials AzureServiceCredentials) + { + if (AzureServiceCredentials.ConnectionString == null) + { + throw new InvalidOperationException(SR.ConnectionStringNotProvidedOnAzureServiceCredentials); + } + + return new ConnectionStringSecurityToken(AzureServiceCredentials.ConnectionString); + } + + private SecurityToken CreateTokenCredentialSecurityToken(AzureServiceCredentials AzureServiceCredentials) + { + if (AzureServiceCredentials.Token == null) + { + throw new InvalidOperationException(SR.TokenCredentialNotProvidedOnAzureServiceCredentials); + } + + return new TokenCredentialSecurityToken(AzureServiceCredentials.Token); + } + + private SecurityToken CreateStorageSharedKeySecurityToken(AzureServiceCredentials AzureServiceCredentials) + { + if (AzureServiceCredentials.StorageSharedKey == null) + { + throw new InvalidOperationException(SR.StorageSharedKeyCredentialNotProvidedOnAzureServiceCredentials); + } + + return new StorageSharedKeySecurityToken(AzureServiceCredentials.StorageSharedKey); + } + + private SecurityToken CreateSasSecurityToken(AzureServiceCredentials AzureServiceCredentials) + { + if (AzureServiceCredentials.Sas == null) + { + throw new InvalidOperationException(SR.SasCredentialNotProvidedOnAzureServiceCredentials); + } + + return new SasSecurityToken(AzureServiceCredentials.Sas); + } + + private SecurityToken CreateDefaultSecurityToken(AzureServiceCredentials AzureServiceCredentials) + { + DefaultAzureCredential cred; + if (AzureServiceCredentials.DefaultAzureCredentialOptions != null) + { + cred = new DefaultAzureCredential(AzureServiceCredentials.DefaultAzureCredentialOptions); + } + else + { + cred = new DefaultAzureCredential(); + } + + return new TokenCredentialSecurityToken(cred); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureServiceCredentials.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureServiceCredentials.cs new file mode 100644 index 000000000000..6c5cacc62b1c --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureServiceCredentials.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure; +using Azure.Core; +using Azure.Identity; +using Azure.Storage; +using Azure.Storage.Queues.Models; +using CoreWCF.Description; +using CoreWCF.IdentityModel.Selectors; + +namespace Microsoft.CoreWCF.Azure +{ + /// + /// Represents the credentials used to authenticate with Azure services. + /// + public class AzureServiceCredentials : ServiceCredentials + { + /// + /// Initializes a new instance of the class. + /// + public AzureServiceCredentials() { } + + /// + /// Initializes a new instance of the class cloning an existing instance. + /// + /// The instance to copy. + protected AzureServiceCredentials(AzureServiceCredentials other) : base(other) + { + Audience = other.Audience; + EnableTenantDiscovery = other.EnableTenantDiscovery; + DefaultAzureCredentialOptions = other.DefaultAzureCredentialOptions; + Sas = other.Sas; + StorageSharedKey = other.StorageSharedKey; + Token = other.Token; + ConnectionString = other.ConnectionString; + } + + /// + /// Gets or sets the audience to use for authentication with Microsoft Entra ID. The audience isn't considered when using a shared key. + /// + public QueueAudience Audience { get; set; } + + /// + /// Enables tenant discovery through the authorization challenge when the client is configured to use a TokenCredential. + /// + public bool EnableTenantDiscovery { get; set; } + + /// + /// Gets of sets the Azure.Identity.DefaultAzureCredentialOptions instance used with DefaultAzureCredential. + /// + public DefaultAzureCredentialOptions DefaultAzureCredentialOptions { get; set; } + + /// + /// Gets or sets the Azure.AzureSasCredential (shared access signature) credential. + /// + public AzureSasCredential Sas { get; set; } + + /// + /// Gets or sets the Azure.Storage.StorageSharedKeyCredential credential. + /// + public StorageSharedKeyCredential StorageSharedKey { get; set; } + + /// + /// Gets or sets the Azure.Core.TokenCredential credential. + /// + public TokenCredential Token { get; set; } + + /// + /// Gets or sets the connection string containing credentials. See how to Configure Azure Storage connection strings. + /// + public string ConnectionString { get; set; } + + /// + /// Creates a security token manager for this instance. + /// + /// A AzureServiceCredentialsSecurityTokenManager for this AzureServiceCredentials. + public override SecurityTokenManager CreateSecurityTokenManager() + { + return new AzureServiceCredentialsSecurityTokenManager(Clone() as AzureServiceCredentials); + } + + /// + /// Creates a new copy of this AzureServiceCredentials instance. + /// + /// A AzureServiceCredentials instance. + protected override ServiceCredentials CloneCore() + { + return new AzureServiceCredentials(this); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureServiceCredentialsSecurityTokenManager.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureServiceCredentialsSecurityTokenManager.cs new file mode 100644 index 000000000000..afd0ea74313a --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/AzureServiceCredentialsSecurityTokenManager.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CoreWCF.IdentityModel.Selectors; +using CoreWCF.IdentityModel.Tokens; +using CoreWCF.Security; +using Microsoft.CoreWCF.Azure.StorageQueues.Channels; +using Microsoft.CoreWCF.Azure.Tokens; +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.CoreWCF.Azure +{ + /// + /// Manages Azure credential security tokens for a CoreWCF service. + /// + public class AzureServiceCredentialsSecurityTokenManager : ServiceCredentialsSecurityTokenManager + { + private AzureServiceCredentials _azureServiceCredentials; + + /// + /// Initializes a new instance of the class. + /// + /// + public AzureServiceCredentialsSecurityTokenManager(AzureServiceCredentials azureServiceCredentials) : base(azureServiceCredentials) + { + _azureServiceCredentials = azureServiceCredentials; + } + + /// + /// Creates a security token authenticator. + /// + /// The SecurityTokenRequirement. + /// When this method returns, contains a SecurityTokenResolver. This parameter is passed uninitialized. + /// The SecurityTokenAuthenticator object. + /// `tokenRequirement` is `null`. + public override SecurityTokenAuthenticator CreateSecurityTokenAuthenticator(SecurityTokenRequirement tokenRequirement, out SecurityTokenResolver outOfBandTokenResolver) + { + outOfBandTokenResolver = null; + if (tokenRequirement is null) throw new ArgumentNullException(nameof(tokenRequirement)); + + if (tokenRequirement is InitiatorServiceModelSecurityTokenRequirement initiatorRequirement) + { + if (initiatorRequirement.TokenType == SecurityTokenTypes.X509Certificate && initiatorRequirement.KeyUsage == SecurityKeyUsage.Exchange) + { + X509CertificateValidator certValidator = GetX509CertificateValidator(); + if (certValidator != null) + { + return new X509SecurityTokenAuthenticator(certValidator); + } + else + { + return null; + } + } + } + + return base.CreateSecurityTokenAuthenticator(tokenRequirement, out outOfBandTokenResolver); + } + + private X509CertificateValidator GetX509CertificateValidator() + { + var certAuthentication = _azureServiceCredentials.ClientCertificate.Authentication; + switch (certAuthentication.CertificateValidationMode) + { + case X509CertificateValidationMode.None: + return X509CertificateValidator.None; + case X509CertificateValidationMode.PeerTrust: + return X509CertificateValidator.PeerTrust; + case X509CertificateValidationMode.Custom: + return _azureServiceCredentials.ClientCertificate.Authentication.CustomCertificateValidator; + } + + bool useMachineContext = certAuthentication.TrustedStoreLocation == StoreLocation.LocalMachine; + X509ChainPolicy chainPolicy = new X509ChainPolicy(); + chainPolicy.ApplicationPolicy.Add(new Oid("1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.1")); + chainPolicy.RevocationMode = certAuthentication.RevocationMode; + if (certAuthentication.CertificateValidationMode == X509CertificateValidationMode.ChainTrust) + { + // Returning null means use the QueueClient default implementation + return null; + } + else + { + return X509CertificateValidator.CreatePeerOrChainTrustValidator(useMachineContext, chainPolicy); + } + } + + /// + /// Creates a security token provider. + /// + /// The SecurityTokenRequirement. + /// The SecurityTokenProvider object. + /// `tokenRequirement` is `null`. + public override SecurityTokenProvider CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement) + { + if (tokenRequirement is null) throw new ArgumentNullException(nameof(tokenRequirement)); + + if (tokenRequirement.TokenType.StartsWith(AzureSecurityTokenTypes.Namespace)) + return new AzureSecurityTokenProvider(_azureServiceCredentials, tokenRequirement); + return base.CreateSecurityTokenProvider(tokenRequirement); + } + + /// + /// Creates a security token serializer. + /// + /// The SecurityTokenVersion of the security token. + /// The SecurityTokenSerializer object. + public override SecurityTokenSerializer CreateSecurityTokenSerializer(SecurityTokenVersion version) + { + return base.CreateSecurityTokenSerializer(version); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/ServiceCredentialsExtensions.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/ServiceCredentialsExtensions.cs new file mode 100644 index 000000000000..ffa0a9618cd2 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/ServiceCredentialsExtensions.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CoreWCF; +using CoreWCF.Collections.Generic; +using CoreWCF.Configuration; +using CoreWCF.Description; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.CoreWCF.Azure +{ + /// + /// Provides extension methods for configuring Azure credentials with a service credential. + /// + public static class ServiceCredentialsExtensions + { + /// + /// Configures the service host to use Azure credentials. + /// + /// The service host. + /// The Azure service credentials. + public static AzureServiceCredentials UseAzureCredentials(this ServiceHostBase serviceHostBase) + { + var creds = new AzureServiceCredentials(); + var behaviors = serviceHostBase.Description.Behaviors; + behaviors.Remove(); + behaviors.Add(creds); + return creds; + } + + /// + /// Configures the service type to use Azure credentials. + /// + /// The type of the service. + /// The service builder. + /// The Azure service credentials. + public static AzureServiceCredentials UseAzureCredentials(this IServiceBuilder serviceBuilder) where TService : class + { + var creds = new AzureServiceCredentials(); + serviceBuilder.ConfigureServiceHostBase(serviceHostBase => + { + var behaviors = serviceHostBase.Description.Behaviors; + behaviors.Remove(); + behaviors.Add(creds); + }); + return creds; + } + + /// + /// Configures the service type to use Azure credentials and executes a configuration delegate to modify those credentials. + /// + /// The type of the service. + /// The service builder. + /// An action to configure the Azure service credentials. + public static void UseAzureCredentials(this IServiceBuilder serviceBuilder, Action configure) where TService : class + { + var creds = new AzureServiceCredentials(); + serviceBuilder.ConfigureServiceHostBase(serviceHostBase => + { + var behaviors = serviceHostBase.Description.Behaviors; + behaviors.Remove(); + behaviors.Add(creds); + if (configure is not null) + { + configure(creds); + } + }); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageBinding.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageBinding.cs new file mode 100644 index 000000000000..239f056f0b06 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageBinding.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using CoreWCF.Channels; +using Microsoft.CoreWCF.Azure.StorageQueues.Channels; +using System; + +namespace Microsoft.CoreWCF.Azure.StorageQueues +{ + /// + /// The class that contains the binding elements that specify the protocols, transports, + /// and message encoders used for communication between clients and services. + /// + public class AzureQueueStorageBinding : Binding + { + private TextMessageEncodingBindingElement _textMessageEncodingBindingElement; + private AzureQueueStorageTransportBindingElement _transport; + private BinaryMessageEncodingBindingElement _binaryMessageEncodingBindingElement; + private bool _isInitialized; + + /// + /// Initializes a new instance of the AzureQueueStorageBinding class. + /// + public AzureQueueStorageBinding(string deadLetterQueueName = "default-dead-letter-queue") + { + Security = new AzureQueueStorageSecurity(); + Initialize(); + DeadLetterQueueName = deadLetterQueueName; + } + + /// + /// Gets the URI scheme that specifies the transport used by the channel and listener + /// factories that are built by the bindings. + /// + public override string Scheme => _transport.Scheme; + + /// + /// Gets or sets the security used with this binding. + /// + public AzureQueueStorageSecurity Security { get; set; } + + /// + /// Gets or sets the name of the dead letter queue. + /// + /// + /// The dead letter queue is a storage queue where messages that cannot be delivered to the intended recipient are moved to. + /// By default, the dead letter queue name is set to "DefaultDeadLetterQueue". + /// + public string DeadLetterQueueName + { + get => _transport.DeadLetterQueueName; + set => _transport.DeadLetterQueueName = value; + } + + /// + /// Overidden method to create a collection that contains the binding elements that are part of the current binding. + /// + public override BindingElementCollection CreateBindingElements() + { + BindingElementCollection elements = new(); + + switch (MessageEncoding) + { + case AzureQueueStorageMessageEncoding.Binary: + elements.Add(_binaryMessageEncodingBindingElement); + _transport.QueueMessageEncoding = QueueMessageEncoding.Base64; + break; + case AzureQueueStorageMessageEncoding.Text: + elements.Add(_textMessageEncodingBindingElement); + _transport.QueueMessageEncoding = QueueMessageEncoding.None; + break; + default: + elements.Add(_textMessageEncodingBindingElement); + _transport.QueueMessageEncoding = QueueMessageEncoding.None; + break; + } + Security.Transport.ConfigureTransportSecurity(_transport); + elements.Add(_transport); + + return elements; + } + + private void Initialize() + { + if (!_isInitialized) + { + _transport = new AzureQueueStorageTransportBindingElement(); + _textMessageEncodingBindingElement = new TextMessageEncodingBindingElement(); + _binaryMessageEncodingBindingElement = new BinaryMessageEncodingBindingElement(); + MaxMessageSize = TransportDefaults.DefaultMaxMessageSize; + _isInitialized = true; + } + } + + /// + /// Gets or sets the maximum encoded message size. + /// + public long MaxMessageSize + { + get => _transport.MaxReceivedMessageSize; + set => _transport.MaxReceivedMessageSize = value; + } + + /// + /// Gets and sets the message encoding. + /// + public AzureQueueStorageMessageEncoding MessageEncoding { get; set; } = AzureQueueStorageMessageEncoding.Binary; + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageChannelHelpers.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageChannelHelpers.cs new file mode 100644 index 000000000000..39391aea1c60 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageChannelHelpers.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CoreWCF; +using System; +using System.Globalization; + +namespace Microsoft.CoreWCF.Azure.StorageQueues +{ + internal static class AzureQueueStorageChannelHelpers + { + /// + /// The Channel layer normalizes exceptions thrown by the underlying networking implementations + /// into subclasses of CommunicationException, so that Channels can be used polymorphically from + /// an exception handling perspective. + /// + internal static CommunicationException ConvertTransferException(Exception e) + { + return new CommunicationException( + string.Format(CultureInfo.CurrentCulture, + SR.SendError, e.Message), + e); + } + + internal static Uri ExtractQueueUriFromConnectionString(string connectionString) + { + string[] parts = connectionString.Split(';'); + + foreach (string part in parts) + { + var keyValue = part.Trim().Split('='); + if (keyValue[0].Equals("QueueEndpoint", StringComparison.OrdinalIgnoreCase)) + { + if (Uri.TryCreate(keyValue[1], default, out Uri endpointUri)) + { + return endpointUri; + } + } + } + + return null; + } + + internal static void ExtractAccountAndQueueNameFromUri(Uri endpointUri, bool queueNameRequired, out string accountName, out string queueName) + { + if (endpointUri.HostNameType == UriHostNameType.Dns && !endpointUri.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + // Account name should be first component of hostname, eg + // https://.queue.core.windows.net/ + + if (queueNameRequired) + { + // If queue name required, then the uri should have 2 segments, "/" and the queue name + if (endpointUri.Segments.Length != 2) + { + throw new ArgumentException(string.Format(SR.AccountNameShouldBePartOfHostName, endpointUri)); + } + queueName = endpointUri.Segments[1].TrimEnd('/'); + } + else + { + // If the queue name is not required, then there could be 2 or 1 segments. Any more than 2 means the account + // name was in the path + if (endpointUri.Segments.Length > 2) + { + throw new ArgumentException(string.Format(SR.AccountNameShouldBePartOfHostName, endpointUri)); + } + if (endpointUri.Segments.Length == 2) + { + // Queue name is part of path, so extract it + queueName = endpointUri.Segments[1].TrimEnd('/'); + } + else + { + // Queue name wasn't provided in url, and wasn't required + queueName = String.Empty; + } + } + + accountName = endpointUri.Host.Split('.')[0]; + } + else + { + // Hostname is not a Dns hostname or is localhost. Likely using Azure where the account name is + // a path segment, eg + // https://127.0.0.1// + if (queueNameRequired) + { + if (endpointUri.Segments.Length != 3) + { + throw new ArgumentException(string.Format(SR.AccountNameShouldBePartOfUriPath, endpointUri)); + } + queueName = endpointUri.Segments[2].TrimEnd('/'); + } + else + { + // If the queue name is not required, then there could be 3 or 2 segments. + if (endpointUri.Segments.Length < 2) + { + throw new ArgumentException(string.Format(SR.AccountNameShouldBePartOfHostName, endpointUri)); + } + if (endpointUri.Segments.Length == 3) + { + // Queue name is part of path, so extract it + queueName = endpointUri.Segments[2].TrimEnd('/'); + } + else + { + // Queue name wasn't provided in url, and wasn't required + queueName = String.Empty; + } + } + + accountName = endpointUri.Segments[1].TrimEnd('/'); + } + } + + internal static string ReplaceQueueUriInConnectionString(string connectionString, Uri via) + { + if (!via.Scheme.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + var builder = new UriBuilder(via); + var connectionStringUri = ExtractQueueUriFromConnectionString(connectionString); + if (connectionStringUri != null) + { + // If using an AQS emulator, might be using http. Pull the scheme from the connection string to account for that + builder.Scheme = connectionStringUri.Scheme; + } + else + { + // In case the queue endpoint isn't in the connection string, just presume https. Only case this is wrong is when + // using http with AQS emulator not configured for HTTPS, in which case just add it to the connection string. + builder.Scheme = "https"; + } + + via = builder.Uri; + } + + string[] parts = connectionString.Split(';'); + + for (int i = 0; i < parts.Length; i++) + { + var keyValue = parts[i].Trim().Split('='); + if (keyValue[0].Equals("QueueEndpoint", StringComparison.OrdinalIgnoreCase)) + { + parts[i] = $"QueueEndpoint={via.AbsoluteUri}"; + break; + } + } + + return string.Join(";", parts); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageMessageEncoding.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageMessageEncoding.cs new file mode 100644 index 000000000000..a527f4078fd3 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageMessageEncoding.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.CoreWCF.Azure.StorageQueues +{ + /// + /// The enum which will be used by message encoder. + /// + public enum AzureQueueStorageMessageEncoding + { + /// + /// Indicates using Binary message encoder. + /// + Binary, + + /// + /// Indicates using Text message encoder. + /// + Text, + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageSecurity.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageSecurity.cs new file mode 100644 index 000000000000..40fa3e5d3a4c --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageSecurity.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.CoreWCF.Azure.StorageQueues +{ + /// + /// Class to configure the binding security. + /// + public class AzureQueueStorageSecurity + { + private AzureQueueStorageTransportSecurity _transport; + + /// + /// Initializes a new instance of the class. + /// + public AzureQueueStorageSecurity() : this(new AzureQueueStorageTransportSecurity()) { } + + private AzureQueueStorageSecurity(AzureQueueStorageTransportSecurity azureQueueStorageTransportSecurity) + { + Transport = azureQueueStorageTransportSecurity; + } + + /// + /// Gets an object that contains the transport-level security settings for this binding. + /// + public AzureQueueStorageTransportSecurity Transport + { + get => _transport; + set + { + _transport = value ?? throw new ArgumentNullException(nameof(Transport)); + } + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageTransportSecurity.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageTransportSecurity.cs new file mode 100644 index 000000000000..70b55e170df0 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/AzureQueueStorageTransportSecurity.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.CoreWCF.Azure.StorageQueues.Channels; + +namespace Microsoft.CoreWCF.Azure.StorageQueues +{ + /// + /// Represents the transport security settings for AzureQueueStorageBinding. + /// + public class AzureQueueStorageTransportSecurity + { + private AzureClientCredentialType _clientCredentialType = TransportDefaults.DefaultClientCredentialType; + + /// + /// Gets or sets the type of client credential used for authentication. + /// + /// The client credential type. + public AzureClientCredentialType ClientCredentialType + { + get { return _clientCredentialType; } + set + { + if (!AzureClientCredentialTypeHelper.IsDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _clientCredentialType = value; + } + } + + internal void ConfigureTransportSecurity(AzureQueueStorageTransportBindingElement transport) + { + transport.ClientCredentialType = ClientCredentialType; + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/AzureQueueReceiveContext.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/AzureQueueReceiveContext.cs new file mode 100644 index 000000000000..827a11a2ef86 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/AzureQueueReceiveContext.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues.Models; +using CoreWCF.Channels; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Channels +{ + internal class AzureQueueReceiveContext : ReceiveContext + { + private readonly MessageQueue _queueClient; + private readonly QueueMessage _queueMessage; + private readonly TimeSpan _pollingInterval; + private readonly ILogger _logger; + + public AzureQueueReceiveContext( + MessageQueue queueClient, + QueueMessage queueMessage, + TimeSpan pollingInterval, + ILogger _azureQueueReceiveContextLogger) + { + _queueClient = queueClient; + _queueMessage = queueMessage; + _pollingInterval = pollingInterval; + _logger = _azureQueueReceiveContextLogger; + } + + protected override async Task OnAbandonAsync(CancellationToken token) + { + bool deadLettered = false; + do + { + try + { + await _queueClient.SendToDeadLetterQueueAsync(_queueMessage.MessageId, _queueMessage.Body, token).ConfigureAwait(false); + _logger.LogDebug("OnAbandonAsync: Dead lettered message with id: " + _queueMessage.MessageId); + deadLettered = true; + break; + } + catch (Exception e) + { + _logger.LogWarning("OnAbandonAsync: Exception: " + e.Message); + await Task.Delay(_pollingInterval).ConfigureAwait(false); + } + } + while (token.CanBeCanceled && !token.IsCancellationRequested); + if (deadLettered) + { + await DeleteMessage(token).ConfigureAwait(false); + } + } + + protected override Task OnCompleteAsync(CancellationToken token) + { + return DeleteMessage(token); + } + + private async Task DeleteMessage(CancellationToken token) + { + do + { + try + { + await _queueClient.DeleteMessageAsync(_queueMessage.MessageId, _queueMessage.PopReceipt, token).ConfigureAwait(false); + _logger.LogDebug("DeleteMessage: Deleted message with id: " + _queueMessage.MessageId); + } + catch (Exception e) + { + _logger.LogWarning("DeleteMessage: Exception: " + e.Message); + await Task.Delay(_pollingInterval).ConfigureAwait(false); + } + } + while (token.CanBeCanceled && !token.IsCancellationRequested); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/AzureQueueStorageQueueTransport.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/AzureQueueStorageQueueTransport.cs new file mode 100644 index 000000000000..aa5718306e96 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/AzureQueueStorageQueueTransport.cs @@ -0,0 +1,386 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core.Pipeline; +using Azure.Storage.Queues; +using Azure.Storage.Queues.Models; +using CoreWCF; +using CoreWCF.Channels; +using CoreWCF.Configuration; +using CoreWCF.IdentityModel.Policy; +using CoreWCF.IdentityModel.Selectors; +using CoreWCF.IdentityModel.Tokens; +using CoreWCF.Queue.Common; +using CoreWCF.Security; +using Microsoft.CoreWCF.Azure.Tokens; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Buffers; +using System.Collections.ObjectModel; +using System.IO.Pipelines; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Channels +{ + internal class AzureQueueStorageQueueTransport : IQueueTransport + { + private const string SecurityTokenTypesNamespace = "http://schemas.microsoft.com/ws/2006/05/identitymodel/tokens"; + private const string X509CertificateTokenType = SecurityTokenTypesNamespace + "/X509Certificate"; + private const string RequirementNamespace = "http://schemas.microsoft.com/ws/2006/05/servicemodel/securitytokenrequirement"; + private const string PreferSslCertificateAuthenticatorProperty = RequirementNamespace + "/PreferSslCertificateAuthenticator"; + + private MessageQueue _messageQueue; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private TimeSpan _receiveMessageVisibilityTimeout; + private AzureQueueStorageTransportBindingElement _azureQueueStorageTransportBindingElement; + private readonly ILogger _azureQueueReceiveContextLogger; + private SecurityCredentialsManager _serviceCredentials; + + public AzureQueueStorageQueueTransport(BindingContext context, AzureQueueStorageTransportBindingElement azureQueueStorageTransportBindingElement) + { + _azureQueueStorageTransportBindingElement = azureQueueStorageTransportBindingElement; + _receiveMessageVisibilityTimeout = _azureQueueStorageTransportBindingElement.MaxReceiveTimeout; + var serviceDispatcher = context.BindingParameters.Find(); + _serviceCredentials = serviceDispatcher.Host.Credentials; + BaseAddress = serviceDispatcher.BaseAddress; + var serviceProvider = context.BindingParameters.Find(); + _loggerFactory = serviceProvider.GetRequiredService(); + _logger = _loggerFactory.CreateLogger(); + _azureQueueReceiveContextLogger = _loggerFactory.CreateLogger(); + QueueClientConfigureDelegate = context.BindingParameters.Find>(); + ValidateConfiguration(); + } + + private void ValidateConfiguration() + { + var azureServiceCredentials = _serviceCredentials as AzureServiceCredentials; + if (azureServiceCredentials is null) + { + // It could be a custom credentials implementation and that needs to be used on an async code path + // as the token manager might implement OpenAsync. We can only do a best effort check if it's AzureServiceCredentials + return; + } + + switch (_azureQueueStorageTransportBindingElement.ClientCredentialType) + { + case AzureClientCredentialType.Default: // We don't need an AzureServiceCredentials in this case + return; + case AzureClientCredentialType.ConnectionString: + if (azureServiceCredentials.ConnectionString is null) + { + throw new InvalidOperationException(SR.ConnectionStringNotProvidedOnAzureServiceCredentials); + } + + string queueName; + var connectionStringUri = AzureQueueStorageChannelHelpers.ExtractQueueUriFromConnectionString(azureServiceCredentials.ConnectionString); + AzureQueueStorageChannelHelpers.ExtractAccountAndQueueNameFromUri(connectionStringUri, queueNameRequired: false, out string connectionStringAccountName, out string connectionStringQueueName); + if (!BaseAddress.AbsoluteUri.Equals(AzureQueueStorageTransportBindingElement.DummyNetAqsAddress)) + { + AzureQueueStorageChannelHelpers.ExtractAccountAndQueueNameFromUri(BaseAddress, queueNameRequired: true, out string viaAccountName, out string viaQueueName); + if (!string.IsNullOrEmpty(connectionStringQueueName) && !string.IsNullOrEmpty(viaQueueName) && + !connectionStringQueueName.Equals(viaQueueName, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException(string.Format(SR.ConnectionStringAndViaQueueNameMismatch, connectionStringQueueName, viaQueueName)); + } + queueName = string.IsNullOrEmpty(connectionStringQueueName) ? viaQueueName : connectionStringQueueName; + + if (!string.IsNullOrEmpty(connectionStringAccountName) && !string.IsNullOrEmpty(viaAccountName) && + !connectionStringAccountName.Equals(viaAccountName, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException(string.Format(SR.ConnectionStringAndViaAccountNameMismatch, connectionStringAccountName, viaAccountName)); + } + } + else + { + // Using ConnectionString and address wasn't passed to AddServiceEndpoint so we only have the QueueEndpoint from the ConnectionString + queueName = connectionStringQueueName; + } + + if (string.IsNullOrEmpty(queueName)) + { + throw new ArgumentException(SR.MissingQueueName); + } + + return; + case AzureClientCredentialType.Token: + if (azureServiceCredentials.Token == null) + { + throw new InvalidOperationException(SR.TokenCredentialNotProvidedOnAzureServiceCredentials); + } + return; + case AzureClientCredentialType.Sas: + if (azureServiceCredentials.Sas == null) + { + throw new InvalidOperationException(SR.SasCredentialNotProvidedOnAzureServiceCredentials); + } + return; + case AzureClientCredentialType.StorageSharedKey: + if (azureServiceCredentials.StorageSharedKey == null) + { + throw new InvalidOperationException(SR.StorageSharedKeyCredentialNotProvidedOnAzureServiceCredentials); + } + return; + default: + return; + } + } + + internal QueueMessageEncoding QueueMessageEncoding => _azureQueueStorageTransportBindingElement.QueueMessageEncoding; + + public int ConcurrencyLevel => 1; + + internal Uri BaseAddress { get; } + + internal Func QueueClientConfigureDelegate { get; } + + internal async Task CreateAndOpenTokenProviderAsync(CancellationToken token) + { + var tokenProviderContainer = CreateTokenProvider(token); + + if (tokenProviderContainer != null) + { + await tokenProviderContainer.OpenAsync(token).ConfigureAwait(false); + } + + return tokenProviderContainer; + } + + private SecurityTokenProviderContainer CreateTokenProvider(CancellationToken token) + { + var target = new EndpointAddress(BaseAddress); + var via = BaseAddress; + var securityTokenManager = _serviceCredentials.CreateSecurityTokenManager(); + string scheme = _azureQueueStorageTransportBindingElement.Scheme; + SecurityTokenProvider tokenProvider = null; + switch (_azureQueueStorageTransportBindingElement.ClientCredentialType) + { + case AzureClientCredentialType.Default: + tokenProvider = SecurityUtils.GetDefaultTokenProvider(securityTokenManager, target, via, scheme); + break; + case AzureClientCredentialType.Sas: + tokenProvider = SecurityUtils.GetSasTokenProvider(securityTokenManager, target, via, scheme); + break; + case AzureClientCredentialType.StorageSharedKey: + tokenProvider = SecurityUtils.GetStorageSharedKeyTokenProvider(securityTokenManager, target, via, scheme); + break; + case AzureClientCredentialType.Token: + tokenProvider = SecurityUtils.GetTokenTokenProvider(securityTokenManager, target, via, scheme); + break; + case AzureClientCredentialType.ConnectionString: + tokenProvider = SecurityUtils.GetConnectionStringTokenProvider(securityTokenManager, target, via, scheme); + break; + default: + // The setter for this property should prevent this. + throw new ArgumentOutOfRangeException(nameof(_azureQueueStorageTransportBindingElement.ClientCredentialType), _azureQueueStorageTransportBindingElement.ClientCredentialType, null); + } + + return tokenProvider == null ? null : new SecurityTokenProviderContainer(tokenProvider); + } + + public async ValueTask ReceiveQueueMessageContextAsync(CancellationToken cancellationToken) + { + if (_messageQueue is null) + { + await EnsureMessageQueueAsync(cancellationToken).ConfigureAwait(false); + } + cancellationToken.ThrowIfCancellationRequested(); + _logger.LogDebug("ReceiveQueueMessageContextAsync: ReceiveQueueMessageContextAsync called"); + + QueueMessage queueMessage; + try + { + queueMessage = await _messageQueue.ReceiveMessageAsync(_receiveMessageVisibilityTimeout, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + _logger.LogDebug("ReceiveQueueMessageContextAsync: Request cancelled"); + throw; + } + + if (queueMessage == null) + { + _logger.LogDebug("ReceiveQueueMessageContextAsync: ReceiveMessageAsync returned null"); + return null; + } + + _logger.LogDebug("ReceiveQueueMessageContextAsync: ReceiveMessageAsync returned message with id: " + queueMessage.MessageId); + var reader = PipeReader.Create(new ReadOnlySequence(queueMessage.Body.ToMemory())); + return GetContext(reader, new EndpointAddress(BaseAddress), queueMessage); + } + + private async Task EnsureMessageQueueAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (_messageQueue is null) + { + var messageQueue = new MessageQueue( + this, + _azureQueueStorageTransportBindingElement.PollingInterval, + _loggerFactory.CreateLogger(), + _azureQueueStorageTransportBindingElement.DeadLetterQueueName, + _loggerFactory.CreateLogger()); + await messageQueue.InitAsync(cancellationToken).ConfigureAwait(false); + Interlocked.CompareExchange(ref _messageQueue, messageQueue, null); + } + } + + internal async Task GetQueueClientAsync(Uri via, SecurityTokenProviderContainer tokenProvider, bool forceEncodingNone, CancellationToken cancellationToken) + { + QueueClientOptions queueClientOptions = BuildQueueClientOptions(forceEncodingNone); + switch (_azureQueueStorageTransportBindingElement.ClientCredentialType) + { + case AzureClientCredentialType.Default: + case AzureClientCredentialType.Token: + return await SecurityUtils.CreateQueueClientWithTokenCredentialAsync(tokenProvider, via, queueClientOptions, cancellationToken).ConfigureAwait(false); + case AzureClientCredentialType.Sas: + return await SecurityUtils.CreateQueueClientWithSasCredentialAsync(tokenProvider, via, queueClientOptions, cancellationToken).ConfigureAwait(false); + case AzureClientCredentialType.StorageSharedKey: + return await SecurityUtils.CreateQueueClientWithStorageSharedKeyCredentialAsync(tokenProvider, via, queueClientOptions, cancellationToken).ConfigureAwait(false); + case AzureClientCredentialType.ConnectionString: + via = await UseConnectionStringQueueUrifNeededAsync(tokenProvider, via, cancellationToken).ConfigureAwait(false); + return await SecurityUtils.CreateQueueClientWithConnectionStringAsync(tokenProvider, via, queueClientOptions, cancellationToken).ConfigureAwait(false); + default: + return null; + } + } + + private async Task UseConnectionStringQueueUrifNeededAsync(SecurityTokenProviderContainer tokenProvider, Uri via, CancellationToken cancellationToken) + { + if (via.AbsoluteUri.Equals(AzureQueueStorageTransportBindingElement.DummyNetAqsAddress)) + { + var result = await tokenProvider.TokenProvider.GetTokenAsync(cancellationToken).ConfigureAwait(false) as ConnectionStringSecurityToken; + return AzureQueueStorageChannelHelpers.ExtractQueueUriFromConnectionString(result.ConnectionString); + } + + return via; + } + + internal Task GetDeadLetterQueueClientAsync(Uri via, SecurityTokenProviderContainer tokenProvider, bool forceEncodingNone, CancellationToken cancellationToken) + { + switch (_azureQueueStorageTransportBindingElement.ClientCredentialType) + { + case AzureClientCredentialType.ConnectionString: + QueueClientOptions queueClientOptions = BuildQueueClientOptions(forceEncodingNone); + return SecurityUtils.CreateDeadLetterQueueClientWithConnectionStringAsync(tokenProvider, via, queueClientOptions, cancellationToken); + default: + return GetQueueClientAsync(via, tokenProvider, forceEncodingNone, cancellationToken); + } + } + + private QueueClientOptions BuildQueueClientOptions(bool forceEncodingNone) + { + QueueClientOptions options = new(); + var azureClientCredentials = _serviceCredentials as AzureServiceCredentials; + if (azureClientCredentials != null) + { + options.Audience = azureClientCredentials.Audience; + options.EnableTenantDiscovery = azureClientCredentials.EnableTenantDiscovery; + } + + options.MessageEncoding = forceEncodingNone ? QueueMessageEncoding.None : QueueMessageEncoding; + + var certificateValidationCallback = GetServiceCertificateValidationCallback(); + if (certificateValidationCallback != null) + { + HttpClientHandler httpClientHandler = new HttpClientHandler(); + httpClientHandler.ServerCertificateCustomValidationCallback = certificateValidationCallback; + HttpClient httpClient = new(httpClientHandler); + HttpClientTransport httpClientTransport = new HttpClientTransport(httpClient); + options.Transport = httpClientTransport; + } + + options.MessageDecodingFailed += Options_MessageDecodingFailed; + + return options; + } + + private async Task Options_MessageDecodingFailed(QueueMessageDecodingFailedEventArgs args) + { + if (args.ReceivedMessage != null) + { + bool deadLettered = false; + try + { + await _messageQueue.SendRawToDeadLetterQueueAsync(args.ReceivedMessage.MessageId, args.ReceivedMessage.Body, default).ConfigureAwait(false); + _logger.LogDebug("MessageDecodingFailed: Dead lettered message with id: " + args.ReceivedMessage.MessageId); + deadLettered = true; + } + catch (Exception e) + { + _logger.LogWarning("MessageDecodingFailed: Exception: " + e.Message); + } + if (deadLettered) + { + try + { + await _messageQueue.DeleteMessageAsync(args.ReceivedMessage.MessageId, args.ReceivedMessage.PopReceipt).ConfigureAwait(false); + _logger.LogDebug("MessageDecodingFailed: Deleted message with id: " + args.ReceivedMessage.MessageId); + } + catch (Exception e) + { + _logger.LogWarning("MessageDecodingFailed: Exception while deleting message with id " + args.ReceivedMessage.MessageId + " : " + e.Message); + } + } + } + } + + private Func GetServiceCertificateValidationCallback() + { + var securityTokenManager = _serviceCredentials.CreateSecurityTokenManager(); + InitiatorServiceModelSecurityTokenRequirement serverCertRequirement = new() + { + TokenType = X509CertificateTokenType, + RequireCryptographicToken = true, + KeyUsage = SecurityKeyUsage.Exchange, + TransportScheme = _azureQueueStorageTransportBindingElement.Scheme + }; + serverCertRequirement.Properties[PreferSslCertificateAuthenticatorProperty] = true; + + var securityTokenAuthenticator = securityTokenManager.CreateSecurityTokenAuthenticator(serverCertRequirement, out SecurityTokenResolver dummy) as X509SecurityTokenAuthenticator; + if (securityTokenAuthenticator == null) + { + return null; + } + + bool RemoteCertificateValidationCallback(HttpRequestMessage sender, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + try + { + SecurityToken token = new X509SecurityToken(certificate); + ReadOnlyCollection authorizationPolicies = securityTokenAuthenticator.ValidateToken(token); + return true; + } + catch (Exception) + { + return false; + } + } + + return RemoteCertificateValidationCallback; + } + + private QueueMessageContext GetContext(PipeReader reader, EndpointAddress endpointAddress, QueueMessage queueMessage) + { + var context = new QueueMessageContext + { + QueueMessageReader = reader, + LocalAddress = endpointAddress + }; + + var receiveContext = new AzureQueueReceiveContext( + _messageQueue, + queueMessage, + _azureQueueStorageTransportBindingElement.PollingInterval, + _azureQueueReceiveContextLogger); + context.ReceiveContext = receiveContext; + + return context; + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/AzureQueueStorageTransportBindingElement.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/AzureQueueStorageTransportBindingElement.cs new file mode 100644 index 000000000000..e94650aae6e6 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/AzureQueueStorageTransportBindingElement.cs @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using CoreWCF.Channels; +using CoreWCF.Configuration; +using CoreWCF.Queue.Common; +using CoreWCF.Queue.Common.Configuration; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Channels +{ + /// + /// Class that represents Azure Queue Storage transport binding element. + /// + public class AzureQueueStorageTransportBindingElement : QueueBaseTransportBindingElement, ITransportServiceBuilder + { + internal const string DummyNetAqsAddress = "net.aqs://tempuri.org/"; + private long _maxReceivedMessageSize; + private TimeSpan _receiveMessagevisibilityTimeout = TransportDefaults.ReceiveMessagevisibilityTimeout; + private AzureClientCredentialType _clientCredentialType; + private TimeSpan _pollingInterval = TimeSpan.FromSeconds(1); + private QueueMessageEncoding _queueMessageEncoding; + private string _deadLetterQueueName; + + /// + /// Creates a new instance of the AzureQueueStorageTransportBindingElement Class. + /// + public AzureQueueStorageTransportBindingElement() + { + ClientCredentialType = AzureClientCredentialType.Default; + } + + /// + /// Creates a new instance of this class from an existing instance. + /// + protected AzureQueueStorageTransportBindingElement(AzureQueueStorageTransportBindingElement other) : base(other) + { + ClientCredentialType = other.ClientCredentialType; + MaxReceivedMessageSize = other.MaxReceivedMessageSize; + DeadLetterQueueName = other.DeadLetterQueueName; + PollingInterval = other.PollingInterval; + QueueMessageEncoding = other.QueueMessageEncoding; + } + + /// + /// Overridden method to return a copy of the binding AzureQueueStorageTransportBindingElement object. + /// + public override BindingElement Clone() + { + return new AzureQueueStorageTransportBindingElement(); + } + + /// + /// Gets a property from the specified BindingContext. + /// + public override T GetProperty(BindingContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (typeof(T) == typeof(ITransportServiceBuilder)) + { + return (T)(object)this; + } + + return base.GetProperty(context); + } + + /// + /// Configures CoreWCF with a dummy net.aqs base address in the case that the endpoint uri is provided in a connection string. + /// + /// The ASP.NET Core application builder for the service + void ITransportServiceBuilder.Configure(IApplicationBuilder app) + { + // When using a ConnectionString, the queue endpoint url could be provided + // in the connection string so the endpoint Uri passed to AddServiceEndpoint + // could be the emptry string. In that case, CoreWCF will expect there to be + // a configured base address for this binding. If there isn't a net.aqs base + // address already configured, we add a dummy one. + if (ClientCredentialType == AzureClientCredentialType.ConnectionString) + { + var serviceBuilder = app.ApplicationServices.GetRequiredService(); + if (!serviceBuilder.BaseAddresses.Where(u => u.Scheme == "net.aqs").Any()) + { + var dummyBaseAddress = new Uri(DummyNetAqsAddress); + serviceBuilder.BaseAddresses.Add(dummyBaseAddress); + } + } + } + + /// + /// Method to build transport pump from binding context. + /// + public override QueueTransportPump BuildQueueTransportPump(BindingContext context) + { + IQueueTransport queueTransport = CreateMyQueueTransport(context); + return QueueTransportPump.CreateDefaultPump(queueTransport); + } + + private IQueueTransport CreateMyQueueTransport(BindingContext context) + { + return new AzureQueueStorageQueueTransport(context, this); + } + + /// + /// Gets or sets the type of client credential used for authentication when communicating with Azure. + /// + /// The client credential type. + public AzureClientCredentialType ClientCredentialType + { + get { return _clientCredentialType; } + set + { + if (!AzureClientCredentialTypeHelper.IsDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _clientCredentialType = value; + } + } + + /// + /// Gets the URI scheme for the transport. + /// + public override string Scheme => "net.aqs"; + + /// + /// Gets or sets the maximum allowable message size, in bytes, that can be received. The default value is + /// + public override long MaxReceivedMessageSize + { + get { return _maxReceivedMessageSize; } + set + { + if (value < 0 || value > 8000L) + { + throw new ArgumentOutOfRangeException(nameof(MaxReceivedMessageSize), SR.MaxReceivedMessageSizeOutOfRange); + } + + _maxReceivedMessageSize = value; + } + } + + /// + /// Gets or Sets the receive Message Visibility Timeout for the queue. The default value is 15 minutes. + /// + public TimeSpan MaxReceiveTimeout + { + get { return _receiveMessagevisibilityTimeout; } + set + { + if (value < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(MaxReceiveTimeout), SR.MaxReceiveTimeoutGreaterThanOrEqualToZero); + _receiveMessagevisibilityTimeout = value; + } + } + + /// + /// Gets or sets the name of the Azure Queue Storage dead letter queue. + /// + public string DeadLetterQueueName + { + get => _deadLetterQueueName; + set + { + if (!ValidateDeadLetterQueueName(value)) throw new ArgumentException(SR.InvalidDeadLetterQueueName, nameof(value)); + _deadLetterQueueName = value; + } + } + + /// + /// Gets or sets the queue polling interval. See <a href="https://learn.microsoft.com/azure/storage/queues/storage-performance-checklist#queue-polling-interval">Performance and scalability checklist for Queue Storage.</a> + /// The default value is 1 second; + /// + public TimeSpan PollingInterval + { + get => _pollingInterval; + set + { + if (value <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(PollingInterval), SR.PollingIntervalMustBePositive); + _pollingInterval = value; + } + } + + /// + /// Gets the QueueMessageEncoding for the transport. + /// + public QueueMessageEncoding QueueMessageEncoding + { + get => _queueMessageEncoding; + set + { + if (value != QueueMessageEncoding.None && value != QueueMessageEncoding.Base64) throw new ArgumentOutOfRangeException(nameof(QueueMessageEncoding)); + _queueMessageEncoding = value; + } + } + + private bool ValidateDeadLetterQueueName(string deadLetterQueueName) + { + if (deadLetterQueueName == null) + return false; + + if (deadLetterQueueName.Length < 3 || deadLetterQueueName.Length > 63) + return false; + + if (deadLetterQueueName[0] == '-' || deadLetterQueueName[deadLetterQueueName.Length - 1] == '-') + return false; + + bool previousDash = false; + for (int i = 0; i < deadLetterQueueName.Length; i++) + { + if (deadLetterQueueName[i] == '-') + { + if (previousDash) return false; + previousDash = true; + continue; + } + + if (!(char.IsLower(deadLetterQueueName[i]) || char.IsDigit(deadLetterQueueName[i]))) + return false; + + previousDash = false; + } + + return true; + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/DeadLetterQueue.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/DeadLetterQueue.cs new file mode 100644 index 000000000000..2dafb3ee4339 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/DeadLetterQueue.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure; +using Azure.Storage.Queues; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Channels +{ + internal class DeadLetterQueue + { + private QueueClient _client; + private readonly ILogger _logger; + + public DeadLetterQueue( + QueueClient queueClient, + ILogger logger) + { + _logger = logger; + _logger.LogInformation("DeadLetterQueue constructor: QueueEndPoint: " + queueClient.Uri); + _client = queueClient; + } + + public async Task SendMessageAsync(BinaryData binaryData, CancellationToken token) + { + try + { + await _client.SendMessageAsync(binaryData, default, default, token).ConfigureAwait(false); + _logger.LogInformation(Task.CurrentId + " DeadLetterQueue SendMessageAsync: Sent message with data length: " + binaryData.ToMemory().Length); + } + catch (Exception e) + { + _logger.LogDebug(Task.CurrentId + "DeadLetterQueue SendMessageAsync: SendMessageAsync failed with error message: " + e.Message); + throw AzureQueueStorageChannelHelpers.ConvertTransferException(e); + } + } + + public Task CreateIfNotExistsAsync(IDictionary metadata = default, CancellationToken cancellationToken = default) + { + return _client.CreateIfNotExistsAsync(metadata, cancellationToken); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/InitiatorServiceModelSecurityTokenRequirement.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/InitiatorServiceModelSecurityTokenRequirement.cs new file mode 100644 index 000000000000..065bfbe68428 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/InitiatorServiceModelSecurityTokenRequirement.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CoreWCF; +using CoreWCF.Security.Tokens; +using System; +using System.Globalization; +using System.Text; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Channels +{ + // If/when CoreWCF implementation is made public, this internal implementation can be removed + internal sealed class InitiatorServiceModelSecurityTokenRequirement : ServiceModelSecurityTokenRequirement + { + public InitiatorServiceModelSecurityTokenRequirement() : base() + { + Properties.Add(IsInitiatorProperty, (object)true); + } + + public EndpointAddress TargetAddress + { + get + { + return GetPropertyOrDefault(TargetAddressProperty, null); + } + set + { + Properties[TargetAddressProperty] = value; + } + } + + public Uri Via + { + get + { + return GetPropertyOrDefault(ViaProperty, null); + } + set + { + Properties[ViaProperty] = value; + } + } + + internal TValue GetPropertyOrDefault(string propertyName, TValue defaultValue) + { + if (!TryGetProperty(propertyName, out TValue result)) + { + result = defaultValue; + } + return result; + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "{0}:", GetType().ToString())); + foreach (string propertyName in Properties.Keys) + { + object propertyValue = Properties[propertyName]; + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "PropertyName: {0}", propertyName)); + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "PropertyValue: {0}", propertyValue)); + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "---")); + } + return sb.ToString().Trim(); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/MessageQueue.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/MessageQueue.cs new file mode 100644 index 000000000000..38ddb065f090 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/MessageQueue.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure; +using Azure.Storage.Queues; +using Azure.Storage.Queues.Models; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Channels +{ + internal class MessageQueue + { + private QueueClient _client; + private TimeSpan _pollingInterval; + private string _deadLetterQueueName; + private ILogger _dlqLogger; + private AzureQueueStorageQueueTransport _parent; + private DeadLetterQueue _deadLetterQueue; + private DeadLetterQueue _rawDeadLetterQueue; + private readonly ILogger _logger; + + public MessageQueue( + AzureQueueStorageQueueTransport azureQueueStorageQueueTransport, + TimeSpan pollingInterval, + ILogger logger, + string deadLetterQueueName, + ILogger dlqLogger) + { + _logger = logger; + _parent = azureQueueStorageQueueTransport; + _pollingInterval = pollingInterval; + _deadLetterQueueName = deadLetterQueueName; + _dlqLogger = dlqLogger; + } + + internal async Task InitAsync(CancellationToken cancellationToken) + { + if (_client is not null) + return; + + var tokenContainer = await _parent.CreateAndOpenTokenProviderAsync(cancellationToken).ConfigureAwait(false); + QueueClient queueClient = await _parent.GetQueueClientAsync(_parent.BaseAddress, tokenContainer, forceEncodingNone: false, cancellationToken).ConfigureAwait(false); + Uri deadLetterQueueUri = CreateDeadLetterQueueUri(queueClient.Uri, _deadLetterQueueName); + var deadLetterQueueClient = await _parent.GetDeadLetterQueueClientAsync(deadLetterQueueUri, tokenContainer, forceEncodingNone: false, cancellationToken).ConfigureAwait(false); + bool createRawDlqClient = _parent.QueueMessageEncoding == QueueMessageEncoding.Base64; + var rawDeadLetterQueueClient = createRawDlqClient ? await _parent.GetDeadLetterQueueClientAsync(deadLetterQueueUri, tokenContainer, forceEncodingNone: true, cancellationToken).ConfigureAwait(false) + : deadLetterQueueClient; + if (_parent.QueueClientConfigureDelegate is not null) + { + var configuredQueueClient = _parent.QueueClientConfigureDelegate(queueClient); + if (configuredQueueClient is not null) + { + queueClient = configuredQueueClient; + } + + configuredQueueClient = _parent.QueueClientConfigureDelegate(deadLetterQueueClient); + if (configuredQueueClient is not null) + { + deadLetterQueueClient = configuredQueueClient; + } + + if (createRawDlqClient) // If we didn't create it, we're using the regular DLQ client and don't need to do this + { + configuredQueueClient = _parent.QueueClientConfigureDelegate(rawDeadLetterQueueClient); + if (configuredQueueClient is not null) + { + rawDeadLetterQueueClient = configuredQueueClient; + } + } + } + _client = queueClient; + _deadLetterQueue = new DeadLetterQueue(deadLetterQueueClient, _dlqLogger); + _rawDeadLetterQueue = createRawDlqClient ? new DeadLetterQueue(rawDeadLetterQueueClient, _dlqLogger) : _deadLetterQueue; + await CreateIfNotExistsAsync(null, cancellationToken).ConfigureAwait(false); + await _deadLetterQueue.CreateIfNotExistsAsync(null, cancellationToken ).ConfigureAwait(false); + } + + private Uri CreateDeadLetterQueueUri(Uri baseAddress, string deadLetterQueueName) + { + var uriBuilder = new UriBuilder(baseAddress); + var splitPath = uriBuilder.Path.Split(new[] { '/' }, StringSplitOptions.None); + splitPath[splitPath.Length - 1] = deadLetterQueueName; + uriBuilder.Path = string.Join("/", splitPath); + return uriBuilder.Uri; + } + + public QueueClient QueueClient { get => _client; set => _client = value; } + + public Task DeleteMessageAsync(string messageId, string popReceipt, CancellationToken cancellationToken = default) + { + _logger.LogInformation(Task.CurrentId + " Deleting message with Id " + messageId + " from Queue"); + return _client.DeleteMessageAsync(messageId, popReceipt, cancellationToken); + } + + public async Task ReceiveMessageAsync(TimeSpan? visibilityTimeout = null, CancellationToken cancellationToken = default) + { + Debug.Assert(_client is not null); + QueueMessage message = null; + try + { + message = await _client.ReceiveMessageAsync(visibilityTimeout, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogDebug(Task.CurrentId + "MessageQueue ReceiveMessageAsync: ReceiveMessageAsync failed with error message: " + e.Message); + } + if (message == null) + { + await Task.Delay(_pollingInterval).ConfigureAwait(false); + } + if (message == null) + { + _logger.LogInformation(Task.CurrentId + " MessageQueue ReceiveMessageAsync: Received null message"); + } + else + { + _logger.LogInformation(Task.CurrentId + " MessageQueue ReceiveMessageAsync: Received message with id: " + message.MessageId); + } + return message; + } + + public Task CreateIfNotExistsAsync(IDictionary metadata = default, CancellationToken cancellationToken = default) + { + return _client.CreateIfNotExistsAsync(metadata, cancellationToken); + } + + internal Task SendToDeadLetterQueueAsync(string messageId, BinaryData body, CancellationToken token) + { + _dlqLogger.LogDebug(Task.CurrentId + " Sending message with Id " + messageId + " to DLQ"); + return _deadLetterQueue.SendMessageAsync(body, token); + } + + // This variant always uses MessageEncoding.None. This is needed for when the encoding of the message doesn't + // match how the CoreWCF endpoint has been configured and the message is sent to the MessageDecodingFailed + // event handler. If the endpoint is configured to Base64 and an unencded message is received, using a QueueClient + // configured for Base64 will cause the unencoded message to be encoded before placing in the queue. This can cause + // 2 problems. First, it becomes unclear why the message was rejected (if using a custom binding with Base64 MessageEncoding + // and TextMessageEncodingBindingElement). Second, Base64 encoding grows a message size by ~30% and then it might not + // be possible to put it in the DLQ. + internal Task SendRawToDeadLetterQueueAsync(string messageId, BinaryData body, CancellationToken token) + { + _dlqLogger.LogDebug(Task.CurrentId + " Sending raw message with Id " + messageId + " to DLQ"); + return _rawDeadLetterQueue.SendMessageAsync(body, token); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/SecurityUtils.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/SecurityUtils.cs new file mode 100644 index 000000000000..ab3631c9ae6a --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/SecurityUtils.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using CoreWCF; +using CoreWCF.IdentityModel.Selectors; +using CoreWCF.IdentityModel.Tokens; +using Microsoft.CoreWCF.Azure.Tokens; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Channels +{ + internal class SecurityUtils + { + public static SecurityTokenProvider GetDefaultTokenProvider(SecurityTokenManager tokenManager, EndpointAddress target, Uri via, + string transportScheme) + { + if (tokenManager != null) + { + var tokenRequirement = CreateTokenRequirement(target, via, transportScheme, AzureSecurityTokenTypes.DefaultTokenType); + return tokenManager.CreateSecurityTokenProvider(tokenRequirement); + } + + return null; + } + + public static SecurityTokenProvider GetSasTokenProvider(SecurityTokenManager tokenManager, EndpointAddress target, Uri via, + string transportScheme) + { + if (tokenManager != null) + { + var tokenRequirement = CreateTokenRequirement(target, via, transportScheme, AzureSecurityTokenTypes.SasTokenType); + return tokenManager.CreateSecurityTokenProvider(tokenRequirement); + } + + return null; + } + + public static SecurityTokenProvider GetStorageSharedKeyTokenProvider(SecurityTokenManager tokenManager, EndpointAddress target, Uri via, + string transportScheme) + { + if (tokenManager != null) + { + var tokenRequirement = CreateTokenRequirement(target, via, transportScheme, AzureSecurityTokenTypes.StorageSharedKeyTokenType); + return tokenManager.CreateSecurityTokenProvider(tokenRequirement); + } + + return null; + } + + public static SecurityTokenProvider GetTokenTokenProvider(SecurityTokenManager tokenManager, EndpointAddress target, Uri via, + string transportScheme) + { + if (tokenManager != null) + { + var tokenRequirement = CreateTokenRequirement(target, via, transportScheme, AzureSecurityTokenTypes.TokenTokenType); + return tokenManager.CreateSecurityTokenProvider(tokenRequirement); + } + + return null; + } + + public static SecurityTokenProvider GetConnectionStringTokenProvider(SecurityTokenManager tokenManager, EndpointAddress target, Uri via, + string transportScheme) + { + if (tokenManager != null) + { + var tokenRequirement = CreateTokenRequirement(target, via, transportScheme, AzureSecurityTokenTypes.ConnectionStringTokenType); + return tokenManager.CreateSecurityTokenProvider(tokenRequirement); + } + + return null; + } + + private static SecurityTokenRequirement CreateTokenRequirement(EndpointAddress target, Uri via, string transportScheme, string tokenType) + { + InitiatorServiceModelSecurityTokenRequirement azureTokenRequirement = new InitiatorServiceModelSecurityTokenRequirement(); + azureTokenRequirement.TokenType = tokenType; + azureTokenRequirement.RequireCryptographicToken = false; + azureTokenRequirement.TransportScheme = transportScheme; + azureTokenRequirement.TargetAddress = target; + azureTokenRequirement.Via = via; + return azureTokenRequirement; + } + + internal static Task OpenTokenProviderIfRequiredAsync(SecurityTokenProvider tokenProvider, CancellationToken token) + { + if (tokenProvider is ICommunicationObject communicationObject && communicationObject != null) + { + return communicationObject.OpenAsync(); + } + + return Task.CompletedTask; + } + + internal static Task CloseTokenProviderIfRequiredAsync(SecurityTokenProvider tokenProvider, CancellationToken token) + { + if (tokenProvider is ICommunicationObject communicationObject && communicationObject != null) + { + return communicationObject.CloseAsync(); + } + + return Task.CompletedTask; + } + + internal static void AbortTokenProviderIfRequired(SecurityTokenProvider tokenProvider) + { + if (tokenProvider is ICommunicationObject communicationObject && communicationObject != null) + { + try + { + communicationObject.Abort(); + } + catch (CommunicationException) + { + } + } + } + + private static async Task GetTokenAsync(SecurityTokenProvider tokenProvider, CancellationToken token) where T : SecurityToken + { + SecurityToken result = await tokenProvider.GetTokenAsync(token).ConfigureAwait(false); + if ((result != null) && result is not T) + { + throw new InvalidOperationException(String.Format(SR.InvalidTokenProvided, tokenProvider.GetType(), typeof(T))); + } + return result as T; + } + + internal static async Task CreateQueueClientWithTokenCredentialAsync(SecurityTokenProviderContainer tokenProvider, Uri via, QueueClientOptions queueClientOptions, CancellationToken token) + { + var provider = tokenProvider.TokenProvider; + var endpointUri = new UriBuilder(via) { Scheme = "https" }.Uri; + TokenCredentialSecurityToken result = await GetTokenAsync(provider, token).ConfigureAwait(false); + var queueClient = new QueueClient(endpointUri, result.TokenCredential, queueClientOptions); + return queueClient; + } + + internal static async Task CreateQueueClientWithSasCredentialAsync(SecurityTokenProviderContainer tokenProvider, Uri via, QueueClientOptions queueClientOptions, CancellationToken token) + { + var provider = tokenProvider.TokenProvider; + var endpointUri = new UriBuilder(via) { Scheme = "https" }.Uri; + SasSecurityToken result = await GetTokenAsync(provider, token).ConfigureAwait(false); + var queueClient = new QueueClient(endpointUri, result.SasCredential, queueClientOptions); + return queueClient; + } + + internal static async Task CreateQueueClientWithStorageSharedKeyCredentialAsync(SecurityTokenProviderContainer tokenProvider, Uri via, QueueClientOptions queueClientOptions, CancellationToken token) + { + var provider = tokenProvider.TokenProvider; + var endpointUri = new UriBuilder(via) { Scheme = "https" }.Uri; + StorageSharedKeySecurityToken result = await GetTokenAsync(provider, token).ConfigureAwait(false); + var queueClient = new QueueClient(endpointUri, result.StorageSharedKeyCredential, queueClientOptions); + return queueClient; + } + + internal static async Task CreateQueueClientWithConnectionStringAsync(SecurityTokenProviderContainer tokenProvider, Uri via, QueueClientOptions queueClientOptions, CancellationToken token) + { + var provider = tokenProvider.TokenProvider; + ConnectionStringSecurityToken result = await GetTokenAsync(provider, token).ConfigureAwait(false); + var connectionStringUri = AzureQueueStorageChannelHelpers.ExtractQueueUriFromConnectionString(result.ConnectionString); + AzureQueueStorageChannelHelpers.ExtractAccountAndQueueNameFromUri(connectionStringUri, queueNameRequired: false, out string connectionStringAccountName, out string connectionStringQueueName); + AzureQueueStorageChannelHelpers.ExtractAccountAndQueueNameFromUri(via, queueNameRequired: true, out string viaAccountName, out string viaQueueName); + string queueName; + if (!string.IsNullOrEmpty(connectionStringQueueName) && !string.IsNullOrEmpty(viaQueueName) && + !connectionStringQueueName.Equals(viaQueueName, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException(string.Format(SR.ConnectionStringAndViaQueueNameMismatch, connectionStringQueueName, viaQueueName)); + } + queueName = string.IsNullOrEmpty(connectionStringQueueName) ? viaQueueName : connectionStringQueueName; + if (string.IsNullOrEmpty(queueName)) + { + throw new ArgumentException(SR.MissingQueueName); + } + if (!string.IsNullOrEmpty(connectionStringAccountName) && !string.IsNullOrEmpty(viaAccountName) && + !connectionStringAccountName.Equals(viaAccountName, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException(string.Format(SR.ConnectionStringAndViaAccountNameMismatch, connectionStringAccountName, viaAccountName)); + } + + var queueClient = new QueueClient(result.ConnectionString, queueName, queueClientOptions); + return queueClient; + } + + internal static async Task CreateDeadLetterQueueClientWithConnectionStringAsync(SecurityTokenProviderContainer tokenProvider, Uri via, QueueClientOptions queueClientOptions, CancellationToken token) + { + // Any necessary validation has already been done when creating the main QueueClient instance. + var provider = tokenProvider.TokenProvider; + ConnectionStringSecurityToken result = await GetTokenAsync(provider, token).ConfigureAwait(false); + var deadLetterQueueConnectionString = AzureQueueStorageChannelHelpers.ReplaceQueueUriInConnectionString(result.ConnectionString, via); + AzureQueueStorageChannelHelpers.ExtractAccountAndQueueNameFromUri(via, queueNameRequired: false, out string connectionStringAccountName, out string connectionStringQueueName); + string queueName = connectionStringQueueName; + var queueClient = new QueueClient(deadLetterQueueConnectionString, connectionStringQueueName, queueClientOptions); + return queueClient; + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/TransportDefaults.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/TransportDefaults.cs new file mode 100644 index 000000000000..f86bc193f3d1 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/StorageQueues/Channels/TransportDefaults.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Channels +{ + internal class TransportDefaults + { + //max size of Azure Queue message can be upto 64KB + internal const long DefaultMaxMessageSize = 8000L; + + internal static readonly TimeSpan ReceiveMessagevisibilityTimeout = TimeSpan.FromMinutes(15); + internal const AzureClientCredentialType DefaultClientCredentialType = AzureClientCredentialType.Default; + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/AzureSecurityTokenTypes.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/AzureSecurityTokenTypes.cs new file mode 100644 index 000000000000..1214c414b351 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/AzureSecurityTokenTypes.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.CoreWCF.Azure.Tokens +{ + // Consider making this public in the future similar to System.ServiceModel.Security.Tokens.ServiceModelSecurityTokenTypes + internal static class AzureSecurityTokenTypes + { + public const string Namespace = "http://schemas.microsoft.com/ws/2006/05/servicemodel/tokens/Azure"; + public const string DefaultTokenType = Namespace + "/Default"; + public const string SasTokenType = Namespace + "/Sas"; + public const string StorageSharedKeyTokenType = Namespace + "/StorageSharedKey"; + public const string TokenTokenType = Namespace + "/Token"; + public const string ConnectionStringTokenType = Namespace + "/ConnectionString"; + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/ConnectionStringSecurityToken.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/ConnectionStringSecurityToken.cs new file mode 100644 index 000000000000..013a96ad7a4c --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/ConnectionStringSecurityToken.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CoreWCF.IdentityModel.Tokens; +using System; +using System.Collections.ObjectModel; + +namespace Microsoft.CoreWCF.Azure.Tokens +{ + internal class ConnectionStringSecurityToken : SecurityToken + { + public ConnectionStringSecurityToken(string connectionString) : this(connectionString, SecurityTokenUtils.CreateUniqueId()) { } + + public ConnectionStringSecurityToken(string connectionString, string id) + { + ConnectionString = connectionString; + Id = id; + ValidFrom = DateTime.UtcNow; + } + + public string ConnectionString { get; } + public override string Id { get; } + public override ReadOnlyCollection SecurityKeys => SecurityTokenUtils.EmptyReadOnlyCollection.Instance; + public override DateTime ValidFrom { get; } + public override DateTime ValidTo => SecurityTokenUtils.MaxUtcDateTime; + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/SasSecurityToken.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/SasSecurityToken.cs new file mode 100644 index 000000000000..b754b56a9cdd --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/SasSecurityToken.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure; +using CoreWCF.IdentityModel.Tokens; +using System; +using System.Collections.ObjectModel; + +namespace Microsoft.CoreWCF.Azure.Tokens +{ + internal class SasSecurityToken : SecurityToken + { + public SasSecurityToken(AzureSasCredential sasCredential) : this(sasCredential, SecurityTokenUtils.CreateUniqueId()) { } + + public SasSecurityToken(AzureSasCredential sasCredential, string id) + { + SasCredential = sasCredential; + Id = id; + ValidFrom = DateTime.UtcNow; + } + + public AzureSasCredential SasCredential { get; } + public override string Id { get; } + public override ReadOnlyCollection SecurityKeys => SecurityTokenUtils.EmptyReadOnlyCollection.Instance; + public override DateTime ValidFrom { get; } + public override DateTime ValidTo => SecurityTokenUtils.MaxUtcDateTime; + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/SecurityTokenProviderContainer.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/SecurityTokenProviderContainer.cs new file mode 100644 index 000000000000..40f428622dd4 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/SecurityTokenProviderContainer.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CoreWCF.IdentityModel.Selectors; +using Microsoft.CoreWCF.Azure.StorageQueues.Channels; +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CoreWCF.Azure.Tokens +{ + internal class SecurityTokenProviderContainer + { + public SecurityTokenProviderContainer(SecurityTokenProvider tokenProvider) + { + if (tokenProvider is null) throw new ArgumentNullException(nameof(tokenProvider)); + TokenProvider = tokenProvider; + } + + public SecurityTokenProvider TokenProvider { get; } + + public Task OpenAsync(CancellationToken token) + { + return SecurityUtils.OpenTokenProviderIfRequiredAsync(TokenProvider, token); + } + + public Task CloseAsync(CancellationToken token) + { + return SecurityUtils.CloseTokenProviderIfRequiredAsync(TokenProvider, token); + } + + public void Abort() + { + SecurityUtils.AbortTokenProviderIfRequired(TokenProvider); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/SecurityTokenUtils.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/SecurityTokenUtils.cs new file mode 100644 index 000000000000..88e801c4c17d --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/SecurityTokenUtils.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using System.Threading; + +namespace Microsoft.CoreWCF.Azure.Tokens +{ + internal static class SecurityTokenUtils + { + private static long s_nextId = 0; + private static string s_commonPrefix = "uuid-" + Guid.NewGuid().ToString() + "-"; + + internal static string CreateUniqueId() => s_commonPrefix + Interlocked.Increment(ref s_nextId); + + public static DateTime MaxUtcDateTime + { + get + { + // + and - TimeSpan.TicksPerDay is to compensate the DateTime.ParseExact (to localtime) overflow. + return new DateTime(DateTime.MaxValue.Ticks - TimeSpan.TicksPerDay, DateTimeKind.Utc); + } + } + + internal static class EmptyReadOnlyCollection + { + public static ReadOnlyCollection Instance = new ReadOnlyCollection(Array.Empty()); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/StorageSharedKeySecurityToken.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/StorageSharedKeySecurityToken.cs new file mode 100644 index 000000000000..eff15883d7ab --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/StorageSharedKeySecurityToken.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage; +using CoreWCF.IdentityModel.Tokens; +using System; +using System.Collections.ObjectModel; + +namespace Microsoft.CoreWCF.Azure.Tokens +{ + internal class StorageSharedKeySecurityToken : SecurityToken + { + public StorageSharedKeySecurityToken(StorageSharedKeyCredential storageSharedKeyCredential) : this(storageSharedKeyCredential, SecurityTokenUtils.CreateUniqueId()) { } + + public StorageSharedKeySecurityToken(StorageSharedKeyCredential storageSharedKeyCredential, string id) + { + StorageSharedKeyCredential = storageSharedKeyCredential; + Id = id; + ValidFrom = DateTime.UtcNow; + } + + public StorageSharedKeyCredential StorageSharedKeyCredential { get; } + public override string Id { get; } + public override ReadOnlyCollection SecurityKeys => SecurityTokenUtils.EmptyReadOnlyCollection.Instance; + public override DateTime ValidFrom { get; } + public override DateTime ValidTo => SecurityTokenUtils.MaxUtcDateTime; + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/TokenCredentialSecurityToken.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/TokenCredentialSecurityToken.cs new file mode 100644 index 000000000000..2945ac26472f --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Microsoft/CoreWCF/Azure/Tokens/TokenCredentialSecurityToken.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core; +using System; +using System.Collections.ObjectModel; +using CoreWCF.IdentityModel.Tokens; + +namespace Microsoft.CoreWCF.Azure.Tokens +{ + internal class TokenCredentialSecurityToken : SecurityToken + { + public TokenCredentialSecurityToken(TokenCredential tokenCredential) : this(tokenCredential, SecurityTokenUtils.CreateUniqueId()) { } + + public TokenCredentialSecurityToken(TokenCredential tokenCredential, string id) + { + TokenCredential = tokenCredential; + Id = id; + ValidFrom = DateTime.UtcNow; + } + + public TokenCredential TokenCredential { get; } + public override string Id { get; } + public override ReadOnlyCollection SecurityKeys => SecurityTokenUtils.EmptyReadOnlyCollection.Instance; + public override DateTime ValidFrom { get; } + public override DateTime ValidTo => SecurityTokenUtils.MaxUtcDateTime; + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Resources/SR.Designer.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Resources/SR.Designer.cs new file mode 100644 index 000000000000..a110c5a5b0d1 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Resources/SR.Designer.cs @@ -0,0 +1,198 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CoreWCF.Azure.StorageQueues { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class SR { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal SR() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Resources.SR", typeof(SR).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The endpoint Uri '{0}' should only have a single path segment. The account name should be the first part of the hostname and not part of the path.. + /// + internal static string AccountNameShouldBePartOfHostName { + get { + return ResourceManager.GetString("AccountNameShouldBePartOfHostName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The endpoint Uri '{0}' should have two path segments when not using an Azure dns hostname.. + /// + internal static string AccountNameShouldBePartOfUriPath { + get { + return ResourceManager.GetString("AccountNameShouldBePartOfUriPath", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Account name mismatch. The connection string is using account name '{0}', and the channel uri is using account name '{1}'. When specifying the account name using both methods, they must match.. + /// + internal static string ConnectionStringAndViaAccountNameMismatch { + get { + return ResourceManager.GetString("ConnectionStringAndViaAccountNameMismatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Queue name mismatch. The connection string is using queue name '{0}', and the channel uri is using queue name '{1}'. When specifying the queue name using both methods, they must match.. + /// + internal static string ConnectionStringAndViaQueueNameMismatch { + get { + return ResourceManager.GetString("ConnectionStringAndViaQueueNameMismatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The connection string is not provided. Specify a connection string in AzureServiceCredentials.. + /// + internal static string ConnectionStringNotProvidedOnAzureServiceCredentials { + get { + return ResourceManager.GetString("ConnectionStringNotProvidedOnAzureServiceCredentials", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dead letter queue name doesn't meet naming requirements. See https://learn.microsoft.com/rest/api/storageservices/naming-queues-and-metadata for more details.. + /// + internal static string InvalidDeadLetterQueueName { + get { + return ResourceManager.GetString("InvalidDeadLetterQueueName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token provider of type '{0}' did not return a token of type '{1}'. Check the credential configuration.. + /// + internal static string InvalidTokenProvided { + get { + return ResourceManager.GetString("InvalidTokenProvided", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MaxReceivedMessageSize must be greater than 0 not more than 65536.. + /// + internal static string MaxReceivedMessageSizeOutOfRange { + get { + return ResourceManager.GetString("MaxReceivedMessageSizeOutOfRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MaxReceiveTimeout must be greater than or equal to TimeSpan.Zero. + /// + internal static string MaxReceiveTimeoutGreaterThanOrEqualToZero { + get { + return ResourceManager.GetString("MaxReceiveTimeoutGreaterThanOrEqualToZero", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ue name must be provided either in the connection string or as part of the channel endpoint uri.. + /// + internal static string MissingQueueName { + get { + return ResourceManager.GetString("MissingQueueName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PollingInterval must a be positive duration.. + /// + internal static string PollingIntervalMustBePositive { + get { + return ResourceManager.GetString("PollingIntervalMustBePositive", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Sas credential is not provided. Specify a Sas credential in AzureServiceCredentials.. + /// + internal static string SasCredentialNotProvidedOnAzureServiceCredentials { + get { + return ResourceManager.GetString("SasCredentialNotProvidedOnAzureServiceCredentials", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An error ({0}) occurred while transmitting message.. + /// + internal static string SendError { + get { + return ResourceManager.GetString("SendError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The StorageSharedKey credential is not provided. Specify a StorageSharedKey credential in AzureServiceCredentials.. + /// + internal static string StorageSharedKeyCredentialNotProvidedOnAzureServiceCredentials { + get { + return ResourceManager.GetString("StorageSharedKeyCredentialNotProvidedOnAzureServiceCredentials", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token credential is not provided. Specify a token credential in AzureServiceCredentials.. + /// + internal static string TokenCredentialNotProvidedOnAzureServiceCredentials { + get { + return ResourceManager.GetString("TokenCredentialNotProvidedOnAzureServiceCredentials", resourceCulture); + } + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Resources/SR.resx b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Resources/SR.resx new file mode 100644 index 000000000000..7b38495365d9 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/src/Resources/SR.resx @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The endpoint Uri '{0}' should only have a single path segment. The account name should be the first part of the hostname and not part of the path. + + + The endpoint Uri '{0}' should have two path segments when not using an Azure dns hostname. + + + Account name mismatch. The connection string is using account name '{0}', and the channel uri is using account name '{1}'. When specifying the account name using both methods, they must match. + + + Queue name mismatch. The connection string is using queue name '{0}', and the channel uri is using queue name '{1}'. When specifying the queue name using both methods, they must match. + + + The connection string is not provided. Specify a connection string in AzureServiceCredentials. + + + Dead letter queue name doesn't meet naming requirements. See https://learn.microsoft.com/rest/api/storageservices/naming-queues-and-metadata for more details. + + + The token provider of type '{0}' did not return a token of type '{1}'. Check the credential configuration. + + + ue name must be provided either in the connection string or as part of the channel endpoint uri. + + + The Sas credential is not provided. Specify a Sas credential in AzureServiceCredentials. + + + The StorageSharedKey credential is not provided. Specify a StorageSharedKey credential in AzureServiceCredentials. + + + The token credential is not provided. Specify a token credential in AzureServiceCredentials. + + + MaxReceiveTimeout must be greater than or equal to TimeSpan.Zero + + + PollingInterval must a be positive duration. + + + MaxReceivedMessageSize must be greater than 0 not more than 65536. + + + An error ({0}) occurred while transmitting message. + + \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/AuthenticationTests.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/AuthenticationTests.cs new file mode 100644 index 000000000000..94bf50eac505 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/AuthenticationTests.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#if NET6_0_OR_GREATER + +using Azure.Storage; +using Azure.Storage.Queues; +using Contracts; +using CoreWCF.Configuration; +using CoreWCF.Queue.Common.Configuration; +using CoreWCF.Security; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using System; +using System.Net; +using System.Threading.Tasks; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class AuthenticationTests + { + [TestCase(AzureClientCredentialType.StorageSharedKey)] + [TestCase(AzureClientCredentialType.ConnectionString)] + [TestCase(AzureClientCredentialType.Token)] + public async Task ConnectionStringAuthentication_SuccessAsync(AzureClientCredentialType clientCredentialType) + { + var builder = WebApplication.CreateBuilder(); + // Work around bug in CoreWCF which prevents using a Generic Host when using an ITransportServiceBuilder + builder.WebHost.ConfigureKestrel(options => + { + options.Listen(IPAddress.Loopback, 0); + }); + builder.Services.AddServiceModelServices(); + builder.Services.AddQueueTransport(); + builder.Services.AddSingleton(); + var app = builder.Build(); + var queueName = TestHelper.GenerateUniqueQueueName(); + app.UseServiceModel(serviceBuilder => + { + serviceBuilder.AddService(); + AddAQSEndpoint(serviceBuilder, clientCredentialType, queueName); + }); + await app.StartAsync(); + var connectionString = AzuriteNUnitFixture.Instance.GetAzureAccount().ConnectionString; + var queueClientOptions = new QueueClientOptions { Transport = AzuriteNUnitFixture.Instance.GetTransport(), MessageEncoding = QueueMessageEncoding.None }; + var queueClient = new QueueClient(connectionString, queueName, queueClientOptions); + await queueClient.CreateIfNotExistsAsync(); + await queueClient.SendMessageAsync("http://tempuri.org/ITestContract/Createtest"); + var testService = app.Services.GetRequiredService(); + Assert.True(testService.ManualResetEvent.Wait(TimeSpan.FromSeconds(5))); + } + + private static void AddAQSEndpoint(IServiceBuilder serviceBuilder, AzureClientCredentialType clientCredentialType, string queueName) + { + var binding = new AzureQueueStorageBinding(); + binding.Security.Transport.ClientCredentialType = clientCredentialType; + binding.MessageEncoding = AzureQueueStorageMessageEncoding.Text; + var azuriteAccount = AzuriteNUnitFixture.Instance.GetAzureAccount(); + var endpointUriBuilder = new UriBuilder(azuriteAccount.QueueEndpoint + "/" + queueName) + { + Scheme = "net.aqs" + }; + serviceBuilder.AddServiceEndpoint(binding, endpointUriBuilder.Uri.AbsoluteUri); + + serviceBuilder.UseAzureCredentials(credentials => + { + switch (clientCredentialType) + { + case AzureClientCredentialType.ConnectionString: + credentials.ConnectionString = azuriteAccount.ConnectionString; + break; + case AzureClientCredentialType.StorageSharedKey: + var accountName = azuriteAccount.Name; + var accountKey = azuriteAccount.Key; + credentials.StorageSharedKey = new StorageSharedKeyCredential(accountName, accountKey); + break; + case AzureClientCredentialType.Token: + credentials.Token = AzuriteNUnitFixture.Instance.GetCredential(); + break; + } + + credentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None; + }); + } + } +} +#endif \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Helpers/DependencyResolverHelper.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Helpers/DependencyResolverHelper.cs new file mode 100644 index 000000000000..e751fd40cb88 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Helpers/DependencyResolverHelper.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests.Helpers +{ + internal class DependencyResolverHelper + { + private readonly IWebHost _webHost; + + public DependencyResolverHelper(IWebHost webHost) + { + _webHost = webHost; + } + + public T GetService() + { + using IServiceScope serviceScope = _webHost.Services.CreateScope(); + IServiceProvider services = serviceScope.ServiceProvider; + T scopedService = services.GetRequiredService(); + return scopedService; + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Helpers/ServiceHelper.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Helpers/ServiceHelper.cs new file mode 100644 index 000000000000..d14138635664 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Helpers/ServiceHelper.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests.Helpers +{ + public static class ServiceHelper + { + public static IWebHostBuilder CreateWebHostBuilder() + where TStartup : class + { + return WebHost.CreateDefaultBuilder(Array.Empty()) +#if DEBUG + .ConfigureLogging((logging) => + { + logging.AddFilter("Default", LogLevel.Debug); + logging.AddFilter("Microsoft", LogLevel.Debug); + logging.SetMinimumLevel(LogLevel.Debug); + }) +#endif // DEBUG + .UseStartup(); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/ITestContract.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/ITestContract.cs new file mode 100644 index 000000000000..7fcc94d60309 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/ITestContract.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using CoreWCF; + +namespace Contracts +{ + [ServiceContract] + public interface ITestContract + { + [OperationContract(IsOneWay = true)] + void Create(string name); + } + + public class TestService : ITestContract + { + public TestService() + { + ManualResetEvent = new ManualResetEventSlim(false); + } + + public void Create(string name) + { + if (string.IsNullOrEmpty(name)) + throw new FaultException(); + + ManualResetEvent.Set(); + } + + public ManualResetEventSlim ManualResetEvent { get; } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/ITestContract_EndToEndTest.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/ITestContract_EndToEndTest.cs new file mode 100644 index 000000000000..ce229f698477 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/ITestContract_EndToEndTest.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#if NET6_0_OR_GREATER +using System.ServiceModel; +using System.Threading; + +namespace Contracts +{ + [ServiceContract] + public interface ITestContract_EndToEndTest + { + [OperationContract(IsOneWay = true)] + void Create(string name); + } + + public class TestService_EndToEnd : ITestContract_EndToEndTest + { + public TestService_EndToEnd() + { + ManualResetEvent = new ManualResetEventSlim(false); + } + + public void Create(string name) + { + if (string.IsNullOrEmpty(name)) + throw new FaultException(); + ReceivedName = name; + ManualResetEvent.Set(); + } + + public string ReceivedName { get; set; } + + public ManualResetEventSlim ManualResetEvent { get; } + } +} +#endif \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_EndToEnd.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_EndToEnd.cs new file mode 100644 index 000000000000..0051e5f90126 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_EndToEnd.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#if NET6_0_OR_GREATER + +using Azure.Storage.Queues; +using Azure.Storage.Queues.Models; +using Contracts; +using Microsoft.CoreWCF.Azure.StorageQueues.Tests; +using Microsoft.CoreWCF.Azure.StorageQueues.Tests.Helpers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.WCF.Azure.StorageQueues; +using NUnit.Framework; +using System; +using System.Threading.Tasks; +using Microsoft.WCF.Azure; + +namespace CoreWCF +{ + public class IntegrationTests_EndToEnd + { + private IWebHost host; + + [SetUp] + public void Setup() + { + host = ServiceHelper.CreateWebHostBuilder().Build(); + host.Start(); + } + + [TearDown] + public void TearDown() + { + if (host is not null) + { + host.StopAsync().Wait(); + host.Dispose(); + } + } + + [Test] + public void DefaultQueueConfiguration_ReceiveMessage_Success_EndToEnd() + { + var queueName = Startup_EndToEnd.QueueName; + var azuriteFixture = AzuriteNUnitFixture.Instance; + var connectionString = azuriteFixture.GetAzureAccount().ConnectionString; + var endpointUriBuilder = new UriBuilder(azuriteFixture.GetAzureAccount().QueueEndpoint + "/" + queueName) + { + Scheme = "net.aqs" + }; + var endpointUrlString = endpointUriBuilder.Uri.AbsoluteUri; + + AzureQueueStorageBinding azureQueueStorageBinding = new(); + azureQueueStorageBinding.Security.Transport.ClientCredentialType = Microsoft.WCF.Azure.AzureClientCredentialType.ConnectionString; + azureQueueStorageBinding.MessageEncoding = AzureQueueStorageMessageEncoding.Text; + var channelFactory = new System.ServiceModel.ChannelFactory( + azureQueueStorageBinding, + new System.ServiceModel.EndpointAddress(endpointUrlString)); + var azureCredentials = channelFactory.UseAzureCredentials(); + + azureCredentials.ServiceCertificate.SslCertificateAuthentication = new System.ServiceModel.Security.X509ServiceCertificateAuthentication + { + CertificateValidationMode = System.ServiceModel.Security.X509CertificateValidationMode.None + }; + azureCredentials.ConnectionString = connectionString; + + var channel = channelFactory.CreateChannel(); + ((System.ServiceModel.Channels.IChannel)channel).Open(); + channel.Create("TestService_EndToEnd"); + + var testService = host.Services.GetRequiredService(); + Assert.True(testService.ManualResetEvent.Wait(TimeSpan.FromSeconds(5))); + Assert.AreEqual("TestService_EndToEnd", testService.ReceivedName); + } + + [Test] + public async Task DefaultQueueConfiguration_ReceiveMessage_Success_EndToEnd_DeadLetterQueue() + { + var queueName = Startup_EndToEnd.QueueName; + var azuriteFixture = AzuriteNUnitFixture.Instance; + var connectionString = azuriteFixture.GetAzureAccount().ConnectionString; + var endpointUriBuilder = new UriBuilder(azuriteFixture.GetAzureAccount().QueueEndpoint + "/" + queueName) + { + Scheme = "net.aqs" + }; + var endpointUrlString = endpointUriBuilder.Uri.AbsoluteUri; + + AzureQueueStorageBinding azureQueueStorageBinding = new(); + azureQueueStorageBinding.Security.Transport.ClientCredentialType = Microsoft.WCF.Azure.AzureClientCredentialType.ConnectionString; + + var channelFactory = new System.ServiceModel.ChannelFactory( + azureQueueStorageBinding, + new System.ServiceModel.EndpointAddress(endpointUrlString)); + + var azureCredentials = channelFactory.UseAzureCredentials(); + azureCredentials.ServiceCertificate.SslCertificateAuthentication = new System.ServiceModel.Security.X509ServiceCertificateAuthentication + { + CertificateValidationMode = System.ServiceModel.Security.X509CertificateValidationMode.None + }; + azureCredentials.ConnectionString = connectionString; + + var channel = channelFactory.CreateChannel(); + ((System.ServiceModel.Channels.IChannel)channel).Open(); + channel.Create("TestService_EndToEnd"); + + var testService = host.Services.GetRequiredService(); + Assert.False(testService.ManualResetEvent.Wait(TimeSpan.FromSeconds(5))); + QueueClient queueClient = TestHelper.GetQueueClient(azuriteFixture.GetTransport(), connectionString, Startup_EndToEnd.DlqQueueName, QueueMessageEncoding.Base64); + QueueMessage message = await queueClient.ReceiveMessageAsync(); + Assert.IsNotNull(message.MessageText); + } + } +} +#endif \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_EndToEnd_BinaryEncoding.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_EndToEnd_BinaryEncoding.cs new file mode 100644 index 000000000000..ba1ef8441821 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_EndToEnd_BinaryEncoding.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#if NET6_0_OR_GREATER + +using Contracts; +using Microsoft.CoreWCF.Azure.StorageQueues.Tests; +using Microsoft.CoreWCF.Azure.StorageQueues.Tests.Helpers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.WCF.Azure.StorageQueues; +using NUnit.Framework; +using System; +using Microsoft.WCF.Azure; + +namespace CoreWCF +{ + public class IntegrationTests_EndToEnd_BinaryEncoding + { + private IWebHost host; + + [SetUp] + public void Setup() + { + host = ServiceHelper.CreateWebHostBuilder().Build(); + host.Start(); + } + + [TearDown] + public void TearDown() + { + host.StopAsync().Wait(); + host.Dispose(); + } + + [Test] + public void DefaultQueueConfiguration_ReceiveBinaryMessage_Success_EndToEnd() + { + var queueName = Startup_EndToEnd_BinaryEncoding.QueueName; + var azuriteFixture = AzuriteNUnitFixture.Instance; + var connectionString = azuriteFixture.GetAzureAccount().ConnectionString; + var endpointUriBuilder = new UriBuilder(azuriteFixture.GetAzureAccount().QueueEndpoint + "/" + queueName) + { + Scheme = "net.aqs" + }; + var endpointUrlString = endpointUriBuilder.Uri.AbsoluteUri; + + AzureQueueStorageBinding azureQueueStorageBinding = new(); + azureQueueStorageBinding.MessageEncoding = AzureQueueStorageMessageEncoding.Binary; + azureQueueStorageBinding.Security.Transport.ClientCredentialType = AzureClientCredentialType.ConnectionString; + var channelFactory = new System.ServiceModel.ChannelFactory( + azureQueueStorageBinding, + new System.ServiceModel.EndpointAddress(endpointUrlString)); + + var credentials = channelFactory.UseAzureCredentials(); + credentials.ServiceCertificate.SslCertificateAuthentication = new System.ServiceModel.Security.X509ServiceCertificateAuthentication + { + CertificateValidationMode = System.ServiceModel.Security.X509CertificateValidationMode.None + }; + credentials.ConnectionString = connectionString; + + var channel = channelFactory.CreateChannel(); + ((System.ServiceModel.Channels.IChannel)channel).Open(); + channel.Create("TestService_EndToEnd"); + + var testService = host.Services.GetRequiredService(); + Assert.True(testService.ManualResetEvent.Wait(TimeSpan.FromSeconds(5))); + Assert.AreEqual("TestService_EndToEnd", testService.ReceivedName); + } + + [Test] + public void DefaultQueueConfiguration_ReceiveBinaryMessage_Success_EndToEnd_conflict() + { + var queueName = Startup_EndToEnd_BinaryEncoding.QueueName; + var azuriteFixture = AzuriteNUnitFixture.Instance; + var connectionString = azuriteFixture.GetAzureAccount().ConnectionString; + var endpointUriBuilder = new UriBuilder(azuriteFixture.GetAzureAccount().QueueEndpoint + "/" + queueName) + { + Scheme = "net.aqs" + }; + var endpointUrlString = endpointUriBuilder.Uri.AbsoluteUri; + + AzureQueueStorageBinding azureQueueStorageBinding = new(); + azureQueueStorageBinding.Security.Transport.ClientCredentialType = AzureClientCredentialType.ConnectionString; + azureQueueStorageBinding.MessageEncoding = AzureQueueStorageMessageEncoding.Text; + var channelFactory = new System.ServiceModel.ChannelFactory( + azureQueueStorageBinding, + new System.ServiceModel.EndpointAddress(endpointUrlString)); + + var credentials = channelFactory.UseAzureCredentials(); + credentials.ServiceCertificate.SslCertificateAuthentication = new System.ServiceModel.Security.X509ServiceCertificateAuthentication + { + CertificateValidationMode = System.ServiceModel.Security.X509CertificateValidationMode.None + }; + credentials.ConnectionString = connectionString; + + var channel = channelFactory.CreateChannel(); + ((System.ServiceModel.Channels.IChannel)channel).Open(); + channel.Create("TestService_EndToEnd"); + + var testService = host.Services.GetRequiredService(); + Assert.False(testService.ManualResetEvent.Wait(TimeSpan.FromSeconds(5))); + } + } +} +#endif \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_QueueConfigurationErrors.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_QueueConfigurationErrors.cs new file mode 100644 index 000000000000..9e793cfea55a --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_QueueConfigurationErrors.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.CoreWCF.Azure.StorageQueues.Tests.Helpers; +using Microsoft.AspNetCore.Hosting; +using NUnit.Framework; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class IntegrationTests_QueueConfigurationErrors + { + private IWebHost host; + + [SetUp] + public void Setup() + { + } + + [TearDown] + public void TearDown() + { + host.StopAsync().Wait(); + host.Dispose(); + } + + [Test] + public void QueueConfigurationWithConflictingQueueName_ThrowArgumentException() + { + host = ServiceHelper.CreateWebHostBuilder().Build(); + System.ArgumentException exception = Assert.Throws(() => host.Start()); + Assert.That(exception.Message, Is.EqualTo($"Queue name mismatch. The connection string is using queue name '{Startup2_ConflictingQueueName.QueueName}', and the channel uri is using queue name 'conflicting-queue-name'. When specifying the queue name using both methods, they must match.")); + } + + [Test] + public void QueueConfigurationWithNoQueueName_ThrowArgumentException() + { + host = ServiceHelper.CreateWebHostBuilder().Build(); + System.ArgumentException exception = Assert.Throws(() => host.Start()); + var expectedExceptionMessage = $"The endpoint Uri '{TestHelper.GetEndpointWithNoQueueName()}' should have two path segments when not using an Azure dns hostname."; + Assert.That(exception.Message, Is.EqualTo(expectedExceptionMessage)); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_QueueConfigurationWithEmptyUri.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_QueueConfigurationWithEmptyUri.cs new file mode 100644 index 000000000000..aaa12c22b446 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_QueueConfigurationWithEmptyUri.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Contracts; +using Microsoft.CoreWCF.Azure.StorageQueues.Tests.Helpers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class IntegrationTests_QueueConfigurationWithEmptyUri + { + private IWebHost host; + + [SetUp] + public void Setup() + { + host = ServiceHelper.CreateWebHostBuilder().Build(); + host.Start(); + } + + [TearDown] + public void TearDown() + { + host.StopAsync().Wait(); + host.Dispose(); + } + + [Test] + public async Task QueueConfigurationWithEmptyUri_ReceiveMessage_Success() + { + var queue = host.Services.GetRequiredService(); + await queue.SendMessageAsync("http://tempuri.org/ITestContract/Createtest"); + + var testService = host.Services.GetRequiredService(); + Assert.True(testService.ManualResetEvent.Wait(System.TimeSpan.FromSeconds(5))); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_ReceiveMessage_Success.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_ReceiveMessage_Success.cs new file mode 100644 index 000000000000..115d2b3d33ec --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_ReceiveMessage_Success.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Contracts; +using Microsoft.CoreWCF.Azure.StorageQueues.Tests.Helpers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class IntegrationTests_ReceiveMessage_Success + { + private IWebHost host; + + [SetUp] + public void Setup() + { + host = ServiceHelper.CreateWebHostBuilder().Build(); + host.Start(); + } + + [TearDown] + public void TearDown() + { + host.StopAsync().Wait(); + host.Dispose(); + } + + [Test] + public async Task DefaultQueueConfiguration_ReceiveMessage_Success() + { + var queue = host.Services.GetRequiredService(); + await queue.SendMessageAsync("http://tempuri.org/ITestContract/Createtest"); + + var testService = host.Services.GetRequiredService(); + Assert.True(testService.ManualResetEvent.Wait(System.TimeSpan.FromSeconds(5))); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_Test_DeadLetterQueue.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_Test_DeadLetterQueue.cs new file mode 100644 index 000000000000..50b43ab7f022 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_Test_DeadLetterQueue.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Azure.Storage.Queues.Models; +using Contracts; +using Microsoft.CoreWCF.Azure.StorageQueues.Tests.Helpers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class IntegrationTests_Test_DeadLetterQueue + { + private IWebHost host; + + [SetUp] + public void Setup() + { + host = ServiceHelper.CreateWebHostBuilder().Build(); + host.Start(); + } + + [TearDown] + public void TearDown() + { + host.StopAsync().Wait(); + host.Dispose(); + } + + [Test] + public async Task Test_DeadLetterQueue() + { + var queue = host.Services.GetRequiredService(); + string inputMessage = "\"http://tempuri.org/ITestContract/Createtest\""; + await queue.SendMessageAsync(inputMessage); + + var testService = host.Services.GetRequiredService(); + Assert.False(testService.ManualResetEvent.Wait(System.TimeSpan.FromSeconds(5))); + var connectionString = AzuriteNUnitFixture.Instance.GetAzureAccount().ConnectionString; + + QueueClient queueClient = TestHelper.GetQueueClient(AzuriteNUnitFixture.Instance.GetTransport(), connectionString, Startup_ReceiveBinaryMessage_Success.DlqQueueName, QueueMessageEncoding.Base64); + QueueMessage message = await queueClient.ReceiveMessageAsync(); + Assert.AreEqual(inputMessage, message.MessageText); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_Test_DeadLetterQueueSendBinary.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_Test_DeadLetterQueueSendBinary.cs new file mode 100644 index 000000000000..35d71ffda24f --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_Test_DeadLetterQueueSendBinary.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Contracts; +using CoreWCF; +using CoreWCF.Channels; +using Microsoft.AspNetCore.Hosting; +using Microsoft.CoreWCF.Azure.StorageQueues.Tests.Helpers; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class IntegrationTests_Test_DeadLetterQueueSendBinary + { + private IWebHost host; + + [SetUp] + public void Setup() + { + host = ServiceHelper.CreateWebHostBuilder().Build(); + host.Start(); + } + + [TearDown] + public void TearDown() + { + host.StopAsync().Wait(); + host.Dispose(); + } + + [Test] + public async Task Test_DeadLetterQueueTextServiceQueueBinaryClientQueue() + { + var queue = host.Services.GetRequiredService(); + var body = XDocument.Parse("test"); + var message = Message.CreateMessage(MessageVersion.CreateVersion(EnvelopeVersion.Soap12), "http://tempuri.org/ITestContract/Create", body.CreateReader()); + var bmebe = new BinaryMessageEncodingBindingElement(); + var encoderFactory = bmebe.CreateMessageEncoderFactory(); + var bufferManager = BufferManager.CreateBufferManager(64 * 1024, 64 * 1024); + ReadOnlyMemory encodedMessage = encoderFactory.Encoder.WriteMessage(message, int.MaxValue, bufferManager); + BinaryData queueBody = new BinaryData(encodedMessage); + var receipt = await queue.SendMessageAsync(queueBody); + var testService = host.Services.GetRequiredService(); + Assert.False(testService.ManualResetEvent.Wait(TimeSpan.FromSeconds(5))); + var connectionString = AzuriteNUnitFixture.Instance.GetAzureAccount().ConnectionString; + QueueClient queueClient = TestHelper.GetQueueClient(AzuriteNUnitFixture.Instance.GetTransport(), connectionString, Startup_TextServiceQueueBinaryClientQueue.DlqQueueName, QueueMessageEncoding.Base64); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var response = await queueClient.ReceiveMessageAsync(default, cts.Token); + Assert.NotNull(response.Value); + Assert.AreEqual(queueBody.ToArray(), response.Value.Body.ToArray()); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_Test_DeadLetterQueueSendText.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_Test_DeadLetterQueueSendText.cs new file mode 100644 index 000000000000..07d882cfa2e5 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/IntegrationTests_Test_DeadLetterQueueSendText.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Contracts; +using Microsoft.AspNetCore.Hosting; +using Microsoft.CoreWCF.Azure.StorageQueues.Tests.Helpers; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class IntegrationTests_Test_DeadLetterQueueSendText + { + private IWebHost host; + + [SetUp] + public void Setup() + { + host = ServiceHelper.CreateWebHostBuilder().Build(); + host.Start(); + } + + [TearDown] + public void TearDown() + { + host.StopAsync().Wait(); + host.Dispose(); + } + + [Test] + public async Task Test_DeadLetterQueueBinaryServiceQueueTextClientQueue() + { + var queue = host.Services.GetRequiredService(); + string inputMessage = @"http://tempuri.org/ITestContract/Createtest"; + var receipt = await queue.SendMessageAsync(inputMessage); + var testService = host.Services.GetRequiredService(); + Assert.False(testService.ManualResetEvent.Wait(TimeSpan.FromSeconds(5))); + var connectionString = AzuriteNUnitFixture.Instance.GetAzureAccount().ConnectionString; + QueueClient queueClient = TestHelper.GetQueueClient(AzuriteNUnitFixture.Instance.GetTransport(), connectionString, Startup_BinaryServiceQueueTextClientQueue.DlqQueueName, QueueMessageEncoding.None); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var response = await queueClient.ReceiveMessageAsync(default, cts.Token); + Assert.NotNull(response.Value); + Assert.AreEqual(inputMessage, response.Value.MessageText); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Microsoft.CoreWCF.Azure.StorageQueues.Tests.csproj b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Microsoft.CoreWCF.Azure.StorageQueues.Tests.csproj new file mode 100644 index 000000000000..2d1fe9cb3cb3 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Microsoft.CoreWCF.Azure.StorageQueues.Tests.csproj @@ -0,0 +1,58 @@ + + + + $(RequiredTargetFrameworks) + true + false + + + + + + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Properties/appsettings.json b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Properties/appsettings.json new file mode 100644 index 000000000000..ecc5a1401e2d --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Properties/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Certificates": { + "Default": { + "Path": "azurite_cert.pem", + "KeyPath": "key.pem" + } + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Properties/serviceDependencies.json b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Properties/serviceDependencies.json new file mode 100644 index 000000000000..f3d42edac559 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Properties/serviceDependencies.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "secrets1": { + "type": "secrets" + }, + "storage1": { + "type": "storage", + "connectionId": "StorageConnectionString" + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Properties/serviceDependencies.local.json b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Properties/serviceDependencies.local.json new file mode 100644 index 000000000000..bcd26d7ff8b5 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Properties/serviceDependencies.local.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "secrets1": { + "type": "secrets.user" + }, + "storage1": { + "secretStore": "LocalSecretsFile", + "type": "storage.emulator", + "connectionId": "StorageConnectionString" + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup2_ConflictingQueueName.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup2_ConflictingQueueName.cs new file mode 100644 index 000000000000..0c1353538081 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup2_ConflictingQueueName.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Contracts; +using CoreWCF.Configuration; +using CoreWCF.Security; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class Startup2_ConflictingQueueName + { + public static string QueueName { get; } = TestHelper.GenerateUniqueQueueName(); + public static string DlqQueueName { get; } = "dlq-" + QueueName; + private string connectionString = null; + private string endpointUrlString = null; + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + TestHelper.ConfigureService(services, typeof(TestService).FullName, QueueName, out connectionString, out endpointUrlString, "conflicting-queue-name"); + } + + public void Configure(IApplicationBuilder app) + { + QueueClient queue = app.ApplicationServices.GetRequiredService(); + + app.UseServiceModel(services => + { + services.AddService(); + var binding = new AzureQueueStorageBinding(DlqQueueName); + binding.Security.Transport.ClientCredentialType = AzureClientCredentialType.ConnectionString; + services.AddServiceEndpoint(binding, endpointUrlString); + services.UseAzureCredentials(credentials => + { + credentials.ConnectionString = connectionString; + credentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None; + }); + }); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup2_NoQueueName.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup2_NoQueueName.cs new file mode 100644 index 000000000000..69c5b60e5397 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup2_NoQueueName.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Contracts; +using CoreWCF.Configuration; +using CoreWCF.Security; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class Startup2_NoQueueName + { + public static string QueueName { get; } = TestHelper.GenerateUniqueQueueName(); + public static string DlqQueueName { get; } = "dlq-" + QueueName; + private string connectionString = null; + private string endpointUrlString = null; + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + TestHelper.ConfigureService(services, typeof(TestService).FullName, QueueName, out connectionString, out endpointUrlString, "", true); + } + + public void Configure(IApplicationBuilder app) + { + QueueClient queue = app.ApplicationServices.GetRequiredService(); + + app.UseServiceModel(services => + { + services.AddService(); + var binding = new AzureQueueStorageBinding(DlqQueueName); + binding.Security.Transport.ClientCredentialType = AzureClientCredentialType.ConnectionString; + services.AddServiceEndpoint(binding, endpointUrlString); + services.UseAzureCredentials(credentials => + { + credentials.ConnectionString = connectionString; + credentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None; + }); + }); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_BinaryServiceQueueTextClientQueue.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_BinaryServiceQueueTextClientQueue.cs new file mode 100644 index 000000000000..a4db06d1840a --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_BinaryServiceQueueTextClientQueue.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Contracts; +using CoreWCF.Configuration; +using CoreWCF.Security; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class Startup_BinaryServiceQueueTextClientQueue + { + public static string QueueName { get; } = TestHelper.GenerateUniqueQueueName(); + public static string DlqQueueName { get; } = "dlq-" + QueueName; + private string connectionString = null; + private string endpointUrlString = null; + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + TestHelper.ConfigureService(services, typeof(TestService).FullName, QueueName, out connectionString, out endpointUrlString); + } + + public void Configure(IApplicationBuilder app) + { + QueueClient queue = app.ApplicationServices.GetRequiredService(); + + app.UseServiceModel(services => + { + services.AddService(); + var binding = new AzureQueueStorageBinding(DlqQueueName); + binding.Security.Transport.ClientCredentialType = AzureClientCredentialType.ConnectionString; + binding.MessageEncoding = AzureQueueStorageMessageEncoding.Binary; + services.AddServiceEndpoint(binding, endpointUrlString); + services.UseAzureCredentials(creds => + { + creds.ConnectionString = connectionString; + creds.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None; + }); + }); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_EndToEnd.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_EndToEnd.cs new file mode 100644 index 000000000000..10a8b54e2c91 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_EndToEnd.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#if NET6_0_OR_GREATER +using Azure.Storage.Queues; +using Contracts; +using CoreWCF.Configuration; +using CoreWCF.Security; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class Startup_EndToEnd + { + public static string QueueName { get; } = TestHelper.GenerateUniqueQueueName(); + public static string DlqQueueName { get; } = "dlq-" + QueueName; + private string connectionString = null; + private string endpointUrlString = null; + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + TestHelper.ConfigureService(services, typeof(TestService_EndToEnd).FullName, QueueName, out connectionString, out endpointUrlString); + } + + public void Configure(IApplicationBuilder app) + { + QueueClient queue = app.ApplicationServices.GetRequiredService(); + + app.UseServiceModel(services => + { + services.AddService(); + var binding = new AzureQueueStorageBinding(DlqQueueName); + binding.Security.Transport.ClientCredentialType = AzureClientCredentialType.ConnectionString; + binding.MessageEncoding = AzureQueueStorageMessageEncoding.Text; + services.AddServiceEndpoint(binding, endpointUrlString); + services.UseAzureCredentials(credentials => + { + credentials.ConnectionString = connectionString; + credentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None; + }); + }); + } + } +} +#endif \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_EndToEnd_BinaryEncoding.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_EndToEnd_BinaryEncoding.cs new file mode 100644 index 000000000000..d9b748c304b9 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_EndToEnd_BinaryEncoding.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#if NET6_0_OR_GREATER +using Azure.Storage.Queues; +using Contracts; +using CoreWCF.Configuration; +using CoreWCF.Security; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class Startup_EndToEnd_BinaryEncoding + { + public static string QueueName { get; } = TestHelper.GenerateUniqueQueueName(); + public static string DlqQueueName { get; } = "dlq-" + QueueName; private string connectionString = null; + private string endpointUrlString = null; + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + TestHelper.ConfigureService(services, typeof(TestService_EndToEnd).FullName, QueueName, out connectionString, out endpointUrlString, "", false, QueueMessageEncoding.Base64); + } + + public void Configure(IApplicationBuilder app) + { + QueueClient queue = app.ApplicationServices.GetRequiredService(); + + app.UseServiceModel(services => + { + services.AddService(); + var binding = new AzureQueueStorageBinding(DlqQueueName); + binding.Security.Transport.ClientCredentialType = AzureClientCredentialType.ConnectionString; + binding.MessageEncoding = AzureQueueStorageMessageEncoding.Binary; + services.AddServiceEndpoint(binding, endpointUrlString); + services.UseAzureCredentials(creds => + { + creds.ConnectionString = connectionString; + creds.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None; + }); + }); + } + } +} +#endif \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_QueueConfigurationWithEmptyUri.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_QueueConfigurationWithEmptyUri.cs new file mode 100644 index 000000000000..5859617672cd --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_QueueConfigurationWithEmptyUri.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Contracts; +using CoreWCF.Configuration; +using CoreWCF.Security; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class Startup_QueueConfigurationWithEmptyUri + { + public static string QueueName { get; } = TestHelper.GenerateUniqueQueueName(); + public static string DlqQueueName { get; } = "dlq-" + QueueName; + private string connectionString = null; + private string endpointUrlString = null; + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + TestHelper.ConfigureService(services, typeof(TestService).FullName, QueueName, out connectionString, out endpointUrlString); + endpointUrlString = ""; + } + + public void Configure(IApplicationBuilder app) + { + QueueClient queue = app.ApplicationServices.GetRequiredService(); + + app.UseServiceModel(services => + { + services.AddService(); + var binding = new AzureQueueStorageBinding(DlqQueueName); + binding.Security.Transport.ClientCredentialType = AzureClientCredentialType.ConnectionString; + binding.MessageEncoding = AzureQueueStorageMessageEncoding.Text; + services.AddServiceEndpoint(binding, endpointUrlString); + services.UseAzureCredentials(creds => + { + creds.ConnectionString = connectionString; + creds.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None; + }); + }); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_ReceiveBinaryMessage_Success.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_ReceiveBinaryMessage_Success.cs new file mode 100644 index 000000000000..28b904a09bda --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_ReceiveBinaryMessage_Success.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Contracts; +using CoreWCF.Configuration; +using CoreWCF.Security; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class Startup_ReceiveBinaryMessage_Success + { + public static string QueueName { get; } = TestHelper.GenerateUniqueQueueName(); + public static string DlqQueueName { get; } = "dlq-" + QueueName; + private string connectionString = null; + private string endpointUrlString = null; + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + TestHelper.ConfigureService(services, typeof(TestService).FullName, QueueName, out connectionString, out endpointUrlString, "", false, QueueMessageEncoding.Base64); + } + + public void Configure(IApplicationBuilder app) + { + QueueClient queue = app.ApplicationServices.GetRequiredService(); + + app.UseServiceModel(services => + { + services.AddService(); + var binding = new AzureQueueStorageBinding(DlqQueueName); + binding.Security.Transport.ClientCredentialType = AzureClientCredentialType.ConnectionString; + binding.MessageEncoding = AzureQueueStorageMessageEncoding.Binary; + services.AddServiceEndpoint(binding, endpointUrlString); + services.UseAzureCredentials(creds => + { + creds.ConnectionString = connectionString; + creds.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None; + }); + }); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_ReceiveMessage_Success.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_ReceiveMessage_Success.cs new file mode 100644 index 000000000000..aa9a086797bc --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_ReceiveMessage_Success.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Contracts; +using CoreWCF.Configuration; +using CoreWCF.Queue.Common.Configuration; +using CoreWCF.Security; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class Startup_ReceiveMessage_Success + { + public static string QueueName { get; } = TestHelper.GenerateUniqueQueueName(); + public static string DlqQueueName { get; } = "dlq-" + QueueName; + private string connectionString = null; + private string endpointUrlString = null; + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.AddQueueTransport(); + TestHelper.ConfigureService(services, typeof(TestService).FullName, QueueName, out connectionString, out endpointUrlString); + } + + public void Configure(IApplicationBuilder app) + { + QueueClient queue = app.ApplicationServices.GetRequiredService(); + + app.UseServiceModel(services => + { + services.AddService(); + var binding = new AzureQueueStorageBinding(DlqQueueName); + binding.Security.Transport.ClientCredentialType = AzureClientCredentialType.ConnectionString; + binding.MessageEncoding = AzureQueueStorageMessageEncoding.Text; + services.AddServiceEndpoint(binding, endpointUrlString); + services.UseAzureCredentials(creds => + { + creds.ConnectionString = connectionString; + creds.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None; + }); + }); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_TextServiceQueueBinaryClientQueue.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_TextServiceQueueBinaryClientQueue.cs new file mode 100644 index 000000000000..f16e2062d013 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/Startup_TextServiceQueueBinaryClientQueue.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Contracts; +using CoreWCF.Configuration; +using CoreWCF.Security; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + public class Startup_TextServiceQueueBinaryClientQueue + { + public static string QueueName { get; } = TestHelper.GenerateUniqueQueueName(); + public static string DlqQueueName { get; } = "dlq-" + QueueName; + private string connectionString = null; + private string endpointUrlString = null; + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + TestHelper.ConfigureService(services, typeof(TestService).FullName, QueueName, out connectionString, out endpointUrlString, string.Empty, false, QueueMessageEncoding.Base64); + } + + public void Configure(IApplicationBuilder app) + { + QueueClient queue = app.ApplicationServices.GetRequiredService(); + + app.UseServiceModel(services => + { + services.AddService(); + var binding = new AzureQueueStorageBinding(DlqQueueName); + binding.Security.Transport.ClientCredentialType = AzureClientCredentialType.ConnectionString; + binding.MessageEncoding = AzureQueueStorageMessageEncoding.Text; + services.AddServiceEndpoint(binding, endpointUrlString); + services.UseAzureCredentials(creds => + { + creds.ConnectionString = connectionString; + creds.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None; + }); + }); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/TestHelper.cs b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/TestHelper.cs new file mode 100644 index 000000000000..57e3cd910a99 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues/tests/TestHelper.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core.Pipeline; +using Azure.Storage.Queues; +using CoreWCF.Configuration; +using CoreWCF.Queue.Common.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Net.Http; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests +{ + internal class TestHelper + { + internal static void ConfigureService(IServiceCollection services, + string serviceName, + string queueName, + out string connectionString, + out string endpointUrlString, + string conflictingQueueName = "", + bool createWithNoQueueName = false, + QueueMessageEncoding queueMessageEncoding = QueueMessageEncoding.None + ) + { + if (conflictingQueueName == "") + { + conflictingQueueName = queueName; + } + + services.AddServiceModelServices(); + services.AddQueueTransport(); + services.AddHttpClient(serviceName) + .ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler() + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true + }; + }); + + var azuriteFixture = AzuriteNUnitFixture.Instance; + var transport = azuriteFixture.GetTransport(); + UriBuilder endpointUriBuilder = null; + if (createWithNoQueueName) + { + connectionString = azuriteFixture.GetAzureAccount().ConnectionString; + endpointUriBuilder = new UriBuilder(azuriteFixture.GetAzureAccount().QueueEndpoint) + { + Scheme = "net.aqs" + }; + } + else + { + connectionString = azuriteFixture.GetAzureAccount().ConnectionString.TrimEnd(';') + "/" + queueName + ";"; + endpointUriBuilder = new UriBuilder(azuriteFixture.GetAzureAccount().QueueEndpoint + "/" + conflictingQueueName) + { + Scheme = "net.aqs" + }; + } + endpointUrlString = endpointUriBuilder.Uri.AbsoluteUri; + var queueClient = new QueueClient(connectionString, queueName, new QueueClientOptions { Transport = transport , MessageEncoding = queueMessageEncoding });; + queueClient.CreateIfNotExists(); + services.AddSingleton(queueClient); + } + + internal static string GenerateUniqueQueueName() => Guid.NewGuid().ToString("D").ToLowerInvariant(); + + internal static string GetEndpointWithNoQueueName() + { + var azuriteFixture = AzuriteNUnitFixture.Instance; + UriBuilder endpointUriBuilder = new UriBuilder(azuriteFixture.GetAzureAccount().QueueEndpoint) + { + Scheme = "net.aqs" + }; + return endpointUriBuilder.Uri.AbsoluteUri; + } + + internal static QueueClient GetQueueClient( + HttpPipelineTransport transport, + string connectionString, + string queueName, + QueueMessageEncoding queueMessageEncoding) + { + //var transport = azuriteFixture.GetTransport(); + //var connectionString = azuriteFixture.GetAzureAccount().ConnectionString; + var queueClient = new QueueClient(connectionString, queueName, new QueueClientOptions { Transport = transport, MessageEncoding = queueMessageEncoding }); + queueClient.CreateIfNotExists(); + return queueClient; + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/CHANGELOG.md b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/CHANGELOG.md new file mode 100644 index 000000000000..f50dd6a1a0bf --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/CHANGELOG.md @@ -0,0 +1,12 @@ +# Release History + +## 1.0.0-beta.1 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/README.md b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/README.md new file mode 100644 index 000000000000..f5b156d4b54d --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/README.md @@ -0,0 +1,128 @@ +# WCF Azure Queue Storage client library for .NET + +WCF Azure Queue Storage client is the client side library that will enable WCF clients to be able to send requests to a CoreWCF service which uses Azure Queue Storage as a transport. + +## Getting started + +### Install the package + +Install the Microsoft.WCF.Azure.StorageQueues.Client library for .NET with [NuGet][nuget]: + +```dotnetcli +dotnet add package Microsoft.WCF.Azure.StorageQueues.Client +``` + +### Prerequisites + +You need an [Azure subscription][azure_sub] and a +[Storage Account][storage_account_docs] to use this package. + +To create a new Storage Account, you can use the [Azure portal][storage_account_create_portal], +[Azure PowerShell][storage_account_create_ps], or the [Azure CLI][storage_account_create_cli]. +Here's an example using the Azure CLI: + +```azurecli +az storage account create --name MyStorageAccount --resource-group MyResourceGroup --location westus --sku Standard_LRS +``` + +### Authenticate the service to Azure Queue Storage + +To send requests to the Azure Queue Storage service, you'll need to configure WCF with the appropriate endpoint, and configure credentials. The [Azure Identity library][identity] makes it easy to add Microsoft Entra ID support for authenticating with Azure services. + +There are multiple authentication mechanisms for Azure Queue Storage. Which mechanism to use is configured via the property `AzureQueueStorageBinding.Security.Transport.ClientCredentialType`. The default authentication mechanism is `Default` which uses `DefaultAzureCredential`. + +```C# Snippet:WCF_Azure_Storage_Queues_Sample_DefaultAzureCredential +// Create a binding instance to use Azure Queue Storage. +// The default client credential type is Default, which uses DefaultAzureCredential +var aqsBinding = new AzureQueueStorageBinding(); + +// Create a ChannelFactory to using the binding and endpoint address, open it, and create a channel +string queueEndpointString = "https://MYSTORAGEACCOUNT.queue.core.windows.net/QUEUENAME"; +var factory = new ChannelFactory(aqsBinding, new EndpointAddress(queueEndpointString)); +factory.Open(); +IService channel = factory.CreateChannel(); + +// IService dervies from IChannel so you can call channel.Open without casting +channel.Open(); +await channel.SendDataAsync(42); +``` + +Learn more about enabling Microsoft Entra ID for authentication with Azure Storage in [our documentation][storage_ad]. + +### Other authentication credential mechanisms + +If you are using a different credential mechanism such as `StorageSharedKeyCredential`, you configure the appropriate `ClientCredentialType` on the binding and set the credential on an `AzureClientCredentials` instance via an extension method. + +```C# Snippet:WCF_Azure_Storage_Queus_Sample_StorageSharedKey +// Create a binding instance to use Azure Queue Storage. +var aqsBinding = new AzureQueueStorageBinding(); + +// Configure the client credential type to use StorageSharedKeyCredential +aqsBinding.Security.Transport.ClientCredentialType = AzureClientCredentialType.StorageSharedKey; + +// Create a ChannelFactory to using the binding and endpoint address +string queueEndpointString = "https://MYSTORAGEACCOUNT.queue.core.windows.net/QUEUENAME"; +var factory = new ChannelFactory(aqsBinding, new EndpointAddress(queueEndpointString)); + +// Use extension method to configure WCF to use AzureClientCredentials and set the +// StorageSharedKeyCredential instance. +factory.UseAzureCredentials(credentials => +{ + credentials.StorageSharedKey = GetStorageSharedKey(); +}); + +// Local function to get the StorageSharedKey +StorageSharedKeyCredential GetStorageSharedKey() +{ + // Fetch shared key using a secure mechanism such as Azure Key Vault + // and construct an instance of StorageSharedKeyCredential to return; + return default; +} + +// Open the factory and create a channel +factory.Open(); +IService channel = factory.CreateChannel(); + +// IService dervies from IChannel so you can call channel.Open without casting +channel.Open(); +await channel.SendDataAsync(42); +``` + +Other values for ClientCredentialType are `Sas`, `Token`, and `ConnectionString`. The credentials class `AzureClientCredentials` has corresponding properties to set each of these credential types. + +## Troubleshooting + +If there is a problem with sending a request to Azure Queue Storage, the operation call will throw a `CommunicationException` exception. See `CommunicationException.InnerException` for the original exception thrown by the Azure Storage Queues client. + +## Key concepts + +The goal of this project is to enable migrating existing WCF clients to .NET that are currently using MSMQ and wish to deploy their service to Azure, replacing MSMQ with Azure Queue Storage. + +More general samples of using WCF can be found in the [.NET samples repository][dotnet_repo_wcf_samples]. +To create a service to receive messages sent to an Azure Storage Queue, see the [Microsoft.CoreWCF.Azure.StorageQueues documentation][corewcf_docs]. + +## Contributing + +See the [Storage CONTRIBUTING.md][storage_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. + + +[nuget]: https://www.nuget.org/ +[storage_account_docs]: https://learn.microsoft.com/azure/storage/common/storage-account-overview +[storage_account_create_ps]: https://learn.microsoft.com/azure/storage/common/storage-account-create?tabs=azure-powershell +[storage_account_create_cli]: https://learn.microsoft.com/azure/storage/common/storage-account-create?tabs=azure-cli +[storage_account_create_portal]: https://learn.microsoft.com/azure/storage/common/storage-account-create?tabs=azure-portal +[azure_sub]: https://azure.microsoft.com/free/dotnet/ +[identity]: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/identity/Azure.Identity/README.md +[storage_ad]: https://learn.microsoft.com/azure/storage/blobs/authorize-access-azure-active-directory +[storage_contrib]: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/storage/CONTRIBUTING.md +[cla]: https://opensource.microsoft.com/cla/ +[coc]: https://opensource.microsoft.com/codeofconduct/ +[coc_faq]: https://opensource.microsoft.com/codeofconduct/faq/ +[coc_contact]: mailto:opencode@microsoft.com +[dotnet_repo_wcf_samples]: https://github.com/dotnet/samples/tree/main/framework/wcf +[corewcf_docs]: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/extensions/wcf/Microsoft.CoreWCF.Azure.StorageQueues \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/GlobalSuppressions.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/GlobalSuppressions.cs new file mode 100644 index 000000000000..cfdefe6747b5 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/GlobalSuppressions.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Containers, Azure.Core.Expressions, Azure.Data, Azure.DigitalTwins, Azure.Identity, Azure.IoT, Azure.Learn, Azure.Management, Azure.Media, Azure.Messaging, Azure.MixedReality, Azure.Monitor, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.WCF.Azure.StorageQueues")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Containers, Azure.Core.Expressions, Azure.Data, Azure.DigitalTwins, Azure.Identity, Azure.IoT, Azure.Learn, Azure.Management, Azure.Media, Azure.Messaging, Azure.MixedReality, Azure.Monitor, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.WCF.Azure.StorageQueues.Channels")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Containers, Azure.Core.Expressions, Azure.Data, Azure.DigitalTwins, Azure.Identity, Azure.IoT, Azure.Learn, Azure.Management, Azure.Media, Azure.Messaging, Azure.MixedReality, Azure.Monitor, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.WCF.Azure.Tokens")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Containers, Azure.Core.Expressions, Azure.Data, Azure.DigitalTwins, Azure.Identity, Azure.IoT, Azure.Learn, Azure.Management, Azure.Media, Azure.Messaging, Azure.MixedReality, Azure.Monitor, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.WCF.Azure")] +[assembly: SuppressMessage("Usage", "AZC0001:Use one of the following pre-approved namespace groups (https://azure.github.io/azure-sdk/registered_namespaces.html): Azure.AI, Azure.Analytics, Azure.Communication, Azure.Containers, Azure.Core.Expressions, Azure.Data, Azure.DigitalTwins, Azure.Identity, Azure.IoT, Azure.Learn, Azure.Management, Azure.Media, Azure.Messaging, Azure.MixedReality, Azure.Monitor, Azure.ResourceManager, Azure.Search, Azure.Security, Azure.Storage, Azure.Template, Microsoft.Extensions.Azure", Justification = "", Scope = "namespace", Target = "~N:Microsoft.WCF.Azure.StorageQueues.Resources")] diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft.WCF.Azure.StorageQueues.csproj b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft.WCF.Azure.StorageQueues.csproj new file mode 100644 index 000000000000..c3965849ff9c --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft.WCF.Azure.StorageQueues.csproj @@ -0,0 +1,41 @@ + + + + Icon.png + net6.0 + net6.0 + false + 1.0.0-beta.1 + True + + + + + + + + + + + + + + + + + + + True + True + SR.resx + + + + + + ResXFileCodeGenerator + SR.Designer.cs + Microsoft.WCF.Azure.StorageQueues + + + \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureClientCredentialType.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureClientCredentialType.cs new file mode 100644 index 000000000000..4ec1a2999f88 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureClientCredentialType.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.WCF.Azure +{ + /// + /// Represents the types of credentials that can be passed to authenticate with Azure + /// + public enum AzureClientCredentialType + { + /// + /// Use the Azure.Identity.DefaultAzureCredential credential + /// + Default, + + /// + /// Use a Azure.AzureSasCredential credential + /// + Sas, + + /// + /// Use a Azure.Storage.StorageSharedKeyCredential credential + /// + StorageSharedKey, + + /// + /// Use a Azure.Core.TokenCredential credential + /// + Token, + + /// + /// Use a connection string to provide credentials + /// + ConnectionString + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureClientCredentialTypeHelper.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureClientCredentialTypeHelper.cs new file mode 100644 index 000000000000..a0734d134c50 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureClientCredentialTypeHelper.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.WCF.Azure +{ + internal static class AzureClientCredentialTypeHelper + { + internal static bool IsDefined(AzureClientCredentialType value) + { + return (value == AzureClientCredentialType.Default || + value == AzureClientCredentialType.Sas || + value == AzureClientCredentialType.StorageSharedKey || + value == AzureClientCredentialType.Token || + value == AzureClientCredentialType.ConnectionString); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureClientCredentials.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureClientCredentials.cs new file mode 100644 index 000000000000..2265a4be2f87 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureClientCredentials.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure; +using Azure.Core; +using Azure.Identity; +using Azure.Storage; +using Azure.Storage.Queues.Models; +using Microsoft.WCF.Azure.StorageQueues; +using System; +using System.IdentityModel.Selectors; +using System.ServiceModel.Description; +using System.ServiceModel.Security; + +namespace Microsoft.WCF.Azure +{ + /// + /// Represents the credentials used to authenticate with Azure services. + /// + public class AzureClientCredentials : ClientCredentials + { + /// + /// Initializes a new instance of the class. + /// + public AzureClientCredentials() { } + + /// + /// Initializes a new instance of the class cloning an existing instance. + /// + /// An existing instance of to clone + protected AzureClientCredentials(AzureClientCredentials other) : base(other) + { + Audience = other.Audience; + EnableTenantDiscovery = other.EnableTenantDiscovery; + DefaultAzureCredentialOptions = other.DefaultAzureCredentialOptions; + Sas = other.Sas; + StorageSharedKey = other.StorageSharedKey; + Token = other.Token; + ConnectionString = other.ConnectionString; + } + + /// + /// Gets or sets the audience to use for authentication with Microsoft Entra ID. The audience isn't considered when using a shared key. + /// + public QueueAudience Audience { get; set; } + + /// + /// Enables tenant discovery through the authorization challenge when the client is configured to use a TokenCredential. + /// + public bool EnableTenantDiscovery { get; set; } + + /// + /// Gets of sets the Azure.Identity.DefaultAzureCredentialOptions instance used with DefaultAzureCredentials. + /// + public DefaultAzureCredentialOptions DefaultAzureCredentialOptions { get; set; } + + /// + /// Gets or sets the Azure.AzureSasCredential (shared access signature) credential. + /// + public AzureSasCredential Sas { get; set; } + + /// + /// Gets or sets the Azure.Storage.StorageSharedKeyCredential credential. + /// + public StorageSharedKeyCredential StorageSharedKey { get; set; } + + /// + /// Gets or sets the Azure.Core.TokenCredential credential. + /// + public TokenCredential Token { get; set; } + + /// + /// Gets or sets the connection string containing credentials. + /// + public string ConnectionString { get; set; } + + /// + /// Creates a security token manager for this instance. + /// + /// A AzureClientCredentialsSecurityTokenManager for this AzureClientCredentials. + public override SecurityTokenManager CreateSecurityTokenManager() + { + return new AzureClientCredentialsSecurityTokenManager(Clone() as AzureClientCredentials); + } + + /// + /// Creates a new copy of this AzureClientCredentials instance. + /// + /// A AzureClientCredentials instance. + protected override ClientCredentials CloneCore() + { + // Implement the cloning functionality. + return new AzureClientCredentials(this); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureClientCredentialsSecurityTokenManager.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureClientCredentialsSecurityTokenManager.cs new file mode 100644 index 000000000000..63854de22aca --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureClientCredentialsSecurityTokenManager.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.WCF.Azure.Tokens; +using System; +using System.IdentityModel.Selectors; +using System.ServiceModel; + +namespace Microsoft.WCF.Azure +{ + /// + /// Manages Azure credential security tokens for the client. + /// + public class AzureClientCredentialsSecurityTokenManager : ClientCredentialsSecurityTokenManager + { + private AzureClientCredentials _azureClientCredentials; + + /// + /// Initializes a new instance of the class. + /// + /// + public AzureClientCredentialsSecurityTokenManager(AzureClientCredentials azureClientCredentials) : base(azureClientCredentials) + { + _azureClientCredentials = azureClientCredentials; + } + + /// + /// Creates a security token authenticator. + /// + /// The SecurityTokenRequirement. + /// When this method returns, contains a SecurityTokenResolver. This parameter is passed uninitialized. + /// The SecurityTokenAuthenticator object. + /// `tokenRequirement` is `null`. + + public override SecurityTokenAuthenticator CreateSecurityTokenAuthenticator(SecurityTokenRequirement tokenRequirement, out SecurityTokenResolver outOfBandTokenResolver) + { + return base.CreateSecurityTokenAuthenticator(tokenRequirement, out outOfBandTokenResolver); + } + + /// + /// Creates a security token provider. + /// + /// The SecurityTokenRequirement. + /// The SecurityTokenProvider object. + /// `tokenRequirement` is `null`. + public override SecurityTokenProvider CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement) + { + ArgumentNullException.ThrowIfNull(tokenRequirement); + + if (tokenRequirement.TokenType.StartsWith(AzureSecurityTokenTypes.Namespace)) + return new AzureSecurityTokenProvider(_azureClientCredentials, tokenRequirement); + return base.CreateSecurityTokenProvider(tokenRequirement); + } + + /// + /// Creates a security token serializer. + /// + /// The SecurityTokenVersion of the security token. + /// The SecurityTokenSerializer object. + public override SecurityTokenSerializer CreateSecurityTokenSerializer(SecurityTokenVersion version) + { + return base.CreateSecurityTokenSerializer(version); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureSecurityTokenProvider.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureSecurityTokenProvider.cs new file mode 100644 index 000000000000..e01e2a7d7ddf --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/AzureSecurityTokenProvider.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Identity; +using Microsoft.WCF.Azure.Tokens; +using Microsoft.WCF.Azure.StorageQueues; +using System; +using System.IdentityModel.Selectors; +using System.IdentityModel.Tokens; +using System.Threading.Tasks; + +namespace Microsoft.WCF.Azure +{ + internal class AzureSecurityTokenProvider : SecurityTokenProvider + { + private SecurityToken _securityToken; + + public AzureSecurityTokenProvider(AzureClientCredentials azureClientCredentials, SecurityTokenRequirement tokenRequirement) + { + InitToken(azureClientCredentials, tokenRequirement); + } + + private void InitToken(AzureClientCredentials azureClientCredentials, SecurityTokenRequirement tokenRequirement) + { + switch (tokenRequirement.TokenType) + { + case AzureSecurityTokenTypes.DefaultTokenType: + _securityToken = CreateDefaultSecurityToken(azureClientCredentials); + break; + case AzureSecurityTokenTypes.SasTokenType: + _securityToken = CreateSasSecurityToken(azureClientCredentials); + break; + case AzureSecurityTokenTypes.StorageSharedKeyTokenType: + _securityToken = CreateStorageSharedKeySecurityToken(azureClientCredentials); + break; + case AzureSecurityTokenTypes.TokenTokenType: + _securityToken = CreateTokenCredentialSecurityToken(azureClientCredentials); + break; + case AzureSecurityTokenTypes.ConnectionStringTokenType: + _securityToken = CreateConnectionStringSecurityToken(azureClientCredentials); + break; + default: + _securityToken = null; + break; + } + } + + protected override SecurityToken GetTokenCore(TimeSpan timeout) + { + return _securityToken; + } + + protected override Task GetTokenCoreAsync(TimeSpan timeout) + { + return Task.FromResult(_securityToken); + } + + private SecurityToken CreateConnectionStringSecurityToken(AzureClientCredentials azureClientCredentials) + { + if (azureClientCredentials.ConnectionString == null) + { + throw new InvalidOperationException(SR.ConnectionStringNotProvidedOnAzureClientCredentials); + } + + return new ConnectionStringSecurityToken(azureClientCredentials.ConnectionString); + } + + private SecurityToken CreateTokenCredentialSecurityToken(AzureClientCredentials azureClientCredentials) + { + if (azureClientCredentials.Token == null) + { + throw new InvalidOperationException(SR.TokenCredentialNotProvidedOnAzureClientCredentials); + } + + return new TokenCredentialSecurityToken(azureClientCredentials.Token); + } + + private SecurityToken CreateStorageSharedKeySecurityToken(AzureClientCredentials azureClientCredentials) + { + if (azureClientCredentials.StorageSharedKey == null) + { + throw new InvalidOperationException(SR.StorageSharedKeyCredentialNotProvidedOnAzureClientCredentials); + } + + return new StorageSharedKeySecurityToken(azureClientCredentials.StorageSharedKey); + } + + private SecurityToken CreateSasSecurityToken(AzureClientCredentials azureClientCredentials) + { + if (azureClientCredentials.Sas == null) + { + throw new InvalidOperationException(SR.SasCredentialNotProvidedOnAzureClientCredentials); + } + + return new SasSecurityToken(azureClientCredentials.Sas); + } + + private SecurityToken CreateDefaultSecurityToken(AzureClientCredentials azureClientCredentials) + { + DefaultAzureCredential cred; + if (azureClientCredentials.DefaultAzureCredentialOptions != null) + { + cred = new DefaultAzureCredential(azureClientCredentials.DefaultAzureCredentialOptions); + } + else + { + cred = new DefaultAzureCredential(); + } + + return new TokenCredentialSecurityToken(cred); + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/ClientCredentialsExtensions.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/ClientCredentialsExtensions.cs new file mode 100644 index 000000000000..703c00426a5c --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/ClientCredentialsExtensions.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ServiceModel; +using System.ServiceModel.Description; + +namespace Microsoft.WCF.Azure +{ + /// + /// Provides extension methods for working with client credentials in Azure. + /// + public static class ClientCredentialsExtensions + { + /// + /// Configures the to use a new instance. + /// + /// The channel factory. + /// The instance that was created. + public static AzureClientCredentials UseAzureCredentials(this ChannelFactory channelFactory) + { + var creds = new AzureClientCredentials(); + var behaviors = channelFactory.Endpoint.EndpointBehaviors as KeyedByTypeCollection; + behaviors.Remove(); + behaviors.Add(creds); + return creds; + } + + /// + /// Configures the to use a new instance and allows additional configuration. + /// + /// The channel factory. + /// The configuration action. + /// The instance that was created. + public static AzureClientCredentials UseAzureCredentials(this ChannelFactory channelFactory, Action configure) + { + var creds = channelFactory.UseAzureCredentials(); + if (configure != null) + { + configure(creds); + } + + return creds; + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageBinding.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageBinding.cs new file mode 100644 index 000000000000..a28b2bf02b2f --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageBinding.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Microsoft.WCF.Azure.StorageQueues.Channels; +using System.ServiceModel; +using System.ServiceModel.Channels; + +namespace Microsoft.WCF.Azure.StorageQueues +{ + /// + /// The class that contains the binding elements that specify the protocols, transports, + /// and message encoders used for communication between clients and services. + /// + public class AzureQueueStorageBinding : Binding + { + private AzureQueueStorageTransportBindingElement _transport; + private TextMessageEncodingBindingElement _textMessageEncodingBindingElement; + private BinaryMessageEncodingBindingElement _binaryMessageEncodingBindingElement; + private bool _isInitialized; + + /// + /// Initializes a new instance of the AzureQueueStorageBinding class. + /// + public AzureQueueStorageBinding() + { + Security = new AzureQueueStorageSecurity(); + Initialize(); + } + + /// + /// Gets the URI scheme that specifies the transport used by the channel and listener + /// factories that are built by the bindings. + /// + public override string Scheme => _transport.Scheme; + + /// + /// Gets or sets the security used with this binding. + /// + public AzureQueueStorageSecurity Security { get; set; } + + /// + /// Overidden method to create a collection that contains the binding elements that are part of the current binding. + /// + public override BindingElementCollection CreateBindingElements() + { + BindingElementCollection elements = new(); + + switch (MessageEncoding) + { + case AzureQueueStorageMessageEncoding.Binary: + elements.Add(_binaryMessageEncodingBindingElement); + _transport.QueueMessageEncoding = QueueMessageEncoding.Base64; + break; + case AzureQueueStorageMessageEncoding.Text: + elements.Add(_textMessageEncodingBindingElement); + _transport.QueueMessageEncoding = QueueMessageEncoding.None; + break; + default: + elements.Add(_textMessageEncodingBindingElement); + _transport.QueueMessageEncoding = QueueMessageEncoding.None; + break; + } + Security.Transport.ConfigureTransportSecurity(_transport); + elements.Add(_transport); + + return elements; + } + + private void Initialize() + { + if (!_isInitialized) + { + _transport = new AzureQueueStorageTransportBindingElement(); + _textMessageEncodingBindingElement = new TextMessageEncodingBindingElement(); + _binaryMessageEncodingBindingElement = new BinaryMessageEncodingBindingElement(); + _isInitialized = true; + } + } + + /// + /// Gets and sets the message encoding. + /// + public AzureQueueStorageMessageEncoding MessageEncoding { get; set; } = AzureQueueStorageMessageEncoding.Binary; + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageChannelHelpers.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageChannelHelpers.cs new file mode 100644 index 000000000000..b12177c5615c --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageChannelHelpers.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using System; +using System.Globalization; +using System.Linq; +using System.ServiceModel; + +namespace Microsoft.WCF.Azure.StorageQueues +{ + internal static class AzureQueueStorageChannelHelpers + { + /// + /// The Channel layer normalizes exceptions thrown by the underlying networking implementations + /// into subclasses of CommunicationException, so that Channels can be used polymorphically from + /// an exception handling perspective. + /// + internal static CommunicationException ConvertTransferException(Exception e) + { + return new CommunicationException( + string.Format(CultureInfo.CurrentCulture, + SR.SendError, e.Message), + e); + } + + internal static void ThrowIfDisposedOrNotOpen(object state) + { + switch (state) + { + case CommunicationState.Created: + case CommunicationState.Opening: + case CommunicationState.Closing: + case CommunicationState.Closed: + case CommunicationState.Faulted: + throw new CommunicationException(string.Format(SR.CommunicationObjectNotOpen, state)); + default: + throw new CommunicationException("Unknown CommunicationObject.state"); + case CommunicationState.Opened: + break; + } + } + + internal static Uri ExtractQueueUriFromConnectionString(string connectionString) + { + string[] parts = connectionString.Split(';'); + + foreach (string part in parts) + { + var keyValue = part.Trim().Split('='); + if (keyValue[0].Equals("QueueEndpoint", StringComparison.OrdinalIgnoreCase)) + { + if (Uri.TryCreate(keyValue[1], default, out Uri endpointUri)) + { + return endpointUri; + } + } + } + + return null; + } + + internal static void ExtractAccountAndQueueNameFromUri(Uri endpointUri, bool queueNameRequired, out string accountName, out string queueName) + { + if (endpointUri.HostNameType == UriHostNameType.Dns && !endpointUri.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + // Account name should be first component of hostname, eg + // https://.queue.core.windows.net/ + + if (queueNameRequired) + { + // If queue name required, then the uri should have 2 segments, "/" and the queue name + if (endpointUri.Segments.Length != 2) + { + throw new ArgumentException(string.Format(SR.AccountNameShouldBePartOfHostName, endpointUri)); + } + queueName = endpointUri.Segments[1].TrimEnd('/'); + } + else + { + // If the queue name is not required, then there could be 2 or 1 segments. Any more than 2 means the account + // name was in the path + if (endpointUri.Segments.Length > 2) + { + throw new ArgumentException(string.Format(SR.AccountNameShouldBePartOfHostName, endpointUri)); + } + if (endpointUri.Segments.Length == 2) + { + // Queue name is part of path, so extract it + queueName = endpointUri.Segments[1].TrimEnd('/'); + } + else + { + // Queue name wasn't provided in url, and wasn't required + queueName = String.Empty; + } + } + + accountName = endpointUri.Host.Split('.')[0]; + } + else + { + // Hostname is not a Dns hostname or is localhost. Likely using Azure where the account name is + // a path segment, eg + // https://127.0.0.1// + if (queueNameRequired) + { + if (endpointUri.Segments.Length != 3) + { + throw new ArgumentException(string.Format(SR.AccountNameShouldBePartOfUriPath, endpointUri)); + } + queueName = endpointUri.Segments[2].TrimEnd('/'); + } + else + { + // If the queue name is not required, then there could be 3 or 2 segments. + if (endpointUri.Segments.Length < 2) + { + throw new ArgumentException(string.Format(SR.AccountNameShouldBePartOfHostName, endpointUri)); + } + if (endpointUri.Segments.Length == 3) + { + // Queue name is part of path, so extract it + queueName = endpointUri.Segments[2].TrimEnd('/'); + } + else + { + // Queue name wasn't provided in url, and wasn't required + queueName = String.Empty; + } + } + + accountName = endpointUri.Segments[1].TrimEnd('/'); + } + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageConstants.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageConstants.cs new file mode 100644 index 000000000000..351762c1d4a6 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageConstants.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ServiceModel.Channels; + +namespace Microsoft.WCF.Azure.StorageQueues +{ + /// + /// Collection of constants used by the AzureQueueStorage Channel classes + /// + internal static class AzureQueueStorageConstants + { + internal const string EventLogSourceName = "Microsoft.ServiceModel.AQS"; + internal const string Scheme = "net.aqs"; + private static readonly MessageEncoderFactory s_messageEncoderFactory; + + static AzureQueueStorageConstants() + { + s_messageEncoderFactory = new TextMessageEncodingBindingElement().CreateMessageEncoderFactory(); + } + + // ensure our advertised MessageVersion matches the version we're + // using to serialize/deserialize data to/from the wire + internal static MessageVersion MessageVersion => s_messageEncoderFactory.MessageVersion; + + internal static MessageEncoderFactory DefaultMessageEncoderFactory => s_messageEncoderFactory; + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageMessageEncoding.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageMessageEncoding.cs new file mode 100644 index 000000000000..77c9b4565217 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageMessageEncoding.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.WCF.Azure.StorageQueues +{ + /// + /// The enum which will be used by message encoder. + /// + public enum AzureQueueStorageMessageEncoding + { + /// + /// Indicates using Binary message encoder. + /// + Binary, + + /// + /// Indicates using Text message encoder. + /// + Text, + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageSecurity.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageSecurity.cs new file mode 100644 index 000000000000..bbb81610fcbc --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageSecurity.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.WCF.Azure.StorageQueues +{ + /// + /// Class to configure the binding security. + /// + public class AzureQueueStorageSecurity + { + private AzureQueueStorageTransportSecurity _transport; + + /// + /// Initializes a new instance of the class. + /// + public AzureQueueStorageSecurity() : this(new AzureQueueStorageTransportSecurity()) { } + + private AzureQueueStorageSecurity(AzureQueueStorageTransportSecurity azureQueueStorageTransportSecurity) + { + Transport = azureQueueStorageTransportSecurity; + } + + /// + /// Gets an object that contains the transport-level security settings for this binding. + /// + public AzureQueueStorageTransportSecurity Transport + { + get => _transport; + set + { + _transport = value ?? throw new ArgumentNullException(nameof(Transport)); + } + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageTransportSecurity.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageTransportSecurity.cs new file mode 100644 index 000000000000..f44cdb0acd8a --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/AzureQueueStorageTransportSecurity.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.WCF.Azure.StorageQueues.Channels; +using System; + +namespace Microsoft.WCF.Azure.StorageQueues +{ + /// + /// Represents the transport security settings for AzureQueueStorageBinding. + /// + public class AzureQueueStorageTransportSecurity + { + internal const AzureClientCredentialType DefaultClientCredentialType = AzureClientCredentialType.Default; + private AzureClientCredentialType _clientCredentialType = DefaultClientCredentialType; + + /// + /// Gets or sets the type of client credential used for authentication. + /// + /// The client credential type. + public AzureClientCredentialType ClientCredentialType + { + get { return _clientCredentialType; } + set + { + if (!AzureClientCredentialTypeHelper.IsDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _clientCredentialType = value; + } + } + + internal void ConfigureTransportSecurity(AzureQueueStorageTransportBindingElement transport) + { + transport.ClientCredentialType = ClientCredentialType; + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/Channels/AzureQueueStorageChannelFactory.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/Channels/AzureQueueStorageChannelFactory.cs new file mode 100644 index 000000000000..56143311b108 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/Channels/AzureQueueStorageChannelFactory.cs @@ -0,0 +1,275 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core.Pipeline; +using Azure.Storage.Queues; +using Microsoft.WCF.Azure.Tokens; +using System; +using System.Collections.ObjectModel; +using System.IdentityModel.Policy; +using System.IdentityModel.Selectors; +using System.IdentityModel.Tokens; +using System.Net.Http; +using System.Net.Security; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.X509Certificates; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.ServiceModel.Security; +using System.ServiceModel.Security.Tokens; +using System.Threading.Tasks; + +namespace Microsoft.WCF.Azure.StorageQueues.Channels +{ + /// + /// IChannelFactory implementation for AzureQueueStorage. + /// + internal class AzureQueueStorageChannelFactory : ChannelFactoryBase + { + private const string SecurityTokenTypesNamespace = "http://schemas.microsoft.com/ws/2006/05/identitymodel/tokens"; + private const string X509CertificateTokenType = SecurityTokenTypesNamespace + "/X509Certificate"; + private const string RequirementNamespace = "http://schemas.microsoft.com/ws/2006/05/servicemodel/securitytokenrequirement"; + private const string PreferSslCertificateAuthenticatorProperty = RequirementNamespace + "/PreferSslCertificateAuthenticator"; + + private readonly AzureQueueStorageTransportBindingElement _azureQueueStorageTransportBindingElement; + private SecurityCredentialsManager _channelCredentials; + + public AzureQueueStorageChannelFactory(AzureQueueStorageTransportBindingElement bindingElement, BindingContext context) + : base(context.Binding) + { + _azureQueueStorageTransportBindingElement = bindingElement; + var messageEncoderBindingElement = context.BindingParameters.Find(); + MessageEncoderFactory = messageEncoderBindingElement == null + ? AzureQueueStorageConstants.DefaultMessageEncoderFactory + : messageEncoderBindingElement.CreateMessageEncoderFactory(); + + BufferManager = BufferManager.CreateBufferManager(bindingElement.MaxBufferPoolSize, int.MaxValue); + + _channelCredentials = context.BindingParameters.Find(); + } + + private SecurityTokenAuthenticator SecurityTokenAuthenticator { get; } + + internal HttpClient HttpClient { get; } + + public BufferManager BufferManager { get; } + + internal string ConnectionString { get; } + + public MessageEncoderFactory MessageEncoderFactory { get; } = null; + + public SecurityTokenManager SecurityTokenManager { get; private set; } + + public override T GetProperty() + { + T messageEncoderProperty = MessageEncoderFactory.Encoder.GetProperty(); + if (messageEncoderProperty != null) + { + return messageEncoderProperty; + } + + if (typeof(T) == typeof(MessageVersion)) + { + return (T)(object)MessageEncoderFactory.Encoder.MessageVersion; + } + + return base.GetProperty(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void InitializeSecurityTokenManager() + { + var channelCredentials = _channelCredentials as AzureClientCredentials; + if (channelCredentials == null) + { + // If using the Default credential type, no need to configure AzureClientCredentials on the ChannelFactory + // and we can use a default instance which just wraps the DefaultAzureCredential with no config needed. If + // a different client credential type is configured, then AzureClientCredentials will throw with a message + // indicating the need to configure the credential. + channelCredentials = new AzureClientCredentials(); + } + + SecurityTokenManager = channelCredentials.CreateSecurityTokenManager(); + } + + protected override void OnOpen(TimeSpan timeout) + { + InitializeSecurityTokenManager(); + } + + protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state) + { + InitializeSecurityTokenManager(); + return Task.CompletedTask.ToApm(callback, state); + } + + protected override void OnEndOpen(IAsyncResult result) + { + result.ToApmEnd(); + } + + /// + /// Create a new Azure Queue Storage Channel. Supports IOutputChannel. + /// + /// The address of the remote endpoint + /// The via address. + /// + protected override IOutputChannel OnCreateChannel(EndpointAddress remoteAddress, Uri via) + { + return new AzureQueueStorageOutputChannel(this, remoteAddress, via, MessageEncoderFactory.Encoder, _azureQueueStorageTransportBindingElement); + } + + protected override void OnClosed() + { + base.OnClosed(); + BufferManager.Clear(); + } + + internal async Task CreateAndOpenTokenProviderAsync(TimeSpan timeout, EndpointAddress target, Uri via, ChannelParameterCollection channelParameters) + { + TimeoutHelper timeoutHelper = new TimeoutHelper(timeout); + var tokenProviderContainer = CreateTokenProvider(timeout, target, via, channelParameters); + + if (tokenProviderContainer != null) + { + await tokenProviderContainer.OpenAsync(timeoutHelper.RemainingTime()).ConfigureAwait(false); + } + + return tokenProviderContainer; + } + + internal SecurityTokenProviderContainer CreateAndOpenTokenProvider(TimeSpan timeout, EndpointAddress target, Uri via, ChannelParameterCollection channelParameters) + { + TimeoutHelper timeoutHelper = new TimeoutHelper(timeout); + var tokenProviderContainer = CreateTokenProvider(timeout, target, via, channelParameters); + + if (tokenProviderContainer != null) + { + tokenProviderContainer.Open(timeoutHelper.RemainingTime()); + } + + return tokenProviderContainer; + } + + private SecurityTokenProviderContainer CreateTokenProvider(TimeSpan timeout, EndpointAddress target, Uri via, ChannelParameterCollection channelParameters) + { + SecurityTokenProvider tokenProvider = null; + switch (_azureQueueStorageTransportBindingElement.ClientCredentialType) + { + case AzureClientCredentialType.Default: + tokenProvider = SecurityUtils.GetDefaultTokenProvider(SecurityTokenManager, target, via, AzureQueueStorageConstants.Scheme, channelParameters); + break; + case AzureClientCredentialType.Sas: + tokenProvider = SecurityUtils.GetSasTokenProvider(SecurityTokenManager, target, via, AzureQueueStorageConstants.Scheme, channelParameters); + break; + case AzureClientCredentialType.StorageSharedKey: + tokenProvider = SecurityUtils.GetStorageSharedKeyTokenProvider(SecurityTokenManager, target, via, AzureQueueStorageConstants.Scheme, channelParameters); + break; + case AzureClientCredentialType.Token: + tokenProvider = SecurityUtils.GetTokenTokenProvider(SecurityTokenManager, target, via, AzureQueueStorageConstants.Scheme, channelParameters); + break; + case AzureClientCredentialType.ConnectionString: + tokenProvider = SecurityUtils.GetConnectionStringTokenProvider(SecurityTokenManager, target, via, AzureQueueStorageConstants.Scheme, channelParameters); + break; + default: + // The setter for this property should prevent this. +#pragma warning disable CA2208 // Instantiate argument exceptions correctly + throw new ArgumentOutOfRangeException(nameof(_azureQueueStorageTransportBindingElement.ClientCredentialType), _azureQueueStorageTransportBindingElement.ClientCredentialType, null); +#pragma warning restore CA2208 // Instantiate argument exceptions correctly + } + + return tokenProvider == null ? null : new SecurityTokenProviderContainer(tokenProvider); + } + + internal Task GetQueueClientAsync(Uri via, SecurityTokenProviderContainer tokenProvider, TimeSpan timeout) + { + return GetQueueClientAsyncCore(via, tokenProvider, timeout); + } + + internal QueueClient GetQueueClient(Uri via, SecurityTokenProviderContainer tokenProvider, TimeSpan timeout) + { +#pragma warning disable AZC0106 // Non-public asynchronous method needs 'async' parameter. + return GetQueueClientAsyncCore(via, tokenProvider, timeout).EnsureCompleted(); +#pragma warning restore AZC0106 // Non-public asynchronous method needs 'async' parameter. + } + + internal Task GetQueueClientAsyncCore(Uri via, SecurityTokenProviderContainer tokenProvider, TimeSpan timeout) + { + QueueClientOptions queueClientOptions = BuildQueueClientOptions(); + switch (_azureQueueStorageTransportBindingElement.ClientCredentialType) + { + case AzureClientCredentialType.Default: + case AzureClientCredentialType.Token: + return SecurityUtils.CreateQueueClientWithTokenCredentialAsync(tokenProvider, via, queueClientOptions, timeout); + case AzureClientCredentialType.Sas: + return SecurityUtils.CreateQueueClientWithSasCredentialAsync(tokenProvider, via, queueClientOptions, timeout); + case AzureClientCredentialType.StorageSharedKey: + return SecurityUtils.CreateQueueClientWithStorageSharedKeyCredentialAsync(tokenProvider, via, queueClientOptions, timeout); + case AzureClientCredentialType.ConnectionString: + return SecurityUtils.CreateQueueClientWithConnectionStringAsync(tokenProvider, via, queueClientOptions, timeout); + default: + return Task.FromResult(null); + } + } + + private QueueClientOptions BuildQueueClientOptions() + { + QueueClientOptions options = new(); + var azureClientCredentials = _channelCredentials as AzureClientCredentials; + if (azureClientCredentials != null) + { + options.Audience = azureClientCredentials.Audience; + options.EnableTenantDiscovery = azureClientCredentials.EnableTenantDiscovery; + } + + options.MessageEncoding = _azureQueueStorageTransportBindingElement.QueueMessageEncoding; + + var certificateValidationCallback = GetServiceCertificateValidationCallback(); + if (certificateValidationCallback != null) + { + HttpClientHandler httpClientHandler = new HttpClientHandler(); + httpClientHandler.ServerCertificateCustomValidationCallback = certificateValidationCallback; + HttpClient httpClient = new(httpClientHandler); + HttpClientTransport httpClientTransport = new HttpClientTransport(httpClient); + options.Transport = httpClientTransport; + } + + return options; + } + + private Func GetServiceCertificateValidationCallback() + { + var securityTokenManager = _channelCredentials.CreateSecurityTokenManager(); + InitiatorServiceModelSecurityTokenRequirement serverCertRequirement = new() + { + TokenType = X509CertificateTokenType, + RequireCryptographicToken = true, + KeyUsage = SecurityKeyUsage.Exchange, + TransportScheme = _azureQueueStorageTransportBindingElement.Scheme + }; + serverCertRequirement.Properties[PreferSslCertificateAuthenticatorProperty] = true; + + var securityTokenAuthenticator = securityTokenManager.CreateSecurityTokenAuthenticator(serverCertRequirement, out SecurityTokenResolver dummy) as X509SecurityTokenAuthenticator; + if (securityTokenAuthenticator == null) + { + return null; + } + + bool RemoteCertificateValidationCallback(HttpRequestMessage sender, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + try + { + SecurityToken token = new X509SecurityToken(certificate); + ReadOnlyCollection authorizationPolicies = securityTokenAuthenticator.ValidateToken(token); + return true; + } + catch (Exception) + { + return false; + } + } + + return RemoteCertificateValidationCallback; + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/Channels/AzureQueueStorageOutputChannel.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/Channels/AzureQueueStorageOutputChannel.cs new file mode 100644 index 000000000000..20f90239c13d --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/Channels/AzureQueueStorageOutputChannel.cs @@ -0,0 +1,324 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.Pipeline; +using Azure.Storage.Queues; +using Microsoft.WCF.Azure.Tokens; + +namespace Microsoft.WCF.Azure.StorageQueues.Channels +{ + /// + /// IOutputChannel implementation for AzureQueueStorage. + /// + internal class AzureQueueStorageOutputChannel : ChannelBase, IOutputChannel + { + #region member_variables + private readonly EndpointAddress _remoteAddress; + private readonly Uri _via; + private readonly MessageEncoder _encoder; + private readonly AzureQueueStorageChannelFactory _parent; + private QueueClient _queueClient; + private ArraySegment _messageBuffer; + private ChannelParameterCollection _channelParameters; + private SecurityTokenProviderContainer _tokenProvider; + #endregion + + public AzureQueueStorageOutputChannel( + AzureQueueStorageChannelFactory factory, + EndpointAddress remoteAddress, + Uri via, + MessageEncoder encoder, + AzureQueueStorageTransportBindingElement azureQueueStorageTransportBindingElement) + : base(factory) + { + _remoteAddress = remoteAddress; + _via = via; + _encoder = encoder; + _parent = factory; + _queueClient = null; + } + + #region IOutputChannel_Properties + /// + /// Gets the destination of the service to which messages are sent out on the output channel. + /// + EndpointAddress IOutputChannel.RemoteAddress + { + get + { + return _remoteAddress; + } + } + + /// + /// Gets the URI that contains the transport address to which messages are sent on the output channel. + /// + Uri IOutputChannel.Via + { + get + { + return _via; + } + } + #endregion + + public override T GetProperty() + { + if (typeof(T) == typeof(IOutputChannel)) + { + return (T)(object)this; + } + + if (typeof(T) == typeof(ChannelParameterCollection)) + { + if (State == CommunicationState.Created) + { + lock (ThisLock) + { + if (_channelParameters == null) + { + _channelParameters = new ChannelParameterCollection(); + } + } + } + return (T)(object)_channelParameters; + } + + T messageEncoderProperty = _encoder.GetProperty(); + if (messageEncoderProperty != null) + { + return messageEncoderProperty; + } + + return base.GetProperty(); + } + + /// + /// Open the channel for use. We do not have any blocking work to perform so this is a no-op + /// + protected override void OnOpen(TimeSpan timeout) + { + TimeoutHelper timeoutHelper = new TimeoutHelper(timeout); + _tokenProvider = _parent.CreateAndOpenTokenProvider(timeout, _remoteAddress, _via, _channelParameters); + EnsureQueueClient(timeoutHelper.RemainingTime()); + } + + protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state) + { + return OnOpenAsync(timeout).ToApm(callback, state); + } + + protected override void OnEndOpen(IAsyncResult result) + { + result.ToApmEnd(); + } + + private async Task OnOpenAsync(TimeSpan timeout) + { + TimeoutHelper timeoutHelper = new TimeoutHelper(timeout); + _tokenProvider = await _parent.CreateAndOpenTokenProviderAsync(timeout, _remoteAddress, _via, _channelParameters).ConfigureAwait(false); + await EnsureQueueClientAsync(timeoutHelper.RemainingTime()).ConfigureAwait(false); + } + + #region Shutdown + /// + /// Shutdown ungracefully + /// + protected override void OnAbort() + { + if (_tokenProvider != null) + { + _tokenProvider.Abort(); + } + } + + /// + /// Shutdown gracefully + /// + protected override void OnClose(TimeSpan timeout) + { + CloseTokenProviders(timeout); + } + + protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state) + { + return OnCloseAsync(timeout).ToApm(callback, state); + } + + protected override void OnEndClose(IAsyncResult result) + { + result.ToApmEnd(); + } + + private Task OnCloseAsync(TimeSpan timeout) + { + return CloseTokenProviderAsync(timeout); + } + + private Task CloseTokenProviderAsync(TimeSpan timeout) + { + if (_tokenProvider != null) + { + return _tokenProvider.CloseAsync(timeout); + } + + return Task.CompletedTask; + } + + private void CloseTokenProviders(TimeSpan timeout) + { + if (_tokenProvider != null) + { + _tokenProvider.Close(timeout); + } + } + #endregion + + #region Send_Synchronous + public void Send(Message message) + { + Send(message, default); + } + + public void Send(Message message, TimeSpan timeout) + { + using CancellationTokenSource cts = new(timeout); + + try + { + ArraySegment messageBuffer = EncodeMessage(message); + BinaryData binaryData = new(new ReadOnlyMemory(messageBuffer.Array, messageBuffer.Offset, messageBuffer.Count)); + _queueClient.SendMessage(binaryData, default, default, cts.Token); + } + catch (Exception e) + { + throw AzureQueueStorageChannelHelpers.ConvertTransferException(e); + } + finally + { + CleanupBuffer(); + } + } + #endregion + + #region Send_Asynchronous + public IAsyncResult BeginSend(Message message, AsyncCallback callback, object state) + { + return BeginSend(message, default, callback, state); + } + + public IAsyncResult BeginSend(Message message, TimeSpan timeout, AsyncCallback callback, object state) + { + AzureQueueStorageChannelHelpers.ThrowIfDisposedOrNotOpen(state); + return SendAsync(message, timeout).ToApm(callback, state); + } + + public void EndSend(IAsyncResult result) + { + result.ToApmEnd(); + } + + private async Task SendAsync(Message message, TimeSpan timeout) + { + CancellationTokenSource cts = new(timeout); + + try + { + _messageBuffer = EncodeMessage(message); + BinaryData binaryData = new(_messageBuffer); + await _queueClient.SendMessageAsync(binaryData, default, default, cts.Token).ConfigureAwait(false); + } + catch (Exception e) + { + throw AzureQueueStorageChannelHelpers.ConvertTransferException(e); + } + finally + { + CleanupBuffer(); + cts.Dispose(); + } + } + + private async Task EnsureQueueClientAsync(TimeSpan timeout) + { + if (_queueClient == null) + { + QueueClient queueClient = await _parent.GetQueueClientAsync(_via, _tokenProvider, timeout).ConfigureAwait(false); + var channelParameters = GetProperty(); + if (channelParameters is not null) + { + foreach (object obj in channelParameters) + { + if (obj is Func queueClientConfigfunc) + { + var configuredQueueClient = queueClientConfigfunc(queueClient); + if (configuredQueueClient != null) + { + queueClient = configuredQueueClient; + } + } + } + } + + Interlocked.CompareExchange(ref _queueClient, queueClient, null); + } + } + + private void EnsureQueueClient(TimeSpan timeout) + { + if (_queueClient == null) + { + QueueClient queueClient = _parent.GetQueueClient(_via, _tokenProvider, timeout); + var channelParameters = GetProperty(); + if (channelParameters is not null) + { + foreach (object obj in channelParameters) + { + if (obj is Func queueClientConfigfunc) + { + var configuredQueueClient = queueClientConfigfunc(queueClient); + if (configuredQueueClient != null) + { + queueClient = configuredQueueClient; + } + } + } + } + + Interlocked.CompareExchange(ref _queueClient, queueClient, null); + } + } + #endregion + + /// + /// Address the Message and serialize it into a byte array. + /// + private ArraySegment EncodeMessage(Message message) + { + try + { + _remoteAddress.ApplyTo(message); + return _encoder.WriteMessage(message, int.MaxValue, _parent.BufferManager); + } + finally + { + // We have consumed the message by serializing it, so clean up + message.Close(); + } + } + + private void CleanupBuffer() + { + if (_messageBuffer.Array != null) + { + _parent.BufferManager.ReturnBuffer(_messageBuffer.Array); + _messageBuffer = default; + } + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/Channels/AzureQueueStorageTransportBindingElement.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/Channels/AzureQueueStorageTransportBindingElement.cs new file mode 100644 index 000000000000..bf2c30db8427 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/Channels/AzureQueueStorageTransportBindingElement.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using System; +using System.ServiceModel.Channels; + +namespace Microsoft.WCF.Azure.StorageQueues.Channels +{ + /// + /// Class that represents Azure Queue Storage transport binding element. + /// + public class AzureQueueStorageTransportBindingElement : TransportBindingElement + { + private AzureClientCredentialType _clientCredentialType; + + /// + /// Creates a new instance of the AzureQueueStorageTransportBindingElement Class. + /// + public AzureQueueStorageTransportBindingElement() + { + ClientCredentialType = AzureClientCredentialType.Default; + } + + /// + /// Creates a new instance of this class from an existing instance. + /// + protected AzureQueueStorageTransportBindingElement(AzureQueueStorageTransportBindingElement other) : base(other) + { + ClientCredentialType = other.ClientCredentialType; + QueueMessageEncoding = other.QueueMessageEncoding; + } + + /// + /// Overridden method to build channel factory from binding context. + /// + public override IChannelFactory BuildChannelFactory(BindingContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return (IChannelFactory)(object)new AzureQueueStorageChannelFactory(this, context); + } + + /// + /// Used by higher layers to determine what types of channel factories this + /// binding element supports. Which in this case is just IOutputChannel. + /// + public override bool CanBuildChannelFactory(BindingContext context) + { + return typeof(TChannel) == typeof(IOutputChannel); + } + + /// + /// Gets or sets the type of client credential used for authentication. + /// + /// The client credential type. + public AzureClientCredentialType ClientCredentialType + { + get { return _clientCredentialType; } + set + { + if (!AzureClientCredentialTypeHelper.IsDefined(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _clientCredentialType = value; + } + } + + /// + /// Gets the URI scheme for the transport. + /// + public override string Scheme => AzureQueueStorageConstants.Scheme; + + /// + /// Overridden method to return a copy of the binding AzureQueueStorageTransportBindingElement object. + /// + public override BindingElement Clone() => new AzureQueueStorageTransportBindingElement(this); + + /// + /// Gets a property from the specified BindingContext. + /// + public override T GetProperty(BindingContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return context.GetInnerProperty(); + } + + /// + /// Gets the QueueMessageEncoding for the transport. + /// + public QueueMessageEncoding QueueMessageEncoding { get; set; } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/Channels/SecurityUtils.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/Channels/SecurityUtils.cs new file mode 100644 index 000000000000..58094c352298 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/Channels/SecurityUtils.cs @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Microsoft.WCF.Azure.Tokens; +using System; +using System.IdentityModel.Selectors; +using System.IdentityModel.Tokens; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.ServiceModel.Security.Tokens; +using System.Threading.Tasks; + +namespace Microsoft.WCF.Azure.StorageQueues.Channels +{ + internal class SecurityUtils + { + public static SecurityTokenProvider GetDefaultTokenProvider(SecurityTokenManager tokenManager, EndpointAddress target, Uri via, + string transportScheme, ChannelParameterCollection channelParameters) + { + if (tokenManager != null) + { + var tokenRequirement = CreateTokenRequirement(target, via, transportScheme, channelParameters, AzureSecurityTokenTypes.DefaultTokenType); + return tokenManager.CreateSecurityTokenProvider(tokenRequirement); + } + + return null; + } + + public static SecurityTokenProvider GetSasTokenProvider(SecurityTokenManager tokenManager, EndpointAddress target, Uri via, + string transportScheme, ChannelParameterCollection channelParameters) + { + if (tokenManager != null) + { + var tokenRequirement = CreateTokenRequirement(target, via, transportScheme, channelParameters, AzureSecurityTokenTypes.SasTokenType); + return tokenManager.CreateSecurityTokenProvider(tokenRequirement); + } + + return null; + } + + public static SecurityTokenProvider GetStorageSharedKeyTokenProvider(SecurityTokenManager tokenManager, EndpointAddress target, Uri via, + string transportScheme, ChannelParameterCollection channelParameters) + { + if (tokenManager != null) + { + var tokenRequirement = CreateTokenRequirement(target, via, transportScheme, channelParameters, AzureSecurityTokenTypes.StorageSharedKeyTokenType); + return tokenManager.CreateSecurityTokenProvider(tokenRequirement); + } + + return null; + } + + public static SecurityTokenProvider GetTokenTokenProvider(SecurityTokenManager tokenManager, EndpointAddress target, Uri via, + string transportScheme, ChannelParameterCollection channelParameters) + { + if (tokenManager != null) + { + var tokenRequirement = CreateTokenRequirement(target, via, transportScheme, channelParameters, AzureSecurityTokenTypes.TokenTokenType); + return tokenManager.CreateSecurityTokenProvider(tokenRequirement); + } + + return null; + } + + public static SecurityTokenProvider GetConnectionStringTokenProvider(SecurityTokenManager tokenManager, EndpointAddress target, Uri via, + string transportScheme, ChannelParameterCollection channelParameters) + { + if (tokenManager != null) + { + var tokenRequirement = CreateTokenRequirement(target, via, transportScheme, channelParameters, AzureSecurityTokenTypes.ConnectionStringTokenType); + return tokenManager.CreateSecurityTokenProvider(tokenRequirement); + } + + return null; + } + + private static SecurityTokenRequirement CreateTokenRequirement(EndpointAddress target, Uri via, string transportScheme, ChannelParameterCollection channelParameters, string tokenType) + { + InitiatorServiceModelSecurityTokenRequirement azureTokenRequirement = new InitiatorServiceModelSecurityTokenRequirement(); + azureTokenRequirement.TokenType = tokenType; + azureTokenRequirement.RequireCryptographicToken = false; + azureTokenRequirement.TransportScheme = transportScheme; + azureTokenRequirement.TargetAddress = target; + azureTokenRequirement.Via = via; + if (channelParameters != null) + { + azureTokenRequirement.Properties[ServiceModelSecurityTokenRequirement.ChannelParametersCollectionProperty] = channelParameters; + } + + return azureTokenRequirement; + } + + internal static void OpenTokenProviderIfRequired(SecurityTokenProvider tokenProvider, TimeSpan timeout) + { + if (tokenProvider is ICommunicationObject communicationObject && communicationObject != null) + { + communicationObject.Open(timeout); + } + } + + internal static void CloseTokenProviderIfRequired(SecurityTokenProvider tokenProvider, TimeSpan timeout) + { + CloseCommunicationObject(tokenProvider, false, timeout); + } + + internal static Task OpenTokenProviderIfRequiredAsync(SecurityTokenProvider tokenProvider, TimeSpan timeout) + { + if (tokenProvider is ICommunicationObject communicationObject && communicationObject != null) + { + return Task.Factory.FromAsync(communicationObject.BeginOpen, communicationObject.EndOpen, timeout, null, TaskCreationOptions.None); + } + + return Task.CompletedTask; + } + + internal static Task CloseTokenProviderIfRequiredAsync(SecurityTokenProvider tokenProvider, TimeSpan timeout) + { + if (tokenProvider is ICommunicationObject communicationObject && communicationObject != null) + { + return Task.Factory.FromAsync(communicationObject.BeginClose, communicationObject.EndClose, timeout, null, TaskCreationOptions.None); + } + + return Task.CompletedTask; + } + + internal static void AbortTokenProviderIfRequired(SecurityTokenProvider tokenProvider) + { + CloseCommunicationObject(tokenProvider, true, TimeSpan.Zero); + } + + private static void CloseCommunicationObject(Object obj, bool aborted, TimeSpan timeout) + { + if (obj != null) + { + ICommunicationObject co = obj as ICommunicationObject; + if (co != null) + { + if (aborted) + { + try + { + co.Abort(); + } + catch (CommunicationException) + { + } + } + else + { + co.Close(timeout); + } + } + else if (obj is IDisposable) + { + ((IDisposable)obj).Dispose(); + } + } + } + + private static async Task GetTokenAsync(SecurityTokenProvider tokenProvider, TimeSpan timeout) where T : SecurityToken + { + SecurityToken result = await tokenProvider.GetTokenAsync(timeout).ConfigureAwait(false); + if ((result != null) && !(result is T)) + { + throw new InvalidOperationException(String.Format(SR.InvalidTokenProvided, tokenProvider.GetType(), typeof(T))); + } + return result as T; + } + + internal static async Task CreateQueueClientWithTokenCredentialAsync(SecurityTokenProviderContainer tokenProvider, Uri via, QueueClientOptions queueClientOptions, TimeSpan timeout) + { + var provider = tokenProvider.TokenProvider; + var endpointUri = new UriBuilder(via) { Scheme = "https" }.Uri; + TokenCredentialSecurityToken result = await GetTokenAsync(provider, timeout).ConfigureAwait(false); + var queueClient = new QueueClient(endpointUri, result.TokenCredential, queueClientOptions); + return queueClient; + } + + internal static async Task CreateQueueClientWithSasCredentialAsync(SecurityTokenProviderContainer tokenProvider, Uri via, QueueClientOptions queueClientOptions, TimeSpan timeout) + { + var provider = tokenProvider.TokenProvider; + var endpointUri = new UriBuilder(via) { Scheme = "https" }.Uri; + SasSecurityToken result = await GetTokenAsync(provider, timeout).ConfigureAwait(false); + var queueClient = new QueueClient(endpointUri, result.SasCredential, queueClientOptions); + return queueClient; + } + + internal static async Task CreateQueueClientWithStorageSharedKeyCredentialAsync(SecurityTokenProviderContainer tokenProvider, Uri via, QueueClientOptions queueClientOptions, TimeSpan timeout) + { + var provider = tokenProvider.TokenProvider; + var endpointUri = new UriBuilder(via) { Scheme = "https" }.Uri; + StorageSharedKeySecurityToken result = await GetTokenAsync(provider, timeout).ConfigureAwait(false); + var queueClient = new QueueClient(endpointUri, result.StorageSharedKeyCredential, queueClientOptions); + return queueClient; + } + + internal static async Task CreateQueueClientWithConnectionStringAsync(SecurityTokenProviderContainer tokenProvider, Uri via, QueueClientOptions queueClientOptions, TimeSpan timeout) + { + var provider = tokenProvider.TokenProvider; + ConnectionStringSecurityToken result = await GetTokenAsync(provider, timeout).ConfigureAwait(false); + var connectionStringUri = AzureQueueStorageChannelHelpers.ExtractQueueUriFromConnectionString(result.ConnectionString); + AzureQueueStorageChannelHelpers.ExtractAccountAndQueueNameFromUri(connectionStringUri, queueNameRequired: false, out string connectionStringAccountName, out string connectionStringQueueName); + AzureQueueStorageChannelHelpers.ExtractAccountAndQueueNameFromUri(via, queueNameRequired: true, out string viaAccountName, out string viaQueueName); + string queueName; + if (!string.IsNullOrEmpty(connectionStringQueueName) && !string.IsNullOrEmpty(viaQueueName) && + !connectionStringQueueName.Equals(viaQueueName, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException(string.Format(SR.ConnectionStringAndViaQueueNameMismatch, connectionStringQueueName, viaQueueName)); + } + queueName = string.IsNullOrEmpty(connectionStringQueueName) ? viaQueueName : connectionStringQueueName; + if (string.IsNullOrEmpty(queueName)) + { + throw new ArgumentException(SR.MissingQueueName); + } + if (!string.IsNullOrEmpty(connectionStringAccountName) && !string.IsNullOrEmpty(viaAccountName) && + !connectionStringAccountName.Equals(viaAccountName, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException(string.Format(SR.ConnectionStringAndViaAccountNameMismatch, connectionStringAccountName, viaAccountName)); + } + + var queueClient = new QueueClient(result.ConnectionString, queueName, queueClientOptions); + return queueClient; + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/TaskHelpers.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/TaskHelpers.cs new file mode 100644 index 000000000000..cf10a97bba27 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/TaskHelpers.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.Contracts; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.WCF.Azure.StorageQueues +{ + /// + /// Helper class for some tasks. + /// + internal static class TaskHelpers + { + /// + /// Converts a task to the APM Begin-End pattern. + /// + public static Task ToApm(this Task task, AsyncCallback callback, object state) + { + // When using APM, the returned IAsyncResult must have the passed in state object stored in AsyncState. This + // is so the callback can regain state. If the incoming task already holds the state object, there's no need + // to create a TaskCompletionSource to ensure the returned (IAsyncResult)Task has the right state object. + // This is a performance optimization for this special case. + if (task.AsyncState == state) + { + if (callback != null) + { + task.ContinueWith((antecedent, obj) => + { + var callbackObj = obj as AsyncCallback; + callbackObj(antecedent); + }, callback, CancellationToken.None, TaskContinuationOptions.HideScheduler, TaskScheduler.Default); + } + return task; + } + + // Need to create a TaskCompletionSource so that the returned Task object has the correct AsyncState value. + // As we intend to create a task with no Result value, we don't care what result type the TCS holds as we + // won't be using it. As Task derives from Task, the returned Task is compatible. + var tcs = new TaskCompletionSource(state, TaskCreationOptions.RunContinuationsAsynchronously); + var continuationState = Tuple.Create(tcs, callback); + task.ContinueWith((antecedent, obj) => + { + var tuple = obj as Tuple, AsyncCallback>; + var tcsObj = tuple.Item1; + var callbackObj = tuple.Item2; + if (antecedent.IsFaulted) + { + tcsObj.TrySetException(antecedent.Exception.InnerException); + } + else if (antecedent.IsCanceled) + { + tcsObj.TrySetCanceled(); + } + else + { + tcsObj.TrySetResult(null); + } + + if (callbackObj != null) + { + callbackObj(tcsObj.Task); + } + }, continuationState, CancellationToken.None, TaskContinuationOptions.HideScheduler, TaskScheduler.Default); + return tcs.Task; + } + + /// + /// Converts the specified IAsyncResult to a Task and waits for it to complete. + /// + public static void ToApmEnd(this IAsyncResult iar) + { + Task task = iar as Task; + Contract.Assert(task != null, "IAsyncResult must be an instance of Task"); +#pragma warning disable AZC0102 // Do not use GetAwaiter().GetResult(). Use the TaskExtensions.EnsureCompleted() extension method instead. + task.GetAwaiter().GetResult(); +#pragma warning restore AZC0102 // Do not use GetAwaiter().GetResult(). Use the TaskExtensions.EnsureCompleted() extension method instead. + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/TimeoutHelper.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/TimeoutHelper.cs new file mode 100644 index 000000000000..2635033d4c8c --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/StorageQueues/TimeoutHelper.cs @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics.Contracts; +using System.Security; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.WCF.Azure.StorageQueues +{ + internal struct TimeoutHelper + { + public static readonly TimeSpan MaxWait = TimeSpan.FromMilliseconds(int.MaxValue); + private static readonly CancellationToken s_precancelledToken = new CancellationToken(true); + + private bool _cancellationTokenInitialized; + private bool _deadlineSet; + + private CancellationToken _cancellationToken; + private DateTime _deadline; + + public TimeoutHelper(TimeSpan timeout) + { + Contract.Assert(timeout >= TimeSpan.Zero, "timeout must be non-negative"); + _cancellationToken = default; + _cancellationTokenInitialized = false; + OriginalTimeout = timeout; + _deadline = DateTime.MaxValue; + _deadlineSet = (timeout == TimeSpan.MaxValue); + } + + public CancellationToken GetCancellationToken() + { + return GetCancellationTokenAsync().Result; + } + + public async Task GetCancellationTokenAsync() + { + if (!_cancellationTokenInitialized) + { + var timeout = RemainingTime(); + if (timeout >= MaxWait || timeout == Timeout.InfiniteTimeSpan) + { + _cancellationToken = CancellationToken.None; + } + else if (timeout > TimeSpan.Zero) + { + _cancellationToken = await TimeoutTokenSource.FromTimeoutAsync((int)timeout.TotalMilliseconds).ConfigureAwait(false); + } + else + { + _cancellationToken = s_precancelledToken; + } + _cancellationTokenInitialized = true; + } + + return _cancellationToken; + } + + public TimeSpan OriginalTimeout { get; } + + public TimeSpan RemainingTime() + { + if (!_deadlineSet) + { + SetDeadline(); + return OriginalTimeout; + } + else if (_deadline == DateTime.MaxValue) + { + return TimeSpan.MaxValue; + } + else + { + TimeSpan remaining = _deadline - DateTime.UtcNow; + if (remaining <= TimeSpan.Zero) + { + return TimeSpan.Zero; + } + else + { + return remaining; + } + } + } + + private void SetDeadline() + { + Contract.Assert(!_deadlineSet, "TimeoutHelper deadline set twice."); + _deadline = DateTime.UtcNow + OriginalTimeout; + _deadlineSet = true; + } + } + + /// + /// This class coalesces timeout tokens because cancelation tokens with timeouts are more expensive to expose. + /// Disposing too many such tokens will cause thread contentions in high throughput scenario. + /// + /// Tokens with target cancelation time 15ms apart would resolve to the same instance. + /// + internal static class TimeoutTokenSource + { + /// + /// These are constants use to calculate timeout coalescing, for more description see method FromTimeoutAsync + /// + private const int CoalescingFactor = 15; + private const int GranularityFactor = 2000; + private const int SegmentationFactor = CoalescingFactor * GranularityFactor; + + private static readonly ConcurrentDictionary> s_tokenCache = + new ConcurrentDictionary>(); + + private static readonly Action s_deregisterToken = (object state) => + { + var args = (Tuple)state; + Task ignored; + try + { + s_tokenCache.TryRemove(args.Item1, out ignored); + } + finally + { + args.Item2.Dispose(); + } + }; + + public static Task FromTimeoutAsync(int millisecondsTimeout) + { + // Note that CancellationTokenSource constructor requires input to be >= -1, + // restricting millisecondsTimeout to be >= -1 would enforce that + if (millisecondsTimeout < -1) + { + throw new ArgumentOutOfRangeException("Invalid millisecondsTimeout value " + millisecondsTimeout); + } + + // To prevent s_tokenCache growing too large, we have to adjust the granularity of the our coalesce depending + // on the value of millisecondsTimeout. The coalescing span scales proportionally with millisecondsTimeout which + // would guarantee constant s_tokenCache size in the case where similar millisecondsTimeout values are accepted. + // If the method is given a wildly different millisecondsTimeout values all the time, the dictionary would still + // only grow logarithmically with respect to the range of the input values + + uint currentTime = (uint)Environment.TickCount; + long targetTime = millisecondsTimeout + currentTime; + + // Formula for our coalescing span: + // Divide millisecondsTimeout by SegmentationFactor and take the highest bit and then multiply CoalescingFactor back + var segmentValue = millisecondsTimeout / SegmentationFactor; + var coalescingSpanMs = CoalescingFactor; + while (segmentValue > 0) + { + segmentValue >>= 1; + coalescingSpanMs <<= 1; + } + targetTime = ((targetTime + (coalescingSpanMs - 1)) / coalescingSpanMs) * coalescingSpanMs; + + Task tokenTask; + + if (!s_tokenCache.TryGetValue(targetTime, out tokenTask)) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // only a single thread may succeed adding its task into the cache + if (s_tokenCache.TryAdd(targetTime, tcs.Task)) + { + // Since this thread was successful reserving a spot in the cache, it would be the only thread + // that construct the CancellationTokenSource + var tokenSource = new CancellationTokenSource((int)(targetTime - currentTime)); + var token = tokenSource.Token; + + // Clean up cache when Token is canceled + token.Register(s_deregisterToken, Tuple.Create(targetTime, tokenSource)); + + // set the result so other thread may observe the token, and return + tcs.TrySetResult(token); + tokenTask = tcs.Task; + } + else + { + // for threads that failed when calling TryAdd, there should be one already in the cache + if (!s_tokenCache.TryGetValue(targetTime, out tokenTask)) + { + // In unlikely scenario the token was already cancelled and timed out, we would not find it in cache. + // In this case we would simply create a non-coalesced token + tokenTask = Task.FromResult(new CancellationTokenSource(millisecondsTimeout).Token); + } + } + } + return tokenTask; + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/AzureSecurityTokenTypes.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/AzureSecurityTokenTypes.cs new file mode 100644 index 000000000000..28a265501802 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/AzureSecurityTokenTypes.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.WCF.Azure.Tokens +{ + // Consider making this public in the future similar to System.ServiceModel.Security.Tokens.ServiceModelSecurityTokenTypes + internal static class AzureSecurityTokenTypes + { + public const string Namespace = "http://schemas.microsoft.com/ws/2006/05/servicemodel/tokens/Azure"; + public const string DefaultTokenType = Namespace + "/Default"; + public const string SasTokenType = Namespace + "/Sas"; + public const string StorageSharedKeyTokenType = Namespace + "/StorageSharedKey"; + public const string TokenTokenType = Namespace + "/Token"; + public const string ConnectionStringTokenType = Namespace + "/ConnectionString"; + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/ConnectionStringSecurityToken.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/ConnectionStringSecurityToken.cs new file mode 100644 index 000000000000..e83ada8325df --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/ConnectionStringSecurityToken.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using System.IdentityModel.Tokens; + +namespace Microsoft.WCF.Azure.Tokens +{ + internal class ConnectionStringSecurityToken : SecurityToken + { + public ConnectionStringSecurityToken(string connectionString) : this(connectionString, SecurityTokenUtils.CreateUniqueId()) { } + + public ConnectionStringSecurityToken(string connectionString, string id) + { + ConnectionString = connectionString; + Id = id; + ValidFrom = DateTime.UtcNow; + } + + public string ConnectionString { get; } + public override string Id { get; } + public override ReadOnlyCollection SecurityKeys => SecurityTokenUtils.EmptyReadOnlyCollection.Instance; + public override DateTime ValidFrom { get; } + public override DateTime ValidTo => SecurityTokenUtils.MaxUtcDateTime; + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/SasSecurityToken.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/SasSecurityToken.cs new file mode 100644 index 000000000000..6774214e62fb --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/SasSecurityToken.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure; +using Azure.Core; +using System; +using System.Collections.ObjectModel; +using System.IdentityModel.Tokens; + +namespace Microsoft.WCF.Azure.Tokens +{ + internal class SasSecurityToken : SecurityToken + { + public SasSecurityToken(AzureSasCredential sasCredential) : this(sasCredential, SecurityTokenUtils.CreateUniqueId()) { } + + public SasSecurityToken(AzureSasCredential sasCredential, string id) + { + SasCredential = sasCredential; + Id = id; + ValidFrom = DateTime.UtcNow; + } + + public AzureSasCredential SasCredential { get; } + public override string Id { get; } + public override ReadOnlyCollection SecurityKeys => SecurityTokenUtils.EmptyReadOnlyCollection.Instance; + public override DateTime ValidFrom { get; } + public override DateTime ValidTo => SecurityTokenUtils.MaxUtcDateTime; + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/SecurityTokenProviderContainer.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/SecurityTokenProviderContainer.cs new file mode 100644 index 000000000000..e7df8da32b34 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/SecurityTokenProviderContainer.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.WCF.Azure.StorageQueues.Channels; +using System; +using System.IdentityModel.Selectors; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Microsoft.WCF.Azure.Tokens +{ + internal class SecurityTokenProviderContainer + { + public SecurityTokenProviderContainer(SecurityTokenProvider tokenProvider) + { + ArgumentNullException.ThrowIfNull(tokenProvider); + TokenProvider = tokenProvider; + } + + public SecurityTokenProvider TokenProvider { get; } + + public void Open(TimeSpan timeout) + { + SecurityUtils.OpenTokenProviderIfRequired(TokenProvider, timeout); + } + + public void Close(TimeSpan timeout) + { + SecurityUtils.CloseTokenProviderIfRequired(TokenProvider, timeout); + } + + public Task OpenAsync(TimeSpan timeout) + { + return SecurityUtils.OpenTokenProviderIfRequiredAsync(TokenProvider, timeout); + } + + public Task CloseAsync(TimeSpan timeout) + { + return SecurityUtils.CloseTokenProviderIfRequiredAsync(TokenProvider, timeout); + } + + public void Abort() + { + SecurityUtils.AbortTokenProviderIfRequired(TokenProvider); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/SecurityTokenUtils.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/SecurityTokenUtils.cs new file mode 100644 index 000000000000..a4ce90230f06 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/SecurityTokenUtils.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading; + +namespace Microsoft.WCF.Azure.Tokens +{ + internal static class SecurityTokenUtils + { + private static long s_nextId = 0; + private static string s_commonPrefix = "uuid-" + Guid.NewGuid().ToString() + "-"; + + internal static string CreateUniqueId() => s_commonPrefix + Interlocked.Increment(ref s_nextId); + + public static DateTime MaxUtcDateTime + { + get + { + // + and - TimeSpan.TicksPerDay is to compensate the DateTime.ParseExact (to localtime) overflow. + return new DateTime(DateTime.MaxValue.Ticks - TimeSpan.TicksPerDay, DateTimeKind.Utc); + } + } + + internal static class EmptyReadOnlyCollection + { + public static ReadOnlyCollection Instance = new ReadOnlyCollection(Array.Empty()); + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/StorageSharedKeySecurityToken.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/StorageSharedKeySecurityToken.cs new file mode 100644 index 000000000000..057280b42d7f --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/StorageSharedKeySecurityToken.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage; +using System; +using System.Collections.ObjectModel; +using System.IdentityModel.Tokens; + +namespace Microsoft.WCF.Azure.Tokens +{ + internal class StorageSharedKeySecurityToken : SecurityToken + { + public StorageSharedKeySecurityToken(StorageSharedKeyCredential storageSharedKeyCredential) : this(storageSharedKeyCredential, SecurityTokenUtils.CreateUniqueId()) { } + + public StorageSharedKeySecurityToken(StorageSharedKeyCredential storageSharedKeyCredential, string id) + { + StorageSharedKeyCredential = storageSharedKeyCredential; + Id = id; + ValidFrom = DateTime.UtcNow; + } + + public StorageSharedKeyCredential StorageSharedKeyCredential { get; } + public override string Id { get; } + public override ReadOnlyCollection SecurityKeys => SecurityTokenUtils.EmptyReadOnlyCollection.Instance; + public override DateTime ValidFrom { get; } + public override DateTime ValidTo => SecurityTokenUtils.MaxUtcDateTime; + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/TokenCredentialSecurityToken.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/TokenCredentialSecurityToken.cs new file mode 100644 index 000000000000..b9e5b65b86d2 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Microsoft/WCF/Azure/Tokens/TokenCredentialSecurityToken.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core; +using System; +using System.Collections.ObjectModel; +using System.IdentityModel.Tokens; + +namespace Microsoft.WCF.Azure.Tokens +{ + internal class TokenCredentialSecurityToken : SecurityToken + { + public TokenCredentialSecurityToken(TokenCredential tokenCredential) : this(tokenCredential, SecurityTokenUtils.CreateUniqueId()) { } + + public TokenCredentialSecurityToken(TokenCredential tokenCredential, string id) + { + TokenCredential = tokenCredential; + Id = id; + ValidFrom = DateTime.UtcNow; + } + + public TokenCredential TokenCredential { get; } + public override string Id { get; } + public override ReadOnlyCollection SecurityKeys => SecurityTokenUtils.EmptyReadOnlyCollection.Instance; + public override DateTime ValidFrom { get; } + public override DateTime ValidTo => SecurityTokenUtils.MaxUtcDateTime; + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Resources/SR.Designer.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Resources/SR.Designer.cs new file mode 100644 index 000000000000..3baf80294707 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Resources/SR.Designer.cs @@ -0,0 +1,171 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.WCF.Azure.StorageQueues { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class SR { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal SR() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.WCF.Azure.StorageQueues.Resources.SR", typeof(SR).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The endpoint Uri '{0}' should only have a single path segment. The account name should be the first part of the hostname and not part of the path.. + /// + internal static string AccountNameShouldBePartOfHostName { + get { + return ResourceManager.GetString("AccountNameShouldBePartOfHostName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The endpoint Uri '{0}' should have two path segments when not using an Azure dns hostname.. + /// + internal static string AccountNameShouldBePartOfUriPath { + get { + return ResourceManager.GetString("AccountNameShouldBePartOfUriPath", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Communicate object not in Open state. Current state: {0}.. + /// + internal static string CommunicationObjectNotOpen { + get { + return ResourceManager.GetString("CommunicationObjectNotOpen", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Account name mismatch. The connection string is using account name '{0}', and the channel uri is using account name '{1}'. When specifying the account name using both methods, they must match.. + /// + internal static string ConnectionStringAndViaAccountNameMismatch { + get { + return ResourceManager.GetString("ConnectionStringAndViaAccountNameMismatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Queue name mismatch. The connection string is using queue name '{0}', and the channel uri is using queue name '{1}'. When specifying the queue name using both methods, they must match.. + /// + internal static string ConnectionStringAndViaQueueNameMismatch { + get { + return ResourceManager.GetString("ConnectionStringAndViaQueueNameMismatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The connection string is not provided. Specify a connection string in AzureClientCredentials.. + /// + internal static string ConnectionStringNotProvidedOnAzureClientCredentials { + get { + return ResourceManager.GetString("ConnectionStringNotProvidedOnAzureClientCredentials", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token provider of type '{0}' did not return a token of type '{1}'. Check the credential configuration.. + /// + internal static string InvalidTokenProvided { + get { + return ResourceManager.GetString("InvalidTokenProvided", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ue name must be provided either in the connection string or as part of the channel endpoint uri.. + /// + internal static string MissingQueueName { + get { + return ResourceManager.GetString("MissingQueueName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Sas credential is not provided. Specify a Sas credential in AzureClientCredentials.. + /// + internal static string SasCredentialNotProvidedOnAzureClientCredentials { + get { + return ResourceManager.GetString("SasCredentialNotProvidedOnAzureClientCredentials", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An error ({0}) occurred while transmitting message.. + /// + internal static string SendError { + get { + return ResourceManager.GetString("SendError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The StorageSharedKey credential is not provided. Specify a StorageSharedKey credential in AzureClientCredentials.. + /// + internal static string StorageSharedKeyCredentialNotProvidedOnAzureClientCredentials { + get { + return ResourceManager.GetString("StorageSharedKeyCredentialNotProvidedOnAzureClientCredentials", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token credential is not provided. Specify a token credential in AzureClientCredentials.. + /// + internal static string TokenCredentialNotProvidedOnAzureClientCredentials { + get { + return ResourceManager.GetString("TokenCredentialNotProvidedOnAzureClientCredentials", resourceCulture); + } + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Resources/SR.resx b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Resources/SR.resx new file mode 100644 index 000000000000..e759b6c5dc3d --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/src/Resources/SR.resx @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The endpoint Uri '{0}' should only have a single path segment. The account name should be the first part of the hostname and not part of the path. + + + The endpoint Uri '{0}' should have two path segments when not using an Azure dns hostname. + + + Account name mismatch. The connection string is using account name '{0}', and the channel uri is using account name '{1}'. When specifying the account name using both methods, they must match. + + + Queue name mismatch. The connection string is using queue name '{0}', and the channel uri is using queue name '{1}'. When specifying the queue name using both methods, they must match. + + + The connection string is not provided. Specify a connection string in AzureClientCredentials. + + + The token provider of type '{0}' did not return a token of type '{1}'. Check the credential configuration. + + + ue name must be provided either in the connection string or as part of the channel endpoint uri. + + + The Sas credential is not provided. Specify a Sas credential in AzureClientCredentials. + + + The StorageSharedKey credential is not provided. Specify a StorageSharedKey credential in AzureClientCredentials. + + + The token credential is not provided. Specify a token credential in AzureClientCredentials. + + + An error ({0}) occurred while transmitting message. + + + Communicate object not in Open state. Current state: {0}. + + \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/AuthenticationTests.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/AuthenticationTests.cs new file mode 100644 index 000000000000..1370723408fe --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/AuthenticationTests.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure; +using Azure.Storage; +using Azure.Storage.Queues; +using Azure.Storage.Queues.Models; +using Azure.Storage.Sas; +using Contracts; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using NUnit.Framework; +using System; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.ServiceModel.Security; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.WCF.Azure.StorageQueues.Tests +{ + public class AuthenticationTests + { + // [TestCase(AzureClientCredentialType.Sas)] // Bugs with Azurite prevents testing with Sas + [TestCase(AzureClientCredentialType.StorageSharedKey)] + [TestCase(AzureClientCredentialType.ConnectionString)] + [TestCase(AzureClientCredentialType.Token)] + public async Task ConnectionStringAuthentication_SuccessAsync(AzureClientCredentialType clientCredentialType) + { + var queueName = "azure-queue"; + var azuriteFixture = AzuriteNUnitFixture.Instance; + var transport = azuriteFixture.GetTransport(); + var connectionString = azuriteFixture.GetAzureAccount().ConnectionString; + var queueClient = CreateQueueClient(clientCredentialType, queueName); + queueClient.CreateIfNotExists(); + + ChannelFactory channelFactory = CreateChannelFactory(clientCredentialType, queueName); + string endpointUrlString = channelFactory.Endpoint.Address.Uri.ToString(); + channelFactory.Open(); + ITestContract channel = channelFactory.CreateChannel(); + (channel as IChannel).Open(); + channel.Create("TestService"); + string inputMessage = $"http://tempuri.org/ITestContract/Create{endpointUrlString}TestService"; + using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromSeconds(5)); + QueueMessage message = await queueClient.ReceiveMessageAsync(null, cancellationTokenSource.Token); + Assert.AreEqual(inputMessage, message.MessageText.ToString()); + } + + private static QueueClient CreateQueueClient(AzureClientCredentialType clientCredentialType, string queueName) + { + var transport = AzuriteNUnitFixture.Instance.GetTransport(); + switch (clientCredentialType) + { + case AzureClientCredentialType.ConnectionString: + var connectionString = AzuriteNUnitFixture.Instance.GetAzureAccount().ConnectionString; + return new QueueClient(connectionString, queueName, new QueueClientOptions { Transport = transport }); + case AzureClientCredentialType.StorageSharedKey: + var accountName = AzuriteNUnitFixture.Instance.GetAzureAccount().Name; + var accountKey = AzuriteNUnitFixture.Instance.GetAzureAccount().Key; + return new QueueClient(GetEndpointUri(queueName), new StorageSharedKeyCredential(accountName, accountKey), new QueueClientOptions { Transport = transport }); + case AzureClientCredentialType.Token: + var tokenCredential = AzuriteNUnitFixture.Instance.GetCredential(); + return new QueueClient(GetEndpointUri(queueName), tokenCredential, new QueueClientOptions { Transport = transport }); + default: + return null; + } + } + + private static Uri GetEndpointUri(string queueName) + { + var azuriteAccount = AzuriteNUnitFixture.Instance.GetAzureAccount(); + var uriBuilder = new UriBuilder(azuriteAccount.QueueEndpoint); + //uriBuilder.Host = "localhost"; + uriBuilder.Path = uriBuilder.Path + "/" + queueName; + return uriBuilder.Uri; + } + + private static ChannelFactory CreateChannelFactory(AzureClientCredentialType clientCredentialType, string queueName) + { + var azuriteAccount = AzuriteNUnitFixture.Instance.GetAzureAccount(); + var endpointUriBuilder = new UriBuilder(GetEndpointUri(queueName)) + { + Scheme = "net.aqs" + }; + var endpointUrlString = endpointUriBuilder.Uri.AbsoluteUri; + AzureQueueStorageBinding azureQueueStorageBinding = new() + { + Security = new() + { + Transport = new() + { + ClientCredentialType = clientCredentialType + } + }, + MessageEncoding = AzureQueueStorageMessageEncoding.Text + }; + var channelFactory = new ChannelFactory(azureQueueStorageBinding, new EndpointAddress(endpointUrlString)); + channelFactory.UseAzureCredentials(creds => + { + switch (clientCredentialType) + { + case AzureClientCredentialType.ConnectionString: + creds.ConnectionString = azuriteAccount.ConnectionString; + break; + case AzureClientCredentialType.StorageSharedKey: + var accountName = azuriteAccount.Name; + var accountKey = azuriteAccount.Key; + creds.StorageSharedKey = new StorageSharedKeyCredential(accountName, accountKey); + break; + case AzureClientCredentialType.Token: + creds.Token = AzuriteNUnitFixture.Instance.GetCredential(); + break; + } + + creds.ServiceCertificate.SslCertificateAuthentication = new X509ServiceCertificateAuthentication + { + CertificateValidationMode = X509CertificateValidationMode.None + }; + }); + + return channelFactory; + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/Helpers/DependencyResolverHelper.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/Helpers/DependencyResolverHelper.cs new file mode 100644 index 000000000000..e751fd40cb88 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/Helpers/DependencyResolverHelper.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Tests.Helpers +{ + internal class DependencyResolverHelper + { + private readonly IWebHost _webHost; + + public DependencyResolverHelper(IWebHost webHost) + { + _webHost = webHost; + } + + public T GetService() + { + using IServiceScope serviceScope = _webHost.Services.CreateScope(); + IServiceProvider services = serviceScope.ServiceProvider; + T scopedService = services.GetRequiredService(); + return scopedService; + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/ITestContract.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/ITestContract.cs new file mode 100644 index 000000000000..0ee71a42288c --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/ITestContract.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ServiceModel; +using System.Threading; + +namespace Contracts +{ + [ServiceContract] + public interface ITestContract + { + [OperationContract(IsOneWay = true)] + void Create(string name); + } + + public class TestService : ITestContract + { + public TestService() + { + ManualResetEvent = new ManualResetEventSlim(false); + } + + public void Create(string name) + { + if (string.IsNullOrEmpty(name)) + throw new FaultException(); + + ManualResetEvent.Set(); + } + + public ManualResetEventSlim ManualResetEvent { get; } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/IntegrationTests.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/IntegrationTests.cs new file mode 100644 index 000000000000..800be28d852d --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/IntegrationTests.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using Contracts; +using NUnit.Framework; +using System; +using System.Threading.Tasks; +using System.ServiceModel; +using System.Threading; +using System.ServiceModel.Security; +using Azure.Storage.Queues.Models; +using Azure.Storage.Test.Shared; + +namespace Microsoft.WCF.Azure.StorageQueues.Tests +{ + public class IntegrationTests + { + [Test] + public async Task DefaultQueueConfiguration_SendReceiveTextMessage_Success() + { + var queueName = "azure-queue"; + var azuriteFixture = AzuriteNUnitFixture.Instance; + QueueClient queueClient = CreateQueueClient(azuriteFixture, queueName, QueueMessageEncoding.None); + + string endpointUrlString = CreateTestService(azuriteFixture, queueName, AzureQueueStorageMessageEncoding.Text); + + string inputMessage = $"http://tempuri.org/ITestContract/Create{endpointUrlString}TestService"; + using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromSeconds(5)); + QueueMessage message = await queueClient.ReceiveMessageAsync(null, cancellationTokenSource.Token); + Assert.AreEqual(inputMessage, message.MessageText.ToString()); + } + + [Test] + public async Task DefaultQueueConfiguration_SendReceiveBinaryMessage_Success() + { + var queueName = "azure-queue"; + var azuriteFixture = AzuriteNUnitFixture.Instance; + QueueClient queueClient = CreateQueueClient(azuriteFixture, queueName, QueueMessageEncoding.Base64); + + CreateTestService(azuriteFixture, queueName, AzureQueueStorageMessageEncoding.Binary); + + using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromSeconds(5)); + QueueMessage message = await queueClient.ReceiveMessageAsync(null, cancellationTokenSource.Token); + Assert.IsNotNull(message.MessageText); + } + + [Test] + public async Task DefaultQueueConfiguration_SendBinaryReceiveTextMessage_Success() + { + var queueName = "azure-queue"; + var azuriteFixture = AzuriteNUnitFixture.Instance; + QueueClient queueClient = CreateQueueClient(azuriteFixture, queueName, QueueMessageEncoding.None); + + CreateTestService(azuriteFixture, queueName, AzureQueueStorageMessageEncoding.Binary); + + using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromSeconds(5)); + QueueMessage message = await queueClient.ReceiveMessageAsync(null, cancellationTokenSource.Token); + Assert.IsNotNull(message.MessageText); + } + + [Test] + public void DefaultQueueConfiguration_SendTextReceiveBinaryMessage_Failure() + { + var queueName = "azure-queue"; + var azuriteFixture = AzuriteNUnitFixture.Instance; + QueueClient queueClient = CreateQueueClient(azuriteFixture, queueName, QueueMessageEncoding.Base64); + + CreateTestService(azuriteFixture, queueName, AzureQueueStorageMessageEncoding.Text); + + using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromSeconds(5)); + var exception = Assert.ThrowsAsync(async () => + { + QueueMessage message = await queueClient.ReceiveMessageAsync(null, cancellationTokenSource.Token); + }); + Assert.That(exception.Message, Is.EqualTo("The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.")); + } + + private static QueueClient CreateQueueClient( + AzuriteFixture azuriteFixture, + string queueName, + QueueMessageEncoding queueMessageEncoding) + { + var transport = azuriteFixture.GetTransport(); + var connectionString = azuriteFixture.GetAzureAccount().ConnectionString; + var queueClient = new QueueClient(connectionString, queueName, new QueueClientOptions { Transport = transport, MessageEncoding = queueMessageEncoding }); + queueClient.CreateIfNotExists(); + return queueClient; + } + + private static string CreateTestService( + AzuriteFixture azuriteFixture, + string queueName, + AzureQueueStorageMessageEncoding azureQueueStorageMessageEncoding) + { + var connectionString = azuriteFixture.GetAzureAccount().ConnectionString; + var endpointUriBuilder = new UriBuilder(azuriteFixture.GetAzureAccount().QueueEndpoint + "/" + queueName) + { + Scheme = "net.aqs" + }; + var endpointUrlString = endpointUriBuilder.Uri.AbsoluteUri; + AzureQueueStorageBinding azureQueueStorageBinding = new() + { + Security = new() + { + Transport = new() + { + ClientCredentialType = AzureClientCredentialType.ConnectionString + } + }, + MessageEncoding = azureQueueStorageMessageEncoding + }; + var channelFactory = new ChannelFactory(azureQueueStorageBinding, new EndpointAddress(endpointUrlString)); + channelFactory.UseAzureCredentials(creds => + { + creds.ConnectionString = connectionString; + creds.ServiceCertificate.SslCertificateAuthentication = new X509ServiceCertificateAuthentication + { + CertificateValidationMode = X509CertificateValidationMode.None + }; + }); + + var channel = channelFactory.CreateChannel(); + ((System.ServiceModel.Channels.IChannel)channel).Open(); + channel.Create("TestService"); + return endpointUrlString; + } + } +} diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/Microsoft.WCF.Azure.StorageQueues.Tests.csproj b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/Microsoft.WCF.Azure.StorageQueues.Tests.csproj new file mode 100644 index 000000000000..6111a9b6e788 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/Microsoft.WCF.Azure.StorageQueues.Tests.csproj @@ -0,0 +1,46 @@ + + + + $(RequiredTargetFrameworks) + true + false + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + PreserveNewest + + + + + + + \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/Properties/serviceDependencies.json b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/Properties/serviceDependencies.json new file mode 100644 index 000000000000..f3d42edac559 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/Properties/serviceDependencies.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "secrets1": { + "type": "secrets" + }, + "storage1": { + "type": "storage", + "connectionId": "StorageConnectionString" + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/Properties/serviceDependencies.local.json b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/Properties/serviceDependencies.local.json new file mode 100644 index 000000000000..bcd26d7ff8b5 --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/Properties/serviceDependencies.local.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "secrets1": { + "type": "secrets.user" + }, + "storage1": { + "secretStore": "LocalSecretsFile", + "type": "storage.emulator", + "connectionId": "StorageConnectionString" + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/QueueClientConfigurationTests.cs b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/QueueClientConfigurationTests.cs new file mode 100644 index 000000000000..1be170db1daf --- /dev/null +++ b/sdk/extensions/wcf/Microsoft.WCF.Azure.StorageQueues/tests/QueueClientConfigurationTests.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage.Queues; +using NUnit.Framework; +using System; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.Threading.Tasks; + +namespace Microsoft.WCF.Azure.StorageQueues.Tests +{ + public class QueueClientConfigurationTests + { + [Test] + public async Task QueueClient_ManualConfigurationDelegate_Called() + { + var queueName = Guid.NewGuid().ToString("D").ToLowerInvariant(); + IClientChannel channel = CreateChannel(queueName); + var channelParameters = channel.GetProperty(); + channelParameters.Add(new Func(ConfigureQueueClient)); + bool configureQueueClientCalled = false; + Task assertionChecks = null; + channel.Open(); + Assert.IsTrue(configureQueueClientCalled); + Assert.IsNotNull(assertionChecks); + assertionChecks.RunSynchronously(); + await assertionChecks; + + QueueClient ConfigureQueueClient(QueueClient queueClient) + { + configureQueueClientCalled = true; + assertionChecks = new Task(() => + { + Assert.AreEqual(queueName, queueClient.Name); + Assert.AreEqual(AzuriteNUnitFixture.Instance.GetAzureAccount().Name, queueClient.AccountName); + }); + return queueClient; + } + } + + [Test] + public void QueueClient_MismatachedAccountName_Fails() + { + var queueName = Guid.NewGuid().ToString("D").ToLowerInvariant(); + var accountName = Guid.NewGuid().ToString("D").ToLowerInvariant().Substring(5); + IClientChannel channel = CreateChannel(queueName, accountName); + Assert.Throws(() => channel.Open()); + } + + private IClientChannel CreateChannel(string queueName, string accountName = null) + { + var azuriteFixture = AzuriteNUnitFixture.Instance; + var queueEndpoint = azuriteFixture.GetAzureAccount().QueueEndpoint; + if (!queueEndpoint.EndsWith('/')) + { + queueEndpoint += "/"; + } + + Uri baseUri = new Uri(queueEndpoint); + Uri endpointUri; + if (string.IsNullOrEmpty(accountName)) + { + var endpointUriBuilder = new UriBuilder(new Uri(baseUri, queueName)) + { + Scheme = "net.aqs" + }; + endpointUri = endpointUriBuilder.Uri; + } + else + { + var endpointUriBuilder = new UriBuilder(baseUri) + { + Scheme = "net.aqs", + Path = $"/{accountName}/{queueName}" + }; + endpointUri = endpointUriBuilder.Uri; + } + + var endpointUrlString = endpointUri.AbsoluteUri; + AzureQueueStorageBinding azureQueueStorageBinding = new(); + azureQueueStorageBinding.Security.Transport.ClientCredentialType = AzureClientCredentialType.ConnectionString; + + var channelFactory = new ChannelFactory(azureQueueStorageBinding, new EndpointAddress(endpointUrlString)); + channelFactory.UseAzureCredentials(creds => + { + creds.ConnectionString = azuriteFixture.GetAzureAccount().ConnectionString; + }); + + return channelFactory.CreateChannel() as IClientChannel; + } + } +} diff --git a/sdk/extensions/wcf/WCF.sln b/sdk/extensions/wcf/WCF.sln new file mode 100644 index 000000000000..daafc6de9c19 --- /dev/null +++ b/sdk/extensions/wcf/WCF.sln @@ -0,0 +1,79 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34221.43 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A8861001-CA29-4FCE-81FB-93A0B3725C40}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F44B9E86-431A-454B-AB2E-AC2154813FE3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CoreWCF.Azure.StorageQueues", "Microsoft.CoreWCF.Azure.StorageQueues\src\Microsoft.CoreWCF.Azure.StorageQueues.csproj", "{190C23E4-DA1C-424C-A672-78DB9D171671}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.WCF.Azure.StorageQueues", "Microsoft.WCF.Azure.StorageQueues\src\Microsoft.WCF.Azure.StorageQueues.csproj", "{CCB3DDE6-1A1E-4B7D-89FB-43371202CEEB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CoreWCF.Azure.StorageQueues.Tests", "Microsoft.CoreWCF.Azure.StorageQueues\tests\Microsoft.CoreWCF.Azure.StorageQueues.Tests.csproj", "{F77BE7C6-7D8B-4CBE-9A94-C0B32FA71D5A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.WCF.Azure.StorageQueues.Tests", "Microsoft.WCF.Azure.StorageQueues\tests\Microsoft.WCF.Azure.StorageQueues.Tests.csproj", "{C0A027EC-8F73-4C98-9817-D9A0CE318744}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{722B7AA2-F05E-45A3-9BAD-3279B18F549C}" + ProjectSection(SolutionItems) = preProject + Directory.Build.Props = Directory.Build.Props + Directory.Build.targets = Directory.Build.targets + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{D1AAA1FA-6052-45C6-87AC-314FE1E961A9}" + ProjectSection(SolutionItems) = preProject + samples\Directory.Build.props = samples\Directory.Build.props + samples\Directory.Build.targets = samples\Directory.Build.targets + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CoreWCF.Azure.StorageQueues.Samples", "samples\Microsoft.CoreWCF.Azure.StorageQueues.Samples\Microsoft.CoreWCF.Azure.StorageQueues.Samples.csproj", "{7DFAC4DF-1AEC-47E9-AC2E-191799D2667D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.WCF.Azure.StorageQueues.Samples", "samples\Microsoft.WCF.Azure.StorageQueues.Samples\Microsoft.WCF.Azure.StorageQueues.Samples.csproj", "{249D8A2B-BAEA-482A-8364-38A5E12FB6D1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {190C23E4-DA1C-424C-A672-78DB9D171671}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {190C23E4-DA1C-424C-A672-78DB9D171671}.Debug|Any CPU.Build.0 = Debug|Any CPU + {190C23E4-DA1C-424C-A672-78DB9D171671}.Release|Any CPU.ActiveCfg = Release|Any CPU + {190C23E4-DA1C-424C-A672-78DB9D171671}.Release|Any CPU.Build.0 = Release|Any CPU + {CCB3DDE6-1A1E-4B7D-89FB-43371202CEEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCB3DDE6-1A1E-4B7D-89FB-43371202CEEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCB3DDE6-1A1E-4B7D-89FB-43371202CEEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCB3DDE6-1A1E-4B7D-89FB-43371202CEEB}.Release|Any CPU.Build.0 = Release|Any CPU + {F77BE7C6-7D8B-4CBE-9A94-C0B32FA71D5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F77BE7C6-7D8B-4CBE-9A94-C0B32FA71D5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F77BE7C6-7D8B-4CBE-9A94-C0B32FA71D5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F77BE7C6-7D8B-4CBE-9A94-C0B32FA71D5A}.Release|Any CPU.Build.0 = Release|Any CPU + {C0A027EC-8F73-4C98-9817-D9A0CE318744}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0A027EC-8F73-4C98-9817-D9A0CE318744}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0A027EC-8F73-4C98-9817-D9A0CE318744}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0A027EC-8F73-4C98-9817-D9A0CE318744}.Release|Any CPU.Build.0 = Release|Any CPU + {7DFAC4DF-1AEC-47E9-AC2E-191799D2667D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DFAC4DF-1AEC-47E9-AC2E-191799D2667D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DFAC4DF-1AEC-47E9-AC2E-191799D2667D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DFAC4DF-1AEC-47E9-AC2E-191799D2667D}.Release|Any CPU.Build.0 = Release|Any CPU + {249D8A2B-BAEA-482A-8364-38A5E12FB6D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {249D8A2B-BAEA-482A-8364-38A5E12FB6D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {249D8A2B-BAEA-482A-8364-38A5E12FB6D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {249D8A2B-BAEA-482A-8364-38A5E12FB6D1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {190C23E4-DA1C-424C-A672-78DB9D171671} = {A8861001-CA29-4FCE-81FB-93A0B3725C40} + {CCB3DDE6-1A1E-4B7D-89FB-43371202CEEB} = {A8861001-CA29-4FCE-81FB-93A0B3725C40} + {F77BE7C6-7D8B-4CBE-9A94-C0B32FA71D5A} = {F44B9E86-431A-454B-AB2E-AC2154813FE3} + {C0A027EC-8F73-4C98-9817-D9A0CE318744} = {F44B9E86-431A-454B-AB2E-AC2154813FE3} + {7DFAC4DF-1AEC-47E9-AC2E-191799D2667D} = {D1AAA1FA-6052-45C6-87AC-314FE1E961A9} + {249D8A2B-BAEA-482A-8364-38A5E12FB6D1} = {D1AAA1FA-6052-45C6-87AC-314FE1E961A9} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {64495885-88F4-4409-830C-C28B36E5402C} + EndGlobalSection +EndGlobal diff --git a/sdk/extensions/wcf/samples/Directory.Build.props b/sdk/extensions/wcf/samples/Directory.Build.props new file mode 100644 index 000000000000..516ad66259a8 --- /dev/null +++ b/sdk/extensions/wcf/samples/Directory.Build.props @@ -0,0 +1,14 @@ + + + false + false + true + + + + + + false + false + + \ No newline at end of file diff --git a/sdk/extensions/wcf/samples/Directory.Build.targets b/sdk/extensions/wcf/samples/Directory.Build.targets new file mode 100644 index 000000000000..c6721c19d1b7 --- /dev/null +++ b/sdk/extensions/wcf/samples/Directory.Build.targets @@ -0,0 +1,12 @@ + + + + $(TargetFramework) + + + + + + + + \ No newline at end of file diff --git a/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/CompositeType.cs b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/CompositeType.cs new file mode 100644 index 000000000000..27c6496769e9 --- /dev/null +++ b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/CompositeType.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Samples +{ + [DataContract] + public class CompositeType + { + private bool boolValue = true; + private string stringValue = "Hello "; + + [DataMember] + public bool BoolValue + { + get { return boolValue; } + set { boolValue = value; } + } + + [DataMember] + public string StringValue + { + get { return stringValue; } + set { stringValue = value; } + } + } +} diff --git a/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/IService.cs b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/IService.cs new file mode 100644 index 000000000000..1accac732c2c --- /dev/null +++ b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/IService.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CoreWCF; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Samples +{ + [ServiceContract] + public interface IService + { + [OperationContract(IsOneWay = true)] + void SendData(int value); + + [OperationContract(IsOneWay = true)] + void SendDataUsingDataContract(CompositeType composite); + } +} diff --git a/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples.csproj b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples.csproj new file mode 100644 index 000000000000..c94302b469b6 --- /dev/null +++ b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples.csproj @@ -0,0 +1,30 @@ + + + net6.0 + $(RequiredTargetFrameworks) + false + true + false + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/Program.cs b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/Program.cs new file mode 100644 index 000000000000..ce7ccd539192 --- /dev/null +++ b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/Program.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Samples +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + #region Snippet:CoreWCF_Azure_Storage_Queus_Sample_Logging + .ConfigureLogging((logging) => + { + logging.AddFilter("Microsoft.CoreWCF", LogLevel.Debug); + }); + #endregion + } +} diff --git a/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/Service.cs b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/Service.cs new file mode 100644 index 000000000000..55972682ae68 --- /dev/null +++ b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/Service.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Samples +{ + public class Service : IService + { + public void SendData(int value) + { + } + + public void SendDataUsingDataContract(CompositeType composite) + { + if (composite == null) + { + throw new ArgumentNullException(nameof(composite)); + } + } + } +} diff --git a/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/StartupDefaultCredentials.cs b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/StartupDefaultCredentials.cs new file mode 100644 index 000000000000..f4dd172b98a0 --- /dev/null +++ b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/StartupDefaultCredentials.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CoreWCF.Configuration; +using CoreWCF.Queue.Common.Configuration; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Samples +{ + public class StartupDefaultCredentials + { + #region Snippet:Configure_CoreWCF_QueueTransport + public void ConfigureServices(IServiceCollection services) + { + services.AddServiceModelServices() + .AddQueueTransport(); + } + #endregion + + #region Snippet:CoreWCF_Azure_Storage_Queues_Sample_DefaultAzureCredential + public void Configure(IApplicationBuilder app) + { + app.UseServiceModel(serviceBuilder => + { + // Configure CoreWCF to dispatch to service type Service + serviceBuilder.AddService(); + + // Create a binding instance to use Azure Queue Storage, passing an optional queue name for the dead letter queue + // The default client credential type is Default, which uses DefaultAzureCredential + var aqsBinding = new AzureQueueStorageBinding("DEADLETTERQUEUENAME"); + + // Add a service endpoint using the AzureQueueStorageBinding + string queueEndpointString = "https://MYSTORAGEACCOUNT.queue.core.windows.net/QUEUENAME"; + serviceBuilder.AddServiceEndpoint(aqsBinding, queueEndpointString); + }); + } + #endregion + } +} diff --git a/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/StartupStorageSharedKey.cs b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/StartupStorageSharedKey.cs new file mode 100644 index 000000000000..634b334f314a --- /dev/null +++ b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/StartupStorageSharedKey.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage; +using CoreWCF.Configuration; +using CoreWCF.Queue.Common.Configuration; +using System.Web.Services.Description; + +namespace Microsoft.CoreWCF.Azure.StorageQueues.Samples +{ + public class StartupStorageSharedKey + { + public void ConfigureServices(IServiceCollection services) + { + services.AddServiceModelServices() + .AddQueueTransport(); + } + + #region Snippet:CoreWCF_Azure_Storage_Queus_Sample_StorageSharedKey + public void Configure(IApplicationBuilder app) + { + app.UseServiceModel(serviceBuilder => + { + // Configure CoreWCF to dispatch to service type Service + serviceBuilder.AddService(); + + // Create a binding instance to use Azure Queue Storage, passing an optional queue name for the dead letter queue + var aqsBinding = new AzureQueueStorageBinding("DEADLETTERQUEUENAME"); + + // Configure the client credential type to use StorageSharedKeyCredential + aqsBinding.Security.Transport.ClientCredentialType = AzureClientCredentialType.StorageSharedKey; + + // Add a service endpoint using the AzureQueueStorageBinding + string queueEndpointString = "https://MYSTORAGEACCOUNT.queue.core.windows.net/QUEUENAME"; + serviceBuilder.AddServiceEndpoint(aqsBinding, queueEndpointString); + + // Use extension method to configure CoreWCF to use AzureServiceCredentials and set the + // StorageSharedKeyCredential instance. + serviceBuilder.UseAzureCredentials(credentials => + { + credentials.StorageSharedKey = GetStorageSharedKey(); + }); + }); + } + + public StorageSharedKeyCredential GetStorageSharedKey() + { + // Fetch shared key using a secure mechanism such as Azure Key Vault + // and construct an instance of StorageSharedKeyCredential to return; + return default; + } + #endregion + } +} diff --git a/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/appsettings.Development.json b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/appsettings.Development.json new file mode 100644 index 000000000000..0c208ae9181e --- /dev/null +++ b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/appsettings.json b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/appsettings.json new file mode 100644 index 000000000000..10f68b8c8b4f --- /dev/null +++ b/sdk/extensions/wcf/samples/Microsoft.CoreWCF.Azure.StorageQueues.Samples/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/sdk/extensions/wcf/samples/Microsoft.WCF.Azure.StorageQueues.Samples/CompositeType.cs b/sdk/extensions/wcf/samples/Microsoft.WCF.Azure.StorageQueues.Samples/CompositeType.cs new file mode 100644 index 000000000000..7cbc58b259d0 --- /dev/null +++ b/sdk/extensions/wcf/samples/Microsoft.WCF.Azure.StorageQueues.Samples/CompositeType.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.WCF.Azure.StorageQueues.Samples +{ + [DataContract] + public class CompositeType + { + private bool boolValue = true; + private string stringValue = "Hello "; + + [DataMember] + public bool BoolValue + { + get { return boolValue; } + set { boolValue = value; } + } + + [DataMember] + public string StringValue + { + get { return stringValue; } + set { stringValue = value; } + } + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/samples/Microsoft.WCF.Azure.StorageQueues.Samples/IService.cs b/sdk/extensions/wcf/samples/Microsoft.WCF.Azure.StorageQueues.Samples/IService.cs new file mode 100644 index 000000000000..1feaacf00f9e --- /dev/null +++ b/sdk/extensions/wcf/samples/Microsoft.WCF.Azure.StorageQueues.Samples/IService.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.WCF.Azure.StorageQueues.Samples +{ + [ServiceContract] + public interface IService : IChannel + { + [OperationContract(IsOneWay = true)] + Task SendDataAsync(int value); + + [OperationContract(IsOneWay = true)] + Task SendDataUsingDataContractAsync(CompositeType composite); + } +} \ No newline at end of file diff --git a/sdk/extensions/wcf/samples/Microsoft.WCF.Azure.StorageQueues.Samples/Microsoft.WCF.Azure.StorageQueues.Samples.csproj b/sdk/extensions/wcf/samples/Microsoft.WCF.Azure.StorageQueues.Samples/Microsoft.WCF.Azure.StorageQueues.Samples.csproj new file mode 100644 index 000000000000..bb0358214602 --- /dev/null +++ b/sdk/extensions/wcf/samples/Microsoft.WCF.Azure.StorageQueues.Samples/Microsoft.WCF.Azure.StorageQueues.Samples.csproj @@ -0,0 +1,21 @@ + + + + Exe + net6.0 + enable + + + + + + + + + + + + + + + diff --git a/sdk/extensions/wcf/samples/Microsoft.WCF.Azure.StorageQueues.Samples/Program.cs b/sdk/extensions/wcf/samples/Microsoft.WCF.Azure.StorageQueues.Samples/Program.cs new file mode 100644 index 000000000000..6b429da5f85d --- /dev/null +++ b/sdk/extensions/wcf/samples/Microsoft.WCF.Azure.StorageQueues.Samples/Program.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Storage; +using System.ServiceModel; + +namespace Microsoft.WCF.Azure.StorageQueues.Samples +{ + internal class Program + { + private static async Task Main(string[] args) + { + await UseAqsBindingWithDefaultCredentials(); + } + + private static async Task UseAqsBindingWithDefaultCredentials() + { + #region Snippet:WCF_Azure_Storage_Queues_Sample_DefaultAzureCredential + // Create a binding instance to use Azure Queue Storage. + // The default client credential type is Default, which uses DefaultAzureCredential + var aqsBinding = new AzureQueueStorageBinding(); + + // Create a ChannelFactory to using the binding and endpoint address, open it, and create a channel + string queueEndpointString = "https://MYSTORAGEACCOUNT.queue.core.windows.net/QUEUENAME"; + var factory = new ChannelFactory(aqsBinding, new EndpointAddress(queueEndpointString)); + factory.Open(); + IService channel = factory.CreateChannel(); + + // IService dervies from IChannel so you can call channel.Open without casting + channel.Open(); + await channel.SendDataAsync(42); + #endregion + + channel.Close(); + await (factory as IAsyncDisposable).DisposeAsync(); + } + + private static async Task UseAqsBindingWithStorageSharedKey() + { + #region Snippet:WCF_Azure_Storage_Queus_Sample_StorageSharedKey + // Create a binding instance to use Azure Queue Storage. + var aqsBinding = new AzureQueueStorageBinding(); + + // Configure the client credential type to use StorageSharedKeyCredential + aqsBinding.Security.Transport.ClientCredentialType = AzureClientCredentialType.StorageSharedKey; + + // Create a ChannelFactory to using the binding and endpoint address + string queueEndpointString = "https://MYSTORAGEACCOUNT.queue.core.windows.net/QUEUENAME"; + var factory = new ChannelFactory(aqsBinding, new EndpointAddress(queueEndpointString)); + + // Use extension method to configure WCF to use AzureClientCredentials and set the + // StorageSharedKeyCredential instance. + factory.UseAzureCredentials(credentials => + { + credentials.StorageSharedKey = GetStorageSharedKey(); + }); + + // Local function to get the StorageSharedKey + StorageSharedKeyCredential GetStorageSharedKey() + { + // Fetch shared key using a secure mechanism such as Azure Key Vault + // and construct an instance of StorageSharedKeyCredential to return; + return default; + } + + // Open the factory and create a channel + factory.Open(); + IService channel = factory.CreateChannel(); + + // IService dervies from IChannel so you can call channel.Open without casting + channel.Open(); + await channel.SendDataAsync(42); + #endregion + + channel.Close(); + await (factory as IAsyncDisposable).DisposeAsync(); + } + } +}