diff --git a/Deployment/deploy.json b/Deployment/deploy.json new file mode 100644 index 0000000..a5b9b4b --- /dev/null +++ b/Deployment/deploy.json @@ -0,0 +1,92 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "namespaceName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Relay namespace" + } + }, + "relayName": { + "type": "string", + "defaultValue": "botrelay", + "metadata": { + "description": "Name of the relay connection" + } + }, + "relaySharedAccessPolicyName": { + "type": "string", + "defaultValue": "SendAndListenPolicy", + "metadata": { + "description": "Name of the relay's Shared Access Policy" + } + } + }, + "variables": { + "apiVersion": "2017-04-01", + "location": "[resourceGroup().location]", + "hcAuthorizationRuleResourceId": "[resourceId('Microsoft.Relay/namespaces/HybridConnections/authorizationRules', parameters('namespaceName'), parameters('relayName'), parameters ('relaySharedAccessPolicyName'))]" + }, + "resources": [ + { + "apiVersion": "[variables('apiVersion')]", + "name": "[parameters('namespaceName')]", + "type": "Microsoft.Relay/Namespaces", + "location": "[variables('location')]", + "kind": "Relay", + "resources": [ + { + "apiVersion": "[variables('apiVersion')]", + "name": "[parameters('relayName')]", + "type": "HybridConnections", + "dependsOn": [ + "[concat('Microsoft.Relay/namespaces/', parameters('namespaceName'))]" + ], + "properties": { + "requiresClientAuthorization": "false", + "userMetadata": "Meta Data supplied by user hybridConnections" + }, + "resources": [ + { + "apiVersion": "[variables('apiVersion')]", + "name": "[parameters('relaySharedAccessPolicyName')]", + "type": "authorizationRules", + "dependsOn": [ + "[parameters('relayName')]" + ], + "properties": { + "Rights": [ + "Send", + "Listen" + ] + } + } + ] + } + ] + } + ], + "outputs": { + "SharedAccessPolicyKey": { + "type": "string", + "value": "[listkeys(variables('hcAuthorizationRuleResourceId'), variables('apiVersion')).primaryKey]" + }, + "SharedAccessPolicyName": { + "type": "string", + "value": "[parameters ('relaySharedAccessPolicyName')]" + }, + "RelayName": { + "type": "string", + "value": "[parameters('relayName')]" + }, + "Namespace": { + "type": "string", + "value": "[concat(parameters('namespaceName'),'.servicebus.windows.net')]" + }, + "MessagingEndpoint": { + "type": "string", + "value": "[concat('https://', parameters('namespaceName'), '.servicebus.windows.net/', parameters('relayName'), '/api/messages')]" + } + } +} \ No newline at end of file diff --git a/Docs/CommandLine.md b/Docs/CommandLine.md new file mode 100644 index 0000000..286fb3d --- /dev/null +++ b/Docs/CommandLine.md @@ -0,0 +1,49 @@ +# Command Line Instructions + +## Overview + +The Azure Bot Relay command line works similar to ngrok. It is a command line tool that will connect to your Azure Service Bus and forward all the incoming bot messages to your local bot. + +## Setup + +### Deploy an Azure Relay service + +Set up the bot relay in the same way indicated in the [README.md](../README.md) + +#### Launch your bot locally + +1. Start your bot as usual on your local machine + +2. Once your bot is running, note the localhost endpoint it is attached to + +#### Building and run the relay tool + +1. Clone the solution to your machine, open and build the solution in Visual Studio. + +2. Run the command line tool with the settings you have captured up until now. The tool has five required options. Four of these are from the deployment outputs of the relaye. The fifth is the URI to your bot (do not include the "/api/messages" portion). + +````text + -n, --namespace The name of the relay's namespace, e.g. '[Your Namespace].servicebus.windows.net' + + -r, --relay The name of the relay + + -p, --policy The name of the relay's Shared Access Policy + + -k, --key The Shared Access Policy's key + + -b, --botUri The url to your local bot e.g. 'http://localhost:[PORT]' +```` + +You can specify the parameters either through the Visual Studio project's properties page, or you can run the tool from a command prompt. + +Here is an example command line + +````text + +ServiceBusRelayUtilNetCore.exe -n benwillidemorelay.servicebus.windows.net -r botrelay +-p SendAndListenPolicy -k XOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXOX -b http://localhost:3980 +```` + +### Test your bot + +1. Test your bot on a channel (Test in Web Chat, Skype, Teams, etc.). \ No newline at end of file diff --git a/Docs/architecture.png b/Docs/architecture.png new file mode 100644 index 0000000..a8fe9ac Binary files /dev/null and b/Docs/architecture.png differ diff --git a/README.md b/README.md index ef9c25e..b092df9 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,69 @@ -# Overview -A relay utility for bots based on Azure Service Bus. +# AzureServiceBusBotRelay + +## Overview + +A relay utility for bots based on [Azure Relay](https://docs.microsoft.com/en-us/azure/azure-relay/relay-what-is-it). This utility allows you to forward a message sent to a bot hosted on any channel to your local machine. -It is useful for debug scenarios or for more complex situations where the BotEmulator is not enough (i.e.: you use the WebChat control hosted on a site and you need to receive ChannelData in your requests). +It is useful for debug scenarios or for more complex situations where the BotEmulator is not enough (i.e.: you use the WebChat control hosted on a site and you need to receive ChannelData in your requests or you are testing Teams specific events). + +It uses the Azure Relay service along with a small local service to recieve messages from the bot service and forward them to your locally hosted bot. This service can be hosted with the command line tool or installed into your Bot Composer bot as an Adapter package from Nuget. + +![architecture diagram](Docs/architecture.png) + +### Acknowledgments + +Part of this code is based on the work that [Pedro Felix](https://github.com/pmhsfelix/WebApi.Explorations.ServiceBusRelayHost) +and [Gabo Gilbert](https://github.com/gabog/AzureServiceBusBotRelay) have previously done -## Acknowledgments -Part of this code is based on the work that [Pedro Felix](https://github.com/pmhsfelix) did in his project [here](https://github.com/pmhsfelix/WebApi.Explorations.ServiceBusRelayHost). +## Setup -# How to configure and run the utility -### Building with .Net Framework +The relay can be used with traditional code based bots or as a simple add in component to Bot Composer -1. Once the solution has been cloned to your machine, open the solution in Visual Studio. +To setup the Azure Bot Service to connect to your local bot you need to -2. In Solution Explorer, expand the **ServiceBusRelayUtil** folder. +1. Deploy an Azure Relay service +2. Configure your Azure Bot Service to send messages to the Azure Relay +3. Connect to the relay from Bot Composer -3. Open the **App.config** file and replace the following values with those from your service bus (not the hybrid connection). - - a. "RelayNamespace" is the name of your service bus created earlier. Enter the value in place of **[Your Namespace]**. - - b. "RelayName" is the name of the shared access policy created in steps 9 through 11 during the service bus set up process. Enter the value in place of **[Your Relay Name]**. - - c. "PolicyName" is the value to the shared access policy created in steps 9 through 11 during the service bus set up process. Enter the value in place of **[Your Shared Access Policy Name]**. - - d. "PolicyKey" is the WCF relay to be used. Remember, this relay is programmatically created and only exists on your machine. Create a new, unused name and enter the value in place of **[Your Policy's Key]**. - - e. "TargetServiceAddress" sets the port to be used for localhost. The address and port number should match the address and port used by your bot. Enter a value in place of the "TODO" string part. For example, "http://localhost:[PORT]". - -4. Before testing the relay, your Azure Web Bot's messaging endpoint must be updated to match the relay. - - a. Login to the Azure portal and open your Web App Bot. - - b. Select **Settings** under Bot management to open the settings blade. - - c. In the **Messaging endpoint** field, enter the service bus namespace and relay. The relay should match the relay name entered in the **App.config** file and should not exist in Azure. - - d. Append **"/api/messages"** to the end to create the full endpoint to be used. For example, “https://example-service-bus.servicebus.windows.net/wcf-example-relay/api/messages". - - e. Click **Save** when completed. - -5. In Visual Studio, press **F5** to run the project. - -6. Open and run your locally hosted bot. - -7. Test your bot on a channel (Test in Web Chat, Skype, Teams, etc.). User data is captured and logged as activity occurs. +* If you are using a code only bot and not using Bot Composer, once you set up the Azure Service Bus Relay, see the [command line instructions](Docs/Commandline.md) to connect it to your bot) - - When using the Bot Framework Emulator: The endpoint entered in Emulator must be the service bus endpoint saved in your Azure Web Bot **Settings** blade, under **Messaging Endpoint**. +### Deploy an Azure Relay service -8. Once testing is completed, you can compile the project into an executable. +You can use this button to deploy an Azure Relay service with the correct configuration. You will just need to supply it with a unique name for the Azure Service Bus namespace. - a. Right click the project folder in Visual Studio and select **Build**. +After it completes, select the Outputs tab and copy the 5 values. You will need those to configure the bot relay tool and your bot service. - b. The .exe will output to the **/bin/debug** folder, along with other necessary files, located in the project’s directory folder. All the files are necessary to run and should be included when moving the .exe to a new folder/location. - - The **app.config** is in the same folder and can be edited as credentials change without needing to recompile the project. +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fnegativeeddy%2FAzureServiceBusBotRelay%2Fcommandline%2FDeployment%2Fdeploy.json) -### Building with .Net Core +If you want to deploy the relay service manually you will need to -1. Once the solution has been cloned to your machine, open the solution in Visual Studio. +1. ensure the relay does not require authentication +2. add a shared access policy to the hybrid relay that has permission to send & listen -2. In Solution Explorer, expand the **ServiceBusRelayUtilNetCore** folder. +### Configure your Azure Web App Bot or Azure Bot Registration -3. Open the **appsettings.json** file and replace the following values with those from your service bus hybrid connection. - - a. "RelayNamespace" is the name of your service bus created earlier. Enter the value in place of **[Your Namespace]**. +Before testing the relay, your Azure Web App Bot's messaging endpoint must be updated to match the relay. - b. "RelayName" is the name of the hybrid connection created in step 12. Enter the value in place of **[Your Relay Name]**. +1. Login to the Azure portal and open your Web App Bot or Bot Registration. - c. "PolicyName" is the name of the shared access policy created in steps 9 through 11 during the service bus set up process. Enter the value in place of **[Your Shared Access Policy Name]**. +2. Select **Settings** under Bot management to open the settings blade. - d. "PolicyKey" is the value to the shared access policy created in steps 9 through 11 during the service bus set up process. Enter the value in place of **[Your Policy's Key]**. - - e. "TargetServiceAddress" sets the port to be used for localhost. The address and port number should match the address and port used by your bot. Enter a value in place of the **"http://localhost:[PORT]"**. For example, "http://localhost:3978". - -4. Before testing the relay, your Azure Web App Bot's messaging endpoint must be updated to match the relay. - - a. Login to the Azure portal and open your Web App Bot. - - b. Select **Settings** under Bot management to open the settings blade. - - c. In the **Messaging endpoint** field, enter the service bus namespace and relay. - - d. Append **"/api/messages"** to the end to create the full endpoint to be used. For example, “https://example-service-bus.servicebus.windows.net/hc1/api/messages". - - e. Click **Save** when completed. - -5. In Visual Studio, press **F5** to run the project. - -6. Open and run your locally hosted bot. - -7. Test your bot on a channel (Test in Web Chat, Skype, Teams, etc.). User data is captured and logged as activity occurs. +3. In the **Messaging endpoint** field, enter the service bus namespace and relay. This is the "messagingEndpoint" value from the output of the deployment step above. - - When using the Bot Framework Emulator: The endpoint entered in Emulator must be the service bus endpoint saved in your Azure Web Bot **Settings** blade, under **Messaging Endpoint**. + Ensure that the URI ends with "/api/messages" -8. Once testing is completed, you can compile the project into an executable. + For example, “https://example-service-bus.servicebus.windows.net/hc1/api/messages". - a. Right click the project folder in Visual Studio and select **Publish**. +4. Click **Save** when completed. (You might have to click save twice) - b. For **Pick a publish Target**, select **Folder**. +### Connect to the relay from Bot Composer - c. For **Folder or File Share**, choose an output location or keep the default. +1. Add the [NegativeEddy.Bots.AzureServiceBusRelay.Adapter]([https://www.nuget.org/packages/NegativeEddy.Bots.AzureServiceBusRelay.Adapter) package to your bot +2. Enable the bot in the External Connections section and fill in the options from the values captured when you deployed the Azure Service Bus +3. Restart your bot - d. Click **Create Profile** to create a publish profile. +### Test your bot - e. Click **Configure...** to change the build configuration and change the following: - - - **Configuration** to "Debug | Any CPU" - - **Deployment Mode** to "Self-contained" - - **Target Runtime** to "win-x64" - - f. Click **Save** and then **Publish** - - g. The .exe will output to the **/bin/debug** folder, along with other necessary files, located in the project’s directory folder. All the files are necessary to run and should be included when moving the .exe to a new folder/location. - - The **appsettings.json** is in the same folder and can be edited as credentials change without needing to recompile the project. +1. Test your bot on a channel (Test in Web Chat, Skype, Teams, etc.). diff --git a/Src/BotServiceBusRelay.sln b/Src/BotServiceBusRelay.sln new file mode 100644 index 0000000..5d4a0d6 --- /dev/null +++ b/Src/BotServiceBusRelay.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31515.178 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NegativeEddy.Bots.AzureServiceBusRelay.Adapter", "NegativeEddy.Bots.AzureServiceBusRelay.Adapter\NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj", "{94BAD246-4EDE-45FA-B5F3-14A6D01BFE16}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NegativeEddy.Bots.AzureServiceBusRelay.CommandLine", "NegativeEddy.Bots.AzureServiceBusRelay.CommandLine\NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj", "{4E7F7A58-BB94-4167-B7FD-DF747DB5412E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {94BAD246-4EDE-45FA-B5F3-14A6D01BFE16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94BAD246-4EDE-45FA-B5F3-14A6D01BFE16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94BAD246-4EDE-45FA-B5F3-14A6D01BFE16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94BAD246-4EDE-45FA-B5F3-14A6D01BFE16}.Release|Any CPU.Build.0 = Release|Any CPU + {4E7F7A58-BB94-4167-B7FD-DF747DB5412E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E7F7A58-BB94-4167-B7FD-DF747DB5412E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E7F7A58-BB94-4167-B7FD-DF747DB5412E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E7F7A58-BB94-4167-B7FD-DF747DB5412E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2C96A03F-F825-402A-B109-0F1FC60BA3CE} + EndGlobalSection +EndGlobal diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/AzureServiceBusRelayAdapter.cs b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/AzureServiceBusRelayAdapter.cs new file mode 100644 index 0000000..c38a0c0 --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/AzureServiceBusRelayAdapter.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Bot.Builder; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace NegativeEddy.Bots.AzureServiceBusRelay.Adapter +{ + public class AzureServiceBusRelayAdapter : BotAdapter + { + public AzureServiceBusRelayAdapter(ILogger logger) + { + logger.LogInformation("Created AzureServiceBusRelayAdapter"); + } + + public override Task DeleteActivityAsync(ITurnContext turnContext, ConversationReference reference, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task SendActivitiesAsync(ITurnContext turnContext, Activity[] activities, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task UpdateActivityAsync(ITurnContext turnContext, Activity activity, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/AzureServiceBusRelayAdapterBotComponent.cs b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/AzureServiceBusRelayAdapterBotComponent.cs new file mode 100644 index 0000000..eab819a --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/AzureServiceBusRelayAdapterBotComponent.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Bot.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NegativeEddy.Bots.AzureServiceBusRelay.Service; + +namespace NegativeEddy.Bots.AzureServiceBusRelay.Adapter +{ + public class AzureServiceBusRelayAdapterBotComponent : BotComponent + { + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + if (configuration != null) + { + services.AddSingleton(new RelayOptions + { + PolicyKey = configuration["SASKey"], + PolicyName = configuration["SASPolicy"], + RelayNamespace = configuration["Namespace"], + RelayName = configuration["Relay"], + }); + services.AddHostedService(); + } + } + } +} diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/DispatcherService.cs b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/DispatcherService.cs new file mode 100644 index 0000000..3a9e578 --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/DispatcherService.cs @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.Azure.Relay; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace NegativeEddy.Bots.AzureServiceBusRelay.Service +{ + public class DispatcherService : IHostedService + { + private HttpClient _httpClient; + private string _hybridConnectionSubPath; + private HybridConnectionListener _listener; + private Uri _targetServiceAddress; + private readonly RelayOptions _options; + private readonly ILogger _logger; + private readonly IServer _server; + + public DispatcherService(IServer server, RelayOptions options, ILogger logger) + { + _options = options; + _logger = logger; + _server = server; + } + + private async void ListenerRequestHandler(RelayedHttpListenerContext context) + { + // generate the httpClient as late as possible because this hosted service may + // be started before the server's URI & port have been determined by the web host + if (_httpClient == null) + { + var addressFeature = _server.Features.Get(); + foreach (var address in addressFeature.Addresses) + { + try + { + // check if this is the http URI + Uri uri = new Uri(address); + if (uri.Scheme == "http") + { + if (uri.Host == "0.0.0.0") + { + uri = new Uri($"http://localhost:{uri.Port}"); + } + + _logger.LogInformation("Forwarding to bot at " + address); + _options.TargetServiceAddress = address; + _targetServiceAddress = uri; + _httpClient = new HttpClient + { + BaseAddress = _targetServiceAddress + }; + _httpClient.DefaultRequestHeaders.ExpectContinue = false; + + break; + } + } + catch + { + // not a valid URI, skip it + } + } + } + + var startTimeUtc = DateTime.UtcNow; + HttpStatusCode responseStatus = 0; + try + { + + _logger.LogInformation("Received message"); + var requestMessage = await CreateHttpRequestMessage(context); + _logger.LogInformation($"{requestMessage.Method} to {_targetServiceAddress}"); + var responseMessage = await _httpClient.SendAsync(requestMessage); + responseStatus = responseMessage.StatusCode; + await SendResponseAsync(context, responseMessage); + await context.Response.CloseAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + SendErrorResponse(ex, context); + } + finally + { + var stopTimeUtc = DateTime.UtcNow; + double milliseconds = stopTimeUtc.Subtract(startTimeUtc).TotalMilliseconds; + _logger.LogInformation("Response {0} took {1:N0} ms", responseStatus, milliseconds); + } + } + + private async Task SendResponseAsync(RelayedHttpListenerContext context, HttpResponseMessage responseMessage) + { + context.Response.StatusCode = responseMessage.StatusCode; + context.Response.StatusDescription = responseMessage.ReasonPhrase; + foreach (var header in responseMessage.Headers) + { + if (string.Equals(header.Key, "Transfer-Encoding")) + { + continue; + } + + context.Response.Headers.Add(header.Key, string.Join(",", header.Value)); + } + + var responseStream = await responseMessage.Content.ReadAsStreamAsync(); + await responseStream.CopyToAsync(context.Response.OutputStream); + } + + private void SendErrorResponse(Exception ex, RelayedHttpListenerContext context) + { + context.Response.StatusCode = HttpStatusCode.InternalServerError; + context.Response.StatusDescription = $"Internal Server Error: {ex.GetType().FullName}: {ex.Message}"; + context.Response.Close(); + } + + private async Task CreateHttpRequestMessage(RelayedHttpListenerContext context) + { + var requestMessage = new HttpRequestMessage(); + if (context.Request.HasEntityBody) + { + requestMessage.Content = new StreamContent(context.Request.InputStream); + var contentType = context.Request.Headers[HttpRequestHeader.ContentType]; + if (!string.IsNullOrEmpty(contentType)) + { + requestMessage.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + } + + var relativePath = context.Request.Url.GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped); + relativePath = relativePath.Replace(_hybridConnectionSubPath, string.Empty, StringComparison.OrdinalIgnoreCase); + requestMessage.RequestUri = new Uri(relativePath, UriKind.RelativeOrAbsolute); + requestMessage.Method = new HttpMethod(context.Request.HttpMethod); + + foreach (var headerName in context.Request.Headers.AllKeys) + { + if (string.Equals(headerName, "Host", StringComparison.OrdinalIgnoreCase) || + string.Equals(headerName, "Content-Type", StringComparison.OrdinalIgnoreCase)) + { + // Don't flow these headers here + continue; + } + + requestMessage.Headers.Add(headerName, context.Request.Headers[headerName]); + } + + await LogRequestActivity(requestMessage); + + return requestMessage; + } + + private async Task LogRequestActivity(HttpRequestMessage requestMessage) + { + if (requestMessage.Content is null) + { + _logger.LogInformation(""); + return; + } + string content = await requestMessage.Content.ReadAsStringAsync(); + + var formatted = content; + + try + { + // attempt to parse and pretty print as json + var doc = JsonDocument.Parse(content); + formatted = PrettyPrint(doc.RootElement, true); + } + catch { } + + _logger.LogDebug(formatted); + } + + public static string PrettyPrint(JsonElement element, bool indent) + => element.ValueKind == JsonValueKind.Undefined ? "" : JsonSerializer.Serialize(element, new JsonSerializerOptions { WriteIndented = indent }); + + public async Task StartAsync(CancellationToken cancellationToken) + { + try + { + var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(_options.PolicyName, _options.PolicyKey); + _listener = new HybridConnectionListener(new Uri($"sb://{_options.RelayNamespace}/{_options.RelayName}"), tokenProvider); + + _hybridConnectionSubPath = EnsureEndsWith(_listener.Address.AbsolutePath, "/"); + + _listener.RequestHandler = ListenerRequestHandler; + await _listener.OpenAsync(cancellationToken); + + _logger.LogInformation($"Listening to Azure Service Bus on {_listener.Address}"); + + string EnsureEndsWith(string s, string endValue) + { + if (!string.IsNullOrEmpty(s) && s.EndsWith(endValue, StringComparison.Ordinal)) + { + return s; + } + + return s + endValue; + } + } + catch(Exception ex) + { + _logger.LogError(ex, "Failed to initialize Azure Service Bus Relay"); + throw; + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _httpClient?.Dispose(); + await _listener?.CloseAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj new file mode 100644 index 0000000..9624765 --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp3.1 + NegativeEddy.Bots.AzureServiceBusRelay.Adapter + Library for connecting bots with Azure Service Bus Relay + Library for connecting bots with Azure Service Bus Relay + content + msbot-component;msbot-adapter;composer;botframework;botbuilder + 1.0.0-preview1 + true + NegativeEddy.Bots.AzureServiceBusRelay.Adapter + NegativeEddy.Bots.AzureServiceBusRelay.Adapter + https://github.com/negativeeddy/AzureServiceBusBotRelay + https://github.com/negativeeddy/AzureServiceBusBotRelay + Ben Williams + + + + + + + + + + + diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/RelayOptions.cs b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/RelayOptions.cs new file mode 100644 index 0000000..34ca69d --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/RelayOptions.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace NegativeEddy.Bots.AzureServiceBusRelay.Service +{ + public class RelayOptions + { + public string RelayNamespace { get; set; } + public string RelayName { get; set; } + public string PolicyName { get; set; } + public string PolicyKey { get; set; } + public string TargetServiceAddress { get; set; } + } +} \ No newline at end of file diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/Schemas/NegativeEddy.AzureServiceBusRelayAdapter.schema b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/Schemas/NegativeEddy.AzureServiceBusRelayAdapter.schema new file mode 100644 index 0000000..6ddced2 --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/Schemas/NegativeEddy.AzureServiceBusRelayAdapter.schema @@ -0,0 +1,42 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema", + "$role": "implements(Microsoft.IAdapter)", + "title": "Azure Service Bus Relay connection", + "description": "Connects a bot to an Azure Service Bus Relay.", + "type": "object", + "properties": { + "Namespace": { + "type": "string", + "title": "Namespace", + "description": "The name of the relay's namespace, e.g. '[Your Namespace].servicebus.windows.net'" + }, + "Relay": { + "type": "string", + "title": "Relay name", + "description": "The name of the relay" + }, + "SASPolicy": { + "type": "string", + "title": "SAS Policy name", + "description": "The name of the relay's Shared Access Policy" + }, + "SASKey": { + "type": "string", + "title": "SAS Policy Key", + "description": "The Shared Access Policy's key", + "default": "" + }, + "type": { + "type": "string", + "title": "type", + "description": "Adapter full type name.", + "default": "NegativeEddy.Bots.AzureServiceBusRelay.Adapter.AzureServiceBusRelayAdapter" + } + }, + "required": [ + "Namespace", + "Relay", + "SASPolicy", + "SASKey" + ] +} diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/Schemas/NegativeEddy.AzureServiceBusRelayAdapter.uischema b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/Schemas/NegativeEddy.AzureServiceBusRelayAdapter.uischema new file mode 100644 index 0000000..b37e9ca --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/Schemas/NegativeEddy.AzureServiceBusRelayAdapter.uischema @@ -0,0 +1,18 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/ui/v1.0/ui.schema", + "form": { + "label": "Azure Service Bus Relay connection", + "description": "Connects a bot to an Azure Service Bus Relay", + "helpLink": "https://github.com/negativeeddy/AzureServiceBusBotRelay", + "order": [ + "Namespace", + "Relay", + "SASPolicy", + "SASKey", + "*" + ], + "hidden": [ + "type" + ] + } +} diff --git a/Src/ServiceBusRelayUtil/App.ico b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/App.ico similarity index 100% rename from Src/ServiceBusRelayUtil/App.ico rename to Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/App.ico diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/CommandLineOptions.cs b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/CommandLineOptions.cs new file mode 100644 index 0000000..153bd95 --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/CommandLineOptions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using CommandLine; + +namespace NegativeEddy.Bots.AzureServiceBusRelay.CommandLine +{ + public class CommandLineOptions + { + [Option( + 'n', + "namespace", + Required = true, + HelpText = "The name of the relay's namespace, e.g. '[Your Namespace].servicebus.windows.net'")] + public string RelayNamespace { get; set; } + + [Option( + 'r', + "relay", + Required = true, + HelpText = "The name of the relay")] + public string RelayName { get; set; } + + [Option( + 'p', + "policy", + Required = true, + HelpText = "The name of the relay's Shared Access Policy")] + public string PolicyName { get; set; } + + [Option( + 'k', + "key", + Required = true, + HelpText = "The Shared Access Policy's key")] + public string PolicyKey { get; set; } + + [Option( + 'b', + "botUri", + Required = true, + HelpText = "The url to your local bot e.g. 'http://localhost:[PORT]'")] + public string TargetServiceAddress { get; set; } + } +} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/DispatcherService.cs similarity index 54% rename from Src/ServiceBusRelayUtilNetCore/DispatcherService.cs rename to Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/DispatcherService.cs index 1704890..8f36873 100644 --- a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/DispatcherService.cs @@ -1,74 +1,59 @@ -using System; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Azure.Relay; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using GaboG.ServiceBusRelayUtilNetCore.Extensions; -using Microsoft.Azure.Relay; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace GaboG.ServiceBusRelayUtilNetCore +namespace NegativeEddy.Bots.AzureServiceBusRelay.CommandLine { - internal class DispatcherService + internal class DispatcherService : IHostedService { - private readonly HttpClient _httpClient; - private readonly string _hybridConnectionSubPath; - private readonly HybridConnectionListener _listener; - private readonly Uri _targetServiceAddress; - - public DispatcherService(string relayNamespace, string connectionName, string keyName, string key, Uri targetServiceAddress) - { - _targetServiceAddress = targetServiceAddress; - - var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(keyName, key); - _listener = new HybridConnectionListener(new Uri($"sb://{relayNamespace}/{connectionName}"), tokenProvider); - - _httpClient = new HttpClient - { - BaseAddress = targetServiceAddress - }; - _httpClient.DefaultRequestHeaders.ExpectContinue = false; - - _hybridConnectionSubPath = _listener.Address.AbsolutePath.EnsureEndsWith("/"); - } - - public async Task OpenAsync(CancellationToken cancelToken) + private HttpClient _httpClient; + private string _hybridConnectionSubPath; + private HybridConnectionListener _listener; + private Uri _targetServiceAddress; + private readonly CommandLineOptions _options; + private readonly ILogger _logger; + + public DispatcherService(CommandLineOptions options, ILogger logger) { - _listener.RequestHandler = ListenerRequestHandler; - await _listener.OpenAsync(cancelToken); - Console.WriteLine("Azure Service Bus is listening on \n\r\t{0}\n\rand routing requests to \n\r\t{1}\n\r\n\r", _listener.Address, _httpClient.BaseAddress); - Console.WriteLine("Press [Enter] to exit"); - } - - public Task CloseAsync(CancellationToken cancelToken) - { - _httpClient.Dispose(); - return _listener.CloseAsync(cancelToken); + _options = options; + _logger = logger; } private async void ListenerRequestHandler(RelayedHttpListenerContext context) { var startTimeUtc = DateTime.UtcNow; + HttpStatusCode responseStatus = 0; try { - Console.WriteLine("Calling {0}...", _targetServiceAddress); - var requestMessage = CreateHttpRequestMessage(context); + + _logger.LogInformation("Received message"); + var requestMessage = await CreateHttpRequestMessage(context); + _logger.LogInformation($"{requestMessage.Method} to {_targetServiceAddress}"); var responseMessage = await _httpClient.SendAsync(requestMessage); + responseStatus = responseMessage.StatusCode; await SendResponseAsync(context, responseMessage); await context.Response.CloseAsync(); } - catch (Exception ex) { - LogException(ex); + _logger.LogError(ex, ex.Message); SendErrorResponse(ex, context); } finally { - LogRequest(startTimeUtc); + var stopTimeUtc = DateTime.UtcNow; + double milliseconds = stopTimeUtc.Subtract(startTimeUtc).TotalMilliseconds; + _logger.LogInformation("Response {0} took {1:N0} ms", responseStatus, milliseconds); } } @@ -97,7 +82,7 @@ private void SendErrorResponse(Exception ex, RelayedHttpListenerContext context) context.Response.Close(); } - private HttpRequestMessage CreateHttpRequestMessage(RelayedHttpListenerContext context) + private async Task CreateHttpRequestMessage(RelayedHttpListenerContext context) { var requestMessage = new HttpRequestMessage(); if (context.Request.HasEntityBody) @@ -135,71 +120,71 @@ private HttpRequestMessage CreateHttpRequestMessage(RelayedHttpListenerContext c requestMessage.Headers.Add(headerName, context.Request.Headers[headerName]); } - LogRequestActivity(requestMessage); + await LogRequestActivity(requestMessage); return requestMessage; } - private void LogRequest(DateTime startTimeUtc) + private async Task LogRequestActivity(HttpRequestMessage requestMessage) { - var stopTimeUtc = DateTime.UtcNow; - //var buffer = new StringBuilder(); - //buffer.Append($"{startTimeUtc.ToString("s", CultureInfo.InvariantCulture)}, "); - //buffer.Append($"\"{context.Request.HttpMethod} {context.Request.Url.GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped)}\", "); - //buffer.Append($"{(int)context.Response.StatusCode}, "); - //buffer.Append($"{(int)stopTimeUtc.Subtract(startTimeUtc).TotalMilliseconds}"); - //Console.WriteLine(buffer); - - Console.WriteLine("...and back {0:N0} ms...", stopTimeUtc.Subtract(startTimeUtc).TotalMilliseconds); - Console.WriteLine(""); - } - - private void LogRequestActivity(HttpRequestMessage requestMessage) - { - var content = requestMessage.Content.ReadAsStringAsync().Result; - Console.ForegroundColor = ConsoleColor.Yellow; + if (requestMessage.Content is null) + { + _logger.LogInformation(""); + return; + } + string content = await requestMessage.Content.ReadAsStringAsync(); var formatted = content; - if (IsValidJson(formatted)) - { - var s = new JsonSerializerSettings - { - Formatting = Formatting.Indented - }; - dynamic o = JsonConvert.DeserializeObject(content); - formatted = JsonConvert.SerializeObject(o, s); + try + { + // attempt to parse and pretty print as json + var doc = JsonDocument.Parse(content); + formatted = PrettyPrint(doc.RootElement, true); } + catch { } - Console.WriteLine(formatted); - Console.ResetColor(); + _logger.LogDebug(formatted); } - private static void LogException(Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(ex); - Console.WriteLine(""); - Console.ResetColor(); - } + public static string PrettyPrint(JsonElement element, bool indent) + => element.ValueKind == JsonValueKind.Undefined ? "" : JsonSerializer.Serialize(element, new JsonSerializerOptions { WriteIndented = indent }); - private static bool IsValidJson(string strInput) - { - strInput = strInput.Trim(); - if ((!strInput.StartsWith("{") || !strInput.EndsWith("}")) && (!strInput.StartsWith("[") || !strInput.EndsWith("]"))) - { - return false; - } + public async Task StartAsync(CancellationToken cancellationToken) + { + _targetServiceAddress = new Uri(_options.TargetServiceAddress); - try + var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(_options.PolicyName, _options.PolicyKey); + _listener = new HybridConnectionListener(new Uri($"sb://{_options.RelayNamespace}/{_options.RelayName}"), tokenProvider); + + _httpClient = new HttpClient { - JToken.Parse(strInput); - return true; - } - catch + BaseAddress = _targetServiceAddress + }; + _httpClient.DefaultRequestHeaders.ExpectContinue = false; + + _hybridConnectionSubPath = EnsureEndsWith(_listener.Address.AbsolutePath, "/"); + + _listener.RequestHandler = ListenerRequestHandler; + await _listener.OpenAsync(cancellationToken); + + _logger.LogInformation("Azure Service Bus is listening on {0}\nand routing requests to {1}", _listener.Address, _httpClient.BaseAddress); + + string EnsureEndsWith(string s, string endValue) { - return false; + if (!string.IsNullOrEmpty(s) && s.EndsWith(endValue, StringComparison.Ordinal)) + { + return s; + } + + return s + endValue; } } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _httpClient.Dispose(); + await _listener.CloseAsync(cancellationToken); + } } } \ No newline at end of file diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj new file mode 100644 index 0000000..1cc6f8b --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj @@ -0,0 +1,30 @@ + + + + Exe + net6.0 + App.ico + NegativeEddy.Bots.AzureServiceBusRelay.CommandLine + 57e4c1c9-b95f-4a5c-9dc2-57b1e6ccc769 + AzureServiceBusRelay + + + + + + Always + + + + + + + + + + + + + + + diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/Program.cs b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/Program.cs new file mode 100644 index 0000000..99e4ddb --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/Program.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using CommandLine; + +namespace NegativeEddy.Bots.AzureServiceBusRelay.CommandLine +{ + public partial class Program + { + public static void Main(string[] args) + { + Parser.Default.ParseArguments(args) + .WithParsed(opt => CreateHostBuilder(opt).Build().Run()); + } + + public static IHostBuilder CreateHostBuilder(CommandLineOptions options) => + Host.CreateDefaultBuilder() + .ConfigureAppConfiguration(cb => cb.AddUserSecrets(typeof(Program).Assembly)) + .ConfigureServices((hostContext, services) => + { + services.AddSingleton(options); + services.AddHostedService(); + }); + } +} \ No newline at end of file diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/appsettings.json b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/appsettings.json new file mode 100644 index 0000000..600bd43 --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Service/DispatcherService.cs b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Service/DispatcherService.cs new file mode 100644 index 0000000..0a0556c --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Service/DispatcherService.cs @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.Azure.Relay; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace NegativeEddy.Bots.AzureServiceBusRelay.Service +{ + public class DispatcherService : IHostedService + { + private HttpClient _httpClient; + private string _hybridConnectionSubPath; + private HybridConnectionListener _listener; + private Uri _targetServiceAddress; + private readonly RelayOptions _options; + private readonly ILogger _logger; + private readonly IServer _server; + + public DispatcherService(IServer server, RelayOptions options, ILogger logger) + { + _options = options; + _logger = logger; + _server = server; + } + + private async void ListenerRequestHandler(RelayedHttpListenerContext context) + { + // generate the httpClient as late as possible because this hosted service may + // be started before the server's URI & port have been determined by the web host + if (_httpClient == null) + { + var addressFeature = _server.Features.Get(); + foreach (var address in addressFeature.Addresses) + { + _logger.LogInformation("Forwarding to bot at " + address); + try + { + // check if this is the http URI + Uri uri = new Uri(address); + if (uri.Scheme == "http") + { + if (uri.Host == "0.0.0.0") + { + uri = new Uri($"http://localhost:{uri.Port}"); + } + + _options.TargetServiceAddress = address; + _targetServiceAddress = uri; + _httpClient = new HttpClient + { + BaseAddress = _targetServiceAddress + }; + _httpClient.DefaultRequestHeaders.ExpectContinue = false; + + break; + } + } + catch + { + // not a valid URI, skip it + } + } + } + + var startTimeUtc = DateTime.UtcNow; + HttpStatusCode responseStatus = 0; + try + { + + _logger.LogInformation("Received message"); + var requestMessage = await CreateHttpRequestMessage(context); + _logger.LogInformation($"{requestMessage.Method} to {_targetServiceAddress}"); + var responseMessage = await _httpClient.SendAsync(requestMessage); + responseStatus = responseMessage.StatusCode; + await SendResponseAsync(context, responseMessage); + await context.Response.CloseAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + SendErrorResponse(ex, context); + } + finally + { + var stopTimeUtc = DateTime.UtcNow; + double milliseconds = stopTimeUtc.Subtract(startTimeUtc).TotalMilliseconds; + _logger.LogInformation("Response {0} took {1:N0} ms", responseStatus, milliseconds); + } + } + + private async Task SendResponseAsync(RelayedHttpListenerContext context, HttpResponseMessage responseMessage) + { + context.Response.StatusCode = responseMessage.StatusCode; + context.Response.StatusDescription = responseMessage.ReasonPhrase; + foreach (var header in responseMessage.Headers) + { + if (string.Equals(header.Key, "Transfer-Encoding")) + { + continue; + } + + context.Response.Headers.Add(header.Key, string.Join(",", header.Value)); + } + + var responseStream = await responseMessage.Content.ReadAsStreamAsync(); + await responseStream.CopyToAsync(context.Response.OutputStream); + } + + private void SendErrorResponse(Exception ex, RelayedHttpListenerContext context) + { + context.Response.StatusCode = HttpStatusCode.InternalServerError; + context.Response.StatusDescription = $"Internal Server Error: {ex.GetType().FullName}: {ex.Message}"; + context.Response.Close(); + } + + private async Task CreateHttpRequestMessage(RelayedHttpListenerContext context) + { + var requestMessage = new HttpRequestMessage(); + if (context.Request.HasEntityBody) + { + requestMessage.Content = new StreamContent(context.Request.InputStream); + var contentType = context.Request.Headers[HttpRequestHeader.ContentType]; + if (!string.IsNullOrEmpty(contentType)) + { + requestMessage.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + } + + var relativePath = context.Request.Url.GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped); + relativePath = relativePath.Replace(_hybridConnectionSubPath, string.Empty, StringComparison.OrdinalIgnoreCase); + requestMessage.RequestUri = new Uri(relativePath, UriKind.RelativeOrAbsolute); + requestMessage.Method = new HttpMethod(context.Request.HttpMethod); + + foreach (var headerName in context.Request.Headers.AllKeys) + { + if (string.Equals(headerName, "Host", StringComparison.OrdinalIgnoreCase) || + string.Equals(headerName, "Content-Type", StringComparison.OrdinalIgnoreCase)) + { + // Don't flow these headers here + continue; + } + + requestMessage.Headers.Add(headerName, context.Request.Headers[headerName]); + } + + await LogRequestActivity(requestMessage); + + return requestMessage; + } + + private async Task LogRequestActivity(HttpRequestMessage requestMessage) + { + if (requestMessage.Content is null) + { + _logger.LogInformation(""); + return; + } + string content = await requestMessage.Content.ReadAsStringAsync(); + + var formatted = content; + + try + { + // attempt to parse and pretty print as json + var doc = JsonDocument.Parse(content); + formatted = PrettyPrint(doc.RootElement, true); + } + catch { } + + _logger.LogDebug(formatted); + } + + public static string PrettyPrint(JsonElement element, bool indent) + => element.ValueKind == JsonValueKind.Undefined ? "" : JsonSerializer.Serialize(element, new JsonSerializerOptions { WriteIndented = indent }); + + public async Task StartAsync(CancellationToken cancellationToken) + { + try + { + var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(_options.PolicyName, _options.PolicyKey); + _listener = new HybridConnectionListener(new Uri($"sb://{_options.RelayNamespace}/{_options.RelayName}"), tokenProvider); + + _hybridConnectionSubPath = EnsureEndsWith(_listener.Address.AbsolutePath, "/"); + + _listener.RequestHandler = ListenerRequestHandler; + await _listener.OpenAsync(cancellationToken); + + _logger.LogInformation($"Listening to Azure Service Bus on {_listener.Address}"); + + string EnsureEndsWith(string s, string endValue) + { + if (!string.IsNullOrEmpty(s) && s.EndsWith(endValue, StringComparison.Ordinal)) + { + return s; + } + + return s + endValue; + } + } + catch(Exception ex) + { + _logger.LogError(ex, "Failed to initialize Azure Service Bus Relay"); + throw; + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _httpClient?.Dispose(); + await _listener?.CloseAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Service/NegativeEddy.Bots.AzureServiceBusRelay.Service.csproj b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Service/NegativeEddy.Bots.AzureServiceBusRelay.Service.csproj new file mode 100644 index 0000000..06a5ff1 --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Service/NegativeEddy.Bots.AzureServiceBusRelay.Service.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.1 + + + + + + + + diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Service/RelayOptions.cs b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Service/RelayOptions.cs new file mode 100644 index 0000000..34ca69d --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Service/RelayOptions.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace NegativeEddy.Bots.AzureServiceBusRelay.Service +{ + public class RelayOptions + { + public string RelayNamespace { get; set; } + public string RelayName { get; set; } + public string PolicyName { get; set; } + public string PolicyKey { get; set; } + public string TargetServiceAddress { get; set; } + } +} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtil.sln b/Src/ServiceBusRelayUtil.sln deleted file mode 100644 index d334a17..0000000 --- a/Src/ServiceBusRelayUtil.sln +++ /dev/null @@ -1,31 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27004.2005 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceBusRelayUtil", "ServiceBusRelayUtil\ServiceBusRelayUtil.csproj", "{B9DA41E3-4E0A-41D6-B7D1-64AB017D4FED}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceBusRelayUtilNetCore", "ServiceBusRelayUtilNetCore\ServiceBusRelayUtilNetCore.csproj", "{9AECFF0E-26C7-4D96-A00A-8A09198711EC}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B9DA41E3-4E0A-41D6-B7D1-64AB017D4FED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B9DA41E3-4E0A-41D6-B7D1-64AB017D4FED}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B9DA41E3-4E0A-41D6-B7D1-64AB017D4FED}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B9DA41E3-4E0A-41D6-B7D1-64AB017D4FED}.Release|Any CPU.Build.0 = Release|Any CPU - {9AECFF0E-26C7-4D96-A00A-8A09198711EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9AECFF0E-26C7-4D96-A00A-8A09198711EC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9AECFF0E-26C7-4D96-A00A-8A09198711EC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9AECFF0E-26C7-4D96-A00A-8A09198711EC}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {2C96A03F-F825-402A-B109-0F1FC60BA3CE} - EndGlobalSection -EndGlobal diff --git a/Src/ServiceBusRelayUtil/App.config b/Src/ServiceBusRelayUtil/App.config deleted file mode 100644 index 496dff6..0000000 --- a/Src/ServiceBusRelayUtil/App.config +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Src/ServiceBusRelayUtil/DispatcherService.cs b/Src/ServiceBusRelayUtil/DispatcherService.cs deleted file mode 100644 index 13f81ea..0000000 --- a/Src/ServiceBusRelayUtil/DispatcherService.cs +++ /dev/null @@ -1,269 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.ServiceModel; -using System.ServiceModel.Channels; -using System.ServiceModel.Web; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; -using Microsoft.ServiceBus.Web; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Formatting = Newtonsoft.Json.Formatting; - -namespace GaboG.ServiceBusRelayUtil -{ - [ServiceContract(Namespace = "http://samples.microsoft.com/ServiceModel/Relay/")] - [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)] - internal class DispatcherService - { - private static readonly HashSet _httpContentHeaders = new HashSet - { - "Allow", - "Content-Encoding", - "Content-Language", - "Content-Length", - "Content-Location", - "Content-MD5", - "Content-Range", - "Content-Type", - "Expires", - "Last-Modified" - }; - - private readonly ServiceBusRelayUtilConfig _config; - - public DispatcherService(ServiceBusRelayUtilConfig config) - { - _config = config; - } - - [WebGet(UriTemplate = "*")] - [OperationContract(AsyncPattern = true)] - public async Task GetAsync() - { - try - { - var ti0 = DateTime.Now; - Console.WriteLine("In GetAsync:"); - var context = WebOperationContext.Current; - var request = BuildForwardedRequest(context, null); - Console.WriteLine("...calling {0}...", request.RequestUri); - HttpResponseMessage response; - using (var client = new HttpClient()) - { - response = await client.SendAsync(request, CancellationToken.None); - } - - Console.WriteLine("...and back {0:N0} ms...", DateTime.Now.Subtract(ti0).TotalMilliseconds); - Console.WriteLine(""); - - Console.WriteLine("...reading and creating response..."); - CopyHttpResponseMessageToOutgoingResponse(response, context.OutgoingResponse); - var stream = response.Content != null ? await response.Content.ReadAsStreamAsync() : null; - var message = StreamMessageHelper.CreateMessage(MessageVersion.None, "GETRESPONSE", stream ?? new MemoryStream()); - Console.WriteLine("...and done (total time: {0:N0} ms).", DateTime.Now.Subtract(ti0).TotalMilliseconds); - Console.WriteLine(""); - return message; - } - catch (Exception ex) - { - WriteException(ex); - throw; - } - } - - [WebInvoke(UriTemplate = "*", Method = "*")] - [OperationContract(AsyncPattern = true)] - public async Task InvokeAsync(Message msg) - { - try - { - var ti0 = DateTime.Now; - WriteFlowerLine(); - Console.WriteLine("In InvokeAsync:"); - var context = WebOperationContext.Current; - var request = BuildForwardedRequest(context, msg); - Console.WriteLine("...calling {0}", request.RequestUri); - HttpResponseMessage response; - using (var client = new HttpClient()) - { - response = await client.SendAsync(request, CancellationToken.None); - } - - Console.WriteLine("...and done {0:N0} ms...", DateTime.Now.Subtract(ti0).TotalMilliseconds); - - Console.WriteLine("...reading and creating response..."); - CopyHttpResponseMessageToOutgoingResponse(response, context.OutgoingResponse); - var stream = response.Content != null ? await response.Content.ReadAsStreamAsync() : null; - var message = StreamMessageHelper.CreateMessage(MessageVersion.None, "GETRESPONSE", stream ?? new MemoryStream()); - Console.WriteLine("...and done (total time: {0:N0} ms).", DateTime.Now.Subtract(ti0).TotalMilliseconds); - return message; - } - catch (Exception ex) - { - WriteException(ex); - throw; - } - } - - private HttpRequestMessage BuildForwardedRequest(WebOperationContext context, Message msg) - { - var incomingRequest = context.IncomingRequest; - - var mappedUri = new Uri(incomingRequest.UriTemplateMatch.RequestUri.ToString().Replace(_config.RelayAddress.ToString(), _config.TargetAddress.ToString())); - var newRequest = new HttpRequestMessage(new HttpMethod(incomingRequest.Method), mappedUri); - - // Copy headers - var hostHeader = _config.TargetAddress.Host + (_config.TargetAddress.Port != 80 || _config.TargetAddress.Port != 443 ? ":" + _config.TargetAddress.Port : ""); - foreach (var name in incomingRequest.Headers.AllKeys.Where(name => !_httpContentHeaders.Contains(name))) - { - newRequest.Headers.TryAddWithoutValidation(name, name == "Host" ? hostHeader : incomingRequest.Headers.Get(name)); - } - - if (msg != null) - { - Stream messageStream = null; - if (msg.Properties.TryGetValue("WebBodyFormatMessageProperty", out var value)) - { - if (value is WebBodyFormatMessageProperty prop && (prop.Format == WebContentFormat.Json || prop.Format == WebContentFormat.Raw)) - { - messageStream = StreamMessageHelper.GetStream(msg); - } - } - else - { - var ms = new MemoryStream(); - using (var xw = XmlDictionaryWriter.CreateTextWriter(ms, Encoding.UTF8, false)) - { - msg.WriteBodyContents(xw); - } - - ms.Seek(0, SeekOrigin.Begin); - messageStream = ms; - } - - if (messageStream != null) - { - if (_config.BufferRequestContent) - { - var ms1 = new MemoryStream(); - messageStream.CopyTo(ms1); - ms1.Seek(0, SeekOrigin.Begin); - newRequest.Content = new StreamContent(ms1); - } - else - { - var ms1 = new MemoryStream(); - messageStream.CopyTo(ms1); - ms1.Seek(0, SeekOrigin.Begin); - - var debugMs = new MemoryStream(); - ms1.CopyTo(debugMs); - debugMs.Seek(0, SeekOrigin.Begin); - - var result = Encoding.UTF8.GetString(debugMs.ToArray()); - WriteJsonObject(result); - - ms1.Seek(0, SeekOrigin.Begin); - newRequest.Content = new StreamContent(ms1); - } - - foreach (var name in incomingRequest.Headers.AllKeys.Where(name => _httpContentHeaders.Contains(name))) - { - newRequest.Content.Headers.TryAddWithoutValidation(name, incomingRequest.Headers.Get(name)); - } - } - } - - return newRequest; - } - - private static void WriteException(Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(ex); - Console.WriteLine(""); - Console.ResetColor(); - } - - private static void WriteJsonObject(string result) - { - Console.ForegroundColor = ConsoleColor.Yellow; - - var formatted = result; - if (IsValidJson(result)) - { - var s = new JsonSerializerSettings - { - Formatting = Formatting.Indented - }; - - dynamic o = JsonConvert.DeserializeObject(result); - formatted = JsonConvert.SerializeObject(o, s); - } - - Console.WriteLine(formatted); - Console.ResetColor(); - } - - private static void WriteFlowerLine() - { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("\r\n=> {0:MM/dd/yyyy hh:mm:ss.fff tt} {1}", DateTime.Now, new string('*', 80)); - Console.ResetColor(); - } - - private static void CopyHttpResponseMessageToOutgoingResponse(HttpResponseMessage response, OutgoingWebResponseContext outgoingResponse) - { - outgoingResponse.StatusCode = response.StatusCode; - outgoingResponse.StatusDescription = response.ReasonPhrase; - if (response.Content == null) - { - outgoingResponse.SuppressEntityBody = true; - } - - foreach (var kvp in response.Headers) - { - foreach (var value in kvp.Value) - { - outgoingResponse.Headers.Add(kvp.Key, value); - } - } - - if (response.Content != null) - { - foreach (var kvp in response.Content.Headers) - { - foreach (var value in kvp.Value) - { - outgoingResponse.Headers.Add(kvp.Key, value); - } - } - } - } - - private static bool IsValidJson(string strInput) - { - strInput = strInput.Trim(); - if ((!strInput.StartsWith("{") || !strInput.EndsWith("}")) && (!strInput.StartsWith("[") || !strInput.EndsWith("]"))) - { - return false; - } - - try - { - JToken.Parse(strInput); - return true; - } - catch //some other exception - { - return false; - } - } - } -} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtil/Program.cs b/Src/ServiceBusRelayUtil/Program.cs deleted file mode 100644 index 9de1000..0000000 --- a/Src/ServiceBusRelayUtil/Program.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Configuration; -using System.ServiceModel.Channels; -using System.ServiceModel.Web; -using Microsoft.ServiceBus; - -namespace GaboG.ServiceBusRelayUtil -{ - internal class Program - { - // https://github.com/pmhsfelix/WebApi.Explorations.ServiceBusRelayHost - // https://docs.microsoft.com/en-us/azure/service-bus-relay/service-bus-relay-rest-tutorial - private static void Main() - { - var relayNamespace = ConfigurationManager.AppSettings["RelayNamespace"]; - var relayAddress = ServiceBusEnvironment.CreateServiceUri("https", relayNamespace, ConfigurationManager.AppSettings["RelayName"]); - - var config = new ServiceBusRelayUtilConfig - { - RelayAddress = relayAddress, - RelayPolicyName = ConfigurationManager.AppSettings["PolicyName"], - RelayPolicyKey = ConfigurationManager.AppSettings["PolicyKey"], - MaxReceivedMessageSize = long.Parse(ConfigurationManager.AppSettings["MaxReceivedMessageSize"]), - TargetAddress = new Uri(ConfigurationManager.AppSettings["TargetServiceAddress"]) - }; - - var host = CreateWebServiceHost(config, relayAddress); - host.Open(); - - Console.WriteLine("Azure Service Bus is listening at \n\r\t{0}\n\rrouting requests to \n\r\t{1}\n\r\n\r", relayAddress, config.TargetAddress); - Console.WriteLine(); - Console.WriteLine("Press [Enter] to exit"); - Console.ReadLine(); - - host.Close(); - } - - private static WebServiceHost CreateWebServiceHost(ServiceBusRelayUtilConfig config, Uri address) - { - var host = new WebServiceHost(new DispatcherService(config)); - var binding = GetBinding(config.MaxReceivedMessageSize); - var endpoint = host.AddServiceEndpoint(typeof(DispatcherService), binding, address); - var behavior = GetTransportBehavior(config.RelayPolicyName, config.RelayPolicyKey); - endpoint.Behaviors.Add(behavior); - return host; - } - - private static Binding GetBinding(long maxReceivedMessageSize) - { - var webHttpRelayBinding = new WebHttpRelayBinding(EndToEndWebHttpSecurityMode.None, RelayClientAuthenticationType.None) - { - MaxReceivedMessageSize = maxReceivedMessageSize - }; - var bindingElements = webHttpRelayBinding.CreateBindingElements(); - var webMessageEncodingBindingElement = bindingElements.Find(); - webMessageEncodingBindingElement.ContentTypeMapper = new RawContentTypeMapper(); - return new CustomBinding(bindingElements); - } - - private static TransportClientEndpointBehavior GetTransportBehavior(string keyName, string sharedAccessKey) - { - return new TransportClientEndpointBehavior - { - TokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(keyName, sharedAccessKey) - }; - } - } -} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtil/Properties/AssemblyInfo.cs b/Src/ServiceBusRelayUtil/Properties/AssemblyInfo.cs deleted file mode 100644 index 7ac5c7a..0000000 --- a/Src/ServiceBusRelayUtil/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("ServiceBusRelayUtil")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ServiceBusRelayUtil")] -[assembly: AssemblyCopyright("Copyright © 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("b9da41e3-4e0a-41d6-b7d1-64ab017d4fed")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Src/ServiceBusRelayUtil/RawContentTypeMapper.cs b/Src/ServiceBusRelayUtil/RawContentTypeMapper.cs deleted file mode 100644 index 40289c4..0000000 --- a/Src/ServiceBusRelayUtil/RawContentTypeMapper.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.ServiceModel.Channels; - -namespace GaboG.ServiceBusRelayUtil -{ - internal class RawContentTypeMapper : WebContentTypeMapper - { - public override WebContentFormat GetMessageFormatForContentType(string contentType) - { - return WebContentFormat.Raw; - } - } -} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtil/ServiceBusRelayUtil.csproj b/Src/ServiceBusRelayUtil/ServiceBusRelayUtil.csproj deleted file mode 100644 index bc6f885..0000000 --- a/Src/ServiceBusRelayUtil/ServiceBusRelayUtil.csproj +++ /dev/null @@ -1,143 +0,0 @@ - - - - - Debug - AnyCPU - {B9DA41E3-4E0A-41D6-B7D1-64AB017D4FED} - Exe - GaboG.ServiceBusRelayUtil - ServiceBusRelayUtil - v4.7 - 512 - true - - - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - App.ico - - - - ..\packages\Microsoft.Azure.Amqp.2.3.7\lib\net45\Microsoft.Azure.Amqp.dll - - - ..\packages\Microsoft.Azure.ServiceBus.3.3.0\lib\net461\Microsoft.Azure.ServiceBus.dll - - - ..\packages\Microsoft.Azure.Services.AppAuthentication.1.0.3\lib\net452\Microsoft.Azure.Services.AppAuthentication.dll - - - ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.19.8\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll - - - ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.19.8\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll - - - ..\packages\Microsoft.IdentityModel.Logging.5.2.2\lib\net451\Microsoft.IdentityModel.Logging.dll - - - ..\packages\Microsoft.IdentityModel.Tokens.5.2.2\lib\net451\Microsoft.IdentityModel.Tokens.dll - - - ..\packages\WindowsAzure.ServiceBus.5.1.0\lib\net46\Microsoft.ServiceBus.dll - - - ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll - - - - - - - ..\packages\System.Diagnostics.DiagnosticSource.4.4.1\lib\net46\System.Diagnostics.DiagnosticSource.dll - - - ..\packages\System.IdentityModel.Tokens.Jwt.5.2.2\lib\net451\System.IdentityModel.Tokens.Jwt.dll - - - ..\packages\System.IO.4.3.0\lib\net462\System.IO.dll - - - ..\packages\System.Net.WebSockets.4.3.0\lib\net46\System.Net.WebSockets.dll - - - ..\packages\System.Net.WebSockets.Client.4.3.2\lib\net46\System.Net.WebSockets.Client.dll - - - ..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll - - - - ..\packages\System.Runtime.Serialization.Primitives.4.3.0\lib\net46\System.Runtime.Serialization.Primitives.dll - - - ..\packages\System.Runtime.Serialization.Xml.4.3.0\lib\net46\System.Runtime.Serialization.Xml.dll - - - ..\packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net463\System.Security.Cryptography.Algorithms.dll - - - ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll - - - ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll - - - ..\packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net461\System.Security.Cryptography.X509Certificates.dll - - - - - - - - - - - - - - - - - - - - - - Designer - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - \ No newline at end of file diff --git a/Src/ServiceBusRelayUtil/ServiceBusRelayUtilConfig.cs b/Src/ServiceBusRelayUtil/ServiceBusRelayUtilConfig.cs deleted file mode 100644 index f4944e4..0000000 --- a/Src/ServiceBusRelayUtil/ServiceBusRelayUtilConfig.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace GaboG.ServiceBusRelayUtil -{ - public class ServiceBusRelayUtilConfig - { - public string RelayPolicyName { get; set; } - public string RelayPolicyKey { get; set; } - public Uri RelayAddress { get; set; } - - public bool BufferRequestContent { get; set; } - public long MaxReceivedMessageSize { get; set; } - public Uri TargetAddress { get; set; } - } -} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtil/azureservicebusrelaylogo_150.png b/Src/ServiceBusRelayUtil/azureservicebusrelaylogo_150.png deleted file mode 100644 index 3fe665a..0000000 Binary files a/Src/ServiceBusRelayUtil/azureservicebusrelaylogo_150.png and /dev/null differ diff --git a/Src/ServiceBusRelayUtil/packages.config b/Src/ServiceBusRelayUtil/packages.config deleted file mode 100644 index 79eb4aa..0000000 --- a/Src/ServiceBusRelayUtil/packages.config +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Src/ServiceBusRelayUtilNetCore/App.ico b/Src/ServiceBusRelayUtilNetCore/App.ico deleted file mode 100644 index f3c2e20..0000000 Binary files a/Src/ServiceBusRelayUtilNetCore/App.ico and /dev/null differ diff --git a/Src/ServiceBusRelayUtilNetCore/Extensions/StringEx.cs b/Src/ServiceBusRelayUtilNetCore/Extensions/StringEx.cs deleted file mode 100644 index e11dc7c..0000000 --- a/Src/ServiceBusRelayUtilNetCore/Extensions/StringEx.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; - -namespace GaboG.ServiceBusRelayUtilNetCore.Extensions -{ - public static class StringEx - { - /// - /// Ensures the given string ends with the requested pattern. If it does no allocations are performed. - /// - public static string EnsureEndsWith(this string s, string value, StringComparison comparisonType = StringComparison.Ordinal) - { - if (!string.IsNullOrEmpty(s) && s.EndsWith(value, comparisonType)) - { - return s; - } - - return s + value; - } - } -} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtilNetCore/Program.cs b/Src/ServiceBusRelayUtilNetCore/Program.cs deleted file mode 100644 index e4ba1b7..0000000 --- a/Src/ServiceBusRelayUtilNetCore/Program.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; - -// https://docs.microsoft.com/en-us/azure/service-bus-relay/service-bus-relay-rest-tutorial -// https://github.com/Azure/azure-relay-dotnet -// https://docs.microsoft.com/en-us/azure/service-bus-relay/relay-hybrid-connections-http-requests-dotnet-get-started - -// This is what I think I need -// https://github.com/Azure/azure-relay/blob/master/samples/hybrid-connections/dotnet/hcreverseproxy/README.md -// https://github.com/Azure/azure-relay/tree/master/samples/hybrid-connections/dotnet/hcreverseproxy - -// Publish -// https://stackoverflow.com/questions/44074121/build-net-core-console-application-to-output-an-exe -// https://docs.microsoft.com/en-us/dotnet/core/rid-catalog - -namespace GaboG.ServiceBusRelayUtilNetCore -{ - public class Program - { - public static IConfiguration Configuration { get; set; } - - public static void Main(string[] args) - { - var builder = new ConfigurationBuilder() - .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) - .AddJsonFile("appsettings.json", true, true) - .AddEnvironmentVariables(); - Configuration = builder.Build(); - - RunAsync().GetAwaiter().GetResult(); - } - - static async Task RunAsync() - { - var relayNamespace = Configuration["RelayNamespace"]; - var connectionName = Configuration["RelayName"]; - var keyName = Configuration["PolicyName"]; - var key = Configuration["PolicyKey"]; - var targetServiceAddress = new Uri(Configuration["TargetServiceAddress"]); - - var hybridProxy = new DispatcherService(relayNamespace, connectionName, keyName, key, targetServiceAddress); - - await hybridProxy.OpenAsync(CancellationToken.None); - - Console.ReadLine(); - - await hybridProxy.CloseAsync(CancellationToken.None); - } - } -} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj deleted file mode 100644 index 2d26a18..0000000 --- a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - Exe - netcoreapp2.1 - App.ico - GaboG.ServiceBusRelayUtilNetCore - - - - - - - - - - - Always - - - - - - - - - - - diff --git a/Src/ServiceBusRelayUtilNetCore/appsettings.json b/Src/ServiceBusRelayUtilNetCore/appsettings.json deleted file mode 100644 index 6429582..0000000 --- a/Src/ServiceBusRelayUtilNetCore/appsettings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "RelayNamespace": "[Your Namespace].servicebus.windows.net", - "RelayName": "[Your Relay Name]", - "PolicyName": "[Your Shared Access Policy Name]", - "PolicyKey": "[Your Policy's Key]", - "TargetServiceAddress": "http://localhost:[PORT]" -}