From df0dd58eede245f0dd7623dccba19bfbeb68f676 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Tue, 8 Jun 2021 15:46:19 -0500 Subject: [PATCH 01/26] allow for messages with no content (e.g. OPTIONS) --- Src/ServiceBusRelayUtilNetCore/DispatcherService.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs index 1704890..0e85879 100644 --- a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs +++ b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs @@ -156,7 +156,12 @@ private void LogRequest(DateTime startTimeUtc) private void LogRequestActivity(HttpRequestMessage requestMessage) { - var content = requestMessage.Content.ReadAsStringAsync().Result; + var content = requestMessage.Content?.ReadAsStringAsync().Result; + if (content is null) + { + Console.WriteLine(""); + return; + } Console.ForegroundColor = ConsoleColor.Yellow; var formatted = content; From 4deb9a945497ac75e9a8ae82f30e5aca51f4eaf4 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Tue, 8 Jun 2021 15:54:43 -0500 Subject: [PATCH 02/26] upgrade project to .NET 5 --- .../ServiceBusRelayUtilNetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj index 2d26a18..0156f69 100644 --- a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj +++ b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1 + net5.0 App.ico GaboG.ServiceBusRelayUtilNetCore From 3800edb2ccf53563e914e709ab459d198b6fc6d9 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Tue, 8 Jun 2021 15:57:29 -0500 Subject: [PATCH 03/26] updating nuget packages to latest and re-add Newtonsoft back in --- .../ServiceBusRelayUtilNetCore.csproj | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj index 0156f69..4015572 100644 --- a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj +++ b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj @@ -20,9 +20,10 @@ - - - + + + + From 4363fa9465d46d0288cae12228a5dd6ba89009f0 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Tue, 8 Jun 2021 16:01:50 -0500 Subject: [PATCH 04/26] enable using user-secrets --- Src/ServiceBusRelayUtilNetCore/Program.cs | 3 ++- .../ServiceBusRelayUtilNetCore.csproj | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Src/ServiceBusRelayUtilNetCore/Program.cs b/Src/ServiceBusRelayUtilNetCore/Program.cs index e4ba1b7..7994b71 100644 --- a/Src/ServiceBusRelayUtilNetCore/Program.cs +++ b/Src/ServiceBusRelayUtilNetCore/Program.cs @@ -27,7 +27,8 @@ public static void Main(string[] args) var builder = new ConfigurationBuilder() .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) .AddJsonFile("appsettings.json", true, true) - .AddEnvironmentVariables(); + .AddEnvironmentVariables() + .AddUserSecrets(typeof(Program).Assembly); Configuration = builder.Build(); RunAsync().GetAwaiter().GetResult(); diff --git a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj index 4015572..b95cecc 100644 --- a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj +++ b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj @@ -5,6 +5,7 @@ net5.0 App.ico GaboG.ServiceBusRelayUtilNetCore + 57e4c1c9-b95f-4a5c-9dc2-57b1e6ccc769 @@ -23,6 +24,7 @@ + From 353464b7bb31d1be923bf475b0764a78c8f1dd53 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Tue, 8 Jun 2021 21:31:29 -0500 Subject: [PATCH 05/26] quick convert to default hosting --- .../DispatcherService.cs | 63 ++++++++++++------- Src/ServiceBusRelayUtilNetCore/Program.cs | 34 +++------- .../ServiceBusRelayUtilNetCore.csproj | 2 + 3 files changed, 53 insertions(+), 46 deletions(-) diff --git a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs index 0e85879..7981b0d 100644 --- a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs +++ b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs @@ -6,44 +6,34 @@ using System.Threading.Tasks; using GaboG.ServiceBusRelayUtilNetCore.Extensions; using Microsoft.Azure.Relay; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace GaboG.ServiceBusRelayUtilNetCore - { - internal class DispatcherService + internal class DispatcherService : IHostedService { - private readonly HttpClient _httpClient; - private readonly string _hybridConnectionSubPath; - private readonly HybridConnectionListener _listener; - private readonly Uri _targetServiceAddress; + private HttpClient _httpClient; + private string _hybridConnectionSubPath; + private HybridConnectionListener _listener; + private Uri _targetServiceAddress; + private readonly IConfiguration _config; - public DispatcherService(string relayNamespace, string connectionName, string keyName, string key, Uri targetServiceAddress) + public DispatcherService(IConfiguration config) { - _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("/"); + _config = config; } - public async Task OpenAsync(CancellationToken cancelToken) + private async Task OpenAsync(CancellationToken cancelToken) { _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) + private Task CloseAsync(CancellationToken cancelToken) { _httpClient.Dispose(); return _listener.CloseAsync(cancelToken); @@ -206,5 +196,34 @@ private static bool IsValidJson(string strInput) return false; } } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var config = _config; + string relayNamespace = config["RelayNamespace"]; + string connectionName = config["RelayName"]; + string keyName = config["PolicyName"]; + string key = config["PolicyKey"]; + + _targetServiceAddress = new Uri(config["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("/"); + + await OpenAsync(CancellationToken.None); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await CloseAsync(CancellationToken.None); + } } } \ No newline at end of file diff --git a/Src/ServiceBusRelayUtilNetCore/Program.cs b/Src/ServiceBusRelayUtilNetCore/Program.cs index 7994b71..8ab0730 100644 --- a/Src/ServiceBusRelayUtilNetCore/Program.cs +++ b/Src/ServiceBusRelayUtilNetCore/Program.cs @@ -3,6 +3,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; // https://docs.microsoft.com/en-us/azure/service-bus-relay/service-bus-relay-rest-tutorial // https://github.com/Azure/azure-relay-dotnet @@ -24,31 +26,15 @@ public class Program public static void Main(string[] args) { - var builder = new ConfigurationBuilder() - .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) - .AddJsonFile("appsettings.json", true, true) - .AddEnvironmentVariables() - .AddUserSecrets(typeof(Program).Assembly); - Configuration = builder.Build(); - - RunAsync().GetAwaiter().GetResult(); + CreateHostBuilder(args).Build().Run(); } - 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); - } + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(cb => cb.AddUserSecrets(typeof(Program).Assembly)) + .ConfigureServices((hostContext, services) => + { + services.AddHostedService(); + }); } } \ No newline at end of file diff --git a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj index b95cecc..8143420 100644 --- a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj +++ b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj @@ -25,6 +25,8 @@ + + From 3f35048c0eff7ec03dae86e5ccbc9e263935625f Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Tue, 8 Jun 2021 21:48:20 -0500 Subject: [PATCH 06/26] remove Newtonsoft dependency --- .../DispatcherService.cs | 51 +++++++------------ .../ServiceBusRelayUtilNetCore.csproj | 1 - 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs index 7981b0d..fdfd40b 100644 --- a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs +++ b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs @@ -8,8 +8,7 @@ using Microsoft.Azure.Relay; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; namespace GaboG.ServiceBusRelayUtilNetCore { @@ -45,7 +44,7 @@ private async void ListenerRequestHandler(RelayedHttpListenerContext context) try { Console.WriteLine("Calling {0}...", _targetServiceAddress); - var requestMessage = CreateHttpRequestMessage(context); + var requestMessage = await CreateHttpRequestMessage(context); var responseMessage = await _httpClient.SendAsync(requestMessage); await SendResponseAsync(context, responseMessage); await context.Response.CloseAsync(); @@ -87,7 +86,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) @@ -125,7 +124,7 @@ private HttpRequestMessage CreateHttpRequestMessage(RelayedHttpListenerContext c requestMessage.Headers.Add(headerName, context.Request.Headers[headerName]); } - LogRequestActivity(requestMessage); + await LogRequestActivity(requestMessage); return requestMessage; } @@ -144,32 +143,35 @@ private void LogRequest(DateTime startTimeUtc) Console.WriteLine(""); } - private void LogRequestActivity(HttpRequestMessage requestMessage) + private async Task LogRequestActivity(HttpRequestMessage requestMessage) { - var content = requestMessage.Content?.ReadAsStringAsync().Result; + var content = await requestMessage.Content?.ReadAsStringAsync(); if (content is null) { Console.WriteLine(""); return; } + Console.ForegroundColor = ConsoleColor.Yellow; 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(); } + public static string PrettyPrint(JsonElement element, bool indent) + => element.ValueKind == JsonValueKind.Undefined ? "" : JsonSerializer.Serialize(element, new JsonSerializerOptions { WriteIndented = indent }); + + private static void LogException(Exception ex) { Console.ForegroundColor = ConsoleColor.Red; @@ -178,25 +180,6 @@ private static void LogException(Exception ex) Console.ResetColor(); } - 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 - { - return false; - } - } - public async Task StartAsync(CancellationToken cancellationToken) { var config = _config; diff --git a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj index 8143420..ad76408 100644 --- a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj +++ b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj @@ -27,7 +27,6 @@ - From 6ad26707d6894c0f4af7a3fc0d2fc5fa15ec8460 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Tue, 8 Jun 2021 21:55:19 -0500 Subject: [PATCH 07/26] refactoring --- .../DispatcherService.cs | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs index fdfd40b..48cf047 100644 --- a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs +++ b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs @@ -25,19 +25,6 @@ public DispatcherService(IConfiguration config) _config = config; } - private async Task OpenAsync(CancellationToken cancelToken) - { - _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); - } - - private Task CloseAsync(CancellationToken cancelToken) - { - _httpClient.Dispose(); - return _listener.CloseAsync(cancelToken); - } - private async void ListenerRequestHandler(RelayedHttpListenerContext context) { var startTimeUtc = DateTime.UtcNow; @@ -49,7 +36,6 @@ private async void ListenerRequestHandler(RelayedHttpListenerContext context) await SendResponseAsync(context, responseMessage); await context.Response.CloseAsync(); } - catch (Exception ex) { LogException(ex); @@ -201,12 +187,16 @@ public async Task StartAsync(CancellationToken cancellationToken) _hybridConnectionSubPath = _listener.Address.AbsolutePath.EnsureEndsWith("/"); - await OpenAsync(CancellationToken.None); + _listener.RequestHandler = ListenerRequestHandler; + await _listener.OpenAsync(cancellationToken); + + 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); } public async Task StopAsync(CancellationToken cancellationToken) { - await CloseAsync(CancellationToken.None); + _httpClient.Dispose(); + await _listener.CloseAsync(cancellationToken); } } } \ No newline at end of file From bab03dd8332a09cf44e1299f4f79eac7280246a4 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Tue, 8 Jun 2021 22:56:02 -0500 Subject: [PATCH 08/26] switch to logger from console --- .../DispatcherService.cs | 46 ++++++++----------- .../ServiceBusRelayUtilNetCore.csproj | 5 -- .../appsettings.json | 11 ++++- 3 files changed, 29 insertions(+), 33 deletions(-) diff --git a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs index 48cf047..82807d1 100644 --- a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs +++ b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs @@ -1,14 +1,15 @@ -using System; +using GaboG.ServiceBusRelayUtilNetCore.Extensions; +using Microsoft.Azure.Relay; +using Microsoft.Extensions.Configuration; +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 Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using System.Text.Json; namespace GaboG.ServiceBusRelayUtilNetCore { @@ -19,10 +20,12 @@ internal class DispatcherService : IHostedService private HybridConnectionListener _listener; private Uri _targetServiceAddress; private readonly IConfiguration _config; + private readonly ILogger _logger; - public DispatcherService(IConfiguration config) + public DispatcherService(IConfiguration config, ILogger logger) { _config = config; + _logger = logger; } private async void ListenerRequestHandler(RelayedHttpListenerContext context) @@ -30,7 +33,8 @@ private async void ListenerRequestHandler(RelayedHttpListenerContext context) var startTimeUtc = DateTime.UtcNow; try { - Console.WriteLine("Calling {0}...", _targetServiceAddress); + + _logger.LogInformation("Calling {0}...", _targetServiceAddress); var requestMessage = await CreateHttpRequestMessage(context); var responseMessage = await _httpClient.SendAsync(requestMessage); await SendResponseAsync(context, responseMessage); @@ -38,7 +42,7 @@ private async void ListenerRequestHandler(RelayedHttpListenerContext context) } catch (Exception ex) { - LogException(ex); + _logger.LogError(ex, ex.Message); SendErrorResponse(ex, context); } finally @@ -123,10 +127,10 @@ private void LogRequest(DateTime startTimeUtc) //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); + //_logger.LogInformation(buffer); - Console.WriteLine("...and back {0:N0} ms...", stopTimeUtc.Subtract(startTimeUtc).TotalMilliseconds); - Console.WriteLine(""); + _logger.LogInformation("...and back {0:N0} ms...", stopTimeUtc.Subtract(startTimeUtc).TotalMilliseconds); + _logger.LogInformation(""); } private async Task LogRequestActivity(HttpRequestMessage requestMessage) @@ -134,12 +138,10 @@ private async Task LogRequestActivity(HttpRequestMessage requestMessage) var content = await requestMessage.Content?.ReadAsStringAsync(); if (content is null) { - Console.WriteLine(""); + _logger.LogInformation(""); return; } - Console.ForegroundColor = ConsoleColor.Yellow; - var formatted = content; try @@ -150,22 +152,12 @@ private async Task LogRequestActivity(HttpRequestMessage requestMessage) } catch { } - Console.WriteLine(formatted); - Console.ResetColor(); + _logger.LogDebug(formatted); } public static string PrettyPrint(JsonElement element, bool indent) => element.ValueKind == JsonValueKind.Undefined ? "" : JsonSerializer.Serialize(element, new JsonSerializerOptions { WriteIndented = indent }); - - private static void LogException(Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(ex); - Console.WriteLine(""); - Console.ResetColor(); - } - public async Task StartAsync(CancellationToken cancellationToken) { var config = _config; @@ -190,7 +182,7 @@ public async Task StartAsync(CancellationToken cancellationToken) _listener.RequestHandler = ListenerRequestHandler; await _listener.OpenAsync(cancellationToken); - 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); + _logger.LogInformation("Azure Service Bus is listening on {0}\nand routing requests to {1}", _listener.Address, _httpClient.BaseAddress); } public async Task StopAsync(CancellationToken cancellationToken) diff --git a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj index ad76408..18764af 100644 --- a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj +++ b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj @@ -8,11 +8,6 @@ 57e4c1c9-b95f-4a5c-9dc2-57b1e6ccc769 - - - - - diff --git a/Src/ServiceBusRelayUtilNetCore/appsettings.json b/Src/ServiceBusRelayUtilNetCore/appsettings.json index 6429582..0dadb1a 100644 --- a/Src/ServiceBusRelayUtilNetCore/appsettings.json +++ b/Src/ServiceBusRelayUtilNetCore/appsettings.json @@ -3,5 +3,14 @@ "RelayName": "[Your Relay Name]", "PolicyName": "[Your Shared Access Policy Name]", "PolicyKey": "[Your Policy's Key]", - "TargetServiceAddress": "http://localhost:[PORT]" + "TargetServiceAddress": "http://localhost:[PORT]", + + "Logging": { + "LogLevel": { + "Default": "Trace", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "GaboG" : "Trace" + } + } } From 4a41f1f654fdc07c00cee3fb076e1bcbc7fb6426 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Tue, 8 Jun 2021 23:51:37 -0500 Subject: [PATCH 09/26] add command line parsing for options --- .../DispatcherService.cs | 25 ++++------ Src/ServiceBusRelayUtilNetCore/Program.cs | 47 +++++++++++++++++-- .../ServiceBusRelayUtilNetCore.csproj | 1 + .../appsettings.json | 6 --- 4 files changed, 54 insertions(+), 25 deletions(-) diff --git a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs index 82807d1..e9f7c01 100644 --- a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs +++ b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs @@ -10,6 +10,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using static GaboG.ServiceBusRelayUtilNetCore.Program; namespace GaboG.ServiceBusRelayUtilNetCore { @@ -19,12 +20,12 @@ internal class DispatcherService : IHostedService private string _hybridConnectionSubPath; private HybridConnectionListener _listener; private Uri _targetServiceAddress; - private readonly IConfiguration _config; + private readonly RelayOptions _options; private readonly ILogger _logger; - public DispatcherService(IConfiguration config, ILogger logger) + public DispatcherService(RelayOptions options, ILogger logger) { - _config = config; + _options = options; _logger = logger; } @@ -135,12 +136,12 @@ private void LogRequest(DateTime startTimeUtc) private async Task LogRequestActivity(HttpRequestMessage requestMessage) { - var content = await requestMessage.Content?.ReadAsStringAsync(); - if (content is null) + if (requestMessage.Content is null) { _logger.LogInformation(""); return; } + string content = await requestMessage.Content.ReadAsStringAsync(); var formatted = content; @@ -159,17 +160,11 @@ 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) - { - var config = _config; - string relayNamespace = config["RelayNamespace"]; - string connectionName = config["RelayName"]; - string keyName = config["PolicyName"]; - string key = config["PolicyKey"]; - - _targetServiceAddress = new Uri(config["TargetServiceAddress"]); + { + _targetServiceAddress = new Uri(_options.TargetServiceAddress); - var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(keyName, key); - _listener = new HybridConnectionListener(new Uri($"sb://{relayNamespace}/{connectionName}"), tokenProvider); + var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(_options.PolicyName, _options.PolicyKey); + _listener = new HybridConnectionListener(new Uri($"sb://{_options.RelayNamespace}/{_options.RelayName}"), tokenProvider); _httpClient = new HttpClient { diff --git a/Src/ServiceBusRelayUtilNetCore/Program.cs b/Src/ServiceBusRelayUtilNetCore/Program.cs index 8ab0730..28579cc 100644 --- a/Src/ServiceBusRelayUtilNetCore/Program.cs +++ b/Src/ServiceBusRelayUtilNetCore/Program.cs @@ -2,6 +2,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using CommandLine; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -22,18 +23,56 @@ namespace GaboG.ServiceBusRelayUtilNetCore { public class Program { - public static IConfiguration Configuration { get; set; } + public class RelayOptions + { + [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; } + } public static void Main(string[] args) { - CreateHostBuilder(args).Build().Run(); + CommandLine.Parser.Default.ParseArguments(args) + .WithParsed(opt => CreateHostBuilder(opt).Build().Run()); } - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) + public static IHostBuilder CreateHostBuilder(RelayOptions options) => + Host.CreateDefaultBuilder() .ConfigureAppConfiguration(cb => cb.AddUserSecrets(typeof(Program).Assembly)) .ConfigureServices((hostContext, services) => { + services.AddSingleton(options); services.AddHostedService(); }); } diff --git a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj index 18764af..db6dd22 100644 --- a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj +++ b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj @@ -16,6 +16,7 @@ + diff --git a/Src/ServiceBusRelayUtilNetCore/appsettings.json b/Src/ServiceBusRelayUtilNetCore/appsettings.json index 0dadb1a..28892b3 100644 --- a/Src/ServiceBusRelayUtilNetCore/appsettings.json +++ b/Src/ServiceBusRelayUtilNetCore/appsettings.json @@ -1,10 +1,4 @@ { - "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]", - "Logging": { "LogLevel": { "Default": "Trace", From e5ebdec839b45c8eb03421b99d8e27826fe37860 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Tue, 8 Jun 2021 23:52:19 -0500 Subject: [PATCH 10/26] refactor --- Src/ServiceBusRelayUtilNetCore/Program.cs | 40 +------------ .../RelayOptions.cs | 57 +++++++++++++++++++ 2 files changed, 58 insertions(+), 39 deletions(-) create mode 100644 Src/ServiceBusRelayUtilNetCore/RelayOptions.cs diff --git a/Src/ServiceBusRelayUtilNetCore/Program.cs b/Src/ServiceBusRelayUtilNetCore/Program.cs index 28579cc..96b4db3 100644 --- a/Src/ServiceBusRelayUtilNetCore/Program.cs +++ b/Src/ServiceBusRelayUtilNetCore/Program.cs @@ -21,46 +21,8 @@ namespace GaboG.ServiceBusRelayUtilNetCore { - public class Program + public partial class Program { - public class RelayOptions - { - [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; } - } - public static void Main(string[] args) { CommandLine.Parser.Default.ParseArguments(args) diff --git a/Src/ServiceBusRelayUtilNetCore/RelayOptions.cs b/Src/ServiceBusRelayUtilNetCore/RelayOptions.cs new file mode 100644 index 0000000..4594c80 --- /dev/null +++ b/Src/ServiceBusRelayUtilNetCore/RelayOptions.cs @@ -0,0 +1,57 @@ +using CommandLine; + +// 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 partial class Program + { + public class RelayOptions + { + [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 From d85e696aae8110ffb62f8b5b3685158bc451e843 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Wed, 9 Jun 2021 00:34:34 -0500 Subject: [PATCH 11/26] publishing info --- .../ServiceBusRelayUtilNetCore.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj index db6dd22..e2f7e2d 100644 --- a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj +++ b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj @@ -6,6 +6,12 @@ App.ico GaboG.ServiceBusRelayUtilNetCore 57e4c1c9-b95f-4a5c-9dc2-57b1e6ccc769 + + true + true + win-x64 + + true From 2bd60da9dce30770ced9d333e520e8317d1271c6 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Wed, 9 Jun 2021 00:34:45 -0500 Subject: [PATCH 12/26] update logging --- .../DispatcherService.cs | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs index e9f7c01..bcfbc28 100644 --- a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs +++ b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs @@ -32,12 +32,15 @@ public DispatcherService(RelayOptions options, ILogger logger private async void ListenerRequestHandler(RelayedHttpListenerContext context) { var startTimeUtc = DateTime.UtcNow; + HttpStatusCode responseStatus = 0; try { - _logger.LogInformation("Calling {0}...", _targetServiceAddress); + _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(); } @@ -48,7 +51,9 @@ private async void ListenerRequestHandler(RelayedHttpListenerContext context) } finally { - LogRequest(startTimeUtc); + var stopTimeUtc = DateTime.UtcNow; + double milliseconds = stopTimeUtc.Subtract(startTimeUtc).TotalMilliseconds; + _logger.LogInformation("Response {0} took {1:N0} ms", responseStatus, milliseconds); } } @@ -120,20 +125,6 @@ private async Task CreateHttpRequestMessage(RelayedHttpListe return requestMessage; } - private void LogRequest(DateTime startTimeUtc) - { - 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}"); - //_logger.LogInformation(buffer); - - _logger.LogInformation("...and back {0:N0} ms...", stopTimeUtc.Subtract(startTimeUtc).TotalMilliseconds); - _logger.LogInformation(""); - } - private async Task LogRequestActivity(HttpRequestMessage requestMessage) { if (requestMessage.Content is null) From 57089bfd417997a492924455727bdd247c714cd1 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Wed, 9 Jun 2021 08:32:16 -0500 Subject: [PATCH 13/26] remove publishing options from project (defer that to devops pipeline) --- .../ServiceBusRelayUtilNetCore.csproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj index e2f7e2d..db6dd22 100644 --- a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj +++ b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj @@ -6,12 +6,6 @@ App.ico GaboG.ServiceBusRelayUtilNetCore 57e4c1c9-b95f-4a5c-9dc2-57b1e6ccc769 - - true - true - win-x64 - - true From 218bc78cf74dd30ab565211cfda981eaa8a7a586 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Wed, 9 Jun 2021 09:48:41 -0500 Subject: [PATCH 14/26] add arm template for relay --- Deployment/deploy.json | 92 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 Deployment/deploy.json diff --git a/Deployment/deploy.json b/Deployment/deploy.json new file mode 100644 index 0000000..05341d4 --- /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": { + "HybridConnectionSharedAccessPolicyPrimaryKey": { + "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 From 454f66a1780e28bdfe383efbd43e81202e4cb697 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Wed, 9 Jun 2021 09:49:42 -0500 Subject: [PATCH 15/26] add Deploy to Azure button --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ef9c25e..17b614c 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,9 @@ Part of this code is based on the work that [Pedro Felix](https://github.com/pmh 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. +### Depoloy an Azure Relay service +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https://raw.githubusercontent.com/negativeeddy/AzureServiceBusBotRelay/commandline/Deployment/deploy.json) + ### Building with .Net Core 1. Once the solution has been cloned to your machine, open the solution in Visual Studio. From 164b5089b8451c149e1155846763d8dfb8c3c48a Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Wed, 9 Jun 2021 09:58:52 -0500 Subject: [PATCH 16/26] encode url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 17b614c..e939d2f 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Part of this code is based on the work that [Pedro Felix](https://github.com/pmh - The **app.config** is in the same folder and can be edited as credentials change without needing to recompile the project. ### Depoloy an Azure Relay service -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https://raw.githubusercontent.com/negativeeddy/AzureServiceBusBotRelay/commandline/Deployment/deploy.json) +[![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 From 14c8f90a197b276ca7e2ff897e89a21afc57de3d Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Wed, 9 Jun 2021 15:38:25 -0500 Subject: [PATCH 17/26] fix name of output variable --- Deployment/deploy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Deployment/deploy.json b/Deployment/deploy.json index 05341d4..a5b9b4b 100644 --- a/Deployment/deploy.json +++ b/Deployment/deploy.json @@ -68,7 +68,7 @@ } ], "outputs": { - "HybridConnectionSharedAccessPolicyPrimaryKey": { + "SharedAccessPolicyKey": { "type": "string", "value": "[listkeys(variables('hcAuthorizationRuleResourceId'), variables('apiVersion')).primaryKey]" }, From 19a5f6ac05495f446bbbb83f72259902181e850e Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Wed, 9 Jun 2021 15:38:42 -0500 Subject: [PATCH 18/26] update readme for console command line --- Docs/architecture.png | Bin 0 -> 42486 bytes README.md | 160 ++++++++++++++++++------------------------ 2 files changed, 68 insertions(+), 92 deletions(-) create mode 100644 Docs/architecture.png diff --git a/Docs/architecture.png b/Docs/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..a8fe9ac6a4e2491a46c365a5b0f392bf9bc0d083 GIT binary patch literal 42486 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 YWZe$8l}~n)J^{S+ZX0X8)NqXcFWp=~B>(^b literal 0 HcmV?d00001 diff --git a/README.md b/README.md index e939d2f..c018a37 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,93 @@ -# 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). -## Acknowledgments +It uses the Azure Relay service along with a small local application to recieve messages from the bot service and forward them to your locally hosted bot. + +![architecture diagram](Docs/architecture.png) + +### 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). -# How to configure and run the utility -### Building with .Net Framework - -1. Once the solution has been cloned to your machine, open the solution in Visual Studio. - -2. In Solution Explorer, expand the **ServiceBusRelayUtil** folder. - -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. - - - 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**. - -8. Once testing is completed, you can compile the project into an executable. - - a. Right click the project folder in Visual Studio and select **Build**. - - 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. - -### Depoloy an Azure Relay service +## Setup + +To setup the Azure Bot Service to connect to your local bot you need to + +1. Deploy an Azure Relay service +2. Configure your Azure Bot Service to send messages to the Azure Relay +3. Launch your bot locally +4. Run the AzureServiceBusBotRelay tool configured to listen to your relay and push messages to your bot + +### Deploy an Azure Relay service + +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. + +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. + [![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. ensure the relay does not require authentication +2. add a shared access policy to the hybrid relay that has permission to send & listen + +### Configure your Azure Web App Bot or Azure Bot Registration + +Before testing the relay, your Azure Web App Bot's messaging endpoint must be updated to match the relay. + +1. Login to the Azure portal and open your Web App Bot or Bot Registration. + +2. Select **Settings** under Bot management to open the settings blade. + +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. + + Ensure that the URI ends with "/api/messages" + + For example, “https://example-service-bus.servicebus.windows.net/hc1/api/messages". + +4. Click **Save** when completed. (You might have to click save twice) + +### Launch your bot locally -1. Once the solution has been cloned to your machine, open the solution in Visual Studio. +1. Start your bot as usual on your local machine -2. In Solution Explorer, expand the **ServiceBusRelayUtilNetCore** folder. +2. Once your bot is running, note the localhost endpoint it is attached to -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]**. +### Building and run the relay tool - b. "RelayName" is the name of the hybrid connection created in step 12. Enter the value in place of **[Your Relay Name]**. +1. Once the solution has been cloned to your machine, open and build the solution in Visual Studio. - 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. Run the tool with the settings you have captured up until now. The tool has five required options. Four of these are from the deployment outputs copied above. The fifth is the URI to your bot (do not include the "/api/messages" portion). - 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. +````text + -n, --namespace The name of the relay's namespace, e.g. '[Your Namespace].servicebus.windows.net' - - 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**. + -r, --relay The name of the relay -8. Once testing is completed, you can compile the project into an executable. + -p, --policy The name of the relay's Shared Access Policy - a. Right click the project folder in Visual Studio and select **Publish**. + -k, --key The Shared Access Policy's key - b. For **Pick a publish Target**, select **Folder**. + -b, --botUri The url to your local bot e.g. 'http://localhost:[PORT]' +```` - c. For **Folder or File Share**, choose an output location or keep the default. +You can specify the parameters either through the Visual Studio project's properties page, or you can run the tool from a command prompt. - d. Click **Create Profile** to create a publish profile. +Here is an example command line - e. Click **Configure...** to change the build configuration and change the following: +````text - - **Configuration** to "Debug | Any CPU" - - **Deployment Mode** to "Self-contained" - - **Target Runtime** to "win-x64" +ServiceBusRelayUtilNetCore.exe -n benwillidemorelay.servicebus.windows.net -r botrelay +-p SendAndListenPolicy -k XOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXOX -b http://localhost:3980 +```` - f. Click **Save** and then **Publish** +### Test your bot - 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.). User data is captured and logged as activity occurs. From 5a13623a9f1bb364799dfffc665b70a595f31ef7 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Tue, 10 Aug 2021 07:52:54 -0500 Subject: [PATCH 19/26] working nuget package --- Src/BotServiceBusRelay.sln | 31 ++ .../AzureServiceBusRelayAdapter.cs | 35 +++ ...AzureServiceBusRelayAdapterBotComponent.cs | 28 ++ .../DispatcherService.cs | 223 +++++++++++++++ ...y.Bots.AzureServiceBusRelay.Adapter.csproj | 25 ++ .../RelayOptions.cs | 14 + ...iveEddy.AzureServiceBusRelayAdapter.schema | 42 +++ ...eEddy.AzureServiceBusRelayAdapter.uischema | 18 ++ .../App.ico | Bin .../CommandLineOptions.cs | 45 +++ .../DispatcherService.cs | 24 +- ...s.AzureServiceBusRelay.CommandLine.csproj} | 2 +- .../Program.cs | 28 ++ .../appsettings.json | 3 +- .../DispatcherService.cs | 223 +++++++++++++++ ...y.Bots.AzureServiceBusRelay.Service.csproj | 12 + .../RelayOptions.cs | 14 + Src/ServiceBusRelayUtil.sln | 31 -- Src/ServiceBusRelayUtil/App.config | 68 ----- Src/ServiceBusRelayUtil/DispatcherService.cs | 269 ------------------ Src/ServiceBusRelayUtil/Program.cs | 68 ----- .../Properties/AssemblyInfo.cs | 36 --- .../RawContentTypeMapper.cs | 13 - .../ServiceBusRelayUtil.csproj | 143 ---------- .../ServiceBusRelayUtilConfig.cs | 15 - .../azureservicebusrelaylogo_150.png | Bin 3562 -> 0 bytes Src/ServiceBusRelayUtil/packages.config | 23 -- Src/ServiceBusRelayUtilNetCore/App.ico | Bin 25214 -> 0 bytes .../Extensions/StringEx.cs | 23 -- Src/ServiceBusRelayUtilNetCore/Program.cs | 41 --- .../RelayOptions.cs | 57 ---- 31 files changed, 757 insertions(+), 797 deletions(-) create mode 100644 Src/BotServiceBusRelay.sln create mode 100644 Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/AzureServiceBusRelayAdapter.cs create mode 100644 Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/AzureServiceBusRelayAdapterBotComponent.cs create mode 100644 Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/DispatcherService.cs create mode 100644 Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj create mode 100644 Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/RelayOptions.cs create mode 100644 Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/Schemas/NegativeEddy.AzureServiceBusRelayAdapter.schema create mode 100644 Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/Schemas/NegativeEddy.AzureServiceBusRelayAdapter.uischema rename Src/{ServiceBusRelayUtil => NegativeEddy.Bots.AzureServiceBusRelay.CommandLine}/App.ico (100%) create mode 100644 Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/CommandLineOptions.cs rename Src/{ServiceBusRelayUtilNetCore => NegativeEddy.Bots.AzureServiceBusRelay.CommandLine}/DispatcherService.cs (90%) rename Src/{ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj => NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj} (93%) create mode 100644 Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/Program.cs rename Src/{ServiceBusRelayUtilNetCore => NegativeEddy.Bots.AzureServiceBusRelay.CommandLine}/appsettings.json (58%) create mode 100644 Src/NegativeEddy.Bots.AzureServiceBusRelay.Service/DispatcherService.cs create mode 100644 Src/NegativeEddy.Bots.AzureServiceBusRelay.Service/NegativeEddy.Bots.AzureServiceBusRelay.Service.csproj create mode 100644 Src/NegativeEddy.Bots.AzureServiceBusRelay.Service/RelayOptions.cs delete mode 100644 Src/ServiceBusRelayUtil.sln delete mode 100644 Src/ServiceBusRelayUtil/App.config delete mode 100644 Src/ServiceBusRelayUtil/DispatcherService.cs delete mode 100644 Src/ServiceBusRelayUtil/Program.cs delete mode 100644 Src/ServiceBusRelayUtil/Properties/AssemblyInfo.cs delete mode 100644 Src/ServiceBusRelayUtil/RawContentTypeMapper.cs delete mode 100644 Src/ServiceBusRelayUtil/ServiceBusRelayUtil.csproj delete mode 100644 Src/ServiceBusRelayUtil/ServiceBusRelayUtilConfig.cs delete mode 100644 Src/ServiceBusRelayUtil/azureservicebusrelaylogo_150.png delete mode 100644 Src/ServiceBusRelayUtil/packages.config delete mode 100644 Src/ServiceBusRelayUtilNetCore/App.ico delete mode 100644 Src/ServiceBusRelayUtilNetCore/Extensions/StringEx.cs delete mode 100644 Src/ServiceBusRelayUtilNetCore/Program.cs delete mode 100644 Src/ServiceBusRelayUtilNetCore/RelayOptions.cs 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..0a0556c --- /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) + { + _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.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj new file mode 100644 index 0000000..b01f0ec --- /dev/null +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj @@ -0,0 +1,25 @@ + + + + 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 + 1.0.0-preview4 + true + NegativeEddy.Bots.AzureServiceBusRelay.Adapter + NegativeEddy.Bots.AzureServiceBusRelay.Adapter + https://github.com/negativeeddy/AzureServiceBusBotRelay + + + + + + + + + + + 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 90% rename from Src/ServiceBusRelayUtilNetCore/DispatcherService.cs rename to Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/DispatcherService.cs index bcfbc28..8f36873 100644 --- a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/DispatcherService.cs @@ -1,6 +1,7 @@ -using GaboG.ServiceBusRelayUtilNetCore.Extensions; +// 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.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; @@ -10,9 +11,8 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using static GaboG.ServiceBusRelayUtilNetCore.Program; -namespace GaboG.ServiceBusRelayUtilNetCore +namespace NegativeEddy.Bots.AzureServiceBusRelay.CommandLine { internal class DispatcherService : IHostedService { @@ -20,10 +20,10 @@ internal class DispatcherService : IHostedService private string _hybridConnectionSubPath; private HybridConnectionListener _listener; private Uri _targetServiceAddress; - private readonly RelayOptions _options; + private readonly CommandLineOptions _options; private readonly ILogger _logger; - public DispatcherService(RelayOptions options, ILogger logger) + public DispatcherService(CommandLineOptions options, ILogger logger) { _options = options; _logger = logger; @@ -163,12 +163,22 @@ public async Task StartAsync(CancellationToken cancellationToken) }; _httpClient.DefaultRequestHeaders.ExpectContinue = false; - _hybridConnectionSubPath = _listener.Address.AbsolutePath.EnsureEndsWith("/"); + _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) + { + if (!string.IsNullOrEmpty(s) && s.EndsWith(endValue, StringComparison.Ordinal)) + { + return s; + } + + return s + endValue; + } } public async Task StopAsync(CancellationToken cancellationToken) diff --git a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj similarity index 93% rename from Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj rename to Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj index db6dd22..1758c0e 100644 --- a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj @@ -4,7 +4,7 @@ Exe net5.0 App.ico - GaboG.ServiceBusRelayUtilNetCore + NegativeEddy.Bots.AzureServiceBusRelay.CommandLine 57e4c1c9-b95f-4a5c-9dc2-57b1e6ccc769 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/ServiceBusRelayUtilNetCore/appsettings.json b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/appsettings.json similarity index 58% rename from Src/ServiceBusRelayUtilNetCore/appsettings.json rename to Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/appsettings.json index 28892b3..600bd43 100644 --- a/Src/ServiceBusRelayUtilNetCore/appsettings.json +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/appsettings.json @@ -3,8 +3,7 @@ "LogLevel": { "Default": "Trace", "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information", - "GaboG" : "Trace" + "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 3fe665a213009c9ca636bb7ed94f0bfa4c8f2ba9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3562 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 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 f3c2e20ce1ed59f67f9aff31ee81b25df43fd2fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25214 zcmeG^33wCL)@PD5dnQR&T3VVy%dV77K*S=7vZ{bSx2GT|7R3NA4+Lz3WqS%mMMXv5 zldaH=vWsOaAfSj*76pCCOD&{@hQ9Lq{`aat^Pf92H9zbMHClo^9^j znLC*o00wlBnrcC;f^;>&djMdw@$Cu)KrPZN7QWq)0dVU8_4MbjOYXP>! z5_!Iz9tQA4TPkmZbtwP`J5YISqnv#tmA65<3LyFkfR5+`nt*W##4l1DJmTts8wWCx z;&AW?w^&7RKuJjnGQ8YBc)g^CfPaKUHndy~Vod;cVbi3VC zB|~SOPWxn;4%u#xr)peqhv^yis_6@Z>7eo0B~-23?XI$Gsyt9Zi-U-`JylMpQ|q*Y zM<`V#gr4EF+ud%~;{oi(|8t?ciMU7Ou0nY?_s6FM<;&d_T92E^J3R>YFNhwG+l5=T z+d~?pWZOAC86Folw00+E${n@^y}U4aBm6}6k~oa`VH86J$R3WLTvi>bm82pRqNxZO_7Ki1V4y4^mGkWoKI zh{@ySF;7)YS+-1u>#Jn>9a zvYk$i+l}%f8duG>ll?;ck`~rESxVyCxR>PU3+8*=3l;`*Fx9fh1*W-bqAOS)eGm4M zh|L$LQq&K#9gqVbD==j~GGv$d>*5+KZjd?XU)(h0^&S%NegV%BFe6}oy7JaSCO3Co z@!>RnF3aNNY9I?B5ujR(%|cs@Gqb!`NV_gf^dP^6;Iu4nHK$iu18GDb;!FX1dCVa8 zdb4WMcw9|zOywN=F7w9z4eBL|a?yw;YM4;yxP+&v0IPj!`3_P!J2)9^m z#Cb-GoyEmBxA8;`KV$hMSbLxVj#-C~iIoidQK2x65H+kF+k!qqu7l6o9fUt)MmY*<7tY|ONET0Qspk*OK`X^>Kb+8;H}`-*?{4c2*!i z+m2+eXxvVWhqKKSvfVE7$2Z6}8~F}@6Kj$gDZ^Rr67zAs_xpwn(aDGt(}=YbigY&D za$;oTy^O=}7lcv-7eYxU50|iM1r3F=OWs2wSm0t@W&sirXL>J{c}o#4UE1%h?ym9H zSk2xm)%U4$NlU0CV;O>^%hV)87HUMsTI!JS51HKBOKLkf#DqK^VC6j8Ivq15cqAD%TsqL!ih4?!q{5uTKEa zhA6nWDHbX=Bp{B5s;!A|YHbIEPVmj@-tgVJ9&l!TDxA#k3ulW`;OC-FP_d~kc(&N! z!sZmjDR6FMN2u7+1&6P8AJ?V+BLu z`=X(6YQr%2Jm)d^Y3nFBwrMPUxB1Ud_UWB)cKZ-0-F`1r?7k1q7LSJWdmaFH@uRRe zZwegQ_&l82J`s*@odl;odk*&IzYO~dXTay{XTp)K8Sw447vTHtQ{mY5zrc^XUV!7D z&4llFBJ7wAXZK8i^ZUlYrOzLL>I09!nUa^_^xjM;|MF=l&Yug#1@qyX;@9BF_WAJb zu5389cOIY68`~Qh_~T}vd*fWcTVZWf2Zch>7z?o|ueiU#7VaS8ijWsRL3(&`MM4lm zp3p{+S9F%=OQ`*+cmqAUtzEka(xpSYb^|1;LN&V*`A|%OKHQ`eN&*P-Lxai~pOVf* zC13PZo)SdZCm-o2)+K*0?a(B7Q-T%$+ERbG7NKx|dg4K#erilMKgqu{`M!X(}j9*Tv_1P$h~8iTv@WB}kMbi1K+q={izIOs`Yu?h)uB`CcMZ0lkutLG6m< zb)n=(6Gk^)gySoHm8z zcpoAa^S610f%3Qb*}=7&hy}5{DOe_uae)m^#->L z3aS0ZeXHNoeJf#|suJxk(K_`)9>c9uJ^5;Gom!sHLPfrwTdUG_>e+%Qs3^2@>(%q? zqoJ~>m9SoI4gbtR(FO|xi`VloLk^{Zht5(Xba^VlHlBi6u7V{3Ea3gJ-4|V zlx^+bxRC*02735vvK3aiuO-U_XvZ-4V{Yie zU}+PYZD1G$!;pJx^2HXRV8U$t#h1JUqll4X#1X>`(+?EL=j;4dI9U_BXt^Kqkk6l& z$(BZg!bY>%d@CrBj}^WceV3Zpg*Qv|`-==8`}<4%eEj)^6tvG*h<#satzd+;GTQIl zrAybY(L(%vx_0fF8td~4LlS}(`dhfBLeW#u#fMC`yne#RhVN!Vicq2izL{YnQ~LNE z@mGZS{g!!FD#P5w)r9`&kG~J8F$#X%@O)fzCI|9+qsACqSCReG8GF-9e4kW%;=8ba zvFfAyDz_OqKK5r6aXzZJ(m#>kzlma^q%Qa`q0(kjB*u{xdVCtV{UE=t;B=6rx2D=; za7UUa86HP6$V46>OZT{l`1|_ZLAXv4j}KB8+Go~ALZxu6ay~B%&gYYR={hLOkAt#; z1UQ+K0;h7?!s)#BaBNLCIJK@5UXR=iC)W0Y?{j;?cX_wMo)u$Y$Et}?y!K_-v2Hqi zlRFr`&K(NhB0iQs1Wv3U28Y+(&s}>Q$WMnail)Hc!dKw%W&*RJIByQ@D9D1H1rAXb zZ&Yt&;6?^oJ_C}uJ5kvxz@}a)nmNM5!$FzchASu)p;W7rlu>F@lz4)Pz#spB0723= z(jt_snI(L)MXe!1Mx#;B_0jW_bUf0qognA{4a>EazH_uenWT(V2HeCahD0U6U!*50 zqeWb-`r~ch-z{yN83#UV*HWch3TrvNLcjaXm#h!Bl#Q0s%i@07HaC0`{&wrVSg3J7 zHYBc7OWA4(y|1Mw4)f$IeB z5@>F#&8KhYqUevP&w*HRdC{c!KQ*7F=GXN6nD7z!%)-?`zxgplXq5%J(gCUFq~^DL z?HX_IYP9~m*7S2I-zoj@GV8uhHe-EP%Gd5tBQ@+>8oLU8tfhX|fopdzj(fF{>RVJi zIbLU6_CoZsWz!-)A#ma&HfTS5wkY->$NH-f(;9gU{#TI zK{%_jkPdm7ksGD?A2%{_BH=|?bjs!cqN^cgV0Uw?!ynrIhR+H8=2>s@)(IP1|1xTj znF;0pi2jydRoglrX%6@-+xA96v{GGP3;F>o_YKy0ef{^&dOTw8|408XZiu*YadX0NQmES2$wTrHYWxrH#}1J8 zX=oddzo7GCQ}h4Ul-84UT9r-)xRpjI|4D2zedP26(U!iL^uBZ)H`V{eljEKuzJw}u z`5&rJee@+$ZSVY{{hyNfhxWf{UhImX{Ev-k()@G1$4|eTOqPl~z2uriQ9Jv|gkklK zyMr?G|Mm1=r(skZ=S7lZH{jaPool$*qf>+{?DF}JgdIG zQocJ^d$wD$f!qM9xBTAxugq8ccju^~2w1w3;V9K2I0L z7lhW{I8L`@Z6|zB<5zrV!%N}R%7mi2v^3QJNOKtTkCjn7rT%Yy_olzprN1#eT$^jo z<+4ABZItC`E~K_Ma%){VT@Cer{K!_f<9jW?iglmQRa|~>aO--WB#^Tjnzky#Hb-m& z>$UrqumwHZ8zbds0;_`Ck3M6dVf{J8ycI9yQlbyfgNm!qj)`m3{77xqhhKj#elVUp z{vB)+?++G);e847CC}DQ2Z)|f{txfV_VN@O{w1pe*Y8{Bv5V^S<68I&C&f%Umjl0q zvX5)LCf|Q3+hF^=O{~7&XKAm+f6{mQBf7z!LcM$ro8&)p^~{PkI|lSJ$R11A>aSe? zdv7y$JhnV?r>8&_xTbEb|AqQXYhI0fp-qgs-uqB;eq0N!O|)M9r$OxCU5g_M+_}bT zJoaiEHh!4eiUPKJ%fi@Y!)}f2a4kMGSKlseO>xiM7ui4kKI=H5Gt}{$6dM+SGDEtz zw@7}cY%WilM^7mKn@4-|Ye`Pfv+qqYXTN9Hzf0iwa@{|rbJS{eWmBz-QxZ-3 z=J%xO8uz~){UQ1sutt@}Ahc^&&5E4ebW1JMvtOzy3V%O37s6-X8nZMvW=8}|pU7(& zUz)1*`3Iv$;P1X6_d#W$zT%;w@wTSwzaIT|X17`+yASYOvEaFQ-@9nn!(j^nK;$4iH(#bP99d@C-=%})5Apl}$&gwF%*HHh zHs^n#zH(r9eG5M6(lYxb+NxqtuVN3r`E2Y1En`7mtN%MQ^m{7w`zZ9gDFUY7Q}KUi zC5`*8N)03uTmy3e@O7nXVg?^%4vifGALWT$+8qA7GcveF;8%0{m}>qzHX_W~!*(XI zhZy}Jn0$8z^>W{vLF`H+?L;2enCIY^Hqc*Y4iJ0=92g+^?hN@x3<5rV%3%j6Ia~t# zcV}uGe7s&EKSJE1@6PainkV0PXJ{U2UTL2B@6L2YyEcS@2n<)lZ&ZX_siRN>%d9`M zkMCb<$}t3U0A0)Uy0WT3np48%L=s+Q4^+6?SIqs8J)}AYn8v; z-%nS^W7y5>Nak46vm|a}zcP=|b1^IYQ|=Q90IH2#V_^>2i>d7f$akUo|=UMOz3AD!EpA|4xT8vEE_<9I2I zzQcIyvV2d;0^PRz^W1`Mhk*9Oh_@wyw$F1_HeX{@2 z{>2_C>Yko_2Cpd@92X74XI8Vv@x2K~>Ot>L`hGMvIV`qM_#}R`-%C!mPTG2-SE-;6 z%=*0`_UX9ha1nb2f1kfh;qV8xmBI0AyHb8#*i&TU?hkN!>##5Od;IW-d(>eHH3gND zQBwOfe&Rjl*M%pHjG5@uU59;N{CJ!{F~mB^pB=tN?Grh&|E2s;?Xkn6CJ_6LiN7xU z;`l?^6w?LhVzF|wbpIzy;Jv}X^!7qd2FLaT?U zeHwpKtUBh)cg@?-XYLtwLdWNh|M#BWI+-`rMCux#5&>R;m2(1@mj30?mJjeG-@0K8>HAe@PpE7jjP<6D~WqG~9BaWVd8p(i`dQ z+#gq%zxrxLKf|eM|IPY25C0s(%l^)s#y4*8}(LplegHPURta znESNl+3R6bYGdjH`p5S#v948zF+tZ*QrU*Ixo>%vdhvUn1Twu|aytUSgfv4fxGayv0#VZsQRBf?oc+q1Yxg{EjHO zPeE==*bwM_ih;ggu#s3tu^5%w18-@HQ{uzwdfz#uhQ;ZczmoB^caeHG=99jF7gAo# z=+R!&{=1JfWu*U4D^yie#~3Gw`EvF9`fKlAHAB6D!fWF#FOmL^ai)2zXIVd#gMq1P z`Xw-U^Vu-|ouESPReWFh>L)XzxOYo0vdJgr4_V3{qb3rB%-r#XLWzl-#%JDo!X#y{7}uKobJ&&-+dZt|J!BE z(J$YEua3UYaL-N9{u4*)pCa_H-&K5#&wu>4bd|C^E<))$ugrQ>o%!Y4%3~CczOOtj zwm;($eFkYC|4?~m>wNvT-W^o|@7s;&XGq)scKD98R2}zRiPZm3Ue+!rbL=f$4DD$g zVjLT(vba{M&K>(eb@KU9x~Cq#(>RLQoBoJl_S`4cub$6ke%txFW{0?sw0=uR<;^EQ z4&&});Jy4eN&f{;o8BV$t9LYq+r_Kve5Q%$GwKxDM+RxeJU7ZTp7fjfm|@N%gS4aZ zK1>6T8~_}@Dc>zuach&)pC~;T&p4^C^|k5ovuCkpk4~#>)bT@fV;q`&+1j0L<5cw? pzudQWGx2^af`jqH!b@1k5$?zWShxgnB0vd(D~*?}L3z^m{{VzBFd_f| 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 96b4db3..0000000 --- a/Src/ServiceBusRelayUtilNetCore/Program.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using CommandLine; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -// 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 partial class Program - { - public static void Main(string[] args) - { - CommandLine.Parser.Default.ParseArguments(args) - .WithParsed(opt => CreateHostBuilder(opt).Build().Run()); - } - - public static IHostBuilder CreateHostBuilder(RelayOptions 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/ServiceBusRelayUtilNetCore/RelayOptions.cs b/Src/ServiceBusRelayUtilNetCore/RelayOptions.cs deleted file mode 100644 index 4594c80..0000000 --- a/Src/ServiceBusRelayUtilNetCore/RelayOptions.cs +++ /dev/null @@ -1,57 +0,0 @@ -using CommandLine; - -// 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 partial class Program - { - public class RelayOptions - { - [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 From 7c9993a73da31ca0852a6a65f0f7424557fe7bcf Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Tue, 10 Aug 2021 07:57:58 -0500 Subject: [PATCH 20/26] moved log message to correct place --- .../DispatcherService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/DispatcherService.cs b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/DispatcherService.cs index 0a0556c..3a9e578 100644 --- a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/DispatcherService.cs +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/DispatcherService.cs @@ -42,7 +42,6 @@ private async void ListenerRequestHandler(RelayedHttpListenerContext context) 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 @@ -54,6 +53,7 @@ private async void ListenerRequestHandler(RelayedHttpListenerContext context) uri = new Uri($"http://localhost:{uri.Port}"); } + _logger.LogInformation("Forwarding to bot at " + address); _options.TargetServiceAddress = address; _targetServiceAddress = uri; _httpClient = new HttpClient From 47d9c24c73fe4f73b448e7ced834d2412a4fe9ce Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Wed, 11 Aug 2021 20:14:31 -0500 Subject: [PATCH 21/26] docs for composer --- Docs/CommandLine.md | 49 ++++++++++++++++++++++++++++++++++++++++ README.md | 54 +++++++++++++-------------------------------- 2 files changed, 64 insertions(+), 39 deletions(-) create mode 100644 Docs/CommandLine.md 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/README.md b/README.md index c018a37..cfd3fd8 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,28 @@ A relay utility for bots based on [Azure Relay](https://docs.microsoft.com/en-us 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 application to recieve messages from the bot service and forward them to your locally hosted bot. +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) did in his project [here](https://github.com/pmhsfelix/WebApi.Explorations.ServiceBusRelayHost). +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 ## Setup +The relay can be used with traditional code based bots or as a simple add in component to Bot Composer + To setup the Azure Bot Service to connect to your local bot you need to 1. Deploy an Azure Relay service -2. Configure your Azure Bot Service to send messages to the Azure Relay -3. Launch your bot locally -4. Run the AzureServiceBusBotRelay tool configured to listen to your relay and push messages to your bot +2. Configure your Azure Bot Service to send messages to the Azure Relay +3. Connect to the relay from Bot Composer + +* 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) ### Deploy an Azure Relay service @@ -54,40 +58,12 @@ Before testing the relay, your Azure Web App Bot's messaging endpoint must be up 4. Click **Save** when completed. (You might have to click save twice) -### 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. Once the solution has been cloned to your machine, open and build the solution in Visual Studio. - -2. Run the tool with the settings you have captured up until now. The tool has five required options. Four of these are from the deployment outputs copied above. 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 +### Connect to the relay from Bot Composer -ServiceBusRelayUtilNetCore.exe -n benwillidemorelay.servicebus.windows.net -r botrelay --p SendAndListenPolicy -k XOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXOX -b http://localhost:3980 -```` +1. Add the AzureServiceBusRelay 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 ### Test your bot -1. Test your bot on a channel (Test in Web Chat, Skype, Teams, etc.). User data is captured and logged as activity occurs. +1. Test your bot on a channel (Test in Web Chat, Skype, Teams, etc.). From 126fe83812b4aff3b9df12982a9e75d63a7f5353 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Wed, 11 Aug 2021 20:23:20 -0500 Subject: [PATCH 22/26] set package version --- .../NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj index b01f0ec..d226db7 100644 --- a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj @@ -7,7 +7,7 @@ Library for connecting bots with Azure Service Bus Relay content msbot-component;msbot-adapter - 1.0.0-preview4 + 1.0.0-preview1 true NegativeEddy.Bots.AzureServiceBusRelay.Adapter NegativeEddy.Bots.AzureServiceBusRelay.Adapter From cba40fe2c0094cc9ad025d652078fb4fb23f8d88 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Wed, 11 Aug 2021 20:27:56 -0500 Subject: [PATCH 23/26] update package --- .../NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj index d226db7..8b89296 100644 --- a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj @@ -12,6 +12,7 @@ NegativeEddy.Bots.AzureServiceBusRelay.Adapter NegativeEddy.Bots.AzureServiceBusRelay.Adapter https://github.com/negativeeddy/AzureServiceBusBotRelay + https://github.com/negativeeddy/AzureServiceBusBotRelay From 4faf7e687d4c2a1995b574dc78a7d329d0da2fdd Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Wed, 11 Aug 2021 20:36:03 -0500 Subject: [PATCH 24/26] publishing edits --- README.md | 2 +- .../NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cfd3fd8..b092df9 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Before testing the relay, your Azure Web App Bot's messaging endpoint must be up ### Connect to the relay from Bot Composer -1. Add the AzureServiceBusRelay package to your bot +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 diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj index 8b89296..9624765 100644 --- a/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.Adapter/NegativeEddy.Bots.AzureServiceBusRelay.Adapter.csproj @@ -6,13 +6,14 @@ Library for connecting bots with Azure Service Bus Relay Library for connecting bots with Azure Service Bus Relay content - msbot-component;msbot-adapter + 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 From 7d609bed1af40778e09a6dd4fe1023b711efa443 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Tue, 24 Aug 2021 13:41:04 -0500 Subject: [PATCH 25/26] change CLI exe name to AzureServiceBiusRelay.exe --- .../NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj index 1758c0e..eebeab3 100644 --- a/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj @@ -6,6 +6,7 @@ App.ico NegativeEddy.Bots.AzureServiceBusRelay.CommandLine 57e4c1c9-b95f-4a5c-9dc2-57b1e6ccc769 + AzureServiceBusRelay From 1e30bfc2b4058f1ab048bb7db28ef578fe1121b0 Mon Sep 17 00:00:00 2001 From: Ben Williams Date: Wed, 5 Oct 2022 09:21:10 -0500 Subject: [PATCH 26/26] update to project and dependencies to .NET 6 --- ...dy.Bots.AzureServiceBusRelay.CommandLine.csproj | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj index eebeab3..1cc6f8b 100644 --- a/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj +++ b/Src/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine/NegativeEddy.Bots.AzureServiceBusRelay.CommandLine.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net6.0 App.ico NegativeEddy.Bots.AzureServiceBusRelay.CommandLine 57e4c1c9-b95f-4a5c-9dc2-57b1e6ccc769 @@ -17,13 +17,13 @@ - + - - - - - + + + + +